diff --git a/Telegram/Tests/Sources/UITests.swift b/Telegram/Tests/Sources/UITests.swift index 722c782f14..6df5294fe7 100644 --- a/Telegram/Tests/Sources/UITests.swift +++ b/Telegram/Tests/Sources/UITests.swift @@ -31,7 +31,7 @@ class UITests: XCTestCase { } func testSignUp() throws { - deleteTestAccount(phone: "9996629999") + deleteTestAccount(phone: "9996625296") app.launch() // Welcome screen — tap Start Messaging diff --git a/Tests/AnimationCacheTest/BUILD b/Tests/AnimationCacheTest/BUILD index b42fca4697..43d5b08ae6 100644 --- a/Tests/AnimationCacheTest/BUILD +++ b/Tests/AnimationCacheTest/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/rlottie:RLottieBinding", diff --git a/Tests/AnimationCacheTest/Sources/ViewController.swift b/Tests/AnimationCacheTest/Sources/ViewController.swift index bfd2798873..f0ae8edcdc 100644 --- a/Tests/AnimationCacheTest/Sources/ViewController.swift +++ b/Tests/AnimationCacheTest/Sources/ViewController.swift @@ -3,6 +3,7 @@ import UIKit import Display import AnimationCache +import DCTAnimationCacheImpl import SwiftSignalKit import VideoAnimationCache import LottieAnimationCache @@ -50,7 +51,7 @@ public final class ViewController: UIViewController { let basePath = NSTemporaryDirectory() + "/animation-cache" let _ = try? FileManager.default.removeItem(atPath: basePath) let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: basePath), withIntermediateDirectories: true) - self.cache = AnimationCacheImpl(basePath: basePath, allocateTempFile: { + self.cache = DCTAnimationCacheImpl(basePath: basePath, allocateTempFile: { return basePath + "/\(Int64.random(in: 0 ... Int64.max))" }) diff --git a/build-system/Make/DeployBuild.py b/build-system/Make/DeployBuild.py index 33b0ae4fdc..56d150f285 100644 --- a/build-system/Make/DeployBuild.py +++ b/build-system/Make/DeployBuild.py @@ -6,6 +6,7 @@ import sys import json import hashlib import base64 +import time import requests def sha256_file(path): @@ -26,16 +27,55 @@ def init_build(host, token, files, channel): r.raise_for_status() return r.json() +class ProgressFileReader: + def __init__(self, path, size): + self._file = open(path, 'rb') + self._size = size + self._sent = 0 + self._start_time = time.time() + self._last_print = self._start_time + + def __len__(self): + return self._size + + def read(self, chunk_size=-1): + if chunk_size == -1: + chunk_size = self._size + data = self._file.read(chunk_size) + if not data: + elapsed = time.time() - self._start_time + speed = self._size / elapsed / 1024 / 1024 if elapsed > 0 else 0 + print(' 100% - all bytes sent in {:.1f}s ({:.2f} MB/s), waiting for server response...'.format(elapsed, speed)) + return data + self._sent += len(data) + now = time.time() + if now - self._last_print >= 5: + elapsed = now - self._start_time + speed = self._sent / elapsed / 1024 / 1024 if elapsed > 0 else 0 + print(' {:.1f}% ({:.1f} / {:.1f} MB) {:.2f} MB/s'.format( + self._sent * 100 / self._size, self._sent / 1024 / 1024, self._size / 1024 / 1024, speed)) + self._last_print = now + return data + + def close(self): + self._file.close() + def upload_file(path, upload_info): url = upload_info.get('url') headers = dict(upload_info.get('headers', {})) - + size = os.path.getsize(path) headers['Content-Length'] = str(size) - print('Uploading', path) - with open(path, 'rb') as f: - r = requests.put(url, data=f, headers=headers, timeout=900) + print('Uploading {} ({:.1f} MB)'.format(path, size / 1024 / 1024)) + start_time = time.time() + + body = ProgressFileReader(path, size) + try: + r = requests.put(url, data=body, headers=headers, timeout=900) + finally: + body.close() + print(' Server responded: {} ({:.1f}s total)'.format(r.status_code, time.time() - start_time)) if r.status_code != 200: print('Upload failed', r.status_code) print(r.text[:500]) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b605a693a8..3ce4e72602 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -7177,9 +7177,6 @@ private final class ChatListLocationContext { titleContent = NetworkStatusTitle(text: presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case let .connecting(proxy): let text = presentationData.strings.State_Connecting - /*if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 {*/ - //text = self.presentationData.strings.State_ConnectingToProxy - //} if let proxy = proxy, proxy.hasConnectionIssues { checkProxy = true } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 76692de347..af1a1be48c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2722,7 +2722,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let messageString: NSAttributedString if !messageText.isEmpty && entities.count > 0 { - messageString = foldLineBreaks(stringWithAppliedEntities(messageText, entities: entities, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())) + let appliedString = stringWithAppliedEntities(messageText, entities: entities, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) + messageString = foldLineBreaks(appliedString) } else if spoilers != nil || customEmojiRanges != nil { let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) if let spoilers = spoilers { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index e82cfbb84d..824887b5d4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -287,7 +287,9 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } if !processed { if !message.text.isEmpty { - messageText = "📎 \(messageText)" + if enableMediaEmoji { + messageText = "📎 \(messageText)" + } } else { if fileMedia.isAnimatedSticker { messageText = strings.Message_Sticker diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 452bff9c44..ecc22273d0 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1216,11 +1216,11 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { public static let all: RenderContentTypes = [.text, .emoji] } - final class DrawingParameters: NSObject { + public final class DrawingParameters: NSObject { let cachedLayout: TextNodeLayout? let renderContentTypes: RenderContentTypes - init(cachedLayout: TextNodeLayout?, renderContentTypes: RenderContentTypes) { + public init(cachedLayout: TextNodeLayout?, renderContentTypes: RenderContentTypes) { self.cachedLayout = cachedLayout self.renderContentTypes = renderContentTypes @@ -1295,7 +1295,7 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { } } - private static func calculateLayoutV2( + public static func calculateLayoutV2( attributedString: NSAttributedString, minimumNumberOfLines: Int, maximumNumberOfLines: Int, @@ -1685,7 +1685,7 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { ) } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { + public static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { guard let attributedString else { return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) } diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index e42c2bc549..92b6ab69cf 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -264,14 +264,11 @@ static NSData *base64_decode(NSString *str) { - (NSString * _Nonnull)serializeToString { NSData *data = [self serialize]; - if ([data respondsToSelector:@selector(base64EncodedDataWithOptions:)]) { - return [[data base64EncodedStringWithOptions:kNilOptions] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]]; - } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - return [self.serialize base64Encoding]; -#pragma clang diagnostic pop - } + NSString *result = [data base64EncodedStringWithOptions:kNilOptions]; + result = [result stringByReplacingOccurrencesOfString:@"+" withString:@"-"]; + result = [result stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; + result = [result stringByReplacingOccurrencesOfString:@"=" withString:@""]; + return result; } - (BOOL)isEqual:(id)object { diff --git a/submodules/MtProtoKit/Sources/MTTcpConnection.m b/submodules/MtProtoKit/Sources/MTTcpConnection.m index ca07b8f467..407748aa3a 100644 --- a/submodules/MtProtoKit/Sources/MTTcpConnection.m +++ b/submodules/MtProtoKit/Sources/MTTcpConnection.m @@ -113,6 +113,28 @@ static bool MTFillRandomBytes(uint8_t *buffer, size_t length) { return SecRandomCopyBytes(kSecRandomDefault, length, buffer) == errSecSuccess; } +static bool generate_ml_kem_public_key(uint8_t key[1184]) { + for (int i = 0; i < 384; i++) { + uint32_t randA = 0; + uint32_t randB = 0; + if (SecRandomCopyBytes(kSecRandomDefault, 4, (uint8_t *)&randA) != errSecSuccess) { + return false; + } + if (SecRandomCopyBytes(kSecRandomDefault, 4, (uint8_t *)&randB) != errSecSuccess) { + return false; + } + uint32_t a = randA % 3329; + uint32_t b = randB % 3329; + key[i * 3 + 0] = (uint8_t)(a & 0xFF); + key[i * 3 + 1] = (uint8_t)((a >> 8) | ((b & 0x0F) << 4)); + key[i * 3 + 2] = (uint8_t)(b >> 4); + } + if (SecRandomCopyBytes(kSecRandomDefault, 32, key + 1152) != errSecSuccess) { + return false; + } + return true; +} + static bool MTGenerateGreaseValues(uint8_t grease[8]) { if (!MTFillRandomBytes(grease, 8)) { return false; @@ -137,7 +159,12 @@ typedef enum { HelloGenerationCommandGrease = 5, HelloGenerationCommandKey = 6, HelloGenerationCommandPushLengthPosition = 7, - HelloGenerationCommandPopLengthPosition = 8 + HelloGenerationCommandPopLengthPosition = 8, + HelloGenerationCommandMlKemKey = 9, + HelloGenerationCommandBeginChoice = 10, + HelloGenerationCommandBeginAlternative = 11, + HelloGenerationCommandEndAlternative = 12, + HelloGenerationCommandEndChoice = 13 } HelloGenerationCommand; typedef struct { @@ -148,7 +175,7 @@ static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *st while (state->position < string.length) { unichar c = [string characterAtIndex:state->position]; state->position += 1; - if (c == '\n' || c == '\r') { + if (c == '\n' || c == '\r' || c == ' ') { continue; } else if (c == 'S') { return HelloGenerationCommandString; @@ -162,10 +189,20 @@ static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *st return HelloGenerationCommandGrease; } else if (c == 'K') { return HelloGenerationCommandKey; + } else if (c == 'M') { + return HelloGenerationCommandMlKemKey; } else if (c == '[') { return HelloGenerationCommandPushLengthPosition; } else if (c == ']') { return HelloGenerationCommandPopLengthPosition; + } else if (c == '<') { + return HelloGenerationCommandBeginChoice; + } else if (c == '(') { + return HelloGenerationCommandBeginAlternative; + } else if (c == ')') { + return HelloGenerationCommandEndAlternative; + } else if (c == '>') { + return HelloGenerationCommandEndChoice; } else { return HelloGenerationCommandInvalid; } @@ -187,21 +224,6 @@ static bool parseSpace(NSString *string, HelloParseState *state) { return seenSpace; } -static bool parseEndlineOrEnd(NSString *string, HelloParseState *state) { - while (state->position < string.length) { - unichar c = [string characterAtIndex:state->position]; - if (c == '\n') { - state->position += 1; - return true; - } else if (c == ' ' || c == '\r') { - state->position += 1; - continue; - } else { - return false; - } - } - return true; -} static bool parseHexByte(unichar c, uint8_t *output) { if (c >= '0' && c <= '9') { @@ -284,13 +306,11 @@ static bool parseIntArgument(NSString *string, HelloParseState *state, int *outp value = value * 10 + (c - '0'); hasDigit = true; state->position += 1; - } else if (c == ' ') { - state->position += 1; } else if (c == '\n') { state->position += 1; break; } else { - return false; + break; } } @@ -305,14 +325,173 @@ static bool parseIntArgument(NSString *string, HelloParseState *state, int *outp return true; } +static bool executeGenerationCodeRecursive(NSString *code, HelloParseState *state, uint8_t grease[8], NSData *domain, id provider, NSMutableData *resultData, NSMutableArray *lengthStack) { + while (true) { + HelloGenerationCommand command = parseCommand(code, state); + if (command == HelloGenerationCommandInvalid) { + break; + } + + switch (command) { + case HelloGenerationCommandString: { + if (!parseSpace(code, state)) { + return false; + } + NSData *data = parseHexStringArgument(code, state); + if (data == nil) { + return false; + } + [resultData appendData:data]; + break; + } + case HelloGenerationCommandZero: { + if (!parseSpace(code, state)) { + return false; + } + int zeroLength = 0; + if (!parseIntArgument(code, state, &zeroLength)) { + return false; + } + if (zeroLength < 0) { + return false; + } + NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength]; + [resultData appendData:zeros]; + break; + } + case HelloGenerationCommandRandom: { + if (!parseSpace(code, state)) { + return false; + } + int randomLength = 0; + if (!parseIntArgument(code, state, &randomLength)) { + return false; + } + if (randomLength < 0) { + return false; + } + NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength]; + if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) { + return false; + } + [resultData appendData:randomData]; + break; + } + case HelloGenerationCommandDomain: { + [resultData appendData:domain]; + break; + } + case HelloGenerationCommandGrease: { + if (!parseSpace(code, state)) { + return false; + } + int greaseIndex = 0; + if (!parseIntArgument(code, state, &greaseIndex)) { + return false; + } + if (greaseIndex < 0 || greaseIndex >= 8) { + return false; + } + uint8_t value = grease[greaseIndex]; + [resultData appendBytes:&value length:1]; + [resultData appendBytes:&value length:1]; + break; + } + case HelloGenerationCommandKey: { + NSMutableData *key = [[NSMutableData alloc] initWithLength:32]; + generate_public_key((unsigned char *)key.mutableBytes, provider); + [resultData appendData:key]; + break; + } + case HelloGenerationCommandMlKemKey: { + NSMutableData *key = [[NSMutableData alloc] initWithLength:1184]; + if (!generate_ml_kem_public_key((uint8_t *)key.mutableBytes)) { + return false; + } + [resultData appendData:key]; + break; + } + case HelloGenerationCommandPushLengthPosition: { + NSUInteger lengthPosition = resultData.length; + uint8_t zeroBytes[2] = { 0, 0 }; + [resultData appendBytes:zeroBytes length:2]; + [lengthStack addObject:@(lengthPosition)]; + break; + } + case HelloGenerationCommandPopLengthPosition: { + if (lengthStack.count == 0) { + return false; + } + NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue]; + [lengthStack removeLastObject]; + if (resultData.length < position + 2) { + return false; + } + uint16_t blockLength = (uint16_t)(resultData.length - position - 2); + ((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff); + ((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff); + break; + } + case HelloGenerationCommandBeginChoice: { + NSMutableArray *alternatives = [[NSMutableArray alloc] init]; + while (true) { + HelloGenerationCommand nextCommand = parseCommand(code, state); + if (nextCommand == HelloGenerationCommandEndChoice) { + break; + } + if (nextCommand != HelloGenerationCommandBeginAlternative) { + return false; + } + NSMutableData *altData = [[NSMutableData alloc] init]; + NSMutableArray *altLengthStack = [[NSMutableArray alloc] init]; + if (!executeGenerationCodeRecursive(code, state, grease, domain, provider, altData, altLengthStack)) { + return false; + } + if (altLengthStack.count != 0) { + return false; + } + [alternatives addObject:altData]; + } + if (alternatives.count == 0) { + return false; + } + uint32_t choiceIndex = arc4random_uniform((uint32_t)alternatives.count); + [resultData appendData:alternatives[choiceIndex]]; + break; + } + case HelloGenerationCommandEndAlternative: { + // signals end of current alternative — return to caller + return true; + } + case HelloGenerationCommandBeginAlternative: + case HelloGenerationCommandEndChoice: { + // should not be encountered directly at this level + return false; + } + default: { + return false; + } + } + } + + return true; +} + static NSMutableData *executeGenerationCode(id provider, NSData *domain) { - NSString *code = @"S \"\\x16\\x03\\x01\\x02\\x00\\x01\\x00\\x01\\xfc\\x03\\x03\"\n" + NSString *code = @"S \"\\x16\\x03\\x01\"\n" + "[\n" + "S \"\\x01\\x00\"\n" + "[\n" + "S \"\\x03\\x03\"\n" "Z 32\n" "S \"\\x20\"\n" "R 32\n" - "S \"\\x00\\x2a\"\n" - "G 0\n" - "S \"\\x13\\x01\\x13\\x02\\x13\\x03\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\\x01\\x00\\x01\\x89\"\n" + "<\n" + "( S \"\\x00\\x1c\" G 0 S \"\\x13\\x02\\x13\\x01\\x13\\x03\\xc0\\x2c\\xc0\\x30\\xc0\\x2b\\xcc\\xa9\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\" )\n" + "( S \"\\x00\\x2a\" G 0 S \"\\x13\\x02\\x13\\x03\\x13\\x01\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\" )\n" + ">\n" + "S \"\\x01\\x00\"\n" + "[\n" "G 2\n" "S \"\\x00\\x00\\x00\\x00\"\n" "[\n" @@ -323,17 +502,28 @@ static NSMutableData *executeGenerationCode(id provider, NSD "]\n" "]\n" "]\n" - "S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0c\\x00\\x0a\"\n" + "S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0e\\x00\\x0c\"\n" "G 4\n" - "S \"\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x00\\x2b\\x00\\x29\"\n" + "S \"\\x11\\xec\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\"\n" + "<\n" + "( S \"\\x00\\x10\\x00\\x0b\\x00\\x09\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\" )\n" + "( S \"\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\" )\n" + ">\n" + "S \"\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x04\\xef\\x04\\xed\"\n" "G 4\n" - "S \"\\x00\\x01\\x00\\x00\\x1d\\x00\\x20\"\n" + "S \"\\x00\\x01\\x00\\x11\\xec\\x04\\xc0\"\n" + "M\n" "K\n" - "S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x0b\\x0a\"\n" + "S \"\\x00\\x1d\\x00\\x20\"\n" + "K\n" + "S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x07\\x06\"\n" "G 6\n" - "S \"\\x03\\x04\\x03\\x03\\x03\\x02\\x03\\x01\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n" + "S \"\\x03\\x04\\x03\\x03\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n" "G 3\n" - "S \"\\x00\\x01\\x00\"\n"; + "S \"\\x00\\x01\\x00\"\n" + "]\n" + "]\n" + "]\n"; uint8_t grease[8]; if (!MTGenerateGreaseValues(grease)) { @@ -345,120 +535,8 @@ static NSMutableData *executeGenerationCode(id provider, NSD HelloParseState state = { .position = 0 }; - while (true) { - HelloGenerationCommand command = parseCommand(code, &state); - if (command == HelloGenerationCommandInvalid) { - break; - } - - switch (command) { - case HelloGenerationCommandString: { - if (!parseSpace(code, &state)) { - return nil; - } - NSData *data = parseHexStringArgument(code, &state); - if (data == nil) { - return nil; - } - [resultData appendData:data]; - break; - } - case HelloGenerationCommandZero: { - if (!parseSpace(code, &state)) { - return nil; - } - int zeroLength = 0; - if (!parseIntArgument(code, &state, &zeroLength)) { - return nil; - } - if (zeroLength < 0) { - return nil; - } - NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength]; - [resultData appendData:zeros]; - break; - } - case HelloGenerationCommandRandom: { - if (!parseSpace(code, &state)) { - return nil; - } - int randomLength = 0; - if (!parseIntArgument(code, &state, &randomLength)) { - return nil; - } - if (randomLength < 0) { - return nil; - } - NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength]; - if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) { - return nil; - } - [resultData appendData:randomData]; - break; - } - case HelloGenerationCommandDomain: { - [resultData appendData:domain]; - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - break; - } - case HelloGenerationCommandGrease: { - if (!parseSpace(code, &state)) { - return nil; - } - int greaseIndex = 0; - if (!parseIntArgument(code, &state, &greaseIndex)) { - return nil; - } - if (greaseIndex < 0 || greaseIndex >= 8) { - return nil; - } - uint8_t value = grease[greaseIndex]; - [resultData appendBytes:&value length:1]; - [resultData appendBytes:&value length:1]; - break; - } - case HelloGenerationCommandKey: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - NSMutableData *key = [[NSMutableData alloc] initWithLength:32]; - generate_public_key((unsigned char *)key.mutableBytes, provider); - [resultData appendData:key]; - break; - } - case HelloGenerationCommandPushLengthPosition: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - NSUInteger lengthPosition = resultData.length; - uint8_t zeroBytes[2] = { 0, 0 }; - [resultData appendBytes:zeroBytes length:2]; - [lengthStack addObject:@(lengthPosition)]; - break; - } - case HelloGenerationCommandPopLengthPosition: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - if (lengthStack.count == 0) { - return nil; - } - NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue]; - [lengthStack removeLastObject]; - if (resultData.length < position + 2) { - return nil; - } - uint16_t blockLength = (uint16_t)(resultData.length - position - 2); - ((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff); - ((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff); - break; - } - default: { - return nil; - } - } + if (!executeGenerationCodeRecursive(code, &state, grease, domain, provider, resultData, lengthStack)) { + return nil; } if (lengthStack.count != 0) { diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index cb3ce76c7c..623c6ad991 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TextFormat:TextFormat", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 45045f3107..c31f36300b 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -20,6 +20,7 @@ import EntityKeyboard import ComponentDisplayAdapters import AnimationCache import MultiAnimationRenderer +import DCTMultiAnimationRendererImpl import EmojiTextAttachmentView import TextFormat import GZip @@ -504,8 +505,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.reactionsLocked = reactionsLocked self.animationCache = animationCache - self.animationRenderer = MultiAnimationRendererImpl() - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache + self.animationRenderer = DCTMultiAnimationRendererImpl() + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache self.backgroundMaskNode = ASDisplayNode() var backgroundGlassParams: ReactionContextBackgroundNode.GlassParams? @@ -3341,7 +3342,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let currentItemNode = currentItemNode { itemNode = currentItemNode } else { - let animationRenderer = MultiAnimationRendererImpl() + let animationRenderer = DCTMultiAnimationRendererImpl() itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) } self.itemNode = itemNode diff --git a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift index 2e8250db45..31bed130c4 100644 --- a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift @@ -54,6 +54,7 @@ public final class SearchBarPlaceholderContentView: UIView { let fieldStyle: SearchBarStyle let plainBackgroundView: UIImageView + let glassBackgroundContainerView: GlassBackgroundContainerView? let glassBackgroundView: GlassBackgroundView? private var fillBackgroundColor: UIColor private var foregroundColor: UIColor @@ -82,8 +83,10 @@ public final class SearchBarPlaceholderContentView: UIView { switch fieldStyle { case .legacy, .modern: + self.glassBackgroundContainerView = nil self.glassBackgroundView = nil case .inlineNavigation, .glass: + self.glassBackgroundContainerView = GlassBackgroundContainerView() self.glassBackgroundView = GlassBackgroundView() } @@ -111,8 +114,9 @@ public final class SearchBarPlaceholderContentView: UIView { self.plainBackgroundView.addSubview(self.plainIconNode.view) self.plainBackgroundView.addSubview(self.plainLabelNode.view) - if let glassBackgroundView = self.glassBackgroundView { - self.addSubview(glassBackgroundView) + if let glassBackgroundContainerView = self.glassBackgroundContainerView, let glassBackgroundView = self.glassBackgroundView { + self.addSubview(glassBackgroundContainerView) + glassBackgroundContainerView.contentView.addSubview(glassBackgroundView) glassBackgroundView.contentView.addSubview(self.iconNode.view) glassBackgroundView.contentView.addSubview(self.labelNode.view) @@ -300,9 +304,14 @@ public final class SearchBarPlaceholderContentView: UIView { transition.updateFrame(view: self.plainBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.constrainedSize.width, height: height))) } - if let glassBackgroundView = self.glassBackgroundView { - transition.updatePosition(layer: glassBackgroundView.layer, position: backgroundFrame.center) + if let glassBackgroundContainerView = self.glassBackgroundContainerView, let glassBackgroundView = self.glassBackgroundView { + + transition.updatePosition(layer: glassBackgroundContainerView.layer, position: backgroundFrame.center) + transition.updateBounds(layer: glassBackgroundContainerView.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + transition.updatePosition(layer: glassBackgroundView.layer, position: CGRect(origin: CGPoint(), size: backgroundFrame.size).center) transition.updateBounds(layer: glassBackgroundView.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + var backgroundAlpha: CGFloat = 1.0 if backgroundFrame.height < 16.0 { backgroundAlpha = max(0.0, min(1.0, backgroundFrame.height / 16.0)) @@ -310,9 +319,10 @@ public final class SearchBarPlaceholderContentView: UIView { if !params.isActive { backgroundAlpha = 0.0 } - ComponentTransition(transition).setAlpha(view: glassBackgroundView, alpha: backgroundAlpha) + ComponentTransition(transition).setAlpha(view: glassBackgroundContainerView, alpha: backgroundAlpha) let isDark = params.backgroundColor.hsb.b < 0.5 if params.isActive { + glassBackgroundContainerView.update(size: backgroundFrame.size, isDark: isDark, transition: ComponentTransition(transition)) glassBackgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: isDark, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: true, transition: ComponentTransition(transition)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index b2ba003fb7..62f826c99e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -16,7 +16,7 @@ private func shareLink(for server: ProxyServerSettings) -> String { switch server.connection { case let .mtp(secret): let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? "" - link = "https://t.me/proxy?server=\(server.host)&port=\(server.port)" + link = "tg://proxy?server=\(server.host)&port=\(server.port)" link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" case let .socks5(username, password): link = "https://t.me/socks?server=\(server.host)&port=\(server.port)" @@ -332,10 +332,10 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: let signal = combineLatest(updatedPresentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { dismissImpl?() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { + let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: { if let proxyServerSettings = proxyServerSettings(with: state) { let _ = (updateProxySettingsInteractively(accountManager: accountManager, { settings in var settings = settings diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index c53cfdcae3..96e730748f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -283,6 +283,8 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent", diff --git a/submodules/TelegramUI/Components/AnimationCache/BUILD b/submodules/TelegramUI/Components/AnimationCache/BUILD index e655aa9fe6..3d182a866a 100644 --- a/submodules/TelegramUI/Components/AnimationCache/BUILD +++ b/submodules/TelegramUI/Components/AnimationCache/BUILD @@ -11,9 +11,6 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/CryptoUtils:CryptoUtils", - "//submodules/ManagedFile:ManagedFile", - "//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 0c99aada38..6c6391bb73 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -1,51 +1,19 @@ import Foundation import UIKit import SwiftSignalKit -import CryptoUtils -import ManagedFile -import Compression - -private let algorithm: compression_algorithm = COMPRESSION_LZFSE - -private func alignUp(size: Int, align: Int) -> Int { - precondition(((align - 1) & align) == 0, "Align must be a power of two") - - let alignmentMask = align - 1 - return (size + alignmentMask) & ~alignmentMask -} - -private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { - if useTotalFileAllocatedSize { - let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { - if values.isRegularFile ?? false { - if let fileSize = values.fileAllocatedSize { - return Int64(fileSize) - } - } - } - } - - var value = stat() - if stat(path, &value) == 0 { - return value.st_size - } else { - return nil - } -} public final class AnimationCacheItemFrame { public enum RequestedFormat { case rgba case yuva(rowAlignment: Int) } - + public final class Plane { public let data: Data public let width: Int public let height: Int public let bytesPerRow: Int - + public init(data: Data, width: Int, height: Int, bytesPerRow: Int) { self.data = data self.width = width @@ -53,15 +21,15 @@ public final class AnimationCacheItemFrame { self.bytesPerRow = bytesPerRow } } - + public enum Format { case rgba(data: Data, width: Int, height: Int, bytesPerRow: Int) case yuva(y: Plane, u: Plane, v: Plane, a: Plane) } - + public let format: Format public let duration: Double - + public init(format: Format, duration: Double) { self.format = format self.duration = duration @@ -73,31 +41,31 @@ public final class AnimationCacheItem { case duration(Double) case frames(Int) } - + public struct AdvanceResult { public let frame: AnimationCacheItemFrame public let didLoop: Bool - + public init(frame: AnimationCacheItemFrame, didLoop: Bool) { self.frame = frame self.didLoop = didLoop } } - + public let numFrames: Int private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? private let resetImpl: () -> Void - + public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult?, resetImpl: @escaping () -> Void) { self.numFrames = numFrames self.advanceImpl = advanceImpl self.resetImpl = resetImpl } - + public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? { return self.advanceImpl(advance, requestedFormat) } - + public func reset() { self.resetImpl() } @@ -109,7 +77,7 @@ public struct AnimationCacheItemDrawingSurface { public let height: Int public let bytesPerRow: Int public let length: Int - + public init( argb: UnsafeMutablePointer, width: Int, @@ -128,7 +96,7 @@ public struct AnimationCacheItemDrawingSurface { public protocol AnimationCacheItemWriter: AnyObject { var queue: Queue { get } var isCancelled: Bool { get } - + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) func finish() } @@ -136,7 +104,7 @@ public protocol AnimationCacheItemWriter: AnyObject { public final class AnimationCacheItemResult { public let item: AnimationCacheItem? public let isFinal: Bool - + public init(item: AnimationCacheItem?, isFinal: Bool) { self.item = item self.isFinal = isFinal @@ -147,7 +115,7 @@ public struct AnimationCacheFetchOptions { public let size: CGSize public let writer: AnimationCacheItemWriter public let firstFrameOnly: Bool - + public init( size: CGSize, writer: AnimationCacheItemWriter, @@ -164,1525 +132,3 @@ public protocol AnimationCache: AnyObject { func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable } - -private func md5Hash(_ string: String) -> String { - let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in - return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) - } - return hashData.withUnsafeBytes { bytes -> String in - let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) - return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) - } -} - -private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { - assert(hashString.count == 32) - var directory = "" - - for i in 0 ..< 1 { - if !directory.isEmpty { - directory.append("/") - } - directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) - } - - return (directory, "\(hashString)_\(width)x\(height)") -} - -private func roundUp(_ numToRound: Int, multiple: Int) -> Int { - if multiple == 0 { - return numToRound - } - - let remainder = numToRound % multiple - if remainder == 0 { - return numToRound; - } - - return numToRound + multiple - remainder -} - -private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { - let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! - defer { - free(scratchData) - } - - let headerSize = addSizeHeader ? 4 : 0 - var compressedData = Data(count: headerSize + data.count + 16 * 1024) - let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in - guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return 0 - } - - if addSizeHeader { - var decompressedSize: UInt32 = UInt32(data.count) - memcpy(bytes, &decompressedSize, 4) - } - - return data.withUnsafeBytes { sourceBuffer -> Int in - return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) - } - } - - if resultSize <= 0 { - return nil - } - compressedData.count = headerSize + resultSize - return compressedData -} - -private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { - let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! - defer { - free(scratchData) - } - - var decompressedFrameData = Data(count: decompressedSize) - let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in - guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return 0 - } - return data.withUnsafeBytes { sourceBuffer -> Int in - return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) - } - } - - if resultSize <= 0 { - return nil - } - if decompressedFrameData.count != resultSize { - decompressedFrameData.count = resultSize - } - return decompressedFrameData -} - -private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { - enum WriteError: Error { - case generic - } - - struct CompressedResult { - var animationPath: String - } - - private struct FrameMetadata { - var duration: Double - } - - var queue: Queue { - return self.innerQueue - } - let innerQueue: Queue - var isCancelled: Bool = false - - private let compressedPath: String - private var file: ManagedFile? - private var compressedWriter: CompressedFileWriter? - private let completion: (CompressedResult?) -> Void - - - private var currentSurface: ImageARGB? - private var currentYUVASurface: ImageYUVA420? - private var currentFrameFloat: FloatCoefficientsYUVA420? - private var previousFrameCoefficients: DctCoefficientsYUVA420? - private var deltaFrameFloat: FloatCoefficientsYUVA420? - private var previousYUVASurface: ImageYUVA420? - private var currentDctData: DctData? - private var differenceCoefficients: DctCoefficientsYUVA420? - private var currentDctCoefficients: DctCoefficientsYUVA420? - private var contentLengthOffset: Int? - private var isFailed: Bool = false - private var isFinished: Bool = false - - private var frames: [FrameMetadata] = [] - - private let dctQualityLuma: Int - private let dctQualityChroma: Int - private let dctQualityDelta: Int - - private let lock = Lock() - - init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { - self.dctQualityLuma = 70 - self.dctQualityChroma = 88 - self.dctQualityDelta = 22 - - self.innerQueue = queue - self.compressedPath = allocateTempFile() - - guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { - return nil - } - self.file = file - self.compressedWriter = CompressedFileWriter(file: file) - self.completion = completion - } - - func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { - do { - try self.lock.throwingLocked { - let width = roundUp(proposedWidth, multiple: 16) - let height = roundUp(proposedHeight, multiple: 16) - - let surface: ImageARGB - if let current = self.currentSurface { - if current.argbPlane.width == width && current.argbPlane.height == height { - surface = current - surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in - memset(bytes.baseAddress!, 0, bytes.count) - } - } else { - self.isFailed = true - return - } - } else { - surface = ImageARGB(width: width, height: height, rowAlignment: 32) - self.currentSurface = surface - } - - let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in - return drawingBlock(AnimationCacheItemDrawingSurface( - argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), - width: width, - height: height, - bytesPerRow: surface.argbPlane.bytesPerRow, - length: bytes.count - )) - } - - guard let duration = duration else { - return - } - - try addInternal(with: { yuvaSurface in - surface.toYUVA420(target: yuvaSurface) - - return duration - }, width: width, height: height, insertKeyframe: insertKeyframe) - } - } catch { - } - } - - func addYUV(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) throws { - let width = roundUp(proposedWidth, multiple: 16) - let height = roundUp(proposedHeight, multiple: 16) - - do { - try self.lock.throwingLocked { - try addInternal(with: { yuvaSurface in - return drawingBlock(yuvaSurface) - }, width: width, height: height, insertKeyframe: insertKeyframe) - } - } catch { - } - } - - func addInternal(with drawingBlock: (ImageYUVA420) -> Double?, width: Int, height: Int, insertKeyframe: Bool) throws { - if width == 0 || height == 0 { - self.isFailed = true - throw WriteError.generic - } - if self.isFailed || self.isFinished { - throw WriteError.generic - } - - guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { - throw WriteError.generic - } - - var isFirstFrame = false - - let yuvaSurface: ImageYUVA420 - if let current = self.currentYUVASurface { - if current.yPlane.width == width && current.yPlane.height == height { - yuvaSurface = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - isFirstFrame = true - - yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) - self.currentYUVASurface = yuvaSurface - } - - let currentFrameFloat: FloatCoefficientsYUVA420 - if let current = self.currentFrameFloat { - if current.yPlane.width == width && current.yPlane.height == height { - currentFrameFloat = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - currentFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) - self.currentFrameFloat = currentFrameFloat - } - - let previousFrameCoefficients: DctCoefficientsYUVA420 - if let current = self.previousFrameCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - previousFrameCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - previousFrameCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.previousFrameCoefficients = previousFrameCoefficients - } - - let deltaFrameFloat: FloatCoefficientsYUVA420 - if let current = self.deltaFrameFloat { - if current.yPlane.width == width && current.yPlane.height == height { - deltaFrameFloat = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - deltaFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) - self.deltaFrameFloat = deltaFrameFloat - } - - let dctData: DctData - if let current = self.currentDctData { - dctData = current - } else { - dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma, delta: self.dctQualityDelta) - self.currentDctData = dctData - } - - let duration = drawingBlock(yuvaSurface) - - guard let duration = duration else { - return - } - - let dctCoefficients: DctCoefficientsYUVA420 - if let current = self.currentDctCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - dctCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.currentDctCoefficients = dctCoefficients - } - - let differenceCoefficients: DctCoefficientsYUVA420 - if let current = self.differenceCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - differenceCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - differenceCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.differenceCoefficients = differenceCoefficients - } - - #if !arch(arm64) - var insertKeyframe = insertKeyframe - insertKeyframe = true - #endif - - let previousYUVASurface: ImageYUVA420 - if let current = self.previousYUVASurface { - previousYUVASurface = current - } else { - previousYUVASurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) - self.previousYUVASurface = previousYUVASurface - } - - let isKeyframe: Bool - if !isFirstFrame && !insertKeyframe { - isKeyframe = false - - //previous + delta = current - //delta = current - previous - yuvaSurface.toCoefficients(target: differenceCoefficients) - differenceCoefficients.subtract(other: previousFrameCoefficients) - differenceCoefficients.dct4x4(dctData: dctData, target: dctCoefficients) - - //previous + delta = current - dctCoefficients.idct4x4Add(dctData: dctData, target: previousFrameCoefficients) - //previousFrameCoefficients.add(other: differenceCoefficients) - } else { - isKeyframe = true - - yuvaSurface.dct8x8(dctData: dctData, target: dctCoefficients) - - dctCoefficients.idct8x8(dctData: dctData, target: yuvaSurface) - yuvaSurface.toCoefficients(target: previousFrameCoefficients) - } - - if isFirstFrame { - file.write(6 as UInt32) - - file.write(UInt32(dctCoefficients.yPlane.width)) - file.write(UInt32(dctCoefficients.yPlane.height)) - - let lumaDctTable = dctData.lumaTable.serializedData() - file.write(UInt32(lumaDctTable.count)) - let _ = file.write(lumaDctTable) - - let chromaDctTable = dctData.chromaTable.serializedData() - file.write(UInt32(chromaDctTable.count)) - let _ = file.write(chromaDctTable) - - let deltaDctTable = dctData.deltaTable.serializedData() - file.write(UInt32(deltaDctTable.count)) - let _ = file.write(deltaDctTable) - - self.contentLengthOffset = Int(file.position()) - file.write(0 as UInt32) - } - - do { - let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count - try compressedWriter.writeUInt32(UInt32(frameLength)) - - try compressedWriter.writeUInt32(isKeyframe ? 1 : 0) - - for i in 0 ..< 4 { - let dctPlane: DctCoefficientPlane - switch i { - case 0: - dctPlane = dctCoefficients.yPlane - case 1: - dctPlane = dctCoefficients.uPlane - case 2: - dctPlane = dctCoefficients.vPlane - case 3: - dctPlane = dctCoefficients.aPlane - default: - preconditionFailure() - } - - try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) - try dctPlane.data.withUnsafeBytes { bytes in - try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - } - - self.frames.append(FrameMetadata(duration: duration)) - } catch { - self.isFailed = true - throw WriteError.generic - } - } - - func finish() { - do { - let result = try self.finishInternal() - self.completion(result) - } catch { - } - } - - func finishInternal() throws -> CompressedResult? { - var shouldComplete = false - self.lock.locked { - if !self.isFinished { - self.isFinished = true - shouldComplete = true - - guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { - self.isFailed = true - return - } - assert(contentLengthOffset >= 0) - - do { - try compressedWriter.flush() - - let metadataPosition = file.position() - let contentLength = Int(metadataPosition) - contentLengthOffset - 4 - let _ = file.seek(position: Int64(contentLengthOffset)) - file.write(UInt32(contentLength)) - - let _ = file.seek(position: metadataPosition) - file.write(UInt32(self.frames.count)) - for frame in self.frames { - file.write(Float32(frame.duration)) - } - - if !self.isFailed { - self.compressedWriter = nil - self.file = nil - - file._unsafeClose() - } - } catch { - self.isFailed = true - } - } - } - - if shouldComplete { - if !self.isFailed { - return CompressedResult(animationPath: self.compressedPath) - } else { - let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) - return nil - } - } else { - return nil - } - } -} - -private final class AnimationCacheItemAccessor { - private enum ReadError: Error { - case generic - } - - final class CurrentFrame { - let index: Int - var remainingDuration: Double - let duration: Double - let yuva: ImageYUVA420 - - init(index: Int, duration: Double, yuva: ImageYUVA420) { - self.index = index - self.duration = duration - self.remainingDuration = duration - self.yuva = yuva - } - } - - struct FrameInfo { - let duration: Double - } - - private let data: Data - private var compressedDataReader: DecompressedData? - private let range: Range - private let frameMapping: [Int: FrameInfo] - private let width: Int - private let height: Int - private let durationMapping: [Double] - - private var currentFrame: CurrentFrame? - - private var currentYUVASurface: ImageYUVA420? - private var currentCoefficients: DctCoefficientsYUVA420? - private let currentDctData: DctData - private var sharedDctCoefficients: DctCoefficientsYUVA420? - private var deltaCoefficients: DctCoefficientsYUVA420? - - init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) { - self.data = data - self.range = range - self.width = width - self.height = height - - var resultFrameMapping: [Int: FrameInfo] = [:] - var durationMapping: [Double] = [] - - for i in 0 ..< frameMapping.count { - let frame = frameMapping[i] - resultFrameMapping[i] = frame - durationMapping.append(frame.duration) - } - - self.frameMapping = resultFrameMapping - self.durationMapping = durationMapping - - self.currentDctData = dctData - } - - private func loadNextFrame() -> Bool { - var didLoop = false - let index: Int - if let currentFrame = self.currentFrame { - if currentFrame.index + 1 >= self.durationMapping.count { - index = 0 - self.compressedDataReader = nil - didLoop = true - } else { - index = currentFrame.index + 1 - } - } else { - index = 0 - self.compressedDataReader = nil - } - - if self.compressedDataReader == nil { - self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) - } - - guard let compressedDataReader = self.compressedDataReader else { - self.currentFrame = nil - return didLoop - } - - do { - let frameLength = Int(try compressedDataReader.readUInt32()) - - let frameType = Int(try compressedDataReader.readUInt32()) - - let dctCoefficients: DctCoefficientsYUVA420 - if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height, !"".isEmpty { - dctCoefficients = sharedDctCoefficients - } else { - dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) - self.sharedDctCoefficients = dctCoefficients - } - - var frameOffset = 0 - for i in 0 ..< 4 { - let planeLength = Int(try compressedDataReader.readUInt32()) - if planeLength < 0 || planeLength > 20 * 1024 * 1024 { - throw ReadError.generic - } - - let plane: DctCoefficientPlane - switch i { - case 0: - plane = dctCoefficients.yPlane - case 1: - plane = dctCoefficients.uPlane - case 2: - plane = dctCoefficients.vPlane - case 3: - plane = dctCoefficients.aPlane - default: - throw ReadError.generic - } - - if planeLength != plane.data.count { - throw ReadError.generic - } - - if frameOffset + plane.data.count > frameLength { - throw ReadError.generic - } - - try plane.data.withUnsafeMutableBytes { bytes in - try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - frameOffset += plane.data.count - } - - let yuvaSurface: ImageYUVA420 - if let currentYUVASurface = self.currentYUVASurface { - yuvaSurface = currentYUVASurface - } else { - yuvaSurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) - } - - let currentCoefficients: DctCoefficientsYUVA420 - if let current = self.currentCoefficients { - currentCoefficients = current - } else { - currentCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) - self.currentCoefficients = currentCoefficients - } - - /*let deltaCoefficients: DctCoefficientsYUVA420 - if let current = self.deltaCoefficients { - deltaCoefficients = current - } else { - deltaCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) - self.deltaCoefficients = deltaCoefficients - }*/ - - switch frameType { - case 1: - dctCoefficients.idct8x8(dctData: self.currentDctData, target: yuvaSurface) - yuvaSurface.toCoefficients(target: currentCoefficients) - default: - dctCoefficients.idct4x4Add(dctData: self.currentDctData, target: currentCoefficients) - //currentCoefficients.add(other: deltaCoefficients) - - currentCoefficients.toYUVA420(target: yuvaSurface) - } - - self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], yuva: yuvaSurface) - } catch { - self.currentFrame = nil - self.compressedDataReader = nil - } - - return didLoop - } - - func reset() { - self.currentFrame = nil - } - - func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? { - var didLoop = false - switch advance { - case let .frames(count): - for _ in 0 ..< count { - if self.loadNextFrame() { - didLoop = true - } - } - case let .duration(duration): - var durationOverflow = duration - while true { - if let currentFrame = self.currentFrame { - currentFrame.remainingDuration -= durationOverflow - if currentFrame.remainingDuration <= 0.0 { - durationOverflow = -currentFrame.remainingDuration - if self.loadNextFrame() { - didLoop = true - } - } else { - break - } - } else { - if self.loadNextFrame() { - didLoop = true - } - break - } - } - } - - guard let currentFrame = self.currentFrame else { - return nil - } - - switch requestedFormat { - case .rgba: - let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) - currentFrame.yuva.toARGB(target: currentSurface) - - return AnimationCacheItem.AdvanceResult( - frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration), - didLoop: didLoop - ) - case .yuva: - return AnimationCacheItem.AdvanceResult( - frame: AnimationCacheItemFrame( - format: .yuva( - y: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.yPlane.data, - width: currentFrame.yuva.yPlane.width, - height: currentFrame.yuva.yPlane.height, - bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow - ), - u: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.uPlane.data, - width: currentFrame.yuva.uPlane.width, - height: currentFrame.yuva.uPlane.height, - bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow - ), - v: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.vPlane.data, - width: currentFrame.yuva.vPlane.width, - height: currentFrame.yuva.vPlane.height, - bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow - ), - a: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.aPlane.data, - width: currentFrame.yuva.aPlane.width, - height: currentFrame.yuva.aPlane.height, - bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow - ) - ), - duration: currentFrame.duration - ), - didLoop: didLoop - ) - } - } -} - -private func readData(data: Data, offset: Int, count: Int) -> Data { - var result = Data(count: count) - result.withUnsafeMutableBytes { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count) - } - } - return result -} - -private func readUInt32(data: Data, offset: Int) -> UInt32 { - var value: UInt32 = 0 - withUnsafeMutableBytes(of: &value, { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) - } - }) - - return value -} - -private func readFloat32(data: Data, offset: Int) -> Float32 { - var value: Float32 = 0 - withUnsafeMutableBytes(of: &value, { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) - } - }) - - return value -} - -private func writeUInt32(data: inout Data, value: UInt32) { - var value: UInt32 = value - withUnsafeBytes(of: &value, { bytes -> Void in - data.count += 4 - data.withUnsafeMutableBytes { dataBytes -> Void in - memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) - } - }) -} - -private func writeFloat32(data: inout Data, value: Float32) { - var value: Float32 = value - withUnsafeBytes(of: &value, { bytes -> Void in - data.count += 4 - data.withUnsafeMutableBytes { dataBytes -> Void in - memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) - } - }) -} - -private final class CompressedFileWriter { - enum WriteError: Error { - case generic - } - - private let file: ManagedFile - private let stream: UnsafeMutablePointer - - private let tempOutputBufferSize: Int = 64 * 1024 - private let tempOutputBuffer: UnsafeMutablePointer - private let tempInputBufferCapacity: Int = 64 * 1024 - private let tempInputBuffer: UnsafeMutablePointer - private var tempInputBufferSize: Int = 0 - - private var didFail: Bool = false - - init?(file: ManagedFile) { - self.file = file - - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, algorithm) != COMPRESSION_STATUS_ERROR else { - self.stream.deallocate() - return nil - } - - self.tempOutputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempOutputBufferSize) - self.tempInputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempInputBufferCapacity) - } - - deinit { - compression_stream_destroy(self.stream) - self.stream.deallocate() - self.tempOutputBuffer.deallocate() - self.tempInputBuffer.deallocate() - } - - private func flushBuffer() throws { - if self.didFail { - throw WriteError.generic - } - - if self.tempInputBufferSize <= 0 { - return - } - - self.stream.pointee.src_ptr = UnsafePointer(self.tempInputBuffer) - self.stream.pointee.src_size = self.tempInputBufferSize - - while true { - self.stream.pointee.dst_ptr = self.tempOutputBuffer - self.stream.pointee.dst_size = self.tempOutputBufferSize - - let status = compression_stream_process(self.stream, 0) - if status == COMPRESSION_STATUS_ERROR { - self.didFail = true - throw WriteError.generic - } - - let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size - if writtenBytes > 0 { - let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) - } - - if status == COMPRESSION_STATUS_END { - break - } else { - if self.stream.pointee.src_size == 0 { - break - } - } - } - - self.tempInputBufferSize = 0 - } - - func write(bytes: UnsafePointer, count: Int) throws { - var writtenBytes = 0 - while writtenBytes < count { - let availableBytes = self.tempInputBufferCapacity - self.tempInputBufferSize - if availableBytes == 0 { - try flushBuffer() - } else { - let writeCount = min(availableBytes, count - writtenBytes) - - memcpy(self.tempInputBuffer.advanced(by: self.tempInputBufferSize), bytes.advanced(by: writtenBytes), writeCount) - self.tempInputBufferSize += writeCount - writtenBytes += writeCount - } - } - } - - func flush() throws { - if self.didFail { - throw WriteError.generic - } - - try self.flushBuffer() - - while true { - self.stream.pointee.dst_ptr = self.tempOutputBuffer - self.stream.pointee.dst_size = self.tempOutputBufferSize - - let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) - if status == COMPRESSION_STATUS_ERROR { - self.didFail = true - throw WriteError.generic - } - - let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size - if writtenBytes > 0 { - let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) - } - - if status == COMPRESSION_STATUS_END { - break - } - } - } - - func writeUInt32(_ value: UInt32) throws { - var value: UInt32 = value - try withUnsafeBytes(of: &value, { bytes -> Void in - try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - } - - func writeFloat32(_ value: Float32) throws { - var value: Float32 = value - try withUnsafeBytes(of: &value, { bytes -> Void in - try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - } -} - -private final class DecompressedData { - enum ReadError: Error { - case didReadToEnd - } - - private let compressedData: Data - private let dataRange: Range - private let stream: UnsafeMutablePointer - private var isComplete = false - - init?(compressedData: Data, dataRange: Range) { - self.compressedData = compressedData - self.dataRange = dataRange - - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { - self.stream.deallocate() - return nil - } - - self.compressedData.withUnsafeBytes { bytes in - self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) - self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound - } - } - - deinit { - compression_stream_destroy(self.stream) - self.stream.deallocate() - } - - func read(bytes: UnsafeMutablePointer, count: Int) throws { - if self.isComplete { - throw ReadError.didReadToEnd - } - - self.stream.pointee.dst_ptr = bytes - self.stream.pointee.dst_size = count - - let status = compression_stream_process(self.stream, 0) - - if status == COMPRESSION_STATUS_ERROR { - self.isComplete = true - throw ReadError.didReadToEnd - } else if status == COMPRESSION_STATUS_END { - if self.stream.pointee.src_size == 0 { - self.isComplete = true - } - } - - if self.stream.pointee.dst_size != 0 { - throw ReadError.didReadToEnd - } - } - - func readUInt32() throws -> UInt32 { - var value: UInt32 = 0 - try withUnsafeMutableBytes(of: &value, { bytes -> Void in - try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - return value - } - - func readFloat32() throws -> Float32 { - var value: Float32 = 0 - try withUnsafeMutableBytes(of: &value, { bytes -> Void in - try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - return value - } -} - -private enum LoadItemError: Error { - case dataError -} - -private func loadItem(path: String) throws -> AnimationCacheItem { - guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { - throw LoadItemError.dataError - } - - var offset: Int = 0 - let dataLength = compressedData.count - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let formatVersion = readUInt32(data: compressedData, offset: offset) - offset += 4 - if formatVersion != 6 { - throw LoadItemError.dataError - } - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let width = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let height = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctLumaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctLumaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength)) - offset += Int(dctLumaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctChromaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctChromaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength)) - offset += Int(dctChromaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctDeltaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctDeltaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctDeltaData = readData(data: compressedData, offset: offset, count: Int(dctDeltaTableLength)) - offset += Int(dctDeltaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let contentLength = Int(readUInt32(data: compressedData, offset: offset)) - offset += 4 - - let compressedFrameDataRange = offset ..< (offset + contentLength) - offset += contentLength - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let frameCount = Int(readUInt32(data: compressedData, offset: offset)) - offset += 4 - - var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] - for _ in 0 ..< frameCount { - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let frameDuration = readFloat32(data: compressedData, offset: offset) - offset += 4 - - frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) - } - - guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData, deltaTable: dctDeltaData) else { - throw LoadItemError.dataError - } - - let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData) - - return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in - return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) - }, resetImpl: { - itemAccessor.reset() - }) -} - -private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { - guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { - return nil - } - guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { - _ in - }) else { - return nil - } - - do { - for _ in 0 ..< higherResolutionItem.numFrames { - try writer.addYUV(with: { yuva in - guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { - return nil - } - switch frame.frame.format { - case .rgba: - return nil - case let .yuva(y, u, v, a): - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) - } - - return frame.frame.duration - }, proposedWidth: width, proposedHeight: height, insertKeyframe: true) - } - - guard let result = try writer.finishInternal() else { - return nil - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return nil - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return nil - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - - guard let item = try? loadItem(path: itemPath) else { - return nil - } - return item - } catch { - return nil - } -} - -private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { - guard let animationItem = try? loadItem(path: animationItemPath) else { - return false - } - guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { _ in - }) else { - return false - } - - do { - for _ in 0 ..< min(1, animationItem.numFrames) { - guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { - return false - } - switch frame.frame.format { - case .rgba: - return false - case let .yuva(y, u, v, a): - try writer.addYUV(with: { yuva in - assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) - assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) - assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) - assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) - - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) - - return frame.frame.duration - }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) - } - } - - guard let result = try writer.finishInternal() else { - return false - } - - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return false - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - return true - } catch { - return false - } -} - -private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { - var candidates: [(path: String, width: Int, height: Int)] = [] - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { - for url in enumerator { - guard let url = url as? URL else { - continue - } - let fileName = url.lastPathComponent - if fileName.hasPrefix(baseName) { - let scanner = Scanner(string: fileName) - guard scanner.scanString(baseName) != nil else { - continue - } - guard let itemWidth = scanner.scanInt() else { - continue - } - guard scanner.scanString("x") != nil else { - continue - } - guard let itemHeight = scanner.scanInt() else { - continue - } - if !baseSuffix.isEmpty { - guard scanner.scanString(baseSuffix) != nil else { - continue - } - } - guard scanner.isAtEnd else { - continue - } - if itemWidth > width && itemHeight > height { - candidates.append((url.path, itemWidth, itemHeight)) - } - } - } - } - if !candidates.isEmpty { - candidates.sort(by: { $0.width < $1.width }) - return candidates[0].path - } - return nil -} - -public final class AnimationCacheImpl: AnimationCache { - private final class Impl { - private struct ItemKey: Hashable { - var id: String - var width: Int - var height: Int - } - - private final class ItemContext { - let subscribers = Bag<(AnimationCacheItemResult) -> Void>() - let disposable = MetaDisposable() - - deinit { - self.disposable.dispose() - } - } - - private let queue: Queue - private let basePath: String - private let allocateTempFile: () -> String - private let updateStorageStats: (String, Int64) -> Void - - private let fetchQueues: [Queue] - private var nextFetchQueueIndex: Int = 0 - - private var itemContexts: [ItemKey: ItemContext] = [:] - - init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { - self.queue = queue - - let fetchQueueCount: Int - if ProcessInfo.processInfo.processorCount > 2 { - fetchQueueCount = 3 - } else { - fetchQueueCount = 2 - } - - self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) } - self.basePath = basePath - self.allocateTempFile = allocateTempFile - self.updateStorageStats = updateStorageStats - } - - deinit { - } - - func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" - let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { - updateResult(AnimationCacheItemResult(item: item, isFinal: true)) - - return EmptyDisposable - } - let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) - - let itemContext: ItemContext - var beginFetch = false - if let current = self.itemContexts[key] { - itemContext = current - } else { - itemContext = ItemContext() - self.itemContexts[key] = itemContext - beginFetch = true - } - - let queue = self.queue - let index = itemContext.subscribers.add(updateResult) - - updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) - - if beginFetch { - let fetchQueueIndex = self.nextFetchQueueIndex - self.nextFetchQueueIndex += 1 - let allocateTempFile = self.allocateTempFile - let updateStorageStats = self.updateStorageStats - guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in - queue.async { - guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { - return - } - - strongSelf.itemContexts.removeValue(forKey: key) - - guard let result = result else { - return - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - - let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) - - for f in itemContext.subscribers.copyItems() { - guard let item = try? loadItem(path: itemPath) else { - continue - } - f(AnimationCacheItemResult(item: item, isFinal: true)) - } - } - }) else { - return EmptyDisposable - } - - let fetchDisposable = MetaDisposable() - fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) - - itemContext.disposable.set(ActionDisposable { [weak writer] in - if let writer = writer { - writer.isCancelled = true - } - - fetchDisposable.dispose() - }) - } - - return ActionDisposable { [weak self, weak itemContext] in - queue.async { - guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { - return - } - itemContext.subscribers.remove(index) - if itemContext.subscribers.isEmpty { - itemContext.disposable.dispose() - strongSelf.itemContexts.removeValue(forKey: key) - } - } - } - } - - static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { - let hashString = md5Hash(sourceId) - let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemFirstFramePath) { - if let item = try? loadItem(path: itemFirstFramePath) { - return item - } - } - - if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { - return adaptedItem - } - } - - return nil - } - - static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let hashString = md5Hash(sourceId) - let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { - completion(AnimationCacheItemResult(item: item, isFinal: true)) - return EmptyDisposable - } - - if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { - completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) - return EmptyDisposable - } - } - - if let fetch = fetch { - completion(AnimationCacheItemResult(item: nil, isFinal: false)) - - guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in - queue.async { - guard let result = result else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemFirstFramePath) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - if let size = fileSize(itemFirstFramePath) { - updateStorageStats(itemFirstFramePath, size) - } - guard let item = try? loadItem(path: itemFirstFramePath) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - - completion(AnimationCacheItemResult(item: item, isFinal: true)) - } - }) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return EmptyDisposable - } - - let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) - return fetchDisposable - } else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return EmptyDisposable - } - } - } - - private let queue: Queue - private let basePath: String - private let impl: QueueLocalObject - private let allocateTempFile: () -> String - private let updateStorageStats: (String, Int64) -> Void - - public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { - let queue = Queue() - self.queue = queue - self.basePath = basePath - self.allocateTempFile = allocateTempFile - self.updateStorageStats = updateStorageStats - self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) - }) - } - - public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { - return Signal { subscriber in - let disposable = MetaDisposable() - - self.impl.with { impl in - disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in - subscriber.putNext(result) - if result.isFinal { - subscriber.putCompletion() - } - })) - } - - return disposable - } - |> runOn(self.queue) - } - - public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { - return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) - } - - public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let disposable = MetaDisposable() - - let basePath = self.basePath - let allocateTempFile = self.allocateTempFile - let updateStorageStats = self.updateStorageStats - queue.async { - disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) - } - - return disposable - } -} diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index 6c8e0d451e..18aedceb49 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -42,6 +42,8 @@ swift_library( "//submodules/AttachmentTextInputPanelNode", "//submodules/TelegramUI/Components/BatchVideoRendering", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl", + "//submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl:SubcodecMultiAnimationRendererImpl", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD new file mode 100644 index 0000000000..273ab8aead --- /dev/null +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DCTAnimationCacheImpl", + module_name = "DCTAnimationCacheImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/CryptoUtils:CryptoUtils", + "//submodules/ManagedFile:ManagedFile", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift new file mode 100644 index 0000000000..cd2c8f6e37 --- /dev/null +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift @@ -0,0 +1,1558 @@ +import Foundation +import UIKit +import SwiftSignalKit +import CryptoUtils +import ManagedFile +import Compression +import AnimationCache + +private let algorithm: compression_algorithm = COMPRESSION_LZFSE + +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + +private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { + if useTotalFileAllocatedSize { + let url = URL(fileURLWithPath: path) + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { + if values.isRegularFile ?? false { + if let fileSize = values.fileAllocatedSize { + return Int64(fileSize) + } + } + } + } + + var value = stat() + if stat(path, &value) == 0 { + return value.st_size + } else { + return nil + } +} + +private func md5Hash(_ string: String) -> String { + let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return hashData.withUnsafeBytes { bytes -> String in + let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) + } +} + +private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { + assert(hashString.count == 32) + var directory = "" + + for i in 0 ..< 1 { + if !directory.isEmpty { + directory.append("/") + } + directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) + } + + return (directory, "\(hashString)_\(width)x\(height)") +} + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound; + } + + return numToRound + multiple - remainder +} + +private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { + let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + let headerSize = addSizeHeader ? 4 : 0 + var compressedData = Data(count: headerSize + data.count + 16 * 1024) + let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + + if addSizeHeader { + var decompressedSize: UInt32 = UInt32(data.count) + memcpy(bytes, &decompressedSize, 4) + } + + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + compressedData.count = headerSize + resultSize + return compressedData +} + +private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { + let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + var decompressedFrameData = Data(count: decompressedSize) + let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + if decompressedFrameData.count != resultSize { + decompressedFrameData.count = resultSize + } + return decompressedFrameData +} + +private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { + enum WriteError: Error { + case generic + } + + struct CompressedResult { + var animationPath: String + } + + private struct FrameMetadata { + var duration: Double + } + + var queue: Queue { + return self.innerQueue + } + let innerQueue: Queue + var isCancelled: Bool = false + + private let compressedPath: String + private var file: ManagedFile? + private var compressedWriter: CompressedFileWriter? + private let completion: (CompressedResult?) -> Void + + + private var currentSurface: ImageARGB? + private var currentYUVASurface: ImageYUVA420? + private var currentFrameFloat: FloatCoefficientsYUVA420? + private var previousFrameCoefficients: DctCoefficientsYUVA420? + private var deltaFrameFloat: FloatCoefficientsYUVA420? + private var previousYUVASurface: ImageYUVA420? + private var currentDctData: DctData? + private var differenceCoefficients: DctCoefficientsYUVA420? + private var currentDctCoefficients: DctCoefficientsYUVA420? + private var contentLengthOffset: Int? + private var isFailed: Bool = false + private var isFinished: Bool = false + + private var frames: [FrameMetadata] = [] + + private let dctQualityLuma: Int + private let dctQualityChroma: Int + private let dctQualityDelta: Int + + private let lock = Lock() + + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.dctQualityLuma = 70 + self.dctQualityChroma = 88 + self.dctQualityDelta = 22 + + self.innerQueue = queue + self.compressedPath = allocateTempFile() + + guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + return nil + } + self.file = file + self.compressedWriter = CompressedFileWriter(file: file) + self.completion = completion + } + + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { + do { + try self.lock.throwingLocked { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + let surface: ImageARGB + if let current = self.currentSurface { + if current.argbPlane.width == width && current.argbPlane.height == height { + surface = current + surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in + memset(bytes.baseAddress!, 0, bytes.count) + } + } else { + self.isFailed = true + return + } + } else { + surface = ImageARGB(width: width, height: height, rowAlignment: 32) + self.currentSurface = surface + } + + let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in + return drawingBlock(AnimationCacheItemDrawingSurface( + argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), + width: width, + height: height, + bytesPerRow: surface.argbPlane.bytesPerRow, + length: bytes.count + )) + } + + guard let duration = duration else { + return + } + + try addInternal(with: { yuvaSurface in + surface.toYUVA420(target: yuvaSurface) + + return duration + }, width: width, height: height, insertKeyframe: insertKeyframe) + } + } catch { + } + } + + func addYUV(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) throws { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + do { + try self.lock.throwingLocked { + try addInternal(with: { yuvaSurface in + return drawingBlock(yuvaSurface) + }, width: width, height: height, insertKeyframe: insertKeyframe) + } + } catch { + } + } + + func addInternal(with drawingBlock: (ImageYUVA420) -> Double?, width: Int, height: Int, insertKeyframe: Bool) throws { + if width == 0 || height == 0 { + self.isFailed = true + throw WriteError.generic + } + if self.isFailed || self.isFinished { + throw WriteError.generic + } + + guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { + throw WriteError.generic + } + + var isFirstFrame = false + + let yuvaSurface: ImageYUVA420 + if let current = self.currentYUVASurface { + if current.yPlane.width == width && current.yPlane.height == height { + yuvaSurface = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + isFirstFrame = true + + yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) + self.currentYUVASurface = yuvaSurface + } + + let currentFrameFloat: FloatCoefficientsYUVA420 + if let current = self.currentFrameFloat { + if current.yPlane.width == width && current.yPlane.height == height { + currentFrameFloat = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + currentFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) + self.currentFrameFloat = currentFrameFloat + } + + let previousFrameCoefficients: DctCoefficientsYUVA420 + if let current = self.previousFrameCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + previousFrameCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + previousFrameCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.previousFrameCoefficients = previousFrameCoefficients + } + + let deltaFrameFloat: FloatCoefficientsYUVA420 + if let current = self.deltaFrameFloat { + if current.yPlane.width == width && current.yPlane.height == height { + deltaFrameFloat = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + deltaFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) + self.deltaFrameFloat = deltaFrameFloat + } + + let dctData: DctData + if let current = self.currentDctData { + dctData = current + } else { + dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma, delta: self.dctQualityDelta) + self.currentDctData = dctData + } + + let duration = drawingBlock(yuvaSurface) + + guard let duration = duration else { + return + } + + let dctCoefficients: DctCoefficientsYUVA420 + if let current = self.currentDctCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + dctCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.currentDctCoefficients = dctCoefficients + } + + let differenceCoefficients: DctCoefficientsYUVA420 + if let current = self.differenceCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + differenceCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + differenceCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.differenceCoefficients = differenceCoefficients + } + + #if !arch(arm64) + var insertKeyframe = insertKeyframe + insertKeyframe = true + #endif + + let previousYUVASurface: ImageYUVA420 + if let current = self.previousYUVASurface { + previousYUVASurface = current + } else { + previousYUVASurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) + self.previousYUVASurface = previousYUVASurface + } + + let isKeyframe: Bool + if !isFirstFrame && !insertKeyframe { + isKeyframe = false + + //previous + delta = current + //delta = current - previous + yuvaSurface.toCoefficients(target: differenceCoefficients) + differenceCoefficients.subtract(other: previousFrameCoefficients) + differenceCoefficients.dct4x4(dctData: dctData, target: dctCoefficients) + + //previous + delta = current + dctCoefficients.idct4x4Add(dctData: dctData, target: previousFrameCoefficients) + //previousFrameCoefficients.add(other: differenceCoefficients) + } else { + isKeyframe = true + + yuvaSurface.dct8x8(dctData: dctData, target: dctCoefficients) + + dctCoefficients.idct8x8(dctData: dctData, target: yuvaSurface) + yuvaSurface.toCoefficients(target: previousFrameCoefficients) + } + + if isFirstFrame { + file.write(6 as UInt32) + + file.write(UInt32(dctCoefficients.yPlane.width)) + file.write(UInt32(dctCoefficients.yPlane.height)) + + let lumaDctTable = dctData.lumaTable.serializedData() + file.write(UInt32(lumaDctTable.count)) + let _ = file.write(lumaDctTable) + + let chromaDctTable = dctData.chromaTable.serializedData() + file.write(UInt32(chromaDctTable.count)) + let _ = file.write(chromaDctTable) + + let deltaDctTable = dctData.deltaTable.serializedData() + file.write(UInt32(deltaDctTable.count)) + let _ = file.write(deltaDctTable) + + self.contentLengthOffset = Int(file.position()) + file.write(0 as UInt32) + } + + do { + let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count + try compressedWriter.writeUInt32(UInt32(frameLength)) + + try compressedWriter.writeUInt32(isKeyframe ? 1 : 0) + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) + try dctPlane.data.withUnsafeBytes { bytes in + try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + } + + self.frames.append(FrameMetadata(duration: duration)) + } catch { + self.isFailed = true + throw WriteError.generic + } + } + + func finish() { + do { + let result = try self.finishInternal() + self.completion(result) + } catch { + } + } + + func finishInternal() throws -> CompressedResult? { + var shouldComplete = false + self.lock.locked { + if !self.isFinished { + self.isFinished = true + shouldComplete = true + + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { + self.isFailed = true + return + } + assert(contentLengthOffset >= 0) + + do { + try compressedWriter.flush() + + let metadataPosition = file.position() + let contentLength = Int(metadataPosition) - contentLengthOffset - 4 + let _ = file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(contentLength)) + + let _ = file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(Float32(frame.duration)) + } + + if !self.isFailed { + self.compressedWriter = nil + self.file = nil + + file._unsafeClose() + } + } catch { + self.isFailed = true + } + } + } + + if shouldComplete { + if !self.isFailed { + return CompressedResult(animationPath: self.compressedPath) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) + return nil + } + } else { + return nil + } + } +} + +private final class AnimationCacheItemAccessor { + private enum ReadError: Error { + case generic + } + + final class CurrentFrame { + let index: Int + var remainingDuration: Double + let duration: Double + let yuva: ImageYUVA420 + + init(index: Int, duration: Double, yuva: ImageYUVA420) { + self.index = index + self.duration = duration + self.remainingDuration = duration + self.yuva = yuva + } + } + + struct FrameInfo { + let duration: Double + } + + private let data: Data + private var compressedDataReader: DecompressedData? + private let range: Range + private let frameMapping: [Int: FrameInfo] + private let width: Int + private let height: Int + private let durationMapping: [Double] + + private var currentFrame: CurrentFrame? + + private var currentYUVASurface: ImageYUVA420? + private var currentCoefficients: DctCoefficientsYUVA420? + private let currentDctData: DctData + private var sharedDctCoefficients: DctCoefficientsYUVA420? + private var deltaCoefficients: DctCoefficientsYUVA420? + + init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) { + self.data = data + self.range = range + self.width = width + self.height = height + + var resultFrameMapping: [Int: FrameInfo] = [:] + var durationMapping: [Double] = [] + + for i in 0 ..< frameMapping.count { + let frame = frameMapping[i] + resultFrameMapping[i] = frame + durationMapping.append(frame.duration) + } + + self.frameMapping = resultFrameMapping + self.durationMapping = durationMapping + + self.currentDctData = dctData + } + + private func loadNextFrame() -> Bool { + var didLoop = false + let index: Int + if let currentFrame = self.currentFrame { + if currentFrame.index + 1 >= self.durationMapping.count { + index = 0 + self.compressedDataReader = nil + didLoop = true + } else { + index = currentFrame.index + 1 + } + } else { + index = 0 + self.compressedDataReader = nil + } + + if self.compressedDataReader == nil { + self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) + } + + guard let compressedDataReader = self.compressedDataReader else { + self.currentFrame = nil + return didLoop + } + + do { + let frameLength = Int(try compressedDataReader.readUInt32()) + + let frameType = Int(try compressedDataReader.readUInt32()) + + let dctCoefficients: DctCoefficientsYUVA420 + if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height, !"".isEmpty { + dctCoefficients = sharedDctCoefficients + } else { + dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) + self.sharedDctCoefficients = dctCoefficients + } + + var frameOffset = 0 + for i in 0 ..< 4 { + let planeLength = Int(try compressedDataReader.readUInt32()) + if planeLength < 0 || planeLength > 20 * 1024 * 1024 { + throw ReadError.generic + } + + let plane: DctCoefficientPlane + switch i { + case 0: + plane = dctCoefficients.yPlane + case 1: + plane = dctCoefficients.uPlane + case 2: + plane = dctCoefficients.vPlane + case 3: + plane = dctCoefficients.aPlane + default: + throw ReadError.generic + } + + if planeLength != plane.data.count { + throw ReadError.generic + } + + if frameOffset + plane.data.count > frameLength { + throw ReadError.generic + } + + try plane.data.withUnsafeMutableBytes { bytes in + try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameOffset += plane.data.count + } + + let yuvaSurface: ImageYUVA420 + if let currentYUVASurface = self.currentYUVASurface { + yuvaSurface = currentYUVASurface + } else { + yuvaSurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) + } + + let currentCoefficients: DctCoefficientsYUVA420 + if let current = self.currentCoefficients { + currentCoefficients = current + } else { + currentCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) + self.currentCoefficients = currentCoefficients + } + + /*let deltaCoefficients: DctCoefficientsYUVA420 + if let current = self.deltaCoefficients { + deltaCoefficients = current + } else { + deltaCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) + self.deltaCoefficients = deltaCoefficients + }*/ + + switch frameType { + case 1: + dctCoefficients.idct8x8(dctData: self.currentDctData, target: yuvaSurface) + yuvaSurface.toCoefficients(target: currentCoefficients) + default: + dctCoefficients.idct4x4Add(dctData: self.currentDctData, target: currentCoefficients) + //currentCoefficients.add(other: deltaCoefficients) + + currentCoefficients.toYUVA420(target: yuvaSurface) + } + + self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], yuva: yuvaSurface) + } catch { + self.currentFrame = nil + self.compressedDataReader = nil + } + + return didLoop + } + + func reset() { + self.currentFrame = nil + } + + func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? { + var didLoop = false + switch advance { + case let .frames(count): + for _ in 0 ..< count { + if self.loadNextFrame() { + didLoop = true + } + } + case let .duration(duration): + var durationOverflow = duration + while true { + if let currentFrame = self.currentFrame { + currentFrame.remainingDuration -= durationOverflow + if currentFrame.remainingDuration <= 0.0 { + durationOverflow = -currentFrame.remainingDuration + if self.loadNextFrame() { + didLoop = true + } + } else { + break + } + } else { + if self.loadNextFrame() { + didLoop = true + } + break + } + } + } + + guard let currentFrame = self.currentFrame else { + return nil + } + + switch requestedFormat { + case .rgba: + let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) + currentFrame.yuva.toARGB(target: currentSurface) + + return AnimationCacheItem.AdvanceResult( + frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration), + didLoop: didLoop + ) + case .yuva: + return AnimationCacheItem.AdvanceResult( + frame: AnimationCacheItemFrame( + format: .yuva( + y: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.yPlane.data, + width: currentFrame.yuva.yPlane.width, + height: currentFrame.yuva.yPlane.height, + bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow + ), + u: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.uPlane.data, + width: currentFrame.yuva.uPlane.width, + height: currentFrame.yuva.uPlane.height, + bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow + ), + v: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.vPlane.data, + width: currentFrame.yuva.vPlane.width, + height: currentFrame.yuva.vPlane.height, + bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow + ), + a: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.aPlane.data, + width: currentFrame.yuva.aPlane.width, + height: currentFrame.yuva.aPlane.height, + bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow + ) + ), + duration: currentFrame.duration + ), + didLoop: didLoop + ) + } + } +} + +private func readData(data: Data, offset: Int, count: Int) -> Data { + var result = Data(count: count) + result.withUnsafeMutableBytes { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count) + } + } + return result +} + +private func readUInt32(data: Data, offset: Int) -> UInt32 { + var value: UInt32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func readFloat32(data: Data, offset: Int) -> Float32 { + var value: Float32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func writeUInt32(data: inout Data, value: UInt32) { + var value: UInt32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private func writeFloat32(data: inout Data, value: Float32) { + var value: Float32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private final class CompressedFileWriter { + enum WriteError: Error { + case generic + } + + private let file: ManagedFile + private let stream: UnsafeMutablePointer + + private let tempOutputBufferSize: Int = 64 * 1024 + private let tempOutputBuffer: UnsafeMutablePointer + private let tempInputBufferCapacity: Int = 64 * 1024 + private let tempInputBuffer: UnsafeMutablePointer + private var tempInputBufferSize: Int = 0 + + private var didFail: Bool = false + + init?(file: ManagedFile) { + self.file = file + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, algorithm) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.tempOutputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempOutputBufferSize) + self.tempInputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempInputBufferCapacity) + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + self.tempOutputBuffer.deallocate() + self.tempInputBuffer.deallocate() + } + + private func flushBuffer() throws { + if self.didFail { + throw WriteError.generic + } + + if self.tempInputBufferSize <= 0 { + return + } + + self.stream.pointee.src_ptr = UnsafePointer(self.tempInputBuffer) + self.stream.pointee.src_size = self.tempInputBufferSize + + while true { + self.stream.pointee.dst_ptr = self.tempOutputBuffer + self.stream.pointee.dst_size = self.tempOutputBufferSize + + let status = compression_stream_process(self.stream, 0) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } else { + if self.stream.pointee.src_size == 0 { + break + } + } + } + + self.tempInputBufferSize = 0 + } + + func write(bytes: UnsafePointer, count: Int) throws { + var writtenBytes = 0 + while writtenBytes < count { + let availableBytes = self.tempInputBufferCapacity - self.tempInputBufferSize + if availableBytes == 0 { + try flushBuffer() + } else { + let writeCount = min(availableBytes, count - writtenBytes) + + memcpy(self.tempInputBuffer.advanced(by: self.tempInputBufferSize), bytes.advanced(by: writtenBytes), writeCount) + self.tempInputBufferSize += writeCount + writtenBytes += writeCount + } + } + } + + func flush() throws { + if self.didFail { + throw WriteError.generic + } + + try self.flushBuffer() + + while true { + self.stream.pointee.dst_ptr = self.tempOutputBuffer + self.stream.pointee.dst_size = self.tempOutputBufferSize + + let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } + } + } + + func writeUInt32(_ value: UInt32) throws { + var value: UInt32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } + + func writeFloat32(_ value: Float32) throws { + var value: Float32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } +} + +private final class DecompressedData { + enum ReadError: Error { + case didReadToEnd + } + + private let compressedData: Data + private let dataRange: Range + private let stream: UnsafeMutablePointer + private var isComplete = false + + init?(compressedData: Data, dataRange: Range) { + self.compressedData = compressedData + self.dataRange = dataRange + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.compressedData.withUnsafeBytes { bytes in + self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) + self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound + } + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + } + + func read(bytes: UnsafeMutablePointer, count: Int) throws { + if self.isComplete { + throw ReadError.didReadToEnd + } + + self.stream.pointee.dst_ptr = bytes + self.stream.pointee.dst_size = count + + let status = compression_stream_process(self.stream, 0) + + if status == COMPRESSION_STATUS_ERROR { + self.isComplete = true + throw ReadError.didReadToEnd + } else if status == COMPRESSION_STATUS_END { + if self.stream.pointee.src_size == 0 { + self.isComplete = true + } + } + + if self.stream.pointee.dst_size != 0 { + throw ReadError.didReadToEnd + } + } + + func readUInt32() throws -> UInt32 { + var value: UInt32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } + + func readFloat32() throws -> Float32 { + var value: Float32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } +} + +private enum LoadItemError: Error { + case dataError +} + +private func loadItem(path: String) throws -> AnimationCacheItem { + guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { + throw LoadItemError.dataError + } + + var offset: Int = 0 + let dataLength = compressedData.count + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let formatVersion = readUInt32(data: compressedData, offset: offset) + offset += 4 + if formatVersion != 6 { + throw LoadItemError.dataError + } + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let width = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let height = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctLumaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctLumaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength)) + offset += Int(dctLumaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctChromaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctChromaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength)) + offset += Int(dctChromaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctDeltaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctDeltaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctDeltaData = readData(data: compressedData, offset: offset, count: Int(dctDeltaTableLength)) + offset += Int(dctDeltaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let contentLength = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + let compressedFrameDataRange = offset ..< (offset + contentLength) + offset += contentLength + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameCount = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] + for _ in 0 ..< frameCount { + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameDuration = readFloat32(data: compressedData, offset: offset) + offset += 4 + + frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) + } + + guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData, deltaTable: dctDeltaData) else { + throw LoadItemError.dataError + } + + let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData) + + return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in + return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) + }, resetImpl: { + itemAccessor.reset() + }) +} + +private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { + guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { + return nil + } + guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { + _ in + }) else { + return nil + } + + do { + for _ in 0 ..< higherResolutionItem.numFrames { + try writer.addYUV(with: { yuva in + guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { + return nil + } + switch frame.frame.format { + case .rgba: + return nil + case let .yuva(y, u, v, a): + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + } + + return frame.frame.duration + }, proposedWidth: width, proposedHeight: height, insertKeyframe: true) + } + + guard let result = try writer.finishInternal() else { + return nil + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return nil + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return nil + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + + guard let item = try? loadItem(path: itemPath) else { + return nil + } + return item + } catch { + return nil + } +} + +private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { + guard let animationItem = try? loadItem(path: animationItemPath) else { + return false + } + guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { _ in + }) else { + return false + } + + do { + for _ in 0 ..< min(1, animationItem.numFrames) { + guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { + return false + } + switch frame.frame.format { + case .rgba: + return false + case let .yuva(y, u, v, a): + try writer.addYUV(with: { yuva in + assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) + assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) + assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) + assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) + + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + + return frame.frame.duration + }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) + } + } + + guard let result = try writer.finishInternal() else { + return false + } + + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return false + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + return true + } catch { + return false + } +} + +private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { + var candidates: [(path: String, width: Int, height: Int)] = [] + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + let fileName = url.lastPathComponent + if fileName.hasPrefix(baseName) { + let scanner = Scanner(string: fileName) + guard scanner.scanString(baseName) != nil else { + continue + } + guard let itemWidth = scanner.scanInt() else { + continue + } + guard scanner.scanString("x") != nil else { + continue + } + guard let itemHeight = scanner.scanInt() else { + continue + } + if !baseSuffix.isEmpty { + guard scanner.scanString(baseSuffix) != nil else { + continue + } + } + guard scanner.isAtEnd else { + continue + } + if itemWidth > width && itemHeight > height { + candidates.append((url.path, itemWidth, itemHeight)) + } + } + } + } + if !candidates.isEmpty { + candidates.sort(by: { $0.width < $1.width }) + return candidates[0].path + } + return nil +} + +public final class DCTAnimationCacheImpl: AnimationCache { + private final class Impl { + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + } + + private final class ItemContext { + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() + let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + } + + private let queue: Queue + private let basePath: String + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + + private var itemContexts: [ItemKey: ItemContext] = [:] + + init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + self.queue = queue + + let fetchQueueCount: Int + if ProcessInfo.processInfo.processorCount > 2 { + fetchQueueCount = 3 + } else { + fetchQueueCount = 2 + } + + self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "DCTAnimationCacheImpl-Fetch\(i)", qos: .default) } + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + } + + deinit { + } + + func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) + + return EmptyDisposable + } + let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) + + let itemContext: ItemContext + var beginFetch = false + if let current = self.itemContexts[key] { + itemContext = current + } else { + itemContext = ItemContext() + self.itemContexts[key] = itemContext + beginFetch = true + } + + let queue = self.queue + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) + + if beginFetch { + let fetchQueueIndex = self.nextFetchQueueIndex + self.nextFetchQueueIndex += 1 + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + + strongSelf.itemContexts.removeValue(forKey: key) + + guard let result = result else { + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + + let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + + for f in itemContext.subscribers.copyItems() { + guard let item = try? loadItem(path: itemPath) else { + continue + } + f(AnimationCacheItemResult(item: item, isFinal: true)) + } + } + }) else { + return EmptyDisposable + } + + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) + + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + + fetchDisposable.dispose() + }) + } + + return ActionDisposable { [weak self, weak itemContext] in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + itemContext.subscribers.remove(index) + if itemContext.subscribers.isEmpty { + itemContext.disposable.dispose() + strongSelf.itemContexts.removeValue(forKey: key) + } + } + } + } + + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath) { + if let item = try? loadItem(path: itemFirstFramePath) { + return item + } + } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { + return adaptedItem + } + } + + return nil + } + + static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { + completion(AnimationCacheItemResult(item: item, isFinal: true)) + return EmptyDisposable + } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { + completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) + return EmptyDisposable + } + } + + if let fetch = fetch { + completion(AnimationCacheItemResult(item: nil, isFinal: false)) + + guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in + queue.async { + guard let result = result else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemFirstFramePath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + if let size = fileSize(itemFirstFramePath) { + updateStorageStats(itemFirstFramePath, size) + } + guard let item = try? loadItem(path: itemFirstFramePath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + + completion(AnimationCacheItemResult(item: item, isFinal: true)) + } + }) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return EmptyDisposable + } + + let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) + return fetchDisposable + } else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return EmptyDisposable + } + } + } + + private let queue: Queue + private let basePath: String + private let impl: QueueLocalObject + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + let queue = Queue() + self.queue = queue + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + }) + } + + public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in + subscriber.putNext(result) + if result.isFinal { + subscriber.putCompletion() + } + })) + } + + return disposable + } + |> runOn(self.queue) + } + + public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) + } + + public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let disposable = MetaDisposable() + + let basePath = self.basePath + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + queue.async { + disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) + } + + return disposable + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift similarity index 99% rename from submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift rename to submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift index 66c853510f..7dfaecfc0b 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AnimationCache import ImageDCT import Accelerate diff --git a/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD new file mode 100644 index 0000000000..6c27e9c86b --- /dev/null +++ b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DCTMultiAnimationRendererImpl", + module_name = "DCTMultiAnimationRendererImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift new file mode 100644 index 0000000000..8ab7c6f9d0 --- /dev/null +++ b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift @@ -0,0 +1,1078 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import Accelerate +import IOSurface + +private final class LoadFrameGroupTask { + let task: () -> () -> Void + let queueAffinity: Int + + init(task: @escaping () -> () -> Void, queueAffinity: Int) { + self.task = task + self.queueAffinity = queueAffinity + } +} + +private var yuvToRgbConversion: vImage_YpCbCrToARGB = { + var info = vImage_YpCbCrToARGB() + var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0) + vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0) + return info +}() + +private final class ItemAnimationContext { + fileprivate final class Frame { + let frame: AnimationCacheItemFrame + let duration: Double + + let contentsAsImage: UIImage? + let contentsAsCVPixelBuffer: CVPixelBuffer? + + let size: CGSize + + var remainingDuration: Double + + private var blurredRepresentationValue: UIImage? + + init?(frame: AnimationCacheItemFrame) { + self.frame = frame + self.duration = frame.duration + self.remainingDuration = frame.duration + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { + return nil + } + + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + + guard let image = context.generateImage() else { + return nil + } + + self.contentsAsImage = image + self.contentsAsCVPixelBuffer = nil + self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) + case let .yuva(y, u, v, a): + var pixelBuffer: CVPixelBuffer? = nil + let _ = CVPixelBufferCreate(kCFAllocatorDefault, y.width, y.height, kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, [ + kCVPixelBufferIOSurfacePropertiesKey: NSDictionary() + ] as CFDictionary, &pixelBuffer) + guard let pixelBuffer else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + } + guard let baseAddressY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return nil + } + guard let baseAddressCbCr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return nil + } + guard let baseAddressA = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else { + return nil + } + + let dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressY), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)) + let dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressCbCr), height: vImagePixelCount(y.height / 2), width: vImagePixelCount(y.width / 2), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)) + let dstBufferA = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressA), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)) + + y.data.withUnsafeBytes { (yBytes: UnsafeRawBufferPointer) -> Void in + if dstBufferY.rowBytes == y.bytesPerRow { + memcpy(dstBufferY.data, yBytes.baseAddress!, yBytes.count) + } else { + for i in 0 ..< y.height { + memcpy(dstBufferY.data.advanced(by: dstBufferY.rowBytes * i), yBytes.baseAddress!.advanced(by: y.bytesPerRow * i), y.bytesPerRow) + } + } + } + + a.data.withUnsafeBytes { (aBytes: UnsafeRawBufferPointer) -> Void in + if dstBufferA.rowBytes == a.bytesPerRow { + memcpy(dstBufferA.data, aBytes.baseAddress!, aBytes.count) + } else { + for i in 0 ..< y.height { + memcpy(dstBufferA.data.advanced(by: dstBufferA.rowBytes * i), aBytes.baseAddress!.advanced(by: a.bytesPerRow * i), a.bytesPerRow) + } + } + } + + u.data.withUnsafeBytes { (uBytes: UnsafeRawBufferPointer) -> Void in + v.data.withUnsafeBytes { (vBytes: UnsafeRawBufferPointer) -> Void in + let sourceU = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: uBytes.baseAddress!), + height: vImagePixelCount(u.height), + width: vImagePixelCount(u.width), + rowBytes: u.bytesPerRow + ) + let sourceV = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: vBytes.baseAddress!), + height: vImagePixelCount(v.height), + width: vImagePixelCount(v.width), + rowBytes: v.bytesPerRow + ) + + withUnsafePointer(to: sourceU, { sourceU in + withUnsafePointer(to: sourceV, { sourceV in + var srcPlanarBuffers: [ + UnsafePointer? + ] = [sourceU, sourceV] + var destChannels: [UnsafeMutableRawPointer?] = [ + dstBufferCbCr.data.advanced(by: 1), + dstBufferCbCr.data + ] + + let channelCount = 2 + + vImageConvert_PlanarToChunky8( + &srcPlanarBuffers, + &destChannels, + UInt32(channelCount), + MemoryLayout.stride * channelCount, + vImagePixelCount(u.width), + vImagePixelCount(u.height), + dstBufferCbCr.rowBytes, + vImage_Flags(kvImageDoNotTile) + ) + }) + }) + } + } + + self.contentsAsImage = nil + self.contentsAsCVPixelBuffer = pixelBuffer + self.size = CGSize(width: CGFloat(y.width), height: CGFloat(y.height)) + } + } + + func blurredRepresentation(color: UIColor?) -> UIImage? { + if let blurredRepresentationValue = self.blurredRepresentationValue { + return blurredRepresentationValue + } + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + let blurredWidth = 12 + let blurredHeight = 12 + guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) else { + return nil + } + + let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) + + data.withUnsafeBytes { bytes -> Void in + if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { + let image = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) + if let image = image { + context.withFlippedContext { c in + c.setFillColor((color ?? .white).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + c.draw(image, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) + } + } + } + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(blurredWidth) + destinationBuffer.height = UInt(blurredHeight) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = context.bytesPerRow + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + + context.withFlippedContext { c in + c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + case let .yuva(y, u, v, a): + let blurredWidth = 12 + let blurredHeight = 12 + let size = CGSize(width: blurredWidth, height: blurredHeight) + + var sourceY = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: y.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(y.height), + width: vImagePixelCount(y.width), + rowBytes: y.bytesPerRow + ) + + var sourceU = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: u.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(u.height), + width: vImagePixelCount(u.width), + rowBytes: u.bytesPerRow + ) + + var sourceV = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: v.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(v.height), + width: vImagePixelCount(v.width), + rowBytes: v.bytesPerRow + ) + + var sourceA = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: a.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(a.height), + width: vImagePixelCount(a.width), + rowBytes: a.bytesPerRow + ) + + let scaledYData = malloc(blurredWidth * blurredHeight)! + defer { + free(scaledYData) + } + + let scaledUData = malloc(blurredWidth * blurredHeight / 4)! + defer { + free(scaledUData) + } + + let scaledVData = malloc(blurredWidth * blurredHeight / 4)! + defer { + free(scaledVData) + } + + let scaledAData = malloc(blurredWidth * blurredHeight)! + defer { + free(scaledAData) + } + + var scaledY = vImage_Buffer( + data: scaledYData, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: blurredWidth + ) + + var scaledU = vImage_Buffer( + data: scaledUData, + height: vImagePixelCount(blurredHeight / 2), + width: vImagePixelCount(blurredWidth / 2), + rowBytes: blurredWidth / 2 + ) + + var scaledV = vImage_Buffer( + data: scaledVData, + height: vImagePixelCount(blurredHeight / 2), + width: vImagePixelCount(blurredWidth / 2), + rowBytes: blurredWidth / 2 + ) + + var scaledA = vImage_Buffer( + data: scaledAData, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: blurredWidth + ) + + vImageScale_Planar8(&sourceY, &scaledY, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceU, &scaledU, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceV, &scaledV, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceA, &scaledA, nil, vImage_Flags(kvImageHighQualityResampling)) + + guard let context = DrawingContext(size: size, scale: 1.0, clear: true) else { + return nil + } + + var destinationBuffer = vImage_Buffer( + data: context.bytes, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: context.bytesPerRow + ) + + var result = kvImageNoError + + var permuteMap: [UInt8] = [1, 2, 3, 0] + result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&scaledY, &scaledU, &scaledV, &destinationBuffer, &yuvToRgbConversion, &permuteMap, 255, vImage_Flags(kvImageDoNotTile)) + if result != kvImageNoError { + return nil + } + + result = vImageOverwriteChannels_ARGB8888(&scaledA, &destinationBuffer, &destinationBuffer, 1 << 0, vImage_Flags(kvImageDoNotTile)); + if result != kvImageNoError { + return nil + } + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + + context.withFlippedContext { c in + c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + } + } + } + + static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default) + static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default) + + private let useYuvA: Bool + + private let cache: AnimationCache + let queueAffinity: Int + private let stateUpdated: () -> Void + + private var disposable: Disposable? + private var displayLink: ConstantDisplayLinkAnimator? + private var item: Atomic? + private var itemPlaceholderAndFrameIndex: (UIImage, Int)? + + private var currentFrame: Frame? + private var loadingFrameTaskId: Int? + private var nextLoadingFrameTaskId: Int = 0 + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + let targets = Bag>() + + init(cache: AnimationCache, queueAffinity: Int, itemId: String, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { + self.cache = cache + self.queueAffinity = queueAffinity + self.useYuvA = useYuvA + self.stateUpdated = stateUpdated + + self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let item = result.item { + strongSelf.item = Atomic(value: item) + } + if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { + strongSelf.itemPlaceholderAndFrameIndex = nil + strongSelf.setFrameIndex(index: index, placeholder: placeholder) + } + strongSelf.updateIsPlaying() + } + }) + } + + deinit { + self.disposable?.dispose() + self.displayLink?.invalidate() + } + + func setFrameIndex(index: Int, placeholder: UIImage) { + if let item = self.item { + let nextFrame = item.with { item -> AnimationCacheItemFrame? in + item.reset() + for i in 0 ... index { + let result = item.advance(advance: .frames(1), requestedFormat: .rgba) + if i == index { + return result?.frame + } + } + return nil + } + + self.loadingFrameTaskId = nil + + if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { + self.currentFrame = currentFrame + + for target in self.targets.copyItems() { + if let target = target.value { + if let image = currentFrame.contentsAsImage { + target.transitionToContents(image.cgImage!, didLoop: false) + } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: false) + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } else { + for target in self.targets.copyItems() { + if let target = target.value { + target.transitionToContents(placeholder.cgImage!, didLoop: false) + } + } + + self.itemPlaceholderAndFrameIndex = (placeholder, index) + } + } + + func updateAddedTarget(target: MultiAnimationRenderTarget) { + if let currentFrame = self.currentFrame { + if let cgImage = currentFrame.contentsAsImage?.cgImage { + target.transitionToContents(cgImage, didLoop: false) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: false) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + + self.updateIsPlaying() + } + + func updateIsPlaying() { + var isPlaying = true + if self.item == nil { + isPlaying = false + } + + var shouldBeAnimating = false + for target in self.targets.copyItems() { + if let target = target.value { + if target.shouldBeAnimating { + shouldBeAnimating = true + break + } + } + } + if !shouldBeAnimating { + isPlaying = false + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { + return self.update(advanceTimestamp: advanceTimestamp) + } + + private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { + guard let item = self.item else { + return nil + } + + var frameAdvance: AnimationCacheItem.Advance? + if self.loadingFrameTaskId == nil { + if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { + let divisionFactor = advanceTimestamp / currentFrame.remainingDuration + let wholeFactor = round(divisionFactor) + if abs(wholeFactor - divisionFactor) < 0.005 { + currentFrame.remainingDuration = 0.0 + frameAdvance = .frames(Int(wholeFactor)) + } else { + currentFrame.remainingDuration -= advanceTimestamp + if currentFrame.remainingDuration <= 0.0 { + frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) + } + } + } else if self.currentFrame == nil { + frameAdvance = .frames(1) + } + } + + if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { + let taskId = self.nextLoadingFrameTaskId + self.nextLoadingFrameTaskId += 1 + + self.loadingFrameTaskId = taskId + let useYuvA = self.useYuvA + + return LoadFrameGroupTask(task: { [weak self] in + let currentFrame: (frame: Frame, didLoop: Bool)? + do { + if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in + let defaultFormat: AnimationCacheItemFrame.RequestedFormat + if useYuvA { + defaultFormat = .yuva(rowAlignment: 1) + } else { + defaultFormat = .rgba + } + + if let result = item.advance(advance: frameAdvance, requestedFormat: defaultFormat) { + return (result.frame, result.didLoop) + } else { + return nil + } + }), let mappedFrame = Frame(frame: frame) { + currentFrame = (mappedFrame, didLoop) + } else { + currentFrame = nil + } + } catch { + assertionFailure() + currentFrame = nil + } + + return { + guard let strongSelf = self else { + return + } + + if strongSelf.loadingFrameTaskId != taskId { + return + } + + strongSelf.loadingFrameTaskId = nil + + if let currentFrame = currentFrame { + strongSelf.currentFrame = currentFrame.frame + for target in strongSelf.targets.copyItems() { + if let target = target.value { + if let image = currentFrame.frame.contentsAsImage { + target.transitionToContents(image.cgImage!, didLoop: currentFrame.didLoop) + } else if let pixelBuffer = currentFrame.frame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: currentFrame.didLoop) + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } + }, queueAffinity: self.queueAffinity) + } + + if let _ = self.currentFrame { + for target in self.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + } + } + } + + return nil + } +} + +public final class DCTMultiAnimationRendererImpl: MultiAnimationRenderer { + private final class GroupContext { + private let firstFrameQueue: Queue + private let stateUpdated: () -> Void + + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + var uniqueId: Int + } + + private var itemContexts: [ItemKey: ItemAnimationContext] = [:] + private var nextQueueAffinity: Int = 0 + private var nextUniqueId: Int = 1 + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { + self.firstFrameQueue = firstFrameQueue + self.stateUpdated = stateUpdated + } + + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + var uniqueId = 0 + if unique { + uniqueId = self.nextUniqueId + self.nextUniqueId += 1 + } + + let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId) + let itemContext: ItemAnimationContext + if let current = self.itemContexts[itemKey] { + itemContext = current + } else { + let queueAffinity = self.nextQueueAffinity + self.nextQueueAffinity += 1 + itemContext = ItemAnimationContext(cache: cache, queueAffinity: queueAffinity, itemId: itemId, size: size, useYuvA: useYuvA, fetch: fetch, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.itemContexts[itemKey] = itemContext + } + + let index = itemContext.targets.add(Weak(target)) + itemContext.updateAddedTarget(target: target) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in + Queue.mainQueue().async { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { + return + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemKey) + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in + guard let itemContext = itemContext else { + return + } + itemContext.updateIsPlaying() + } + + return ActionDisposable { [weak self, weak itemContext, weak target] in + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemKey) + } + }.strict() + } + + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { + guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { + return false + } + guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else { + return false + } + + if let image = loadedFrame.contentsAsImage { + target.contents = image.cgImage + } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { + target.contents = pixelBuffer + } + target.numFrames = item.numFrames + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + + return true + } else { + return false + } + } + + func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + var hadIntermediateUpdate = false + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] item in + guard let item = item.item else { + let isFinal = item.isFinal + hadIntermediateUpdate = true + Queue.mainQueue().async { + completion(false, isFinal) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + guard let target = target else { + completion(false, true) + return + } + target.numFrames = item.numFrames + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.contentsAsImage?.cgImage { + if hadIntermediateUpdate { + target.transitionToContents(cgImage, didLoop: false) + } else { + target.contents = cgImage + } + } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { + if hadIntermediateUpdate { + target.transitionToContents(pixelBuffer, didLoop: false) + } else { + target.contents = pixelBuffer + } + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + + completion(true, true) + } else { + completion(false, true) + } + } + }).strict() + } + + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in + guard let item = item.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.contentsAsImage?.cgImage { + completion(cgImage) + } else { + completion(nil) + } + } else { + completion(nil) + } + } + }).strict() + } + + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { + itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { + var tasks: [LoadFrameGroupTask] = [] + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { + tasks.append(task) + } + } + } + + return tasks + } + } + + public static let firstFrameQueue = Queue(name: "DCTMultiAnimationRenderer-FirstFrame", qos: .userInteractive) + + public var useYuvA: Bool = false + private var groupContext: GroupContext? + private var frameSkip: Int + private var displayTimer: Foundation.Timer? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayTimer == nil { + final class TimerTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func timerEvent() { + self.f() + } + } + let frameInterval = Double(self.frameSkip) / 60.0 + let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick(frameInterval: frameInterval) + }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) + self.displayTimer = displayTimer + RunLoop.main.add(displayTimer, forMode: .common) + } + } else { + if let displayTimer = self.displayTimer { + self.displayTimer = nil + displayTimer.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, useYuvA: self.useYuvA, fetch: fetch) + + return ActionDisposable { + disposable.dispose() + }.strict() + } + + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) + } + + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() + } + + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() + } + + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let groupContext = self.groupContext { + groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) + } + } + + private func updateIsPlaying() { + var isPlaying = false + if let groupContext = self.groupContext { + if groupContext.isPlaying { + isPlaying = true + } + } + + self.isPlaying = isPlaying + } + + private func animationTick(frameInterval: Double) { + let secondsPerFrame = frameInterval + + var tasks: [LoadFrameGroupTask] = [] + if let groupContext = self.groupContext { + if groupContext.isPlaying { + tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) + } + } + + if !tasks.isEmpty { + let tasks0 = tasks.filter { $0.queueAffinity % 2 == 0 } + let tasks1 = tasks.filter { $0.queueAffinity % 2 == 1 } + let allTasks = [tasks0, tasks1] + + let taskCompletions = Atomic<[Int: [() -> Void]]>(value: [:]) + let queues: [Queue] = [ItemAnimationContext.queue0, ItemAnimationContext.queue1] + + for i in 0 ..< 2 { + let partTasks = allTasks[i] + let id = i + queues[i].async { + var completions: [() -> Void] = [] + for task in partTasks { + let complete = task.task() + completions.append(complete) + } + + var complete = false + let _ = taskCompletions.modify { current in + var current = current + current[id] = completions + if current.count == 2 { + complete = true + } + return current + } + + if complete { + Queue.mainQueue().async { + let allCompletions = taskCompletions.with { $0 } + for (_, fs) in allCompletions { + for f in fs { + f() + } + } + } + } + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index 294e001b6f..cdfd5f6774 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 344e3fc428..b4ac019707 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -13,6 +13,7 @@ import AnimationCache import LottieAnimationCache import VideoAnimationCache import MultiAnimationRenderer +import DCTMultiAnimationRendererImpl import ShimmerEffect import TextFormat import TelegramUIPreferences @@ -817,7 +818,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let isThumbnailCancelled = Atomic(value: false) self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { - MultiAnimationRendererImpl.firstFrameQueue.async { + DCTMultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 0ca5c0ae13..1c0a044379 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", "//submodules/TelegramUI/Components/LottieComponent", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift index 7de681f13f..114955ef56 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift @@ -8,6 +8,7 @@ import Postbox import SwiftSignalKit import MultiAnimationRenderer import AnimationCache +import DCTMultiAnimationRendererImpl import AccountContext import TelegramUIPreferences import GenerateStickerPlaceholderImage @@ -277,7 +278,7 @@ public final class InlineFileIconLayer: MultiAnimationRenderTarget { size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { - MultiAnimationRendererImpl.firstFrameQueue.async { + DCTMultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD index 4f5a66267d..e5a853e167 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD @@ -1,44 +1,4 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -load( - "@build_bazel_rules_apple//apple:resources.bzl", - "apple_resource_bundle", - "apple_resource_group", -) -load("//build-system/bazel-utils:plist_fragment.bzl", - "plist_fragment", -) - -filegroup( - name = "MultiAnimationRendererMetalResources", - srcs = glob([ - "Resources/**/*.metal", - ]), - visibility = ["//visibility:public"], -) - -plist_fragment( - name = "WallpaperBackgroundNodeBundleInfoPlist", - extension = "plist", - template = - """ - CFBundleIdentifier - org.telegram.MultiAnimationRenderer - CFBundleDevelopmentRegion - en - CFBundleName - MultiAnimationRenderer - """ -) - -apple_resource_bundle( - name = "MultiAnimationRendererBundle", - infoplists = [ - ":WallpaperBackgroundNodeBundleInfoPlist", - ], - resources = [ - ":MultiAnimationRendererMetalResources", - ], -) swift_library( name = "MultiAnimationRenderer", @@ -46,9 +6,6 @@ swift_library( srcs = glob([ "Sources/**/*.swift", ]), - data = [ - ":MultiAnimationRendererBundle", - ], copts = [ "-warnings-as-errors", ], diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal b/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal deleted file mode 100644 index 8343f753f0..0000000000 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal +++ /dev/null @@ -1,46 +0,0 @@ -#include -using namespace metal; - -typedef struct { - packed_float2 position; - packed_float2 texCoord; -} Vertex; - -typedef struct { - float4 position[[position]]; - float2 texCoord; -} Varyings; - -vertex Varyings multiAnimationVertex( - unsigned int vid[[vertex_id]], - constant Vertex *verticies[[buffer(0)]], - constant uint2 &resolution[[buffer(1)]], - constant uint2 &slotSize[[buffer(2)]], - constant uint2 &slotPosition[[buffer(3)]] -) { - Varyings out; - constant Vertex &v = verticies[vid]; - - out.position = float4(float2(v.position), 0.0, 1.0); - out.texCoord = v.texCoord; - - return out; -} - -fragment half4 multiAnimationFragment( - Varyings in[[stage_in]], - texture2d textureY[[texture(0)]], - texture2d textureU[[texture(1)]], - texture2d textureV[[texture(2)]], - texture2d textureA[[texture(3)]] -) { - constexpr sampler s(address::clamp_to_edge, filter::linear); - - half y = textureY.sample(s, in.texCoord).r; - half u = textureU.sample(s, in.texCoord).r - 0.5; - half v = textureV.sample(s, in.texCoord).r - 0.5; - half a = textureA.sample(s, in.texCoord).r; - - half4 out = half4(1.5748 * v + y, -0.1873 * v + y, 1.8556 * u + y, a); - return half4(out.b, out.g, out.r, out.a); -} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift deleted file mode 100644 index 8b13789179..0000000000 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index e0671fd539..6bf0029d65 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -3,8 +3,6 @@ import UIKit import SwiftSignalKit import Display import AnimationCache -import Accelerate -import IOSurface public protocol MultiAnimationRenderer: AnyObject { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable @@ -19,10 +17,10 @@ private var nextRenderTargetId: Int64 = 1 open class MultiAnimationRenderTarget: SimpleLayer { public let id: Int64 public var numFrames: Int? - - let deinitCallbacks = Bag<() -> Void>() - let updateStateCallbacks = Bag<() -> Void>() - + + public let deinitCallbacks = Bag<() -> Void>() + public let updateStateCallbacks = Bag<() -> Void>() + public final var shouldBeAnimating: Bool = false { didSet { if self.shouldBeAnimating != oldValue { @@ -32,7 +30,7 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } } - + public var blurredRepresentationBackgroundColor: UIColor? public var blurredRepresentationTarget: CALayer? { didSet { @@ -43,1109 +41,39 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } } - + public override init() { assert(Thread.isMainThread) - + self.id = nextRenderTargetId nextRenderTargetId += 1 - + super.init() } - + public override init(layer: Any) { guard let layer = layer as? MultiAnimationRenderTarget else { preconditionFailure() } - + self.id = layer.id - + super.init(layer: layer) } - + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { for f in self.deinitCallbacks.copyItems() { f() } } - + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { } - + open func transitionToContents(_ contents: AnyObject, didLoop: Bool) { } } - -private final class LoadFrameGroupTask { - let task: () -> () -> Void - let queueAffinity: Int - - init(task: @escaping () -> () -> Void, queueAffinity: Int) { - self.task = task - self.queueAffinity = queueAffinity - } -} - -private var yuvToRgbConversion: vImage_YpCbCrToARGB = { - var info = vImage_YpCbCrToARGB() - var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0) - vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0) - return info -}() - -private final class ItemAnimationContext { - fileprivate final class Frame { - let frame: AnimationCacheItemFrame - let duration: Double - - let contentsAsImage: UIImage? - let contentsAsCVPixelBuffer: CVPixelBuffer? - - let size: CGSize - - var remainingDuration: Double - - private var blurredRepresentationValue: UIImage? - - init?(frame: AnimationCacheItemFrame) { - self.frame = frame - self.duration = frame.duration - self.remainingDuration = frame.duration - - switch frame.format { - case let .rgba(data, width, height, bytesPerRow): - guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { - return nil - } - - data.withUnsafeBytes { bytes -> Void in - memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) - } - - guard let image = context.generateImage() else { - return nil - } - - self.contentsAsImage = image - self.contentsAsCVPixelBuffer = nil - self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) - case let .yuva(y, u, v, a): - var pixelBuffer: CVPixelBuffer? = nil - let _ = CVPixelBufferCreate(kCFAllocatorDefault, y.width, y.height, kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, [ - kCVPixelBufferIOSurfacePropertiesKey: NSDictionary() - ] as CFDictionary, &pixelBuffer) - guard let pixelBuffer else { - return nil - } - - CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - defer { - CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - } - guard let baseAddressY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { - return nil - } - guard let baseAddressCbCr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { - return nil - } - guard let baseAddressA = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else { - return nil - } - - let dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressY), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)) - let dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressCbCr), height: vImagePixelCount(y.height / 2), width: vImagePixelCount(y.width / 2), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)) - let dstBufferA = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressA), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)) - - y.data.withUnsafeBytes { (yBytes: UnsafeRawBufferPointer) -> Void in - if dstBufferY.rowBytes == y.bytesPerRow { - memcpy(dstBufferY.data, yBytes.baseAddress!, yBytes.count) - } else { - for i in 0 ..< y.height { - memcpy(dstBufferY.data.advanced(by: dstBufferY.rowBytes * i), yBytes.baseAddress!.advanced(by: y.bytesPerRow * i), y.bytesPerRow) - } - } - } - - a.data.withUnsafeBytes { (aBytes: UnsafeRawBufferPointer) -> Void in - if dstBufferA.rowBytes == a.bytesPerRow { - memcpy(dstBufferA.data, aBytes.baseAddress!, aBytes.count) - } else { - for i in 0 ..< y.height { - memcpy(dstBufferA.data.advanced(by: dstBufferA.rowBytes * i), aBytes.baseAddress!.advanced(by: a.bytesPerRow * i), a.bytesPerRow) - } - } - } - - u.data.withUnsafeBytes { (uBytes: UnsafeRawBufferPointer) -> Void in - v.data.withUnsafeBytes { (vBytes: UnsafeRawBufferPointer) -> Void in - let sourceU = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: uBytes.baseAddress!), - height: vImagePixelCount(u.height), - width: vImagePixelCount(u.width), - rowBytes: u.bytesPerRow - ) - let sourceV = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: vBytes.baseAddress!), - height: vImagePixelCount(v.height), - width: vImagePixelCount(v.width), - rowBytes: v.bytesPerRow - ) - - withUnsafePointer(to: sourceU, { sourceU in - withUnsafePointer(to: sourceV, { sourceV in - var srcPlanarBuffers: [ - UnsafePointer? - ] = [sourceU, sourceV] - var destChannels: [UnsafeMutableRawPointer?] = [ - dstBufferCbCr.data.advanced(by: 1), - dstBufferCbCr.data - ] - - let channelCount = 2 - - vImageConvert_PlanarToChunky8( - &srcPlanarBuffers, - &destChannels, - UInt32(channelCount), - MemoryLayout.stride * channelCount, - vImagePixelCount(u.width), - vImagePixelCount(u.height), - dstBufferCbCr.rowBytes, - vImage_Flags(kvImageDoNotTile) - ) - }) - }) - } - } - - self.contentsAsImage = nil - self.contentsAsCVPixelBuffer = pixelBuffer - self.size = CGSize(width: CGFloat(y.width), height: CGFloat(y.height)) - } - } - - func blurredRepresentation(color: UIColor?) -> UIImage? { - if let blurredRepresentationValue = self.blurredRepresentationValue { - return blurredRepresentationValue - } - - switch frame.format { - case let .rgba(data, width, height, bytesPerRow): - let blurredWidth = 12 - let blurredHeight = 12 - guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) else { - return nil - } - - let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) - - data.withUnsafeBytes { bytes -> Void in - if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { - let image = CGImage( - width: width, - height: height, - bitsPerComponent: 8, - bitsPerPixel: 32, - bytesPerRow: bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, - bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, - provider: dataProvider, - decode: nil, - shouldInterpolate: true, - intent: .defaultIntent - ) - if let image = image { - context.withFlippedContext { c in - c.setFillColor((color ?? .white).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - c.draw(image, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) - } - } - } - - var destinationBuffer = vImage_Buffer() - destinationBuffer.width = UInt(blurredWidth) - destinationBuffer.height = UInt(blurredHeight) - destinationBuffer.data = context.bytes - destinationBuffer.rowBytes = context.bytesPerRow - - vImageBoxConvolve_ARGB8888(&destinationBuffer, - &destinationBuffer, - nil, - 0, 0, - UInt32(15), - UInt32(15), - nil, - vImage_Flags(kvImageTruncateKernel)) - - let divisor: Int32 = 0x1000 - - let rwgt: CGFloat = 0.3086 - let gwgt: CGFloat = 0.6094 - let bwgt: CGFloat = 0.0820 - - let adjustSaturation: CGFloat = 1.7 - - let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation - let b = (1.0 - adjustSaturation) * rwgt - let c = (1.0 - adjustSaturation) * rwgt - let d = (1.0 - adjustSaturation) * gwgt - let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation - let f = (1.0 - adjustSaturation) * gwgt - let g = (1.0 - adjustSaturation) * bwgt - let h = (1.0 - adjustSaturation) * bwgt - let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation - - let satMatrix: [CGFloat] = [ - a, b, c, 0, - d, e, f, 0, - g, h, i, 0, - 0, 0, 0, 1 - ] - - var matrix: [Int16] = satMatrix.map { value in - return Int16(value * CGFloat(divisor)) - } - - vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) - - context.withFlippedContext { c in - c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - } - - self.blurredRepresentationValue = context.generateImage() - return self.blurredRepresentationValue - case let .yuva(y, u, v, a): - let blurredWidth = 12 - let blurredHeight = 12 - let size = CGSize(width: blurredWidth, height: blurredHeight) - - var sourceY = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: y.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(y.height), - width: vImagePixelCount(y.width), - rowBytes: y.bytesPerRow - ) - - var sourceU = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: u.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(u.height), - width: vImagePixelCount(u.width), - rowBytes: u.bytesPerRow - ) - - var sourceV = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: v.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(v.height), - width: vImagePixelCount(v.width), - rowBytes: v.bytesPerRow - ) - - var sourceA = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: a.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(a.height), - width: vImagePixelCount(a.width), - rowBytes: a.bytesPerRow - ) - - let scaledYData = malloc(blurredWidth * blurredHeight)! - defer { - free(scaledYData) - } - - let scaledUData = malloc(blurredWidth * blurredHeight / 4)! - defer { - free(scaledUData) - } - - let scaledVData = malloc(blurredWidth * blurredHeight / 4)! - defer { - free(scaledVData) - } - - let scaledAData = malloc(blurredWidth * blurredHeight)! - defer { - free(scaledAData) - } - - var scaledY = vImage_Buffer( - data: scaledYData, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: blurredWidth - ) - - var scaledU = vImage_Buffer( - data: scaledUData, - height: vImagePixelCount(blurredHeight / 2), - width: vImagePixelCount(blurredWidth / 2), - rowBytes: blurredWidth / 2 - ) - - var scaledV = vImage_Buffer( - data: scaledVData, - height: vImagePixelCount(blurredHeight / 2), - width: vImagePixelCount(blurredWidth / 2), - rowBytes: blurredWidth / 2 - ) - - var scaledA = vImage_Buffer( - data: scaledAData, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: blurredWidth - ) - - vImageScale_Planar8(&sourceY, &scaledY, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceU, &scaledU, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceV, &scaledV, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceA, &scaledA, nil, vImage_Flags(kvImageHighQualityResampling)) - - guard let context = DrawingContext(size: size, scale: 1.0, clear: true) else { - return nil - } - - var destinationBuffer = vImage_Buffer( - data: context.bytes, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: context.bytesPerRow - ) - - var result = kvImageNoError - - var permuteMap: [UInt8] = [1, 2, 3, 0] - result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&scaledY, &scaledU, &scaledV, &destinationBuffer, &yuvToRgbConversion, &permuteMap, 255, vImage_Flags(kvImageDoNotTile)) - if result != kvImageNoError { - return nil - } - - result = vImageOverwriteChannels_ARGB8888(&scaledA, &destinationBuffer, &destinationBuffer, 1 << 0, vImage_Flags(kvImageDoNotTile)); - if result != kvImageNoError { - return nil - } - - vImageBoxConvolve_ARGB8888(&destinationBuffer, - &destinationBuffer, - nil, - 0, 0, - UInt32(15), - UInt32(15), - nil, - vImage_Flags(kvImageTruncateKernel)) - - let divisor: Int32 = 0x1000 - - let rwgt: CGFloat = 0.3086 - let gwgt: CGFloat = 0.6094 - let bwgt: CGFloat = 0.0820 - - let adjustSaturation: CGFloat = 1.7 - - let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation - let b = (1.0 - adjustSaturation) * rwgt - let c = (1.0 - adjustSaturation) * rwgt - let d = (1.0 - adjustSaturation) * gwgt - let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation - let f = (1.0 - adjustSaturation) * gwgt - let g = (1.0 - adjustSaturation) * bwgt - let h = (1.0 - adjustSaturation) * bwgt - let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation - - let satMatrix: [CGFloat] = [ - a, b, c, 0, - d, e, f, 0, - g, h, i, 0, - 0, 0, 0, 1 - ] - - var matrix: [Int16] = satMatrix.map { value in - return Int16(value * CGFloat(divisor)) - } - - vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) - - context.withFlippedContext { c in - c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - - self.blurredRepresentationValue = context.generateImage() - return self.blurredRepresentationValue - } - } - } - - static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default) - static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default) - - private let useYuvA: Bool - - private let cache: AnimationCache - let queueAffinity: Int - private let stateUpdated: () -> Void - - private var disposable: Disposable? - private var displayLink: ConstantDisplayLinkAnimator? - private var item: Atomic? - private var itemPlaceholderAndFrameIndex: (UIImage, Int)? - - private var currentFrame: Frame? - private var loadingFrameTaskId: Int? - private var nextLoadingFrameTaskId: Int = 0 - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - let targets = Bag>() - - init(cache: AnimationCache, queueAffinity: Int, itemId: String, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { - self.cache = cache - self.queueAffinity = queueAffinity - self.useYuvA = useYuvA - self.stateUpdated = stateUpdated - - self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - if let item = result.item { - strongSelf.item = Atomic(value: item) - } - if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { - strongSelf.itemPlaceholderAndFrameIndex = nil - strongSelf.setFrameIndex(index: index, placeholder: placeholder) - } - strongSelf.updateIsPlaying() - } - }) - } - - deinit { - self.disposable?.dispose() - self.displayLink?.invalidate() - } - - func setFrameIndex(index: Int, placeholder: UIImage) { - if let item = self.item { - let nextFrame = item.with { item -> AnimationCacheItemFrame? in - item.reset() - for i in 0 ... index { - let result = item.advance(advance: .frames(1), requestedFormat: .rgba) - if i == index { - return result?.frame - } - } - return nil - } - - self.loadingFrameTaskId = nil - - if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { - self.currentFrame = currentFrame - - for target in self.targets.copyItems() { - if let target = target.value { - if let image = currentFrame.contentsAsImage { - target.transitionToContents(image.cgImage!, didLoop: false) - } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: false) - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - } - } else { - for target in self.targets.copyItems() { - if let target = target.value { - target.transitionToContents(placeholder.cgImage!, didLoop: false) - } - } - - self.itemPlaceholderAndFrameIndex = (placeholder, index) - } - } - - func updateAddedTarget(target: MultiAnimationRenderTarget) { - if let currentFrame = self.currentFrame { - if let cgImage = currentFrame.contentsAsImage?.cgImage { - target.transitionToContents(cgImage, didLoop: false) - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: false) - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - - self.updateIsPlaying() - } - - func updateIsPlaying() { - var isPlaying = true - if self.item == nil { - isPlaying = false - } - - var shouldBeAnimating = false - for target in self.targets.copyItems() { - if let target = target.value { - if target.shouldBeAnimating { - shouldBeAnimating = true - break - } - } - } - if !shouldBeAnimating { - isPlaying = false - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { - return self.update(advanceTimestamp: advanceTimestamp) - } - - private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { - guard let item = self.item else { - return nil - } - - var frameAdvance: AnimationCacheItem.Advance? - if self.loadingFrameTaskId == nil { - if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { - let divisionFactor = advanceTimestamp / currentFrame.remainingDuration - let wholeFactor = round(divisionFactor) - if abs(wholeFactor - divisionFactor) < 0.005 { - currentFrame.remainingDuration = 0.0 - frameAdvance = .frames(Int(wholeFactor)) - } else { - currentFrame.remainingDuration -= advanceTimestamp - if currentFrame.remainingDuration <= 0.0 { - frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) - } - } - } else if self.currentFrame == nil { - frameAdvance = .frames(1) - } - } - - if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { - let taskId = self.nextLoadingFrameTaskId - self.nextLoadingFrameTaskId += 1 - - self.loadingFrameTaskId = taskId - let useYuvA = self.useYuvA - - return LoadFrameGroupTask(task: { [weak self] in - let currentFrame: (frame: Frame, didLoop: Bool)? - do { - if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in - let defaultFormat: AnimationCacheItemFrame.RequestedFormat - if useYuvA { - defaultFormat = .yuva(rowAlignment: 1) - } else { - defaultFormat = .rgba - } - - if let result = item.advance(advance: frameAdvance, requestedFormat: defaultFormat) { - return (result.frame, result.didLoop) - } else { - return nil - } - }), let mappedFrame = Frame(frame: frame) { - currentFrame = (mappedFrame, didLoop) - } else { - currentFrame = nil - } - } catch { - assertionFailure() - currentFrame = nil - } - - return { - guard let strongSelf = self else { - return - } - - if strongSelf.loadingFrameTaskId != taskId { - return - } - - strongSelf.loadingFrameTaskId = nil - - if let currentFrame = currentFrame { - strongSelf.currentFrame = currentFrame.frame - for target in strongSelf.targets.copyItems() { - if let target = target.value { - if let image = currentFrame.frame.contentsAsImage { - target.transitionToContents(image.cgImage!, didLoop: currentFrame.didLoop) - } else if let pixelBuffer = currentFrame.frame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: currentFrame.didLoop) - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - } - } - }, queueAffinity: self.queueAffinity) - } - - if let _ = self.currentFrame { - for target in self.targets.copyItems() { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: false) - } - } - } - - return nil - } -} - -public final class MultiAnimationRendererImpl: MultiAnimationRenderer { - private final class GroupContext { - private let firstFrameQueue: Queue - private let stateUpdated: () -> Void - - private struct ItemKey: Hashable { - var id: String - var width: Int - var height: Int - var uniqueId: Int - } - - private var itemContexts: [ItemKey: ItemAnimationContext] = [:] - private var nextQueueAffinity: Int = 0 - private var nextUniqueId: Int = 1 - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { - self.firstFrameQueue = firstFrameQueue - self.stateUpdated = stateUpdated - } - - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - var uniqueId = 0 - if unique { - uniqueId = self.nextUniqueId - self.nextUniqueId += 1 - } - - let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId) - let itemContext: ItemAnimationContext - if let current = self.itemContexts[itemKey] { - itemContext = current - } else { - let queueAffinity = self.nextQueueAffinity - self.nextQueueAffinity += 1 - itemContext = ItemAnimationContext(cache: cache, queueAffinity: queueAffinity, itemId: itemId, size: size, useYuvA: useYuvA, fetch: fetch, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.itemContexts[itemKey] = itemContext - } - - let index = itemContext.targets.add(Weak(target)) - itemContext.updateAddedTarget(target: target) - - let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { - return - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemKey) - } - } - } - - let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in - guard let itemContext = itemContext else { - return - } - itemContext.updateIsPlaying() - } - - return ActionDisposable { [weak self, weak itemContext, weak target] in - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { - return - } - if let target = target { - target.deinitCallbacks.remove(deinitIndex) - target.updateStateCallbacks.remove(updateStateIndex) - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemKey) - } - }.strict() - } - - func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { - guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { - return false - } - guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else { - return false - } - - if let image = loadedFrame.contentsAsImage { - target.contents = image.cgImage - } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { - target.contents = pixelBuffer - } - target.numFrames = item.numFrames - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - - return true - } else { - return false - } - } - - func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - var hadIntermediateUpdate = false - return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] item in - guard let item = item.item else { - let isFinal = item.isFinal - hadIntermediateUpdate = true - Queue.mainQueue().async { - completion(false, isFinal) - } - return - } - - let loadedFrame: ItemAnimationContext.Frame? - if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { - loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) - } else { - loadedFrame = nil - } - - Queue.mainQueue().async { - guard let target = target else { - completion(false, true) - return - } - target.numFrames = item.numFrames - if let loadedFrame = loadedFrame { - if let cgImage = loadedFrame.contentsAsImage?.cgImage { - if hadIntermediateUpdate { - target.transitionToContents(cgImage, didLoop: false) - } else { - target.contents = cgImage - } - } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { - if hadIntermediateUpdate { - target.transitionToContents(pixelBuffer, didLoop: false) - } else { - target.contents = pixelBuffer - } - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - - completion(true, true) - } else { - completion(false, true) - } - } - }).strict() - } - - func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { - return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in - guard let item = item.item else { - Queue.mainQueue().async { - completion(nil) - } - return - } - - let loadedFrame: ItemAnimationContext.Frame? - if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { - loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) - } else { - loadedFrame = nil - } - - Queue.mainQueue().async { - if let loadedFrame = loadedFrame { - if let cgImage = loadedFrame.contentsAsImage?.cgImage { - completion(cgImage) - } else { - completion(nil) - } - } else { - completion(nil) - } - } - }).strict() - } - - func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { - itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { - var tasks: [LoadFrameGroupTask] = [] - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { - tasks.append(task) - } - } - } - - return tasks - } - } - - public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) - - public var useYuvA: Bool = false - private var groupContext: GroupContext? - private var frameSkip: Int - private var displayTimer: Foundation.Timer? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - if self.isPlaying { - if self.displayTimer == nil { - final class TimerTarget: NSObject { - private let f: () -> Void - - init(_ f: @escaping () -> Void) { - self.f = f - } - - @objc func timerEvent() { - self.f() - } - } - let frameInterval = Double(self.frameSkip) / 60.0 - let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animationTick(frameInterval: frameInterval) - }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) - self.displayTimer = displayTimer - RunLoop.main.add(displayTimer, forMode: .common) - } - } else { - if let displayTimer = self.displayTimer { - self.displayTimer = nil - displayTimer.invalidate() - } - } - } - } - } - - public init() { - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { - self.frameSkip = 1 - } else { - self.frameSkip = 2 - } - } - - public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, useYuvA: self.useYuvA, fetch: fetch) - - return ActionDisposable { - disposable.dispose() - }.strict() - } - - public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) - } - - public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() - } - - public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() - } - - public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - if let groupContext = self.groupContext { - groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) - } - } - - private func updateIsPlaying() { - var isPlaying = false - if let groupContext = self.groupContext { - if groupContext.isPlaying { - isPlaying = true - } - } - - self.isPlaying = isPlaying - } - - private func animationTick(frameInterval: Double) { - let secondsPerFrame = frameInterval - - var tasks: [LoadFrameGroupTask] = [] - if let groupContext = self.groupContext { - if groupContext.isPlaying { - tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) - } - } - - if !tasks.isEmpty { - let tasks0 = tasks.filter { $0.queueAffinity % 2 == 0 } - let tasks1 = tasks.filter { $0.queueAffinity % 2 == 1 } - let allTasks = [tasks0, tasks1] - - let taskCompletions = Atomic<[Int: [() -> Void]]>(value: [:]) - let queues: [Queue] = [ItemAnimationContext.queue0, ItemAnimationContext.queue1] - - for i in 0 ..< 2 { - let partTasks = allTasks[i] - let id = i - queues[i].async { - var completions: [() -> Void] = [] - for task in partTasks { - let complete = task.task() - completions.append(complete) - } - - var complete = false - let _ = taskCompletions.modify { current in - var current = current - current[id] = completions - if current.count == 2 { - complete = true - } - return current - } - - if complete { - Queue.mainQueue().async { - let allCompletions = taskCompletions.with { $0 } - for (_, fs) in allCompletions { - for f in fs { - f() - } - } - } - } - } - } - } - } -} diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD index becdf07725..47e6c76e66 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD +++ b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD @@ -36,6 +36,8 @@ swift_library( "//submodules/TelegramUI/Components/TelegramUIDeclareEncodables", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods", "//submodules/TelegramUI/Components/PeerSelectionController", "//submodules/TelegramUI/Components/ContextMenuScreen", diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index 9d236de495..c3b242c393 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -27,6 +27,8 @@ import ManagedFile import TelegramUIDeclareEncodables import AnimationCache import MultiAnimationRenderer +import DCTAnimationCacheImpl +import DCTMultiAnimationRendererImpl import TelegramUIDeclareEncodables import TelegramAccountAuxiliaryMethods import PeerSelectionController @@ -105,14 +107,14 @@ private final class ShareControllerAccountContextExtension: ShareControllerAccou self.stateManager = stateManager self.engineData = TelegramEngine.EngineData(accountPeerId: stateManager.accountPeerId, postbox: stateManager.postbox) let cacheStorageBox = stateManager.postbox.mediaBox.cacheStorageBox - self.animationCache = AnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + self.animationCache = DCTAnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }, updateStorageStats: { path, size in if let pathData = path.data(using: .utf8) { cacheStorageBox.update(id: pathData, size: size) } }) - self.animationRenderer = MultiAnimationRendererImpl() + self.animationRenderer = DCTMultiAnimationRendererImpl() self.contentSettings = contentSettings self.appConfiguration = appConfiguration } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index f5b2a60cb2..3b0074726e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -1689,14 +1689,15 @@ public final class StoryPeerListComponent: Component { NSAttributedString.Key.font: Font.semibold(17.0), NSAttributedString.Key.foregroundColor: component.theme.rootController.navigationBar.primaryTextColor ]) - var boundingRect = attributedText.boundingRect(with: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 30.0), height: 100.0), options: .usesLineFragmentOrigin, context: nil) - boundingRect.size.width = ceil(boundingRect.size.width) - boundingRect.size.height = ceil(boundingRect.size.height) + + let cachedLayout = TextNode.calculateLayout(attributedString: attributedText, minimumNumberOfLines: 1, maximumNumberOfLines: 1, truncationType: .end, backgroundColor: nil, constrainedSize: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 46.0), height: 100.0), alignment: .left, verticalAlignment: .middle, lineSpacingFactor: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textShadowBlur: nil, textStroke: nil, displaySpoilers: false, displayEmbeddedItemsUnderSpoilers: false, customTruncationToken: nil) - let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: boundingRect.size)) + let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: cachedLayout.size)) let image = renderer.image { context in UIGraphicsPushContext(context.cgContext) - attributedText.draw(at: CGPoint()) + + TextNode.draw(CGRect(origin: CGPoint(), size: cachedLayout.size), withParameters: TextNode.DrawingParameters(cachedLayout: cachedLayout, renderContentTypes: .all), isCancelled: { return false }, isRasterizing: false) + UIGraphicsPopContext() } self.titleView.image = image diff --git a/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/BUILD b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/BUILD new file mode 100644 index 0000000000..cf937fcc4c --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SubcodecAnimationCacheImpl", + module_name = "SubcodecAnimationCacheImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/CryptoUtils:CryptoUtils", + "//submodules/ManagedFile:ManagedFile", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/Sources/SubcodecAnimationCacheImpl.swift b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/Sources/SubcodecAnimationCacheImpl.swift new file mode 100644 index 0000000000..52481a826b --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/Sources/SubcodecAnimationCacheImpl.swift @@ -0,0 +1,800 @@ +import Foundation +import UIKit +import SwiftSignalKit +import CryptoUtils +import ManagedFile +import AnimationCache +import SubcodecObjC + +public struct MbsMetadata { + public let frameCount: Int + public let frameDurations: [Double] +} + +private func md5Hash(_ string: String) -> String { + let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return hashData.withUnsafeBytes { bytes -> String in + let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) + } +} + +private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { + assert(hashString.count == 32) + let directory = String(hashString[hashString.startIndex ..< hashString.index(hashString.startIndex, offsetBy: 2)]) + return (directory, "\(hashString)_\(width)x\(height)") +} + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound + } + return numToRound + multiple - remainder +} + +private func convertARGBToYUVA420( + argb: UnsafePointer, + width: Int, + height: Int, + bytesPerRow: Int +) -> (y: Data, cb: Data, cr: Data, alpha: Data, yStride: Int, cbStride: Int, crStride: Int, alphaStride: Int) { + let chromaWidth = width / 2 + let chromaHeight = height / 2 + + var yData = Data(count: width * height) + var cbData = Data(count: chromaWidth * chromaHeight) + var crData = Data(count: chromaWidth * chromaHeight) + var alphaData = Data(count: width * height) + + yData.withUnsafeMutableBytes { yBuf in + cbData.withUnsafeMutableBytes { cbBuf in + crData.withUnsafeMutableBytes { crBuf in + alphaData.withUnsafeMutableBytes { aBuf in + let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let aPtr = aBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + for row in 0 ..< height { + let srcRow = argb.advanced(by: row * bytesPerRow) + for col in 0 ..< width { + let px = srcRow.advanced(by: col * 4) + // BGRA layout (CoreGraphics premultiplied) + let b = Int(px[0]) + let g = Int(px[1]) + let r = Int(px[2]) + let a = Int(px[3]) + + // Un-premultiply + let rr: Int + let gg: Int + let bb: Int + if a > 0 { + rr = min(255, r * 255 / a) + gg = min(255, g * 255 / a) + bb = min(255, b * 255 / a) + } else { + rr = 0 + gg = 0 + bb = 0 + } + + // BT.709 + let y = 16 + (65 * rr + 129 * gg + 25 * bb + 128) / 256 + yPtr[row * width + col] = UInt8(clamping: y) + aPtr[row * width + col] = UInt8(a) + } + } + + // Chroma at half resolution (average 2x2 blocks) + for row in 0 ..< chromaHeight { + for col in 0 ..< chromaWidth { + var sumR = 0 + var sumG = 0 + var sumB = 0 + for dy in 0 ..< 2 { + for dx in 0 ..< 2 { + let srcRow = argb.advanced(by: (row * 2 + dy) * bytesPerRow) + let px = srcRow.advanced(by: (col * 2 + dx) * 4) + let b = Int(px[0]) + let g = Int(px[1]) + let r = Int(px[2]) + let a = Int(px[3]) + if a > 0 { + sumR += min(255, r * 255 / a) + sumG += min(255, g * 255 / a) + sumB += min(255, b * 255 / a) + } + } + } + let avgR = sumR / 4 + let avgG = sumG / 4 + let avgB = sumB / 4 + + let cb = 128 + (-38 * avgR - 74 * avgG + 112 * avgB + 128) / 256 + let cr = 128 + (112 * avgR - 94 * avgG - 18 * avgB + 128) / 256 + cbPtr[row * chromaWidth + col] = UInt8(clamping: cb) + crPtr[row * chromaWidth + col] = UInt8(clamping: cr) + } + } + } + } + } + } + + return (yData, cbData, crData, alphaData, width, chromaWidth, chromaWidth, width) +} + +private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { + struct CompressedResult { + var mbsPath: String + var metaPath: String + } + + var queue: Queue { + return self.innerQueue + } + let innerQueue: Queue + var isCancelled: Bool = false + + private let mbsOutputPath: String + private let metaOutputPath: String + private let completion: (CompressedResult?) -> Void + + private var spriteExtractor: SCSprite? + private var frameDurations: [Double] = [] + private var isFailed: Bool = false + private var isFinished: Bool = false + private var spriteWidth: Int = 0 + private var spriteHeight: Int = 0 + + private let lock = Lock() + + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.innerQueue = queue + self.mbsOutputPath = allocateTempFile() + self.metaOutputPath = allocateTempFile() + self.completion = completion + } + + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { + self.lock.locked { + if self.isFailed || self.isFinished { + return + } + + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + if width == 0 || height == 0 { + self.isFailed = true + return + } + + // Create extractor on first frame + if self.spriteExtractor == nil { + self.spriteWidth = width + self.spriteHeight = height + let spriteSize = max(width, height) + do { + self.spriteExtractor = try SCSprite.extractor(withSpriteSize: Int32(spriteSize), qp: 26, outputPath: self.mbsOutputPath) + } catch { + self.isFailed = true + return + } + } + + guard self.spriteWidth == width && self.spriteHeight == height else { + self.isFailed = true + return + } + + // Allocate ARGB surface + let bytesPerRow = width * 4 + let bufferSize = height * bytesPerRow + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + defer { buffer.deallocate() } + memset(buffer, 0, bufferSize) + + guard let duration = drawingBlock(AnimationCacheItemDrawingSurface( + argb: buffer, + width: width, + height: height, + bytesPerRow: bytesPerRow, + length: bufferSize + )) else { + return + } + + // Convert ARGB → YUV planes + let planes = convertARGBToYUVA420( + argb: buffer, + width: width, + height: height, + bytesPerRow: bytesPerRow + ) + + // Feed to extractor + do { + try self.spriteExtractor?.addFrameY( + planes.y, yStride: Int32(planes.yStride), + cb: planes.cb, cbStride: Int32(planes.cbStride), + cr: planes.cr, crStride: Int32(planes.crStride), + alpha: planes.alpha, alphaStride: Int32(planes.alphaStride) + ) + } catch { + self.isFailed = true + return + } + + self.frameDurations.append(duration) + } + } + + func finish() { + var result: CompressedResult? + + self.lock.locked { + if self.isFinished { + return + } + self.isFinished = true + + if self.isFailed || self.spriteExtractor == nil { + return + } + + do { + try self.spriteExtractor?.finalizeExtraction() + } catch { + self.isFailed = true + return + } + + // Write metadata file: frame count + durations + var metaData = Data() + var frameCount = UInt32(self.frameDurations.count) + metaData.append(Data(bytes: &frameCount, count: 4)) + for duration in self.frameDurations { + var d = Float32(duration) + metaData.append(Data(bytes: &d, count: 4)) + } + do { + try metaData.write(to: URL(fileURLWithPath: self.metaOutputPath)) + } catch { + self.isFailed = true + return + } + + result = CompressedResult(mbsPath: self.mbsOutputPath, metaPath: self.metaOutputPath) + } + + if !self.isFailed { + self.completion(result) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.mbsOutputPath) + let _ = try? FileManager.default.removeItem(atPath: self.metaOutputPath) + self.completion(nil) + } + } +} + +private func decodeSingleFrameFromMbs(mbsPath: String, metadata: MbsMetadata) -> AnimationCacheItemFrame? { + guard let mbsData = try? Data(contentsOf: URL(fileURLWithPath: mbsPath), options: .mappedIfSafe) else { + return nil + } + guard mbsData.count >= 14 else { + return nil + } + guard mbsData[0] == 0x4D, mbsData[1] == 0x42, mbsData[2] == 0x53, mbsData[3] == 0x36 else { + return nil + } + let _ = Int(mbsData[4]) | (Int(mbsData[5]) << 8) + let heightMbs = Int(mbsData[6]) | (Int(mbsData[7]) << 8) + let qp = Int(mbsData[10]) + + let spriteContentSize = heightMbs * 16 - 32 + guard spriteContentSize > 0 else { + return nil + } + + var nalData: Data? + let sinkBlock: (Data) -> Void = { data in + if nalData == nil { + nalData = data + } else { + nalData!.append(data) + } + } + + guard let surface = try? SCMuxSurface.create( + withSpriteWidth: Int32(spriteContentSize), + spriteHeight: Int32(spriteContentSize), + maxSlots: 1, + qp: Int32(qp), + sink: sinkBlock + ) else { + return nil + } + + guard let region = try? surface.addSprite(atPath: mbsPath) else { + return nil + } + + nalData = nil + guard let _ = try? surface.advanceFrame(sink: sinkBlock) else { + return nil + } + + guard let streamData = nalData else { + return nil + } + + guard let decoder = try? SCVideoToolboxDecoder.createDecoder() else { + return nil + } + guard let frames = try? decoder.decodeStream(streamData) else { + return nil + } + guard let decodedFrame = frames.first else { + return nil + } + + return extractSpriteFrame( + decodedFrame: decodedFrame, + region: region, + spriteContentWidth: spriteContentSize, + spriteContentHeight: spriteContentSize + ) +} + +private func extractSpriteFrame( + decodedFrame: SCDecodedFrame, + region: SCSpriteRegion, + spriteContentWidth: Int, + spriteContentHeight: Int +) -> AnimationCacheItemFrame? { + let colorRect = region.colorRect + let alphaRect = region.alphaRect + + let frameWidth = Int(colorRect.width) + let frameHeight = Int(colorRect.height) + + guard frameWidth > 0 && frameHeight > 0 else { + return nil + } + + let decodedWidth = Int(decodedFrame.width) + + let bytesPerRow = frameWidth * 4 + var argbData = Data(count: frameHeight * bytesPerRow) + + decodedFrame.y.withUnsafeBytes { yBuf in + decodedFrame.cb.withUnsafeBytes { cbBuf in + decodedFrame.cr.withUnsafeBytes { crBuf in + argbData.withUnsafeMutableBytes { outBuf in + let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let outPtr = outBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + let colorX = Int(colorRect.origin.x) + let colorY = Int(colorRect.origin.y) + let alphaX = Int(alphaRect.origin.x) + let alphaY = Int(alphaRect.origin.y) + + let chromaWidth = decodedWidth / 2 + + for row in 0 ..< frameHeight { + for col in 0 ..< frameWidth { + let srcY = yPtr[(colorY + row) * decodedWidth + (colorX + col)] + let srcCb = cbPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcCr = crPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcA = yPtr[(alphaY + row) * decodedWidth + (alphaX + col)] + + let yVal = Int(srcY) - 16 + let cbVal = Int(srcCb) - 128 + let crVal = Int(srcCr) - 128 + + var r = (298 * yVal + 459 * crVal + 128) >> 8 + var g = (298 * yVal - 55 * cbVal - 136 * crVal + 128) >> 8 + var b = (298 * yVal + 541 * cbVal + 128) >> 8 + + r = max(0, min(255, r)) + g = max(0, min(255, g)) + b = max(0, min(255, b)) + + let a = Int(srcA) + + let pr = (r * a + 127) / 255 + let pg = (g * a + 127) / 255 + let pb = (b * a + 127) / 255 + + let outOffset = (row * frameWidth + col) * 4 + outPtr[outOffset + 0] = UInt8(pb) + outPtr[outOffset + 1] = UInt8(pg) + outPtr[outOffset + 2] = UInt8(pr) + outPtr[outOffset + 3] = UInt8(a) + } + } + } + } + } + } + + return AnimationCacheItemFrame( + format: .rgba(data: argbData, width: frameWidth, height: frameHeight, bytesPerRow: bytesPerRow), + duration: 1.0 / 30.0 + ) +} + +private func loadMbsMetadata(metaPath: String) -> MbsMetadata? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: metaPath), options: .mappedIfSafe) else { + return nil + } + guard data.count >= 4 else { + return nil + } + var frameCount: UInt32 = 0 + withUnsafeMutableBytes(of: &frameCount) { buf in + data.copyBytes(to: buf.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 0 ..< 4) + } + let expectedSize = 4 + Int(frameCount) * 4 + guard data.count >= expectedSize else { + return nil + } + var durations: [Double] = [] + for i in 0 ..< Int(frameCount) { + var d: Float32 = 0 + let offset = 4 + i * 4 + withUnsafeMutableBytes(of: &d) { buf in + data.copyBytes(to: buf.baseAddress!.assumingMemoryBound(to: UInt8.self), from: offset ..< offset + 4) + } + durations.append(Double(d)) + } + return MbsMetadata(frameCount: Int(frameCount), frameDurations: durations) +} + +public final class SubcodecAnimationCacheImpl: AnimationCache { + private final class Impl { + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + } + + private final class ItemContext { + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() + let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + } + + private let queue: Queue + private let basePath: String + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + + private var itemContexts: [ItemKey: ItemContext] = [:] + + init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + self.queue = queue + + let fetchQueueCount: Int + if ProcessInfo.processInfo.processorCount > 2 { + fetchQueueCount = 3 + } else { + fetchQueueCount = 2 + } + self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "SubcodecAnimationCacheImpl-Fetch\(i)", qos: .default) } + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + } + + func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + + if FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) { + if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) { + let item = AnimationCacheItem(numFrames: metadata.frameCount, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) + return EmptyDisposable + } + } + + let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) + + let itemContext: ItemContext + var beginFetch = false + if let current = self.itemContexts[key] { + itemContext = current + } else { + itemContext = ItemContext() + self.itemContexts[key] = itemContext + beginFetch = true + } + + let queue = self.queue + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) + + if beginFetch { + let fetchQueueIndex = self.nextFetchQueueIndex + self.nextFetchQueueIndex += 1 + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: allocateTempFile, completion: { [weak self, weak itemContext] result in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + + strongSelf.itemContexts.removeValue(forKey: key) + + guard let result = result else { + for f in itemContext.subscribers.copyItems() { + f(AnimationCacheItemResult(item: nil, isFinal: true)) + } + return + } + + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: mbsPath) + let _ = try? FileManager.default.removeItem(atPath: metaPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.mbsPath, toPath: mbsPath) else { + return + } + guard let _ = try? FileManager.default.moveItem(atPath: result.metaPath, toPath: metaPath) else { + return + } + + if let size = SubcodecAnimationCacheImpl.fileSize(mbsPath) { + updateStorageStats(mbsPath, size) + } + + guard let metadata = loadMbsMetadata(metaPath: metaPath) else { + return + } + + for f in itemContext.subscribers.copyItems() { + if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) { + let item = AnimationCacheItem(numFrames: metadata.frameCount, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + f(AnimationCacheItemResult(item: item, isFinal: true)) + } else { + f(AnimationCacheItemResult(item: nil, isFinal: true)) + } + } + } + }) else { + return EmptyDisposable + } + + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) + + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + fetchDisposable.dispose() + }) + } + + return ActionDisposable { [weak self, weak itemContext] in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + itemContext.subscribers.remove(index) + if itemContext.subscribers.isEmpty { + itemContext.disposable.dispose() + strongSelf.itemContexts.removeValue(forKey: key) + } + } + } + } + } + + private static func fileSize(_ path: String) -> Int64? { + var value = stat() + if stat(path, &value) == 0 { + return value.st_size + } + return nil + } + + private let queue: Queue + private let basePath: String + private let impl: QueueLocalObject + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + let queue = Queue() + self.queue = queue + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + }) + } + + // MARK: - AnimationCache protocol + + public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in + subscriber.putNext(result) + if result.isFinal { + subscriber.putCompletion() + } + })) + } + return disposable + } + |> runOn(self.queue) + } + + public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + + guard FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) else { + return nil + } + guard let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) else { + return nil + } + return AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + } + + public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let disposable = MetaDisposable() + let basePath = self.basePath + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + + queue.async { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + + if FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) { + if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) { + let item = AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + completion(AnimationCacheItemResult(item: item, isFinal: true)) + return + } + } + + if let fetch = fetch { + completion(AnimationCacheItemResult(item: nil, isFinal: false)) + guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in + queue.async { + guard let result = result else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let _ = try? FileManager.default.removeItem(atPath: mbsPath) + let _ = try? FileManager.default.removeItem(atPath: metaPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.mbsPath, toPath: mbsPath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.moveItem(atPath: result.metaPath, toPath: metaPath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + if let size = SubcodecAnimationCacheImpl.fileSize(mbsPath) { + updateStorageStats(mbsPath, size) + } + guard let metadata = loadMbsMetadata(metaPath: metaPath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let item = AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + completion(AnimationCacheItemResult(item: item, isFinal: true)) + } + }) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) + disposable.set(fetchDisposable) + } else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + } + } + + return disposable + } + + // MARK: - Subcodec-specific API (used by renderer) + + public func getMbsPath(sourceId: String, size: CGSize) -> String { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + return "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + } + + public func getMetaPath(sourceId: String, size: CGSize) -> String { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + return "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + } + + public func loadMetadata(sourceId: String, size: CGSize) -> MbsMetadata? { + let metaPath = getMetaPath(sourceId: sourceId, size: size) + return loadMbsMetadata(metaPath: metaPath) + } + + public func fetchIfNeeded(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, completion: @escaping (String?) -> Void) -> Disposable { + let mbsPath = getMbsPath(sourceId: sourceId, size: size) + if FileManager.default.fileExists(atPath: mbsPath) { + completion(mbsPath) + return EmptyDisposable + } + + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { [weak self] result in + guard let strongSelf = self else { + completion(nil) + return + } + if result.isFinal { + let path = strongSelf.getMbsPath(sourceId: sourceId, size: size) + if FileManager.default.fileExists(atPath: path) { + completion(path) + } else { + completion(nil) + } + } + })) + } + return disposable + } +} diff --git a/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/BUILD b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/BUILD new file mode 100644 index 0000000000..a80393fc96 --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SubcodecMultiAnimationRendererImpl", + module_name = "SubcodecMultiAnimationRendererImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/Sources/SubcodecMultiAnimationRendererImpl.swift b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/Sources/SubcodecMultiAnimationRendererImpl.swift new file mode 100644 index 0000000000..d9bd6a3799 --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/Sources/SubcodecMultiAnimationRendererImpl.swift @@ -0,0 +1,653 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import SubcodecObjC +import SubcodecAnimationCacheImpl +import Accelerate + +private let maxSlotsLimit: Int = 882 + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { return numToRound } + let remainder = numToRound % multiple + if remainder == 0 { return numToRound } + return numToRound + multiple - remainder +} + +private final class SpriteContext { + let mbsPath: String + let region: SCSpriteRegion + let targets = Bag>() + let metadata: MbsMetadata + + var currentFrameIndex: Int = 0 + var remainingDuration: Double = 0.0 + + var isPlaying: Bool { + for target in self.targets.copyItems() { + if let target = target.value, target.shouldBeAnimating { + return true + } + } + return false + } + + init(mbsPath: String, region: SCSpriteRegion, metadata: MbsMetadata) { + self.mbsPath = mbsPath + self.region = region + self.metadata = metadata + if !metadata.frameDurations.isEmpty { + self.remainingDuration = metadata.frameDurations[0] + } + } +} + +private final class SurfaceGroup { + let spriteWidth: Int + let spriteHeight: Int + + private(set) var surface: SCMuxSurface? + private(set) var decoder: SCVideoToolboxDecoder? + private(set) var spriteContexts: [Int: SpriteContext] = [:] + private var currentMaxSlots: Int = 64 + private var accumulatedNalData = Data() + private var lastDecodedFrame: SCDecodedFrame? + + init(spriteWidth: Int, spriteHeight: Int) { + self.spriteWidth = spriteWidth + self.spriteHeight = spriteHeight + } + + func ensureInitialized() -> Bool { + if self.surface != nil { + return true + } + let sink: (Data) -> Void = { [weak self] data in + self?.accumulatedNalData.append(data) + } + guard let surface = try? SCMuxSurface.create( + withSpriteWidth: Int32(self.spriteWidth), + spriteHeight: Int32(self.spriteHeight), + maxSlots: Int32(self.currentMaxSlots), + qp: 26, + sink: sink + ) else { + return false + } + self.surface = surface + + guard let decoder = try? SCVideoToolboxDecoder.createDecoder() else { + return false + } + self.decoder = decoder + + return true + } + + func addSprite(mbsPath: String, metadata: MbsMetadata) -> SpriteContext? { + guard self.ensureInitialized(), let surface = self.surface else { + return nil + } + + guard let region = try? surface.addSprite(atPath: mbsPath) else { + if !self.tryGrow() { + return nil + } + guard let region = try? surface.addSprite(atPath: mbsPath) else { + return nil + } + let context = SpriteContext(mbsPath: mbsPath, region: region, metadata: metadata) + self.spriteContexts[Int(region.slot)] = context + return context + } + + let context = SpriteContext(mbsPath: mbsPath, region: region, metadata: metadata) + self.spriteContexts[Int(region.slot)] = context + return context + } + + func removeSprite(slot: Int) { + self.surface?.removeSprite(atSlot: Int32(slot)) + self.spriteContexts.removeValue(forKey: slot) + } + + private func tryGrow() -> Bool { + guard let surface = self.surface, let decoder = self.decoder else { + return false + } + + let newMaxSlots: Int + if self.currentMaxSlots * 2 <= maxSlotsLimit { + newMaxSlots = self.currentMaxSlots * 2 + } else if self.currentMaxSlots < maxSlotsLimit { + newMaxSlots = maxSlotsLimit + } else { + return false + } + + if self.lastDecodedFrame == nil { + self.accumulatedNalData = Data() + guard let _ = try? surface.advanceFrame(sink: { [weak self] data in + self?.accumulatedNalData.append(data) + }) else { + return false + } + if !self.accumulatedNalData.isEmpty { + if let frames = try? decoder.decodeStream(self.accumulatedNalData) { + self.lastDecodedFrame = frames.last + } + } + } + + guard let decodedFrame = self.lastDecodedFrame else { + return false + } + + self.accumulatedNalData = Data() + let yData = decodedFrame.y + let cbData = decodedFrame.cb + let crData = decodedFrame.cr + let decodedWidth = Int(decodedFrame.width) + let decodedHeight = Int(decodedFrame.height) + let chromaWidth = decodedWidth / 2 + + guard let resizeResult = try? surface.resize( + toMaxSlots: Int32(newMaxSlots), + yPlane: yData, + cbPlane: cbData, + crPlane: crData, + decodedWidth: Int32(decodedWidth), + decodedHeight: Int32(decodedHeight), + strideY: Int32(decodedWidth), + strideCb: Int32(chromaWidth), + strideCr: Int32(chromaWidth), + withSink: { [weak self] data in + self?.accumulatedNalData.append(data) + } + ) else { + return false + } + + if !self.accumulatedNalData.isEmpty { + if let frames = try? decoder.decodeStream(self.accumulatedNalData) { + self.lastDecodedFrame = frames.last + } + } + + var updatedContexts: [Int: SpriteContext] = [:] + for newRegion in resizeResult.regions { + let newSlot = Int(newRegion.slot) + for (_, context) in self.spriteContexts { + if updatedContexts.values.contains(where: { $0 === context }) { + continue + } + let updated = SpriteContext(mbsPath: context.mbsPath, region: newRegion, metadata: context.metadata) + updated.currentFrameIndex = context.currentFrameIndex + updated.remainingDuration = context.remainingDuration + for target in context.targets.copyItems() { + if let target = target.value { + let _ = updated.targets.add(Weak(target)) + } + } + updatedContexts[newSlot] = updated + break + } + } + + self.spriteContexts = updatedContexts + self.currentMaxSlots = newMaxSlots + self.accumulatedNalData = Data() + return true + } + + func tick(advanceTimestamp: Double) { + guard let surface = self.surface, let decoder = self.decoder else { + return + } + + var anyAdvanced = false + for (slot, context) in self.spriteContexts { + guard context.isPlaying else { + continue + } + if advanceTimestamp > 0.0 { + context.remainingDuration -= advanceTimestamp + if context.remainingDuration <= 0.0 { + surface.advanceSprite(atSlot: Int32(slot)) + context.currentFrameIndex += 1 + if context.currentFrameIndex >= context.metadata.frameCount { + context.currentFrameIndex = 0 + } + if context.currentFrameIndex < context.metadata.frameDurations.count { + context.remainingDuration = context.metadata.frameDurations[context.currentFrameIndex] + } + anyAdvanced = true + } + } else { + surface.advanceSprite(atSlot: Int32(slot)) + anyAdvanced = true + } + } + + if !anyAdvanced { + return + } + + self.accumulatedNalData = Data() + guard let _ = try? surface.emitFrameIfNeeded(sink: { [weak self] data in + self?.accumulatedNalData.append(data) + }) else { + return + } + + guard !self.accumulatedNalData.isEmpty else { + return + } + + guard let frames = try? decoder.decodeStream(self.accumulatedNalData) else { + return + } + guard let decodedFrame = frames.last else { + return + } + + self.lastDecodedFrame = decodedFrame + + for (_, context) in self.spriteContexts { + let region = context.region + let colorRect = region.colorRect + let alphaRect = region.alphaRect + + let frameWidth = Int(colorRect.width) + let frameHeight = Int(colorRect.height) + guard frameWidth > 0 && frameHeight > 0 else { + continue + } + + let cgImage = extractCGImage( + decodedFrame: decodedFrame, + colorRect: colorRect, + alphaRect: alphaRect, + frameWidth: frameWidth, + frameHeight: frameHeight + ) + + let didLoop = context.currentFrameIndex == 0 && context.metadata.frameCount > 1 + + for targetRef in context.targets.copyItems() { + if let target = targetRef.value { + if let cgImage = cgImage { + target.transitionToContents(cgImage, didLoop: didLoop) + } + } + } + } + } + + var isEmpty: Bool { + return self.spriteContexts.isEmpty + } + + var hasPlayingSprites: Bool { + for (_, context) in self.spriteContexts { + if context.isPlaying { + return true + } + } + return false + } +} + +private func extractCGImage( + decodedFrame: SCDecodedFrame, + colorRect: CGRect, + alphaRect: CGRect, + frameWidth: Int, + frameHeight: Int +) -> CGImage? { + let decodedWidth = Int(decodedFrame.width) + let chromaWidth = decodedWidth / 2 + + let bytesPerRow = frameWidth * 4 + let bufferSize = frameHeight * bytesPerRow + guard let buffer = malloc(bufferSize) else { + return nil + } + let outPtr = buffer.assumingMemoryBound(to: UInt8.self) + + let colorX = Int(colorRect.origin.x) + let colorY = Int(colorRect.origin.y) + let alphaX = Int(alphaRect.origin.x) + let alphaY = Int(alphaRect.origin.y) + + decodedFrame.y.withUnsafeBytes { yBuf in + decodedFrame.cb.withUnsafeBytes { cbBuf in + decodedFrame.cr.withUnsafeBytes { crBuf in + let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + for row in 0 ..< frameHeight { + for col in 0 ..< frameWidth { + let srcY = yPtr[(colorY + row) * decodedWidth + (colorX + col)] + let srcCb = cbPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcCr = crPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcA = yPtr[(alphaY + row) * decodedWidth + (alphaX + col)] + + let yVal = Int(srcY) - 16 + let cbVal = Int(srcCb) - 128 + let crVal = Int(srcCr) - 128 + + var r = (298 * yVal + 459 * crVal + 128) >> 8 + var g = (298 * yVal - 55 * cbVal - 136 * crVal + 128) >> 8 + var b = (298 * yVal + 541 * cbVal + 128) >> 8 + + r = max(0, min(255, r)) + g = max(0, min(255, g)) + b = max(0, min(255, b)) + + let a = Int(srcA) + let pr = (r * a + 127) / 255 + let pg = (g * a + 127) / 255 + let pb = (b * a + 127) / 255 + + let outOffset = (row * frameWidth + col) * 4 + outPtr[outOffset + 0] = UInt8(pb) + outPtr[outOffset + 1] = UInt8(pg) + outPtr[outOffset + 2] = UInt8(pr) + outPtr[outOffset + 3] = UInt8(a) + } + } + } + } + } + + guard let dataProvider = CGDataProvider(dataInfo: nil, data: buffer, size: bufferSize, releaseData: { _, data, _ in + free(UnsafeMutableRawPointer(mutating: data)) + }) else { + free(buffer) + return nil + } + + return CGImage( + width: frameWidth, + height: frameHeight, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue), + provider: dataProvider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent + ) +} + +public final class SubcodecMultiAnimationRendererImpl: MultiAnimationRenderer { + private struct SizeKey: Hashable { + let width: Int + let height: Int + } + + private var surfaceGroups: [SizeKey: SurfaceGroup] = [:] + private var frameSkip: Int + private var displayTimer: Foundation.Timer? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayTimer == nil { + final class TimerTarget: NSObject { + private let f: () -> Void + init(_ f: @escaping () -> Void) { + self.f = f + } + @objc func timerEvent() { + self.f() + } + } + let frameInterval = Double(self.frameSkip) / 60.0 + let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick(frameInterval: frameInterval) + }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) + self.displayTimer = displayTimer + RunLoop.main.add(displayTimer, forMode: .common) + } + } else { + if let displayTimer = self.displayTimer { + self.displayTimer = nil + displayTimer.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + guard let subcodecCache = cache as? SubcodecAnimationCacheImpl else { + return EmptyDisposable + } + + let spriteWidth = roundUp(Int(size.width), multiple: 16) + let spriteHeight = roundUp(Int(size.height), multiple: 16) + let sizeKey = SizeKey(width: spriteWidth, height: spriteHeight) + + let fetchDisposable = MetaDisposable() + + fetchDisposable.set(subcodecCache.fetchIfNeeded(sourceId: itemId, size: size, fetch: fetch, completion: { [weak self, weak target] mbsPath in + Queue.mainQueue().async { + guard let strongSelf = self, let target = target, let mbsPath = mbsPath else { + return + } + + guard let metadata = subcodecCache.loadMetadata(sourceId: itemId, size: size) else { + return + } + + let surfaceGroup: SurfaceGroup + if let existing = strongSelf.surfaceGroups[sizeKey] { + surfaceGroup = existing + } else { + surfaceGroup = SurfaceGroup(spriteWidth: spriteWidth, spriteHeight: spriteHeight) + strongSelf.surfaceGroups[sizeKey] = surfaceGroup + } + + guard let spriteContext = surfaceGroup.addSprite(mbsPath: mbsPath, metadata: metadata) else { + return + } + + let targetIndex = spriteContext.targets.add(Weak(target)) + target.numFrames = metadata.frameCount + + let slot = Int(spriteContext.region.slot) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak surfaceGroup] in + Queue.mainQueue().async { + guard let strongSelf = self, let surfaceGroup = surfaceGroup else { + return + } + spriteContext.targets.remove(targetIndex) + if spriteContext.targets.isEmpty { + surfaceGroup.removeSprite(slot: slot) + if surfaceGroup.isEmpty { + strongSelf.surfaceGroups.removeValue(forKey: sizeKey) + } + strongSelf.updateIsPlaying() + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak self] in + self?.updateIsPlaying() + } + + fetchDisposable.set(ActionDisposable { [weak self, weak surfaceGroup, weak target] in + guard let strongSelf = self, let surfaceGroup = surfaceGroup else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + spriteContext.targets.remove(targetIndex) + if spriteContext.targets.isEmpty { + surfaceGroup.removeSprite(slot: slot) + if surfaceGroup.isEmpty { + strongSelf.surfaceGroups.removeValue(forKey: sizeKey) + } + strongSelf.updateIsPlaying() + } + }) + + strongSelf.updateIsPlaying() + } + })) + + return ActionDisposable { + fetchDisposable.dispose() + }.strict() + } + + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { + guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { + return false + } + switch frame.frame.format { + case let .rgba(data, width, height, bytesPerRow): + guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { + return false + } + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + guard let image = context.generateImage() else { + return false + } + target.contents = image.cgImage + target.numFrames = item.numFrames + return true + default: + return false + } + } + return false + } + + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + return cache.getFirstFrame(queue: .mainQueue(), sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] result in + guard let item = result.item else { + Queue.mainQueue().async { + completion(false, result.isFinal) + } + return + } + + let loaded: Bool + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + switch frame.frame.format { + case let .rgba(data, width, height, bytesPerRow): + Queue.mainQueue().async { + guard let target = target else { + completion(false, true) + return + } + target.numFrames = item.numFrames + if let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) { + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + if let image = context.generateImage() { + target.contents = image.cgImage + completion(true, true) + return + } + } + completion(false, true) + } + return + default: + loaded = false + } + } else { + loaded = false + } + + Queue.mainQueue().async { + completion(loaded, true) + } + }).strict() + } + + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: .mainQueue(), sourceId: itemId, size: size, fetch: fetch, completion: { result in + guard let item = result.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + switch frame.frame.format { + case let .rgba(data, width, height, bytesPerRow): + Queue.mainQueue().async { + if let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) { + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + completion(context.generateImage()?.cgImage) + } else { + completion(nil) + } + } + return + default: + break + } + } + + Queue.mainQueue().async { + completion(nil) + } + }).strict() + } + + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, group) in self.surfaceGroups { + if group.hasPlayingSprites { + isPlaying = true + break + } + } + self.isPlaying = isPlaying + } + + private func animationTick(frameInterval: Double) { + for (_, group) in self.surfaceGroups { + if group.hasPlayingSprites { + group.tick(advanceTimestamp: frameInterval) + } + } + } +} diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index eca7ccd52f..419d1a1e27 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -20,6 +20,8 @@ import FetchManagerImpl import InAppPurchaseManager import AnimationCache import MultiAnimationRenderer +import DCTAnimationCacheImpl +import DCTMultiAnimationRendererImpl import AppBundle import DirectMediaImageCache @@ -317,15 +319,15 @@ public final class AccountContextImpl: AccountContext { self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl() let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox - self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + self.animationCache = DCTAnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }, updateStorageStats: { path, size in if let pathData = path.data(using: .utf8) { cacheStorageBox.update(id: pathData, size: size) } }) - self.animationRenderer = MultiAnimationRendererImpl() - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache + self.animationRenderer = DCTMultiAnimationRendererImpl() + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration]) |> map { preferences -> LimitsConfiguration in @@ -496,7 +498,7 @@ public final class AccountContextImpl: AccountContext { guard let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) else { return } - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache }) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 959b23e6bc..059a2934cc 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -445,6 +445,22 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea } if preload { + var channelMessage: Signal = .single(Void()) + if case .channelPost = subject, let channelMessageId = replyThreadMessage.channelMessageId { + channelMessage = context.engine.messages.getMessagesLoadIfNecessary([channelMessageId], strategy: .cloud(skipLocal: false)) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .never() + case .result: + return .single(Void()) + } + } + |> `catch` { _ -> Signal in + return .single(Void()) + } + } + let preloadSignal = preloadedChatHistoryViewForLocation( input, context: context, @@ -455,8 +471,8 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea tag: nil, additionalData: [] ) - return preloadSignal - |> map { historyView -> Bool? in + return combineLatest(preloadSignal, channelMessage) + |> map { historyView, _ -> Bool? in switch historyView { case .Loading: return nil diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 8e54a2d406..34257ee68c 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -600,9 +600,12 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .peer(.name(pathComponents[1]), .boost) } else if pathComponents[0] == "giftcode", pathComponents.count == 2 { return .premiumGiftCode(slug: pathComponents[1]) - } else if pathComponents.count >= 3 && pathComponents[0] == "newbot" { + } else if pathComponents.count >= 2 && pathComponents[0] == "newbot" { let parentBot = pathComponents[1] - let username = pathComponents[2] + var username: String? + if pathComponents.count >= 3 { + username = pathComponents[2] + } var title: String? for queryItem in components.queryItems ?? [] { if let value = queryItem.value { diff --git a/third-party/openh264/BUILD b/third-party/openh264/BUILD index 845e670c25..cb340b6fb7 100644 --- a/third-party/openh264/BUILD +++ b/third-party/openh264/BUILD @@ -267,3 +267,72 @@ cc_library( "//visibility:public", ], ) + +cc_library( + name = "openh264_decoder", + srcs = [ + "third_party/openh264/src/codec/decoder/core/src/au_parser.cpp", + "third_party/openh264/src/codec/decoder/core/src/bit_stream.cpp", + "third_party/openh264/src/codec/decoder/core/src/cabac_decoder.cpp", + "third_party/openh264/src/codec/decoder/core/src/deblocking.cpp", + "third_party/openh264/src/codec/decoder/core/src/decode_mb_aux.cpp", + "third_party/openh264/src/codec/decoder/core/src/decode_slice.cpp", + "third_party/openh264/src/codec/decoder/core/src/decoder.cpp", + "third_party/openh264/src/codec/decoder/core/src/decoder_core.cpp", + "third_party/openh264/src/codec/decoder/core/src/decoder_data_tables.cpp", + "third_party/openh264/src/codec/decoder/core/src/error_concealment.cpp", + "third_party/openh264/src/codec/decoder/core/src/fmo.cpp", + "third_party/openh264/src/codec/decoder/core/src/get_intra_predictor.cpp", + "third_party/openh264/src/codec/decoder/core/src/manage_dec_ref.cpp", + "third_party/openh264/src/codec/decoder/core/src/memmgr_nal_unit.cpp", + "third_party/openh264/src/codec/decoder/core/src/mv_pred.cpp", + "third_party/openh264/src/codec/decoder/core/src/parse_mb_syn_cabac.cpp", + "third_party/openh264/src/codec/decoder/core/src/parse_mb_syn_cavlc.cpp", + "third_party/openh264/src/codec/decoder/core/src/pic_queue.cpp", + "third_party/openh264/src/codec/decoder/core/src/rec_mb.cpp", + "third_party/openh264/src/codec/decoder/core/src/wels_decoder_thread.cpp", + "third_party/openh264/src/codec/decoder/plus/src/welsDecoderExt.cpp", + "third_party/openh264/src/codec/decoder/core/inc/au_parser.h", + "third_party/openh264/src/codec/decoder/core/inc/bit_stream.h", + "third_party/openh264/src/codec/decoder/core/inc/cabac_decoder.h", + "third_party/openh264/src/codec/decoder/core/inc/deblocking.h", + "third_party/openh264/src/codec/decoder/core/inc/dec_frame.h", + "third_party/openh264/src/codec/decoder/core/inc/dec_golomb.h", + "third_party/openh264/src/codec/decoder/core/inc/decode_mb_aux.h", + "third_party/openh264/src/codec/decoder/core/inc/decode_slice.h", + "third_party/openh264/src/codec/decoder/core/inc/decoder.h", + "third_party/openh264/src/codec/decoder/core/inc/decoder_context.h", + "third_party/openh264/src/codec/decoder/core/inc/decoder_core.h", + "third_party/openh264/src/codec/decoder/core/inc/error_code.h", + "third_party/openh264/src/codec/decoder/core/inc/error_concealment.h", + "third_party/openh264/src/codec/decoder/core/inc/fmo.h", + "third_party/openh264/src/codec/decoder/core/inc/get_intra_predictor.h", + "third_party/openh264/src/codec/decoder/core/inc/manage_dec_ref.h", + "third_party/openh264/src/codec/decoder/core/inc/mb_cache.h", + "third_party/openh264/src/codec/decoder/core/inc/memmgr_nal_unit.h", + "third_party/openh264/src/codec/decoder/core/inc/mv_pred.h", + "third_party/openh264/src/codec/decoder/core/inc/nal_prefix.h", + "third_party/openh264/src/codec/decoder/core/inc/nalu.h", + "third_party/openh264/src/codec/decoder/core/inc/parameter_sets.h", + "third_party/openh264/src/codec/decoder/core/inc/parse_mb_syn_cabac.h", + "third_party/openh264/src/codec/decoder/core/inc/parse_mb_syn_cavlc.h", + "third_party/openh264/src/codec/decoder/core/inc/pic_queue.h", + "third_party/openh264/src/codec/decoder/core/inc/picture.h", + "third_party/openh264/src/codec/decoder/core/inc/rec_mb.h", + "third_party/openh264/src/codec/decoder/core/inc/slice.h", + "third_party/openh264/src/codec/decoder/core/inc/vlc_decoder.h", + "third_party/openh264/src/codec/decoder/core/inc/wels_common_basis.h", + "third_party/openh264/src/codec/decoder/plus/inc/welsDecoderExt.h", + ], + includes = ["."], + copts = [ + "-Ithird-party/openh264/third_party/openh264/src/codec/decoder/core/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/decoder/plus/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/common/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/wels", + "-Os", + "-Wno-all", + ], + deps = [":openh264"], + visibility = ["//visibility:public"], +) diff --git a/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h b/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h index 9b13c33cda..6fd8bd41e5 100644 --- a/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h +++ b/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h @@ -313,7 +313,10 @@ typedef enum { LEVEL_4_2 = 42, LEVEL_5_0 = 50, LEVEL_5_1 = 51, - LEVEL_5_2 = 52 + LEVEL_5_2 = 52, + LEVEL_6_0 = 60, + LEVEL_6_1 = 61, + LEVEL_6_2 = 62 } ELevelIdc; /** @@ -592,6 +595,7 @@ typedef struct TagEncParamExt { bool bIsLosslessLink; ///< LTR advanced setting bool bFixRCOverShoot; ///< fix rate control overshooting int iIdrBitrateRatio; ///< the target bits of IDR is (idr_bitrate_ratio/100) * average target bit per frame. + bool bSubcodecMode; ///< enable subcodec sprite-compositing constraints (MV cap, no intra in P-frames, no sub-partitions, padding recon override, reduced log2_max_frame_num); default false } SEncParamExt; /** diff --git a/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h b/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h index 96ba01a79d..e3ef1b4070 100644 --- a/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h +++ b/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h @@ -44,7 +44,7 @@ namespace WelsCommon { #define CTX_NA 0 #define WELS_CONTEXT_COUNT 460 -#define LEVEL_NUMBER 17 +#define LEVEL_NUMBER 20 typedef struct TagLevelLimits { ELevelIdc uiLevelIdc; // level idc uint32_t uiMaxMBPS; // Max macroblock processing rate(MB/s) diff --git a/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp b/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp index 117b28d089..6dfe9536a9 100644 --- a/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp @@ -363,10 +363,14 @@ const SLevelLimits g_ksLevelLimits[LEVEL_NUMBER] = { {LEVEL_5_0, 589824, 22080, 110400, 135000, 135000, -2048, 2047, 2, 16}, /* level 5 */ {LEVEL_5_1, 983040, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16}, /* level 5.1 */ - {LEVEL_5_2, 2073600, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16} /* level 5.2 */ + {LEVEL_5_2, 2073600, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16}, /* level 5.2 */ + + {LEVEL_6_0, 4177920, 139264, 696320, 240000, 240000, -8192, 8191, 2, 16}, /* level 6.0 */ + {LEVEL_6_1, 8355840, 139264, 696320, 480000, 480000, -8192, 8191, 2, 16}, /* level 6.1 */ + {LEVEL_6_2, 16711680, 139264, 696320, 800000, 800000, -8192, 8191, 2, 16} /* level 6.2 */ }; const uint32_t g_kuiLevelMaps[LEVEL_NUMBER] = { - 10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52 + 10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52, 60, 61, 62 }; //for cabac /* this table is from Table9-12 to Table 9-24 */ diff --git a/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp b/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp index 91f89b4374..f74149c462 100644 --- a/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp @@ -829,6 +829,12 @@ const SLevelLimits* GetLevelLimits (int32_t iLevelIdx, bool bConstraint3) { return &g_ksLevelLimits[15]; case 52: return &g_ksLevelLimits[16]; + case 60: + return &g_ksLevelLimits[17]; + case 61: + return &g_ksLevelLimits[18]; + case 62: + return &g_ksLevelLimits[19]; default: return NULL; } diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h index a12106e9cb..b7b4b18af4 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h @@ -107,7 +107,7 @@ int32_t WelsWritePpsSyntax (SWelsPPS* pPps, SBitStringAux* pBitStringAux, IWelsP int32_t WelsInitSps (SWelsSPS* pSps, SSpatialLayerConfig* pLayerParam, SSpatialLayerInternal* pLayerParamInternal, const uint32_t kuiIntraPeriod, const int32_t kiNumRefFrame, const uint32_t kiSpsId, const bool kbEnableFrameCropping, bool bEnableRc, - const int32_t kiDlayerCount,bool bSVCBaselayer); + const int32_t kiDlayerCount, bool bSVCBaselayer, bool bSubcodecMode); /*! * \brief initialize subset pSps based on configurable parameters in svc diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h index dff33c490a..3a5e6d642c 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h @@ -345,6 +345,7 @@ typedef struct TagWelsSvcCodingParam: SEncParamExt { bIsLosslessLink = pCodingParam.bIsLosslessLink; bFixRCOverShoot = pCodingParam.bFixRCOverShoot; iIdrBitrateRatio = pCodingParam.iIdrBitrateRatio; + bSubcodecMode = pCodingParam.bSubcodecMode; if (iUsageType == SCREEN_CONTENT_REAL_TIME && !bIsLosslessLink && bEnableLongTermReference) { bEnableLongTermReference = false; } diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp index eeac1ec114..78e7523f7a 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp @@ -485,14 +485,14 @@ static inline bool WelsGetPaddingOffset (int32_t iActualWidth, int32_t iActualHe int32_t WelsInitSps (SWelsSPS* pSps, SSpatialLayerConfig* pLayerParam, SSpatialLayerInternal* pLayerParamInternal, const uint32_t kuiIntraPeriod, const int32_t kiNumRefFrame, const uint32_t kuiSpsId, const bool kbEnableFrameCropping, bool bEnableRc, - const int32_t kiDlayerCount, bool bSVCBaselayer) { + const int32_t kiDlayerCount, bool bSVCBaselayer, bool bSubcodecMode) { memset (pSps, 0, sizeof (SWelsSPS)); pSps->uiSpsId = kuiSpsId; pSps->iMbWidth = (pLayerParam->iVideoWidth + 15) >> 4; pSps->iMbHeight = (pLayerParam->iVideoHeight + 15) >> 4; //max value of both iFrameNum and POC are 2^16-1, in our encoder, iPOC=2*iFrameNum, so max of iFrameNum should be 2^15-1.-- - pSps->uiLog2MaxFrameNum = 15;//16; + pSps->uiLog2MaxFrameNum = bSubcodecMode ? 4 : 15;//16; pSps->iLog2MaxPocLsb = 1 + pSps->uiLog2MaxFrameNum; pSps->iNumRefFrames = kiNumRefFrame; /* min pRef size when fifo pRef operation*/ @@ -565,7 +565,7 @@ int32_t WelsInitSubsetSps (SSubsetSps* pSubsetSps, SSpatialLayerConfig* pLayerPa memset (pSubsetSps, 0, sizeof (SSubsetSps)); WelsInitSps (pSps, pLayerParam, pLayerParamInternal, kuiIntraPeriod, kiNumRefFrame, kuiSpsId, kbEnableFrameCropping, - bEnableRc, kiDlayerCount, false); + bEnableRc, kiDlayerCount, false, false); pSps->uiProfileIdc = pLayerParam->uiProfileIdc ; diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp index 7fdbc614bf..7e0db425d6 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp @@ -1526,6 +1526,13 @@ void GetMvMvdRange (SWelsSvcCodingParam* pParam, int32_t& iMvRange, int32_t& iMv iMvRange = WELS_MIN (iMvRange, iFixMvRange); + // [subcodec] Limit MV range to 16 pixels for sprite compositing. + // Sprites use a 16px padded canvas; tighter MV range prevents referencing + // pixels outside the padding that will differ in the composite frame. + if (pParam->bSubcodecMode) { + iMvRange = WELS_MIN (iMvRange, 16); + } + iMvdRange = (iMvRange + 1) << 1; iMvdRange = WELS_MIN (iMvdRange, iFixMvdRange); diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp index a506362a47..5fb50dcba9 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp @@ -94,7 +94,7 @@ static int32_t WelsGenerateNewSps (sWelsEncCtx* pCtx, const bool kbUseSubsetSps, iRet = WelsInitSps (pSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod, pParam->iMaxNumRefFrame, kiSpsId, pParam->bEnableFrameCroppingFlag, pParam->iRCMode != RC_OFF_MODE, iDlayerCount, - bSVCBaselayer); + bSVCBaselayer, pParam->bSubcodecMode); } else { iRet = WelsInitSubsetSps (pSubsetSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod, pParam->iMaxNumRefFrame, @@ -178,7 +178,7 @@ int32_t FindExistingSps (SWelsSvcCodingParam* pParam, const bool kbUseSubsetSps, WelsInitSps (&sTmpSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod, pParam->iMaxNumRefFrame, 0, pParam->bEnableFrameCroppingFlag, pParam->iRCMode != RC_OFF_MODE, iDlayerCount, - bSVCBaseLayer); + bSVCBaseLayer, pParam->bSubcodecMode); for (int32_t iId = 0; iId < iSpsNumInUse; iId++) { if (CheckMatchedSps (&sTmpSps, &pSpsArray[iId])) { return iId; diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp index 5acc2d922b..7ba01a86bb 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp @@ -930,6 +930,11 @@ int32_t WelsMdIntraChroma (SWelsFuncPtrList* pFunc, SDqLayer* pCurDqLayer, SMbCa return iBestCost; } int32_t WelsMdIntraFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) { + // [subcodec] I_4x4 disabled for sprite compositing — only I_16x16 allowed. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return pWelsMd->iCostLuma; + } + int32_t iCosti4x4 = WelsMdI4x4 (pEncCtx, pWelsMd, pCurMb, pMbCache); if (iCosti4x4 < pWelsMd->iCostLuma) { @@ -940,6 +945,10 @@ int32_t WelsMdIntraFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* p } int32_t WelsMdIntraFinePartitionVaa (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) { + // [subcodec] I_4x4 disabled for sprite compositing — only I_16x16 allowed. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return pWelsMd->iCostLuma; + } if (MdIntraAnalysisVaaInfo (pEncCtx, pMbCache->SPicData.pEncMb[0])) { int32_t iCosti4x4 = WelsMdI4x4Fast (pEncCtx, pWelsMd, pCurMb, pMbCache); @@ -1236,6 +1245,11 @@ int32_t WelsMdP4x8 (SWelsFuncPtrList* pFunc, SDqLayer* pCurDqLayer, SWelsMD* pWe } void WelsMdInterFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) { + // [subcodec] No sub-partition modes for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return; + } + SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer; // SMbCache *pMbCache = &pSlice->sMbCacheInfo; int32_t iCost = 0; @@ -1269,6 +1283,11 @@ void WelsMdInterFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* p void WelsMdInterFinePartitionVaa (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) { + // [subcodec] No sub-partition modes for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return; + } + SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer; // SMbCache *pMbCache = &pSlice->sMbCacheInfo; int32_t iCostP8x16, iCostP16x8, iCostP8x8; @@ -1827,6 +1846,11 @@ void WelsMdInterMbRefinement (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurM } bool WelsMdFirstIntraMode (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) { + // [subcodec] Never select intra MBs in P-frames for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return false; + } + SWelsFuncPtrList* pFunc = pEncCtx->pFuncList; int32_t iCostI16x16 = WelsMdI16x16 (pFunc, pEncCtx->pCurDqLayer, pMbCache, pWelsMd->iLambda); diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp index 90139136f9..bff5ac0e9c 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp @@ -564,6 +564,33 @@ TRY_REENCODING: if (ENC_RETURN_SUCCESS != iEncReturn) return iEncReturn; + // [subcodec] For padding MBs, overwrite reconstruction buffer with exact black. + // This ensures content MBs' I_16x16 predictions are computed against exact-black + // neighbors, making their coefficients transplantable to the composite frame. + { + if (pEncCtx->pSvcParam->bSubcodecMode) { + const int32_t kiMbX = iCurMbIdx % pCurLayer->iMbWidth; + const int32_t kiMbY = iCurMbIdx / pCurLayer->iMbWidth; + if (kiMbX < 1 || kiMbX >= pCurLayer->iMbWidth - 1 || + kiMbY < 1 || kiMbY >= pCurLayer->iMbHeight - 1) { + // Overwrite luma reconstruction to black (Y=0) + uint8_t* pCsY = pMbCache->SPicData.pCsMb[0]; + const int32_t kiCsStrideY = pCurLayer->iCsStride[0]; + for (int32_t r = 0; r < MB_WIDTH_LUMA; r++) { + memset(pCsY + r * kiCsStrideY, 0, MB_WIDTH_LUMA); + } + // Overwrite chroma reconstruction to neutral (Cb=128, Cr=128) + uint8_t* pCsCb = pMbCache->SPicData.pCsMb[1]; + uint8_t* pCsCr = pMbCache->SPicData.pCsMb[2]; + const int32_t kiCsStrideUV = pCurLayer->iCsStride[1]; + for (int32_t r = 0; r < MB_WIDTH_CHROMA; r++) { + memset(pCsCb + r * kiCsStrideUV, 128, MB_WIDTH_CHROMA); + memset(pCsCr + r * kiCsStrideUV, 128, MB_WIDTH_CHROMA); + } + } + } + } + pCurMb->uiSliceIdc = kiSliceIdx; #if defined(MB_TYPES_CHECK) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp index 5b4793561c..e6541cf9cc 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp @@ -611,6 +611,11 @@ bool TryModeMerge (SMbCache* pMbCache, SWelsMD* pWelsMd, SMB* pCurMb) { void WelsMdInterFinePartitionVaaOnScreen (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) { + // [subcodec] No sub-partition modes for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return; + } + SMbCache* pMbCache = &pSlice->sMbCacheInfo; SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer; int32_t iCostP8x8; diff --git a/third-party/subcodec/.gitignore b/third-party/subcodec/.gitignore new file mode 100644 index 0000000000..e31c357df5 --- /dev/null +++ b/third-party/subcodec/.gitignore @@ -0,0 +1,11 @@ +build/ +build-*/ +build_*/ +.build/ +.swiftpm/ +*.h264 +*.yuv +*.mbs +*.trace/ +Tests/SubcodecTests/Fixtures/ +.DS_Store diff --git a/third-party/subcodec/BUILD b/third-party/subcodec/BUILD new file mode 100644 index 0000000000..1a9d598989 --- /dev/null +++ b/third-party/subcodec/BUILD @@ -0,0 +1,84 @@ +cc_library( + name = "h264bitstream", + srcs = [ + "third_party/h264bitstream/h264_stream.c", + "third_party/h264bitstream/h264_nal.c", + "third_party/h264bitstream/h264_sei.c", + ], + hdrs = glob(["third_party/h264bitstream/*.h"]), + includes = ["third_party/h264bitstream"], + copts = ["-Wno-all"], +) + +cc_library( + name = "subcodec", + srcs = [ + "src/frame_writer.cpp", + "src/cavlc.cpp", + "src/h264_parser.cpp", + "src/sprite_data.cpp", + "src/mbs_encode.cpp", + "src/mbs_mux_common.cpp", + "src/mux_surface.cpp", + ], + hdrs = glob(["src/*.h"]), + includes = ["src"], + copts = [ + "-std=c++2b", + "-Wno-all", + ], + deps = [":h264bitstream"], +) + +cc_library( + name = "sprite_encode", + srcs = [ + "src/sprite_encode.cpp", + "src/sprite_extractor.cpp", + ], + hdrs = glob(["src/*.h"]), + includes = ["src"], + copts = [ + "-std=c++2b", + "-Wno-all", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/wels", + ], + deps = [ + ":subcodec", + "//third-party/openh264:openh264", + ], +) + +objc_library( + name = "SubcodecObjC", + module_name = "SubcodecObjC", + enable_modules = True, + srcs = [ + "Sources/SubcodecObjC/SCSprite.mm", + "Sources/SubcodecObjC/SCMuxSurface.mm", + "Sources/SubcodecObjC/SCSpriteRegion.mm", + "Sources/SubcodecObjC/SCDecodedFrame.mm", + "Sources/SubcodecObjC/SCOpenH264Decoder.mm", + "Sources/SubcodecObjC/SCVideoToolboxDecoder.mm", + "Sources/SubcodecObjC/AnnexBSplitter.h", + ], + hdrs = glob(["Sources/SubcodecObjC/include/*.h"]), + includes = ["Sources/SubcodecObjC/include"], + copts = [ + "-std=c++2b", + "-Wno-all", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/wels", + ], + deps = [ + ":subcodec", + ":sprite_encode", + "//third-party/openh264:openh264", + "//third-party/openh264:openh264_decoder", + ], + sdk_frameworks = [ + "VideoToolbox", + "CoreMedia", + "CoreVideo", + ], + visibility = ["//visibility:public"], +) diff --git a/third-party/subcodec/CLAUDE.md b/third-party/subcodec/CLAUDE.md new file mode 100644 index 0000000000..4753ebbcb4 --- /dev/null +++ b/third-party/subcodec/CLAUDE.md @@ -0,0 +1,383 @@ +# subcodec + +C++23 library that constructs H.264 Baseline/High Profile bitstreams for sprite multiplexing. Composites many small sprite animations into a single H.264 stream decoded by one hardware decoder, using selective macroblock updates (P_16x16, I_16x16, skip) in P-frames. + +## Build + +```bash +cmake -B build && cmake --build build +``` + +- This project lives inside `telegram-ios/third-party/subcodec/` +- CMake 3.16+, C++23 standard (for `std::expected`); OpenH264 compiled as C++11 +- Dependencies: FFmpeg (libavcodec, libavformat, libavutil, libswscale) — required for sprite_extract video input; vendored h264bitstream; OpenH264 encoder + decoder (Telegram's patched copy at the sibling `../openh264/` directory, accessed via symlink `third_party/openh264_codec` — not vendored here) +- Produces: `libsubcodec.a`, 25 test executables, `sprite_extract` tool, `sprite_mux` tool + +### SwiftPM + +```bash +cmake --build build --target fixtures # generate YUV fixtures before running Swift tests +swift build # builds core library + OpenH264 + sprite_encode + ObjC++ wrapper +swift test # runs Swift XCTest suite (12 tests including 160-frame e2e) +``` + +- Swift 5.9+, targets macOS 10.15+/iOS 13+/tvOS 13+ +- SwiftPM targets: `h264bitstream`, `subcodec`, `oh264_common`, `oh264_processing`, `oh264_encoder`, `oh264_decoder`, `sprite_encode`, `SubcodecObjC`, `SubcodecTests` +- OpenH264 is referenced via the `third_party/openh264_codec` symlink pointing to Telegram's sibling `../openh264/` directory +- CMake remains the primary build system; SwiftPM builds alongside it + +## Project Structure + +- `src/types.h` — Core types: `MacroblockData`, `FrameParams`, `MbContext`, `MbsRow`, `MbsEncodedFrame` (merged rows only), `MbsFrame` (`merged_rows` span), `MbsSprite` (bulk data + row storage) +- `src/error.h` — `subcodec::Error` enum class +- `src/tables.h` — Compile-time lookup tables (CBP, block scan order) +- `src/frame_writer.cpp/.h` — `subcodec::frame_writer` namespace: `write_headers` (auto-selects Baseline/High Profile + level from frame size), `write_idr_frame_ex`, `write_p_frame_ex` +- `src/cavlc.cpp/.h` — `subcodec::cavlc` namespace: CAVLC entropy coding and decoding (spec tables, block read/write, nC context) +- `src/h264_parser.cpp/.h` — `subcodec::H264Parser` class: H.264 Baseline CAVLC slice parser with RAII buffers +- `src/mux_surface.cpp/.h` — `subcodec::MuxSurface` class: streaming mux surface with sprite loading/removal/looping, P-frame emission, dynamic resize. `Params` takes `sprite_width`/`sprite_height` in content pixels (padding added internally). `add_sprite` returns `SpriteRegion` (slot index + color/alpha content pixel rects in composite frame); also sets `needs_emit`/`dirty_` so the next `emit_frame_if_needed` emits the sprite's frame 0 introduction. `advance_sprite(slot)` schedules one sprite for emit+advance (sets `needs_emit` flag); `emit_frame_if_needed(sink)` emits a P-frame if any sprites are scheduled, then advances their frame indices (emit-then-advance semantics). `advance_frame(sink)` is a convenience wrapper that schedules all active sprites then calls `emit_frame_if_needed`. `resize` emits new SPS/PPS + all-I_PCM IDR to change grid dimensions mid-stream, compacting active sprites. `check_compaction_opportunity` returns `CompactionInfo` for caller-driven resize decisions +- `src/mbs_mux_common.cpp/.h` — `subcodec::mux` namespace: shared muxing primitives, `EbspWriter` (single-pass direct EBSP output with inline escape checking + zero-run metadata fast path + `flush_bytes` for NEON-accelerated bulk byte writing), `RbspWriter` (branchless RBSP staging writer, no escape checking), `MicroOp` (pre-resolved blob operation), `RowOp`/`CompositeRowPlan` (precomputed composite grid layout), `build_row_plans`, `build_micro_ops`, `write_p_frame_micro` (active P-frame path), `write_p_frame_rbsp` (legacy two-pass path), `write_idr_ipcm` (inline in header — all-I_PCM IDR for resize transitions, single-pass EbspWriter with precomputed black-MB fast path), `write_skip_safe` (splits long mb_skip_runs with dummy P_16x16 zero-residual MBs for VT compatibility), `scan_zero_runs`, `rbsp_to_ebsp_neon`, exp-golomb LUT, row-blob copy, RBSP↔EBSP conversion +- `src/mbs_encode.cpp/.h` — `subcodec::mbs` namespace: MBS binary format encoding with real nC context and zero-run scanning, returns `MbsEncodedFrame` +- `src/mbs_format.h` — MBS binary format constants (MBS6 format). Header: magic `MBS6` (4 bytes) + width_mbs(2) + height_mbs(2) + num_frames(2) + qp(1) + qp_delta_idr(1) + qp_delta_p(1) + flags(1) = 14 bytes +- `src/sprite_data.cpp` — `MbsSprite` file I/O (v6 bulk load/save with pre-merged blobs), `set_frames()` — no runtime merge needed +- `src/sprite_encode.cpp/.h` — `subcodec::SpriteEncoder` class: OpenH264 encode + parse pipeline. `Params` takes content `width`/`height` in pixels (padding added internally). Encodes on a double-wide canvas (color left, alpha right) and returns `EncodeResult{color, alpha}`. +- `src/sprite_extractor.cpp/.h` — `subcodec::SpriteExtractor` class: raw YUV+alpha → padded canvas → SpriteEncoder → .mbs file with pre-merged row blobs. `Params` takes content `sprite_size` in pixels (padding always 16px internally) +- `tools/sprite_extract.cpp` — CLI: video file → `.mbs` via FFmpeg decode + SpriteEncoder +- `tools/sprite_mux.cpp` — CLI: `.mbs` files → composite H.264 stream via MuxSurface +- `third_party/h264bitstream/` — Vendored NAL unit / bitstream primitives (C) +- `third_party/openh264_codec` — Symlink to Telegram's OpenH264 at the sibling `../openh264/` directory (not vendored here; patches documented below) +- `test/` — 25 standalone C++ test programs (no framework) +- `Sources/SubcodecObjC/` — ObjC++ wrapper: SCSprite, SCMuxSurface, SCSpriteRegion, SCResizeResult, SCCompactionInfo, SCOpenH264Decoder, SCVideoToolboxDecoder, SCDecodedFrame, SCDecoding protocol +- `Sources/SpriteEncode/` — SwiftPM wrapper compilation units for sprite_encode.cpp/sprite_extractor.cpp +- `Tests/SubcodecTests/` — Swift XCTest suite with YUV fixtures (3 sprites × 160 frames) +- `docs/plans/` — Design documents +- `docs/superpowers/specs/` — Feature specs + +## Tests + +```bash +cd build && ctest +``` + +Each test is a standalone C++ executable (no framework): +- `test_cavlc` — CAVLC write encoding correctness +- `test_cavlc_read` — CAVLC read/write round-trip across all 5 VLC tables +- `test_cavlc_diag` — CAVLC diagnostic tests +- `test_cavlc_split` — CAVLC split encoding tests +- `test_ct_lut` — Coeff token lookup table tests +- `test_mb_p16x16` — P_16x16 macroblock encoder + MV prediction +- `test_mb_i16x16` — I_16x16 macroblock encoder +- `test_p_frame_ex` — Extended P-frame writer (all MB types) +- `test_h264_parse` — H.264 slice parser round-trip (write -> parse -> verify) +- `test_idr_frame_ex` — Extended IDR frame writer round-trip +- `test_mbs_format` — MBS binary format encoding/decoding +- `test_mbs_encode` — MBS frame encoding +- `test_mux` (requires OpenH264) — End-to-end: 4 sprites x 8 frames, encode -> mux_surface -> decode -> pixel-identical verification +- `test_mux_surface` (requires OpenH264) — Streaming mux surface: staggered sprite add/remove, mid-stream I_16x16 introduction, sprite looping (2-cycle pixel-identical verification), pixel verification +- `test_sprite_extractor` (requires OpenH264) — SpriteExtractor pipeline test +- `test_bs_copy_bits` — Bulk bit copy correctness (aligned, unaligned, random round-trip) +- `test_mux_perf` — Mux performance stress test: 1764 sprites, 160 frames, per-frame timing +- `test_high_profile` (requires OpenH264) — High Profile SPS acceptance + large grid (52K MBs) mux verification +- `test_ebsp_writer` — EbspWriter unit tests: flush_byte EBSP escaping, write_bits accumulation, exp-golomb LUT correctness, copy_blob aligned/unaligned/partial/sequence verification against bs_t+rbsp_to_ebsp reference, fast-path (5-arg copy_blob) correctness at all bit alignments with escape-free and boundary-escape scenarios +- `test_row_plans` — Row plan precomputation correctness: single sprite, 2x2 grid, partial grid with inactive slots, empty grid (all double-wide slots) +- `test_sprite_encode_alpha` (requires OpenH264) — SpriteEncoder double-wide canvas: alpha MB split, cbp_chroma=0 verification +- `test_mux_alpha` (requires OpenH264) — End-to-end alpha mux: 2 sprites with varying alpha, pixel-identical verification of color and alpha regions against independent reference decode +- `test_rbsp_writer` — RbspWriter + rbsp_to_ebsp_neon verification: compares two-pass (RbspWriter → rbsp_to_ebsp_neon) output against single-pass EbspWriter reference at all bit alignments, with escape-triggering patterns and random data +- `test_ipcm` (requires OpenH264) — I_PCM IDR frame round-trip: write all-I_PCM IDR with gradient pattern, decode via OpenH264, pixel-identical verification +- `test_resize` (requires OpenH264) — MuxSurface dynamic resize: compaction info query, grow/shrink resize, error handling (too few slots), frame counter preservation, pixel-identical verification of sprite content across resize + +### Swift Tests (`swift test`) + +**Note:** Generate YUV fixtures before running Swift tests: `cmake --build build --target fixtures` + +- `testDecodedFrameCreation` — SCDecodedFrame data container +- `testSpriteExtractor` — YUV fixture → SpriteExtractor → .mbs round-trip (160 frames) +- `testSpriteEncoder` — YUV fixture → SpriteEncoder → H.264 stream (160 frames) +- `testEncoderDecodeRoundTrip` — Encode → OpenH264 decode → verify dimensions (8 frames) +- `testMuxSurfaceBasic` — MuxSurface create → add sprite → advance frame +- `testStaggeredAddRemove` — **Main e2e test**: 3 sprites × 160 frames, staggered add/remove, pixel-identical verification against independent reference decode +- `testVideoToolboxDecoder` — Encode 8 frames → VideoToolbox decode → verify frame count and dimensions +- `testDecoderCrossComparison` — Encode 8 frames → decode with both OpenH264 and VideoToolbox → pixel-by-pixel YUV comparison with +/-1 tolerance +- `testSpriteLooping` — 1 sprite × 320 frames (2 loops of 160), pixel-identical verification across loop boundary +- `testAdvanceSpriteIndependent` — 2 sprites with independent frame rates via `advanceSpriteAtSlot:` + `emitFrameIfNeededWithSink:`, pixel-identical verification that only advanced sprite progresses +- `testVideoToolboxPartialFill` — Partially-filled grid (10 sprites in 361-slot grid) decoded via VideoToolbox. Regression test for the write_skip_safe workaround — without it, VT rejects long skip_runs in partially-filled grids. +- `testVideoToolboxIPCM` — MuxSurface resize with I_PCM transition frame decoded via VideoToolbox. Verifies VT handles I_PCM IDR + post-resize P-frames. +- `testResizePerformance` — Resize performance: 420 sprites, resize 420→882 slots with real decoded pixels (OpenH264), wall-clock timing with performance gate (<200ms debug, ~5ms release). + +## Sprite Multiplexing Pipeline + +``` +Input video/YUV+alpha -> SpriteEncoder (double-wide padded canvas: color left, alpha right) + -> H264Parser -> MacroblockData (split into color + alpha halves) + -> save to .mbs (MbsSprite, pre-merged color+alpha row blobs per frame) + -> MuxSurface (double-wide grid: color+alpha side-by-side per slot) -> composite H.264 stream + -> hardware decoder (color and alpha regions decoded in a single frame) +``` + +### How compositing works + +1. **SpriteEncoder** (or **sprite_extract** CLI) encodes each sprite on a double-wide black-padded canvas: color left half (sprite content + padding border) and alpha right half (alpha-as-luma + neutral chroma Cb=Cr=128). For a 64x64 sprite with 16px padding, the canvas is 192x96 pixels (12x6 MBs). It parses the OpenH264 NAL output into `MacroblockData` via `H264Parser`, then splits into color (left half MBs) and alpha (right half MBs). Alpha MBs naturally have `cbp_chroma=0` (no chroma residual) since chroma is uniform 128. + +2. **MBS encoding** (`mbs::encode_frame_merged`) serializes `MacroblockData` into the `.mbs` binary format (MBS v6, magic `MBS6`) with **real nC context** (not canonical nC=0) and **pre-merged color+alpha row blobs**. Color and alpha halves are encoded separately, then merged at encode time into a single blob per row: `[color_data][ue(inter_skip)][alpha_data]`, with leading/trailing skips relative to the full double-wide slot width. Alpha is always present — every sprite has both color and alpha data. The CAVLC is encoded with the actual neighbor-derived nC values, computed from the padded sprite canvas context. MBs are grouped into **row blobs**: each row's non-skip MBs are packed into a contiguous bitstream segment with interleaved skip_run exp-golomb codes. Each row's 6-byte header stores `leading_skips`, `trailing_skips`, packed `blob_bit_count` (15-bit count + 1-bit `has_long_zero_run` flag), `leading_zero_bits`, and `trailing_zero_bits`. The zero-run metadata is computed by scanning each merged blob at encode time. This means the CAVLC bitstream is already correct for the composite grid layout (see "Why real-nC works" below). No runtime merge is needed at load or mux time. + +3. **MuxSurface** arranges sprites in a double-wide grid with shared padding borders. Each slot is `sprite_w * 2 - padding` MBs wide with color and alpha side-by-side, sharing a padding border between them. For the IDR frame (frame 0), all MBs are I_16x16 with DC prediction and zero residual (except MB(0,0) which needs a compensating luma DC coefficient since DC prediction defaults to 128 with no neighbors). This produces an all-black reference frame in ~1-2 bytes/MB. For P-frames, `advance_frame` has two phases: (a) `build_micro_ops` walks the precomputed `RowOp` plans and resolves all pointer chains into a flat `MicroOp` array (one entry per merged blob with data — inactive slots and all-skip rows are folded into skip counts); (b) `write_p_frame_micro` iterates the `MicroOp` array in a tight loop — `write_ue(skip)` + `copy_blob(merged_blob)` per op, using `EbspWriter` for inline EBSP escaping. When the blob's `has_long_zero_run` flag is false, `copy_blob` uses a fast path: only 3 boundary bytes go through `flush_byte` for EBSP escape checking, then the interior is bulk-copied (memcpy for aligned, NEON shift+write for non-aligned) with no EBSP checking — this is safe because the absence of 16-bit zero runs is alignment-invariant. No CAVLC re-encoding and no intermediate RBSP buffer needed. The composite grid layout is precomputed into flat `CompositeRowPlan` / `RowOp` arrays at `add_sprite`/`remove_sprite` time. `__builtin_prefetch` hides cache miss latency in both `build_micro_ops` and `write_p_frame_micro`. + +4. New sprites are introduced mid-stream as I_16x16 MBs in P-frames (their frame 0 data), without requiring an IDR reset. + +5. **Dynamic resize** (`MuxSurface::resize`) changes the grid size mid-stream. The caller provides decoded pixels of the last frame. MuxSurface emits new SPS/PPS + an all-I_PCM IDR with sprite pixels remapped to compacted slot positions in the new grid. Active sprites are compacted into slots 0..N-1 with frame counters preserved. Subsequent P-frames continue from the I_PCM reference. `check_compaction_opportunity` returns `CompactionInfo` (active/max slots, current/min grid MBs) so callers can decide when to resize. I_PCM costs 384 bytes/MB (up to 580 with EBSP escaping) — acceptable as a one-shot cost for an infrequent operation. The intermediate YUV planes use `make_unique_for_overwrite` + explicit `memset` (not `std::vector` fill) to avoid debug-mode per-element initialization overhead. The I_PCM IDR writer (`write_idr_ipcm`, inline in `mbs_mux_common.h`) uses single-pass EbspWriter with two paths: **black-MB fast path** (NEON detects all-black MBs Y=0/Cb=Cr=128, memcpy's precomputed EBSP pattern) and **non-black bulk path** (gathers MB samples into contiguous 384-byte buffer, single `flush_bytes` call with NEON-accelerated EBSP escaping). For a 420→882 slot resize (~45K MBs), this runs in ~2ms (Release) / ~16ms (Swift debug). + +### Key design decisions and why + +**Black-padded canvas (sprite + 1 MB border, hardcoded):** +Padding is always exactly 1 MB (16px) — hardcoded throughout the pipeline, not configurable via any API. All user-facing APIs take content-only dimensions; padding is added internally. Motion vectors in P-frames may reference pixels outside the sprite content area. The black padding ensures these references produce identical pixels in both the independent encode and the composite. OpenH264's MV range is capped at 16px (1 MB) to stay within the padding. + +**I_16x16 for IDR background:** +The IDR frame uses all-I_16x16 with DC prediction to establish exact black pixels (Y=0, Cb=128, Cr=128). MB(0,0) has no neighbors so DC prediction defaults to 128 for luma; a compensating luma DC coefficient (`black_dc_level(qp)`) corrects this to Y=0. All other MBs predict 0 from already-decoded black neighbors → zero residual. This produces ~1-2 bytes/MB (vs ~385 bytes/MB with the previous I_PCM approach). Removed sprites become SKIP MBs referencing the black IDR — no active cleanup needed. Sprites loop indefinitely (frame counter wraps to 0 at `num_frames`), replaying their I_16x16 introduction frame at each loop boundary. Sprites are only removed via explicit `remove_sprite()`. + +**Real-nC MBS encoding with row blobs (no CAVLC re-encoding at mux time):** +The 1-MB padding border ensures that the nC context for every content block is identical between the original sprite canvas and the composite grid: +- Content MBs neighbor either other content MBs from the same sprite (identical total_coeff) or SKIP padding (nC=0) +- This is true in both the original encode and the composite layout +- Therefore, CAVLC encoded with real nC on the sprite canvas is already correct for the composite + +The same argument applies to MV prediction (SKIP padding has MV=0 in both contexts). Each row blob (exp-golomb skip runs + CAVLC blocks) can be copied verbatim at mux time via `EbspWriter::copy_blob`. + +**Note:** Chroma DC blocks use nC=-1 (a fixed VLC table regardless of neighbors), so they never need re-encoding. Luma and chroma AC blocks use neighbor-derived nC, which is why real-nC encoding matters. + +**Alpha channel (side-by-side in composite):** +Alpha is required for all sprites — callers pass an alpha plane alongside Y/Cb/Cr (all-255 for opaque). SpriteEncoder encodes color+alpha on a single double-wide OpenH264 canvas (2× padded width × padded height). Alpha uses luma=alpha values with neutral chroma (Cb=Cr=128), producing `cbp_chroma=0` for all alpha MBs — no chroma data stored. In the composite, each slot places color and alpha side-by-side with shared padding: `[pad][color][shared pad][alpha][pad]` = `sprite_w * 2 - padding` MBs per slot. Color and alpha row blobs are pre-merged at encode time (MBS v6) into a single blob per row: `[color_data][ue(inter_skip)][alpha_data]`, with leading/trailing skips relative to the full slot width. The mux loop emits one merged blob per RowOp via a single `copy_blob` call. No runtime merge needed at load or mux time. Removed sprites' alpha regions become Y=0 (transparent) via SKIP referencing the all-black IDR. + +**No intra MBs in P-frames (OpenH264 patched):** +OpenH264 is patched (via `bSubcodecMode`) to prevent intra MB selection in P-slices and restrict all MBs to I_16x16/P_16x16/SKIP during encoding. This ensures P-frame MBs use only P_16x16 and SKIP (inter prediction from reference frame). I_16x16 MBs in P-frames are used only at the composite mux level for introducing new sprites — their I_16x16 data comes from the sprite's original IDR frame, not from OpenH264's P-frame encoding. + +**OpenH264 for both encode and decode:** +Using the same library for encoding (in sprite_extract) and decoding (in test verification) guarantees consistent behavior. Previously used x264+FFmpeg but hit QP mismatches and format inconsistencies. + +**Automatic profile/level selection:** +`write_headers` computes the H.264 level from the frame's MB count and selects Baseline Profile (up to Level 5.2 / 36,864 MBs) or High Profile (Level 6.0 / 139,264 MBs) automatically. High Profile uses CAVLC (not CABAC) and no 8x8 transforms — the bitstream content is identical to Baseline, just the SPS signals High Profile to unlock higher level limits. Required for 4K+ grids. + +## Vendored OpenH264 Patches + +> **Note:** These patches exist in Telegram's OpenH264 at `third-party/openh264/`. Subcodec does not carry its own OpenH264 copy — it uses the sibling directory via the `third_party/openh264_codec` symlink. + +The OpenH264 used by subcodec has patches gated on `SEncParamExt::bSubcodecMode` (default false). When false, all encoder behavior is stock OpenH264. When true, sprite-compositing constraints activate. Set via `eparam.bSubcodecMode = true` before `InitializeExt()`. The flag is copied in `SWelsSvcCodingParam::ParamTranscode` (`encoder/core/inc/param_svc.h`). + +**Conditional patches (bSubcodecMode=true):** + +1. **No intra in P-frames** (`encoder/core/src/svc_base_layer_md.cpp`): `WelsMdFirstIntraMode()` returns false, preventing I_4x4/I_16x16 selection in P-slices. + +2. **No I_4x4 in intra frames** (`encoder/core/src/svc_base_layer_md.cpp`): `WelsMdIntraFinePartition()` and `WelsMdIntraFinePartitionVaa()` skip I_4x4 evaluation — only I_16x16 allowed. + +3. **No sub-partition modes** (`encoder/core/src/svc_base_layer_md.cpp`, `encoder/core/src/svc_mode_decision.cpp`): `WelsMdInterFinePartition()`, `WelsMdInterFinePartitionVaa()`, `WelsMdInterFinePartitionVaaOnScreen()` skip P_8x8/P_16x8/P_8x16 evaluation — only P_16x16/SKIP allowed. + +4. **MV range cap** (`encoder/core/src/encoder_ext.cpp`): `GetMvMvdRange()` clamps `iMvRange` to 16 pixels max, matching the 1-MB padding border size. + +5. **Padding reconstruction override** (`encoder/core/src/svc_encode_slice.cpp`): After encoding each MB, if it falls within the 1-MB border ring, reconstruction buffer is overwritten to exact black (Y=0, Cb=128, Cr=128). Hardcoded 1-MB padding. + +6. **Log2MaxFrameNum = 4** (`encoder/core/src/au_set.cpp`): `WelsInitSps()` sets `uiLog2MaxFrameNum` to 4 (vs 15 stock) to match subcodec's H.264 parser. The bool is passed as a parameter through call sites in `paraset_strategy.cpp`. + +**Unconditional patches (always active):** + +7. **Level 6.0/6.1/6.2 support** (`api/wels/codec_app_def.h`, `common/inc/wels_common_defs.h`, `common/src/common_tables.cpp`, `decoder/core/src/au_parser.cpp`): Added H.264 Level 6.x entries (up to 139,264 MBs) to the level enum, limit table, level map, and decoder lookup. Stock OpenH264 maxes at Level 5.2 (36,864 MBs). + +## Data Flow Details + +### MacroblockData (types.h) + +Stores all data needed to encode a macroblock: +- `mb_type` — MbType::SKIP, P_16x16, I_16x16 +- `mv_x, mv_y` — Motion vector (half-pel, for P_16x16) +- `intra_pred_mode, intra_chroma_mode` — Prediction modes (for I_16x16) +- `luma_dc[16]` — I_16x16 DC coefficients +- `luma_ac[16][15]` — AC coefficients per 4x4 block +- `cb_dc[4], cr_dc[4], cb_ac[4][15], cr_ac[4][15]` — Chroma coefficients +- `cbp_luma, cbp_chroma` — Coded block pattern + +### MbsEncodedFrame (types.h) + +Owned frame data returned by `mbs::encode_frame_merged()`: +- `data` — `vector` raw frame data (row metadata + merged blobs) +- `rows` — `vector` pre-merged row descriptors (color+alpha combined) + +### MbsFrame (types.h) + +View into bulk-owned frame data (used by MbsSprite after load or set_frames): +- `merged_rows` — `span` pre-merged color+alpha rows (slot_w-relative skips). Used by `build_micro_ops` for the P-frame mux path. + +### MbsRow (types.h) + +Per-row blob descriptor (6-byte on-disk header in MBS v6): +- `leading_skips` — Content SKIPs before first non-skip MB in row +- `trailing_skips` — Content SKIPs after last non-skip MB in row +- `blob_bit_count` — Packed uint16_t: [14:0] = total bits in row blob, [15] = `has_long_zero_run` flag +- `leading_zero_bits` — Zero bits at blob start (capped at 255) +- `trailing_zero_bits` — Zero bits at blob end (capped at 255) +- `blob_data` — Pointer into frame data buffer +- `bit_count()` — Accessor returning lower 15 bits of `blob_bit_count` +- `has_long_zero_run()` — Accessor returning top bit (true if any run of ≥16 consecutive zero bits) + +### MbsSprite (types.h) + +Binary MBS format sprite: per-frame `MbsFrame` views for streaming mux. Move-only. Every sprite has both color and alpha data (pre-merged in v6 format). Loaded from `.mbs` via `MbsSprite::load()` (single bulk read + view parse). Built from encoded frames via `set_frames()` which consolidates into bulk storage. Internally owns: `bulk_data_` (pre-merged blob bytes), `all_rows_` (merged MbsRow descriptors). All `MbsFrame` spans point into these. + +Public fields: `width_mbs`, `height_mbs` (padded dimensions in MBs), `num_frames`, `qp`, `qp_delta_idr`, `qp_delta_p`, `frames` (vector of `MbsFrame` views). Padding is always 1 MB — not stored in the format or struct. + +### FrameParams (types.h) + +Frame dimensions and SPS parameters for the H.264 writers: `width_mbs`, `height_mbs`, `qp`, `log2_max_frame_num`, `pic_order_cnt_type`. + +## Mux Performance + +The mux path was heavily optimized. Key results at 1764 sprites (421x211 MB grid, double-wide with alpha): + +| Metric | Value | +|---|---| +| Sprite add (1764) | ~35 ms (0.02 ms/sprite; v6 pre-merged blobs, no runtime merge) | +| Per-frame p50 | 0.18 ms | +| Per-frame avg | 0.18 ms | + +The hot path in `advance_frame` → `write_p_frame_micro` is: +1. `build_micro_ops`: pre-resolve all blob pointers from `row_ops` + slot state into a flat `MicroOp` array (~33% of advance_frame time). Uses pre-merged rows (color+alpha combined at encode time in v6 format). +2. `write_p_frame_micro`: tight loop over MicroOps — `write_ue(skip)` + `copy_blob(merged_blob)` per op, with EbspWriter's inline EBSP escaping (~66% of advance_frame time). With LTO, `copy_blob` is inlined. + +Key optimizations applied: +- **Pre-resolved MicroOps** (`build_micro_ops` walks `RowOp` plans once per frame and resolves all pointer chains `slot → sprite → frame → merged_rows → MbsRow` into a flat `MicroOp` array. The write loop iterates this array with zero pointer chasing, zero branching on inactive slots, and zero overlap conditionals) +- **Pre-merged color/alpha row blobs** (at encode time, `encode_frame_merged` merges each sprite row's color and alpha blobs into a single blob: `[color_data][ue(inter_skip)][alpha_data]`. Stored pre-merged in MBS v6 format — no runtime merge at load or mux time. Halves the number of `copy_blob` calls per frame. Merged row metadata uses slot-relative leading/trailing skips) +- **Single-pass EbspWriter** (`EbspWriter` writes directly to the output buffer with inline EBSP escape checking — no intermediate RBSP buffer, no separate `rbsp_to_ebsp` scan pass) +- **Zero-run metadata fast path** (each row blob is scanned at encode time for runs of ≥16 consecutive zero bits. If none exist, `copy_blob` skips EBSP escape checking for interior bytes — only 3 boundary bytes go through `flush_byte`, then the interior is bulk-copied via memcpy (aligned) or NEON shift+write (non-aligned). This is safe because the absence of 16-bit zero runs is alignment-invariant: bit shifting doesn't create or destroy bits, so no alignment can produce two consecutive `0x00` bytes) +- **NEON-accelerated non-aligned copy** (ARM NEON `vshlq_u8` processes 16 bytes per iteration for bit-shifted blob copies: two loads, two shifts, one OR, one store. Replaces scalar byte-at-a-time shift+write. Only used for escape-free blobs on the non-aligned path) +- **Exp-golomb LUT** (pre-computed bit patterns for values 0-4095 — skip run writes are a single `write_bits` call instead of per-bit `bs_write_ue`) +- **SWAR zero-byte detection** in `copy_blob` fallback path for blobs with long zero runs (8-byte chunks checked for zero bytes via `(v - 0x0101...) & ~v & 0x8080...`; safe regions memcpy'd directly) +- **Bulk sprite loading** (`MbsSprite::load` reads entire file payload in one `fread`, parses view types into it — 3 heap allocs per sprite instead of ~3,200) +- **Pre-built row blobs** eliminate CAVLC parsing at load time and enable row-level bulk copy at mux time +- **Real-nC MBS encoding** (eliminates CAVLC re-encoding at mux time — row blob copy instead) +- **Per-frame allocation reuse** (output buffer in MuxSurface, not per-frame heap allocs) +- **Zero-free buffer allocation** (`buf_` uses `make_unique_for_overwrite` — no zeroing) +- **All-I_16x16 IDR** (~1-2 bytes/MB vs ~385 bytes/MB with I_PCM) — negligible IDR overhead even for 4K+ grids +- **Precomputed row plans** (grid layout is fixed between `add_sprite`/`remove_sprite`. `build_row_plans` precomputes a flat `RowOp` array with skip counts and overlap offsets. The mux loop iterates this plan with zero divisions, bounds checks, or slot_idx computation) +- **Blob prefetch** (`__builtin_prefetch` on the next sprite's `blob_data` pointer hides L2/L3 cache miss latency. Both within-row and cross-row prefetch in both `build_micro_ops` and `write_p_frame_micro`) +- **Lazy row plan rebuild** (row plans are only rebuilt on the first `advance_frame` after `add_sprite`/`remove_sprite`, not on every add/remove. Eliminates O(N²) plan rebuild cost when adding N sprites sequentially) + +**Build note:** For best performance, build with `-DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON`. LTO enables cross-TU inlining of `copy_blob` into `write_p_frame_micro`, which is critical for performance (~40% improvement over non-LTO Release). + +**Note:** `RbspWriter`, `rbsp_to_ebsp_neon`, and `write_p_frame_rbsp` (two-pass RBSP staging path) remain in the codebase for reference and testing. The active P-frame path is `write_p_frame_micro`. `bs_copy_bits` and `rbsp_to_ebsp` are used by the IDR path (`write_idr_black`), the encode-time merge in `encode_frame_merged`, and test reference comparisons. The resize I_PCM path (`write_idr_ipcm`) uses single-pass EbspWriter directly — no RBSP buffer or `rbsp_to_ebsp` needed. + +## ObjC++ Wrapper (SubcodecObjC) + +Test-quality ObjC++ bridge exposing the C++ API to Swift. `SC` prefix. One protocol and six classes: + +- `SCSprite` — Two modes: **extractor** (`SCSprite.extractor(withSpriteSize:qp:outputPath:)` — raw YUV+alpha → .mbs file via SpriteExtractor) and **encoder** (`SCSprite.encoder(withWidth:height:qp:)` — raw content-size YUV+alpha → NAL data in memory via SpriteEncoder, for reference decode). Note: `finalizeExtraction()` not `finalize()` (avoids NSObject collision). +- `SCMuxSurface` — Wraps MuxSurface. `SCMuxSurface.create(withSpriteWidth:spriteHeight:maxSlots:qp:sink:)` takes content pixels (no padding param). `addSpriteAtPath:error:` returns `SCSpriteRegion *` (slot, colorRect, alphaRect). `advanceSpriteAtSlot:` schedules one sprite for emit+advance. `emitFrameIfNeededWithSink:error:` emits a P-frame if any sprites are scheduled. `advanceFrameWithSink:error:` convenience that schedules all sprites then emits. `resizeToMaxSlots:yPlane:cbPlane:crPlane:...` returns `SCResizeResult *` (array of `SCSpriteRegion`). `checkCompactionOpportunity` returns `SCCompactionInfo *`. +- `SCResizeResult` — Result of `resizeToMaxSlots:`: `regions` array of `SCSpriteRegion` with compacted slot assignments. +- `SCCompactionInfo` — Result of `checkCompactionOpportunity`: `activeSprites`, `maxSlots`, `currentGridMbs`, `minGridMbs`. +- `SCSpriteRegion` — Result of `addSpriteAtPath:error:`: `slot` index, `colorRect` and `alphaRect` as content pixel regions in composite frame. +- `SCDecoding` — Protocol defining shared decoder interface: `createDecoderWithError:` and `decodeStream:error:`. +- `SCOpenH264Decoder` — OpenH264 software decoder. Factory: `SCOpenH264Decoder.createDecoder()`. Conforms to `SCDecoding`. +- `SCVideoToolboxDecoder` — VideoToolbox hardware decoder. Factory: `SCVideoToolboxDecoder.createDecoder()`. Conforms to `SCDecoding`. Converts Annex B → AVCC internally. Outputs NV12, deinterleaves to separate Y/Cb/Cr planes for `SCDecodedFrame`. +- `SCDecodedFrame` — YUV plane data container (width, height, y, cb, cr as NSData). + +## Production Readiness + +### What's production-ready +The core C++ library (`libsubcodec`) — .mbs format, real-nC encoding, MuxSurface compositing with merged-blob micro-op path — is proven correct with pixel-identical verification across 160 frames in both C++ and Swift tests. Stress-tested at 1764 sprites with ~0.18ms/frame p50 mux time (LTO Release). Verified with ffmpeg decode of real sprite data. + +### What's needed for a production Apple app + +1. **VideoToolbox integration (partially done)** — `SCVideoToolboxDecoder` provides bulk synchronous decode via `VTDecompressionSession`. For production, still needs: + - Streaming frame-at-a-time decode timed to display cadence (CADisplayLink) + - Display pipeline (CVPixelBuffer → Metal texture or CALayer) + - The current wrapper copies YUV planes per frame; production should pass CVPixelBuffers directly to the display layer + +2. **Streaming frame API** — Current ObjC++ wrapper writes into pre-allocated buffers. Production needs frame-at-a-time output timed to display cadence (CADisplayLink or similar). + +3. **Encoding can be offline** — SpriteExtractor/.mbs generation can happen at build time or on a server. The app only needs MuxSurface (compositing) + VideoToolbox (decode). OpenH264 encoder need not ship in the app binary. + +4. **Thread safety, error recovery, memory pressure** — Not addressed in current wrapper. + +## Known Issues + +- **OpenH264 OOM at large resolutions:** OpenH264's decoder runs out of memory for frame sizes around 3K+ pixels per dimension. This only affects test verification — production uses VideoToolbox for decode. + +- **I_PCM + I_16x16 mixing in IDR (OpenH264 bug):** OpenH264 returns `dsBitstreamError` when an IDR slice contains a mix of I_PCM and I_16x16 MBs due to `InitReadBits(pBs, 0)` corrupting bit-reader state after I_PCM parsing. The composite IDR uses all-I_16x16 (no mixing). The resize transition IDR uses all-I_PCM (no mixing). Both avoid the bug. + +- **VideoToolbox rejects long skip_runs in partially-filled grids (worked around):** VT's H.264 decoder fails with `kVTVideoDecoderBadDataErr` (-8969) on structurally valid P-frames when large mb_skip_run values (from empty grid rows) interact with certain CAVLC blob patterns. The bitstream passes H264Parser and ffmpeg validation. Worked around by `write_skip_safe`: skip_runs exceeding `MAX_SKIP_RUN` (2048) are split by inserting dummy P_16x16 zero-residual MBs (4 bits each, semantically identical to SKIP). Tested by `testVideoToolboxPartialFill`. + +## Profiling (advance_frame hot path) + +### Tool: `bench_profile` + +`tools/bench_profile.cpp` — parameterized profiling binary for `MuxSurface::advance_frame` and `MuxSurface::resize`. Uses `os_signpost` instrumentation, designed for `xctrace` CLI (non-interactive). + +```bash +# Generate demo .mbs (one-time, requires FFmpeg) +cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build +./build/sprite_extract demo/input.mp4 demo/output0.mbs 64 + +# Build (MUST use Release for meaningful profiles) +cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --target bench_profile + +# Profile advance_frame with xctrace +xctrace record --template 'Time Profiler' --output profile.trace \ + --launch -- ./build/bench_profile --mbs-path demo/output0.mbs \ + --sprite-count 1764 --frame-count 160 --loops 100 + +# Profile resize (420 active sprites, resize to 882 slots, 50 iterations) +xctrace record --template 'Time Profiler' --output resize.trace \ + --launch -- ./build/bench_profile --mbs-path demo/output0.mbs \ + --profile-resize --resize-from 420 --resize-to 882 --resize-loops 50 + +# Export trace to XML for automated analysis +xctrace export --input profile.trace \ + --xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]' > profile.xml +``` + +CLI args: `--mbs-path ` or `--input