From 0e64dbe1be029a4b2816a0c92d909d3883348dfa Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:09:15 -0500 Subject: [PATCH 01/12] Stage 4: add batched easing evaluation API --- RetroFE/Source/Graphics/Animate/Tween.cpp | 134 +++++++- RetroFE/Source/Graphics/Animate/Tween.h | 12 +- .../Source/Graphics/Component/Component.cpp | 296 ++++++++++-------- docs/animation-batching-simd-plan.md | 163 ++++++++++ 4 files changed, 478 insertions(+), 127 deletions(-) create mode 100644 docs/animation-batching-simd-plan.md diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index 2e752286a..b584e134d 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -20,6 +20,19 @@ #include #include #include +#include + +namespace { +std::string trimPlaylistToken(const std::string& token) { + const size_t first = token.find_first_not_of(" \t\n\r"); + if (first == std::string::npos) { + return ""; + } + + const size_t last = token.find_last_not_of(" \t\n\r"); + return token.substr(first, last - first + 1); +} +} std::unordered_map Tween::tweenTypeMap_ = { {"easeinquadratic", EASE_IN_QUADRATIC}, @@ -78,6 +91,24 @@ Tween::Tween(TweenProperty property, TweenAlgorithm type, float start, float end , type(type) , start(start) , end(end) { + if (!playlistFilter.empty()) { + std::stringstream ss(playlistFilter); + std::string playlist; + while (std::getline(ss, playlist, ',')) { + playlist = trimPlaylistToken(playlist); + if (!playlist.empty()) { + playlistFilterTokens.push_back(std::move(playlist)); + } + } + } +} + +bool Tween::matchesPlaylist(const std::string& currentPlaylist) const { + if (playlistFilterTokens.empty() || currentPlaylist.empty()) { + return true; + } + + return std::find(playlistFilterTokens.begin(), playlistFilterTokens.end(), currentPlaylist) != playlistFilterTokens.end(); } std::optional Tween::getTweenProperty(const std::string& name) { @@ -103,6 +134,107 @@ TweenAlgorithm Tween::getTweenType(const std::string& name) { return LINEAR; } + +Tween::EasingKernel Tween::getKernel(TweenAlgorithm type) { + switch (type) { + case EASE_IN_QUADRATIC: return &Tween::easeInQuadratic; + case EASE_OUT_QUADRATIC: return &Tween::easeOutQuadratic; + case EASE_INOUT_QUADRATIC: return &Tween::easeInOutQuadratic; + case EASE_IN_CUBIC: return &Tween::easeInCubic; + case EASE_OUT_CUBIC: return &Tween::easeOutCubic; + case EASE_INOUT_CUBIC: return &Tween::easeInOutCubic; + case EASE_IN_QUARTIC: return &Tween::easeInQuartic; + case EASE_OUT_QUARTIC: return &Tween::easeOutQuartic; + case EASE_INOUT_QUARTIC: return &Tween::easeInOutQuartic; + case EASE_IN_QUINTIC: return &Tween::easeInQuintic; + case EASE_OUT_QUINTIC: return &Tween::easeOutQuintic; + case EASE_INOUT_QUINTIC: return &Tween::easeInOutQuintic; + case EASE_IN_SINE: return &Tween::easeInSine; + case EASE_OUT_SINE: return &Tween::easeOutSine; + case EASE_INOUT_SINE: return &Tween::easeInOutSine; + case EASE_IN_EXPONENTIAL: return &Tween::easeInExponential; + case EASE_OUT_EXPONENTIAL: return &Tween::easeOutExponential; + case EASE_INOUT_EXPONENTIAL: return &Tween::easeInOutExponential; + case EASE_IN_CIRCULAR: return &Tween::easeInCircular; + case EASE_OUT_CIRCULAR: return &Tween::easeOutCircular; + case EASE_INOUT_CIRCULAR: return &Tween::easeInOutCircular; + case LINEAR: + default: return &Tween::linear; + } +} + +void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const float* start, const float* change, float* out, size_t count) { + switch (type) { + case EASE_IN_QUADRATIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInQuadratic(progress[i], start[i], change[i]); + break; + case EASE_OUT_QUADRATIC: + for (size_t i = 0; i < count; ++i) out[i] = easeOutQuadratic(progress[i], start[i], change[i]); + break; + case EASE_INOUT_QUADRATIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutQuadratic(progress[i], start[i], change[i]); + break; + case EASE_IN_CUBIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInCubic(progress[i], start[i], change[i]); + break; + case EASE_OUT_CUBIC: + for (size_t i = 0; i < count; ++i) out[i] = easeOutCubic(progress[i], start[i], change[i]); + break; + case EASE_INOUT_CUBIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutCubic(progress[i], start[i], change[i]); + break; + case EASE_IN_QUARTIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInQuartic(progress[i], start[i], change[i]); + break; + case EASE_OUT_QUARTIC: + for (size_t i = 0; i < count; ++i) out[i] = easeOutQuartic(progress[i], start[i], change[i]); + break; + case EASE_INOUT_QUARTIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutQuartic(progress[i], start[i], change[i]); + break; + case EASE_IN_QUINTIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInQuintic(progress[i], start[i], change[i]); + break; + case EASE_OUT_QUINTIC: + for (size_t i = 0; i < count; ++i) out[i] = easeOutQuintic(progress[i], start[i], change[i]); + break; + case EASE_INOUT_QUINTIC: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutQuintic(progress[i], start[i], change[i]); + break; + case EASE_IN_SINE: + for (size_t i = 0; i < count; ++i) out[i] = easeInSine(progress[i], start[i], change[i]); + break; + case EASE_OUT_SINE: + for (size_t i = 0; i < count; ++i) out[i] = easeOutSine(progress[i], start[i], change[i]); + break; + case EASE_INOUT_SINE: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutSine(progress[i], start[i], change[i]); + break; + case EASE_IN_EXPONENTIAL: + for (size_t i = 0; i < count; ++i) out[i] = easeInExponential(progress[i], start[i], change[i]); + break; + case EASE_OUT_EXPONENTIAL: + for (size_t i = 0; i < count; ++i) out[i] = easeOutExponential(progress[i], start[i], change[i]); + break; + case EASE_INOUT_EXPONENTIAL: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutExponential(progress[i], start[i], change[i]); + break; + case EASE_IN_CIRCULAR: + for (size_t i = 0; i < count; ++i) out[i] = easeInCircular(progress[i], start[i], change[i]); + break; + case EASE_OUT_CIRCULAR: + for (size_t i = 0; i < count; ++i) out[i] = easeOutCircular(progress[i], start[i], change[i]); + break; + case EASE_INOUT_CIRCULAR: + for (size_t i = 0; i < count; ++i) out[i] = easeInOutCircular(progress[i], start[i], change[i]); + break; + case LINEAR: + default: + for (size_t i = 0; i < count; ++i) out[i] = linear(progress[i], start[i], change[i]); + break; + } +} + float Tween::animate(double elapsedTime) const { return animateSingle(type, start, end, duration, static_cast(elapsedTime)); } @@ -263,4 +395,4 @@ float Tween::easeInOutCircular(float p, float b, float c) { if (p < 1.0f) return -c / 2.0f * (sqrtf(1.0f - p * p) - 1.0f) + b; p -= 2.0f; return c / 2.0f * (sqrtf(1.0f - p * p) + 1.0f); -} \ No newline at end of file +} diff --git a/RetroFE/Source/Graphics/Animate/Tween.h b/RetroFE/Source/Graphics/Animate/Tween.h index d3ffa0f5f..16e588f57 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.h +++ b/RetroFE/Source/Graphics/Animate/Tween.h @@ -19,11 +19,14 @@ #include #include #include +#include +#include class ViewInfo; class Tween { public: + using EasingKernel = float (*)(float, float, float); Tween(TweenProperty property, TweenAlgorithm type, float start, float end, float duration, const std::string& playlistFilter = ""); @@ -36,11 +39,18 @@ class Tween { static TweenAlgorithm getTweenType(const std::string& name); static std::optional getTweenProperty(const std::string& name); + bool matchesPlaylist(const std::string& currentPlaylist) const; + TweenAlgorithm algorithm() const { return type; } + float startValue() const { return start; } + float endValue() const { return end; } + static EasingKernel getKernel(TweenAlgorithm type); + static void evaluateBatch(TweenAlgorithm type, const float* progress, const float* start, const float* change, float* out, size_t count); TweenProperty property; float duration; bool startDefined{ true }; std::string playlistFilter; + std::vector playlistFilterTokens; private: // Easing functions use a normalized progress value for calculation. @@ -74,4 +84,4 @@ class Tween { TweenAlgorithm type; float start; float end; -}; \ No newline at end of file +}; diff --git a/RetroFE/Source/Graphics/Component/Component.cpp b/RetroFE/Source/Graphics/Component/Component.cpp index 173be9aaa..786046efd 100644 --- a/RetroFE/Source/Graphics/Component/Component.cpp +++ b/RetroFE/Source/Graphics/Component/Component.cpp @@ -19,6 +19,8 @@ #include "../../Utility/Log.h" #include "../../SDL.h" #include "../PageBuilder.h" +#include +#include Component::Component(Page &p) : page(p) @@ -241,184 +243,228 @@ bool Component::animate() { auto tweens = sharedTweens->tweenSet(currentTweenIndex_); if (!tweens) return true; // Additional check for safety - std::string playlist; - bool foundFiltered; + struct TweenEvaluation { + const Tween* tween; + TweenProperty property; + double elapsedTime; + float startValue; + float value; + }; + + constexpr size_t tweenAlgorithmCount = static_cast(EASE_INOUT_CIRCULAR) + 1; + std::array, tweenAlgorithmCount> algorithmBuckets; + std::vector evaluations; + evaluations.reserve(tweens->size()); + + auto getStoreValueForProperty = [this](TweenProperty property) -> float { + switch (property) { + case TWEEN_PROPERTY_X: return storeViewInfo_.X; + case TWEEN_PROPERTY_Y: return storeViewInfo_.Y; + case TWEEN_PROPERTY_HEIGHT: return storeViewInfo_.Height; + case TWEEN_PROPERTY_WIDTH: return storeViewInfo_.Width; + case TWEEN_PROPERTY_ANGLE: return storeViewInfo_.Angle; + case TWEEN_PROPERTY_ALPHA: return storeViewInfo_.Alpha; + case TWEEN_PROPERTY_X_ORIGIN: return storeViewInfo_.XOrigin; + case TWEEN_PROPERTY_Y_ORIGIN: return storeViewInfo_.YOrigin; + case TWEEN_PROPERTY_X_OFFSET: return storeViewInfo_.XOffset; + case TWEEN_PROPERTY_Y_OFFSET: return storeViewInfo_.YOffset; + case TWEEN_PROPERTY_FONT_SIZE: return storeViewInfo_.FontSize; + case TWEEN_PROPERTY_BACKGROUND_ALPHA: return storeViewInfo_.BackgroundAlpha; + case TWEEN_PROPERTY_MAX_WIDTH: return storeViewInfo_.MaxWidth; + case TWEEN_PROPERTY_MAX_HEIGHT: return storeViewInfo_.MaxHeight; + case TWEEN_PROPERTY_LAYER: return static_cast(storeViewInfo_.Layer); + case TWEEN_PROPERTY_CONTAINER_X: return storeViewInfo_.ContainerX; + case TWEEN_PROPERTY_CONTAINER_Y: return storeViewInfo_.ContainerY; + case TWEEN_PROPERTY_CONTAINER_WIDTH: return storeViewInfo_.ContainerWidth; + case TWEEN_PROPERTY_CONTAINER_HEIGHT: return storeViewInfo_.ContainerHeight; + case TWEEN_PROPERTY_VOLUME: return storeViewInfo_.Volume; + case TWEEN_PROPERTY_MONITOR: return static_cast(storeViewInfo_.Monitor); + case TWEEN_PROPERTY_NOP: + case TWEEN_PROPERTY_RESTART: + default: return 0.0f; + } + }; for (unsigned int i = 0; i < tweens->size(); i++) { const Tween* tween = tweens->getTween(i); // Ensure const correctness - - if (!tween->playlistFilter.empty() && !playlistName.empty()) { - foundFiltered = false; - std::stringstream ss(tween->playlistFilter); - while (getline(ss, playlist, ',')) { - if (playlistName == playlist) { - foundFiltered = true; - break; - } - } - if (!foundFiltered) continue; + if (!tween || !tween->matchesPlaylist(playlistName)) { + continue; } double elapsedTime = elapsedTweenTime_; - if (elapsedTime < tween->duration) + if (elapsedTime < tween->duration) { currentDone = false; - else + } + else { elapsedTime = tween->duration; + } + + const auto algorithmIndex = static_cast(tween->algorithm()); + if (algorithmIndex >= tweenAlgorithmCount) { + continue; + } + + const float resolvedStartValue = tween->startDefined + ? tween->startValue() + : getStoreValueForProperty(tween->property); + + TweenEvaluation evaluation { + tween, + tween->property, + elapsedTime, + resolvedStartValue, + 0.0f + }; + + const size_t evaluationIndex = evaluations.size(); + evaluations.push_back(evaluation); + algorithmBuckets[algorithmIndex].push_back(evaluationIndex); + } + + for (size_t algorithmIndex = 0; algorithmIndex < tweenAlgorithmCount; ++algorithmIndex) { + const auto algorithm = static_cast(algorithmIndex); + const auto& bucket = algorithmBuckets[algorithmIndex]; + if (bucket.empty()) { + continue; + } + + std::vector progress; + std::vector start; + std::vector change; + std::vector values; + std::vector outputIndices; + progress.reserve(bucket.size()); + start.reserve(bucket.size()); + change.reserve(bucket.size()); + values.reserve(bucket.size()); + outputIndices.reserve(bucket.size()); + + for (const size_t evaluationIndex : bucket) { + auto& evaluation = evaluations[evaluationIndex]; + const float endValue = evaluation.tween->endValue(); + const float duration = evaluation.tween->duration; + + if (duration <= 0.0f) { + evaluation.value = endValue; + continue; + } + + progress.push_back(static_cast(evaluation.elapsedTime) / duration); + start.push_back(evaluation.startValue); + change.push_back(endValue - evaluation.startValue); + outputIndices.push_back(evaluationIndex); + } + + if (outputIndices.empty()) { + continue; + } + + values.resize(outputIndices.size()); + Tween::evaluateBatch(algorithm, + progress.data(), + start.data(), + change.data(), + values.data(), + values.size()); + + for (size_t i = 0; i < outputIndices.size(); ++i) { + evaluations[outputIndices[i]].value = values[i]; + } + } - switch (tween->property) { + for (const auto& evaluation : evaluations) { + const float value = evaluation.value; + switch (evaluation.property) { case TWEEN_PROPERTY_X: - if (tween->startDefined) - baseViewInfo.X = tween->animate(elapsedTime); - else - baseViewInfo.X = tween->animate(elapsedTime, storeViewInfo_.X); - break; + baseViewInfo.X = value; + break; case TWEEN_PROPERTY_Y: - if (tween->startDefined) - baseViewInfo.Y = tween->animate(elapsedTime); - else - baseViewInfo.Y = tween->animate(elapsedTime, storeViewInfo_.Y); - break; + baseViewInfo.Y = value; + break; case TWEEN_PROPERTY_HEIGHT: - if (tween->startDefined) - baseViewInfo.Height = tween->animate(elapsedTime); - else - baseViewInfo.Height = tween->animate(elapsedTime, storeViewInfo_.Height); - break; + baseViewInfo.Height = value; + break; case TWEEN_PROPERTY_WIDTH: - if (tween->startDefined) - baseViewInfo.Width = tween->animate(elapsedTime); - else - baseViewInfo.Width = tween->animate(elapsedTime, storeViewInfo_.Width); - break; + baseViewInfo.Width = value; + break; case TWEEN_PROPERTY_ANGLE: - if (tween->startDefined) - baseViewInfo.Angle = tween->animate(elapsedTime); - else - baseViewInfo.Angle = tween->animate(elapsedTime, storeViewInfo_.Angle); - break; + baseViewInfo.Angle = value; + break; case TWEEN_PROPERTY_ALPHA: - if (tween->startDefined) - baseViewInfo.Alpha = tween->animate(elapsedTime); - else - baseViewInfo.Alpha = tween->animate(elapsedTime, storeViewInfo_.Alpha); - break; + baseViewInfo.Alpha = value; + break; case TWEEN_PROPERTY_X_ORIGIN: - if (tween->startDefined) - baseViewInfo.XOrigin = tween->animate(elapsedTime); - else - baseViewInfo.XOrigin = tween->animate(elapsedTime, storeViewInfo_.XOrigin); - break; + baseViewInfo.XOrigin = value; + break; case TWEEN_PROPERTY_Y_ORIGIN: - if (tween->startDefined) - baseViewInfo.YOrigin = tween->animate(elapsedTime); - else - baseViewInfo.YOrigin = tween->animate(elapsedTime, storeViewInfo_.YOrigin); - break; + baseViewInfo.YOrigin = value; + break; case TWEEN_PROPERTY_X_OFFSET: - if (tween->startDefined) - baseViewInfo.XOffset = tween->animate(elapsedTime); - else - baseViewInfo.XOffset = tween->animate(elapsedTime, storeViewInfo_.XOffset); - break; + baseViewInfo.XOffset = value; + break; case TWEEN_PROPERTY_Y_OFFSET: - if (tween->startDefined) - baseViewInfo.YOffset = tween->animate(elapsedTime); - else - baseViewInfo.YOffset = tween->animate(elapsedTime, storeViewInfo_.YOffset); - break; + baseViewInfo.YOffset = value; + break; case TWEEN_PROPERTY_FONT_SIZE: - if (tween->startDefined) - baseViewInfo.FontSize = tween->animate(elapsedTime); - else - baseViewInfo.FontSize = tween->animate(elapsedTime, storeViewInfo_.FontSize); - break; + baseViewInfo.FontSize = value; + break; case TWEEN_PROPERTY_BACKGROUND_ALPHA: - if (tween->startDefined) - baseViewInfo.BackgroundAlpha = tween->animate(elapsedTime); - else - baseViewInfo.BackgroundAlpha = tween->animate(elapsedTime, storeViewInfo_.BackgroundAlpha); - break; + baseViewInfo.BackgroundAlpha = value; + break; case TWEEN_PROPERTY_MAX_WIDTH: - if (tween->startDefined) - baseViewInfo.MaxWidth = tween->animate(elapsedTime); - else - baseViewInfo.MaxWidth = tween->animate(elapsedTime, storeViewInfo_.MaxWidth); - break; + baseViewInfo.MaxWidth = value; + break; case TWEEN_PROPERTY_MAX_HEIGHT: - if (tween->startDefined) - baseViewInfo.MaxHeight = tween->animate(elapsedTime); - else - baseViewInfo.MaxHeight = tween->animate(elapsedTime, storeViewInfo_.MaxHeight); - break; + baseViewInfo.MaxHeight = value; + break; case TWEEN_PROPERTY_LAYER: - if (tween->startDefined) - baseViewInfo.Layer = static_cast(tween->animate(elapsedTime)); - else - baseViewInfo.Layer = static_cast(tween->animate(elapsedTime, static_cast(storeViewInfo_.Layer))); - break; + baseViewInfo.Layer = static_cast(value); + break; case TWEEN_PROPERTY_CONTAINER_X: - if (tween->startDefined) - baseViewInfo.ContainerX = tween->animate(elapsedTime); - else - baseViewInfo.ContainerX = tween->animate(elapsedTime, storeViewInfo_.ContainerX); - break; + baseViewInfo.ContainerX = value; + break; case TWEEN_PROPERTY_CONTAINER_Y: - if (tween->startDefined) - baseViewInfo.ContainerY = tween->animate(elapsedTime); - else - baseViewInfo.ContainerY = tween->animate(elapsedTime, storeViewInfo_.ContainerY); - break; + baseViewInfo.ContainerY = value; + break; case TWEEN_PROPERTY_CONTAINER_WIDTH: - if (tween->startDefined) - baseViewInfo.ContainerWidth = tween->animate(elapsedTime); - else - baseViewInfo.ContainerWidth = tween->animate(elapsedTime, storeViewInfo_.ContainerWidth); - break; + baseViewInfo.ContainerWidth = value; + break; case TWEEN_PROPERTY_CONTAINER_HEIGHT: - if (tween->startDefined) - baseViewInfo.ContainerHeight = tween->animate(elapsedTime); - else - baseViewInfo.ContainerHeight = tween->animate(elapsedTime, storeViewInfo_.ContainerHeight); - break; + baseViewInfo.ContainerHeight = value; + break; case TWEEN_PROPERTY_VOLUME: - if (tween->startDefined) - baseViewInfo.Volume = tween->animate(elapsedTime); - else - baseViewInfo.Volume = tween->animate(elapsedTime, storeViewInfo_.Volume); - break; + baseViewInfo.Volume = value; + break; case TWEEN_PROPERTY_MONITOR: - if (tween->startDefined) - baseViewInfo.Monitor = static_cast(tween->animate(elapsedTime)); - else - baseViewInfo.Monitor = static_cast(tween->animate(elapsedTime, static_cast(storeViewInfo_.Monitor))); - break; + baseViewInfo.Monitor = static_cast(value); + break; case TWEEN_PROPERTY_NOP: - break; + break; + case TWEEN_PROPERTY_RESTART: - // Compare tween's float duration to a float literal. - baseViewInfo.Restart = (tween->duration != 0.0f) && (elapsedTime == 0.0); - break; + baseViewInfo.Restart = (evaluation.tween->duration != 0.0f) && (evaluation.elapsedTime == 0.0); + break; } } diff --git a/docs/animation-batching-simd-plan.md b/docs/animation-batching-simd-plan.md new file mode 100644 index 000000000..3a6654a4d --- /dev/null +++ b/docs/animation-batching-simd-plan.md @@ -0,0 +1,163 @@ +# Animation System: Batching + SIMD Opportunities + +This note identifies where RetroFE’s current animation path can benefit from batching and SIMD, what data layout changes would help most, and whether each change is likely worth implementing. + +## Current bottlenecks in the code path + +1. **Per-component, per-tween virtual-ish workflow with pointer chasing** + `Component::animate()` iterates each tween in the current set and reads it through `TweenSet::getTween(i)` (`unique_ptr` under the hood), then dispatches through a large `switch` on property and calls `Tween::animate(...)`. This creates branch-heavy code and poor cache locality. + Relevant code: `Component::animate`, `TweenSet`, `Tween::animateSingle`. + +2. **AoS/object layout limits vectorization** + Tweens are stored as heap objects (`std::unique_ptr`), and animations are nested containers of `shared_ptr`. This is flexible but not SIMD-friendly and expensive to traverse at scale. + +3. **Repeated playlist filter parsing in hot loop** + For every tween each update, code can build a `stringstream` and split comma-separated filters. That is substantial overhead relative to easing math. + +4. **Multiple branches per animated property** + For each tween there is an inner `switch` for property assignment and a branch on `startDefined`, plus algorithm branching in `Tween::animateSingle`. + +## Where batching should be introduced first (highest ROI) + +## 1) Batch *evaluation* in `Component::animate()` (low-risk, immediate gains) + +### Change +Introduce a precompiled runtime tween representation for each `TweenSet` that removes string parsing and minimizes branching: + +- Build once when loading XML / creating animations: + - `property` as compact enum + - `algorithm` as compact enum + - `startDefined` + resolved `start` + - `end`, `duration`, `invDuration` + - parsed playlist filter mask/list (no per-frame stringstream) +- At runtime, evaluate these precompiled entries with a tight loop. + +### Data structure +Use **SoA-like grouped arrays per algorithm** inside a compiled tween set: + +- `std::vector start, end, change, duration, invDuration` +- `std::vector property` +- `std::vector flags` (`startDefined`, etc.) +- Optionally maintain groups per algorithm (`linearGroup`, `easeInQuadGroup`, ...). + +This lets you run a contiguous loop per algorithm, opening a straightforward SIMD path. + +### Why here +This is exactly where per-frame cost accumulates and where data is currently most fragmented. + +--- + +## 2) Batch *property writes* by mapping tween properties to offsets (moderate risk) + +### Change +Replace the giant property `switch` in `Component::animate()` with a small property table: + +- For float properties, map `TweenProperty -> offsetof(ViewInfo, field)`. +- For integer-like properties (`Layer`, `Monitor`), keep separate conversion handlers. + +### Data structure + +- `constexpr` table of metadata per property: + - destination kind (`float`/`uint`/special) + - offset + - clamp/cast policy + +### Why +Cuts branch pressure and keeps hot loop more vectorization-friendly. + +--- + +## 3) Batch across components in `Page::update()` (moderate-high effort) + +### Change +Add an animation scheduler that gathers active components into work buckets each frame: + +- Bucket key: `(algorithm, propertyKind, monitor?)` +- Evaluate bucket arrays in one pass, then scatter results back to components. + +### Data structure +A frame-local job buffer (SoA): + +- `componentPtr[]` +- `propertyId[]` +- `elapsed[]` +- `start[]`, `change[]`, `invDuration[]` +- grouped/radix-partitioned by algorithm + +### Why +Enables wider SIMD batches and improves threading opportunities later. + +## SIMD strategy (practical for this codebase) + +## Stage A: auto-vectorization first + +- Keep easing kernels as small `inline` free functions over arrays. +- Use SoA and contiguous loops; compile with high optimization (`-O3`) and target-specific flags. +- This often gives “free” SSE/AVX/NEON wins without architecture-specific code. + +## Stage B: explicit SIMD only for top algorithms + +Implement explicit SIMD kernels for high-frequency easings first: + +- `linear` +- `easeIn/Out/InOutQuadratic` +- `easeIn/Out/InOutCubic` + +Leave transcendental-heavy easings (`sine`, `exponential`, `circular`) scalar initially unless profiling proves they dominate. + +## Stage C: optional approximate math + +If needed, add optional fast approximations for `sin/cos/pow/sqrt` behind a config flag (`fastAnimationMath=true`) with acceptable visual tolerance. + +## Recommended migration plan + +1. **Instrument before/after** + - Add timing counters around `Page::update()`, `Component::animate()`, and tween count processed. +2. **Compile tween sets** (no behavior change) + - Pre-parse playlist filters. + - Cache `invDuration` and `change`. +3. **Swap hot loop to compiled representation** + - Keep scalar math first, verify identical output. +4. **Algorithm-grouped loops + auto-vectorization** + - Measure gains. +5. **Optional explicit SIMD kernels** for top 2–4 easing families. +6. **Only then consider cross-component frame scheduler**. + +## Worthwhile? Expected payoff + +- **Yes, worthwhile** if typical layouts animate many components simultaneously (menus + media + text effects). +- Biggest likely wins: + 1. removing per-frame playlist filter string parsing, + 2. eliminating pointer-heavy tween traversal, + 3. improving cache locality with SoA, + 4. reducing switch/branch density. +- SIMD alone without data-layout cleanup will likely underperform expectations. + +## What *not* to do first + +- Don’t start with architecture-specific intrinsics before SoA + profiling. +- Don’t parallelize tiny per-component loops prematurely; synchronization overhead can erase gains. +- Don’t convert every easing to approximations unless profiling proves transcendental math is a bottleneck. + +## Concrete “best insertion points” in current source + +- **Primary hotspot:** `RetroFE/Source/Graphics/Component/Component.cpp` (`Component::animate`) + Introduce compiled tween buffers and property metadata here first. +- **Model/storage layer:** `RetroFE/Source/Graphics/Animate/TweenSet.*`, `Animation.*` + Add compiled/packed storage alongside existing objects, then phase out pointer-based traversal. +- **Frame-level batching point:** `RetroFE/Source/Graphics/Page.cpp` (`Page::update`) + Add optional scheduler once single-component path is optimized and benchmarked. +- **Kernel math location:** `RetroFE/Source/Graphics/Animate/Tween.*` + Split scalar kernels from dispatch; add batched evaluation entry points. + +## Suggested target data model (end state) + +- `CompiledTweenSet` + - `AlgorithmGroup groups[NUM_ALGOS]` +- `AlgorithmGroup` + - SoA arrays: `start[]`, `change[]`, `invDuration[]`, `duration[]`, `elapsedClamp[]` + - `property[]`, `componentIndex[]`, `flags[]` +- optional `FrameAnimationBatch` + - transient per-frame grouped views for cross-component processing + +This end state keeps authoring flexibility while making runtime animation updates data-oriented and SIMD-ready. From ccd2b6baf20916c156ebc82f2751391386dbe82d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:33:13 -0500 Subject: [PATCH 02/12] Stage 5: reuse component animation scratch buffers --- .../Source/Graphics/Component/Component.cpp | 92 ++++++++++--------- RetroFE/Source/Graphics/Component/Component.h | 19 ++++ 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/Component.cpp b/RetroFE/Source/Graphics/Component/Component.cpp index 786046efd..9fa76abeb 100644 --- a/RetroFE/Source/Graphics/Component/Component.cpp +++ b/RetroFE/Source/Graphics/Component/Component.cpp @@ -19,7 +19,6 @@ #include "../../Utility/Log.h" #include "../../SDL.h" #include "../PageBuilder.h" -#include #include Component::Component(Page &p) @@ -60,6 +59,16 @@ void Component::freeGraphicsMemory() { currentTweenComplete_ = true; elapsedTweenTime_ = 0; + tweenEvaluations_.clear(); + for (auto& bucket : tweenAlgorithmBuckets_) { + bucket.clear(); + } + tweenProgressBatch_.clear(); + tweenStartBatch_.clear(); + tweenChangeBatch_.clear(); + tweenValueBatch_.clear(); + tweenOutputIndices_.clear(); + if (backgroundTexture_) { SDL_DestroyTexture(backgroundTexture_); backgroundTexture_ = nullptr; @@ -243,18 +252,11 @@ bool Component::animate() { auto tweens = sharedTweens->tweenSet(currentTweenIndex_); if (!tweens) return true; // Additional check for safety - struct TweenEvaluation { - const Tween* tween; - TweenProperty property; - double elapsedTime; - float startValue; - float value; - }; - - constexpr size_t tweenAlgorithmCount = static_cast(EASE_INOUT_CIRCULAR) + 1; - std::array, tweenAlgorithmCount> algorithmBuckets; - std::vector evaluations; - evaluations.reserve(tweens->size()); + tweenEvaluations_.clear(); + tweenEvaluations_.reserve(tweens->size()); + for (auto& bucket : tweenAlgorithmBuckets_) { + bucket.clear(); + } auto getStoreValueForProperty = [this](TweenProperty property) -> float { switch (property) { @@ -300,7 +302,7 @@ bool Component::animate() { } const auto algorithmIndex = static_cast(tween->algorithm()); - if (algorithmIndex >= tweenAlgorithmCount) { + if (algorithmIndex >= kTweenAlgorithmCount) { continue; } @@ -316,31 +318,31 @@ bool Component::animate() { 0.0f }; - const size_t evaluationIndex = evaluations.size(); - evaluations.push_back(evaluation); - algorithmBuckets[algorithmIndex].push_back(evaluationIndex); + const size_t evaluationIndex = tweenEvaluations_.size(); + tweenEvaluations_.push_back(evaluation); + tweenAlgorithmBuckets_[algorithmIndex].push_back(evaluationIndex); } - for (size_t algorithmIndex = 0; algorithmIndex < tweenAlgorithmCount; ++algorithmIndex) { + for (size_t algorithmIndex = 0; algorithmIndex < kTweenAlgorithmCount; ++algorithmIndex) { const auto algorithm = static_cast(algorithmIndex); - const auto& bucket = algorithmBuckets[algorithmIndex]; + const auto& bucket = tweenAlgorithmBuckets_[algorithmIndex]; if (bucket.empty()) { continue; } - std::vector progress; - std::vector start; - std::vector change; - std::vector values; - std::vector outputIndices; - progress.reserve(bucket.size()); - start.reserve(bucket.size()); - change.reserve(bucket.size()); - values.reserve(bucket.size()); - outputIndices.reserve(bucket.size()); + tweenProgressBatch_.clear(); + tweenStartBatch_.clear(); + tweenChangeBatch_.clear(); + tweenValueBatch_.clear(); + tweenOutputIndices_.clear(); + tweenProgressBatch_.reserve(bucket.size()); + tweenStartBatch_.reserve(bucket.size()); + tweenChangeBatch_.reserve(bucket.size()); + tweenValueBatch_.reserve(bucket.size()); + tweenOutputIndices_.reserve(bucket.size()); for (const size_t evaluationIndex : bucket) { - auto& evaluation = evaluations[evaluationIndex]; + auto& evaluation = tweenEvaluations_[evaluationIndex]; const float endValue = evaluation.tween->endValue(); const float duration = evaluation.tween->duration; @@ -349,30 +351,30 @@ bool Component::animate() { continue; } - progress.push_back(static_cast(evaluation.elapsedTime) / duration); - start.push_back(evaluation.startValue); - change.push_back(endValue - evaluation.startValue); - outputIndices.push_back(evaluationIndex); + tweenProgressBatch_.push_back(static_cast(evaluation.elapsedTime) / duration); + tweenStartBatch_.push_back(evaluation.startValue); + tweenChangeBatch_.push_back(endValue - evaluation.startValue); + tweenOutputIndices_.push_back(evaluationIndex); } - if (outputIndices.empty()) { + if (tweenOutputIndices_.empty()) { continue; } - values.resize(outputIndices.size()); + tweenValueBatch_.resize(tweenOutputIndices_.size()); Tween::evaluateBatch(algorithm, - progress.data(), - start.data(), - change.data(), - values.data(), - values.size()); - - for (size_t i = 0; i < outputIndices.size(); ++i) { - evaluations[outputIndices[i]].value = values[i]; + tweenProgressBatch_.data(), + tweenStartBatch_.data(), + tweenChangeBatch_.data(), + tweenValueBatch_.data(), + tweenValueBatch_.size()); + + for (size_t i = 0; i < tweenOutputIndices_.size(); ++i) { + tweenEvaluations_[tweenOutputIndices_[i]].value = tweenValueBatch_[i]; } } - for (const auto& evaluation : evaluations) { + for (const auto& evaluation : tweenEvaluations_) { const float value = evaluation.value; switch (evaluation.property) { case TWEEN_PROPERTY_X: diff --git a/RetroFE/Source/Graphics/Component/Component.h b/RetroFE/Source/Graphics/Component/Component.h index e9abc0704..a29b444f7 100644 --- a/RetroFE/Source/Graphics/Component/Component.h +++ b/RetroFE/Source/Graphics/Component/Component.h @@ -15,6 +15,7 @@ */ #pragma once +#include #include #include "../../SDL.h" @@ -82,6 +83,16 @@ class Component private: + struct TweenEvaluation { + const Tween* tween; + TweenProperty property; + double elapsedTime; + float startValue; + float value; + }; + + static constexpr size_t kTweenAlgorithmCount = static_cast(EASE_INOUT_CIRCULAR) + 1; + bool animate(); std::shared_ptr tweens_; // Use shared_ptr for tweens_ @@ -100,4 +111,12 @@ class Component int menuIndex_; int id_; + std::vector tweenEvaluations_; + std::array, kTweenAlgorithmCount> tweenAlgorithmBuckets_; + std::vector tweenProgressBatch_; + std::vector tweenStartBatch_; + std::vector tweenChangeBatch_; + std::vector tweenValueBatch_; + std::vector tweenOutputIndices_; + }; From d6f61f7ca302ba6ab250d95f9a04b77c81fc7e94 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:31:57 -0500 Subject: [PATCH 03/12] Stage 6: compile TweenSet into packed runtime entries --- RetroFE/Source/Graphics/Animate/Tween.cpp | 10 ++++-- RetroFE/Source/Graphics/Animate/Tween.h | 2 ++ RetroFE/Source/Graphics/Animate/TweenSet.cpp | 26 ++++++++++++++- RetroFE/Source/Graphics/Animate/TweenSet.h | 14 ++++++++ .../Source/Graphics/Component/Component.cpp | 32 ++++++++++--------- RetroFE/Source/Graphics/Component/Component.h | 3 +- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index b584e134d..f52eb2d15 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -103,12 +103,16 @@ Tween::Tween(TweenProperty property, TweenAlgorithm type, float start, float end } } -bool Tween::matchesPlaylist(const std::string& currentPlaylist) const { - if (playlistFilterTokens.empty() || currentPlaylist.empty()) { +bool Tween::matchesPlaylistTokens(const std::vector& tokens, const std::string& currentPlaylist) { + if (tokens.empty() || currentPlaylist.empty()) { return true; } - return std::find(playlistFilterTokens.begin(), playlistFilterTokens.end(), currentPlaylist) != playlistFilterTokens.end(); + return std::find(tokens.begin(), tokens.end(), currentPlaylist) != tokens.end(); +} + +bool Tween::matchesPlaylist(const std::string& currentPlaylist) const { + return matchesPlaylistTokens(playlistFilterTokens, currentPlaylist); } std::optional Tween::getTweenProperty(const std::string& name) { diff --git a/RetroFE/Source/Graphics/Animate/Tween.h b/RetroFE/Source/Graphics/Animate/Tween.h index 16e588f57..02b3022f2 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.h +++ b/RetroFE/Source/Graphics/Animate/Tween.h @@ -40,6 +40,7 @@ class Tween { static TweenAlgorithm getTweenType(const std::string& name); static std::optional getTweenProperty(const std::string& name); bool matchesPlaylist(const std::string& currentPlaylist) const; + static bool matchesPlaylistTokens(const std::vector& tokens, const std::string& currentPlaylist); TweenAlgorithm algorithm() const { return type; } float startValue() const { return start; } float endValue() const { return end; } @@ -50,6 +51,7 @@ class Tween { float duration; bool startDefined{ true }; std::string playlistFilter; + const std::vector& playlistTokens() const { return playlistFilterTokens; } std::vector playlistFilterTokens; private: diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.cpp b/RetroFE/Source/Graphics/Animate/TweenSet.cpp index c77a96f8c..0546c108a 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.cpp +++ b/RetroFE/Source/Graphics/Animate/TweenSet.cpp @@ -17,20 +17,37 @@ TweenSet::TweenSet() = default; +TweenSet::CompiledTweenEntry TweenSet::compileTween(const Tween& tween) { + return CompiledTweenEntry { + tween.property, + tween.algorithm(), + tween.duration, + tween.startDefined, + tween.startValue(), + tween.endValue(), + tween.playlistTokens() + }; +} + TweenSet::TweenSet(const TweenSet& copy) { set_.reserve(copy.set_.size()); + compiledSet_.reserve(copy.compiledSet_.size()); for (const auto& tween : copy.set_) { set_.push_back(std::make_unique(*tween)); } + compiledSet_ = copy.compiledSet_; } TweenSet& TweenSet::operator=(const TweenSet& other) { if (this != &other) { set_.clear(); // Clear existing tweens + compiledSet_.clear(); set_.reserve(other.set_.size()); + compiledSet_.reserve(other.compiledSet_.size()); for (const auto& tween : other.set_) { set_.push_back(std::make_unique(*tween)); } + compiledSet_ = other.compiledSet_; } return *this; } @@ -42,11 +59,15 @@ TweenSet::~TweenSet() } void TweenSet::push(std::unique_ptr tween) { + if (tween) { + compiledSet_.push_back(compileTween(*tween)); + } set_.push_back(std::move(tween)); } void TweenSet::clear() { set_.clear(); + compiledSet_.clear(); } Tween* TweenSet::getTween(unsigned int index) const { @@ -56,8 +77,11 @@ Tween* TweenSet::getTween(unsigned int index) const { return nullptr; } +const std::vector& TweenSet::compiledTweens() const { + return compiledSet_; +} size_t TweenSet::size() const { - return set_.size(); + return compiledSet_.size(); } diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.h b/RetroFE/Source/Graphics/Animate/TweenSet.h index e8016998d..2ebe4cc27 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.h +++ b/RetroFE/Source/Graphics/Animate/TweenSet.h @@ -22,6 +22,16 @@ class TweenSet { public: + struct CompiledTweenEntry { + TweenProperty property; + TweenAlgorithm algorithm; + float duration; + bool startDefined; + float startValue; + float endValue; + std::vector playlistTokens; + }; + TweenSet(); TweenSet(const TweenSet& copy); TweenSet& operator=(const TweenSet& other); @@ -29,9 +39,13 @@ class TweenSet void push(std::unique_ptr tween); void clear(); Tween* getTween(unsigned int index) const; + const std::vector& compiledTweens() const; size_t size() const; private: + static CompiledTweenEntry compileTween(const Tween& tween); + std::vector> set_; + std::vector compiledSet_; }; diff --git a/RetroFE/Source/Graphics/Component/Component.cpp b/RetroFE/Source/Graphics/Component/Component.cpp index 9fa76abeb..e05958d05 100644 --- a/RetroFE/Source/Graphics/Component/Component.cpp +++ b/RetroFE/Source/Graphics/Component/Component.cpp @@ -252,8 +252,10 @@ bool Component::animate() { auto tweens = sharedTweens->tweenSet(currentTweenIndex_); if (!tweens) return true; // Additional check for safety + const auto& compiledTweens = tweens->compiledTweens(); + tweenEvaluations_.clear(); - tweenEvaluations_.reserve(tweens->size()); + tweenEvaluations_.reserve(compiledTweens.size()); for (auto& bucket : tweenAlgorithmBuckets_) { bucket.clear(); } @@ -287,34 +289,34 @@ bool Component::animate() { } }; - for (unsigned int i = 0; i < tweens->size(); i++) { - const Tween* tween = tweens->getTween(i); // Ensure const correctness - if (!tween || !tween->matchesPlaylist(playlistName)) { + for (const auto& compiledTween : compiledTweens) { + if (!Tween::matchesPlaylistTokens(compiledTween.playlistTokens, playlistName)) { continue; } double elapsedTime = elapsedTweenTime_; - if (elapsedTime < tween->duration) { + if (elapsedTime < compiledTween.duration) { currentDone = false; } else { - elapsedTime = tween->duration; + elapsedTime = compiledTween.duration; } - const auto algorithmIndex = static_cast(tween->algorithm()); + const auto algorithmIndex = static_cast(compiledTween.algorithm); if (algorithmIndex >= kTweenAlgorithmCount) { continue; } - const float resolvedStartValue = tween->startDefined - ? tween->startValue() - : getStoreValueForProperty(tween->property); + const float resolvedStartValue = compiledTween.startDefined + ? compiledTween.startValue + : getStoreValueForProperty(compiledTween.property); TweenEvaluation evaluation { - tween, - tween->property, + compiledTween.property, elapsedTime, + compiledTween.duration, resolvedStartValue, + compiledTween.endValue, 0.0f }; @@ -343,8 +345,8 @@ bool Component::animate() { for (const size_t evaluationIndex : bucket) { auto& evaluation = tweenEvaluations_[evaluationIndex]; - const float endValue = evaluation.tween->endValue(); - const float duration = evaluation.tween->duration; + const float endValue = evaluation.endValue; + const float duration = evaluation.duration; if (duration <= 0.0f) { evaluation.value = endValue; @@ -465,7 +467,7 @@ bool Component::animate() { break; case TWEEN_PROPERTY_RESTART: - baseViewInfo.Restart = (evaluation.tween->duration != 0.0f) && (evaluation.elapsedTime == 0.0); + baseViewInfo.Restart = (evaluation.duration != 0.0f) && (evaluation.elapsedTime == 0.0); break; } } diff --git a/RetroFE/Source/Graphics/Component/Component.h b/RetroFE/Source/Graphics/Component/Component.h index a29b444f7..86271b35f 100644 --- a/RetroFE/Source/Graphics/Component/Component.h +++ b/RetroFE/Source/Graphics/Component/Component.h @@ -84,10 +84,11 @@ class Component private: struct TweenEvaluation { - const Tween* tween; TweenProperty property; double elapsedTime; + float duration; float startValue; + float endValue; float value; }; From 4761c6bd58f6cb359a8947f488beb2719c808735 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:40:50 -0500 Subject: [PATCH 04/12] Stage 7: switch compiled playlist filters to integer IDs --- RetroFE/Source/Graphics/Animate/Tween.cpp | 36 +++++++++++++++++-- RetroFE/Source/Graphics/Animate/Tween.h | 7 ++++ RetroFE/Source/Graphics/Animate/TweenSet.cpp | 2 +- RetroFE/Source/Graphics/Animate/TweenSet.h | 3 +- .../Source/Graphics/Component/Component.cpp | 4 ++- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index f52eb2d15..b5b35e0e1 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace { std::string trimPlaylistToken(const std::string& token) { @@ -84,6 +85,10 @@ std::unordered_map Tween::tweenPropertyMap_ = { {"restart", TWEEN_PROPERTY_RESTART} }; +std::unordered_map Tween::playlistTokenIdMap_ = {}; +uint32_t Tween::nextPlaylistTokenId_ = 1; + + Tween::Tween(TweenProperty property, TweenAlgorithm type, float start, float end, float duration, const std::string& playlistFilter) : property(property) , duration(duration) @@ -97,7 +102,8 @@ Tween::Tween(TweenProperty property, TweenAlgorithm type, float start, float end while (std::getline(ss, playlist, ',')) { playlist = trimPlaylistToken(playlist); if (!playlist.empty()) { - playlistFilterTokens.push_back(std::move(playlist)); + playlistFilterTokens.push_back(playlist); + playlistFilterTokenIds.push_back(playlistTokenId(playlist)); } } } @@ -111,8 +117,34 @@ bool Tween::matchesPlaylistTokens(const std::vector& tokens, const return std::find(tokens.begin(), tokens.end(), currentPlaylist) != tokens.end(); } +uint32_t Tween::playlistTokenId(const std::string& token) { + static std::mutex playlistTokenIdMapMutex; + std::lock_guard lock(playlistTokenIdMapMutex); + + auto it = playlistTokenIdMap_.find(token); + if (it != playlistTokenIdMap_.end()) { + return it->second; + } + + const uint32_t assignedId = nextPlaylistTokenId_++; + playlistTokenIdMap_[token] = assignedId; + return assignedId; +} + +bool Tween::matchesPlaylistTokenIds(const std::vector& tokenIds, uint32_t currentPlaylistId, bool hasCurrentPlaylist) { + if (tokenIds.empty() || !hasCurrentPlaylist) { + return true; + } + + return std::find(tokenIds.begin(), tokenIds.end(), currentPlaylistId) != tokenIds.end(); +} + bool Tween::matchesPlaylist(const std::string& currentPlaylist) const { - return matchesPlaylistTokens(playlistFilterTokens, currentPlaylist); + if (playlistFilterTokenIds.empty() || currentPlaylist.empty()) { + return true; + } + + return matchesPlaylistTokenIds(playlistFilterTokenIds, playlistTokenId(currentPlaylist), true); } std::optional Tween::getTweenProperty(const std::string& name) { diff --git a/RetroFE/Source/Graphics/Animate/Tween.h b/RetroFE/Source/Graphics/Animate/Tween.h index 02b3022f2..27be65036 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.h +++ b/RetroFE/Source/Graphics/Animate/Tween.h @@ -21,6 +21,7 @@ #include #include #include +#include class ViewInfo; @@ -41,6 +42,8 @@ class Tween { static std::optional getTweenProperty(const std::string& name); bool matchesPlaylist(const std::string& currentPlaylist) const; static bool matchesPlaylistTokens(const std::vector& tokens, const std::string& currentPlaylist); + static uint32_t playlistTokenId(const std::string& token); + static bool matchesPlaylistTokenIds(const std::vector& tokenIds, uint32_t currentPlaylistId, bool hasCurrentPlaylist); TweenAlgorithm algorithm() const { return type; } float startValue() const { return start; } float endValue() const { return end; } @@ -52,7 +55,9 @@ class Tween { bool startDefined{ true }; std::string playlistFilter; const std::vector& playlistTokens() const { return playlistFilterTokens; } + const std::vector& playlistTokenIds() const { return playlistFilterTokenIds; } std::vector playlistFilterTokens; + std::vector playlistFilterTokenIds; private: // Easing functions use a normalized progress value for calculation. @@ -82,6 +87,8 @@ class Tween { static std::unordered_map tweenTypeMap_; static std::unordered_map tweenPropertyMap_; + static std::unordered_map playlistTokenIdMap_; + static uint32_t nextPlaylistTokenId_; TweenAlgorithm type; float start; diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.cpp b/RetroFE/Source/Graphics/Animate/TweenSet.cpp index 0546c108a..392c509e9 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.cpp +++ b/RetroFE/Source/Graphics/Animate/TweenSet.cpp @@ -25,7 +25,7 @@ TweenSet::CompiledTweenEntry TweenSet::compileTween(const Tween& tween) { tween.startDefined, tween.startValue(), tween.endValue(), - tween.playlistTokens() + tween.playlistTokenIds() }; } diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.h b/RetroFE/Source/Graphics/Animate/TweenSet.h index 2ebe4cc27..fb7bc0056 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.h +++ b/RetroFE/Source/Graphics/Animate/TweenSet.h @@ -18,6 +18,7 @@ #include "Tween.h" #include #include +#include class TweenSet { @@ -29,7 +30,7 @@ class TweenSet bool startDefined; float startValue; float endValue; - std::vector playlistTokens; + std::vector playlistTokenIds; }; TweenSet(); diff --git a/RetroFE/Source/Graphics/Component/Component.cpp b/RetroFE/Source/Graphics/Component/Component.cpp index e05958d05..47fde9b32 100644 --- a/RetroFE/Source/Graphics/Component/Component.cpp +++ b/RetroFE/Source/Graphics/Component/Component.cpp @@ -253,6 +253,8 @@ bool Component::animate() { if (!tweens) return true; // Additional check for safety const auto& compiledTweens = tweens->compiledTweens(); + const bool hasCurrentPlaylist = !playlistName.empty(); + const uint32_t currentPlaylistId = hasCurrentPlaylist ? Tween::playlistTokenId(playlistName) : 0; tweenEvaluations_.clear(); tweenEvaluations_.reserve(compiledTweens.size()); @@ -290,7 +292,7 @@ bool Component::animate() { }; for (const auto& compiledTween : compiledTweens) { - if (!Tween::matchesPlaylistTokens(compiledTween.playlistTokens, playlistName)) { + if (!Tween::matchesPlaylistTokenIds(compiledTween.playlistTokenIds, currentPlaylistId, hasCurrentPlaylist)) { continue; } From eeaf05998d1a31c00cd583cf46de5cdf8a8d5380 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:44:26 -0500 Subject: [PATCH 05/12] Stage 8: make TweenSet runtime-compiled only --- RetroFE/Source/Graphics/Animate/TweenSet.cpp | 26 ++------------------ RetroFE/Source/Graphics/Animate/TweenSet.h | 2 -- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.cpp b/RetroFE/Source/Graphics/Animate/TweenSet.cpp index 392c509e9..e71d27052 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.cpp +++ b/RetroFE/Source/Graphics/Animate/TweenSet.cpp @@ -29,30 +29,17 @@ TweenSet::CompiledTweenEntry TweenSet::compileTween(const Tween& tween) { }; } -TweenSet::TweenSet(const TweenSet& copy) { - set_.reserve(copy.set_.size()); - compiledSet_.reserve(copy.compiledSet_.size()); - for (const auto& tween : copy.set_) { - set_.push_back(std::make_unique(*tween)); - } - compiledSet_ = copy.compiledSet_; +TweenSet::TweenSet(const TweenSet& copy) + : compiledSet_(copy.compiledSet_) { } TweenSet& TweenSet::operator=(const TweenSet& other) { if (this != &other) { - set_.clear(); // Clear existing tweens - compiledSet_.clear(); - set_.reserve(other.set_.size()); - compiledSet_.reserve(other.compiledSet_.size()); - for (const auto& tween : other.set_) { - set_.push_back(std::make_unique(*tween)); - } compiledSet_ = other.compiledSet_; } return *this; } - TweenSet::~TweenSet() { clear(); @@ -62,21 +49,12 @@ void TweenSet::push(std::unique_ptr tween) { if (tween) { compiledSet_.push_back(compileTween(*tween)); } - set_.push_back(std::move(tween)); } void TweenSet::clear() { - set_.clear(); compiledSet_.clear(); } -Tween* TweenSet::getTween(unsigned int index) const { - if (index < set_.size()) { - return set_[index].get(); - } - return nullptr; -} - const std::vector& TweenSet::compiledTweens() const { return compiledSet_; } diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.h b/RetroFE/Source/Graphics/Animate/TweenSet.h index fb7bc0056..0db5fcc4e 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.h +++ b/RetroFE/Source/Graphics/Animate/TweenSet.h @@ -39,7 +39,6 @@ class TweenSet ~TweenSet(); void push(std::unique_ptr tween); void clear(); - Tween* getTween(unsigned int index) const; const std::vector& compiledTweens() const; size_t size() const; @@ -47,6 +46,5 @@ class TweenSet private: static CompiledTweenEntry compileTween(const Tween& tween); - std::vector> set_; std::vector compiledSet_; }; From 87f02a93c3b1a5060e46fe2e7b8bf88bb80a90ec Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:56:06 -0500 Subject: [PATCH 06/12] Stage 9: switch ScrollingList to compiled tween authoring --- RetroFE/Source/Graphics/Animate/TweenSet.cpp | 5 ++ RetroFE/Source/Graphics/Animate/TweenSet.h | 1 + .../Graphics/Component/ScrollingList.cpp | 49 +++++++++++-------- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.cpp b/RetroFE/Source/Graphics/Animate/TweenSet.cpp index e71d27052..d6fa687d9 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.cpp +++ b/RetroFE/Source/Graphics/Animate/TweenSet.cpp @@ -51,6 +51,11 @@ void TweenSet::push(std::unique_ptr tween) { } } + +void TweenSet::pushCompiled(CompiledTweenEntry tween) { + compiledSet_.push_back(std::move(tween)); +} + void TweenSet::clear() { compiledSet_.clear(); } diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.h b/RetroFE/Source/Graphics/Animate/TweenSet.h index 0db5fcc4e..63f840876 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.h +++ b/RetroFE/Source/Graphics/Animate/TweenSet.h @@ -38,6 +38,7 @@ class TweenSet TweenSet& operator=(const TweenSet& other); ~TweenSet(); void push(std::unique_ptr tween); + void pushCompiled(CompiledTweenEntry tween); void clear(); const std::vector& compiledTweens() const; diff --git a/RetroFE/Source/Graphics/Component/ScrollingList.cpp b/RetroFE/Source/Graphics/Component/ScrollingList.cpp index 0c6b58cea..975886e72 100644 --- a/RetroFE/Source/Graphics/Component/ScrollingList.cpp +++ b/RetroFE/Source/Graphics/Component/ScrollingList.cpp @@ -903,63 +903,72 @@ void ScrollingList::resetTweens(Component* c, std::shared_ptr s // Define a small epsilon for floating-point comparisons const float EPSILON_FLOAT = 0.0001f; - // NOTE: Using static_cast on all numeric start/end values to explicitly - // convert them to the type expected by the Tween constructor. This resolves all C4244 warnings. + auto pushLinearTween = [set = set.get()](TweenProperty property, float start, float end, float duration) { + set->pushCompiled(TweenSet::CompiledTweenEntry{ + property, + LINEAR, + duration, + true, + start, + end, + {} + }); + }; // Apply conditional push for each Tween property if (currentViewInfo->Restart != nextViewInfo->Restart && scrollPeriod_ > minScrollTime_) { - set->push(std::make_unique(TWEEN_PROPERTY_RESTART, LINEAR, static_cast(currentViewInfo->Restart), static_cast(nextViewInfo->Restart), 0.0f)); + pushLinearTween(TWEEN_PROPERTY_RESTART, static_cast(currentViewInfo->Restart), static_cast(nextViewInfo->Restart), 0.0f); } if (std::abs(currentViewInfo->Height - nextViewInfo->Height) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_HEIGHT, LINEAR, static_cast(currentViewInfo->Height), static_cast(nextViewInfo->Height), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_HEIGHT, static_cast(currentViewInfo->Height), static_cast(nextViewInfo->Height), scrollTime); } if (std::abs(currentViewInfo->Width - nextViewInfo->Width) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_WIDTH, LINEAR, static_cast(currentViewInfo->Width), static_cast(nextViewInfo->Width), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_WIDTH, static_cast(currentViewInfo->Width), static_cast(nextViewInfo->Width), scrollTime); } if (std::abs(currentViewInfo->Angle - nextViewInfo->Angle) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_ANGLE, LINEAR, static_cast(currentViewInfo->Angle), static_cast(nextViewInfo->Angle), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_ANGLE, static_cast(currentViewInfo->Angle), static_cast(nextViewInfo->Angle), scrollTime); } if (std::abs(currentViewInfo->Alpha - nextViewInfo->Alpha) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_ALPHA, LINEAR, static_cast(currentViewInfo->Alpha), static_cast(nextViewInfo->Alpha), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_ALPHA, static_cast(currentViewInfo->Alpha), static_cast(nextViewInfo->Alpha), scrollTime); } if (std::abs(currentViewInfo->X - nextViewInfo->X) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_X, LINEAR, static_cast(currentViewInfo->X), static_cast(nextViewInfo->X), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_X, static_cast(currentViewInfo->X), static_cast(nextViewInfo->X), scrollTime); } if (std::abs(currentViewInfo->Y - nextViewInfo->Y) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_Y, LINEAR, static_cast(currentViewInfo->Y), static_cast(nextViewInfo->Y), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_Y, static_cast(currentViewInfo->Y), static_cast(nextViewInfo->Y), scrollTime); } if (std::abs(currentViewInfo->XOrigin - nextViewInfo->XOrigin) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_X_ORIGIN, LINEAR, static_cast(currentViewInfo->XOrigin), static_cast(nextViewInfo->XOrigin), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_X_ORIGIN, static_cast(currentViewInfo->XOrigin), static_cast(nextViewInfo->XOrigin), scrollTime); } if (std::abs(currentViewInfo->YOrigin - nextViewInfo->YOrigin) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_Y_ORIGIN, LINEAR, static_cast(currentViewInfo->YOrigin), static_cast(nextViewInfo->YOrigin), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_Y_ORIGIN, static_cast(currentViewInfo->YOrigin), static_cast(nextViewInfo->YOrigin), scrollTime); } if (std::abs(currentViewInfo->XOffset - nextViewInfo->XOffset) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_X_OFFSET, LINEAR, static_cast(currentViewInfo->XOffset), static_cast(nextViewInfo->XOffset), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_X_OFFSET, static_cast(currentViewInfo->XOffset), static_cast(nextViewInfo->XOffset), scrollTime); } if (std::abs(currentViewInfo->YOffset - nextViewInfo->YOffset) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_Y_OFFSET, LINEAR, static_cast(currentViewInfo->YOffset), static_cast(nextViewInfo->YOffset), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_Y_OFFSET, static_cast(currentViewInfo->YOffset), static_cast(nextViewInfo->YOffset), scrollTime); } if (std::abs(currentViewInfo->FontSize - nextViewInfo->FontSize) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_FONT_SIZE, LINEAR, static_cast(currentViewInfo->FontSize), static_cast(nextViewInfo->FontSize), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_FONT_SIZE, static_cast(currentViewInfo->FontSize), static_cast(nextViewInfo->FontSize), scrollTime); } if (std::abs(currentViewInfo->BackgroundAlpha - nextViewInfo->BackgroundAlpha) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_BACKGROUND_ALPHA, LINEAR, static_cast(currentViewInfo->BackgroundAlpha), static_cast(nextViewInfo->BackgroundAlpha), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_BACKGROUND_ALPHA, static_cast(currentViewInfo->BackgroundAlpha), static_cast(nextViewInfo->BackgroundAlpha), scrollTime); } if (std::abs(currentViewInfo->MaxWidth - nextViewInfo->MaxWidth) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_MAX_WIDTH, LINEAR, static_cast(currentViewInfo->MaxWidth), static_cast(nextViewInfo->MaxWidth), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_MAX_WIDTH, static_cast(currentViewInfo->MaxWidth), static_cast(nextViewInfo->MaxWidth), scrollTime); } if (std::abs(currentViewInfo->MaxHeight - nextViewInfo->MaxHeight) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_MAX_HEIGHT, LINEAR, static_cast(currentViewInfo->MaxHeight), static_cast(nextViewInfo->MaxHeight), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_MAX_HEIGHT, static_cast(currentViewInfo->MaxHeight), static_cast(nextViewInfo->MaxHeight), scrollTime); } if (currentViewInfo->Layer != nextViewInfo->Layer) { - set->push(std::make_unique(TWEEN_PROPERTY_LAYER, LINEAR, static_cast(currentViewInfo->Layer), static_cast(nextViewInfo->Layer), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_LAYER, static_cast(currentViewInfo->Layer), static_cast(nextViewInfo->Layer), scrollTime); } if (std::abs(currentViewInfo->Volume - nextViewInfo->Volume) > EPSILON_FLOAT) { - set->push(std::make_unique(TWEEN_PROPERTY_VOLUME, LINEAR, static_cast(currentViewInfo->Volume), static_cast(nextViewInfo->Volume), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_VOLUME, static_cast(currentViewInfo->Volume), static_cast(nextViewInfo->Volume), scrollTime); } if (currentViewInfo->Monitor != nextViewInfo->Monitor) { - set->push(std::make_unique(TWEEN_PROPERTY_MONITOR, LINEAR, static_cast(currentViewInfo->Monitor), static_cast(nextViewInfo->Monitor), scrollTime)); + pushLinearTween(TWEEN_PROPERTY_MONITOR, static_cast(currentViewInfo->Monitor), static_cast(nextViewInfo->Monitor), scrollTime); } // Only push the set if there are any tweens in it From bb9ef8f061e4075b0ba8adfdcbf291c18ee1c86c Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:33:51 -0500 Subject: [PATCH 07/12] Stage 10: prebucket compiled tweens and precompute duration math --- RetroFE/Source/Graphics/Animate/TweenSet.cpp | 27 +++++- RetroFE/Source/Graphics/Animate/TweenSet.h | 7 ++ .../Source/Graphics/Component/Component.cpp | 91 ++++++++----------- RetroFE/Source/Graphics/Component/Component.h | 1 - .../Graphics/Component/ScrollingList.cpp | 2 + 5 files changed, 70 insertions(+), 58 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.cpp b/RetroFE/Source/Graphics/Animate/TweenSet.cpp index d6fa687d9..2f54e36d5 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.cpp +++ b/RetroFE/Source/Graphics/Animate/TweenSet.cpp @@ -18,24 +18,30 @@ TweenSet::TweenSet() = default; TweenSet::CompiledTweenEntry TweenSet::compileTween(const Tween& tween) { + const float duration = tween.duration; + const float deltaValue = tween.endValue() - tween.startValue(); return CompiledTweenEntry { tween.property, tween.algorithm(), - tween.duration, + duration, + duration > 0.0f ? (1.0f / duration) : 0.0f, tween.startDefined, tween.startValue(), tween.endValue(), + deltaValue, tween.playlistTokenIds() }; } TweenSet::TweenSet(const TweenSet& copy) - : compiledSet_(copy.compiledSet_) { + : compiledSet_(copy.compiledSet_), + compiledByAlgorithm_(copy.compiledByAlgorithm_) { } TweenSet& TweenSet::operator=(const TweenSet& other) { if (this != &other) { compiledSet_ = other.compiledSet_; + compiledByAlgorithm_ = other.compiledByAlgorithm_; } return *this; } @@ -47,23 +53,38 @@ TweenSet::~TweenSet() void TweenSet::push(std::unique_ptr tween) { if (tween) { - compiledSet_.push_back(compileTween(*tween)); + pushCompiled(compileTween(*tween)); } } void TweenSet::pushCompiled(CompiledTweenEntry tween) { + tween.deltaValue = tween.endValue - tween.startValue; + tween.invDuration = tween.duration > 0.0f ? (1.0f / tween.duration) : 0.0f; + + const size_t algorithmIndex = static_cast(tween.algorithm); + if (algorithmIndex < kTweenAlgorithmCount) { + compiledByAlgorithm_[algorithmIndex].push_back(compiledSet_.size()); + } + compiledSet_.push_back(std::move(tween)); } void TweenSet::clear() { compiledSet_.clear(); + for (auto& bucket : compiledByAlgorithm_) { + bucket.clear(); + } } const std::vector& TweenSet::compiledTweens() const { return compiledSet_; } +const std::array, TweenSet::kTweenAlgorithmCount>& TweenSet::compiledTweensByAlgorithm() const { + return compiledByAlgorithm_; +} + size_t TweenSet::size() const { return compiledSet_.size(); diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.h b/RetroFE/Source/Graphics/Animate/TweenSet.h index 63f840876..ace831f13 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.h +++ b/RetroFE/Source/Graphics/Animate/TweenSet.h @@ -19,17 +19,22 @@ #include #include #include +#include class TweenSet { public: + static constexpr size_t kTweenAlgorithmCount = static_cast(EASE_INOUT_CIRCULAR) + 1; + struct CompiledTweenEntry { TweenProperty property; TweenAlgorithm algorithm; float duration; + float invDuration; bool startDefined; float startValue; float endValue; + float deltaValue; std::vector playlistTokenIds; }; @@ -41,6 +46,7 @@ class TweenSet void pushCompiled(CompiledTweenEntry tween); void clear(); const std::vector& compiledTweens() const; + const std::array, kTweenAlgorithmCount>& compiledTweensByAlgorithm() const; size_t size() const; @@ -48,4 +54,5 @@ class TweenSet static CompiledTweenEntry compileTween(const Tween& tween); std::vector compiledSet_; + std::array, kTweenAlgorithmCount> compiledByAlgorithm_; }; diff --git a/RetroFE/Source/Graphics/Component/Component.cpp b/RetroFE/Source/Graphics/Component/Component.cpp index 47fde9b32..36828e238 100644 --- a/RetroFE/Source/Graphics/Component/Component.cpp +++ b/RetroFE/Source/Graphics/Component/Component.cpp @@ -60,9 +60,6 @@ void Component::freeGraphicsMemory() { elapsedTweenTime_ = 0; tweenEvaluations_.clear(); - for (auto& bucket : tweenAlgorithmBuckets_) { - bucket.clear(); - } tweenProgressBatch_.clear(); tweenStartBatch_.clear(); tweenChangeBatch_.clear(); @@ -253,14 +250,12 @@ bool Component::animate() { if (!tweens) return true; // Additional check for safety const auto& compiledTweens = tweens->compiledTweens(); + const auto& compiledTweensByAlgorithm = tweens->compiledTweensByAlgorithm(); const bool hasCurrentPlaylist = !playlistName.empty(); const uint32_t currentPlaylistId = hasCurrentPlaylist ? Tween::playlistTokenId(playlistName) : 0; tweenEvaluations_.clear(); tweenEvaluations_.reserve(compiledTweens.size()); - for (auto& bucket : tweenAlgorithmBuckets_) { - bucket.clear(); - } auto getStoreValueForProperty = [this](TweenProperty property) -> float { switch (property) { @@ -291,45 +286,9 @@ bool Component::animate() { } }; - for (const auto& compiledTween : compiledTweens) { - if (!Tween::matchesPlaylistTokenIds(compiledTween.playlistTokenIds, currentPlaylistId, hasCurrentPlaylist)) { - continue; - } - - double elapsedTime = elapsedTweenTime_; - if (elapsedTime < compiledTween.duration) { - currentDone = false; - } - else { - elapsedTime = compiledTween.duration; - } - - const auto algorithmIndex = static_cast(compiledTween.algorithm); - if (algorithmIndex >= kTweenAlgorithmCount) { - continue; - } - - const float resolvedStartValue = compiledTween.startDefined - ? compiledTween.startValue - : getStoreValueForProperty(compiledTween.property); - - TweenEvaluation evaluation { - compiledTween.property, - elapsedTime, - compiledTween.duration, - resolvedStartValue, - compiledTween.endValue, - 0.0f - }; - - const size_t evaluationIndex = tweenEvaluations_.size(); - tweenEvaluations_.push_back(evaluation); - tweenAlgorithmBuckets_[algorithmIndex].push_back(evaluationIndex); - } - for (size_t algorithmIndex = 0; algorithmIndex < kTweenAlgorithmCount; ++algorithmIndex) { const auto algorithm = static_cast(algorithmIndex); - const auto& bucket = tweenAlgorithmBuckets_[algorithmIndex]; + const auto& bucket = compiledTweensByAlgorithm[algorithmIndex]; if (bucket.empty()) { continue; } @@ -345,20 +304,44 @@ bool Component::animate() { tweenValueBatch_.reserve(bucket.size()); tweenOutputIndices_.reserve(bucket.size()); - for (const size_t evaluationIndex : bucket) { - auto& evaluation = tweenEvaluations_[evaluationIndex]; - const float endValue = evaluation.endValue; - const float duration = evaluation.duration; - - if (duration <= 0.0f) { - evaluation.value = endValue; + for (const size_t compiledIndex : bucket) { + const auto& compiledTween = compiledTweens[compiledIndex]; + if (!Tween::matchesPlaylistTokenIds(compiledTween.playlistTokenIds, currentPlaylistId, hasCurrentPlaylist)) { continue; } - tweenProgressBatch_.push_back(static_cast(evaluation.elapsedTime) / duration); - tweenStartBatch_.push_back(evaluation.startValue); - tweenChangeBatch_.push_back(endValue - evaluation.startValue); - tweenOutputIndices_.push_back(evaluationIndex); + double elapsedTime = elapsedTweenTime_; + if (elapsedTime < compiledTween.duration) { + currentDone = false; + } + else { + elapsedTime = compiledTween.duration; + } + + const float resolvedStartValue = compiledTween.startDefined + ? compiledTween.startValue + : getStoreValueForProperty(compiledTween.property); + + TweenEvaluation evaluation { + compiledTween.property, + elapsedTime, + compiledTween.duration, + resolvedStartValue, + compiledTween.endValue, + 0.0f + }; + + if (compiledTween.duration <= 0.0f) { + evaluation.value = compiledTween.endValue; + } + else { + tweenProgressBatch_.push_back(static_cast(elapsedTime) * compiledTween.invDuration); + tweenStartBatch_.push_back(resolvedStartValue); + tweenChangeBatch_.push_back(compiledTween.startDefined ? compiledTween.deltaValue : (compiledTween.endValue - resolvedStartValue)); + tweenOutputIndices_.push_back(tweenEvaluations_.size()); + } + + tweenEvaluations_.push_back(evaluation); } if (tweenOutputIndices_.empty()) { diff --git a/RetroFE/Source/Graphics/Component/Component.h b/RetroFE/Source/Graphics/Component/Component.h index 86271b35f..91a4a0b3c 100644 --- a/RetroFE/Source/Graphics/Component/Component.h +++ b/RetroFE/Source/Graphics/Component/Component.h @@ -113,7 +113,6 @@ class Component int id_; std::vector tweenEvaluations_; - std::array, kTweenAlgorithmCount> tweenAlgorithmBuckets_; std::vector tweenProgressBatch_; std::vector tweenStartBatch_; std::vector tweenChangeBatch_; diff --git a/RetroFE/Source/Graphics/Component/ScrollingList.cpp b/RetroFE/Source/Graphics/Component/ScrollingList.cpp index 975886e72..b5f7dd108 100644 --- a/RetroFE/Source/Graphics/Component/ScrollingList.cpp +++ b/RetroFE/Source/Graphics/Component/ScrollingList.cpp @@ -908,9 +908,11 @@ void ScrollingList::resetTweens(Component* c, std::shared_ptr s property, LINEAR, duration, + 0.0f, true, start, end, + 0.0f, {} }); }; From 46b9ec9405ddf163947b93a5a69e214037654790 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:41:21 -0500 Subject: [PATCH 08/12] Stage 11: add SIMD kernels for core easing batches --- RetroFE/Source/Graphics/Animate/Tween.cpp | 116 ++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index b5b35e0e1..9a74adaaf 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -23,6 +23,10 @@ #include #include +#if defined(__SSE2__) +#include +#endif + namespace { std::string trimPlaylistToken(const std::string& token) { const size_t first = token.find_first_not_of(" \t\n\r"); @@ -33,6 +37,98 @@ std::string trimPlaylistToken(const std::string& token) { const size_t last = token.find_last_not_of(" \t\n\r"); return token.substr(first, last - first + 1); } + +#if defined(__SSE2__) +inline void evaluateLinearSse(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const __m128 p = _mm_loadu_ps(progress + i); + const __m128 b = _mm_loadu_ps(start + i); + const __m128 c = _mm_loadu_ps(change + i); + const __m128 result = _mm_add_ps(_mm_mul_ps(c, p), b); + _mm_storeu_ps(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] + start[i]; + } +} + +inline void evaluateEaseInQuadraticSse(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const __m128 p = _mm_loadu_ps(progress + i); + const __m128 b = _mm_loadu_ps(start + i); + const __m128 c = _mm_loadu_ps(change + i); + const __m128 p2 = _mm_mul_ps(p, p); + const __m128 result = _mm_add_ps(_mm_mul_ps(c, p2), b); + _mm_storeu_ps(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] * progress[i] + start[i]; + } +} + +inline void evaluateEaseOutQuadraticSse(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + const __m128 two = _mm_set1_ps(2.0f); + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const __m128 p = _mm_loadu_ps(progress + i); + const __m128 b = _mm_loadu_ps(start + i); + const __m128 c = _mm_loadu_ps(change + i); + const __m128 pTerm = _mm_sub_ps(two, p); // 2 - p + const __m128 result = _mm_add_ps(_mm_mul_ps(c, _mm_mul_ps(p, pTerm)), b); + _mm_storeu_ps(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] * (2.0f - progress[i]) + start[i]; + } +} + +inline void evaluateEaseInCubicSse(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const __m128 p = _mm_loadu_ps(progress + i); + const __m128 b = _mm_loadu_ps(start + i); + const __m128 c = _mm_loadu_ps(change + i); + const __m128 p2 = _mm_mul_ps(p, p); + const __m128 p3 = _mm_mul_ps(p2, p); + const __m128 result = _mm_add_ps(_mm_mul_ps(c, p3), b); + _mm_storeu_ps(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] * progress[i] * progress[i] + start[i]; + } +} + +inline void evaluateEaseOutCubicSse(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + const __m128 one = _mm_set1_ps(1.0f); + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const __m128 p = _mm_loadu_ps(progress + i); + const __m128 b = _mm_loadu_ps(start + i); + const __m128 c = _mm_loadu_ps(change + i); + const __m128 q = _mm_sub_ps(p, one); + const __m128 q2 = _mm_mul_ps(q, q); + const __m128 q3 = _mm_mul_ps(q2, q); + const __m128 result = _mm_add_ps(_mm_mul_ps(c, _mm_add_ps(q3, one)), b); + _mm_storeu_ps(out + i, result); + } + + for (; i < count; ++i) { + const float q = progress[i] - 1.0f; + out[i] = change[i] * (q * q * q + 1.0f) + start[i]; + } +} +#endif } std::unordered_map Tween::tweenTypeMap_ = { @@ -202,19 +298,35 @@ Tween::EasingKernel Tween::getKernel(TweenAlgorithm type) { void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const float* start, const float* change, float* out, size_t count) { switch (type) { case EASE_IN_QUADRATIC: +#if defined(__SSE2__) + evaluateEaseInQuadraticSse(progress, start, change, out, count); +#else for (size_t i = 0; i < count; ++i) out[i] = easeInQuadratic(progress[i], start[i], change[i]); +#endif break; case EASE_OUT_QUADRATIC: +#if defined(__SSE2__) + evaluateEaseOutQuadraticSse(progress, start, change, out, count); +#else for (size_t i = 0; i < count; ++i) out[i] = easeOutQuadratic(progress[i], start[i], change[i]); +#endif break; case EASE_INOUT_QUADRATIC: for (size_t i = 0; i < count; ++i) out[i] = easeInOutQuadratic(progress[i], start[i], change[i]); break; case EASE_IN_CUBIC: +#if defined(__SSE2__) + evaluateEaseInCubicSse(progress, start, change, out, count); +#else for (size_t i = 0; i < count; ++i) out[i] = easeInCubic(progress[i], start[i], change[i]); +#endif break; case EASE_OUT_CUBIC: +#if defined(__SSE2__) + evaluateEaseOutCubicSse(progress, start, change, out, count); +#else for (size_t i = 0; i < count; ++i) out[i] = easeOutCubic(progress[i], start[i], change[i]); +#endif break; case EASE_INOUT_CUBIC: for (size_t i = 0; i < count; ++i) out[i] = easeInOutCubic(progress[i], start[i], change[i]); @@ -266,7 +378,11 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa break; case LINEAR: default: +#if defined(__SSE2__) + evaluateLinearSse(progress, start, change, out, count); +#else for (size_t i = 0; i < count; ++i) out[i] = linear(progress[i], start[i], change[i]); +#endif break; } } From 3c2349484e356803792cd58e697abfbd18087310 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:51:55 -0500 Subject: [PATCH 09/12] Fix cross-compiler SSE2 detection for tween SIMD path --- RetroFE/Source/Graphics/Animate/Tween.cpp | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index 9a74adaaf..d1b1a6e5c 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -23,7 +23,21 @@ #include #include +#if defined(_MSC_VER) +#if defined(_M_X64) || defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) +#define HAVE_SSE2 1 +#else +#define HAVE_SSE2 0 +#endif +#else #if defined(__SSE2__) +#define HAVE_SSE2 1 +#else +#define HAVE_SSE2 0 +#endif +#endif + +#if HAVE_SSE2 #include #endif @@ -38,7 +52,7 @@ std::string trimPlaylistToken(const std::string& token) { return token.substr(first, last - first + 1); } -#if defined(__SSE2__) +#if HAVE_SSE2 inline void evaluateLinearSse(const float* progress, const float* start, const float* change, float* out, size_t count) { constexpr size_t kWidth = 4; size_t i = 0; @@ -298,14 +312,14 @@ Tween::EasingKernel Tween::getKernel(TweenAlgorithm type) { void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const float* start, const float* change, float* out, size_t count) { switch (type) { case EASE_IN_QUADRATIC: -#if defined(__SSE2__) +#if HAVE_SSE2 evaluateEaseInQuadraticSse(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeInQuadratic(progress[i], start[i], change[i]); #endif break; case EASE_OUT_QUADRATIC: -#if defined(__SSE2__) +#if HAVE_SSE2 evaluateEaseOutQuadraticSse(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeOutQuadratic(progress[i], start[i], change[i]); @@ -315,14 +329,14 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa for (size_t i = 0; i < count; ++i) out[i] = easeInOutQuadratic(progress[i], start[i], change[i]); break; case EASE_IN_CUBIC: -#if defined(__SSE2__) +#if HAVE_SSE2 evaluateEaseInCubicSse(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeInCubic(progress[i], start[i], change[i]); #endif break; case EASE_OUT_CUBIC: -#if defined(__SSE2__) +#if HAVE_SSE2 evaluateEaseOutCubicSse(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeOutCubic(progress[i], start[i], change[i]); @@ -378,7 +392,7 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa break; case LINEAR: default: -#if defined(__SSE2__) +#if HAVE_SSE2 evaluateLinearSse(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = linear(progress[i], start[i], change[i]); From ae59162b4750a63030a09e4e884f6a9eb19a362a Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:55:46 -0500 Subject: [PATCH 10/12] Add NEON batched easing kernels and feature detection --- RetroFE/Source/Graphics/Animate/Tween.cpp | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index d1b1a6e5c..ab1585261 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -37,10 +37,28 @@ #endif #endif +#if defined(_MSC_VER) +#if defined(_M_ARM64) || defined(_M_ARM64EC) +#define HAVE_NEON 1 +#else +#define HAVE_NEON 0 +#endif +#else +#if defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(__aarch64__) +#define HAVE_NEON 1 +#else +#define HAVE_NEON 0 +#endif +#endif + #if HAVE_SSE2 #include #endif +#if HAVE_NEON +#include +#endif + namespace { std::string trimPlaylistToken(const std::string& token) { const size_t first = token.find_first_not_of(" \t\n\r"); @@ -143,6 +161,98 @@ inline void evaluateEaseOutCubicSse(const float* progress, const float* start, c } } #endif + +#if HAVE_NEON +inline void evaluateLinearNeon(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const float32x4_t p = vld1q_f32(progress + i); + const float32x4_t b = vld1q_f32(start + i); + const float32x4_t c = vld1q_f32(change + i); + const float32x4_t result = vmlaq_f32(b, c, p); + vst1q_f32(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] + start[i]; + } +} + +inline void evaluateEaseInQuadraticNeon(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const float32x4_t p = vld1q_f32(progress + i); + const float32x4_t b = vld1q_f32(start + i); + const float32x4_t c = vld1q_f32(change + i); + const float32x4_t p2 = vmulq_f32(p, p); + const float32x4_t result = vmlaq_f32(b, c, p2); + vst1q_f32(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] * progress[i] + start[i]; + } +} + +inline void evaluateEaseOutQuadraticNeon(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + const float32x4_t two = vdupq_n_f32(2.0f); + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const float32x4_t p = vld1q_f32(progress + i); + const float32x4_t b = vld1q_f32(start + i); + const float32x4_t c = vld1q_f32(change + i); + const float32x4_t pTerm = vsubq_f32(two, p); + const float32x4_t result = vmlaq_f32(b, c, vmulq_f32(p, pTerm)); + vst1q_f32(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] * (2.0f - progress[i]) + start[i]; + } +} + +inline void evaluateEaseInCubicNeon(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const float32x4_t p = vld1q_f32(progress + i); + const float32x4_t b = vld1q_f32(start + i); + const float32x4_t c = vld1q_f32(change + i); + const float32x4_t p2 = vmulq_f32(p, p); + const float32x4_t p3 = vmulq_f32(p2, p); + const float32x4_t result = vmlaq_f32(b, c, p3); + vst1q_f32(out + i, result); + } + + for (; i < count; ++i) { + out[i] = change[i] * progress[i] * progress[i] * progress[i] + start[i]; + } +} + +inline void evaluateEaseOutCubicNeon(const float* progress, const float* start, const float* change, float* out, size_t count) { + constexpr size_t kWidth = 4; + const float32x4_t one = vdupq_n_f32(1.0f); + size_t i = 0; + for (; i + kWidth <= count; i += kWidth) { + const float32x4_t p = vld1q_f32(progress + i); + const float32x4_t b = vld1q_f32(start + i); + const float32x4_t c = vld1q_f32(change + i); + const float32x4_t q = vsubq_f32(p, one); + const float32x4_t q2 = vmulq_f32(q, q); + const float32x4_t q3 = vmulq_f32(q2, q); + const float32x4_t result = vmlaq_f32(b, c, vaddq_f32(q3, one)); + vst1q_f32(out + i, result); + } + + for (; i < count; ++i) { + const float q = progress[i] - 1.0f; + out[i] = change[i] * (q * q * q + 1.0f) + start[i]; + } +} +#endif } std::unordered_map Tween::tweenTypeMap_ = { @@ -314,6 +424,8 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa case EASE_IN_QUADRATIC: #if HAVE_SSE2 evaluateEaseInQuadraticSse(progress, start, change, out, count); +#elif HAVE_NEON + evaluateEaseInQuadraticNeon(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeInQuadratic(progress[i], start[i], change[i]); #endif @@ -321,6 +433,8 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa case EASE_OUT_QUADRATIC: #if HAVE_SSE2 evaluateEaseOutQuadraticSse(progress, start, change, out, count); +#elif HAVE_NEON + evaluateEaseOutQuadraticNeon(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeOutQuadratic(progress[i], start[i], change[i]); #endif @@ -331,6 +445,8 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa case EASE_IN_CUBIC: #if HAVE_SSE2 evaluateEaseInCubicSse(progress, start, change, out, count); +#elif HAVE_NEON + evaluateEaseInCubicNeon(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeInCubic(progress[i], start[i], change[i]); #endif @@ -338,6 +454,8 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa case EASE_OUT_CUBIC: #if HAVE_SSE2 evaluateEaseOutCubicSse(progress, start, change, out, count); +#elif HAVE_NEON + evaluateEaseOutCubicNeon(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = easeOutCubic(progress[i], start[i], change[i]); #endif @@ -394,6 +512,8 @@ void Tween::evaluateBatch(TweenAlgorithm type, const float* progress, const floa default: #if HAVE_SSE2 evaluateLinearSse(progress, start, change, out, count); +#elif HAVE_NEON + evaluateLinearNeon(progress, start, change, out, count); #else for (size_t i = 0; i < count; ++i) out[i] = linear(progress[i], start[i], change[i]); #endif From 7c52dd35813184b808e77cb882ac5f017c7313ea Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:09:21 -0500 Subject: [PATCH 11/12] Make SSE2 and NEON feature gates mutually exclusive --- RetroFE/Source/Graphics/Animate/Tween.cpp | 24 ++++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index ab1585261..961455f85 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -24,29 +24,25 @@ #include #if defined(_MSC_VER) -#if defined(_M_X64) || defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) -#define HAVE_SSE2 1 -#else +#if defined(_M_ARM64) || defined(_M_ARM64EC) +#define HAVE_NEON 1 #define HAVE_SSE2 0 -#endif -#else -#if defined(__SSE2__) +#elif defined(_M_X64) || defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) #define HAVE_SSE2 1 +#define HAVE_NEON 0 #else #define HAVE_SSE2 0 -#endif -#endif - -#if defined(_MSC_VER) -#if defined(_M_ARM64) || defined(_M_ARM64EC) -#define HAVE_NEON 1 -#else #define HAVE_NEON 0 #endif #else -#if defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(__aarch64__) +#if defined(__aarch64__) || defined(__ARM_NEON) || defined(__ARM_NEON__) #define HAVE_NEON 1 +#define HAVE_SSE2 0 +#elif defined(__SSE2__) +#define HAVE_SSE2 1 +#define HAVE_NEON 0 #else +#define HAVE_SSE2 0 #define HAVE_NEON 0 #endif #endif From bbb011de59616577cf567f8be514eb07dfd6287a Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:28:10 -0500 Subject: [PATCH 12/12] Cache Page idle state from animation update pass --- RetroFE/Source/Graphics/Page.cpp | 88 ++++++++++++++++++++++++-------- RetroFE/Source/Graphics/Page.h | 8 +++ 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/RetroFE/Source/Graphics/Page.cpp b/RetroFE/Source/Graphics/Page.cpp index dcf2a7259..13ae1fdcb 100644 --- a/RetroFE/Source/Graphics/Page.cpp +++ b/RetroFE/Source/Graphics/Page.cpp @@ -295,38 +295,77 @@ void Page::setStatusTextComponent(Text* t) { bool Page::addComponent(Component* c) { if (c->baseViewInfo.Layer < NUM_LAYERS) { - // No need to resizeguaranteed by constructor - LayerComponents_[c->baseViewInfo.Layer].push_back(c); - return true; + if (!idleCacheValid_) { + refreshIdleStates(); + return cachedMenuIdle_; + + + if (!idleCacheValid_) { + refreshIdleStates(); + return cachedIdle_; + if (!idleCacheValid_) { + refreshIdleStates(); } - else { - std::stringstream ss; - ss << "Component layer too large. Layer: " << c->baseViewInfo.Layer; - LOG_ERROR("Page", ss.str()); - return false; + return cachedAttractIdle_; +} + + +bool Page::isGraphicsIdle() { + if (!idleCacheValid_) { + refreshIdleStates(); } + return cachedGraphicsIdle_; } -bool Page::isMenuIdle() { - if (playlistMenu_ && !playlistMenu_->isScrollingListIdle()) - return false; +void Page::refreshIdleStates() { + bool menuIdle = true; + bool menuAttractIdle = true; - for (auto it = menus_.begin(); it != menus_.end(); ++it) { - for (auto it2 = it->begin(); it2 != it->end(); ++it2) { - ScrollingList* menu = *it2; - if (menu && !menu->isScrollingListIdle()) { - return false; - } + if (playlistMenu_) { + if (!playlistMenu_->isScrollingListIdle()) { + menuIdle = false; + } + if (!playlistMenu_->isAttractIdle()) { + menuAttractIdle = false; } } - return true; -} + if (!menu) { + continue; + } -bool Page::isIdle() { - if (!isMenuIdle()) return false; - for (int i = NUM_LAYERS - 1; i >= 0; --i) { + if (menuIdle && !menu->isScrollingListIdle()) { + menuIdle = false; + } + if (menuAttractIdle && !menu->isAttractIdle()) { + menuAttractIdle = false; + } + + if (!menuIdle && !menuAttractIdle) { + break; + } + + bool graphicsIdle = true; + bool graphicsAttractIdle = true; + if (!component) { + continue; + } + if (graphicsIdle && !component->isIdle()) { + graphicsIdle = false; + } + if (graphicsAttractIdle && !component->isAttractIdle()) { + graphicsAttractIdle = false; + } + if (!graphicsIdle && !graphicsAttractIdle) { + break; + } + + cachedMenuIdle_ = menuIdle; + cachedGraphicsIdle_ = graphicsIdle; + cachedIdle_ = menuIdle && graphicsIdle; + cachedAttractIdle_ = menuAttractIdle && graphicsAttractIdle; + idleCacheValid_ = true; const auto& layer = LayerComponents_[i]; for (const Component* component : layer) { if (!component->isIdle()) return false; @@ -1372,6 +1411,7 @@ bool Page::playlistExists(const std::string& playlist) { void Page::update(float dt) { + idleCacheValid_ = false; std::string playlistName = getPlaylistName(); // Check if the playlist name has changed since the last update @@ -1420,9 +1460,12 @@ void Page::update(float dt) { config_.setProperty("status", status); textStatusComponent_->setText(status); } + + refreshIdleStates(); } void Page::updateReloadables(float dt) { + idleCacheValid_ = false; for (auto& layer : LayerComponents_) { for (Component* component : layer) { if (component) { @@ -1430,6 +1473,7 @@ void Page::updateReloadables(float dt) { } } } + refreshIdleStates(); } void Page::cleanup() { diff --git a/RetroFE/Source/Graphics/Page.h b/RetroFE/Source/Graphics/Page.h index a2de6053f..8a93c8c7a 100644 --- a/RetroFE/Source/Graphics/Page.h +++ b/RetroFE/Source/Graphics/Page.h @@ -245,4 +245,12 @@ class Page bool jukebox_; bool isLaunched_ = false; + bool idleCacheValid_ = false; + bool cachedMenuIdle_ = true; + bool cachedGraphicsIdle_ = true; + bool cachedIdle_ = true; + bool cachedAttractIdle_ = true; + + void refreshIdleStates(); + };