[WIP] MetalEngine

This commit is contained in:
Ali 2023-11-11 00:43:25 +04:00
parent 5976d495b0
commit 88a0fd7a81
10 changed files with 162 additions and 43 deletions

View File

@ -71,6 +71,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case keepChatNavigationStack(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool)
case skipReadHistory(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool)
case unidirectionalSwipeToReply(Bool) case unidirectionalSwipeToReply(Bool)
case dustEffect(Bool)
case crashOnSlowQueries(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool)
case crashOnMemoryPressure(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool)
case clearTips(PresentationTheme) case clearTips(PresentationTheme)
@ -119,7 +120,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logs.rawValue return DebugControllerSection.logs.rawValue
case .logToFile, .logToConsole, .redactSensitiveData: case .logToFile, .logToConsole, .redactSensitiveData:
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .keepChatNavigationStack, .skipReadHistory, .unidirectionalSwipeToReply, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .unidirectionalSwipeToReply, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
@ -168,70 +169,72 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 15 return 15
case .unidirectionalSwipeToReply: case .unidirectionalSwipeToReply:
return 16 return 16
case .crashOnSlowQueries: case .dustEffect:
return 17 return 17
case .crashOnMemoryPressure: case .crashOnSlowQueries:
return 18 return 18
case .clearTips: case .crashOnMemoryPressure:
return 19 return 19
case .resetNotifications: case .clearTips:
return 20 return 20
case .crash: case .resetNotifications:
return 21 return 21
case .resetData: case .crash:
return 22 return 22
case .resetDatabase: case .resetData:
return 23 return 23
case .resetDatabaseAndCache: case .resetDatabase:
return 24 return 24
case .resetHoles: case .resetDatabaseAndCache:
return 25 return 25
case .reindexUnread: case .resetHoles:
return 26 return 26
case .resetCacheIndex: case .reindexUnread:
return 27 return 27
case .reindexCache: case .resetCacheIndex:
return 28 return 28
case .resetBiometricsData: case .reindexCache:
return 29 return 29
case .resetWebViewCache: case .resetBiometricsData:
return 30 return 30
case .optimizeDatabase: case .resetWebViewCache:
return 31 return 31
case .photoPreview: case .optimizeDatabase:
return 32 return 32
case .knockoutWallpaper: case .photoPreview:
return 33 return 33
case .experimentalCompatibility: case .knockoutWallpaper:
return 34 return 34
case .enableDebugDataDisplay: case .experimentalCompatibility:
return 35 return 35
case .acceleratedStickers: case .enableDebugDataDisplay:
return 36 return 36
case .inlineForums: case .acceleratedStickers:
return 37 return 37
case .localTranscription: case .inlineForums:
return 38 return 38
case .enableReactionOverrides: case .localTranscription:
return 39 return 39
case .restorePurchases: case .enableReactionOverrides:
return 40 return 40
case .logTranslationRecognition: case .restorePurchases:
return 41 return 41
case .resetTranslationStates: case .logTranslationRecognition:
return 42 return 42
case .storiesExperiment: case .resetTranslationStates:
return 43 return 43
case .storiesJpegExperiment: case .storiesExperiment:
return 44 return 44
case .playlistPlayback: case .storiesJpegExperiment:
return 45 return 45
case .enableQuickReactionSwitch: case .playlistPlayback:
return 46 return 46
case .voiceConference: case .enableQuickReactionSwitch:
return 47 return 47
case .voiceConference:
return 48
case let .preferredVideoCodec(index, _, _, _): case let .preferredVideoCodec(index, _, _, _):
return 48 + index return 49 + index
case .disableVideoAspectScaling: case .disableVideoAspectScaling:
return 100 return 100
case .enableNetworkFramework: case .enableNetworkFramework:
@ -939,6 +942,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return settings return settings
}).start() }).start()
}) })
case let .dustEffect(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Dust Effect", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
var settings = settings
settings.dustEffect = value
return settings
}).start()
})
case let .crashOnSlowQueries(_, value): case let .crashOnSlowQueries(_, value):
return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
@ -1387,6 +1398,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory))
#endif #endif
entries.append(.unidirectionalSwipeToReply(experimentalSettings.unidirectionalSwipeToReply)) entries.append(.unidirectionalSwipeToReply(experimentalSettings.unidirectionalSwipeToReply))
entries.append(.dustEffect(experimentalSettings.dustEffect))
} }
entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries))
entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure)) entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure))

View File

