From b5b772a41b556458f8eb481dddb3df329dbca641 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 14 Jun 2024 11:49:48 +0400 Subject: [PATCH] Lottie updates --- .../SoftwareLottieRenderer.h | 2 +- .../Sources/CoreGraphicsCanvasImpl.h | 13 +- .../Sources/CoreGraphicsCanvasImpl.mm | 172 +++++++++++------- .../Sources/SkiaCanvasImpl.cpp | 82 ++++----- .../Sources/SkiaCanvasImpl.h | 12 +- .../Sources/SoftwareLottieRenderer.mm | 112 ++++-------- .../Sources/ThorVGCanvasImpl.cpp | 55 +----- .../Sources/ThorVGCanvasImpl.h | 9 +- .../Sources/CompareToReferenceRendering.swift | 109 +++++++---- .../Sources/ViewController.swift | 158 +++++++++++++++- submodules/LottieCpp/lottiecpp | 2 +- 11 files changed, 416 insertions(+), 310 deletions(-) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h index acdb6cbdfe..a2f624d816 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h @@ -19,7 +19,7 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path); - (instancetype _Nullable)initWithData:(NSData * _Nonnull)data; - (void)setFrame:(NSInteger)index; -- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering; +- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering canUseMoreMemory:(bool)canUseMoreMemory skipImageGeneration:(bool)skipImageGeneration; @end diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h index 0c34cd3c97..571f5199d4 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h @@ -23,30 +23,25 @@ public: public: CoreGraphicsCanvasImpl(int width, int height); - CoreGraphicsCanvasImpl(CGContextRef context, int width, int height); virtual ~CoreGraphicsCanvasImpl(); - std::shared_ptr makeLayer(int width, int height) override; - virtual void saveState() override; virtual void restoreState() override; virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override; virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; + virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) override; virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) override; virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override; - virtual void setBlendMode(BlendMode blendMode) override; + virtual void clip(CGRect const &rect) override; virtual void concatenate(lottie::Transform2D const &transform) override; virtual std::shared_ptr makeImage(); - virtual void draw(std::shared_ptr const &other, float alpha, lottie::CGRect const &rect) override; - virtual void pushLayer(CGRect const &rect) override; + virtual bool pushLayer(CGRect const &rect, float alpha, std::optional maskMode) override; virtual void popLayer() override; std::vector &backingData(); @@ -56,6 +51,8 @@ private: std::shared_ptr ¤tLayer(); private: + int _width = 0; + int _height = 0; CGContextRef _topContext = nil; std::vector> _layerStack; }; diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm index 4ffa901d3c..96288db99d 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm @@ -64,9 +64,23 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera class CoreGraphicsCanvasImpl::Layer { public: - explicit Layer(int width, int height) { + struct Composition { + CGRect rect; + float alpha; + Transform2D transform; + std::optional maskMode; + + Composition(CGRect rect_, float alpha_, Transform2D transform_, std::optional maskMode_) : + rect(rect_), alpha(alpha_), transform(transform_), maskMode(maskMode_) { + } + }; + +public: + explicit Layer(int width, int height, std::optional composition) { _width = width; _height = height; + _composition = composition; + _bytesPerRow = alignUp(width * 4, 16); _backingData.resize(_bytesPerRow * _height); memset(_backingData.data(), 0, _backingData.size()); @@ -77,7 +91,6 @@ public: CFRelease(colorSpace); CGContextClearRect(_context, CGRectMake(0.0, 0.0, _width, _height)); - } ~Layer() { @@ -88,12 +101,29 @@ public: return _context; } + std::optional composition() const { + return _composition; + } + + std::shared_ptr makeImage() { + ::CGImageRef nativeImage = CGBitmapContextCreateImage(_context); + if (nativeImage) { + auto image = std::make_shared(nativeImage); + CFRelease(nativeImage); + return image; + } else { + return nil; + } + } + public: CGContextRef _context = nil; int _width = 0; int _height = 0; int _bytesPerRow = 0; std::vector _backingData; + + std::optional _composition; }; CoreGraphicsCanvasImpl::Image::Image(::CGImageRef image) { @@ -108,24 +138,13 @@ CoreGraphicsCanvasImpl::Image::~Image() { return _image; } -CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(int width, int height) { - _layerStack.push_back(std::make_shared(width, height)); - _topContext = CGContextRetain(currentLayer()->context()); -} - -CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(CGContextRef context, int width, int height) { - _layerStack.push_back(std::make_shared(width, height)); - _topContext = CGContextRetain(context); +CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(int width, int height) : +_width(width), +_height(height) { + _layerStack.push_back(std::make_shared(width, height, std::nullopt)); } CoreGraphicsCanvasImpl::~CoreGraphicsCanvasImpl() { - if (_topContext) { - CFRelease(_topContext); - } -} - -std::shared_ptr CoreGraphicsCanvasImpl::makeLayer(int width, int height) { - return std::make_shared(_topContext, width, height); } void CoreGraphicsCanvasImpl::saveState() { @@ -142,7 +161,7 @@ void CoreGraphicsCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, } CGFloat components[4] = { color.r, color.g, color.b, color.a }; - CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(currentLayer()->context()), components); CGContextSetFillColorWithColor(currentLayer()->context(), nativeColor); CFRelease(nativeColor); @@ -194,7 +213,7 @@ void CoreGraphicsCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const & locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { CGContextDrawLinearGradient(currentLayer()->context(), nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); @@ -204,7 +223,7 @@ void CoreGraphicsCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const & CGContextRestoreGState(currentLayer()->context()); } -void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { +void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) { CGContextSaveGState(currentLayer()->context()); if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { @@ -240,9 +259,9 @@ void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const & locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { - CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(center.x, center.y), 0.0, CGPointMake(center.x, center.y), radius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); } @@ -256,7 +275,7 @@ void CoreGraphicsCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePat } CGFloat components[4] = { color.r, color.g, color.b, color.a }; - CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(currentLayer()->context()), components); CGContextSetStrokeColorWithColor(currentLayer()->context(), nativeColor); CFRelease(nativeColor); @@ -385,7 +404,7 @@ void CoreGraphicsCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { CGContextDrawLinearGradient(currentLayer()->context(), nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); @@ -470,7 +489,7 @@ void CoreGraphicsCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); @@ -480,32 +499,8 @@ void CoreGraphicsCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const CGContextRestoreGState(currentLayer()->context()); } -void CoreGraphicsCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) { - CGFloat components[4] = { fillColor.r, fillColor.g, fillColor.b, fillColor.a }; - CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); - CGContextSetFillColorWithColor(currentLayer()->context(), nativeColor); - CFRelease(nativeColor); - - CGContextFillRect(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height)); -} - -void CoreGraphicsCanvasImpl::setBlendMode(BlendMode blendMode) { - ::CGBlendMode nativeMode = kCGBlendModeNormal; - switch (blendMode) { - case BlendMode::Normal: { - nativeMode = kCGBlendModeNormal; - break; - } - case BlendMode::DestinationIn: { - nativeMode = kCGBlendModeDestinationIn; - break; - } - case BlendMode::DestinationOut: { - nativeMode = kCGBlendModeDestinationOut; - break; - } - } - CGContextSetBlendMode(currentLayer()->context(), nativeMode); +void CoreGraphicsCanvasImpl::clip(CGRect const &rect) { + CGContextClipToRect(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height)); } void CoreGraphicsCanvasImpl::concatenate(lottie::Transform2D const &transform) { @@ -513,29 +508,70 @@ void CoreGraphicsCanvasImpl::concatenate(lottie::Transform2D const &transform) { } std::shared_ptr CoreGraphicsCanvasImpl::makeImage() { - ::CGImageRef nativeImage = CGBitmapContextCreateImage(currentLayer()->context()); - if (nativeImage) { - auto image = std::make_shared(nativeImage); - CFRelease(nativeImage); - return image; + return currentLayer()->makeImage(); +} + +bool CoreGraphicsCanvasImpl::pushLayer(CGRect const &rect, float alpha, std::optional maskMode) { + auto currentTransform = fromNativeTransform(CATransform3DMakeAffineTransform(CGContextGetCTM(currentLayer()->context()))); + + CGRect globalRect(0.0f, 0.0f, 0.0f, 0.0f); + if (rect == CGRect::veryLarge()) { + globalRect = CGRect(0.0f, 0.0f, (float)_width, (float)_height); } else { - return nil; + CGRect transformedRect = rect.applyingTransform(currentTransform); + + CGRect integralTransformedRect( + std::floor(transformedRect.x), + std::floor(transformedRect.y), + std::ceil(transformedRect.width + transformedRect.x - floor(transformedRect.x)), + std::ceil(transformedRect.height + transformedRect.y - floor(transformedRect.y)) + ); + globalRect = integralTransformedRect.intersection(CGRect(0.0, 0.0, (CGFloat)_width, (CGFloat)_height)); } -} - -void CoreGraphicsCanvasImpl::draw(std::shared_ptr const &other, float alpha, lottie::CGRect const &rect) { - CGContextSetAlpha(currentLayer()->context(), alpha); - CoreGraphicsCanvasImpl *impl = (CoreGraphicsCanvasImpl *)other.get(); - auto image = impl->makeImage(); - CGContextDrawImage(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height), ((CoreGraphicsCanvasImpl::Image *)image.get())->nativeImage()); - CGContextSetAlpha(currentLayer()->context(), 1.0); -} - -void CoreGraphicsCanvasImpl::pushLayer(CGRect const &rect) { + if (globalRect.width <= 0.0f || globalRect.height <= 0.0f) { + return false; + } + + _layerStack.push_back(std::make_shared(globalRect.width, globalRect.height, Layer::Composition(globalRect, alpha, currentTransform, maskMode))); + concatenate(Transform2D::identity().translated(Vector2D(-globalRect.x, -globalRect.y))); + concatenate(currentTransform); + + return true; } void CoreGraphicsCanvasImpl::popLayer() { + auto layer = _layerStack[_layerStack.size() - 1]; + _layerStack.pop_back(); + if (const auto composition = layer->composition()) { + saveState(); + concatenate(composition->transform.inverted()); + + CGContextSetAlpha(currentLayer()->context(), composition->alpha); + + if (composition->maskMode) { + switch (composition->maskMode.value()) { + case Canvas::MaskMode::Normal: { + CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeDestinationIn); + break; + } + case Canvas::MaskMode::Inverse: { + CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeDestinationOut); + break; + } + default: { + break; + } + } + } + + auto image = layer->makeImage(); + CGContextDrawImage(currentLayer()->context(), CGRectMake(composition->rect.x, composition->rect.y, composition->rect.width, composition->rect.height), ((CoreGraphicsCanvasImpl::Image *)image.get())->nativeImage()); + CGContextSetAlpha(currentLayer()->context(), 1.0); + CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeNormal); + + restoreState(); + } } std::shared_ptr &CoreGraphicsCanvasImpl::currentLayer() { diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp index 02816e411e..d659821a79 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp @@ -17,6 +17,8 @@ #include "include/effects/SkDashPathEffect.h" #include "include/effects/SkGradientShader.h" +#include + namespace lottie { namespace { @@ -89,10 +91,6 @@ SkiaCanvasImpl::~SkiaCanvasImpl() { } } -std::shared_ptr SkiaCanvasImpl::makeLayer(int width, int height) { - return std::make_shared(width, height); -} - void SkiaCanvasImpl::saveState() { _canvas->save(); } @@ -105,7 +103,6 @@ void SkiaCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie: SkPaint paint; paint.setColor(skColor(color)); paint.setAntiAlias(true); - paint.setBlendMode(_blendMode); SkPath nativePath; skPath(enumeratePath, nativePath); @@ -117,7 +114,6 @@ void SkiaCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie: void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { SkPaint paint; paint.setAntiAlias(true); - paint.setBlendMode(_blendMode); paint.setDither(false); paint.setStyle(SkPaint::Style::kFill_Style); @@ -136,7 +132,7 @@ void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumerat locations.push_back(location); } - paint.setShader(SkGradientShader::MakeLinear(linearPoints, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kMirror)); + paint.setShader(SkGradientShader::MakeLinear(linearPoints, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kClamp)); SkPath nativePath; skPath(enumeratePath, nativePath); @@ -145,11 +141,9 @@ void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumerat _canvas->drawPath(nativePath, paint); } -void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { +void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) { SkPaint paint; paint.setAntiAlias(true); - paint.setBlendMode(_blendMode); - paint.setDither(false); paint.setStyle(SkPaint::Style::kFill_Style); std::vector colors; @@ -162,7 +156,7 @@ void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumerat locations.push_back(location); } - paint.setShader(SkGradientShader::MakeRadial(SkPoint::Make(startCenter.x, startCenter.y), endRadius, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kMirror)); + paint.setShader(SkGradientShader::MakeRadial(SkPoint::Make(center.x, center.y), radius, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kClamp)); SkPath nativePath; skPath(enumeratePath, nativePath); @@ -172,9 +166,11 @@ void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumerat } void SkiaCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) { + if (lineWidth <= FLT_EPSILON) { + return; + } SkPaint paint; paint.setAntiAlias(true); - paint.setBlendMode(_blendMode); paint.setColor(skColor(color)); paint.setStyle(SkPaint::Style::kStroke_Style); @@ -223,6 +219,9 @@ void SkiaCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float for (auto value : dashPattern) { intervals.push_back(value); } + if (intervals.size() == 1) { + intervals.push_back(intervals[0]); + } paint.setPathEffect(SkDashPathEffect::Make(intervals.data(), (int)intervals.size(), dashPhase)); } @@ -240,34 +239,8 @@ void SkiaCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumer assert(false); } -void SkiaCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) { - SkPaint paint; - paint.setAntiAlias(true); - paint.setColor(skColor(fillColor)); - paint.setBlendMode(_blendMode); - - _canvas->drawRect(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), paint); -} - -void SkiaCanvasImpl::setBlendMode(BlendMode blendMode) { - switch (blendMode) { - case BlendMode::Normal: { - _blendMode = SkBlendMode::kSrcOver; - break; - } - case BlendMode::DestinationIn: { - _blendMode = SkBlendMode::kDstIn; - break; - } - case BlendMode::DestinationOut: { - _blendMode = SkBlendMode::kDstOut; - break; - } - default: { - _blendMode = SkBlendMode::kSrcOver; - break; - } - } +void SkiaCanvasImpl::clip(CGRect const &rect) { + _canvas->clipRect(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), true); } void SkiaCanvasImpl::concatenate(lottie::Transform2D const &transform) { @@ -281,13 +254,32 @@ void SkiaCanvasImpl::concatenate(lottie::Transform2D const &transform) { _canvas->concat(matrix); } -void SkiaCanvasImpl::draw(std::shared_ptr const &other, float alpha, lottie::CGRect const &rect) { - SkiaCanvasImpl *impl = (SkiaCanvasImpl *)other.get(); - auto image = impl->surface()->makeImageSnapshot(); +bool SkiaCanvasImpl::pushLayer(CGRect const &rect, float alpha, std::optional maskMode) { SkPaint paint; - paint.setBlendMode(_blendMode); + paint.setAntiAlias(true); paint.setAlphaf(alpha); - _canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint); + if (maskMode) { + switch (maskMode.value()) { + case Canvas::MaskMode::Normal: { + paint.setBlendMode(SkBlendMode::kDstIn); + break; + } + case Canvas::MaskMode::Inverse: { + paint.setBlendMode(SkBlendMode::kDstOut); + break; + } + default: { + break; + } + } + } + + _canvas->saveLayer(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), &paint); + return true; +} + +void SkiaCanvasImpl::popLayer() { + _canvas->restore(); } void SkiaCanvasImpl::flush() { diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h index cccf8b9075..ea8ddec7d6 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h @@ -14,24 +14,21 @@ public: SkiaCanvasImpl(int width, int height, int bytesPerRow, void *pixelData); virtual ~SkiaCanvasImpl(); - virtual std::shared_ptr makeLayer(int width, int height) override; - virtual void saveState() override; virtual void restoreState() override; virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override; virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; + virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) override; virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) override; virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override; - - virtual void setBlendMode(BlendMode blendMode) override; + virtual void clip(CGRect const &rect) override; virtual void concatenate(lottie::Transform2D const &transform) override; - virtual void draw(std::shared_ptr const &other, float alpha, lottie::CGRect const &rect) override; + virtual bool pushLayer(CGRect const &rect, float alpha, std::optional maskMode) override; + virtual void popLayer() override; void flush(); sk_sp surface() const; @@ -41,7 +38,6 @@ private: bool _ownsPixelData = false; sk_sp _surface; SkCanvas *_canvas = nullptr; - SkBlendMode _blendMode = SkBlendMode::kSrcOver; }; } diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index e00ea55fa0..e8d108c44f 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -57,98 +57,62 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { _renderer->setFrame((int)index); } -- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering { +- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering canUseMoreMemory:(bool)canUseMoreMemory skipImageGeneration:(bool)skipImageGeneration { std::shared_ptr renderNode = _renderer->renderNode(); if (!renderNode) { return nil; } + lottie::CanvasRenderer::Configuration configuration; + configuration.canUseMoreMemory = canUseMoreMemory; + //configuration.canUseMoreMemory = true; + //configuration.disableGroupTransparency = true; + if (useReferenceRendering) { auto context = std::make_shared((int)size.width, (int)size.height); - _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height)); + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); auto image = context->makeImage(); return [[UIImage alloc] initWithCGImage:std::static_pointer_cast(image)->nativeImage()]; } else { if ((int64_t)"" > 0) { - /*auto surface = SkSurfaces::Raster(SkImageInfo::MakeN32Premul((int)size.width, (int)size.height)); - - int bytesPerRow = ((int)size.width) * 4; - void *pixelData = malloc(bytesPerRow * (int)size.height); - - sk_sp surface2 = SkSurfaces::WrapPixels( - SkImageInfo::MakeN32Premul((int)size.width, (int)size.height), - pixelData, - bytesPerRow, - nullptr - ); - - SkCanvas *canvas = surface->getCanvas(); - - SkPaint paint; - paint.setAntiAlias(true); - SkPath path; - path.moveTo(124, 108); - path.lineTo(172, 24); - path.addCircle(50, 50, 30); - path.moveTo(36, 148); - path.quadTo(66, 188, 120, 136); - canvas->drawPath(path, paint); - paint.setStyle(SkPaint::kStroke_Style); - paint.setColor(SK_ColorBLUE); - paint.setStrokeWidth(3); - canvas->drawPath(path, paint); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; - - CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo); - CGColorSpaceRelease(colorSpace); - - CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext); - UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored]; - CGImageRelease(bitmapImage); - - CGContextRelease(targetContext); - - free(pixelData); - - return image;*/ - int bytesPerRow = ((int)size.width) * 4; void *pixelData = malloc(bytesPerRow * (int)size.height); auto context = std::make_shared((int)size.width, (int)size.height, bytesPerRow, pixelData); - _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height)); - + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); context->flush(); - vImage_Buffer src; - src.data = (void *)pixelData; - src.width = (int)size.width; - src.height = (int)size.height; - src.rowBytes = bytesPerRow; - - uint8_t permuteMap[4] = {2, 1, 0, 3}; - vImagePermuteChannels_ARGB8888(&src, &src, permuteMap, kvImageDoNotTile); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; - - CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo); - CGColorSpaceRelease(colorSpace); - - CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext); - UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored]; - CGImageRelease(bitmapImage); - - CGContextRelease(targetContext); - - free(pixelData); - - return image; + if (skipImageGeneration) { + free(pixelData); + } else { + vImage_Buffer src; + src.data = (void *)pixelData; + src.width = (int)size.width; + src.height = (int)size.height; + src.rowBytes = bytesPerRow; + + uint8_t permuteMap[4] = {2, 1, 0, 3}; + vImagePermuteChannels_ARGB8888(&src, &src, permuteMap, kvImageDoNotTile); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + + CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo); + CGColorSpaceRelease(colorSpace); + + CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext); + UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored]; + CGImageRelease(bitmapImage); + + CGContextRelease(targetContext); + + free(pixelData); + + return image; + } } else if ((int64_t)"" < 0) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -158,7 +122,7 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { int bytesPerRow = ((int)size.width) * 4; auto context = std::make_shared((int)size.width, (int)size.height, bytesPerRow); - _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height)); + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); context->flush(); @@ -177,7 +141,7 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { return image; } else { auto context = std::make_shared((int)size.width, (int)size.height); - _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height)); + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); return nil; } diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.cpp b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.cpp index 6ff62ca987..30b64828b6 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.cpp +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.cpp @@ -63,10 +63,6 @@ _transform(lottie::Transform2D::identity()) { ThorVGCanvasImpl::~ThorVGCanvasImpl() { } -std::shared_ptr ThorVGCanvasImpl::makeLayer(int width, int height) { - return std::make_shared(width, height, width * 4); -} - void ThorVGCanvasImpl::saveState() { _stateStack.push_back(_transform); } @@ -120,14 +116,14 @@ void ThorVGCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumer _canvas->push(std::move(shape)); } -void ThorVGCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { +void ThorVGCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) { auto shape = tvg::Shape::gen(); tvgPath(enumeratePath, shape.get()); shape->transform(tvgTransform(_transform)); auto fill = tvg::RadialGradient::gen(); - fill->radial(startCenter.x, startCenter.y, endRadius); + fill->radial(center.x, center.y, radius); std::vector colors; for (size_t i = 0; i < gradient.colors().size(); i++) { @@ -214,55 +210,8 @@ void ThorVGCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enum void ThorVGCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { } -void ThorVGCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) { - auto shape = tvg::Shape::gen(); - shape->appendRect(rect.x, rect.y, rect.width, rect.height, 0.0f, 0.0f); - - shape->transform(tvgTransform(_transform)); - - shape->fill((int)(fillColor.r * 255.0), (int)(fillColor.g * 255.0), (int)(fillColor.b * 255.0), (int)(fillColor.a * 255.0)); - - _canvas->push(std::move(shape)); -} - -void ThorVGCanvasImpl::setBlendMode(BlendMode blendMode) { - /*switch (blendMode) { - case CGBlendMode::Normal: { - _blendMode = SkBlendMode::kSrcOver; - break; - } - case CGBlendMode::DestinationIn: { - _blendMode = SkBlendMode::kDstIn; - break; - } - case CGBlendMode::DestinationOut: { - _blendMode = SkBlendMode::kDstOut; - break; - } - default: { - _blendMode = SkBlendMode::kSrcOver; - break; - } - }*/ -} - void ThorVGCanvasImpl::concatenate(lottie::Transform2D const &transform) { _transform = transform * _transform; - /*_canvas->concat(SkM44( - transform.m11, transform.m21, transform.m31, transform.m41, - transform.m12, transform.m22, transform.m32, transform.m42, - transform.m13, transform.m23, transform.m33, transform.m43, - transform.m14, transform.m24, transform.m34, transform.m44 - ));*/ -} - -void ThorVGCanvasImpl::draw(std::shared_ptr const &other, float alpha, lottie::CGRect const &rect) { - /*ThorVGCanvasImpl *impl = (ThorVGCanvasImpl *)other.get(); - auto image = impl->surface()->makeImageSnapshot(); - SkPaint paint; - paint.setBlendMode(_blendMode); - paint.setAlphaf(_alpha); - _canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint);*/ } void ThorVGCanvasImpl::flush() { diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h index d1c83b6525..9b20b6344e 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h @@ -14,25 +14,18 @@ public: ThorVGCanvasImpl(int width, int height, int bytesPerRow); virtual ~ThorVGCanvasImpl(); - virtual std::shared_ptr makeLayer(int width, int height) override; - virtual void saveState() override; virtual void restoreState() override; virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override; virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; + virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) override; virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) override; virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override; - - virtual void setBlendMode(BlendMode blendMode) override; virtual void concatenate(lottie::Transform2D const &transform) override; - virtual void draw(std::shared_ptr const &other, float alpha, lottie::CGRect const &rect) override; - uint32_t *backingData() { return _backingData; } diff --git a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift index 2d7babbfed..d5c356726f 100644 --- a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift +++ b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift @@ -10,17 +10,26 @@ import SoftwareLottieRenderer import LottieSwift @available(iOS 13.0, *) -func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? { +func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage, allowedDifference: Double) -> (UIImage?, UIImage) { let lhsBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!) let rhsBuffer = try! vImage_Buffer(cgImage: rhs.cgImage!) + let deltaBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!) + defer { + lhsBuffer.free() + rhsBuffer.free() + deltaBuffer.free() + } - let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * 0.01)) + memset(deltaBuffer.data, 0, Int(deltaBuffer.height) * deltaBuffer.rowBytes) + + let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * allowedDifference)) var foundDifferenceCount = 0 outer: for y in 0 ..< Int(lhs.size.height) { let lhsRowPixels = lhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) let rhsRowPixels = rhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) + let deltaRowPixels = deltaBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) for x in 0 ..< Int(lhs.size.width) { let lhs0 = lhsRowPixels.advanced(by: x * 4 + 0).pointee @@ -36,32 +45,30 @@ func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? { let maxDiff = 25 if abs(Int(lhs0) - Int(rhs0)) > maxDiff || abs(Int(lhs1) - Int(rhs1)) > maxDiff || abs(Int(lhs2) - Int(rhs2)) > maxDiff || abs(Int(lhs3) - Int(rhs3)) > maxDiff { - /*if false { - lhsRowPixels.advanced(by: x * 4 + 0).pointee = 255 - lhsRowPixels.advanced(by: x * 4 + 1).pointee = 0 - lhsRowPixels.advanced(by: x * 4 + 2).pointee = 0 - lhsRowPixels.advanced(by: x * 4 + 3).pointee = 255 - }*/ + deltaRowPixels.advanced(by: x * 4 + 0).pointee = 255 + deltaRowPixels.advanced(by: x * 4 + 1).pointee = 0 + deltaRowPixels.advanced(by: x * 4 + 2).pointee = 0 + deltaRowPixels.advanced(by: x * 4 + 3).pointee = 255 foundDifferenceCount += 1 } } } - lhsBuffer.free() - rhsBuffer.free() + let colorSpace = Unmanaged.passRetained(lhs.cgImage!.colorSpace!) + let deltaImage = try! deltaBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile) if foundDifferenceCount > maxDifferenceCount { - let colorSpace = Unmanaged.passRetained(lhs.cgImage!.colorSpace!) let diffImage = try! lhsBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile) - return UIImage(cgImage: diffImage) + + return (UIImage(cgImage: diffImage), UIImage(cgImage: deltaImage)) } else { - return nil + return (nil, UIImage(cgImage: deltaImage)) } } @available(iOS 13.0, *) -func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, alwaysDraw: Bool, useNonReferenceRendering: Bool, updateImage: @escaping (UIImage?, UIImage?) -> Void) async -> Bool { +func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, allowedDifference: Double, alwaysDraw: Bool, useNonReferenceRendering: Bool, updateImage: @escaping (UIImage?, UIImage?, UIImage?) -> Void) async -> Bool { guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { print("Could not load \(path)") return false @@ -85,16 +92,18 @@ func processDrawAnimation(baseCachePath: String, path: String, name: String, siz let referenceImage = decompressImageFrame(data: referenceImageData) renderer.setFrame(frameIndex) - let image = renderer.render(for: size, useReferenceRendering: !useNonReferenceRendering)! + let image = renderer.render(for: size, useReferenceRendering: !useNonReferenceRendering, canUseMoreMemory: false, skipImageGeneration: false)! - if !useNonReferenceRendering, let diffImage = areImagesEqual(image, referenceImage) { - updateImage(diffImage, referenceImage) + let (diffImage, deltaImage) = areImagesEqual(image, referenceImage, allowedDifference: allowedDifference) + + if !useNonReferenceRendering, let diffImage { + updateImage(diffImage, referenceImage, deltaImage) print("Mismatch in frame \(frameIndex)") return false } else { if alwaysDraw { - updateImage(image, referenceImage) + updateImage(image, referenceImage, diffImage) } return true } @@ -279,6 +288,43 @@ func decompressImageFrame(data: Data) -> UIImage { return decodeImageQOI(data)! } +final class ReferenceLottieAnimationItem { + private let referenceAnimation: Animation + private let referenceLayer: MainThreadAnimationLayer + let frameCount: Int + + init?(path: String) { + guard let referenceAnimation = Animation.filepath(path) else { + return nil + } + self.referenceAnimation = referenceAnimation + + self.referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider()) + self.referenceLayer.position = referenceAnimation.bounds.center + self.referenceLayer.isOpaque = false + self.referenceLayer.backgroundColor = nil + + self.frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame) + } + + func setFrame(index: Int) { + self.referenceLayer.currentFrame = self.referenceAnimation.startFrame + CGFloat(index) + self.referenceLayer.displayUpdate() + } + + func makeImage(width: Int, height: Int) -> UIImage? { + let size = CGSize(width: CGFloat(width), height: CGFloat(width)) + + let referenceContext = ImageContext(width: width, height: height) + referenceContext.context.clear(CGRect(origin: CGPoint(), size: size)) + referenceContext.context.scaleBy(x: size.width / CGFloat(self.referenceAnimation.width), y: size.height / CGFloat(self.referenceAnimation.height)) + + referenceLayer.render(in: referenceContext.context) + + return referenceContext.makeImage() + } +} + @MainActor func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, name: String) -> String { let targetFolderPath = cacheReferenceFolderPath(baseCachePath: baseCachePath, width: width, name: name) @@ -286,34 +332,19 @@ func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, na return targetFolderPath } - guard let referenceAnimation = Animation.filepath(path) else { - preconditionFailure("Could not parse reference animation at \(path)") + guard let referenceItem = ReferenceLottieAnimationItem(path: path) else { + preconditionFailure("Could not load reference animation at \(path)") } - let referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider()) let cacheFolderPath = NSTemporaryDirectory() + "\(UInt64.random(in: 0 ... UInt64.max))" let _ = try? FileManager.default.createDirectory(atPath: cacheFolderPath, withIntermediateDirectories: true) - let frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame) - - let size = CGSize(width: CGFloat(width), height: CGFloat(width)) - - for i in 0 ..< min(100000, frameCount) { - let frameIndex = i % frameCount + for i in 0 ..< min(100000, referenceItem.frameCount) { + let frameIndex = i % referenceItem.frameCount - referenceLayer.currentFrame = CGFloat(frameIndex) - referenceLayer.displayUpdate() - referenceLayer.position = referenceAnimation.bounds.center + referenceItem.setFrame(index: frameIndex) - referenceLayer.isOpaque = false - referenceLayer.backgroundColor = nil - let referenceContext = ImageContext(width: width, height: width) - referenceContext.context.clear(CGRect(origin: CGPoint(), size: size)) - referenceContext.context.scaleBy(x: size.width / CGFloat(referenceAnimation.width), y: size.height / CGFloat(referenceAnimation.height)) - - referenceLayer.render(in: referenceContext.context) - - let referenceImage = referenceContext.makeImage() + let referenceImage = referenceItem.makeImage(width: width, height: width)! try! compressImageFrame(image: referenceImage).write(to: URL(fileURLWithPath: cacheFolderPath + "/frame\(i)")) } diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 72550a1319..9ff203ede4 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -13,6 +13,7 @@ private final class ReferenceCompareTest { private let view: UIView private let imageView = UIImageView() private let referenceImageView = UIImageView() + private let deltaImageView = UIImageView() init(view: UIView, testNonReference: Bool) { lottieSwift_getPathNativeBoundingBox = { path in @@ -37,6 +38,12 @@ private final class ReferenceCompareTest { self.referenceImageView.backgroundColor = self.view.backgroundColor self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + self.view.addSubview(self.deltaImageView) + self.deltaImageView.layer.magnificationFilter = .nearest + self.deltaImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0 + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0)) + self.deltaImageView.backgroundColor = self.view.backgroundColor + self.deltaImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! Task.detached { @@ -67,6 +74,9 @@ private final class ReferenceCompareTest { "1391391008142393350.json": 1024 ] + let allowedDifferences: [String: Double] = [ + "1258816259754165.json": 0.04 + ] let defaultSize = 128 let baseCachePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).path + "/frame-cache" @@ -78,7 +88,7 @@ private final class ReferenceCompareTest { } var continueFromName: String? - //continueFromName = "5089561049196134821.json" + //continueFromName = "1258816259754165.json" let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: !testNonReference, process: { path, name, alwaysDraw in if let continueFromNameValue = continueFromName { @@ -91,10 +101,11 @@ private final class ReferenceCompareTest { let size = sizeMapping[name] ?? defaultSize - let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), alwaysDraw: alwaysDraw, useNonReferenceRendering: testNonReference, updateImage: { image, referenceImage in + let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), allowedDifference: allowedDifferences[name] ?? 0.01, alwaysDraw: alwaysDraw, useNonReferenceRendering: testNonReference, updateImage: { image, referenceImage, differenceImage in DispatchQueue.main.async { self.imageView.image = image self.referenceImageView.image = referenceImage + self.deltaImageView.image = differenceImage } }) return result @@ -103,6 +114,139 @@ private final class ReferenceCompareTest { } } +@available(iOS 13.0, *) +private final class ManualReferenceCompareTest { + private final class Item { + let renderer: SoftwareLottieRenderer + let referenceRenderer: ReferenceLottieAnimationItem + + init(renderer: SoftwareLottieRenderer, referenceRenderer: ReferenceLottieAnimationItem) { + self.renderer = renderer + self.referenceRenderer = referenceRenderer + } + } + + private let view: UIView + private let imageView = UIImageView() + private let referenceImageView = UIImageView() + private let labelView = UILabel() + + private let renderSize: CGSize + private let testNonReference: Bool + + private let fileList: [(filePath: String, fileName: String)] + private var currentFileIndex: Int = 0 + private var currentItem: Item? + + private var frameDisplayLink: SharedDisplayLinkDriver.Link? + + init(view: UIView) { + self.testNonReference = true + + self.currentFileIndex = 0 + + lottieSwift_getPathNativeBoundingBox = { path in + return getPathNativeBoundingBox(path) + } + + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! + self.fileList = buildAnimationFolderItems(basePath: bundlePath, path: "") + + self.renderSize = CGSize(width: 128.0, height: 128.0) + + self.view = view + self.view.backgroundColor = .white + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + let topInset: CGFloat = 50.0 + + self.view.addSubview(self.imageView) + self.imageView.layer.magnificationFilter = .nearest + self.imageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset), size: CGSize(width: 256.0, height: 256.0)) + self.imageView.backgroundColor = self.view.backgroundColor + self.imageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + + self.view.addSubview(self.referenceImageView) + self.referenceImageView.layer.magnificationFilter = .nearest + self.referenceImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0)) + self.referenceImageView.backgroundColor = self.view.backgroundColor + self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + + self.view.addSubview(self.labelView) + + self.updateCurrentAnimation() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if recognizer.location(in: self.view).x <= self.view.bounds.width * 0.5 { + if self.currentFileIndex != 0 { + self.currentFileIndex = self.currentFileIndex - 1 + } + } else { + self.currentFileIndex = (self.currentFileIndex + 1) % self.fileList.count + } + self.updateCurrentAnimation() + } + } + + private func updateCurrentAnimation() { + self.imageView.image = nil + self.referenceImageView.image = nil + self.currentItem = nil + + self.labelView.text = "\(self.currentFileIndex + 1) / \(self.fileList.count)" + self.labelView.sizeToFit() + self.labelView.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.height - 10.0 - self.labelView.bounds.height) + + self.frameDisplayLink?.invalidate() + self.frameDisplayLink = nil + + let (filePath, _) = self.fileList[self.currentFileIndex] + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + print("Could not load \(filePath)") + return + } + guard let renderer = SoftwareLottieRenderer(data: data) else { + print("Could not load animation at \(filePath)") + return + } + guard let referenceRenderer = ReferenceLottieAnimationItem(path: filePath) else { + print("Could not load reference animation at \(filePath)") + return + } + + let currentItem = Item(renderer: renderer, referenceRenderer: referenceRenderer) + self.currentItem = currentItem + + var animationTime = 0.0 + let secondsPerFrame = 1.0 / Double(renderer.framesPerSecond) + + let frameDisplayLink = SharedDisplayLinkDriver.shared.add { [weak self] deltaTime in + guard let self, let currentItem = self.currentItem else { + return + } + + var frameIndex = Int(animationTime / secondsPerFrame) + frameIndex = frameIndex % currentItem.renderer.frameCount + + currentItem.renderer.setFrame(frameIndex) + let image = currentItem.renderer.render(for: self.renderSize, useReferenceRendering: !self.testNonReference, canUseMoreMemory: false, skipImageGeneration: false)! + self.imageView.image = image + + currentItem.referenceRenderer.setFrame(index: frameIndex) + let referenceImage = currentItem.referenceRenderer.makeImage(width: Int(self.renderSize.width), height: Int(self.renderSize.height))! + self.referenceImageView.image = referenceImage + + animationTime += deltaTime + } + self.frameDisplayLink = frameDisplayLink + frameDisplayLink.isPaused = false + } +} + public final class ViewController: UIViewController { private var link: SharedDisplayLinkDriver.Link? private var test: AnyObject? @@ -115,14 +259,18 @@ public final class ViewController: UIViewController { let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! let filePath = bundlePath + "/fire.json" - let performanceFrameSize = 512 + let performanceFrameSize = 128 self.view.layer.addSublayer(MetalEngine.shared.rootLayer) - if "".isEmpty { + if !"".isEmpty { if #available(iOS 13.0, *) { self.test = ReferenceCompareTest(view: self.view, testNonReference: false) } + } else if !"".isEmpty { + if #available(iOS 13.0, *) { + self.test = ManualReferenceCompareTest(view: self.view) + } } else if !"".isEmpty { /*let cachedAnimation = cacheLottieMetalAnimation(path: filePath)! let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)! @@ -161,7 +309,7 @@ public final class ViewController: UIViewController { var frameIndex = 0 while true { animationRenderer.setFrame(frameIndex) - let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false) + let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false, canUseMoreMemory: true, skipImageGeneration: true) frameIndex = (frameIndex + 1) % animationRenderer.frameCount numUpdates += 1 let timestamp = CFAbsoluteTimeGetCurrent() diff --git a/submodules/LottieCpp/lottiecpp b/submodules/LottieCpp/lottiecpp index 97ed00c853..4aa40846d2 160000 --- a/submodules/LottieCpp/lottiecpp +++ b/submodules/LottieCpp/lottiecpp @@ -1 +1 @@ -Subproject commit 97ed00c853de5078fd2a7f68d0da0b8872ac95b8 +Subproject commit 4aa40846d24842e1168375c3db79d85e913ca1e9