init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,354 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_cache.h"
#include "lottie/details/lottie_frame_renderer.h"
#include "ffmpeg/ffmpeg_utility.h"
#include "base/bytes.h"
#include "base/assertion.h"
#include <QDataStream>
#include <QIODevice>
#include <range/v3/numeric/accumulate.hpp>
namespace Lottie {
namespace {
// Must not exceed max database allowed entry size.
constexpr auto kMaxCacheSize = 10 * 1024 * 1024;
} // namespace
Cache::Cache(
const QByteArray &data,
const FrameRequest &request,
FnMut<void(QByteArray &&cached)> put)
: _data(data)
, _put(std::move(put)) {
if (!readHeader(request)) {
_framesReady = 0;
_framesInData = 0;
_data = QByteArray();
}
}
Cache::Cache(Cache &&) = default;
Cache &Cache::operator=(Cache&&) = default;
Cache::~Cache() {
finalizeEncoding();
}
void Cache::init(
QSize original,
int frameRate,
int framesCount,
const FrameRequest &request) {
_size = request.size(original, sizeRounding());
_original = original;
_frameRate = frameRate;
_framesCount = framesCount;
_framesReady = 0;
_framesInData = 0;
prepareBuffers();
}
int Cache::sizeRounding() const {
return 8;
}
int Cache::frameRate() const {
return _frameRate;
}
int Cache::framesReady() const {
return _framesReady;
}
int Cache::framesCount() const {
return _framesCount;
}
QSize Cache::originalSize() const {
return _original;
}
bool Cache::readHeader(const FrameRequest &request) {
if (_data.isEmpty()) {
return false;
}
QDataStream stream(&_data, QIODevice::ReadOnly);
auto encoder = qint32(0);
stream >> encoder;
if (static_cast<Encoder>(encoder) != Encoder::YUV420A4_LZ4) {
return false;
}
auto size = QSize();
auto original = QSize();
auto frameRate = qint32(0);
auto framesCount = qint32(0);
auto framesReady = qint32(0);
stream
>> size
>> original
>> frameRate
>> framesCount
>> framesReady;
if (stream.status() != QDataStream::Ok
|| original.isEmpty()
|| (original.width() > kMaxSize)
|| (original.height() > kMaxSize)
|| (frameRate <= 0)
|| (frameRate > kNormalFrameRate && frameRate != kMaxFrameRate)
|| (framesCount <= 0)
|| (framesCount > kMaxFramesCount)
|| (framesReady <= 0)
|| (framesReady > framesCount)
|| request.size(original, sizeRounding()) != size) {
return false;
}
_encoder = static_cast<Encoder>(encoder);
_size = size;
_original = original;
_frameRate = frameRate;
_framesCount = framesCount;
_framesReady = framesReady;
_framesInData = framesReady;
prepareBuffers();
return (renderFrame(_firstFrame, request, 0) == FrameRenderResult::Ok);
}
QImage Cache::takeFirstFrame() {
return std::move(_firstFrame);
}
FrameRenderResult Cache::renderFrame(
QImage &to,
const FrameRequest &request,
int index) {
const auto result = renderFrame(_readContext, to, request, index);
if (result == FrameRenderResult::Ok) {
if (index + 1 == _framesReady && _data.size() > _readContext.offset) {
_data.resize(_readContext.offset);
}
} else if (result == FrameRenderResult::BadCacheSize) {
_framesReady = 0;
_framesInData = 0;
_data.clear();
}
return result;
}
FrameRenderResult Cache::renderFrame(
CacheReadContext &context,
QImage &to,
const FrameRequest &request,
int index) const {
Expects(index >= _framesReady
|| index == context.offsetFrameIndex
|| index == 0);
Expects(index >= _framesReady || context.ready());
if (index >= _framesReady) {
return FrameRenderResult::NotReady;
} else if (request.size(_original, sizeRounding()) != _size) {
return FrameRenderResult::BadCacheSize;
} else if (index == 0) {
context.offsetFrameIndex = 0;
context.offset = headerSize();
}
const auto [ok, xored] = readCompressedFrame(context);
if (!ok || (xored && index == 0)) {
return FrameRenderResult::Failed;
}
if (xored) {
Xor(context.previous, context.uncompressed);
} else {
std::swap(context.uncompressed, context.previous);
}
Decode(to, context.previous, _size, context.decodeContext);
return FrameRenderResult::Ok;
}
void Cache::appendFrame(
const QImage &frame,
const FrameRequest &request,
int index) {
if (request.size(_original, sizeRounding()) != _size) {
_framesReady = 0;
_framesInData = 0;
_data = QByteArray();
}
if (index != _framesReady) {
return;
} else if (index == 0) {
_size = request.size(_original, sizeRounding());
_encode = EncodeFields();
_encode.compressedFrames.reserve(_framesCount);
prepareBuffers();
}
Assert(frame.size() == _size);
Assert(_readContext.ready());
Encode(_readContext.uncompressed, frame, _encode.cache, _encode.context);
CompressAndSwapFrame(
_encode.compressBuffer,
(index != 0) ? &_encode.xorCompressBuffer : nullptr,
_readContext.uncompressed,
_readContext.previous);
const auto compressed = _encode.compressBuffer;
const auto nowSize = (_data.isEmpty() ? headerSize() : _data.size())
+ _encode.totalSize;
const auto totalSize = nowSize + compressed.size();
if (nowSize <= kMaxCacheSize && totalSize > kMaxCacheSize) {
// Write to cache while we still can.
finalizeEncoding();
}
_encode.totalSize += compressed.size();
_encode.compressedFrames.push_back(compressed);
_encode.compressedFrames.back().detach();
++_readContext.offsetFrameIndex;
_readContext.offset += compressed.size();
if (++_framesReady == _framesCount) {
finalizeEncoding();
}
}
void Cache::finalizeEncoding() {
if (_encode.compressedFrames.empty() || !_put) {
return;
}
const auto size = (_data.isEmpty() ? headerSize() : _data.size())
+ _encode.totalSize;
if (_data.isEmpty()) {
_data.reserve(size);
writeHeader();
} else {
updateFramesReadyCount();
}
const auto offset = _data.size();
_data.resize(size);
auto to = _data.data() + offset;
for (const auto &block : _encode.compressedFrames) {
const auto amount = qint32(block.size());
memcpy(to, block.data(), amount);
to += amount;
}
_framesInData = _framesReady;
if (_data.size() <= kMaxCacheSize) {
_put(QByteArray(_data));
}
_encode = EncodeFields();
}
int Cache::headerSize() const {
return 8 * sizeof(qint32);
}
void Cache::writeHeader() {
Expects(_data.isEmpty());
QDataStream stream(&_data, QIODevice::WriteOnly);
stream
<< static_cast<qint32>(_encoder)
<< _size
<< _original
<< qint32(_frameRate)
<< qint32(_framesCount)
<< qint32(_framesReady);
}
void Cache::updateFramesReadyCount() {
Expects(_data.size() >= headerSize());
QDataStream stream(&_data, QIODevice::ReadWrite);
stream.device()->seek(headerSize() - sizeof(qint32));
stream << qint32(_framesReady);
}
void Cache::prepareBuffers() {
prepareBuffers(_readContext);
}
void Cache::prepareBuffers(CacheReadContext &context) const {
if (_size.isEmpty()) {
return;
}
// 12 bit per pixel in YUV420P.
const auto bytesPerLine = _size.width();
context.offset = headerSize();
context.offsetFrameIndex = 0;
context.uncompressed.allocate(bytesPerLine, _size.height());
context.previous.allocate(bytesPerLine, _size.height());
}
void Cache::keepUpContext(CacheReadContext &context) const {
Expects(!context.ready()
|| context.previous.size() == _readContext.previous.size());
Expects(!context.ready()
|| context.uncompressed.size() == _readContext.uncompressed.size());
Expects(&context != &_readContext);
if (!context.ready()) {
prepareBuffers(context);
}
context.offset = _readContext.offset;
context.offsetFrameIndex = _readContext.offsetFrameIndex;
memcpy(
context.previous.data(),
_readContext.previous.data(),
_readContext.previous.size());
memcpy(
context.uncompressed.data(),
_readContext.uncompressed.data(),
_readContext.uncompressed.size());
}
Cache::ReadResult Cache::readCompressedFrame(
CacheReadContext &context) const {
Expects(context.ready());
auto length = qint32(0);
const auto part = [&] {
if (context.offsetFrameIndex >= _framesInData) {
// One reader is still accumulating compressed frames,
// while second reader already started reading after the first.
const auto readyIndex = context.offsetFrameIndex - _framesInData;
Assert(readyIndex < _encode.compressedFrames.size());
return bytes::make_span(_encode.compressedFrames[readyIndex]);
} else if (_data.size() > context.offset) {
return bytes::make_span(_data).subspan(context.offset);
} else {
return bytes::const_span();
}
}();
if (part.size() < sizeof(length)) {
return { false };
}
bytes::copy(
bytes::object_as_span(&length),
part.subspan(0, sizeof(length)));
const auto bytes = part.subspan(sizeof(length));
const auto xored = (length < 0);
if (xored) {
length = -length;
}
const auto ok = (length <= bytes.size())
? UncompressToRaw(context.uncompressed, bytes.subspan(0, length))
: false;
if (ok) {
context.offset += sizeof(length) + length;
++context.offsetFrameIndex;
}
return { ok, xored };
}
} // namespace Lottie