@ -4669,6 +4669,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
} }
} }
public func forEachRemovedItemNode(_ f: (ASDisplayNode) -> Void) {
for itemNode in self.itemNodes {
if itemNode.index == nil {
f(itemNode)
}
}
}
public func enumerateItemNodes(_ f: (ASDisplayNode) -> Bool) { public func enumerateItemNodes(_ f: (ASDisplayNode) -> Bool) {
for itemNode in self.itemNodes { for itemNode in self.itemNodes {

View File

@ -411,6 +411,7 @@ swift_library(
"//submodules/TelegramUI/Components/ContextMenuScreen", "//submodules/TelegramUI/Components/ContextMenuScreen",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen", "//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/MetalEngine", "//submodules/MetalEngine",
"//submodules/TelegramUI/Components/DustEffect",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

@ -5382,4 +5382,22 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
override public func contentFrame() -> CGRect { override public func contentFrame() -> CGRect {
return self.backgroundNode.frame return self.backgroundNode.frame
} }
override public func makeContentSnapshot() -> (UIImage, CGRect)? {
UIGraphicsBeginImageContextWithOptions(self.backgroundNode.view.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.translateBy(x: -self.backgroundNode.frame.minX, y: -self.insets.top - self.backgroundNode.frame.minY)
self.view.drawHierarchy(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.view.bounds.size), afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let image else {
return nil
}
return (image, self.backgroundNode.frame)
}
} }

View File

@ -716,6 +716,10 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
return nil return nil
} }
open func makeContentSnapshot() -> (UIImage, CGRect)? {
return nil
}
open func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { open func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? {
return nil return nil
} }

View File

@ -124,6 +124,6 @@ fragment half4 dustEffectFragment(
) { ) {
constexpr sampler sampler(coord::normalized, address::clamp_to_edge, filter::linear); constexpr sampler sampler(coord::normalized, address::clamp_to_edge, filter::linear);
half3 color = inTexture.sample(sampler, float2(in.uv.x, 1.0 - in.uv.y)).rgb; half4 color = inTexture.sample(sampler, float2(in.uv.x, 1.0 - in.uv.y));
return half4(color * in.alpha, in.alpha); return color * in.alpha;
} }

View File

@ -95,9 +95,10 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
} }
private var updateLink: SharedDisplayLinkDriver.Link? private var updateLink: SharedDisplayLinkDriver.Link?
private var items: [Item] = [] private var items: [Item] = []
public var becameEmpty: (() -> Void)?
override public init() { override public init() {
super.init() super.init()
@ -127,14 +128,20 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
} }
private func updateItems() { private func updateItems() {
var didRemoveItems = false
for i in (0 ..< self.items.count).reversed() { for i in (0 ..< self.items.count).reversed() {
self.items[i].phase += 1.0 / 60.0 self.items[i].phase += (1.0 / 60.0) / Float(UIView.animationDurationFactor())
if self.items[i].phase >= 4.0 { if self.items[i].phase >= 4.0 {
self.items.remove(at: i) self.items.remove(at: i)
didRemoveItems = true
} }
} }
self.updateNeedsAnimation() self.updateNeedsAnimation()
if didRemoveItems && self.items.isEmpty {
self.becameEmpty?()
}
} }
private func updateNeedsAnimation() { private func updateNeedsAnimation() {
@ -222,7 +229,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
computeEncoder.setBytes(&particleCount, length: 4 * 2, index: 1) computeEncoder.setBytes(&particleCount, length: 4 * 2, index: 1)
var phase = item.phase var phase = item.phase
computeEncoder.setBytes(&phase, length: 4, index: 2) computeEncoder.setBytes(&phase, length: 4, index: 2)
var timeStep: Float = 1.0 / 60.0 var timeStep: Float = (1.0 / 60.0) / Float(UIView.animationDurationFactor())
computeEncoder.setBytes(&timeStep, length: 4, index: 3) computeEncoder.setBytes(&timeStep, length: 4, index: 3)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
} }

View File

@ -17117,11 +17117,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
} }
contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in
if let strongSelf = self { if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
f(.dismissWithoutContent) if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect {
c.dismiss(completion: { [weak strongSelf] in
guard let strongSelf else {
return
}
strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds)
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
})
} else {
f(.dismissWithoutContent)
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
}
} }
}))) })))
items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in

View File

