diff --git a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift index e9e8fc340..ae1bcf36a 100644 --- a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift +++ b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift @@ -536,13 +536,28 @@ extension GraphicsContext { #endif } + // FIXME + struct ORBLayerFlags: OptionSet { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + } + + func drawLayer( + flags: ORBLayerFlags, + content: (inout GraphicsContext) throws -> Void + ) throws { + _openSwiftUIUnimplementedFailure() + } + package mutating func translateBy(x: CGFloat, y: CGFloat) { guard x != 0 || y != 0 else { return } _openSwiftUIUnimplementedFailure() } // FIXME - #if canImport(CoreGraphics) static func renderingTo( cgContext: CGContext, environment: EnvironmentValues, @@ -551,5 +566,4 @@ extension GraphicsContext { ) { _openSwiftUIUnimplementedFailure() } - #endif } diff --git a/Sources/OpenSwiftUICore/Graphic/GraphicsFilter.swift b/Sources/OpenSwiftUICore/Graphic/GraphicsFilter.swift index 2b3845ade..1e3a3dc6f 100644 --- a/Sources/OpenSwiftUICore/Graphic/GraphicsFilter.swift +++ b/Sources/OpenSwiftUICore/Graphic/GraphicsFilter.swift @@ -24,7 +24,7 @@ package enum GraphicsFilter { case vibrantColorMatrix(_ColorMatrix) case luminanceCurve(GraphicsFilter.LuminanceCurve) case colorCurves(GraphicsFilter.ColorCurves) - // case shader(GraphicsFilter.ShaderFilter) + case shader(GraphicsFilter.ShaderFilter) package struct ColorMonochrome: Equatable { package var color: Color.Resolved @@ -73,7 +73,7 @@ package enum GraphicsFilter { } } -// package struct ShaderFilter { + package struct ShaderFilter { // package var shader: Shader.ResolvedShader // package var size: CGSize // @@ -81,7 +81,7 @@ package enum GraphicsFilter { // self.shader = shader // self.size = size // } -// } + } } package enum GraphicsBlendMode: Equatable { diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index c6eb2c883..3a030c77f 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -218,9 +218,10 @@ extension DisplayList { case rotation3D(_Rotation3DEffect.Data) } -// package typealias AnyEffectAnimation = _DisplayList_AnyEffectAnimation -// package typealias AnyEffectAnimator = _DisplayList_AnyEffectAnimator - + package typealias AnyEffectAnimation = _DisplayList_AnyEffectAnimation + + package typealias AnyEffectAnimator = _DisplayList_AnyEffectAnimator + package struct ArchiveIDs { package var uuid: UUID package var stableIDs: StableIdentityMap @@ -481,7 +482,22 @@ extension DisplayList.Item { // TODO eg. .opacity(1.0) -> .identity } - // package func matchesTopLevelStructure(of other: DisplayList.Item) -> Bool + // TBA + package func matchesTopLevelStructure(of other: DisplayList.Item) -> Bool { + guard identity == other.identity else { return false } + switch (value, other.value) { + case (.empty, .empty): + return true + case (.content, .content): + return true + case (.effect, .effect): + return true + case (.states, .states): + return true + default: + return false + } + } package var features: DisplayList.Features { // TODO @@ -551,7 +567,19 @@ extension DisplayList { package struct AccessibilityNodeAttachment {} -package protocol _DisplayList_AnyEffectAnimation {} +package protocol _DisplayList_AnyEffectAnimation: ProtobufMessage { + // FIXME: CodableEffectAnimation + static var leafProtobufTag: CodableAnimation.Tag? { get } + func makeAnimator() -> any _DisplayList_AnyEffectAnimator +} + +package protocol _DisplayList_AnyEffectAnimator { + func evaluate( + _ animation: any DisplayList.AnyEffectAnimation, + at time: Time, + size: CGSize + ) -> (DisplayList.Effect, finished: Bool) +} extension DisplayList.Item { func addDrawingGroup(contentSeed: DisplayList.Seed) { diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListPlatformEffect.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListPlatformEffect.swift index aeccc9564..8cf43185c 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListPlatformEffect.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListPlatformEffect.swift @@ -2,21 +2,40 @@ // DisplayListPlatformEffect.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete +// MARK: - DisplayList.PlatformEffect + extension DisplayList { package enum PlatformEffect { case identity - package var features: DisplayList.Features { [] } - package func encode(to encoder: any Encoder) throws {} - package init(from decoder: any Decoder) throws { self = .identity } - package func print(into sexp: inout SExpPrinter) {} + + package var features: DisplayList.Features { + [] + } + + package func encode(to encoder: any Encoder) throws { + _openSwiftUIEmptyStub() + } + + package init(from decoder: any Decoder) throws { + self = .identity + } + + package func print(into sexp: inout SExpPrinter) { + _openSwiftUIEmptyStub() + } } } extension DisplayList.PlatformEffect: ProtobufMessage { - package func encode(to encoder: inout ProtobufEncoder) throws {} - package init(from decoder: inout ProtobufDecoder) throws { self = .identity } + package func encode(to encoder: inout ProtobufEncoder) throws { + _openSwiftUIEmptyStub() + } + + package init(from decoder: inout ProtobufDecoder) throws { + self = .identity + } } extension DisplayList.ViewUpdater { @@ -29,3 +48,8 @@ extension DisplayList.ViewUpdater.Platform { package struct PlatformState { } } + +extension DisplayList.ViewUpdater.Model { + package struct PlatformState { + } +} diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift index 239d6c458..99a3d3181 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift @@ -2,52 +2,451 @@ // DisplayListViewCache.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: WIP +// Audited for 6.5.4 +// Status: Complete // ID: A9949015C771FF99F7528BB7239FD006 (SwiftUICore) import Foundation +import OpenSwiftUI_SPI +import OpenQuartzCoreShims +import QuartzCore_Private extension DisplayList.ViewUpdater { + + // MARK: - ViewCache + struct ViewCache { - enum Tag { + + // MARK: - Tag + + enum Tag: UInt8 { case item case inherited } - struct Key { + // MARK: - Key + + struct Key: Hashable { var id: DisplayList.Index.ID var tag: Tag } + let platform: Platform + + var map: [Key: ViewInfo] = [:] + + var reverseMap: [OpaquePointer: Key] = [:] + + var removed: Set = [] + + private struct AnimatorInfo { + enum State { + case idle + case active(any DisplayList.AnyEffectAnimator) + case finished(DisplayList.Effect, DisplayList.Version) + } + + var state: State + var deadline: Time + } + + private var animators: [Key: AnimatorInfo] = [:] + private struct AsyncValues { var animations: Set - var modifiers: [String: Void /*CAPresentationModifier*/] + var modifiers: [String: CAPresentationModifier] } + private var asyncValues: [ObjectIdentifier: AsyncValues] = [:] + private struct PendingAsyncValue { var keyPath: String var value: NSObject var usesPresentationModifier: Bool } - private struct AnimatorInfo { - enum State { - // case active(_DisplayList_AnyEffectAnimator) - case finished(DisplayList.Effect, DisplayList.Version) - case idle + private var pendingAsyncValues: [ObjectIdentifier: [PendingAsyncValue]] = [:] + + var asyncModifierGroup: CAPresentationModifierGroup? + + var pendingAsyncUpdates: [() -> Void] = [] + + var index: DisplayList.Index = .init() + + var cacheSeed: UInt32 = .zero + + var currentList: DisplayList = .init() + + init(platform: Platform) { + self.platform = platform + } + + mutating func clearAsyncValues() { + #if canImport(QuartzCore) + for (layerID, asyncValueArray) in asyncValues { + // Recover CALayer from ObjectIdentifier — the layer must still be alive + // since ViewCache holds strong refs to views containing these layers. + let layer = unsafeBitCast(layerID, to: CALayer.self) + for animation in asyncValueArray.animations { + layer.removeAnimation(forKey: animation) + } + for modifier in asyncValueArray.modifiers.values { + layer.remove(modifier) + } } + #endif + asyncValues = [:] + asyncModifierGroup = nil + } - var state: State - var deadline: Time + mutating func reclaim(time: Time) { + removed.forEach { key in + guard let info = map[key], info.isRemoved else { return } + removeRecursively(info as AnyObject) + } + removed.removeAll() + animators = animators.filter { $0.value.deadline >= time } + cacheSeed &+= 1 } + /// Removes a managed subview from the cache and recursively + /// cleans up its container children from the view hierarchy. + private mutating func removeRecursively(_ object: AnyObject) { + let info = object as! ViewInfo + platform.forEachChild(of: info) { view in + let pointer = unsafeBitCast(view, to: OpaquePointer.self) + if let key = reverseMap.removeValue(forKey: pointer), + let newInfo = map.removeValue(forKey: key) { + removeRecursively(newInfo as AnyObject) + } + #if canImport(Darwin) + CoreViewRemoveFromSuperview(system: platform.viewSystem, view: view) + #endif + } + } - let platform: Platform - // TODO - var pendingAsyncUpdates: [() -> ()] - var index: DisplayList.Index - var cacheSeed: Swift.UInt32 - var currentList: DisplayList + mutating func commitAsyncValues(targetTimestamp: Time?) { + #if canImport(QuartzCore) + guard !pendingAsyncValues.isEmpty || !pendingAsyncUpdates.isEmpty else { + return + } + // Activate background CA context if not on main thread + if !Thread.isMainThread { + CATransaction.activateBackground(true) + } + // Suppress implicit animations during commit + let savedDisableActions = CATransaction.disableActions() + if !savedDisableActions { + CATransaction.setDisableActions(true) + } + // Track which modifier groups need flushing + var modifiedGroups: Set = [] + // Apply each pending async value to its layer + for (layerID, pendingAsyncValueArray) in pendingAsyncValues { + let layer = unsafeBitCast(layerID, to: CALayer.self) + var asyncValueArray = asyncValues[layerID, default: .init(animations: [], modifiers: [:])] + for pending in pendingAsyncValueArray { + if pending.usesPresentationModifier { + if let existing = asyncValueArray.modifiers[pending.keyPath] { + existing.value = pending.value + modifiedGroups.insert(ObjectIdentifier(existing.group!)) + } else { + let group: CAPresentationModifierGroup + if let existingGroup = asyncModifierGroup, + existingGroup.count < existingGroup.capacity { + group = existingGroup + } else { + group = CAPresentationModifierGroup(capacity: 100) + group.updatesAsynchronously = false + asyncModifierGroup = group + } + let modifier = CAPresentationModifier( + keyPath: pending.keyPath, + initialValue: pending.value, + additive: false, + group: group + ) + layer.add(modifier) + asyncValueArray.modifiers[pending.keyPath] = modifier + modifiedGroups.insert(ObjectIdentifier(group)) + } + } else { + let animation = CABasicAnimation(keyPath: pending.keyPath) + animation.beginTime = -1 + animation.duration = 1 + animation.fillMode = .forwards + animation.toValue = pending.value + animation.isRemovedOnCompletion = false + layer.add(animation, forKey: pending.keyPath) + asyncValueArray.animations.insert(pending.keyPath) + } + } + asyncValues[layerID] = asyncValueArray + } + // Restore disableActions + if !savedDisableActions { + CATransaction.setDisableActions(false) + } + // Flush modified presentation modifier groups + for groupID in modifiedGroups { + let group = unsafeBitCast(groupID, to: CAPresentationModifierGroup.self) + group.flushWithTransaction() + } + // Execute all completion closures + for update in pendingAsyncUpdates { + update() + } + // Reset state + pendingAsyncValues = [:] + pendingAsyncUpdates = [] + #else + _openSwiftUIPlatformUnimplementedWarning() + #endif + } + + mutating func prepare( + item: inout DisplayList.Item, + parentState: UnsafePointer + ) -> Time { + switch item.value { + case let .content(content): + if case let .shape(_, paint, _) = content.value, !paint.isCALayerCompatible { + item.addDrawingGroup(contentSeed: .init(item.version)) + } + return .infinity + case let .effect(effect, displayList): + switch effect { + case let .archive(archiveIDs): + index.updateArchive(entering: archiveIDs != nil) + return .infinity + case let .filter(filter): + if case .shader = filter { + item.addDrawingGroup(contentSeed: .init(item.version)) + } + return .infinity + case let .animation(animation): + return prepareAnimation( + animation, + displayList: displayList, + item: &item, + parentState: parentState, + ) + default: + return .infinity + } + default: + return .infinity + } + } + + mutating func update( + item: DisplayList.Item, + state: UnsafePointer, + tag: Tag, + in parentID: ViewInfo.ID, + makeView: (DisplayList.Index, DisplayList.Item, UnsafePointer) -> ViewInfo, + updateView: (inout ViewInfo, DisplayList.Index, DisplayList.Item, UnsafePointer) -> Void + ) -> Result { + let key = Key(id: index.id, tag: tag) + let version = item.version + if let existingInfo = map[key] { + var info = existingInfo + guard info.cacheSeed != cacheSeed else { + let description = currentList.description + Log.internalError( + "repeated view: %u, %u, %u, %u, %s, %s", + key.id.identity.value, + key.id.serial, + key.id.archiveIdentity.value, + key.id.archiveSerial, + String(describing: info.state.kind), + description + ) + preconditionFailure("repeated view: #\(key.id.identity.value), \(key.id.serial), \(key.id.archiveIdentity.value), \(key.id.archiveSerial), \(info.state.kind), \(description)") + } + defer { map[key] = info } + // Update isRemoved + if info.isRemoved { + info.isRemoved = false + removed.remove(key) + } + // Update cacheSeed + info.cacheSeed = cacheSeed + // Update nextUpdate + let newSeed = DisplayList.Seed(version) + var isInserted = info.seeds.item != newSeed || state.pointee.globals.pointee.time >= info.nextUpdate + info.nextUpdate = .infinity + // Update parentID + let oldParentID = info.parentID + if oldParentID != parentID { + info.parentID = parentID + info.seeds.invalidate() + } + // Update view + let oldView = info.view + updateView(&info, index, item, state) + if !info.isInvalid { + info.seeds.item = DisplayList.Seed(version) + } + if info.view !== oldView { + reverseMap.removeValue(forKey: unsafeBitCast(oldView, to: OpaquePointer.self)) + #if canImport(Darwin) + CoreViewRemoveFromSuperview(system: platform.viewSystem, view: oldView) + #endif + reverseMap[unsafeBitCast(info.view, to: OpaquePointer.self)] = key + #if canImport(QuartzCore) + if index.archiveIdentity == .none, item.identity != .none { + info.layer.displayListID = item.identity + } + #endif + isInserted = true + } + return Result( + view: info.view, + container: info.container, + id: info.id, + key: key, + isInserted: isInserted, + isValid: !info.isInvalid, + nextUpdate: info.nextUpdate + ) + } else { + var info = makeView(index, item, state) + info.parentID = parentID + info.cacheSeed = cacheSeed + info.seeds.item = DisplayList.Seed(version) + map[key] = info + // If this view was previously cached under a different key, + // remove the stale map entry for that old key. + let viewPointer = unsafeBitCast(info.view, to: OpaquePointer.self) + if let oldKey = reverseMap[viewPointer] { + map.removeValue(forKey: oldKey) + } + reverseMap[viewPointer] = key + #if canImport(QuartzCore) + if index.archiveIdentity == .none, item.identity != .none { + info.layer.displayListID = item.identity + } + #endif + return Result( + view: info.view, + container: info.container, + id: info.id, + key: key, + isInserted: true, + isValid: !info.isInvalid, + nextUpdate: info.nextUpdate + ) + } + } + + struct Result { + var view: AnyObject + var container: AnyObject + var id: ViewInfo.ID + var key: Key + var isInserted: Bool + var isValid: Bool + var nextUpdate: Time + } + + mutating func setNextUpdate( + _ time: Time, + in result: inout Result + ) { + guard time < result.nextUpdate else { return } + result.nextUpdate = time + map[result.key]!.nextUpdate = time + } + + struct AsyncResult { + var unknown: AnyObject // FIXME + var key: Key + var nextUpdate: Time + } + + mutating func setNextUpdate( + _ time: Time, + in result: inout AsyncResult + ) { + guard time < result.nextUpdate else { return } + result.nextUpdate = time + map[result.key]!.nextUpdate = time + } + + mutating func setAsyncValue( + _ value: NSObject, + for key: String, + in layer: CALayer, + usingPresentationModifier: Bool + ) { + let layerID = ObjectIdentifier(layer) + pendingAsyncValues[layerID, default: []].append( + PendingAsyncValue( + keyPath: key, + value: value, + usesPresentationModifier: usingPresentationModifier + ) + ) + } + + private mutating func prepareAnimation( + _ animation: any DisplayList.AnyEffectAnimation, + displayList: DisplayList, + item: inout DisplayList.Item, + parentState: UnsafePointer + ) -> Time { + let key = Key(id: index.id, tag: .item) + let time = parentState.pointee.globals.pointee.time + var animatorInfo = animators[key, default: .init(state: .idle, deadline: .zero)] + if case .idle = animatorInfo.state { + // If idle, initialize by creating an animator from the animation + animatorInfo.state = .active(animation.makeAnimator()) + } + switch animatorInfo.state { + case let .active(animator): + // Reset to idle before evaluation + animatorInfo.state = .idle + // Evaluate the animation effect + let (effect, finished) = animator.evaluate( + animation, + at: time, + size: item.size, + ) + // Swap item value with the animation effect + item.value = .effect(effect, displayList) + let maxVersion = parentState.pointee.globals.pointee.maxVersion + item.version = maxVersion + if finished { + animatorInfo.state = .finished(effect, maxVersion) + } else { + animatorInfo.state = .active(animator) + } + animatorInfo.deadline = time + animators[key] = animatorInfo + return finished ? .infinity : time + case let .finished(effect, version): + // Re-apply the stored final effect + item.value = .effect(effect, displayList) + item.version = version + animatorInfo.deadline = time + animators[key] = animatorInfo + return .infinity + case .idle: + _openSwiftUIUnreachableCode() + } + } + } +} + +#if canImport(QuartzCore) +import OpenSwiftUI_SPI +package import QuartzCore + +extension CALayer { + package var displayListID: DisplayList.Identity { + get { DisplayList.Identity(value: .init(openSwiftUI_displayListID)) } + set { openSwiftUI_displayListID = .init(newValue.value) } } } +#endif diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewPlatform.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewPlatform.swift index cc2c53913..6a6a35f22 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewPlatform.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewPlatform.swift @@ -2,16 +2,14 @@ // DisplayListViewPlatform.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: Blocked by PlatformDrawable and GraphicsContext +// Audited for 6.5.4 +// Status: Blocked by GraphicsContext and Platform // ID: 8BBC66CBE42B8A65F8A2F3799C81A349 (SwiftUICore) +public import OpenQuartzCoreShims import OpenSwiftUI_SPI -#if canImport(QuartzCore) -public import QuartzCore -#else -import Foundation -#endif + +// MARK: - PlatformViewDefinition @_spi(DisplayList_ViewSystem) @available(OpenSwiftUI_v6_0, *) @@ -71,6 +69,8 @@ open class PlatformViewDefinition: @unchecked Sendable { open class func setHitTestsAsOpaque(_ value: Bool, for view: AnyObject) { _openSwiftUIBaseClassAbstractMethod() } } +// MARK: - DisplayList.ViewUpdater.Platform Definition + extension DisplayList.ViewUpdater { package struct Platform { let rawValue: UInt @@ -89,6 +89,8 @@ extension DisplayList.ViewUpdater { } } +// MARK: - DisplayList.ViewUpdater.Platform API [WIP] + extension DisplayList.ViewUpdater.Platform { package init(definition: PlatformViewDefinition.Type) { self.init(rawValue: UInt(bitPattern: ObjectIdentifier(definition)) | UInt(definition.system.base.rawValue)) @@ -104,23 +106,78 @@ extension DisplayList.ViewUpdater.Platform { return unsafeBitCast(UInt8(rawValue & 3), to: ViewSystem.self) } - #if canImport(QuartzCore) package func viewLayer(_ view: AnyObject) -> CALayer { + #if canImport(QuartzCore) CoreViewLayer(system: viewSystem, view: view) + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif + } + + func updateDrawingView( + _ drawingView: inout AnyObject, + options: RasterizationOptions, + contentsScale: CGFloat + ) -> any PlatformDrawable { + var drawable = (drawingView as? PlatformDrawable) ?? definition.makeDrawingView(options: .init(base: options)) + let oldOption = drawable.options.base + if options != oldOption { + if oldOption.flags.symmetricDifference(options.flags).contains(.isAccelerated) { + drawable = definition.makeDrawingView(options: .init(base: options)) + } else { + drawable.options.base = options + } + } + drawable.setContentsScale(contentsScale) + drawingView = drawable + return drawable + } + + // TODO: + // private func updateDrawingView + // private func updateDrawingViewAsync + + func forEachChild( + of viewInfo: DisplayList.ViewUpdater.ViewInfo, + do body: (AnyObject) -> Void + ) { + #if canImport(Darwin) + let kind = viewInfo.state.kind + if kind.isContainer { + for subview in CoreViewSubviews(system: viewSystem, view: viewInfo.container) { + body(subview as AnyObject) + } + } + if kind == .mask, + let maskView = CoreViewMaskView(system: viewSystem, view: viewInfo.view) { + for subview in CoreViewSubviews(system: viewSystem, view: maskView) { + body(subview as AnyObject) + } + } + #endif } - #endif } +// MARK: - DisplayList.GraphicsRenderer + Platform [WIP] + extension DisplayList.GraphicsRenderer { - #if canImport(Darwin) - final package func drawPlatformLayer(_ layer: CALayer, in ctx: GraphicsContext, size: CGSize, update: Bool) { + package func drawPlatformLayer( + _ layer: CALayer, + in ctx: GraphicsContext, + size: CGSize, + update: Bool + ) { + #if canImport(Darwin) if update { layer.bounds = CGRect(origin: .zero, size: size) layer.layoutIfNeeded() } - // TODO: Blocked by GraphicsContext - _openSwiftUIUnimplementedFailure() - // ctx.drawLayer + try? ctx.drawLayer(flags: []) { _ in + // TODO: Blocked by GraphicsContext + _openSwiftUIUnimplementedFailure() + } + #else + _openSwiftUIPlatformUnimplementedWarning() + #endif } - #endif } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift index a8cc2c6ea..b3ebcace6 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewRenderer.swift @@ -2,37 +2,56 @@ // DisplayListViewRenderer.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: Blocked by ViewUpdater and ViewRasterizer +// Audited for 6.5.4 +// Status: Complete // ID: 21FFA3C7D88AC65BB559906758271BFC (SwiftUICore) +import OpenSwiftUI_SPI package import Foundation protocol ViewRendererBase: AnyObject { var platform: DisplayList.ViewUpdater.Platform { get } + var exportedObject: AnyObject? { get } - func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time - func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? + + func render( + rootView: AnyObject, + from list: DisplayList, + time: Time, + version: DisplayList.Version, + maxVersion: DisplayList.Version, + environment: DisplayList.ViewRenderer.Environment + ) -> Time + + func renderAsync( + to list: DisplayList, + time: Time, + targetTimestamp: Time?, + version: DisplayList.Version, + maxVersion: DisplayList.Version + ) -> Time? + func destroy(rootView: AnyObject) + var viewCacheIsEmpty: Bool { get } } @_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) extension DisplayList { final public class ViewRenderer { package struct Environment: Equatable { package var contentsScale: CGFloat - #if os(macOS) package var opaqueBackground: Bool = false #endif - + package static let invalid = Environment(contentsScale: .zero) package init(contentsScale: CGFloat) { self.contentsScale = contentsScale } - + #if os(macOS) package init(contentsScale: CGFloat, opaqueBackground: Bool) { self.contentsScale = contentsScale @@ -40,7 +59,7 @@ extension DisplayList { } #endif } - + let platform: DisplayList.ViewUpdater.Platform package var configuration: _RendererConfiguration = .init() @@ -59,24 +78,24 @@ extension DisplayList { private var state: State = .none - private var renderer: (any ViewRendererBase)? = nil + private var renderer: (any ViewRendererBase)? private var configChanged: Bool = true package init(platform: DisplayList.ViewUpdater.Platform) { self.platform = platform } - + private func updateRenderer(rootView: AnyObject) -> any ViewRendererBase { guard configChanged else { return renderer! } configChanged = false - let renderStateMatchCheck = switch configuration.renderer { + let isValid = switch configuration.renderer { case .default: state == .updating case .rasterized: state == .rasterizing } - if !renderStateMatchCheck { + if !isValid { if let renderer { renderer.destroy(rootView: rootView) } @@ -95,27 +114,31 @@ extension DisplayList { } else { switch configuration.renderer { case .default: - let updater = ViewUpdater() - // TODO: ViewUpdater + let updater = ViewUpdater(platform: platform, host: host) renderer = updater state = .updating case let .rasterized(options): - let rasterizer = ViewRasterizer(platform: platform, host: host, rootView: rootView, options: options) + let rasterizer = ViewRasterizer( + platform: platform, + host: host, + rootView: rootView, + options: options + ) renderer = rasterizer state = .rasterizing } } return renderer! } - + package func exportedObject(rootView: AnyObject) -> AnyObject? { let renderer = updateRenderer(rootView: rootView) return renderer.exportedObject } - #if canImport(Darwin) && _OPENSWIFTUI_SWIFTUI_RENDER + #if canImport(SwiftUI, _underlyingVersion: 6.5.4) && _OPENSWIFTUI_SWIFTUI_RENDER @_silgen_name("OpenSwiftUITestStub_DisplayListViewRendererRenderRootView") - package func swiftUI_render( + private func swiftUI_render( rootView: AnyObject, from list: DisplayList, time: Time, @@ -124,16 +147,6 @@ extension DisplayList { maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment ) -> Time - - @_silgen_name("OpenSwiftUITestStub_DisplayListViewRendererRenderAsync") - package func swiftUI_renderAsync( - to list: DisplayList, - time: Time, - nextTime: Time, - targetTimestamp: Time?, - version: DisplayList.Version, - maxVersion: DisplayList.Version - ) -> Time? #endif package func render( @@ -145,13 +158,43 @@ extension DisplayList { maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment ) -> Time { + #if canImport(SwiftUI, _underlyingVersion: 6.5.4) && _OPENSWIFTUI_SWIFTUI_RENDER + swiftUI_render( + rootView: rootView, + from: list, + time: time, + nextTime: nextTime, + version: version, + maxVersion: maxVersion, + environment: environment + ) + #else let renderer = updateRenderer(rootView: rootView) - let result = renderer.render(rootView: rootView, from: list, time: time, version: version, maxVersion: maxVersion, environment: environment) - let interval = min(nextTime, result) - time - let maxInterval = max(interval, configuration.minFrameInterval) - return time + maxInterval + let nextUpdate = renderer.render( + rootView: rootView, + from: list, + time: time, + version: version, + maxVersion: maxVersion, + environment: environment + ) + let interval = max(min(nextTime, nextUpdate) - time, configuration.minFrameInterval) + return time + interval + #endif } - + + #if canImport(SwiftUI, _underlyingVersion: 6.5.4) && _OPENSWIFTUI_SWIFTUI_RENDER + @_silgen_name("OpenSwiftUITestStub_DisplayListViewRendererRenderAsync") + private func swiftUI_renderAsync( + to list: DisplayList, + time: Time, + nextTime: Time, + targetTimestamp: Time?, + version: DisplayList.Version, + maxVersion: DisplayList.Version + ) -> Time? + #endif + package func renderAsync( to list: DisplayList, time: Time, @@ -160,19 +203,28 @@ extension DisplayList { version: DisplayList.Version, maxVersion: DisplayList.Version ) -> Time? { + #if canImport(SwiftUI, _underlyingVersion: 6.5.4) && _OPENSWIFTUI_SWIFTUI_RENDER + swiftUI_renderAsync( + to: list, + time: time, + nextTime: nextTime, + targetTimestamp: targetTimestamp, + version: version, + maxVersion: maxVersion + ) + #else guard !configChanged, let renderer else { return nil } - let result = renderer.renderAsync(to: list, time: time, targetTimestamp: targetTimestamp, version: version, maxVersion: maxVersion) - if let result { - let interval = min(nextTime, result) - time - let maxInterval = max(interval, configuration.minFrameInterval) - return time + maxInterval - } else { - return nil + let nextUpdate = renderer.renderAsync(to: list, time: time, targetTimestamp: targetTimestamp, version: version, maxVersion: maxVersion) + guard let nextUpdate else { + return nextUpdate } + let interval = max(min(nextTime, nextUpdate) - time, configuration.minFrameInterval) + return time + interval + #endif } - + package var viewCacheIsEmpty: Bool { renderer?.viewCacheIsEmpty ?? true } @@ -186,42 +238,132 @@ private var printTree: Bool? extension DisplayList { private final class ViewRasterizer: ViewRendererBase { let platform: DisplayList.ViewUpdater.Platform - weak var host: ViewRendererHost? - var drawingView: AnyObject? + weak var host: (any ViewRendererHost)? = nil + var drawingView: AnyObject? = nil var options: _RendererConfiguration.RasterizationOptions let renderer: DisplayList.GraphicsRenderer - var seed: DisplayList.Seed - var lastContentsScale: CGFloat + var seed: DisplayList.Seed = .init() + var lastContentsScale: CGFloat = .zero - init(platform: DisplayList.ViewUpdater.Platform, host: ViewRendererHost?, rootView: AnyObject, options: _RendererConfiguration.RasterizationOptions) { - _openSwiftUIBaseClassAbstractMethod() + init( + platform: DisplayList.ViewUpdater.Platform, + host: (any ViewRendererHost)?, + rootView: AnyObject, + options: _RendererConfiguration.RasterizationOptions + ) { + self.platform = platform + self.host = host + self.options = options + self.renderer = DisplayList.GraphicsRenderer(platformViewMode: options.drawsPlatformViews ? .rendered(update: true) : .unsupported) + self.drawingView = platform.definition.makeDrawingView(options: .init(base: .init(options))) + #if canImport(Darwin) + CoreViewAddSubview( + system: platform.viewSystem, + parent: rootView, + child: drawingView!, + index: 0 + ) + #else + _openSwiftUIPlatformUnimplementedWarning() + #endif } var exportedObject: AnyObject? { - platform.definition.getRBLayer(drawingView: drawingView!) + let drawingView = drawingView! + let rbLayer = platform.definition + .getRBLayer(drawingView: drawingView) + return rbLayer } - func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time { - // _openSwiftUIUnimplementedFailure() - if printTree == nil { - printTree = ProcessEnvironment.bool(forKey: "OPENSWIFTUI_PRINT_TREE") + func render( + rootView: AnyObject, + from list: DisplayList, + time: Time, + version: DisplayList.Version, + maxVersion: DisplayList.Version, + environment: DisplayList.ViewRenderer.Environment + ) -> Time { + let contentsScale = environment.contentsScale + if contentsScale != lastContentsScale { + lastContentsScale = contentsScale + seed = .init() + } + #if canImport(Darwin) + let drawingViewFrame = drawingView!.frame + if let rootViewBounds = rootView.bounds, drawingViewFrame != rootViewBounds { + CoreViewSetFrame( + system: platform.viewSystem, + view: drawingView!, + frame: rootView.bounds! + ) + seed = .init() + } + #endif + let newSeed = DisplayList.Seed(version) + if newSeed == seed, renderer.nextTime >= time { + return renderer.nextTime + } + let drawable = platform.updateDrawingView( + &drawingView!, + options: .init(options), + contentsScale: lastContentsScale + ) + let content = drawingContent(list: list, time: time) + var result = time + let updated = drawable.update(content: content, required: false) + if updated { + result = .infinity } - if let printTree, printTree { - print("View \(Unmanaged.passUnretained(rootView).toOpaque()) at \(time):\n\(list.description)") + if let host, let observer = host.as(ViewGraphRenderObserver.self) { + observer.didRender() } - return .zero + return result } - func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? { - _openSwiftUIUnimplementedFailure() + private func drawingContent(list: DisplayList, time: Time) -> PlatformDrawableContent { + var content = PlatformDrawableContent() + content.storage = .graphicsCallback { [weak host, renderer] ctx, size in + if printTree == nil { + printTree = ProcessEnvironment.bool(forKey: "OPENSWIFTUI_PRINT_TREE") + } + if let printTree, printTree { + print("View \(Unmanaged.passUnretained(self).toOpaque()) at \(time):\n\(list.description)") + } + renderer.renderDisplayList(list, at: time, in: &ctx) + let duration = renderer.nextTime - time + let delay = max(duration, 1e-6) + if delay != .infinity { + DispatchQueue.main.async { [weak host] in + host?.requestUpdate(after: delay) + } + } + } + return content + } + + func renderAsync( + to list: DisplayList, + time: Time, + targetTimestamp: Time?, + version: DisplayList.Version, + maxVersion: DisplayList.Version + ) -> Time? { + nil } func destroy(rootView: AnyObject) { - _openSwiftUIUnimplementedFailure() + #if canImport(Darwin) + CoreViewRemoveFromSuperview( + system: platform.viewSystem, + view: drawingView! + ) + #else + _openSwiftUIPlatformUnimplementedWarning() + #endif } var viewCacheIsEmpty: Bool { - _openSwiftUIUnimplementedFailure() + true } } } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift index 9bb8c50b1..7ec92cae6 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewUpdater.swift @@ -1,5 +1,5 @@ // -// DisplayList.ViewUpdater.swift +// DisplayListViewUpdater.swift // OpenSwiftUICore // // Audited for 6.0.87 @@ -9,14 +9,12 @@ private var printTree: Bool? import Foundation -#if canImport(Darwin) -import QuartzCore -#endif +import OpenQuartzCoreShims extension DisplayList { // FIXME final package class ViewUpdater: ViewRendererBase { - weak var host: ViewRendererHost? + weak var host: (any ViewRendererHost)? var viewCache: DisplayList.ViewUpdater.ViewCache var seed: DisplayList.Seed var asyncSeed: DisplayList.Seed @@ -27,36 +25,172 @@ extension DisplayList { var isValid: Bool var wasValid: Bool - init() { - _openSwiftUIUnimplementedFailure() + init(platform: Platform, host: (any ViewRendererHost)?){ + self.host = host + self.viewCache = ViewCache(platform: platform) + self.seed = DisplayList.Seed() + self.asyncSeed = DisplayList.Seed() + self.nextUpdate = .infinity + self.lastEnv = .invalid + self.lastList = DisplayList() + self.lastTime = .zero + self.isValid = false + self.wasValid = false } - func render(rootView: AnyObject, from list: DisplayList, time: Time, version: DisplayList.Version, maxVersion: DisplayList.Version, environment: DisplayList.ViewRenderer.Environment) -> Time { - // TODO + func render( + rootView: AnyObject, + from list: DisplayList, + time: Time, + version: DisplayList.Version, + maxVersion: DisplayList.Version, + environment: DisplayList.ViewRenderer.Environment + ) -> Time { + viewCache.clearAsyncValues() if printTree == nil { printTree = ProcessEnvironment.bool(forKey: "OPENSWIFTUI_PRINT_TREE") } if let printTree, printTree { print("View \(Unmanaged.passUnretained(rootView).toOpaque()) at \(time):\n\(list.description)") } - return .zero + + let newSeed = DisplayList.Seed(version) + let seedChanged = newSeed != seed + let envChanged = environment != lastEnv + + wasValid = isValid + + if seedChanged || envChanged || !isValid { + // TODO: Walk display list items and create/update platform views + viewCache.currentList = list + seed = newSeed + lastEnv = environment + isValid = true + } + + lastList = list + lastTime = time + nextUpdate = .infinity + + return nextUpdate } - func renderAsync(to list: DisplayList, time: Time, targetTimestamp: Time?, version: DisplayList.Version, maxVersion: DisplayList.Version) -> Time? { - nil + func renderAsync( + to list: DisplayList, + time: Time, + targetTimestamp: Time?, + version: DisplayList.Version, + maxVersion: DisplayList.Version + ) -> Time? { + // Phase 1: Version hash early return + let newAsyncSeed = DisplayList.Seed(version) + if newAsyncSeed == asyncSeed, lastTime >= time { + return nextUpdate + } + + // Phase 2: Debug print + if printTree == nil { + printTree = ProcessEnvironment.bool(forKey: "OPENSWIFTUI_PRINT_TREE") + } + if let printTree, printTree { + print("Async view at \(time):\n\(list.description)") + } + + // Phase 3: Save state and call updateAsync + wasValid = isValid + let oldList = viewCache.currentList + + guard let resultTime = updateAsync(oldList: oldList, newList: list) else { + // Cancelled: rollback + isValid = wasValid + return nil + } + + // Phase 4: Commit + viewCache.commitAsyncValues(targetTimestamp: targetTimestamp) + viewCache.currentList = list + asyncSeed = newAsyncSeed + lastTime = time + nextUpdate = resultTime + isValid = true + + return resultTime + } + + private func updateAsync(oldList: DisplayList, newList: DisplayList) -> Time? { + let oldItems = oldList.items + let newItems = newList.items + guard oldItems.count == newItems.count else { + return nil + } + + var nextTime: Time = .infinity + + for i in 0 ..< oldItems.count { + let oldItem = oldItems[i] + let newItem = newItems[i] + + guard oldItem.matchesTopLevelStructure(of: newItem) else { + return nil + } + + switch (oldItem.value, newItem.value) { + case let (.effect(_, oldChild), .effect(_, newChild)): + guard let childTime = updateAsync(oldList: oldChild, newList: newChild) else { + return nil + } + nextTime = min(nextTime, childTime) + case let (.states(oldStates), .states(newStates)): + guard oldStates.count == newStates.count else { + return nil + } + for j in 0 ..< oldStates.count { + let (oldHash, oldChild) = oldStates[j] + let (newHash, newChild) = newStates[j] + guard oldHash == newHash else { + return nil + } + guard let stateTime = updateAsync( + oldList: oldChild, + newList: newChild + ) else { + return nil + } + nextTime = min(nextTime, stateTime) + } + case (.content, .content): + // TODO: updateItemViewAsync — leaf platform view property update + if oldItem.version != newItem.version { + // Content changed but structure same — would update platform view here + } + case (.empty, .empty): + break + default: + break + } + } + + return nextTime } func destroy(rootView: AnyObject) { + isValid = false + wasValid = false + lastList = DisplayList() + lastEnv = .invalid + seed = DisplayList.Seed() + asyncSeed = DisplayList.Seed() + nextUpdate = .infinity + viewCache.currentList = DisplayList() + viewCache.pendingAsyncUpdates.removeAll() } var viewCacheIsEmpty: Bool { - // TODO - false + viewCache.map.isEmpty } var platform: Platform { - // TODO - _openSwiftUIUnimplementedFailure() + viewCache.platform } var exportedObject: AnyObject? { @@ -79,16 +213,26 @@ extension DisplayList.ViewUpdater { var shadow: DisplayList.Seed var properties: DisplayList.Seed var platformSeeds: DisplayList.ViewUpdater.PlatformViewInfo.Seeds + + mutating func invalidate() { + item.invalidate() + content.invalidate() + opacity.invalidate() + blend.invalidate() + transform.invalidate() + clips.invalidate() + filters.invalidate() + shadow.invalidate() + properties.invalidate() + } } - struct ID { + struct ID: Equatable { var value: Int } var view: AnyObject - #if canImport(Darwin) var layer: CALayer - #endif var container: AnyObject var state: DisplayList.ViewUpdater.Platform.State var id: ID @@ -97,5 +241,114 @@ extension DisplayList.ViewUpdater { var cacheSeed: UInt32 var isRemoved: Bool var isInvalid: Bool + var nextUpdate: Time + + init( + view: AnyObject, + layer: CALayer, + container: AnyObject, + state: Platform.State + ) { + self.view = view + self.layer = layer + self.container = container + self.state = state + self.id = ID(value: 0) + self.parentID = ID(value: 0) + self.seeds = Seeds( + item: .init(), content: .init(), opacity: .init(), + blend: .init(), transform: .init(), clips: .init(), + filters: .init(), shadow: .init(), properties: .init(), + platformSeeds: .init() + ) + self.cacheSeed = 0 + self.isRemoved = false + self.isInvalid = false + self.nextUpdate = .infinity + } + + init( + platform: Platform, + kind: PlatformViewDefinition.ViewKind + ) { + _openSwiftUIUnimplementedFailure() + } + + func reset() { + _openSwiftUIUnimplementedFailure() + } + } +} + +// MARK: - DisplayList.ViewUpdater.Model [WIP] + +extension DisplayList.ViewUpdater { + enum Model { + struct Clip { + var path: Path + var transform: CGAffineTransform? + var style: FillStyle + + var isEmpty: Bool { + // TODO + false + } + } + + struct State { + struct Versions { + var opacity: DisplayList.Version + var blend: DisplayList.Version + var transform: DisplayList.Version + var clips: DisplayList.Version + var filters: DisplayList.Version + var shadow: DisplayList.Version + var properties: DisplayList.Version + } + + struct Globals { + var updater: DisplayList.ViewUpdater + var time: Time + var maxVersion: DisplayList.Version + var environment: DisplayList.ViewRenderer.Environment + } + + var globals: UnsafePointer + var opacity: Float + var blend: GraphicsBlendMode + var transform: CGAffineTransform + var clips: [Clip] + var filters: [GraphicsFilter] + var shadow: Indirect + var properties: DisplayList.Properties + var rewriteVibrantColorMatrix: Bool + var compositingGroup: Bool + var backdropGroupID: UInt32 + var stateHashes: [StrongHash] + var platformState: PlatformState + var versions: Versions + + var hasDODEffects: Bool { + // TODO + false + } + + func reset() { + // TODO + } + + func clipRect() -> FixedRoundedRect? { + // TODO + nil + } + + func adjust(for transform: CGAffineTransform) { + // TODO + } + + mutating func addClip(_ path: Path, style: FillStyle) { + // TODO + } + } } } diff --git a/Sources/OpenSwiftUICore/Render/RendererConfiguration.swift b/Sources/OpenSwiftUICore/Render/RendererConfiguration.swift index c837130ff..88f55097d 100644 --- a/Sources/OpenSwiftUICore/Render/RendererConfiguration.swift +++ b/Sources/OpenSwiftUICore/Render/RendererConfiguration.swift @@ -5,7 +5,10 @@ // Audited for 6.5.4 // Status: Complete +// MARK: - _RendererConfiguration + /// Renderer configuration for a hosting view. +@available(OpenSwiftUI_v2_0, *) public struct _RendererConfiguration { /// The available renderer kind and their configuration. @@ -37,6 +40,8 @@ public struct _RendererConfiguration { _RendererConfiguration(renderer: .rasterized(options)) } + // MARK: - _RendererConfiguration.RasterizationOptions + /// Options for the `rasterized` renderer. public struct RasterizationOptions { @@ -84,3 +89,32 @@ extension _RendererConfiguration.Renderer: Sendable {} @available(*, unavailable) extension _RendererConfiguration.RasterizationOptions: Sendable {} + +// MARK: - RasterizationOptions + _RendererConfiguration.RasterizationOptions + +extension RasterizationOptions { + + /// Convert from the public `_RendererConfiguration.RasterizationOptions` + /// to the internal `RasterizationOptions`. + package init(_ options: _RendererConfiguration.RasterizationOptions) { + var flags: RasterizationOptions.Flags = .defaultFlags + flags.formUnion(.isAccelerated) + if options.isOpaque { + flags.formUnion(.isOpaque) + } else { + flags.subtract([.isOpaque, .rendersAsynchronously, .prefersDisplayCompositing]) + } + if options.rendersAsynchronously { + flags.formUnion(.rendersAsynchronously) + } + if options.prefersDisplayCompositing { + flags.formUnion(.prefersDisplayCompositing) + } + self.init( + colorMode: options.colorMode, + rbColorMode: options.rbColorMode, + flags: flags, + maxDrawableCount: Int8(truncatingIfNeeded: options.maxDrawableCount) + ) + } +} diff --git a/Sources/OpenSwiftUICore/Test/Test.swift b/Sources/OpenSwiftUICore/Test/Test.swift index 2d54ecf8b..c9439b14f 100644 --- a/Sources/OpenSwiftUICore/Test/Test.swift +++ b/Sources/OpenSwiftUICore/Test/Test.swift @@ -163,6 +163,7 @@ package struct PlatformViewTestProperties: OptionSet { } #if canImport(QuartzCore) +import OpenSwiftUI_SPI package import QuartzCore extension CALayer { diff --git a/Sources/OpenSwiftUICore/Util/QuartzCoreShims.swift b/Sources/OpenSwiftUICore/Util/QuartzCoreShims.swift new file mode 100644 index 000000000..2d52013c3 --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/QuartzCoreShims.swift @@ -0,0 +1,16 @@ +// +// QuartzCoreShims.swift +// OpenSwiftUICore + +#if !canImport(QuartzCore) +public import Foundation + +// MARK: - CAPresentationModifierGroup + +open class CAPresentationModifierGroup: NSObject {} + +// MARK: - CAPresentationModifier + +open class CAPresentationModifier: NSObject {} + +#endif diff --git a/Sources/OpenSwiftUICore/View/Graph/EmptyViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/EmptyViewRendererHost.swift deleted file mode 100644 index b21f12920..000000000 --- a/Sources/OpenSwiftUICore/View/Graph/EmptyViewRendererHost.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// EmptyViewRendererHost.swift -// OpenSwiftUICore -// -// Audited for 6.5.4 -// Status: Complete - -final package class EmptyViewRendererHost: ViewRendererHost { - package let viewGraph: ViewGraph - - package var propertiesNeedingUpdate: ViewRendererHostProperties = [] - - package var renderingPhase: ViewRenderingPhase = .none - - package var externalUpdateCount: Int = .zero - - package var currentTimestamp: Time = .zero - - package init(environment: EnvironmentValues = EnvironmentValues()) { - Update.begin() - viewGraph = ViewGraph(rootViewType: EmptyView.self, requestedOutputs: []) - viewGraph.setEnvironment(environment) - viewGraph.setRootView(EmptyView()) - initializeViewGraph() - Update.end() - } - - package func requestUpdate(after delay: Double) {} - - package func updateRootView() {} - - package func updateEnvironment() {} - - package func updateSize() {} - - package func updateSafeArea() {} - - package func updateContainerSize() {} - - package func forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) {} -} diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index 285c525e9..91049321c 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -312,17 +312,6 @@ extension ViewRendererHost { let rootView = delegate.renderingRootView // TODO: CustomEventTrace return delegate.withMainThreadRender(wasAsync: false) { - #if canImport(SwiftUI, _underlyingVersion: 6.0.87) && _OPENSWIFTUI_SWIFTUI_RENDER - renderer.swiftUI_render( - rootView: self, - from: list, - time: time, - nextTime: nextTime, - version: version, - maxVersion: maxVersion, - environment: environment - ) - #else renderer.render( rootView: self, from: list, @@ -332,21 +321,10 @@ extension ViewRendererHost { maxVersion: maxVersion, environment: environment ) - #endif } } if asynchronously { // TODO: CustomEventTrace - #if canImport(SwiftUI, _underlyingVersion: 6.0.87) && _OPENSWIFTUI_SWIFTUI_RENDER - let renderedTime = renderer.swiftUI_renderAsync( - to: list, - time: time, - nextTime: nextTime, - targetTimestamp: targetTimestamp, - version: version, - maxVersion: maxVersion - ) - #else let renderedTime = renderer.renderAsync( to: list, time: time, @@ -355,7 +333,6 @@ extension ViewRendererHost { version: version, maxVersion: maxVersion ) - #endif if let renderedTime { return renderedTime } else { @@ -572,3 +549,40 @@ extension ViewRendererHost { viewGraph.graph.archiveJSON(name: name) } } + +// MARK: - EmptyViewRendererHost [6.5.4] + +final package class EmptyViewRendererHost: ViewRendererHost { + package let viewGraph: ViewGraph + + package var propertiesNeedingUpdate: ViewRendererHostProperties = [] + + package var renderingPhase: ViewRenderingPhase = .none + + package var externalUpdateCount: Int = .zero + + package var currentTimestamp: Time = .zero + + package init(environment: EnvironmentValues = EnvironmentValues()) { + Update.begin() + viewGraph = ViewGraph(rootViewType: EmptyView.self, requestedOutputs: []) + viewGraph.setEnvironment(environment) + viewGraph.setRootView(EmptyView()) + initializeViewGraph() + Update.end() + } + + package func requestUpdate(after delay: Double) {} + + package func updateRootView() {} + + package func updateEnvironment() {} + + package func updateSize() {} + + package func updateSafeArea() {} + + package func updateContainerSize() {} + + package func forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) {} +} diff --git a/Sources/OpenSwiftUI_SPI/Overlay/QuartzCore/CALayer+OpenSwiftUIAdditions.h b/Sources/OpenSwiftUI_SPI/Overlay/QuartzCore/CALayer+OpenSwiftUIAdditions.h new file mode 100644 index 000000000..86e580618 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/QuartzCore/CALayer+OpenSwiftUIAdditions.h @@ -0,0 +1,31 @@ +// +// CALayer+OpenSwiftUIAddition.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef CALayer_OpenSwiftUIAdditions_h +#define CALayer_OpenSwiftUIAdditions_h + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#import + +// MARK: - CALayer (OpenSwiftUIAdditions) + +@interface CALayer (OpenSwiftUIAdditions) + +@property (nonatomic, assign) uint64_t openSwiftUI_viewTestProperties; + +@property (nonatomic, assign) int64_t openSwiftUI_displayListID; + +- (void)openSwiftUI_setNoAnimationDelegate; + +@end + +#endif /* __has_include() */ + +#endif /* CALayer_OpenSwiftUIAdditions_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/QuartzCore/CALayer+OpenSwiftUIAdditions.m b/Sources/OpenSwiftUI_SPI/Overlay/QuartzCore/CALayer+OpenSwiftUIAdditions.m new file mode 100644 index 000000000..e9b25dd6d --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/QuartzCore/CALayer+OpenSwiftUIAdditions.m @@ -0,0 +1,61 @@ +// +// CALayer+OpenSwiftUIAdditions.m +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#import "CALayer+OpenSwiftUIAdditions.h" +#import "CANullAction.h" + +#if __has_include() + +// MARK: - _OpenSwiftUI_NoAnimationDelegate + +/// A class whose sole purpose is to act as a CALayer delegate that +/// suppresses all implicit animations by returning kCFNull. +/// Used as a class object (not an instance) — the class method +/// +actionForLayer:forKey: is dispatched via the metaclass. +@interface _OpenSwiftUI_NoAnimationDelegate : NSObject +@end + +@implementation _OpenSwiftUI_NoAnimationDelegate + ++ (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { + return _CANullAction(); +} + +@end + +// MARK: - CALayer (OpenSwiftUIAdditions) + +@implementation CALayer (OpenSwiftUIAdditions) + +- (uint64_t)openSwiftUI_viewTestProperties { + NSNumber *properties = [self valueForKey:@"_openSwiftUI_viewTestProperties"]; + return properties.integerValue; +} + +- (void)setOpenSwiftUI_viewTestProperties:(uint64_t)properties { + [self setValue:[NSNumber numberWithUnsignedLongLong:properties] forKey:@"_openSwiftUI_viewTestProperties"]; +} + +- (int64_t)openSwiftUI_displayListID { + NSNumber *value = [self valueForKey:@"_openSwiftUI_displayListID"]; + if (value == nil) { + return INT64_MAX; + } + return value.integerValue; +} + +- (void)setOpenSwiftUI_displayListID:(int64_t)displayListID { + [self setValue:[NSNumber numberWithInteger:displayListID] forKey:@"_openSwiftUI_displayListID"]; +} + +- (void)openSwiftUI_setNoAnimationDelegate { + self.delegate = (id)[_OpenSwiftUI_NoAnimationDelegate class]; +} + +@end + +#endif /* __has_include() */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.h b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.h index db17afa08..1116dcb57 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.h +++ b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.h @@ -23,8 +23,6 @@ typedef NSString *CALayerContentsScaling NS_TYPED_ENUM; @property (nonatomic, assign) BOOL allowsGroupBlending_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(allowsGroupBlending); @property (nonatomic, assign) BOOL allowsHitTesting_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(allowsHitTesting); -@property (nonatomic, assign) uint64_t openSwiftUI_viewTestProperties; - /// Private property to control contents alpha channel swizzling. /// When set to kCALayerContentsSwizzleAAAA, the alpha channel is replicated to all channels. /// When set to kCALayerContentsSwizzleRGBA, normal RGBA behavior is used. diff --git a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.m b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.m index c56cb2179..95599e258 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.m +++ b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CALayerPrivate.m @@ -51,14 +51,6 @@ - (void)setAllowsHitTesting_openswiftui_safe_wrapper:(BOOL)allows { func(self, selector, allows); } -- (uint64_t)openSwiftUI_viewTestProperties { - NSNumber *properties = [self valueForKey:@"_viewTestProperties"]; - return properties.integerValue; -} - -- (void)setOpenSwiftUI_viewTestProperties:(uint64_t)properties { - [self setValue:[NSNumber numberWithUnsignedLongLong:properties] forKey:@"_viewTestProperties"]; -} @end void _CALayerSetSplatsContentsAlpha(CALayer * _Nonnull layer, BOOL splatAlpha) { diff --git a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CAPresentationModifier.h b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CAPresentationModifier.h new file mode 100644 index 000000000..bb6d068fe --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CAPresentationModifier.h @@ -0,0 +1,141 @@ +// +// CAPresentationModifier.h +// OpenSwiftUI_SPI +// +// Status: Complete +// Audited for 6.5.4 + +#ifndef CAPresentationModifier_h +#define CAPresentationModifier_h + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#import + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +// MARK: - CAPresentationModifierGroup + +/// Private QuartzCore class that manages a group of presentation modifiers +/// sharing a single shared-memory region for efficient batch updates. +/// +/// Available since iOS 12.4. Used by SwiftUI for async rendering and by +/// WebKit for off-main-thread animation updates. +/// +/// Source: WebKit QuartzCoreSPI.h +@interface CAPresentationModifierGroup : NSObject + +/// Create a group with a fixed capacity for modifier slots. ++ (instancetype)groupWithCapacity:(NSUInteger)capacity; + +/// Flush all pending modifier values to the Render Server via atomic signal. +/// Safe to call from any thread. +- (void)flush; + +/// Flush with a target timestamp for frame pacing. +- (void)flushWithTargetTime:(double)targetTime; + +/// Flush locally (copy pending → current buffer) without notifying Render Server. +- (void)flushLocally; + +/// Flush locally with target timestamp. +- (void)flushLocallyWithTargetTime:(double)targetTime; + +/// Flush via CA::Transaction path. Must be called on the main thread. +- (void)flushWithTransaction; + +/// Flush via CA::Transaction with target timestamp. +- (void)flushWithTransactionAndTargetTime:(double)targetTime; + +/// Whether the group updates asynchronously (controls shmem header bit 30). +@property (nonatomic) BOOL updatesAsynchronously; + +/// Number of modifiers currently in this group. +@property (nonatomic, readonly) NSUInteger count; + +/// Maximum number of modifiers this group can hold. +@property (nonatomic, readonly) NSUInteger capacity; + +@end + +// MARK: - CAPresentationModifier + +/// Private QuartzCore class that allows direct modification of CALayer +/// properties via shared memory, bypassing CATransaction. +/// +/// Values written via `setValue:` are picked up by the Render Server on +/// the next frame without requiring a main-thread round-trip. +/// +/// Available since iOS 12.4. +/// +/// Source: WebKit QuartzCoreSPI.h +@interface CAPresentationModifier : NSObject + +/// The target CALayer property keyPath (e.g. "opacity", "transform"). +@property (nonatomic, copy, readonly) NSString *keyPath; + +/// Whether this modifier is currently active. +@property (nonatomic, getter=isEnabled) BOOL enabled; + +/// Whether the modifier value is additive (added to model value vs. replacing it). +@property (nonatomic, readonly) BOOL additive; + +/// The group this modifier belongs to (nil for standalone modifiers). +@property (nonatomic, readonly, nullable) CAPresentationModifierGroup *group; + +/// The current value. +@property (nonatomic, strong) id value; + +/// Convenience initializer without a group (creates standalone shared memory). +- (instancetype)initWithKeyPath:(NSString *)keyPath + initialValue:(id)initialValue + additive:(BOOL)additive; + +/// Convenience initializer with a group (allocates a slot in the group's shared memory). +- (instancetype)initWithKeyPath:(NSString *)keyPath + initialValue:(id)initialValue + additive:(BOOL)additive + group:(nullable CAPresentationModifierGroup *)group; + +/// Designated initializer with full parameters. +- (instancetype)initWithKeyPath:(NSString *)keyPath + initialValue:(id)initialValue + initialVelocity:(nullable id)initialVelocity + additive:(BOOL)additive + preferredFrameRateRangeMaximum:(NSInteger)preferredFrameRateRangeMaximum + group:(nullable CAPresentationModifierGroup *)group; + +/// Set a new value (thread-safe, writes to shared memory). +- (void)setValue:(id)value; + +/// Set a new value with velocity for interpolation. +- (void)setValue:(id)value velocity:(nullable id)velocity; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +// MARK: - CALayer (PresentationModifiers) + +@interface CALayer (OpenSwiftUI_PresentationModifiers) + +/// The presentation modifiers currently attached to this layer. +@property (nonatomic, copy, nullable) NSArray *presentationModifiers; + +/// Bind a presentation modifier to this layer. +/// Triggers a CA::Transaction; must be called on the main thread. +- (void)addPresentationModifier:(CAPresentationModifier *)modifier; + +/// Remove a presentation modifier from this layer. +/// Triggers a CA::Transaction; must be called on the main thread. +- (void)removePresentationModifier:(CAPresentationModifier *)modifier; + +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif /* */ + +#endif /* CAPresentationModifier_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CoreAnimation_Private.h b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CoreAnimation_Private.h index a3bf51e2e..12b1bd35a 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CoreAnimation_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/QuartzCore/CoreAnimation_Private.h @@ -39,6 +39,16 @@ typedef struct CAColorMatrix CAColorMatrix; - (void)setHighFrameRateReasons_openswiftui_safe_wrapper:(const uint32_t *)reasons count:(NSInteger)count OPENSWIFTUI_SWIFT_NAME(setHighFrameRateReasons(_:count:)); @end +// MARK: - CATransaction (Private) + +@interface CATransaction (OpenSwiftUI_Private) + +/// Activate a background Core Animation context for off-main-thread rendering. +/// Must be called before any CA operations on a background thread. ++ (void)activateBackground:(BOOL)activate; + +@end + OPENSWIFTUI_ASSUME_NONNULL_END #endif /* CoreAnimation.h */