View File

@@ -0,0 +1,114 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ffmpeg/ffmpeg_utility.h"
#include "lottie/details/lottie_cache_frame_storage.h"
#include "lottie/lottie_common.h"
#include <QImage>
#include <QSize>
#include <QByteArray>
namespace Lottie {
struct FrameRequest;
struct CacheReadContext {
EncodedStorage uncompressed;
EncodedStorage previous;
FFmpeg::SwscalePointer decodeContext;
int offset = 0;
int offsetFrameIndex = 0;
[[nodiscard]] bool ready() const {
return (offset != 0);
}
};
class Cache {
public:
enum class Encoder : qint8 {
YUV420A4_LZ4,
};
Cache(
const QByteArray &data,
const FrameRequest &request,
FnMut<void(QByteArray &&cached)> put);
Cache(Cache &&);
Cache &operator=(Cache&&);
~Cache();
void init(
QSize original,
int frameRate,
int framesCount,
const FrameRequest &request);
[[nodiscard]] int sizeRounding() const;
[[nodiscard]] int frameRate() const;
[[nodiscard]] int framesReady() const;
[[nodiscard]] int framesCount() const;
[[nodiscard]] QSize originalSize() const;
[[nodiscard]] QImage takeFirstFrame();
void prepareBuffers(CacheReadContext &context) const;
void keepUpContext(CacheReadContext &context) const;
[[nodiscard]] FrameRenderResult renderFrame(
QImage &to,
const FrameRequest &request,
int index);
[[nodiscard]] FrameRenderResult renderFrame(
CacheReadContext &context,
QImage &to,
const FrameRequest &request,
int index) const;
void appendFrame(
const QImage &frame,
const FrameRequest &request,
int index);
private:
struct ReadResult {
bool ok = false;
bool xored = false;
};
struct EncodeFields {
std::vector<QByteArray> compressedFrames;
QByteArray compressBuffer;
QByteArray xorCompressBuffer;
QImage cache;
FFmpeg::SwscalePointer context;
int totalSize = 0;
};
int headerSize() const;
void prepareBuffers();
void finalizeEncoding();
void writeHeader();
void updateFramesReadyCount();
[[nodiscard]] bool readHeader(const FrameRequest &request);
[[nodiscard]] ReadResult readCompressedFrame(
CacheReadContext &context) const;
QByteArray _data;
EncodeFields _encode;
QSize _size;
QSize _original;
CacheReadContext _readContext;
QImage _firstFrame;
int _frameRate = 0;
int _framesCount = 0;
int _framesReady = 0;
int _framesInData = 0;
Encoder _encoder = Encoder::YUV420A4_LZ4;
FnMut<void(QByteArray &&cached)> _put;
};
} // namespace Lottie

View File

