Browse Source

Rebuild the Scenes page rows only when their inputs change

Every frame the page resolved every scene of the state through prepare_scene,
which since the cast is read from every participant walks the state's actors and
the generated world for each of them; with the page open the game fell to
12 fps. The rows now carry the inputs they were computed from (catalog,
scenario, state, ActivityClient generation, lease revisions and flags, region,
search text) and are rebuilt when one of them changes, after an Advance, or
every 250 ms as a bound on staleness. What each row shows is unchanged.

The omitted-cast warning moved out of scene_dependencies, which the availability
check reaches as often as the activation does, to the two activation paths; the
prepared scene carries the cast size for it.
Fozkais 1 week ago
parent
commit
f11921fec7

+ 2 - 0
Sunrise/src/server/activity/activity_sdk_mission_internal.h

@@ -16,6 +16,8 @@ struct PreparedScene final {
     state::build_data::scenarios::RosterGroup rosterGroup{};
     middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies
         sceneDependencies{};
+    /** Squads the resolved cast draws from; more than the wire carries leaves the set empty. */
+    std::size_t castCount{};
     std::uint64_t activityClientGeneration{};
     std::uint32_t scenarioRow{state::activity_sdk::format::kAbsentIndex};
     std::uint32_t stateRow{state::activity_sdk::format::kAbsentIndex};

+ 2 - 1
Sunrise/src/server/activity/activity_sdk_mission_prepare.cpp

@@ -358,7 +358,8 @@ namespace {
     if (collected != SceneStatus::ready) {
         return collected;
     }
-    scene_dependencies(catalog, slotRow, cast, output.sceneDependencies);
+    output.castCount = cast.count;
+    static_cast<void>(scene_dependencies(cast, output.sceneDependencies));
 
     const SceneStatus lease = scene_lease_status(view, link, occurrence.stateIndex);
     if (lease != SceneStatus::ready) {

+ 7 - 0
Sunrise/src/server/activity/activity_sdk_mission_runtime.cpp

@@ -9,6 +9,7 @@
 #include "../../state/activity/runtime.h"
 #include "activity_sdk_behavior_scope.h"
 #include "activity_sdk_mission_internal.h"
+#include "activity_sdk_scene_spawn.h"
 #include "activity_sdk_scriptable_route.h"
 #include "host_runtime.h"
 
@@ -126,6 +127,9 @@ SceneStatus activate_authored_scene(const sdk::BoundView& view,
     if (status != SceneStatus::ready) {
         return status;
     }
+    if (prepared.sceneDependencies.count == 0 && prepared.castCount != 0) {
+        log_omitted_scene_dependencies(*view.catalog, slotRow, prepared.castCount);
+    }
     if (server::bap::request_activity_state_local_authored_scene_override(
             view.binding,
             prepared.target,
@@ -752,6 +756,9 @@ SceneStatus activate_authored_scene_reserved(const sdk::BoundView& view,
     if (status != SceneStatus::ready) {
         return status;
     }
+    if (prepared.sceneDependencies.count == 0 && prepared.castCount != 0) {
+        log_omitted_scene_dependencies(*view.catalog, slotRow, prepared.castCount);
+    }
     if (server::bap::request_activity_state_local_authored_scene_override(
             view.binding,
             prepared.target,

+ 28 - 22
Sunrise/src/server/activity/activity_sdk_scene_spawn.cpp

@@ -119,33 +119,13 @@ prepare_pair(const sdk::BoundView& view, std::uint32_t sceneState, SceneSpawnPai
 } // namespace
 
 /** Sends the cast's squads when the wire can carry them all, otherwise none. */
-void scene_dependencies(
-    const sdk::Catalog& catalog,
-    std::uint32_t sceneSlotRow,
+bool scene_dependencies(
     const SceneSpawnPlan& plan,
     middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
         output) noexcept {
     output = {};
     if (plan.count > output.references.size()) {
-        std::array<char, core::log::kLineCapacity> line{};
-        const std::string_view scene = sceneSlotRow < catalog.slots().size()
-                                           ? catalog.string(catalog.slots()[sceneSlotRow].id)
-                                           : std::string_view{};
-        const int written = std::snprintf(line.data(),
-                                          line.size(),
-                                          "ev=scene_dependencies result=omitted scene=%.*s "
-                                          "cast=%zu capacity=%zu",
-                                          static_cast<int>(scene.size()),
-                                          scene.data(),
-                                          plan.count,
-                                          output.references.size());
-        if (written > 0) {
-            core::log::write(
-                core::log::Channel::server,
-                core::log::Level::warn,
-                {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
-        }
-        return;
+        return false;
     }
     for (std::size_t index = 0; index < plan.count; ++index) {
         const host::ScriptableTarget& source = plan.pairs[index].sourceTarget;
@@ -153,6 +133,32 @@ void scene_dependencies(
                                              static_cast<std::int8_t>(source.slotType),
                                              static_cast<std::int16_t>(source.slotIndex)};
     }
+    return true;
+}
+
+/** Logged once per activation; an availability check resolves the same cast every frame. */
+void log_omitted_scene_dependencies(const sdk::Catalog& catalog,
+                                    std::uint32_t sceneSlotRow,
+                                    std::size_t castCount) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const std::string_view scene = sceneSlotRow < catalog.slots().size()
+                                       ? catalog.string(catalog.slots()[sceneSlotRow].id)
+                                       : std::string_view{};
+    const int written = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=scene_dependencies result=omitted scene=%.*s cast=%zu capacity=%zu",
+        static_cast<int>(scene.size()),
+        scene.data(),
+        castCount,
+        middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies{}
+            .references.size());
+    if (written > 0) {
+        core::log::write(
+            core::log::Channel::server,
+            core::log::Level::warn,
+            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
+    }
 }
 
 /** Returns immutable scene cast identities without consulting live output state. */

+ 11 - 9
Sunrise/src/server/activity/activity_sdk_scene_spawn.h

@@ -33,18 +33,20 @@ struct SceneSpawnPlan final {
  *
  * The client uses them for one thing: once the scene runs, it marks each squad's remaining
  * spawn budget as consumed. It binds roles from its own content, so a scene plays without
- * them. The schema carries at most eight; a wider cast sends none and logs the omission.
- * @param catalog Authenticated SDK data, for the log line.
- * @param sceneSlotRow Type 43 slot the plan was resolved for.
+ * them. The schema carries at most eight; a wider cast sends none.
  * @param plan Complete resolved cast.
  * @param output Receives the bounded set; empty when the cast exceeds the wire capacity.
+ * @return False when the cast was omitted.
  */
-void scene_dependencies(
-    const state::activity_sdk::Catalog& catalog,
-    std::uint32_t sceneSlotRow,
-    const SceneSpawnPlan& plan,
-    middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
-        output) noexcept;
+[[nodiscard]] bool
+scene_dependencies(const SceneSpawnPlan& plan,
+                   middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
+                       output) noexcept;
+
+/** Logs, at warn, a cast the wire could not carry; for the activation paths only. */
+void log_omitted_scene_dependencies(const state::activity_sdk::Catalog& catalog,
+                                    std::uint32_t sceneSlotRow,
+                                    std::size_t castCount) noexcept;
 
 /** Resolves the complete cast from exact package edges without reading Host state. */
 [[nodiscard]] SceneStatus

+ 54 - 2
Sunrise/src/server/ui/activity_host/activity_host_sdk_mission_view.cpp

@@ -8,7 +8,9 @@
 #include <cstdio>
 #include <imgui.h>
 #include <span>
+#include <string>
 #include <string_view>
+#include <utility>
 #include <vector>
 
 #include "../../../core/ui/components/section/ui_section_component.h"
@@ -50,6 +52,32 @@ struct SceneBrowserRow final {
 
 std::vector<SceneBrowserRow> g_visibleScenes{};
 
+/**
+ * Everything the scene rows are computed from. Resolving one row prepares its whole cast, so
+ * the rows are rebuilt when this changes or after kSceneRefreshSeconds, not every frame.
+ */
+struct SceneBrowserKey final {
+    const sdk::Catalog* catalog{};
+    std::uint32_t scenarioRow{sdk::format::kAbsentIndex};
+    std::uint32_t stateRow{sdk::format::kAbsentIndex};
+    std::uint64_t activityClientGeneration{};
+    std::uint64_t revision{};
+    std::uint64_t publishedRevision{};
+    std::int32_t effectiveRegion{-1};
+    bool configured{};
+    bool publicationPending{};
+    bool regionArrivalPending{};
+    std::string query{};
+
+    [[nodiscard]] bool operator==(const SceneBrowserKey&) const noexcept = default;
+};
+
+/** Bounds how stale a row's availability can be when nothing observable changed. */
+constexpr double kSceneRefreshSeconds = 0.25;
+SceneBrowserKey g_visibleScenesKey{};
+double g_visibleScenesTime{-1.0};
+bool g_visibleScenesReady{};
+
 /** @return The global row of one borrowed slot or the absent marker. */
 [[nodiscard]] std::uint32_t global_slot_row(const sdk::Catalog& catalog,
                                             const sdk::format::Slot& slot) noexcept {
@@ -146,8 +174,28 @@ std::vector<SceneBrowserRow> g_visibleScenes{};
 
 /** Builds type-43 rows from only the selected state-ordinal-0 occurrence set. */
 [[nodiscard]] bool materialize_visible_scenes(const sdk::BoundView& view,
-                                              std::uint32_t stateRow) noexcept {
+                                              const mission::Snapshot& snapshot) noexcept {
+    const std::uint32_t stateRow = snapshot.plan.stateRow;
     try {
+        SceneBrowserKey key{.catalog = view.catalog.get(),
+                            .scenarioRow = view.scenarioRow,
+                            .stateRow = stateRow,
+                            .activityClientGeneration = snapshot.activityClientGeneration,
+                            .revision = snapshot.revision,
+                            .publishedRevision = snapshot.publishedRevision,
+                            .effectiveRegion = snapshot.effectiveRegion,
+                            .configured = snapshot.configured,
+                            .publicationPending = snapshot.publicationPending,
+                            .regionArrivalPending = snapshot.regionArrivalPending,
+                            .query = std::string(search_text())};
+        const double now = ImGui::GetTime();
+        if (key == g_visibleScenesKey && g_visibleScenesTime >= 0.0
+            && now - g_visibleScenesTime < kSceneRefreshSeconds) {
+            return g_visibleScenesReady;
+        }
+        g_visibleScenesKey = std::move(key);
+        g_visibleScenesTime = now;
+        g_visibleScenesReady = false;
         g_visibleScenes.clear();
         if (view.catalog == nullptr || stateRow >= view.catalog->states().size()) {
             return false;
@@ -185,16 +233,19 @@ std::vector<SceneBrowserRow> g_visibleScenes{};
                 }
             }
         }
+        g_visibleScenesReady = true;
         return true;
     } catch (...) {
         g_visibleScenes.clear();
+        g_visibleScenesKey = {};
+        g_visibleScenesTime = -1.0;
         return false;
     }
 }
 
 /** Draws searchable exact type-43 rows and their manual one-generation action. */
 void draw_authored_scenes(const sdk::BoundView& view, const mission::Snapshot& snapshot) noexcept {
-    if (!materialize_visible_scenes(view, snapshot.plan.stateRow)) {
+    if (!materialize_visible_scenes(view, snapshot)) {
         ImGui::TextDisabled("No scene in this state.");
         return;
     }
@@ -328,6 +379,7 @@ void draw_authored_scenes(const sdk::BoundView& view, const mission::Snapshot& s
                 g_sceneActionStatus =
                     mission::activate_authored_scene(view, row.occurrenceRow, row.slotRow);
                 g_hasSceneActionStatus = true;
+                g_visibleScenesTime = -1.0;
             }
             ImGui::EndDisabled();
             if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {