import Foundation import UIKit import LottieMetal import LottieCpp import RLottieBinding import MetalEngine import Display import LottieSwift import SoftwareLottieRenderer @available(iOS 13.0, *) 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 return getPathNativeBoundingBox(path) } self.view = view self.view.backgroundColor = .white 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.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 { let sizeMapping: [String: Int] = [ "5170488605398795246.json": 512, "35707580709863506.json": 512, "35707580709863507.json": 512, "1258816259754246.json": 512, "1258816259754248.json": 512, "35707580709863489.json": 512, "1258816259754150.json": 512, "35707580709863494.json": 512, "5021586753580958116.json": 512, "35707580709863509.json": 512, "5282957555314728059.json": 512, "fireworks.json": 512, "750766425144033565.json": 512, "1258816259754276.json": 1024, "1471004892762996753.json": 1024, "4985886809322947159.json": 1024, "35707580709863490.json": 1024, "4986037051573928320.json": 512, "1258816259754029.json": 1024, "4987794066860147124.json": 1024, "1258816259754212.json": 1024, "750766425144033464.json": 1024, "750766425144033567.json": 1024, "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" let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: baseCachePath), withIntermediateDirectories: true, attributes: nil) print("Frame cache: \(baseCachePath)") for (filePath, fileName) in buildAnimationFolderItems(basePath: bundlePath, path: "") { let _ = await cacheReferenceAnimation(baseCachePath: baseCachePath, width: sizeMapping[fileName] ?? defaultSize, path: filePath, name: fileName) } var continueFromName: String? //continueFromName = "562563904580878375.json" let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: !testNonReference, process: { path, name, alwaysDraw in if let continueFromNameValue = continueFromName { if continueFromNameValue == name { continueFromName = nil } else { return true } } let size = sizeMapping[name] ?? defaultSize 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 }) } } } @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: "") if let index = self.fileList.firstIndex(where: { $0.fileName == "shit.json" }) { self.currentFileIndex = index } self.renderSize = CGSize(width: 256.0, height: 256.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(framesPerSecond: .max, { [weak self] deltaTime in guard let self, let currentItem = self.currentItem else { return } var frameIndex = animationTime / secondsPerFrame frameIndex = frameIndex.truncatingRemainder(dividingBy: Double(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: Int(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? override public func viewDidLoad() { super.viewDidLoad() SharedDisplayLinkDriver.shared.updateForegroundState(true) let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! let filePath = bundlePath + "/fire.json" let performanceFrameSize = 128 self.view.layer.addSublayer(MetalEngine.shared.rootLayer) 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)! /*let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) var startTime = CFAbsoluteTimeGetCurrent() let animation = LottieAnimation(data: animationData)! print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") startTime = CFAbsoluteTimeGetCurrent() let animationContainer = LottieAnimationContainer(animation: animation) animationContainer.update(0) print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")*/ let lottieLayer = LottieContentLayer(content: animation) lottieLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 50.0), size: CGSize(width: 256.0, height: 256.0)) self.view.layer.addSublayer(lottieLayer) lottieLayer.setNeedsUpdate() self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { _ in lottieLayer.frameIndex = (lottieLayer.frameIndex + 1) % animation.frameCount lottieLayer.setNeedsUpdate() })*/ } else if "".isEmpty { Thread { let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) var startTime = CFAbsoluteTimeGetCurrent() let animationRenderer = SoftwareLottieRenderer(data: animationData)! print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") startTime = CFAbsoluteTimeGetCurrent() var numUpdates: Int = 0 var frameIndex = 0 while true { animationRenderer.setFrame(CGFloat(frameIndex)) 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() let deltaTime = timestamp - startTime if deltaTime > 2.0 { let updatesPerSecond = Double(numUpdates) / deltaTime startTime = timestamp numUpdates = 0 print("Ours: updatesPerSecond: \(updatesPerSecond)") } } }.start() } else { Thread { var startTime = CFAbsoluteTimeGetCurrent() let animationInstance = LottieInstance(data: try! Data(contentsOf: URL(fileURLWithPath: filePath)), fitzModifier: .none, colorReplacements: nil, cacheKey: "")! print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") let frameBuffer = malloc(performanceFrameSize * 4 * performanceFrameSize)! defer { free(frameBuffer) } startTime = CFAbsoluteTimeGetCurrent() var numUpdates: Int = 0 var frameIndex = 0 while true { animationInstance.renderFrame(with: Int32(frameIndex), into: frameBuffer, width: Int32(performanceFrameSize), height: Int32(performanceFrameSize), bytesPerRow: Int32(performanceFrameSize * 4)) frameIndex = (frameIndex + 1) % Int(animationInstance.frameCount) numUpdates += 1 let timestamp = CFAbsoluteTimeGetCurrent() let deltaTime = timestamp - startTime if deltaTime > 2.0 { let updatesPerSecond = Double(numUpdates) / deltaTime startTime = timestamp numUpdates = 0 print("Rlottie: updatesPerSecond: \(updatesPerSecond)") } } }.start() } } }