@@ -0,0 +1,361 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_cache_frame_storage.h"
#include "base/assertion.h"
#include <QtGui/QImage>
#include <lz4.h>
#include <lz4hc.h>
namespace Lottie {
namespace {
constexpr auto kAlignStorage = 16;
void DecodeYUV2RGB(
QImage &to,
const EncodedStorage &from,
FFmpeg::SwscalePointer &context) {
context = FFmpeg::MakeSwscalePointer(
to.size(),
AV_PIX_FMT_YUV420P,
to.size(),
AV_PIX_FMT_BGRA,
&context);
Assert(context != nullptr);
// AV_NUM_DATA_POINTERS defined in AVFrame struct
const uint8_t *src[AV_NUM_DATA_POINTERS] = {
from.yData(),
from.uData(),
from.vData(),
nullptr
};
int srcLineSize[AV_NUM_DATA_POINTERS] = {
from.yBytesPerLine(),
from.uBytesPerLine(),
from.vBytesPerLine(),
0
};
uint8_t *dst[AV_NUM_DATA_POINTERS] = { to.bits(), nullptr };
int dstLineSize[AV_NUM_DATA_POINTERS] = { int(to.bytesPerLine()), 0 };
sws_scale(
context.get(),
src,
srcLineSize,
0,
to.height(),
dst,
dstLineSize);
}
void DecodeAlpha(QImage &to, const EncodedStorage &from) {
auto bytes = to.bits();
auto alpha = from.aData();
const auto perLine = to.bytesPerLine();
const auto width = to.width();
const auto height = to.height();
for (auto i = 0; i != height; ++i) {
auto ints = reinterpret_cast<uint32*>(bytes);
const auto till = ints + width;
while (ints != till) {
const auto value = uint32(*alpha++);
*ints = (*ints & 0x00FFFFFFU)
| ((value & 0xF0U) << 24)
| ((value & 0xF0U) << 20);
++ints;
*ints = (*ints & 0x00FFFFFFU)
| (value << 28)
| ((value & 0x0FU) << 24);
++ints;
}
bytes += perLine;
}
}
void EncodeRGB2YUV(
EncodedStorage &to,
const QImage &from,
FFmpeg::SwscalePointer &context) {
context = FFmpeg::MakeSwscalePointer(
from.size(),
AV_PIX_FMT_BGRA,
from.size(),
AV_PIX_FMT_YUV420P,
&context);
Assert(context != nullptr);
// AV_NUM_DATA_POINTERS defined in AVFrame struct
const uint8_t *src[AV_NUM_DATA_POINTERS] = { from.bits(), nullptr };
int srcLineSize[AV_NUM_DATA_POINTERS] = { int(from.bytesPerLine()), 0 };
uint8_t *dst[AV_NUM_DATA_POINTERS] = {
to.yData(),
to.uData(),
to.vData(),
nullptr
};
int dstLineSize[AV_NUM_DATA_POINTERS] = {
to.yBytesPerLine(),
to.uBytesPerLine(),
to.vBytesPerLine(),
0
};
sws_scale(
context.get(),
src,
srcLineSize,
0,
from.height(),
dst,
dstLineSize);
}
void EncodeAlpha(EncodedStorage &to, const QImage &from) {
auto bytes = from.bits();
auto alpha = to.aData();
const auto perLine = from.bytesPerLine();
const auto width = from.width();
const auto height = from.height();
for (auto i = 0; i != height; ++i) {
auto ints = reinterpret_cast<const uint32*>(bytes);
const auto till = ints + width;
for (; ints != till; ints += 2) {
*alpha++ = (((*ints) >> 24) & 0xF0U) | ((*(ints + 1)) >> 28);
}
bytes += perLine;
}
}
int YLineSize(int width) {
return ((width + kAlignStorage - 1) / kAlignStorage) * kAlignStorage;
}
int UVLineSize(int width) {
return (((width / 2) + kAlignStorage - 1) / kAlignStorage) * kAlignStorage;
}
int YSize(int width, int height) {
return YLineSize(width) * height;
}
int UVSize(int width, int height) {
return UVLineSize(width) * (height / 2);
}
int ASize(int width, int height) {
return (width * height) / 2;
}
} // namespace
void EncodedStorage::allocate(int width, int height) {
Expects((width % 2) == 0 && (height % 2) == 0);
if (YSize(width, height) != YSize(_width, _height)
|| UVSize(width, height) != UVSize(_width, _height)
|| ASize(width, height) != ASize(_width, _height)) {
_width = width;
_height = height;
reallocate();
}
}
void EncodedStorage::reallocate() {
const auto total = YSize(_width, _height)
+ 2 * UVSize(_width, _height)
+ ASize(_width, _height);
_data = QByteArray(total + kAlignStorage - 1, 0);
}
int EncodedStorage::width() const {
return _width;
}
int EncodedStorage::height() const {
return _height;
}
int EncodedStorage::size() const {
return YSize(_width, _height)
+ 2 * UVSize(_width, _height)
+ ASize(_width, _height);
}
char *EncodedStorage::data() {
const auto result = reinterpret_cast<quintptr>(_data.data());
return reinterpret_cast<char*>(kAlignStorage
* ((result + kAlignStorage - 1) / kAlignStorage));
}
const char *EncodedStorage::data() const {
const auto result = reinterpret_cast<quintptr>(_data.data());
return reinterpret_cast<const char*>(kAlignStorage
* ((result + kAlignStorage - 1) / kAlignStorage));
}
uint8_t *EncodedStorage::yData() {
return reinterpret_cast<uint8_t*>(data());
}
const uint8_t *EncodedStorage::yData() const {
return reinterpret_cast<const uint8_t*>(data());
}
int EncodedStorage::yBytesPerLine() const {
return YLineSize(_width);
}
uint8_t *EncodedStorage::uData() {
return yData() + YSize(_width, _height);
}
const uint8_t *EncodedStorage::uData() const {
return yData() + YSize(_width, _height);
}
int EncodedStorage::uBytesPerLine() const {
return UVLineSize(_width);
}
uint8_t *EncodedStorage::vData() {
return uData() + UVSize(_width, _height);
}
const uint8_t *EncodedStorage::vData() const {
return uData() + UVSize(_width, _height);
}
int EncodedStorage::vBytesPerLine() const {
return UVLineSize(_width);
}
uint8_t *EncodedStorage::aData() {
return uData() + 2 * UVSize(_width, _height);
}
const uint8_t *EncodedStorage::aData() const {
return uData() + 2 * UVSize(_width, _height);
}
int EncodedStorage::aBytesPerLine() const {
return _width / 2;
}
void Xor(EncodedStorage &to, const EncodedStorage &from) {
Expects(to.size() == from.size());
using Block = std::conditional_t<
sizeof(void*) == sizeof(uint64),
uint64,
uint32>;
constexpr auto kBlockSize = sizeof(Block);
const auto amount = from.size();
const auto fromBytes = reinterpret_cast<const uchar*>(from.data());
const auto toBytes = reinterpret_cast<uchar*>(to.data());
const auto blocks = amount / kBlockSize;
const auto fromBlocks = reinterpret_cast<const Block*>(fromBytes);
const auto toBlocks = reinterpret_cast<Block*>(toBytes);
for (auto i = 0; i != blocks; ++i) {
toBlocks[i] ^= fromBlocks[i];
}
const auto left = amount - (blocks * kBlockSize);
for (auto i = amount - left; i != amount; ++i) {
toBytes[i] ^= fromBytes[i];
}
}
void Encode(
EncodedStorage &to,
const QImage &from,
QImage &cache,
FFmpeg::SwscalePointer &context) {
FFmpeg::UnPremultiply(cache, from);
EncodeRGB2YUV(to, cache, context);
EncodeAlpha(to, cache);
}
void Decode(
QImage &to,
const EncodedStorage &from,
const QSize &fromSize,
FFmpeg::SwscalePointer &context) {
if (!FFmpeg::GoodStorageForFrame(to, fromSize)) {
to = FFmpeg::CreateFrameStorage(fromSize);
}
DecodeYUV2RGB(to, from, context);
DecodeAlpha(to, from);
FFmpeg::PremultiplyInplace(to);
}
void CompressFromRaw(QByteArray &to, const EncodedStorage &from) {
const auto size = from.size();
const auto max = sizeof(qint32) + LZ4_compressBound(size);
to.reserve(max);
to.resize(max);
const auto compressed = LZ4_compress_default(
from.data(),
to.data() + sizeof(qint32),
size,
to.size() - sizeof(qint32));
Assert(compressed > 0);
if (compressed >= size + sizeof(qint32)) {
to.resize(size + sizeof(qint32));
memcpy(to.data() + sizeof(qint32), from.data(), size);
} else {
to.resize(compressed + sizeof(qint32));
}
const auto length = qint32(to.size() - sizeof(qint32));
bytes::copy(
bytes::make_detached_span(to),
bytes::object_as_span(&length));
}
void CompressAndSwapFrame(
QByteArray &to,
QByteArray *additional,
EncodedStorage &frame,
EncodedStorage &previous) {
CompressFromRaw(to, frame);
std::swap(frame, previous);
if (!additional) {
return;
}
// Check if XOR-d delta compresses better.
Xor(frame, previous);
CompressFromRaw(*additional, frame);
if (additional->size() >= to.size()) {
return;
}
std::swap(to, *additional);
// Negative length means we XOR-d with the previous frame.
const auto negativeLength = -qint32(to.size() - sizeof(qint32));
bytes::copy(
bytes::make_detached_span(to),
bytes::object_as_span(&negativeLength));
}
bool UncompressToRaw(EncodedStorage &to, bytes::const_span from) {
if (from.empty() || from.size() > to.size()) {
return false;
} else if (from.size() == to.size()) {
memcpy(to.data(), from.data(), from.size());
return true;
}
const auto result = LZ4_decompress_safe(
reinterpret_cast<const char*>(from.data()),
to.data(),
from.size(),
to.size());
return (result == to.size());
}
} // namespace Lottie

