Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2026-04-07 19:21:40 +02:00
commit 9d2094fe23
145 changed files with 34121 additions and 2954 deletions

View file

@ -31,7 +31,7 @@ class UITests: XCTestCase {
}
func testSignUp() throws {
deleteTestAccount(phone: "9996629999")
deleteTestAccount(phone: "9996625296")
app.launch()
// Welcome screen tap Start Messaging

View file

@ -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",

View file

@ -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))"
})

View file

@ -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])

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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 {

View file

@ -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<EncryptionProvider> provider, NSMutableData *resultData, NSMutableArray<NSNumber *> *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<NSMutableData *> *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<NSNumber *> *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<EncryptionProvider> 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<EncryptionProvider> 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<EncryptionProvider> 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) {

View file

@ -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",

View file

@ -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

View file

@ -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))
}

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",
],
)

View file

@ -1,5 +1,6 @@
import Foundation
import UIKit
import AnimationCache
import ImageDCT
import Accelerate

View file

@ -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",
],
)

View file

@ -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",

View file

@ -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<Bool>(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 {

View file

@ -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",

View file

@ -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 {

View file

@ -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 =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.MultiAnimationRenderer</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>MultiAnimationRenderer</string>
"""
)
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",
],

View file

@ -1,46 +0,0 @@
#include <metal_stdlib>
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<float, access::sample> textureY[[texture(0)]],
texture2d<float, access::sample> textureU[[texture(1)]],
texture2d<float, access::sample> textureV[[texture(2)]],
texture2d<float, access::sample> 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);
}

View file

@ -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",

View file

@ -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
}

View file

@ -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

View file

@ -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",
],
)

View file

@ -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<UInt8>,
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<UInt8>.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<Impl>
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<AnimationCacheItemResult, NoError> {
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
}
}

View file

@ -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",
],
)

View file

@ -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<Weak<MultiAnimationRenderTarget>>()
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)
}
}
}
}

View file

@ -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
})
}

View file

@ -445,6 +445,22 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
}
if preload {
var channelMessage: Signal<Void, NoError> = .single(Void())
if case .channelPost = subject, let channelMessageId = replyThreadMessage.channelMessageId {
channelMessage = context.engine.messages.getMessagesLoadIfNecessary([channelMessageId], strategy: .cloud(skipLocal: false))
|> mapToSignal { result -> Signal<Void, GetMessagesError> in
switch result {
case .progress:
return .never()
case .result:
return .single(Void())
}
}
|> `catch` { _ -> Signal<Void, NoError> 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

View file

@ -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 {

View file

@ -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"],
)

View file

@ -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;
/**

View file

@ -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)

View file

@ -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 */

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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 ;

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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)

View file

@ -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;

11
third-party/subcodec/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
build/
build-*/
build_*/
.build/
.swiftpm/
*.h264
*.yuv
*.mbs
*.trace/
Tests/SubcodecTests/Fixtures/
.DS_Store

84
third-party/subcodec/BUILD vendored Normal file
View file

@ -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"],
)

383
third-party/subcodec/CLAUDE.md vendored Normal file
View file

@ -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<uint8_t>` raw frame data (row metadata + merged blobs)
- `rows``vector<MbsRow>` 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<MbsRow>` 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 <file>` or `--input <video> --sprite-size <N>`, `--sprite-count <N>` (default 1764), `--frame-count <N>` (default 160), `--loops <N>` (default 50, total frames = frame-count × loops), `--qp <N>` (default 26), `--profile-resize` (resize mode instead of advance_frame), `--resize-from <N>` (active sprites before resize, default 420), `--resize-to <N>` (target max_slots, default 882), `--resize-loops <N>` (iterations, default 10). The `--input` mode extracts .mbs from video via FFmpeg+SpriteExtractor (one-time setup, not profiled).
Sprites are pre-loaded into memory and added via `add_sprite(MbsSprite)` by move, so file I/O doesn't contaminate the profile. In advance_frame mode, only `advance_frame` is in the profiled section. In resize mode, each iteration creates a fresh MuxSurface with `resize-from` sprites, advances one frame, then times the resize to `resize-to` slots. Reports p50/avg/min/max across all iterations.
### Parsing xctrace XML export
The `time-profile` XML contains `<row>` elements with `<weight>`, `<tagged-backtrace>`, and `<sample-time>` children. Elements use `id`/`ref` deduplication — most rows reference the first occurrence via `ref="N"`. **Critical:** count samples (rows), don't sum weights. Weight refs often all point to the same 1ms value, making weight-based aggregation unreliable. Sample count gives the correct profile.
Parse pattern:
1. Build `id_map` (all elements with `id` attr → element)
2. For each `<row>`: resolve `<tagged-backtrace>` (may be ref), find `<backtrace>`, get `<frame>` list
3. `frames[0]` = leaf (self time). `frames[0].get('name')` = function name
4. Count samples per leaf function name
### Baseline profile (1764 sprites × 16K frames, demo/output0.mbs, LTO Release)
| Function | % CPU | Role |
|---|---|---|
| `write_p_frame_micro` (incl. inlined copy_blob) | 65% | Tight micro-op loop: write_ue + copy_blob per merged blob |
| `build_micro_ops` | 33% | Pre-resolve blob pointers from row_ops + slot state |
| `MuxSurface::advance_frame` | 1% | Frame dispatch overhead |
| Other (memmove, memset) | 1% | |
Hot files: `src/mbs_mux_common.cpp` (write_p_frame_micro, build_micro_ops, EbspWriter::copy_blob), `src/mux_surface.cpp` (advance_frame dispatch).
Of the advance_frame-only time (~0.18ms p50), `write_p_frame_micro` is ~66% and `build_micro_ops` is ~33%. The `build_micro_ops` cost is dominated by pointer chasing through `slot → sprite → frames[idx] → merged_rows[row]` for ~8,862 ops per frame.
### Profile-optimize loop
1. **Profile:** `xctrace record` → export XML → parse sample counts per function
2. **Analyze:** identify top self-time function, read its source
3. **Optimize:** make ONE targeted change
4. **Verify correctness:** `cd build && ctest` (all 25 tests must pass)
5. **Re-profile:** same xctrace command, compare sample distribution
6. **Commit or revert** based on results
### Important notes
- `test_mux_perf` is the existing wall-clock benchmark (1764 sprites, 160 frames, reports p50/p95/p99). Run it before and after optimizations for wall-clock comparison.
- `test_ebsp_writer` specifically tests copy_blob correctness at all bit alignments — run after any copy_blob changes.
- The demo .mbs file (`demo/output0.mbs`) must be generated first: `./build/sprite_extract demo/input.mp4 demo/output0.mbs 64`. It's a 64×64 sprite, 6×6 MBs padded, 160 frames.
## Conventions
- PascalCase for classes/structs (`MacroblockData`, `MbsSprite`, `MuxSurface`); snake_case for functions and variables; `UPPER_CASE` for enums and macros
- Namespaces: `subcodec`, `subcodec::cavlc`, `subcodec::frame_writer`, `subcodec::mbs`, `subcodec::mux`, `subcodec::tables`
- 4-space indentation
- `std::expected<T, Error>` for fallible operations; `std::span<uint8_t>` for output buffers
- RAII and `std::vector` for dynamic allocations; `std::unique_ptr` for pimpl
- C++23 for all library and test code; OpenH264 (Telegram's copy) compiled as C++11
- h264bitstream (vendored) remains C; `const_cast` used at `bs_t` interop boundaries

181
third-party/subcodec/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,181 @@
cmake_minimum_required(VERSION 3.16)
project(subcodec C CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# OpenH264 root — sibling directory in telegram-ios/third-party/
set(OH264_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../openh264/third_party/openh264/src/codec"
CACHE PATH "Path to OpenH264 codec source directory")
if(NOT EXISTS "${OH264_ROOT}/api/wels/codec_api.h")
message(FATAL_ERROR "OpenH264 not found at ${OH264_ROOT}. "
"Expected sibling directory: ../openh264/third_party/openh264/src/codec/")
endif()
# Find FFmpeg via pkg-config (required)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libavformat
libavutil
libswscale
)
# --- OpenH264 (from sibling directory) ---
# Common sources (shared by encoder and decoder)
file(GLOB OH264_COMMON_SOURCES ${OH264_ROOT}/common/src/*.cpp)
list(FILTER OH264_COMMON_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx)")
add_library(oh264_common STATIC ${OH264_COMMON_SOURCES})
set_target_properties(oh264_common PROPERTIES CXX_STANDARD 11)
target_compile_options(oh264_common PRIVATE -w)
target_include_directories(oh264_common PUBLIC
${OH264_ROOT}/api/wels
${OH264_ROOT}/common/inc
)
# Processing sources
file(GLOB_RECURSE OH264_PROCESSING_SOURCES ${OH264_ROOT}/processing/src/*.cpp)
list(FILTER OH264_PROCESSING_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx|loongarch)")
add_library(oh264_processing STATIC ${OH264_PROCESSING_SOURCES})
set_target_properties(oh264_processing PROPERTIES CXX_STANDARD 11)
target_compile_options(oh264_processing PRIVATE -w)
target_link_libraries(oh264_processing PRIVATE oh264_common)
target_include_directories(oh264_processing PRIVATE
${OH264_ROOT}/processing/interface
${OH264_ROOT}/processing/src/common
)
# Encoder sources
file(GLOB OH264_ENCODER_SOURCES ${OH264_ROOT}/encoder/core/src/*.cpp)
list(FILTER OH264_ENCODER_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx)")
list(APPEND OH264_ENCODER_SOURCES ${OH264_ROOT}/encoder/plus/src/welsEncoderExt.cpp)
add_library(oh264_encoder STATIC ${OH264_ENCODER_SOURCES})
set_target_properties(oh264_encoder PROPERTIES CXX_STANDARD 11)
target_compile_options(oh264_encoder PRIVATE -w)
target_link_libraries(oh264_encoder PRIVATE oh264_common oh264_processing)
target_include_directories(oh264_encoder PRIVATE
${OH264_ROOT}/encoder/core/inc
${OH264_ROOT}/encoder/plus/inc
${OH264_ROOT}/processing/interface
)
# Decoder sources
file(GLOB OH264_DECODER_SOURCES
${OH264_ROOT}/decoder/core/src/*.cpp
${OH264_ROOT}/decoder/plus/src/*.cpp
)
list(FILTER OH264_DECODER_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx|DllEntry)")
add_library(oh264_decoder STATIC ${OH264_DECODER_SOURCES})
set_target_properties(oh264_decoder PROPERTIES CXX_STANDARD 11)
target_compile_options(oh264_decoder PRIVATE -w)
target_link_libraries(oh264_decoder PRIVATE oh264_common)
target_include_directories(oh264_decoder PRIVATE
${OH264_ROOT}/decoder/core/inc
${OH264_ROOT}/decoder/plus/inc
)
# Umbrella interface library
add_library(openh264 INTERFACE)
target_link_libraries(openh264 INTERFACE oh264_encoder oh264_decoder oh264_common oh264_processing)
target_include_directories(openh264 INTERFACE ${OH264_ROOT}/api/wels)
# --- Vendored h264bitstream ---
add_library(h264bitstream STATIC
third_party/h264bitstream/h264_stream.c
third_party/h264bitstream/h264_nal.c
third_party/h264bitstream/h264_sei.c
)
target_include_directories(h264bitstream PUBLIC third_party/h264bitstream)
# --- Core subcodec library ---
add_library(subcodec STATIC
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
)
target_include_directories(subcodec PUBLIC src)
target_link_libraries(subcodec PUBLIC h264bitstream)
# --- Sprite encoder library (OpenH264 wrapper) ---
add_library(sprite_encode STATIC src/sprite_encode.cpp src/sprite_extractor.cpp)
target_compile_options(sprite_encode PRIVATE -w)
target_link_libraries(sprite_encode PUBLIC subcodec openh264)
target_include_directories(sprite_encode PUBLIC src)
# --- Tools (require FFmpeg) ---
add_executable(sprite_extract tools/sprite_extract.cpp)
target_link_libraries(sprite_extract sprite_encode PkgConfig::LIBAV)
add_executable(sprite_mux tools/sprite_mux.cpp)
target_link_libraries(sprite_mux subcodec)
target_include_directories(sprite_mux PRIVATE src)
add_executable(bench_profile tools/bench_profile.cpp)
target_link_libraries(bench_profile sprite_encode PkgConfig::LIBAV)
target_compile_options(bench_profile PRIVATE -w)
# --- Fixture generation ---
add_executable(generate_fixtures Tests/SubcodecTests/generate_fixtures.cpp)
set(FIXTURE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Tests/SubcodecTests/Fixtures)
add_custom_target(fixtures
COMMAND ${CMAKE_COMMAND} -E make_directory ${FIXTURE_DIR}
COMMAND $<TARGET_FILE:generate_fixtures> ${FIXTURE_DIR}
DEPENDS generate_fixtures
COMMENT "Generating YUV test fixtures in Tests/SubcodecTests/Fixtures/"
)
# --- Tests ---
# Core tests (link subcodec only)
set(CORE_TESTS
test_cavlc test_cavlc_read test_cavlc_split test_ct_lut
test_mb_p16x16 test_mb_i16x16
test_p_frame_ex test_idr_frame_ex test_h264_parse
test_mbs_format test_mbs_encode
test_bs_copy_bits test_ebsp_writer test_rbsp_writer
test_row_plans test_mux_perf
)
foreach(t ${CORE_TESTS})
add_executable(${t} test/${t}.cpp)
target_link_libraries(${t} subcodec)
target_include_directories(${t} PRIVATE src third_party/h264bitstream)
endforeach()
# E2E tests (link sprite_encode = subcodec + openh264)
set(E2E_TESTS
test_mux test_mux_surface test_mux_alpha
test_cavlc_diag test_sprite_extractor test_sprite_encode_alpha
test_high_profile test_ipcm test_resize
)
foreach(t ${E2E_TESTS})
add_executable(${t} test/${t}.cpp)
target_link_libraries(${t} sprite_encode)
endforeach()
# --- CTest registration ---
enable_testing()
foreach(t ${CORE_TESTS} ${E2E_TESTS})
add_test(NAME ${t} COMMAND ${t})
endforeach()

142
third-party/subcodec/Package.swift vendored Normal file
View file

@ -0,0 +1,142 @@
// swift-tools-version: 5.9
import PackageDescription
let oh264 = "third_party/openh264_codec"
let package = Package(
name: "subcodec",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
],
products: [
.library(name: "SubcodecObjC", targets: ["SubcodecObjC"]),
],
targets: [
.target(
name: "oh264_common",
path: "\(oh264)/common",
exclude: [
"arm", "arm64", "x86", "mips", "loongarch",
],
sources: ["src"],
publicHeadersPath: "inc",
cxxSettings: [
.headerSearchPath("../api/wels"),
.unsafeFlags(["-w", "-std=c++11"]),
]
),
.target(
name: "oh264_processing",
dependencies: ["oh264_common"],
path: "\(oh264)/processing",
exclude: [
"src/arm", "src/arm64", "src/x86", "src/mips", "src/loongarch",
"src/common/WelsVP.rc", "src/common/WelsVP.def",
],
sources: ["src"],
publicHeadersPath: "interface",
cxxSettings: [
.headerSearchPath("src/common"),
.headerSearchPath("../common/inc"),
.headerSearchPath("../api/wels"),
.unsafeFlags(["-w", "-std=c++11"]),
]
),
.target(
name: "oh264_encoder",
dependencies: ["oh264_common", "oh264_processing"],
path: "\(oh264)/encoder",
exclude: [
"core/arm", "core/arm64", "core/x86", "core/mips", "core/loongarch",
"plus/src/DllEntry.cpp", "plus/src/wels_enc_export.def",
],
sources: ["core/src", "plus/src"],
publicHeadersPath: "core/inc",
cxxSettings: [
.headerSearchPath("plus/inc"),
.headerSearchPath("../common/inc"),
.headerSearchPath("../api/wels"),
.headerSearchPath("../processing/interface"),
.unsafeFlags(["-w", "-std=c++11"]),
]
),
.target(
name: "oh264_decoder",
dependencies: ["oh264_common"],
path: "\(oh264)/decoder",
exclude: [
"core/arm", "core/arm64", "core/x86", "core/mips", "core/loongarch",
"plus/src/wels_dec_export.def",
],
sources: ["core/src", "plus/src"],
publicHeadersPath: "core/inc",
cxxSettings: [
.headerSearchPath("plus/inc"),
.headerSearchPath("../common/inc"),
.headerSearchPath("../api/wels"),
.unsafeFlags(["-w", "-std=c++11"]),
]
),
.target(
name: "h264bitstream",
path: "third_party/h264bitstream",
sources: ["h264_stream.c", "h264_nal.c", "h264_sei.c"],
publicHeadersPath: "."
),
.target(
name: "sprite_encode",
dependencies: ["subcodec", "oh264_common", "oh264_processing", "oh264_encoder", "oh264_decoder"],
path: "Sources/SpriteEncode",
cxxSettings: [
.headerSearchPath("../../src"),
.headerSearchPath("../../third_party/h264bitstream"),
.headerSearchPath("../../\(oh264)/api/wels"),
.headerSearchPath("../../\(oh264)/encoder/core/inc"),
.headerSearchPath("../../\(oh264)/encoder/plus/inc"),
.headerSearchPath("../../\(oh264)/decoder/core/inc"),
.headerSearchPath("../../\(oh264)/decoder/plus/inc"),
.headerSearchPath("../../\(oh264)/common/inc"),
.unsafeFlags(["-std=c++23"]),
]
),
.target(
name: "subcodec",
dependencies: ["h264bitstream"],
path: "src",
exclude: ["sprite_encode.cpp", "sprite_extractor.cpp"],
publicHeadersPath: ".",
cxxSettings: [
.headerSearchPath("../third_party/h264bitstream"),
]
),
.target(
name: "SubcodecObjC",
dependencies: ["subcodec", "sprite_encode"],
path: "Sources/SubcodecObjC",
publicHeadersPath: "include",
cxxSettings: [
.headerSearchPath("../../src"),
.headerSearchPath("../../third_party/h264bitstream"),
.headerSearchPath("../../\(oh264)/api/wels"),
.headerSearchPath("../../\(oh264)/common/inc"),
.headerSearchPath("../../\(oh264)/encoder/core/inc"),
.headerSearchPath("../../\(oh264)/decoder/core/inc"),
],
linkerSettings: [
.linkedFramework("VideoToolbox"),
.linkedFramework("CoreMedia"),
.linkedFramework("CoreVideo"),
]
),
.testTarget(
name: "SubcodecTests",
dependencies: ["SubcodecObjC"],
path: "Tests/SubcodecTests",
exclude: ["generate_fixtures.cpp"],
resources: [.copy("Fixtures")]
),
],
cxxLanguageStandard: .cxx2b
)

View file

@ -0,0 +1 @@
// Placeholder header for SwiftPM target — actual headers are in src/

View file

@ -0,0 +1 @@
#include "../../src/sprite_encode.cpp"

View file

@ -0,0 +1 @@
#include "../../src/sprite_extractor.cpp"

View file

@ -0,0 +1,46 @@
// Sources/SubcodecObjC/AnnexBSplitter.h
// Internal shared header — not in public include/ directory
#pragma once
#include <cstdint>
#include <vector>
struct AnnexBFrame {
const uint8_t *data;
size_t size;
};
inline std::vector<AnnexBFrame> split_annex_b_frames(const uint8_t* data, size_t size) {
std::vector<AnnexBFrame> frames;
size_t frame_start = 0;
bool current_has_slice = false;
for (size_t i = 0; i + 3 < size; ) {
int sc_len = 0;
if (i + 3 < size && data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1)
sc_len = 4;
else if (i + 2 < size && data[i] == 0 && data[i+1] == 0 && data[i+2] == 1)
sc_len = 3;
if (sc_len > 0 && i > 0) {
uint8_t nal_type = data[i + sc_len] & 0x1F;
if ((nal_type == 1 || nal_type == 5) && i > frame_start) {
if (current_has_slice) {
frames.push_back({data + frame_start, i - frame_start});
frame_start = i;
current_has_slice = false;
}
current_has_slice = true;
}
}
if (sc_len > 0) i += sc_len + 1;
else i++;
}
if (frame_start < size) {
frames.push_back({data + frame_start, size - frame_start});
}
return frames;
}

View file

@ -0,0 +1,22 @@
// Sources/SubcodecObjC/SCDecodedFrame.mm
#import "SCDecodedFrame.h"
@implementation SCDecodedFrame
- (instancetype)initWithWidth:(int)width
height:(int)height
y:(NSData *)y
cb:(NSData *)cb
cr:(NSData *)cr {
self = [super init];
if (self) {
_width = width;
_height = height;
_y = [y copy];
_cb = [cb copy];
_cr = [cr copy];
}
return self;
}
@end

View file

@ -0,0 +1,227 @@
// Sources/SubcodecObjC/SCMuxSurface.mm
#import "SCMuxSurface.h"
#include "mux_surface.h"
#include <optional>
using namespace subcodec;
static NSError* makeError(NSString* msg) {
return [NSError errorWithDomain:@"SCMuxSurface" code:-1
userInfo:@{NSLocalizedDescriptionKey: msg}];
}
@implementation SCCompactionInfo
- (instancetype)initWithActiveSprites:(int)active
maxSlots:(int)max
currentGridMbs:(int)current
minGridMbs:(int)min {
self = [super init];
if (self) {
_activeSprites = active;
_maxSlots = max;
_currentGridMbs = current;
_minGridMbs = min;
}
return self;
}
@end
@implementation SCResizeResult
- (instancetype)initWithRegions:(NSArray<SCSpriteRegion *> *)regions {
self = [super init];
if (self) {
_regions = [regions copy];
}
return self;
}
@end
@implementation SCMuxSurface {
std::optional<MuxSurface> _surface;
}
+ (nullable SCMuxSurface *)createWithSpriteWidth:(int)width
spriteHeight:(int)height
maxSlots:(int)slots
qp:(int)qp
sink:(void (^)(NSData *))sink
error:(NSError **)error {
MuxSurface::Params params;
params.sprite_width = width;
params.sprite_height = height;
params.max_slots = slots;
params.qp = qp;
params.qp_delta_idr = 0;
params.qp_delta_p = 0;
auto result = MuxSurface::create(params, [sink](std::span<const uint8_t> data) {
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
length:data.size()
freeWhenDone:NO];
sink(nsData);
});
if (!result) {
if (error) *error = makeError(@"MuxSurface::create failed");
return nil;
}
SCMuxSurface* obj = [[SCMuxSurface alloc] init];
obj->_surface.emplace(std::move(*result));
return obj;
}
- (int)widthMbs {
return _surface ? _surface->width_mbs() : 0;
}
- (int)heightMbs {
return _surface ? _surface->height_mbs() : 0;
}
- (nullable SCSpriteRegion *)addSpriteAtPath:(NSString *)path
error:(NSError **)error {
if (!_surface) {
if (error) *error = makeError(@"Surface not initialized");
return nil;
}
auto result = _surface->add_sprite(path.UTF8String);
if (!result) {
if (error) *error = makeError(@"add_sprite failed");
return nil;
}
auto& region = *result;
CGRect colorRect = CGRectMake(region.color.x, region.color.y,
region.color.width, region.color.height);
CGRect alphaRect = CGRectMake(region.alpha.x, region.alpha.y,
region.alpha.width, region.alpha.height);
return [[SCSpriteRegion alloc] initWithSlot:region.slot
colorRect:colorRect
alphaRect:alphaRect];
}
- (void)removeSpriteAtSlot:(int)slot {
if (_surface) {
_surface->remove_sprite(slot);
}
}
- (void)advanceSpriteAtSlot:(int)slot {
if (_surface) {
_surface->advance_sprite(slot);
}
}
- (BOOL)emitFrameIfNeededWithSink:(void (^)(NSData *))sink
error:(NSError **)error {
if (!_surface) {
if (error) *error = makeError(@"Surface not initialized");
return NO;
}
auto result = _surface->emit_frame_if_needed([sink](std::span<const uint8_t> data) {
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
length:data.size()
freeWhenDone:NO];
sink(nsData);
});
if (!result) {
if (error) *error = makeError(@"emit_frame_if_needed failed");
return NO;
}
return YES;
}
- (BOOL)advanceFrameWithSink:(void (^)(NSData *))sink
error:(NSError **)error {
if (!_surface) {
if (error) *error = makeError(@"Surface not initialized");
return NO;
}
auto result = _surface->advance_frame([sink](std::span<const uint8_t> data) {
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
length:data.size()
freeWhenDone:NO];
sink(nsData);
});
if (!result) {
if (error) *error = makeError(@"advance_frame failed");
return NO;
}
return YES;
}
- (nullable SCResizeResult *)resizeToMaxSlots:(int)newMaxSlots
yPlane:(NSData *)yPlane
cbPlane:(NSData *)cbPlane
crPlane:(NSData *)crPlane
decodedWidth:(int)decodedWidth
decodedHeight:(int)decodedHeight
strideY:(int)strideY
strideCb:(int)strideCb
strideCr:(int)strideCr
withSink:(void (^)(NSData *))sink
error:(NSError **)error {
if (!_surface) {
if (error) *error = makeError(@"Surface not initialized");
return nil;
}
auto result = _surface->resize(
newMaxSlots,
{(const uint8_t*)yPlane.bytes, yPlane.length},
{(const uint8_t*)cbPlane.bytes, cbPlane.length},
{(const uint8_t*)crPlane.bytes, crPlane.length},
decodedWidth, decodedHeight,
strideY, strideCb, strideCr,
[sink](std::span<const uint8_t> data) {
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
length:data.size()
freeWhenDone:NO];
sink(nsData);
});
if (!result) {
if (error) *error = makeError(@"resize failed");
return nil;
}
NSMutableArray<SCSpriteRegion *> *regions = [NSMutableArray array];
for (auto& region : result->regions) {
CGRect colorRect = CGRectMake(region.color.x, region.color.y,
region.color.width, region.color.height);
CGRect alphaRect = CGRectMake(region.alpha.x, region.alpha.y,
region.alpha.width, region.alpha.height);
[regions addObject:[[SCSpriteRegion alloc] initWithSlot:region.slot
colorRect:colorRect
alphaRect:alphaRect]];
}
return [[SCResizeResult alloc] initWithRegions:regions];
}
- (SCCompactionInfo *)checkCompactionOpportunity {
if (!_surface) {
return [[SCCompactionInfo alloc] initWithActiveSprites:0
maxSlots:0
currentGridMbs:0
minGridMbs:0];
}
auto info = _surface->check_compaction_opportunity();
return [[SCCompactionInfo alloc] initWithActiveSprites:info.active_sprites
maxSlots:info.max_slots
currentGridMbs:info.current_grid_mbs
minGridMbs:info.min_grid_mbs];
}
@end

View file

@ -0,0 +1,98 @@
// Sources/SubcodecObjC/SCOpenH264Decoder.mm
#import "SCOpenH264Decoder.h"
#include "codec_api.h"
#include "codec_app_def.h"
#include "codec_def.h"
#include "AnnexBSplitter.h"
#include <cstring>
#include <vector>
static NSError* makeError(NSString* msg) {
return [NSError errorWithDomain:@"SCOpenH264Decoder" code:-1
userInfo:@{NSLocalizedDescriptionKey: msg}];
}
@implementation SCOpenH264Decoder {
ISVCDecoder* _decoder;
}
+ (nullable SCOpenH264Decoder *)createDecoderWithError:(NSError **)error {
SCOpenH264Decoder* obj = [[SCOpenH264Decoder alloc] initInternal];
if (!obj) {
if (error) *error = makeError(@"Failed to create decoder");
}
return obj;
}
- (nullable instancetype)initInternal {
self = [super init];
if (!self) return nil;
if (WelsCreateDecoder(&_decoder) != 0 || !_decoder) return nil;
SDecodingParam decParam;
memset(&decParam, 0, sizeof(decParam));
decParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC;
if (_decoder->Initialize(&decParam) != 0) {
WelsDestroyDecoder(_decoder);
_decoder = nullptr;
return nil;
}
return self;
}
- (void)dealloc {
if (_decoder) {
WelsDestroyDecoder(_decoder);
}
}
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
error:(NSError **)error {
auto packets = split_annex_b_frames((const uint8_t*)data.bytes, data.length);
NSMutableArray<SCDecodedFrame *>* frames = [NSMutableArray array];
for (auto& pkt : packets) {
unsigned char* pDst[3] = {nullptr};
SBufferInfo dstInfo;
memset(&dstInfo, 0, sizeof(dstInfo));
_decoder->DecodeFrameNoDelay(const_cast<unsigned char*>(pkt.data),
(int)pkt.size, pDst, &dstInfo);
if (dstInfo.iBufferStatus == 1) {
int w = dstInfo.UsrData.sSystemBuffer.iWidth;
int h = dstInfo.UsrData.sSystemBuffer.iHeight;
int stride_y = dstInfo.UsrData.sSystemBuffer.iStride[0];
int stride_uv = dstInfo.UsrData.sSystemBuffer.iStride[1];
NSMutableData* yData = [NSMutableData dataWithLength:w * h];
NSMutableData* cbData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
NSMutableData* crData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
uint8_t* yDst = (uint8_t*)yData.mutableBytes;
uint8_t* cbDst = (uint8_t*)cbData.mutableBytes;
uint8_t* crDst = (uint8_t*)crData.mutableBytes;
for (int r = 0; r < h; r++)
memcpy(yDst + r * w, pDst[0] + r * stride_y, w);
for (int r = 0; r < h / 2; r++) {
memcpy(cbDst + r * (w / 2), pDst[1] + r * stride_uv, w / 2);
memcpy(crDst + r * (w / 2), pDst[2] + r * stride_uv, w / 2);
}
SCDecodedFrame* frame = [[SCDecodedFrame alloc] initWithWidth:w height:h
y:yData cb:cbData cr:crData];
[frames addObject:frame];
}
}
return frames;
}
@end

View file

@ -0,0 +1,172 @@
// Sources/SubcodecObjC/SCSprite.mm
#import "SCSprite.h"
#include "sprite_extractor.h"
#include "sprite_encode.h"
#include "frame_writer.h"
#include "types.h"
#include <memory>
#include <vector>
#include <optional>
using namespace subcodec;
static NSError* makeError(NSString* msg) {
return [NSError errorWithDomain:@"SCSprite" code:-1
userInfo:@{NSLocalizedDescriptionKey: msg}];
}
@implementation SCSprite {
// Extractor mode
std::optional<SpriteExtractor> _extractor;
// Encoder mode
std::optional<SpriteEncoder> _encoder;
std::vector<std::vector<uint8_t>> _nalFrames;
int _paddedWidth;
int _paddedHeight;
int _qp;
}
+ (nullable SCSprite *)extractorWithSpriteSize:(int)size
qp:(int)qp
outputPath:(NSString *)path
error:(NSError **)error {
SpriteExtractor::Params params;
params.sprite_size = size;
params.qp = qp;
auto result = SpriteExtractor::create(params, path.UTF8String);
if (!result) {
if (error) *error = makeError(@"Failed to create SpriteExtractor");
return nil;
}
SCSprite* sprite = [[SCSprite alloc] init];
sprite->_extractor.emplace(std::move(*result));
return sprite;
}
- (BOOL)addFrameY:(NSData *)y yStride:(int)ys
cb:(NSData *)cb cbStride:(int)cbs
cr:(NSData *)cr crStride:(int)crs
alpha:(NSData *)alpha alphaStride:(int)as
error:(NSError **)error {
if (!_extractor) {
if (error) *error = makeError(@"Not in extractor mode");
return NO;
}
auto result = _extractor->add_frame(
(const uint8_t*)y.bytes, ys,
(const uint8_t*)cb.bytes, cbs,
(const uint8_t*)cr.bytes, crs,
(const uint8_t*)alpha.bytes, as);
if (!result) {
if (error) *error = makeError(@"add_frame failed");
return NO;
}
return YES;
}
- (BOOL)finalizeExtraction:(NSError **)error {
if (!_extractor) {
if (error) *error = makeError(@"Not in extractor mode");
return NO;
}
auto result = _extractor->finalize();
if (!result) {
if (error) *error = makeError(@"finalize failed");
return NO;
}
return YES;
}
// Encoder path
+ (nullable SCSprite *)encoderWithWidth:(int)width
height:(int)height
qp:(int)qp
error:(NSError **)error {
SpriteEncoder::Params params;
params.width = width;
params.height = height;
params.qp = qp;
auto result = SpriteEncoder::create(params);
if (!result) {
if (error) *error = makeError(@"Failed to create SpriteEncoder");
return nil;
}
SCSprite* sprite = [[SCSprite alloc] init];
sprite->_encoder.emplace(std::move(*result));
sprite->_paddedWidth = width + 2 * 16;
sprite->_paddedHeight = height + 2 * 16;
sprite->_qp = qp;
return sprite;
}
- (BOOL)encodeFrameY:(NSData *)y yStride:(int)ys
cb:(NSData *)cb cbStride:(int)cbs
cr:(NSData *)cr crStride:(int)crs
frameIndex:(int)idx
error:(NSError **)error {
if (!_encoder) {
if (error) *error = makeError(@"Not in encoder mode");
return NO;
}
// Create opaque alpha buffer for encoder (no alpha source in ObjC wrapper yet)
std::vector<uint8_t> alpha_buf(_paddedWidth * _paddedHeight, 255);
std::vector<uint8_t> nal;
auto result = _encoder->encode(
(const uint8_t*)y.bytes, ys,
(const uint8_t*)cb.bytes, cbs,
(const uint8_t*)cr.bytes, crs,
alpha_buf.data(), _paddedWidth,
idx, &nal);
if (!result) {
if (error) *error = makeError(@"encode failed");
return NO;
}
_nalFrames.push_back(std::move(nal));
return YES;
}
- (nullable NSData *)buildStreamWithError:(NSError **)error {
if (!_encoder) {
if (error) *error = makeError(@"Not in encoder mode");
return nil;
}
int paddedMbs = _paddedWidth / 16;
FrameParams fp;
fp.width_mbs = paddedMbs * 2; // double-wide canvas (color + alpha)
fp.height_mbs = paddedMbs;
fp.qp = _qp;
fp.log2_max_frame_num = 4;
uint8_t hdr[128];
size_t hdr_size = frame_writer::write_headers({hdr, sizeof(hdr)}, fp);
size_t total = hdr_size;
for (auto& nal : _nalFrames) total += nal.size();
NSMutableData* stream = [NSMutableData dataWithLength:total];
uint8_t* dst = (uint8_t*)stream.mutableBytes;
memcpy(dst, hdr, hdr_size);
size_t off = hdr_size;
for (auto& nal : _nalFrames) {
memcpy(dst + off, nal.data(), nal.size());
off += nal.size();
}
return stream;
}
@end

View file

@ -0,0 +1,18 @@
// Sources/SubcodecObjC/SCSpriteRegion.mm
#import "SCSpriteRegion.h"
@implementation SCSpriteRegion
- (instancetype)initWithSlot:(int)slot
colorRect:(CGRect)colorRect
alphaRect:(CGRect)alphaRect {
self = [super init];
if (self) {
_slot = slot;
_colorRect = colorRect;
_alphaRect = alphaRect;
}
return self;
}
@end

View file

@ -0,0 +1,306 @@
// Sources/SubcodecObjC/SCVideoToolboxDecoder.mm
#import "SCVideoToolboxDecoder.h"
#include "AnnexBSplitter.h"
#import <VideoToolbox/VideoToolbox.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#include <vector>
static NSString* const kErrorDomain = @"SCVideoToolboxDecoder";
static NSError* makeVTError(NSString* msg, OSStatus status) {
return [NSError errorWithDomain:kErrorDomain code:status
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"%@ (OSStatus %d)", msg, (int)status]}];
}
static NSError* makeError(NSString* msg) {
return [NSError errorWithDomain:kErrorDomain code:-1
userInfo:@{NSLocalizedDescriptionKey: msg}];
}
// Returns the LAST occurrence of the target NAL type in the data.
// This is important because buildStream() prepends subcodec's SPS/PPS before
// OpenH264's SPS/PPS, and we need OpenH264's (the last ones) for VideoToolbox.
static const uint8_t* findNAL(const uint8_t* data, size_t size,
uint8_t targetType, size_t* nalSize) {
const uint8_t* found = nullptr;
size_t foundSize = 0;
for (size_t i = 0; i + 3 < size; ) {
int sc_len = 0;
if (i + 3 < size && data[i]==0 && data[i+1]==0 && data[i+2]==0 && data[i+3]==1)
sc_len = 4;
else if (i + 2 < size && data[i]==0 && data[i+1]==0 && data[i+2]==1)
sc_len = 3;
if (sc_len > 0) {
const uint8_t* nalStart = data + i + sc_len;
uint8_t nal_type = nalStart[0] & 0x1F;
size_t nalEnd = size;
for (size_t j = i + sc_len + 1; j + 2 < size; j++) {
if (data[j]==0 && data[j+1]==0 &&
(data[j+2]==1 || (j + 3 < size && data[j+2]==0 && data[j+3]==1))) {
nalEnd = j;
break;
}
}
if (nal_type == targetType) {
found = nalStart;
foundSize = nalEnd - (i + sc_len);
}
i = nalEnd;
} else {
i++;
}
}
if (found) {
*nalSize = foundSize;
}
return found;
}
static std::vector<uint8_t> annexBToAVCC(const uint8_t* data, size_t size) {
std::vector<uint8_t> avcc;
avcc.reserve(size);
for (size_t i = 0; i + 3 < size; ) {
int sc_len = 0;
if (i + 3 < size && data[i]==0 && data[i+1]==0 && data[i+2]==0 && data[i+3]==1)
sc_len = 4;
else if (i + 2 < size && data[i]==0 && data[i+1]==0 && data[i+2]==1)
sc_len = 3;
if (sc_len > 0) {
size_t nalStart = i + sc_len;
size_t nalEnd = size;
for (size_t j = nalStart + 1; j + 2 < size; j++) {
if (data[j]==0 && data[j+1]==0 &&
(data[j+2]==1 || (j + 3 < size && data[j+2]==0 && data[j+3]==1))) {
nalEnd = j;
break;
}
}
uint8_t nal_type = data[nalStart] & 0x1F;
if (nal_type != 7 && nal_type != 8) {
uint32_t nalLen = (uint32_t)(nalEnd - nalStart);
uint8_t lenBuf[4] = {
(uint8_t)(nalLen >> 24), (uint8_t)(nalLen >> 16),
(uint8_t)(nalLen >> 8), (uint8_t)(nalLen)
};
avcc.insert(avcc.end(), lenBuf, lenBuf + 4);
avcc.insert(avcc.end(), data + nalStart, data + nalEnd);
}
i = nalEnd;
} else {
i++;
}
}
return avcc;
}
struct DecodeContext {
NSMutableArray<SCDecodedFrame *>* frames;
};
static void decompressionCallback(void* decompressionOutputRefCon,
void* sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration) {
if (status != noErr || !imageBuffer) return;
DecodeContext* ctx = (DecodeContext*)decompressionOutputRefCon;
CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)imageBuffer;
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
int w = (int)CVPixelBufferGetWidth(pixelBuffer);
int h = (int)CVPixelBufferGetHeight(pixelBuffer);
OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
NSMutableData* yData = [NSMutableData dataWithLength:w * h];
NSMutableData* cbData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
NSMutableData* crData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
uint8_t* yDst = (uint8_t*)yData.mutableBytes;
uint8_t* cbDst = (uint8_t*)cbData.mutableBytes;
uint8_t* crDst = (uint8_t*)crData.mutableBytes;
if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange ||
pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
uint8_t* yPlane = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
uint8_t* uvPlane = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
for (int r = 0; r < h; r++)
memcpy(yDst + r * w, yPlane + r * yStride, w);
int cw = w / 2;
int ch = h / 2;
for (int r = 0; r < ch; r++) {
const uint8_t* uvRow = uvPlane + r * uvStride;
for (int c = 0; c < cw; c++) {
cbDst[r * cw + c] = uvRow[c * 2];
crDst[r * cw + c] = uvRow[c * 2 + 1];
}
}
} else if (pixelFormat == kCVPixelFormatType_420YpCbCr8Planar) {
for (int p = 0; p < 3; p++) {
uint8_t* src = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, p);
size_t stride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, p);
int pw = (p == 0) ? w : w / 2;
int ph = (p == 0) ? h : h / 2;
uint8_t* dst = (p == 0) ? yDst : (p == 1) ? cbDst : crDst;
for (int r = 0; r < ph; r++)
memcpy(dst + r * pw, src + r * stride, pw);
}
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
SCDecodedFrame* frame = [[SCDecodedFrame alloc] initWithWidth:w height:h
y:yData cb:cbData cr:crData];
[ctx->frames addObject:frame];
}
@implementation SCVideoToolboxDecoder
+ (nullable SCVideoToolboxDecoder *)createDecoderWithError:(NSError **)error {
return [[SCVideoToolboxDecoder alloc] init];
}
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
error:(NSError **)error {
const uint8_t* bytes = (const uint8_t*)data.bytes;
size_t length = data.length;
auto packets = split_annex_b_frames(bytes, length);
if (packets.empty()) {
if (error) *error = makeError(@"No frames found in stream");
return nil;
}
size_t spsSize = 0, ppsSize = 0;
const uint8_t* spsNAL = findNAL(packets[0].data, packets[0].size, 7, &spsSize);
const uint8_t* ppsNAL = findNAL(packets[0].data, packets[0].size, 8, &ppsSize);
if (!spsNAL || !ppsNAL) {
if (error) *error = makeError(@"SPS or PPS not found in stream");
return nil;
}
const uint8_t* paramSets[2] = { spsNAL, ppsNAL };
size_t paramSizes[2] = { spsSize, ppsSize };
CMVideoFormatDescriptionRef formatDesc = NULL;
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(
kCFAllocatorDefault, 2, paramSets, paramSizes, 4, &formatDesc);
if (status != noErr) {
if (error) *error = makeVTError(@"CMVideoFormatDescriptionCreateFromH264ParameterSets failed", status);
return nil;
}
DecodeContext ctx;
ctx.frames = [NSMutableArray array];
VTDecompressionOutputCallbackRecord callbackRecord;
callbackRecord.decompressionOutputCallback = decompressionCallback;
callbackRecord.decompressionOutputRefCon = &ctx;
NSDictionary* destAttrs = @{
(NSString*)kCVPixelBufferPixelFormatTypeKey:
@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
};
VTDecompressionSessionRef session = NULL;
status = VTDecompressionSessionCreate(
kCFAllocatorDefault, formatDesc, NULL,
(__bridge CFDictionaryRef)destAttrs,
&callbackRecord, &session);
if (status != noErr) {
CFRelease(formatDesc);
if (error) *error = makeVTError(@"VTDecompressionSessionCreate failed", status);
return nil;
}
NSError* decodeError = nil;
for (auto& pkt : packets) {
auto avcc = annexBToAVCC(pkt.data, pkt.size);
if (avcc.empty()) continue;
CMBlockBufferRef blockBuf = NULL;
status = CMBlockBufferCreateWithMemoryBlock(
kCFAllocatorDefault, NULL, avcc.size(), kCFAllocatorDefault,
NULL, 0, avcc.size(), kCMBlockBufferAssureMemoryNowFlag, &blockBuf);
if (status != noErr) {
decodeError = makeVTError(@"CMBlockBufferCreateWithMemoryBlock failed", status);
break;
}
status = CMBlockBufferReplaceDataBytes(avcc.data(), blockBuf, 0, avcc.size());
if (status != noErr) {
CFRelease(blockBuf);
decodeError = makeVTError(@"CMBlockBufferReplaceDataBytes failed", status);
break;
}
CMSampleBufferRef sampleBuf = NULL;
size_t sampleSize = avcc.size();
status = CMSampleBufferCreate(
kCFAllocatorDefault, blockBuf, true, NULL, NULL,
formatDesc, 1, 0, NULL, 1, &sampleSize, &sampleBuf);
CFRelease(blockBuf);
if (status != noErr) {
decodeError = makeVTError(@"CMSampleBufferCreate failed", status);
break;
}
VTDecodeInfoFlags flagsOut = 0;
status = VTDecompressionSessionDecodeFrame(
session, sampleBuf,
kVTDecodeFrame_1xRealTimePlayback,
NULL, &flagsOut);
CFRelease(sampleBuf);
if (status != noErr) {
decodeError = makeVTError(@"VTDecompressionSessionDecodeFrame failed", status);
break;
}
}
VTDecompressionSessionWaitForAsynchronousFrames(session);
VTDecompressionSessionInvalidate(session);
CFRelease(session);
CFRelease(formatDesc);
if (decodeError) {
if (error) *error = decodeError;
return nil;
}
return ctx.frames;
}
@end

View file

@ -0,0 +1,22 @@
// Sources/SubcodecObjC/include/SCDecodedFrame.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SCDecodedFrame : NSObject
@property (nonatomic, readonly) int width;
@property (nonatomic, readonly) int height;
@property (nonatomic, readonly) NSData *y;
@property (nonatomic, readonly) NSData *cb;
@property (nonatomic, readonly) NSData *cr;
- (instancetype)initWithWidth:(int)width
height:(int)height
y:(NSData *)y
cb:(NSData *)cb
cr:(NSData *)cr;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,16 @@
// Sources/SubcodecObjC/include/SCDecoding.h
#import <Foundation/Foundation.h>
#import "SCDecodedFrame.h"
NS_ASSUME_NONNULL_BEGIN
@protocol SCDecoding <NSObject>
+ (nullable id<SCDecoding>)createDecoderWithError:(NSError **)error;
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
error:(NSError **)error;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,63 @@
// Sources/SubcodecObjC/include/SCMuxSurface.h
#import <Foundation/Foundation.h>
#import "SCSpriteRegion.h"
NS_ASSUME_NONNULL_BEGIN
@interface SCCompactionInfo : NSObject
@property (nonatomic, readonly) int activeSprites;
@property (nonatomic, readonly) int maxSlots;
@property (nonatomic, readonly) int currentGridMbs;
@property (nonatomic, readonly) int minGridMbs;
- (instancetype)initWithActiveSprites:(int)active
maxSlots:(int)max
currentGridMbs:(int)current
minGridMbs:(int)min;
@end
@interface SCResizeResult : NSObject
@property (nonatomic, readonly) NSArray<SCSpriteRegion *> *regions;
- (instancetype)initWithRegions:(NSArray<SCSpriteRegion *> *)regions;
@end
@interface SCMuxSurface : NSObject
@property (nonatomic, readonly) int widthMbs;
@property (nonatomic, readonly) int heightMbs;
+ (nullable SCMuxSurface *)createWithSpriteWidth:(int)width
spriteHeight:(int)height
maxSlots:(int)slots
qp:(int)qp
sink:(void (^)(NSData *))sink
error:(NSError **)error;
- (nullable SCSpriteRegion *)addSpriteAtPath:(NSString *)path
error:(NSError **)error;
- (void)removeSpriteAtSlot:(int)slot;
- (void)advanceSpriteAtSlot:(int)slot;
- (BOOL)emitFrameIfNeededWithSink:(void (^)(NSData *))sink
error:(NSError **)error;
- (BOOL)advanceFrameWithSink:(void (^)(NSData *))sink
error:(NSError **)error;
- (nullable SCResizeResult *)resizeToMaxSlots:(int)newMaxSlots
yPlane:(NSData *)yPlane
cbPlane:(NSData *)cbPlane
crPlane:(NSData *)crPlane
decodedWidth:(int)decodedWidth
decodedHeight:(int)decodedHeight
strideY:(int)strideY
strideCb:(int)strideCb
strideCr:(int)strideCr
withSink:(void (^)(NSData *))sink
error:(NSError **)error;
- (SCCompactionInfo *)checkCompactionOpportunity;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,17 @@
// Sources/SubcodecObjC/include/SCOpenH264Decoder.h
#import <Foundation/Foundation.h>
#import "SCDecoding.h"
#import "SCDecodedFrame.h"
NS_ASSUME_NONNULL_BEGIN
@interface SCOpenH264Decoder : NSObject <SCDecoding>
+ (nullable SCOpenH264Decoder *)createDecoderWithError:(NSError **)error;
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
error:(NSError **)error;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,38 @@
// Sources/SubcodecObjC/include/SCSprite.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SCSprite : NSObject
// SpriteExtractor path: raw YUV → .mbs file on disk
+ (nullable SCSprite *)extractorWithSpriteSize:(int)size
qp:(int)qp
outputPath:(NSString *)path
error:(NSError **)error;
- (BOOL)addFrameY:(NSData *)y yStride:(int)ys
cb:(NSData *)cb cbStride:(int)cbs
cr:(NSData *)cr crStride:(int)crs
alpha:(NSData *)alpha alphaStride:(int)as
error:(NSError **)error;
- (BOOL)finalizeExtraction:(NSError **)error;
// SpriteEncoder path: raw YUV → NAL data in memory (for reference decode)
+ (nullable SCSprite *)encoderWithWidth:(int)width
height:(int)height
qp:(int)qp
error:(NSError **)error;
- (BOOL)encodeFrameY:(NSData *)y yStride:(int)ys
cb:(NSData *)cb cbStride:(int)cbs
cr:(NSData *)cr crStride:(int)crs
frameIndex:(int)idx
error:(NSError **)error;
- (nullable NSData *)buildStreamWithError:(NSError **)error;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,19 @@
// Sources/SubcodecObjC/include/SCSpriteRegion.h
#import <Foundation/Foundation.h>
#import <CoreGraphics/CGGeometry.h>
NS_ASSUME_NONNULL_BEGIN
@interface SCSpriteRegion : NSObject
@property (nonatomic, readonly) int slot;
@property (nonatomic, readonly) CGRect colorRect;
@property (nonatomic, readonly) CGRect alphaRect;
- (instancetype)initWithSlot:(int)slot
colorRect:(CGRect)colorRect
alphaRect:(CGRect)alphaRect;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,17 @@
// Sources/SubcodecObjC/include/SCVideoToolboxDecoder.h
#import <Foundation/Foundation.h>
#import "SCDecoding.h"
#import "SCDecodedFrame.h"
NS_ASSUME_NONNULL_BEGIN
@interface SCVideoToolboxDecoder : NSObject <SCDecoding>
+ (nullable SCVideoToolboxDecoder *)createDecoderWithError:(NSError **)error;
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
error:(NSError **)error;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,8 @@
// Sources/SubcodecObjC/include/SubcodecObjC.h
#import <Foundation/Foundation.h>
#import "SCDecodedFrame.h"
#import "SCSprite.h"
#import "SCDecoding.h"
#import "SCOpenH264Decoder.h"
#import "SCMuxSurface.h"
#import "SCVideoToolboxDecoder.h"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
// Tests/SubcodecTests/generate_fixtures.cpp
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#define SPRITE_PX 64
#define NUM_FRAMES 160
static void generate_sprite_frame(uint8_t* y_plane, uint8_t* cb_plane, uint8_t* cr_plane,
int sprite_id, int frame) {
uint8_t cb_val = (uint8_t)(128 + sprite_id * 20);
uint8_t cr_val = (uint8_t)(128 - sprite_id * 20);
for (int py = 0; py < SPRITE_PX; py++) {
for (int px = 0; px < SPRITE_PX; px++) {
uint8_t y_val;
switch (sprite_id) {
case 0: y_val = (uint8_t)((px + frame * 8) % 256); break;
case 1: y_val = (uint8_t)((py + frame * 8) % 256); break;
case 2: y_val = (uint8_t)((px + py + frame * 8) % 256); break;
default: y_val = 128; break;
}
y_plane[py * SPRITE_PX + px] = y_val;
}
}
for (int cy = 0; cy < SPRITE_PX / 2; cy++) {
for (int cx = 0; cx < SPRITE_PX / 2; cx++) {
cb_plane[cy * (SPRITE_PX / 2) + cx] = cb_val;
cr_plane[cy * (SPRITE_PX / 2) + cx] = cr_val;
}
}
}
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <output_dir>\n", argv[0]);
return 1;
}
const char* dir = argv[1];
const int y_size = SPRITE_PX * SPRITE_PX;
const int c_size = (SPRITE_PX / 2) * (SPRITE_PX / 2);
uint8_t y[y_size], cb[c_size], cr[c_size];
for (int s = 0; s < 3; s++) {
char path[512];
snprintf(path, sizeof(path), "%s/sprite%d.yuv", dir, s);
FILE* f = fopen(path, "wb");
if (!f) { perror(path); return 1; }
for (int frame = 0; frame < NUM_FRAMES; frame++) {
generate_sprite_frame(y, cb, cr, s, frame);
fwrite(y, 1, y_size, f);
fwrite(cb, 1, c_size, f);
fwrite(cr, 1, c_size, f);
}
fclose(f);
printf("Wrote %s (%d frames, %d bytes)\n", path, NUM_FRAMES,
NUM_FRAMES * (y_size + c_size + c_size));
}
return 0;
}

BIN
third-party/subcodec/demo/input.mp4 vendored Normal file

Binary file not shown.

856
third-party/subcodec/src/cavlc.cpp vendored Normal file
View file

@ -0,0 +1,856 @@
#include "cavlc.h"
#include <cassert>
#include <cstring>
namespace subcodec::cavlc {
// coeff_token VLC tables from H.264 spec Table 9-5
// Format: {code, length} for each (TotalCoeff, TrailingOnes) pair
// Indexed by [trailing_ones][total_coeff]
struct vlc_t {
uint16_t code;
uint8_t len;
};
// Table 9-5(a): 0 <= nC < 2
// Indexed by [trailing_ones][total_coeff]
// trailing_ones: 0-3, total_coeff: 0-16
static const vlc_t coeff_token_table_0[4][17] = {
// TrailingOnes = 0
{
{0x01, 1}, {0x05, 6}, {0x07, 8}, {0x07, 9}, {0x07, 10}, {0x07, 11}, {0x0F, 13}, {0x0B, 13}, {0x08, 13},
{0x0F, 14}, {0x0B, 14}, {0x0F, 15}, {0x0B, 15}, {0x0F, 16}, {0x0B, 16}, {0x07, 16}, {0x04, 16},
},
// TrailingOnes = 1
{
{0x00, 0}, {0x01, 2}, {0x04, 6}, {0x06, 8}, {0x06, 9}, {0x06, 10}, {0x06, 11}, {0x0E, 13}, {0x0A, 13},
{0x0E, 14}, {0x0A, 14}, {0x0E, 15}, {0x0A, 15}, {0x01, 15}, {0x0E, 16}, {0x0A, 16}, {0x06, 16},
},
// TrailingOnes = 2
{
{0x00, 0}, {0x00, 0}, {0x01, 3}, {0x05, 7}, {0x05, 8}, {0x05, 9}, {0x05, 10}, {0x05, 11}, {0x0D, 13},
{0x09, 13}, {0x0D, 14}, {0x09, 14}, {0x0D, 15}, {0x09, 15}, {0x0D, 16}, {0x09, 16}, {0x05, 16},
},
// TrailingOnes = 3
{
{0x00, 0}, {0x00, 0}, {0x00, 0}, {0x03, 5}, {0x03, 6}, {0x04, 7}, {0x04, 8}, {0x04, 9}, {0x04, 10},
{0x04, 11}, {0x0C, 13}, {0x0C, 14}, {0x08, 14}, {0x0C, 15}, {0x08, 15}, {0x0C, 16}, {0x08, 16},
},
};
// Table 9-5(b): 2 <= nC < 4
static const vlc_t coeff_token_table_2[4][17] = {
// TrailingOnes = 0
{
{0x03, 2}, {0x0B, 6}, {0x07, 6}, {0x07, 7}, {0x07, 8}, {0x04, 8}, {0x07, 9}, {0x0F, 11}, {0x0B, 11},
{0x0F, 12}, {0x0B, 12}, {0x08, 12}, {0x0F, 13}, {0x0B, 13}, {0x07, 13}, {0x09, 14}, {0x07, 14},
},
// TrailingOnes = 1
{
{0x00, 0}, {0x02, 2}, {0x07, 5}, {0x0A, 6}, {0x06, 6}, {0x06, 7}, {0x06, 8}, {0x06, 9}, {0x0E, 11},
{0x0A, 11}, {0x0E, 12}, {0x0A, 12}, {0x0E, 13}, {0x0A, 13}, {0x0B, 14}, {0x08, 14}, {0x06, 14},
},
// TrailingOnes = 2
{
{0x00, 0}, {0x00, 0}, {0x03, 3}, {0x09, 6}, {0x05, 6}, {0x05, 7}, {0x05, 8}, {0x05, 9}, {0x0D, 11},
{0x09, 11}, {0x0D, 12}, {0x09, 12}, {0x0D, 13}, {0x09, 13}, {0x06, 13}, {0x0A, 14}, {0x05, 14},
},
// TrailingOnes = 3
{
{0x00, 0}, {0x00, 0}, {0x00, 0}, {0x05, 4}, {0x04, 4}, {0x06, 5}, {0x08, 6}, {0x04, 6}, {0x04, 7},
{0x04, 9}, {0x0C, 11}, {0x08, 11}, {0x0C, 12}, {0x0C, 13}, {0x08, 13}, {0x01, 13}, {0x04, 14},
},
};
// Table 9-5(c): 4 <= nC < 8
static const vlc_t coeff_token_table_4[4][17] = {
// TrailingOnes = 0
{
{0x0F, 4}, {0x0F, 6}, {0x0B, 6}, {0x08, 6}, {0x0F, 7}, {0x0B, 7}, {0x09, 7}, {0x08, 7}, {0x0F, 8},
{0x0B, 8}, {0x0F, 9}, {0x0B, 9}, {0x08, 9}, {0x0D, 10}, {0x09, 10}, {0x05, 10}, {0x01, 10},
},
// TrailingOnes = 1
{
{0x00, 0}, {0x0E, 4}, {0x0F, 5}, {0x0C, 5}, {0x0A, 5}, {0x08, 5}, {0x0E, 6}, {0x0A, 6}, {0x0E, 7},
{0x0E, 8}, {0x0A, 8}, {0x0E, 9}, {0x0A, 9}, {0x07, 9}, {0x0C, 10}, {0x08, 10}, {0x04, 10},
},
// TrailingOnes = 2
{
{0x00, 0}, {0x00, 0}, {0x0D, 4}, {0x0E, 5}, {0x0B, 5}, {0x09, 5}, {0x0D, 6}, {0x09, 6}, {0x0D, 7},
{0x0A, 7}, {0x0D, 8}, {0x09, 8}, {0x0D, 9}, {0x09, 9}, {0x0B, 10}, {0x07, 10}, {0x03, 10},
},
// TrailingOnes = 3
{
{0x00, 0}, {0x00, 0}, {0x00, 0}, {0x0C, 4}, {0x0B, 4}, {0x0A, 4}, {0x09, 4}, {0x08, 4}, {0x0D, 5},
{0x0C, 6}, {0x0C, 7}, {0x0C, 8}, {0x08, 8}, {0x0C, 9}, {0x0A, 10}, {0x06, 10}, {0x02, 10},
},
};
// Table 9-5(e): ChromaDCLevel (nC == -1), 4:2:0
// Chroma DC has max 4 coefficients
static const vlc_t coeff_token_chroma_dc[4][5] = {
// TrailingOnes = 0
{
{0x01, 2}, // TotalCoeff = 0
{0x07, 6}, // TotalCoeff = 1
{0x04, 6}, // TotalCoeff = 2
{0x03, 6}, // TotalCoeff = 3
{0x02, 6}, // TotalCoeff = 4
},
// TrailingOnes = 1
{
{0x00, 0}, // TotalCoeff = 0 (invalid)
{0x01, 1}, // TotalCoeff = 1
{0x06, 6}, // TotalCoeff = 2
{0x03, 7}, // TotalCoeff = 3
{0x03, 8}, // TotalCoeff = 4
},
// TrailingOnes = 2
{
{0x00, 0}, // TotalCoeff = 0 (invalid)
{0x00, 0}, // TotalCoeff = 1 (invalid)
{0x01, 3}, // TotalCoeff = 2
{0x02, 7}, // TotalCoeff = 3
{0x02, 8}, // TotalCoeff = 4
},
// TrailingOnes = 3
{
{0x00, 0}, // TotalCoeff = 0 (invalid)
{0x00, 0}, // TotalCoeff = 1 (invalid)
{0x00, 0}, // TotalCoeff = 2 (invalid)
{0x05, 6}, // TotalCoeff = 3
{0x00, 7}, // TotalCoeff = 4
},
};
// Table 9-7: total_zeros for 4x4 blocks (chroma AC or luma)
// Indexed by [total_coeff - 1][total_zeros]
// total_coeff ranges from 1 to 15 (0 coeffs means no total_zeros coded)
// total_zeros ranges from 0 to (16 - total_coeff)
static const vlc_t total_zeros_table[15][16] = {
// total_coeff = 1: total_zeros can be 0-15
{{0x1, 1}, {0x3, 3}, {0x2, 3}, {0x3, 4}, {0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6},
{0x2, 6}, {0x3, 7}, {0x2, 7}, {0x3, 8}, {0x2, 8}, {0x3, 9}, {0x2, 9}, {0x1, 9}},
// total_coeff = 2: total_zeros can be 0-14
{{0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x5, 4}, {0x4, 4}, {0x3, 4},
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6}, {0x2, 6}, {0x1, 6}, {0x0, 6}, {0x0, 0}},
// total_coeff = 3: total_zeros can be 0-13
{{0x5, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 4}, {0x3, 4}, {0x4, 3}, {0x3, 3},
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x1, 6}, {0x1, 5}, {0x0, 6}, {0x0, 0}, {0x0, 0}},
// total_coeff = 4: total_zeros can be 0-12
{{0x3, 5}, {0x7, 3}, {0x5, 4}, {0x4, 4}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 4},
{0x3, 3}, {0x2, 4}, {0x2, 5}, {0x1, 5}, {0x0, 5}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 5: total_zeros can be 0-11
{{0x5, 4}, {0x4, 4}, {0x3, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3},
{0x2, 4}, {0x1, 5}, {0x1, 4}, {0x0, 5}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 6: total_zeros can be 0-10
{{0x1, 6}, {0x1, 5}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x2, 3},
{0x1, 4}, {0x1, 3}, {0x0, 6}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 7: total_zeros can be 0-9
{{0x1, 6}, {0x1, 5}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x3, 2}, {0x2, 3}, {0x1, 4},
{0x1, 3}, {0x0, 6}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 8: total_zeros can be 0-8
{{0x1, 6}, {0x1, 4}, {0x1, 5}, {0x3, 3}, {0x3, 2}, {0x2, 2}, {0x2, 3}, {0x1, 3},
{0x0, 6}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 9: total_zeros can be 0-7
{{0x1, 6}, {0x0, 6}, {0x1, 4}, {0x3, 2}, {0x2, 2}, {0x1, 3}, {0x1, 2}, {0x1, 5},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 10: total_zeros can be 0-6
{{0x1, 5}, {0x0, 5}, {0x1, 3}, {0x3, 2}, {0x2, 2}, {0x1, 2}, {0x1, 4},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 11: total_zeros can be 0-5
{{0x0, 4}, {0x1, 4}, {0x1, 3}, {0x2, 3}, {0x1, 1}, {0x3, 3},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 12: total_zeros can be 0-4
{{0x0, 4}, {0x1, 4}, {0x1, 2}, {0x1, 1}, {0x1, 3},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 13: total_zeros can be 0-3
{{0x0, 3}, {0x1, 3}, {0x1, 1}, {0x1, 2},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 14: total_zeros can be 0-2
{{0x0, 2}, {0x1, 2}, {0x1, 1},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 15: total_zeros can be 0-1
{{0x0, 1}, {0x1, 1},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
};
// Table 9-8: total_zeros for 15-coefficient blocks (I_16x16 AC blocks)
// Indexed by [total_coeff - 1][total_zeros]
// total_coeff ranges from 1 to 14 (0 coeffs means no total_zeros coded)
// total_zeros ranges from 0 to (15 - total_coeff)
static const vlc_t total_zeros_table_15[14][15] = {
// total_coeff = 1: total_zeros can be 0-14
{{0x1, 1}, {0x3, 3}, {0x2, 3}, {0x3, 4}, {0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6},
{0x2, 6}, {0x3, 7}, {0x2, 7}, {0x3, 8}, {0x2, 8}, {0x3, 9}, {0x2, 9}},
// total_coeff = 2: total_zeros can be 0-13
{{0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x5, 4}, {0x4, 4}, {0x3, 4},
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6}, {0x2, 6}, {0x1, 6}, {0x0, 0}},
// total_coeff = 3: total_zeros can be 0-12
{{0x5, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 4}, {0x3, 4}, {0x4, 3}, {0x3, 3},
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x1, 6}, {0x1, 5}, {0x0, 0}, {0x0, 0}},
// total_coeff = 4: total_zeros can be 0-11
{{0x3, 5}, {0x7, 3}, {0x5, 4}, {0x4, 4}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 4},
{0x3, 3}, {0x2, 4}, {0x2, 5}, {0x1, 5}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 5: total_zeros can be 0-10
{{0x5, 4}, {0x4, 4}, {0x3, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3},
{0x2, 4}, {0x1, 5}, {0x1, 4}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 6: total_zeros can be 0-9
{{0x1, 6}, {0x1, 5}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x2, 3},
{0x1, 4}, {0x1, 3}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 7: total_zeros can be 0-8
{{0x1, 6}, {0x1, 5}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x3, 2}, {0x2, 3}, {0x1, 4},
{0x1, 3}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 8: total_zeros can be 0-7
{{0x1, 6}, {0x1, 4}, {0x1, 5}, {0x3, 3}, {0x3, 2}, {0x2, 2}, {0x2, 3}, {0x1, 3},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 9: total_zeros can be 0-6
{{0x1, 6}, {0x0, 6}, {0x1, 4}, {0x3, 2}, {0x2, 2}, {0x1, 3}, {0x1, 2},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 10: total_zeros can be 0-5
{{0x1, 5}, {0x0, 5}, {0x1, 3}, {0x3, 2}, {0x2, 2}, {0x1, 2},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 11: total_zeros can be 0-4
{{0x0, 4}, {0x1, 4}, {0x1, 3}, {0x2, 3}, {0x1, 1},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 12: total_zeros can be 0-3
{{0x0, 4}, {0x1, 4}, {0x1, 2}, {0x1, 1},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 13: total_zeros can be 0-2
{{0x0, 3}, {0x1, 3}, {0x1, 1},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// total_coeff = 14: total_zeros can be 0-1
{{0x0, 2}, {0x1, 2},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
};
// Table 9-9: total_zeros for chroma DC 4:2:0
// Indexed by [total_coeff - 1][total_zeros]
// total_coeff ranges from 1 to 3 (max 4 coeffs, so if TotalCoeff=4, no zeros)
// total_zeros ranges from 0 to (4 - total_coeff)
static const vlc_t total_zeros_chroma_dc[3][4] = {
// total_coeff = 1: total_zeros can be 0-3
{{0x1, 1}, {0x1, 2}, {0x1, 3}, {0x0, 3}},
// total_coeff = 2: total_zeros can be 0-2
{{0x1, 1}, {0x1, 2}, {0x0, 2}, {0x0, 0}},
// total_coeff = 3: total_zeros can be 0-1
{{0x1, 1}, {0x0, 1}, {0x0, 0}, {0x0, 0}},
};
// Table 9-10: run_before
// Indexed by [min(zeros_left - 1, 6)][run_before]
// zeros_left: remaining zeros to distribute
// run_before: zeros before this coefficient (0 to zeros_left)
static const vlc_t run_before_table[7][15] = {
// zeros_left = 1
{{0x1, 1}, {0x0, 1},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// zeros_left = 2
{{0x1, 1}, {0x1, 2}, {0x0, 2},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// zeros_left = 3
{{0x3, 2}, {0x2, 2}, {0x1, 2}, {0x0, 2},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// zeros_left = 4
{{0x3, 2}, {0x2, 2}, {0x1, 2}, {0x1, 3}, {0x0, 3},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// zeros_left = 5
{{0x3, 2}, {0x2, 2}, {0x3, 3}, {0x2, 3}, {0x1, 3}, {0x0, 3},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// zeros_left = 6
{{0x3, 2}, {0x0, 3}, {0x1, 3}, {0x3, 3}, {0x2, 3}, {0x5, 3}, {0x4, 3},
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
// zeros_left >= 7
{{0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x2, 3}, {0x1, 3}, {0x1, 4}, {0x1, 5}, {0x1, 6}, {0x1, 7}, {0x1, 8}, {0x1, 9}, {0x1, 10}, {0x1, 11}},
};
void write_coeff_token(bs_t* b, int total_coeff, int trailing_ones, int nc) {
// Input validation
assert(trailing_ones >= 0 && trailing_ones <= 3);
assert(total_coeff >= 0);
if (nc == -1) {
assert(total_coeff <= 4); // Chroma DC 4:2:0 has max 4 coefficients
} else {
assert(total_coeff <= 16);
}
const vlc_t* entry;
if (nc == -1) {
// Chroma DC (4:2:0)
entry = &coeff_token_chroma_dc[trailing_ones][total_coeff];
} else if (nc < 2) {
entry = &coeff_token_table_0[trailing_ones][total_coeff];
} else if (nc < 4) {
entry = &coeff_token_table_2[trailing_ones][total_coeff];
} else if (nc < 8) {
entry = &coeff_token_table_4[trailing_ones][total_coeff];
} else {
// nC >= 8: Table 9-5(d) - fixed-length 6-bit encoding
// code=3 → tc=0; otherwise code = ((tc-1) << 2) | t1
int code;
if (total_coeff == 0) {
code = 3;
} else {
code = ((total_coeff - 1) << 2) | trailing_ones;
}
bs_write_u(b, 6, code);
return;
}
assert(entry->len > 0); // Invalid coeff_token combination
bs_write_u(b, entry->len, entry->code);
}
void write_level_prefix(bs_t* b, int level_prefix) {
// level_prefix is encoded as level_prefix zeros followed by a 1
for (int i = 0; i < level_prefix; i++) {
bs_write_u1(b, 0);
}
bs_write_u1(b, 1);
}
void write_total_zeros(bs_t* b, int total_zeros, int total_coeff, int max_num_coeff) {
assert(total_coeff > 0);
assert(total_zeros >= 0);
const vlc_t* entry;
if (max_num_coeff == 4) {
// Chroma DC 4:2:0
assert(total_coeff <= 3);
assert(total_zeros <= 4 - total_coeff);
entry = &total_zeros_chroma_dc[total_coeff - 1][total_zeros];
} else if (max_num_coeff == 15) {
// AC blocks for I_16x16 (Table 9-8)
assert(total_coeff <= 14);
assert(total_zeros <= 15 - total_coeff);
entry = &total_zeros_table_15[total_coeff - 1][total_zeros];
} else if (max_num_coeff == 16) {
// 4x4 luma block (full)
assert(total_coeff <= 15);
assert(total_zeros <= 16 - total_coeff);
entry = &total_zeros_table[total_coeff - 1][total_zeros];
} else {
assert(0 && "Unsupported max_num_coeff value");
return;
}
assert(entry->len > 0);
bs_write_u(b, entry->len, entry->code);
}
void write_run_before(bs_t* b, int run_before, int zeros_left) {
assert(zeros_left > 0);
assert(run_before >= 0 && run_before <= zeros_left);
int idx = (zeros_left > 7) ? 6 : zeros_left - 1;
const vlc_t* entry = &run_before_table[idx][run_before];
assert(entry->len > 0);
bs_write_u(b, entry->len, entry->code);
}
// Static helper for level encoding with suffix_length adaptation
void write_level(bs_t* b, int level, int* suffix_length) {
int sign = (level < 0) ? 1 : 0;
int abs_level = sign ? -level : level;
// levelCode = 2 * abs_level - 2 + sign (for abs_level > 0)
// But we've already adjusted for first non-T1 level
int level_code = (abs_level << 1) - 2 + sign;
// Determine level_prefix and level_suffix
int level_prefix;
int level_suffix_size = *suffix_length;
int level_suffix = 0;
if (*suffix_length == 0) {
// level_prefix = min(14, level_code)
// If level_code >= 14, level_suffix uses escape coding
level_prefix = (level_code < 14) ? level_code : 14;
if (level_code >= 14) {
level_suffix_size = 4;
level_suffix = level_code - 14;
if (level_code >= 30) {
// Extended escape
level_prefix = 15;
level_suffix_size = 12;
level_suffix = level_code - 30;
}
} else {
level_suffix_size = 0;
}
} else {
// Normal case with suffix
// Escape threshold: level_prefix 0..14 with suffixLength suffix bits
// can represent levelCodes 0..(15 << suffixLength)-1
int threshold = (15 << *suffix_length);
if (level_code < threshold) {
level_prefix = level_code >> *suffix_length;
level_suffix = level_code & ((1 << *suffix_length) - 1);
} else {
// Escape: level_prefix = 15, 12-bit suffix
level_prefix = 15;
level_suffix_size = 12;
level_suffix = level_code - threshold;
}
}
write_level_prefix(b, level_prefix);
if (level_suffix_size > 0) {
bs_write_u(b, level_suffix_size, level_suffix);
}
// NOTE: suffix_length update is done by the caller (write_block)
// so it can use the original abs_level before first-non-T1 adjustment.
}
// --- Read (decode) helpers ---
// Read coeff_token by searching VLC table for matching code
// Sets *total_coeff and *trailing_ones, returns 0 on success
static int read_coeff_token_vlc(bs_t* b, const vlc_t table[][17],
int max_tc, int* total_coeff, int* trailing_ones) {
// We need to match against variable-length codes.
// Strategy: try all valid entries, find the one that matches the next bits.
// Since codes are prefix-free, peek at enough bits and check each candidate.
// Max code length in tables is 16 bits.
uint32_t lookahead = bs_next_bits(b, 16);
for (int t1 = 0; t1 <= 3; t1++) {
for (int tc = 0; tc <= max_tc; tc++) {
if (t1 > tc) continue; // invalid: trailing_ones > total_coeff
if (tc == 0 && t1 != 0) continue;
const vlc_t* e = &table[t1][tc];
if (e->len == 0) continue; // invalid entry
uint32_t code = lookahead >> (16 - e->len);
if (code == e->code) {
*total_coeff = tc;
*trailing_ones = t1;
bs_skip_u(b, e->len);
return 0;
}
}
}
return -1; // no match found
}
static int read_coeff_token_chroma_dc(bs_t* b, int* total_coeff, int* trailing_ones) {
uint32_t lookahead = bs_next_bits(b, 8);
for (int t1 = 0; t1 <= 3; t1++) {
for (int tc = 0; tc <= 4; tc++) {
if (t1 > tc) continue;
if (tc == 0 && t1 != 0) continue;
const vlc_t* e = &coeff_token_chroma_dc[t1][tc];
if (e->len == 0) continue;
uint32_t code = lookahead >> (8 - e->len);
if (code == e->code) {
*total_coeff = tc;
*trailing_ones = t1;
bs_skip_u(b, e->len);
return 0;
}
}
}
return -1;
}
int read_coeff_token(bs_t* b, int nc, int* total_coeff, int* trailing_ones) {
if (nc == -1) {
return read_coeff_token_chroma_dc(b, total_coeff, trailing_ones);
} else if (nc >= 8) {
// Fixed-length 6-bit code (H.264 Table 9-5(d))
// code=3 → tc=0; otherwise tc = (code>>2)+1, t1 = code&3
uint32_t code = bs_read_u(b, 6);
if (code == 3) {
*total_coeff = 0;
*trailing_ones = 0;
} else {
*total_coeff = (int)(code >> 2) + 1;
*trailing_ones = (int)(code & 3);
}
return 0;
} else {
const vlc_t (*table)[17];
if (nc < 2) table = coeff_token_table_0;
else if (nc < 4) table = coeff_token_table_2;
else table = coeff_token_table_4;
return read_coeff_token_vlc(b, table, 16, total_coeff, trailing_ones);
}
}
// Read level_prefix (unary: count zeros before a 1)
static int read_level_prefix(bs_t* b) {
int prefix = 0;
while (bs_read_u1(b) == 0 && !bs_eof(b)) {
prefix++;
}
return prefix;
}
// Read a level value given current suffix_length, update suffix_length
// Per H.264 spec section 9.2.2.1 (Table 9-6)
static int read_level(bs_t* b, int* suffix_length) {
int level_prefix = read_level_prefix(b);
int level_code;
int level_suffix_size;
int level_suffix = 0;
// Determine suffix size per spec
if (level_prefix == 14 && *suffix_length == 0) {
level_suffix_size = 4;
} else if (level_prefix >= 15) {
level_suffix_size = level_prefix - 3;
} else {
level_suffix_size = *suffix_length;
}
if (level_suffix_size > 0) {
level_suffix = bs_read_u(b, level_suffix_size);
}
// Compute levelCode per spec:
// levelCode = (Min(15, level_prefix) << suffixLength) + level_suffix
int capped_prefix = (level_prefix < 15) ? level_prefix : 15;
level_code = (capped_prefix << *suffix_length) + level_suffix;
// Additional adjustments per spec
if (level_prefix >= 15 && *suffix_length == 0) {
level_code += 15;
}
if (level_prefix >= 16) {
level_code += (1 << (level_prefix - 3)) - 4096;
}
// Decode level from level_code
int sign = level_code & 1;
int abs_level = (level_code + 2) >> 1;
int level = sign ? -abs_level : abs_level;
// NOTE: suffix_length update is done by the caller (read_block)
// so it can use the adjusted abs_level after first-non-T1 correction.
return level;
}
// Read total_zeros by searching VLC table
static int read_total_zeros_from_table(bs_t* b, const vlc_t* row, int max_zeros) {
uint32_t lookahead = bs_next_bits(b, 16);
for (int tz = 0; tz <= max_zeros; tz++) {
if (row[tz].len == 0) continue;
uint32_t code = lookahead >> (16 - row[tz].len);
if (code == row[tz].code) {
bs_skip_u(b, row[tz].len);
return tz;
}
}
return -1;
}
static int read_total_zeros(bs_t* b, int total_coeff, int max_num_coeff) {
if (max_num_coeff == 4) {
// Chroma DC
int max_zeros = 4 - total_coeff;
return read_total_zeros_from_table(b, total_zeros_chroma_dc[total_coeff - 1], max_zeros);
} else if (max_num_coeff == 15) {
int max_zeros = 15 - total_coeff;
return read_total_zeros_from_table(b, total_zeros_table_15[total_coeff - 1], max_zeros);
} else {
int max_zeros = 16 - total_coeff;
return read_total_zeros_from_table(b, total_zeros_table[total_coeff - 1], max_zeros);
}
}
// Read run_before by searching VLC table
static int read_run_before(bs_t* b, int zeros_left) {
int idx = (zeros_left > 7) ? 6 : zeros_left - 1;
int max_run = (zeros_left > 14) ? 14 : zeros_left;
uint32_t lookahead = bs_next_bits(b, 16);
for (int rb = 0; rb <= max_run; rb++) {
const vlc_t* e = &run_before_table[idx][rb];
if (e->len == 0) continue;
uint32_t code = lookahead >> (16 - e->len);
if (code == e->code) {
bs_skip_u(b, e->len);
return rb;
}
}
return -1;
}
int read_block(bs_t* b, int16_t* coeffs, int nc, int max_num_coeff) {
memset(coeffs, 0, max_num_coeff * sizeof(int16_t));
// Step 1: Read coeff_token
int total_coeff = 0, trailing_ones = 0;
if (read_coeff_token(b, nc, &total_coeff, &trailing_ones) < 0) {
return -1;
}
if (total_coeff == 0) {
return 0;
}
// levels[] in reverse scan order (same as writer: [0] = last nonzero, etc.)
int16_t levels[16];
// Step 2: Read trailing ones signs
for (int i = trailing_ones - 1; i >= 0; i--) {
int sign = bs_read_u1(b);
levels[i] = sign ? -1 : 1;
}
// Step 3: Read remaining levels
int suffix_length = (total_coeff > 10 && trailing_ones < 3) ? 1 : 0;
for (int i = trailing_ones; i < total_coeff; i++) {
int level = read_level(b, &suffix_length);
// First non-T1 level adjustment (inverse of writer)
if (i == trailing_ones && trailing_ones < 3) {
level = (level >= 0) ? level + 1 : level - 1;
}
levels[i] = (int16_t)level;
// Update suffix_length using the final abs_level (after adjustment)
int abs_level = (level < 0) ? -level : level;
if (suffix_length == 0) {
suffix_length = 1;
}
if (abs_level > (3 << (suffix_length - 1)) && suffix_length < 6) {
suffix_length++;
}
}
// Step 4: Read total_zeros
int total_zeros = 0;
if (total_coeff < max_num_coeff) {
total_zeros = read_total_zeros(b, total_coeff, max_num_coeff);
if (total_zeros < 0) {
return -1;
}
}
// Step 5: Read run_before for each coefficient and place in output
int zeros_left = total_zeros;
int runs[16] = {0};
for (int i = 0; i < total_coeff - 1; i++) {
if (zeros_left > 0) {
runs[i] = read_run_before(b, zeros_left);
if (runs[i] < 0) {
return -1;
}
zeros_left -= runs[i];
}
}
// Last coefficient gets remaining zeros
runs[total_coeff - 1] = zeros_left;
// Step 6: Place coefficients in zig-zag order
// levels[0] is the highest-frequency nonzero coeff, levels[total_coeff-1] is lowest
// runs[i] is the number of zeros between levels[i] and levels[i+1] (going downward)
// Start at the highest occupied position and work down
int pos = total_coeff + total_zeros - 1;
for (int i = 0; i < total_coeff; i++) {
if (pos < 0 || pos >= max_num_coeff) {
return -1;
}
coeffs[pos] = levels[i];
pos--;
pos -= runs[i];
}
return total_coeff;
}
int calc_nc(int nc_left, int nc_above) {
if (nc_left < 0 && nc_above < 0) return 0;
if (nc_left < 0) return nc_above;
if (nc_above < 0) return nc_left;
return (nc_left + nc_above + 1) >> 1;
}
int write_block(bs_t* b, const int16_t* coeffs, int nc, int max_num_coeff) {
// Step 1: Scan coefficients to find non-zeros
// Coefficients are in zig-zag order. We need to:
// - Count TotalCoeff (total non-zero coefficients)
// - Count TrailingOnes (up to 3 consecutive +/-1 at the end)
// - Build arrays of levels[] and runs[] for encoding
int16_t levels[16]; // Non-zero coefficient values (reverse order)
int total_coeff = 0;
int trailing_ones = 0;
// Scan from end to start (high frequency to low frequency)
int last_nz = -1;
for (int i = max_num_coeff - 1; i >= 0; i--) {
if (coeffs[i] != 0) {
if (last_nz < 0) last_nz = i;
levels[total_coeff] = coeffs[i];
total_coeff++;
}
}
if (total_coeff == 0) {
// No coefficients - just write coeff_token(0, 0)
write_coeff_token(b, 0, 0, nc);
return 0;
}
// Count trailing ones (must be +/-1, up to 3 consecutive from the end)
for (int i = 0; i < total_coeff && i < 3; i++) {
if (levels[i] == 1 || levels[i] == -1) {
trailing_ones++;
} else {
break;
}
}
// Step 2: Write coeff_token
write_coeff_token(b, total_coeff, trailing_ones, nc);
// Step 3: Write trailing ones signs (in reverse order)
for (int i = trailing_ones - 1; i >= 0; i--) {
bs_write_u1(b, levels[i] < 0 ? 1 : 0);
}
// Step 4: Write remaining levels
// Per H.264 spec 9.2.2: Start with suffixLength=1 when TotalCoeff > 10 and T1 < 3
int suffix_length = (total_coeff > 10 && trailing_ones < 3) ? 1 : 0;
for (int i = trailing_ones; i < total_coeff; i++) {
int original_level = levels[i];
int level = original_level;
// First non-T1 level adjustment
if (i == trailing_ones && trailing_ones < 3) {
// Subtract 1 from magnitude (encoded level can't be +/-1)
level = (level > 0) ? level - 1 : level + 1;
}
write_level(b, level, &suffix_length);
// Update suffix_length using the ORIGINAL abs_level (before adjustment)
int abs_level = (original_level < 0) ? -original_level : original_level;
if (suffix_length == 0) {
suffix_length = 1;
}
if (abs_level > (3 << (suffix_length - 1)) && suffix_length < 6) {
suffix_length++;
}
}
// Step 5: Write total_zeros (if TotalCoeff < max_num_coeff)
if (total_coeff < max_num_coeff) {
int total_zeros = last_nz + 1 - total_coeff;
write_total_zeros(b, total_zeros, total_coeff, max_num_coeff);
}
// Step 6: Compute and write run_before values
// Need to re-scan to get the actual runs
int zeros_left = last_nz + 1 - total_coeff;
int coeff_idx = 0;
for (int i = max_num_coeff - 1; i >= 0 && coeff_idx < total_coeff - 1; i--) {
if (coeffs[i] != 0) {
// Count zeros before this coefficient
int run = 0;
for (int j = i - 1; j >= 0; j--) {
if (coeffs[j] == 0) run++;
else break;
}
if (zeros_left > 0) {
write_run_before(b, run, zeros_left);
zeros_left -= run;
}
coeff_idx++;
}
}
return total_coeff;
}
int copy_tail(bs_t* src, bs_t* dst, int tc, int t1, int max_num_coeff) {
if (tc == 0) return 0;
/* 1. Copy trailing_ones sign bits */
for (int i = 0; i < t1; i++) {
bs_write_u1(dst, bs_read_u1(src));
}
/* 2. Parse and copy level codes (must track suffix_length) */
int suffix_length = (tc > 10 && t1 < 3) ? 1 : 0;
for (int i = t1; i < tc; i++) {
/* Read and copy level_prefix (unary) */
int level_prefix = 0;
while (bs_read_u1(src) == 0) {
bs_write_u1(dst, 0);
level_prefix++;
}
bs_write_u1(dst, 1);
/* Determine suffix size (Table 9-6) */
int level_suffix_size;
if (level_prefix == 14 && suffix_length == 0) {
level_suffix_size = 4;
} else if (level_prefix >= 15) {
level_suffix_size = level_prefix - 3;
} else {
level_suffix_size = suffix_length;
}
/* Copy suffix bits */
int level_suffix = 0;
if (level_suffix_size > 0) {
level_suffix = (int)bs_read_u(src, level_suffix_size);
bs_write_u(dst, level_suffix_size, (uint32_t)level_suffix);
}
/* Reconstruct level_code for suffix_length update */
int capped_prefix = (level_prefix < 15) ? level_prefix : 15;
int level_code = (capped_prefix << suffix_length) + level_suffix;
if (level_prefix >= 15 && suffix_length == 0) {
level_code += 15;
}
if (level_prefix >= 16) {
level_code += (1 << (level_prefix - 3)) - 4096;
}
/* Decode abs_level */
int abs_level = (level_code + 2) >> 1;
/* First non-trailing-one adjustment */
if (i == t1 && t1 < 3) {
abs_level += 1;
}
/* Update suffix_length */
if (suffix_length == 0) suffix_length = 1;
if (abs_level > (3 << (suffix_length - 1)) && suffix_length < 6) {
suffix_length++;
}
}
/* 3. Decode and re-encode total_zeros (same VLC tables, bit-identical) */
int total_zeros = 0;
if (tc < max_num_coeff) {
total_zeros = read_total_zeros(src, tc, max_num_coeff);
if (total_zeros < 0) return -1;
write_total_zeros(dst, total_zeros, tc, max_num_coeff);
}
/* 4. Decode and re-encode run_before */
int zeros_left = total_zeros;
for (int i = 0; i < tc - 1 && zeros_left > 0; i++) {
int run = read_run_before(src, zeros_left);
if (run < 0) return -1;
write_run_before(dst, run, zeros_left);
zeros_left -= run;
}
return 0;
}
} // namespace subcodec::cavlc

19
third-party/subcodec/src/cavlc.h vendored Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
#include "bs.h"
namespace subcodec::cavlc {
void write_coeff_token(bs_t* b, int total_coeff, int trailing_ones, int nc);
void write_level_prefix(bs_t* b, int level_prefix);
void write_total_zeros(bs_t* b, int total_zeros, int total_coeff, int max_num_coeff);
void write_run_before(bs_t* b, int run_before, int zeros_left);
int write_block(bs_t* b, const int16_t* coeffs, int nc, int max_num_coeff);
int read_block(bs_t* b, int16_t* coeffs, int nc, int max_num_coeff);
void write_level(bs_t* b, int level, int* suffix_length);
int calc_nc(int nc_left, int nc_above);
int read_coeff_token(bs_t* b, int nc, int* total_coeff, int* trailing_ones);
int copy_tail(bs_t* src, bs_t* dst, int tc, int t1, int max_num_coeff);
} // namespace subcodec::cavlc

15
third-party/subcodec/src/error.h vendored Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include <expected>
namespace subcodec {
enum class Error {
OUT_OF_SPACE,
PARSE_ERROR,
INVALID_INPUT,
IO_ERROR,
ENCODE_ERROR,
};
} // namespace subcodec

View file

@ -0,0 +1,713 @@
#include "frame_writer.h"
#include "types.h"
#include "cavlc.h"
#include "h264_stream.h"
#include "bs.h"
#include "tables.h"
#include <cstring>
#include <vector>
namespace subcodec::frame_writer {
// H.264 4x4 block index <-> (x4, y4) conversions
// Block layout in a macroblock:
// 0 1 4 5
// 2 3 6 7
// 8 9 12 13
// 10 11 14 15
static inline int blk_to_x4(int blk_idx) {
return (blk_idx & 1) | ((blk_idx >> 1) & 2);
}
static inline int blk_to_y4(int blk_idx) {
return ((blk_idx >> 1) & 1) | ((blk_idx >> 2) & 2);
}
static inline int xy4_to_blk(int x4, int y4) {
return (x4 & 1) | ((y4 & 1) << 1) | ((x4 & 2) << 1) | ((y4 & 2) << 2);
}
// Calculate nC for a luma 4x4 block using neighbor context
static int calc_nc(int blk_idx, const MbContext* out_ctx,
const MbContext* left, const MbContext* above) {
int nc_left = -1, nc_above = -1;
int x4 = blk_to_x4(blk_idx);
int y4 = blk_to_y4(blk_idx);
if (x4 > 0) {
nc_left = out_ctx->nc[xy4_to_blk(x4 - 1, y4)];
} else if (left) {
nc_left = left->nc[xy4_to_blk(3, y4)];
}
if (y4 > 0) {
nc_above = out_ctx->nc[xy4_to_blk(x4, y4 - 1)];
} else if (above) {
nc_above = above->nc[xy4_to_blk(x4, 3)];
}
return subcodec::cavlc::calc_nc(nc_left, nc_above);
}
// Calculate nC for a chroma 2x2 block using neighbor context
// Chroma 2x2 layout: [0 1 / 2 3] — same as h264_parse.c calc_chroma_nc
static int calc_chroma_nc(int blk_idx, const int* chroma_nc,
const int* left_nc, const int* above_nc) {
int cx = blk_idx % 2, cy = blk_idx / 2;
int nc_left = -1, nc_above = -1;
if (cx > 0) nc_left = chroma_nc[blk_idx - 1];
else if (left_nc) nc_left = left_nc[cy * 2 + 1];
if (cy > 0) nc_above = chroma_nc[blk_idx - 2];
else if (above_nc) nc_above = above_nc[cx + 2];
return subcodec::cavlc::calc_nc(nc_left, nc_above);
}
// Table 9-4(b): Coded block pattern mapping for Inter prediction
using subcodec::tables::cbp_to_code_inter;
using subcodec::tables::luma_block_order;
using subcodec::tables::block_to_8x8;
// Write a NAL unit with start code to buffer
// Returns bytes written
static size_t write_nal_with_start_code(uint8_t* buf, size_t buf_size,
h264_stream_t* h) {
if (buf_size < 5) return 0;
// Start code
buf[0] = 0x00;
buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0x01;
// Write NAL unit to a temp buffer
// h264bitstream's write_nal_unit has a quirk: it prepends an extra 0x00 byte
// So we write to buf+3, then the actual NAL starts at buf+4
uint8_t temp[1024];
int nal_size = write_nal_unit(h, temp, (int)sizeof(temp));
if (nal_size < 1) return 0;
// Skip the spurious leading 0x00 byte from write_nal_unit
// Copy the rest starting at position 1
if ((size_t)(nal_size - 1) > buf_size - 4) return 0;
memcpy(buf + 4, temp + 1, (size_t)(nal_size - 1));
return 4 + (size_t)(nal_size - 1);
}
size_t write_headers(std::span<uint8_t> output, const FrameParams& params) {
h264_stream_t* h = h264_new();
if (!h) return 0;
uint8_t* buf = output.data();
size_t buf_size = output.size();
size_t offset = 0;
// SPS
h->nal->nal_ref_idc = 3;
h->nal->nal_unit_type = NAL_UNIT_TYPE_SPS;
sps_t* sps = h->sps;
/* Pick the lowest level that supports this frame size (MB count).
* Baseline Profile supports up to Level 5.2 (36,864 MBs).
* High Profile is needed for Level 6.0+ (up to 139,264 MBs). */
int total_mbs = params.width_mbs * params.height_mbs;
bool need_high = (total_mbs > 36864);
sps->profile_idc = need_high ? 100 : 66; // High or Baseline
sps->constraint_set0_flag = need_high ? 0 : 1;
sps->constraint_set1_flag = need_high ? 0 : 1;
sps->constraint_set2_flag = 0;
sps->constraint_set3_flag = 0;
sps->constraint_set4_flag = 0;
sps->constraint_set5_flag = 0;
if (need_high) {
/* High Profile SPS extensions — all at simplest/default values.
* We still use CAVLC, no 8x8 transform, no scaling matrices. */
sps->chroma_format_idc = 1; // 4:2:0
sps->bit_depth_luma_minus8 = 0;
sps->bit_depth_chroma_minus8 = 0;
sps->qpprime_y_zero_transform_bypass_flag = 0;
sps->seq_scaling_matrix_present_flag = 0;
}
int level;
if (total_mbs <= 1620) level = 30; // Level 3.0
else if (total_mbs <= 3600) level = 31; // Level 3.1
else if (total_mbs <= 5120) level = 32; // Level 3.2
else if (total_mbs <= 8192) level = 40; // Level 4.0
else if (total_mbs <= 8704) level = 42; // Level 4.2
else if (total_mbs <= 22080) level = 50; // Level 5.0
else if (total_mbs <= 36864) level = 51; // Level 5.1
else level = 60; // Level 6.0 (139,264 MBs)
sps->level_idc = level;
sps->seq_parameter_set_id = 0;
int log2mfn = (params.log2_max_frame_num > 4) ? params.log2_max_frame_num : 4;
sps->log2_max_frame_num_minus4 = log2mfn - 4;
sps->pic_order_cnt_type = 2; // No POC, infer from frame_num
sps->num_ref_frames = 1;
sps->gaps_in_frame_num_value_allowed_flag = 0;
sps->pic_width_in_mbs_minus1 = params.width_mbs - 1;
sps->pic_height_in_map_units_minus1 = params.height_mbs - 1;
sps->frame_mbs_only_flag = 1;
sps->direct_8x8_inference_flag = 1;
sps->frame_cropping_flag = 0;
sps->vui_parameters_present_flag = 0;
size_t sps_size = write_nal_with_start_code(buf + offset, buf_size - offset, h);
if (sps_size == 0) { h264_free(h); return 0; }
offset += sps_size;
// PPS
h->nal->nal_ref_idc = 3;
h->nal->nal_unit_type = NAL_UNIT_TYPE_PPS;
pps_t* pps = h->pps;
pps->pic_parameter_set_id = 0;
pps->seq_parameter_set_id = 0;
pps->entropy_coding_mode_flag = 0; // CAVLC
pps->pic_order_present_flag = 0;
pps->num_slice_groups_minus1 = 0;
pps->num_ref_idx_l0_active_minus1 = 0;
pps->num_ref_idx_l1_active_minus1 = 0;
pps->weighted_pred_flag = 0;
pps->weighted_bipred_idc = 0;
int qp = (params.qp > 0) ? params.qp : 26;
pps->pic_init_qp_minus26 = qp - 26;
pps->pic_init_qs_minus26 = 0;
pps->chroma_qp_index_offset = 0;
pps->deblocking_filter_control_present_flag = 1;
pps->constrained_intra_pred_flag = 0;
pps->redundant_pic_cnt_present_flag = 0;
size_t pps_size = write_nal_with_start_code(buf + offset, buf_size - offset, h);
if (pps_size == 0) { h264_free(h); return 0; }
offset += pps_size;
h264_free(h);
return offset;
}
// Return median of three int16_t values
int16_t median3(int16_t a, int16_t b, int16_t c) {
if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
return c;
}
// Predict motion vector using median of neighbors
// Unavailable neighbors are treated as (0, 0)
void predict_mv(const MbContext* left, const MbContext* above,
const MbContext* above_right, int16_t* mvp) {
int16_t mv_a[2] = {0, 0}; // left
int16_t mv_b[2] = {0, 0}; // above
int16_t mv_c[2] = {0, 0}; // above-right
if (left) {
mv_a[0] = left->mv[0];
mv_a[1] = left->mv[1];
}
if (above) {
mv_b[0] = above->mv[0];
mv_b[1] = above->mv[1];
}
if (above_right) {
mv_c[0] = above_right->mv[0];
mv_c[1] = above_right->mv[1];
}
mvp[0] = median3(mv_a[0], mv_b[0], mv_c[0]);
mvp[1] = median3(mv_a[1], mv_b[1], mv_c[1]);
}
// Write P_16x16 macroblock to bitstream
void write_mb_p16x16(bs_t* b, const MacroblockData& mb,
const MbContext* left, const MbContext* above,
const MbContext* above_right,
MbContext& out_ctx) {
// 1. mb_type = 0 for P_L0_16x16 in P-slice (ref_idx_l0=0 implied for single ref)
bs_write_ue(b, 0);
// 2. Motion vector delta (predicted MV subtracted)
int16_t mvp[2];
predict_mv(left, above, above_right, mvp);
bs_write_se(b, mb.mv_x - mvp[0]);
bs_write_se(b, mb.mv_y - mvp[1]);
// 3. coded_block_pattern (always written per H.264 spec for P_L0_16x16)
int cbp = (mb.cbp_chroma << 4) | mb.cbp_luma;
// Write CBP using mapped exp-golomb code (Table 9-4(b))
bs_write_ue(b, cbp_to_code_inter[cbp]);
if (cbp != 0) {
// mb_qp_delta = 0 (no QP change)
bs_write_se(b, 0);
// 4. Write residual with CAVLC
// For P_16x16, residual structure is:
// - 16 luma 4x4 blocks (in raster scan of 8x8 blocks, then 4x4 within)
// - Chroma DC Cb, Chroma DC Cr (if cbp_chroma >= 1)
// - 4 Chroma AC Cb blocks, 4 Chroma AC Cr blocks (if cbp_chroma == 2)
// Initialize nC tracking for output context
for (int i = 0; i < 16; i++) {
out_ctx.nc[i] = 0;
}
// Luma residual: 4 8x8 blocks, each containing 4 4x4 blocks
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int parent_8x8 = block_to_8x8[blk_idx];
// Check if this 8x8 block has coefficients
if (!(mb.cbp_luma & (1 << parent_8x8))) {
out_ctx.nc[blk_idx] = 0;
continue;
}
int nc = calc_nc(blk_idx, &out_ctx, left, above);
// Write the 4x4 block coefficients
int16_t coeffs[16];
coeffs[0] = mb.luma_dc[blk_idx]; // Use luma_dc for the DC coefficient
for (int j = 0; j < 15; j++) {
coeffs[j + 1] = mb.luma_ac[blk_idx][j];
}
int tc = subcodec::cavlc::write_block(b, coeffs, nc, 16);
out_ctx.nc[blk_idx] = tc;
}
// Chroma DC (if cbp_chroma >= 1)
if (mb.cbp_chroma >= 1) {
// Cb DC (4 coefficients for 4:2:0)
subcodec::cavlc::write_block(b, mb.cb_dc, -1, 4);
// Cr DC (4 coefficients for 4:2:0)
subcodec::cavlc::write_block(b, mb.cr_dc, -1, 4);
}
// Chroma AC (if cbp_chroma == 2)
if (mb.cbp_chroma == 2) {
for (int i = 0; i < 4; i++) {
int nc = calc_chroma_nc(i, out_ctx.nc_cb,
left ? left->nc_cb : NULL,
above ? above->nc_cb : NULL);
out_ctx.nc_cb[i] = subcodec::cavlc::write_block(b, mb.cb_ac[i], nc, 15);
}
for (int i = 0; i < 4; i++) {
int nc = calc_chroma_nc(i, out_ctx.nc_cr,
left ? left->nc_cr : NULL,
above ? above->nc_cr : NULL);
out_ctx.nc_cr[i] = subcodec::cavlc::write_block(b, mb.cr_ac[i], nc, 15);
}
}
} else {
// No residual - just initialize context
for (int i = 0; i < 16; i++) {
out_ctx.nc[i] = 0;
}
}
// 5. Update output context with this macroblock's MV
out_ctx.mv[0] = mb.mv_x;
out_ctx.mv[1] = mb.mv_y;
}
// Compute I_16x16 mb_type in P-slice
// I_16x16 mb_type in P-slice: 6 + mode + 4*cbp_chroma + 12*cbp_dc
// mode: 0=Vertical, 1=Horizontal, 2=DC, 3=Plane
// cbp_chroma: 0, 1, or 2
// ac_has_nonzero: 0 or 1 (whether any of the 16 AC blocks have non-zero coeffs)
static int i16x16_mb_type_p_slice(int pred_mode, int cbp_chroma, int ac_has_nonzero) {
return 6 + pred_mode + 4 * cbp_chroma + 12 * ac_has_nonzero;
}
// Write I_16x16 macroblock to bitstream
void write_mb_i16x16(bs_t* b, const MacroblockData& mb,
const MbContext* left, const MbContext* above,
MbContext& out_ctx) {
// 1. Determine if any AC block has non-zero coefficients
int ac_has_nonzero = 0;
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 15; j++) {
if (mb.luma_ac[i][j] != 0) {
ac_has_nonzero = 1;
break;
}
}
if (ac_has_nonzero) break;
}
// 2. Write mb_type (I_16x16 in P-slice)
int mb_type = i16x16_mb_type_p_slice(static_cast<int>(mb.intra_pred_mode), mb.cbp_chroma, ac_has_nonzero);
bs_write_ue(b, (uint32_t)mb_type);
// 3. Write intra_chroma_pred_mode
bs_write_ue(b, static_cast<uint32_t>(mb.intra_chroma_mode));
// 4. Write mb_qp_delta = 0
bs_write_se(b, 0);
// 5. Write Luma DC block (nC from block 0 neighbors, max_num_coeff = 16)
{
int dc_nc = calc_nc(0, &out_ctx, left, above);
subcodec::cavlc::write_block(b, mb.luma_dc, dc_nc, 16);
}
// Initialize nC tracking for output context
for (int i = 0; i < 16; i++) {
out_ctx.nc[i] = 0;
}
// 6. Write 16 Luma AC blocks (max_num_coeff = 15, DC is separate)
if (ac_has_nonzero) {
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int nc = calc_nc(blk_idx, &out_ctx, left, above);
// Write the AC block (15 coefficients, no DC)
out_ctx.nc[blk_idx] = subcodec::cavlc::write_block(b, mb.luma_ac[blk_idx], nc, 15);
}
}
// 7. Write chroma DC blocks (nC = -1 for chroma DC, max_num_coeff = 4)
// Cb DC then Cr DC
if (mb.cbp_chroma > 0) {
subcodec::cavlc::write_block(b, mb.cb_dc, -1, 4);
subcodec::cavlc::write_block(b, mb.cr_dc, -1, 4);
}
// 8. Write chroma AC blocks if cbp_chroma == 2
if (mb.cbp_chroma == 2) {
for (int i = 0; i < 4; i++) {
int nc = calc_chroma_nc(i, out_ctx.nc_cb,
left ? left->nc_cb : NULL,
above ? above->nc_cb : NULL);
out_ctx.nc_cb[i] = subcodec::cavlc::write_block(b, mb.cb_ac[i], nc, 15);
}
for (int i = 0; i < 4; i++) {
int nc = calc_chroma_nc(i, out_ctx.nc_cr,
left ? left->nc_cr : NULL,
above ? above->nc_cr : NULL);
out_ctx.nc_cr[i] = subcodec::cavlc::write_block(b, mb.cr_ac[i], nc, 15);
}
}
// 9. Update context - I-macroblock has no motion vector
out_ctx.mv[0] = 0;
out_ctx.mv[1] = 0;
}
std::expected<size_t, Error> write_p_frame_ex(
std::span<uint8_t> output,
const FrameParams& params,
const MacroblockData* mbs,
int frame_num) {
uint8_t* buf = output.data();
size_t buf_size = output.size();
if (buf_size < 8) return std::unexpected(Error::OUT_OF_SPACE);
// NAL header
buf[0] = 0x00;
buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0x01;
buf[4] = (2 << 5) | 1; // nal_ref_idc=2, nal_unit_type=1 (non-IDR slice)
uint8_t rbsp[256 * 1024];
bs_t b;
bs_init(&b, rbsp, sizeof(rbsp));
// Slice header (same as write_p_frame)
bs_write_ue(&b, 0); // first_mb_in_slice = 0
bs_write_ue(&b, 5); // slice_type = 5 (P, all macroblocks)
bs_write_ue(&b, 0); // pic_parameter_set_id = 0
{
int l2mfn = (params.log2_max_frame_num > 4) ? params.log2_max_frame_num : 4;
int max_fn = 1 << l2mfn;
bs_write_u(&b, l2mfn, frame_num % max_fn);
}
// num_ref_idx_active_override_flag
bs_write_u(&b, 1, 0);
// ref_pic_list_modification - no modifications
bs_write_u(&b, 1, 0); // ref_pic_list_modification_flag_l0
// dec_ref_pic_marking - adaptive_ref_pic_marking_mode_flag
bs_write_u(&b, 1, 0);
// slice_qp_delta
bs_write_se(&b, params.slice_qp_delta);
// deblocking_filter_control_present_flag = 1, so:
bs_write_ue(&b, 1); // disable_deblocking_filter_idc = 1 (disabled)
// Allocate context for neighbor tracking
int num_mbs = params.width_mbs * params.height_mbs;
std::vector<MbContext> ctx_row_vec(params.width_mbs);
MbContext* ctx_row = ctx_row_vec.data();
MbContext ctx_left = {};
int mb_idx = 0;
int prev_was_skip = 0; // Track if previous iteration was a skip run
while (mb_idx < num_mbs) {
const MacroblockData& mb = mbs[mb_idx];
int mb_x = mb_idx % params.width_mbs;
int mb_y = mb_idx / params.width_mbs;
// Get neighbor contexts
MbContext* above = (mb_y > 0) ? &ctx_row[mb_x] : NULL;
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
MbContext* above_right = (mb_y > 0 && mb_x < params.width_mbs - 1)
? &ctx_row[mb_x + 1] : NULL;
// Handle skip run
if (mb.mb_type == MbType::SKIP) {
int skip_count = 1;
while (mb_idx + skip_count < num_mbs &&
mbs[mb_idx + skip_count].mb_type == MbType::SKIP) {
skip_count++;
}
bs_write_ue(&b, (uint32_t)skip_count);
// Update context for skipped MBs (MV=0, nC=0)
for (int i = 0; i < skip_count; i++) {
int idx = mb_idx + i;
int x = idx % params.width_mbs;
MbContext skip_ctx = {};
ctx_left = skip_ctx;
ctx_row[x] = skip_ctx;
}
mb_idx += skip_count;
prev_was_skip = 1;
continue;
}
// Non-skip: write mb_skip_run = 0 only if we didn't just write a skip run
if (!prev_was_skip) {
bs_write_ue(&b, 0);
}
prev_was_skip = 0;
// Write macroblock based on type
MbContext out_ctx = {};
switch (mb.mb_type) {
case MbType::P_16x16:
write_mb_p16x16(&b, mb, left_ctx, above, above_right, out_ctx);
break;
case MbType::I_16x16:
write_mb_i16x16(&b, mb, left_ctx, above, out_ctx);
break;
default:
// Unknown type, treat as skip (should not happen)
out_ctx = MbContext{};
break;
}
// Update context
ctx_left = out_ctx;
ctx_row[mb_x] = out_ctx;
mb_idx++;
}
// RBSP trailing bits
write_rbsp_trailing_bits(&b);
size_t rbsp_size = (size_t)bs_pos(&b);
// Copy RBSP to output with emulation prevention
size_t out_pos = 5;
int zero_count = 0;
for (size_t i = 0; i < rbsp_size && out_pos < buf_size; i++) {
uint8_t byte = rbsp[i];
if (zero_count >= 2 && byte <= 3) {
if (out_pos >= buf_size) {
return std::unexpected(Error::OUT_OF_SPACE);
}
buf[out_pos++] = 0x03;
zero_count = 0;
}
if (out_pos >= buf_size) {
return std::unexpected(Error::OUT_OF_SPACE);
}
buf[out_pos++] = byte;
zero_count = (byte == 0) ? zero_count + 1 : 0;
}
return out_pos;
}
// Compute I_16x16 mb_type in I-slice
// I_16x16 mb_type in I-slice: 1 + pred_mode + 4*cbp_chroma + 12*ac_has_nonzero
// (offset is 1, not 6 as in P-slice)
static int i16x16_mb_type_i_slice(int pred_mode, int cbp_chroma, int ac_has_nonzero) {
return 1 + pred_mode + 4 * cbp_chroma + 12 * ac_has_nonzero;
}
std::expected<size_t, Error> write_idr_frame_ex(
std::span<uint8_t> output,
const FrameParams& params,
const MacroblockData* mbs) {
uint8_t* buf = output.data();
size_t buf_size = output.size();
if (buf_size < 8) return std::unexpected(Error::OUT_OF_SPACE);
// NAL header: nal_ref_idc=3, nal_unit_type=5 (IDR slice)
buf[0] = 0x00;
buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0x01;
buf[4] = (3 << 5) | 5;
uint8_t rbsp[256 * 1024];
bs_t b;
bs_init(&b, rbsp, sizeof(rbsp));
// Slice header for IDR I-slice
bs_write_ue(&b, 0); // first_mb_in_slice = 0
bs_write_ue(&b, 7); // slice_type = 7 (I, all macroblocks)
bs_write_ue(&b, 0); // pic_parameter_set_id = 0
{
int l2mfn = (params.log2_max_frame_num > 4) ? params.log2_max_frame_num : 4;
bs_write_u(&b, l2mfn, 0); // frame_num = 0
}
bs_write_ue(&b, 0); // idr_pic_id = 0
// dec_ref_pic_marking for IDR with nal_ref_idc > 0
bs_write_u(&b, 1, 0); // no_output_of_prior_pics_flag = 0
bs_write_u(&b, 1, 0); // long_term_reference_flag = 0
bs_write_se(&b, params.slice_qp_delta); // slice_qp_delta
// deblocking_filter_control_present_flag = 1, so:
bs_write_ue(&b, 1); // disable_deblocking_filter_idc = 1 (disabled)
// Allocate context for neighbor tracking
int num_mbs = params.width_mbs * params.height_mbs;
std::vector<MbContext> ctx_row_vec(params.width_mbs);
MbContext* ctx_row = ctx_row_vec.data();
MbContext ctx_left = {};
for (int mb_idx = 0; mb_idx < num_mbs; mb_idx++) {
const MacroblockData& mb = mbs[mb_idx];
int mb_x = mb_idx % params.width_mbs;
int mb_y = mb_idx / params.width_mbs;
// Get neighbor contexts
MbContext* above = (mb_y > 0) ? &ctx_row[mb_x] : NULL;
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
MbContext out_ctx = {};
switch (mb.mb_type) {
case MbType::I_16x16: {
// Determine if any AC block has non-zero coefficients
int ac_has_nonzero = 0;
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 15; j++) {
if (mb.luma_ac[i][j] != 0) {
ac_has_nonzero = 1;
break;
}
}
if (ac_has_nonzero) break;
}
// Write mb_type (I_16x16 in I-slice, offset 1)
int mbt = i16x16_mb_type_i_slice(static_cast<int>(mb.intra_pred_mode), mb.cbp_chroma, ac_has_nonzero);
bs_write_ue(&b, (uint32_t)mbt);
// Write intra_chroma_pred_mode
bs_write_ue(&b, static_cast<uint32_t>(mb.intra_chroma_mode));
// Write mb_qp_delta = 0
bs_write_se(&b, 0);
// Write Luma DC block (nC from block 0 neighbors, max_num_coeff = 16)
{
int dc_nc = calc_nc(0, &out_ctx, left_ctx, above);
subcodec::cavlc::write_block(&b, mb.luma_dc, dc_nc, 16);
}
// Initialize nC tracking
for (int i = 0; i < 16; i++) {
out_ctx.nc[i] = 0;
}
// Write 16 Luma AC blocks if ac_has_nonzero
if (ac_has_nonzero) {
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int nc = calc_nc(blk_idx, &out_ctx, left_ctx, above);
out_ctx.nc[blk_idx] = subcodec::cavlc::write_block(&b, mb.luma_ac[blk_idx], nc, 15);
}
}
// Write chroma DC blocks
if (mb.cbp_chroma > 0) {
subcodec::cavlc::write_block(&b, mb.cb_dc, -1, 4);
subcodec::cavlc::write_block(&b, mb.cr_dc, -1, 4);
}
// Write chroma AC blocks if cbp_chroma == 2
if (mb.cbp_chroma == 2) {
for (int i = 0; i < 4; i++) {
subcodec::cavlc::write_block(&b, mb.cb_ac[i], 0, 15);
}
for (int i = 0; i < 4; i++) {
subcodec::cavlc::write_block(&b, mb.cr_ac[i], 0, 15);
}
}
// I-macroblock has no motion vector
out_ctx.mv[0] = 0;
out_ctx.mv[1] = 0;
break;
}
default:
// Unknown type - treat as empty (should not happen in IDR)
out_ctx = MbContext{};
break;
}
// Update context
ctx_left = out_ctx;
ctx_row[mb_x] = out_ctx;
}
// RBSP trailing bits
write_rbsp_trailing_bits(&b);
size_t rbsp_size = (size_t)bs_pos(&b);
// Copy RBSP to output with emulation prevention
size_t out_pos = 5;
int zero_count = 0;
for (size_t i = 0; i < rbsp_size && out_pos < buf_size; i++) {
uint8_t byte = rbsp[i];
if (zero_count >= 2 && byte <= 3) {
if (out_pos >= buf_size) {
return std::unexpected(Error::OUT_OF_SPACE);
}
buf[out_pos++] = 0x03;
zero_count = 0;
}
if (out_pos >= buf_size) {
return std::unexpected(Error::OUT_OF_SPACE);
}
buf[out_pos++] = byte;
zero_count = (byte == 0) ? zero_count + 1 : 0;
}
return out_pos;
}
} // namespace subcodec::frame_writer

40
third-party/subcodec/src/frame_writer.h vendored Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <span>
#include <expected>
#include "types.h"
#include "error.h"
#include "bs.h"
namespace subcodec::frame_writer {
size_t write_headers(std::span<uint8_t> output, const FrameParams& params);
int16_t median3(int16_t a, int16_t b, int16_t c);
void predict_mv(const MbContext* left, const MbContext* above,
const MbContext* above_right, int16_t* mvp);
void write_mb_p16x16(bs_t* b, const MacroblockData& mb,
const MbContext* left, const MbContext* above,
const MbContext* above_right,
MbContext& out_ctx);
void write_mb_i16x16(bs_t* b, const MacroblockData& mb,
const MbContext* left, const MbContext* above,
MbContext& out_ctx);
std::expected<size_t, Error> write_p_frame_ex(
std::span<uint8_t> output,
const FrameParams& params,
const MacroblockData* mbs,
int frame_num);
std::expected<size_t, Error> write_idr_frame_ex(
std::span<uint8_t> output,
const FrameParams& params,
const MacroblockData* mbs);
} // namespace subcodec::frame_writer

684
third-party/subcodec/src/h264_parser.cpp vendored Normal file
View file

@ -0,0 +1,684 @@
#include "h264_parser.h"
#include "cavlc.h"
#include "bs.h"
#include "tables.h"
#include "frame_writer.h"
#include <cstdio>
#include <cstring>
using namespace subcodec;
using subcodec::tables::cbp_to_code_inter;
using subcodec::tables::cbp_to_code_intra;
using subcodec::tables::luma_block_order;
using subcodec::tables::block_to_8x8;
namespace {
// Inverse of cbp_to_code_inter: given codeNum, find cbp_luma and cbp_chroma
static int decode_cbp_inter(uint32_t code_num, uint8_t* cbp_luma, uint8_t* cbp_chroma) {
for (int i = 0; i < 48; i++) {
if (cbp_to_code_inter[i] == code_num) {
*cbp_chroma = (uint8_t)(i / 16);
*cbp_luma = (uint8_t)(i % 16);
return 0;
}
}
return -1;
}
// Inverse of cbp_to_code_intra: given codeNum, find cbp_luma and cbp_chroma
static int decode_cbp_intra(uint32_t code_num, uint8_t* cbp_luma, uint8_t* cbp_chroma) {
for (int i = 0; i < 48; i++) {
if (cbp_to_code_intra[i] == code_num) {
*cbp_chroma = (uint8_t)(i / 16);
*cbp_luma = (uint8_t)(i % 16);
return 0;
}
}
return -1;
}
// Remove emulation prevention bytes (00 00 03 -> 00 00)
static size_t remove_emulation_prevention(const uint8_t* src, size_t src_len,
uint8_t* dst, size_t dst_size) {
size_t si = 0, di = 0;
while (si < src_len && di < dst_size) {
if (si + 2 < src_len && src[si] == 0x00 && src[si + 1] == 0x00 && src[si + 2] == 0x03) {
dst[di++] = 0x00;
if (di < dst_size) dst[di++] = 0x00;
si += 3;
} else {
dst[di++] = src[si++];
}
}
return di;
}
// Check if there is more RBSP data (i.e., not just trailing bits)
static int more_rbsp_data(bs_t* b) {
// If we're at or past the end, no more data
if (bs_eof(b)) return 0;
// Save position
bs_t saved;
bs_clone(&saved, b);
// Find the last 1 bit in the remaining data
// If all remaining bits are 0 after the current position, no data
// If the remaining bits are exactly: 1 followed by 0s to byte boundary, no data
// Simple approach: check if we have more than 8 bits remaining
// (trailing bits are at most 8 bits)
int bits_remaining = 0;
uint8_t* p = b->p;
int bl = b->bits_left;
// Count remaining bits
bits_remaining = bl; // bits left in current byte
if (p < b->end) {
bits_remaining += (int)(b->end - p - 1) * 8;
}
if (bits_remaining <= 0) return 0;
if (bits_remaining > 8) return 1; // More than trailing bits
// 8 or fewer bits remaining - check if it's just trailing bits
// Trailing bits: 1 followed by 0s
// Read remaining bits and check
uint32_t remaining = 0;
for (int i = 0; i < bits_remaining; i++) {
remaining = (remaining << 1) | bs_read_u1(&saved);
}
// Check if remaining == 1 << (bits_remaining - 1) (just a stop bit + zeros)
// Or more generally: remaining should be a power of 2
if (remaining != 0 && (remaining & (remaining - 1)) == 0) {
return 0; // It's just trailing bits
}
return 1; // There's real data
}
// H.264 4x4 block index <-> (x4, y4) conversions
// Block layout in a macroblock:
// 0 1 4 5
// 2 3 6 7
// 8 9 12 13
// 10 11 14 15
static inline int blk_to_x4(int blk_idx) {
return (blk_idx & 1) | ((blk_idx >> 1) & 2);
}
static inline int blk_to_y4(int blk_idx) {
return ((blk_idx >> 1) & 1) | ((blk_idx >> 2) & 2);
}
static inline int xy4_to_blk(int x4, int y4) {
return (x4 & 1) | ((y4 & 1) << 1) | ((x4 & 2) << 1) | ((y4 & 2) << 2);
}
// Calculate nC for a luma 4x4 block using neighbor context
static int calc_block_nc(int blk_idx, const MbContext* out_ctx,
const MbContext* left, const MbContext* above) {
int nc_left = -1, nc_above = -1;
int x4 = blk_to_x4(blk_idx);
int y4 = blk_to_y4(blk_idx);
// Left neighbor
if (x4 > 0) {
nc_left = out_ctx->nc[xy4_to_blk(x4 - 1, y4)];
} else if (left) {
nc_left = left->nc[xy4_to_blk(3, y4)];
}
// Above neighbor
if (y4 > 0) {
nc_above = out_ctx->nc[xy4_to_blk(x4, y4 - 1)];
} else if (above) {
nc_above = above->nc[xy4_to_blk(x4, 3)];
}
return subcodec::cavlc::calc_nc(nc_left, nc_above);
}
// Calculate nC for a chroma 4x4 block (Cb or Cr)
static int calc_chroma_nc(int blk_idx, const int* chroma_nc,
const int* left_nc, const int* above_nc) {
int cx = blk_idx % 2, cy = blk_idx / 2;
int nc_left = -1, nc_above = -1;
// Chroma 2x2 block layout: [0 1 / 2 3]
if (cx > 0) nc_left = chroma_nc[blk_idx - 1];
else if (left_nc) nc_left = left_nc[cy * 2 + 1]; // left MB's right column at same y
if (cy > 0) nc_above = chroma_nc[blk_idx - 2];
else if (above_nc) nc_above = above_nc[cx + 2]; // above MB's bottom row at same x
return subcodec::cavlc::calc_nc(nc_left, nc_above);
}
// Read chroma AC blocks and update context.
static void read_chroma_ac(bs_t* b, MacroblockData* mb,
MbContext* out_ctx,
const MbContext* left,
const MbContext* above) {
for (int i = 0; i < 4; i++) {
int nc = calc_chroma_nc(i, out_ctx->nc_cb,
left ? left->nc_cb : NULL,
above ? above->nc_cb : NULL);
int16_t coeffs[15];
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 15);
out_ctx->nc_cb[i] = tc;
for (int j = 0; j < 15; j++)
mb->cb_ac[i][j] = coeffs[j];
}
for (int i = 0; i < 4; i++) {
int nc = calc_chroma_nc(i, out_ctx->nc_cr,
left ? left->nc_cr : NULL,
above ? above->nc_cr : NULL);
int16_t coeffs[15];
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 15);
out_ctx->nc_cr[i] = tc;
for (int j = 0; j < 15; j++)
mb->cr_ac[i][j] = coeffs[j];
}
}
// Read P_16x16 macroblock (inverse of write_mb_p16x16)
static void read_mb_p16x16(bs_t* b, MacroblockData* mb,
const MbContext* left, const MbContext* above,
const MbContext* above_right,
MbContext* out_ctx) {
mb->mb_type = MbType::P_16x16;
// Read MV delta and reconstruct MV
int16_t mvp[2];
subcodec::frame_writer::predict_mv(left, above, above_right, mvp);
int32_t mvd_x = bs_read_se(b);
int32_t mvd_y = bs_read_se(b);
mb->mv_x = (int16_t)(mvp[0] + mvd_x);
mb->mv_y = (int16_t)(mvp[1] + mvd_y);
// Read CBP (always present per H.264 spec for P_L0_16x16)
uint32_t cbp_code = bs_read_ue(b);
*out_ctx = MbContext{};
if (decode_cbp_inter(cbp_code, &mb->cbp_luma, &mb->cbp_chroma) != 0) {
mb->cbp_luma = 0;
mb->cbp_chroma = 0;
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
return;
}
if (mb->cbp_luma == 0 && mb->cbp_chroma == 0) {
// No residual
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
return;
}
// Read mb_qp_delta (discarded)
bs_read_se(b);
// Read luma residual
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int parent_8x8 = block_to_8x8[blk_idx];
if (!(mb->cbp_luma & (1 << parent_8x8))) {
out_ctx->nc[blk_idx] = 0;
continue;
}
int nc = calc_block_nc(blk_idx, out_ctx, left, above);
int16_t coeffs[16];
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 16);
out_ctx->nc[blk_idx] = tc;
// Split: coeffs[0] -> luma_dc, coeffs[1..15] -> luma_ac
mb->luma_dc[blk_idx] = coeffs[0];
for (int j = 0; j < 15; j++) {
mb->luma_ac[blk_idx][j] = coeffs[j + 1];
}
}
// Chroma DC
if (mb->cbp_chroma >= 1) {
subcodec::cavlc::read_block(b, mb->cb_dc, -1, 4);
subcodec::cavlc::read_block(b, mb->cr_dc, -1, 4);
}
// Chroma AC
if (mb->cbp_chroma == 2) {
read_chroma_ac(b, mb, out_ctx, left, above);
}
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
}
// Read P_8x8 or P_8x8ref0 macroblock (mb_type 3 or 4 in P-slice)
// We store as P_16x16 with the first sub-partition's MV for simplicity
static void read_mb_p8x8(bs_t* b, MacroblockData* mb, int is_ref0,
const MbContext* left, const MbContext* above,
const MbContext* above_right,
MbContext* out_ctx) {
mb->mb_type = MbType::P_16x16; // Approximate as P_16x16
// Read 4 sub_mb_type values
int sub_mb_type[4];
int num_sub_parts[4];
for (int i = 0; i < 4; i++) {
sub_mb_type[i] = (int)bs_read_ue(b);
// P sub_mb_type: 0=8x8(1 part), 1=8x4(2), 2=4x8(2), 3=4x4(4)
switch (sub_mb_type[i]) {
case 0: num_sub_parts[i] = 1; break;
case 1: num_sub_parts[i] = 2; break;
case 2: num_sub_parts[i] = 2; break;
case 3: num_sub_parts[i] = 4; break;
default: num_sub_parts[i] = 1; break;
}
}
// ref_idx: for P_8x8 (not ref0), read ref_idx for each 8x8 partition
if (!is_ref0) {
for (int i = 0; i < 4; i++) {
bs_read_ue(b); // ref_idx_l0[i] (typically 0 with single ref)
}
}
// MVD for each sub-partition
int16_t first_mv_x = 0, first_mv_y = 0;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < num_sub_parts[i]; j++) {
int32_t mvd_x = bs_read_se(b);
int32_t mvd_y = bs_read_se(b);
if (i == 0 && j == 0) {
// Use first partition's MV as representative
int16_t mvp[2];
subcodec::frame_writer::predict_mv(left, above, above_right, mvp);
first_mv_x = (int16_t)(mvp[0] + mvd_x);
first_mv_y = (int16_t)(mvp[1] + mvd_y);
}
}
}
mb->mv_x = first_mv_x;
mb->mv_y = first_mv_y;
// CBP
uint32_t cbp_code = bs_read_ue(b);
*out_ctx = MbContext{};
if (decode_cbp_inter(cbp_code, &mb->cbp_luma, &mb->cbp_chroma) != 0) {
mb->cbp_luma = 0;
mb->cbp_chroma = 0;
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
return;
}
if (mb->cbp_luma == 0 && mb->cbp_chroma == 0) {
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
return;
}
// mb_qp_delta
bs_read_se(b);
// Luma residual
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int parent_8x8 = block_to_8x8[blk_idx];
if (!(mb->cbp_luma & (1 << parent_8x8))) {
out_ctx->nc[blk_idx] = 0;
continue;
}
int nc = calc_block_nc(blk_idx, out_ctx, left, above);
int16_t coeffs[16];
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 16);
out_ctx->nc[blk_idx] = tc;
mb->luma_dc[blk_idx] = coeffs[0];
for (int j = 0; j < 15; j++)
mb->luma_ac[blk_idx][j] = coeffs[j + 1];
}
// Chroma DC
if (mb->cbp_chroma >= 1) {
subcodec::cavlc::read_block(b, mb->cb_dc, -1, 4);
subcodec::cavlc::read_block(b, mb->cr_dc, -1, 4);
}
// Chroma AC
if (mb->cbp_chroma == 2) {
read_chroma_ac(b, mb, out_ctx, left, above);
}
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
}
// Read I_16x16 macroblock (inverse of write_mb_i16x16)
// offset: mb_type_code - 6 for P-slice, mb_type_code - 1 for I-slice
static void read_mb_i16x16(bs_t* b, MacroblockData* mb, int offset,
const MbContext* left, const MbContext* above,
MbContext* out_ctx) {
mb->mb_type = MbType::I_16x16;
int ac_has_nonzero = offset / 12;
int cbp_chroma = (offset % 12) / 4;
int pred_mode = offset % 4;
mb->intra_pred_mode = static_cast<subcodec::I16PredMode>(pred_mode);
mb->cbp_chroma = (uint8_t)cbp_chroma;
// Read intra_chroma_pred_mode
mb->intra_chroma_mode = static_cast<subcodec::ChromaPredMode>(bs_read_ue(b));
// Read mb_qp_delta
int32_t qpd = bs_read_se(b);
*out_ctx = MbContext{};
// Read luma DC (16 coefficients) - nC from block 0 neighbors per H.264 spec
int dc_nc = calc_block_nc(0, out_ctx, left, above);
int dc_tc = subcodec::cavlc::read_block(b, mb->luma_dc, dc_nc, 16);
// Read luma AC blocks if present
if (ac_has_nonzero) {
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int nc = calc_block_nc(blk_idx, out_ctx, left, above);
int16_t coeffs[15];
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 15);
out_ctx->nc[blk_idx] = tc;
for (int j = 0; j < 15; j++)
mb->luma_ac[blk_idx][j] = coeffs[j];
}
}
// Chroma DC
if (cbp_chroma > 0) {
subcodec::cavlc::read_block(b, mb->cb_dc, -1, 4);
subcodec::cavlc::read_block(b, mb->cr_dc, -1, 4);
}
// Chroma AC
if (cbp_chroma == 2) {
read_chroma_ac(b, mb, out_ctx, left, above);
}
out_ctx->mv[0] = 0;
out_ctx->mv[1] = 0;
}
} // anonymous namespace
namespace subcodec {
std::expected<std::vector<MacroblockData>, Error>
H264Parser::parse_slice(std::span<const uint8_t> nal_data,
const FrameParams& params) {
return parse_slice_ex(nal_data, params, nullptr);
}
std::expected<std::vector<MacroblockData>, Error>
H264Parser::parse_slice_ex(std::span<const uint8_t> nal_data,
const FrameParams& params,
int* out_slice_qp_delta) {
const uint8_t* buf = nal_data.data();
size_t buf_size = nal_data.size();
if (!buf || buf_size < 5)
return std::unexpected(Error::INVALID_INPUT);
// Verify start code
if (buf[0] != 0x00 || buf[1] != 0x00 || buf[2] != 0x00 || buf[3] != 0x01)
return std::unexpected(Error::INVALID_INPUT);
// Read NAL header
uint8_t nal_header = buf[4];
int nal_ref_idc = (nal_header >> 5) & 0x3;
int nal_unit_type = nal_header & 0x1F;
int is_idr = (nal_unit_type == 5);
// Remove emulation prevention bytes using member buffer
rbsp_buf_.resize(buf_size);
size_t rbsp_size = remove_emulation_prevention(buf + 5, buf_size - 5,
rbsp_buf_.data(), rbsp_buf_.size());
bs_t b;
bs_init(&b, rbsp_buf_.data(), rbsp_size);
// Slice header
bs_read_ue(&b); // first_mb_in_slice
uint32_t slice_type = bs_read_ue(&b); // slice_type
bs_read_ue(&b); // pic_parameter_set_id
int frame_num_bits = (params.log2_max_frame_num > 0) ? params.log2_max_frame_num : 4;
bs_read_u(&b, frame_num_bits); // frame_num
int is_p_slice = (slice_type == 0 || slice_type == 5);
int is_i_slice = (slice_type == 2 || slice_type == 7);
if (is_idr) {
bs_read_ue(&b); // idr_pic_id
}
// pic_order_cnt parsing (depends on SPS pic_order_cnt_type)
if (params.log2_max_frame_num > 0) {
int poc_type = params.pic_order_cnt_type;
if (poc_type == 0) {
int poc_lsb_bits = (params.log2_max_pic_order_cnt_lsb > 0)
? params.log2_max_pic_order_cnt_lsb : 4;
bs_read_u(&b, poc_lsb_bits); // pic_order_cnt_lsb
} else if (poc_type == 1) {
bs_read_se(&b); // delta_pic_order_cnt[0]
}
// poc_type == 2: nothing in slice header
}
// P-slice header extras
if (is_p_slice) {
if (bs_read_u1(&b)) { // num_ref_idx_active_override_flag
bs_read_ue(&b); // num_ref_idx_l0_active_minus1
}
if (bs_read_u1(&b)) { // ref_pic_list_modification_flag_l0
uint32_t mod_op;
do {
mod_op = bs_read_ue(&b);
if (mod_op != 3) {
bs_read_ue(&b);
}
} while (mod_op != 3);
}
}
// dec_ref_pic_marking
if (nal_ref_idc > 0) {
if (is_idr) {
bs_read_u1(&b); // no_output_of_prior_pics_flag
bs_read_u1(&b); // long_term_reference_flag
} else {
bs_read_u1(&b); // adaptive_ref_pic_marking_mode_flag
}
}
int32_t slice_qp_delta = bs_read_se(&b); // slice_qp_delta
if (out_slice_qp_delta) *out_slice_qp_delta = (int)slice_qp_delta;
uint32_t disable_deblocking = bs_read_ue(&b); // disable_deblocking_filter_idc
if (disable_deblocking != 1) {
bs_read_se(&b); // slice_alpha_c0_offset_div2
bs_read_se(&b); // slice_beta_offset_div2
}
// Parse macroblocks
int num_mbs = params.width_mbs * params.height_mbs;
std::vector<MacroblockData> out_mbs(static_cast<size_t>(num_mbs));
ctx_row_.assign(static_cast<size_t>(params.width_mbs), MbContext{});
MbContext ctx_left = {};
int mb_idx = 0;
bool error = false;
if (is_p_slice) {
while (mb_idx < num_mbs) {
// Read mb_skip_run (always, per H.264 spec syntax)
uint32_t skip_run = bs_read_ue(&b);
for (uint32_t i = 0; i < skip_run && mb_idx < num_mbs; i++) {
int mb_x = mb_idx % params.width_mbs;
out_mbs[mb_idx].mb_type = MbType::SKIP;
MbContext skip_ctx = {};
ctx_left = skip_ctx;
ctx_row_[mb_x] = skip_ctx;
mb_idx++;
}
if (skip_run > 0 && !more_rbsp_data(&b)) {
// Fill remaining MBs as skip
while (mb_idx < num_mbs) {
int mb_x2 = mb_idx % params.width_mbs;
out_mbs[mb_idx].mb_type = MbType::SKIP;
MbContext skip_ctx2 = {};
ctx_left = skip_ctx2;
ctx_row_[mb_x2] = skip_ctx2;
mb_idx++;
}
break;
}
if (mb_idx >= num_mbs) break;
int mb_x = mb_idx % params.width_mbs;
int mb_y = mb_idx / params.width_mbs;
MbContext* above = (mb_y > 0) ? &ctx_row_[mb_x] : NULL;
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
MbContext* above_right = (mb_y > 0 && mb_x < params.width_mbs - 1)
? &ctx_row_[mb_x + 1] : NULL;
uint32_t mb_type_code = bs_read_ue(&b);
MbContext out_ctx = {};
if (mb_type_code == 0) {
read_mb_p16x16(&b, &out_mbs[mb_idx], left_ctx, above, above_right, &out_ctx);
} else if (mb_type_code >= 1 && mb_type_code <= 2) {
// P_L0_L0_16x8 or P_L0_L0_8x16 - approximate as P_16x16
out_mbs[mb_idx].mb_type = MbType::P_16x16;
// 2 partitions, each with MVD
int16_t mvp[2];
subcodec::frame_writer::predict_mv(left_ctx, above, above_right, mvp);
int32_t mvd_x = bs_read_se(&b);
int32_t mvd_y = bs_read_se(&b);
out_mbs[mb_idx].mv_x = (int16_t)(mvp[0] + mvd_x);
out_mbs[mb_idx].mv_y = (int16_t)(mvp[1] + mvd_y);
// Second partition MVD
bs_read_se(&b); // mvd_x
bs_read_se(&b); // mvd_y
// CBP + residual (same as P_16x16)
uint32_t cbp_code = bs_read_ue(&b);
out_ctx = MbContext{};
if (decode_cbp_inter(cbp_code, &out_mbs[mb_idx].cbp_luma, &out_mbs[mb_idx].cbp_chroma) == 0 &&
(out_mbs[mb_idx].cbp_luma != 0 || out_mbs[mb_idx].cbp_chroma != 0)) {
bs_read_se(&b); // qp_delta
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int parent_8x8 = block_to_8x8[blk_idx];
if (!(out_mbs[mb_idx].cbp_luma & (1 << parent_8x8))) {
out_ctx.nc[blk_idx] = 0; continue;
}
int nc = calc_block_nc(blk_idx, &out_ctx, left_ctx, above);
int16_t coeffs[16];
out_ctx.nc[blk_idx] = subcodec::cavlc::read_block(&b, coeffs, nc, 16);
}
if (out_mbs[mb_idx].cbp_chroma >= 1) {
int16_t dummy4[4];
subcodec::cavlc::read_block(&b, dummy4, -1, 4);
subcodec::cavlc::read_block(&b, dummy4, -1, 4);
}
if (out_mbs[mb_idx].cbp_chroma == 2) {
for (int i = 0; i < 8; i++) {
int16_t dummy15[15];
subcodec::cavlc::read_block(&b, dummy15, 0, 15);
}
}
}
out_ctx.mv[0] = out_mbs[mb_idx].mv_x;
out_ctx.mv[1] = out_mbs[mb_idx].mv_y;
} else if (mb_type_code == 3) {
read_mb_p8x8(&b, &out_mbs[mb_idx], 0, left_ctx, above, above_right, &out_ctx);
} else if (mb_type_code == 4) {
read_mb_p8x8(&b, &out_mbs[mb_idx], 1, left_ctx, above, above_right, &out_ctx);
} else if (mb_type_code >= 6 && mb_type_code <= 29) {
int offset = (int)mb_type_code - 6;
read_mb_i16x16(&b, &out_mbs[mb_idx], offset, left_ctx, above, &out_ctx);
} else {
fprintf(stderr, "h264_parse: P-slice bad mb_type=%u at mb_idx=%d/%d\n",
mb_type_code, mb_idx, num_mbs);
error = true;
break;
}
ctx_left = out_ctx;
ctx_row_[mb_x] = out_ctx;
mb_idx++;
if (!more_rbsp_data(&b)) {
// Remaining MBs are skip (end of slice data)
while (mb_idx < num_mbs) {
int mb_x2 = mb_idx % params.width_mbs;
out_mbs[mb_idx].mb_type = MbType::SKIP;
MbContext skip_ctx2 = {};
ctx_left = skip_ctx2;
ctx_row_[mb_x2] = skip_ctx2;
mb_idx++;
}
break;
}
}
} else if (is_i_slice) {
for (mb_idx = 0; mb_idx < num_mbs; mb_idx++) {
int mb_x = mb_idx % params.width_mbs;
int mb_y = mb_idx / params.width_mbs;
MbContext* above = (mb_y > 0) ? &ctx_row_[mb_x] : NULL;
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
if (bs_eof(&b)) {
error = true;
break;
}
uint32_t mb_type_code = bs_read_ue(&b);
MbContext out_ctx = {};
if (mb_type_code >= 1 && mb_type_code <= 24) {
int offset = (int)mb_type_code - 1;
read_mb_i16x16(&b, &out_mbs[mb_idx], offset, left_ctx, above, &out_ctx);
} else {
fprintf(stderr, "h264_parse: I-slice bad mb_type=%u at mb_idx=%d/%d\n",
mb_type_code, mb_idx, num_mbs);
error = true;
break;
}
ctx_left = out_ctx;
ctx_row_[mb_x] = out_ctx;
}
} else {
error = true;
}
if (error) {
return std::unexpected(Error::PARSE_ERROR);
}
return out_mbs;
}
} // namespace subcodec

29
third-party/subcodec/src/h264_parser.h vendored Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <span>
#include <expected>
#include <vector>
#include "types.h"
#include "error.h"
namespace subcodec {
class H264Parser {
public:
std::expected<std::vector<MacroblockData>, Error> parse_slice(
std::span<const uint8_t> nal_data,
const FrameParams& params);
std::expected<std::vector<MacroblockData>, Error> parse_slice_ex(
std::span<const uint8_t> nal_data,
const FrameParams& params,
int* out_slice_qp_delta = nullptr);
private:
std::vector<uint8_t> rbsp_buf_;
std::vector<MbContext> ctx_row_;
};
} // namespace subcodec

627
third-party/subcodec/src/mbs_encode.cpp vendored Normal file
View file

@ -0,0 +1,627 @@
#include "mbs_encode.h"
#include "cavlc.h"
#include "bs.h"
#include "tables.h"
#include "frame_writer.h"
#include "mbs_mux_common.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <tuple>
#include <vector>
namespace subcodec::mbs {
using subcodec::tables::cbp_to_code_inter;
using subcodec::tables::luma_block_order;
using subcodec::tables::block_to_8x8;
using subcodec::frame_writer::predict_mv;
/* ---- nC computation (same algorithm as mbs_mux_common) ---- */
static int blk_to_x4(int blk_idx) {
return (blk_idx & 1) | ((blk_idx >> 1) & 2);
}
static int blk_to_y4(int blk_idx) {
return ((blk_idx >> 1) & 1) | ((blk_idx >> 2) & 2);
}
static int xy4_to_blk(int x4, int y4) {
return (x4 & 1) | ((y4 & 1) << 1) | ((x4 & 2) << 1) | ((y4 & 2) << 2);
}
static int enc_calc_nc(int nc_left, int nc_above) {
if (nc_left >= 0 && nc_above >= 0) return (nc_left + nc_above + 1) >> 1;
if (nc_left >= 0) return nc_left;
if (nc_above >= 0) return nc_above;
return 0;
}
static int enc_calc_nc_luma(int blk_idx, const MbContext* cur,
const MbContext* left, const MbContext* above) {
int nc_left = -1, nc_above = -1;
int x4 = blk_to_x4(blk_idx);
int y4 = blk_to_y4(blk_idx);
if (x4 > 0) {
nc_left = cur->nc[xy4_to_blk(x4 - 1, y4)];
} else if (left) {
nc_left = left->nc[xy4_to_blk(3, y4)];
}
if (y4 > 0) {
nc_above = cur->nc[xy4_to_blk(x4, y4 - 1)];
} else if (above) {
nc_above = above->nc[xy4_to_blk(x4, 3)];
}
return enc_calc_nc(nc_left, nc_above);
}
static int enc_calc_nc_chroma(int blk_idx, const int* cur_nc,
const int* left_nc, const int* above_nc) {
int cx = blk_idx % 2, cy = blk_idx / 2;
int nc_left = -1, nc_above = -1;
if (cx > 0) nc_left = cur_nc[blk_idx - 1];
else if (left_nc) nc_left = left_nc[cy * 2 + 1];
if (cy > 0) nc_above = cur_nc[blk_idx - 2];
else if (above_nc) nc_above = above_nc[cx + 2];
return enc_calc_nc(nc_left, nc_above);
}
/* ---- Per-MB bitstream encoding into a temporary buffer ---- */
/* Compute bit count from a bs_t state */
static int bs_bit_count(bs_t* b) {
int byte_count = (int)(b->p - b->start);
int partial = (b->bits_left < 8) ? (8 - b->bits_left) : 0;
return byte_count * 8 + partial;
}
/* Byte-align a bs_t and return total bytes written */
static int bs_byte_align(bs_t* b) {
if (b->bits_left != 8) {
b->p++;
b->bits_left = 8;
}
return (int)(b->p - b->start);
}
/* Encode P_16x16 MB bitstream (exp-golomb header + CAVLC blocks) into bs_t.
* Returns 0 on success, -1 on error. Populates out_ctx. */
static int encode_mb_p16x16_bs(bs_t* b, const MacroblockData* mb,
const MbContext* left, const MbContext* above,
const MbContext* above_right,
MbContext* out_ctx) {
/* Header blob: ue(0) + se(mvd_x) + se(mvd_y) + ue(cbp_code) [+ se(qp_delta)] */
bs_write_ue(b, 0); /* mb_type = P_L0_16x16 */
int16_t mvp[2];
predict_mv(left, above, above_right, mvp);
bs_write_se(b, mb->mv_x - mvp[0]);
bs_write_se(b, mb->mv_y - mvp[1]);
int cbp = (mb->cbp_chroma << 4) | mb->cbp_luma;
bs_write_ue(b, cbp_to_code_inter[cbp]);
if (cbp != 0) {
bs_write_se(b, 0); /* qp_delta */
}
/* Initialize output context */
*out_ctx = MbContext{};
if (cbp != 0) {
/* Luma blocks — real nC from neighbor context */
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int parent_8x8 = block_to_8x8[blk_idx];
if (!(mb->cbp_luma & (1 << parent_8x8))) {
out_ctx->nc[blk_idx] = 0;
continue;
}
int16_t coeffs[16];
coeffs[0] = mb->luma_dc[blk_idx];
for (int j = 0; j < 15; j++) {
coeffs[j + 1] = mb->luma_ac[blk_idx][j];
}
int nc = enc_calc_nc_luma(blk_idx, out_ctx, left, above);
int tc = subcodec::cavlc::write_block(b, coeffs, nc, 16);
out_ctx->nc[blk_idx] = tc;
}
/* Chroma DC — canonical nC=-1 */
if (mb->cbp_chroma >= 1) {
subcodec::cavlc::write_block(b, mb->cb_dc, -1, 4);
subcodec::cavlc::write_block(b, mb->cr_dc, -1, 4);
}
/* Chroma AC — real nC from neighbor context */
if (mb->cbp_chroma == 2) {
for (int i = 0; i < 4; i++) {
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cb,
left ? left->nc_cb : nullptr,
above ? above->nc_cb : nullptr);
int tc = subcodec::cavlc::write_block(b, mb->cb_ac[i], nc, 15);
out_ctx->nc_cb[i] = tc;
}
for (int i = 0; i < 4; i++) {
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cr,
left ? left->nc_cr : nullptr,
above ? above->nc_cr : nullptr);
int tc = subcodec::cavlc::write_block(b, mb->cr_ac[i], nc, 15);
out_ctx->nc_cr[i] = tc;
}
}
}
/* Update MV in context */
out_ctx->mv[0] = mb->mv_x;
out_ctx->mv[1] = mb->mv_y;
return 0;
}
/* Encode I_16x16 MB bitstream (exp-golomb header + CAVLC blocks) into bs_t.
* Returns 0 on success, -1 on error. Populates out_ctx. */
static int encode_mb_i16x16_bs(bs_t* b, const MacroblockData* mb,
const MbContext* left, const MbContext* above,
MbContext* out_ctx) {
/* Determine if any AC block has non-zero coefficients */
int ac_has_nonzero = 0;
for (int i = 0; i < 16 && !ac_has_nonzero; i++) {
for (int j = 0; j < 15; j++) {
if (mb->luma_ac[i][j] != 0) {
ac_has_nonzero = 1;
break;
}
}
}
/* Header blob */
int mb_type = 6 + static_cast<int>(mb->intra_pred_mode) + 4 * mb->cbp_chroma + 12 * ac_has_nonzero;
bs_write_ue(b, (uint32_t)mb_type);
bs_write_ue(b, static_cast<uint32_t>(mb->intra_chroma_mode));
bs_write_se(b, 0); /* qp_delta */
/* Initialize output context */
*out_ctx = MbContext{};
/* Luma DC: real nC */
{
int nc = enc_calc_nc_luma(0, out_ctx, left, above);
subcodec::cavlc::write_block(b, mb->luma_dc, nc, 16);
}
/* Luma AC: real nC from neighbor context */
if (ac_has_nonzero) {
for (int i = 0; i < 16; i++) {
int blk_idx = luma_block_order[i];
int nc = enc_calc_nc_luma(blk_idx, out_ctx, left, above);
int tc = subcodec::cavlc::write_block(b, mb->luma_ac[blk_idx], nc, 15);
out_ctx->nc[blk_idx] = tc;
}
}
/* Chroma DC: canonical nC=-1 */
if (mb->cbp_chroma >= 1) {
subcodec::cavlc::write_block(b, mb->cb_dc, -1, 4);
subcodec::cavlc::write_block(b, mb->cr_dc, -1, 4);
}
/* Chroma AC: real nC from neighbor context */
if (mb->cbp_chroma == 2) {
for (int i = 0; i < 4; i++) {
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cb,
left ? left->nc_cb : nullptr,
above ? above->nc_cb : nullptr);
int tc = subcodec::cavlc::write_block(b, mb->cb_ac[i], nc, 15);
out_ctx->nc_cb[i] = tc;
}
for (int i = 0; i < 4; i++) {
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cr,
left ? left->nc_cr : nullptr,
above ? above->nc_cr : nullptr);
int tc = subcodec::cavlc::write_block(b, mb->cr_ac[i], nc, 15);
out_ctx->nc_cr[i] = tc;
}
}
out_ctx->mv[0] = 0;
out_ctx->mv[1] = 0;
return 0;
}
/* ---- Row blob assembly ---- */
/* Per-MB encoded data (temporary, before row assembly) */
struct MbEncoded {
bool is_skip;
int bit_count; /* bits in bitstream (0 for SKIP) */
uint8_t buf[4096]; /* bitstream data (only valid if !is_skip) */
};
/* Assemble row blob from per-MB encoded data.
* Appends [leading_skips][trailing_skips][blob_bit_count LE][blob bytes...] to out.
* For all-skip rows, appends just [leading_skips=width][trailing_skips=0][blob_bit_count=0]. */
static void assemble_row_blob(const MbEncoded* mbs, int width,
std::vector<uint8_t>& out) {
/* Find first and last non-skip */
int first_nonskip = -1, last_nonskip = -1;
for (int i = 0; i < width; i++) {
if (!mbs[i].is_skip) {
if (first_nonskip < 0) first_nonskip = i;
last_nonskip = i;
}
}
if (first_nonskip < 0) {
/* All-skip row */
out.push_back(static_cast<uint8_t>(width)); /* leading_skips */
out.push_back(0); /* trailing_skips */
uint16_t zero = 0;
out.push_back(static_cast<uint8_t>(zero & 0xFF));
out.push_back(static_cast<uint8_t>((zero >> 8) & 0xFF));
out.push_back(0); /* leading_zero_bits */
out.push_back(0); /* trailing_zero_bits */
return;
}
uint8_t leading = static_cast<uint8_t>(first_nonskip);
uint8_t trailing = static_cast<uint8_t>(width - 1 - last_nonskip);
/* Build the blob: non-skip MB bitstreams with interleaved ue(skip_count) */
/* First, compute total bits needed */
/* We need a temporary bs_t to write the blob */
/* Max size: sum of all MB bitstreams + skip run exp-golomb codes */
int total_mb_bits = 0;
for (int i = first_nonskip; i <= last_nonskip; i++) {
if (!mbs[i].is_skip) {
total_mb_bits += mbs[i].bit_count;
}
}
/* Skip run ue() codes: at most width * 32 bits each (generous) */
int max_blob_bytes = (total_mb_bits + width * 32 + 7) / 8 + 16;
std::vector<uint8_t> blob_buf(max_blob_bytes, 0);
bs_t blob;
bs_init(&blob, blob_buf.data(), blob_buf.size());
bool first_coded = true;
int skip_count = 0;
for (int i = first_nonskip; i <= last_nonskip; i++) {
if (mbs[i].is_skip) {
skip_count++;
continue;
}
if (!first_coded) {
/* Write ue(skip_count) before this non-skip MB */
bs_write_ue(&blob, (uint32_t)skip_count);
skip_count = 0;
} else {
first_coded = false;
skip_count = 0;
}
/* Copy MB bitstream into blob */
subcodec::mux::bs_copy_bits(&blob, mbs[i].buf, 0, mbs[i].bit_count);
}
int blob_bits = bs_bit_count(&blob);
auto [max_run, leading_zb, trailing_zb] = subcodec::mux::scan_zero_runs(blob_buf.data(), blob_bits);
/* Byte-align the blob */
int blob_bytes = bs_byte_align(&blob);
/* Write 6-byte row header + blob to output */
out.push_back(leading);
out.push_back(trailing);
uint16_t bbc = static_cast<uint16_t>(blob_bits);
if (max_run >= 16) bbc |= 0x8000;
out.push_back(static_cast<uint8_t>(bbc & 0xFF));
out.push_back(static_cast<uint8_t>((bbc >> 8) & 0xFF));
out.push_back(static_cast<uint8_t>(std::min(leading_zb, 255)));
out.push_back(static_cast<uint8_t>(std::min(trailing_zb, 255)));
/* Append blob bytes */
out.insert(out.end(), blob_buf.data(), blob_buf.data() + blob_bytes);
}
/* ---- Public API ---- */
MbsEncodedFrame encode_frame(
const FrameParams& params,
const MacroblockData* mbs) {
int width = params.width_mbs;
int height = params.height_mbs;
int num_mbs = width * height;
if (num_mbs <= 0 || !mbs) return {};
MbsEncodedFrame result;
result.data.reserve(4096);
result.rows.resize(height);
/* Allocate context row for nC / MV prediction */
std::vector<MbContext> ctx_row(width);
MbContext ctx_left{};
/* Temporary per-MB encoded data for one row */
std::vector<MbEncoded> row_mbs(width);
int err = 0;
for (int mb_y = 0; mb_y < height && !err; mb_y++) {
ctx_left = MbContext{};
for (int mb_x = 0; mb_x < width && !err; mb_x++) {
int mb_idx = mb_y * width + mb_x;
const MacroblockData* mb = &mbs[mb_idx];
MbContext* above = (mb_y > 0) ? &ctx_row[mb_x] : nullptr;
MbContext* left_ptr = (mb_x > 0) ? &ctx_left : nullptr;
MbContext* above_right = (mb_y > 0 && mb_x < width - 1)
? &ctx_row[mb_x + 1] : nullptr;
MbContext out_ctx{};
MbEncoded& enc = row_mbs[mb_x];
switch (mb->mb_type) {
case MbType::SKIP:
enc.is_skip = true;
enc.bit_count = 0;
break;
case MbType::P_16x16: {
enc.is_skip = false;
memset(enc.buf, 0, sizeof(enc.buf));
bs_t b;
bs_init(&b, enc.buf, sizeof(enc.buf));
err = encode_mb_p16x16_bs(&b, mb, left_ptr, above,
above_right, &out_ctx);
enc.bit_count = bs_bit_count(&b);
break;
}
case MbType::I_16x16: {
enc.is_skip = false;
memset(enc.buf, 0, sizeof(enc.buf));
bs_t b;
bs_init(&b, enc.buf, sizeof(enc.buf));
err = encode_mb_i16x16_bs(&b, mb, left_ptr, above, &out_ctx);
enc.bit_count = bs_bit_count(&b);
break;
}
default:
err = -1;
break;
}
ctx_left = out_ctx;
ctx_row[mb_x] = out_ctx;
}
if (!err) {
/* Record where this row's data starts in result.data */
size_t row_data_start = result.data.size();
/* Assemble row blob and append to result.data */
assemble_row_blob(row_mbs.data(), width, result.data);
/* Parse the row descriptor from what we just wrote */
MbsRow& row = result.rows[mb_y];
const uint8_t* rp = result.data.data() + row_data_start;
row.leading_skips = rp[0];
row.trailing_skips = rp[1];
row.blob_bit_count = static_cast<uint16_t>(rp[2]) |
(static_cast<uint16_t>(rp[3]) << 8);
row.leading_zero_bits = rp[4];
row.trailing_zero_bits = rp[5];
if (row.bit_count() > 0) {
row.blob_data = rp + 6;
} else {
row.blob_data = nullptr;
}
}
}
if (err) return {};
/* Fix up blob_data pointers (they may have been invalidated by vector growth).
* Re-parse from the final data buffer. */
{
const uint8_t* dp = result.data.data();
for (int mb_y = 0; mb_y < height; mb_y++) {
MbsRow& row = result.rows[mb_y];
row.leading_skips = dp[0];
row.trailing_skips = dp[1];
row.blob_bit_count = static_cast<uint16_t>(dp[2]) |
(static_cast<uint16_t>(dp[3]) << 8);
row.leading_zero_bits = dp[4];
row.trailing_zero_bits = dp[5];
int blob_bytes = (row.bit_count() + 7) / 8;
if (row.bit_count() > 0) {
row.blob_data = dp + 6;
} else {
row.blob_data = nullptr;
}
dp += 6 + blob_bytes;
}
}
return result;
}
/* ---- Merged row blob (local copy for encode_frame_merged) ---- */
static MbsRow merge_color_alpha_row_local(
const MbsRow& color, const MbsRow& alpha,
int sprite_w, int padding,
std::vector<uint8_t>& out_data) {
int slot_w = sprite_w * 2 - padding;
bool has_color = color.bit_count() > 0;
bool has_alpha = alpha.bit_count() > 0;
MbsRow merged;
merged.blob_data = nullptr;
if (!has_color && !has_alpha) {
merged.leading_skips = static_cast<uint8_t>(std::min(slot_w, 255));
merged.trailing_skips = 0;
merged.blob_bit_count = 0;
merged.leading_zero_bits = 0;
merged.trailing_zero_bits = 0;
return merged;
}
/* Compute merged leading/trailing skips */
int merged_leading, merged_trailing;
if (has_color) {
merged_leading = color.leading_skips;
} else {
merged_leading = (sprite_w - padding) + alpha.leading_skips;
}
if (has_alpha) {
merged_trailing = alpha.trailing_skips;
} else {
merged_trailing = color.trailing_skips + (sprite_w - padding);
}
/* Build merged blob bitstream */
int max_bits = (has_color ? color.bit_count() : 0) +
25 /* max ue bits */ +
(has_alpha ? alpha.bit_count() : 0);
int max_bytes = (max_bits + 7) / 8 + 4;
size_t blob_start = out_data.size();
out_data.resize(blob_start + max_bytes, 0);
bs_t bs;
bs_init(&bs, out_data.data() + blob_start, max_bytes);
if (has_color) {
subcodec::mux::bs_copy_bits(&bs, color.blob_data, 0, color.bit_count());
}
if (has_color && has_alpha) {
int inter_skip;
if (padding >= alpha.leading_skips) {
inter_skip = color.trailing_skips;
} else {
inter_skip = color.trailing_skips + alpha.leading_skips - padding;
}
bs_write_ue(&bs, static_cast<uint32_t>(inter_skip));
}
if (has_alpha) {
subcodec::mux::bs_copy_bits(&bs, alpha.blob_data, 0, alpha.bit_count());
}
int merged_bits = static_cast<int>(bs.p - bs.start) * 8 + (8 - bs.bits_left);
if (bs.bits_left < 8) { bs.p++; bs.bits_left = 8; }
int merged_bytes = (merged_bits + 7) / 8;
out_data.resize(blob_start + merged_bytes);
auto [max_run, leading_zb, trailing_zb] = subcodec::mux::scan_zero_runs(
out_data.data() + blob_start, merged_bits);
merged.leading_skips = static_cast<uint8_t>(std::min(merged_leading, 255));
merged.trailing_skips = static_cast<uint8_t>(std::min(merged_trailing, 255));
uint16_t bbc = static_cast<uint16_t>(merged_bits & 0x7FFF);
if (max_run >= 16) bbc |= 0x8000;
merged.blob_bit_count = bbc;
merged.leading_zero_bits = static_cast<uint8_t>(std::min(leading_zb, 255));
merged.trailing_zero_bits = static_cast<uint8_t>(std::min(trailing_zb, 255));
merged.blob_data = out_data.data() + blob_start;
return merged;
}
/* ---- encode_frame_merged ---- */
MbsEncodedFrame encode_frame_merged(
const FrameParams& color_params, const MacroblockData* color_mbs,
const FrameParams& alpha_params, const MacroblockData* alpha_mbs,
int sprite_w, int padding) {
// Encode color and alpha halves separately
auto color_ef = encode_frame(color_params, color_mbs);
if (color_ef.data.empty()) return {};
auto alpha_ef = encode_frame(alpha_params, alpha_mbs);
if (alpha_ef.data.empty()) return {};
int height = color_params.height_mbs;
// Merge each row pair
MbsEncodedFrame result;
result.rows.resize(height);
std::vector<uint8_t> merged_data;
merged_data.reserve(color_ef.data.size() + alpha_ef.data.size());
std::vector<size_t> blob_offsets(height, SIZE_MAX);
for (int y = 0; y < height; y++) {
size_t offset_before = merged_data.size();
MbsRow merged = merge_color_alpha_row_local(
color_ef.rows[y], alpha_ef.rows[y],
sprite_w, padding, merged_data);
result.rows[y] = merged;
if (merged.blob_data) {
blob_offsets[y] = offset_before;
}
}
// Consolidate blob data and fix up pointers
result.data = std::move(merged_data);
for (int y = 0; y < height; y++) {
if (blob_offsets[y] != SIZE_MAX) {
result.rows[y].blob_data = result.data.data() + blob_offsets[y];
}
}
// Write serialized form: 6-byte header + blob bytes per row
std::vector<uint8_t> serialized;
serialized.reserve(result.data.size() + height * 6);
for (int y = 0; y < height; y++) {
const MbsRow& row = result.rows[y];
serialized.push_back(row.leading_skips);
serialized.push_back(row.trailing_skips);
serialized.push_back(static_cast<uint8_t>(row.blob_bit_count & 0xFF));
serialized.push_back(static_cast<uint8_t>((row.blob_bit_count >> 8) & 0xFF));
serialized.push_back(row.leading_zero_bits);
serialized.push_back(row.trailing_zero_bits);
int blob_bytes = (row.bit_count() + 7) / 8;
if (blob_bytes > 0 && row.blob_data) {
serialized.insert(serialized.end(), row.blob_data, row.blob_data + blob_bytes);
}
}
// Replace data with serialized form and re-parse row pointers
result.data = std::move(serialized);
const uint8_t* dp = result.data.data();
for (int y = 0; y < height; y++) {
MbsRow& row = result.rows[y];
row.leading_skips = dp[0];
row.trailing_skips = dp[1];
row.blob_bit_count = static_cast<uint16_t>(dp[2]) | (static_cast<uint16_t>(dp[3]) << 8);
row.leading_zero_bits = dp[4];
row.trailing_zero_bits = dp[5];
int blob_bytes = (row.bit_count() + 7) / 8;
row.blob_data = (row.bit_count() > 0) ? dp + 6 : nullptr;
dp += 6 + blob_bytes;
}
return result;
}
} // namespace subcodec::mbs

25
third-party/subcodec/src/mbs_encode.h vendored Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <vector>
#include "types.h"
#include "mbs_format.h"
namespace subcodec::mbs {
MbsEncodedFrame encode_frame(
const FrameParams& params,
const MacroblockData* mbs);
// Encode color and alpha halves into a single frame with pre-merged row blobs.
// color_params/color_mbs: color half MacroblockData (padded sprite dimensions).
// alpha_params/alpha_mbs: alpha half MacroblockData (same dimensions).
// sprite_w: padded sprite width in MBs. padding: always 1.
// Returns MbsEncodedFrame with merged rows (no separate alpha_rows).
MbsEncodedFrame encode_frame_merged(
const FrameParams& color_params, const MacroblockData* color_mbs,
const FrameParams& alpha_params, const MacroblockData* alpha_mbs,
int sprite_w, int padding);
} // namespace subcodec::mbs

9
third-party/subcodec/src/mbs_format.h vendored Normal file
View file

@ -0,0 +1,9 @@
#pragma once
#include "types.h"
#include <cstdint>
// .mbs binary format constants (MB type codes in serialized frame data)
inline constexpr uint32_t MBS_MAGIC_V6 = 0x3653424D; // 'MBS6' — pre-merged blobs
inline constexpr int MBS_MB_SKIP = 0;
inline constexpr int MBS_MB_P16x16 = 1;
inline constexpr int MBS_MB_I16x16 = 2;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,398 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <span>
#include <expected>
#include <vector>
#include <tuple>
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
#include <arm_neon.h>
#endif
#include "types.h"
#include "error.h"
#include "bs.h"
#include "mbs_format.h"
#include "cavlc.h"
namespace subcodec::mux {
/* ---- Exp-golomb LUT ---- */
struct UeEntry {
uint32_t pattern; /* bit pattern, MSB-first */
uint8_t len; /* number of bits (max 25 for values up to 4095) */
};
static constexpr int UE_LUT_SIZE = 4096;
extern UeEntry ue_lut[UE_LUT_SIZE];
void build_ue_lut();
/* ---- EbspWriter: single-pass direct EBSP output ---- */
/* Writes bits directly to an output buffer with inline EBSP escape byte
* insertion (0x00 0x00 [0x00-0x03] 0x00 0x00 0x03 [0x00-0x03]).
* Replaces the two-stage bs_tRBSP + rbsp_to_ebsp pipeline. */
struct EbspWriter {
uint8_t* out; /* next complete-byte write position */
uint32_t partial; /* accumulated bits not yet flushed (MSB-first) */
int bits; /* number of valid bits in partial (0-7) */
int zero_count; /* consecutive 0x00 bytes written (for EBSP escaping) */
/* Write one complete byte with EBSP escape check. */
inline void flush_byte(uint8_t byte) {
if (zero_count >= 2 && byte <= 3) {
*out++ = 0x03;
zero_count = 0;
}
*out++ = byte;
zero_count = (byte == 0) ? zero_count + 1 : 0;
}
/* Accumulate n bits (max 25) and flush complete bytes.
* val must have its bits in the low n positions. */
inline void write_bits(uint32_t val, int n) {
partial = (partial << n) | val;
bits += n;
while (bits >= 8) {
bits -= 8;
flush_byte(static_cast<uint8_t>((partial >> bits) & 0xFF));
}
}
/* Write unsigned exp-golomb code. Uses LUT for values < 4096,
* falls back to computed encoding for larger values. */
inline void write_ue(uint32_t val) {
if (val < UE_LUT_SIZE) {
const auto& e = ue_lut[val];
write_bits(e.pattern, e.len);
} else {
/* Fallback: compute exp-golomb encoding */
uint32_t v = val + 1;
int len = 0;
uint32_t tmp = v;
while (tmp > 0) { tmp >>= 1; len++; }
write_bits(v, 2 * len - 1);
}
}
/* Write signed exp-golomb code. */
inline void write_se(int32_t val) {
if (val <= 0)
write_ue(static_cast<uint32_t>(-val * 2));
else
write_ue(static_cast<uint32_t>(val * 2 - 1));
}
/* Bulk write complete bytes with EBSP escaping.
* Requires bits == 0 (byte-aligned). Uses NEON to detect zero bytes
* and bulk-copy safe regions. For I_PCM pixel data. */
void flush_bytes(const uint8_t* src, int nbytes);
/* Bulk copy blob bits into output with inline EBSP escaping.
* src is a byte array; nbits bits starting from bit 0 are copied.
* Handles both aligned (bits == 0) and non-aligned cases. */
void copy_blob(const uint8_t* src, int nbits);
/* Bulk copy blob bits with fast path when blob has no 16+ consecutive zero bits.
* has_long_zero_run: if false, interior bytes are guaranteed EBSP-safe.
* leading_zero_bits/trailing_zero_bits: for boundary handling. */
void copy_blob(const uint8_t* src, int nbits,
bool has_long_zero_run, uint8_t leading_zero_bits,
uint8_t trailing_zero_bits);
};
/* ---- RbspWriter: branchless bitstream writer for RBSP staging ---- */
/* No EBSP escape checking — caller must run rbsp_to_ebsp after.
* Used in the two-pass P-frame mux path. */
struct RbspWriter {
uint8_t* out; /* next complete-byte write position */
uint32_t partial; /* accumulated bits not yet flushed (MSB-first) */
int bits; /* number of valid bits in partial (0-7) */
inline void write_bits(uint32_t val, int n) {
partial = (partial << n) | val;
bits += n;
while (bits >= 8) {
bits -= 8;
*out++ = static_cast<uint8_t>((partial >> bits) & 0xFF);
}
}
inline void write_ue(uint32_t val) {
if (val < UE_LUT_SIZE) {
const auto& e = ue_lut[val];
write_bits(e.pattern, e.len);
} else {
uint32_t v = val + 1;
int len = 0;
uint32_t tmp = v;
while (tmp > 0) { tmp >>= 1; len++; }
write_bits(v, 2 * len - 1);
}
}
inline void write_se(int32_t val) {
if (val <= 0) write_ue(static_cast<uint32_t>(-val * 2));
else write_ue(static_cast<uint32_t>(val * 2 - 1));
}
/* Bulk copy blob bits — no EBSP checking.
* Aligned: memcpy. Non-aligned: NEON shift+write. */
void copy_blob(const uint8_t* src, int nbits);
};
/* ---- Coeff_token LUT ---- */
void build_ct_lut();
/* ---- Grid layout helpers ---- */
int ceil_div(int a, int b);
int ceil_sqrt(int n);
/* ---- Row plan types (precomputed composite layout) ---- */
struct RowOp {
uint16_t slot_idx; /* which slot this sprite is in */
uint16_t sprite_row; /* which row within the sprite */
uint16_t pre_skip; /* composite skip MBs from previous sprite-region end
(or row start) to this sprite-region start.
Clamped to >= 0. Fixed for a given layout. */
uint16_t overlap; /* MBs at start of this sprite's region already covered
by the previous sprite (shared padding). The mux loop
uses this as `already_inside` same role as in the
current grid walk code. 0 for the first sprite in a row
or when there's a gap between sprites. */
};
struct CompositeRowPlan {
uint16_t trailing_skips; /* composite skip MBs after last sprite-region end */
uint16_t ops_offset; /* index into flat ops array */
uint16_t ops_count; /* number of RowOps in this row */
};
/* Build precomputed row plans from slot active state.
* Called from MuxSurface::add_sprite / remove_sprite. */
void build_row_plans(
const bool* slot_active, int max_slots,
int sprite_w, int sprite_h, int padding,
int total_w, int total_h,
std::vector<CompositeRowPlan>& row_plans,
std::vector<RowOp>& row_ops);
/* Pre-resolved blob operation for the tight mux loop.
* Built once per frame from row_ops + slot state.
* Only active blobs with data appear inactive slots and
* all-skip rows are folded into skip counts. */
struct MicroOp {
const uint8_t* blob_data;
uint16_t blob_bits; /* bit count (lower 15 bits of blob_bit_count) */
uint16_t skip; /* composite skip MBs to write before this blob */
uint8_t flags; /* [0] = has_long_zero_run */
uint8_t leading_zb;
uint8_t trailing_zb;
uint8_t _pad;
};
/* ---- Zero-run scanning ---- */
/* Scan blob bits for zero-run metadata.
* Returns: {max_consecutive_zero_bits, leading_zero_bits, trailing_zero_bits} */
std::tuple<int, int, int> scan_zero_runs(const uint8_t* blob, int blob_bits);
/* ---- Bit writing helpers ---- */
/* Bulk copy nbits from src byte array at src_bit_offset into dst bs_t.
* Uses byte-level operations where possible for speed. */
void bs_copy_bits(bs_t* dst, const uint8_t* src, int src_bit_offset, int nbits);
/* ---- RBSP to EBSP ---- */
size_t rbsp_to_ebsp(const uint8_t* rbsp, size_t rbsp_size,
uint8_t* ebsp, size_t ebsp_size);
/* NEON-accelerated EBSP escape insertion.
* Scans 16 bytes at a time for zero bytes; bulk-copies safe regions.
* Returns output byte count, 0 on error. */
size_t rbsp_to_ebsp_neon(const uint8_t* rbsp, size_t rbsp_size,
uint8_t* ebsp, size_t ebsp_size);
/* ---- Frame writers ---- */
/* Write all-black IDR frame (I_16x16 DC prediction for every MB). */
std::expected<size_t, Error> write_idr_black(
int total_w, int total_h,
int8_t qp_delta_idr, int log2_max_frame_num,
std::span<uint8_t> output);
/* Write all-I_PCM IDR frame from caller-provided decoded YUV planes.
* Every MB is I_PCM (384 raw bytes: 256 luma + 64 Cb + 64 Cr).
* Inline: enables cross-TU inlining into MuxSurface::resize at -O2. */
inline std::expected<size_t, Error> write_idr_ipcm(
int total_w, int total_h,
int log2_max_frame_num,
const uint8_t* y_plane, int stride_y,
const uint8_t* cb_plane, int stride_cb,
const uint8_t* cr_plane, int stride_cr,
std::span<uint8_t> output) {
int num_mbs = total_w * total_h;
size_t needed = static_cast<size_t>(num_mbs) * 580 + 4096;
if (output.size() < needed)
return std::unexpected(Error::OUT_OF_SPACE);
uint8_t* buf = output.data();
/* NAL header: nal_ref_idc=3, nal_unit_type=5 (IDR) */
buf[0] = 0x00; buf[1] = 0x00; buf[2] = 0x00; buf[3] = 0x01;
buf[4] = (3 << 5) | 5;
/* Single-pass: write directly to EBSP output via EbspWriter. */
EbspWriter w;
w.out = buf + 5;
w.partial = 0;
w.bits = 0;
w.zero_count = 0;
/* Slice header */
w.write_ue(0); /* first_mb_in_slice */
w.write_ue(7); /* slice_type = I */
w.write_ue(0); /* pps_id */
w.write_bits(0, log2_max_frame_num); /* frame_num = 0 */
w.write_ue(0); /* idr_pic_id */
w.write_bits(0, 1); /* no_output_of_prior_pics_flag */
w.write_bits(0, 1); /* long_term_reference_flag */
w.write_se(0); /* slice_qp_delta = 0 */
w.write_ue(1); /* disable_deblocking_filter_idc */
/* Precompute EBSP-escaped all-zero luma pattern (256 input zeros → 384 output bytes).
* After mb_type + alignment, zero_count == 1 (from alignment byte 0x00).
* Each pair of input zeros produces [0x00, 0x03, 0x00] (3 bytes). */
uint8_t black_luma_ebsp[384];
for (int i = 0; i < 128; i++) {
black_luma_ebsp[i * 3 + 0] = 0x00;
black_luma_ebsp[i * 3 + 1] = 0x03;
black_luma_ebsp[i * 3 + 2] = 0x00;
}
uint8_t neutral_chroma[128];
memset(neutral_chroma, 0x80, 128);
/* Write MBs — fast path for all-black MBs (Y=0, Cb=Cr=128). */
for (int mb_idx = 0; mb_idx < num_mbs; mb_idx++) {
int mb_x = mb_idx % total_w;
int mb_y = mb_idx / total_w;
w.write_ue(25); /* mb_type = I_PCM */
if (w.bits > 0) {
w.write_bits(0, 8 - w.bits); /* byte-align */
}
int y_base = mb_y * 16;
int x_base = mb_x * 16;
int cb_y_base = mb_y * 8;
int cb_x_base = mb_x * 8;
bool is_black = true;
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
{
uint8x16_t vzero = vdupq_n_u8(0);
uint8x16_t v128 = vdupq_n_u8(128);
for (int row = 0; row < 16 && is_black; row++) {
uint8x16_t v = vld1q_u8(y_plane + (y_base + row) * stride_y + x_base);
if (vmaxvq_u8(v) != 0) is_black = false;
}
for (int row = 0; row < 8 && is_black; row++) {
uint8x8_t v = vld1_u8(cb_plane + (cb_y_base + row) * stride_cb + cb_x_base);
uint8x8_t cmp = vceq_u8(v, vget_low_u8(v128));
if (vminv_u8(cmp) == 0) is_black = false;
}
for (int row = 0; row < 8 && is_black; row++) {
uint8x8_t v = vld1_u8(cr_plane + (cb_y_base + row) * stride_cr + cb_x_base);
uint8x8_t cmp = vceq_u8(v, vget_low_u8(v128));
if (vminv_u8(cmp) == 0) is_black = false;
}
}
#else
if (y_plane[y_base * stride_y + x_base] != 0) is_black = false;
if (is_black && cb_plane[cb_y_base * stride_cb + cb_x_base] != 128) is_black = false;
#endif
if (is_black) {
memcpy(w.out, black_luma_ebsp, 384);
w.out += 384;
memcpy(w.out, neutral_chroma, 128);
w.out += 128;
w.zero_count = 0;
} else {
/* Gather MB samples into contiguous buffer for bulk EBSP processing.
* 256 luma + 64 Cb + 64 Cr = 384 bytes flush_bytes processes
* 24 NEON chunks (16 bytes each) instead of 384 scalar flush_byte calls. */
uint8_t mb_buf[384];
uint8_t* dst = mb_buf;
for (int row = 0; row < 16; row++) {
memcpy(dst, y_plane + (y_base + row) * stride_y + x_base, 16);
dst += 16;
}
for (int row = 0; row < 8; row++) {
memcpy(dst, cb_plane + (cb_y_base + row) * stride_cb + cb_x_base, 8);
dst += 8;
}
for (int row = 0; row < 8; row++) {
memcpy(dst, cr_plane + (cb_y_base + row) * stride_cr + cb_x_base, 8);
dst += 8;
}
w.flush_bytes(mb_buf, 384);
}
}
w.write_bits(1, 1); /* RBSP trailing bits */
if (w.bits > 0) {
w.write_bits(0, 8 - w.bits);
}
return static_cast<size_t>(w.out - buf);
}
/* Per-slot info needed by the row-blob mux path */
struct SlotInfo {
const MbsSprite* sprite = nullptr;
int frame_index = 0;
};
/* Build flat micro-op array from row plans and current frame state.
* Returns trailing skip count (MBs after the last blob). */
int build_micro_ops(
const SlotInfo* slots,
const CompositeRowPlan* row_plans, int num_rows,
const RowOp* row_ops,
int sprite_w, int padding,
std::vector<MicroOp>& ops);
/* Two-pass P-frame writer using pre-resolved micro-ops.
* Pass 1: write to RBSP staging buffer via RbspWriter (no EBSP checking).
* Pass 2: NEON-accelerated EBSP escape insertion.
* micro_ops/trailing_skip: from build_micro_ops().
* rbsp_buf: staging buffer (caller-owned, at least output.size() bytes). */
std::expected<size_t, Error> write_p_frame_rbsp(
const MicroOp* micro_ops, int num_ops, int trailing_skip,
int frame_idx, int log2_max_frame_num,
int8_t qp_delta_p,
std::span<uint8_t> rbsp_buf,
std::span<uint8_t> output);
/* Single-pass P-frame writer using pre-resolved micro-ops + EbspWriter.
* Uses EbspWriter's inline EBSP escaping with fast-path for escape-free blobs.
* micro_ops/trailing_skip: from build_micro_ops(). */
std::expected<size_t, Error> write_p_frame_micro(
const MicroOp* micro_ops, int num_ops, int trailing_skip,
int frame_idx, int log2_max_frame_num,
int8_t qp_delta_p,
std::span<uint8_t> output);
} // namespace subcodec::mux

486
third-party/subcodec/src/mux_surface.cpp vendored Normal file
View file

@ -0,0 +1,486 @@
#include "mux_surface.h"
#include "frame_writer.h"
#include <algorithm>
#include <cstring>
namespace subcodec {
std::expected<MuxSurface, Error>
MuxSurface::create(const Params& params, FrameSink sink) {
if (params.max_slots <= 0) return std::unexpected(Error::INVALID_INPUT);
if (params.sprite_width <= 0 || params.sprite_width % 16 != 0 ||
params.sprite_height <= 0 || params.sprite_height % 16 != 0)
return std::unexpected(Error::INVALID_INPUT);
constexpr int padding_mbs = 1;
int content_w_mbs = params.sprite_width / 16;
int content_h_mbs = params.sprite_height / 16;
int sw = content_w_mbs + 2 * padding_mbs;
int sh = content_h_mbs + 2 * padding_mbs;
int slot_w = sw * 2 - padding_mbs;
int stride_x = slot_w - padding_mbs;
int stride_y = sh - padding_mbs;
mux::build_ct_lut();
mux::build_ue_lut();
int cols = mux::ceil_sqrt(params.max_slots);
int rows = mux::ceil_div(params.max_slots, cols);
int total_w = stride_x * cols + padding_mbs;
int total_h = stride_y * rows + padding_mbs;
int num_mbs = total_w * total_h;
MuxSurface s;
s.params_ = params;
s.sprite_w_mbs_ = sw;
s.sprite_h_mbs_ = sh;
s.content_w_ = params.sprite_width;
s.content_h_ = params.sprite_height;
s.total_w_ = total_w;
s.total_h_ = total_h;
s.cols_ = cols;
s.rows_ = rows;
s.stride_x_ = stride_x;
s.stride_y_ = stride_y;
s.frame_num_ = 0;
s.num_mbs_ = num_mbs;
s.slots_.resize(static_cast<size_t>(params.max_slots));
s.slot_infos_.resize(static_cast<size_t>(params.max_slots));
s.buf_size_ = static_cast<size_t>(num_mbs) * 600 + 4096;
s.buf_ = std::make_unique_for_overwrite<uint8_t[]>(s.buf_size_);
s.rbsp_buf_size_ = s.buf_size_;
s.rbsp_buf_ = std::make_unique_for_overwrite<uint8_t[]>(s.rbsp_buf_size_);
s.micro_ops_.reserve(static_cast<size_t>(params.max_slots) * static_cast<size_t>(sh) * 2);
FrameParams fp{};
fp.width_mbs = static_cast<uint16_t>(total_w);
fp.height_mbs = static_cast<uint16_t>(total_h);
fp.qp = params.qp;
fp.log2_max_frame_num = static_cast<uint8_t>(s.log2_max_frame_num_);
std::span<uint8_t> out{s.buf_.get(), s.buf_size_};
size_t offset = frame_writer::write_headers(out, fp);
if (offset == 0) return std::unexpected(Error::OUT_OF_SPACE);
auto idr_result = mux::write_idr_black(total_w, total_h,
params.qp_delta_idr,
s.log2_max_frame_num_,
out.subspan(offset));
if (!idr_result) return std::unexpected(idr_result.error());
offset += *idr_result;
sink(std::span<const uint8_t>{s.buf_.get(), offset});
return std::move(s);
}
std::expected<MuxSurface::SpriteRegion, Error> MuxSurface::add_sprite(const std::filesystem::path& mbs_path) {
int slot_idx = -1;
for (int i = 0; i < params_.max_slots; i++) {
if (!slots_[i].sprite) {
slot_idx = i;
break;
}
}
if (slot_idx < 0) return std::unexpected(Error::OUT_OF_SPACE);
auto result = MbsSprite::load(mbs_path);
if (!result) return std::unexpected(result.error());
if (result->width_mbs != sprite_w_mbs_ ||
result->height_mbs != sprite_h_mbs_) {
return std::unexpected(Error::INVALID_INPUT);
}
slots_[slot_idx].sprite = std::move(*result);
slots_[slot_idx].current_frame = 0;
slots_[slot_idx].active = true;
slots_[slot_idx].needs_emit = true;
plans_dirty_ = true;
dirty_ = true;
constexpr int padding_px = 16;
int col = slot_idx % cols_;
int row = slot_idx / cols_;
int color_x = col * stride_x_ * 16 + padding_px;
int color_y = row * stride_y_ * 16 + padding_px;
int content_w_mbs = sprite_w_mbs_ - 2;
int alpha_x = color_x + (content_w_mbs + 1) * 16;
SpriteRegion region;
region.slot = slot_idx;
region.color = {color_x, color_y, content_w_, content_h_};
region.alpha = {alpha_x, color_y, content_w_, content_h_};
return region;
}
std::expected<MuxSurface::SpriteRegion, Error> MuxSurface::add_sprite(MbsSprite sprite) {
int slot_idx = -1;
for (int i = 0; i < params_.max_slots; i++) {
if (!slots_[i].sprite) {
slot_idx = i;
break;
}
}
if (slot_idx < 0) return std::unexpected(Error::OUT_OF_SPACE);
if (sprite.width_mbs != sprite_w_mbs_ ||
sprite.height_mbs != sprite_h_mbs_) {
return std::unexpected(Error::INVALID_INPUT);
}
slots_[slot_idx].sprite = std::move(sprite);
slots_[slot_idx].current_frame = 0;
slots_[slot_idx].active = true;
slots_[slot_idx].needs_emit = true;
plans_dirty_ = true;
dirty_ = true;
constexpr int padding_px = 16;
int col = slot_idx % cols_;
int row = slot_idx / cols_;
int color_x = col * stride_x_ * 16 + padding_px;
int color_y = row * stride_y_ * 16 + padding_px;
int content_w_mbs = sprite_w_mbs_ - 2;
int alpha_x = color_x + (content_w_mbs + 1) * 16;
SpriteRegion region;
region.slot = slot_idx;
region.color = {color_x, color_y, content_w_, content_h_};
region.alpha = {alpha_x, color_y, content_w_, content_h_};
return region;
}
void MuxSurface::remove_sprite(int slot) {
if (slot < 0 || slot >= params_.max_slots) return;
slots_[slot].sprite.reset();
slots_[slot].active = false;
slots_[slot].current_frame = 0;
plans_dirty_ = true;
}
CompactionInfo MuxSurface::check_compaction_opportunity() const {
int active = 0;
for (int i = 0; i < params_.max_slots; i++) {
if (slots_[i].active) active++;
}
int min_grid_mbs = 0;
if (active > 0) {
int cols = mux::ceil_sqrt(active);
int rows = mux::ceil_div(active, cols);
int slot_w = sprite_w_mbs_ * 2 - 1; /* padding_mbs = 1 */
int sx = slot_w - 1;
int sy = sprite_h_mbs_ - 1;
int tw = sx * cols + 1;
int th = sy * rows + 1;
min_grid_mbs = tw * th;
}
return CompactionInfo{
active,
params_.max_slots,
total_w_ * total_h_,
min_grid_mbs
};
}
std::expected<MuxSurface::ResizeResult, Error> MuxSurface::resize(
int new_max_slots,
std::span<const uint8_t> decoded_y,
std::span<const uint8_t> decoded_cb,
std::span<const uint8_t> decoded_cr,
int decoded_width,
int decoded_height,
int stride_y,
int stride_cb,
int stride_cr,
FrameSink sink) {
/* Count active sprites */
int active_count = 0;
for (int i = 0; i < params_.max_slots; i++) {
if (slots_[i].active) active_count++;
}
/* Validate */
if (new_max_slots < active_count)
return std::unexpected(Error::INVALID_INPUT);
if (decoded_width != total_w_ * 16 || decoded_height != total_h_ * 16)
return std::unexpected(Error::INVALID_INPUT);
/* Collect active sprites with their old slot positions */
struct SpriteInfo {
MbsSprite sprite;
int current_frame;
int old_col, old_row;
};
std::vector<SpriteInfo> active_sprites;
active_sprites.reserve(active_count);
for (int i = 0; i < params_.max_slots; i++) {
if (slots_[i].active && slots_[i].sprite) {
int col = i % cols_;
int row = i / cols_;
active_sprites.push_back({
std::move(*slots_[i].sprite),
slots_[i].current_frame,
col, row
});
}
}
/* Save old grid layout for pixel remapping */
int old_stride_x = stride_x_;
int old_stride_y_val = stride_y_;
/* Compute new grid layout (same algorithm as create()) */
constexpr int padding_mbs = 1;
int slot_w = sprite_w_mbs_ * 2 - padding_mbs;
int new_stride_x = slot_w - padding_mbs;
int new_stride_y_val = sprite_h_mbs_ - padding_mbs;
int new_cols = mux::ceil_sqrt(new_max_slots);
int new_rows = mux::ceil_div(new_max_slots, new_cols);
int new_total_w = new_stride_x * new_cols + padding_mbs;
int new_total_h = new_stride_y_val * new_rows + padding_mbs;
int new_num_mbs = new_total_w * new_total_h;
/* Build the remapped YUV planes for the new grid */
int new_w_px = new_total_w * 16;
int new_h_px = new_total_h * 16;
int new_cw = new_w_px / 2;
int new_ch = new_h_px / 2;
size_t y_size = static_cast<size_t>(new_w_px) * new_h_px;
size_t c_size = static_cast<size_t>(new_cw) * new_ch;
auto new_y_buf = std::make_unique_for_overwrite<uint8_t[]>(y_size);
auto new_cb_buf = std::make_unique_for_overwrite<uint8_t[]>(c_size);
auto new_cr_buf = std::make_unique_for_overwrite<uint8_t[]>(c_size);
memset(new_y_buf.get(), 0, y_size);
memset(new_cb_buf.get(), 128, c_size);
memset(new_cr_buf.get(), 128, c_size);
uint8_t* new_y = new_y_buf.get();
uint8_t* new_cb = new_cb_buf.get();
uint8_t* new_cr = new_cr_buf.get();
/* Copy sprite pixel regions from old to new positions */
int sprite_h_px = sprite_h_mbs_ * 16;
int slot_w_px = slot_w * 16;
for (int si = 0; si < (int)active_sprites.size(); si++) {
auto& sp = active_sprites[si];
int old_x_px = sp.old_col * old_stride_x * 16;
int old_y_px = sp.old_row * old_stride_y_val * 16;
int new_col = si % new_cols;
int new_row = si / new_cols;
int new_x_px = new_col * new_stride_x * 16;
int new_y_px = new_row * new_stride_y_val * 16;
/* Copy luma */
for (int r = 0; r < sprite_h_px; r++) {
const uint8_t* src = decoded_y.data() + (old_y_px + r) * stride_y + old_x_px;
uint8_t* dst = new_y + (new_y_px + r) * new_w_px + new_x_px;
memcpy(dst, src, slot_w_px);
}
/* Copy chroma */
int old_cx = old_x_px / 2;
int old_cy = old_y_px / 2;
int new_cx = new_x_px / 2;
int new_cy = new_y_px / 2;
int slot_cw = slot_w_px / 2;
int sprite_ch = sprite_h_px / 2;
for (int r = 0; r < sprite_ch; r++) {
memcpy(new_cb + (new_cy + r) * new_cw + new_cx,
decoded_cb.data() + (old_cy + r) * stride_cb + old_cx, slot_cw);
memcpy(new_cr + (new_cy + r) * new_cw + new_cx,
decoded_cr.data() + (old_cy + r) * stride_cr + old_cx, slot_cw);
}
}
/* Reallocate frame buffers if new grid is larger.
* buf_ at 600 bytes/MB is sufficient for both I_PCM (580 bytes/MB)
* and subsequent P-frames. rbsp_buf_ is used for I_PCM output. */
size_t new_buf_size = static_cast<size_t>(new_num_mbs) * 600 + 4096;
if (new_buf_size > buf_size_) {
buf_size_ = new_buf_size;
buf_ = std::make_unique_for_overwrite<uint8_t[]>(buf_size_);
rbsp_buf_size_ = buf_size_;
rbsp_buf_ = std::make_unique_for_overwrite<uint8_t[]>(rbsp_buf_size_);
}
/* Write new SPS/PPS into buf_ */
FrameParams fp{};
fp.width_mbs = static_cast<uint16_t>(new_total_w);
fp.height_mbs = static_cast<uint16_t>(new_total_h);
fp.qp = params_.qp;
fp.log2_max_frame_num = static_cast<uint8_t>(log2_max_frame_num_);
std::span<uint8_t> hdr_out{buf_.get(), buf_size_};
size_t hdr_offset = frame_writer::write_headers(hdr_out, fp);
if (hdr_offset == 0) return std::unexpected(Error::OUT_OF_SPACE);
/* Write I_PCM IDR into rbsp_buf_ (reused, avoids separate allocation) */
auto idr_result = mux::write_idr_ipcm(
new_total_w, new_total_h,
log2_max_frame_num_,
new_y, new_w_px,
new_cb, new_cw,
new_cr, new_cw,
{rbsp_buf_.get(), rbsp_buf_size_});
if (!idr_result) return std::unexpected(idr_result.error());
/* Emit SPS+PPS then I_PCM IDR */
sink(std::span<const uint8_t>{buf_.get(), hdr_offset});
sink(std::span<const uint8_t>{rbsp_buf_.get(), *idr_result});
/* Update internal state */
params_.max_slots = new_max_slots;
total_w_ = new_total_w;
total_h_ = new_total_h;
cols_ = new_cols;
rows_ = new_rows;
stride_x_ = new_stride_x;
stride_y_ = new_stride_y_val;
num_mbs_ = new_num_mbs;
frame_num_ = 0;
/* Resize slot arrays */
slots_.clear();
slots_.resize(static_cast<size_t>(new_max_slots));
slot_infos_.resize(static_cast<size_t>(new_max_slots));
micro_ops_.reserve(static_cast<size_t>(new_max_slots) *
static_cast<size_t>(sprite_h_mbs_) * 2);
/* Assign sprites to compacted slots and build result */
ResizeResult result;
result.regions.reserve(active_sprites.size());
constexpr int padding_px = 16;
int content_w_mbs = sprite_w_mbs_ - 2;
for (int si = 0; si < (int)active_sprites.size(); si++) {
slots_[si].sprite = std::move(active_sprites[si].sprite);
slots_[si].current_frame = active_sprites[si].current_frame;
slots_[si].active = true;
int col = si % new_cols;
int row = si / new_cols;
int color_x = col * new_stride_x * 16 + padding_px;
int color_y = row * new_stride_y_val * 16 + padding_px;
int alpha_x = color_x + (content_w_mbs + 1) * 16;
SpriteRegion region;
region.slot = si;
region.color = {color_x, color_y, content_w_, content_h_};
region.alpha = {alpha_x, color_y, content_w_, content_h_};
result.regions.push_back(region);
}
plans_dirty_ = true;
return result;
}
void MuxSurface::rebuild_row_plans_() {
bool active_buf[4096];
bool* active = (params_.max_slots <= 4096) ? active_buf
: new bool[static_cast<size_t>(params_.max_slots)];
for (int i = 0; i < params_.max_slots; i++) {
active[i] = slots_[i].active;
}
constexpr int padding_mbs = 1;
mux::build_row_plans(active, params_.max_slots,
sprite_w_mbs_, sprite_h_mbs_,
padding_mbs,
total_w_, total_h_,
row_plans_, row_ops_);
if (active != active_buf) delete[] active;
}
void MuxSurface::advance_sprite(int slot) {
if (slot < 0 || slot >= params_.max_slots) return;
auto& sl = slots_[slot];
if (!sl.active || !sl.sprite) return;
sl.needs_emit = true;
dirty_ = true;
}
std::expected<bool, Error> MuxSurface::emit_frame_if_needed(FrameSink sink) {
if (!dirty_) return false;
if (plans_dirty_) {
rebuild_row_plans_();
plans_dirty_ = false;
}
frame_num_++;
int max_slots = params_.max_slots;
for (int slot = 0; slot < max_slots; slot++) {
auto& sl = slots_[slot];
if (sl.active && sl.sprite && sl.needs_emit) {
slot_infos_[slot].sprite = &(*sl.sprite);
slot_infos_[slot].frame_index = sl.current_frame;
} else {
slot_infos_[slot].sprite = nullptr;
slot_infos_[slot].frame_index = 0;
}
}
int trailing_skip = mux::build_micro_ops(
slot_infos_.data(),
row_plans_.data(), total_h_,
row_ops_.data(),
sprite_w_mbs_, 1,
micro_ops_);
std::span<uint8_t> out{buf_.get(), buf_size_};
auto p_result = mux::write_p_frame_micro(
micro_ops_.data(), static_cast<int>(micro_ops_.size()), trailing_skip,
frame_num_,
log2_max_frame_num_,
params_.qp_delta_p,
out);
if (!p_result) return std::unexpected(p_result.error());
sink(std::span<const uint8_t>{buf_.get(), *p_result});
// Advance emitted sprites and clear needs_emit
for (int slot = 0; slot < max_slots; slot++) {
auto& sl = slots_[slot];
if (sl.needs_emit && sl.active && sl.sprite) {
sl.current_frame++;
if (sl.current_frame >= sl.sprite->num_frames)
sl.current_frame = 0;
}
sl.needs_emit = false;
}
dirty_ = false;
return true;
}
std::expected<void, Error> MuxSurface::advance_frame(FrameSink sink) {
// Mark all active sprites for emit, then emit + advance
for (int i = 0; i < params_.max_slots; i++) {
auto& sl = slots_[i];
if (sl.active && sl.sprite)
sl.needs_emit = true;
}
dirty_ = true;
auto r = emit_frame_if_needed(std::move(sink));
if (!r) return std::unexpected(r.error());
return {};
}
} // namespace subcodec

118
third-party/subcodec/src/mux_surface.h vendored Normal file
View file

@ -0,0 +1,118 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <memory>
#include <span>
#include <expected>
#include <functional>
#include <vector>
#include <optional>
#include <filesystem>
#include "types.h"
#include "error.h"
#include "mbs_mux_common.h"
namespace subcodec {
using FrameSink = std::function<void(std::span<const uint8_t>)>;
struct CompactionInfo {
int active_sprites;
int max_slots;
int current_grid_mbs;
int min_grid_mbs;
};
class MuxSurface {
public:
struct Params {
int sprite_width = 0; // Content width in pixels (multiple of 16)
int sprite_height = 0; // Content height in pixels (multiple of 16)
int max_slots = 0;
uint8_t qp = 0;
int8_t qp_delta_idr = 0;
int8_t qp_delta_p = 0;
};
struct SpriteRegion {
int slot;
struct Rect { int x, y, width, height; };
Rect color;
Rect alpha;
};
struct ResizeResult {
std::vector<SpriteRegion> regions;
};
static std::expected<MuxSurface, Error>
create(const Params& params, FrameSink sink);
~MuxSurface() = default;
MuxSurface(MuxSurface&&) = default;
MuxSurface& operator=(MuxSurface&&) = default;
std::expected<SpriteRegion, Error> add_sprite(const std::filesystem::path& mbs_path);
std::expected<SpriteRegion, Error> add_sprite(MbsSprite sprite);
void remove_sprite(int slot);
std::expected<void, Error> advance_frame(FrameSink sink);
void advance_sprite(int slot);
std::expected<bool, Error> emit_frame_if_needed(FrameSink sink);
std::expected<ResizeResult, Error> resize(
int new_max_slots,
std::span<const uint8_t> decoded_y,
std::span<const uint8_t> decoded_cb,
std::span<const uint8_t> decoded_cr,
int decoded_width,
int decoded_height,
int stride_y,
int stride_cb,
int stride_cr,
FrameSink sink);
CompactionInfo check_compaction_opportunity() const;
[[nodiscard]] int width_mbs() const { return total_w_; }
[[nodiscard]] int height_mbs() const { return total_h_; }
[[nodiscard]] int frame_num() const { return frame_num_; }
private:
MuxSurface() = default;
Params params_;
int sprite_w_mbs_ = 0; // padded sprite width in MBs
int sprite_h_mbs_ = 0; // padded sprite height in MBs
int content_w_ = 0; // content width in pixels
int content_h_ = 0; // content height in pixels
int total_w_ = 0, total_h_ = 0;
int cols_ = 0, rows_ = 0;
int stride_x_ = 0, stride_y_ = 0;
int frame_num_ = 0;
int log2_max_frame_num_ = 8;
int num_mbs_ = 0;
struct Slot {
std::optional<MbsSprite> sprite;
int current_frame = 0;
bool active = false;
bool needs_emit = false; // set by advance_sprite, cleared by emit
};
void rebuild_row_plans_();
std::vector<Slot> slots_;
std::vector<mux::SlotInfo> slot_infos_;
std::unique_ptr<uint8_t[]> buf_;
size_t buf_size_ = 0;
std::vector<mux::CompositeRowPlan> row_plans_;
std::vector<mux::RowOp> row_ops_;
bool plans_dirty_ = true;
bool dirty_ = false;
std::unique_ptr<uint8_t[]> rbsp_buf_;
size_t rbsp_buf_size_ = 0;
std::vector<mux::MicroOp> micro_ops_;
};
} // namespace subcodec

166
third-party/subcodec/src/sprite_data.cpp vendored Normal file
View file

@ -0,0 +1,166 @@
#include "types.h"
#include "mbs_format.h"
#include <cstdio>
namespace subcodec {
void MbsSprite::set_frames(std::vector<MbsEncodedFrame>&& encoded) {
num_frames = static_cast<uint16_t>(encoded.size());
size_t total_data = 0;
size_t total_rows = 0;
for (auto& ef : encoded) {
total_data += ef.data.size();
total_rows += ef.rows.size();
}
bulk_data_ = std::make_unique_for_overwrite<uint8_t[]>(total_data);
all_rows_.resize(total_rows);
frames.resize(encoded.size());
size_t data_off = 0;
size_t row_off = 0;
for (size_t i = 0; i < encoded.size(); i++) {
auto& ef = encoded[i];
size_t dsz = ef.data.size();
size_t rsz = ef.rows.size();
std::memcpy(bulk_data_.get() + data_off, ef.data.data(), dsz);
for (size_t r = 0; r < rsz; r++) {
all_rows_[row_off + r] = ef.rows[r];
if (ef.rows[r].blob_data) {
ptrdiff_t blob_off = ef.rows[r].blob_data - ef.data.data();
all_rows_[row_off + r].blob_data = bulk_data_.get() + data_off + blob_off;
}
}
frames[i].merged_rows = std::span<MbsRow>(all_rows_.data() + row_off, rsz);
data_off += dsz;
row_off += rsz;
}
}
std::expected<MbsSprite, Error> MbsSprite::load(const std::filesystem::path& path) {
FILE* f = fopen(path.c_str(), "rb");
if (!f) return std::unexpected(Error::IO_ERROR);
uint32_t magic;
MbsSprite sp;
uint8_t flags;
bool ok = true;
ok &= fread(&magic, 4, 1, f) == 1;
ok &= fread(&sp.width_mbs, 2, 1, f) == 1;
ok &= fread(&sp.height_mbs, 2, 1, f) == 1;
ok &= fread(&sp.num_frames, 2, 1, f) == 1;
ok &= fread(&sp.qp, 1, 1, f) == 1;
ok &= fread(&sp.qp_delta_idr, 1, 1, f) == 1;
ok &= fread(&sp.qp_delta_p, 1, 1, f) == 1;
ok &= fread(&flags, 1, 1, f) == 1;
if (!ok || magic != MBS_MAGIC_V6) {
fclose(f);
return std::unexpected(Error::IO_ERROR);
}
int h = sp.height_mbs;
int nf = sp.num_frames;
long header_pos = ftell(f);
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, header_pos, SEEK_SET);
size_t payload_size = static_cast<size_t>(file_size - header_pos);
sp.bulk_data_ = std::make_unique_for_overwrite<uint8_t[]>(payload_size);
if (fread(sp.bulk_data_.get(), 1, payload_size, f) != payload_size) {
fclose(f);
return std::unexpected(Error::IO_ERROR);
}
fclose(f);
sp.all_rows_.resize(static_cast<size_t>(nf) * h);
sp.frames.resize(nf);
const uint8_t* ptr = sp.bulk_data_.get();
const uint8_t* end = ptr + payload_size;
for (int i = 0; i < nf; i++) {
if (ptr + 4 > end) return std::unexpected(Error::PARSE_ERROR);
uint32_t sz;
std::memcpy(&sz, ptr, 4);
ptr += 4;
if (ptr + sz > end) return std::unexpected(Error::PARSE_ERROR);
const uint8_t* fp = ptr;
const uint8_t* fe = ptr + sz;
size_t row_base = static_cast<size_t>(i) * h;
for (int y = 0; y < h; y++) {
if (fp + 6 > fe) return std::unexpected(Error::PARSE_ERROR);
auto& row = sp.all_rows_[row_base + y];
row.leading_skips = fp[0];
row.trailing_skips = fp[1];
row.blob_bit_count = static_cast<uint16_t>(fp[2] | (fp[3] << 8));
row.leading_zero_bits = fp[4];
row.trailing_zero_bits = fp[5];
fp += 6;
int blob_bytes = (row.bit_count() + 7) / 8;
if (fp + blob_bytes > fe) return std::unexpected(Error::PARSE_ERROR);
row.blob_data = (row.bit_count() > 0) ? fp : nullptr;
fp += blob_bytes;
}
sp.frames[i].merged_rows = std::span<MbsRow>(&sp.all_rows_[row_base], h);
ptr += sz;
}
return sp;
}
std::expected<void, Error> MbsSprite::save(const std::filesystem::path& path) const {
FILE* f = fopen(path.c_str(), "wb");
if (!f) return std::unexpected(Error::IO_ERROR);
uint32_t magic = MBS_MAGIC_V6;
uint8_t flags = 0;
bool ok = true;
ok &= fwrite(&magic, 4, 1, f) == 1;
ok &= fwrite(&width_mbs, 2, 1, f) == 1;
ok &= fwrite(&height_mbs, 2, 1, f) == 1;
ok &= fwrite(&num_frames, 2, 1, f) == 1;
ok &= fwrite(&qp, 1, 1, f) == 1;
ok &= fwrite(&qp_delta_idr, 1, 1, f) == 1;
ok &= fwrite(&qp_delta_p, 1, 1, f) == 1;
ok &= fwrite(&flags, 1, 1, f) == 1;
for (int i = 0; i < num_frames; i++) {
uint32_t sz = 0;
for (auto& row : frames[i].merged_rows) {
sz += 6 + (row.bit_count() + 7) / 8;
}
ok &= fwrite(&sz, 4, 1, f) == 1;
for (auto& row : frames[i].merged_rows) {
uint8_t hdr[6];
hdr[0] = row.leading_skips;
hdr[1] = row.trailing_skips;
hdr[2] = static_cast<uint8_t>(row.blob_bit_count & 0xFF);
hdr[3] = static_cast<uint8_t>((row.blob_bit_count >> 8) & 0xFF);
hdr[4] = row.leading_zero_bits;
hdr[5] = row.trailing_zero_bits;
ok &= fwrite(hdr, 1, 6, f) == 6;
int blob_bytes = (row.bit_count() + 7) / 8;
if (blob_bytes > 0 && row.blob_data)
ok &= fwrite(row.blob_data, 1, blob_bytes, f) == static_cast<size_t>(blob_bytes);
}
}
fclose(f);
return ok ? std::expected<void, Error>{} : std::unexpected(Error::IO_ERROR);
}
} // namespace subcodec

View file

@ -0,0 +1,291 @@
#include "sprite_encode.h"
#include "h264_parser.h"
#include "codec_api.h"
#include "codec_app_def.h"
#include "codec_def.h"
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <vector>
namespace subcodec {
struct SpriteEncoder::Impl {
ISVCEncoder* enc = nullptr;
FrameParams parse_params{};
int half_width = 0; // single sprite padded width
int half_height = 0; // single sprite padded height
int canvas_width = 0; // double-wide: half_width * 2
int canvas_height = 0; // same as half_height
int half_width_mbs = 0;
int half_height_mbs = 0;
int canvas_width_mbs = 0;
H264Parser parser;
~Impl() {
if (enc) {
enc->Uninitialize();
WelsDestroySVCEncoder(enc);
}
}
};
// Find a slice NAL (type 1 or 5) in Annex B bitstream and parse it.
static std::expected<std::vector<MacroblockData>, Error>
find_and_parse_slice(H264Parser& parser,
const uint8_t* data, size_t data_size,
const FrameParams& params) {
size_t pos = 0;
while (pos + 4 < data_size) {
int sc_len = 0;
if (data[pos] == 0 && data[pos+1] == 0 && data[pos+2] == 0 && data[pos+3] == 1)
sc_len = 4;
else if (data[pos] == 0 && data[pos+1] == 0 && data[pos+2] == 1)
sc_len = 3;
if (sc_len == 0) { pos++; continue; }
uint8_t nal_type = data[pos + sc_len] & 0x1F;
// Find end of this NAL
size_t next_pos = pos + sc_len + 1;
while (next_pos + 3 <= data_size) {
if (data[next_pos] == 0 && data[next_pos+1] == 0 &&
((next_pos + 2 < data_size && data[next_pos+2] == 1) ||
(next_pos + 3 < data_size && data[next_pos+2] == 0 && data[next_pos+3] == 1)))
break;
next_pos++;
}
size_t nal_end = (next_pos + 3 <= data_size) ? next_pos : data_size;
if (nal_type == 1 || nal_type == 5) {
const uint8_t* nal_data = data + pos;
size_t nal_size = nal_end - pos;
// Ensure 4-byte start code for parser
std::vector<uint8_t> normalized;
if (sc_len == 3) {
normalized.push_back(0x00);
normalized.insert(normalized.end(), nal_data, nal_data + nal_size);
nal_data = normalized.data();
nal_size = normalized.size();
}
return parser.parse_slice({nal_data, nal_size}, params);
}
pos = nal_end;
}
return std::unexpected(Error::PARSE_ERROR);
}
SpriteEncoder::SpriteEncoder() = default;
SpriteEncoder::~SpriteEncoder() = default;
SpriteEncoder::SpriteEncoder(SpriteEncoder&&) noexcept = default;
SpriteEncoder& SpriteEncoder::operator=(SpriteEncoder&&) noexcept = default;
std::expected<SpriteEncoder, Error> SpriteEncoder::create(const Params& params) {
if (params.width <= 0 || params.height <= 0 ||
params.width % 16 != 0 || params.height % 16 != 0)
return std::unexpected(Error::INVALID_INPUT);
constexpr int padding_px = 16;
int padded_width = params.width + 2 * padding_px;
int padded_height = params.height + 2 * padding_px;
int canvas_width = padded_width * 2;
int canvas_height = padded_height;
ISVCEncoder* enc = nullptr;
if (WelsCreateSVCEncoder(&enc) != 0 || !enc)
return std::unexpected(Error::ENCODE_ERROR);
SEncParamExt eparam;
enc->GetDefaultParams(&eparam);
eparam.iUsageType = CAMERA_VIDEO_REAL_TIME;
eparam.iPicWidth = canvas_width;
eparam.iPicHeight = canvas_height;
eparam.fMaxFrameRate = 30.0f;
eparam.iRCMode = RC_OFF_MODE;
eparam.iEntropyCodingModeFlag = 0; // CAVLC
eparam.iSpatialLayerNum = 1;
eparam.iTemporalLayerNum = 1;
eparam.bEnableFrameSkip = false;
eparam.iMultipleThreadIdc = 1;
eparam.sSpatialLayers[0].uiProfileIdc = PRO_BASELINE;
eparam.sSpatialLayers[0].iVideoWidth = canvas_width;
eparam.sSpatialLayers[0].iVideoHeight = canvas_height;
eparam.sSpatialLayers[0].fFrameRate = 30.0f;
eparam.sSpatialLayers[0].iSpatialBitrate = 500000;
eparam.sSpatialLayers[0].iMaxSpatialBitrate = 500000;
eparam.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SINGLE_SLICE;
eparam.sSpatialLayers[0].iDLayerQp = params.qp;
eparam.uiIntraPeriod = 0; // Only first frame is IDR
eparam.iNumRefFrame = 1;
eparam.iLoopFilterDisableIdc = 1;
eparam.bSubcodecMode = true; // Enable subcodec sprite-compositing constraints
eparam.bEnableAdaptiveQuant = false;
eparam.iMinQp = params.qp;
eparam.iMaxQp = params.qp;
if (enc->InitializeExt(&eparam) != 0) {
WelsDestroySVCEncoder(enc);
return std::unexpected(Error::ENCODE_ERROR);
}
int videoFormat = videoFormatI420;
enc->SetOption(ENCODER_OPTION_DATAFORMAT, &videoFormat);
auto impl = std::make_unique<Impl>();
impl->enc = enc;
impl->half_width = padded_width;
impl->half_height = padded_height;
impl->canvas_width = canvas_width;
impl->canvas_height = canvas_height;
impl->half_width_mbs = padded_width / 16;
impl->half_height_mbs = padded_height / 16;
impl->canvas_width_mbs = canvas_width / 16;
// Parse params use canvas dimensions (what OpenH264 actually encoded)
impl->parse_params.width_mbs = static_cast<uint16_t>(impl->canvas_width_mbs);
impl->parse_params.height_mbs = static_cast<uint16_t>(impl->half_height_mbs);
impl->parse_params.log2_max_frame_num = 4;
impl->parse_params.pic_order_cnt_type = 0;
impl->parse_params.log2_max_pic_order_cnt_lsb = 5;
impl->parse_params.qp = static_cast<uint8_t>(params.qp);
SpriteEncoder se;
se.impl_ = std::move(impl);
return se;
}
std::expected<EncodeResult, Error> SpriteEncoder::encode(
const uint8_t* y, int y_stride,
const uint8_t* cb, int cb_stride,
const uint8_t* cr, int cr_stride,
const uint8_t* alpha, int alpha_stride,
int frame_index,
std::vector<uint8_t>* out_nal_data) {
if (!impl_ || !y || !cb || !cr || !alpha)
return std::unexpected(Error::INVALID_INPUT);
int hw = impl_->half_width;
int hh = impl_->half_height;
int cw = impl_->canvas_width;
int ch = impl_->canvas_height;
int half_chroma_w = hw / 2;
int half_chroma_h = hh / 2;
int canvas_chroma_w = cw / 2;
int canvas_chroma_h = ch / 2;
// Build double-wide YUV canvas
// Left half: color Y/Cb/Cr; Right half: alpha as luma, Cb=Cr=128
std::vector<uint8_t> canvas_y(cw * ch, 0);
std::vector<uint8_t> canvas_cb(canvas_chroma_w * canvas_chroma_h, 128);
std::vector<uint8_t> canvas_cr(canvas_chroma_w * canvas_chroma_h, 128);
// Copy color luma to left half
for (int row = 0; row < hh; row++)
memcpy(canvas_y.data() + row * cw, y + row * y_stride, hw);
// Copy alpha as luma to right half
for (int row = 0; row < hh; row++)
memcpy(canvas_y.data() + row * cw + hw, alpha + row * alpha_stride, hw);
// Copy color chroma to left half (right half stays at 128)
for (int row = 0; row < half_chroma_h; row++) {
memcpy(canvas_cb.data() + row * canvas_chroma_w, cb + row * cb_stride, half_chroma_w);
memcpy(canvas_cr.data() + row * canvas_chroma_w, cr + row * cr_stride, half_chroma_w);
}
SSourcePicture pic;
memset(&pic, 0, sizeof(pic));
pic.iColorFormat = videoFormatI420;
pic.iPicWidth = cw;
pic.iPicHeight = ch;
pic.iStride[0] = cw;
pic.iStride[1] = canvas_chroma_w;
pic.iStride[2] = canvas_chroma_w;
pic.pData[0] = canvas_y.data();
pic.pData[1] = canvas_cb.data();
pic.pData[2] = canvas_cr.data();
pic.uiTimeStamp = frame_index * 33;
SFrameBSInfo info;
memset(&info, 0, sizeof(info));
int rv = impl_->enc->EncodeFrame(&pic, &info);
if (rv != cmResultSuccess) {
fprintf(stderr, "SpriteEncoder: EncodeFrame failed frame %d (rv=%d)\n", frame_index, rv);
return std::unexpected(Error::ENCODE_ERROR);
}
if (info.eFrameType == videoFrameTypeSkip) {
fprintf(stderr, "SpriteEncoder: unexpected skip frame %d\n", frame_index);
return std::unexpected(Error::ENCODE_ERROR);
}
// Collect all NAL data
std::vector<uint8_t> frame_nals;
for (int layer = 0; layer < info.iLayerNum; layer++) {
SLayerBSInfo* layerInfo = &info.sLayerInfo[layer];
uint8_t* buf = layerInfo->pBsBuf;
for (int nal = 0; nal < layerInfo->iNalCount; nal++) {
int nalLen = layerInfo->pNalLengthInByte[nal];
frame_nals.insert(frame_nals.end(), buf, buf + nalLen);
buf += nalLen;
}
}
// Optionally return NAL data copy
if (out_nal_data) {
*out_nal_data = frame_nals;
}
// Parse full double-wide slice into macroblock data
auto parse_result = find_and_parse_slice(
impl_->parser, frame_nals.data(), frame_nals.size(), impl_->parse_params);
if (!parse_result) {
fprintf(stderr, "SpriteEncoder: parse failed frame %d\n", frame_index);
return std::unexpected(parse_result.error());
}
auto& all_mbs = *parse_result;
int hwm = impl_->half_width_mbs;
int hhm = impl_->half_height_mbs;
int cwm = impl_->canvas_width_mbs;
// Split into color (left half) and alpha (right half)
std::vector<MacroblockData> color(hwm * hhm);
std::vector<MacroblockData> alpha_mbs(hwm * hhm);
for (int mb_y = 0; mb_y < hhm; mb_y++) {
for (int mb_x = 0; mb_x < hwm; mb_x++) {
color[mb_y * hwm + mb_x] = all_mbs[mb_y * cwm + mb_x];
alpha_mbs[mb_y * hwm + mb_x] = all_mbs[mb_y * cwm + hwm + mb_x];
}
}
// For IDR frames, mark padding MBs as SKIP in both halves
if (frame_index == 0) {
int padding = 1; // matches iPaddingMbs set in create
for (int mb_y = 0; mb_y < hhm; mb_y++) {
for (int mb_x = 0; mb_x < hwm; mb_x++) {
if (mb_x < padding || mb_x >= hwm - padding ||
mb_y < padding || mb_y >= hhm - padding) {
MacroblockData& cmb = color[mb_y * hwm + mb_x];
cmb = MacroblockData{};
cmb.mb_type = MbType::SKIP;
MacroblockData& amb = alpha_mbs[mb_y * hwm + mb_x];
amb = MacroblockData{};
amb.mb_type = MbType::SKIP;
}
}
}
}
return EncodeResult{std::move(color), std::move(alpha_mbs)};
}
} // namespace subcodec

View file

@ -0,0 +1,46 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <memory>
#include <vector>
#include <expected>
#include "types.h"
#include "error.h"
namespace subcodec {
struct EncodeResult {
std::vector<MacroblockData> color;
std::vector<MacroblockData> alpha;
};
class SpriteEncoder {
public:
struct Params {
int width = 0; // Content width in pixels (multiple of 16)
int height = 0; // Content height in pixels (multiple of 16)
int qp = 26;
};
static std::expected<SpriteEncoder, Error> create(const Params& params);
~SpriteEncoder();
SpriteEncoder(SpriteEncoder&&) noexcept;
SpriteEncoder& operator=(SpriteEncoder&&) noexcept;
std::expected<EncodeResult, Error> encode(
const uint8_t* y, int y_stride,
const uint8_t* cb, int cb_stride,
const uint8_t* cr, int cr_stride,
const uint8_t* alpha, int alpha_stride,
int frame_index,
std::vector<uint8_t>* out_nal_data = nullptr);
private:
SpriteEncoder();
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace subcodec

View file

@ -0,0 +1,219 @@
#include "sprite_extractor.h"
#include "sprite_encode.h"
#include "mbs_encode.h"
#include "mbs_format.h"
#include <cstdio>
#include <cstring>
#include <vector>
namespace subcodec {
struct SpriteExtractor::Impl {
SpriteEncoder encoder;
FILE* file = nullptr;
std::filesystem::path output_path;
FrameParams frame_params{};
uint16_t frame_count = 0;
int sprite_size = 0;
int padded_size = 0;
int padded_stride = 0;
// Reusable padded YUV buffers
std::vector<uint8_t> pad_y;
std::vector<uint8_t> pad_cb;
std::vector<uint8_t> pad_cr;
std::vector<uint8_t> pad_alpha;
bool failed = false;
Impl(SpriteEncoder&& enc) : encoder(std::move(enc)) {}
~Impl() {
if (file) {
fclose(file);
// Remove partial file if finalize() was never called
std::filesystem::remove(output_path);
}
}
};
SpriteExtractor::SpriteExtractor() = default;
SpriteExtractor::~SpriteExtractor() = default;
SpriteExtractor::SpriteExtractor(SpriteExtractor&&) noexcept = default;
SpriteExtractor& SpriteExtractor::operator=(SpriteExtractor&&) noexcept = default;
std::expected<SpriteExtractor, Error> SpriteExtractor::create(
const Params& params, const std::filesystem::path& output_path) {
if (params.sprite_size <= 0 || params.sprite_size % 16 != 0)
return std::unexpected(Error::INVALID_INPUT);
constexpr int padding_px = 16;
int padded_size = params.sprite_size + 2 * padding_px;
auto enc_result = SpriteEncoder::create({params.sprite_size, params.sprite_size, params.qp});
if (!enc_result)
return std::unexpected(enc_result.error());
FILE* f = fopen(output_path.c_str(), "wb");
if (!f)
return std::unexpected(Error::IO_ERROR);
auto impl = std::make_unique<Impl>(std::move(*enc_result));
impl->file = f;
impl->output_path = output_path;
impl->sprite_size = params.sprite_size;
impl->padded_size = padded_size;
uint16_t width_mbs = static_cast<uint16_t>(padded_size / 16);
uint16_t height_mbs = static_cast<uint16_t>(padded_size / 16);
impl->frame_params.width_mbs = width_mbs;
impl->frame_params.height_mbs = height_mbs;
impl->frame_params.qp = static_cast<uint8_t>(params.qp);
// Allocate padded YUV buffers (reused across frames)
impl->padded_stride = padded_size;
int chroma_size = (padded_size / 2) * (padded_size / 2);
impl->pad_y.resize(padded_size * padded_size);
impl->pad_cb.resize(chroma_size);
impl->pad_cr.resize(chroma_size);
impl->pad_alpha.resize(padded_size * padded_size, 0); // black = transparent
// Write MBS v6 header with num_frames=0 (patched in finalize)
uint32_t magic = MBS_MAGIC_V6;
uint16_t num_frames = 0;
uint8_t qp = static_cast<uint8_t>(params.qp);
uint8_t zero = 0;
uint8_t flags = 0; // reserved
bool ok = true;
ok &= fwrite(&magic, 4, 1, f) == 1;
ok &= fwrite(&width_mbs, 2, 1, f) == 1;
ok &= fwrite(&height_mbs, 2, 1, f) == 1;
ok &= fwrite(&num_frames, 2, 1, f) == 1;
ok &= fwrite(&qp, 1, 1, f) == 1;
ok &= fwrite(&zero, 1, 1, f) == 1; // qp_delta_idr
ok &= fwrite(&zero, 1, 1, f) == 1; // qp_delta_p
ok &= fwrite(&flags, 1, 1, f) == 1; // flags (reserved)
if (!ok) {
fclose(f);
impl->file = nullptr;
std::filesystem::remove(output_path);
return std::unexpected(Error::IO_ERROR);
}
SpriteExtractor ext;
ext.impl_ = std::move(impl);
return ext;
}
std::expected<void, Error> SpriteExtractor::add_frame(
const uint8_t* y, int y_stride,
const uint8_t* cb, int cb_stride,
const uint8_t* cr, int cr_stride,
const uint8_t* alpha, int alpha_stride) {
if (!impl_ || !impl_->file || impl_->failed)
return std::unexpected(Error::INVALID_INPUT);
int ss = impl_->sprite_size;
constexpr int pp = 16;
int ps = impl_->padded_size;
int chroma_pp = pp / 2;
int chroma_ss = ss / 2;
int chroma_ps = ps / 2;
// Clear padded buffers to black (Y=0, Cb=128, Cr=128)
memset(impl_->pad_y.data(), 0, impl_->pad_y.size());
memset(impl_->pad_cb.data(), 128, impl_->pad_cb.size());
memset(impl_->pad_cr.data(), 128, impl_->pad_cr.size());
// Copy luma
for (int row = 0; row < ss; row++) {
memcpy(impl_->pad_y.data() + (row + pp) * ps + pp,
y + row * y_stride, ss);
}
// Copy chroma
for (int row = 0; row < chroma_ss; row++) {
memcpy(impl_->pad_cb.data() + (row + chroma_pp) * chroma_ps + chroma_pp,
cb + row * cb_stride, chroma_ss);
memcpy(impl_->pad_cr.data() + (row + chroma_pp) * chroma_ps + chroma_pp,
cr + row * cr_stride, chroma_ss);
}
// Clear alpha padding to 0 (transparent) then copy alpha content
memset(impl_->pad_alpha.data(), 0, impl_->pad_alpha.size());
for (int row = 0; row < ss; row++) {
memcpy(impl_->pad_alpha.data() + (row + pp) * ps + pp,
alpha + row * alpha_stride, ss);
}
// Encode with OpenH264 + parse into MacroblockData
auto encode_result = impl_->encoder.encode(
impl_->pad_y.data(), ps,
impl_->pad_cb.data(), chroma_ps,
impl_->pad_cr.data(), chroma_ps,
impl_->pad_alpha.data(), ps,
impl_->frame_count, nullptr);
if (!encode_result) {
impl_->failed = true;
return std::unexpected(encode_result.error());
}
// MBS-encode color+alpha as merged frame
auto merged_mbs = mbs::encode_frame_merged(
impl_->frame_params, encode_result->color.data(),
impl_->frame_params, encode_result->alpha.data(),
impl_->frame_params.width_mbs, 1 /* padding */);
if (merged_mbs.data.empty()) {
impl_->failed = true;
return std::unexpected(Error::ENCODE_ERROR);
}
// Write [frame_data_size][merged_row_data] to file
uint32_t sz = static_cast<uint32_t>(merged_mbs.data.size());
bool ok = true;
ok &= fwrite(&sz, 4, 1, impl_->file) == 1;
ok &= fwrite(merged_mbs.data.data(), 1, merged_mbs.data.size(), impl_->file)
== merged_mbs.data.size();
if (!ok) {
impl_->failed = true;
return std::unexpected(Error::IO_ERROR);
}
impl_->frame_count++;
return {};
}
std::expected<void, Error> SpriteExtractor::finalize() {
if (!impl_ || !impl_->file)
return std::unexpected(Error::INVALID_INPUT);
if (impl_->failed) {
fclose(impl_->file);
impl_->file = nullptr;
std::filesystem::remove(impl_->output_path);
return std::unexpected(Error::IO_ERROR);
}
// Patch num_frames in header (offset 8: magic(4) + width(2) + height(2))
fseek(impl_->file, 8, SEEK_SET);
uint16_t nf = impl_->frame_count;
bool ok = fwrite(&nf, 2, 1, impl_->file) == 1;
fclose(impl_->file);
impl_->file = nullptr;
if (!ok) {
std::filesystem::remove(impl_->output_path);
return std::unexpected(Error::IO_ERROR);
}
return {};
}
} // namespace subcodec

View file

@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <memory>
#include <expected>
#include <filesystem>
#include "error.h"
namespace subcodec {
class SpriteExtractor {
public:
struct Params {
int sprite_size; // content size in pixels (must be multiple of 16)
int qp = 26; // quantization parameter
};
static std::expected<SpriteExtractor, Error> create(
const Params& params, const std::filesystem::path& output_path);
std::expected<void, Error> add_frame(
const uint8_t* y, int y_stride,
const uint8_t* cb, int cb_stride,
const uint8_t* cr, int cr_stride,
const uint8_t* alpha, int alpha_stride);
std::expected<void, Error> finalize();
~SpriteExtractor();
SpriteExtractor(SpriteExtractor&&) noexcept;
SpriteExtractor& operator=(SpriteExtractor&&) noexcept;
private:
SpriteExtractor();
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace subcodec

44
third-party/subcodec/src/tables.h vendored Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include <cstdint>
namespace subcodec::tables {
// Table 9-4(b): CBP to exp-golomb codeNum for Inter prediction
// Indexed by [cbp_chroma * 16 + cbp_luma]
inline constexpr uint8_t cbp_to_code_inter[48] = {
// cbp_chroma = 0
0, 2, 3, 7, 4, 8, 17, 13, 5, 18, 9, 14, 10, 15, 16, 11,
// cbp_chroma = 1
1, 32, 33, 36, 34, 37, 44, 40, 35, 45, 38, 41, 39, 42, 43, 19,
// cbp_chroma = 2
6, 24, 25, 20, 26, 21, 46, 28, 27, 47, 22, 29, 23, 30, 31, 12,
};
// Table 9-4(a): CBP to exp-golomb codeNum for Intra prediction
// Indexed by [cbp_chroma * 16 + cbp_luma]
inline constexpr uint8_t cbp_to_code_intra[48] = {
// cbp_chroma = 0
3, 29, 30, 17, 31, 18, 37, 8, 32, 38, 19, 9, 20, 10, 11, 2,
// cbp_chroma = 1
16, 33, 34, 21, 35, 22, 39, 4, 36, 40, 23, 5, 24, 6, 7, 1,
// cbp_chroma = 2
41, 42, 43, 25, 44, 26, 46, 12, 45, 47, 27, 13, 28, 14, 15, 0,
};
// H.264 Table 6-9: 4x4 block scan order within a macroblock
// 8x8 block N contains 4x4 blocks N*4..N*4+3
inline constexpr int luma_block_order[16] = {
0, 1, 2, 3, // 8x8 block 0
4, 5, 6, 7, // 8x8 block 1
8, 9, 10, 11, // 8x8 block 2
12, 13, 14, 15 // 8x8 block 3
};
// Map 4x4 block index to its 8x8 parent block
inline constexpr int block_to_8x8[16] = {
0, 0, 0, 0, 1, 1, 1, 1,
2, 2, 2, 2, 3, 3, 3, 3
};
} // namespace subcodec::tables

Some files were not shown because too many files have changed in this diff Show more