From 169fece59ce7f87b9a6fdcdd045b49036966e4b1 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 9 Nov 2023 22:28:02 +0400 Subject: [PATCH] Move code --- Tests/CallUITest/BUILD | 44 +- Tests/CallUITest/Sources/ViewController.swift | 13 +- build-system/Make/ProjectGeneration.py | 21 +- .../Components/Calls/CallScreen/BUILD | 45 + .../Calls/CallScreen/Metal/Shaders.metal | 349 +++++++ .../Sources/Animation/Animation.swift | 158 +++ .../Sources/Animation/AnimationManager.swift | 50 + .../CallScreen/Sources/Animation/Bezier.swift | 53 + .../Sources/Components/AvatarLayer.swift | 40 + .../Sources/Components/ButtonGroupView.swift | 126 +++ .../Components/CallBackgroundLayer.swift | 154 +++ .../Sources/Components/CallBlobsLayer.swift | 154 +++ .../Components/ContentOverlayButton.swift | 111 +++ .../Sources/Components/ContentView.swift | 176 ++++ .../Sources/Components/KeyEmojiView.swift | 43 + .../Sources/Components/MainVideoLayer.swift | 256 +++++ .../Sources/Components/StatusView.swift | 140 +++ .../Sources/Components/TitleView.swift | 63 ++ .../Sources/ContentOverlayContainer.swift | 60 ++ .../Sources/ContentOverlayLayer.swift | 71 ++ .../CallScreen/Sources/Media/VideoInput.swift | 118 +++ .../CallScreen/Sources/MirroringLayer.swift | 97 ++ .../Sources/PrivateCallScreen.swift | 94 ++ .../Sources/Rendering/MetalLayerContext.swift | 921 ++++++++++++++++++ .../CallScreen/Sources/SimpleLayer.swift | 67 ++ .../CallScreen/Sources/Utils/AlignUp.swift | 8 + .../Sources/Utils/Interpolate.swift | 9 + .../Sources/Utils/SharedDisplayLink.swift | 102 ++ submodules/Utils/ShelfPack/BUILD | 24 + .../PublicHeaders/ShelfPack/ShelfPack.h | 31 + .../Utils/ShelfPack/Sources/ShelfPack.mm | 51 + .../Utils/ShelfPack/Sources/shelf-pack.hpp | 535 ++++++++++ 32 files changed, 4173 insertions(+), 11 deletions(-) create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/SimpleLayer.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/AlignUp.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/Interpolate.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/SharedDisplayLink.swift create mode 100644 submodules/Utils/ShelfPack/BUILD create mode 100644 submodules/Utils/ShelfPack/PublicHeaders/ShelfPack/ShelfPack.h create mode 100644 submodules/Utils/ShelfPack/Sources/ShelfPack.mm create mode 100644 submodules/Utils/ShelfPack/Sources/shelf-pack.hpp diff --git a/Tests/CallUITest/BUILD b/Tests/CallUITest/BUILD index 8e537a5bb8..5191d4bfc2 100644 --- a/Tests/CallUITest/BUILD +++ b/Tests/CallUITest/BUILD @@ -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( CFBundleDisplayName Test CFBundleIdentifier - org.telegram.CallUITest + org.telegram.Telegram-iOS CFBundleName Telegram CFBundlePackageType @@ -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" + ) diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index 11a15bca30..ac6a8c57bd 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -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)) } } diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index 37e2ca22bc..a8759a89e2 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -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]) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index f218434db1..059d137551 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -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 = + """ + CFBundleIdentifier + org.telegram.CallScreenMetalSources + CFBundleDevelopmentRegion + en + CFBundleName + AnimationCompression + """ +) + +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", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal new file mode 100644 index 0000000000..7bf649b0fc --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal @@ -0,0 +1,349 @@ +#include + +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 inTextureY [[ texture(0) ]], + texture2d inTextureUV [[ texture(1) ]], + texture2d 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 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 inTexture, + texture2d 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 inTexture [[ texture(0) ]], + texture2d outTexture [[ texture(1) ]], + uint2 gid [[ thread_position_in_grid ]] +) { + gaussianBlur(inTexture, outTexture, float2(1, 0), gid); +} + +kernel void gaussianBlurVertical( + texture2d inTexture [[ texture(0) ]], + texture2d outTexture [[ texture(1) ]], + uint2 gid [[ thread_position_in_grid ]] +) { + gaussianBlur(inTexture, outTexture, float2(0, 1), gid); +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift new file mode 100644 index 0000000000..531f7de1bf --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift @@ -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 { + 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 { get } +} + +private let CGFloatInterpolator = AnimationInterpolator { from, to, fraction in + return from * (1.0 - fraction) + to * fraction +} +extension CGFloat: AnimationInterpolatable { + public static var animationInterpolator: AnimationInterpolator { + return CGFloatInterpolator + } +} + +private let CGPointInterpolator = AnimationInterpolator { 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 { + return CGPointInterpolator + } +} + +#if targetEnvironment(simulator) +@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float +#endif + +public final class Animation: AnyAnimation { + private let from: T + private let to: T + private let duration: Double + private let curve: AnimationCurve + private let interpolator: AnimationInterpolator + + 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: AnyAnimatedProperty { + private var animation: Animation? + + 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 + } + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift new file mode 100644 index 0000000000..0830499b95 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift @@ -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?() + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift new file mode 100644 index 0000000000..4cc8562b9e --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift @@ -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 +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift new file mode 100644 index 0000000000..b0a316f966 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift @@ -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) { + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift new file mode 100644 index 0000000000..0d4b159d91 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -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) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift new file mode 100644 index 0000000000..c6c1b5f497 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift @@ -0,0 +1,154 @@ +import Foundation +import MetalKit +import UIKit + +private func shiftArray(array: [SIMD2], offset: Int) -> [SIMD2] { + 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]) -> [SIMD2] { + var result: [SIMD2] = [] + for i in 0 ..< list.count / 2 { + result.append(list[i * 2]) + } + return result +} + +private func hexToFloat(_ hex: Int) -> SIMD4 { + 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(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] = [ + SIMD2(x: 0.80, y: 0.10), + SIMD2(x: 0.60, y: 0.20), + SIMD2(x: 0.35, y: 0.25), + SIMD2(x: 0.25, y: 0.60), + SIMD2(x: 0.20, y: 0.90), + SIMD2(x: 0.40, y: 0.80), + SIMD2(x: 0.65, y: 0.75), + SIMD2(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(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>(repeating: SIMD2(), count: 4) + for i in 0 ..< 4 { + positions[i] = interpolatePoints(positions0[i], positions1[i], at: nextStepInterpolation) + } + encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout>.size, index: 0) + + var colors: [[SIMD4]] = [ + [ + hexToFloat(0x568FD6), + hexToFloat(0x626ED5), + hexToFloat(0xA667D5), + hexToFloat(0x7664DA) + ], + [ + hexToFloat(0xACBD65), + hexToFloat(0x459F8D), + hexToFloat(0x53A4D1), + hexToFloat(0x3E917A) + ] + ] + + encoder.setFragmentBytes(&colors[0], length: 4 * MemoryLayout>.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) + }) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift new file mode 100644 index 0000000000..0d6732c4f2 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift @@ -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(blobRect.minX), Float(blobRect.minY), Float(blobRect.width), Float(blobRect.height)) + + encoder.setVertexBytes(&rect, length: 4 * 4, index: 0) + encoder.setVertexBytes(&points, length: MemoryLayout.size * points.count, index: 1) + encoder.setVertexBytes(&count, length: MemoryLayout.size, index: 2) + + encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3 * 8 * points.count) + } + }) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift new file mode 100644 index 0000000000..880a6a41fd --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -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, with event: UIEvent?) { + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + self.internalHighligthedChanged(false) + } + + super.touchesCancelled(touches, with: event) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift new file mode 100644 index 0000000000..b735b03a43 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift @@ -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(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) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift new file mode 100644 index 0000000000..8654f96a73 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift @@ -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") + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift new file mode 100644 index 0000000000..c360549739 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift @@ -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(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(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) + }) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift new file mode 100644 index 0000000000..24b6f5ba7d --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift @@ -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 + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift new file mode 100644 index 0000000000..d3ba5c58e1 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift new file mode 100644 index 0000000000..460f003478 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift @@ -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() + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift new file mode 100644 index 0000000000..7e29a77a13 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift @@ -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))) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift new file mode 100644 index 0000000000..c00a5ebe2d --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift @@ -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() + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift new file mode 100644 index 0000000000..410c8ddb37 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift new file mode 100644 index 0000000000..1653cbb78f --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift new file mode 100644 index 0000000000..03b45b95a9 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift @@ -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 { + var contents: Resolved? +} + +private let noInputPlaceholder: Placeholder = { + let value = Placeholder() + value.contents = Void() + return value +}() + +private struct PlaceholderResolveError: Error { +} + +private func resolvePlaceholder(_ value: Placeholder) 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 + public let spec: TextureSpec + + init(placeholer: Placeholder, 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() + 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( + spec: RenderLayerSpec, + state: RenderToLayerStateType.Type, + layer: MetalSubjectLayer, + inputs: repeat Placeholder, + 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( + 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( + state: ComputeStateType.Type, + inputs: repeat Placeholder, + commands: @escaping (MTLCommandBuffer, ComputeStateType, repeat each Resolved) -> Output + ) -> Placeholder { + 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() + 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( + state: ComputeStateType.Type, + commands: @escaping (MTLCommandBuffer, ComputeStateType) -> Output + ) -> Placeholder { + 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() + 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] = [] + + 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] = [ + SIMD2(Float(layerRect.minX), Float(layerRect.minY)), + SIMD2(Float(layerRect.maxX), Float(layerRect.minY)), + SIMD2(Float(layerRect.minX), Float(layerRect.maxY)), + SIMD2(Float(layerRect.maxX), Float(layerRect.minY)), + SIMD2(Float(layerRect.minX), Float(layerRect.maxY)), + SIMD2(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(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) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/SimpleLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/SimpleLayer.swift new file mode 100644 index 0000000000..4bab9bcb80 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/SimpleLayer.swift @@ -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") + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/AlignUp.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/AlignUp.swift new file mode 100644 index 0000000000..f0a3e5c762 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/AlignUp.swift @@ -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)) +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/Interpolate.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/Interpolate.swift new file mode 100644 index 0000000000..21fbcc2a0f --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/Interpolate.swift @@ -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, _ point2: SIMD2, at factor: Float) -> SIMD2 { + return SIMD2(x: interpolateFloat(point1.x, point2.x, at: factor), y: interpolateFloat(point1.y, point2.y, at: factor)) +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/SharedDisplayLink.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/SharedDisplayLink.swift new file mode 100644 index 0000000000..a7b5bd8021 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/SharedDisplayLink.swift @@ -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) + } + } +} diff --git a/submodules/Utils/ShelfPack/BUILD b/submodules/Utils/ShelfPack/BUILD new file mode 100644 index 0000000000..bade2d97bd --- /dev/null +++ b/submodules/Utils/ShelfPack/BUILD @@ -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", + ], +) diff --git a/submodules/Utils/ShelfPack/PublicHeaders/ShelfPack/ShelfPack.h b/submodules/Utils/ShelfPack/PublicHeaders/ShelfPack/ShelfPack.h new file mode 100644 index 0000000000..0979ed143c --- /dev/null +++ b/submodules/Utils/ShelfPack/PublicHeaders/ShelfPack/ShelfPack.h @@ -0,0 +1,31 @@ +#ifndef ShelfPack_h +#define ShelfPack_h + +#import + +#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 */ diff --git a/submodules/Utils/ShelfPack/Sources/ShelfPack.mm b/submodules/Utils/ShelfPack/Sources/ShelfPack.mm new file mode 100644 index 0000000000..950aefc720 --- /dev/null +++ b/submodules/Utils/ShelfPack/Sources/ShelfPack.mm @@ -0,0 +1,51 @@ +#import + +#import "shelf-pack.hpp" +#import + +@interface ShelfPackContext () { + std::unique_ptr _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(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 diff --git a/submodules/Utils/ShelfPack/Sources/shelf-pack.hpp b/submodules/Utils/ShelfPack/Sources/shelf-pack.hpp new file mode 100644 index 0000000000..3a78bfe05c --- /dev/null +++ b/submodules/Utils/ShelfPack/Sources/shelf-pack.hpp @@ -0,0 +1,535 @@ +#ifndef SHELF_PACK_HPP +#define SHELF_PACK_HPP + +#include +#include +#include +#include +#include +#include + +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 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} 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} Array of Bin pointers - each bin is a struct with `x`, `y`, `w`, `h` values + * + * @example + * std::vector 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 results = sprite.pack(moreBins, options); + */ + std::vector pack(std::vector &bins, const PackOptions &options = PackOptions{}) { + std::vector 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::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::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 shelves_; + std::map usedbins_; + std::vector freebins_; + std::map stats_; +}; + + +} // namespace mapbox + +#endif