View File

@@ -0,0 +1,69 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ffmpeg/ffmpeg_utility.h"
namespace Lottie {
class EncodedStorage {
public:
void allocate(int width, int height);
int width() const;
int height() const;
char *data();
const char *data() const;
int size() const;
uint8_t *yData();
const uint8_t *yData() const;
int yBytesPerLine() const;
uint8_t *uData();
const uint8_t *uData() const;
int uBytesPerLine() const;
uint8_t *vData();
const uint8_t *vData() const;
int vBytesPerLine() const;
uint8_t *aData();
const uint8_t *aData() const;
int aBytesPerLine() const;
private:
void reallocate();
int _width = 0;
int _height = 0;
QByteArray _data;
};
void Xor(EncodedStorage &to, const EncodedStorage &from);
void Encode(
EncodedStorage &to,
const QImage &from,
QImage &cache,
FFmpeg::SwscalePointer &context);
void Decode(
QImage &to,
const EncodedStorage &from,
const QSize &fromSize,
FFmpeg::SwscalePointer &context);
void CompressFromRaw(QByteArray &to, const EncodedStorage &from);
void CompressAndSwapFrame(
QByteArray &to,
QByteArray *additional,
EncodedStorage &frame,
EncodedStorage &previous);
bool UncompressToRaw(EncodedStorage &to, bytes::const_span from);
} // namespace Lottie

View File

@@ -0,0 +1,45 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "lottie/lottie_common.h"
namespace Lottie {
struct FrameProviderToken {
virtual ~FrameProviderToken() = default;
FrameRenderResult result = FrameRenderResult::Ok;
bool exclusive = false;
};
class FrameProvider {
public:
virtual ~FrameProvider() = default;
virtual QImage construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) = 0;
[[nodiscard]] virtual const Information &information() = 0;
[[nodiscard]] virtual bool valid() = 0;
[[nodiscard]] virtual int sizeRounding() = 0;
[[nodiscard]] virtual std::unique_ptr<FrameProviderToken> createToken() {
// Used for shared frame provider.
return nullptr;
}
virtual bool render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) = 0;
};
} // namespace Lottie

View File

@@ -0,0 +1,128 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_frame_provider_cached.h"
namespace Lottie {
FrameProviderCached::FrameProviderCached(
const QByteArray &content,
FnMut<void(QByteArray &&cached)> put,
const QByteArray &cached,
const FrameRequest &request,
Quality quality,
const ColorReplacements *replacements)
: _cache(cached, request, std::move(put))
, _direct(quality)
, _content(content)
, _replacements(replacements) {
if (!_cache.framesCount()
|| (_cache.framesReady() < _cache.framesCount())) {
if (!_direct.load(content, replacements)) {
return;
}
} else {
_direct.setInformation({
.size = _cache.originalSize(),
.frameRate = _cache.frameRate(),
.framesCount = _cache.framesCount(),
});
}
}
QImage FrameProviderCached::construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) {
auto cover = _cache.takeFirstFrame();
using Token = FrameProviderCachedToken;
const auto my = static_cast<Token*>(token.get());
if (!my || my->exclusive) {
if (!cover.isNull()) {
if (my) {
_cache.keepUpContext(my->context);
}
return cover;
}
const auto &info = information();
_cache.init(
info.size,
info.frameRate,
info.framesCount,
request);
}
render(token, cover, request, 0);
return cover;
}
const Information &FrameProviderCached::information() {
return _direct.information();
}
bool FrameProviderCached::valid() {
return _direct.valid();
}
int FrameProviderCached::sizeRounding() {
return _cache.sizeRounding();
}
std::unique_ptr<FrameProviderToken> FrameProviderCached::createToken() {
auto result = std::make_unique<FrameProviderCachedToken>();
_cache.prepareBuffers(result->context);
return result;
}
bool FrameProviderCached::render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) {
if (!valid()) {
if (token) {
token->result = FrameRenderResult::Failed;
}
return false;
}
const auto original = information().size;
const auto size = request.box.isEmpty()
? original
: request.size(original, sizeRounding());
if (!GoodStorageForFrame(to, size)) {
to = CreateFrameStorage(size);
}
using Token = FrameProviderCachedToken;
const auto my = static_cast<Token*>(token.get());
if (my && !my->exclusive) {
// This must be a thread-safe request.
my->result = _cache.renderFrame(my->context, to, request, index);
return (my->result == FrameRenderResult::Ok);
}
const auto result = _cache.renderFrame(to, request, index);
if (result == FrameRenderResult::Ok) {
if (my) {
_cache.keepUpContext(my->context);
}
return true;
} else if (result == FrameRenderResult::Failed
// We don't support changing size on the fly for shared providers.
|| (result == FrameRenderResult::BadCacheSize && my)
|| (!_direct.loaded() && !_direct.load(_content, _replacements))) {
_direct.setInformation({});
return false;
}
_direct.renderToPrepared(to, index);
_cache.appendFrame(to, request, index);
if (_cache.framesReady() == _cache.framesCount()) {
_direct.unload();
}
if (my) {
_cache.keepUpContext(my->context);
}
return true;
}
} // namespace Lottie

View File

@@ -0,0 +1,52 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "lottie/details/lottie_frame_provider_direct.h"
#include "lottie/details/lottie_cache.h"
namespace Lottie {
struct FrameProviderCachedToken : FrameProviderToken {
CacheReadContext context;
};
class FrameProviderCached final : public FrameProvider {
public:
FrameProviderCached(
const QByteArray &content,
FnMut<void(QByteArray &&cached)> put,
const QByteArray &cached,
const FrameRequest &request,
Quality quality,
const ColorReplacements *replacements);
QImage construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) override;
const Information &information() override;
bool valid() override;
int sizeRounding() override;
std::unique_ptr<FrameProviderToken> createToken() override;
bool render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) override;
private:
Cache _cache;
FrameProviderDirect _direct;
const QByteArray _content;
const ColorReplacements *_replacements = nullptr;
};
} // namespace Lottie

View File

