From 66e21507a31c2b88b1dbb68f265b31a7835d28ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=8D=E7=A0=8D?= Date: Tue, 13 Aug 2024 14:54:20 +0800 Subject: [PATCH] Fixed Bug In Frame --- Example/ColorfulApp.xcodeproj/project.pbxproj | 8 +- .../xcschemes/ColorfulApp.xcscheme | 3 +- .../{ColorfulAppApp.swift => App.swift} | 2 +- Example/ColorfulApp/ContentView.swift | 13 ++- ...nimatedMulticolorGradientView+Update.swift | 13 +-- .../AnimatedMulticolorGradientView.swift | 105 ++++++++++++++---- Sources/ColorfulX/DisplayLinkDriver+CV.swift | 2 +- Sources/ColorfulX/MetalLink.swift | 7 +- .../ColorfulX/MulticolorGradientView.swift | 12 +- 9 files changed, 115 insertions(+), 50 deletions(-) rename Example/ColorfulApp/{ColorfulAppApp.swift => App.swift} (94%) diff --git a/Example/ColorfulApp.xcodeproj/project.pbxproj b/Example/ColorfulApp.xcodeproj/project.pbxproj index 6451e44..1b6709b 100644 --- a/Example/ColorfulApp.xcodeproj/project.pbxproj +++ b/Example/ColorfulApp.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 507C2F092C36FB1000BCB5FA /* ChessboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507C2F082C36FB1000BCB5FA /* ChessboardView.swift */; }; - 50C663882B19B04800AF7053 /* ColorfulAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C663872B19B04800AF7053 /* ColorfulAppApp.swift */; }; + 50C663882B19B04800AF7053 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C663872B19B04800AF7053 /* App.swift */; }; 50C6638A2B19B04800AF7053 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C663892B19B04800AF7053 /* ContentView.swift */; }; 50C6638C2B19B04900AF7053 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50C6638B2B19B04900AF7053 /* Assets.xcassets */; }; 50C663982B19B07B00AF7053 /* ColorfulX in Frameworks */ = {isa = PBXBuildFile; productRef = 50C663972B19B07B00AF7053 /* ColorfulX */; }; @@ -17,7 +17,7 @@ /* Begin PBXFileReference section */ 507C2F082C36FB1000BCB5FA /* ChessboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChessboardView.swift; sourceTree = ""; }; 50C663842B19B04800AF7053 /* ColorfulApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ColorfulApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 50C663872B19B04800AF7053 /* ColorfulAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorfulAppApp.swift; sourceTree = ""; }; + 50C663872B19B04800AF7053 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 50C663892B19B04800AF7053 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50C6638B2B19B04900AF7053 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50C6638D2B19B04900AF7053 /* ColorfulApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ColorfulApp.entitlements; sourceTree = ""; }; @@ -55,7 +55,7 @@ 50C663862B19B04800AF7053 /* ColorfulApp */ = { isa = PBXGroup; children = ( - 50C663872B19B04800AF7053 /* ColorfulAppApp.swift */, + 50C663872B19B04800AF7053 /* App.swift */, 50C663892B19B04800AF7053 /* ContentView.swift */, 507C2F082C36FB1000BCB5FA /* ChessboardView.swift */, 50C6638B2B19B04900AF7053 /* Assets.xcassets */, @@ -145,7 +145,7 @@ files = ( 50C6638A2B19B04800AF7053 /* ContentView.swift in Sources */, 507C2F092C36FB1000BCB5FA /* ChessboardView.swift in Sources */, - 50C663882B19B04800AF7053 /* ColorfulAppApp.swift in Sources */, + 50C663882B19B04800AF7053 /* App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/ColorfulApp.xcodeproj/xcshareddata/xcschemes/ColorfulApp.xcscheme b/Example/ColorfulApp.xcodeproj/xcshareddata/xcschemes/ColorfulApp.xcscheme index 39f1347..06b2365 100644 --- a/Example/ColorfulApp.xcodeproj/xcshareddata/xcschemes/ColorfulApp.xcscheme +++ b/Example/ColorfulApp.xcodeproj/xcshareddata/xcschemes/ColorfulApp.xcscheme @@ -30,7 +30,7 @@ shouldAutocreateTestPlan = "YES"> diff --git a/Example/ColorfulApp/ColorfulAppApp.swift b/Example/ColorfulApp/App.swift similarity index 94% rename from Example/ColorfulApp/ColorfulAppApp.swift rename to Example/ColorfulApp/App.swift index e568d89..57099d6 100644 --- a/Example/ColorfulApp/ColorfulAppApp.swift +++ b/Example/ColorfulApp/App.swift @@ -1,5 +1,5 @@ // -// ColorfulAppApp.swift +// App.swift // ColorfulApp // // Created by QAQ on 2023/12/1. diff --git a/Example/ColorfulApp/ContentView.swift b/Example/ColorfulApp/ContentView.swift index 3629a7c..7a6d888 100644 --- a/Example/ColorfulApp/ContentView.swift +++ b/Example/ColorfulApp/ContentView.swift @@ -29,6 +29,7 @@ struct ContentView: View { bias: $bias, noise: $noise, transitionSpeed: $duration, + frameLimit: 45, renderScale: .init(scale) ) .background(ChessboardView().opacity(0.25)) @@ -97,7 +98,7 @@ struct ContentView: View { Spacer() Text("\(speed, specifier: "%.1f")") } - #if !os(tvOS) + #if os(iOS) Slider(value: $speed, in: 0.0 ... 10.0, step: 0.1) { _ in } #endif @@ -110,7 +111,7 @@ struct ContentView: View { Spacer() Text("\(bias, specifier: "%.5f")") } - #if !os(tvOS) + #if os(iOS) Slider(value: $bias, in: 0.00001 ... 0.01, step: 0.00001) { _ in } #endif @@ -123,7 +124,7 @@ struct ContentView: View { Spacer() Text("\(noise, specifier: "%.2f")") } - #if !os(tvOS) + #if os(iOS) Slider(value: $noise, in: 0 ... 64, step: 1) { _ in } #endif @@ -136,7 +137,7 @@ struct ContentView: View { Spacer() Text("\(duration, specifier: "%.2f")") } - #if !os(tvOS) + #if os(iOS) Slider(value: $duration, in: 0.0 ... 10.0, step: 0.1) { _ in } #endif @@ -149,8 +150,8 @@ struct ContentView: View { Spacer() Text("\(scale, specifier: "%.4f")") } - #if !os(tvOS) - Slider(value: $scale, in: 0.0001 ... 2.0, step: 0.0001) { _ in + #if os(iOS) + Slider(value: $scale, in: 0.001 ... 2.0, step: 0.001) { _ in } #endif } diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift index 2bb56c0..3bd36f6 100644 --- a/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift @@ -25,16 +25,9 @@ extension AnimatedMulticolorGradientView { } } - func updateRenderParameters() { - defer { needsUpdateRenderParameters = false } - - var deltaTime = -Date(timeIntervalSince1970: lastUpdate).timeIntervalSinceNow - lastUpdate = Date().timeIntervalSince1970 - guard deltaTime > 0 else { return } - - // when the app goes back from background, deltaTime could be very large - let maxDeltaAllowed = 1.0 / Double(frameLimit > 0 ? frameLimit : 30) - deltaTime = min(deltaTime, maxDeltaAllowed) + func updateRenderParameters(deltaTime: Double) { + // clear the flag + defer { renderInputWasModified = false } let moveDelta = deltaTime * speed * 0.5 // just slow down diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView.swift index f76565d..6ec1853 100644 --- a/Sources/ColorfulX/AnimatedMulticolorGradientView.swift +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView.swift @@ -15,36 +15,43 @@ private let SPRING_CONFIG = SpringInterpolation.Configuration( dampingRatio: 0.2 ) private let SPRING_ENGINE = SpringInterpolation2D(SPRING_CONFIG) +private let defaultFrameRate: Int = 60 open class AnimatedMulticolorGradientView: MulticolorGradientView { - public var lastUpdate: Double = 0 - public var lastRender: Double = 0 - public var needsUpdateRenderParameters: Bool = false + // MARK: - PROPERTY + + public private(set) var lastRenderParametersUpdate: Double = 0 + public private(set) var lastRenderExecution: Double = 0 + public var renderInputWasModified: Bool = false { + didSet { lastRenderParametersUpdate = obtainCurrentTimestamp() } + } public internal(set) var colorElements: [Speckle] { - didSet { needsUpdateRenderParameters = true } + didSet { renderInputWasModified = true } } public var speed: Double = 1.0 { - didSet { needsUpdateRenderParameters = true } + didSet { renderInputWasModified = true } } public var bias: Double = 0.01 { - didSet { needsUpdateRenderParameters = true } + didSet { renderInputWasModified = true } } public var noise: Double = 0 { - didSet { needsUpdateRenderParameters = true } + didSet { renderInputWasModified = true } } public var transitionSpeed: Double = 1 { - didSet { needsUpdateRenderParameters = true } + didSet { renderInputWasModified = true } } public var frameLimit: Int = 0 { - didSet { needsUpdateRenderParameters = true } + didSet { renderInputWasModified = true } } + // MARK: - FUNCTION + override public init() { colorElements = .init(repeating: .init(position: SPRING_ENGINE), count: Uniforms.COLOR_SLOT) super.init() @@ -72,25 +79,77 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { } } + // MARK: - GETTER + + @inline(__always) + func obtainCurrentTimestamp() -> Double { CACurrentMediaTime() } + + @inline(__always) + func frameLimiterShouldScheduleNextFrame() -> Bool { + guard frameLimit > 0 else { return true } + + let currentTime = obtainCurrentTimestamp() + let deltaTime = currentTime - lastRenderExecution + let requiredDeltaTime = 1.0 / Double(frameLimit) + + let nextTierFrameRate = Double(frameLimit * 2) + let nextTierDeltaTime = 1.0 / nextTierFrameRate + + let decisionDeltaTime = requiredDeltaTime - nextTierDeltaTime + + return deltaTime >= decisionDeltaTime + + /* + we are not dead loop here so vsync already delays for 16ms in 60hz display + if we give and one frame each time, there would be 30 fps to actually draw on display + + based on this fact, frame limit can be 7, 15, 30, 60, 120... + requiredDeltaTime needs to shift in order to comply with our goal + */ + } + + @inline(__always) + private func deltaTimeForRenderParametersUpdate() -> Double { + let currentTime = obtainCurrentTimestamp() + let realDeltaTime = currentTime - lastRenderParametersUpdate + var frameRate = frameLimit + if frameRate < 1 { frameRate = defaultFrameRate } + let maxAllowedDeltaTime = 1.0 / Double(frameRate) + if realDeltaTime > maxAllowedDeltaTime { return maxAllowedDeltaTime } + return realDeltaTime + } + + // MARK: - RENDER LIFE CYCLE + override public func layoutSublayers(of layer: CALayer) { super.layoutSublayers(of: layer) - needsUpdateRenderParameters = true - updateRenderParameters() - super.vsync() + + // skip any vsync check and force an update + renderInputWasModified = true + updateRenderParameters(deltaTime: deltaTimeForRenderParametersUpdate()) + renderIfNeeded() + } + + override func renderIfNeeded() { + super.renderIfNeeded() + } + + override func render() { + super.render() + lastRenderExecution = obtainCurrentTimestamp() } override func vsync() { - defer { super.vsync() } - guard needsUpdateRenderParameters || speed > 0 else { return } - defer { self.updateRenderParameters() } - guard frameLimit > 0 else { return } - - var decisionFrameRate = frameLimit - 1 - if decisionFrameRate < 1 { decisionFrameRate = 1 } - let wantedDeltaTime = 1.0 / Double(decisionFrameRate) - let now = CACurrentMediaTime() - guard now - lastUpdate >= wantedDeltaTime else { return } - lastRender = now + // check if we should render + guard frameLimiterShouldScheduleNextFrame() else { return } + + // check if we need to update the render parameter + // the underlying MulticolorGradientView will only render if parameter was modified + guard speed > 0 || renderInputWasModified else { return } + updateRenderParameters(deltaTime: deltaTimeForRenderParametersUpdate()) + + // sine the render parameters were updated, we call super.vsync to render + super.vsync() } func computeSpeckleColor(_ speckle: Speckle) -> ColorVector { diff --git a/Sources/ColorfulX/DisplayLinkDriver+CV.swift b/Sources/ColorfulX/DisplayLinkDriver+CV.swift index 387b88a..ae9a00f 100644 --- a/Sources/ColorfulX/DisplayLinkDriver+CV.swift +++ b/Sources/ColorfulX/DisplayLinkDriver+CV.swift @@ -74,7 +74,7 @@ import Foundation CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) guard let displayLink else { return } CVDisplayLinkSetOutputCallback(displayLink, { _, _, _, _, _, _ -> CVReturn in - CVDisplayLinkDriverHelper.dispatchUpdate() + autoreleasepool { CVDisplayLinkDriverHelper.dispatchUpdate() } return kCVReturnSuccess }, nil) CVDisplayLinkStart(displayLink) diff --git a/Sources/ColorfulX/MetalLink.swift b/Sources/ColorfulX/MetalLink.swift index 1052bcc..52a7dbd 100644 --- a/Sources/ColorfulX/MetalLink.swift +++ b/Sources/ColorfulX/MetalLink.swift @@ -18,7 +18,7 @@ class MetalLink: DisplayLinkDelegate { var onSynchronizationUpdate: SynchornizationUpdate? var scaleFactor: Double = 1.0 { - didSet { updateDrawableSize(withBounds: metalLayer.bounds) } + didSet { updateDrawableSizeFromFrame() } } enum MetalError: Error { @@ -48,7 +48,12 @@ class MetalLink: DisplayLinkDelegate { } func updateDrawableSize(withBounds bounds: CGRect) { + guard metalLayer.frame != bounds else { return } metalLayer.frame = bounds + } + + func updateDrawableSizeFromFrame() { + let bounds = metalLayer.bounds var width = bounds.width * scaleFactor var height = bounds.height * scaleFactor if width <= 0 { width = 1 } diff --git a/Sources/ColorfulX/MulticolorGradientView.swift b/Sources/ColorfulX/MulticolorGradientView.swift index 1fa9fd1..711ce40 100644 --- a/Sources/ColorfulX/MulticolorGradientView.swift +++ b/Sources/ColorfulX/MulticolorGradientView.swift @@ -42,12 +42,18 @@ open class MulticolorGradientView: MetalView { override func vsync() { super.vsync() + renderIfNeeded() + } - guard lock.try() else { return } - defer { lock.unlock() } - + func renderIfNeeded() { guard needsRender else { return } defer { needsRender = false } + render() + } + + func render() { + guard lock.try() else { return } + defer { lock.unlock() } guard let metalLink, let computePipelineState