From cfff744cb1fbcf7de41e1ffa9f275de733076139 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 May 2019 00:57:59 +0300 Subject: [PATCH] Parse and render lottie in the background. --- Telegram/SourceFiles/base/timer.h | 3 +- .../history/media/history_media_sticker.cpp | 41 +- .../history/media/history_media_sticker.h | 6 +- .../SourceFiles/lottie/lottie_animation.cpp | 155 +++-- .../SourceFiles/lottie/lottie_animation.h | 59 +- Telegram/SourceFiles/lottie/lottie_common.h | 71 ++ .../lottie/lottie_frame_renderer.cpp | 643 ++++++++++++------ .../lottie/lottie_frame_renderer.h | 153 +++-- .../media/view/media_view_overlay_widget.cpp | 26 +- .../SourceFiles/ui/image/image_prepare.cpp | 19 +- Telegram/SourceFiles/ui/image/image_prepare.h | 1 + Telegram/ThirdParty/qtlottie | 2 +- .../QtBodymovin/private/bmscene_p.h | 6 +- Telegram/gyp/lib_lottie.gyp | 1 + 14 files changed, 772 insertions(+), 414 deletions(-) create mode 100644 Telegram/SourceFiles/lottie/lottie_common.h diff --git a/Telegram/SourceFiles/base/timer.h b/Telegram/SourceFiles/base/timer.h index fa11716e6..8ed2b3bb3 100644 --- a/Telegram/SourceFiles/base/timer.h +++ b/Telegram/SourceFiles/base/timer.h @@ -9,9 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#include "base/observer.h" #include "base/flat_map.h" +#include + namespace base { class Timer final : private QObject { diff --git a/Telegram/SourceFiles/history/media/history_media_sticker.cpp b/Telegram/SourceFiles/history/media/history_media_sticker.cpp index 3e132bcea..f5054d6a9 100644 --- a/Telegram/SourceFiles/history/media/history_media_sticker.cpp +++ b/Telegram/SourceFiles/history/media/history_media_sticker.cpp @@ -32,8 +32,7 @@ HistorySticker::HistorySticker( not_null document) : HistoryMedia(parent) , _data(document) -, _emoji(_data->sticker()->alt) -, _timer([=] { parent->data()->history()->owner().requestViewRepaint(parent); }) { +, _emoji(_data->sticker()->alt) { _data->loadThumbnail(parent->data()->fullId()); if (const auto emoji = Ui::Emoji::Find(_emoji)) { _emoji = emoji->text(); @@ -93,12 +92,25 @@ QSize HistorySticker::countCurrentSize(int newWidth) { return { newWidth, minHeight() }; } +void HistorySticker::setupLottie() { + _lottie = _data->data().isEmpty() + ? Lottie::FromFile(_data->filepath()) + : Lottie::FromData(_data->data()); + _lottie->updates( + ) | rpl::start_with_next_error([=](Lottie::Update update) { + update.data.match([&](const Lottie::Information &information) { + _parent->data()->history()->owner().requestViewResize(_parent); + }, [&](const Lottie::DisplayFrameRequest &request) { + _parent->data()->history()->owner().requestViewRepaint(_parent); + }); + }, [=](Lottie::Error error) { + }, _lifetime); +} + void HistorySticker::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms) const { if (!_lottie && _data->filename().endsWith(qstr(".json"))) { if (_data->loaded()) { - _lottie = _data->data().isEmpty() - ? Lottie::FromFile(_data->filepath()) - : Lottie::FromData(_data->data()); + const_cast(this)->setupLottie(); } else { _data->automaticLoad(_parent->data()->fullId(), _parent->data()); } @@ -129,16 +141,17 @@ void HistorySticker::draw(Painter &p, const QRect &r, TextSelection selection, c if (rtl()) usex = width() - usex - usew; if (_lottie) { - auto frame = _lottie->frame(crl::now()); - if (selected) { - frame = Images::prepareColored( - st::msgStickerOverlay, - std::move(frame)); + if (_lottie->ready()) { + auto request = Lottie::FrameRequest(); + request.resize = QSize(_pixw, _pixh) * cIntRetinaFactor(); + if (selected) { + request.colored = st::msgStickerOverlay->c; + } + _lottie->markFrameShown(); + p.drawImage( + QRect(usex + (usew - _pixw) / 2, (minHeight() - _pixh) / 2, _pixw, _pixh), + _lottie->frame(request)); } - p.drawImage( - QRect(usex + (usew - _pixw) / 2, (minHeight() - _pixh) / 2, _pixw, _pixh), - frame); - _timer.callOnce(crl::time(1000) / _lottie->frameRate()); } else { const auto &pixmap = [&]() -> const QPixmap & { const auto o = item->fullId(); diff --git a/Telegram/SourceFiles/history/media/history_media_sticker.h b/Telegram/SourceFiles/history/media/history_media_sticker.h index 1b6a92f44..def8df863 100644 --- a/Telegram/SourceFiles/history/media/history_media_sticker.h +++ b/Telegram/SourceFiles/history/media/history_media_sticker.h @@ -63,12 +63,14 @@ private: int additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply) const; int additionalWidth() const; + void setupLottie(); + int _pixw = 1; int _pixh = 1; ClickHandlerPtr _packLink; not_null _data; QString _emoji; - mutable base::Timer _timer; - mutable std::unique_ptr _lottie; + std::unique_ptr _lottie; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/lottie/lottie_animation.cpp b/Telegram/SourceFiles/lottie/lottie_animation.cpp index be39d3830..42c4d4c6a 100644 --- a/Telegram/SourceFiles/lottie/lottie_animation.cpp +++ b/Telegram/SourceFiles/lottie/lottie_animation.cpp @@ -10,23 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_frame_renderer.h" #include "base/algorithm.h" -#include -#include +#include +#include #include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include - -#include #include "rasterrenderer/lottierasterrenderer.h" @@ -58,71 +45,117 @@ std::unique_ptr FromData(const QByteArray &data) { return std::make_unique(data); } -Animation::Animation(const QByteArray &content) { - parse(content); +Animation::Animation(const QByteArray &content) +: _timer([=] { checkNextFrame(); }) { + const auto weak = base::make_weak(this); + crl::async([=] { + auto error = QJsonParseError(); + const auto document = QJsonDocument::fromJson(content, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(lcLottieQtBodymovinParser) + << "Lottie Error: Parse failed with code " + << error.error + << "( " << error.errorString() << ")"; + crl::on_main(weak, [=] { + parseFailed(); + }); + } else { + auto state = std::make_unique(document.object()); + crl::on_main(weak, [this, result = std::move(state)]() mutable { + parseDone(std::move(result)); + }); + } + }); } Animation::~Animation() { + if (_renderer) { + Assert(_state != nullptr); + _renderer->remove(_state); + } } -QImage Animation::frame(crl::time now) const { - if (_scene->startFrame() == _scene->endFrame() - || _scene->width() <= 0 - || _scene->height() <= 0) { - return QImage(); +void Animation::parseDone(std::unique_ptr state) { + Expects(state != nullptr); + + auto information = state->information(); + if (!information.frameRate + || information.framesCount <= 0 + || information.size.isEmpty()) { + _updates.fire_error(Error::NotSupported); + } else { + _state = state.get(); + _state->start(this, crl::now()); + _renderer = FrameRenderer::Instance(); + _renderer->append(std::move(state)); + _updates.fire({ std::move(information) }); } - auto result = QImage( - _scene->width(), - _scene->height(), - QImage::Format_ARGB32_Premultiplied); - result.fill(Qt::transparent); +} - { - QPainter p(&result); - p.setRenderHints(QPainter::Antialiasing); - p.setRenderHints(QPainter::SmoothPixmapTransform); +void Animation::parseFailed() { + _updates.fire_error(Error::ParseFailed); +} - const auto position = now; - const auto elapsed = int((_scene->frameRate() * position + 500) / 1000); - const auto from = _scene->startFrame(); - const auto till = _scene->endFrame(); - const auto frames = (till - from); - const auto frame = _options.loop - ? (from + (elapsed % frames)) - : std::min(from + elapsed, till); +QImage Animation::frame(const FrameRequest &request) const { + Expects(_renderer != nullptr); - _scene->updateProperties(frame); - - LottieRasterRenderer renderer(&p); - _scene->render(renderer, frame); + const auto frame = _state->frameForPaint(); + const auto changed = (frame->request != request) + && (request.strict || !frame->request.strict); + if (changed) { + frame->request = request; + _renderer->updateFrameRequest(_state, request); } + return PrepareFrameByRequest(frame, !changed); +} + +rpl::producer Animation::updates() const { + return _updates.events(); +} + +bool Animation::ready() const { + return (_renderer != nullptr); +} + +crl::time Animation::markFrameDisplayed(crl::time now) { + Expects(_renderer != nullptr); + + const auto result = _state->markFrameDisplayed(now); + return result; } -int Animation::frameRate() const { - return _scene->frameRate(); +crl::time Animation::markFrameShown() { + Expects(_renderer != nullptr); + + const auto result = _state->markFrameShown(); + _renderer->frameShown(_state); + + return result; } -crl::time Animation::duration() const { - return (_scene->endFrame() - _scene->startFrame()) * crl::time(1000) / _scene->frameRate(); -} +void Animation::checkNextFrame() { + Expects(_renderer != nullptr); -void Animation::play(const PlaybackOptions &options) { - _options = options; - _started = crl::now(); -} - -void Animation::parse(const QByteArray &content) { - const auto document = QJsonDocument::fromJson(content); - const auto root = document.object(); - - if (root.empty()) { - _failed = true; + const auto time = _state->nextFrameDisplayTime(); + if (time == kTimeUnknown) { return; } - _scene = std::make_unique(); - _scene->parse(root); + const auto now = crl::now(); + if (time > now) { + _timer.callOnce(time - now); + } else { + _timer.cancel(); + + const auto position = markFrameDisplayed(now); + _updates.fire({ DisplayFrameRequest{ position } }); + } } +//void Animation::play(const PlaybackOptions &options) { +// _options = options; +// _started = crl::now(); +//} + } // namespace Lottie diff --git a/Telegram/SourceFiles/lottie/lottie_animation.h b/Telegram/SourceFiles/lottie/lottie_animation.h index 999b4a8d1..264bd9781 100644 --- a/Telegram/SourceFiles/lottie/lottie_animation.h +++ b/Telegram/SourceFiles/lottie/lottie_animation.h @@ -9,69 +9,62 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/basic_types.h" #include "base/flat_map.h" +#include "base/weak_ptr.h" +#include "base/timer.h" +#include "lottie/lottie_common.h" + +#include #include +#include #include +#include class QImage; class QString; class QByteArray; -class BMScene; - namespace Lottie { class Animation; +class SharedState; +class FrameRenderer; bool ValidateFile(const QString &path); std::unique_ptr FromFile(const QString &path); std::unique_ptr FromData(const QByteArray &data); -struct PlaybackOptions { - float64 speed = 1.; - bool loop = true; -}; - -class Animation final { +class Animation final : public base::has_weak_ptr { public: explicit Animation(const QByteArray &content); ~Animation(); - void play(const PlaybackOptions &options); + //void play(const PlaybackOptions &options); - QImage frame(crl::time now) const; + [[nodiscard]] QImage frame(const FrameRequest &request) const; - int frameRate() const; - crl::time duration() const; + [[nodiscard]] rpl::producer updates() const; - void play(); - void pause(); - void resume(); - void stop(); - - [[nodiscard]] bool active() const; [[nodiscard]] bool ready() const; - [[nodiscard]] bool unsupported() const; - [[nodiscard]] float64 speed() const; - void setSpeed(float64 speed); // 0.5 <= speed <= 2. + // Returns frame position, if any frame was marked as displayed. + crl::time markFrameDisplayed(crl::time now); + crl::time markFrameShown(); - [[nodiscard]] bool playing() const; - [[nodiscard]] bool buffering() const; - [[nodiscard]] bool paused() const; - [[nodiscard]] bool finished() const; + void checkNextFrame(); private: - void parse(const QByteArray &content); + void parseDone(std::unique_ptr state); + void parseFailed(); - bool _initialized = false; - bool _unsupported = false; - bool _failed = false; - bool _paused = false; - crl::time _started = 0; - PlaybackOptions _options; + //crl::time _started = 0; + //PlaybackOptions _options; - std::unique_ptr _scene; + base::Timer _timer; + SharedState *_state = nullptr; + std::shared_ptr _renderer; + rpl::event_stream _updates; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/lottie/lottie_common.h b/Telegram/SourceFiles/lottie/lottie_common.h new file mode 100644 index 000000000..be003fd10 --- /dev/null +++ b/Telegram/SourceFiles/lottie/lottie_common.h @@ -0,0 +1,71 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/basic_types.h" +#include "base/variant.h" + +#include +#include +#include + +namespace Lottie { + +class Animation; + +struct PlaybackOptions { + float64 speed = 1.; + bool loop = true; +}; + +struct Information { + int frameRate = 0; + int framesCount = 0; + QSize size; +}; + +struct DisplayFrameRequest { + crl::time time = 0; +}; + +struct Update { + base::variant< + Information, + DisplayFrameRequest> data; +}; + +enum class Error { + ParseFailed, + NotSupported, +}; + +struct FrameRequest { + QSize resize; + std::optional colored; + bool strict = true; + + static FrameRequest NonStrict() { + auto result = FrameRequest(); + result.strict = false; + return result; + } + + bool empty() const { + return resize.isEmpty(); + } + + bool operator==(const FrameRequest &other) const { + return (resize == other.resize) + && (colored == other.colored); + } + bool operator!=(const FrameRequest &other) const { + return !(*this == other); + } +}; + +} // namespace Lottie diff --git a/Telegram/SourceFiles/lottie/lottie_frame_renderer.cpp b/Telegram/SourceFiles/lottie/lottie_frame_renderer.cpp index 0073e6b69..567271e04 100644 --- a/Telegram/SourceFiles/lottie/lottie_frame_renderer.cpp +++ b/Telegram/SourceFiles/lottie/lottie_frame_renderer.cpp @@ -8,222 +8,439 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_frame_renderer.h" #include "lottie/lottie_animation.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - #include "rasterrenderer/lottierasterrenderer.h" +#include "logs.h" -Q_LOGGING_CATEGORY(lcLottieQtBodymovinRenderThread, "qt.lottieqt.bodymovin.render.thread"); +#include +#include +#include + +namespace Images { +QImage prepareColored(QColor add, QImage image); +} // namespace Images namespace Lottie { -// -//FrameRenderer *FrameRenderer::_rendererInstance = nullptr; -// -//FrameRenderer::~FrameRenderer() -//{ -// QMutexLocker mlocker(&_mutex); -// qDeleteAll(_animData); -// qDeleteAll(_frameCache); -//} -// -//FrameRenderer *FrameRenderer::instance() -//{ -// if (!_rendererInstance) -// _rendererInstance = new FrameRenderer; -// -// return _rendererInstance; -//} -// -//void FrameRenderer::deleteInstance() -//{ -// delete _rendererInstance; -// _rendererInstance = nullptr; -//} -// -//void FrameRenderer::registerAnimator(Animation *animator) -//{ -// QMutexLocker mlocker(&_mutex); -// -// qCDebug(lcLottieQtBodymovinRenderThread) << "Register Animator:" -// << static_cast(animator); -// -// Entry *entry = new Entry; -// entry->animator = animator; -// entry->startFrame = animator->startFrame(); -// entry->endFrame = animator->endFrame(); -// entry->currentFrame = animator->startFrame(); -// entry->animDir = animator->direction(); -// entry->bmTreeBlueprint = new BMBase; -// parse(entry->bmTreeBlueprint, animator->jsonSource()); -// _animData.insert(animator, entry); -// _waitCondition.wakeAll(); -//} -// -//void FrameRenderer::deregisterAnimator(Animation *animator) -//{ -// QMutexLocker mlocker(&_mutex); -// -// qCDebug(lcLottieQtBodymovinRenderThread) << "Deregister Animator:" -// << static_cast(animator); -// -// Entry *entry = _animData.value(animator, nullptr); -// if (entry) { -// qDeleteAll(entry->frameCache); -// delete entry->bmTreeBlueprint; -// delete entry; -// _animData.remove(animator); -// } -//} -// -//bool FrameRenderer::gotoFrame(Animation *animator, int frame) -//{ -// QMutexLocker mlocker(&_mutex); -// Entry *entry = _animData.value(animator, nullptr); -// if (entry) { -// qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" -// << static_cast(animator) -// << "Goto frame:" << frame; -// entry->currentFrame = frame; -// entry->animDir = animator->direction(); -// pruneFrameCache(entry); -// _waitCondition.wakeAll(); -// return true; -// } -// return false; -//} -// -//FrameRenderer::FrameRenderer() : QThread() { -// const QByteArray cacheStr = qgetenv("QLOTTIE_RENDER_CACHE_SIZE"); -// int cacheSize = cacheStr.toInt(); -// if (cacheSize > 0) { -// qCDebug(lcLottieQtBodymovinRenderThread) << "Setting frame cache size to" << cacheSize; -// _cacheSize = cacheSize; -// } -//} -// -//void FrameRenderer::pruneFrameCache(Entry* e) -//{ -// QHash::iterator it = e->frameCache.begin(); -// -// while (it != e->frameCache.end()) { -// if (it.key() == e->currentFrame) { -// ++it; -// } else { -// delete it.value(); -// it = e->frameCache.erase(it); -// } -// } -//} -// -//BMBase *FrameRenderer::getFrame(Animation *animator, int frameNumber) -//{ -// QMutexLocker mlocker(&_mutex); -// -// Entry *entry = _animData.value(animator, nullptr); -// if (entry) -// return entry->frameCache.value(frameNumber, nullptr); -// else -// return nullptr; -//} -// -//void FrameRenderer::prerender(Entry *animEntry) -//{ -// while (animEntry->frameCache.count() < _cacheSize) { -// if (!animEntry->frameCache.contains(animEntry->currentFrame)) { -// BMBase *bmTree = new BMBase(*animEntry->bmTreeBlueprint); -// -// for (BMBase *elem : bmTree->children()) { -// if (elem->active(animEntry->currentFrame)) -// elem->updateProperties( animEntry->currentFrame); -// } -// -// animEntry->frameCache.insert( animEntry->currentFrame, bmTree); -// } -// -// qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" -// << static_cast(animEntry->animator) -// << "Frame drawn to cache. FN:" -// << animEntry->currentFrame; -// emit frameReady(animEntry->animator, animEntry->currentFrame); -// -// animEntry->currentFrame += animEntry->animDir; -// -// if (animEntry->currentFrame > animEntry->endFrame) { -// animEntry->currentFrame = animEntry->startFrame; -// } else if (animEntry->currentFrame < animEntry->startFrame) { -// animEntry->currentFrame = animEntry->endFrame; -// } -// } -//} -// -//void FrameRenderer::frameRendered(Animation *animator, int frameNumber) -//{ -// QMutexLocker mlocker(&_mutex); -// Entry *entry = _animData.value(animator, nullptr); -// if (entry) { -// qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" << static_cast(animator) -// << "Remove frame from cache" << frameNumber; -// -// BMBase *root = entry->frameCache.value(frameNumber, nullptr); -// delete root; -// entry->frameCache.remove(frameNumber); -// _waitCondition.wakeAll(); -// } -//} -// -//void FrameRenderer::run() -//{ -// qCDebug(lcLottieQtBodymovinRenderThread) << "rendering thread" << QThread::currentThread(); -// -// while (!isInterruptionRequested()) { -// QMutexLocker mlocker(&_mutex); -// -// for (Entry *e : qAsConst(_animData)) -// prerender(e); -// -// _waitCondition.wait(&_mutex); -// } -//} -// -//int FrameRenderer::parse(BMBase* rootElement, const QByteArray &jsonSource) -//{ -// QJsonDocument doc = QJsonDocument::fromJson(jsonSource); -// QJsonObject rootObj = doc.object(); -// -// if (rootObj.empty()) -// return -1; -// -// QJsonArray jsonLayers = rootObj.value(QLatin1String("layers")).toArray(); -// QJsonArray::const_iterator jsonLayerIt = jsonLayers.constEnd(); -// while (jsonLayerIt != jsonLayers.constBegin()) { -// jsonLayerIt--; -// QJsonObject jsonLayer = (*jsonLayerIt).toObject(); -// BMLayer *layer = BMLayer::construct(jsonLayer); -// if (layer) { -// layer->setParent(rootElement); -// // Mask layers must be rendered before the layers they affect to -// // although they appear before in layer hierarchy. For this reason -// // move a mask after the affected layers, so it will be rendered first -// if (layer->isMaskLayer()) -// rootElement->prependChild(layer); -// else -// rootElement->appendChild(layer); -// } -// } -// -// return 0; -//} +namespace { + +constexpr auto kDisplaySkipped = crl::time(-1); + +std::weak_ptr GlobalInstance; + +constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied; + +bool GoodStorageForFrame(const QImage &storage, QSize size) { + return !storage.isNull() + && (storage.format() == kImageFormat) + && (storage.size() == size) + && storage.isDetached(); +} + +QImage CreateFrameStorage(QSize size) { + return QImage(size, kImageFormat); +} + +} // namespace + +class FrameRendererObject final { +public: + explicit FrameRendererObject( + crl::weak_on_queue weak); + + void append(std::unique_ptr entry); + void frameShown(not_null entry); + void updateFrameRequest( + not_null entry, + const FrameRequest &request); + void remove(not_null entry); + +private: + struct Entry { + std::unique_ptr state; + FrameRequest request; + }; + + static not_null StateFromEntry(const Entry &entry) { + return entry.state.get(); + } + + void queueGenerateFrames(); + void generateFrames(); + + crl::weak_on_queue _weak; + std::vector _entries; + bool _queued = false; + +}; + +[[nodiscard]] bool GoodForRequest( + const QImage &image, + const FrameRequest &request) { + if (request.resize.isEmpty()) { + return true; + } else if (request.colored.has_value()) { + return false; + } + return (request.resize == image.size()); +} + +[[nodiscard]] QImage PrepareByRequest( + const QImage &original, + const FrameRequest &request, + QImage storage) { + Expects(!request.resize.isEmpty()); + + if (!GoodStorageForFrame(storage, request.resize)) { + storage = CreateFrameStorage(request.resize); + } + { + QPainter p(&storage); + p.setRenderHint(QPainter::Antialiasing); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.setRenderHint(QPainter::HighQualityAntialiasing); + p.drawImage(QRect(QPoint(), request.resize), original); + } + if (request.colored.has_value()) { + storage = Images::prepareColored(*request.colored, std::move(storage)); + } + return storage; +} + +QImage PrepareFrameByRequest( + not_null frame, + bool useExistingPrepared = false) { + Expects(!frame->original.isNull()); + + if (GoodForRequest(frame->original, frame->request)) { + return frame->original; + } else if (frame->prepared.isNull() || !useExistingPrepared) { + frame->prepared = PrepareByRequest( + frame->original, + frame->request, + std::move(frame->prepared)); + } + return frame->prepared; +} + +FrameRendererObject::FrameRendererObject( + crl::weak_on_queue weak) +: _weak(std::move(weak)) { +} + +void FrameRendererObject::append(std::unique_ptr state) { + _entries.push_back({ std::move(state) }); + queueGenerateFrames(); +} + +void FrameRendererObject::frameShown(not_null entry) { + queueGenerateFrames(); +} + +void FrameRendererObject::updateFrameRequest( + not_null entry, + const FrameRequest &request) { + const auto i = ranges::find(_entries, entry, &StateFromEntry); + Assert(i != end(_entries)); + i->request = request; +} + +void FrameRendererObject::remove(not_null entry) { + const auto i = ranges::find(_entries, entry, &StateFromEntry); + Assert(i != end(_entries)); + _entries.erase(i); +} + +void FrameRendererObject::generateFrames() { + const auto renderOne = [&](const Entry & entry) { + return entry.state->renderNextFrame(entry.request); + }; + if (ranges::count_if(_entries, renderOne) > 0) { + queueGenerateFrames(); + } +} + +void FrameRendererObject::queueGenerateFrames() { + if (_queued) { + return; + } + _queued = true; + _weak.with([](FrameRendererObject &that) { + that._queued = false; + that.generateFrames(); + }); +} + +SharedState::SharedState(const QJsonObject &definition) +: _scene(definition) { + if (_scene.endFrame() > _scene.startFrame()) { + auto cover = QImage(); + renderFrame(cover, FrameRequest::NonStrict(), 0); + init(std::move(cover)); + } +} + +void SharedState::renderFrame( + QImage &image, + const FrameRequest &request, + int index) { + const auto realSize = QSize(_scene.width(), _scene.height()); + if (realSize.isEmpty() || _scene.endFrame() <= _scene.startFrame()) { + return; + } + + const auto size = request.resize.isEmpty() ? realSize : request.resize; + if (!GoodStorageForFrame(image, size)) { + image = CreateFrameStorage(size); + } + image.fill(Qt::transparent); + + QPainter p(&image); + p.setRenderHints(QPainter::Antialiasing); + p.setRenderHints(QPainter::SmoothPixmapTransform); + p.setRenderHint(QPainter::TextAntialiasing); + p.setRenderHints(QPainter::HighQualityAntialiasing); + if (realSize != size) { + p.scale( + size.width() / float64(realSize.width()), + size.height() / float64(realSize.height())); + } + + const auto frame = std::clamp( + _scene.startFrame() + index, + _scene.startFrame(), + _scene.endFrame() - 1); + _scene.updateProperties(frame); + + LottieRasterRenderer renderer(&p); + _scene.render(renderer, frame); +} + +void SharedState::init(QImage cover) { + Expects(!initialized()); + + _frames[0].original = std::move(cover); + _frames[0].position = 0; + + // Usually main thread sets displayed time before _counter increment. + // But in this case we update _counter, so we set a fake displayed time. + _frames[0].displayed = kDisplaySkipped; + + _counter.store(0, std::memory_order_release); +} + +void SharedState::start(not_null owner, crl::time now) { + _owner = owner; + _started = now; +} + +bool IsRendered(not_null frame) { + return (frame->position != kTimeUnknown) + && (frame->displayed == kTimeUnknown); +} + +void SharedState::renderNextFrame( + not_null frame, + const FrameRequest &request) { + const auto framesCount = (_scene.endFrame() - _scene.startFrame()); + Assert(framesCount > 0); + + renderFrame(frame->original, request, (++_frameIndex) % framesCount); + PrepareFrameByRequest(frame); + frame->position = crl::time(1000) * _frameIndex / _scene.frameRate(); + frame->displayed = kTimeUnknown; +} + +bool SharedState::renderNextFrame(const FrameRequest &request) { + const auto prerender = [&](int index) { + const auto frame = getFrame(index); + const auto next = getFrame((index + 1) % kFramesCount); + if (!IsRendered(frame)) { + renderNextFrame(frame, request); + return true; + } else if (!IsRendered(next)) { + renderNextFrame(next, request); + return true; + } + return false; + }; + const auto present = [&](int counter, int index) { + const auto frame = getFrame(index); + if (!IsRendered(frame)) { + renderNextFrame(frame, request); + } + frame->display = _started + frame->position; + + // Release this frame to the main thread for rendering. + _counter.store( + (counter + 1) % (2 * kFramesCount), + std::memory_order_release); + crl::on_main(_owner, [=] { + _owner->checkNextFrame(); + }); + return true; + }; + + switch (counter()) { + case 0: return present(0, 1); + case 1: return prerender(2); + case 2: return present(2, 2); + case 3: return prerender(3); + case 4: return present(4, 3); + case 5: return prerender(0); + case 6: return present(6, 0); + case 7: return prerender(1); + } + Unexpected("Counter value in VideoTrack::Shared::prepareState."); + +} + +int SharedState::counter() const { + return _counter.load(std::memory_order_acquire); +} + +bool SharedState::initialized() const { + return (counter() != kCounterUninitialized); +} + +not_null SharedState::getFrame(int index) { + Expects(index >= 0 && index < kFramesCount); + + return &_frames[index]; +} + +not_null SharedState::getFrame(int index) const { + Expects(index >= 0 && index < kFramesCount); + + return &_frames[index]; +} + +Information SharedState::information() const { + auto result = Information(); + result.frameRate = _scene.frameRate(); + result.size = QSize(_scene.width(), _scene.height()); + result.framesCount = _scene.endFrame() - _scene.startFrame(); + return result; +} + +not_null SharedState::frameForPaint() { + const auto result = getFrame(counter() / 2); + Assert(!result->original.isNull()); + Assert(result->position != kTimeUnknown); + Assert(result->displayed != kTimeUnknown); + + return result; +} + +crl::time SharedState::nextFrameDisplayTime() const { + const auto frameDisplayTime = [&](int counter) { + const auto next = (counter + 1) % (2 * kFramesCount); + const auto index = next / 2; + const auto frame = getFrame(index); + Assert(IsRendered(frame)); + Assert(frame->display != kTimeUnknown); + + return frame->display; + }; + + switch (counter()) { + case 0: return kTimeUnknown; + case 1: return frameDisplayTime(1); + case 2: return kTimeUnknown; + case 3: return frameDisplayTime(3); + case 4: return kTimeUnknown; + case 5: return frameDisplayTime(5); + case 6: return kTimeUnknown; + case 7: return frameDisplayTime(7); + } + Unexpected("Counter value in VideoTrack::Shared::nextFrameDisplayTime."); +} + +crl::time SharedState::markFrameDisplayed(crl::time now) { + const auto mark = [&](int counter) { + const auto next = (counter + 1) % (2 * kFramesCount); + const auto index = next / 2; + const auto frame = getFrame(index); + Assert(frame->position != kTimeUnknown); + Assert(frame->displayed == kTimeUnknown); + + frame->displayed = now; + return frame->position; + }; + + switch (counter()) { + case 0: Unexpected("Value 0 in SharedState::markFrameDisplayed."); + case 1: return mark(1); + case 2: Unexpected("Value 2 in SharedState::markFrameDisplayed."); + case 3: return mark(3); + case 4: Unexpected("Value 4 in SharedState::markFrameDisplayed."); + case 5: return mark(5); + case 6: Unexpected("Value 6 in SharedState::markFrameDisplayed."); + case 7: return mark(7); + } + Unexpected("Counter value in Lottie::SharedState::markFrameDisplayed."); +} + +crl::time SharedState::markFrameShown() { + const auto jump = [&](int counter) { + const auto next = (counter + 1) % (2 * kFramesCount); + const auto index = next / 2; + const auto frame = getFrame(index); + Assert(frame->position != kTimeUnknown); + if (frame->displayed == kTimeUnknown) { + return kTimeUnknown; + } + _counter.store( + next, + std::memory_order_release); + return frame->position; + }; + + switch (counter()) { + case 0: return kTimeUnknown; + case 1: return jump(1); + case 2: return kTimeUnknown; + case 3: return jump(3); + case 4: return kTimeUnknown; + case 5: return jump(5); + case 6: return kTimeUnknown; + case 7: return jump(7); + } + Unexpected("Counter value in Lottie::SharedState::markFrameShown."); +} + +std::shared_ptr FrameRenderer::Instance() { + if (auto result = GlobalInstance.lock()) { + return result; + } + auto result = std::make_shared(); + GlobalInstance = result; + return result; +} + +void FrameRenderer::append(std::unique_ptr entry) { + _wrapped.with([entry = std::move(entry)]( + FrameRendererObject &unwrapped) mutable { + unwrapped.append(std::move(entry)); + }); +} + +void FrameRenderer::frameShown(not_null entry) { + _wrapped.with([=](FrameRendererObject &unwrapped) { + unwrapped.frameShown(entry); + }); +} + +void FrameRenderer::updateFrameRequest( + not_null entry, + const FrameRequest &request) { + _wrapped.with([=](FrameRendererObject &unwrapped) { + unwrapped.updateFrameRequest(entry, request); + }); +} + +void FrameRenderer::remove(not_null entry) { + _wrapped.with([=](FrameRendererObject &unwrapped) { + unwrapped.remove(entry); + }); +} } // namespace Lottie diff --git a/Telegram/SourceFiles/lottie/lottie_frame_renderer.h b/Telegram/SourceFiles/lottie/lottie_frame_renderer.h index 1c6c879e4..5dcdaa838 100644 --- a/Telegram/SourceFiles/lottie/lottie_frame_renderer.h +++ b/Telegram/SourceFiles/lottie/lottie_frame_renderer.h @@ -7,78 +7,99 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include -#include -#include -#include +#include "base/basic_types.h" +#include "base/weak_ptr.h" + +#include "lottie/lottie_common.h" + +#include +#include + +#include +#include + +#include class BMBase; class QImage; namespace Lottie { +constexpr auto kTimeUnknown = std::numeric_limits::min(); + class Animation; -// -//class FrameRenderer : public QThread { -// Q_OBJECT -// -// struct Entry { -// Animation* animator = nullptr; -// BMBase *bmTreeBlueprint = nullptr; -// int startFrame = 0; -// int endFrame = 0; -// int currentFrame = 0; -// int animDir = 1; -// QHash frameCache; -// }; -// -//public: -// ~FrameRenderer(); -// -// FrameRenderer(const FrameRenderer &other) = delete; -// void operator=(const FrameRenderer &other) = delete; -// -// static FrameRenderer *instance(); -// static void deleteInstance(); -// -// BMBase *getFrame(Animation *animator, int frameNumber); -// -//signals: -// void frameReady(Animation *animator, int frameNumber); -// -//public slots: -// void registerAnimator(Animation *animator); -// void deregisterAnimator(Animation *animator); -// -// bool gotoFrame(Animation *animator, int frame); -// -// void frameRendered(Animation *animator, int frameNumber); -// -//protected: -// void run() override; -// -// int parse(BMBase* rootElement, const QByteArray &jsonSource); -// -// void prerender(Entry *animEntry); -// -//protected: -// QHash _animData; -// int _cacheSize = 2; -// int _currentFrame = 0; -// -// Animation *_animation = nullptr; -// QHash _frameCache; -// -//private: -// FrameRenderer(); -// -// void pruneFrameCache(Entry* e); -// -//private: -// static FrameRenderer *_rendererInstance; -// -// QMutex _mutex; -// QWaitCondition _waitCondition; -//}; + +struct Frame { + QImage original; + crl::time position = kTimeUnknown; + crl::time displayed = kTimeUnknown; + crl::time display = kTimeUnknown; + + FrameRequest request = FrameRequest::NonStrict(); + QImage prepared; +}; + +QImage PrepareFrameByRequest( + not_null frame, + bool useExistingPrepared); + +class SharedState { +public: + explicit SharedState(const QJsonObject &definition); + + void start(not_null owner, crl::time now); + + [[nodiscard]] Information information() const; + [[nodiscard]] bool initialized() const; + + [[nodiscard]] not_null frameForPaint(); + [[nodiscard]] crl::time nextFrameDisplayTime() const; + crl::time markFrameDisplayed(crl::time now); + crl::time markFrameShown(); + + void renderFrame(QImage &image, const FrameRequest &request, int index); + [[nodiscard]] bool renderNextFrame(const FrameRequest &request); + +private: + void init(QImage cover); + void renderNextFrame( + not_null frame, + const FrameRequest &request); + [[nodiscard]] not_null getFrame(int index); + [[nodiscard]] not_null getFrame(int index) const; + [[nodiscard]] int counter() const; + + BMScene _scene; + + static constexpr auto kCounterUninitialized = -1; + std::atomic _counter = kCounterUninitialized; + + static constexpr auto kFramesCount = 4; + std::array _frames; + + base::weak_ptr _owner; + crl::time _started = kTimeUnknown; + int _frameIndex = 0; + +}; + +class FrameRendererObject; + +class FrameRenderer final { +public: + static std::shared_ptr Instance(); + + void append(std::unique_ptr entry); + void updateFrameRequest( + not_null entry, + const FrameRequest &request); + void frameShown(not_null entry); + void remove(not_null state); + +private: + using Implementation = FrameRendererObject; + crl::object_on_queue _wrapped; + +}; } // namespace Lottie diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 078faa13a..5d60c773e 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -192,13 +192,9 @@ struct OverlayWidget::Streamed { }; struct OverlayWidget::LottieFile { - template - LottieFile( - std::unique_ptr data, - Callback &&animationCallback); + LottieFile(std::unique_ptr data); std::unique_ptr data; - Ui::Animations::Basic animation; }; template @@ -215,12 +211,9 @@ OverlayWidget::Streamed::Streamed( st::mediaviewStreamingRadial) { } -template OverlayWidget::LottieFile::LottieFile( - std::unique_ptr data, - Callback &&animationCallback) -: data(std::move(data)) -, animation(std::forward(animationCallback)) { + std::unique_ptr data) +: data(std::move(data)) { } OverlayWidget::OverlayWidget() @@ -1851,9 +1844,11 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { _current = PrepareStaticImage(path); } else if (auto lottie = Lottie::FromFile(path)) { _lottie = std::make_unique( - std::move(lottie), - [=] { update(); }); - _lottie->animation.start(); + std::move(lottie)); + _lottie->data->updates( + ) | rpl::start_with_next([=] { + update(); + }, lifetime()); } } location.accessDisable(); @@ -2924,8 +2919,9 @@ void OverlayWidget::paintThemePreview(Painter &p, QRect clip) { void OverlayWidget::paintLottieFrame(Painter &p, QRect clip) { Expects(_lottie != nullptr); - const auto frame = _lottie->data->frame(crl::now()); - if (!frame.isNull()) { + if (_lottie->data->ready()) { + _lottie->data->markFrameShown(); + const auto frame = _lottie->data->frame(Lottie::FrameRequest()); const auto x = (width() - frame.width()) / 2; const auto y = (height() - frame.height()) / 2; const auto background = _lottieDark ? Qt::black : Qt::white; diff --git a/Telegram/SourceFiles/ui/image/image_prepare.cpp b/Telegram/SourceFiles/ui/image/image_prepare.cpp index e3fda222f..7195de55d 100644 --- a/Telegram/SourceFiles/ui/image/image_prepare.cpp +++ b/Telegram/SourceFiles/ui/image/image_prepare.cpp @@ -476,15 +476,24 @@ void prepareRound( } QImage prepareColored(style::color add, QImage image) { - auto format = image.format(); + return prepareColored(add->c, std::move(image)); +} + +QImage prepareColored(QColor add, QImage image) { + const auto format = image.format(); if (format != QImage::Format_RGB32 && format != QImage::Format_ARGB32_Premultiplied) { image = std::move(image).convertToFormat(QImage::Format_ARGB32_Premultiplied); } - if (auto pix = image.bits()) { - int ca = int(add->c.alphaF() * 0xFF), cr = int(add->c.redF() * 0xFF), cg = int(add->c.greenF() * 0xFF), cb = int(add->c.blueF() * 0xFF); - const int w = image.width(), h = image.height(), size = w * h * 4; - for (auto i = index_type(); i < size; i += 4) { + if (const auto pix = image.bits()) { + const auto ca = int(add.alphaF() * 0xFF); + const auto cr = int(add.redF() * 0xFF); + const auto cg = int(add.greenF() * 0xFF); + const auto cb = int(add .blueF() * 0xFF); + const auto w = image.width(); + const auto h = image.height(); + const auto size = w * h * 4; + for (auto i = index_type(); i != size; i += 4) { int b = pix[i], g = pix[i + 1], r = pix[i + 2], a = pix[i + 3], aca = a * ca; pix[i + 0] = uchar(b + ((aca * (cb - b)) >> 16)); pix[i + 1] = uchar(g + ((aca * (cg - g)) >> 16)); diff --git a/Telegram/SourceFiles/ui/image/image_prepare.h b/Telegram/SourceFiles/ui/image/image_prepare.h index a6de2e7c2..b9f96a560 100644 --- a/Telegram/SourceFiles/ui/image/image_prepare.h +++ b/Telegram/SourceFiles/ui/image/image_prepare.h @@ -41,6 +41,7 @@ void prepareRound( QRect target = QRect()); void prepareCircle(QImage &image); QImage prepareColored(style::color add, QImage image); +QImage prepareColored(QColor add, QImage image); QImage prepareOpaque(QImage image); enum class Option { diff --git a/Telegram/ThirdParty/qtlottie b/Telegram/ThirdParty/qtlottie index ff75b08c3..26d3e9ff5 160000 --- a/Telegram/ThirdParty/qtlottie +++ b/Telegram/ThirdParty/qtlottie @@ -1 +1 @@ -Subproject commit ff75b08c3adabaa33f7f879e12119de8ab1c2153 +Subproject commit 26d3e9ff5f354a20e72b90e2a3d4d57bd73baa8c diff --git a/Telegram/ThirdParty/qtlottie_helper/QtBodymovin/private/bmscene_p.h b/Telegram/ThirdParty/qtlottie_helper/QtBodymovin/private/bmscene_p.h index 6d946f98b..fc56d1d0d 100644 --- a/Telegram/ThirdParty/qtlottie_helper/QtBodymovin/private/bmscene_p.h +++ b/Telegram/ThirdParty/qtlottie_helper/QtBodymovin/private/bmscene_p.h @@ -58,13 +58,12 @@ class BODYMOVIN_EXPORT BMScene : public BMBase public: BMScene(); BMScene(const BMScene &other) = delete; - BMScene &operator=(const BMScene &other) = delete; + BMScene &operator=(const BMScene &other) = delete; + explicit BMScene(const QJsonObject &definition); virtual ~BMScene(); BMBase *clone() const override; - void parse(const QJsonObject &definition) override; - void updateProperties(int frame) override; void render(LottieRenderer &renderer, int frame) const override; @@ -78,6 +77,7 @@ protected: BMScene *resolveTopRoot() const override; private: + void parse(const QJsonObject &definition); void resolveAllAssets(); std::vector> _assets; diff --git a/Telegram/gyp/lib_lottie.gyp b/Telegram/gyp/lib_lottie.gyp index e594c8a75..4702a0e44 100644 --- a/Telegram/gyp/lib_lottie.gyp +++ b/Telegram/gyp/lib_lottie.gyp @@ -63,6 +63,7 @@ # interface for tdesktop '<(src_loc)/lottie/lottie_animation.cpp', '<(src_loc)/lottie/lottie_animation.h', + '<(src_loc)/lottie/lottie_common.h', '<(src_loc)/lottie/lottie_frame_renderer.cpp', '<(src_loc)/lottie/lottie_frame_renderer.h',