@@ -0,0 +1,234 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_frame_provider_cached_multi.h"
#include "base/assertion.h"
#include <range/v3/numeric/accumulate.hpp>
namespace Lottie {
FrameProviderCachedMulti::FrameProviderCachedMulti(
const QByteArray &content,
FnMut<void(int index, QByteArray &&cached)> put,
std::vector<QByteArray> caches,
const FrameRequest &request,
Quality quality,
const ColorReplacements *replacements)
: _content(content)
, _replacements(replacements)
, _put(std::move(put))
, _direct(quality) {
Expects(!caches.empty());
_caches.reserve(caches.size());
const auto emplace = [&](const QByteArray &cached) {
const auto index = int(_caches.size());
_caches.emplace_back(cached, request, [=](QByteArray &&v) {
// We capture reference to _put, so the provider is not movable.
_put(index, std::move(v));
});
};
const auto load = [&] {
if (_direct.loaded() || _direct.load(content, replacements)) {
return true;
}
_caches.clear();
return false;
};
const auto fill = [&] {
if (!load()) {
return false;
}
while (_caches.size() < caches.size()) {
emplace({});
}
return true;
};
for (const auto &cached : caches) {
emplace(cached);
auto &cache = _caches.back();
const auto &first = _caches.front();
Assert(cache.sizeRounding() == first.sizeRounding());
if (!cache.framesCount()) {
if (!fill()) {
return;
}
break;
} else if (cache.framesReady() < cache.framesCount() && !load()) {
return;
} else if (cache.frameRate() != first.frameRate()
|| cache.originalSize() != first.originalSize()) {
_caches.pop_back();
if (!fill()) {
return;
}
break;
}
}
if (!_direct.loaded()) {
_direct.setInformation({
.size = _caches.front().originalSize(),
.frameRate = _caches.front().frameRate(),
.framesCount = ranges::accumulate(
_caches,
0,
std::plus<>(),
&Cache::framesCount),
});
}
if (!validateFramesPerCache() && _framesPerCache > 0) {
fill();
}
}
bool FrameProviderCachedMulti::validateFramesPerCache() {
const auto &info = information();
const auto count = int(_caches.size());
_framesPerCache = (info.framesCount + count - 1) / count;
if (!_framesPerCache
|| (info.framesCount <= (count - 1) * _framesPerCache)) {
_framesPerCache = 0;
return false;
}
for (auto i = 0; i != count; ++i) {
const auto cacheFramesCount = _caches[i].framesCount();
if (!cacheFramesCount) {
break;
}
const auto shouldBe = (i + 1 == count
? (info.framesCount - (count - 1) * _framesPerCache)
: _framesPerCache);
if (cacheFramesCount != shouldBe) {
_caches.erase(begin(_caches) + i, end(_caches));
return false;
}
}
return true;
}
QImage FrameProviderCachedMulti::construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) {
if (!_framesPerCache) {
if (token) {
token->result = FrameRenderResult::Failed;
}
return QImage();
}
auto cover = QImage();
using Token = FrameProviderCachedMultiToken;
const auto my = static_cast<Token*>(token.get());
if (!my || my->exclusive) {
const auto &info = information();
const auto count = int(_caches.size());
for (auto i = 0; i != count; ++i) {
auto cacheCover = _caches[i].takeFirstFrame();
if (cacheCover.isNull()) {
_caches[i].init(
info.size,
info.frameRate,
(i + 1 == count
? (info.framesCount - (count - 1) * _framesPerCache)
: _framesPerCache),
request);
} else if (!i) {
cover = std::move(cacheCover);
}
}
if (!cover.isNull()) {
if (my) {
_caches[0].keepUpContext(my->context);
}
return cover;
}
}
render(token, cover, request, 0);
return cover;
}
const Information &FrameProviderCachedMulti::information() {
return _direct.information();
}
bool FrameProviderCachedMulti::valid() {
return _direct.valid() && (_framesPerCache > 0);
}
int FrameProviderCachedMulti::sizeRounding() {
return _caches.front().sizeRounding();
}
std::unique_ptr<FrameProviderToken> FrameProviderCachedMulti::createToken() {
auto result = std::make_unique<FrameProviderCachedMultiToken>();
if (!_caches.empty()) {
_caches.front().prepareBuffers(result->context);
}
return result;
}
bool FrameProviderCachedMulti::render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) {
if (!valid()) {
if (token) {
token->result = FrameRenderResult::Failed;
}
return false;
}
const auto original = information().size;
const auto size = request.box.isEmpty()
? original
: request.size(original, sizeRounding());
if (!GoodStorageForFrame(to, size)) {
to = CreateFrameStorage(size);
}
const auto cacheIndex = index / _framesPerCache;
const auto indexInCache = index % _framesPerCache;
Assert(cacheIndex < _caches.size());
auto &cache = _caches[cacheIndex];
using Token = FrameProviderCachedMultiToken;
const auto my = static_cast<Token*>(token.get());
if (my && !my->exclusive) {
// Many threads may get here simultaneously.
my->result = cache.renderFrame(
my->context,
to,
request,
indexInCache);
return (my->result == FrameRenderResult::Ok);
}
const auto result = cache.renderFrame(to, request, indexInCache);
if (result == FrameRenderResult::Ok) {
if (my) {
cache.keepUpContext(my->context);
}
return true;
} else if (result == FrameRenderResult::Failed
// We don't support changing size on the fly for shared providers.
|| (result == FrameRenderResult::BadCacheSize && my)
|| (!_direct.loaded() && !_direct.load(_content, _replacements))) {
_direct.setInformation({});
return false;
}
_direct.renderToPrepared(to, index);
cache.appendFrame(to, request, indexInCache);
if (cache.framesReady() == cache.framesCount()
&& cacheIndex + 1 == _caches.size()) {
_direct.unload();
}
if (my) {
cache.keepUpContext(my->context);
}
return true;
}
} // namespace Lottie

View File

@@ -0,0 +1,60 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "lottie/details/lottie_frame_provider_direct.h"
#include "lottie/details/lottie_cache.h"
namespace Lottie {
struct FrameProviderCachedMultiToken : FrameProviderToken {
CacheReadContext context;
};
class FrameProviderCachedMulti final : public FrameProvider {
public:
FrameProviderCachedMulti(
const QByteArray &content,
FnMut<void(int index, QByteArray &&cached)> put,
std::vector<QByteArray> caches,
const FrameRequest &request,
Quality quality,
const ColorReplacements *replacements);
FrameProviderCachedMulti(const FrameProviderCachedMulti &) = delete;
FrameProviderCachedMulti &operator=(const FrameProviderCachedMulti &)
= delete;
QImage construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) override;
const Information &information() override;
bool valid() override;
int sizeRounding() override;
std::unique_ptr<FrameProviderToken> createToken() override;
bool render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) override;
private:
bool validateFramesPerCache();
const QByteArray _content;
const ColorReplacements *_replacements = nullptr;
FnMut<void(int index, QByteArray &&cached)> _put;
FrameProviderDirect _direct;
std::vector<Cache> _caches;
int _framesPerCache = 0;
};
} // namespace Lottie

View File

