diff --git a/RetroFE/Source/Graphics/Animate/Tween.cpp b/RetroFE/Source/Graphics/Animate/Tween.cpp index 2e752286a..961455f85 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.cpp +++ b/RetroFE/Source/Graphics/Animate/Tween.cpp @@ -20,6 +20,236 @@ #include #include #include +#include +#include + +#if defined(_MSC_VER) +#if defined(_M_ARM64) || defined(_M_ARM64EC) +#define HAVE_NEON 1 +#define HAVE_SSE2 0 +#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 +#define HAVE_NEON 0 +#endif +#else +#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 + +#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"); + 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); +} + +#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; + 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 + +#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_ = { {"easeinquadratic", EASE_IN_QUADRATIC}, @@ -71,6 +301,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) @@ -78,6 +312,55 @@ 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(playlist); + playlistFilterTokenIds.push_back(playlistTokenId(playlist)); + } + } + } +} + +bool Tween::matchesPlaylistTokens(const std::vector& tokens, const std::string& currentPlaylist) { + if (tokens.empty() || currentPlaylist.empty()) { + return true; + } + + 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 { + if (playlistFilterTokenIds.empty() || currentPlaylist.empty()) { + return true; + } + + return matchesPlaylistTokenIds(playlistFilterTokenIds, playlistTokenId(currentPlaylist), true); } std::optional Tween::getTweenProperty(const std::string& name) { @@ -103,6 +386,137 @@ 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: +#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 + break; + 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 + 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 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 + break; + 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 + 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: +#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 + break; + } +} + float Tween::animate(double elapsedTime) const { return animateSingle(type, start, end, duration, static_cast(elapsedTime)); } @@ -263,4 +677,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..27be65036 100644 --- a/RetroFE/Source/Graphics/Animate/Tween.h +++ b/RetroFE/Source/Graphics/Animate/Tween.h @@ -19,11 +19,15 @@ #include #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 +40,24 @@ 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); + 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; } + 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; + 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. @@ -70,8 +87,10 @@ 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; float end; -}; \ No newline at end of file +}; diff --git a/RetroFE/Source/Graphics/Animate/TweenSet.cpp b/RetroFE/Source/Graphics/Animate/TweenSet.cpp index c77a96f8c..2f54e36d5 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.cpp +++ b/RetroFE/Source/Graphics/Animate/TweenSet.cpp @@ -17,47 +17,75 @@ TweenSet::TweenSet() = default; -TweenSet::TweenSet(const TweenSet& copy) { - set_.reserve(copy.set_.size()); - for (const auto& tween : copy.set_) { - set_.push_back(std::make_unique(*tween)); - } +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(), + 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_), + compiledByAlgorithm_(copy.compiledByAlgorithm_) { } TweenSet& TweenSet::operator=(const TweenSet& other) { if (this != &other) { - set_.clear(); // Clear existing tweens - set_.reserve(other.set_.size()); - for (const auto& tween : other.set_) { - set_.push_back(std::make_unique(*tween)); - } + compiledSet_ = other.compiledSet_; + compiledByAlgorithm_ = other.compiledByAlgorithm_; } return *this; } - TweenSet::~TweenSet() { clear(); } void TweenSet::push(std::unique_ptr tween) { - set_.push_back(std::move(tween)); + if (tween) { + pushCompiled(compileTween(*tween)); + } } -void TweenSet::clear() { - set_.clear(); + +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)); } -Tween* TweenSet::getTween(unsigned int index) const { - if (index < set_.size()) { - return set_[index].get(); +void TweenSet::clear() { + compiledSet_.clear(); + for (auto& bucket : compiledByAlgorithm_) { + bucket.clear(); } - return nullptr; } +const std::vector& TweenSet::compiledTweens() const { + return compiledSet_; +} + +const std::array, TweenSet::kTweenAlgorithmCount>& TweenSet::compiledTweensByAlgorithm() const { + return compiledByAlgorithm_; +} 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..ace831f13 100644 --- a/RetroFE/Source/Graphics/Animate/TweenSet.h +++ b/RetroFE/Source/Graphics/Animate/TweenSet.h @@ -18,20 +18,41 @@ #include "Tween.h" #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; + }; + TweenSet(); TweenSet(const TweenSet& copy); TweenSet& operator=(const TweenSet& other); ~TweenSet(); void push(std::unique_ptr tween); + void pushCompiled(CompiledTweenEntry tween); void clear(); - Tween* getTween(unsigned int index) const; + const std::vector& compiledTweens() const; + const std::array, kTweenAlgorithmCount>& compiledTweensByAlgorithm() const; size_t size() const; private: - std::vector> set_; + 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 173be9aaa..36828e238 100644 --- a/RetroFE/Source/Graphics/Component/Component.cpp +++ b/RetroFE/Source/Graphics/Component/Component.cpp @@ -19,6 +19,7 @@ #include "../../Utility/Log.h" #include "../../SDL.h" #include "../PageBuilder.h" +#include Component::Component(Page &p) : page(p) @@ -58,6 +59,13 @@ void Component::freeGraphicsMemory() { currentTweenComplete_ = true; elapsedTweenTime_ = 0; + tweenEvaluations_.clear(); + tweenProgressBatch_.clear(); + tweenStartBatch_.clear(); + tweenChangeBatch_.clear(); + tweenValueBatch_.clear(); + tweenOutputIndices_.clear(); + if (backgroundTexture_) { SDL_DestroyTexture(backgroundTexture_); backgroundTexture_ = nullptr; @@ -241,184 +249,211 @@ bool Component::animate() { auto tweens = sharedTweens->tweenSet(currentTweenIndex_); if (!tweens) return true; // Additional check for safety - std::string playlist; - bool foundFiltered; + 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()); + + 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 + for (size_t algorithmIndex = 0; algorithmIndex < kTweenAlgorithmCount; ++algorithmIndex) { + const auto algorithm = static_cast(algorithmIndex); + const auto& bucket = compiledTweensByAlgorithm[algorithmIndex]; + if (bucket.empty()) { + continue; + } + + 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 compiledIndex : bucket) { + const auto& compiledTween = compiledTweens[compiledIndex]; + if (!Tween::matchesPlaylistTokenIds(compiledTween.playlistTokenIds, currentPlaylistId, hasCurrentPlaylist)) { + continue; + } + + double elapsedTime = elapsedTweenTime_; + if (elapsedTime < compiledTween.duration) { + currentDone = false; + } + else { + elapsedTime = compiledTween.duration; + } - if (!tween->playlistFilter.empty() && !playlistName.empty()) { - foundFiltered = false; - std::stringstream ss(tween->playlistFilter); - while (getline(ss, playlist, ',')) { - if (playlistName == playlist) { - foundFiltered = true; - break; - } + 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()); } - if (!foundFiltered) continue; + + tweenEvaluations_.push_back(evaluation); + } + + if (tweenOutputIndices_.empty()) { + continue; } - double elapsedTime = elapsedTweenTime_; - if (elapsedTime < tween->duration) - currentDone = false; - else - elapsedTime = tween->duration; + tweenValueBatch_.resize(tweenOutputIndices_.size()); + Tween::evaluateBatch(algorithm, + tweenProgressBatch_.data(), + tweenStartBatch_.data(), + tweenChangeBatch_.data(), + tweenValueBatch_.data(), + tweenValueBatch_.size()); - switch (tween->property) { + for (size_t i = 0; i < tweenOutputIndices_.size(); ++i) { + tweenEvaluations_[tweenOutputIndices_[i]].value = tweenValueBatch_[i]; + } + } + + for (const auto& evaluation : tweenEvaluations_) { + 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.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 e9abc0704..91a4a0b3c 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,17 @@ class Component private: + struct TweenEvaluation { + TweenProperty property; + double elapsedTime; + float duration; + float startValue; + float endValue; + 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 +112,11 @@ class Component int menuIndex_; int id_; + std::vector tweenEvaluations_; + std::vector tweenProgressBatch_; + std::vector tweenStartBatch_; + std::vector tweenChangeBatch_; + std::vector tweenValueBatch_; + std::vector tweenOutputIndices_; + }; diff --git a/RetroFE/Source/Graphics/Component/ScrollingList.cpp b/RetroFE/Source/Graphics/Component/ScrollingList.cpp index 0c6b58cea..b5f7dd108 100644 --- a/RetroFE/Source/Graphics/Component/ScrollingList.cpp +++ b/RetroFE/Source/Graphics/Component/ScrollingList.cpp @@ -903,63 +903,74 @@ 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, + 0.0f, + true, + start, + end, + 0.0f, + {} + }); + }; // 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 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 resize—guaranteed 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(); + }; 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.