# WebBAR engine performance pass (docs/perf.md): native WASM exceptions/SjLj, THREADPOOL build policy, # no engine-side drawing in WASM builds, WASM speed-control CPU target, bridge export deltas and timers. diff --git a/rts/builds/headless/CMakeLists.txt b/rts/builds/headless/CMakeLists.txt --- a/rts/builds/headless/CMakeLists.txt +++ b/rts/builds/headless/CMakeLists.txt @@ -75,9 +75,9 @@ target_link_libraries(engine-headless no-sound ${engineHeadlessLibraries} GameHe if(EMSCRIPTEN) target_link_libraries(engine-headless BARb) target_link_options(engine-headless PRIVATE - -pthread -fexceptions -msimd128 + -pthread -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -sSUPPORT_LONGJMP=wasm -msimd128 --profiling-funcs -sUSE_LIBPNG=1 -sUSE_LIBJPEG=1 -sUSE_ZLIB=1 -sUSE_FREETYPE=1 - -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=8 + -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=12 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=268435456 -sMAXIMUM_MEMORY=4294967296 -sSTACK_SIZE=8388608 -sDEFAULT_PTHREAD_STACK_SIZE=8388608 -sEXIT_RUNTIME=1 -sENVIRONMENT=node,worker -sNODERAWFS=1) @@ -112,9 +112,9 @@ if(EMSCRIPTEN) target_link_libraries(engine-browser BARb) target_link_libraries(engine-browser no-sound ${engineHeadlessLibraries} GameHeadless no-sound) target_link_options(engine-browser PRIVATE - -pthread -fexceptions -msimd128 + -pthread -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -sSUPPORT_LONGJMP=wasm -msimd128 --profiling-funcs -sUSE_LIBPNG=1 -sUSE_LIBJPEG=1 -sUSE_ZLIB=1 -sUSE_FREETYPE=1 - -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=8 + -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=12 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=268435456 -sMAXIMUM_MEMORY=4294967296 -sSTACK_SIZE=8388608 -sDEFAULT_PTHREAD_STACK_SIZE=2097152 -sEXIT_RUNTIME=1 -sENVIRONMENT=web,worker -sFORCE_FILESYSTEM=1 diff --git a/rts/Game/Game.cpp b/rts/Game/Game.cpp --- a/rts/Game/Game.cpp +++ b/rts/Game/Game.cpp @@ -1463,6 +1463,25 @@ bool CGame::UpdateUnsynced(const spring_time currentTime) bool CGame::Draw() { const spring_time currentTimePreUpdate = spring_gettime(); +#if defined(__EMSCRIPTEN__) + // WebBAR: the page renders from the bridge packets, so the engine never draws (as a minimized + // native window, see !globalRendering->active below). Unsynced state (Lua Update callins, camera, + // unit draw positions) is still updated, at most ~60 times per second; when no simulation frame + // advanced since the last loop the thread sleeps briefly instead of spinning. Unsynced only. + if (!skipping) { + static spring_time lastUnsynced = spring_notime; + static int lastSeenFrame = INT32_MIN; + const bool advanced = (lastSeenFrame != gs->frameNum); + lastSeenFrame = gs->frameNum; + if ((currentTimePreUpdate - lastUnsynced).toMilliSecsf() < 16.0f) { + if (!advanced) + spring_sleep(spring_msecs(1)); + return false; + } + lastUnsynced = currentTimePreUpdate; + } +#endif + if (UpdateUnsynced(currentTimePreUpdate)) return false; @@ -1483,6 +1502,12 @@ bool CGame::Draw() { eventHandler.DrawGenesis(); } +#if defined(__EMSCRIPTEN__) + SetDrawMode(gameNotDrawing); + lastDrawFrameTime = spring_gettime(); + return false; +#endif + if (!globalRendering->active) { spring_sleep(spring_msecs(10)); @@ -1860,7 +1885,10 @@ void CGame::SimFrame() { ASSERT_SYNCED(gsRNG.GetGenState()); LEAVE_SYNCED_CODE(); #if defined(__EMSCRIPTEN__) - PublishWebbarFrame(); + { + SCOPED_TIMER("Webbar::Publish"); + PublishWebbarFrame(); + } #endif } diff --git a/rts/Game/WebbarBrowser.h b/rts/Game/WebbarBrowser.h --- a/rts/Game/WebbarBrowser.h +++ b/rts/Game/WebbarBrowser.h @@ -34,15 +34,28 @@ static bool WebbarProjectileVisible(const CProjectile* projectile) const int allyTeam = projectile->GetAllyteamID(); return (teamHandler.IsValidAllyTeam(allyTeam) && teamHandler.Ally(allyTeam, gu->myAllyTeam)) || losHandler->InLos(projectile, gu->myAllyTeam); } -static std::vector WebbarUnits() +// Visible units, computed once per published frame (snapshot, unit state and poses share it). +static const std::vector& WebbarUnits() { - std::vector units; - const bool playable = std::getenv("WEBBAR_PLAYABLE") != nullptr; + static std::vector units; + static int frame = INT32_MIN; + static const bool playable = std::getenv("WEBBAR_PLAYABLE") != nullptr; + if (frame == gs->frameNum && !units.empty()) return units; + frame = gs->frameNum; + units.clear(); for (CUnit* unit: unitHandler.GetActiveUnits()) if (!playable || WebbarVisible(unit)) units.push_back(unit); return units; } +// 64-bit FNV-1a, for change detection of presentation-only data. +struct WebbarHash { + uint64_t value = 1469598103934665603ull; + void Bytes(const void* data, size_t size) { const auto* b = static_cast(data); for (size_t i = 0; i < size; ++i) { value ^= b[i]; value *= 1099511628211ull; } } + void Word(uint32_t word) { Bytes(&word, sizeof(word)); } + void String(const std::string& text) { Word(uint32_t(text.size())); Bytes(text.data(), text.size()); } +}; + #include "WebbarWeapons.h" #include "WebbarShields.h" #include "WebbarCloak.h" @@ -109,19 +122,32 @@ static void PublishWebbarPlayState() if (cloakCommandsChanged || gs->frameNum % 30 == 0) { // Batch descriptions into one host call, including native state labels. // Strings are decoded synchronously before the temporary pointer table dies. + // Only units whose descriptions changed since their last publication (or that have no + // publication in their current ownership lifetime) are sent; the browser merges by unit. std::vector words(1, 0); const auto pointer = [](const std::string& value) { return uint32_t(reinterpret_cast(value.c_str())); }; for (const CUnit* unit: unitHandler.GetActiveUnits()) { if (unit->team != gu->myTeam) continue; - ++words[0]; const auto& commands = unit->commandAI->GetPossibleCommands(); + WebbarHash signature; + signature.Word(uint32_t(commands.size())); + for (const auto* command: commands) { + signature.Word(uint32_t(command->id)); signature.Word(uint32_t(command->type)); signature.Word(uint32_t(command->disabled) | (uint32_t(command->hidden) << 1)); + signature.String(command->action); signature.String(command->name); signature.String(command->tooltip); + signature.Word(uint32_t(command->params.size())); + for (const auto& param: command->params) signature.String(param); + } + const auto [known, inserted] = webbarCloakEvents::publishedCommandSignatures.emplace(unit->id, signature.value); + if (!inserted && known->second == signature.value) continue; + known->second = signature.value; + ++words[0]; words.push_back(unit->id); words.push_back(commands.size()); for (const auto* command: commands) { words.insert(words.end(), {uint32_t(command->id), uint32_t(command->type), uint32_t(command->disabled), uint32_t(command->hidden), pointer(command->action), pointer(command->name), pointer(command->tooltip), uint32_t(command->params.size())}); for (const auto& param: command->params) words.push_back(pointer(param)); } } - MAIN_THREAD_EM_ASM({ + if (words[0] > 0) MAIN_THREAD_EM_ASM({ if (!Module['webbarCommandDescriptions']) return; const words = HEAPU32.subarray($0 >> 2, ($0 >> 2) + $1); const units = []; let cursor = 1; for (let i = 0; i < words[0]; i++) { @@ -137,6 +163,7 @@ static void PublishWebbarPlayState() Module['webbarCommandDescriptions'](units, $2); }, words.data(), words.size(), gs->frameNum); } + SCOPED_TIMER("Webbar::Publish::State::Rest"); PublishWebbarEnergyConversion(); const CTeam* team = teamHandler.Team(gu->myTeam); const float economy[] = {team->res.metal, team->resStorage.metal, team->resPrevIncome.metal, team->resPrevExpense.metal, team->resPrevPull.metal, @@ -144,7 +171,7 @@ static void PublishWebbarPlayState() MAIN_THREAD_EM_ASM({ if (Module['webbarEconomy']) Module['webbarEconomy']($0, HEAPF32.slice($1 >> 2, ($1 >> 2) + 10).buffer); }, gs->frameNum, economy); - const auto units = WebbarUnits(); + const auto& units = WebbarUnits(); struct Record { uint32_t id; float progress; uint32_t queued; int32_t command; uint32_t target; float metal, energy; uint32_t transporter, cargo, selectionFlags; }; static_assert(sizeof(Record) == 40); const uint32_t header[] = {0x35534257, uint32_t(gs->frameNum), uint32_t(units.size()), 0}; @@ -169,7 +196,7 @@ static void PublishWebbarPlayState() if (gs->frameNum % 300 == 0) PublishWebbarEngineMemory(); if (gs->frameNum > 0 && gs->frameNum % (TeamStatistics::statsPeriod * GAME_SPEED) == 0) PublishWebbarTeamStats(false, nullptr); webbarWorkAudio::Publish(); - PublishWebbarWorld(); + { SCOPED_TIMER("Webbar::Publish::World"); PublishWebbarWorld(); } PublishWebbarResurrection(); } @@ -460,7 +487,7 @@ static void PollWebbarCommands() // matrices from copied animation state, without updating the engine's caches. static void PublishWebbarPoses() { - const auto units = WebbarUnits(); + const auto& units = WebbarUnits(); uint32_t pieceCount = 0; for (const CUnit* unit: units) pieceCount += unit->localModel.pieces.size(); @@ -469,10 +496,12 @@ static void PublishWebbarPoses() uint8_t* cursor = packet.data(); auto append = [&](const void* data, size_t bytes) { std::memcpy(cursor, data, bytes); cursor += bytes; }; append(header, sizeof(header)); - static std::vector definitions; + static std::vector definitions; + static std::vector matrices; for (const CUnit* unit: units) { - if (std::find(definitions.begin(), definitions.end(), unit->unitDef->id) == definitions.end()) { - definitions.push_back(unit->unitDef->id); + if (size_t(unit->unitDef->id) >= definitions.size()) definitions.resize(unit->unitDef->id + 1, false); + if (!definitions[unit->unitDef->id]) { + definitions[unit->unitDef->id] = true; MAIN_THREAD_EM_ASM({ if (Module['webbarDefinition']) Module['webbarDefinition']($0, UTF8ToString($1)); }, unit->unitDef->id, unit->unitDef->name.c_str()); @@ -482,7 +511,7 @@ static void PublishWebbarPoses() append(record, sizeof(record)); const CMatrix44f world = unit->GetTransformMatrix(true); append(world.m, sizeof(world.m)); - std::vector matrices(pieces.size()); + matrices.resize(pieces.size()); for (size_t i = 0; i < pieces.size(); ++i) { const auto& piece = pieces[i]; const CMatrix44f local = piece.CalcPieceSpaceTransform(piece.GetPosition(), piece.GetRotation(), piece.GetScaling()).ToMatrix(); @@ -563,13 +592,15 @@ static void PublishWebbarFrame() } static const char* output = std::getenv("WEBBAR_SNAPSHOTS"); static const bool browser = std::getenv("WEBBAR_BROWSER") != nullptr; - if (browser && std::getenv("WEBBAR_PLAYABLE") != nullptr && gs->frameNum >= 0) GetWebbarEffects().Publish(); + static const bool playableBrowser = browser && std::getenv("WEBBAR_PLAYABLE") != nullptr; + if (playableBrowser && gs->frameNum >= 0) { SCOPED_TIMER("Webbar::Publish::Effects"); GetWebbarEffects().Publish(); } if ((!output && !browser) || gs->frameNum % 3 != 0) return; - const auto units = WebbarUnits(); + SCOPED_TIMER("Webbar::Publish::Frame3"); + const auto& units = WebbarUnits(); std::vector projectiles; - const bool playable = std::getenv("WEBBAR_PLAYABLE") != nullptr; + const bool playable = playableBrowser; for (const CProjectile* p: projectileHandler.GetActiveProjectiles(true)) if (!playable || WebbarProjectileVisible(p)) projectiles.push_back(p); struct UnitRecord { @@ -613,11 +644,11 @@ static void PublishWebbarFrame() } } if (browser) { - if (std::getenv("WEBBAR_PLAYABLE") != nullptr) { - WebbarNano::Publish(); - PublishWebbarTerrain(); - PublishWebbarPlayState(); - PublishWebbarPoses(); + if (playableBrowser) { + { SCOPED_TIMER("Webbar::Publish::Nano"); WebbarNano::Publish(); } + { SCOPED_TIMER("Webbar::Publish::Terrain"); PublishWebbarTerrain(); } + { SCOPED_TIMER("Webbar::Publish::State"); PublishWebbarPlayState(); } + { SCOPED_TIMER("Webbar::Publish::Poses"); PublishWebbarPoses(); } } MAIN_THREAD_EM_ASM({ if (Module['webbarSnapshot']) Module['webbarSnapshot'](HEAPU8.slice($0, $0 + $1).buffer); diff --git a/rts/Game/WebbarCloakEvents.h b/rts/Game/WebbarCloakEvents.h --- a/rts/Game/WebbarCloakEvents.h +++ b/rts/Game/WebbarCloakEvents.h @@ -21,6 +21,9 @@ inline uint32_t nextID = 0, dropped = 0; inline std::unordered_map ownershipLifetimes; inline uint32_t nextLifetime = 0; inline std::unordered_map publishedBuildStates; +// Signature of the native command descriptions last published per owned unit (WebbarBrowser.h). +// A unit whose ownership lifetime ends is published again in full. +inline std::unordered_map publishedCommandSignatures; // Observe only after the same owner/live/applicability/cap filters as WBC4. // A new lifetime or completion needs current native command descriptions in @@ -30,6 +33,8 @@ inline bool CommandMetadataChanged(int unitID, bool beingBuilt) const auto [it, inserted] = publishedBuildStates.emplace(unitID, beingBuilt); const bool changed = inserted || it->second != beingBuilt; it->second = beingBuilt; + // The browser retires descriptions of a changed lifetime unless this publication carries them. + if (changed) publishedCommandSignatures.erase(unitID); return changed; } @@ -59,6 +64,7 @@ inline void OwnershipChanged(int unitID) { ownershipLifetimes.erase(unitID); publishedBuildStates.erase(unitID); + publishedCommandSignatures.erase(unitID); pending.erase(std::remove_if(pending.begin(), pending.end(), [unitID](const Pending& entry) { return entry.event.unit == uint32_t(unitID); }), pending.end()); diff --git a/rts/Net/GameServer.cpp b/rts/Net/GameServer.cpp --- a/rts/Net/GameServer.cpp +++ b/rts/Net/GameServer.cpp @@ -990,7 +990,13 @@ void CGameServer::LagProtection() * This means that we will necessarily have some idle time because the average frame time is now * lower than the nominal sim FPS, i.e. the ideal CPU% to aim at is less than 100%. The constants * below have been determined empirically. The max is of course higher than the median. */ +#if defined(__EMSCRIPTEN__) + // WebBAR: the engine thread never draws (the page renders from bridge packets), so the idle + // share that desktop builds reserve for drawing is not needed; keep ~15% for the bridge export. + float wantedCpuUsage = 0.85f; +#else float wantedCpuUsage = (curSpeedCtrl == 1) ? 0.60f : 0.75f; +#endif //the following line can actually make it go faster than wanted normal speed ( userSpeedFactor ) //if the current cpu of the target is smaller than the aimed cpu target but the clamp will cap it diff --git a/rts/Rendering/WorldDrawer.cpp b/rts/Rendering/WorldDrawer.cpp --- a/rts/Rendering/WorldDrawer.cpp +++ b/rts/Rendering/WorldDrawer.cpp @@ -214,6 +214,15 @@ void CWorldDrawer::Update(bool newSimFrame) LuaObjectDrawer::Update(numUpdates == 0); readMap->UpdateDraw(numUpdates == 0); +#if defined(__EMSCRIPTEN__) + // WebBAR: the engine never draws (CGame::Draw); keep unit/feature draw positions current for + // unsynced Lua, skip ground, projectile draw-flag, sky and water updates. + CUnitDrawer::UpdateStatic(); + CFeatureDrawer::UpdateStatic(); + numUpdates += 1; + return; +#endif + if (globalRendering->drawGround) { ZoneScopedN("GroundDrawer::Update"); (readMap->GetGroundDrawer())->Update(); diff --git a/rts/System/GlobalConfig.cpp b/rts/System/GlobalConfig.cpp --- a/rts/System/GlobalConfig.cpp +++ b/rts/System/GlobalConfig.cpp @@ -68,7 +68,12 @@ CONFIG(bool, VFSCacheArchiveFiles).defaultValue(true); CONFIG(bool, DumpGameStateOnDesync).defaultValue(true).description("Enable writing clientgamestate and servergamestate dumps when a desync is detected"); +#if defined(__EMSCRIPTEN__) +// WebBAR: no engine-side drawing (CGame::Draw); the bridge export needs only a small reserve. +CONFIG(float, MinSimDrawBalance).defaultValue(0.05f).description("Percent of the time for simulation is minimum spend for drawing. E.g. if set to 0.15 then 15% of the total cpu time is exclusively reserved for drawing."); +#else CONFIG(float, MinSimDrawBalance).defaultValue(0.15f).description("Percent of the time for simulation is minimum spend for drawing. E.g. if set to 0.15 then 15% of the total cpu time is exclusively reserved for drawing."); +#endif CONFIG(int, MinDrawFPS).defaultValue(2).description("Defines how many frames per second should minimally be rendered. To reach this number we will delay simframes."); CONFIG(float, SelectThroughGround).defaultValue(200.0f).minimumValue(0.0f).description("How far beyond the ground to allow selecting objects."); diff --git a/rts/System/Platform/Threading.cpp b/rts/System/Platform/Threading.cpp --- a/rts/System/Platform/Threading.cpp +++ b/rts/System/Platform/Threading.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #ifdef __EMSCRIPTEN__ @@ -246,6 +248,18 @@ namespace Threading { return threadCount + ( threadCount < threadCountThreshold ? std::popcount(gc.groupMask & (pm.performanceCoreMask) & (~pm.hyperThreadHighMask)) : 0 ); }); const uint32_t fallbackThreadCount = GetPerformanceCpuCores(); +#if defined(__EMSCRIPTEN__) + // WebBAR: the browser shares the device with the page, renderer and engine service threads. + // Use about half the reported logical cores for the simulation pool (main thread included), + // at most 4; e.g. 12 cores -> 4, 8 -> 3, 4 -> 1. WorkerThreadCount still overrides this. + { + uint32_t webbarThreads = std::clamp(int(GetLogicalCpuCores()) / 2 - 1, 1, 4); + if (const char* forced = std::getenv("WEBBAR_WORKER_THREADS"); forced != nullptr && std::atoi(forced) > 0) + webbarThreads = std::clamp(std::atoi(forced), 1, 8); + std::call_once(optimalThreadCountLogFlag, [&]() { LOG("[Threading] Optimal thread count is %u (browser policy)", webbarThreads); }); + return webbarThreads; + } +#endif std::call_once(optimalThreadCountLogFlag, [&](){ if (optimalThreadCount > 0) {