mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Move code
This commit is contained in:
parent
f15c58c90a
commit
169fece59c
@ -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"
|
||||
|
||||
)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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])
|
||||
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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))
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
24
submodules/Utils/ShelfPack/BUILD
Normal file
24
submodules/Utils/ShelfPack/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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 */
|
51
submodules/Utils/ShelfPack/Sources/ShelfPack.mm
Normal file
51
submodules/Utils/ShelfPack/Sources/ShelfPack.mm
Normal 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
|
535
submodules/Utils/ShelfPack/Sources/shelf-pack.hpp
Normal file
535
submodules/Utils/ShelfPack/Sources/shelf-pack.hpp
Normal 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
|
Loading…
x
Reference in New Issue
Block a user