Move code

This commit is contained in:
Ali 2023-11-09 22:28:02 +04:00
parent f15c58c90a
commit 169fece59c
32 changed files with 4173 additions and 11 deletions

View File

@ -10,6 +10,21 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
load(
"@rules_xcodeproj//xcodeproj:defs.bzl",
"top_level_target",
"top_level_targets",
"xcodeproj",
"xcode_provisioning_profile",
)
load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")
load(
"@build_configuration//:variables.bzl",
"telegram_bazel_path",
)
filegroup(
name = "AppResources",
srcs = glob([
@ -72,7 +87,7 @@ plist_fragment(
<key>CFBundleDisplayName</key>
<string>Test</string>
<key>CFBundleIdentifier</key>
<string>org.telegram.CallUITest</string>
<string>org.telegram.Telegram-iOS</string>
<key>CFBundleName</key>
<string>Telegram</string>
<key>CFBundlePackageType</key>
@ -129,7 +144,7 @@ plist_fragment(
ios_application(
name = "CallUITest",
bundle_id = "org.telegram.TelegramiOS",
bundle_id = "org.telegram.Telegram-iOS",
families = ["iphone", "ipad"],
minimum_os_version = "12.0",
provisioning_profile = "@build_configuration//provisioning:Telegram.mobileprovision",
@ -147,4 +162,29 @@ ios_application(
"//Tests/Common:Main",
":Lib",
],
visibility = ["//visibility:public"],
)
xcodeproj(
name = "CallUITest_xcodeproj",
build_mode = "bazel",
bazel_path = telegram_bazel_path,
project_name = "CallUITest",
tags = ["manual"],
top_level_targets = top_level_targets(
labels = [
":CallUITest",
],
target_environments = ["device", "simulator"],
),
xcode_configurations = {
"Debug": {
"//command_line_option:compilation_mode": "dbg",
},
"Release": {
"//command_line_option:compilation_mode": "opt",
},
},
default_xcode_configuration = "Debug"
)

View File

@ -1,8 +1,19 @@
import Foundation
import UIKit
import CallScreen
public final class ViewController: UIViewController {
public final class ViewController: UIViewController {
override public func viewDidLoad() {
super.viewDidLoad()
let privateCallScreen = PrivateCallScreen(frame: CGRect())
self.view.addSubview(privateCallScreen)
privateCallScreen.frame = self.view.bounds
privateCallScreen.update(size: self.view.bounds.size, insets: UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0))
let context = MetalContext.shared
self.view.layer.addSublayer(context.rootLayer)
context.rootLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -101.0), size: CGSize(width: 100.0, height: 100.0))
}
}

View File

@ -20,19 +20,24 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
app_target_clean = app_target.replace('/', '_')
bazel_generate_arguments = [build_environment.bazel_path]
bazel_generate_arguments += ['run', '//Telegram:Telegram_xcodeproj']
bazel_generate_arguments += ['run', '//{}_xcodeproj'.format(app_target_spec)]
bazel_generate_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
if disable_extensions:
bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
bazel_generate_arguments += ['--//{}:disableStripping'.format('Telegram')]
if target_name == 'Telegram':
if disable_extensions:
bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
bazel_generate_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments = []
for argument in bazel_app_arguments:
project_bazel_arguments.append(argument)
project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
if disable_extensions:
project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
project_bazel_arguments += ['--//{}:disableStripping'.format('Telegram')]
if target_name == 'Telegram':
if disable_extensions:
project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
project_bazel_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments += ['--features=-swift.debug_prefix_map']
@ -45,7 +50,7 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
call_executable(bazel_generate_arguments)
xcodeproj_path = 'Telegram/Telegram.xcodeproj'
xcodeproj_path = '{}.xcodeproj'.format(app_target_spec.replace(':', '/'))
call_executable(['open', xcodeproj_path])

View File

@ -1,5 +1,46 @@
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 = "MetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "MetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.CallScreenMetalSources</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>AnimationCompression</string>
"""
)
apple_resource_bundle(
name = "MetalSourcesBundle",
infoplists = [
":MetalSourcesBundleInfoPlist",
],
resources = [
":MetalSources",
],
)
swift_library(
name = "CallScreen",
module_name = "CallScreen",
@ -9,10 +50,14 @@ swift_library(
copts = [
"-warnings-as-errors",
],
data = [
":MetalSourcesBundle",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Utils/ShelfPack",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,349 @@
#include <metal_stdlib>
using namespace metal;
struct Rectangle {
float2 origin;
float2 size;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
vertex float4 clearVertex(const device float2* vertexArray [[ buffer(0) ]], unsigned int vid [[ vertex_id ]]) {
return float4(vertexArray[vid], 0.0, 1.0);
}
fragment half4 clearFragment(const device float4 &color [[ buffer(0) ]]) {
return half4(color);
}
vertex QuadVertexOut callBackgroundVertex(
const device Rectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
half4 rgb2hsv(half4 c) {
half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
half4 p = mix(half4(c.bg, K.wz), half4(c.gb, K.xy), step(c.b, c.g));
half4 q = mix(half4(p.xyw, c.r), half4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return half4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
}
half4 hsv2rgb(half4 c) {
half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
half3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return half4(c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y), c.a);
}
fragment half4 callBackgroundFragment(
QuadVertexOut in [[stage_in]],
const device float2 *positions [[ buffer(0) ]],
const device float4 *colors [[ buffer(1) ]],
const device float &brightness [[ buffer(2) ]],
const device float &saturation [[ buffer(3) ]]
) {
half centerDistanceX = in.uv.x - 0.5;
half centerDistanceY = in.uv.y - 0.5;
half centerDistance = distance(half2(in.uv), half2(0.5, 0.5));
half swirlFactor = 0.35 * centerDistance;
half theta = swirlFactor * swirlFactor * 0.8 * 8.0;
half sinTheta = sin(theta);
half cosTheta = cos(theta);
half pixelX = max(0.0, min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta));
half pixelY = max(0.0, min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta));
half distanceSum = 0.0;
half r = 0.0;
half g = 0.0;
half b = 0.0;
for (int i = 0; i < 4; i++) {
half4 color = half4(colors[i]);
half2 colorXY = half2(positions[i]);
half2 distanceXY = half2(pixelX - colorXY.x, pixelY - colorXY.y);
half distance = max(0.0, 0.92 - sqrt(distanceXY.x * distanceXY.x + distanceXY.y * distanceXY.y));
distance = distance * distance * distance;
distanceSum += distance;
r = r + distance * color.r;
g = g + distance * color.g;
b = b + distance * color.b;
}
if (distanceSum < 0.00001) {
distanceSum = 0.00001;
}
half pixelB = b / distanceSum;
half pixelG = g / distanceSum;
half pixelR = r / distanceSum;
half4 color(pixelR, pixelG, pixelB, 1.0);
color = rgb2hsv(color);
color.b = clamp(color.b * brightness, 0.0, 1.0);
color.g = clamp(color.g * saturation, 0.0, 1.0);
color = hsv2rgb(color);
return color;
}
struct BlobVertexOut {
float4 position [[position]];
};
float2 blobVertex(float2 center, float angle, float radius) {
return float2(center.x + radius * cos(angle), center.y + radius * sin(angle));
}
float2 mapPointInRect(Rectangle rect, half2 point) {
half2 out(rect.origin.x + rect.size.x * point.x, rect.origin.y + rect.size.y * point.y);
out.x = -1.0 + out.x * 2.0;
out.y = -1.0 + out.y * 2.0;
return float2(out);
}
struct SmoothPoint {
half2 point;
half inAngle;
half inLength;
half outAngle;
half outLength;
half2 smoothIn() {
return smooth(inAngle, inLength);
}
half2 smoothOut() {
return smooth(outAngle, outLength);
}
private:
half2 smooth(half angle, half length) {
return half2(
point.x + length * cos(angle),
point.y + length * sin(angle)
);
}
};
half2 evaluateBlobPoint(const device Rectangle &rect, const device float *positions, int index, int count, int subdivisions) {
float position = positions[index];
float segmentAngle = float(index) / float(count) * 2.0 * 3.1415926;
return half2(blobVertex(float2(0.5, 0.5), segmentAngle, 0.45 + 0.05 * position));
}
SmoothPoint evaluateSmoothBlobPoint(const device Rectangle &rect, const device float *positions, int index, int count, int subdivisions) {
int prevIndex = (index - 1) < 0 ? (count - 1) : (index - 1);
int nextIndex = (index + 1) % count;
half2 prev = evaluateBlobPoint(rect, positions, prevIndex, count, subdivisions);
half2 curr = evaluateBlobPoint(rect, positions, index, count, subdivisions);
half2 next = evaluateBlobPoint(rect, positions, nextIndex, count, subdivisions);
float dx = next.x - prev.x;
float dy = -next.y + prev.y;
float angle = atan2(dy, dx);
if (angle < 0.0) {
angle = abs(angle);
} else {
angle = 2 * 3.1415926 - angle;
}
float smoothAngle = (3.1415926 * 2.0) / float(count);
float smoothness = ((4.0 / 3.0) * tan(smoothAngle / 4.0)) / sin(smoothAngle / 2.0) / 2.0;
SmoothPoint point;
point.point = curr;
point.inAngle = angle + 3.1415926;
point.inLength = smoothness * distance(curr, prev);
point.outAngle = angle;
point.outLength = smoothness * distance(curr, next);
return point;
}
half2 evaluateBezierBlobPoint(thread SmoothPoint &curr, thread SmoothPoint &next, half t) {
half oneMinusT = 1.0 - t;
half2 p0 = curr.point;
half2 p1 = curr.smoothOut();
half2 p2 = next.smoothIn();
half2 p3 = next.point;
return oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3;
}
vertex BlobVertexOut callBlobVertex(
const device Rectangle &rect [[ buffer(0) ]],
const device float *positions [[ buffer(1) ]],
const device int &count [[ buffer(2) ]],
unsigned int vid [[ vertex_id ]]
) {
const int subdivisions = 8;
int triangleIndex = vid / 3;
int segmentIndex = triangleIndex / subdivisions;
int nextIndex = (segmentIndex + 1) % count;
half innerPosition = half(triangleIndex - segmentIndex * subdivisions) / half(subdivisions);
half nextInnerPosition = half(triangleIndex + 1 - segmentIndex * subdivisions) / half(subdivisions);
SmoothPoint curr = evaluateSmoothBlobPoint(rect, positions, segmentIndex, count, subdivisions);
SmoothPoint next = evaluateSmoothBlobPoint(rect, positions, nextIndex, count, subdivisions);
half2 triangle[3];
triangle[0] = half2(0.5, 0.5);
triangle[1] = evaluateBezierBlobPoint(curr, next, innerPosition);
triangle[2] = evaluateBezierBlobPoint(curr, next, nextInnerPosition);
BlobVertexOut out;
out.position = float4(float2(mapPointInRect(rect, triangle[vid % 3])), 0.0, 1.0);
return out;
}
fragment half4 callBlobFragment(
BlobVertexOut in [[stage_in]]
) {
half alpha = 0.15;
return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha);
}
kernel void videoYUVToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureUV [[ texture(1) ]],
texture2d<half, access::write> outTexture [[ texture(2) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
half2 uv = inTextureUV.read(uint2(threadPosition.x / 2, threadPosition.y / 2)).rg - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
vertex QuadVertexOut mainVideoVertex(
const device Rectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = float2(quadVertex.x, 1.0 - quadVertex.y);
return out;
}
fragment half4 mainVideoFragment(
QuadVertexOut in [[stage_in]],
texture2d<half> texture [[ texture(0) ]],
const device float &brightness [[ buffer(0) ]],
const device float &saturation [[ buffer(1) ]]
) {
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
half4 color = texture.sample(sampler, in.uv);
color = rgb2hsv(color);
color.b = clamp(color.b * brightness, 0.0, 1.0);
color.g = clamp(color.g * saturation, 0.0, 1.0);
color = hsv2rgb(color);
return half4(color.r, color.g, color.b, color.a);
}
constant int BLUR_SAMPLE_COUNT = 7;
constant float BLUR_OFFSETS[BLUR_SAMPLE_COUNT] = {
-5.227545617192816,
-3.3147990233346842,
-1.4174297935376852,
0.47225076494548685,
2.364576440741639,
4.268941421369995,
6
};
constant float BLUR_WEIGHTS[BLUR_SAMPLE_COUNT] = {
0.015167713616041436,
0.10117053983645591,
0.2894431725427234,
0.3570581167968804,
0.19014435646109845,
0.0435647539906345,
0.0034513467561660305
};
static void gaussianBlur(
texture2d<half, access::sample> inTexture,
texture2d<half, access::write> outTexture,
float2 offset,
uint2 gid
) {
constexpr sampler sampler(coord::normalized, address::clamp_to_edge, filter::linear);
uint2 textureDim(outTexture.get_width(), outTexture.get_height());
if(all(gid < textureDim)) {
float3 outColor(0.0);
float2 size(inTexture.get_width(), inTexture.get_height());
float2 baseTexCoord = float2(gid);
for (int i = 0; i < BLUR_SAMPLE_COUNT; i++) {
outColor += float3(inTexture.sample(sampler, (baseTexCoord + offset * BLUR_OFFSETS[i]) / size).rgb) * BLUR_WEIGHTS[i];
}
outTexture.write(half4(half3(outColor), 1.0), gid);
}
}
kernel void gaussianBlurHorizontal(
texture2d<half, access::sample> inTexture [[ texture(0) ]],
texture2d<half, access::write> outTexture [[ texture(1) ]],
uint2 gid [[ thread_position_in_grid ]]
) {
gaussianBlur(inTexture, outTexture, float2(1, 0), gid);
}
kernel void gaussianBlurVertical(
texture2d<half, access::sample> inTexture [[ texture(0) ]],
texture2d<half, access::write> outTexture [[ texture(1) ]],
uint2 gid [[ thread_position_in_grid ]]
) {
gaussianBlur(inTexture, outTexture, float2(0, 1), gid);
}

View File

@ -0,0 +1,158 @@
import Foundation
import UIKit
public enum AnimationCurve {
case linear
case easeInOut
case spring
}
extension AnimationCurve {
func map(_ fraction: CGFloat) -> CGFloat {
switch self {
case .linear:
return fraction
case .easeInOut:
return bezierPoint(0.42, 0.0, 0.58, 1.0, fraction)
case .spring:
return bezierPoint(0.23, 1.0, 0.32, 1.0, fraction)
}
}
}
open class AnyAnimation {
}
open class AnimationInterpolator<T> {
private let impl: (T, T, CGFloat) -> T
init(_ impl: @escaping (T, T, CGFloat) -> T) {
self.impl = impl
}
public func interpolate(from: T, to: T, fraction: CGFloat) -> T {
return self.impl(from, to, fraction)
}
}
public protocol AnimationInterpolatable {
static var animationInterpolator: AnimationInterpolator<Self> { get }
}
private let CGFloatInterpolator = AnimationInterpolator<CGFloat> { from, to, fraction in
return from * (1.0 - fraction) + to * fraction
}
extension CGFloat: AnimationInterpolatable {
public static var animationInterpolator: AnimationInterpolator<CGFloat> {
return CGFloatInterpolator
}
}
private let CGPointInterpolator = AnimationInterpolator<CGPoint> { from, to, fraction in
return CGPoint(
x: CGFloatInterpolator.interpolate(from: from.x, to: to.x, fraction: fraction),
y: CGFloatInterpolator.interpolate(from: from.y, to: to.y, fraction: fraction)
)
}
extension CGPoint: AnimationInterpolatable {
public static var animationInterpolator: AnimationInterpolator<CGPoint> {
return CGPointInterpolator
}
}
#if targetEnvironment(simulator)
@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
#endif
public final class Animation<T: AnimationInterpolatable>: AnyAnimation {
private let from: T
private let to: T
private let duration: Double
private let curve: AnimationCurve
private let interpolator: AnimationInterpolator<T>
private var startTime: Double?
public private(set) var isFinished: Bool = false
var didStart: (() -> Void)?
public init(from: T, to: T, duration: Double, curve: AnimationCurve) {
self.from = from
self.to = to
#if targetEnvironment(simulator)
self.duration = duration * Double(UIAnimationDragCoefficient())
#else
self.duration = duration
#endif
self.curve = curve
self.interpolator = T.animationInterpolator
}
func start() {
self.startTime = CACurrentMediaTime()
}
func update(at timestamp: Double) -> T {
guard let startTime = self.startTime else {
return self.from
}
if self.isFinished {
return self.to
}
let fraction = max(0.0, min(1.0, (timestamp - startTime) / self.duration))
if timestamp > startTime + self.duration {
self.isFinished = true
}
if fraction >= 1.0 {
return self.to
}
return self.interpolator.interpolate(from: self.from, to: self.to, fraction: self.curve.map(fraction))
}
}
public class AnyAnimatedProperty {
var didStartAnimation: (() -> Void)?
var hasRunningAnimation: Bool {
return false
}
public func update() {
}
}
public final class AnimatedProperty<T: AnimationInterpolatable>: AnyAnimatedProperty {
private var animation: Animation<T>?
override var hasRunningAnimation: Bool {
return self.animation != nil
}
public private(set) var value: T
public init(_ value: T) {
self.value = value
}
public func animate(to: T, duration: Double, curve: AnimationCurve) {
let timestamp = CACurrentMediaTime()
let fromValue: T
if let animation = self.animation {
fromValue = animation.update(at: timestamp)
} else {
fromValue = self.value
}
self.animation = Animation(from: fromValue, to: to, duration: duration, curve: curve)
self.animation?.start()
self.didStartAnimation?()
}
override public func update() {
if let animation = self.animation {
self.value = animation.update(at: CACurrentMediaTime())
if animation.isFinished {
self.animation = nil
}
}
}
}

View File

@ -0,0 +1,50 @@
import Foundation
import UIKit
public final class ManagedAnimations {
private var displayLinkSubscription: SharedDisplayLink.Subscription?
private var properties: [AnyAnimatedProperty] = []
public var updated: (() -> Void)?
public init() {
}
public func add(property: AnyAnimatedProperty) {
self.properties.append(property)
property.didStartAnimation = { [weak self] in
guard let self else {
return
}
self.updateNeedAnimations()
}
}
private func updateNeedAnimations() {
if self.displayLinkSubscription == nil {
self.displayLinkSubscription = SharedDisplayLink.shared.add { [weak self] in
guard let self else {
return
}
self.update()
}
}
}
private func update() {
var hasRunningAnimations = false
for property in self.properties {
property.update()
if property.hasRunningAnimation {
hasRunningAnimations = true
}
}
if !hasRunningAnimations {
self.displayLinkSubscription = nil
}
self.updated?()
}
}

View File

@ -0,0 +1,53 @@
import Foundation
private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return 1.0 - 3.0 * a2 + 3.0 * a1
}
private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return 3.0 * a2 - 6.0 * a1
}
private func c(_ a1: CGFloat) -> CGFloat
{
return 3.0 * a1
}
private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t
}
private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
}
private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat {
var t = x
var i = 0
while i < 4 {
let currentSlope = calcSlope(t, x1, x2)
if currentSlope == 0.0 {
return t
} else {
let currentX = calcBezier(t, x1, x2) - x
t -= currentX / currentSlope
}
i += 1
}
return t
}
public func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat
{
var value = calcBezier(getTForX(x, x1, x2), y1, y2)
if value >= 0.997 {
value = 1.0
}
return value
}

View File

@ -0,0 +1,40 @@
import Foundation
import UIKit
final class AvatarLayer: SimpleLayer {
var image: UIImage? {
didSet {
if let image = self.image {
let imageSize = CGSize(width: 136.0, height: 136.0)
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: imageSize), format: .preferred())
let image = renderer.image { context in
context.cgContext.addEllipse(in: CGRect(origin: CGPoint(), size: imageSize))
context.cgContext.clip()
context.cgContext.translateBy(x: imageSize.width * 0.5, y: imageSize.height * 0.5)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
context.cgContext.translateBy(x: -imageSize.width * 0.5, y: -imageSize.height * 0.5)
context.cgContext.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: imageSize))
}
self.contents = image.cgImage
} else {
self.contents = nil
}
}
}
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize) {
}
}

View File

@ -0,0 +1,126 @@
import Foundation
import UIKit
final class ButtonGroupView: UIView, ContentOverlayView {
enum Key: Hashable {
case audio
case video
case mic
case close
}
let overlayMaskLayer: CALayer
private var buttons: [Key: ContentOverlayButton] = [:]
var toggleVideo: (() -> Void)?
override init(frame: CGRect) {
self.overlayMaskLayer = SimpleLayer()
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func addSubview(_ view: UIView) {
super.addSubview(view)
if let view = view as? ContentOverlayView {
self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
}
}
override func insertSubview(_ view: UIView, at index: Int) {
super.insertSubview(view, at: index)
if let view = view as? ContentOverlayView {
self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
}
}
override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
super.insertSubview(view, aboveSubview: siblingSubview)
if let view = view as? ContentOverlayView {
self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
}
}
override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
super.insertSubview(view, belowSubview: siblingSubview)
if let view = view as? ContentOverlayView {
self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
}
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
if let view = subview as? ContentOverlayView {
view.overlayMaskLayer.removeFromSuperlayer()
}
}
func update(size: CGSize) {
var keys: [Key] = []
keys.append(.audio)
keys.append(.video)
keys.append(.mic)
keys.append(.close)
let buttonSize: CGFloat = 56.0
let buttonSpacing: CGFloat = 36.0
let buttonY: CGFloat = size.height - 86.0 - buttonSize
var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(keys.count) - buttonSpacing * CGFloat(keys.count - 1)) * 0.5)
for key in keys {
let button: ContentOverlayButton
if let current = self.buttons[key] {
button = current
} else {
button = ContentOverlayButton(frame: CGRect())
self.addSubview(button)
self.buttons[key] = button
let image: UIImage?
switch key {
case .audio:
image = UIImage(named: "Call/ButtonSpeaker")
case .video:
image = UIImage(named: "Call/ButtonVideo")
button.action = { [weak self] in
guard let self else {
return
}
self.toggleVideo?()
}
case .mic:
image = UIImage(named: "Call/ButtonMicMuted")
case .close:
image = UIImage(named: "Call/ButtonEnd")
}
button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal)
button.imageView?.tintColor = .white
}
button.frame = CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize))
buttonX += buttonSize + buttonSpacing
}
var removeKeys: [Key] = []
for (key, button) in self.buttons {
if !keys.contains(key) {
removeKeys.append(key)
button.removeFromSuperview()
}
}
for key in removeKeys {
self.buttons.removeValue(forKey: key)
}
}
}

View File

@ -0,0 +1,154 @@
import Foundation
import MetalKit
import UIKit
private func shiftArray(array: [SIMD2<Float>], offset: Int) -> [SIMD2<Float>] {
var newArray = array
var offset = offset
while offset > 0 {
let element = newArray.removeFirst()
newArray.append(element)
offset -= 1
}
return newArray
}
private func gatherPositions(_ list: [SIMD2<Float>]) -> [SIMD2<Float>] {
var result: [SIMD2<Float>] = []
for i in 0 ..< list.count / 2 {
result.append(list[i * 2])
}
return result
}
private func hexToFloat(_ hex: Int) -> SIMD4<Float> {
let red = Float((hex >> 16) & 0xFF) / 255.0
let green = Float((hex >> 8) & 0xFF) / 255.0
let blue = Float((hex >> 0) & 0xFF) / 255.0
return SIMD4<Float>(x: red, y: green, z: blue, w: 1.0)
}
final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
var internalData: MetalSubjectInternalData?
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
init?(device: MTLDevice, library: MTLLibrary) {
guard let vertexFunction = library.makeFunction(name: "callBackgroundVertex"), let fragmentFunction = library.makeFunction(name: "callBackgroundFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
private static var basePositions: [SIMD2<Float>] = [
SIMD2<Float>(x: 0.80, y: 0.10),
SIMD2<Float>(x: 0.60, y: 0.20),
SIMD2<Float>(x: 0.35, y: 0.25),
SIMD2<Float>(x: 0.25, y: 0.60),
SIMD2<Float>(x: 0.20, y: 0.90),
SIMD2<Float>(x: 0.40, y: 0.80),
SIMD2<Float>(x: 0.65, y: 0.75),
SIMD2<Float>(x: 0.75, y: 0.40)
]
private var isBlur: Bool = false
private var phase: Float = 0.0
private var displayLinkSubscription: SharedDisplayLink.Subscription?
var renderSpec: RenderLayerSpec?
init(isBlur: Bool) {
self.isBlur = isBlur
super.init()
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = SharedDisplayLink.shared.add(framesPerSecond: .fps(30.0), { [weak self] in
guard let self else {
return
}
let stepCount = 8
self.phase = (self.phase + 1.0 / 60.0).truncatingRemainder(dividingBy: Float(stepCount))
self.setNeedsUpdate()
})
}
self.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = nil
}
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(context: MetalSubjectContext) {
guard let renderSpec = self.renderSpec else {
return
}
let isBlur = self.isBlur
let phase = self.phase
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, commands: { encoder, placement in
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
let baseStep = floor(phase)
let nextStepInterpolation = phase - floor(phase)
let positions0 = gatherPositions(shiftArray(array: CallBackgroundLayer.basePositions, offset: Int(baseStep)))
let positions1 = gatherPositions(shiftArray(array: CallBackgroundLayer.basePositions, offset: Int(baseStep) + 1))
var positions = Array<SIMD2<Float>>(repeating: SIMD2<Float>(), count: 4)
for i in 0 ..< 4 {
positions[i] = interpolatePoints(positions0[i], positions1[i], at: nextStepInterpolation)
}
encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout<SIMD2<Float>>.size, index: 0)
var colors: [[SIMD4<Float>]] = [
[
hexToFloat(0x568FD6),
hexToFloat(0x626ED5),
hexToFloat(0xA667D5),
hexToFloat(0x7664DA)
],
[
hexToFloat(0xACBD65),
hexToFloat(0x459F8D),
hexToFloat(0x53A4D1),
hexToFloat(0x3E917A)
]
]
encoder.setFragmentBytes(&colors[0], length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
var brightness: Float = isBlur ? 1.1 : 1.0
var saturation: Float = isBlur ? 1.2 : 1.0
encoder.setFragmentBytes(&brightness, length: 4, index: 2)
encoder.setFragmentBytes(&saturation, length: 4, index: 3)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}

View File

@ -0,0 +1,154 @@
import Foundation
import MetalKit
final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
var internalData: MetalSubjectInternalData?
struct Blob {
var points: [Float]
var nextPoints: [Float]
init(count: Int) {
self.points = (0 ..< count).map { _ in
Float.random(in: 0.0 ... 1.0)
}
self.nextPoints = (0 ..< count).map { _ in
Float.random(in: 0.0 ... 1.0)
}
}
func interpolate(at t: Float) -> [Float] {
var points: [Float] = Array(repeating: 0.0, count: self.points.count)
for i in 0 ..< self.points.count {
points[i] = interpolateFloat(self.points[i], self.nextPoints[i], at: t)
}
return points
}
mutating func advance() {
self.points = self.nextPoints
self.nextPoints = (0 ..< self.points.count).map { _ in
Float.random(in: 0.0 ... 1.0)
}
}
}
final class RenderState: RenderToLayerState {
final class Input {
let rect: CGRect
let blobs: [Blob]
let phase: Float
init(rect: CGRect, blobs: [Blob], phase: Float) {
self.rect = rect
self.blobs = blobs
self.phase = phase
}
}
let pipelineState: MTLRenderPipelineState
required init?(
device: MTLDevice,
library: MTLLibrary
) {
guard let vertexFunction = library.makeFunction(name: "callBlobVertex"), let fragmentFunction = library.makeFunction(name: "callBlobFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
private var phase: Float = 0.0
private var blobs: [Blob] = []
private var displayLinkSubscription: SharedDisplayLink.Subscription?
override init() {
super.init()
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = SharedDisplayLink.shared.add(framesPerSecond: .fps(30.0), { [weak self] in
guard let self else {
return
}
self.phase += 3.0 / 60.0
if self.phase >= 1.0 {
for i in 0 ..< self.blobs.count {
self.blobs[i].advance()
}
}
self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
self.setNeedsUpdate()
})
}
self.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = nil
}
self.isOpaque = false
self.blobs = (0 ..< 2).map { _ in
Blob(count: 8)
}
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(context: MetalSubjectContext) {
if self.bounds.isEmpty {
return
}
let phase = self.phase
let blobs = self.blobs
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0))), state: RenderState.self, layer: self, commands: { encoder, placement in
let rect = placement.effectiveRect
for i in 0 ..< blobs.count {
var points = blobs[i].interpolate(at: phase)
var count: Int32 = Int32(points.count)
let insetFraction: CGFloat = CGFloat(i) * 0.1
let blobRect = rect.insetBy(dx: insetFraction * 0.5 * rect.width, dy: insetFraction * 0.5 * rect.height)
var rect = SIMD4<Float>(Float(blobRect.minX), Float(blobRect.minY), Float(blobRect.width), Float(blobRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
encoder.setVertexBytes(&points, length: MemoryLayout<Float>.size * points.count, index: 1)
encoder.setVertexBytes(&count, length: MemoryLayout<Float>.size, index: 2)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3 * 8 * points.count)
}
})
}
}

View File

@ -0,0 +1,111 @@
import Foundation
import UIKit
final class ContentOverlayButton: UIButton, ContentOverlayView {
var overlayMaskLayer: CALayer {
return self.overlayBackgroundLayer
}
override static var layerClass: AnyClass {
return MirroringLayer.self
}
private let overlayBackgroundLayer: SimpleLayer
private let backgroundLayer: SimpleLayer
private var internalHighlighted = false
private var internalHighligthedChanged: (Bool) -> Void = { _ in }
var highligthedChanged: (Bool) -> Void = { _ in }
var action: (() -> Void)?
override init(frame: CGRect) {
self.overlayBackgroundLayer = SimpleLayer()
self.backgroundLayer = SimpleLayer()
super.init(frame: frame)
self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
let size: CGFloat = 56.0
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)))
self.overlayBackgroundLayer.contents = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
context.cgContext.setFillColor(UIColor.white.cgColor)
context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)))
UIGraphicsPopContext()
}.cgImage
self.backgroundLayer.contents = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
context.cgContext.setFillColor(UIColor.white.withAlphaComponent(0.2).cgColor)
context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)))
UIGraphicsPopContext()
}.cgImage
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: size, height: size))
(self.layer as? MirroringLayer)?.targetLayer = self.overlayBackgroundLayer
self.layer.addSublayer(self.backgroundLayer)
self.internalHighligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.alpha = 0.5
} else {
self.alpha = 1.0
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
self.action?()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
if !self.internalHighlighted {
self.internalHighlighted = true
self.highligthedChanged(true)
self.internalHighligthedChanged(true)
}
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.touchesCancelled(touches, with: event)
}
}

View File

@ -0,0 +1,176 @@
import Foundation
import UIKit
final class ContentView: UIView {
private let blobLayer: CallBlobsLayer
private let avatarLayer: AvatarLayer
private let titleView: TextView
private let statusView: StatusView
private var emojiView: KeyEmojiView?
let blurContentsLayer: SimpleLayer
private let videoLayer: MainVideoLayer
private var videoLayerMask: SimpleShapeLayer?
private var blurredVideoLayerMask: SimpleShapeLayer?
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?
private var phase: CGFloat = 0.0
private var isDisplayingVideo: Bool = false
private var videoDisplayFraction = AnimatedProperty<CGFloat>(0.0)
private var videoInput: VideoInput?
private let managedAnimations: ManagedAnimations
override init(frame: CGRect) {
self.blobLayer = CallBlobsLayer()
self.avatarLayer = AvatarLayer()
self.titleView = TextView()
self.statusView = StatusView()
self.blurContentsLayer = SimpleLayer()
self.videoLayer = MainVideoLayer()
self.managedAnimations = ManagedAnimations()
super.init(frame: frame)
self.layer.addSublayer(self.blobLayer)
self.layer.addSublayer(self.avatarLayer)
self.layer.addSublayer(self.videoLayer)
self.blurContentsLayer.addSublayer(self.videoLayer.blurredLayer)
self.addSubview(self.titleView)
self.addSubview(self.statusView)
self.avatarLayer.image = UIImage(named: "test")
self.managedAnimations.add(property: self.videoDisplayFraction)
self.managedAnimations.updated = { [weak self] in
guard let self else {
return
}
if let currentLayout = self.currentLayout {
self.update(size: currentLayout.size, insets: currentLayout.insets)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, insets: UIEdgeInsets) {
self.currentLayout = (size, insets)
if self.emojiView == nil {
let emojiView = KeyEmojiView(emoji: ["🐱", "🚂", "❄️", "🎨"])
self.emojiView = emojiView
self.addSubview(emojiView)
}
if let emojiView = self.emojiView {
emojiView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - emojiView.size.width, y: insets.top + 27.0), size: emojiView.size)
}
if self.videoInput == nil, let url = Bundle.main.url(forResource: "test2", withExtension: "mp4") {
self.videoInput = VideoInput(device: MetalContext.shared.device, url: url)
self.videoLayer.video = self.videoInput
}
self.phase += 3.0 / 60.0
self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
var avatarScale: CGFloat = 0.05 * sin(CGFloat(self.phase) * CGFloat.pi)
avatarScale *= 1.0 - self.videoDisplayFraction.value
let avatarSize: CGFloat = 136.0
let blobSize: CGFloat = 176.0
let expandedVideoRadius: CGFloat = sqrt(pow(size.width * 0.5, 2.0) + pow(size.height * 0.5, 2.0))
let avatarFrame = CGRect(origin: CGPoint(x: floor((size.width - avatarSize) * 0.5), y: CGFloat.animationInterpolator.interpolate(from: 222.0, to: floor((size.height - avatarSize) * 0.5), fraction: self.videoDisplayFraction.value)), size: CGSize(width: avatarSize, height: avatarSize))
let titleSize = self.titleView.update(string: "Emma Walters", fontSize: CGFloat.animationInterpolator.interpolate(from: 28.0, to: 17.0, fraction: self.videoDisplayFraction.value), fontWeight: CGFloat.animationInterpolator.interpolate(from: 0.0, to: 0.25, fraction: self.videoDisplayFraction.value), constrainedWidth: size.width - 16.0 * 2.0)
let titleFrame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) * 0.5, y: CGFloat.animationInterpolator.interpolate(from: avatarFrame.maxY + 39.0, to: insets.top + 17.0, fraction: self.videoDisplayFraction.value)), size: titleSize)
self.titleView.frame = titleFrame
let statusSize = self.statusView.update(state: .waiting(.requesting))
self.statusView.frame = CGRect(origin: CGPoint(x: (size.width - statusSize.width) * 0.5, y: titleFrame.maxY + CGFloat.animationInterpolator.interpolate(from: 4.0, to: 0.0, fraction: self.videoDisplayFraction.value)), size: statusSize)
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
self.avatarLayer.position = CGPoint(x: avatarFrame.midX, y: avatarFrame.midY)
self.avatarLayer.bounds = CGRect(origin: CGPoint(), size: avatarFrame.size)
let visibleAvatarScale = CGFloat.animationInterpolator.interpolate(from: 1.0 + avatarScale, to: expandedVideoRadius * 2.0 / avatarSize, fraction: self.videoDisplayFraction.value)
self.avatarLayer.transform = CATransform3DMakeScale(visibleAvatarScale, visibleAvatarScale, 1.0)
self.avatarLayer.opacity = Float(1.0 - self.videoDisplayFraction.value)
self.blobLayer.position = CGPoint(x: blobFrame.midX, y: blobFrame.midY)
self.blobLayer.bounds = CGRect(origin: CGPoint(), size: blobFrame.size)
self.blobLayer.transform = CATransform3DMakeScale(1.0 + avatarScale * 2.0, 1.0 + avatarScale * 2.0, 1.0)
let videoResolution = CGSize(width: 400.0, height: 400.0)
let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
let videoRenderingSize = CGSize(width: videoResolution.width * 2.0, height: videoResolution.height * 2.0)
self.videoLayer.frame = videoFrame
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoRenderingSize.width), height: Int(videoRenderingSize.height)))
self.videoLayer.blurredLayer.frame = videoFrame
let videoDisplayFraction = self.videoDisplayFraction.value
self.videoLayer.isHidden = videoDisplayFraction == 0.0
self.videoLayer.opacity = Float(videoDisplayFraction)
self.videoLayer.blurredLayer.isHidden = videoDisplayFraction == 0.0
if videoDisplayFraction != 0.0 && videoDisplayFraction != 1.0 {
let videoLayerMask: SimpleShapeLayer
if let current = self.videoLayerMask {
videoLayerMask = current
} else {
videoLayerMask = SimpleShapeLayer()
self.videoLayerMask = videoLayerMask
self.videoLayer.mask = videoLayerMask
}
let blurredVideoLayerMask: SimpleShapeLayer
if let current = self.blurredVideoLayerMask {
blurredVideoLayerMask = current
} else {
blurredVideoLayerMask = SimpleShapeLayer()
self.blurredVideoLayerMask = blurredVideoLayerMask
self.videoLayer.blurredLayer.mask = blurredVideoLayerMask
}
let fromRadius: CGFloat = avatarSize * 0.5
let toRadius = expandedVideoRadius
let maskPosition = CGPoint(x: avatarFrame.midX, y: avatarFrame.midY)
let maskRadius = CGFloat.animationInterpolator.interpolate(from: fromRadius, to: toRadius, fraction: videoDisplayFraction)
videoLayerMask.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: maskPosition.x - maskRadius, y: maskPosition.y - maskRadius), size: CGSize(width: maskRadius * 2.0, height: maskRadius * 2.0))).cgPath
blurredVideoLayerMask.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: maskPosition.x - maskRadius, y: maskPosition.y - maskRadius), size: CGSize(width: maskRadius * 2.0, height: maskRadius * 2.0))).cgPath
} else {
if let videoLayerMask = self.videoLayerMask {
self.videoLayerMask = nil
videoLayerMask.removeFromSuperlayer()
}
if let blurredVideoLayerMask = self.blurredVideoLayerMask {
self.blurredVideoLayerMask = nil
blurredVideoLayerMask.removeFromSuperlayer()
}
}
}
func toggleDisplayVideo() {
self.isDisplayingVideo = !self.isDisplayingVideo
self.videoDisplayFraction.animate(to: self.isDisplayingVideo ? 1.0 : 0.0, duration: 0.4, curve: .spring)
}
}

View File

@ -0,0 +1,43 @@
import Foundation
import UIKit
final class KeyEmojiView: UIView {
private let emojiViews: [TextView]
let size: CGSize
init(emoji: [String]) {
self.emojiViews = emoji.map { emoji in
TextView()
}
let itemSpacing: CGFloat = 3.0
var height: CGFloat = 0.0
var nextX = 0.0
for i in 0 ..< self.emojiViews.count {
if nextX != 0.0 {
nextX += itemSpacing
}
let emojiView = self.emojiViews[i]
let itemSize = emojiView.update(string: emoji[i], fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 100.0)
if height == 0.0 {
height = itemSize.height
}
emojiView.frame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize)
nextX += itemSize.width
}
self.size = CGSize(width: nextX, height: height)
super.init(frame: CGRect())
for emojiView in self.emojiViews {
self.addSubview(emojiView)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,256 @@
import Foundation
import UIKit
import MetalKit
import MetalPerformanceShaders
import Accelerate
func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
guard let cgImage = image.cgImage, let data = cgImage.dataProvider?.data, let bytes = CFDataGetBytePtr(data), let colorSpace = cgImage.colorSpace, case .rgb = colorSpace.model, cgImage.bitsPerPixel / cgImage.bitsPerComponent == 4 else {
return nil
}
let width = cgImage.width
let height = cgImage.width
var pixelBuffer: CVPixelBuffer? = nil
let _ = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, [
kCVPixelBufferIOSurfacePropertiesKey: NSDictionary()
] as CFDictionary, &pixelBuffer)
guard let pixelBuffer else {
return nil
}
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
defer {
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
}
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
return nil
}
var srcBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bytes), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: cgImage.bytesPerRow)
var dstBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddress), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: CVPixelBufferGetBytesPerRow(pixelBuffer))
vImageCopyBuffer(&srcBuffer, &dstBuffer, 4, vImage_Flags(kvImageDoNotTile))
return pixelBuffer
}
final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
var internalData: MetalSubjectInternalData?
let blurredLayer: MetalSubjectLayer
final class BlurState: ComputeState {
let computePipelineStateYUVToRGBA: MTLComputePipelineState
let computePipelineStateHorizontal: MTLComputePipelineState
let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale
required init?(
device: MTLDevice,
library: MTLLibrary
) {
guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
return nil
}
guard let computePipelineStateYUVToRGBA = try? device.makeComputePipelineState(function: functionVideoYUVToRGBA) else {
return nil
}
self.computePipelineStateYUVToRGBA = computePipelineStateYUVToRGBA
guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else {
return nil
}
guard let computePipelineStateHorizontal = try? device.makeComputePipelineState(function: gaussianBlurHorizontal) else {
return nil
}
self.computePipelineStateHorizontal = computePipelineStateHorizontal
guard let computePipelineStateVertical = try? device.makeComputePipelineState(function: gaussianBlurVertical) else {
return nil
}
self.computePipelineStateVertical = computePipelineStateVertical
self.downscaleKernel = MPSImageBilinearScale(device: device)
}
}
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(
device: MTLDevice,
library: MTLLibrary
) {
guard let vertexFunction = library.makeFunction(name: "mainVideoVertex"), let fragmentFunction = library.makeFunction(name: "mainVideoFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
var video: VideoInput? {
didSet {
self.video?.updated = { [weak self] in
self?.setNeedsUpdate()
}
self.setNeedsUpdate()
}
}
var renderSpec: RenderLayerSpec?
private var rgbaTexture: PooledTexture?
private var downscaledTexture: PooledTexture?
private var blurredHorizontalTexture: PooledTexture?
private var blurredVerticalTexture: PooledTexture?
override init() {
self.blurredLayer = MetalSubjectLayer()
super.init()
}
override init(layer: Any) {
self.blurredLayer = MetalSubjectLayer()
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(context: MetalSubjectContext) {
if self.isHidden {
return
}
guard let renderSpec = self.renderSpec else {
return
}
guard let videoTextures = self.video?.currentOutput else {
return
}
let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized)
if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalContext.shared.pooledTexture(spec: rgbaTextureSpec)
}
if self.downscaledTexture == nil {
self.downscaledTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
if self.blurredHorizontalTexture == nil {
self.blurredHorizontalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
if self.blurredVerticalTexture == nil {
self.blurredVerticalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
guard let rgbaTexture = self.rgbaTexture?.get(context: context) else {
return
}
let _ = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture in
guard let rgbaTexture else {
return
}
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVToRGBA)
computeEncoder.setTexture(videoTextures.y, index: 0)
computeEncoder.setTexture(videoTextures.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
})
if !self.blurredLayer.isHidden {
guard let downscaledTexture = self.downscaledTexture?.get(context: context), let blurredHorizontalTexture = self.blurredHorizontalTexture?.get(context: context), let blurredVerticalTexture = self.blurredVerticalTexture?.get(context: context) else {
return
}
let blurredTexture = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, downscaledTexture.placeholer, blurredHorizontalTexture.placeholer, blurredVerticalTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture, downscaledTexture, blurredHorizontalTexture, blurredVerticalTexture -> MTLTexture? in
guard let rgbaTexture, let downscaledTexture, let blurredHorizontalTexture, let blurredVerticalTexture else {
return nil
}
blurState.downscaleKernel.encode(commandBuffer: commandBuffer, sourceTexture: rgbaTexture, destinationTexture: downscaledTexture)
do {
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return nil
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (downscaledTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (downscaledTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
computeEncoder.setComputePipelineState(blurState.computePipelineStateHorizontal)
computeEncoder.setTexture(downscaledTexture, index: 0)
computeEncoder.setTexture(blurredHorizontalTexture, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.setComputePipelineState(blurState.computePipelineStateVertical)
computeEncoder.setTexture(blurredHorizontalTexture, index: 0)
computeEncoder.setTexture(blurredVerticalTexture, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
}
return blurredVerticalTexture
})
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self.blurredLayer, inputs: blurredTexture, commands: { encoder, placement, blurredTexture in
guard let blurredTexture else {
return
}
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
encoder.setFragmentTexture(blurredTexture, index: 0)
var brightness: Float = 1.4
var saturation: Float = 1.1
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: rgbaTexture.placeholer, commands: { encoder, placement, rgbaTexture in
guard let rgbaTexture else {
return
}
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
encoder.setFragmentTexture(rgbaTexture, index: 0)
var brightness: Float = 1.0
var saturation: Float = 1.0
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}

View File

@ -0,0 +1,140 @@
import Foundation
import UIKit
private final class AnimatedDotsLayer: SimpleLayer {
private let dotLayers: [SimpleLayer]
let size: CGSize
override init() {
self.dotLayers = (0 ..< 3).map { _ in
SimpleLayer()
}
let dotSpacing: CGFloat = 1.0
let dotSize = CGSize(width: 5.0, height: 5.0)
self.size = CGSize(width: CGFloat(self.dotLayers.count) * dotSize.width + CGFloat(self.dotLayers.count - 1) * dotSpacing, height: dotSize.height)
super.init()
let dotImage = UIGraphicsImageRenderer(size: dotSize).image(actions: { context in
context.cgContext.setFillColor(UIColor.white.cgColor)
context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: dotSize))
})
var nextX: CGFloat = 0.0
for dotLayer in self.dotLayers {
dotLayer.contents = dotImage.cgImage
dotLayer.frame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: dotSize)
nextX += dotSpacing + dotSize.width
self.addSublayer(dotLayer)
}
self.didEnterHierarchy = { [weak self] in
self?.updateAnimations()
}
}
override init(layer: Any) {
self.dotLayers = []
self.size = CGSize()
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateAnimations() {
if self.dotLayers[0].animation(forKey: "dotAnimation") != nil {
return
}
let animationDuration: Double = 0.6
for i in 0 ..< self.dotLayers.count {
let dotLayer = self.dotLayers[i]
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = animationDuration
animation.fromValue = 0.3
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
animation.timeOffset = CGFloat(self.dotLayers.count - 1 - i) * animationDuration * 0.33
dotLayer.add(animation, forKey: "dotAnimation")
}
}
}
final class StatusView: UIView {
enum WaitingState {
case requesting
case ringing
case generatingKeys
}
enum State {
case waiting(WaitingState)
}
private var textView: TextView
private var dotsLayer: AnimatedDotsLayer?
override init(frame: CGRect) {
self.textView = TextView()
super.init(frame: frame)
self.addSubview(self.textView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(state: State) -> CGSize {
let textString: String
var needsDots = false
switch state {
case let .waiting(waitingState):
needsDots = true
switch waitingState {
case .requesting:
textString = "Requesting"
case .ringing:
textString = "Ringing"
case .generatingKeys:
textString = "Exchanging encryption keys"
}
}
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 200.0)
self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)
var contentSize = textSize
let dotsSpacing: CGFloat = 6.0
if needsDots {
let dotsLayer: AnimatedDotsLayer
if let current = self.dotsLayer {
dotsLayer = current
} else {
dotsLayer = AnimatedDotsLayer()
self.dotsLayer = dotsLayer
self.layer.addSublayer(dotsLayer)
}
dotsLayer.frame = CGRect(origin: CGPoint(x: textSize.width + dotsSpacing, y: 1.0 + floor((textSize.height - dotsLayer.size.height) * 0.5)), size: dotsLayer.size)
contentSize.width += dotsSpacing + dotsLayer.size.width
} else if let dotsLayer = self.dotsLayer {
self.dotsLayer = nil
dotsLayer.removeFromSuperlayer()
}
return contentSize
}
}

View File

@ -0,0 +1,63 @@
import Foundation
import UIKit
final class TextView: UIView {
private struct Params: Equatable {
var string: String
var fontSize: CGFloat
var fontWeight: CGFloat
var constrainedWidth: CGFloat
}
private struct LayoutState: Equatable {
var params: Params
var size: CGSize
var attributedString: NSAttributedString
}
private var layoutState: LayoutState?
override init(frame: CGRect) {
super.init(frame: CGRect())
self.isOpaque = false
self.backgroundColor = nil
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(string: String, fontSize: CGFloat, fontWeight: CGFloat, constrainedWidth: CGFloat) -> CGSize {
let params = Params(string: string, fontSize: fontSize, fontWeight: fontWeight, constrainedWidth: constrainedWidth)
if let layoutState = self.layoutState, layoutState.params == params {
return layoutState.size
}
let font = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight(fontWeight))
let attributedString = NSAttributedString(string: string, attributes: [
.font: font,
.foregroundColor: UIColor.white
])
let stringBounds = attributedString.boundingRect(with: CGSize(width: constrainedWidth, height: 200.0), options: .usesLineFragmentOrigin, context: nil)
let stringSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
let size = CGSize(width: min(constrainedWidth, stringSize.width), height: stringSize.height)
let layoutState = LayoutState(params: params, size: size, attributedString: attributedString)
if self.layoutState != layoutState {
self.layoutState = layoutState
self.setNeedsDisplay()
}
return size
}
override func draw(_ rect: CGRect) {
guard let layoutState = self.layoutState else {
return
}
layoutState.attributedString.draw(with: rect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], context: nil)
}
}

View File

@ -0,0 +1,60 @@
import Foundation
import UIKit
protocol ContentOverlayView: UIView {
var overlayMaskLayer: CALayer { get }
}
final class ContentOverlayContainer: UIView {
private let overlayLayer: ContentOverlayLayer
init(overlayLayer: ContentOverlayLayer) {
self.overlayLayer = overlayLayer
super.init(frame: CGRect())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func addSubview(_ view: UIView) {
super.addSubview(view)
if let view = view as? ContentOverlayView {
self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
}
}
override func insertSubview(_ view: UIView, at index: Int) {
super.insertSubview(view, at: index)
if let view = view as? ContentOverlayView {
self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
}
}
override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
super.insertSubview(view, aboveSubview: siblingSubview)
if let view = view as? ContentOverlayView {
self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
}
}
override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
super.insertSubview(view, belowSubview: siblingSubview)
if let view = view as? ContentOverlayView {
self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
}
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
if let view = subview as? ContentOverlayView {
view.overlayMaskLayer.removeFromSuperlayer()
}
}
}

View File

@ -0,0 +1,71 @@
import Foundation
import UIKit
final class ContentOverlayLayer: SimpleLayer {
private struct Params: Equatable {
var size: CGSize
var contentInsets: UIEdgeInsets
init(size: CGSize, contentInsets: UIEdgeInsets) {
self.size = size
self.contentInsets = contentInsets
}
}
var contentsLayer: CALayer? {
didSet {
if self.contentsLayer !== oldValue {
oldValue?.mask = nil
oldValue?.removeFromSuperlayer()
if let contentsLayer = self.contentsLayer {
contentsLayer.mask = self.maskContentLayer
self.addSublayer(contentsLayer)
if let params = self.params {
let size = params.size
let contentInsets = params.contentInsets
self.params = nil
self.update(size: size, contentInsets: contentInsets)
}
}
}
}
}
let maskContentLayer: SimpleLayer
private var params: Params?
override init() {
self.maskContentLayer = SimpleLayer()
super.init()
}
override init(layer: Any) {
self.maskContentLayer = SimpleLayer()
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, contentInsets: UIEdgeInsets) {
let params = Params(size: size, contentInsets: contentInsets)
if self.params == params {
return
}
self.params = params
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
self.maskContentLayer.frame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: size)
if let contentsLayer = self.contentsLayer {
contentsLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width - (contentInsets.left + contentInsets.right), height: size.height - (contentInsets.top + contentInsets.bottom)))
}
}
}

View File

@ -0,0 +1,118 @@
import AVFoundation
import Metal
import CoreVideo
class VideoInput {
final class Output {
let y: MTLTexture
let uv: MTLTexture
init(y: MTLTexture, uv: MTLTexture) {
self.y = y
self.uv = uv
}
}
private let playerLooper: AVPlayerLooper
private let queuePlayer: AVQueuePlayer
private var videoOutput: AVPlayerItemVideoOutput
private var device: MTLDevice
private var textureCache: CVMetalTextureCache?
private var targetItem: AVPlayerItem?
private(set) var currentOutput: Output?
var updated: (() -> Void)?
private var displayLink: SharedDisplayLink.Subscription?
init?(device: MTLDevice, url: URL) {
self.device = device
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
let playerItem = AVPlayerItem(url: url)
self.queuePlayer = AVQueuePlayer(playerItem: playerItem)
self.playerLooper = AVPlayerLooper(player: self.queuePlayer, templateItem: playerItem)
let outputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelBufferMetalCompatibilityKey as String: true
]
self.videoOutput = AVPlayerItemVideoOutput(outputSettings: outputSettings)
self.queuePlayer.play()
self.displayLink = SharedDisplayLink.shared.add(framesPerSecond: .fps(60.0), { [weak self] in
guard let self else {
return
}
if self.updateOutput() {
self.updated?()
}
})
}
private func updateOutput() -> Bool {
if self.targetItem !== self.queuePlayer.currentItem {
self.targetItem?.remove(self.videoOutput)
self.targetItem = self.queuePlayer.currentItem
if let targetItem = self.targetItem {
targetItem.add(self.videoOutput)
}
}
guard let currentItem = self.targetItem else {
return false
}
let currentTime = currentItem.currentTime()
guard self.videoOutput.hasNewPixelBuffer(forItemTime: currentTime) else {
return false
}
var pixelBuffer: CVPixelBuffer?
pixelBuffer = self.videoOutput.copyPixelBuffer(forItemTime: currentTime, itemTimeForDisplay: nil)
guard let buffer = pixelBuffer else {
return false
}
let width = CVPixelBufferGetWidth(buffer)
let height = CVPixelBufferGetHeight(buffer)
var cvMetalTextureY: CVMetalTexture?
var status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache!, buffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY)
guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else {
return false
}
var cvMetalTextureUV: CVMetalTexture?
status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache!, buffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV)
guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else {
return false
}
self.currentOutput = Output(y: yTexture, uv: uvTexture)
return true
}
}
class ControlVideoInput {
private let playerLooper: AVPlayerLooper
private let queuePlayer: AVQueuePlayer
private let playerLayer: AVPlayerLayer
private var targetItem: AVPlayerItem?
init(url: URL, playerLayer: AVPlayerLayer) {
let playerItem = AVPlayerItem(url: url)
self.queuePlayer = AVQueuePlayer(playerItem: playerItem)
self.playerLooper = AVPlayerLooper(player: self.queuePlayer, templateItem: playerItem)
self.playerLayer = playerLayer
playerLayer.player = self.queuePlayer
self.queuePlayer.play()
}
}

View File

@ -0,0 +1,97 @@
import Foundation
import UIKit
final class MirroringLayer: SimpleLayer {
var targetLayer: SimpleLayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var position: CGPoint {
get {
return super.position
} set(value) {
if let targetLayer = self.targetLayer {
targetLayer.position = value
}
super.position = value
}
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let targetLayer = self.targetLayer {
targetLayer.bounds = value
}
super.bounds = value
}
}
override var opacity: Float {
get {
return super.opacity
} set(value) {
if let targetLayer = self.targetLayer {
targetLayer.opacity = value
}
super.opacity = value
}
}
override public var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let targetLayer = self.targetLayer {
targetLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override public var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let targetLayer = self.targetLayer {
targetLayer.transform = value
}
super.transform = value
}
}
override public func add(_ animation: CAAnimation, forKey key: String?) {
if let targetLayer = self.targetLayer {
targetLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override public func removeAllAnimations() {
if let targetLayer = self.targetLayer {
targetLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override public func removeAnimation(forKey: String) {
if let targetLayer = self.targetLayer {
targetLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
}

View File

@ -0,0 +1,94 @@
import Foundation
import UIKit
public final class PrivateCallScreen: UIView {
private let backgroundLayer: CallBackgroundLayer
private let contentOverlayLayer: ContentOverlayLayer
private let contentOverlayContainer: ContentOverlayContainer
private let blurContentsLayer: SimpleLayer
private let blurBackgroundLayer: CallBackgroundLayer
private let contentView: ContentView
private let buttonGroupView: ButtonGroupView
public override init(frame: CGRect) {
self.blurContentsLayer = SimpleLayer()
self.backgroundLayer = CallBackgroundLayer(isBlur: false)
self.contentOverlayLayer = ContentOverlayLayer()
self.contentOverlayContainer = ContentOverlayContainer(overlayLayer: self.contentOverlayLayer)
self.blurBackgroundLayer = CallBackgroundLayer(isBlur: true)
self.contentView = ContentView(frame: CGRect())
self.buttonGroupView = ButtonGroupView()
super.init(frame: frame)
self.contentOverlayLayer.contentsLayer = self.blurContentsLayer
self.layer.addSublayer(self.backgroundLayer)
self.blurContentsLayer.addSublayer(self.blurBackgroundLayer)
self.addSubview(self.contentView)
self.blurContentsLayer.addSublayer(self.contentView.blurContentsLayer)
self.layer.addSublayer(self.contentOverlayLayer)
self.addSubview(self.contentOverlayContainer)
self.contentOverlayContainer.addSubview(self.buttonGroupView)
self.buttonGroupView.toggleVideo = { [weak self] in
guard let self else {
return
}
self.contentView.toggleDisplayVideo()
}
}
public required init?(coder: NSCoder) {
fatalError()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
return result
}
public func update(size: CGSize, insets: UIEdgeInsets) {
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
let aspect: CGFloat = size.width / size.height
let sizeNorm: CGFloat = 64.0
let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
let edgeSize: Int = 2
let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(edgeSize) / renderingSize.width * backgroundFrame.width, dy: -CGFloat(edgeSize) / renderingSize.height * backgroundFrame.height)
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
self.backgroundLayer.frame = visualBackgroundFrame
self.contentOverlayLayer.frame = CGRect(origin: CGPoint(), size: size)
self.contentOverlayLayer.update(size: size, contentInsets: UIEdgeInsets())
self.contentOverlayContainer.frame = CGRect(origin: CGPoint(), size: size)
self.blurBackgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
self.blurBackgroundLayer.frame = visualBackgroundFrame
self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: size)
self.buttonGroupView.update(size: size)
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
self.contentView.update(size: size, insets: insets)
}
}

View File

@ -0,0 +1,921 @@
import Foundation
import Metal
import UIKit
import IOSurface
import ShelfPack
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
public final class Placeholder<Resolved> {
var contents: Resolved?
}
private let noInputPlaceholder: Placeholder<Void> = {
let value = Placeholder<Void>()
value.contents = Void()
return value
}()
private struct PlaceholderResolveError: Error {
}
private func resolvePlaceholder<T>(_ value: Placeholder<T>) throws -> T {
guard let contents = value.contents else {
throw PlaceholderResolveError()
}
return contents
}
public struct TextureSpec: Equatable {
public enum PixelFormat {
case r8UnsignedNormalized
case rgba8UnsignedNormalized
}
public var width: Int
public var height: Int
public var pixelFormat: PixelFormat
public init(width: Int, height: Int, pixelFormat: PixelFormat) {
self.width = width
self.height = height
self.pixelFormat = pixelFormat
}
}
extension TextureSpec.PixelFormat {
var metalFormat: MTLPixelFormat {
switch self {
case .r8UnsignedNormalized:
return .r8Unorm
case .rgba8UnsignedNormalized:
return .rgba8Unorm
}
}
}
public final class TexturePlaceholder {
public let placeholer: Placeholder<MTLTexture?>
public let spec: TextureSpec
init(placeholer: Placeholder<MTLTexture?>, spec: TextureSpec) {
self.placeholer = placeholer
self.spec = spec
}
}
public struct RenderSize: Equatable {
public var width: Int
public var height: Int
public init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
public struct RenderLayerSpec: Equatable {
public var size: RenderSize
public var edgeInset: Int
public init(size: RenderSize, edgeInset: Int = 0) {
self.size = size
self.edgeInset = edgeInset
}
}
private extension RenderLayerSpec {
var allocationWidth: Int {
return self.size.width + self.edgeInset * 2
}
var allocationHeight: Int {
return self.size.height + self.edgeInset * 2
}
}
public struct RenderLayerPlacement: Equatable {
public var effectiveRect: CGRect
public init(effectiveRect: CGRect) {
self.effectiveRect = effectiveRect
}
}
public protocol RenderToLayerState: AnyObject {
var pipelineState: MTLRenderPipelineState { get }
init?(device: MTLDevice, library: MTLLibrary)
}
public protocol ComputeState: AnyObject {
init?(device: MTLDevice, library: MTLLibrary)
}
open class MetalSubjectLayer: SimpleLayer {
fileprivate var internalId: Int = -1
fileprivate var surfaceAllocation: MetalContext.SurfaceAllocation?
public override init() {
super.init()
self.setNeedsDisplay()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func setNeedsDisplay() {
if let subject = self as? MetalSubject {
subject.setNeedsUpdate()
}
}
}
public final class PooledTexture {
final class Texture {
let value: MTLTexture
var isInUse: Bool = false
init(value: MTLTexture) {
self.value = value
}
}
public let spec: TextureSpec
private let textures: [Texture]
init(device: MTLDevice, spec: TextureSpec) {
self.spec = spec
self.textures = (0 ..< 3).compactMap { _ -> Texture? in
let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: spec.pixelFormat.metalFormat, width: spec.width, height: spec.height, mipmapped: false)
descriptor.storageMode = .private
descriptor.usage = [.shaderRead, .shaderWrite]
guard let texture = device.makeTexture(descriptor: descriptor) else {
return nil
}
return Texture(value: texture)
}
}
public func get(context: MetalSubjectContext) -> TexturePlaceholder? {
#if DEBUG
if context.freeResourcesOnCompletion.contains(where: { $0 === self }) {
assertionFailure("Trying to get PooledTexture more than once per update cycle")
}
#endif
for texture in self.textures {
if !texture.isInUse {
texture.isInUse = true
let placeholder = Placeholder<MTLTexture?>()
placeholder.contents = texture.value
context.freeResourcesOnCompletion.append(texture)
return TexturePlaceholder(placeholer: placeholder, spec: self.spec)
}
}
print("PooledTexture: all textures are in use")
return nil
}
}
public final class MetalSubjectContext {
fileprivate final class ComputeOperation {
let commands: (MTLCommandBuffer) -> Void
init(commands: @escaping (MTLCommandBuffer) -> Void) {
self.commands = commands
}
}
fileprivate final class RenderToLayerOperation {
let spec: RenderLayerSpec
let state: RenderToLayerState
weak var layer: MetalSubjectLayer?
let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
init(
spec: RenderLayerSpec,
state: RenderToLayerState,
layer: MetalSubjectLayer,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
) {
self.spec = spec
self.state = state
self.layer = layer
self.commands = commands
}
}
private let device: MTLDevice
private let impl: MetalContext.Impl
fileprivate var computeOperations: [ComputeOperation] = []
fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
fileprivate var freeResourcesOnCompletion: [PooledTexture.Texture] = []
fileprivate init(device: MTLDevice, impl: MetalContext.Impl) {
self.device = device
self.impl = impl
}
public func renderToLayer<RenderToLayerStateType: RenderToLayerState, each Resolved>(
spec: RenderLayerSpec,
state: RenderToLayerStateType.Type,
layer: MetalSubjectLayer,
inputs: repeat Placeholder<each Resolved>,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement, repeat each Resolved) -> Void
) {
let stateTypeId = ObjectIdentifier(state)
let resolvedState: RenderToLayerStateType
if let current = self.impl.renderStates[stateTypeId] as? RenderToLayerStateType {
resolvedState = current
} else {
guard let value = RenderToLayerStateType(device: self.device, library: self.impl.library) else {
assertionFailure("Could not initialize render state \(state)")
return
}
resolvedState = value
self.impl.renderStates[stateTypeId] = resolvedState
}
let operation = RenderToLayerOperation(
spec: spec,
state: resolvedState,
layer: layer,
commands: { encoder, placement in
let resolvedInputs: (repeat each Resolved)
do {
resolvedInputs = (repeat try resolvePlaceholder(each inputs))
} catch {
print("Could not resolve renderToLayer inputs")
return
}
commands(encoder, placement, repeat each resolvedInputs)
}
)
if self.renderToLayerOperationsGroupedByState[stateTypeId] == nil {
self.renderToLayerOperationsGroupedByState[stateTypeId] = [operation]
} else {
self.renderToLayerOperationsGroupedByState[stateTypeId]?.append(operation)
}
}
public func renderToLayer<RenderToLayerStateType: RenderToLayerState>(
spec: RenderLayerSpec,
state: RenderToLayerStateType.Type,
layer: MetalSubjectLayer,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
) {
self.renderToLayer(spec: spec, state: state, layer: layer, inputs: noInputPlaceholder, commands: { encoder, placement, _ in
commands(encoder, placement)
})
}
public func compute<ComputeStateType: ComputeState, each Resolved, Output>(
state: ComputeStateType.Type,
inputs: repeat Placeholder<each Resolved>,
commands: @escaping (MTLCommandBuffer, ComputeStateType, repeat each Resolved) -> Output
) -> Placeholder<Output> {
let stateTypeId = ObjectIdentifier(state)
let resolvedState: ComputeStateType
if let current = self.impl.computeStates[stateTypeId] as? ComputeStateType {
resolvedState = current
} else {
guard let value = ComputeStateType(device: self.device, library: self.impl.library) else {
assertionFailure("Could not initialize compute state \(state)")
return Placeholder()
}
resolvedState = value
self.impl.computeStates[stateTypeId] = resolvedState
}
let resultPlaceholder = Placeholder<Output>()
self.computeOperations.append(ComputeOperation(commands: { commandBuffer in
let resolvedInputs: (repeat each Resolved)
do {
resolvedInputs = (repeat try resolvePlaceholder(each inputs))
} catch {
print("Could not resolve renderToLayer inputs")
return
}
resultPlaceholder.contents = commands(commandBuffer, resolvedState, repeat each resolvedInputs)
}))
return resultPlaceholder
}
public func compute<ComputeStateType: ComputeState, Output>(
state: ComputeStateType.Type,
commands: @escaping (MTLCommandBuffer, ComputeStateType) -> Output
) -> Placeholder<Output> {
return self.compute(state: state, inputs: noInputPlaceholder, commands: { commandBuffer, state, _ in
return commands(commandBuffer, state)
})
}
public func temporaryTexture(spec: TextureSpec) -> TexturePlaceholder {
let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: spec.pixelFormat.metalFormat, width: spec.width, height: spec.height, mipmapped: false)
descriptor.storageMode = .private
descriptor.usage = [.shaderRead, .shaderWrite]
let placeholder = Placeholder<MTLTexture?>()
placeholder.contents = self.impl.device.makeTexture(descriptor: descriptor)
return TexturePlaceholder(placeholer: placeholder, spec: spec)
}
}
public final class MetalSubjectInternalData {
var internalId: Int = -1
var renderSurfaceAllocation: MetalContext.SurfaceAllocation?
init() {
}
}
public protocol MetalSubject: AnyObject {
var internalData: MetalSubjectInternalData? { get set }
func setNeedsUpdate()
func update(context: MetalSubjectContext)
}
public extension MetalSubject {
func setNeedsUpdate() {
MetalContext.shared.impl.addSubjectNeedsUpdate(subject: self)
}
}
#if targetEnvironment(simulator)
@available(iOS 13.0, *)
#endif
private final class MetalEventLayer: CAMetalLayer {
var onDisplay: (() -> Void)?
override func display() {
self.onDisplay?()
}
}
public final class MetalContext {
struct SurfaceAllocation {
struct Phase {
let subRect: CGRect
let renderingRect: CGRect
let contentsRect: CGRect
}
let surfaceId: Int
let allocationId0: Int32
let allocationId1: Int32
let renderingParameters: RenderLayerSpec
let phase0: Phase
let phase1: Phase
var currentPhase: Int = 0
var effectivePhase: Phase {
if self.currentPhase == 0 {
return self.phase0
} else {
return self.phase1
}
}
init(surfaceId: Int, allocationId0: Int32, allocationId1: Int32, renderingParameters: RenderLayerSpec, phase0: Phase, phase1: Phase) {
self.surfaceId = surfaceId
self.allocationId0 = allocationId0
self.allocationId1 = allocationId1
self.renderingParameters = renderingParameters
self.phase0 = phase0
self.phase1 = phase1
}
}
fileprivate final class Surface {
let id: Int
let width: Int
let height: Int
let ioSurface: IOSurface
let texture: MTLTexture
let packContext: ShelfPackContext
init?(id: Int, device: MTLDevice, width: Int, height: Int) {
self.id = id
self.width = width
self.height = height
self.packContext = ShelfPackContext(width: Int32(width), height: Int32(height))
let ioSurfaceProperties: [String: Any] = [
kIOSurfaceWidth as String: width,
kIOSurfaceHeight as String: height,
kIOSurfaceBytesPerElement as String: 4,
kIOSurfacePixelFormat as String: kCVPixelFormatType_32BGRA
]
guard let ioSurface = IOSurfaceCreate(ioSurfaceProperties as CFDictionary) else {
return nil
}
self.ioSurface = ioSurface
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = Int(width)
textureDescriptor.height = Int(height)
textureDescriptor.storageMode = .shared
textureDescriptor.usage = .renderTarget
guard let texture = device.makeTexture(descriptor: textureDescriptor, iosurface: ioSurface, plane: 0) else {
return nil
}
self.texture = texture
}
private struct AllocationLayout {
let subRect: CGRect
let renderingRect: CGRect
let contentsRect: CGRect
init(baseRect: CGRect, surfaceWidth: Int, surfaceHeight: Int) {
self.subRect = CGRect(origin: CGPoint(x: baseRect.minX, y: baseRect.minY), size: CGSize(width: baseRect.width, height: baseRect.height))
self.renderingRect = CGRect(origin: CGPoint(x: self.subRect.minX / CGFloat(surfaceWidth), y: self.subRect.minY / CGFloat(surfaceHeight)), size: CGSize(width: self.subRect.width / CGFloat(surfaceWidth), height: self.subRect.height / CGFloat(surfaceHeight)))
self.contentsRect = CGRect(origin: CGPoint(x: self.subRect.minX / CGFloat(surfaceWidth), y: 1.0 - self.subRect.minY / CGFloat(surfaceHeight) - self.subRect.height / CGFloat(surfaceHeight)), size: CGSize(width: self.subRect.width / CGFloat(surfaceWidth), height: self.subRect.height / CGFloat(surfaceHeight)))
}
}
func allocateIfPossible(renderingParameters: RenderLayerSpec) -> SurfaceAllocation? {
let width = renderingParameters.allocationWidth
let height = renderingParameters.allocationHeight
let item0 = self.packContext.addItem(withWidth: Int32(width), height: Int32(height))
let item1 = self.packContext.addItem(withWidth: Int32(width), height: Int32(height))
if item0.itemId != -1 && item1.itemId != -1 {
let layout0 = AllocationLayout(
baseRect: CGRect(origin: CGPoint(x: CGFloat(item0.x), y: CGFloat(item0.y)), size: CGSize(width: CGFloat(item0.width), height: CGFloat(item0.height))),
surfaceWidth: self.width,
surfaceHeight: self.height
)
let layout1 = AllocationLayout(
baseRect: CGRect(origin: CGPoint(x: CGFloat(item1.x), y: CGFloat(item1.y)), size: CGSize(width: CGFloat(item1.width), height: CGFloat(item1.height))),
surfaceWidth: self.width,
surfaceHeight: self.height
)
return SurfaceAllocation(
surfaceId: self.id,
allocationId0: item0.itemId,
allocationId1: item1.itemId,
renderingParameters: renderingParameters,
phase0: SurfaceAllocation.Phase(
subRect: layout0.subRect,
renderingRect: layout0.renderingRect,
contentsRect: layout0.contentsRect
),
phase1: SurfaceAllocation.Phase(
subRect: layout1.subRect,
renderingRect: layout1.renderingRect,
contentsRect: layout1.contentsRect
)
)
} else {
if item0.itemId != -1 {
self.packContext.removeItem(item0.itemId)
}
if item1.itemId != -1 {
self.packContext.removeItem(item1.itemId)
}
return nil
}
}
func removeAllocation(id: Int32) {
self.packContext.removeItem(id)
}
}
private final class SubjectReference {
weak var subject: MetalSubject?
init(subject: MetalSubject) {
self.subject = subject
}
}
fileprivate final class Impl {
let device: MTLDevice
let library: MTLLibrary
let commandQueue: MTLCommandQueue
let clearPipelineState: MTLRenderPipelineState
#if targetEnvironment(simulator)
let _layer: CALayer
@available(iOS 13.0, *)
var layer: MetalEventLayer {
return self._layer as! MetalEventLayer
}
#else
let layer: MetalEventLayer
#endif
var nextSurfaceId: Int = 0
var surfaces: [Int: Surface] = [:]
private var nextLayerId: Int = 0
private var nextSubjectId: Int = 0
private var updatedSubjectIds: [Int] = []
private var updatedSubjects: [SubjectReference] = []
private var scheduledClearAllocations: [SurfaceAllocation] = []
fileprivate var renderStates: [ObjectIdentifier: RenderToLayerState] = [:]
fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:]
init?(device: MTLDevice) {
let mainBundle = Bundle(for: Impl.self)
guard let path = mainBundle.path(forResource: "MetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
self.device = device
guard let commandQueue = device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
self.library = library
guard let vertexFunction = library.makeFunction(name: "clearVertex") else {
return nil
}
guard let fragmentFunction = library.makeFunction(name: "clearFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let clearPipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.clearPipelineState = clearPipelineState
#if targetEnvironment(simulator)
if #available(iOS 13.0, *) {
self._layer = MetalEventLayer()
} else {
self._layer = CALayer()
}
#else
self.layer = MetalEventLayer()
#endif
#if targetEnvironment(simulator)
@available(iOS 13.0, *)
#endif
func configureLayer(layer: MetalEventLayer, device: MTLDevice, impl: Impl) {
layer.drawableSize = CGSize(width: 32, height: 32)
layer.contentsScale = 1.0
layer.device = device
layer.presentsWithTransaction = true
layer.framebufferOnly = false
layer.onDisplay = { [unowned impl] in
impl.display()
}
}
#if targetEnvironment(simulator)
if #available(iOS 13.0, *) {
configureLayer(layer: self.layer, device: self.device, impl: self)
}
#else
configureLayer(layer: self.layer, device: self.device, impl: self)
#endif
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addSurface(minSize: CGSize) -> Surface? {
let surfaceId = self.nextSurfaceId
self.nextSurfaceId += 1
let surfaceWidth = max(1024, alignUp(size: Int(minSize.width), align: 64))
let surfaceHeight = max(512, alignUp(size: Int(minSize.height), align: 64))
let surface = Surface(id: surfaceId, device: self.device, width: surfaceWidth, height: surfaceHeight)
self.surfaces[surfaceId] = surface
return surface
}
private func refreshLayerAllocation(layer: MetalSubjectLayer, renderSpec: RenderLayerSpec) {
var previousSurfaceId: Int?
var updatedSurfaceId: Int?
if let allocation = layer.surfaceAllocation {
previousSurfaceId = allocation.surfaceId
if renderSpec != allocation.renderingParameters {
layer.surfaceAllocation = nil
self.scheduledClearAllocations.append(allocation)
}
}
if layer.internalId != -1 {
let renderingParameters = renderSpec
if let currentAllocation = layer.surfaceAllocation {
var updatedAllocation = currentAllocation
updatedAllocation.currentPhase = updatedAllocation.currentPhase == 0 ? 1 : 0
layer.surfaceAllocation = updatedAllocation
updatedSurfaceId = updatedAllocation.surfaceId
layer.contentsRect = updatedAllocation.effectivePhase.contentsRect
} else {
for (_, surface) in self.surfaces {
if let allocation = surface.allocateIfPossible(renderingParameters: renderingParameters) {
layer.surfaceAllocation = allocation
layer.contentsRect = allocation.effectivePhase.contentsRect
updatedSurfaceId = allocation.surfaceId
break
}
}
if updatedSurfaceId == nil {
if let surface = self.addSurface(minSize: CGSize(width: CGFloat(renderingParameters.allocationWidth) * 2.0, height: CGFloat(renderingParameters.allocationHeight))) {
if let allocation = surface.allocateIfPossible(renderingParameters: renderingParameters) {
layer.surfaceAllocation = allocation
layer.contentsRect = allocation.effectivePhase.contentsRect
updatedSurfaceId = allocation.surfaceId
}
}
}
}
} else {
if let currentAllocation = layer.surfaceAllocation {
layer.surfaceAllocation = nil
self.scheduledClearAllocations.append(currentAllocation)
}
}
if previousSurfaceId != updatedSurfaceId {
if let updatedSurfaceId {
layer.contents = self.surfaces[updatedSurfaceId]?.ioSurface
} else {
layer.contents = nil
}
}
}
func addSubjectNeedsUpdate(subject: MetalSubject) {
let internalData: MetalSubjectInternalData
if let current = subject.internalData {
internalData = current
} else {
internalData = MetalSubjectInternalData()
subject.internalData = internalData
}
let internalId: Int
if internalData.internalId != -1 {
internalId = internalData.internalId
} else {
internalId = self.nextSubjectId
self.nextSubjectId += 1
internalData.internalId = internalId
}
let isFirst = self.updatedSubjectIds.isEmpty
if !self.updatedSubjectIds.contains(internalId) {
self.updatedSubjectIds.append(internalId)
self.updatedSubjects.append(SubjectReference(subject: subject))
}
if isFirst {
#if targetEnvironment(simulator)
if #available(iOS 13.0, *) {
self.layer.setNeedsDisplay()
}
#else
self.layer.setNeedsDisplay()
#endif
}
}
func display() {
if !self.scheduledClearAllocations.isEmpty {
for allocation in self.scheduledClearAllocations {
if let surface = self.surfaces[allocation.surfaceId] {
surface.removeAllocation(id: allocation.allocationId0)
surface.removeAllocation(id: allocation.allocationId1)
}
}
self.scheduledClearAllocations.removeAll()
//TODO:remove clear empty surfaces
}
if self.updatedSubjects.isEmpty {
return
}
let wereActionsDisabled = CATransaction.disableActions()
CATransaction.setDisableActions(true)
defer {
CATransaction.setDisableActions(wereActionsDisabled)
}
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
let subjectContext = MetalSubjectContext(device: device, impl: self)
for subjectReference in self.updatedSubjects {
guard let subject = subjectReference.subject else {
continue
}
subject.update(context: subjectContext)
}
self.updatedSubjects.removeAll()
self.updatedSubjectIds.removeAll()
if !subjectContext.computeOperations.isEmpty {
for computeOperation in subjectContext.computeOperations {
computeOperation.commands(commandBuffer)
}
}
if !subjectContext.renderToLayerOperationsGroupedByState.isEmpty {
for (_, renderToLayerOperations) in subjectContext.renderToLayerOperationsGroupedByState {
for renderToLayerOperation in renderToLayerOperations {
guard let layer = renderToLayerOperation.layer else {
continue
}
if layer.internalId == -1 {
layer.internalId = self.nextLayerId
self.nextLayerId += 1
}
self.refreshLayerAllocation(layer: layer, renderSpec: renderToLayerOperation.spec)
}
}
var surfaceIds: [Int] = []
for (id, surface) in self.surfaces {
surfaceIds.append(id)
var clearQuads: [SIMD2<Float>] = []
for (_, renderToLayerOperations) in subjectContext.renderToLayerOperationsGroupedByState {
for renderToLayerOperation in renderToLayerOperations {
guard let layer = renderToLayerOperation.layer else {
continue
}
guard let surfaceAllocation = layer.surfaceAllocation, surfaceAllocation.surfaceId == id else {
continue
}
let layerRect = surfaceAllocation.effectivePhase.renderingRect
//let edgeSize = surfaceAllocation.renderingParameters.edgeInset
//let renderSize = CGSize(width: surfaceAllocation.renderingParameters.size.width, height: surfaceAllocation.renderingParameters.size.height)
//let kx = (CGFloat(edgeSize) * 2.0 + renderSize.width) / layerRect.width
//let ky = (CGFloat(edgeSize) * 2.0 + renderSize.height) / layerRect.height
//let insetX = CGFloat(edgeSize) / kx
//let insetY = CGFloat(edgeSize) / ky
let quadVertices: [SIMD2<Float>] = [
SIMD2<Float>(Float(layerRect.minX), Float(layerRect.minY)),
SIMD2<Float>(Float(layerRect.maxX), Float(layerRect.minY)),
SIMD2<Float>(Float(layerRect.minX), Float(layerRect.maxY)),
SIMD2<Float>(Float(layerRect.maxX), Float(layerRect.minY)),
SIMD2<Float>(Float(layerRect.minX), Float(layerRect.maxY)),
SIMD2<Float>(Float(layerRect.maxX), Float(layerRect.maxY))
].map { v in
var v = v
v.y = -1.0 + v.y * 2.0
v.x = -1.0 + v.x * 2.0
return v
}
clearQuads.append(contentsOf: quadVertices)
}
}
if !subjectContext.renderToLayerOperationsGroupedByState.isEmpty || !clearQuads.isEmpty {
let renderPass = MTLRenderPassDescriptor()
renderPass.colorAttachments[0].texture = surface.texture
renderPass.colorAttachments[0].loadAction = .load
renderPass.colorAttachments[0].storeAction = .store
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) else {
return
}
if !clearQuads.isEmpty {
renderEncoder.setRenderPipelineState(self.clearPipelineState)
//TODO:use buffer if too many vertices
renderEncoder.setVertexBytes(clearQuads, length: 4 * clearQuads.count * 2, index: 0)
var renderingBackgroundColor = SIMD4<Float>(0.0, 0.0, 0.0, 0.0)
renderEncoder.setFragmentBytes(&renderingBackgroundColor, length: 4 * 4, index: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: clearQuads.count)
}
for (stateId, renderToLayerOperations) in subjectContext.renderToLayerOperationsGroupedByState {
guard let state = self.renderStates[stateId] else {
continue
}
if !renderToLayerOperations.isEmpty {
renderEncoder.setRenderPipelineState(state.pipelineState)
}
for renderToLayerOperation in renderToLayerOperations {
guard let layer = renderToLayerOperation.layer else {
continue
}
guard let surfaceAllocation = layer.surfaceAllocation, surfaceAllocation.surfaceId == id else {
continue
}
renderToLayerOperation.commands(renderEncoder, RenderLayerPlacement(effectiveRect: surfaceAllocation.effectivePhase.renderingRect))
}
}
renderEncoder.endEncoding()
}
}
}
if !subjectContext.freeResourcesOnCompletion.isEmpty {
let freeResourcesOnCompletion = subjectContext.freeResourcesOnCompletion
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
for resource in freeResourcesOnCompletion {
resource.isInUse = false
}
}
}
}
#if DEBUG
#if targetEnvironment(simulator)
if #available(iOS 13.0, *) {
if let drawable = self.layer.nextDrawable() {
commandBuffer.present(drawable)
}
}
#else
if let drawable = self.layer.nextDrawable() {
commandBuffer.present(drawable)
}
#endif
#endif
commandBuffer.commit()
commandBuffer.waitUntilScheduled()
}
}
public static let shared = MetalContext()
fileprivate let impl: Impl
public var rootLayer: CALayer {
return self.impl._layer
}
public var device: MTLDevice {
return self.impl.device
}
private init() {
self.impl = Impl(device: MTLCreateSystemDefaultDevice()!)!
}
public func pooledTexture(spec: TextureSpec) -> PooledTexture {
return PooledTexture(device: self.device, spec: spec)
}
}

View File

@ -0,0 +1,67 @@
import Foundation
import UIKit
public final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
public let nullAction = NullActionClass()
open class SimpleLayer: CALayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
open class SimpleShapeLayer: CAShapeLayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,8 @@
import Foundation
func alignUp(_ value: Int, alignment: Int) -> Int {
assert(((alignment - 1) & alignment) == 0)
let alignmentMask = alignment - 1
return ((value + alignmentMask) & (~alignmentMask))
}

View File

@ -0,0 +1,9 @@
import Foundation
func interpolateFloat(_ value1: Float, _ value2: Float, at factor: Float) -> Float {
return value1 * (1.0 - factor) + value2 * factor
}
func interpolatePoints(_ point1: SIMD2<Float>, _ point2: SIMD2<Float>, at factor: Float) -> SIMD2<Float> {
return SIMD2<Float>(x: interpolateFloat(point1.x, point2.x, at: factor), y: interpolateFloat(point1.y, point2.y, at: factor))
}

View File

@ -0,0 +1,102 @@
import Foundation
import UIKit
public final class SharedDisplayLink {
private final class DisplayLinkTarget: NSObject {
let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func event() {
self.f()
}
}
public enum FramesPerSecond {
case fps(Double)
case max
}
public final class Subscription {
fileprivate final class Target {
let event: () -> Void
let framesPerSecond: FramesPerSecond
var lastDuration: Double = 0.0
var totalTicks: Int = 0
var acceptedTicks: Int = 0
init(event: @escaping () -> Void, framesPerSecond: FramesPerSecond) {
self.event = event
self.framesPerSecond = framesPerSecond
}
}
fileprivate let target: Target
fileprivate init(event: @escaping () -> Void, framesPerSecond: FramesPerSecond) {
self.target = Target(event: event, framesPerSecond: framesPerSecond)
}
deinit {
SharedDisplayLink.shared.remove(target: self.target)
}
}
public static let shared: SharedDisplayLink = {
return SharedDisplayLink()
}()
private var displayLink: CADisplayLink?
private var subscriptions: [Subscription.Target] = []
private init() {
self.displayLink = CADisplayLink(target: DisplayLinkTarget { [weak self] in
guard let self, let displayLink = self.displayLink else {
return
}
self.displayLinkEvent(timestamp: displayLink.timestamp, duration: displayLink.duration)
}, selector: #selector(DisplayLinkTarget.event))
if #available(iOS 15.0, *) {
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
}
self.displayLink?.add(to: .main, forMode: .common)
}
private func displayLinkEvent(timestamp: Double, duration: Double) {
loop: for subscription in self.subscriptions {
subscription.totalTicks += 1
switch subscription.framesPerSecond {
case let .fps(value):
let secondsPerFrame = 1.0 / value
subscription.lastDuration += duration
if subscription.lastDuration >= secondsPerFrame * 0.99 {
} else {
continue loop
}
case .max:
break
}
subscription.lastDuration = 0.0
subscription.acceptedTicks += 1
subscription.event()
}
}
public func add(framesPerSecond: FramesPerSecond = .max, _ event: @escaping () -> Void) -> Subscription {
let subscription = Subscription(event: event, framesPerSecond: framesPerSecond)
self.subscriptions.append(subscription.target)
return subscription
}
private func remove(target: Subscription.Target) {
if let index = self.subscriptions.firstIndex(where: { $0 === target }) {
self.subscriptions.remove(at: index)
}
}
}

View File

@ -0,0 +1,24 @@
objc_library(
name = "ShelfPack",
enable_modules = True,
module_name = "ShelfPack",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.mm",
"Sources/**/*.h",
"Sources/**/*.cpp",
]),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
sdk_frameworks = [
"Foundation",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,31 @@
#ifndef ShelfPack_h
#define ShelfPack_h
#import <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
int32_t itemId;
int32_t x;
int32_t y;
int32_t width;
int32_t height;
} ShelfPackItem;
@interface ShelfPackContext : NSObject
- (instancetype _Nonnull)initWithWidth:(int32_t)width height:(int32_t)height;
- (ShelfPackItem)addItemWithWidth:(int32_t)width height:(int32_t)height;
- (void)removeItem:(int32_t)itemId;
@end
#ifdef __cplusplus
}
#endif
#endif /* ShelfPack_h */

View File

@ -0,0 +1,51 @@
#import <ShelfPack/ShelfPack.h>
#import "shelf-pack.hpp"
#import <memory>
@interface ShelfPackContext () {
std::unique_ptr<mapbox::ShelfPack> _pack;
int32_t _nextItemId;
}
@end
@implementation ShelfPackContext
- (instancetype _Nonnull)initWithWidth:(int32_t)width height:(int32_t)height {
self = [super init];
if (self != nil) {
_pack = std::make_unique<mapbox::ShelfPack>(width, height);
}
return self;
}
- (ShelfPackItem)addItemWithWidth:(int32_t)width height:(int32_t)height {
ShelfPackItem item = {
.itemId = -1,
.x = 0,
.y = 0,
.width = 0,
.height = 0
};
int32_t itemId = _nextItemId;
_nextItemId += 1;
if (const auto bin = _pack->packOne(itemId, width, height)) {
item.itemId = bin->id;
item.x = bin->x;
item.y = bin->y;
item.width = bin->w;
item.height = bin->h;
}
return item;
}
- (void)removeItem:(int32_t)itemId {
if (const auto bin = _pack->getBin(itemId)) {
_pack->unref(*bin);
}
}
@end

View File

@ -0,0 +1,535 @@
#ifndef SHELF_PACK_HPP
#define SHELF_PACK_HPP
#include <algorithm>
#include <cstdint>
#include <deque>
#include <limits>
#include <map>
#include <vector>
namespace mapbox {
const char * const SHELF_PACK_VERSION = "2.1.1";
class Bin {
friend class ShelfPack;
public:
/**
* Create a new Bin.
*
* @class Bin
* @param {int32_t} id Unique bin identifier
* @param {int32_t} [w1=-1] Width of the new Bin
* @param {int32_t} [h1=-1] Height of the new Bin
* @param {int32_t} [maxw1=-1] Maximum Width of the new Bin
* @param {int32_t} [maxh1=-1] Maximum Height of the new Bin
* @param {int32_t} [x1=-1] X location of the Bin
* @param {int32_t} [y1=-1] Y location of the Bin
*
* @example
* Bin b(-1, 12, 16);
*/
explicit Bin(
int32_t id1 = -1,
int32_t w1 = -1,
int32_t h1 = -1,
int32_t maxw1 = -1,
int32_t maxh1 = -1,
int32_t x1 = -1,
int32_t y1 = -1
) : id(id1), w(w1), h(h1), maxw(maxw1), maxh(maxh1), x(x1), y(y1), refcount_(0) {
if (maxw == -1) {
maxw = w;
}
if (maxh == -1) {
maxh = h;
}
}
int32_t id;
int32_t w;
int32_t h;
int32_t maxw;
int32_t maxh;
int32_t x;
int32_t y;
int32_t refcount() const { return refcount_; }
private:
int32_t refcount_;
};
class Shelf {
public:
/**
* Create a new Shelf.
*
* @class Shelf
* @param {int32_t} y1 Top coordinate of the new shelf
* @param {int32_t} w1 Width of the new shelf
* @param {int32_t} h1 Height of the new shelf
*
* @example
* Shelf shelf(64, 512, 24);
*/
explicit Shelf(int32_t y1, int32_t w1, int32_t h1) :
x_(0), y_(y1), w_(w1), h_(h1), wfree_(w1) { }
/**
* Allocate a single bin into the shelf.
* Bin is stored in a `bins_` container.
* Returned pointer is stable until the shelf is destroyed.
*
* @param {int32_t} id Unique bin identifier, pass -1 to generate a new one
* @param {int32_t} w1 Width of the bin to allocate
* @param {int32_t} h1 Height of the bin to allocate
* @returns {Bin*} `Bin` pointer with `id`, `x`, `y`, `w`, `h` members
*
* @example
* Bin* result = shelf.alloc(-1, 12, 16);
*/
Bin* alloc(int32_t id, int32_t w1, int32_t h1) {
if (w1 > wfree_ || h1 > h_) {
return nullptr;
}
int32_t x1 = x_;
x_ += w1;
wfree_ -= w1;
bins_.emplace_back(id, w1, h1, w1, h_, x1, y_);
return &bins_.back();
}
/**
* Resize the shelf.
*
* @param {int32_t} w1 Requested new width of the shelf
* @returns {bool} `true` if resize succeeded, `false` if failed
*
* @example
* shelf.resize(512);
*/
bool resize(int32_t w1) {
wfree_ += (w1 - w_);
w_ = w1;
return true;
}
int32_t x() const { return x_; }
int32_t y() const { return y_; }
int32_t w() const { return w_; }
int32_t h() const { return h_; }
int32_t wfree() const { return wfree_; }
private:
int32_t x_;
int32_t y_;
int32_t w_;
int32_t h_;
int32_t wfree_;
std::deque<Bin> bins_;
};
class ShelfPack {
public:
struct ShelfPackOptions {
inline ShelfPackOptions() : autoResize(false) { };
bool autoResize;
};
struct PackOptions {
inline PackOptions() : inPlace(false) { };
bool inPlace;
};
/**
* Create a new ShelfPack bin allocator.
*
* Uses the Shelf Best Height Fit algorithm from
* http://clb.demon.fi/files/RectangleBinPack.pdf
*
* @class ShelfPack
* @param {int32_t} [w=64] Initial width of the sprite
* @param {int32_t} [h=64] Initial width of the sprite
* @param {ShelfPackOptions} [options]
* @param {bool} [options.autoResize=false] If `true`, the sprite will automatically grow
*
* @example
* ShelfPack::ShelfPackOptions options;
* options.autoResize = false;
* ShelfPack sprite = new ShelfPack(64, 64, options);
*/
explicit ShelfPack(int32_t w = 0, int32_t h = 0, const ShelfPackOptions &options = ShelfPackOptions{}) {
width_ = w > 0 ? w : 64;
height_ = h > 0 ? h : 64;
autoResize_ = options.autoResize;
maxId_ = 0;
}
/**
* Batch pack multiple bins into the sprite.
*
* @param {vector<Bin>} bins Array of requested bins - each object should have `w`, `h` values
* @param {PackOptions} [options]
* @param {bool} [options.inPlace=false] If `true`, the supplied bin objects will be updated inplace with `x` and `y` values
* @returns {vector<Bin*>} Array of Bin pointers - each bin is a struct with `x`, `y`, `w`, `h` values
*
* @example
* std::vector<Bin> moreBins;
* moreBins.emplace_back(-1, 12, 24);
* moreBins.emplace_back(-1, 12, 12);
* moreBins.emplace_back(-1, 10, 10);
*
* ShelfPack::PackOptions options;
* options.inPlace = true;
* std::vector<Bin*> results = sprite.pack(moreBins, options);
*/
std::vector<Bin*> pack(std::vector<Bin> &bins, const PackOptions &options = PackOptions{}) {
std::vector<Bin*> results;
for (auto& bin : bins) {
if (bin.w > 0 && bin.h > 0) {
Bin* allocation = packOne(bin.id, bin.w, bin.h);
if (!allocation) {
continue;
}
if (options.inPlace) {
bin.id = allocation->id;
bin.x = allocation->x;
bin.y = allocation->y;
}
results.push_back(allocation);
}
}
shrink();
return results;
}
/**
* Pack a single bin into the sprite.
*
* @param {int32_t} id Unique bin identifier, pass -1 to generate a new one
* @param {int32_t} w Width of the bin to allocate
* @param {int32_t} h Height of the bin to allocate
* @returns {Bin*} Pointer to a packed Bin with `id`, `x`, `y`, `w`, `h` members
*
* @example
* Bin* result = sprite.packOne(-1, 12, 16);
*/
Bin* packOne(int32_t id, int32_t w, int32_t h) {
int32_t y = 0;
int32_t waste = 0;
struct {
Shelf* pshelf = nullptr;
Bin* pfreebin = nullptr;
int32_t waste = std::numeric_limits<std::int32_t>::max();
} best;
// if id was supplied, attempt a lookup..
if (id != -1) {
Bin* pbin = getBin(id);
if (pbin) { // we packed this bin already
ref(*pbin);
return pbin;
}
maxId_ = std::max(id, maxId_);
} else {
id = ++maxId_;
}
// First try to reuse a free bin..
for (auto& freebin : freebins_) {
// exactly the right height and width, use it..
if (h == freebin->maxh && w == freebin->maxw) {
return allocFreebin(freebin, id, w, h);
}
// not enough height or width, skip it..
if (h > freebin->maxh || w > freebin->maxw) {
continue;
}
// extra height or width, minimize wasted area..
if (h <= freebin->maxh && w <= freebin->maxw) {
waste = (freebin->maxw * freebin->maxh) - (w * h);
if (waste < best.waste) {
best.waste = waste;
best.pfreebin = freebin;
}
}
}
// Next find the best shelf
for (auto& shelf : shelves_) {
y += shelf.h();
// not enough width on this shelf, skip it..
if (w > shelf.wfree()) {
continue;
}
// exactly the right height, pack it..
if (h == shelf.h()) {
return allocShelf(shelf, id, w, h);
}
// not enough height, skip it..
if (h > shelf.h()) {
continue;
}
// extra height, minimize wasted area..
if (h < shelf.h()) {
waste = (shelf.h() - h) * w;
if (waste < best.waste) {
best.waste = waste;
best.pshelf = &shelf;
}
}
}
if (best.pfreebin) {
return allocFreebin(best.pfreebin, id, w, h);
}
if (best.pshelf) {
return allocShelf(*best.pshelf, id, w, h);
}
// No free bins or shelves.. add shelf..
if (h <= (height_ - y) && w <= width_) {
shelves_.emplace_back(y, width_, h);
return allocShelf(shelves_.back(), id, w, h);
}
// No room for more shelves..
// If `autoResize` option is set, grow the sprite as follows:
// * double whichever sprite dimension is smaller (`w1` or `h1`)
// * if sprite dimensions are equal, grow width before height
// * accomodate very large bin requests (big `w` or `h`)
if (autoResize_) {
int32_t h1, h2, w1, w2;
h1 = h2 = height_;
w1 = w2 = width_;
if (w1 <= h1 || w > w1) { // grow width..
w2 = std::max(w, w1) * 2;
}
if (h1 < w1 || h > h1) { // grow height..
h2 = std::max(h, h1) * 2;
}
resize(w2, h2);
return packOne(id, w, h); // retry
}
return nullptr;
}
/**
*
* Shrink the width/height of the sprite to the bare minimum.
* Since shelf-pack doubles first width, then height when running out of shelf space
* this can result in fairly large unused space both in width and height if that happens
* towards the end of bin packing.
*/
void shrink() {
if (shelves_.size()) {
int32_t w2 = 0;
int32_t h2 = 0;
for (auto& shelf : shelves_) {
h2 += shelf.h();
w2 = std::max(shelf.w() - shelf.wfree(), w2);
}
resize(w2, h2);
}
}
/**
* Return a packed bin given its id, or nullptr if the id is not found
*
* @param {int32_t} id Unique identifier for this bin,
* @returns {Bin*} Pointer to a packed Bin with `id`, `x`, `y`, `w`, `h` members
*
* @example
* Bin* result = sprite.getBin(5);
*/
Bin* getBin(int32_t id) {
std::map<int32_t, Bin*>::iterator it = usedbins_.find(id);
return (it == usedbins_.end()) ? nullptr : it->second;
}
/**
* Increment the ref count of a bin and update statistics.
*
* @param {Bin&} bin Bin reference
* @returns {int32_t} New refcount of the bin
*
* @example
* Bin* bin = sprite.getBin(5);
* if (bin) {
* sprite.ref(*bin);
* }
*/
int32_t ref(Bin& bin) {
if (++bin.refcount_ == 1) { // a new Bin.. record height in stats historgram..
int32_t h = bin.h;
stats_[h] = (stats_[h] | 0) + 1;
}
return bin.refcount_;
};
/**
* Decrement the ref count of a bin and update statistics.
* The bin will be automatically marked as free space once the refcount reaches 0.
* Memory for the bin is not freed, as unreferenced bins may be reused later.
*
* @param {Bin&} bin Bin reference
* @returns {int32_t} New refcount of the bin
*
* @example
* Bin* bin = sprite.getBin(5);
* if (bin) {
* sprite.unref(*bin);
* }
*/
int32_t unref(Bin& bin) {
if (bin.refcount_ == 0) {
return 0;
}
if (--bin.refcount_ == 0) {
stats_[bin.h]--;
usedbins_.erase(bin.id);
freebins_.push_back(&bin);
}
return bin.refcount_;
}
/**
* Clear the sprite and reset statistics.
*
* @example
* sprite.clear();
*/
void clear() {
shelves_.clear();
freebins_.clear();
usedbins_.clear();
stats_.clear();
maxId_ = 0;
}
/**
* Resize the sprite.
*
* @param {int32_t} w Requested new sprite width
* @param {int32_t} h Requested new sprite height
* @returns {bool} `true` if resize succeeded, `false` if failed
*
* @example
* sprite.resize(256, 256);
*/
bool resize(int32_t w, int32_t h) {
width_ = w;
height_ = h;
for (auto& shelf : shelves_) {
shelf.resize(width_);
}
return true;
}
int32_t width() const { return width_; }
int32_t height() const { return height_; }
private:
/**
* Called by packOne() to allocate a bin by reusing an existing freebin
*
* @private
* @param {Bin*} bin Pointer to a freebin to reuse
* @param {int32_t} w Width of the bin to allocate
* @param {int32_t} h Height of the bin to allocate
* @param {int32_t} id Unique identifier for this bin
* @returns {Bin*} Pointer to a Bin with `id`, `x`, `y`, `w`, `h` properties
*
* @example
* Bin* bin = sprite.allocFreebin(pfreebin, 12, 16, 5);
*/
Bin* allocFreebin(Bin* bin, int32_t id, int32_t w, int32_t h) {
freebins_.erase(std::remove(freebins_.begin(), freebins_.end(), bin), freebins_.end());
bin->id = id;
bin->w = w;
bin->h = h;
bin->refcount_ = 0;
usedbins_[id] = bin;
ref(*bin);
return bin;
}
/**
* Called by `packOne() to allocate bin on an existing shelf
* Memory for the bin is allocated on the heap by `shelf.alloc()`
*
* @private
* @param {Shelf&} shelf Reference to the shelf to allocate the bin on
* @param {int32_t} w Width of the bin to allocate
* @param {int32_t} h Height of the bin to allocate
* @param {int32_t} id Unique identifier for this bin
* @returns {Bin*} Pointer to a Bin with `id`, `x`, `y`, `w`, `h` properties
*
* @example
* Bin* bin = sprite.allocShelf(shelf, 12, 16, 5);
*/
Bin* allocShelf(Shelf& shelf, int32_t id, int32_t w, int32_t h) {
Bin* pbin = shelf.alloc(id, w, h);
if (pbin) {
usedbins_[id] = pbin;
ref(*pbin);
}
return pbin;
}
int32_t width_;
int32_t height_;
int32_t maxId_;
bool autoResize_;
std::deque<Shelf> shelves_;
std::map<int32_t, Bin*> usedbins_;
std::vector<Bin*> freebins_;
std::map<int32_t, int32_t> stats_;
};
} // namespace mapbox
#endif