@ -31,6 +31,7 @@ import ChatMessageItemImpl
import ChatMessageItemView import ChatMessageItemView
import ChatMessageTransitionNode import ChatMessageTransitionNode
import ChatControllerInteraction import ChatControllerInteraction
import DustEffect
struct ChatTopVisibleMessageRange: Equatable { struct ChatTopVisibleMessageRange: Equatable {
var lowerBound: MessageIndex var lowerBound: MessageIndex
@ -699,6 +700,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private var toLang: String? private var toLang: String?
private var dustEffectLayer: DustEffectLayer?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?, source: ChatHistoryListSource = .default, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl? = { nil }) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?, source: ChatHistoryListSource = .default, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl? = { nil }) {
var tagMask = tagMask var tagMask = tagMask
if case .pinnedMessages = subject { if case .pinnedMessages = subject {
@ -3195,6 +3198,50 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
if let currentDeleteAnimationCorrelationIds = strongSelf.currentDeleteAnimationCorrelationIds {
var foundItemNodes: [ChatMessageItemView] = []
strongSelf.forEachRemovedItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
for (message, _) in item.content {
if currentDeleteAnimationCorrelationIds.contains(message.id) {
foundItemNodes.append(itemNode)
}
}
}
}
if !foundItemNodes.isEmpty {
strongSelf.currentDeleteAnimationCorrelationIds = nil
if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect {
if strongSelf.dustEffectLayer == nil {
let dustEffectLayer = DustEffectLayer()
dustEffectLayer.position = strongSelf.bounds.center
dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: strongSelf.bounds.size)
strongSelf.dustEffectLayer = dustEffectLayer
dustEffectLayer.zPosition = 10.0
dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(dustEffectLayer)
dustEffectLayer.becameEmpty = { [weak strongSelf] in
guard let strongSelf else {
return
}
strongSelf.dustEffectLayer?.removeFromSuperlayer()
strongSelf.dustEffectLayer = nil
}
}
if let dustEffectLayer = strongSelf.dustEffectLayer {
for itemNode in foundItemNodes {
guard let (image, subFrame) = itemNode.makeContentSnapshot() else {
continue
}
let itemFrame = itemNode.layer.convert(subFrame, to: dustEffectLayer)
dustEffectLayer.addItem(frame: itemFrame, image: image)
itemNode.isHidden = true
}
}
}
}
}
if !newIncomingReactions.isEmpty { if !newIncomingReactions.isEmpty {
let messageIds = Array(newIncomingReactions.keys) let messageIds = Array(newIncomingReactions.keys)
@ -3872,6 +3919,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
func setCurrentSendAnimationCorrelationIds(_ value: Set<Int64>?) { func setCurrentSendAnimationCorrelationIds(_ value: Set<Int64>?) {
self.currentSendAnimationCorrelationIds = value self.currentSendAnimationCorrelationIds = value
} }
private var currentDeleteAnimationCorrelationIds: Set<MessageId>?
func setCurrentDeleteAnimationCorrelationIds(_ value: Set<MessageId>?) {
self.currentDeleteAnimationCorrelationIds = value
}
var animationCorrelationMessagesFound: (([Int64: ChatMessageItemView]) -> Void)? var animationCorrelationMessagesFound: (([Int64: ChatMessageItemView]) -> Void)?

View File

@ -54,6 +54,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var storiesJpegExperiment: Bool public var storiesJpegExperiment: Bool
public var crashOnMemoryPressure: Bool public var crashOnMemoryPressure: Bool
public var unidirectionalSwipeToReply: Bool public var unidirectionalSwipeToReply: Bool
public var dustEffect: Bool
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -85,7 +86,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
storiesExperiment: false, storiesExperiment: false,
storiesJpegExperiment: false, storiesJpegExperiment: false,
crashOnMemoryPressure: false, crashOnMemoryPressure: false,
unidirectionalSwipeToReply: false unidirectionalSwipeToReply: false,
dustEffect: false
) )
} }
@ -118,7 +120,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
storiesExperiment: Bool, storiesExperiment: Bool,
storiesJpegExperiment: Bool, storiesJpegExperiment: Bool,
crashOnMemoryPressure: Bool, crashOnMemoryPressure: Bool,
unidirectionalSwipeToReply: Bool unidirectionalSwipeToReply: Bool,
dustEffect: Bool
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -149,6 +152,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.storiesJpegExperiment = storiesJpegExperiment self.storiesJpegExperiment = storiesJpegExperiment
self.crashOnMemoryPressure = crashOnMemoryPressure self.crashOnMemoryPressure = crashOnMemoryPressure
self.unidirectionalSwipeToReply = unidirectionalSwipeToReply self.unidirectionalSwipeToReply = unidirectionalSwipeToReply
self.dustEffect = dustEffect
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -183,6 +187,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false
self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false
self.unidirectionalSwipeToReply = try container.decodeIfPresent(Bool.self, forKey: "unidirectionalSwipeToReply") ?? false self.unidirectionalSwipeToReply = try container.decodeIfPresent(Bool.self, forKey: "unidirectionalSwipeToReply") ?? false
self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect") ?? false
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -217,6 +222,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment") try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment")
try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure")
try container.encode(self.unidirectionalSwipeToReply, forKey: "unidirectionalSwipeToReply") try container.encode(self.unidirectionalSwipeToReply, forKey: "unidirectionalSwipeToReply")
try container.encode(self.dustEffect, forKey: "dustEffect")
} }
} }