@@ -0,0 +1,171 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_frame_provider_direct.h"
#include "lottie/lottie_wrap.h"
#include "lottie/details/lottie_frame_renderer.h"
#include "ui/image/image_prepare.h"
#include <rlottie.h>
namespace Lottie {
namespace {
int GetLottieFrameRate(not_null<rlottie::Animation*> animation, Quality quality) {
const auto rate = int(qRound(animation->frameRate()));
return (quality == Quality::Default && rate == 60) ? (rate / 2) : rate;
}
int GetLottieFramesCount(not_null<rlottie::Animation*> animation, Quality quality) {
const auto rate = int(qRound(animation->frameRate()));
const auto count = int(animation->totalFrame());
return (quality == Quality::Default && rate == 60)
? ((count + 1) / 2)
: count;
}
int GetLottieFrameIndex(not_null<rlottie::Animation*> animation, Quality quality, int index) {
const auto rate = int(qRound(animation->frameRate()));
return (quality == Quality::Default && rate == 60) ? (index * 2) : index;
}
[[nodiscard]] rlottie::FitzModifier MapModifier(SkinModifier modifier) {
using Result = rlottie::FitzModifier;
switch (modifier) {
case SkinModifier::None: return Result::None;
case SkinModifier::Color1: return Result::Type12;
case SkinModifier::Color2: return Result::Type3;
case SkinModifier::Color3: return Result::Type4;
case SkinModifier::Color4: return Result::Type5;
case SkinModifier::Color5: return Result::Type6;
}
Unexpected("Unexpected modifier in MapModifier.");
}
} // namespace
FrameProviderDirect::FrameProviderDirect(Quality quality)
: _quality(quality) {
}
FrameProviderDirect::~FrameProviderDirect() = default;
bool FrameProviderDirect::load(
const QByteArray &content,
const ColorReplacements *replacements) {
_information = Information();
const auto string = ReadUtf8(Images::UnpackGzip(content));
if (string.size() > kMaxFileSize) {
return false;
}
_animation = LoadAnimationFromData(
string,
std::string(),
std::string(),
false,
(replacements
? replacements->replacements
: std::vector<std::pair<std::uint32_t, std::uint32_t>>()),
(replacements
? MapModifier(replacements->modifier)
: rlottie::FitzModifier::None));
if (!_animation) {
return false;
}
auto width = size_t(0);
auto height = size_t(0);
_animation->size(width, height);
const auto rate = GetLottieFrameRate(_animation.get(), _quality);
const auto count = GetLottieFramesCount(_animation.get(), _quality);
return setInformation({
.size = QSize(int(width), int(height)),
.frameRate = int(rate),
.framesCount = int(count),
});
}
bool FrameProviderDirect::loaded() const {
return (_animation != nullptr);
}
void FrameProviderDirect::unload() {
_animation = nullptr;
}
bool FrameProviderDirect::setInformation(Information information) {
if (information.size.isEmpty()
|| information.size.width() > kMaxSize
|| information.size.height() > kMaxSize
|| !information.frameRate
|| information.frameRate > kMaxFrameRate
|| !information.framesCount
|| information.framesCount > kMaxFramesCount) {
return false;
}
_information = information;
return true;
}
const Information &FrameProviderDirect::information() {
return _information;
}
bool FrameProviderDirect::valid() {
return _information.framesCount > 0;
}
QImage FrameProviderDirect::construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) {
auto cover = QImage();
render(token, cover, request, 0);
return cover;
}
int FrameProviderDirect::sizeRounding() {
return 2;
}
bool FrameProviderDirect::render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) {
if (token && !token->exclusive) {
token->result = FrameRenderResult::NotReady;
return false;
} else if (!valid()) {
return false;
}
const auto original = information().size;
const auto size = request.box.isEmpty()
? original
: request.size(original, sizeRounding());
if (!GoodStorageForFrame(to, size)) {
to = CreateFrameStorage(size);
}
renderToPrepared(to, index);
return true;
}
void FrameProviderDirect::renderToPrepared(
QImage &to,
int index) const {
to.fill(Qt::transparent);
auto surface = rlottie::Surface(
reinterpret_cast<uint32_t*>(to.bits()),
to.width(),
to.height(),
to.bytesPerLine());
_animation->renderSync(
GetLottieFrameIndex(_animation.get(), _quality, index),
surface);
}
} // namespace Lottie

View File

@@ -0,0 +1,57 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "lottie/details/lottie_frame_provider.h"
#include "lottie/lottie_common.h"
namespace rlottie {
class Animation;
} // namespace rlottie
namespace Lottie {
class FrameProviderDirect final : public FrameProvider {
public:
explicit FrameProviderDirect(Quality quality);
~FrameProviderDirect();
bool load(
const QByteArray &content,
const ColorReplacements *replacements);
[[nodiscard]] bool loaded() const;
void unload();
bool setInformation(Information information);
QImage construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) override;
const Information &information() override;
bool valid() override;
int sizeRounding() override;
bool render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) override;
void renderToPrepared(QImage &to, int index) const;
private:
const FrameProviderDirect *cthis() const {
return this;
}
std::unique_ptr<rlottie::Animation> _animation;
Information _information;
Quality _quality = Quality::Default;
};
} // namespace Lottie

View File

@@ -0,0 +1,103 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_frame_provider_shared.h"
#include "base/assertion.h"
#include <crl/crl_on_main.h>
namespace Lottie {
FrameProviderShared::FrameProviderShared(
FnMut<void(FnMut<void(std::unique_ptr<FrameProvider>)>)> factory) {
_mutex.lockForWrite();
factory(crl::guard(this, [=](std::unique_ptr<FrameProvider> shared) {
_shared = std::move(shared);
_mutex.unlock();
}));
}
QImage FrameProviderShared::construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) {
QWriteLocker lock(&_mutex);
token = createToken();
if (token) {
token->exclusive = !_constructed;
}
auto result = _shared->construct(token, request);
_constructed = true;
return result;
}
const Information &FrameProviderShared::information() {
static auto empty = Information();
QReadLocker lock(&_mutex);
return _shared ? _shared->information() : empty;
}
bool FrameProviderShared::valid() {
QReadLocker lock(&_mutex);
return _shared && _shared->valid();
}
int FrameProviderShared::sizeRounding() {
QReadLocker lock(&_mutex);
Assert(_shared != nullptr);
return _shared->sizeRounding();
}
std::unique_ptr<FrameProviderToken> FrameProviderShared::createToken() {
Expects(_shared != nullptr);
return _shared->createToken();
}
bool FrameProviderShared::render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) {
QReadLocker readLock(&_mutex);
if (!_shared) {
if (token) {
token->result = FrameRenderResult::Failed;
}
return false;
}
if (token) {
token->exclusive = false;
_shared->render(token, to, request, index);
if (token->result == FrameRenderResult::Ok) {
return true;
}
}
readLock.unlock();
QWriteLocker lock(&_mutex);
if (!_shared) {
if (token) {
token->result = FrameRenderResult::Failed;
}
return false;
}
if (token) {
_shared->render(token, to, request, index);
if (token->result == FrameRenderResult::Ok) {
return true;
} else if (token->result == FrameRenderResult::Failed) {
_shared = nullptr;
return false;
}
token->exclusive = true;
}
return _shared->render(token, to, request, index);
}
} // namespace Lottie

View File

@@ -0,0 +1,46 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "lottie/details/lottie_frame_provider.h"
#include "base/weak_ptr.h"
#include <QtCore/QReadWriteLock>
namespace Lottie {
class FrameProviderShared final
: public FrameProvider
, public base::has_weak_ptr {
public:
explicit FrameProviderShared(
FnMut<void(FnMut<void(std::unique_ptr<FrameProvider>)>)> factory);
QImage construct(
std::unique_ptr<FrameProviderToken> &token,
const FrameRequest &request) override;
const Information &information() override;
bool valid() override;
int sizeRounding() override;
std::unique_ptr<FrameProviderToken> createToken() override;
bool render(
const std::unique_ptr<FrameProviderToken> &token,
QImage &to,
const FrameRequest &request,
int index) override;
private:
std::unique_ptr<FrameProvider> _shared;
QReadWriteLock _mutex;
bool _constructed = false;
};
} // namespace Lottie

View File

@@ -0,0 +1,488 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "lottie/details/lottie_frame_renderer.h"
#include "lottie/lottie_player.h"
#include "lottie/lottie_animation.h"
#include "lottie/details/lottie_frame_provider.h"
#include "ui/image/image_prepare.h"
#include "base/flat_map.h"
#include "base/assertion.h"
#include <QPainter>
#include <rlottie.h>
#include <range/v3/algorithm/find.hpp>
#include <range/v3/algorithm/count_if.hpp>
namespace Lottie {
namespace {
std::weak_ptr<FrameRenderer> GlobalInstance;
} // namespace
class FrameRendererObject final {
public:
explicit FrameRendererObject(
crl::weak_on_queue<FrameRendererObject> weak);
void append(
std::unique_ptr<SharedState> entry,
const FrameRequest &request);
void frameShown();
void updateFrameRequest(
not_null<SharedState*> entry,
const FrameRequest &request);
void remove(not_null<SharedState*> entry);
private:
struct Entry {
std::unique_ptr<SharedState> state;
FrameRequest request;
};
static not_null<SharedState*> StateFromEntry(const Entry &entry) {
return entry.state.get();
}
void queueGenerateFrames();
void generateFrames();
crl::weak_on_queue<FrameRendererObject> _weak;
std::vector<Entry> _entries;
bool _queued = false;
};
[[nodiscard]] bool GoodForRequest(
const QImage &image,
const FrameRequest &request) {
if (request.box.isEmpty()) {
return true;
} else if (request.colored.alpha() != 0 || request.mirrorHorizontal) {
return false;
}
const auto size = image.size();
return (request.box.width() == size.width())
|| (request.box.height() == size.height());
}
[[nodiscard]] QImage PrepareByRequest(
const QImage &original,
const FrameRequest &request,
int sizeRounding,
QImage storage) {
Expects(!request.box.isEmpty());
const auto size = request.size(
original.size(),
sizeRounding);
if (!GoodStorageForFrame(storage, size)) {
storage = CreateFrameStorage(size);
}
storage.fill(Qt::transparent);
{
QPainter p(&storage);
p.setRenderHint(QPainter::Antialiasing);
p.setRenderHint(QPainter::SmoothPixmapTransform);
p.drawImage(QRect(QPoint(), size), original);
}
if (request.mirrorHorizontal) {
storage = std::move(storage).mirrored(true, false);
}
if (request.colored.alpha() != 0) {
storage = Images::Colored(std::move(storage), request.colored);
}
return storage;
}
QImage PrepareFrameByRequest(
not_null<Frame*> 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,
frame->sizeRounding,
std::move(frame->prepared));
}
return frame->prepared;
}
FrameRendererObject::FrameRendererObject(
crl::weak_on_queue<FrameRendererObject> weak)
: _weak(std::move(weak)) {
}
void FrameRendererObject::append(
std::unique_ptr<SharedState> state,
const FrameRequest &request) {
_entries.push_back({ std::move(state), request });
queueGenerateFrames();
}
void FrameRendererObject::frameShown() {
queueGenerateFrames();
}
void FrameRendererObject::updateFrameRequest(
not_null<SharedState*> entry,
const FrameRequest &request) {
const auto i = ranges::find(_entries, entry, &StateFromEntry);
Assert(i != end(_entries));
i->request = request;
}
void FrameRendererObject::remove(not_null<SharedState*> entry) {
const auto i = ranges::find(_entries, entry, &StateFromEntry);
Assert(i != end(_entries));
_entries.erase(i);
}
void FrameRendererObject::generateFrames() {
auto players = base::flat_map<Player*, base::weak_ptr<Player>>();
const auto renderOne = [&](const Entry &entry) {
const auto result = entry.state->renderNextFrame(entry.request);
if (const auto player = result.notify.get()) {
players.emplace(player, result.notify);
}
return result.rendered;
};
const auto rendered = ranges::count_if(_entries, renderOne);
if (rendered) {
if (!players.empty()) {
crl::on_main([players = std::move(players)] {
for (const auto &[player, weak] : players) {
if (weak) {
weak->checkStep();
}
}
});
}
queueGenerateFrames();
}
}
void FrameRendererObject::queueGenerateFrames() {
if (_queued) {
return;
}
_queued = true;
_weak.with([](FrameRendererObject &that) {
that._queued = false;
that.generateFrames();
});
}
SharedState::SharedState(
std::shared_ptr<FrameProvider> provider,
const FrameRequest &request)
: _provider(std::move(provider)) {
if (_provider->valid()) {
init(_provider->construct(_token, request), request);
}
}
int SharedState::sizeRounding() const {
return _provider->sizeRounding();
}
void SharedState::init(QImage cover, const FrameRequest &request) {
Expects(!initialized());
_frames[0].request = request;
_frames[0].sizeRounding = sizeRounding();
_frames[0].original = std::move(cover);
_framesCount = _provider->information().framesCount;
}
void SharedState::start(
not_null<Player*> owner,
crl::time started,
crl::time delay,
int skippedFrames) {
_owner = owner;
_started = started;
_delay = delay;
_skippedFrames = skippedFrames;
_counter.store(0, std::memory_order_release);
}
bool IsRendered(not_null<const Frame*> frame) {
return (frame->displayed == kTimeUnknown);
}
void SharedState::renderNextFrame(
not_null<Frame*> frame,
const FrameRequest &request) {
if (!_framesCount) {
return;
}
const auto rendered = _provider->render(
_token,
frame->original,
request,
(++_frameIndex) % _framesCount);
if (!rendered) {
return;
}
frame->request = request;
frame->sizeRounding = sizeRounding();
PrepareFrameByRequest(frame);
frame->index = _frameIndex;
frame->displayed = kTimeUnknown;
}
auto SharedState::renderNextFrame(const FrameRequest &request)
-> RenderResult {
const auto prerender = [&](int index) -> RenderResult {
const auto frame = getFrame(index);
const auto next = getFrame((index + 1) % kFramesCount);
if (!IsRendered(frame)) {
renderNextFrame(frame, request);
return { IsRendered(frame) };
} else if (!IsRendered(next)) {
renderNextFrame(next, request);
return { IsRendered(next) };
}
return { false };
};
const auto present = [&](int counter, int index) -> RenderResult {
const auto frame = getFrame(index);
if (!IsRendered(frame)) {
renderNextFrame(frame, request);
if (!IsRendered(frame)) {
return { false };
}
}
frame->display = countFrameDisplayTime(frame->index);
// Release this frame to the main thread for rendering.
_counter.store(
(counter + 1) % (2 * kFramesCount),
std::memory_order_release);
return { true, _owner };
};
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 Lottie::SharedState::renderNextFrame.");
}
crl::time SharedState::countFrameDisplayTime(int index) const {
const auto rate = _provider->information().frameRate;
return _started
+ _delay
+ crl::time(1000) * (_skippedFrames + index) / rate;
}
int SharedState::counter() const {
return _counter.load(std::memory_order_acquire);
}
bool SharedState::initialized() const {
return (counter() != kCounterUninitialized);
}
not_null<Frame*> SharedState::getFrame(int index) {
Expects(index >= 0 && index < kFramesCount);
return &_frames[index];
}
not_null<const Frame*> SharedState::getFrame(int index) const {
Expects(index >= 0 && index < kFramesCount);
return &_frames[index];
}
Information SharedState::information() const {
return _provider->information();
}
not_null<Frame*> SharedState::frameForPaint() {
const auto result = getFrame(counter() / 2);
Assert(!result->original.isNull());
Assert(result->displayed != kTimeUnknown);
return result;
}
int SharedState::framesCount() const {
return _framesCount;
}
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);
if (frame->displayed != kTimeUnknown) {
// Frame already displayed, but not yet shown.
return kFrameDisplayTimeAlreadyDone;
}
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.");
}
void SharedState::addTimelineDelay(crl::time delayed, int skippedFrames) {
if (!delayed && !skippedFrames) {
return;
}
const auto recountCurrentFrame = [&](int counter) {
_delay += delayed;
_skippedFrames += skippedFrames;
const auto next = (counter + 1) % (2 * kFramesCount);
const auto index = next / 2;
const auto frame = getFrame(index);
if (frame->displayed != kTimeUnknown) {
// Frame already displayed.
return;
}
Assert(IsRendered(frame));
Assert(frame->display != kTimeUnknown);
frame->display = countFrameDisplayTime(frame->index);
};
switch (counter()) {
case 0: Unexpected("Value 0 in SharedState::addTimelineDelay.");
case 1: return recountCurrentFrame(1);
case 2: Unexpected("Value 2 in SharedState::addTimelineDelay.");
case 3: return recountCurrentFrame(3);
case 4: Unexpected("Value 4 in SharedState::addTimelineDelay.");
case 5: return recountCurrentFrame(5);
case 6: Unexpected("Value 6 in SharedState::addTimelineDelay.");
case 7: return recountCurrentFrame(7);
}
Unexpected("Counter value in VideoTrack::Shared::nextFrameDisplayTime.");
}
void 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);
if (frame->displayed == kTimeUnknown) {
frame->displayed = now;
}
};
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.");
}
bool SharedState::markFrameShown() {
const auto jump = [&](int counter) {
const auto next = (counter + 1) % (2 * kFramesCount);
const auto index = next / 2;
const auto frame = getFrame(index);
if (frame->displayed == kTimeUnknown) {
return false;
}
_counter.store(
next,
std::memory_order_release);
return true;
};
switch (counter()) {
case 0: return false;
case 1: return jump(1);
case 2: return false;
case 3: return jump(3);
case 4: return false;
case 5: return jump(5);
case 6: return false;
case 7: return jump(7);
}
Unexpected("Counter value in Lottie::SharedState::markFrameShown.");
}
SharedState::~SharedState() = default;
std::shared_ptr<FrameRenderer> FrameRenderer::CreateIndependent() {
return std::make_shared<FrameRenderer>();
}
std::shared_ptr<FrameRenderer> FrameRenderer::Instance() {
if (auto result = GlobalInstance.lock()) {
return result;
}
auto result = CreateIndependent();
GlobalInstance = result;
return result;
}
void FrameRenderer::append(
std::unique_ptr<SharedState> entry,
const FrameRequest &request) {
_wrapped.with([=, entry = std::move(entry)](
FrameRendererObject &unwrapped) mutable {
unwrapped.append(std::move(entry), request);
});
}
void FrameRenderer::frameShown() {
_wrapped.with([=](FrameRendererObject &unwrapped) {
unwrapped.frameShown();
});
}
void FrameRenderer::updateFrameRequest(
not_null<SharedState*> entry,
const FrameRequest &request) {
_wrapped.with([=](FrameRendererObject &unwrapped) {
unwrapped.updateFrameRequest(entry, request);
});
}
void FrameRenderer::remove(not_null<SharedState*> entry) {
_wrapped.with([=](FrameRendererObject &unwrapped) {
unwrapped.remove(entry);
});
}
} // namespace Lottie

View File

@@ -0,0 +1,136 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/basic_types.h"
#include "base/weak_ptr.h"
#include "lottie/lottie_common.h"
#include <QImage>
#include <QSize>
#include <crl/crl_time.h>
#include <crl/crl_object_on_queue.h>
#include <limits>
namespace Lottie {
// Frame rate can be 1, 2, ... , 29, 30 or 60.
inline constexpr auto kNormalFrameRate = 30;
inline constexpr auto kMaxFrameRate = 60;
inline constexpr auto kMaxSize = 4096;
inline constexpr auto kMaxFramesCount = 210;
inline constexpr auto kFrameDisplayTimeAlreadyDone
= std::numeric_limits<crl::time>::max();
inline constexpr auto kDisplayedInitial = crl::time(-1);
class Player;
class FrameProvider;
struct FrameProviderToken;
struct Frame {
QImage original;
crl::time displayed = kDisplayedInitial;
crl::time display = kTimeUnknown;
int index = 0;
int sizeRounding = 0;
FrameRequest request;
QImage prepared;
};
QImage PrepareFrameByRequest(
not_null<Frame*> frame,
bool useExistingPrepared);
class SharedState {
public:
SharedState(
std::shared_ptr<FrameProvider> provider,
const FrameRequest &request);
void start(
not_null<Player*> owner,
crl::time now,
crl::time delay = 0,
int skippedFrames = 0);
[[nodiscard]] Information information() const;
[[nodiscard]] bool initialized() const;
[[nodiscard]] not_null<Frame*> frameForPaint();
[[nodiscard]] int framesCount() const;
[[nodiscard]] crl::time nextFrameDisplayTime() const;
void addTimelineDelay(crl::time delayed, int skippedFrames = 0);
void markFrameDisplayed(crl::time now);
bool markFrameShown();
struct RenderResult {
bool rendered = false;
base::weak_ptr<Player> notify;
};
[[nodiscard]] RenderResult renderNextFrame(const FrameRequest &request);
~SharedState();
private:
void init(QImage cover, const FrameRequest &request);
void renderNextFrame(
not_null<Frame*> frame,
const FrameRequest &request);
[[nodiscard]] int sizeRounding() const;
[[nodiscard]] crl::time countFrameDisplayTime(int index) const;
[[nodiscard]] not_null<Frame*> getFrame(int index);
[[nodiscard]] not_null<const Frame*> getFrame(int index) const;
[[nodiscard]] int counter() const;
// crl::queue changes 0,2,4,6 to 1,3,5,7.
// main thread changes 1,3,5,7 to 2,4,6,0.
static constexpr auto kCounterUninitialized = -1;
std::atomic<int> _counter = kCounterUninitialized;
static constexpr auto kFramesCount = 4;
std::array<Frame, kFramesCount> _frames;
base::weak_ptr<Player> _owner;
crl::time _started = kTimeUnknown;
// (_counter % 2) == 1 main thread can write _delay.
// (_counter % 2) == 0 crl::queue can read _delay.
crl::time _delay = kTimeUnknown;
int _frameIndex = 0;
int _framesCount = 0;
int _skippedFrames = 0;
const std::shared_ptr<FrameProvider> _provider;
std::unique_ptr<FrameProviderToken> _token;
};
class FrameRendererObject;
class FrameRenderer final {
public:
static std::shared_ptr<FrameRenderer> CreateIndependent();
static std::shared_ptr<FrameRenderer> Instance();
void append(
std::unique_ptr<SharedState> entry,
const FrameRequest &request);
void updateFrameRequest(
not_null<SharedState*> entry,
const FrameRequest &request);
void frameShown();
void remove(not_null<SharedState*> state);
private:
using Implementation = FrameRendererObject;
crl::object_on_queue<Implementation> _wrapped;
};
} // namespace Lottie