diff --git a/Examples/ClientSideResizeChrome/main.swift b/Examples/ClientSideResizeChrome/main.swift index 709a0faf..26fc1e48 100644 --- a/Examples/ClientSideResizeChrome/main.swift +++ b/Examples/ClientSideResizeChrome/main.swift @@ -112,7 +112,7 @@ enum ClientSideResizeChrome { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID): + case .redrawRequested(.window(let windowID)): if let controller = await registry.controller(for: windowID) { try await controller.redrawIfNeeded() } diff --git a/Examples/CursorAnimationSmoke/main.swift b/Examples/CursorAnimationSmoke/main.swift index a106ea4c..e80aa0b9 100644 --- a/Examples/CursorAnimationSmoke/main.swift +++ b/Examples/CursorAnimationSmoke/main.swift @@ -61,7 +61,7 @@ enum CursorAnimationSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redraw(window) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/CursorPolicySmoke/main.swift b/Examples/CursorPolicySmoke/main.swift index d97dd16c..5fc0e9bb 100644 --- a/Examples/CursorPolicySmoke/main.swift +++ b/Examples/CursorPolicySmoke/main.swift @@ -81,7 +81,7 @@ enum CursorPolicySmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redraw(window) case .windowOutputsChanged(let outputs) where outputs.windowID == window.id: log("window outputs=\(outputs.outputs)") diff --git a/Examples/CustomCursorSmoke/main.swift b/Examples/CustomCursorSmoke/main.swift index 35b9fc59..10ff59fd 100644 --- a/Examples/CustomCursorSmoke/main.swift +++ b/Examples/CustomCursorSmoke/main.swift @@ -72,7 +72,7 @@ enum CustomCursorSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redraw(window) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/DataTransferSmoke/main.swift b/Examples/DataTransferSmoke/main.swift index 48019eda..9b8723e2 100644 --- a/Examples/DataTransferSmoke/main.swift +++ b/Examples/DataTransferSmoke/main.swift @@ -92,7 +92,7 @@ enum DataTransferSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redrawIfNeeded(window: window, state: state) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/FrameworkHostSmoke/main.swift b/Examples/FrameworkHostSmoke/main.swift index 4498e852..693f541e 100644 --- a/Examples/FrameworkHostSmoke/main.swift +++ b/Examples/FrameworkHostSmoke/main.swift @@ -69,7 +69,7 @@ enum FrameworkHostSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await loop.redrawIfNeeded() case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() @@ -84,7 +84,7 @@ enum FrameworkHostSmoke { break case .diagnostic(let diagnostic): log("display diagnostic \(diagnostic)") - case .popupDismissed, .popupClosed, .popupRedrawRequested, .outputChanged, + case .popupDismissed, .popupClosed, .outputChanged, .outputRemoved, .windowCloseRequested, .windowClosed, .redrawRequested, .windowOutputsChanged, .keyboardShortcutsInhibitorChanged: break diff --git a/Examples/GraphicsPreviewManagedGPUClear/main.swift b/Examples/GraphicsPreviewManagedGPUClear/main.swift index 33e37457..aeb3b12f 100644 --- a/Examples/GraphicsPreviewManagedGPUClear/main.swift +++ b/Examples/GraphicsPreviewManagedGPUClear/main.swift @@ -164,7 +164,7 @@ enum GraphicsPreviewManagedGPUClear { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == backing.id: + case .redrawRequested(.window(let windowID)) where windowID == backing.id: _ = try await submitClearFrame( backing: backing, state: state, diff --git a/Examples/KeyboardShortcutsInhibitSmoke/main.swift b/Examples/KeyboardShortcutsInhibitSmoke/main.swift index 0fd0f95d..18173728 100644 --- a/Examples/KeyboardShortcutsInhibitSmoke/main.swift +++ b/Examples/KeyboardShortcutsInhibitSmoke/main.swift @@ -62,7 +62,7 @@ enum KeyboardShortcutsInhibitSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await window.redraw(draw) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift new file mode 100644 index 00000000..0690aa7e --- /dev/null +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift @@ -0,0 +1,19 @@ +import WaylandClient + +@main +enum ManagedSurfacePresentationSmoke { + static func main() async throws { + try await WaylandDisplay.withConnection( + applicationID: "org.waylandclientkit.ManagedSurfacePresentationSmoke", + eventStreamConfiguration: try EventStreamConfiguration( + eventCapacity: 128, + inputEventCapacity: 32, + textInputEventCapacity: 16, + dataTransferEventCapacity: 16, + presentationEventCapacity: 64 + ) + ) { display in + try await ManagedSurfaceSmokeRun(display: display).execute() + } + } +} diff --git a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift new file mode 100644 index 00000000..7a8353bb --- /dev/null +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift @@ -0,0 +1,129 @@ +import WaylandClient + +extension ManagedSurfaceSmokeRun { + func presentWindow(_ window: Window, requestsFeedback: Bool) async throws { + var rootEvents = display.events.makeAsyncIterator() + var feedback = window.presentationEvents.makeAsyncIterator() + let outcome = try await retryPresentation(label: "window initial presentation") { + try await window.show(requestPresentationFeedback: requestsFeedback) { frame in + drawManagedSurfaceFrame(frame, color: 0x0030_6090) + } + } + try requirePresented(outcome, label: "window initial presentation") + if requestsFeedback { + try await requireRootFeedback( + from: &rootEvents, + surface: .window(window.id), + label: "window" + ) + try await requireFeedback(from: &feedback, label: "window") + } + print("window initial presentation: presented") + } + + func presentPopup(_ popup: PopupSurface, requestsFeedback: Bool) async throws { + var rootEvents = display.events.makeAsyncIterator() + var feedback = popup.presentationEvents.makeAsyncIterator() + let outcome = try await retryPresentation(label: "popup initial presentation") { + try await popup.show( + requestPresentationFeedback: requestsFeedback, + preparing: { reservation in reservation.id }, + { _, frame in drawManagedSurfaceFrame(frame, color: 0x0080_4080) } + ) + } + try requirePresented(outcome, label: "popup initial presentation") + if requestsFeedback { + try await requireRootFeedback( + from: &rootEvents, + surface: .popup(popup.id), + label: "popup" + ) + try await requireFeedback(from: &feedback, label: "popup") + } + print("popup initial presentation: presented") + } + + func presentSubsurface( + _ subsurface: Subsurface, + label: String, + color: UInt32, + requestsFeedback: Bool + ) async throws { + var rootEvents = display.events.makeAsyncIterator() + var feedback = subsurface.presentationEvents.makeAsyncIterator() + let outcome = try await retryPresentation(label: label) { + try await subsurface.show( + requestPresentationFeedback: requestsFeedback, + preparing: { reservation in reservation.id }, + { _, frame in drawManagedSurfaceFrame(frame, color: color) } + ) + } + try requirePresented(outcome, label: label) + if requestsFeedback { + try await requireRootFeedback( + from: &rootEvents, + surface: .subsurface(subsurface.identity), + label: label + ) + try await requireFeedback(from: &feedback, label: label) + } + print("\(label): presented") + } + + private func requirePresented( + _ outcome: SoftwarePresentationOutcome, + label: String + ) throws { + guard outcome == .presented else { + throw ManagedSurfaceSmokeError.unexpectedPresentationOutcome(label, outcome) + } + } + + func retryPresentation( + label: String, + operation: () async throws -> SoftwarePresentationOutcome + ) async throws -> SoftwarePresentationOutcome { + var latestOutcome = SoftwarePresentationOutcome.deferred + for _ in 0..<8 { + latestOutcome = try await operation() + switch latestOutcome { + case .presented: + return latestOutcome + case .superseded, .deferred: + try await Task.sleep(for: .milliseconds(10)) + case .closed: + throw ManagedSurfaceSmokeError.unexpectedPresentationOutcome( + label, + latestOutcome + ) + } + } + return latestOutcome + } + + private func requireFeedback( + from iterator: inout ManagedSurfacePresentationEvents.AsyncIterator, + label: String + ) async throws { + guard let feedback = try await iterator.next() else { + throw ManagedSurfaceSmokeError.presentationStreamEnded(label) + } + print("\(label) feedback: \(feedback)") + } + + private func requireRootFeedback( + from iterator: inout DisplayEventsIterator, + surface: ManagedSurfaceIdentity, + label: String + ) async throws { + while let event = try await iterator.next() { + if case .presentation(let presentation) = event, + presentation.surface == surface + { + print("\(label) root feedback: \(presentation.feedback)") + return + } + } + throw ManagedSurfaceSmokeError.eventStreamEnded("\(label) root feedback") + } +} diff --git a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift new file mode 100644 index 00000000..c65cdc5a --- /dev/null +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift @@ -0,0 +1,133 @@ +import WaylandClient + +extension ManagedSurfaceSmokeRun { + func routeRedraw(for window: Window, color: UInt32) async throws { + let skipsRoutingEvent = CommandLine.arguments.contains("--skip-redraw-routing") + var events = display.events.makeAsyncIterator() + try await window.requestRedraw() + if skipsRoutingEvent { + try await Task.sleep(for: .milliseconds(20)) + } else { + try await requireRedraw( + from: &events, + surface: .window(window.id), + label: "window redraw" + ) + } + let outcome = try await retryPresentation(label: "window redraw") { + try await window.redraw { frame in + drawManagedSurfaceFrame(frame, color: color) + } + } + try requireRedrawPresented(outcome, label: "window redraw") + print("redraw routing: window") + } + + func routeRedraw(for popup: PopupSurface, color: UInt32) async throws { + let skipsRoutingEvent = CommandLine.arguments.contains("--skip-redraw-routing") + var events = display.events.makeAsyncIterator() + try await popup.requestRedraw() + if skipsRoutingEvent { + try await Task.sleep(for: .milliseconds(20)) + } else { + try await requireRedraw( + from: &events, + surface: .popup(popup.id), + label: "popup redraw" + ) + } + let outcome = try await retryPresentation(label: "popup redraw") { + try await popup.redraw { frame in + drawManagedSurfaceFrame(frame, color: color) + } + } + try requireRedrawPresented(outcome, label: "popup redraw") + print("redraw routing: popup") + } + + func routeRedraw(for subsurface: Subsurface, color: UInt32) async throws { + let skipsRoutingEvent = CommandLine.arguments.contains("--skip-redraw-routing") + var events = display.events.makeAsyncIterator() + try await subsurface.requestRedraw() + if skipsRoutingEvent { + try await Task.sleep(for: .milliseconds(20)) + } else { + try await requireRedraw( + from: &events, + surface: .subsurface(subsurface.identity), + label: "subsurface redraw" + ) + } + let outcome = try await retryPresentation(label: "subsurface redraw") { + try await subsurface.redraw { frame in + drawManagedSurfaceFrame(frame, color: color) + } + } + try requireRedrawPresented(outcome, label: "subsurface redraw") + print("redraw routing: \(subsurface.identity)") + } + + func finishPopup(_ popup: PopupSurface) async throws { + var events = display.events.makeAsyncIterator() + if CommandLine.arguments.contains("--interactive-dismissal") { + print("popup dismissal: click outside the popup") + while let event = try await events.next() { + if case .popupDismissed(let lifecycle) = event, + lifecycle.popup == popup.id + { + print("popup dismissal: compositor event observed") + await popup.close() + return + } + } + throw ManagedSurfaceSmokeError.eventStreamEnded("popup dismissal") + } + + await popup.close() + while let event = try await events.next() { + if case .popupClosed(let lifecycle) = event, + lifecycle.popup == popup.id + { + print("popup cleanup: explicit close observed") + return + } + } + throw ManagedSurfaceSmokeError.eventStreamEnded("popup close") + } + + private func requireRedraw( + from iterator: inout DisplayEventsIterator, + surface: ManagedSurfaceIdentity, + label: String + ) async throws { + while let event = try await iterator.next() { + if case .redrawRequested(let candidate) = event, + candidate == surface + { + return + } + } + throw ManagedSurfaceSmokeError.eventStreamEnded(label) + } + + private func requireRedrawPresented( + _ outcome: SoftwarePresentationOutcome, + label: String + ) throws { + guard outcome == .presented else { + throw ManagedSurfaceSmokeError.unexpectedPresentationOutcome(label, outcome) + } + } +} + +nonisolated func drawManagedSurfaceFrame( + _ frame: borrowing SoftwareFrame, + color: UInt32 +) { + frame.withXRGB8888Rows { row, pixels in + for index in 0.. WindowConfiguration { + try WindowConfiguration( + title: "Managed Surface Presentation Smoke", + appID: "managed-surface-presentation-smoke", + initialWidth: 320, + initialHeight: 220, + closeRequestPolicy: .requestOnly + ) + } + + private func popupConfiguration() throws -> PopupConfiguration { + try PopupConfiguration( + positioner: PopupPositioner( + anchorRect: LogicalRect(x: 16, y: 16, width: 80, height: 48), + size: PositiveLogicalSize(width: 112, height: 72), + anchor: .bottomLeft, + gravity: .bottomRight, + constraintAdjustment: [.slideX, .slideY] + ) + ) + } + + private func subsurfaceConfiguration( + position: LogicalOffset, + mode: SubsurfaceSynchronizationMode + ) throws -> SubsurfaceConfiguration { + SubsurfaceConfiguration( + position: position, + size: try PositiveLogicalSize(width: 104, height: 64), + synchronizationMode: mode + ) + } +} + +enum ManagedSurfaceSmokeError: Error { + case unexpectedPresentationOutcome(String, SoftwarePresentationOutcome) + case eventStreamEnded(String) + case presentationStreamEnded(String) + case parentCloseDidNotCascade +} diff --git a/Examples/Package.swift b/Examples/Package.swift index 29e2b51f..241d3a16 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -76,6 +76,12 @@ let package = Package( path: "FrameworkHostSmoke", swiftSettings: executableSwiftSettings ), + .executableTarget( + name: "ManagedSurfacePresentationSmoke", + dependencies: [.product(name: "WaylandClient", package: "WaylandClientKit")], + path: "ManagedSurfacePresentationSmoke", + swiftSettings: executableSwiftSettings + ), .executableTarget( name: "PresentationFeedbackAnimation", dependencies: [ diff --git a/Examples/PointerCaptureSmoke/main.swift b/Examples/PointerCaptureSmoke/main.swift index eecac048..2b98ef14 100644 --- a/Examples/PointerCaptureSmoke/main.swift +++ b/Examples/PointerCaptureSmoke/main.swift @@ -86,7 +86,7 @@ enum PointerCaptureSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redraw(window) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/PointerGesturesSmoke/main.swift b/Examples/PointerGesturesSmoke/main.swift index 4dc35c2e..9f545298 100644 --- a/Examples/PointerGesturesSmoke/main.swift +++ b/Examples/PointerGesturesSmoke/main.swift @@ -64,7 +64,7 @@ enum PointerGesturesSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await window.redraw(draw) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/PointerWarpSmoke/main.swift b/Examples/PointerWarpSmoke/main.swift index 5a1a0554..4159b5ea 100644 --- a/Examples/PointerWarpSmoke/main.swift +++ b/Examples/PointerWarpSmoke/main.swift @@ -79,7 +79,7 @@ enum PointerWarpSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redraw(window) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/PresentationFeedbackAnimation/main.swift b/Examples/PresentationFeedbackAnimation/main.swift index a1a2179d..4231d02f 100644 --- a/Examples/PresentationFeedbackAnimation/main.swift +++ b/Examples/PresentationFeedbackAnimation/main.swift @@ -101,7 +101,7 @@ enum PresentationFeedbackAnimation { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: let phase = await animation.nextPhase() if usePresentationFeedback { do { @@ -133,7 +133,7 @@ enum PresentationFeedbackAnimation { } nonisolated private static func consumePresentationEvents( - _ events: WindowPresentationEvents, + _ events: ManagedSurfacePresentationEvents, animation: AnimationState ) async throws { var iterator = events.makeAsyncIterator() diff --git a/Examples/SerialActionsProbe/main.swift b/Examples/SerialActionsProbe/main.swift index ec927681..e1f9288b 100644 --- a/Examples/SerialActionsProbe/main.swift +++ b/Examples/SerialActionsProbe/main.swift @@ -86,7 +86,7 @@ enum SerialActionsProbe { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redrawIfNeeded(window: window, state: state) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/SessionStateSmoke/main.swift b/Examples/SessionStateSmoke/main.swift index ba218172..9753fb2d 100644 --- a/Examples/SessionStateSmoke/main.swift +++ b/Examples/SessionStateSmoke/main.swift @@ -126,7 +126,7 @@ enum SessionStateSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await window.redraw { frame in draw(frame, restored: true) } diff --git a/Examples/SurfaceRegionSmoke/main.swift b/Examples/SurfaceRegionSmoke/main.swift index 486078f2..01aeb895 100644 --- a/Examples/SurfaceRegionSmoke/main.swift +++ b/Examples/SurfaceRegionSmoke/main.swift @@ -70,7 +70,7 @@ enum SurfaceRegionSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await window.redraw { frame in draw(frame, restricted: false) } diff --git a/Examples/TabletInputSmoke/main.swift b/Examples/TabletInputSmoke/main.swift index 1c2bc538..9638cf0a 100644 --- a/Examples/TabletInputSmoke/main.swift +++ b/Examples/TabletInputSmoke/main.swift @@ -78,7 +78,7 @@ enum TabletInputSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redraw(window) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/TextInputSmoke/main.swift b/Examples/TextInputSmoke/main.swift index 4d6e7b25..8f44e5e4 100644 --- a/Examples/TextInputSmoke/main.swift +++ b/Examples/TextInputSmoke/main.swift @@ -89,7 +89,7 @@ enum TextInputSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await redrawIfNeeded(window: window, state: state) case .windowCloseRequested(let windowID) where windowID == window.id: await disableAllSessions(state: state) diff --git a/Examples/ToplevelDragSmoke/main.swift b/Examples/ToplevelDragSmoke/main.swift index 59ae3474..736855b3 100644 --- a/Examples/ToplevelDragSmoke/main.swift +++ b/Examples/ToplevelDragSmoke/main.swift @@ -67,7 +67,7 @@ enum ToplevelDragSmoke { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID) where windowID == window.id: + case .redrawRequested(.window(let windowID)) where windowID == window.id: try await window.redraw(draw) case .windowCloseRequested(let windowID) where windowID == window.id: await window.close() diff --git a/Examples/TwoWindowFrameworkHost/main.swift b/Examples/TwoWindowFrameworkHost/main.swift index 6da1a90c..1d9e10c8 100644 --- a/Examples/TwoWindowFrameworkHost/main.swift +++ b/Examples/TwoWindowFrameworkHost/main.swift @@ -103,7 +103,7 @@ enum TwoWindowFrameworkHost { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID): + case .redrawRequested(.window(let windowID)): if let controller = await registry.controller(for: windowID) { log("redraw requested window=\(windowID)") try await controller.redrawIfNeeded() diff --git a/Examples/TwoWindowOrderStress/main.swift b/Examples/TwoWindowOrderStress/main.swift index 747fa1dd..728d162f 100644 --- a/Examples/TwoWindowOrderStress/main.swift +++ b/Examples/TwoWindowOrderStress/main.swift @@ -94,7 +94,7 @@ enum TwoWindowOrderStress { var iterator = events.makeAsyncIterator() while !Task.isCancelled, let event = try await iterator.next() { switch event { - case .redrawRequested(let windowID): + case .redrawRequested(.window(let windowID)): if let controller = await registry.controller(for: windowID) { log("redraw requested window=\(controller.name) id=\(windowID)") try await controller.redrawIfNeeded() diff --git a/Examples/WaylandClientKitDemo/main.swift b/Examples/WaylandClientKitDemo/main.swift index de6645be..37d56f8f 100644 --- a/Examples/WaylandClientKitDemo/main.swift +++ b/Examples/WaylandClientKitDemo/main.swift @@ -48,13 +48,13 @@ enum WaylandClientKitDemo { } case .diagnostic(let diagnostic): DemoLog.write("display diagnostic \(diagnostic)") - case .redrawRequested(let windowID): + case .redrawRequested(.window(let windowID)): guard windowID == window.id else { return false } let redrawState = demoState try await window.redraw { frame in drawDemoFrame(frame, state: redrawState) } - case .popupRedrawRequested: + case .redrawRequested: break case .windowCloseRequested(let windowID): guard windowID == window.id else { return false } diff --git a/IntegrationTests/FrameworkHostClient/Tests/WaylandFrameworkHostClientTests/WaylandFrameworkHostClientTests.swift b/IntegrationTests/FrameworkHostClient/Tests/WaylandFrameworkHostClientTests/WaylandFrameworkHostClientTests.swift index 33a3d7c0..722caf80 100644 --- a/IntegrationTests/FrameworkHostClient/Tests/WaylandFrameworkHostClientTests/WaylandFrameworkHostClientTests.swift +++ b/IntegrationTests/FrameworkHostClient/Tests/WaylandFrameworkHostClientTests/WaylandFrameworkHostClientTests.swift @@ -36,6 +36,11 @@ struct WaylandFrameworkHostClientTests { constraintAdjustment: [.slideX, .slideY] ) ) + let subsurfaceConfiguration = SubsurfaceConfiguration( + position: LogicalOffset(x: 8, y: 8), + size: try PositiveLogicalSize(width: 48, height: 32), + synchronizationMode: .synchronized + ) let graphicsConfiguration = WaylandGraphicsConfiguration( presentationPolicy: .software, synchronizationPolicy: .implicitOnly, @@ -48,6 +53,7 @@ struct WaylandFrameworkHostClientTests { #expect(displayConfiguration.eventStreams == streams) #expect(windowConfiguration.initialSize.width.rawValue == 128) #expect(popupConfiguration.positioner.size.width.rawValue == 64) + #expect(subsurfaceConfiguration.size.width.rawValue == 48) #expect(graphicsConfiguration.presentationPolicy == .software) #expect(metadata.damage == nil) } @@ -72,6 +78,12 @@ struct WaylandFrameworkHostClientTests { ) ) ) + let subsurface = try await window.createSubsurface( + configuration: SubsurfaceConfiguration( + position: LogicalOffset(x: 4, y: 4), + size: try PositiveLogicalSize(width: 24, height: 24) + ) + ) _ = display.events _ = display.inputEvents @@ -79,10 +91,13 @@ struct WaylandFrameworkHostClientTests { _ = display.dataTransferEvents _ = display.diagnostics _ = window.presentationEvents + _ = popup.presentationEvents + _ = subsurface.presentationEvents _ = capabilities.dragAndDrop _ = try await window.geometry _ = try await window.needsRedraw _ = try await popup.placement + _ = try await subsurface.geometry try await window.requestRedraw() try await window.show { frame in @@ -91,6 +106,18 @@ struct WaylandFrameworkHostClientTests { try await window.redraw { frame in fill(frame, color: 0x0030_2010) } + try await popup.show( + preparing: { reservation in reservation.id }, + { _, frame in fill(frame, color: 0x0020_4030) } + ) + try await subsurface.show( + preparing: { reservation in reservation.id }, + { _, frame in fill(frame, color: 0x0040_3020) } + ) + try await popup.requestRedraw() + try await subsurface.requestRedraw() + try await popup.redraw { frame in fill(frame, color: 0x0030_4020) } + try await subsurface.redraw { frame in fill(frame, color: 0x0020_3040) } if capabilities.dragAndDrop.isAvailable { let source = try await window.startDrag( @@ -110,6 +137,7 @@ struct WaylandFrameworkHostClientTests { } await popup.close() + await subsurface.close() await window.close() } @@ -149,6 +177,31 @@ struct WaylandFrameworkHostClientTests { try await window.show { frame in fill(frame, color: 0x0020_3040) } + let popup = try await window.createPopup( + configuration: try PopupConfiguration( + positioner: PopupPositioner( + anchorRect: LogicalRect(x: 0, y: 0, width: 24, height: 24), + size: PositiveLogicalSize(width: 40, height: 32) + ) + ) + ) + let subsurface = try await window.createSubsurface( + configuration: SubsurfaceConfiguration( + position: LogicalOffset(x: 8, y: 8), + size: try PositiveLogicalSize(width: 32, height: 24), + synchronizationMode: .synchronized + ) + ) + #expect( + try await popup.show { frame in fill(frame, color: 0x0030_2040) } + == .presented + ) + #expect( + try await subsurface.show( + preparing: { reservation in reservation.id }, + { _, frame in fill(frame, color: 0x0040_2030) } + ) == .presented + ) let geometry = try await window.geometry #expect(geometry.bufferSize.width.rawValue > 0) #expect(try await !window.isClosed) @@ -167,6 +220,8 @@ struct WaylandFrameworkHostClientTests { } } + await popup.close() + await subsurface.close() await window.close() } } diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift new file mode 100644 index 00000000..4f71c80f --- /dev/null +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift @@ -0,0 +1,113 @@ +import Testing +import WaylandClient + +@Suite("Managed surface presentation public API surface") +struct ManagedSurfacePresentationAPISurfaceTests { + @Test + func softwarePresentationOutcomesCompileForExternalClients() { + let outcomes: [SoftwarePresentationOutcome] = [ + .presented, + .superseded, + .deferred, + .closed, + ] + + #expect(Set(outcomes).count == 4) + _ = useAtomicWindowPresentationAPI + _ = useAtomicPopupPresentationAPI + _ = useAtomicSubsurfacePresentationAPI + } + + private func useAtomicWindowPresentationAPI( + _ window: Window + ) async throws -> [SoftwarePresentationOutcome] { + let dirtyRectangle = try LogicalRect(x: 0, y: 0, width: 1, height: 1) + let metadata = SurfaceFrameMetadata( + contentType: .photo, + presentationHint: .vsync, + alpha: .opaque, + colorRepresentation: SurfaceColorRepresentation( + alphaMode: .premultipliedElectrical + ), + damage: try SurfaceDamageRegion([dirtyRectangle]) + ) + + let simpleShow = try await window.show( + metadata: metadata, + requestPresentationFeedback: true, + timeoutMilliseconds: 1_000 + ) { _ in () } + let simpleRedraw = try await window.redraw( + metadata: metadata, + requestPresentationFeedback: true + ) { _ in () } + let preparedShow = try await window.show( + metadata: metadata, + requestPresentationFeedback: true, + timeoutMilliseconds: 1_000, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + let preparedRedraw = try await window.redraw( + metadata: metadata, + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + return [simpleShow, simpleRedraw, preparedShow, preparedRedraw] + } + + private func useAtomicPopupPresentationAPI( + _ popup: PopupSurface + ) async throws -> [SoftwarePresentationOutcome] { + let simpleShow = try await popup.show( + metadata: .default, + requestPresentationFeedback: true, + timeoutMilliseconds: 1_000 + ) { _ in () } + let simpleRedraw = try await popup.redraw( + metadata: .default, + requestPresentationFeedback: true + ) { _ in () } + let preparedShow = try await popup.show( + metadata: .default, + requestPresentationFeedback: true, + timeoutMilliseconds: 1_000, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + let preparedRedraw = try await popup.redraw( + metadata: .default, + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + return [simpleShow, simpleRedraw, preparedShow, preparedRedraw] + } + + private func useAtomicSubsurfacePresentationAPI( + _ subsurface: Subsurface + ) async throws -> [SoftwarePresentationOutcome] { + let simpleShow = try await subsurface.show( + metadata: .default, + requestPresentationFeedback: true + ) { _ in () } + let simpleRedraw = try await subsurface.redraw( + metadata: .default, + requestPresentationFeedback: true + ) { _ in () } + let preparedShow = try await subsurface.show( + metadata: .default, + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + let preparedRedraw = try await subsurface.redraw( + metadata: .default, + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + return [simpleShow, simpleRedraw, preparedShow, preparedRedraw] + } +} diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift new file mode 100644 index 00000000..61af5fd0 --- /dev/null +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift @@ -0,0 +1,197 @@ +import Testing +import WaylandClient + +@Suite( + "Managed surface presentation feedback integration", + .enabled( + if: PublicIntegrationEnvironment.isEnabled, + "Set WAYLAND_DISPLAY and WAYLAND_CLIENT_KIT_ENABLE_PUBLIC_INTEGRATION_TESTS=1" + ), + .timeLimit(.minutes(1)), + .serialized +) +struct ManagedPresentationFeedbackTests { + @Test + func subsurfaceCreationPublishesManagedRedrawEvent() async throws { + try await withPublicConnection { display in + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await window.show { frame in fill(frame, color: 0x0012_1212) } + + let event = try await displayEvent( + in: display.events, + matching: { event in + if case .redrawRequested(.subsurface) = event { return true } + return false + }, + after: { + _ = try await window.createSubsurface( + configuration: SubsurfaceConfiguration( + position: LogicalOffset(x: 4, y: 4), + size: try PositiveLogicalSize(width: 32, height: 32) + ) + ) + } + ) + guard case .redrawRequested(.subsurface) = event else { + Issue.record("Expected a managed subsurface redraw event") + return + } + await window.close() + } + } + + @Test + func allManagedFeedbackReachesRootAndFilteredStreams() async throws { + try await withPublicConnection { display in + try await exerciseManagedFeedback(on: display) + } + } + + private func exerciseManagedFeedback(on display: WaylandDisplay) async throws { + let capabilities = try await display.capabilities() + guard capabilities.presentationTime.isAvailable else { + try noteOptionalProtocolSkip( + test: "managed surface presentation feedback", + interfaceName: "wp_presentation" + ) + return + } + + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await expectFeedback( + from: window.presentationEvents, + surface: .window(window.id), + rootEvents: display.events + ) { + try await window.show(requestPresentationFeedback: true) { frame in + fill(frame, color: 0x0012_2436) + } + } + + let popup = try await window.createPopup(configuration: testPopupConfiguration()) + try await expectFeedback( + from: popup.presentationEvents, + surface: .popup(popup.id), + rootEvents: display.events + ) { + try await popup.show(requestPresentationFeedback: true) { frame in + fill(frame, color: 0x0036_2412) + } + } + + let subsurface = try await window.createSubsurface( + configuration: SubsurfaceConfiguration( + position: LogicalOffset(x: 8, y: 8), + size: try PositiveLogicalSize(width: 48, height: 48), + synchronizationMode: .synchronized + ) + ) + try await expectFeedback( + from: subsurface.presentationEvents, + surface: .subsurface(subsurface.identity), + rootEvents: display.events + ) { + try await subsurface.show(requestPresentationFeedback: true) { frame in + fill(frame, color: 0x0024_3612) + } + } + + await popup.close() + await subsurface.close() + await window.close() + } + + private func expectFeedback( + from filteredEvents: ManagedSurfacePresentationEvents, + surface: ManagedSurfaceIdentity, + rootEvents: DisplayEvents, + presenting: () async throws -> SoftwarePresentationOutcome + ) async throws { + let subscriptionGate = ManagedFeedbackSubscriptionGate(requiredCount: 2) + async let rootEvent = nextRootPresentation( + in: rootEvents, + matching: surface, + gate: subscriptionGate + ) + async let filteredFeedback = nextFilteredPresentation( + in: filteredEvents, + gate: subscriptionGate + ) + await subscriptionGate.waitUntilReady() + #expect(try await presenting() == .presented) + + let (resolvedRootEvent, resolvedFilteredFeedback) = try await ( + rootEvent, + filteredFeedback + ) + #expect(resolvedRootEvent.feedback == resolvedFilteredFeedback) + } + + private func nextRootPresentation( + in events: DisplayEvents, + matching surface: ManagedSurfaceIdentity, + gate: ManagedFeedbackSubscriptionGate + ) async throws -> ManagedSurfacePresentationEvent { + try await withTimeout( + nanoseconds: publicIntegrationWaitTimeoutNanoseconds, + operation: "waiting for managed root presentation feedback" + ) { + var iterator = events.makeAsyncIterator() + await gate.arrive() + while let event = try await iterator.next() { + if case .presentation(let presentation) = event, + presentation.surface == surface + { + return presentation + } + } + throw PublicIntegrationError.streamEnded + } + } + + private func nextFilteredPresentation( + in events: ManagedSurfacePresentationEvents, + gate: ManagedFeedbackSubscriptionGate + ) async throws -> SurfacePresentationFeedback { + try await withTimeout( + nanoseconds: publicIntegrationWaitTimeoutNanoseconds, + operation: "waiting for managed filtered presentation feedback" + ) { + var iterator = events.makeAsyncIterator() + await gate.arrive() + guard let feedback = try await iterator.next() else { + throw PublicIntegrationError.streamEnded + } + return feedback + } + } +} + +private actor ManagedFeedbackSubscriptionGate { + private let requiredCount: Int + private var count = 0 + private var waiters: [CheckedContinuation] = [] + + init(requiredCount: Int) { + self.requiredCount = requiredCount + } + + func arrive() { + count += 1 + guard count >= requiredCount else { return } + let pendingWaiters = waiters + waiters.removeAll() + for waiter in pendingWaiters { + waiter.resume() + } + } + + func waitUntilReady() async { + guard count < requiredCount else { return } + await withCheckedContinuation { waiters.append($0) } + } +} diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift index d30bb36e..f186adf8 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift @@ -23,6 +23,32 @@ extension WaylandDisplayPublicIntegrationTests { } } + @Test + func closingPopupParentDuringPreparationReturnsClosedWithoutDrawing() async throws { + try await withPublicConnection { display in + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await show(window, color: 0x0011_2233) + let popup = try await window.createPopup(configuration: testPopupConfiguration()) + let gate = AsyncSoftwarePreparationGate() + + async let outcome = popup.show( + timeoutMilliseconds: publicIntegrationTimeoutMilliseconds, + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) + await gate.waitUntilSuspended() + await window.close() + await gate.resumePreparation() + + #expect(try await outcome == .closed) + } + } + @Test func closingDisplayDuringAsyncSoftwarePreparationReturnsClosedWithoutDrawing() async throws { try await withPublicConnection { display in @@ -66,7 +92,7 @@ extension WaylandDisplayPublicIntegrationTests { async let replacementEvent = displayEvent( in: displayEvents, - matching: { $0 == .redrawRequested(window.id) }, + matching: { $0 == .redrawRequested(.window(window.id)) }, after: { try await window.requestRedraw() await gate.resumePreparation() @@ -74,7 +100,7 @@ extension WaylandDisplayPublicIntegrationTests { ) #expect(try await staleOutcome == .superseded) - #expect(try await replacementEvent == .redrawRequested(window.id)) + #expect(try await replacementEvent == .redrawRequested(.window(window.id))) #expect(try await window.needsRedraw) let replacementOutcome = try await window.redraw( preparing: { $0.id }, @@ -113,11 +139,11 @@ extension WaylandDisplayPublicIntegrationTests { async let replacementEvent = displayEvent( in: displayEvents, - matching: { $0 == .redrawRequested(window.id) }, + matching: { $0 == .redrawRequested(.window(window.id)) }, after: { await gate.resumePreparation() } ) let nextOutcome = try await group.next() - #expect(try await replacementEvent == .redrawRequested(window.id)) + #expect(try await replacementEvent == .redrawRequested(.window(window.id))) return try #require(nextOutcome) } #expect(outcome == .superseded) @@ -198,7 +224,7 @@ extension WaylandDisplayPublicIntegrationTests { let replacementEvent = try await displayEvent( in: displayEvents, - matching: { $0 == .redrawRequested(window.id) }, + matching: { $0 == .redrawRequested(.window(window.id)) }, after: { do { _ = try await window.redraw( @@ -211,7 +237,7 @@ extension WaylandDisplayPublicIntegrationTests { } } ) - #expect(replacementEvent == .redrawRequested(window.id)) + #expect(replacementEvent == .redrawRequested(.window(window.id))) #expect( try await window.redraw( preparing: { $0.id }, @@ -267,7 +293,7 @@ private func expectPresentationFeedback( _ = try await displayEvent( in: displayEvents, matching: { event in - event == .redrawRequested(window.id) + event == .redrawRequested(.window(window.id)) }, after: { try await window.requestRedraw() @@ -307,7 +333,7 @@ private func expectPresentationFeedback( } private func nextPresentationFeedback( - in events: WindowPresentationEvents + in events: ManagedSurfacePresentationEvents ) async throws -> SurfacePresentationFeedback? { var iterator = events.makeAsyncIterator() return try await iterator.next() @@ -325,7 +351,7 @@ private func waitForRedrawRequest( ) async throws { _ = try await displayEvent( in: displayEvents, - matching: { $0 == .redrawRequested(window.id) }, + matching: { $0 == .redrawRequested(.window(window.id)) }, after: { try await window.requestRedraw() } ) } diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift index 66746dc5..95fb1516 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift @@ -237,16 +237,18 @@ struct WaylandPresentationAPISurfaceTests { func inspectEvent(_ event: SurfacePresentationFeedback) -> SurfacePresentationIdentity { event.surface } - func inspectWindowEvent(_ event: WindowPresentationEvent) -> WindowID { - event.windowID + func inspectManagedEvent( + _ event: ManagedSurfacePresentationEvent + ) -> ManagedSurfaceIdentity { + event.surface } - func inspectDisplayEvent(_ event: DisplayEvent) -> WindowPresentationEvent? { + func inspectDisplayEvent(_ event: DisplayEvent) -> ManagedSurfacePresentationEvent? { guard case .presentation(let presentation) = event else { return nil } return presentation } _ = inspectFeedback _ = inspectEvent - _ = inspectWindowEvent + _ = inspectManagedEvent _ = inspectDisplayEvent func usePresentationFeedbackAPI(window: Window) async throws { @@ -255,59 +257,16 @@ struct WaylandPresentationAPISurfaceTests { try await window.requestPresentationFeedback() } - _ = usePresentationFeedbackAPI - } - - @Test - func softwarePresentationOutcomesCompileForExternalClients() { - let outcomes: [SoftwarePresentationOutcome] = [ - .presented, - .superseded, - .deferred, - .closed, - ] - - #expect(Set(outcomes).count == 4) - _ = useAtomicSoftwarePresentationAPI - } - - private func useAtomicSoftwarePresentationAPI( - _ window: Window - ) async throws -> [SoftwarePresentationOutcome] { - let dirtyRectangle = try LogicalRect(x: 0, y: 0, width: 1, height: 1) - let metadata = SurfaceFrameMetadata( - contentType: .photo, - presentationHint: .vsync, - alpha: .opaque, - colorRepresentation: SurfaceColorRepresentation( - alphaMode: .premultipliedElectrical - ), - damage: try SurfaceDamageRegion([dirtyRectangle]) - ) + func useManagedSurfacePresentationStreams( + popup: PopupSurface, + subsurface: Subsurface + ) { + _ = popup.presentationEvents.makeAsyncIterator() + _ = subsurface.presentationEvents.makeAsyncIterator() + } - let simpleShow = try await window.show( - metadata: metadata, - requestPresentationFeedback: true, - timeoutMilliseconds: 1_000 - ) { _ in () } - let simpleRedraw = try await window.redraw( - metadata: metadata, - requestPresentationFeedback: true - ) { _ in () } - let preparedShow = try await window.show( - metadata: metadata, - requestPresentationFeedback: true, - timeoutMilliseconds: 1_000, - preparing: { reservation in reservation.id }, - { _, _ in () } - ) - let preparedRedraw = try await window.redraw( - metadata: metadata, - requestPresentationFeedback: true, - preparing: { reservation in reservation.id }, - { _, _ in () } - ) - return [simpleShow, simpleRedraw, preparedShow, preparedRedraw] + _ = usePresentationFeedbackAPI + _ = useManagedSurfacePresentationStreams } } diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift index a3199eb2..952b9ffe 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift @@ -61,13 +61,13 @@ struct WaylandDisplayPublicIntegrationTests { let redrawEvent = try await displayEvent( in: displayEvents, matching: { event in - event == .redrawRequested(window.id) + event == .redrawRequested(.window(window.id)) }, after: { try await window.requestRedraw() } ) - #expect(redrawEvent == .redrawRequested(window.id)) + #expect(redrawEvent == .redrawRequested(.window(window.id))) #expect(try await window.needsRedraw) try await window.redraw { frame in @@ -99,6 +99,36 @@ struct WaylandDisplayPublicIntegrationTests { } } + @Test + func invalidPopupDamageDoesNotPoisonFollowingPresentation() async throws { + try await withPublicConnection { display in + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await show(window, color: 0x0020_2020) + let popup = try await window.createPopup(configuration: testPopupConfiguration()) + let invalidRect = try LogicalRect(x: 65, y: 0, width: 1, height: 1) + let metadata = SurfaceFrameMetadata( + damage: try SurfaceDamageRegion([invalidRect]) + ) + + do { + _ = try await popup.show( + metadata: metadata, + timeoutMilliseconds: publicIntegrationTimeoutMilliseconds, + drawColor(0x0040_4040) + ) + Issue.record("expected invalid popup damage") + } catch let error as SurfaceRegionError { + #expect(error == .damageRectangleOutOfBounds(invalidRect)) + } + + #expect(try await popup.show(drawColor(0x0050_5050)) == .presented) + await popup.close() + await window.close() + } + } + @Test func hiddenCursorRequestWithoutPointerFocusIsDeterministic() async throws { try await WaylandDisplay.withConnection( @@ -151,7 +181,7 @@ func testWindowConfiguration() throws -> WindowConfiguration { ) } -private func testPopupConfiguration() throws -> PopupConfiguration { +func testPopupConfiguration() throws -> PopupConfiguration { let anchorRect = try LogicalRect(x: 0, y: 0, width: 32, height: 32) let popupSize = try PositiveLogicalSize(width: 64, height: 48) @@ -175,7 +205,13 @@ func show(_ window: Window, color: UInt32) async throws { private func show(_ popup: PopupSurface, color: UInt32) async throws { let timeout = publicIntegrationTimeoutMilliseconds - try await popup.show(timeoutMilliseconds: timeout, drawColor(color)) + let outcome = try await popup.show( + requestPresentationFeedback: true, + timeoutMilliseconds: timeout, + preparing: { reservation in reservation.id }, + { _, frame in fill(frame, color: color) } + ) + #expect(outcome == .presented) } private struct PublicStreamSources: Sendable { @@ -264,35 +300,32 @@ private func expectShownPopup(_ popup: PopupSurface) async throws { private func redraw( _ popup: PopupSurface, - parent window: Window, + parent _: Window, events displayEvents: DisplayEvents ) async throws { let redrawEvent = try await displayEvent( in: displayEvents, matching: { event in - isPopupLifecycleEvent( - event, - eventCase: .redrawRequested, - popup: popup.identity, - parentWindowID: window.id - ) + event == .redrawRequested(.popup(popup.identity)) }, after: { try await popup.requestRedraw() } ) - guard case .popupRedrawRequested(let lifecycleEvent) = redrawEvent else { + guard case .redrawRequested(.popup(let identity)) = redrawEvent else { Issue.record("Expected popup redraw event, got \(redrawEvent)") return } - #expect(lifecycleEvent.popup == popup.identity) - #expect(lifecycleEvent.parentWindowID == window.id) + #expect(identity == popup.identity) #expect(try await popup.needsRedraw) - try await popup.redraw { frame in - fill(frame, color: 0x0050_5050) - } + let outcome = try await popup.redraw( + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, frame in fill(frame, color: 0x0050_5050) } + ) + #expect(outcome == .presented) } private func close( @@ -321,9 +354,8 @@ private func close( let closeEvent = try await displayEvent( in: displayEvents, matching: { event in - isPopupLifecycleEvent( + isPopupClosedEvent( event, - eventCase: .closed, popup: popup.identity, parentWindowID: window.id ) @@ -343,26 +375,12 @@ private func close( #expect(try await popup.isClosed) } -private enum PopupEventCase { - case redrawRequested - case closed -} - -private func isPopupLifecycleEvent( +private func isPopupClosedEvent( _ event: DisplayEvent, - eventCase expectedCase: PopupEventCase, popup expectedPopup: PopupSurfaceIdentity, parentWindowID expectedParentWindowID: WindowID ) -> Bool { - let lifecycleEvent: PopupLifecycleEvent - switch (expectedCase, event) { - case (.redrawRequested, .popupRedrawRequested(let event)): - lifecycleEvent = event - case (.closed, .popupClosed(let event)): - lifecycleEvent = event - case (.redrawRequested, _), (.closed, _): - return false - } + guard case .popupClosed(let lifecycleEvent) = event else { return false } return lifecycleEvent.popup == expectedPopup && lifecycleEvent.parentWindowID == expectedParentWindowID diff --git a/README.md b/README.md index 7f16d125..12966084 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Both products are baseline and audit tracked under the Current public coverage includes: -- display, window, popup, dialog, and subsurface lifecycle -- software drawing, damage, scale, regions, and presentation feedback +- display and managed window, popup, dialog, and subsurface lifecycle +- transactional software drawing, damage, scale, regions, and presentation feedback - pointer, keyboard, touch, tablet, gesture, cursor, and text input - clipboard, primary selection, and drag-and-drop - desktop and output integration diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+Events.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+Events.swift index e35647c8..7fe27ec6 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Events.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Events.swift @@ -105,7 +105,6 @@ private func isOutputChange(_ event: DisplayEvent) -> Bool { .popupDismissed, .popupClosed, .redrawRequested, - .popupRedrawRequested, .windowOutputsChanged, .keyboardShortcutsInhibitorChanged: false diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift index a407fa8b..a8fcfd5e 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift @@ -77,28 +77,73 @@ extension DisplayCore { } } - func showPopup( - _ popupID: PopupID, + func reservePopupSoftwareFrameForShow( + id popupID: PopupID, timeoutMilliseconds: Int32, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { try withFatalFailureFinalization { - let popup = try requireOpenPopup(popupID) - try popup.showOnOwnerThread(timeoutMilliseconds: timeoutMilliseconds, draw) - guard !isClosed, let activeSession else { return } - publishSessionEvents(activeSession) + guard !isClosed, let popup = surfaces.popup(popupID), !popup.isClosedOnOwnerThread + else { + return .closed + } + return try popup.reserveSoftwareFrameForShowOnOwnerThread( + timeoutMilliseconds: timeoutMilliseconds, + metadata: metadata + ) } } - func redrawPopup( - _ popupID: PopupID, + func reservePopupSoftwareFrameForRedraw( + id popupID: PopupID, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + try withFatalFailureFinalization { + guard !isClosed, let popup = surfaces.popup(popupID), !popup.isClosedOnOwnerThread + else { + return .closed + } + return try popup.reserveSoftwareFrameForRedrawOnOwnerThread(metadata: metadata) + } + } + + func submitReservedPopupSoftwareFrame( + id popupID: PopupID, + reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + requestPresentationFeedback: Bool, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { + ) throws -> SoftwarePresentationOutcome { try withFatalFailureFinalization { - try requireOpenPopup(popupID).redrawOnOwnerThread(draw) - guard !isClosed else { - throw ClientError.display(.closed) + guard !isClosed, let popup = surfaces.popup(popupID), !popup.isClosedOnOwnerThread + else { + return .closed } + let outcome = try popup.submitReservedSoftwareFrameOnOwnerThread( + reservation, + metadata: metadata, + makePresentationFeedback: { [self] in + try presentationFeedbackCommitRequest( + for: popup, + popupID: popupID, + isRequested: requestPresentationFeedback + ) + }, + draw + ) + guard !isClosed, let activeSession else { return .closed } + publishSessionEvents(activeSession) + return outcome + } + } + + func cancelReservedPopupSoftwareFrame( + id popupID: PopupID, + reservation: SoftwareFrameReservation + ) { + withFatalFailureFinalization { + guard !isClosed, let popup = surfaces.popup(popupID) else { return } + popup.cancelReservedSoftwareFrameOnOwnerThread(reservation) } } @@ -187,11 +232,8 @@ extension DisplayCore { core?.handlePopupClosed(popupID) }, onRedrawRequested: { [weak core = self] in - core?.publishPopupRedrawRequested( - popupID: popupID, - parentWindowID: parentWindowID - ) - } + core?.publishPopupRedrawRequested(popupID: popupID) + }, ) } @@ -230,13 +272,9 @@ extension DisplayCore { assertSurfaceStoreInvariants() } - private func publishPopupRedrawRequested(popupID: PopupID, parentWindowID: WindowID) { + private func publishPopupRedrawRequested(popupID: PopupID) { guard surfaceGraphAcceptsLifecycleCallback() else { return } - eventHub.publish( - .popupRedrawRequested( - PopupLifecycleEvent(popup: popupID, parentWindowID: parentWindowID) - ) - ) + eventHub.publish(.redrawRequested(.popup(PopupSurfaceIdentity(popupID)))) } func popupIDsTopDown(parentedBy windowID: WindowID) -> [PopupID] { diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift index d9eab686..513822af 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift @@ -11,7 +11,10 @@ extension DisplayCore { }, onFeedback: { [weak self] feedback in self?.eventHub.publishPresentation( - WindowPresentationEvent(windowID: windowID, feedback: feedback) + ManagedSurfacePresentationEvent( + surface: .window(windowID), + feedback: feedback + ) ) } ) @@ -23,12 +26,12 @@ extension DisplayCore { windowID: WindowID, isRequested: Bool, onFeedback: (@Sendable (SurfacePresentationFeedback) -> Void)? = nil - ) throws -> WindowPresentationFeedbackCommitRequest? { + ) throws -> SurfacePresentationFeedbackCommitRequest? { guard isRequested else { return nil } let session = try requireSession() let presentation = try session.presentationOnOwnerThread() - return WindowPresentationFeedbackCommitRequest( + return SurfacePresentationFeedbackCommitRequest( request: { try window.requestPresentationFeedbackOnOwnerThread( presentation: presentation, @@ -37,8 +40,8 @@ extension DisplayCore { }, onFeedback: { [weak self] feedback in self?.eventHub.publishPresentation( - WindowPresentationEvent( - windowID: windowID, + ManagedSurfacePresentationEvent( + surface: .window(windowID), feedback: feedback ) ) @@ -51,4 +54,54 @@ extension DisplayCore { } ) } + + func presentationFeedbackCommitRequest( + for popup: PopupRoleSurface, + popupID: PopupID, + isRequested: Bool + ) throws -> SurfacePresentationFeedbackCommitRequest? { + guard isRequested else { return nil } + + let session = try requireSession() + let presentation = try session.presentationOnOwnerThread() + return try popup.presentationFeedbackCommitRequestOnOwnerThread( + presentation: presentation, + outputIDForPresentationSyncOutput: { output in + try session.outputIDForPresentationSyncOutput(output) + }, + onFeedback: { [weak self] feedback in + self?.eventHub.publishPresentation( + ManagedSurfacePresentationEvent( + surface: .popup(PopupSurfaceIdentity(popupID)), + feedback: feedback + ) + ) + } + ) + } + + func presentationFeedbackCommitRequest( + for subsurface: SubsurfaceRoleSurface, + subsurfaceID: SubsurfaceID, + isRequested: Bool + ) throws -> SurfacePresentationFeedbackCommitRequest? { + guard isRequested else { return nil } + + let session = try requireSession() + let presentation = try session.presentationOnOwnerThread() + return try subsurface.presentationFeedbackCommitRequestOnOwnerThread( + presentation: presentation, + outputIDForPresentationSyncOutput: { output in + try session.outputIDForPresentationSyncOutput(output) + }, + onFeedback: { [weak self] feedback in + self?.eventHub.publishPresentation( + ManagedSurfacePresentationEvent( + surface: .subsurface(SubsurfaceIdentity(subsurfaceID)), + feedback: feedback + ) + ) + } + ) + } } diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift index 954de748..477ddc20 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift @@ -7,12 +7,17 @@ extension DisplayCore { let parentWindow = try requireOpenWindow(windowID) let subsurface = try requireSession().createSubsurfaceOnOwnerThread( parent: parentWindow, - configuration: subsurfaceConfiguration + configuration: subsurfaceConfiguration, + failureSink: WeakWindowFailureSink(self) ) guard !isClosed else { subsurface.closeOnOwnerThread() throw ClientError.display(.closed) } + let managedIdentity = SubsurfaceIdentity(subsurface.id) + subsurface.onRedrawRequested = { [weak eventHub] in + eventHub?.publish(.redrawRequested(.subsurface(managedIdentity))) + } registerSubsurface(subsurface) try commitSubsurfaceParentStateIfNeeded( SubsurfaceParentCommitPolicy.requirement( @@ -20,40 +25,83 @@ extension DisplayCore { subsurfaceID: subsurface.id, event: .created )) + try subsurface.requestRedrawOnOwnerThread() return subsurface.id } } - func showSubsurface( - _ subsurfaceID: SubsurfaceID, - damage: SurfaceDamageRegion?, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { + func reserveSubsurfaceSoftwareFrameForShow( + id subsurfaceID: SubsurfaceID, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + try withFatalFailureFinalization { + guard let subsurface = subsurfacesByID[subsurfaceID], + !subsurface.isClosedOnOwnerThread, + let parentWindow = surfaces.window(subsurface.parentWindowID), + !parentWindow.isClosedOnOwnerThread + else { return .closed } + return try subsurface.reserveSoftwareFrameForShowOnOwnerThread(metadata: metadata) + } + } + + func reserveSubsurfaceSoftwareFrameForRedraw( + id subsurfaceID: SubsurfaceID, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + try withFatalFailureFinalization { + guard let subsurface = subsurfacesByID[subsurfaceID], + !subsurface.isClosedOnOwnerThread, + let parentWindow = surfaces.window(subsurface.parentWindowID), + !parentWindow.isClosedOnOwnerThread + else { return .closed } + return try subsurface.reserveSoftwareFrameForRedrawOnOwnerThread(metadata: metadata) + } + } + + // swiftlint:disable:next function_parameter_count + func submitReservedSubsurfaceSoftwareFrame( + id subsurfaceID: SubsurfaceID, + reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + requestPresentationFeedback: Bool, + prepared: sending Prepared, + _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void + ) throws -> SoftwarePresentationOutcome { try withFatalFailureFinalization { - let requirement = try requireOpenSubsurface(subsurfaceID).showOnOwnerThread( - damage: damage, - draw + guard let subsurface = subsurfacesByID[subsurfaceID], + !subsurface.isClosedOnOwnerThread, + let parentWindow = surfaces.window(subsurface.parentWindowID), + !parentWindow.isClosedOnOwnerThread + else { return .closed } + + return try subsurface.submitReservedSoftwareFrameOnOwnerThread( + reservation: reservation, + metadata: metadata, + makePresentationFeedback: { + try self.presentationFeedbackCommitRequest( + for: subsurface, + subsurfaceID: subsurfaceID, + isRequested: requestPresentationFeedback + ) + }, + commitSynchronizedParent: { + parentWindow + .commitSynchronizedSubsurfacePresentationParentStateOnOwnerThread() + }, + { frame in + try draw(prepared, frame) + } ) - try commitSubsurfaceParentStateIfNeeded(requirement) - guard !isClosed, let activeSession else { return } - publishSessionEvents(activeSession) } } - func redrawSubsurface( - _ subsurfaceID: SubsurfaceID, - damage: SurfaceDamageRegion?, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + func cancelReservedSubsurfaceSoftwareFrame( + id subsurfaceID: SubsurfaceID, + reservation: SoftwareFrameReservation ) throws { try withFatalFailureFinalization { - let requirement = try requireOpenSubsurface(subsurfaceID).redrawOnOwnerThread( - damage: damage, - draw - ) - try commitSubsurfaceParentStateIfNeeded(requirement) - guard !isClosed else { - throw ClientError.display(.closed) - } + guard let subsurface = subsurfacesByID[subsurfaceID] else { return } + try subsurface.cancelReservedSoftwareFrameOnOwnerThread(reservation: reservation) } } @@ -136,14 +184,17 @@ extension DisplayCore { } } - func closeSubsurface(_ subsurfaceID: SubsurfaceID) { + func closeSubsurface( + _ subsurfaceID: SubsurfaceID, + parentWindowClosed: Bool = false + ) { withFatalFailureFinalization { guard !hasPendingFatalFailure else { return } guard let subsurface = subsurfacesByID.removeValue(forKey: subsurfaceID) else { return } - subsurface.closeOnOwnerThread() + subsurface.closeOnOwnerThread(parentWindowClosed: parentWindowClosed) closedSubsurfaceIDs.insert(subsurfaceID) let parentWindowID = subsurfaceParentWindowIDs.removeValue(forKey: subsurfaceID) if let parentWindowID { diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore.swift b/Sources/WaylandClient/Internal/Display/DisplayCore.swift index 46d46ade..e7fece31 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore.swift @@ -125,7 +125,7 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { _ windowID: WindowID, timeoutMilliseconds: Int32, metadata: SurfaceFrameMetadata = .default - ) throws -> WindowSoftwareFrameReservationOutcome { + ) throws -> SoftwareSurfaceFrameReservationOutcome { try withFatalFailureFinalization { guard !isClosed, let window = surfaces.window(windowID) else { return .closed @@ -169,7 +169,7 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { func reserveSoftwareFrameForRedraw( _ windowID: WindowID, metadata frameMetadata: SurfaceFrameMetadata = .default - ) throws -> WindowSoftwareFrameReservationOutcome { + ) throws -> SoftwareSurfaceFrameReservationOutcome { try withFatalFailureFinalization { guard !isClosed, let window = surfaces.window(windowID) else { return .closed @@ -320,7 +320,7 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { closeKeyboardShortcutsInhibitor(inhibitorID) } for subsurfaceID in subsurfaceIDsTopDown(parentedBy: windowID) { - closeSubsurface(subsurfaceID) + closeSubsurface(subsurfaceID, parentWindowClosed: true) } for popupID in popupIDsTopDown(parentedBy: windowID) { closePopup(popupID) @@ -414,7 +414,7 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { private func publishWindowRedrawRequested(_ windowID: WindowID) { guard surfaceGraphAcceptsLifecycleCallback() else { return } - eventHub.publish(.redrawRequested(windowID)) + eventHub.publish(.redrawRequested(.window(windowID))) } private func handleWindowOutputsChanged( @@ -488,7 +488,7 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { closeKeyboardShortcutsInhibitor(inhibitorID) } for subsurfaceID in subsurfaceIDsTopDown(parentedBy: windowID) { - closeSubsurface(subsurfaceID) + closeSubsurface(subsurfaceID, parentWindowClosed: true) } for popupID in popupIDsTopDown(parentedBy: windowID) { closePopup(popupID) diff --git a/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift b/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift index 5d06353a..58c087cb 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift @@ -18,7 +18,7 @@ final class DisplayEventHub: Sendable { private let inputBroker: TypedEventBroker private let dataTransferBroker: TypedEventBroker private let textInputBroker: TypedEventBroker - private let presentationBroker: TypedEventBroker + private let presentationBroker: TypedEventBroker private let diagnosticsBroker: TypedEventBroker private let diagnosticIDGenerator: DiagnosticIDGenerator @@ -44,7 +44,7 @@ final class DisplayEventHub: Sendable { stream: .textInputEvents, capacity: configuration.textInputEventCapacity.rawValue ) - presentationBroker = TypedEventBroker( + presentationBroker = TypedEventBroker( stream: .presentationEvents, capacity: configuration.presentationEventCapacity.rawValue ) @@ -77,10 +77,14 @@ final class DisplayEventHub: Sendable { TextInputEvents(InternalEventSubscriptionFactory(textInputBroker)) } - func windowPresentationEvents(windowID: WindowID) -> WindowPresentationEvents { - WindowPresentationEvents( - windowID: windowID, - subscriptions: InternalEventSubscriptionFactory(presentationBroker) + func managedSurfacePresentationEvents( + surface: ManagedSurfaceIdentity + ) -> ManagedSurfacePresentationEvents { + ManagedSurfacePresentationEvents( + surface: surface, + subscriptions: InternalEventSubscriptionFactory(presentationBroker) { event in + event.surface == surface + } ) } @@ -101,7 +105,7 @@ final class DisplayEventHub: Sendable { case .presentation(let presentationEvent): publishPresentation(presentationEvent) case .windowCloseRequested, .windowClosed, .popupDismissed, .popupClosed, - .redrawRequested, .popupRedrawRequested, .outputChanged, .outputRemoved, + .redrawRequested, .outputChanged, .outputRemoved, .windowOutputsChanged, .keyboardShortcutsInhibitorChanged: displayBroker.publish(event) } @@ -143,7 +147,7 @@ final class DisplayEventHub: Sendable { textInputBroker.publish(event) } - func publishPresentation(_ event: WindowPresentationEvent) { + func publishPresentation(_ event: ManagedSurfacePresentationEvent) { displayBroker.publish(.presentation(event)) presentationBroker.publish(event) } diff --git a/Sources/WaylandClient/Internal/Display/DisplaySession+Subsurface.swift b/Sources/WaylandClient/Internal/Display/DisplaySession+Subsurface.swift index beba5ba9..0aec06d7 100644 --- a/Sources/WaylandClient/Internal/Display/DisplaySession+Subsurface.swift +++ b/Sources/WaylandClient/Internal/Display/DisplaySession+Subsurface.swift @@ -1,14 +1,16 @@ extension DisplaySession { package func createSubsurfaceOnOwnerThread( parent parentWindow: TopLevelWindow, - configuration subsurfaceConfiguration: SubsurfaceConfiguration + configuration subsurfaceConfiguration: SubsurfaceConfiguration, + failureSink subsurfaceFailureSink: any WindowFailureSink = DefaultWindowFailureSink() ) throws -> SubsurfaceRoleSurface { connection.preconditionIsOwnerThread() return try SubsurfaceRoleSurface( id: subsurfaceIDs.next(), parent: parentWindow, connection: connection, - configuration: subsurfaceConfiguration + configuration: subsurfaceConfiguration, + failureSink: subsurfaceFailureSink ) } } diff --git a/Sources/WaylandClient/Internal/Events/EventBroker.swift b/Sources/WaylandClient/Internal/Events/EventBroker.swift index c7066372..534eb923 100644 --- a/Sources/WaylandClient/Internal/Events/EventBroker.swift +++ b/Sources/WaylandClient/Internal/Events/EventBroker.swift @@ -125,6 +125,7 @@ final class TypedEventBroker: Sendable { } private struct Subscriber { + let includes: @Sendable (Element) -> Bool var state: SubscriberState = .open(buffer: [], drops: .none) } @@ -140,9 +141,11 @@ final class TypedEventBroker: Sendable { var subscribers: [Int: Subscriber] = [:] var lifecycle = BrokerLifecycle.open - mutating func subscribe() -> Int { + mutating func subscribe( + where includes: @escaping @Sendable (Element) -> Bool + ) -> Int { defer { nextID += 1 } - subscribers[nextID] = Subscriber() + subscribers[nextID] = Subscriber(includes: includes) return nextID } @@ -171,6 +174,7 @@ final class TypedEventBroker: Sendable { for subscriberID in subscribers.keys.sorted() { guard var subscriber = subscribers[subscriberID] else { continue } + guard subscriber.includes(element) else { continue } publish( element, for: &subscriber, @@ -367,8 +371,10 @@ final class TypedEventBroker: Sendable { overflowStrategy = eventOverflowStrategy } - func subscribe() -> InternalEventSubscription { - let subscriberID = state.withLock { $0.subscribe() } + func subscribe( + where includes: @escaping @Sendable (Element) -> Bool = { _ in true } + ) -> InternalEventSubscription { + let subscriberID = state.withLock { $0.subscribe(where: includes) } return InternalEventSubscription(EventSubscription(broker: self, id: subscriberID)) } diff --git a/Sources/WaylandClient/Internal/Popup/PopupModel.swift b/Sources/WaylandClient/Internal/Popup/PopupModel.swift index 5e081591..1ff690e9 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupModel.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupModel.swift @@ -1,68 +1,3 @@ -package enum PopupEvent: Equatable, Sendable { - case initialCommitSent - case configureReceived(PopupConfigureSequence) - case contentInvalidated(bufferAvailability: RedrawBufferAvailability) - case frameBecameReady(bufferAvailability: RedrawBufferAvailability) - case bufferBecameAvailable(bufferAvailability: RedrawBufferAvailability) - case redrawRequestConsumed(bufferAvailability: RedrawBufferAvailability) - case redrawRequestCanceled(bufferAvailability: RedrawBufferAvailability) - case presentationStarted(PopupPresentationRequest) - case presentationBlockedByBuffer - case presentationSucceeded(generation: UInt64, bufferAvailability: RedrawBufferAvailability) - case presentationFailed(generation: UInt64, PresentationError) - case explicitClose - case compositorDismissed - case transientStateReset -} - -package enum PopupEffect: Equatable, Sendable { - case ackConfigure(UInt32) - case publishDismissed(PopupLifecycleEvent) - case publishClosed(PopupLifecycleEvent) - case publishRedrawRequested(PopupLifecycleEvent) - case cancelFrameCallback - case performSoftwarePresent(PopupPresentationRequest) - case retireSwapchain - case destroyRoleObjects -} - -package struct PopupPresentationRequest: Equatable, Sendable { - package let generation: UInt64 - package let placement: PopupPlacement - - var summary: PopupPresentationRequestSummary { - PopupPresentationRequestSummary(generation: generation, placement: placement) - } -} - -package typealias PopupPresentationState = PresentationState - -package enum PopupLifecycle: Equatable, Sendable, CustomStringConvertible { - case created - case waitingForInitialConfigure - case active(ActivePopupState) - case destroyed - - package var description: String { - switch self { - case .created: - "created" - case .waitingForInitialConfigure: - "waitingForInitialConfigure" - case .active: - "active" - case .destroyed: - "destroyed" - } - } -} - -package struct ActivePopupState: Equatable, Sendable { - package var placement: PopupPlacement - var redraw = WindowRedrawState() - package var presentation = PopupPresentationState.idle -} - package struct PopupModel: Equatable, Sendable { package let id: PopupID package let parentWindowID: WindowID @@ -108,6 +43,14 @@ package struct PopupModel: Equatable, Sendable { activeState?.presentation ?? .idle } + package func isCurrentSoftwarePresentation(_ request: PopupPresentationRequest) -> Bool { + guard case .active(let activeState) = lifecycle else { return false } + + return activeState.presentation == .drawing(request: request) + && activeState.placement == request.placement + && activeState.redraw.generationForCurrentDraw == request.generation + } + // swiftlint:disable:next cyclomatic_complexity package mutating func reduce(_ event: PopupEvent) throws -> [PopupEffect] { switch event { @@ -129,6 +72,11 @@ package struct PopupModel: Equatable, Sendable { return try reducePresentationStarted(request) case .presentationBlockedByBuffer: return try reducePresentationBlockedByBuffer() + case .softwarePresentationSuperseded(let generation, let bufferAvailability): + return try reduceSoftwarePresentationSuperseded( + generation: generation, + bufferAvailability: bufferAvailability + ) case .presentationSucceeded(let generation, let bufferAvailability): return try reducePresentationSucceeded( generation: generation, @@ -217,9 +165,14 @@ extension PopupModel { nextActiveState.placement = sequence.placement var effects: [PopupEffect] = [.ackConfigure(sequence.serial)] + let redrawAvailability: RedrawBufferAvailability = + nextActiveState.presentation.isIdle ? .available : .unavailable effects.append( contentsOf: mapRedrawEffects( - nextActiveState.redraw.reduce(.contentInvalidated, bufferAvailability: .available) + nextActiveState.redraw.reduce( + .contentInvalidated, + bufferAvailability: redrawAvailability + ) ) ) lifecycle = .active(nextActiveState) @@ -315,6 +268,26 @@ extension PopupModel { } } + private mutating func reduceSoftwarePresentationSuperseded( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) throws -> [PopupEffect] { + let windowID = parentWindowID + let event = lifecycleEvent + return try transitionActivePopupState { activeState in + try Self.requireActivePresentation( + generation: generation, + in: activeState, + windowID: windowID + ) + activeState.presentation = .idle + let redrawEffects = activeState.redraw.supersedeSoftwarePresentation( + bufferAvailability: bufferAvailability + ) + return Self.mapRedrawEffects(redrawEffects, event: event) + } + } + private mutating func reducePresentationSucceeded( generation: UInt64, bufferAvailability: RedrawBufferAvailability @@ -361,8 +334,12 @@ extension PopupModel { guard !isClosed else { return [] } let lifecycleEvent = lifecycleEvent return updateActivePopupStateIfPresent { activeState in - Self.mapRedrawEffects( - activeState.redraw.reduce(event, bufferAvailability: bufferAvailability), + let redrawAvailability = + event == .contentInvalidated && !activeState.presentation.isIdle + ? RedrawBufferAvailability.unavailable + : bufferAvailability + return Self.mapRedrawEffects( + activeState.redraw.reduce(event, bufferAvailability: redrawAvailability), event: lifecycleEvent ) } diff --git a/Sources/WaylandClient/Internal/Popup/PopupModelTypes.swift b/Sources/WaylandClient/Internal/Popup/PopupModelTypes.swift new file mode 100644 index 00000000..41945658 --- /dev/null +++ b/Sources/WaylandClient/Internal/Popup/PopupModelTypes.swift @@ -0,0 +1,68 @@ +package enum PopupEvent: Equatable, Sendable { + case initialCommitSent + case configureReceived(PopupConfigureSequence) + case contentInvalidated(bufferAvailability: RedrawBufferAvailability) + case frameBecameReady(bufferAvailability: RedrawBufferAvailability) + case bufferBecameAvailable(bufferAvailability: RedrawBufferAvailability) + case redrawRequestConsumed(bufferAvailability: RedrawBufferAvailability) + case redrawRequestCanceled(bufferAvailability: RedrawBufferAvailability) + case presentationStarted(PopupPresentationRequest) + case presentationBlockedByBuffer + case softwarePresentationSuperseded( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) + case presentationSucceeded(generation: UInt64, bufferAvailability: RedrawBufferAvailability) + case presentationFailed(generation: UInt64, PresentationError) + case explicitClose + case compositorDismissed + case transientStateReset +} + +package enum PopupEffect: Equatable, Sendable { + case ackConfigure(UInt32) + case publishDismissed(PopupLifecycleEvent) + case publishClosed(PopupLifecycleEvent) + case publishRedrawRequested(PopupLifecycleEvent) + case cancelFrameCallback + case performSoftwarePresent(PopupPresentationRequest) + case retireSwapchain + case destroyRoleObjects +} + +package struct PopupPresentationRequest: Equatable, Sendable { + package let generation: UInt64 + package let placement: PopupPlacement + + var summary: PopupPresentationRequestSummary { + PopupPresentationRequestSummary(generation: generation, placement: placement) + } +} + +package typealias PopupPresentationState = PresentationState + +package enum PopupLifecycle: Equatable, Sendable, CustomStringConvertible { + case created + case waitingForInitialConfigure + case active(ActivePopupState) + case destroyed + + package var description: String { + switch self { + case .created: + "created" + case .waitingForInitialConfigure: + "waitingForInitialConfigure" + case .active: + "active" + case .destroyed: + "destroyed" + } + } +} + +package struct ActivePopupState: Equatable, Sendable { + package var placement: PopupPlacement + var redraw = WindowRedrawState() + package var presentation = PopupPresentationState.idle +} diff --git a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Effects.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Effects.swift new file mode 100644 index 00000000..25da2562 --- /dev/null +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Effects.swift @@ -0,0 +1,64 @@ +extension PopupRoleSurface { + package func interpretPopupEffects(_ effects: [PopupEffect]) throws { + try WaylandClient.interpretPopupEffects( + effects, + parentWindowID: parentWindowID, + handlers: popupEffectHandlers + ) + } + + func interpretPopupEffects( + _ effects: [PopupEffect], + performSoftwarePresent: (PopupPresentationRequest) throws -> RedrawOutcome + ) throws { + for effect in effects { + switch effect { + case .performSoftwarePresent(let request): + _ = try performSoftwarePresent(request) + default: + try interpretPopupEffects([effect]) + } + } + } + + private var popupEffectHandlers: PopupEffectHandlers { + PopupEffectHandlers( + ackConfigure: { [self] serial in + try acknowledgeSurfaceConfigure(serial: serial) + xdgSurface.ackConfigure(serial: serial) + }, + publishDismissed: { [self] _ in + onDismissed?() + onDismissed = nil + }, + publishClosed: { [self] _ in + onClosed?() + onClosed = nil + }, + publishRedrawRequested: { [self] _ in + onRedrawRequested?() + }, + cancelFrameCallback: { [self] in + pendingFrameRegistration = nil + cancelSurfaceFrameCallback() + }, + retireSwapchain: { [self] in + retireSwapchain() + }, + destroyRoleObjects: { [self] in + try destroyRoleObjects() + } + ) + } + + private func destroyRoleObjects() throws { + softwarePresentationCoordinator.close() + presentationFeedbackCoordinator.close() + onClose?() + onClose = nil + onRedrawRequested = nil + + destroyScaleResources() + try destroyRoleResources() + } +} diff --git a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Lifecycle.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Lifecycle.swift index 46d25dbc..9f05d959 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Lifecycle.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Lifecycle.swift @@ -86,17 +86,6 @@ extension PopupRoleSurface { surfaceRuntime.retireSharedMemoryPools(reason: .windowClosed) } - func drawAndPresent( - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> RedrawOutcome { - guard !model.isClosed else { return .skippedClosed } - - let effects = try model.reduce( - .redrawRequestConsumed(bufferAvailability: try redrawBufferAvailability()) - ) - return try interpretPresentationEffects(effects, draw) - } - package func failPresentationIfStillActive( generation: UInt64, error: PresentationError diff --git a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift index 5d5b4976..de40bb79 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift @@ -1,3 +1,5 @@ +import WaylandRaw + struct StagedPopupPresentationSuccess { let model: PopupModel let publishesRedrawRequest: Bool @@ -47,183 +49,372 @@ struct PopupPresentationSuccessStagingContext { } } +struct PendingPopupSoftwareFrameReservation { + let request: PopupPresentationRequest + let geometry: SurfaceGeometry + let reservedFrame: ReservedSoftwareSurfaceFrame +} + extension PopupRoleSurface { - // swiftlint:disable:next function_body_length - func performSoftwarePresent( - _ request: PopupPresentationRequest, + package func reserveSoftwareFrameForShowOnOwnerThread( + timeoutMilliseconds: Int32, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + connection.preconditionIsOwnerThread() + guard !model.isClosed else { return .closed } + try validateSurfaceFrameMetadataSupport(metadata) + if model.currentPlacement == nil { + _ = try waitForInitialConfigure(timeoutMilliseconds: timeoutMilliseconds) + } else { + _ = try consumeLatestConfigureIfAvailable() + } + return try reserveSoftwareFrameForCurrentRedraw(metadata: metadata) + } + + package func reserveSoftwareFrameForRedrawOnOwnerThread( + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + connection.preconditionIsOwnerThread() + guard !model.isClosed else { return .closed } + try validateSurfaceFrameMetadataSupport(metadata) + _ = try consumeLatestConfigureIfAvailable() + return try reserveSoftwareFrameForCurrentRedraw(metadata: metadata) + } + + // swiftlint:disable:next cyclomatic_complexity function_body_length + package func submitReservedSoftwareFrameOnOwnerThread( + _ reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + makePresentationFeedback: () throws -> SurfacePresentationFeedbackCommitRequest?, _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> RedrawOutcome { - try interpretPopupEffects( - model.reduce(.presentationStarted(request)) - ) + ) throws -> SoftwarePresentationOutcome { + connection.preconditionIsOwnerThread() + + guard !model.isClosed else { return .closed } + guard let pendingReservation = softwarePresentationCoordinator.take(reservation) else { + return model.isClosed ? .closed : .superseded + } + let request = pendingReservation.request + let currentGeometry: SurfaceGeometry do { - guard pendingFrameRegistration == nil else { - failActivePresentation( - generation: request.generation, - error: .frameCallbackRequest("frame callback already pending") - ) - return .skippedPendingFrame - } + _ = try consumeLatestConfigureIfAvailable() + currentGeometry = try self.currentGeometry() + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } - let geometry = try surfaceGeometry(logicalSize: request.placement.size) - let pool = try bufferPool(for: geometry.bufferSize) - dropReleasedRetiredPools() + guard !model.isClosed else { + pendingReservation.reservedFrame.drawingBuffer.discard() + return .closed + } - guard var drawingBuffer = pool.acquireDrawingBuffer() else { - try interpretPopupEffects(model.reduce(.presentationBlockedByBuffer)) - return .waitingForBuffer - } + guard + !Task.isCancelled, + model.isCurrentSoftwarePresentation(request), + currentGeometry == pendingReservation.geometry + else { + pendingReservation.reservedFrame.drawingBuffer.discard() + supersedeSoftwarePresentationIfStillActive(generation: request.generation) + return .superseded + } - do { - try unsafe drawingBuffer.withUnsafeMutableBytes { bytes in - let frame = try unsafe SoftwareFrame( - id: SoftwareFrameBufferID(rawValue: drawingBuffer.identity), - width: drawingBuffer.width, - height: drawingBuffer.height, - stride: drawingBuffer.stride, - geometry: SoftwareFrameGeometry(surface: geometry), - bytes: bytes - ) - try draw(frame) - } - } catch { - failActivePresentation( + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? + do { + try validateSurfaceFrameMetadataSupport(metadata) + try metadata.damage?.validate(within: currentGeometry) + try SurfaceMetadataSupport.ensureObjectsInstalled( + for: metadata.surfaceCommitMetadata, + connection: connection, + surface: surface, + runtime: &surfaceRuntime + ) + presentationFeedback = try makePresentationFeedback() + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } + + let successStagingContext = PopupPresentationSuccessStagingContext( + model: model, + parentWindowID: parentWindowID + ) + var stagedSuccess: StagedPopupPresentationSuccess? + let result: SoftwareSurfacePresentationResult + do { + result = try softwarePresenter().presentReserved( + pendingReservation.reservedFrame, + context: SoftwareSurfacePresentationContext( generation: request.generation, - error: .userDraw(String(describing: error)) - ) - drawingBuffer.discard() - throw error - } + geometry: currentGeometry, + submitConstraints: .default, + metadata: metadata.surfaceCommitMetadata, + damage: metadata.damage, + presentationFeedback: presentationFeedback + ), + draw: draw, + stageSuccess: { currentBufferAvailability in + stagedSuccess = try successStagingContext.stage( + generation: request.generation, + bufferAvailability: currentBufferAvailability + ) + }, + runtime: &surfaceRuntime, + pendingFrameRegistration: &pendingFrameRegistration + ) + } catch let failure as SoftwareSurfacePresentationFailure { + failSoftwarePresentationIfStillActive(generation: request.generation) + throw failure.underlying + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } - guard !model.isClosed else { - try interpretPopupEffects(model.reduce(.transientStateReset)) - drawingBuffer.discard() - return .skippedClosed + if result.outcome == .presented { + guard let stagedSuccess else { + preconditionFailure("Popup presentation committed without staged success") } - - let preparedCommit: PreparedSurfaceFrameCommit - do { - preparedCommit = try prepareSurfaceFrameCommit( - generation: request.generation, - geometry: geometry, - payload: .buffer(drawingBuffer.surfaceBuffer) - ) - } catch { - failActivePresentation( - generation: request.generation, - error: .surfaceCommit(String(describing: error)) - ) - drawingBuffer.discard() - throw error + model = stagedSuccess.model + if stagedSuccess.publishesRedrawRequest { + onRedrawRequested?() } + return .presented + } - do { - try reserveSurfaceFrameCallback(generation: request.generation) - } catch { - failActivePresentation( - generation: request.generation, - error: .frameCallbackRequest(String(describing: error)) + try interpretSoftwarePresentationFollowUp(result.followUp) + return softwarePresentationOutcome(for: result.outcome) + } + + package func cancelReservedSoftwareFrameOnOwnerThread( + _ reservation: SoftwareFrameReservation + ) { + connection.preconditionIsOwnerThread() + guard let pendingReservation = softwarePresentationCoordinator.cancel(reservation) else { + return + } + pendingReservation.reservedFrame.drawingBuffer.discard() + supersedeSoftwarePresentationIfStillActive( + generation: pendingReservation.request.generation + ) + } + + package func presentationFeedbackCommitRequestOnOwnerThread( + presentation: RawPresentation, + outputIDForPresentationSyncOutput: @escaping (RawOutputPointerIdentity) throws -> OutputID?, + onFeedback: @escaping (SurfacePresentationFeedback) -> Void + ) throws -> SurfacePresentationFeedbackCommitRequest { + connection.preconditionIsOwnerThread() + guard !model.isClosed else { throw ClientError.display(.unknownPopup) } + + let feedbackSurface = surface + let coordinator = presentationFeedbackCoordinator + let onFailure: (any Error) -> Void = { [weak self] error in + self?.reportCallbackFailure(operation: .presentationFeedback, error: error) + } + return SurfacePresentationFeedbackCommitRequest( + request: { + try coordinator.request( + presentation: presentation, + surface: feedbackSurface, + outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, + onFeedback: onFeedback, + onFailure: onFailure ) - drawingBuffer.discard() - throw error + }, + cancel: { identity in + coordinator.cancel(identity) } + ) + } + + package func requestPresentationFeedbackOnOwnerThread( + presentation: RawPresentation, + outputIDForPresentationSyncOutput: + @escaping (RawOutputPointerIdentity) throws -> OutputID?, + onFeedback: @escaping (SurfacePresentationFeedback) -> Void + ) throws -> SurfacePresentationIdentity { + connection.preconditionIsOwnerThread() + guard !model.isClosed else { + throw ClientError.display(.unknownPopup) + } + + return try presentationFeedbackCoordinator.request( + presentation: presentation, + surface: surface, + outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, + onFeedback: onFeedback + ) { [weak self] error in + self?.reportCallbackFailure(operation: .presentationFeedback, error: error) + } + } + + package func cancelPresentationFeedbackOnOwnerThread( + _ identity: SurfacePresentationIdentity + ) { + connection.preconditionIsOwnerThread() + presentationFeedbackCoordinator.cancel(identity) + } + + private func reserveSoftwareFrameForCurrentRedraw( + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + let geometry = try currentGeometry() + try metadata.damage?.validate(within: geometry) + try SurfaceMetadataSupport.ensureObjectsInstalled( + for: metadata.surfaceCommitMetadata, + connection: connection, + surface: surface, + runtime: &surfaceRuntime + ) + let currentBufferAvailability = try redrawBufferAvailability() + var presentationRequest: PopupPresentationRequest? + try interpretPopupEffects( + model.reduce( + .redrawRequestConsumed(bufferAvailability: currentBufferAvailability) + ) + ) { request in + presentationRequest = request + return .presented + } - let stagedCommit: StagedSurfaceFrameCommit - let stagedSuccess: StagedPopupPresentationSuccess - do { - stagedCommit = try stageSurfaceFrameCommit(preparedCommit) - stagedSuccess = try PopupPresentationSuccessStagingContext( - model: model, - parentWindowID: parentWindowID - ).stage( + guard let request = presentationRequest else { + return model.isClosed ? .closed : .deferred + } + + try interpretPopupEffects(model.reduce(.presentationStarted(request))) + + do { + let reservationID = softwarePresentationCoordinator.allocateIdentity() + let reservationResult = try softwarePresenter().reserve( + context: SoftwareSurfacePresentationContext( generation: request.generation, - bufferAvailability: try redrawBufferAvailability() - ) - } catch { - pendingFrameRegistration = nil - cancelSurfaceFrameCallback() - drawingBuffer.discard() - throw error - } + geometry: geometry, + submitConstraints: .default, + metadata: metadata.surfaceCommitMetadata, + damage: metadata.damage, + presentationFeedback: nil + ), + reservationID: reservationID, + runtime: &surfaceRuntime, + hasPendingFrameRegistration: pendingFrameRegistration != nil + ) - _ = drawingBuffer.markBusy(commitGeneration: request.generation) - pendingFrameRegistration = requestReservedSurfaceFrameCallback { [weak self] in - self?.handleFrameDone() + if let followUp = reservationResult.followUp { + try interpretSoftwarePresentationFollowUp(followUp) } - commitSurfaceFrame(stagedCommit) - model = stagedSuccess.model - if stagedSuccess.publishesRedrawRequest { - onRedrawRequested?() + guard let reservedFrame = reservationResult.reservedFrame else { + return model.isClosed ? .closed : .deferred } - return .presented - } catch { - failPresentationIfStillActive( - generation: request.generation, - error: .surfaceCommit(String(describing: error)) + + softwarePresentationCoordinator.register( + PendingPopupSoftwareFrameReservation( + request: request, + geometry: geometry, + reservedFrame: reservedFrame + ), + for: reservedFrame.reservation.reservationID ) + return .reserved(reservedFrame.reservation) + } catch let failure as SoftwareSurfacePresentationFailure { + failSoftwarePresentationIfStillActive(generation: request.generation) + throw failure.underlying + } catch { + failSoftwarePresentationIfStillActive(generation: request.generation) throw error } } - func interpretPresentationEffects( - _ effects: [PopupEffect], - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> RedrawOutcome { - var outcome = RedrawOutcome.skippedPendingFrame + private func softwarePresenter() -> SoftwareSurfacePresenter { + SoftwareSurfacePresenter( + surface: surface, + scaleInstallation: scaleInstallation, + createSharedMemoryPool: { [self] bufferSize in + guard let globals = connection.boundGlobals else { + throw ClientError.windowCreationFailed(.requiredGlobalsNotBound) + } + return try globals.sharedMemory.createPool( + width: bufferSize.width.rawValue, + height: bufferSize.height.rawValue, + bufferCount: bufferCount.rawValue + ) { [weak self] in + self?.handleBufferReleased() + } + }, + isSurfaceClosed: { [weak self] in self?.model.isClosed ?? true }, + onFrame: { [weak self] in self?.handleFrameDone() } + ) + } - for effect in effects { - switch effect { - case .performSoftwarePresent(let request): - outcome = try performSoftwarePresent(request, draw) - default: - try interpretPopupEffects([effect]) - } + private func currentGeometry() throws -> SurfaceGeometry { + try currentSurfaceGeometry() + } + + private func interpretSoftwarePresentationFollowUp( + _ followUp: SoftwareSurfacePresentationFollowUp? + ) throws { + guard let followUp else { return } + + switch followUp { + case .fail(let generation, _): + failSoftwarePresentationIfStillActive(generation: generation) + case .blockedByBuffer: + try interpretPopupEffects(model.reduce(.presentationBlockedByBuffer)) + case .resetTransientState: + guard case .drawing(let request) = model.presentation else { return } + supersedeSoftwarePresentationIfStillActive(generation: request.generation) + case .succeeded: + preconditionFailure( + "Popup software-presentation success must be installed from staging") } + } - return effects.isEmpty ? .skippedPendingFrame : outcome + private func failSoftwarePresentationIfStillActive(generation: UInt64) { + supersedeSoftwarePresentationIfStillActive(generation: generation) } - package func interpretPopupEffects(_ effects: [PopupEffect]) throws { - try WaylandClient.interpretPopupEffects( - effects, - parentWindowID: parentWindowID, - handlers: PopupEffectHandlers( - ackConfigure: { [self] serial in - try acknowledgeSurfaceConfigure(serial: serial) - xdgSurface.ackConfigure(serial: serial) - }, - publishDismissed: { [self] _ in - onDismissed?() - onDismissed = nil - }, - publishClosed: { [self] _ in - onClosed?() - onClosed = nil - }, - publishRedrawRequested: { [self] _ in - onRedrawRequested?() - }, - cancelFrameCallback: { [self] in - pendingFrameRegistration = nil - cancelSurfaceFrameCallback() - }, - retireSwapchain: { [self] in - retireSwapchain() - }, - destroyRoleObjects: { [self] in - try destroyRoleObjects() - } + private func supersedeSoftwarePresentationIfStillActive(generation: UInt64) { + guard case .drawing(let request) = model.presentation, + request.generation == generation + else { return } + + do { + try interpretPopupEffects( + model.reduce( + .softwarePresentationSuperseded( + generation: generation, + bufferAvailability: try redrawBufferAvailability() + ) + ) ) - ) + } catch { + reportCallbackFailure(operation: .markNeedsRedraw, error: error) + } } - private func destroyRoleObjects() throws { - onClose?() - onClose = nil - onRedrawRequested = nil + private func softwarePresentationOutcome( + for outcome: RedrawOutcome + ) -> SoftwarePresentationOutcome { + switch outcome { + case .presented: + return .presented + case .skippedClosed: + return .closed + case .skippedPendingFrame, .waitingForBuffer: + return .deferred + } + } - destroyScaleResources() - try destroyRoleResources() + private func validateSurfaceFrameMetadataSupport( + _ metadata: SurfaceFrameMetadata + ) throws { + try SurfaceMetadataSupport.validate( + metadata, + connection: connection, + runtime: &surfaceRuntime + ) } } diff --git a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift index d15cc58a..32ebd372 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift @@ -23,7 +23,6 @@ struct PopupRoleResources { package final class PopupRoleSurface { package let id: PopupID package let parentWindowID: WindowID - package let connection: RawDisplayConnection package let configuration: PopupConfiguration package let bufferCount: PositiveInt @@ -31,6 +30,13 @@ package final class PopupRoleSurface { package let failureSink: any WindowFailureSink package let configureState = PopupConfigureState() + let softwarePresentationCoordinator = + SoftwareSurfaceReservationCoordinator( + reservation: { $0.reservedFrame.reservation }, + retire: { $0.reservedFrame.drawingBuffer.discard() } + ) + let presentationFeedbackCoordinator = SurfacePresentationFeedbackCoordinator() + var surfaceRuntime: SurfaceRuntime package var pendingFrameRegistration: FrameCallbackRegistration? package var model: PopupModel @@ -212,30 +218,6 @@ package final class PopupRoleSurface { } } - package func showOnOwnerThread( - timeoutMilliseconds: Int32, - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws { - connection.preconditionIsOwnerThread() - - if model.currentPlacement == nil { - _ = try waitForInitialConfigure(timeoutMilliseconds: timeoutMilliseconds) - } - - try requestRedrawOnOwnerThread() - _ = try drawAndPresent(draw) - } - - package func redrawOnOwnerThread( - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws { - connection.preconditionIsOwnerThread() - guard !model.isClosed else { return } - - _ = try consumeLatestConfigureIfAvailable() - _ = try drawAndPresent(draw) - } - package func requestRedrawOnOwnerThread() throws { connection.preconditionIsOwnerThread() try markNeedsRedraw(bufferAvailability: try redrawBufferAvailability()) diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift new file mode 100644 index 00000000..68baf195 --- /dev/null +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift @@ -0,0 +1,256 @@ +enum SubsurfaceLifecycle: Equatable, Sendable { + case active + case closed +} + +struct SubsurfacePresentationRequest: Equatable, Sendable { + let generation: UInt64 + let geometry: SurfaceGeometry + let synchronizationMode: SubsurfaceSynchronizationMode +} + +enum SubsurfaceEvent: Equatable, Sendable { + case contentInvalidated(bufferAvailability: RedrawBufferAvailability) + case scaleChanged(bufferAvailability: RedrawBufferAvailability) + case synchronizationModeChanged( + SubsurfaceSynchronizationMode, + bufferAvailability: RedrawBufferAvailability + ) + case frameBecameReady(bufferAvailability: RedrawBufferAvailability) + case bufferBecameAvailable(bufferAvailability: RedrawBufferAvailability) + case redrawRequestConsumed( + geometry: SurfaceGeometry, + bufferAvailability: RedrawBufferAvailability + ) + case redrawRequestCanceled(bufferAvailability: RedrawBufferAvailability) + case presentationStarted(SubsurfacePresentationRequest) + case presentationBlockedByBuffer + case softwarePresentationSuperseded( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) + case presentationSucceeded( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) + case presentationFeedbackFailed + case transientStateReset + case explicitClose + case parentWindowClosed +} + +enum SubsurfaceEffect: Equatable, Sendable { + case publishRedrawRequested + case performSoftwarePresent(SubsurfacePresentationRequest) + case cancelFrameCallback + case retireSwapchain + case destroyRoleObjects +} + +enum SubsurfaceModelError: Error, Equatable, Sendable { + case closed + case nestedPresentation + case presentWithoutRedrawRequest + case presentationRequestMismatch + case presentationGenerationMismatch(expected: UInt64, actual: UInt64) +} + +struct SubsurfaceModel: Equatable, Sendable { + var lifecycle: SubsurfaceLifecycle + var redraw: WindowRedrawState + var presentation: PresentationState + var synchronizationMode: SubsurfaceSynchronizationMode + + init(synchronizationMode mode: SubsurfaceSynchronizationMode) { + lifecycle = .active + presentation = .idle + synchronizationMode = mode + + var initialRedraw = WindowRedrawState() + _ = initialRedraw.reduce(.contentInvalidated, bufferAvailability: .unavailable) + redraw = initialRedraw + } + + func isCurrentSoftwarePresentation( + _ request: SubsurfacePresentationRequest, + geometry: SurfaceGeometry + ) -> Bool { + lifecycle == .active + && presentation == .drawing(request: request) + && request.geometry == geometry + && request.synchronizationMode == synchronizationMode + && request.generation == redraw.generationForCurrentDraw + } + + // swiftlint:disable:next cyclomatic_complexity + mutating func reduce(_ event: SubsurfaceEvent) throws -> [SubsurfaceEffect] { + switch event { + case .explicitClose, .parentWindowClosed: + return close() + case .contentInvalidated(let bufferAvailability), + .scaleChanged(let bufferAvailability): + return invalidateContent(bufferAvailability: bufferAvailability) + case .synchronizationModeChanged(let mode, let bufferAvailability): + guard lifecycle == .active else { return [] } + guard mode != synchronizationMode else { return [] } + synchronizationMode = mode + return invalidateContent(bufferAvailability: bufferAvailability) + case .frameBecameReady(let bufferAvailability): + return reduceRedraw(.frameBecameReady, bufferAvailability: bufferAvailability) + case .bufferBecameAvailable(let bufferAvailability): + return reduceRedraw(.bufferBecameAvailable, bufferAvailability: bufferAvailability) + case .redrawRequestConsumed(let geometry, let bufferAvailability): + return try consumeRedrawRequest( + geometry: geometry, + bufferAvailability: bufferAvailability + ) + case .redrawRequestCanceled(let bufferAvailability): + return reduceRedraw(.redrawRequestCanceled, bufferAvailability: bufferAvailability) + case .presentationStarted(let request): + return try startPresentation(request) + case .presentationBlockedByBuffer: + return try blockPresentationByBuffer() + case .softwarePresentationSuperseded(let generation, let bufferAvailability): + return try supersedePresentation( + generation: generation, + bufferAvailability: bufferAvailability + ) + case .presentationSucceeded(let generation, let bufferAvailability): + return try completePresentation( + generation: generation, + bufferAvailability: bufferAvailability + ) + case .presentationFeedbackFailed: + return [] + case .transientStateReset: + guard lifecycle == .active else { return [] } + presentation = .idle + _ = redraw.reduce(.transientStateReset, bufferAvailability: .unavailable) + return [] + } + } +} + +extension SubsurfaceModel { + private mutating func invalidateContent( + bufferAvailability: RedrawBufferAvailability + ) -> [SubsurfaceEffect] { + guard lifecycle == .active else { return [] } + let redrawAvailability = + presentation.isIdle ? bufferAvailability : .unavailable + return map( + redraw.reduce(.contentInvalidated, bufferAvailability: redrawAvailability) + ) + } + + private mutating func reduceRedraw( + _ event: WindowRedrawEvent, + bufferAvailability: RedrawBufferAvailability + ) -> [SubsurfaceEffect] { + guard lifecycle == .active else { return [] } + return map(redraw.reduce(event, bufferAvailability: bufferAvailability)) + } + + private mutating func consumeRedrawRequest( + geometry: SurfaceGeometry, + bufferAvailability: RedrawBufferAvailability + ) throws -> [SubsurfaceEffect] { + guard lifecycle == .active else { return [] } + guard presentation.isIdle else { throw SubsurfaceModelError.nestedPresentation } + guard redraw.isDirty else { return [] } + + if redraw.isWaitingForBuffer, bufferAvailability.isAvailable { + _ = redraw.reduce(.bufferBecameAvailable, bufferAvailability: bufferAvailability) + } + guard redraw.hasOutstandingRedrawRequest else { return [] } + + _ = redraw.reduce(.redrawRequestConsumed, bufferAvailability: bufferAvailability) + let request = SubsurfacePresentationRequest( + generation: redraw.generationForCurrentDraw, + geometry: geometry, + synchronizationMode: synchronizationMode + ) + presentation = .requested(request: request) + return [.performSoftwarePresent(request)] + } + + private mutating func startPresentation( + _ request: SubsurfacePresentationRequest + ) throws -> [SubsurfaceEffect] { + guard lifecycle == .active else { throw SubsurfaceModelError.closed } + guard let pendingRequest = presentation.requestedRequest else { + throw presentation.isIdle + ? SubsurfaceModelError.presentWithoutRedrawRequest + : SubsurfaceModelError.nestedPresentation + } + guard pendingRequest == request else { + throw SubsurfaceModelError.presentationRequestMismatch + } + + presentation = .drawing(request: request) + return [] + } + + private mutating func blockPresentationByBuffer() throws -> [SubsurfaceEffect] { + guard case .drawing = presentation else { + throw SubsurfaceModelError.presentWithoutRedrawRequest + } + presentation = .idle + return map(redraw.reduce(.drawBlockedByBuffer, bufferAvailability: .unavailable)) + } + + private mutating func supersedePresentation( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) throws -> [SubsurfaceEffect] { + try requireDrawingGeneration(generation) + presentation = .idle + let effects = redraw.supersedeSoftwarePresentation( + bufferAvailability: bufferAvailability + ) + return map(effects) + } + + private mutating func completePresentation( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) throws -> [SubsurfaceEffect] { + try requireDrawingGeneration(generation) + presentation = .idle + return map( + redraw.reduce( + .presented(generation: generation), + bufferAvailability: bufferAvailability + ) + ) + } + + private func requireDrawingGeneration(_ generation: UInt64) throws { + guard case .drawing(let request) = presentation else { + throw SubsurfaceModelError.presentWithoutRedrawRequest + } + guard request.generation == generation else { + throw SubsurfaceModelError.presentationGenerationMismatch( + expected: request.generation, + actual: generation + ) + } + } + + private mutating func close() -> [SubsurfaceEffect] { + guard lifecycle == .active else { return [] } + lifecycle = .closed + presentation = .idle + _ = redraw.reduce(.transientStateReset, bufferAvailability: .unavailable) + return [.cancelFrameCallback, .retireSwapchain, .destroyRoleObjects] + } + + private func map(_ effects: [WindowRedrawEffect]) -> [SubsurfaceEffect] { + effects.map { effect in + switch effect { + case .publishRedrawRequested: + .publishRedrawRequested + } + } + } +} diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Effects.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Effects.swift new file mode 100644 index 00000000..8cf2c927 --- /dev/null +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Effects.swift @@ -0,0 +1,41 @@ +extension SubsurfaceRoleSurface { + func interpretSubsurfaceEffects(_ effects: [SubsurfaceEffect]) throws { + try interpretSubsurfaceEffects(effects) { _ in + throw SubsurfaceModelError.presentWithoutRedrawRequest + } + } + + func interpretSubsurfaceEffects( + _ effects: [SubsurfaceEffect], + performSoftwarePresent: (SubsurfacePresentationRequest) throws -> Void + ) throws { + for effect in effects { + switch effect { + case .publishRedrawRequested: + onRedrawRequested?() + case .performSoftwarePresent(let request): + try performSoftwarePresent(request) + case .cancelFrameCallback: + pendingFrameRegistration = nil + surfaceRuntime.cancelFrameCallback() + case .retireSwapchain: + surfaceRuntime.retireSharedMemoryPools(reason: .windowClosed) + case .destroyRoleObjects: + destroyRoleObjects() + } + } + } + + private func destroyRoleObjects() { + softwarePresentationCoordinator.close() + presentationFeedbackCoordinator.close() + surfaceRuntime.destroyScaleInstallation() + let removedRoleResources = surfaceRuntime.removeRoleResources() + removedRoleResources?.destroy() + do { + try surfaceRuntime.markSurfaceDestroyed() + } catch { + assertionFailure("subsurface surface runtime destroy failed: \(error)") + } + } +} diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift new file mode 100644 index 00000000..ec1d82a9 --- /dev/null +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift @@ -0,0 +1,411 @@ +import WaylandRaw + +struct PendingSubsurfaceSoftwareFrameReservation { + let request: SubsurfacePresentationRequest + let reservedFrame: ReservedSoftwareSurfaceFrame +} + +struct StagedSubsurfacePresentationSuccess { + let model: SubsurfaceModel + let publishesRedrawRequest: Bool +} + +struct SubsurfacePresentationSuccessStagingContext { + let model: SubsurfaceModel + + func stage( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) throws -> StagedSubsurfacePresentationSuccess { + var stagedModel = model + let effects = try stagedModel.reduce( + .presentationSucceeded( + generation: generation, + bufferAvailability: bufferAvailability + ) + ) + var publishesRedrawRequest = false + for effect in effects { + switch effect { + case .publishRedrawRequested: + publishesRedrawRequest = true + case .performSoftwarePresent, .cancelFrameCallback, .retireSwapchain, + .destroyRoleObjects: + preconditionFailure("Subsurface success staged an irreversible effect") + } + } + return StagedSubsurfacePresentationSuccess( + model: stagedModel, + publishesRedrawRequest: publishesRedrawRequest + ) + } +} + +extension SubsurfaceRoleSurface { + package func reserveSoftwareFrameForShowOnOwnerThread( + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + connection.preconditionIsOwnerThread() + return try reserveSoftwareFrame(metadata: metadata) + } + + package func reserveSoftwareFrameForRedrawOnOwnerThread( + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + connection.preconditionIsOwnerThread() + return try reserveSoftwareFrame(metadata: metadata) + } + + // swiftlint:disable:next cyclomatic_complexity function_body_length + package func submitReservedSoftwareFrameOnOwnerThread( + reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + makePresentationFeedback: () throws -> SurfacePresentationFeedbackCommitRequest?, + commitSynchronizedParent: @escaping () -> Void, + _ draw: (borrowing SoftwareFrame) throws -> Void + ) throws -> SoftwarePresentationOutcome { + connection.preconditionIsOwnerThread() + + guard model.lifecycle == .active else { return .closed } + guard let pendingReservation = softwarePresentationCoordinator.take(reservation) else { + return .closed + } + + let request = pendingReservation.request + let geometry = try currentGeometry() + guard !Task.isCancelled, + model.isCurrentSoftwarePresentation(request, geometry: geometry) + else { + pendingReservation.reservedFrame.drawingBuffer.discard() + if model.lifecycle == .closed { return .closed } + supersedeSoftwarePresentationIfStillActive(generation: request.generation) + return .superseded + } + + do { + try validateSurfaceFrameMetadataSupport(metadata) + try SurfaceMetadataSupport.ensureObjectsInstalled( + for: metadata.surfaceCommitMetadata, + connection: connection, + surface: surface, + runtime: &surfaceRuntime + ) + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } + + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? + do { + presentationFeedback = try makePresentationFeedback() + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } + + let successStagingContext = SubsurfacePresentationSuccessStagingContext(model: model) + var stagedSuccess: StagedSubsurfacePresentationSuccess? + let commitFollowUp: () -> Void = + request.synchronizationMode == .synchronized + ? commitSynchronizedParent + : { + // Desynchronized child has no parent commit. + } + + let result: SoftwareSurfacePresentationResult + do { + result = try softwarePresenter().presentReserved( + pendingReservation.reservedFrame, + context: SoftwareSurfacePresentationContext( + generation: request.generation, + geometry: request.geometry, + submitConstraints: .default, + metadata: metadata.surfaceCommitMetadata, + damage: metadata.damage, + presentationFeedback: presentationFeedback, + commitFollowUp: commitFollowUp + ), + draw: draw, + stageSuccess: { availability in + stagedSuccess = try successStagingContext.stage( + generation: request.generation, + bufferAvailability: availability + ) + }, + runtime: &surfaceRuntime, + pendingFrameRegistration: &pendingFrameRegistration + ) + } catch let failure as SoftwareSurfacePresentationFailure { + failSoftwarePresentationIfStillActive(generation: request.generation) + throw failure.underlying + } catch { + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } + + if result.outcome == .presented { + guard let stagedSuccess else { + preconditionFailure("Subsurface presentation committed without staged success") + } + model = stagedSuccess.model + if stagedSuccess.publishesRedrawRequest { + onRedrawRequested?() + } + return .presented + } + + try interpretSoftwarePresentationFollowUp(result.followUp) + return mapPresentationOutcome(result.outcome) + } + + package func cancelReservedSoftwareFrameOnOwnerThread( + reservation: SoftwareFrameReservation + ) throws { + connection.preconditionIsOwnerThread() + guard let pendingReservation = softwarePresentationCoordinator.cancel(reservation) else { + return + } + pendingReservation.reservedFrame.drawingBuffer.discard() + supersedeSoftwarePresentationIfStillActive( + generation: pendingReservation.request.generation + ) + } + + package func presentationFeedbackCommitRequestOnOwnerThread( + presentation: RawPresentation, + outputIDForPresentationSyncOutput: @escaping (RawOutputPointerIdentity) throws -> OutputID?, + onFeedback: @escaping (SurfacePresentationFeedback) -> Void + ) throws -> SurfacePresentationFeedbackCommitRequest { + connection.preconditionIsOwnerThread() + guard model.lifecycle == .active else { throw ClientError.display(.closedSubsurface) } + + let feedbackSurface = surface + let coordinator = presentationFeedbackCoordinator + let onFailure: (any Error) -> Void = { [weak self] error in + self?.reportPresentationFeedbackFailure(error) + } + return SurfacePresentationFeedbackCommitRequest( + request: { + try coordinator.request( + presentation: presentation, + surface: feedbackSurface, + outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, + onFeedback: onFeedback, + onFailure: onFailure + ) + }, + cancel: { identity in + coordinator.cancel(identity) + } + ) + } + + package func requestPresentationFeedbackOnOwnerThread( + presentation: RawPresentation, + outputIDForPresentationSyncOutput: @escaping (RawOutputPointerIdentity) throws -> OutputID?, + onFeedback: @escaping (SurfacePresentationFeedback) -> Void + ) throws -> SurfacePresentationIdentity { + connection.preconditionIsOwnerThread() + guard model.lifecycle == .active else { throw ClientError.display(.closedSubsurface) } + + return try presentationFeedbackCoordinator.request( + presentation: presentation, + surface: surface, + outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, + onFeedback: onFeedback + ) { [weak self] error in + self?.reportPresentationFeedbackFailure(error) + } + } + + package func cancelPresentationFeedbackOnOwnerThread(_ identity: SurfacePresentationIdentity) { + connection.preconditionIsOwnerThread() + presentationFeedbackCoordinator.cancel(identity) + } + + private func reportPresentationFeedbackFailure(_ feedbackError: any Error) { + do { + let effects = try model.reduce(.presentationFeedbackFailed) + assert(effects.isEmpty) + } catch { + failureSink.reportWindowFailure( + WindowFailureClassifier.classify( + windowID: parentWindowID, + operation: .presentationFeedback, + error: error + ) + ) + return + } + + failureSink.reportWindowFailure( + WindowFailureClassifier.classify( + windowID: parentWindowID, + operation: .presentationFeedback, + error: feedbackError + ) + ) + } +} + +extension SubsurfaceRoleSurface { + private func reserveSoftwareFrame( + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + guard model.lifecycle == .active else { return .closed } + try validateSurfaceFrameMetadataSupport(metadata) + try SurfaceMetadataSupport.ensureObjectsInstalled( + for: metadata.surfaceCommitMetadata, + connection: connection, + surface: surface, + runtime: &surfaceRuntime + ) + let geometry = try currentGeometry() + try metadata.damage?.validate(within: geometry) + + var presentationRequest: SubsurfacePresentationRequest? + try interpretSubsurfaceEffects( + model.reduce( + .redrawRequestConsumed( + geometry: geometry, + bufferAvailability: try redrawBufferAvailability() + ) + ) + ) { request in + presentationRequest = request + } + + guard let request = presentationRequest else { + return model.lifecycle == .closed ? .closed : .deferred + } + + do { + _ = try model.reduce(.presentationStarted(request)) + let reservationResult = try softwarePresenter().reserve( + context: SoftwareSurfacePresentationContext( + generation: request.generation, + geometry: request.geometry, + submitConstraints: .default, + metadata: metadata.surfaceCommitMetadata, + damage: metadata.damage, + presentationFeedback: nil + ), + reservationID: softwarePresentationCoordinator.allocateIdentity(), + runtime: &surfaceRuntime, + hasPendingFrameRegistration: pendingFrameRegistration != nil + ) + try interpretSoftwarePresentationFollowUp(reservationResult.followUp) + + guard let reservedFrame = reservationResult.reservedFrame else { + return model.lifecycle == .closed ? .closed : .deferred + } + + let pendingReservation = PendingSubsurfaceSoftwareFrameReservation( + request: request, + reservedFrame: reservedFrame + ) + softwarePresentationCoordinator.register( + pendingReservation, + for: reservedFrame.reservation.reservationID + ) + return .reserved(reservedFrame.reservation) + } catch { + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } + } + + private func softwarePresenter() -> SoftwareSurfacePresenter { + SoftwareSurfacePresenter( + surface: surface, + scaleInstallation: surfaceRuntime.scaleInstallation, + createSharedMemoryPool: { [self] bufferSize in + guard let globals = connection.boundGlobals else { + throw ClientError.windowCreationFailed(.requiredGlobalsNotBound) + } + return try globals.sharedMemory.createPool( + width: bufferSize.width.rawValue, + height: bufferSize.height.rawValue, + bufferCount: bufferCount.rawValue + ) { [weak self] in + self?.handleBufferReleased() + } + }, + isSurfaceClosed: { [self] in model.lifecycle == .closed }, + onFrame: { [weak self] in + self?.handleFrameDone() + } + ) + } + + func redrawBufferAvailability() throws -> RedrawBufferAvailability { + let geometry = try currentGeometry() + return surfaceRuntime.redrawBufferAvailability(matching: geometry.bufferSize.rawSize) + } + + private func interpretSoftwarePresentationFollowUp( + _ followUp: SoftwareSurfacePresentationFollowUp? + ) throws { + guard let followUp else { return } + switch followUp { + case .fail(let generation, _): + failSoftwarePresentationIfStillActive(generation: generation) + case .blockedByBuffer: + try interpretSubsurfaceEffects(model.reduce(.presentationBlockedByBuffer)) + case .resetTransientState: + if case .drawing(let request) = model.presentation { + failSoftwarePresentationIfStillActive(generation: request.generation) + } + case .succeeded: + preconditionFailure("Subsurface success must be installed from staged state") + } + } + + private func supersedeSoftwarePresentationIfStillActive(generation: UInt64) { + guard case .drawing(let request) = model.presentation, + request.generation == generation + else { return } + + do { + try interpretSubsurfaceEffects( + model.reduce( + .softwarePresentationSuperseded( + generation: generation, + bufferAvailability: try redrawBufferAvailability() + ) + ) + ) + } catch { + surfaceRuntime.resetTransientTransactionState() + } + } + + private func failSoftwarePresentationIfStillActive(generation: UInt64) { + supersedeSoftwarePresentationIfStillActive(generation: generation) + } + + private func mapPresentationOutcome( + _ outcome: RedrawOutcome + ) -> SoftwarePresentationOutcome { + switch outcome { + case .presented: + .presented + case .waitingForBuffer, .skippedPendingFrame: + .deferred + case .skippedClosed: + .closed + } + } + + private func validateSurfaceFrameMetadataSupport( + _ metadata: SurfaceFrameMetadata + ) throws { + try SurfaceMetadataSupport.validate( + metadata, + connection: connection, + runtime: &surfaceRuntime + ) + } +} diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift index 2622fc67..d35d5483 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift @@ -1,8 +1,6 @@ -// swiftlint:disable file_length - import WaylandRaw -private struct SubsurfaceRoleResources { +struct SubsurfaceRoleResources { let surface: RawSurface let subsurface: RawSubsurface @@ -16,28 +14,38 @@ package final class SubsurfaceRoleSurface { package let id: SubsurfaceID package let parentWindowID: WindowID - private let connection: RawDisplayConnection - private let bufferCount: PositiveInt - private var size: PositiveLogicalSize - private var synchronizationMode: SubsurfaceSynchronizationMode - private var isClosed = false - private var needsRedrawStorage = true - private var surfaceRuntime: SurfaceRuntime - private var pendingFrameRegistration: FrameCallbackRegistration? + let connection: RawDisplayConnection + let bufferCount: PositiveInt + let failureSink: any WindowFailureSink + var size: PositiveLogicalSize + var model: SubsurfaceModel + let softwarePresentationCoordinator = + SoftwareSurfaceReservationCoordinator( + reservation: { $0.reservedFrame.reservation }, + retire: { $0.reservedFrame.drawingBuffer.discard() } + ) + let presentationFeedbackCoordinator = SurfacePresentationFeedbackCoordinator() + var surfaceRuntime: SurfaceRuntime + var pendingFrameRegistration: FrameCallbackRegistration? + var onRedrawRequested: (() -> Void)? private var onOutputMembershipChanged: (([OutputID]) -> Void)? package init( id subsurfaceID: SubsurfaceID, parent parentWindow: TopLevelWindow, connection rawConnection: RawDisplayConnection, - configuration subsurfaceConfiguration: SubsurfaceConfiguration + configuration subsurfaceConfiguration: SubsurfaceConfiguration, + failureSink subsurfaceFailureSink: any WindowFailureSink = DefaultWindowFailureSink() ) throws { id = subsurfaceID parentWindowID = parentWindow.id connection = rawConnection bufferCount = subsurfaceConfiguration.bufferCount + failureSink = subsurfaceFailureSink size = subsurfaceConfiguration.size - synchronizationMode = subsurfaceConfiguration.synchronizationMode + model = SubsurfaceModel( + synchronizationMode: subsurfaceConfiguration.synchronizationMode + ) let globals = try rawConnection.bindRequiredGlobals() let rawObjects = try rawConnection.createManagedSubsurface( @@ -72,17 +80,17 @@ package final class SubsurfaceRoleSurface { } deinit { - close() + closeOnOwnerThread() } package var isClosedOnOwnerThread: Bool { connection.preconditionIsOwnerThread() - return isClosed + return model.lifecycle == .closed } package var needsRedrawOnOwnerThread: Bool { connection.preconditionIsOwnerThread() - return needsRedrawStorage + return model.redraw.isDirty } package var geometryOnOwnerThread: SurfaceGeometry { @@ -92,26 +100,14 @@ package final class SubsurfaceRoleSurface { } } - package func showOnOwnerThread( - damage: SurfaceDamageRegion?, - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> SubsurfaceParentCommitRequirement? { + package func requestRedrawOnOwnerThread() throws { connection.preconditionIsOwnerThread() - return try present(damage: damage, draw) - } - - package func redrawOnOwnerThread( - damage: SurfaceDamageRegion?, - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> SubsurfaceParentCommitRequirement? { - connection.preconditionIsOwnerThread() - return try present(damage: damage, draw) - } - - package func requestRedrawOnOwnerThread() { - connection.preconditionIsOwnerThread() - guard !isClosed else { return } - needsRedrawStorage = true + guard model.lifecycle == .active else { return } + try interpretSubsurfaceEffects( + model.reduce( + .contentInvalidated(bufferAvailability: try redrawBufferAvailability()) + ) + ) } package func setInputRegionOnOwnerThread(_ region: SurfaceRegion?) throws @@ -136,7 +132,7 @@ package final class SubsurfaceRoleSurface { -> SubsurfaceParentCommitRequirement? { connection.preconditionIsOwnerThread() - guard !isClosed else { return nil } + guard model.lifecycle == .active else { return nil } subsurface.setPosition(x: newPosition.x, y: newPosition.y) return parentCommitRequirement(reason: .positionChanged) } @@ -146,8 +142,10 @@ package final class SubsurfaceRoleSurface { { connection.preconditionIsOwnerThread() try requireValidStackingSibling(sibling) - guard !isClosed else { throw ClientError.display(.closedSubsurface) } - guard !sibling.isClosed else { throw ClientError.display(.closedSubsurface) } + guard model.lifecycle == .active else { throw ClientError.display(.closedSubsurface) } + guard sibling.model.lifecycle == .active else { + throw ClientError.display(.closedSubsurface) + } subsurface.placeAbove(sibling.surface) return parentCommitRequirement(reason: .stackingChanged) } @@ -157,31 +155,55 @@ package final class SubsurfaceRoleSurface { { connection.preconditionIsOwnerThread() try requireValidStackingSibling(sibling) - guard !isClosed else { throw ClientError.display(.closedSubsurface) } - guard !sibling.isClosed else { throw ClientError.display(.closedSubsurface) } + guard model.lifecycle == .active else { throw ClientError.display(.closedSubsurface) } + guard sibling.model.lifecycle == .active else { + throw ClientError.display(.closedSubsurface) + } subsurface.placeBelow(sibling.surface) return parentCommitRequirement(reason: .stackingChanged) } - package func setSynchronizedOnOwnerThread() -> SubsurfaceParentCommitRequirement? { + package func setSynchronizedOnOwnerThread() throws -> SubsurfaceParentCommitRequirement? { connection.preconditionIsOwnerThread() - guard !isClosed else { return nil } - synchronizationMode = .synchronized + guard model.lifecycle == .active else { return nil } + let bufferAvailability = try redrawBufferAvailability() subsurface.setSynchronized() + try interpretSubsurfaceEffects( + model.reduce( + .synchronizationModeChanged( + .synchronized, + bufferAvailability: bufferAvailability + ) + ) + ) return nil } - package func setDesynchronizedOnOwnerThread() -> SubsurfaceParentCommitRequirement? { + package func setDesynchronizedOnOwnerThread() throws -> SubsurfaceParentCommitRequirement? { connection.preconditionIsOwnerThread() - guard !isClosed else { return nil } - synchronizationMode = .desynchronized + guard model.lifecycle == .active else { return nil } + let bufferAvailability = try redrawBufferAvailability() subsurface.setDesynchronized() + try interpretSubsurfaceEffects( + model.reduce( + .synchronizationModeChanged( + .desynchronized, + bufferAvailability: bufferAvailability + ) + ) + ) return nil } - package func closeOnOwnerThread() { + package func closeOnOwnerThread(parentWindowClosed: Bool = false) { connection.preconditionIsOwnerThread() - close() + do { + try interpretSubsurfaceEffects( + model.reduce(parentWindowClosed ? .parentWindowClosed : .explicitClose) + ) + } catch { + assertionFailure("subsurface close transition failed: \(error)") + } } } @@ -196,7 +218,7 @@ extension SubsurfaceRoleSurface { set { surfaceRuntime.scaleInstallation = newValue } } - private var surface: RawSurface { + var surface: RawSurface { guard let surface = roleResources?.surface else { preconditionFailure("Subsurface surface used after destruction") } @@ -257,7 +279,6 @@ extension SubsurfaceRoleSurface { } private func applySynchronizationMode(_ mode: SubsurfaceSynchronizationMode) { - synchronizationMode = mode switch mode { case .synchronized: subsurface.setSynchronized() @@ -270,7 +291,7 @@ extension SubsurfaceRoleSurface { _ region: SurfaceRegion?, setRegion: (RawSurface, RawRegion?) -> Void ) throws -> SubsurfaceParentCommitRequirement? { - guard !isClosed else { return nil } + guard model.lifecycle == .active else { return nil } guard let globals = connection.boundGlobals else { throw ClientError.windowCreationFailed(.requiredGlobalsNotBound) } @@ -285,105 +306,7 @@ extension SubsurfaceRoleSurface { return synchronizedStateCommitRequirement() } - private func present( - damage: SurfaceDamageRegion?, - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> SubsurfaceParentCommitRequirement? { - guard !isClosed else { return nil } - - let generation = surfaceRuntime.nextCommitGeneration - let request = PresentationRequest( - generation: generation, - configuration: resolvedConfiguration() - ) - let result: WindowSoftwarePresentationResult - do { - result = try WindowSoftwarePresenter( - surface: surface, - scaleInstallation: scaleInstallation, - createSharedMemoryPool: { [self] bufferSize in - guard let globals = connection.boundGlobals else { - throw ClientError.windowCreationFailed(.requiredGlobalsNotBound) - } - - return try globals.sharedMemory.createPool( - width: bufferSize.width.rawValue, - height: bufferSize.height.rawValue, - bufferCount: bufferCount.rawValue - ) { [weak self] in - self?.handleBufferReleased() - } - }, - isWindowClosed: { [self] in isClosed }, - onFrame: { [weak self] in - self?.handleFrameDone() - } - ).present( - context: WindowSoftwarePresentationContext( - request: request, - geometry: try currentGeometry(), - submitConstraints: .default, - metadata: .default, - damage: damage, - presentationFeedback: nil - ), - draw: draw, - runtime: &surfaceRuntime, - pendingFrameRegistration: &pendingFrameRegistration - ) - } catch { - throw mapPresentationFailure(error) - } - return try handlePresentationFollowUp(result.followUp) - } - - private func handlePresentationFollowUp( - _ followUp: WindowSoftwarePresentationFollowUp? - ) throws -> SubsurfaceParentCommitRequirement? { - guard let followUp else { return nil } - - switch followUp { - case .succeeded: - needsRedrawStorage = false - return synchronizedStateCommitRequirement() - case .blockedByBuffer: - needsRedrawStorage = true - return nil - case .resetTransientState: - surfaceRuntime.resetTransientTransactionState() - needsRedrawStorage = true - return nil - case .fail(_, let error): - throw ClientError.display( - .subsurfacePresentationFailed( - SubsurfacePresentationFailure( - subsurfaceID: SubsurfaceIdentity(id), - cause: .presentation(error) - ) - )) - } - } - - private func mapPresentationFailure(_ error: any Error) -> ClientError { - let cause: SubsurfacePresentationFailureCause - if let failure = error as? WindowSoftwarePresentationFailure { - cause = .presentation(failure.presentationError) - } else if let failure = error as? WindowSoftwareDrawFailure { - cause = .draw(String(describing: failure.underlying)) - } else { - cause = .operation(String(describing: error)) - } - - return ClientError.display( - .subsurfacePresentationFailed( - SubsurfacePresentationFailure( - subsurfaceID: SubsurfaceIdentity(id), - cause: cause - ) - )) - } - - private func currentGeometry() throws -> SurfaceGeometry { + func currentGeometry() throws -> SurfaceGeometry { do { return try scaleInstallation.geometry(logicalSize: size) } catch let error as WindowError { @@ -402,27 +325,39 @@ extension SubsurfaceRoleSurface { ) } - private func handleFrameDone() { + func handleFrameDone() { do { _ = try surfaceRuntime.completeFrameCallback() + pendingFrameRegistration = nil + surfaceRuntime.dropReleasedRetiredBufferPools() + guard model.lifecycle == .active else { return } + try interpretSubsurfaceEffects( + model.reduce( + .frameBecameReady(bufferAvailability: try redrawBufferAvailability()) + ) + ) } catch { surfaceRuntime.resetTransientTransactionState() } - pendingFrameRegistration = nil - surfaceRuntime.dropReleasedRetiredBufferPools() - guard !isClosed else { return } - if needsRedrawStorage { - onOutputMembershipChanged?(currentOutputIDsOnOwnerThread()) - } } - private func handleBufferReleased() { + func handleBufferReleased() { connection.preconditionIsOwnerThread() surfaceRuntime.dropReleasedRetiredBufferPools() + guard model.lifecycle == .active else { return } + do { + try interpretSubsurfaceEffects( + model.reduce( + .bufferBecameAvailable(bufferAvailability: try redrawBufferAvailability()) + ) + ) + } catch { + surfaceRuntime.resetTransientTransactionState() + } } private func handlePreferredBufferScale(_ factor: Int32) { - guard !isClosed else { return } + guard model.lifecycle == .active else { return } do { guard try surfaceRuntime.updateScaleInstallation({ scaleInstallation in @@ -432,14 +367,16 @@ extension SubsurfaceRoleSurface { ) }) else { return } - needsRedrawStorage = true + try interpretSubsurfaceEffects( + model.reduce(.scaleChanged(bufferAvailability: try redrawBufferAvailability())) + ) } catch { surfaceRuntime.resetTransientTransactionState() } } private func handlePreferredFractionalScale(_ scale: UInt32) { - guard !isClosed else { return } + guard model.lifecycle == .active else { return } do { guard try surfaceRuntime.updateScaleInstallation({ scaleInstallation in @@ -449,14 +386,16 @@ extension SubsurfaceRoleSurface { ) }) else { return } - needsRedrawStorage = true + try interpretSubsurfaceEffects( + model.reduce(.scaleChanged(bufferAvailability: try redrawBufferAvailability())) + ) } catch { surfaceRuntime.resetTransientTransactionState() } } private func handleSurfaceEnteredOutput(_ output: RawOutputPointerIdentity) { - guard !isClosed else { return } + guard model.lifecycle == .active else { return } guard let outputID = connection.boundGlobals?.outputRegistry.outputID(for: output) else { @@ -467,7 +406,7 @@ extension SubsurfaceRoleSurface { } private func handleSurfaceLeftOutput(_ output: RawOutputPointerIdentity) { - guard !isClosed else { return } + guard model.lifecycle == .active else { return } guard let outputID = connection.boundGlobals?.outputRegistry.outputID(for: output) else { @@ -507,7 +446,7 @@ extension SubsurfaceRoleSurface { SubsurfaceParentCommitPolicy.requirement( parentWindowID: parentWindowID, subsurfaceID: id, - event: .surfaceStateCommitted(synchronizationMode) + event: .surfaceStateCommitted(model.synchronizationMode) ) } @@ -520,21 +459,4 @@ extension SubsurfaceRoleSurface { reason: reason ) } - - private func close() { - guard !isClosed else { return } - isClosed = true - needsRedrawStorage = false - pendingFrameRegistration = nil - surfaceRuntime.cancelFrameCallback() - surfaceRuntime.retireSharedMemoryPools(reason: .windowClosed) - surfaceRuntime.destroyScaleInstallation() - let removedRoleResources = surfaceRuntime.removeRoleResources() - removedRoleResources?.destroy() - do { - try surfaceRuntime.markSurfaceDestroyed() - } catch { - assertionFailure("subsurface surface runtime destroy failed: \(error)") - } - } } diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceMetadataSupport.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceMetadataSupport.swift new file mode 100644 index 00000000..6f30c87e --- /dev/null +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceMetadataSupport.swift @@ -0,0 +1,161 @@ +import WaylandRaw + +enum SurfaceMetadataSupport { + static func refreshCapabilities( + connection: RawDisplayConnection, + runtime: inout SurfaceRuntime + ) { + guard let extensions = connection.boundGlobals?.extensions else { + runtime.setContentTypeCapability(.unavailable) + runtime.setAlphaModifierCapability(.unavailable) + runtime.setTearingControlCapability(.unavailable) + runtime.setColorRepresentationCapability(.unavailable) + runtime.setColorCapability(.unavailable) + return + } + + runtime.setContentTypeCapability(extensions.surfaceContentTypeCapability) + runtime.setAlphaModifierCapability(extensions.surfaceAlphaModifierCapability) + runtime.setTearingControlCapability(extensions.surfaceTearingControlCapability) + runtime.setColorRepresentationCapability( + extensions.surfaceColorRepresentationCapability + ) + runtime.setColorCapability(extensions.surfaceColorCapability) + } + + static func validate( + _ metadata: SurfaceFrameMetadata, + connection: RawDisplayConnection, + runtime: inout SurfaceRuntime + ) throws { + refreshCapabilities(connection: connection, runtime: &runtime) + try metadata.validate(capabilities: runtime.capabilitySnapshot()) + } + + static func ensureObjectsInstalled( + for metadata: SurfaceCommitMetadata, + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + if metadata.contentType != nil { + try ensureContentTypeObjectInstalled( + connection: connection, + surface: surface, + runtime: &runtime + ) + } + if metadata.alpha != nil { + try ensureAlphaModifierObjectInstalled( + connection: connection, + surface: surface, + runtime: &runtime + ) + } + if metadata.presentationHint != nil { + try ensureTearingControlObjectInstalled( + connection: connection, + surface: surface, + runtime: &runtime + ) + } + if metadata.colorRepresentation != nil { + try ensureColorRepresentationObjectInstalled( + connection: connection, + surface: surface, + runtime: &runtime + ) + } + if let colorDescription = metadata.colorDescription { + try ensureColorManagementObjectInstalled( + connection: connection, + surface: surface, + runtime: &runtime + ) + try ensureColorDescriptionInstalled( + colorDescription, + connection: connection, + surface: surface, + runtime: &runtime + ) + } + } +} + +extension SurfaceMetadataSupport { + private static func ensureContentTypeObjectInstalled( + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + guard !runtime.hasContentTypeObject else { return } + guard let manager = connection.boundGlobals?.extensions.contentTypeManager.boundObject + else { throw SurfaceCommitMetadataError.contentTypeUnavailable } + runtime.installContentTypeObject(try manager.contentType(for: surface)) + } + + private static func ensureAlphaModifierObjectInstalled( + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + guard !runtime.hasAlphaModifierObject else { return } + guard let manager = connection.boundGlobals?.extensions.alphaModifierManager.boundObject + else { throw SurfaceCommitMetadataError.alphaModifierUnavailable } + runtime.installAlphaModifierObject(try manager.alphaModifier(for: surface)) + } + + private static func ensureTearingControlObjectInstalled( + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + guard !runtime.hasTearingControlObject else { return } + guard let manager = connection.boundGlobals?.extensions.tearingControlManager.boundObject + else { throw SurfaceCommitMetadataError.tearingControlUnavailable } + runtime.installTearingControlObject(try manager.tearingControl(for: surface)) + } + + private static func ensureColorRepresentationObjectInstalled( + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + runtime.setColorRepresentationCapability( + connection.boundGlobals?.extensions.surfaceColorRepresentationCapability + ?? .unavailable + ) + guard !runtime.hasColorRepresentationObject else { return } + guard + let manager = connection.boundGlobals?.extensions.colorRepresentationManager.boundObject + else { throw SurfaceCommitMetadataError.colorRepresentationUnavailable } + runtime.installColorRepresentationObject(try manager.colorRepresentation(for: surface)) + } + + private static func ensureColorManagementObjectInstalled( + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + guard !runtime.hasColorManagementObject else { return } + guard let manager = connection.boundGlobals?.extensions.colorManager.boundObject + else { throw SurfaceCommitMetadataError.colorUnavailable } + runtime.installColorManagementObject(try manager.surface(for: surface)) + } + + private static func ensureColorDescriptionInstalled( + _ reference: SurfaceColorDescriptionReference, + connection: RawDisplayConnection, + surface: RawSurface, + runtime: inout SurfaceRuntime + ) throws { + guard !runtime.hasColorDescription(reference) else { return } + guard let manager = connection.boundGlobals?.extensions.colorManager.boundObject + else { throw SurfaceCommitMetadataError.colorUnavailable } + try runtime.resolveColorDescriptionIfNeeded( + reference, + using: manager, + surface: surface + ) + } +} diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceOwnedResourceLedger.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceOwnedResourceLedger.swift new file mode 100644 index 00000000..afb631b6 --- /dev/null +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceOwnedResourceLedger.swift @@ -0,0 +1,178 @@ +import WaylandRaw + +final class SurfaceOwnedResourceLedger { + private var resources: [Identity: Resource] = [:] + private var isClosed = false + private let retireResource: (Resource) -> Void + + init(retireResource: @escaping (Resource) -> Void) { + self.retireResource = retireResource + } + + var count: Int { resources.count } + var isEmpty: Bool { resources.isEmpty } + + @discardableResult + func insert(_ resource: Resource, for identity: Identity) -> Bool { + guard !isClosed, resources[identity] == nil else { + retireResource(resource) + return false + } + resources[identity] = resource + return true + } + + func take(_ identity: Identity) -> Resource? { + resources.removeValue(forKey: identity) + } + + func take( + _ identity: Identity, + matching predicate: (Resource) -> Bool + ) -> Resource? { + guard let resource = resources[identity], predicate(resource) else { return nil } + return resources.removeValue(forKey: identity) + } + + func retire(_ identity: Identity) { + guard let resource = resources.removeValue(forKey: identity) else { return } + retireResource(resource) + } + + func close() { + guard !isClosed else { return } + isClosed = true + let removed = resources + resources.removeAll() + for identity in removed.keys.sorted(by: stableIdentityOrder) { + guard let resource = removed[identity] else { continue } + retireResource(resource) + } + } + + private func stableIdentityOrder(_ lhs: Identity, _ rhs: Identity) -> Bool { + String(reflecting: lhs) < String(reflecting: rhs) + } +} + +final class SurfacePresentationFeedbackCoordinator { + private var identities = IDGenerator() + private let resources = SurfaceOwnedResourceLedger< + SurfacePresentationIdentity, + RawPresentationFeedback + > { $0.cancel() } + + func request( + presentation: RawPresentation, + surface: RawSurface, + outputIDForPresentationSyncOutput: + @escaping (RawOutputPointerIdentity) throws -> OutputID?, + onFeedback: @escaping (SurfacePresentationFeedback) -> Void, + onFailure: @escaping (any Error) -> Void + ) throws -> SurfacePresentationIdentity { + let identity = identities.next() + let feedback = try presentation.requestFeedback(for: surface) { [weak self] rawEvent in + self?.complete( + identity, + event: rawEvent, + outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, + onFeedback: onFeedback, + onFailure: onFailure + ) + } + _ = resources.insert(feedback, for: identity) + return identity + } + + func cancel(_ identity: SurfacePresentationIdentity) { + resources.retire(identity) + } + + func close() { + resources.close() + } + + private func complete( + _ identity: SurfacePresentationIdentity, + event rawEvent: RawPresentationFeedbackEvent, + outputIDForPresentationSyncOutput: (RawOutputPointerIdentity) throws -> OutputID?, + onFeedback: (SurfacePresentationFeedback) -> Void, + onFailure: (any Error) -> Void + ) { + _ = resources.take(identity) + + do { + switch rawEvent { + case .presented(let rawPresented): + let synchronizedOutput = try rawPresented.synchronizedOutput.flatMap { output in + try outputIDForPresentationSyncOutput(output) + } + onFeedback( + .presented( + PresentationFeedback( + surface: identity, + timestamp: PresentationTimestamp( + seconds: rawPresented.timestamp.seconds, + nanoseconds: rawPresented.timestamp.nanoseconds + ), + refreshNanoseconds: rawPresented.refreshNanoseconds == 0 + ? nil + : rawPresented.refreshNanoseconds, + sequence: PresentationSequence(value: rawPresented.sequence.value), + flags: PresentationFeedbackFlags(rawValue: rawPresented.flags), + synchronizedOutput: synchronizedOutput + ) + ) + ) + case .discarded: + onFeedback(.discarded(identity)) + } + } catch { + onFailure(error) + } + } +} + +final class SoftwareSurfaceReservationCoordinator { + private var identities = IDGenerator() + private let reservation: (PendingReservation) -> SoftwareFrameReservation + private let resources: + SurfaceOwnedResourceLedger< + SoftwareFrameReservationToken, + PendingReservation + > + + init( + reservation reservationForResource: + @escaping (PendingReservation) -> SoftwareFrameReservation, + retire: @escaping (PendingReservation) -> Void + ) { + reservation = reservationForResource + resources = SurfaceOwnedResourceLedger(retireResource: retire) + } + + func allocateIdentity() -> SoftwareFrameReservationToken { + identities.next() + } + + func register( + _ pendingReservation: PendingReservation, + for identity: SoftwareFrameReservationToken + ) { + _ = resources.insert(pendingReservation, for: identity) + } + + func take(_ expectedReservation: SoftwareFrameReservation) -> PendingReservation? { + resources.take(expectedReservation.reservationID) { pendingReservation in + reservation(pendingReservation) == expectedReservation + } + } + + func cancel(_ reservation: SoftwareFrameReservation) -> PendingReservation? { + take(reservation) + } + + func close() { + resources.close() + } +} diff --git a/Sources/WaylandClient/Internal/Windowing/PresentationState+Requests.swift b/Sources/WaylandClient/Internal/Windowing/PresentationState+Requests.swift index 761404b7..0c8e3871 100644 --- a/Sources/WaylandClient/Internal/Windowing/PresentationState+Requests.swift +++ b/Sources/WaylandClient/Internal/Windowing/PresentationState+Requests.swift @@ -4,6 +4,7 @@ protocol GeneratedPresentationRequest: Equatable, Sendable { extension PresentationRequest: GeneratedPresentationRequest {} extension PopupPresentationRequest: GeneratedPresentationRequest {} +extension SubsurfacePresentationRequest: GeneratedPresentationRequest {} extension PresentationState where Request: GeneratedPresentationRequest { var isIdle: Bool { diff --git a/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift new file mode 100644 index 00000000..95e5f313 --- /dev/null +++ b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift @@ -0,0 +1,107 @@ +import WaylandRaw + +struct SoftwareSurfacePresentationResult { + let outcome: RedrawOutcome + let followUp: SoftwareSurfacePresentationFollowUp? +} + +struct ReservedSoftwareSurfaceFrame { + let reservation: SoftwareFrameReservation + let drawingBuffer: RawBuffer.ReservedDrawingBuffer +} + +struct SoftwareSurfaceFrameReservationResult { + let reservedFrame: ReservedSoftwareSurfaceFrame? + let followUp: SoftwareSurfacePresentationFollowUp? +} + +package enum SoftwareSurfaceFrameReservationOutcome: Equatable, Sendable { + case reserved(SoftwareFrameReservation) + case deferred + case closed +} + +enum SoftwareSurfacePresentationFollowUp { + case fail(generation: UInt64, PresentationError) + case blockedByBuffer + case resetTransientState + case succeeded(generation: UInt64) +} + +struct SoftwareSurfacePresentationFailure: Error { + let presentationError: PresentationError + let underlying: any Error +} + +package struct SoftwareSurfaceDrawFailure: Error { + package let underlying: any Error + + package init(underlying drawError: any Error) { + underlying = drawError + } +} + +package struct SurfacePresentationFeedbackCommitRequest { + let request: () throws -> SurfacePresentationIdentity + let cancel: (SurfacePresentationIdentity) -> Void + + package init( + request feedbackRequest: @escaping () throws -> SurfacePresentationIdentity, + cancel cancelFeedback: @escaping (SurfacePresentationIdentity) -> Void + ) { + request = feedbackRequest + cancel = cancelFeedback + } +} + +package enum SoftwareSurfacePresentationCommitSequence { + @discardableResult + package static func perform( + stageSuccess: () throws -> Void, + markDrawingBufferBusy: () -> Void, + requestFrameCallback: () -> Void, + requestPresentationFeedback: () -> SurfacePresentationIdentity?, + commitSurface: () -> Void, + commitFollowUp: () -> Void = { + // No role-level commit is required. + } + ) rethrows -> SurfacePresentationIdentity? { + try stageSuccess() + markDrawingBufferBusy() + requestFrameCallback() + let feedbackIdentity = requestPresentationFeedback() + commitSurface() + commitFollowUp() + return feedbackIdentity + } +} + +struct SoftwareSurfacePresentationContext { + let generation: UInt64 + let geometry: SurfaceGeometry + let submitConstraints: SurfaceSubmitConstraints + let metadata: SurfaceCommitMetadata + let damage: SurfaceDamageRegion? + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? + let commitFollowUp: () -> Void + + init( + generation: UInt64, + geometry: SurfaceGeometry, + submitConstraints: SurfaceSubmitConstraints, + metadata: SurfaceCommitMetadata, + damage: SurfaceDamageRegion?, + presentationFeedback: SurfacePresentationFeedbackCommitRequest?, + commitFollowUp: @escaping () -> Void = { + // Windows and popups have no role-level follow-up commit. + } + ) { + self.generation = generation + self.geometry = geometry + self.submitConstraints = submitConstraints + self.metadata = metadata + self.damage = damage + self.presentationFeedback = presentationFeedback + self.commitFollowUp = commitFollowUp + } +} diff --git a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift similarity index 84% rename from Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift rename to Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift index 25649f5b..a11b9322 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift +++ b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift @@ -1,32 +1,33 @@ import WaylandRaw -private struct WindowSoftwareCommitContext { +private struct SoftwareSurfaceCommitContext { let preparedCommit: PreparedSurfaceFrameCommit - let request: PresentationRequest + let generation: UInt64 let bufferSize: TopLevelSize - let presentationFeedback: WindowPresentationFeedbackCommitRequest? + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? + let commitFollowUp: () -> Void } // swiftlint:disable:next type_body_length -struct WindowSoftwarePresenter { +struct SoftwareSurfacePresenter { let surface: RawSurface let scaleInstallation: SurfaceScaleInstallation let createSharedMemoryPool: (PositivePixelSize) throws -> RawSharedMemoryPool - let isWindowClosed: () -> Bool + let isSurfaceClosed: () -> Bool let onFrame: () -> Void func present( - context: WindowSoftwarePresentationContext, + context: SoftwareSurfacePresentationContext, draw: (borrowing SoftwareFrame) throws -> Void, stageSuccess: (RedrawBufferAvailability) throws -> Void = { _ in () }, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration? - ) throws -> WindowSoftwarePresentationResult { + ) throws -> SoftwareSurfacePresentationResult { guard pendingFrameRegistration == nil else { return .init( outcome: .skippedPendingFrame, followUp: .fail( - generation: context.request.generation, + generation: context.generation, .frameCallbackRequest("frame callback is still pending") ) ) @@ -44,13 +45,13 @@ struct WindowSoftwarePresenter { try drawFrame(&drawingBuffer, geometry: context.geometry, draw: draw) - guard !isWindowClosed() else { + guard !isSurfaceClosed() else { drawingBuffer.discard() return .init(outcome: .skippedClosed, followUp: .resetTransientState) } let preparedCommit = try prepareCommit( - request: context.request, + generation: context.generation, geometry: context.geometry, submitConstraints: context.submitConstraints, metadata: context.metadata, @@ -59,11 +60,12 @@ struct WindowSoftwarePresenter { drawingBuffer: &drawingBuffer ) try performPreparedCommit( - context: WindowSoftwareCommitContext( + context: SoftwareSurfaceCommitContext( preparedCommit: preparedCommit, - request: context.request, + generation: context.generation, bufferSize: context.geometry.bufferSize.rawSize, - presentationFeedback: context.presentationFeedback + presentationFeedback: context.presentationFeedback, + commitFollowUp: context.commitFollowUp ), stageSuccess: stageSuccess, runtime: &runtime, @@ -73,21 +75,21 @@ struct WindowSoftwarePresenter { return .init( outcome: .presented, - followUp: .succeeded(generation: context.request.generation) + followUp: .succeeded(generation: context.generation) ) } func reserve( - context: WindowSoftwarePresentationContext, + context: SoftwareSurfacePresentationContext, reservationID: SoftwareFrameReservationToken, runtime: inout SurfaceRuntime, hasPendingFrameRegistration: Bool - ) throws -> WindowSoftwareFrameReservationResult { + ) throws -> SoftwareSurfaceFrameReservationResult { guard !hasPendingFrameRegistration else { return .init( reservedFrame: nil, followUp: .fail( - generation: context.request.generation, + generation: context.generation, .frameCallbackRequest("frame callback is still pending") ) ) @@ -102,7 +104,7 @@ struct WindowSoftwarePresenter { return .init(reservedFrame: nil, followUp: .blockedByBuffer) } - guard !isWindowClosed() else { + guard !isSurfaceClosed() else { drawingBuffer.discard() return .init(reservedFrame: nil, followUp: .resetTransientState) } @@ -116,7 +118,7 @@ struct WindowSoftwarePresenter { geometry: SoftwareFrameGeometry(surface: context.geometry) ) return .init( - reservedFrame: WindowReservedSoftwareFrame( + reservedFrame: ReservedSoftwareSurfaceFrame( reservation: reservation, drawingBuffer: drawingBuffer ), @@ -125,19 +127,19 @@ struct WindowSoftwarePresenter { } func presentReserved( - _ reservedFrame: WindowReservedSoftwareFrame, - context: WindowSoftwarePresentationContext, + _ reservedFrame: ReservedSoftwareSurfaceFrame, + context: SoftwareSurfacePresentationContext, draw: (borrowing SoftwareFrame) throws -> Void, stageSuccess: (RedrawBufferAvailability) throws -> Void = { _ in () }, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration? - ) throws -> WindowSoftwarePresentationResult { + ) throws -> SoftwareSurfacePresentationResult { guard pendingFrameRegistration == nil else { reservedFrame.drawingBuffer.discard() return .init( outcome: .skippedPendingFrame, followUp: .fail( - generation: context.request.generation, + generation: context.generation, .frameCallbackRequest("frame callback is still pending") ) ) @@ -146,13 +148,13 @@ struct WindowSoftwarePresenter { try drawReservedFrame(reservedFrame, geometry: context.geometry, draw: draw) - guard !isWindowClosed() else { + guard !isSurfaceClosed() else { reservedFrame.drawingBuffer.discard() return .init(outcome: .skippedClosed, followUp: .resetTransientState) } let preparedCommit = try prepareReservedCommit( - request: context.request, + generation: context.generation, geometry: context.geometry, submitConstraints: context.submitConstraints, metadata: context.metadata, @@ -161,11 +163,12 @@ struct WindowSoftwarePresenter { drawingBuffer: reservedFrame.drawingBuffer ) try performPreparedReservedCommit( - context: WindowSoftwareCommitContext( + context: SoftwareSurfaceCommitContext( preparedCommit: preparedCommit, - request: context.request, + generation: context.generation, bufferSize: context.geometry.bufferSize.rawSize, - presentationFeedback: context.presentationFeedback + presentationFeedback: context.presentationFeedback, + commitFollowUp: context.commitFollowUp ), stageSuccess: stageSuccess, runtime: &runtime, @@ -175,12 +178,12 @@ struct WindowSoftwarePresenter { return .init( outcome: .presented, - followUp: .succeeded(generation: context.request.generation) + followUp: .succeeded(generation: context.generation) ) } private func validatePresentation( - _ context: WindowSoftwarePresentationContext, + _ context: SoftwareSurfacePresentationContext, runtime: borrowing SurfaceRuntime ) throws { try context.metadata.validate(capabilities: runtime.capabilitySnapshot()) @@ -188,7 +191,7 @@ struct WindowSoftwarePresenter { } private func performPreparedCommit( - context: WindowSoftwareCommitContext, + context: SoftwareSurfaceCommitContext, stageSuccess: (RedrawBufferAvailability) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration?, @@ -206,12 +209,12 @@ struct WindowSoftwarePresenter { matching: context.bufferSize ) do { - _ = try WindowSoftwarePresentationCommitSequence.perform( + _ = try SoftwareSurfacePresentationCommitSequence.perform( stageSuccess: { try stageSuccess(currentBufferAvailability) }, markDrawingBufferBusy: { - _ = drawingBuffer.markBusy(commitGeneration: context.request.generation) + _ = drawingBuffer.markBusy(commitGeneration: context.generation) }, requestFrameCallback: { requestReservedFrameCallback( @@ -223,12 +226,13 @@ struct WindowSoftwarePresenter { context.presentationFeedback ) }, - commit: { + commitSurface: { SurfaceFrameCommitter.commit( stagedCommit, runtime: &runtime, ) - } + }, + commitFollowUp: context.commitFollowUp ) } catch { runtime.cancelFrameCallback() @@ -256,7 +260,7 @@ struct WindowSoftwarePresenter { } } catch { drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .userDraw(String(describing: error)), underlying: error ) @@ -264,7 +268,7 @@ struct WindowSoftwarePresenter { } private func drawReservedFrame( - _ reservedFrame: WindowReservedSoftwareFrame, + _ reservedFrame: ReservedSoftwareSurfaceFrame, geometry: SurfaceGeometry, draw: (borrowing SoftwareFrame) throws -> Void ) throws { @@ -282,7 +286,7 @@ struct WindowSoftwarePresenter { } } catch { reservedFrame.drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .userDraw(String(describing: error)), underlying: error ) @@ -291,7 +295,7 @@ struct WindowSoftwarePresenter { // swiftlint:disable:next function_parameter_count private func prepareCommit( - request: PresentationRequest, + generation: UInt64, geometry: SurfaceGeometry, submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, @@ -304,7 +308,7 @@ struct WindowSoftwarePresenter { SurfaceFrameCommitRequest( surface: surface, scaleInstallation: scaleInstallation, - generation: request.generation, + generation: generation, geometry: geometry, payload: .buffer(drawingBuffer.surfaceBuffer), submitConstraints: submitConstraints, @@ -315,7 +319,7 @@ struct WindowSoftwarePresenter { ) } catch { drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error ) @@ -324,7 +328,7 @@ struct WindowSoftwarePresenter { // swiftlint:disable:next function_parameter_count private func prepareReservedCommit( - request: PresentationRequest, + generation: UInt64, geometry: SurfaceGeometry, submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, @@ -337,7 +341,7 @@ struct WindowSoftwarePresenter { SurfaceFrameCommitRequest( surface: surface, scaleInstallation: scaleInstallation, - generation: request.generation, + generation: generation, geometry: geometry, payload: .buffer(drawingBuffer.surfaceBuffer), submitConstraints: submitConstraints, @@ -348,7 +352,7 @@ struct WindowSoftwarePresenter { ) } catch { drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error ) @@ -365,7 +369,7 @@ struct WindowSoftwarePresenter { } private func requestPresentationFeedbackAtPointOfNoReturn( - _ presentationFeedback: WindowPresentationFeedbackCommitRequest? + _ presentationFeedback: SurfacePresentationFeedbackCommitRequest? ) -> SurfacePresentationIdentity? { guard let presentationFeedback else { return nil } @@ -379,16 +383,16 @@ struct WindowSoftwarePresenter { } private func stageCommit( - context: WindowSoftwareCommitContext, + context: SoftwareSurfaceCommitContext, runtime: inout SurfaceRuntime ) throws -> StagedSurfaceFrameCommit { do { try SurfaceFrameCommitter.reserveFrameCallback( runtime: &runtime, - generation: context.request.generation + generation: context.generation ) } catch { - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .frameCallbackRequest(String(describing: error)), underlying: error ) @@ -401,7 +405,7 @@ struct WindowSoftwarePresenter { ) } catch { runtime.cancelFrameCallback() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error ) @@ -409,7 +413,7 @@ struct WindowSoftwarePresenter { } private func performPreparedReservedCommit( - context: WindowSoftwareCommitContext, + context: SoftwareSurfaceCommitContext, stageSuccess: (RedrawBufferAvailability) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration?, @@ -427,12 +431,12 @@ struct WindowSoftwarePresenter { matching: context.bufferSize ) do { - _ = try WindowSoftwarePresentationCommitSequence.perform( + _ = try SoftwareSurfacePresentationCommitSequence.perform( stageSuccess: { try stageSuccess(currentBufferAvailability) }, markDrawingBufferBusy: { - _ = drawingBuffer.markBusy(commitGeneration: context.request.generation) + _ = drawingBuffer.markBusy(commitGeneration: context.generation) }, requestFrameCallback: { requestReservedFrameCallback( @@ -444,12 +448,13 @@ struct WindowSoftwarePresenter { context.presentationFeedback ) }, - commit: { + commitSurface: { SurfaceFrameCommitter.commit( stagedCommit, runtime: &runtime, ) - } + }, + commitFollowUp: context.commitFollowUp ) } catch { runtime.cancelFrameCallback() diff --git a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift index 0a14c085..5986cbb0 100644 --- a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift +++ b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift @@ -32,7 +32,7 @@ package struct TopLevelWindowRoleResources { struct PendingSoftwareFrameReservation { let request: PresentationRequest let geometry: SurfaceGeometry - let reservedFrame: WindowReservedSoftwareFrame + let reservedFrame: ReservedSoftwareSurfaceFrame } private struct StagedSoftwarePresentationSuccess { @@ -149,8 +149,12 @@ package final class TopLevelWindow { private let initialConfigurePump: (Int32) throws -> Void private let surface: RawSurface private let configureState = XDGConfigureState() - private let softwarePresentationCoordinator = WindowSoftwareReservationCoordinator() - private let presentationFeedbackCoordinator = WindowPresentationFeedbackCoordinator() + private let softwarePresentationCoordinator = + SoftwareSurfaceReservationCoordinator( + reservation: { $0.reservedFrame.reservation }, + retire: { $0.reservedFrame.drawingBuffer.discard() } + ) + private let presentationFeedbackCoordinator = SurfacePresentationFeedbackCoordinator() private var surfaceRuntime: SurfaceRuntime private let failureSink: any WindowFailureSink @@ -497,7 +501,7 @@ package final class TopLevelWindow { submitConstraints: SurfaceSubmitConstraints = .default, metadata: SurfaceCommitMetadata = .default, damage: SurfaceDamageRegion? = nil, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil, + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws -> RedrawOutcome { guard !model.isClosed else { return .skippedClosed } @@ -536,10 +540,11 @@ package final class TopLevelWindow { private func reserveSoftwareFrameForCurrentRedraw( metadata frameMetadata: SurfaceFrameMetadata = .default ) - throws -> WindowSoftwareFrameReservationOutcome + throws -> SoftwareSurfaceFrameReservationOutcome { guard !model.isClosed else { return .closed } try validateSurfaceFrameMetadataSupport(frameMetadata) + try ensureMetadataObjectsInstalled(for: frameMetadata.surfaceCommitMetadata) guard let request = try consumeSoftwarePresentationRequest() else { return .deferred } try interpretWindowEffects( @@ -550,8 +555,8 @@ package final class TopLevelWindow { let geometry = try surfaceGeometry(logicalSize: request.configuration.size) try frameMetadata.damage?.validate(within: geometry) let result = try softwarePresenter().reserve( - context: WindowSoftwarePresentationContext( - request: request, + context: SoftwareSurfacePresentationContext( + generation: request.generation, geometry: geometry, submitConstraints: .default, metadata: frameMetadata.surfaceCommitMetadata, @@ -576,7 +581,7 @@ package final class TopLevelWindow { for: reservedFrame.reservation.reservationID ) return .reserved(reservedFrame.reservation) - } catch let failure as WindowSoftwarePresentationFailure { + } catch let failure as SoftwareSurfacePresentationFailure { failSoftwarePresentationIfStillActive(generation: request.generation) throw failure.underlying } catch { @@ -682,7 +687,7 @@ package final class TopLevelWindow { submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, damage: SurfaceDamageRegion?, - presentationFeedback: WindowPresentationFeedbackCommitRequest?, + presentationFeedback: SurfacePresentationFeedbackCommitRequest?, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws -> RedrawOutcome { try validateSurfaceCommitMetadataSupport(metadata) @@ -692,12 +697,12 @@ package final class TopLevelWindow { let successStagingContext = softwarePresentationSuccessStagingContext() var stagedSuccess: StagedSoftwarePresentationSuccess? - let result: WindowSoftwarePresentationResult + let result: SoftwareSurfacePresentationResult do { let geometry = try surfaceGeometry(logicalSize: request.configuration.size) result = try softwarePresenter().present( - context: WindowSoftwarePresentationContext( - request: request, + context: SoftwareSurfacePresentationContext( + generation: request.generation, geometry: geometry, submitConstraints: submitConstraints, metadata: metadata, @@ -714,10 +719,10 @@ package final class TopLevelWindow { runtime: &surfaceRuntime, pendingFrameRegistration: &pendingFrameRegistration ) - } catch let failure as WindowSoftwarePresentationFailure { + } catch let failure as SoftwareSurfacePresentationFailure { failSoftwarePresentationIfStillActive(generation: request.generation) if case .userDraw = failure.presentationError { - throw WindowSoftwareDrawFailure(underlying: failure.underlying) + throw SoftwareSurfaceDrawFailure(underlying: failure.underlying) } throw failure.underlying } catch { @@ -745,8 +750,8 @@ package final class TopLevelWindow { } } - private func softwarePresenter() -> WindowSoftwarePresenter { - WindowSoftwarePresenter( + private func softwarePresenter() -> SoftwareSurfacePresenter { + SoftwareSurfacePresenter( surface: surface, scaleInstallation: scaleInstallation, createSharedMemoryPool: { [self] bufferSize in @@ -762,20 +767,20 @@ package final class TopLevelWindow { self?.handleBufferReleased() } }, - isWindowClosed: { [self] in model.isClosed }, + isSurfaceClosed: { [self] in model.isClosed }, onFrame: { [weak self] in self?.handleFrameDone() } ) } - // swiftlint:disable:next function_parameter_count + // swiftlint:disable:next function_parameter_count function_body_length private func submitReservedSoftwareFrame( _ reservation: SoftwareFrameReservation, submitConstraints: SurfaceSubmitConstraints, metadata frameMetadata: SurfaceFrameMetadata, damage: SurfaceDamageRegion?, - makePresentationFeedback: () throws -> WindowPresentationFeedbackCommitRequest?, + makePresentationFeedback: () throws -> SurfacePresentationFeedbackCommitRequest?, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws -> SoftwarePresentationOutcome { guard !model.isClosed else { @@ -787,19 +792,20 @@ package final class TopLevelWindow { } var stagedSuccess: StagedSoftwarePresentationSuccess? - let result: WindowSoftwarePresentationResult + let result: SoftwareSurfacePresentationResult do { guard try isCurrentSoftwarePresentation(pendingReservation) else { return try supersedeSoftwarePresentation(pendingReservation) } try validateSurfaceFrameMetadataSupport(frameMetadata) + try ensureMetadataObjectsInstalled(for: frameMetadata.surfaceCommitMetadata) let successStagingContext = softwarePresentationSuccessStagingContext() let presentationFeedback = try makePresentationFeedback() result = try softwarePresenter().presentReserved( pendingReservation.reservedFrame, - context: WindowSoftwarePresentationContext( - request: pendingReservation.request, + context: SoftwareSurfacePresentationContext( + generation: pendingReservation.request.generation, geometry: pendingReservation.geometry, submitConstraints: submitConstraints, metadata: frameMetadata.surfaceCommitMetadata, @@ -816,13 +822,13 @@ package final class TopLevelWindow { runtime: &surfaceRuntime, pendingFrameRegistration: &pendingFrameRegistration ) - } catch let failure as WindowSoftwarePresentationFailure { + } catch let failure as SoftwareSurfacePresentationFailure { pendingReservation.reservedFrame.drawingBuffer.discard() failSoftwarePresentationIfStillActive( generation: pendingReservation.request.generation ) if case .userDraw = failure.presentationError { - throw WindowSoftwareDrawFailure(underlying: failure.underlying) + throw SoftwareSurfaceDrawFailure(underlying: failure.underlying) } throw failure.underlying } catch { @@ -866,7 +872,7 @@ package final class TopLevelWindow { } private func interpretDeferredSoftwarePresentation( - _ result: WindowSoftwarePresentationResult, + _ result: SoftwareSurfacePresentationResult, pendingReservation: PendingSoftwareFrameReservation ) throws -> SoftwarePresentationOutcome { do { @@ -941,7 +947,7 @@ package final class TopLevelWindow { } private func interpretSoftwarePresentationFollowUp( - _ followUp: WindowSoftwarePresentationFollowUp? + _ followUp: SoftwareSurfacePresentationFollowUp? ) throws { guard let followUp else { return } @@ -1465,7 +1471,7 @@ extension TopLevelWindow { submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, damage: SurfaceDamageRegion?, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil, + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws -> RedrawOutcome { var outcome = RedrawOutcome.skippedPendingFrame @@ -1633,7 +1639,7 @@ extension TopLevelWindow { _ buffer: RawSurfaceBuffer, submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata = .default, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil ) throws -> PreviewBufferPresentationResult { connection.preconditionIsOwnerThread() @@ -1798,22 +1804,12 @@ extension TopLevelWindow { private func ensureMetadataObjectsInstalled( for metadata: SurfaceCommitMetadata ) throws { - if metadata.contentType != nil { - try ensureContentTypeObjectInstalled() - } - if metadata.alpha != nil { - try ensureAlphaModifierObjectInstalled() - } - if metadata.presentationHint != nil { - try ensureTearingControlObjectInstalled() - } - if metadata.colorRepresentation != nil { - try ensureColorRepresentationObjectInstalled() - } - if let colorDescription = metadata.colorDescription { - try ensureColorManagementObjectInstalled() - try ensureColorDescriptionInstalled(colorDescription) - } + try SurfaceMetadataSupport.ensureObjectsInstalled( + for: metadata, + connection: connection, + surface: surface, + runtime: &surfaceRuntime + ) } private func validateSurfaceFrameMetadataSupport( @@ -1831,113 +1827,9 @@ extension TopLevelWindow { } private func refreshMetadataCapabilitiesFromBoundGlobals() { - guard let extensions = connection.boundGlobals?.extensions else { - surfaceRuntime.setContentTypeCapability(.unavailable) - surfaceRuntime.setAlphaModifierCapability(.unavailable) - surfaceRuntime.setTearingControlCapability(.unavailable) - surfaceRuntime.setColorRepresentationCapability(.unavailable) - surfaceRuntime.setColorCapability(.unavailable) - return - } - - surfaceRuntime.setContentTypeCapability( - extensions.surfaceContentTypeCapability - ) - surfaceRuntime.setAlphaModifierCapability( - extensions.surfaceAlphaModifierCapability - ) - surfaceRuntime.setTearingControlCapability( - extensions.surfaceTearingControlCapability - ) - surfaceRuntime.setColorRepresentationCapability( - extensions.surfaceColorRepresentationCapability - ) - surfaceRuntime.setColorCapability(extensions.surfaceColorCapability) - } - - private func ensureContentTypeObjectInstalled() throws { - guard !surfaceRuntime.hasContentTypeObject else { return } - guard - let manager = connection.boundGlobals?.extensions - .contentTypeManager.boundObject - else { - throw SurfaceCommitMetadataError.contentTypeUnavailable - } - - surfaceRuntime.installContentTypeObject(try manager.contentType(for: surface)) - } - - private func ensureAlphaModifierObjectInstalled() throws { - guard !surfaceRuntime.hasAlphaModifierObject else { return } - guard - let manager = connection.boundGlobals?.extensions - .alphaModifierManager.boundObject - else { - throw SurfaceCommitMetadataError.alphaModifierUnavailable - } - - surfaceRuntime.installAlphaModifierObject(try manager.alphaModifier(for: surface)) - } - - private func ensureTearingControlObjectInstalled() throws { - guard !surfaceRuntime.hasTearingControlObject else { return } - guard - let manager = connection.boundGlobals?.extensions - .tearingControlManager.boundObject - else { - throw SurfaceCommitMetadataError.tearingControlUnavailable - } - - surfaceRuntime.installTearingControlObject( - try manager.tearingControl(for: surface) - ) - } - - private func ensureColorRepresentationObjectInstalled() throws { - surfaceRuntime.setColorRepresentationCapability( - connection.boundGlobals?.extensions.surfaceColorRepresentationCapability - ?? .unavailable - ) - guard !surfaceRuntime.hasColorRepresentationObject else { return } - guard - let manager = connection.boundGlobals?.extensions - .colorRepresentationManager.boundObject - else { - throw SurfaceCommitMetadataError.colorRepresentationUnavailable - } - - surfaceRuntime.installColorRepresentationObject( - try manager.colorRepresentation(for: surface) - ) - } - - private func ensureColorManagementObjectInstalled() throws { - guard !surfaceRuntime.hasColorManagementObject else { return } - guard - let manager = connection.boundGlobals?.extensions - .colorManager.boundObject - else { - throw SurfaceCommitMetadataError.colorUnavailable - } - - surfaceRuntime.installColorManagementObject(try manager.surface(for: surface)) - } - - private func ensureColorDescriptionInstalled( - _ reference: SurfaceColorDescriptionReference - ) throws { - guard !surfaceRuntime.hasColorDescription(reference) else { return } - guard - let manager = connection.boundGlobals?.extensions - .colorManager.boundObject - else { - throw SurfaceCommitMetadataError.colorUnavailable - } - - try surfaceRuntime.resolveColorDescriptionIfNeeded( - reference, - using: manager, - surface: surface + SurfaceMetadataSupport.refreshCapabilities( + connection: connection, + runtime: &surfaceRuntime ) } @@ -1960,17 +1852,14 @@ extension TopLevelWindow { ) } - let identity = presentationFeedbackCoordinator.allocateIdentity() - let feedback = try presentation.requestFeedback(for: surface) { [weak self] rawEvent in - self?.handlePresentationFeedback( - identity, - event: rawEvent, - outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, - onFeedback: onFeedback - ) + return try presentationFeedbackCoordinator.request( + presentation: presentation, + surface: surface, + outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, + onFeedback: onFeedback + ) { [weak self] error in + self?.reportCallbackFailure(operation: .presentationFeedback, error: error) } - presentationFeedbackCoordinator.register(feedback, for: identity) - return identity } package func cancelPresentationFeedbackOnOwnerThread( @@ -2171,7 +2060,7 @@ extension TopLevelWindow { submitConstraints: SurfaceSubmitConstraints = .default, metadata: SurfaceCommitMetadata = .default, damage: SurfaceDamageRegion? = nil, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil, + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws { connection.preconditionIsOwnerThread() @@ -2193,7 +2082,7 @@ extension TopLevelWindow { package func reserveShowSoftwareFrameOnOwnerThread( timeoutMilliseconds: Int32 = defaultConfigureTimeoutMS, metadata frameMetadata: SurfaceFrameMetadata = .default - ) throws -> WindowSoftwareFrameReservationOutcome { + ) throws -> SoftwareSurfaceFrameReservationOutcome { connection.preconditionIsOwnerThread() try validateSurfaceFrameMetadataSupport(frameMetadata) @@ -2221,6 +2110,19 @@ extension TopLevelWindow { package func commitSubsurfaceParentStateOnOwnerThread() { connection.preconditionIsOwnerThread() guard !model.isClosed else { return } + commitSubsurfaceParentState() + } + + package func commitSynchronizedSubsurfacePresentationParentStateOnOwnerThread() { + connection.preconditionIsOwnerThread() + precondition( + !model.isClosed, + "prevalidated synchronized subsurface parent closed before activation commit" + ) + commitSubsurfaceParentState() + } + + private func commitSubsurfaceParentState() { surface.commit() #if DEBUG onSubsurfaceParentCommitForTesting?() @@ -2231,7 +2133,7 @@ extension TopLevelWindow { submitConstraints: SurfaceSubmitConstraints = .default, metadata: SurfaceCommitMetadata = .default, damage: SurfaceDamageRegion? = nil, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil, + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws { connection.preconditionIsOwnerThread() @@ -2252,7 +2154,7 @@ extension TopLevelWindow { package func reserveRedrawSoftwareFrameOnOwnerThread( metadata frameMetadata: SurfaceFrameMetadata = .default ) - throws -> WindowSoftwareFrameReservationOutcome + throws -> SoftwareSurfaceFrameReservationOutcome { connection.preconditionIsOwnerThread() @@ -2268,7 +2170,7 @@ extension TopLevelWindow { submitConstraints: SurfaceSubmitConstraints = .default, metadata: SurfaceFrameMetadata = .default, damage: SurfaceDamageRegion? = nil, - makePresentationFeedback: () throws -> WindowPresentationFeedbackCommitRequest? = { nil }, + makePresentationFeedback: () throws -> SurfacePresentationFeedbackCommitRequest? = { nil }, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws -> SoftwarePresentationOutcome { connection.preconditionIsOwnerThread() @@ -2321,7 +2223,7 @@ extension TopLevelWindow { submitConstraints: SurfaceSubmitConstraints = .default, metadata: SurfaceCommitMetadata = .default, damage: SurfaceDamageRegion? = nil, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil, + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil, _ draw: (borrowing SoftwareFrame) throws -> Void ) throws { try showOnOwnerThread( @@ -2345,47 +2247,6 @@ extension TopLevelWindow { } extension TopLevelWindow { - private func handlePresentationFeedback( - _ identity: SurfacePresentationIdentity, - event rawEvent: RawPresentationFeedbackEvent, - outputIDForPresentationSyncOutput: (RawOutputPointerIdentity) throws -> OutputID?, - onFeedback: (SurfacePresentationFeedback) -> Void - ) { - presentationFeedbackCoordinator.complete(identity) - - do { - switch rawEvent { - case .presented(let rawPresented): - let synchronizedOutput = try rawPresented.synchronizedOutput.flatMap { output in - try outputIDForPresentationSyncOutput(output) - } - onFeedback( - .presented( - PresentationFeedback( - surface: identity, - timestamp: PresentationTimestamp( - seconds: rawPresented.timestamp.seconds, - nanoseconds: rawPresented.timestamp.nanoseconds - ), - refreshNanoseconds: rawPresented.refreshNanoseconds == 0 - ? nil - : rawPresented.refreshNanoseconds, - sequence: PresentationSequence( - value: rawPresented.sequence.value - ), - flags: PresentationFeedbackFlags(rawValue: rawPresented.flags), - synchronizedOutput: synchronizedOutput - ) - ) - ) - case .discarded: - onFeedback(.discarded(identity)) - } - } catch { - reportCallbackFailure(operation: .presentationFeedback, error: error) - } - } - private func cancelPresentationFeedbacks() { presentationFeedbackCoordinator.close() } diff --git a/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift b/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift index 40c4494e..d8aacd8a 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift @@ -8,7 +8,7 @@ struct WindowExternalBufferPresentationRequest { let geometry: SurfaceGeometry let submitConstraints: SurfaceSubmitConstraints let metadata: SurfaceCommitMetadata - let presentationFeedback: WindowPresentationFeedbackCommitRequest? + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? let onFrameDone: () -> Void } @@ -89,7 +89,7 @@ enum WindowExternalBufferPresenter { } private static func requestPresentationFeedbackAtPointOfNoReturn( - _ presentationFeedback: WindowPresentationFeedbackCommitRequest? + _ presentationFeedback: SurfacePresentationFeedbackCommitRequest? ) -> SurfacePresentationIdentity? { guard let presentationFeedback else { return nil } diff --git a/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift b/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift deleted file mode 100644 index 826e5f1a..00000000 --- a/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift +++ /dev/null @@ -1,117 +0,0 @@ -import WaylandRaw - -final class WindowOwnedResourceLedger { - private var resources: [Identity: Resource] = [:] - private var isClosed = false - private let retireResource: (Resource) -> Void - - init(retireResource: @escaping (Resource) -> Void) { - self.retireResource = retireResource - } - - var count: Int { resources.count } - var isEmpty: Bool { resources.isEmpty } - - @discardableResult - func insert(_ resource: Resource, for identity: Identity) -> Bool { - guard !isClosed, resources[identity] == nil else { - retireResource(resource) - return false - } - resources[identity] = resource - return true - } - - func take(_ identity: Identity) -> Resource? { - resources.removeValue(forKey: identity) - } - - func take( - _ identity: Identity, - matching predicate: (Resource) -> Bool - ) -> Resource? { - guard let resource = resources[identity], predicate(resource) else { return nil } - return resources.removeValue(forKey: identity) - } - - func retire(_ identity: Identity) { - guard let resource = resources.removeValue(forKey: identity) else { return } - retireResource(resource) - } - - func close() { - guard !isClosed else { return } - isClosed = true - let removed = resources - resources.removeAll() - for identity in removed.keys.sorted(by: stableIdentityOrder) { - guard let resource = removed[identity] else { continue } - retireResource(resource) - } - } - - private func stableIdentityOrder(_ lhs: Identity, _ rhs: Identity) -> Bool { - String(reflecting: lhs) < String(reflecting: rhs) - } -} - -final class WindowPresentationFeedbackCoordinator { - private var identities = IDGenerator() - private let resources = WindowOwnedResourceLedger< - SurfacePresentationIdentity, - RawPresentationFeedback - > { $0.cancel() } - - func allocateIdentity() -> SurfacePresentationIdentity { - identities.next() - } - - func register(_ feedback: RawPresentationFeedback, for identity: SurfacePresentationIdentity) { - _ = resources.insert(feedback, for: identity) - } - - func complete(_ identity: SurfacePresentationIdentity) { - _ = resources.take(identity) - } - - func cancel(_ identity: SurfacePresentationIdentity) { - resources.retire(identity) - } - - func close() { - resources.close() - } -} - -final class WindowSoftwareReservationCoordinator { - private var identities = IDGenerator() - private let resources = WindowOwnedResourceLedger< - SoftwareFrameReservationToken, - PendingSoftwareFrameReservation - > { $0.reservedFrame.drawingBuffer.discard() } - - func allocateIdentity() -> SoftwareFrameReservationToken { - identities.next() - } - - func register( - _ reservation: PendingSoftwareFrameReservation, - for identity: SoftwareFrameReservationToken - ) { - _ = resources.insert(reservation, for: identity) - } - - func take(_ reservation: SoftwareFrameReservation) -> PendingSoftwareFrameReservation? { - resources.take(reservation.reservationID) { pendingReservation in - pendingReservation.reservedFrame.reservation == reservation - } - } - - func cancel(_ reservation: SoftwareFrameReservation) -> PendingSoftwareFrameReservation? { - take(reservation) - } - - func close() { - resources.close() - } -} diff --git a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift b/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift deleted file mode 100644 index 0fe7e221..00000000 --- a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift +++ /dev/null @@ -1,82 +0,0 @@ -import WaylandRaw - -struct WindowSoftwarePresentationResult { - let outcome: RedrawOutcome - let followUp: WindowSoftwarePresentationFollowUp? -} - -struct WindowReservedSoftwareFrame { - let reservation: SoftwareFrameReservation - let drawingBuffer: RawBuffer.ReservedDrawingBuffer -} - -struct WindowSoftwareFrameReservationResult { - let reservedFrame: WindowReservedSoftwareFrame? - let followUp: WindowSoftwarePresentationFollowUp? -} - -package enum WindowSoftwareFrameReservationOutcome: Equatable, Sendable { - case reserved(SoftwareFrameReservation) - case deferred - case closed -} - -enum WindowSoftwarePresentationFollowUp { - case fail(generation: UInt64, PresentationError) - case blockedByBuffer - case resetTransientState - case succeeded(generation: UInt64) -} - -struct WindowSoftwarePresentationFailure: Error { - let presentationError: PresentationError - let underlying: any Error -} - -package struct WindowSoftwareDrawFailure: Error { - package let underlying: any Error - - package init(underlying drawError: any Error) { - underlying = drawError - } -} - -package struct WindowPresentationFeedbackCommitRequest { - let request: () throws -> SurfacePresentationIdentity - let cancel: (SurfacePresentationIdentity) -> Void - - package init( - request feedbackRequest: @escaping () throws -> SurfacePresentationIdentity, - cancel cancelFeedback: @escaping (SurfacePresentationIdentity) -> Void - ) { - request = feedbackRequest - cancel = cancelFeedback - } -} - -package enum WindowSoftwarePresentationCommitSequence { - @discardableResult - package static func perform( - stageSuccess: () throws -> Void, - markDrawingBufferBusy: () -> Void, - requestFrameCallback: () -> Void, - requestPresentationFeedback: () -> SurfacePresentationIdentity?, - commit: () -> Void - ) rethrows -> SurfacePresentationIdentity? { - try stageSuccess() - markDrawingBufferBusy() - requestFrameCallback() - let feedbackIdentity = requestPresentationFeedback() - commit() - return feedbackIdentity - } -} - -struct WindowSoftwarePresentationContext { - let request: PresentationRequest - let geometry: SurfaceGeometry - let submitConstraints: SurfaceSubmitConstraints - let metadata: SurfaceCommitMetadata - let damage: SurfaceDamageRegion? - let presentationFeedback: WindowPresentationFeedbackCommitRequest? -} diff --git a/Sources/WaylandClient/Public/Display/DisplayEvent.swift b/Sources/WaylandClient/Public/Display/DisplayEvent.swift index 18b2377a..b4735bd5 100644 --- a/Sources/WaylandClient/Public/Display/DisplayEvent.swift +++ b/Sources/WaylandClient/Public/Display/DisplayEvent.swift @@ -3,8 +3,7 @@ public enum DisplayEvent: Equatable, Sendable { case windowClosed(WindowID) case popupDismissed(PopupLifecycleEvent) case popupClosed(PopupLifecycleEvent) - case redrawRequested(WindowID) - case popupRedrawRequested(PopupLifecycleEvent) + case redrawRequested(ManagedSurfaceIdentity) case outputChanged(OutputSnapshot) case outputRemoved(OutputID) case windowOutputsChanged(WindowOutputMembershipEvent) @@ -13,7 +12,7 @@ public enum DisplayEvent: Equatable, Sendable { case input(InputEvent) case textInput(TextInputEvent) case dataTransfer(DataTransferEvent) - case presentation(WindowPresentationEvent) + case presentation(ManagedSurfacePresentationEvent) case diagnostic(DisplayDiagnostic) } @@ -118,13 +117,18 @@ package struct InternalEventSubscription: Sendable { @safe package struct InternalEventSubscriptionFactory: Sendable { private let broker: TypedEventBroker + private let includes: @Sendable (Element) -> Bool - init(_ eventBroker: TypedEventBroker) { + init( + _ eventBroker: TypedEventBroker, + where eventFilter: @escaping @Sendable (Element) -> Bool = { _ in true } + ) { broker = eventBroker + includes = eventFilter } package func makeAsyncIterator() -> InternalEventSubscriptionIterator { - broker.subscribe().makeAsyncIterator() + broker.subscribe(where: includes).makeAsyncIterator() } } diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay+PopupSoftwarePresentation.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay+PopupSoftwarePresentation.swift new file mode 100644 index 00000000..8f786597 --- /dev/null +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay+PopupSoftwarePresentation.swift @@ -0,0 +1,47 @@ +extension WaylandDisplay { + package func reservePopupSoftwareFrameForShow( + id: PopupID, + timeoutMilliseconds: Int32, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.reservePopupSoftwareFrameForShow( + id: id, + timeoutMilliseconds: timeoutMilliseconds, + metadata: metadata + ) + } + + package func reservePopupSoftwareFrameForRedraw( + id: PopupID, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.reservePopupSoftwareFrameForRedraw(id: id, metadata: metadata) + } + + package func submitReservedPopupSoftwareFrame( + id: PopupID, + reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + requestPresentationFeedback: Bool, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) throws -> SoftwarePresentationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.submitReservedPopupSoftwareFrame( + id: id, + reservation: reservation, + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + draw + ) + } + + package func cancelReservedPopupSoftwareFrame( + id: PopupID, + reservation: SoftwareFrameReservation + ) throws { + guard let core = coreIfActive() else { return } + core.cancelReservedPopupSoftwareFrame(id: id, reservation: reservation) + } +} diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay+Presentation.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay+Presentation.swift index 142a58b1..97d1a6d2 100644 --- a/Sources/WaylandClient/Public/Display/WaylandDisplay+Presentation.swift +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay+Presentation.swift @@ -1,7 +1,7 @@ extension WaylandDisplay { - package nonisolated func windowPresentationEvents( - for windowID: WindowID - ) -> WindowPresentationEvents { - lifetimeAnchor.eventHub.windowPresentationEvents(windowID: windowID) + package nonisolated func managedSurfacePresentationEvents( + for surface: ManagedSurfaceIdentity + ) -> ManagedSurfacePresentationEvents { + lifetimeAnchor.eventHub.managedSurfacePresentationEvents(surface: surface) } } diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift index 4c7cf183..5af5561b 100644 --- a/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift @@ -40,7 +40,7 @@ extension WaylandDisplay { _ windowID: WindowID, timeoutMilliseconds: Int32, metadata: SurfaceFrameMetadata = .default - ) throws -> WindowSoftwareFrameReservationOutcome { + ) throws -> SoftwareSurfaceFrameReservationOutcome { guard let core = coreIfActive() else { return .closed } return try core.reserveSoftwareFrameForShow( windowID, @@ -84,7 +84,7 @@ extension WaylandDisplay { package func reserveSoftwareFrameForRedraw( _ windowID: WindowID, metadata frameMetadata: SurfaceFrameMetadata = .default - ) throws -> WindowSoftwareFrameReservationOutcome { + ) throws -> SoftwareSurfaceFrameReservationOutcome { guard let core = coreIfActive() else { return .closed } return try core.reserveSoftwareFrameForRedraw( windowID, diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay+Subsurface.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay+Subsurface.swift index 7ee49033..9be8d254 100644 --- a/Sources/WaylandClient/Public/Display/WaylandDisplay+Subsurface.swift +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay+Subsurface.swift @@ -18,22 +18,6 @@ extension WaylandDisplay { ) } - package func showSubsurface( - _ subsurfaceID: SubsurfaceID, - damage: SurfaceDamageRegion?, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { - try requireCore().showSubsurface(subsurfaceID, damage: damage, draw) - } - - package func redrawSubsurface( - _ subsurfaceID: SubsurfaceID, - damage: SurfaceDamageRegion?, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { - try requireCore().redrawSubsurface(subsurfaceID, damage: damage, draw) - } - package func requestSubsurfaceRedraw(_ subsurfaceID: SubsurfaceID) throws { try requireCore().requestSubsurfaceRedraw(subsurfaceID) } diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay+SubsurfaceSoftwarePresentation.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay+SubsurfaceSoftwarePresentation.swift new file mode 100644 index 00000000..cd890e52 --- /dev/null +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay+SubsurfaceSoftwarePresentation.swift @@ -0,0 +1,48 @@ +extension WaylandDisplay { + package func reserveSubsurfaceSoftwareFrameForShow( + id: SubsurfaceID, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.reserveSubsurfaceSoftwareFrameForShow(id: id, metadata: metadata) + } + + package func reserveSubsurfaceSoftwareFrameForRedraw( + id: SubsurfaceID, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.reserveSubsurfaceSoftwareFrameForRedraw(id: id, metadata: metadata) + } + + // swiftlint:disable:next function_parameter_count + package func submitReservedSubsurfaceSoftwareFrame( + id: SubsurfaceID, + reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + requestPresentationFeedback: Bool, + prepared: sending Prepared, + _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void + ) throws -> SoftwarePresentationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.submitReservedSubsurfaceSoftwareFrame( + id: id, + reservation: reservation, + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + prepared: prepared, + draw + ) + } + + package func cancelReservedSubsurfaceSoftwareFrame( + id: SubsurfaceID, + reservation: SoftwareFrameReservation + ) throws { + guard let core = coreIfActive() else { return } + try core.cancelReservedSubsurfaceSoftwareFrame( + id: id, + reservation: reservation + ) + } +} diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay.swift index 737affdd..c07b1cc3 100644 --- a/Sources/WaylandClient/Public/Display/WaylandDisplay.swift +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay.swift @@ -2,7 +2,6 @@ import Foundation import WaylandRaw import WaylandRuntime -// swiftlint:disable:next type_body_length public actor WaylandDisplay { public static let defaultDiscoveryTimeoutMilliseconds: Int32 = 1_000 public static let defaultConfigureTimeoutMilliseconds: Int32 = 1_000 @@ -156,25 +155,6 @@ public actor WaylandDisplay { try requireCore().createPopup(parent: windowID, configuration: popupConfiguration) } - package func showPopup( - _ popupID: PopupID, - timeoutMilliseconds: Int32 = defaultConfigureTimeoutMilliseconds, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { - try requireCore().showPopup( - popupID, - timeoutMilliseconds: timeoutMilliseconds, - draw - ) - } - - package func redrawPopup( - _ popupID: PopupID, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { - try requireCore().redrawPopup(popupID, draw) - } - package func windowIsClosed(_ windowID: WindowID) throws -> Bool { try requireCore().windowIsClosed(windowID) } diff --git a/Sources/WaylandClient/Public/Popup/PopupSurface+SoftwareFramePreparation.swift b/Sources/WaylandClient/Public/Popup/PopupSurface+SoftwareFramePreparation.swift new file mode 100644 index 00000000..3c1af6ec --- /dev/null +++ b/Sources/WaylandClient/Public/Popup/PopupSurface+SoftwareFramePreparation.swift @@ -0,0 +1,117 @@ +extension PopupSurface { + @discardableResult + public func show( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await show( + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + timeoutMilliseconds: timeoutMilliseconds, + preparing: { _ in () }, + { _, frame in try draw(frame) } + ) + } + + @discardableResult + public func show( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, + preparing prepare: sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, + _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + let reservationOutcome = try await display.reservePopupSoftwareFrameForShow( + id: popupID, + timeoutMilliseconds: timeoutMilliseconds, + metadata: metadata + ) + switch reservationOutcome { + case .deferred: + return .deferred + case .closed: + return .closed + case .reserved(let reservation): + do { + let prepared = try await prepare(reservation) + return try await display.submitReservedPopupSoftwareFrame( + id: popupID, + reservation: reservation, + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback + ) { frame in + try draw(prepared, frame) + } + } catch { + let submissionError = error + do { + try await display.cancelReservedPopupSoftwareFrame( + id: popupID, + reservation: reservation + ) + } catch { + throw submissionError + } + throw submissionError + } + } + } + + @discardableResult + public func redraw( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await redraw( + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + preparing: { _ in () }, + { _, frame in try draw(frame) } + ) + } + + @discardableResult + public func redraw( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + preparing prepare: sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, + _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + let reservationOutcome = try await display.reservePopupSoftwareFrameForRedraw( + id: popupID, + metadata: metadata + ) + switch reservationOutcome { + case .deferred: + return .deferred + case .closed: + return .closed + case .reserved(let reservation): + do { + let prepared = try await prepare(reservation) + return try await display.submitReservedPopupSoftwareFrame( + id: popupID, + reservation: reservation, + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback + ) { frame in + try draw(prepared, frame) + } + } catch { + let submissionError = error + do { + try await display.cancelReservedPopupSoftwareFrame( + id: popupID, + reservation: reservation + ) + } catch { + throw submissionError + } + throw submissionError + } + } + } +} diff --git a/Sources/WaylandClient/Public/Popup/PopupSurface.swift b/Sources/WaylandClient/Public/Popup/PopupSurface.swift index e6c0eb03..7197b3bb 100644 --- a/Sources/WaylandClient/Public/Popup/PopupSurface.swift +++ b/Sources/WaylandClient/Public/Popup/PopupSurface.swift @@ -3,7 +3,7 @@ public struct PopupSurface: Sendable, Hashable, Identifiable { public let id: PopupSurfaceIdentity public let parentWindowID: WindowID - private let display: WaylandDisplay + package let display: WaylandDisplay private let ownership: DisplayOwnedIdentity package init( @@ -22,23 +22,14 @@ public struct PopupSurface: Sendable, Hashable, Identifiable { id } - public func show( - timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) async throws { - try await display.showPopup(popupID, timeoutMilliseconds: timeoutMilliseconds, draw) - } - - public func redraw( - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) async throws { - try await display.redrawPopup(popupID, draw) - } - public func requestRedraw() async throws { try await display.requestPopupRedraw(popupID) } + public var presentationEvents: ManagedSurfacePresentationEvents { + display.managedSurfacePresentationEvents(for: .popup(id)) + } + public func setInputRegion(_ region: SurfaceRegion?) async throws { try await display.setPopupInputRegion(popupID, region) } diff --git a/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift index 2cc29f8f..efc333e8 100644 --- a/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift +++ b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift @@ -68,73 +68,83 @@ public struct PresentationFeedback: Equatable, Sendable { } } -public struct WindowPresentationEvent: Equatable, Sendable { - public let windowID: WindowID +/// Presentation feedback correlated to its managed surface. +public struct ManagedSurfacePresentationEvent: Equatable, Sendable { + /// The window, popup, or subsurface that owned the committed feedback request. + public let surface: ManagedSurfaceIdentity + /// The compositor's result for the committed request. public let feedback: SurfacePresentationFeedback + /// Creates a managed-surface presentation event. public init( - windowID eventWindowID: WindowID, + surface eventSurface: ManagedSurfaceIdentity, feedback eventFeedback: SurfacePresentationFeedback ) { - windowID = eventWindowID + surface = eventSurface feedback = eventFeedback } } -/// A window-scoped presentation-feedback convenience stream. +/// A managed-surface-scoped presentation-feedback convenience stream. /// -/// This stream preserves presentation order for its window, but not ordering +/// This stream preserves presentation order for its surface, but not ordering /// relative to other event families. Use ``DisplayEvents`` when cross-family -/// ordering matters. +/// ordering matters. Closing the selected surface cancels its outstanding +/// feedback but does not finish this display-owned sequence. Closing the display +/// drains already-published feedback and then finishes the sequence. @safe -public struct WindowPresentationEvents: AsyncSequence, Sendable { +public struct ManagedSurfacePresentationEvents: AsyncSequence, Sendable { public typealias Element = SurfacePresentationFeedback public typealias Failure = WaylandDisplayError - private let windowID: WindowID - private let subscriptions: InternalEventSubscriptionFactory + private let surface: ManagedSurfaceIdentity + private let subscriptions: InternalEventSubscriptionFactory package init( - windowID eventsWindowID: WindowID, - subscriptions eventSubscriptions: InternalEventSubscriptionFactory + surface eventsSurface: ManagedSurfaceIdentity, + subscriptions eventSubscriptions: + InternalEventSubscriptionFactory ) { - windowID = eventsWindowID + surface = eventsSurface subscriptions = eventSubscriptions } - public func makeAsyncIterator() -> WindowPresentationEventsIterator { - WindowPresentationEventsIterator( - windowID: windowID, + public func makeAsyncIterator() -> ManagedSurfacePresentationEventsIterator { + ManagedSurfacePresentationEventsIterator( + surface: surface, base: subscriptions.makeAsyncIterator() ) } } +/// An iterator over presentation feedback for one managed surface. @safe -public struct WindowPresentationEventsIterator: AsyncIteratorProtocol { +public struct ManagedSurfacePresentationEventsIterator: AsyncIteratorProtocol { public typealias Element = SurfacePresentationFeedback public typealias Failure = WaylandDisplayError - private let windowID: WindowID - private var base: InternalEventSubscriptionIterator + private let surface: ManagedSurfaceIdentity + private var base: InternalEventSubscriptionIterator package init( - windowID iteratorWindowID: WindowID, - base iterator: InternalEventSubscriptionIterator + surface iteratorSurface: ManagedSurfaceIdentity, + base iterator: InternalEventSubscriptionIterator ) { - windowID = iteratorWindowID + surface = iteratorSurface base = iterator } + /// Returns the next feedback result for the selected surface. public mutating func next() async throws(WaylandDisplayError) -> SurfacePresentationFeedback? { try await next(isolation: nil) } + /// Returns the next result while preserving the caller's isolation context. public mutating func next( isolation actor: isolated (any Actor)? ) async throws(WaylandDisplayError) -> SurfacePresentationFeedback? { while let event = try await base.next(isolation: actor) { - guard event.windowID == windowID else { continue } + guard event.surface == surface else { continue } return event.feedback } diff --git a/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift b/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift new file mode 100644 index 00000000..e6db4b86 --- /dev/null +++ b/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift @@ -0,0 +1,9 @@ +/// Identifies a managed software surface independently of a presentation request. +public enum ManagedSurfaceIdentity: Hashable, Sendable { + /// A top-level managed window. + case window(WindowID) + /// An xdg-shell popup. + case popup(PopupSurfaceIdentity) + /// A managed subsurface child. + case subsurface(SubsurfaceIdentity) +} diff --git a/Sources/WaylandClient/Public/Subsurface/Subsurface+SoftwareFramePreparation.swift b/Sources/WaylandClient/Public/Subsurface/Subsurface+SoftwareFramePreparation.swift new file mode 100644 index 00000000..377a2a0c --- /dev/null +++ b/Sources/WaylandClient/Public/Subsurface/Subsurface+SoftwareFramePreparation.swift @@ -0,0 +1,133 @@ +extension Subsurface { + @discardableResult + public func show( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + _ draw: + sending @Sendable ( + borrowing SoftwareFrame + ) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await show( + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + preparing: { _ in () }, + { _, frame in try draw(frame) } + ) + } + + @discardableResult + public func show( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + preparing: + sending @Sendable ( + SoftwareFrameReservation + ) async throws -> Prepared, + _ draw: + sending @Sendable ( + Prepared, + borrowing SoftwareFrame + ) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + switch try await display.reserveSubsurfaceSoftwareFrameForShow( + id: subsurfaceID, + metadata: metadata + ) { + case .reserved(let reservation): + let prepared: Prepared + do { + prepared = try await preparing(reservation) + } catch { + let submissionError = error + do { + try await display.cancelReservedSubsurfaceSoftwareFrame( + id: subsurfaceID, + reservation: reservation + ) + } catch { + throw submissionError + } + throw submissionError + } + return try await display.submitReservedSubsurfaceSoftwareFrame( + id: subsurfaceID, + reservation: reservation, + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + prepared: prepared, + draw + ) + case .deferred: + return .deferred + case .closed: + return .closed + } + } + + @discardableResult + public func redraw( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + _ draw: + sending @Sendable ( + borrowing SoftwareFrame + ) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await redraw( + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + preparing: { _ in () }, + { _, frame in try draw(frame) } + ) + } + + @discardableResult + public func redraw( + metadata: SurfaceFrameMetadata = .default, + requestPresentationFeedback: Bool = false, + preparing: + sending @Sendable ( + SoftwareFrameReservation + ) async throws -> Prepared, + _ draw: + sending @Sendable ( + Prepared, + borrowing SoftwareFrame + ) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + switch try await display.reserveSubsurfaceSoftwareFrameForRedraw( + id: subsurfaceID, + metadata: metadata + ) { + case .reserved(let reservation): + let prepared: Prepared + do { + prepared = try await preparing(reservation) + } catch { + let submissionError = error + do { + try await display.cancelReservedSubsurfaceSoftwareFrame( + id: subsurfaceID, + reservation: reservation + ) + } catch { + throw submissionError + } + throw submissionError + } + return try await display.submitReservedSubsurfaceSoftwareFrame( + id: subsurfaceID, + reservation: reservation, + metadata: metadata, + requestPresentationFeedback: requestPresentationFeedback, + prepared: prepared, + draw + ) + case .deferred: + return .deferred + case .closed: + return .closed + } + } +} diff --git a/Sources/WaylandClient/Public/Subsurface/Subsurface.swift b/Sources/WaylandClient/Public/Subsurface/Subsurface.swift index 4c2b6758..ee2951a1 100644 --- a/Sources/WaylandClient/Public/Subsurface/Subsurface.swift +++ b/Sources/WaylandClient/Public/Subsurface/Subsurface.swift @@ -3,7 +3,7 @@ public struct Subsurface: Sendable, Hashable, Identifiable { public let id: SubsurfaceIdentity public let parentWindowID: WindowID - private let display: WaylandDisplay + package let display: WaylandDisplay private let ownership: DisplayOwnedIdentity package init( @@ -26,36 +26,14 @@ public struct Subsurface: Sendable, Hashable, Identifiable { ownership.isOwned(by: owningDisplay) } - public func show( - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) async throws { - try await display.showSubsurface(subsurfaceID, damage: nil, draw) - } - - public func show( - damage: SurfaceDamageRegion?, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) async throws { - try await display.showSubsurface(subsurfaceID, damage: damage, draw) - } - - public func redraw( - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) async throws { - try await display.redrawSubsurface(subsurfaceID, damage: nil, draw) - } - - public func redraw( - damage: SurfaceDamageRegion?, - _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) async throws { - try await display.redrawSubsurface(subsurfaceID, damage: damage, draw) - } - public func requestRedraw() async throws { try await display.requestSubsurfaceRedraw(subsurfaceID) } + public var presentationEvents: ManagedSurfacePresentationEvents { + display.managedSurfacePresentationEvents(for: .subsurface(id)) + } + public func setInputRegion(_ region: SurfaceRegion?) async throws { try await display.setSubsurfaceInputRegion(subsurfaceID, region) } diff --git a/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift index 567e9b66..2d36c3dc 100644 --- a/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift +++ b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift @@ -1,13 +1,13 @@ -/// The terminal disposition of an asynchronously prepared software frame. +/// The terminal disposition of a managed-surface software frame. public enum SoftwarePresentationOutcome: Equatable, Sendable { - /// The frame was committed to the window's Wayland surface. + /// The frame was committed through the managed surface's role boundary. case presented /// Preparation completed after the transaction became stale or was canceled /// during final validation. case superseded /// No presentation started because redraw pacing or buffers deferred it. case deferred - /// The window or its display closed before the frame could be committed. + /// The managed surface, its parent, or its display closed before commit. case closed } @@ -71,12 +71,15 @@ extension Window { } } catch { let submissionError = error + let callerError = + (submissionError as? SoftwareSurfaceDrawFailure)?.underlying + ?? submissionError do { try await display.cancelSoftwareFrameReservation(id, reservation: reservation) } catch { - throw submissionError + throw callerError } - throw submissionError + throw callerError } } @@ -135,12 +138,15 @@ extension Window { } } catch { let submissionError = error + let callerError = + (submissionError as? SoftwareSurfaceDrawFailure)?.underlying + ?? submissionError do { try await display.cancelSoftwareFrameReservation(id, reservation: reservation) } catch { - throw submissionError + throw callerError } - throw submissionError + throw callerError } } } diff --git a/Sources/WaylandClient/Public/Window/Window.swift b/Sources/WaylandClient/Public/Window/Window.swift index 9b945323..f09f7143 100644 --- a/Sources/WaylandClient/Public/Window/Window.swift +++ b/Sources/WaylandClient/Public/Window/Window.swift @@ -132,8 +132,8 @@ public struct Window: Sendable, Hashable { try await display.requestRedraw(id) } - public var presentationEvents: WindowPresentationEvents { - display.windowPresentationEvents(for: id) + public var presentationEvents: ManagedSurfacePresentationEvents { + display.managedSurfacePresentationEvents(for: .window(id)) } public func requestPresentationFeedback() async throws { diff --git a/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md b/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md index 2a69935d..0ca926aa 100644 --- a/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md +++ b/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md @@ -5,11 +5,15 @@ window lifecycle, input, text input, data transfer, presentation, and diagnostic events in owner-thread publication order. ``InputEvents``, ``TextInputEvents``, ``DataTransferEvents``, -``WindowPresentationEvents``, and ``DisplayDiagnostics`` remain available as +``ManagedSurfacePresentationEvents``, and ``DisplayDiagnostics`` remain available as specialized convenience streams. ``DisplayEvents`` is the only stream that preserves ordering across event families. A specialized stream preserves order only within its own family. +Window, popup, and subsurface redraws all use +`DisplayEvent.redrawRequested(ManagedSurfaceIdentity)`. Presentation feedback +for all three roles uses `DisplayEvent.presentation(ManagedSurfacePresentationEvent)`. + Each call to `makeAsyncIterator()` creates its own subscription and buffer. Buffering starts when the iterator is created, so events published before that point are not replayed. Copying a sequence is safe: iterators made from either diff --git a/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md new file mode 100644 index 00000000..1a761084 --- /dev/null +++ b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md @@ -0,0 +1,80 @@ +# Managed Surface Drawing + +``Window``, ``PopupSurface``, and ``Subsurface`` expose one software drawing contract through ``SoftwareFrame``, ``SurfaceFrameMetadata``, and ``SoftwarePresentationOutcome``. WaylandClientKit owns shared-memory pool selection, buffer reservation, frame callbacks, metadata and damage staging, optional presentation feedback, commits, and cleanup. + +Use each handle's simple `show` and `redraw` methods when scene preparation is synchronous. Use the prepared forms when expensive work should begin only after WaylandClientKit has selected the authoritative geometry and reusable buffer identity. The preparation closure receives a ``SoftwareFrameReservation``; mutable bytes remain scoped to the final draw closure. + +```swift +let outcome = try await popup.show( + metadata: .default, + requestPresentationFeedback: true, + preparing: { reservation in + await renderer.prepare(for: reservation.geometry) + }, + { prepared, frame in + renderer.draw(prepared, into: frame) + } +) +``` + +Windows and popups include the configure timeout on initial `show`. Subsurfaces are configure-independent and therefore do not. + +## Latest-Wins Transactions + +Prepared presentation reserves one exact redraw generation. After preparation resumes, WaylandClientKit revalidates: + +- surface and parent lifecycle; +- reservation ownership and task cancellation; +- redraw generation; +- current popup placement or subsurface synchronization mode; +- authoritative geometry and scale; and +- metadata capability support. + +Stale work returns `.superseded` before drawing or issuing callback, feedback, damage, attachment, or commit requests. The newest dirty generation remains eligible and produces one replacement `DisplayEvent.redrawRequested(ManagedSurfaceIdentity)` when pacing permits. + +``SoftwarePresentationOutcome`` has four terminal states: + +- `.presented`: the managed surface transaction committed; +- `.superseded`: preparation became stale or was canceled after returning; +- `.deferred`: pacing or buffer availability prevented a reservation, so preparation was not invoked; and +- `.closed`: the managed surface, its parent, or its display closed before commit. + +Preparation and drawing errors release the reservation, preserve eligible redraw work, and rethrow the original caller error. + +## Metadata And Damage + +All three roles accept ``SurfaceFrameMetadata``. Unsupported explicit metadata is rejected before drawing, allocating a new pool, or issuing presentation-side requests. `metadata.damage == nil` is the only full-frame representation. A present-but-empty damage region means no damaged rectangles; it is not a full-frame sentinel. + +Logical damage is validated before drawing, transformed using the final geometry and scale, clipped to the surface, and staged in the same commit transaction as attachment and metadata. + +## Role-Specific Commit Boundaries + +Window and popup commits retain their own configure and lifecycle reducers. The shared presentation engine owns only buffer and commit mechanics. + +A desynchronized subsurface commits only its child surface. A synchronized subsurface commits the child and then its parent inside one irreversible boundary. `.presented` therefore means synchronized child state was latched through the parent, not merely cached for a later parent commit. + +## Redraw Routing + +Every role publishes redraw work through one root event: + +```swift +case .redrawRequested(let surface): + switch surface { + case .window(let id): + // redraw the window + case .popup(let id): + // redraw the popup + case .subsurface(let id): + // redraw the subsurface + } +``` + +The complete ``DisplayEvents`` stream preserves ordering across redraw, presentation, lifecycle, input, data-transfer, output, and diagnostic events. + +## Example + +See `ManagedSurfacePresentationSmoke` in `Examples/ManagedSurfacePresentationSmoke` for window, popup, synchronized-subsurface, and desynchronized-subsurface presentation, redraw routing, optional feedback, popup cleanup, and parent-close cascade behavior. The default run exercises feedback and redraw routing. Use `--skip-feedback --skip-redraw-routing` for a bounded compositor-evidence run when the desktop does not deliver optional feedback or replacement frame callbacks promptly, and `--interactive-dismissal` to wait for a compositor popup dismissal. + +See when updating code that used the +old window-only feedback events, popup redraw case, or subsurface `damage:` +overloads. diff --git a/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md new file mode 100644 index 00000000..36c1978d --- /dev/null +++ b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md @@ -0,0 +1,50 @@ +# Migrating To Managed Surface Presentation + +The managed-surface presentation milestone replaces role-specific drawing and event contracts with one model for windows, popups, and subsurfaces. + +## Redraw Events + +Replace window-only and popup-specific cases: + +```swift +case .redrawRequested(let windowID) +case .popupRedrawRequested(let lifecycle) +``` + +with one identity switch: + +```swift +case .redrawRequested(.window(let windowID)): + // redraw window +case .redrawRequested(.popup(let popupID)): + // redraw popup +case .redrawRequested(.subsurface(let subsurfaceID)): + // redraw subsurface +``` + +Popup lifecycle events continue to carry ``PopupLifecycleEvent`` through `popupDismissed` and `popupClosed`. + +## Presentation Feedback + +Replace `WindowPresentationEvent` and `WindowPresentationEvents` with ``ManagedSurfacePresentationEvent`` and ``ManagedSurfacePresentationEvents``. Root events now expose ``ManagedSurfacePresentationEvent/surface`` instead of `windowID`. Every `Window`, `PopupSurface`, and `Subsurface` exposes a filtered `presentationEvents` sequence. + +Do not use ``SurfacePresentationIdentity`` as a stable surface key. It identifies one feedback request. Use ``ManagedSurfaceIdentity`` for the underlying managed role. + +## Popup And Subsurface Drawing + +Popup `show` and `redraw` now return ``SoftwarePresentationOutcome`` and accept ``SurfaceFrameMetadata`` plus optional feedback. Each also has a prepared overload. + +Subsurface `show` and `redraw` use the same direct and prepared forms. Remove calls to the old `damage:` overloads and place damage in metadata: + +```swift +let metadata = SurfaceFrameMetadata(damage: changedRegion) +let outcome = try await subsurface.redraw(metadata: metadata) { frame in + draw(frame) +} +``` + +Handle `.superseded`, `.deferred`, and `.closed` when work is asynchronous or compositor state changes during preparation. Preparation is not invoked for `.deferred`, and the draw closure is not invoked for any rejected outcome. + +## Synchronized Subsurfaces + +A successful synchronized submission now includes the child commit followed by exactly one parent commit. `.presented` means the child state was latched through its parent. Callers no longer need to arrange a later parent commit for software presentation. diff --git a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md index ed65699c..d82ca401 100644 --- a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md +++ b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md @@ -1,38 +1,32 @@ # Presentation Feedback And Frame Callbacks -Frame callbacks and presentation feedback serve different purposes. Frame -callbacks tell WaylandClientKit when the compositor is ready for another surface -commit. Presentation feedback reports compositor timing facts for an already -submitted commit when the optional presentation-time protocol is available. +Frame callbacks and presentation feedback serve different purposes. Frame callbacks tell WaylandClientKit when a managed surface may submit another paced frame. Presentation feedback reports compositor timing facts for an already committed frame when `wp_presentation` is available. -Use ``WindowPresentationEvents`` and ``PresentationFeedback`` for presentation -timing. Use window redraw requests to schedule drawing work. +Pass `requestPresentationFeedback: true` to the direct or prepared `show` and `redraw` methods on ``Window``, ``PopupSurface``, or ``Subsurface``. Feedback is requested inside the accepted callback-feedback-commit transaction. Superseded, deferred, closed, canceled, and metadata-rejected attempts request neither feedback nor a frame callback and do not commit. -Package-internal GPU preview commits can carry submit constraints and surface -metadata. Submit constraints govern latching or reuse; presentation feedback -reports what the compositor later observed. +Every feedback result enters the root stream as `DisplayEvent.presentation(ManagedSurfacePresentationEvent)`: -`WaylandGraphicsPreview` reports related graphics-path facts without changing -ordinary `Window` redraw behavior. +```swift +case .presentation(let event): + print(event.surface, event.feedback) +``` -## Capability Gate +``ManagedSurfacePresentationEvent/surface`` identifies the window, popup, or subsurface independently of ``SurfacePresentationIdentity``, which identifies one feedback request. Each handle's ``ManagedSurfacePresentationEvents`` sequence filters the same event family by ``ManagedSurfaceIdentity``. + +A synchronized subsurface requests feedback for the child surface commit. The following parent commit makes that child state visible but does not change feedback ownership. + +## Ordering And Lifetime -Presentation feedback requires `wp_presentation`. Missing presentation-time -support is reported as unavailable. Frame callbacks are not treated as fake -presentation feedback. +``DisplayEvents`` preserves root publication order across redraw, presentation, lifecycle, input, data-transfer, output, and diagnostic events. A handle-specific presentation sequence preserves order only for its selected surface. + +Uncommitted feedback objects are canceled on precommit failure. Surface close terminates outstanding feedback for that surface but does not finish display-owned event sequences. Display close terminates all outstanding feedback and finishes both root and filtered streams after already-published events are delivered. + +## Capability Gate -WaylandClientKit owns requests, event correlation, and stream termination. -Frameworks own animation, frame budgeting, and fallback scheduling. +Presentation feedback requires `wp_presentation`. Requesting it when unavailable throws before drawing or presentation-side requests. Frame callbacks are never synthesized as feedback. -For asynchronously prepared software frames, pass -`requestPresentationFeedback: true` to -``Window/show(metadata:requestPresentationFeedback:timeoutMilliseconds:preparing:_:)`` -or ``Window/redraw(metadata:requestPresentationFeedback:preparing:_:)``. The -feedback object is requested inside the same callback-feedback-commit sequence -as the accepted frame. Superseded, deferred, and closed attempts request neither -feedback nor a frame callback and do not commit. Requesting feedback throws when -the compositor does not provide presentation-time support. +WaylandClientKit owns request lifetime, correlation, and stream termination. Frameworks own animation, frame budgeting, and fallback scheduling. -## Example +## Examples -See `PresentationFeedbackAnimation` in `Examples/PresentationFeedbackAnimation`. +See `PresentationFeedbackAnimation` for window timing and `ManagedSurfacePresentationSmoke` for all managed surface roles. diff --git a/Sources/WaylandClient/WaylandClient.docc/SessionReadiness.md b/Sources/WaylandClient/WaylandClient.docc/SessionReadiness.md index bc15387a..ad432185 100644 --- a/Sources/WaylandClient/WaylandClient.docc/SessionReadiness.md +++ b/Sources/WaylandClient/WaylandClient.docc/SessionReadiness.md @@ -26,7 +26,7 @@ Local restoration state does not require a compositor session-management protocol. It does depend on normal window lifecycle: - create a toplevel through ``WaylandDisplay/createTopLevelWindow(configuration:)`` -- wait for the initial configure by calling ``Window/show(timeoutMilliseconds:_:)`` +- wait for the initial configure by calling `Window.show(metadata:requestPresentationFeedback:timeoutMilliseconds:_:)` - capture ``Window/restorationSnapshot`` after configure Activation is optional and capability-gated by diff --git a/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md b/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md index 831c2ead..4ffb4ecd 100644 --- a/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md +++ b/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md @@ -1,25 +1,38 @@ # Subsurfaces -Subsurfaces are managed child surfaces attached to a parent ``Window``. They are -useful for compositor-visible child planes such as popovers, drag feedback, or -framework experiments that need separate surface lifetime without exposing raw -Wayland handles. +Subsurfaces are managed child surfaces attached to a parent ``Window``. They are useful for compositor-visible child planes that need an independent buffer lifecycle while remaining positioned and stacked relative to a parent. -Use ``Window/createSubsurface(configuration:)`` when a child needs its own -surface content but remains positioned and stacked relative to a parent window. -Use a popup instead when xdg-shell transient positioning and dismissal semantics -are required. +Use ``Window/createSubsurface(configuration:)`` when a child needs its own content. Use ``Window/createPopup(configuration:)`` when xdg-shell transient placement and compositor dismissal semantics are required. -## Capability Gate +## Drawing + +``Subsurface`` uses the same direct and prepared `show` and `redraw` contract as windows and popups. It accepts ``SurfaceFrameMetadata``, returns ``SoftwarePresentationOutcome``, exposes ``Subsurface/presentationEvents``, and publishes redraw work through `DisplayEvent.redrawRequested(ManagedSurfaceIdentity)` with `ManagedSurfaceIdentity.subsurface(SubsurfaceIdentity)`. + +Subsurfaces do not wait for an initial configure. Their prepared request captures the redraw generation, authoritative geometry, and synchronization mode. Scale changes, synchronization-mode changes, explicit redraws, cancellation, close, and parent close are revalidated before drawing. + +## Synchronization + +A synchronized child transaction commits in this order: + +1. stage child metadata, damage, and attachment; +2. stage presentation success; +3. mark the child buffer busy; +4. request the child frame callback and optional presentation feedback; +5. commit the child surface; and +6. commit the parent surface. -Subsurfaces require `wl_subcompositor`. Stacking, position, and synchronization -mode requests are still subject to parent/child lifetime and compositor rules. +The parent commit is part of the same nonthrowing point of no return. Rejected, deferred, superseded, canceled, metadata-invalid, and closed attempts commit neither child nor parent. + +A desynchronized child commits only itself. Changing synchronization mode during asynchronous preparation supersedes the old request and keeps the newest generation dirty. + +## Lifecycle And Redraw + +``Subsurface/requestRedraw()`` publishes one root redraw event when work becomes eligible; it does not only set an internal flag. Parent-window close closes descendant subsurfaces, abandons reservations, terminates outstanding feedback, and releases frame callbacks and SHM resources. + +## Capability Gate -WaylandClientKit validates parent ownership, child lifecycle, stacking targets, and -presentation state. Frameworks own layout, z-order policy, and deciding whether -a feature should be a subsurface, popup, or content drawn into the parent -software frame. +Subsurfaces require `wl_subcompositor`. Stacking, position, and synchronization requests remain subject to parent/child lifetime and compositor rules. WaylandClientKit validates parent ownership, child lifecycle, stacking targets, and presentation state. Frameworks own layout and z-order policy. ## Example -See `SubsurfaceSmoke` in `Examples/SubsurfaceSmoke`. +See `ManagedSurfacePresentationSmoke` in `Examples/ManagedSurfacePresentationSmoke` for synchronized and desynchronized child presentation, redraw routing, feedback, and parent-close cleanup. diff --git a/Sources/WaylandClient/WaylandClient.docc/SurfaceRegionsAndDamage.md b/Sources/WaylandClient/WaylandClient.docc/SurfaceRegionsAndDamage.md index 536baeb3..b53e904b 100644 --- a/Sources/WaylandClient/WaylandClient.docc/SurfaceRegionsAndDamage.md +++ b/Sources/WaylandClient/WaylandClient.docc/SurfaceRegionsAndDamage.md @@ -4,12 +4,12 @@ Use regions when compositor-facing surface shape matters. ``SurfaceRegion`` publishes logical rectangles for input and opacity. ``SurfaceDamageRegion`` publishes logical rectangles that changed between frames. -Use ``Window/setInputRegion(_:)`` when only part of a window should receive -pointer targeting from the compositor. Use ``Window/setOpaqueRegion(_:)`` when +Use `setInputRegion(_:)` when only part of a managed surface should receive +pointer targeting from the compositor. Use `setOpaqueRegion(_:)` when rectangles are fully opaque and the compositor can optimize composition. Pass a -``SurfaceFrameMetadata`` value with non-`nil` damage to -``Window/redraw(metadata:requestPresentationFeedback:_:)`` when an already-shown -window only changed part of its surface. +``SurfaceFrameMetadata`` value with non-`nil` damage to a window, popup, or +subsurface `redraw` method when an already-shown surface changed only part of +its content. ## Capability Gate diff --git a/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md b/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md index f47fbf19..62f8b61e 100644 --- a/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md +++ b/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md @@ -4,7 +4,7 @@ Connect to a Wayland compositor from Swift and build client-side Linux GUI substrate code. WaylandClientKit's public `WaylandClient` API covers display connection lifetime, -window and popup surfaces, software rendering through shared memory, input +window, popup, and subsurface roles, software rendering through shared memory, input events, keyboard interpretation, relative pointer and pointer constraints, pointer gestures, pointer warp requests, tablet input, cursor requests, data transfer, text-input sessions, XDG activation, desktop relationship hints, @@ -31,9 +31,10 @@ targets. - ``WaylandCapabilities`` - ``ProtocolAvailability`` -### Windows And Popups +### Managed Surfaces -- +- +- - - - @@ -57,6 +58,7 @@ targets. - ``SurfaceColorRepresentation`` - ``SurfaceColorAlphaMode`` - ``SurfaceFrameMetadataError`` +- ``ManagedSurfaceIdentity`` - ``SurfaceGeometry`` - ``SurfaceScale`` - ``OutputSnapshot`` @@ -154,8 +156,10 @@ targets. ### Presentation - -- ``WindowPresentationEvents`` -- ``WindowPresentationEvent`` +- ``ManagedSurfacePresentationEvents`` +- ``ManagedSurfacePresentationEvent`` +- ``SurfacePresentationFeedback`` +- ``SurfacePresentationIdentity`` - ``PresentationFeedback`` ### Diagnostics diff --git a/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md b/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md deleted file mode 100644 index 4194310d..00000000 --- a/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md +++ /dev/null @@ -1,73 +0,0 @@ -# Window Drawing - -``Window`` exposes software drawing through ``SoftwareFrame`` and the current -``SurfaceGeometry``. WaylandClientKit owns the Wayland surface transaction, frame -callback bookkeeping, shared-memory pool selection, and presentation-feedback -requests. - -Application code draws into the frame payload and asks the window to present it. -Each software frame reports an opaque ``SoftwareFrameBufferID`` for the borrowed -SHM buffer. ``SoftwareFrame/withBuffer(_:)`` provides scoped access to XRGB8888 -bytes, stride, and geometry. The byte span is valid only inside the closure. -Use -``Window/show(metadata:requestPresentationFeedback:timeoutMilliseconds:preparing:_:)`` -and ``Window/redraw(metadata:requestPresentationFeedback:preparing:_:)`` when -expensive scene preparation should -begin after WaylandClientKit has selected the authoritative software frame -geometry and reusable buffer identity. The preparation closure receives a -``SoftwareFrameReservation`` with buffer dimensions, stride, geometry, and -opaque identity. Mutable bytes remain scoped to the final draw closure. These -operations are latest-wins transactions: after preparation resumes, -WaylandClientKit revalidates the exact reservation, window, configure, -authoritative geometry, redraw generation, and task cancellation before it -borrows mutable bytes. - -When asynchronous preparation is unnecessary, use -``Window/show(metadata:requestPresentationFeedback:timeoutMilliseconds:_:)`` or -``Window/redraw(metadata:requestPresentationFeedback:_:)``. Both simple and -prepared methods return ``SoftwarePresentationOutcome``; the result is -discardable when callers do not need the terminal disposition. - -If the prepared generation is still current, WaylandClientKit draws, requests -the frame callback and optional presentation feedback, commits the surface, and -records the generation without another suspension point. If it is no longer -current, the library discards the reservation without invoking the draw closure -or issuing frame-callback, presentation-feedback, or surface-commit requests. -The newest dirty generation remains pending and produces one replacement redraw -request when pacing and buffer availability allow it. -GPU allocation experiments remain package-internal preview code. - -## Asynchronous Preparation Outcomes - -Asynchronous software presentation returns ``SoftwarePresentationOutcome``: - -- `.presented` means the prepared frame was committed. -- `.superseded` means preparation finished after the reserved generation became - stale, or the task was canceled after preparation without throwing. The - prepared value is discarded and the newest redraw remains eligible. -- `.deferred` means no transaction began because redraw pacing or software-buffer - availability did not permit one. The preparation closure is not invoked. -- `.closed` means the window or its display closed before the frame could be - committed. The draw closure is not invoked. - -Errors thrown by preparation or drawing remain errors. The reservation is -released and eligible redraw work is republished before the original operation -error is re-thrown. - -``PopupSurface`` follows the same ownership rule as windows: it is a managed -surface, but popup placement and dismissal are governed by xdg-shell. - -Use ``Window/show(metadata:requestPresentationFeedback:timeoutMilliseconds:_:)`` -for the first frame and -``Window/redraw(metadata:requestPresentationFeedback:_:)`` for later partial -redraws. ``SurfaceFrameMetadata`` carries renderer-neutral surface metadata, -including logical ``SurfaceDamageRegion`` rectangles. WaylandClientKit validates -damage before drawing. `metadata.damage == nil` is the only full-frame -representation; later partial commits map logical damage to scaled buffer -coordinates and clip it to the surface. - -Use ``Window/setInputRegion(_:)`` and ``Window/setOpaqueRegion(_:)`` to publish -surface regions to the compositor. Input regions affect compositor targeting. -Frameworks remain responsible for hit testing. Opaque regions are compositor -optimization hints for fully opaque logical rectangles. Passing `nil` resets the -compositor default region. diff --git a/Sources/WaylandClientKitToolSupport/DocumentationStructureVerifier.swift b/Sources/WaylandClientKitToolSupport/DocumentationStructureVerifier.swift index 1a60a4d9..11530628 100644 --- a/Sources/WaylandClientKitToolSupport/DocumentationStructureVerifier.swift +++ b/Sources/WaylandClientKitToolSupport/DocumentationStructureVerifier.swift @@ -43,7 +43,8 @@ public struct DocumentationStructureVerifier { "Sources/WaylandClient/WaylandClient.docc/SurfaceRegionsAndDamage.md", "Sources/WaylandClient/WaylandClient.docc/TextInputLifecycle.md", "Sources/WaylandClient/WaylandClient.docc/WaylandClient.md", - "Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md", + "Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md", + "Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md", "Sources/WaylandGraphicsPreview/WaylandGraphicsPreview.docc/FrameLeases.md", "Sources/WaylandGraphicsPreview/WaylandGraphicsPreview.docc/GraphicsRuntimePath.md", "Sources/WaylandGraphicsPreview/WaylandGraphicsPreview.docc/ManagedGPUPreview.md", diff --git a/Sources/WaylandClientKitToolSupport/ExampleBuilder.swift b/Sources/WaylandClientKitToolSupport/ExampleBuilder.swift index a5249c5c..7fe1057b 100644 --- a/Sources/WaylandClientKitToolSupport/ExampleBuilder.swift +++ b/Sources/WaylandClientKitToolSupport/ExampleBuilder.swift @@ -18,6 +18,7 @@ public struct ExampleBuilder { "GraphicsPreviewManagedGPUClear", "IdleInhibitSmoke", "KeyboardShortcutsInhibitSmoke", + "ManagedSurfacePresentationSmoke", "ColorManagementSmoke", "OutputManagementSmoke", "OutputTopologySmoke", diff --git a/Sources/WaylandClientKitToolSupport/IdentityGeneration.swift b/Sources/WaylandClientKitToolSupport/IdentityGeneration.swift index 1b5ce2e1..4d95eb1a 100644 --- a/Sources/WaylandClientKitToolSupport/IdentityGeneration.swift +++ b/Sources/WaylandClientKitToolSupport/IdentityGeneration.swift @@ -399,6 +399,10 @@ struct IdentityPolicy: Decodable { func validateVisibility(failures: inout [String]) { guard access == .public, let auditCategory else { return } switch auditCategory { + case .compositeIdentity, .publicValueIdentity: + failures.append( + "\(type) value identities must be declared as manually audited enums" + ) case .rawProtocolIdentity, .applicationIdentity, .opaqueProtocolToken: if storageAccess != .public { failures.append( diff --git a/Sources/WaylandClientKitToolSupport/IdentityGenerationModels.swift b/Sources/WaylandClientKitToolSupport/IdentityGenerationModels.swift index a4babb44..c1e529fe 100644 --- a/Sources/WaylandClientKitToolSupport/IdentityGenerationModels.swift +++ b/Sources/WaylandClientKitToolSupport/IdentityGenerationModels.swift @@ -88,6 +88,8 @@ enum IdentityAuditCategory: String, Codable { case seatScopedIdentity = "seat-scoped identity" case opaqueProtocolToken = "opaque protocol token" case applicationIdentity = "application identity" + case compositeIdentity = "composite identity" + case publicValueIdentity = "public value identity" } struct IdentityAuditManifest: Codable { @@ -97,7 +99,7 @@ struct IdentityAuditManifest: Codable { struct IdentityAuditEntry: Codable { let type: String let category: IdentityAuditCategory - let constructor: IdentityAccess - let storage: String - let storageVisibility: IdentityAccess + let constructor: IdentityAccess? + let storage: String? + let storageVisibility: IdentityAccess? } diff --git a/Sources/WaylandClientKitToolSupport/PublicIdentityAuditor.swift b/Sources/WaylandClientKitToolSupport/PublicIdentityAuditor.swift index 8efbf636..fdd88d06 100644 --- a/Sources/WaylandClientKitToolSupport/PublicIdentityAuditor.swift +++ b/Sources/WaylandClientKitToolSupport/PublicIdentityAuditor.swift @@ -1,5 +1,8 @@ import Foundation +// The auditor deliberately keeps discovery, validation, and report rendering in +// one type so both verify and update traverse exactly the same policy path. +// swiftlint:disable:next type_body_length public struct PublicIdentityAuditor { public let repository: Repository public let fileSystem: FileSystem @@ -64,6 +67,8 @@ public struct PublicIdentityAuditor { "This file is generated from `docs/identity-categories.json` and public Swift " + "declarations. It records which identities callers may construct and which " + "stored values are public.", + "Composite and public-value enums use public cases instead of raw-value storage;", + "their constructor and storage columns are shown as `—`.", "", "Run `swift run wck identity verify --update` after reviewing an intentional " + "identity contract change.", @@ -76,55 +81,75 @@ public struct PublicIdentityAuditor { return lines.joined(separator: "\n") } + // swiftlint:disable:next function_body_length private func auditRecords( manifest: IdentityManifest, declarations: [String: IdentityDeclaration] ) throws -> [IdentityAuditRecord] { + // swiftlint:disable:next closure_body_length try manifest.identities.map { identity in guard let declaration = declarations[identity.type] else { throw ToolError("Missing public identity declaration: \(identity.type)") } - let constructor = try constructorVisibility(in: declaration.body) - let storage = try storageVisibility( - named: identity.storage, - in: declaration.body, - typeName: identity.type - ) - try validate( - identity: identity, - constructor: constructor, - storage: storage - ) - return IdentityAuditRecord( - type: identity.type, - category: identity.category, - constructor: constructor, - storage: identity.storage, - storageVisibility: storage, - source: declaration.source - ) - }.sorted { $0.type < $1.type } - } - private func validate( - identity: IdentityManifestEntry, - constructor: IdentityAccessLevel, - storage: IdentityAccessLevel - ) throws { - guard constructor == identity.constructor else { - throw ToolError( - "\(identity.type) constructor is \(constructor.rawValue), expected " - + identity.constructor.rawValue, - exitCode: ToolExitCode.data - ) - } - guard storage == identity.storageVisibility else { - throw ToolError( - "\(identity.type).\(identity.storage) is \(storage.rawValue), expected " - + identity.storageVisibility.rawValue, - exitCode: ToolExitCode.data - ) - } + switch declaration.kind { + case .structure: + guard let expectedConstructor = identity.constructor, + let storageName = identity.storage, + let expectedStorageVisibility = identity.storageVisibility + else { + throw ToolError( + "struct identity \(identity.type) requires constructor and storage policy" + ) + } + let constructor = try constructorVisibility(in: declaration.body) + let storage = try storageVisibility( + named: storageName, + in: declaration.body, + typeName: identity.type + ) + guard constructor == expectedConstructor else { + throw ToolError( + "\(identity.type) constructor is \(constructor.rawValue), expected " + + expectedConstructor.rawValue, + exitCode: ToolExitCode.data + ) + } + guard storage == expectedStorageVisibility else { + throw ToolError( + "\(identity.type).\(storageName) is \(storage.rawValue), expected " + + expectedStorageVisibility.rawValue, + exitCode: ToolExitCode.data + ) + } + return IdentityAuditRecord( + type: identity.type, + category: identity.category, + constructor: constructor, + storage: storageName, + storageVisibility: storage, + source: declaration.source + ) + case .enumeration: + guard identity.constructor == nil, + identity.storage == nil, + identity.storageVisibility == nil + else { + throw ToolError( + "enum identity \(identity.type) must not declare " + + "constructor or storage policy" + ) + } + return IdentityAuditRecord( + type: identity.type, + category: identity.category, + constructor: nil, + storage: nil, + storageVisibility: nil, + source: declaration.source + ) + } + }.sorted { $0.type < $1.type } } private func loadManifest() throws -> IdentityManifest { @@ -144,7 +169,7 @@ public struct PublicIdentityAuditor { repository.url("Sources/WaylandGraphicsPreview/Public"), ] let declarationPattern = - #"\bpublic\s+struct\s+([A-Za-z_][A-Za-z0-9_]*(?:ID|Identity|Token|Serial))\b"# + #"\bpublic\s+(struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*(?:ID|Identity|Token|Serial))\b"# let expression = try NSRegularExpression(pattern: declarationPattern) var declarations: [String: IdentityDeclaration] = [:] for root in roots { @@ -154,7 +179,8 @@ public struct PublicIdentityAuditor { let range = NSRange(source.startIndex.. String { - "| `\(record.type)` | \(record.category.rawValue) | `\(record.constructor.rawValue)` | " - + "`\(record.storage)` | `\(record.storageVisibility.rawValue)` | " + let constructor = record.constructor.map { "`\($0.rawValue)`" } ?? "—" + let storage = record.storage.map { "`\($0)`" } ?? "—" + let storageVisibility = record.storageVisibility.map { "`\($0.rawValue)`" } ?? "—" + return "| `\(record.type)` | \(record.category.rawValue) | \(constructor) | " + + "\(storage) | \(storageVisibility) | " + "`\(record.source)` |" } } @@ -246,22 +283,28 @@ private struct IdentityManifest: Decodable { private struct IdentityManifestEntry: Decodable { let type: String let category: IdentityAuditCategory - let constructor: IdentityAccessLevel - let storage: String - let storageVisibility: IdentityAccessLevel + let constructor: IdentityAccessLevel? + let storage: String? + let storageVisibility: IdentityAccessLevel? } private struct IdentityDeclaration { + let kind: IdentityDeclarationKind let body: String let source: String } +private enum IdentityDeclarationKind: String { + case structure = "struct" + case enumeration = "enum" +} + private struct IdentityAuditRecord { let type: String let category: IdentityAuditCategory - let constructor: IdentityAccessLevel - let storage: String - let storageVisibility: IdentityAccessLevel + let constructor: IdentityAccessLevel? + let storage: String? + let storageVisibility: IdentityAccessLevel? let source: String } diff --git a/Sources/WaylandGraphicsPreview/Internal/WaylandGraphicsSubmissionState.swift b/Sources/WaylandGraphicsPreview/Internal/WaylandGraphicsSubmissionState.swift index 9db70d2b..f4767184 100644 --- a/Sources/WaylandGraphicsPreview/Internal/WaylandGraphicsSubmissionState.swift +++ b/Sources/WaylandGraphicsPreview/Internal/WaylandGraphicsSubmissionState.swift @@ -2,7 +2,7 @@ import WaylandClient package enum WaylandGraphicsErrorMapper { package static func callerDrawError(from error: any Error) -> (any Error)? { - guard let drawFailure = error as? WindowSoftwareDrawFailure else { + guard let drawFailure = error as? SoftwareSurfaceDrawFailure else { return nil } diff --git a/Tests/WaylandClientKitToolTests/PublicIdentityAuditorTests.swift b/Tests/WaylandClientKitToolTests/PublicIdentityAuditorTests.swift index cb34b7a5..cfdfe074 100644 --- a/Tests/WaylandClientKitToolTests/PublicIdentityAuditorTests.swift +++ b/Tests/WaylandClientKitToolTests/PublicIdentityAuditorTests.swift @@ -43,6 +43,7 @@ struct PublicIdentityAuditorTests { encoding: .utf8 ) #expect(report.contains("| `FixtureID` | client identity | `package` |")) + #expect(report.contains("| `FixtureIdentity` | composite identity | — | — | — |")) #expect(report.contains("| `PreviewFixtureID` | client identity | `package` |")) try fixtureSource(constructor: "public").write( @@ -66,6 +67,10 @@ struct PublicIdentityAuditorTests { "storage": "rawValue", "storageVisibility": "package" }, + { + "type": "FixtureIdentity", + "category": "composite identity" + }, { "type": "PreviewFixtureID", "category": "client identity", @@ -87,6 +92,10 @@ struct PublicIdentityAuditorTests { self.rawValue = rawValue } } + + public enum FixtureIdentity { + case fixture(FixtureID) + } """ } diff --git a/Tests/WaylandClientKitToolTests/ToolingConvergenceTests.swift b/Tests/WaylandClientKitToolTests/ToolingConvergenceTests.swift index 550db03c..3d0964a3 100644 --- a/Tests/WaylandClientKitToolTests/ToolingConvergenceTests.swift +++ b/Tests/WaylandClientKitToolTests/ToolingConvergenceTests.swift @@ -189,6 +189,7 @@ struct ToolingConvergenceTests { #expect(ExampleBuilder.targets.contains("GraphicsPreviewColorMetadataSmoke")) #expect(ExampleBuilder.targets.contains("GraphicsPreviewExternalBufferSmoke")) #expect(ExampleBuilder.targets.contains("KeyboardShortcutsInhibitSmoke")) + #expect(ExampleBuilder.targets.contains("ManagedSurfacePresentationSmoke")) #expect(ExampleBuilder.targets.contains("OutputManagementSmoke")) #expect(ExampleBuilder.targets.contains("OutputTopologySmoke")) #expect(ExampleBuilder.targets.contains("PointerGesturesSmoke")) @@ -198,7 +199,7 @@ struct ToolingConvergenceTests { #expect(ExampleBuilder.targets.contains("TabletInputSmoke")) #expect(ExampleBuilder.targets.contains("ToplevelDragSmoke")) #expect(ExampleBuilder.targets.contains("WaylandClientKitDemo")) - #expect(ExampleBuilder.targets.count == 36) + #expect(ExampleBuilder.targets.count == 37) } @Test diff --git a/Tests/WaylandClientTests/Events/DisplayEventHubPopupTests.swift b/Tests/WaylandClientTests/Events/DisplayEventHubPopupTests.swift index 1cbfc816..bde07abd 100644 --- a/Tests/WaylandClientTests/Events/DisplayEventHubPopupTests.swift +++ b/Tests/WaylandClientTests/Events/DisplayEventHubPopupTests.swift @@ -27,15 +27,12 @@ struct DisplayEventHubPopupTests { @Test func popupRedrawRequestsPublishPopupTargetsOnDisplayStream() async { let hub = DisplayEventHub() - let popup = PopupLifecycleEvent( - popup: PopupID(rawValue: 7), - parentWindowID: WindowID(rawValue: 3) - ) + let popup = PopupSurfaceIdentity(PopupID(rawValue: 7)) var iterator = hub.displayEvents().makeAsyncIterator() - hub.publish(.popupRedrawRequested(popup)) + hub.publish(.redrawRequested(.popup(popup))) - await expectPopupEvent(.popupRedrawRequested(popup), from: &iterator) + await expectPopupEvent(.redrawRequested(.popup(popup)), from: &iterator) } @Test @@ -44,10 +41,7 @@ struct DisplayEventHubPopupTests { let core = DisplayCore(eventHub: hub) let popupID = PopupID(rawValue: 7) let parentWindowID = WindowID(rawValue: 3) - let expected = PopupLifecycleEvent( - popup: popupID, - parentWindowID: parentWindowID - ) + let expected = PopupSurfaceIdentity(popupID) var iterator = hub.displayEvents().makeAsyncIterator() let callbacks = core.popupEventCallbacks( @@ -56,7 +50,7 @@ struct DisplayEventHubPopupTests { ) callbacks.onRedrawRequested() - await expectPopupEvent(.popupRedrawRequested(expected), from: &iterator) + await expectPopupEvent(.redrawRequested(.popup(expected)), from: &iterator) } @Test diff --git a/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift b/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift index 8368530a..3dc33636 100644 --- a/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift +++ b/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift @@ -5,7 +5,7 @@ import Testing @Suite struct DisplayEventHubPresentationTests { @Test - func presentationEventsAreScopedToWindow() async { + func presentationEventsAreScopedToManagedSurface() async { let hub = DisplayEventHub() let expected = SurfacePresentationFeedback.presented( PresentationFeedback( @@ -17,52 +17,94 @@ struct DisplayEventHubPresentationTests { synchronizedOutput: OutputID(rawValue: 3) ) ) - var iterator = hub.windowPresentationEvents( - windowID: WindowID(rawValue: 2) + let popup: ManagedSurfaceIdentity = .popup( + PopupSurfaceIdentity(PopupID(rawValue: 2)) + ) + var iterator = hub.managedSurfacePresentationEvents( + surface: popup ).makeAsyncIterator() var displayIterator = hub.displayEvents().makeAsyncIterator() - let otherWindowEvent = WindowPresentationEvent( - windowID: WindowID(rawValue: 1), + let otherSurfaceEvent = ManagedSurfacePresentationEvent( + surface: .window(WindowID(rawValue: 1)), feedback: .discarded(SurfacePresentationIdentity(rawValue: 99)) ) - let expectedWindowEvent = WindowPresentationEvent( - windowID: WindowID(rawValue: 2), + let expectedPopupEvent = ManagedSurfacePresentationEvent( + surface: popup, feedback: expected ) - hub.publishPresentation(otherWindowEvent) - hub.publishPresentation(expectedWindowEvent) + hub.publishPresentation(otherSurfaceEvent) + hub.publishPresentation(expectedPopupEvent) do { let event = try await iterator.next() #expect(event == expected) let firstDisplayEvent = try await displayIterator.next() let secondDisplayEvent = try await displayIterator.next() - #expect(firstDisplayEvent == .presentation(otherWindowEvent)) - #expect(secondDisplayEvent == .presentation(expectedWindowEvent)) + #expect(firstDisplayEvent == .presentation(otherSurfaceEvent)) + #expect(secondDisplayEvent == .presentation(expectedPopupEvent)) } catch { Issue.record("Expected presentation event, got \(error)") } } + @Test + func unrelatedPresentationEventsDoNotConsumeSurfaceSubscriberCapacity() async throws { + let hub = DisplayEventHub( + configuration: EventStreamConfiguration( + presentationEventCapacity: try PositiveInt(1)) + ) + let selectedSurface: ManagedSurfaceIdentity = .popup( + PopupSurfaceIdentity(PopupID(rawValue: 2)) + ) + let otherSurface: ManagedSurfaceIdentity = .window(WindowID(rawValue: 1)) + let expected = SurfacePresentationFeedback.discarded( + SurfacePresentationIdentity(rawValue: 3) + ) + var iterator = hub.managedSurfacePresentationEvents( + surface: selectedSurface + ).makeAsyncIterator() + + for identity in 1...2 { + hub.publishPresentation( + ManagedSurfacePresentationEvent( + surface: otherSurface, + feedback: .discarded(SurfacePresentationIdentity(rawValue: UInt64(identity))) + ) + ) + } + hub.publishPresentation( + ManagedSurfacePresentationEvent( + surface: selectedSurface, + feedback: expected + ) + ) + + #expect(try await iterator.next() == expected) + } + @Test func presentationSubscriberOverflowUsesConfiguredCapacity() async throws { let hub = DisplayEventHub( configuration: EventStreamConfiguration( presentationEventCapacity: try PositiveInt(1)) ) - let windowID = WindowID(rawValue: 4) - var iterator = hub.windowPresentationEvents(windowID: windowID).makeAsyncIterator() + let surface: ManagedSurfaceIdentity = .subsurface( + SubsurfaceIdentity(SubsurfaceID(rawValue: 4)) + ) + var iterator = hub.managedSurfacePresentationEvents( + surface: surface + ).makeAsyncIterator() hub.publishPresentation( - WindowPresentationEvent( - windowID: windowID, + ManagedSurfacePresentationEvent( + surface: surface, feedback: .discarded(SurfacePresentationIdentity(rawValue: 1)) ) ) hub.publishPresentation( - WindowPresentationEvent( - windowID: windowID, + ManagedSurfacePresentationEvent( + surface: surface, feedback: .discarded(SurfacePresentationIdentity(rawValue: 2)) ) ) diff --git a/Tests/WaylandClientTests/Events/DisplayEventHubStreamTerminationTests.swift b/Tests/WaylandClientTests/Events/DisplayEventHubStreamTerminationTests.swift index 9adb0e2b..c9b51f3d 100644 --- a/Tests/WaylandClientTests/Events/DisplayEventHubStreamTerminationTests.swift +++ b/Tests/WaylandClientTests/Events/DisplayEventHubStreamTerminationTests.swift @@ -11,7 +11,9 @@ struct DisplayEventHubStreamTerminationTests { var inputIterator = hub.inputEvents().makeAsyncIterator() var dataTransferIterator = hub.dataTransferEvents().makeAsyncIterator() var textInputIterator = hub.textInputEvents().makeAsyncIterator() - let presentationEvents = hub.windowPresentationEvents(windowID: WindowID(rawValue: 7)) + let presentationEvents = hub.managedSurfacePresentationEvents( + surface: .window(WindowID(rawValue: 7)) + ) var presentationIterator = presentationEvents.makeAsyncIterator() var diagnosticsIterator = hub.diagnostics().makeAsyncIterator() let error = WaylandDisplayError.internalInvariantViolation( @@ -37,8 +39,8 @@ struct DisplayEventHubStreamTerminationTests { var inputIterator = hub.inputEvents().makeAsyncIterator() var textInputIterator = hub.textInputEvents().makeAsyncIterator() var dataTransferIterator = hub.dataTransferEvents().makeAsyncIterator() - var presentationIterator = hub.windowPresentationEvents( - windowID: events.presentation.windowID + var presentationIterator = hub.managedSurfacePresentationEvents( + surface: events.presentation.surface ).makeAsyncIterator() var diagnosticsIterator = hub.diagnostics().makeAsyncIterator() @@ -101,8 +103,8 @@ private struct StreamTerminationEvents { offerID: DataOfferID(rawValue: 5) ) ) - let presentation = WindowPresentationEvent( - windowID: WindowID(rawValue: 7), + let presentation = ManagedSurfacePresentationEvent( + surface: .window(WindowID(rawValue: 7)), feedback: .discarded(SurfacePresentationIdentity(rawValue: 6)) ) let diagnostic = DisplayDiagnostic( @@ -154,7 +156,7 @@ private func expectFailure( private func expectFailure( _ expectedError: WaylandDisplayError, - from iterator: inout WindowPresentationEventsIterator + from iterator: inout ManagedSurfacePresentationEventsIterator ) async { do { _ = try await iterator.next() diff --git a/Tests/WaylandClientTests/Events/DisplayEventHubTests.swift b/Tests/WaylandClientTests/Events/DisplayEventHubTests.swift index 357aad49..95eb74eb 100644 --- a/Tests/WaylandClientTests/Events/DisplayEventHubTests.swift +++ b/Tests/WaylandClientTests/Events/DisplayEventHubTests.swift @@ -11,9 +11,9 @@ struct DisplayEventHubTests { let stream = hub.displayEvents() var iterator = stream.makeAsyncIterator() - hub.publish(.redrawRequested(windowID)) + hub.publish(.redrawRequested(.window(windowID))) - await expectNext(.redrawRequested(windowID), from: &iterator) + await expectNext(.redrawRequested(.window(windowID)), from: &iterator) } @Test @@ -273,7 +273,7 @@ struct DisplayEventHubFailureTests { var inputIterator = hub.inputEvents().makeAsyncIterator() hub.publishInput(inputEvent) - hub.publish(.redrawRequested(WindowID(rawValue: 9))) + hub.publish(.redrawRequested(.window(WindowID(rawValue: 9)))) await expectNext(.input(inputEvent), from: &displayIterator) await expectNext( @@ -290,7 +290,10 @@ struct DisplayEventHubFailureTests { .inputPipelineOverflow(overflow), from: &inputIterator ) - await expectNext(.redrawRequested(WindowID(rawValue: 9)), from: &displayIterator) + await expectNext( + .redrawRequested(.window(WindowID(rawValue: 9))), + from: &displayIterator + ) } @Test diff --git a/Tests/WaylandClientTests/Events/ManagedSurfaceEventsTests.swift b/Tests/WaylandClientTests/Events/ManagedSurfaceEventsTests.swift new file mode 100644 index 00000000..785c3427 --- /dev/null +++ b/Tests/WaylandClientTests/Events/ManagedSurfaceEventsTests.swift @@ -0,0 +1,64 @@ +import Testing + +@testable import WaylandClient + +@Suite +struct ManagedSurfaceEventsTests { + @Test + func displayEventsPreserveManagedSurfaceRedrawOrder() async { + let hub = DisplayEventHub() + var iterator = hub.displayEvents().makeAsyncIterator() + let identities: [ManagedSurfaceIdentity] = [ + .window(WindowID(rawValue: 1)), + .popup(PopupSurfaceIdentity(PopupID(rawValue: 2))), + .subsurface(SubsurfaceIdentity(SubsurfaceID(rawValue: 3))), + ] + + for identity in identities { + hub.publish(.redrawRequested(identity)) + } + + do { + for identity in identities { + #expect(try await iterator.next() == .redrawRequested(identity)) + } + } catch { + Issue.record("Expected managed-surface redraw events, got \(error)") + } + } + + @Test + func presentationEventsFilterByManagedSurfaceIdentity() async { + let hub = DisplayEventHub() + let window: ManagedSurfaceIdentity = .window(WindowID(rawValue: 1)) + let popup: ManagedSurfaceIdentity = .popup( + PopupSurfaceIdentity(PopupID(rawValue: 2)) + ) + let expected = SurfacePresentationFeedback.discarded( + SurfacePresentationIdentity(rawValue: 9) + ) + var filteredIterator = hub.managedSurfacePresentationEvents( + surface: popup + ).makeAsyncIterator() + var displayIterator = hub.displayEvents().makeAsyncIterator() + let other = ManagedSurfacePresentationEvent( + surface: window, + feedback: .discarded(SurfacePresentationIdentity(rawValue: 8)) + ) + let matching = ManagedSurfacePresentationEvent( + surface: popup, + feedback: expected + ) + + hub.publishPresentation(other) + hub.publishPresentation(matching) + + do { + #expect(try await filteredIterator.next() == expected) + #expect(try await displayIterator.next() == .presentation(other)) + #expect(try await displayIterator.next() == .presentation(matching)) + } catch { + Issue.record("Expected managed-surface presentation events, got \(error)") + } + } +} diff --git a/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift b/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift new file mode 100644 index 00000000..dc482518 --- /dev/null +++ b/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift @@ -0,0 +1,132 @@ +import Testing + +@testable import WaylandClient + +@Suite +struct PopupPreparedPresentationModelTests { + private let popupID = PopupID(rawValue: 40) + private let parentWindowID = WindowID(rawValue: 20) + + @Test + func configureDuringPreparationSupersedesExactRequestAndCoalescesReplacement() throws { + var model = try activeModel() + let request = try startPresentation(in: &model) + + let configureEffects = try model.reduce( + .configureReceived(configure(serial: 2, originX: 99)) + ) + #expect(!model.isCurrentSoftwarePresentation(request)) + #expect(!configureEffects.contains(.publishRedrawRequested(lifecycleEvent))) + + let supersessionEffects = try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + #expect(supersessionEffects == [.publishRedrawRequested(lifecycleEvent)]) + } + + @Test + func explicitRedrawDuringPreparationPublishesOneReplacement() throws { + var model = try activeModel() + let request = try startPresentation(in: &model) + + let invalidationEffects = try model.reduce( + .contentInvalidated(bufferAvailability: .available) + ) + #expect(!model.isCurrentSoftwarePresentation(request)) + #expect(invalidationEffects.isEmpty) + + let repeatedInvalidationEffects = try model.reduce( + .contentInvalidated(bufferAvailability: .available) + ) + #expect(repeatedInvalidationEffects.isEmpty) + + let supersessionEffects = try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) + #expect(supersessionEffects == [.publishRedrawRequested(lifecycleEvent)]) + #expect(model.redraw.isDirty) + } + + @Test + func cancellationWithoutNewContentRepublishesEligibleWork() throws { + var model = try activeModel() + let request = try startPresentation(in: &model) + + let effects = try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) + + #expect(effects == [.publishRedrawRequested(lifecycleEvent)]) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + } + + @Test + func dismissalDuringPreparationClosesInsteadOfRepublishing() throws { + var model = try activeModel() + let request = try startPresentation(in: &model) + + let effects = try model.reduce(.compositorDismissed) + + #expect(model.isClosed) + #expect(model.presentation == .idle) + #expect(effects.contains(.publishDismissed(lifecycleEvent))) + #expect(effects.contains(.publishClosed(lifecycleEvent))) + #expect(!model.isCurrentSoftwarePresentation(request)) + } + + private var lifecycleEvent: PopupLifecycleEvent { + PopupLifecycleEvent(popup: popupID, parentWindowID: parentWindowID) + } + + private func activeModel() throws -> PopupModel { + var model = PopupModel( + id: popupID, + parentWindowID: parentWindowID, + fallbackSize: PositiveLogicalSize( + width: PositiveInt32(unchecked: 80), + height: PositiveInt32(unchecked: 40) + ) + ) + _ = try model.reduce(.initialCommitSent) + _ = try model.reduce(.configureReceived(configure(serial: 1, originX: 10))) + return model + } + + private func startPresentation(in model: inout PopupModel) throws -> PopupPresentationRequest { + let effects = try model.reduce(.redrawRequestConsumed(bufferAvailability: .available)) + let request = try #require( + effects.compactMap { effect -> PopupPresentationRequest? in + guard case .performSoftwarePresent(let request) = effect else { return nil } + return request + }.first + ) + _ = try model.reduce(.presentationStarted(request)) + #expect(model.isCurrentSoftwarePresentation(request)) + return request + } + + private func configure(serial: UInt32, originX: Int32) -> PopupConfigureSequence { + PopupConfigureSequence( + serial: serial, + placement: PopupPlacement( + origin: LogicalOffset(x: originX, y: 20), + size: PositiveLogicalSize( + width: PositiveInt32(unchecked: 100), + height: PositiveInt32(unchecked: 50) + ) + ) + ) + } +} diff --git a/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift similarity index 91% rename from Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift rename to Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift index 15027d12..4764bb49 100644 --- a/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift +++ b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift @@ -7,7 +7,7 @@ @testable import WaylandClient @Suite - struct WindowSoftwarePresentationCommitSequenceTests { + struct SoftwareSurfaceCommitSequenceTests { @Test(arguments: ManagedPresentationOperation.allCases) func pointOfNoReturnMarksBufferBusyBeforeProtocolRequests( operation: ManagedPresentationOperation @@ -15,7 +15,7 @@ var events: [CommitSequenceEvent] = [] let identity = SurfacePresentationIdentity(rawValue: 7) - let returnedIdentity = WindowSoftwarePresentationCommitSequence.perform( + let returnedIdentity = SoftwareSurfacePresentationCommitSequence.perform( stageSuccess: { events.append(.stageSuccess) }, @@ -29,8 +29,11 @@ events.append(.presentationFeedback(operation)) return identity }, - commit: { - events.append(.commit(operation)) + commitSurface: { + events.append(.commitSurface(operation)) + }, + commitFollowUp: { + events.append(.commitFollowUp(operation)) } ) @@ -41,7 +44,8 @@ .markDrawingBufferBusy, .frameCallback, .presentationFeedback(operation), - .commit(operation), + .commitSurface(operation), + .commitFollowUp(operation), ] ) } @@ -51,7 +55,7 @@ var events: [CommitSequenceEvent] = [] do { - _ = try WindowSoftwarePresentationCommitSequence.perform( + _ = try SoftwareSurfacePresentationCommitSequence.perform( stageSuccess: { events.append(.stageSuccess) throw InjectedSuccessStagingFailure() @@ -66,8 +70,11 @@ events.append(.presentationFeedback(.redraw)) return nil }, - commit: { - events.append(.commit(.redraw)) + commitSurface: { + events.append(.commitSurface(.redraw)) + }, + commitFollowUp: { + events.append(.commitFollowUp(.redraw)) } ) Issue.record("expected success staging failure") @@ -82,7 +89,7 @@ } @Suite(.serialized) - struct WindowSoftwarePresenterTests { // swiftlint:disable:this type_body_length + struct SoftwareSurfacePresenterTests { // swiftlint:disable:this type_body_length private struct RoleToken {} private struct FrameIDCaptureComplete: Error {} @@ -105,14 +112,14 @@ var didDraw = false var runtime = SurfaceRuntime(role: .toplevelWindow) var pendingFrameRegistration: FrameCallbackRegistration? - let presenter = WindowSoftwarePresenter( + let presenter = SoftwareSurfacePresenter( surface: try testSurface(pointer: 0x6A71), scaleInstallation: SurfaceScaleInstallation(), createSharedMemoryPool: { _ in requestedPool = true throw UnexpectedPoolRequest() }, - isWindowClosed: { false }, + isSurfaceClosed: { false }, onFrame: { _ = () } ) @@ -258,7 +265,7 @@ pendingFrameRegistration: &pendingFrameRegistration ) Issue.record("expected submit constraint failure") - } catch let failure as WindowSoftwarePresentationFailure { + } catch let failure as SoftwareSurfacePresentationFailure { #expect( failure.underlying as? SurfaceSubmitConstraintError == .explicitSyncUnavailable @@ -296,7 +303,7 @@ pendingFrameRegistration: &pendingFrameRegistration ) Issue.record("expected draw failure") - } catch let failure as WindowSoftwarePresentationFailure { + } catch let failure as SoftwareSurfacePresentationFailure { #expect(failure.underlying is InjectedDrawFailure) guard case .userDraw = failure.presentationError else { Issue.record("expected user draw presentation error") @@ -330,7 +337,7 @@ pendingFrameRegistration: &pendingFrameRegistration ) Issue.record("expected frame ID capture to stop drawing") - } catch let failure as WindowSoftwarePresentationFailure + } catch let failure as SoftwareSurfacePresentationFailure where failure.underlying is FrameIDCaptureComplete { // Capturing the ID during draw is enough for this identity regression. @@ -371,27 +378,14 @@ metadata: SurfaceCommitMetadata = .default, damage: SurfaceDamageRegion? = nil ) throws - -> WindowSoftwarePresentationContext + -> SoftwareSurfacePresentationContext { let geometry = try SurfaceGeometry( logicalSize: PositiveLogicalSize(width: 64, height: 48), scale: .one ) - let configure = try WindowConfigureEvent( - sequence: XDGConfigureSequence( - serial: 1, - topLevel: XDGTopLevelConfigureSuggestion( - size: TopLevelSize(width: 64, height: 48) - ) - ), - previousSize: nil, - fallbackSize: .default - ) - return WindowSoftwarePresentationContext( - request: PresentationRequest( - generation: 1, - configuration: configure.configuration - ), + return SoftwareSurfacePresentationContext( + generation: 1, geometry: geometry, submitConstraints: submitConstraints, metadata: metadata, @@ -403,12 +397,12 @@ private func softwarePresenter( surface: RawSurface, pool: RawSharedMemoryPool - ) -> WindowSoftwarePresenter { - WindowSoftwarePresenter( + ) -> SoftwareSurfacePresenter { + SoftwareSurfacePresenter( surface: surface, scaleInstallation: SurfaceScaleInstallation(), createSharedMemoryPool: { _ in pool }, - isWindowClosed: { false }, + isSurfaceClosed: { false }, onFrame: { _ = () } @@ -453,7 +447,8 @@ case markDrawingBufferBusy case frameCallback case presentationFeedback(ManagedPresentationOperation) - case commit(ManagedPresentationOperation) + case commitSurface(ManagedPresentationOperation) + case commitFollowUp(ManagedPresentationOperation) } private struct InjectedDrawFailure: Error {} diff --git a/Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift b/Tests/WaylandClientTests/Rendering/SurfaceOwnedResourceLedgerTests.swift similarity index 89% rename from Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift rename to Tests/WaylandClientTests/Rendering/SurfaceOwnedResourceLedgerTests.swift index 88b15887..747fe2ec 100644 --- a/Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift +++ b/Tests/WaylandClientTests/Rendering/SurfaceOwnedResourceLedgerTests.swift @@ -3,7 +3,7 @@ import Testing @testable import WaylandClient @Suite -struct WindowOwnedResourceLedgerTests { +struct SurfaceOwnedResourceLedgerTests { @Test func closeBeforeStartRetiresLateResourceImmediately() { let log = RetirementLog() @@ -18,7 +18,7 @@ struct WindowOwnedResourceLedgerTests { func closeDuringRetirementIsSafe() { let log = RetirementLog() let holder = LedgerHolder() - let ledger = WindowOwnedResourceLedger { resource in + let ledger = SurfaceOwnedResourceLedger { resource in log.append(resource.id) holder.ledger?.close() } @@ -84,8 +84,8 @@ struct WindowOwnedResourceLedgerTests { private func makeLedger( _ log: RetirementLog - ) -> WindowOwnedResourceLedger { - WindowOwnedResourceLedger { log.append($0.id) } + ) -> SurfaceOwnedResourceLedger { + SurfaceOwnedResourceLedger { log.append($0.id) } } } @@ -102,5 +102,5 @@ private final class RetirementLog { } private final class LedgerHolder { - var ledger: WindowOwnedResourceLedger? + var ledger: SurfaceOwnedResourceLedger? } diff --git a/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift new file mode 100644 index 00000000..31043e3b --- /dev/null +++ b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift @@ -0,0 +1,206 @@ +import Testing + +@testable import WaylandClient + +@Suite +struct SubsurfaceModelTests { + @Test + func initialPresentationCanBeReservedAndCompleted() throws { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + let geometry = try geometry(width: 80) + let request = try startPresentation(in: &model, geometry: geometry) + + #expect(model.isCurrentSoftwarePresentation(request, geometry: geometry)) + #expect( + try model.reduce( + .presentationSucceeded( + generation: request.generation, + bufferAvailability: .available + ) + ).isEmpty + ) + #expect(model.presentation == .idle) + #expect(!model.redraw.isDirty) + } + + @Test + func explicitRedrawPublishesOneRootStreamEffect() throws { + var model = try presentedModel() + + #expect( + try model.reduce(.contentInvalidated(bufferAvailability: .available)) + == [.publishRedrawRequested] + ) + #expect( + try model.reduce(.contentInvalidated(bufferAvailability: .available)).isEmpty + ) + #expect(model.redraw.isDirty) + #expect(model.redraw.hasOutstandingRedrawRequest) + } + + @Test + func presentationFeedbackFailurePreservesCommittedFramePacing() throws { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + let request = try startPresentation(in: &model, geometry: geometry(width: 80)) + + #expect( + try model.reduce( + .presentationSucceeded( + generation: request.generation, + bufferAvailability: .available + ) + ).isEmpty + ) + #expect( + try model.reduce(.contentInvalidated(bufferAvailability: .available)).isEmpty + ) + let stateBeforeFeedbackFailure = model + + #expect(try model.reduce(.presentationFeedbackFailed).isEmpty) + #expect(model == stateBeforeFeedbackFailure) + #expect( + try model.reduce(.frameBecameReady(bufferAvailability: .available)) + == [.publishRedrawRequested] + ) + } + + @Test + func scaleChangeDuringPreparationSupersedesWithoutDuplicatePublication() throws { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + let originalGeometry = try geometry(width: 80) + let replacementGeometry = try geometry(width: 96) + let request = try startPresentation(in: &model, geometry: originalGeometry) + + #expect(try model.reduce(.scaleChanged(bufferAvailability: .available)).isEmpty) + #expect(!model.isCurrentSoftwarePresentation(request, geometry: replacementGeometry)) + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested] + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + } + + @Test + func synchronizationModeChangeDuringPreparationSupersedesExactRequest() throws { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + let geometry = try geometry(width: 80) + let request = try startPresentation(in: &model, geometry: geometry) + + #expect( + try model.reduce( + .synchronizationModeChanged( + .desynchronized, + bufferAvailability: .available + ) + ).isEmpty + ) + #expect(model.synchronizationMode == .desynchronized) + #expect(!model.isCurrentSoftwarePresentation(request, geometry: geometry)) + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested] + ) + } + + @Test + func cancellationRepublishesUncommittedGeneration() throws { + var model = SubsurfaceModel(synchronizationMode: .desynchronized) + let request = try startPresentation(in: &model, geometry: geometry(width: 80)) + + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested] + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + } + + @Test + func bufferExhaustionDefersBeforePresentationStarts() throws { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + + #expect( + try model.reduce( + .redrawRequestConsumed( + geometry: geometry(width: 80), + bufferAvailability: .unavailable + ) + ).isEmpty + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isWaitingForBuffer) + #expect( + try model.reduce(.bufferBecameAvailable(bufferAvailability: .available)) + == [.publishRedrawRequested] + ) + } + + @Test(arguments: [SubsurfaceEvent.explicitClose, .parentWindowClosed]) + func closeTransitionsRetireAllRoleResources(event: SubsurfaceEvent) throws { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + let request = try startPresentation(in: &model, geometry: geometry(width: 80)) + + #expect( + try model.reduce(event) + == [.cancelFrameCallback, .retireSwapchain, .destroyRoleObjects] + ) + #expect(model.lifecycle == .closed) + #expect(model.presentation == .idle) + let closedGeometry = try geometry(width: 80) + #expect(!model.isCurrentSoftwarePresentation(request, geometry: closedGeometry)) + #expect(try model.reduce(event).isEmpty) + } + + private func presentedModel() throws -> SubsurfaceModel { + var model = SubsurfaceModel(synchronizationMode: .synchronized) + let request = try startPresentation(in: &model, geometry: geometry(width: 80)) + _ = try model.reduce( + .presentationSucceeded( + generation: request.generation, + bufferAvailability: .available + ) + ) + _ = try model.reduce(.frameBecameReady(bufferAvailability: .available)) + return model + } + + private func startPresentation( + in model: inout SubsurfaceModel, + geometry: SurfaceGeometry + ) throws -> SubsurfacePresentationRequest { + let effects = try model.reduce( + .redrawRequestConsumed( + geometry: geometry, + bufferAvailability: .available + ) + ) + let request = try #require( + effects.compactMap { effect -> SubsurfacePresentationRequest? in + guard case .performSoftwarePresent(let request) = effect else { return nil } + return request + }.first + ) + #expect(try model.reduce(.presentationStarted(request)).isEmpty) + return request + } + + private func geometry(width: Int32) throws -> SurfaceGeometry { + try SurfaceGeometry( + logicalSize: PositiveLogicalSize(width: width, height: 48), + scale: .one + ) + } +} diff --git a/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift b/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift index deeff440..6e5da850 100644 --- a/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift +++ b/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift @@ -6,6 +6,9 @@ @testable import WaylandClient + // Request-path helpers remain colocated so every raw recorder uses the same + // serialized lifecycle and teardown boundary. + // swiftlint:disable type_body_length @Suite( .enabled( if: SubsurfaceRequestTestEnvironment.isEnabled, @@ -38,8 +41,12 @@ let probe = try await installParentCommitProbe(in: display, for: window) probe.reset() - try await subsurface.show(drawSolid) + let outcome = try await subsurface.show( + requestPresentationFeedback: true, + drawSolid + ) + #expect(outcome == .presented) #expect(probe.count == 1) await subsurface.close() } @@ -54,13 +61,68 @@ let probe = try await installParentCommitProbe(in: display, for: window) probe.reset() - try await subsurface.show(drawSolid) + let outcome = try await subsurface.show(drawSolid) + + #expect(outcome == .presented) + #expect(probe.isEmpty) + await subsurface.close() + } + } + + @Test + func synchronizationModeChangeDuringPreparationSupersedesWithoutParentCommit() async throws + { + try await withSubsurfaceConnection { display, window in + let subsurface = try await window.createSubsurface( + configuration: subsurfaceConfiguration(synchronizationMode: .synchronized) + ) + let probe = try await installParentCommitProbe(in: display, for: window) + probe.reset() + let gate = SubsurfacePreparationGate() + + async let staleOutcome = subsurface.show( + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { _, _ in throw UnexpectedSubsurfaceDraw() } + ) + await gate.waitUntilSuspended() + try await subsurface.setDesynchronized() + await gate.resumePreparation() + #expect(try await staleOutcome == .superseded) #expect(probe.isEmpty) + #expect(try await subsurface.needsRedraw) await subsurface.close() } } + @Test + func preparationFailureReleasesSingleBufferReservation() async throws { + try await withSubsurfaceConnection { _, window in + let subsurface = try await window.createSubsurface( + configuration: try subsurfaceConfiguration( + bufferCount: PositiveInt(unchecked: 1) + ) + ) + + do { + _ = try await subsurface.show( + preparing: { _ -> Int in throw InjectedPreparationFailure() }, + { _, frame in drawSolid(frame) } + ) + Issue.record("expected preparation failure") + } catch is InjectedPreparationFailure { + // The original error remains observable after cancellation cleanup. + } + + #expect(try await subsurface.show(drawSolid) == .presented) + await subsurface.close() + await window.close() + } + } + @Test func setPositionCommitsParentAfterParentWasDrawn() async throws { try await withSubsurfaceConnection { display, window in @@ -287,11 +349,13 @@ } private func subsurfaceConfiguration( + bufferCount: PositiveInt = SubsurfaceConfiguration.defaultBufferCount, synchronizationMode: SubsurfaceSynchronizationMode = .synchronized ) throws -> SubsurfaceConfiguration { SubsurfaceConfiguration( position: LogicalOffset(x: 8, y: 8), size: try PositiveLogicalSize(width: 48, height: 48), + bufferCount: bufferCount, synchronizationMode: synchronizationMode ) } @@ -381,6 +445,35 @@ } } + private actor SubsurfacePreparationGate { + private var isSuspended = false + private var suspensionWaiters: [CheckedContinuation] = [] + private var resumeContinuation: CheckedContinuation? + + func suspendPreparation() async { + isSuspended = true + let waiters = suspensionWaiters + suspensionWaiters.removeAll() + for waiter in waiters { + waiter.resume() + } + await withCheckedContinuation { resumeContinuation = $0 } + } + + func waitUntilSuspended() async { + guard !isSuspended else { return } + await withCheckedContinuation { suspensionWaiters.append($0) } + } + + func resumePreparation() { + resumeContinuation?.resume() + resumeContinuation = nil + } + } + + private struct UnexpectedSubsurfaceDraw: Error {} + private struct InjectedPreparationFailure: Error {} + private enum SubsurfaceRequestTestEnvironment { static var isEnabled: Bool { let environment = ProcessInfo.processInfo.environment @@ -390,4 +483,6 @@ } } +// swiftlint:enable type_body_length + #endif diff --git a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift index 3c302614..f563e05a 100644 --- a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift +++ b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift @@ -222,8 +222,8 @@ } ) Issue.record("expected draw failure") - } catch let failure as WindowSoftwareDrawFailure { - #expect(failure.underlying is InjectedSoftwareDrawFailure) + } catch is InjectedSoftwareDrawFailure { + // The original draw error must remain observable. } } } @@ -305,7 +305,7 @@ var iterator = displayEvents.makeAsyncIterator() try await trigger() while let event = try await iterator.next() { - if event == .redrawRequested(window.id) { + if event == .redrawRequested(.window(window.id)) { return event } } diff --git a/Tests/WaylandGraphicsPreviewAPITests/WaylandGraphicsSubmissionFailureTests.swift b/Tests/WaylandGraphicsPreviewAPITests/WaylandGraphicsSubmissionFailureTests.swift index 0f12835f..39ce99c6 100644 --- a/Tests/WaylandGraphicsPreviewAPITests/WaylandGraphicsSubmissionFailureTests.swift +++ b/Tests/WaylandGraphicsPreviewAPITests/WaylandGraphicsSubmissionFailureTests.swift @@ -145,7 +145,7 @@ struct WaylandGraphicsSubmissionFailureTests { @Test func submitSoftwarePreservesCallerDrawErrorCause() throws { let original = InjectedDrawFailure() - let wrapped = WindowSoftwareDrawFailure(underlying: original) + let wrapped = SoftwareSurfaceDrawFailure(underlying: original) let extracted = try #require( WaylandGraphicsErrorMapper.callerDrawError(from: wrapped) @@ -447,7 +447,7 @@ actor FakeManagedGraphicsWindow: WaylandGraphicsManagedWindow { recordedSubmitConstraints.append(submitConstraints) if remainingShowDrawFailures > 0 { remainingShowDrawFailures -= 1 - throw WindowSoftwareDrawFailure(underlying: InjectedDrawFailure()) + throw SoftwareSurfaceDrawFailure(underlying: InjectedDrawFailure()) } } diff --git a/docs/compositor-matrix.md b/docs/compositor-matrix.md index 75286e4b..ea421f7f 100644 --- a/docs/compositor-matrix.md +++ b/docs/compositor-matrix.md @@ -43,6 +43,7 @@ swift run --package-path Examples TabletInputSmoke -- --auto-close --print-summa swift run --package-path Examples CompositorSessionSmoke -- --auto-close --print-summary swift run --package-path Examples TextInputSmoke -- --auto-close --print-summary swift run --package-path Examples OutputTopologySmoke -- --auto-close --print-summary +swift run --package-path Examples ManagedSurfacePresentationSmoke swift run --package-path Examples GPUPreviewSmokeClient swift run --package-path Examples GPUPreviewSmokeClient -- --sync prefer-explicit --pacing fifo swift run GraphicsPreviewManagedGPUClear -- --metadata prefer --content-type game --presentation-hint async --auto-close --print-summary @@ -68,7 +69,8 @@ example or manual probe has been run: | partial damage | `DamageRegionSmoke` logical and mapped damage logs | | subsurface creation | `SubsurfaceSmoke` child creation and cleanup | | subsurface positioning | `SubsurfaceSmoke` movement logs | -| subsurface sync/desync | `SubsurfaceSmoke` mode logs | +| subsurface sync/desync | `ManagedSurfacePresentationSmoke` child-then-parent and child-only presentation logs | +| managed surface presentation | `ManagedSurfacePresentationSmoke` window, popup, synchronized/desynchronized subsurface, redraw routing, feedback, and cleanup | | custom cursor image | `CustomCursorSmoke` custom/hidden/theme transitions | | cursor animation | `CursorAnimationSmoke` animated/theme/hidden/static/default transitions | | cursor scale policy | `CursorPolicySmoke` focused-output cursor scale logs | @@ -112,6 +114,19 @@ active. | KDE / KWin | KDE / plasma session, 2026-06-09 plus manual pointer/serial/data-transfer/managed-GPU-resize addendum on 2026-06-11 | `wayland-info`: dmabuf v5, linux-drm-syncobj v1, FIFO v1, presentation v2, text-input v3 v1, cursor-shape v2, pointer constraints v1, relative pointer v1, top-level icon v1, idle inhibit v1, system bell v1, xdg activation v1, color metadata advertised, commit timing unavailable | `swift run wck smoke live`, `swift run wck smoke integration`, `swift run wck smoke gpu-preview`, and individual auto-close feature examples passed | live integration smoke passed, `swift run wck ci check` was attempted but hung after building `wck` with no child process/output | `GPUPreviewSmokeClient` and `GraphicsPreviewManagedGPUClear` reported active managed GPU submission | Active GPU presentation and managed GPU resize/reconfigure are proven for clear-frame submission on this run. Manual pointer lock/confine with relative motion is proven. Manual data-transfer drag-source/drop/read/finish is proven. Manual serial move/window-menu requests are proven, serial resize is proven through the managed GPU resize run and drag-source serial is proven through `DataTransferSmoke`. | | Sway / wlroots | nested Sway/wlroots under KDE/Plasma, 2026-06-09 | `wayland-info`: dmabuf v4, linux-drm-syncobj v1, presentation v2, text-input v3 v1, cursor-shape v1, pointer constraints v1, relative pointer v1, idle inhibit v1, xdg activation v1, content type/alpha/tearing metadata advertised, FIFO/color management/color representation/top-level icon/system bell unavailable | `swift run wck smoke live`, `swift run wck smoke integration`, and `swift run wck smoke gpu-preview` passed inside nested Sway | nested integration smoke passed | `GPUPreviewSmokeClient` and `GraphicsPreviewManagedGPUClear` reported active managed GPU submission | Nested wlroots evidence. Active GPU clear-frame submission was proven at 96x96, full bare-metal Sway evidence is still desirable. | +## Managed Surface Presentation Evidence + +| Compositor | Date | Command | Result | +| ---------- | ---- | ------- | ------ | +| Weston 15.0.0 headless | 2026-07-28 | `ManagedSurfacePresentationSmoke` on the headless `wck-managed` socket | PASS: window, popup, synchronized subsurface, and desynchronized subsurface presented; all four produced presentation feedback; popup and both subsurfaces routed redraw events; popup explicit close and parent-close cascade cleaned up. | +| KDE / KWin 6.6.6 (Plasma 6.6.6) | 2026-07-28 | `ManagedSurfacePresentationSmoke --skip-feedback --skip-redraw-routing` on `wayland-0` | PASS: popup-specific desktop presentation, synchronized and desynchronized subsurface presentation, popup explicit close, and parent-close cascade. KWin's bounded run used the compositor-evidence flags because feedback and paced replacement redraws did not complete inside the 20-second probe; those paths remain covered by Weston and public integration tests. | + +`ManagedSurfacePresentationSmoke --interactive-dismissal` waits for a compositor +`popup_done` event after prompting for an outside click. The automated evidence +rows above use explicit popup close; interactive dismissal can be recorded as an +additional desktop observation without changing the bounded evidence mode that +uses `--skip-feedback --skip-redraw-routing`. + ## External Graphics Buffer Evidence These rows record the pre-reuse `GraphicsPreviewExternalBufferSmoke` evidence diff --git a/docs/documentation-symbol-coverage.json b/docs/documentation-symbol-coverage.json index 31daaa17..2a1409ee 100644 --- a/docs/documentation-symbol-coverage.json +++ b/docs/documentation-symbol-coverage.json @@ -1,8 +1,8 @@ { "products" : { "WaylandClient" : { - "documented" : 176, - "eligible" : 1547 + "documented" : 183, + "eligible" : 1552 }, "WaylandGraphicsPreview" : { "documented" : 32, diff --git a/docs/identity-categories.json b/docs/identity-categories.json index 16264800..91ae80e8 100644 --- a/docs/identity-categories.json +++ b/docs/identity-categories.json @@ -4,13 +4,16 @@ {"category":"opaque protocol token","constructor":"public","storage":"value","storageVisibility":"public","type":"ActivationToken"}, {"category":"public projection","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"ClipboardOfferIdentity"}, {"category":"public projection","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"ClipboardSourceIdentity"}, + {"category":"composite identity","type":"DataTransferOfferIdentity"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"DiagnosticID"}, {"category":"public projection","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"DragOfferIdentity"}, {"category":"public projection","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"DragSourceIdentity"}, + {"category":"public value identity","type":"EventStreamIdentity"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"ForeignToplevelID"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"IdleInhibitorID"}, {"category":"raw protocol identity","constructor":"public","storage":"rawValue","storageVisibility":"public","type":"InputSerial"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"KeyboardShortcutsInhibitorID"}, + {"category":"composite identity","type":"ManagedSurfaceIdentity"}, {"category":"raw protocol identity","constructor":"public","storage":"rawValue","storageVisibility":"public","type":"OutputID"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"OutputManagementHeadID"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"OutputManagementModeID"}, @@ -37,6 +40,7 @@ {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"WaylandGraphicsExternalSyncTimelineID"}, {"category":"raw protocol identity","constructor":"public","storage":"rawValue","storageVisibility":"public","type":"WaylandProtocolObjectID"}, {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"WindowDialogID"}, - {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"WindowID"} + {"category":"client identity","constructor":"package","storage":"rawValue","storageVisibility":"package","type":"WindowID"}, + {"category":"public value identity","type":"WindowStateToken"} ] } diff --git a/docs/identity-visibility.md b/docs/identity-visibility.md index 95bb678f..a75ab5f9 100644 --- a/docs/identity-visibility.md +++ b/docs/identity-visibility.md @@ -1,6 +1,8 @@ # Public identity visibility This file is generated from `docs/identity-categories.json` and public Swift declarations. It records which identities callers may construct and which stored values are public. +Composite and public-value enums use public cases instead of raw-value storage; +their constructor and storage columns are shown as `—`. Run `swift run wck identity verify --update` after reviewing an intentional identity contract change. @@ -10,13 +12,16 @@ Run `swift run wck identity verify --update` after reviewing an intentional iden | `ActivationToken` | opaque protocol token | `public` | `value` | `public` | `Sources/WaylandClient/Public/Activation/ActivationDomainTypes.swift` | | `ClipboardOfferIdentity` | public projection | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `ClipboardSourceIdentity` | public projection | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | +| `DataTransferOfferIdentity` | composite identity | — | — | — | `Sources/WaylandClient/Public/DataTransfer/DataTransferDomainTypes.swift` | | `DiagnosticID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `DragOfferIdentity` | public projection | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `DragSourceIdentity` | public projection | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | +| `EventStreamIdentity` | public value identity | — | — | — | `Sources/WaylandClient/Public/Display/DisplayEvent.swift` | | `ForeignToplevelID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `IdleInhibitorID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `InputSerial` | raw protocol identity | `public` | `rawValue` | `public` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `KeyboardShortcutsInhibitorID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | +| `ManagedSurfaceIdentity` | composite identity | — | — | — | `Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift` | | `OutputID` | raw protocol identity | `public` | `rawValue` | `public` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `OutputManagementHeadID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `OutputManagementModeID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | @@ -44,3 +49,4 @@ Run `swift run wck identity verify --update` after reviewing an intentional iden | `WaylandProtocolObjectID` | raw protocol identity | `public` | `rawValue` | `public` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `WindowDialogID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | | `WindowID` | client identity | `package` | `rawValue` | `package` | `Sources/WaylandClient/Public/Generated/GeneratedPublicIdentityDeclarations.swift` | +| `WindowStateToken` | public value identity | — | — | — | `Sources/WaylandClient/Public/Window/WindowStateToken.swift` | diff --git a/docs/public-api-audit.md b/docs/public-api-audit.md index c5d5681b..e4e31e41 100644 --- a/docs/public-api-audit.md +++ b/docs/public-api-audit.md @@ -26,6 +26,7 @@ Intentionally public: - `WaylandDisplay` - `Window` - `PopupSurface` +- `Subsurface` - `WindowConfiguration` - `WindowStateSnapshot` - `WindowRestorationSnapshot` @@ -35,6 +36,8 @@ Intentionally public: - `PopupPositioner` - `PopupPlacement` - `PopupLifecycleEvent` +- `SubsurfaceConfiguration` +- `SubsurfaceIdentity` - `SurfaceScale` - `SurfaceGeometry` - `SoftwareFrameBufferID` @@ -54,12 +57,13 @@ Intentionally public: - `SurfaceDamageRegion` - `SurfacePresentationIdentity` - `SurfacePresentationFeedback` +- `ManagedSurfaceIdentity` - `PresentationFeedback` - `PresentationTimestamp` - `PresentationSequence` - `PresentationFeedbackFlags` -- `WindowPresentationEvent` -- `WindowPresentationEvents` +- `ManagedSurfacePresentationEvent` +- `ManagedSurfacePresentationEvents` - `DisplayEvent` - `EventStreamConfiguration` - `EventStreamIdentity` @@ -180,7 +184,7 @@ Current user-facing contract: - Display connection, window creation and close, request-redraw, software XRGB8888 drawing, basic pointer/keyboard/touch events, interpreted keyboard payloads, server-side decoration negotiation, scale-aware window geometry, - popup surfaces, restoration snapshots, presentation feedback, regular + popup and subsurface roles, restoration snapshots, presentation feedback, regular clipboard selection, primary selection, receive-side and source-side drag-and-drop data transfer, drag icon surfaces, xdg activation, relative pointer, pointer lock/confine, pointer warp, tablet input facts, cursor @@ -219,21 +223,27 @@ Current user-facing contract: spans, stride, and geometry without exposing raw Wayland or SHM handles. `SoftwareFrameReservation` lets async preparation observe the selected software buffer identity and geometry before the final scoped draw borrow. - `Window.show` and `Window.redraw` each expose one simple and one prepared - method. All four accept `SurfaceFrameMetadata`, return a discardable - `SoftwarePresentationOutcome`, and provide the same atomic presentation- - feedback option. Explicitly requested unsupported surface metadata throws + `Window`, `PopupSurface`, and `Subsurface` each expose one simple and one + prepared `show`, plus one simple and one prepared `redraw`. All twelve methods + accept `SurfaceFrameMetadata`, return a discardable `SoftwarePresentationOutcome`, + and provide the same atomic presentation-feedback option. Explicitly requested + unsupported surface metadata throws `SurfaceFrameMetadataError` before drawing or issuing any Wayland request for that presentation attempt. Prepared calls are latest-wins transactions. After preparation resumes, the - library revalidates the exact reservation, window, configure, authoritative - geometry, redraw generation, and task cancellation. Supersession is resolved + library revalidates the exact reservation, role lifecycle, parent lifecycle, + configure or synchronization mode, authoritative geometry, redraw generation, + and task cancellation. Supersession is resolved before final metadata-support revalidation, so stale attempts still return `.superseded` if protocol capabilities change while preparation is suspended. A stale attempt is discarded without drawing, requesting a frame callback or presentation feedback, or committing the surface. Its replacement redraw is normal pacing. Presentation feedback requested by these APIs belongs to the - same eventual surface commit. + same eventual surface commit. A synchronized subsurface commits child then + parent inside one nonthrowing point of no return; rejected attempts commit neither. +- Managed surface redraw events use `DisplayEvent.redrawRequested(ManagedSurfaceIdentity)`. + Presentation feedback uses `ManagedSurfacePresentationEvent` on the root stream, + while each handle exposes a filtered `ManagedSurfacePresentationEvents` sequence. - Window sizes are logical surface sizes. `SurfaceGeometry` records the logical size, buffer-pixel size, and exact `SurfaceScale` used by the current SHM frame. @@ -487,7 +497,7 @@ Current preview contract: surface intersection. - Presentation feedback policy can request feedback when available or require it before creating a managed backing. Feedback observations still arrive on - `WindowPresentationEvents`, frame submission results only report whether + `ManagedSurfacePresentationEvents`, frame submission results only report whether feedback was requested for that submit, not whether it was later observed. - Downstream code that wants this boundary imports `WaylandGraphicsPreview` explicitly, importing `WaylandClient` alone does not opt into renderer-facing @@ -522,9 +532,9 @@ Notes: terminate with a closed-backing result. - `PopupSurface` is the public popup handle. Popup lifecycle display events carry the popup identity and parent window identity. -- `WindowPresentationEvents` is a public async sequence for presentation - feedback requested through a managed window. A discarded result is distinct - from a presented result with timestamps and feedback flags. +- `ManagedSurfacePresentationEvents` is the public filtered async sequence for + feedback requested through a managed window, popup, or subsurface. A discarded + result is distinct from a presented result with timestamps and feedback flags. - Every public event sequence creates an independent broker subscription in `makeAsyncIterator()`. Buffering begins at iterator creation; copied sequences do not split one queue, and cancellation or overflow remains local to one diff --git a/docs/public-api-baseline.md b/docs/public-api-baseline.md index 93602f5b..3c59c380 100644 --- a/docs/public-api-baseline.md +++ b/docs/public-api-baseline.md @@ -333,9 +333,8 @@ s:13WaylandClient12DisplayEventO13outputChangedyAcA14OutputSnapshotVcACmF swift. s:13WaylandClient12DisplayEventO13outputRemovedyAcA8OutputIDVcACmF swift.enum.case DisplayEvent.outputRemoved(_:) case outputRemoved(OutputID) - s:13WaylandClient12DisplayEventO11popupClosedyAcA014PopupLifecycleD0VcACmF swift.enum.case DisplayEvent.popupClosed(_:) case popupClosed(PopupLifecycleEvent) - s:13WaylandClient12DisplayEventO14popupDismissedyAcA014PopupLifecycleD0VcACmF swift.enum.case DisplayEvent.popupDismissed(_:) case popupDismissed(PopupLifecycleEvent) - -s:13WaylandClient12DisplayEventO20popupRedrawRequestedyAcA014PopupLifecycleD0VcACmF swift.enum.case DisplayEvent.popupRedrawRequested(_:) case popupRedrawRequested(PopupLifecycleEvent) - -s:13WaylandClient12DisplayEventO12presentationyAcA018WindowPresentationD0VcACmF swift.enum.case DisplayEvent.presentation(_:) case presentation(WindowPresentationEvent) - -s:13WaylandClient12DisplayEventO15redrawRequestedyAcA8WindowIDVcACmF swift.enum.case DisplayEvent.redrawRequested(_:) case redrawRequested(WindowID) - +s:13WaylandClient12DisplayEventO12presentationyAcA026ManagedSurfacePresentationD0VcACmF swift.enum.case DisplayEvent.presentation(_:) case presentation(ManagedSurfacePresentationEvent) - +s:13WaylandClient12DisplayEventO15redrawRequestedyAcA22ManagedSurfaceIdentityOcACmF swift.enum.case DisplayEvent.redrawRequested(_:) case redrawRequested(ManagedSurfaceIdentity) - s:13WaylandClient12DisplayEventO9textInputyAcA04TextfD0OcACmF swift.enum.case DisplayEvent.textInput(_:) case textInput(TextInputEvent) - s:13WaylandClient12DisplayEventO20windowCloseRequestedyAcA8WindowIDVcACmF swift.enum.case DisplayEvent.windowCloseRequested(_:) case windowCloseRequested(WindowID) - s:13WaylandClient12DisplayEventO12windowClosedyAcA8WindowIDVcACmF swift.enum.case DisplayEvent.windowClosed(_:) case windowClosed(WindowID) - @@ -929,6 +928,23 @@ s:13WaylandClient8MIMETypeV9plainTextACvpZ swift.type.property MIMEType.plainTex s:13WaylandClient8MIMETypeV13plainTextUTF8ACvpZ swift.type.property MIMEType.plainTextUTF8 static let plainTextUTF8: MIMEType - s:13WaylandClient8MIMETypeV8rawValueSSvp swift.property MIMEType.rawValue let rawValue: String - s:13WaylandClient8MIMETypeV7uriListACvpZ swift.type.property MIMEType.uriList static let uriList: MIMEType - +s:13WaylandClient22ManagedSurfaceIdentityO swift.enum ManagedSurfaceIdentity enum ManagedSurfaceIdentity - +s:13WaylandClient22ManagedSurfaceIdentityO5popupyAcA05PopupdE0VcACmF swift.enum.case ManagedSurfaceIdentity.popup(_:) case popup(PopupSurfaceIdentity) - +s:13WaylandClient22ManagedSurfaceIdentityO10subsurfaceyAcA010SubsurfaceE0VcACmF swift.enum.case ManagedSurfaceIdentity.subsurface(_:) case subsurface(SubsurfaceIdentity) - +s:13WaylandClient22ManagedSurfaceIdentityO6windowyAcA8WindowIDVcACmF swift.enum.case ManagedSurfaceIdentity.window(_:) case window(WindowID) - +s:13WaylandClient31ManagedSurfacePresentationEventV swift.struct ManagedSurfacePresentationEvent struct ManagedSurfacePresentationEvent - +s:13WaylandClient31ManagedSurfacePresentationEventV8feedbackAA0dE8FeedbackOvp swift.property ManagedSurfacePresentationEvent.feedback let feedback: SurfacePresentationFeedback - +s:13WaylandClient31ManagedSurfacePresentationEventV7surface8feedbackAcA0cD8IdentityO_AA0dE8FeedbackOtcfc swift.init ManagedSurfacePresentationEvent.init(surface:feedback:) init(surface eventSurface: ManagedSurfaceIdentity, feedback eventFeedback: SurfacePresentationFeedback) - +s:13WaylandClient31ManagedSurfacePresentationEventV7surfaceAA0cD8IdentityOvp swift.property ManagedSurfacePresentationEvent.surface let surface: ManagedSurfaceIdentity - +s:13WaylandClient32ManagedSurfacePresentationEventsV swift.struct ManagedSurfacePresentationEvents struct ManagedSurfacePresentationEvents - +s:13WaylandClient32ManagedSurfacePresentationEventsV7Elementa swift.typealias ManagedSurfacePresentationEvents.Element typealias Element = SurfacePresentationFeedback - +s:13WaylandClient32ManagedSurfacePresentationEventsV7Failurea swift.typealias ManagedSurfacePresentationEvents.Failure typealias Failure = WaylandDisplayError - +s:13WaylandClient32ManagedSurfacePresentationEventsV17makeAsyncIteratorAA0cdefI0VyF swift.method ManagedSurfacePresentationEvents.makeAsyncIterator() func makeAsyncIterator() -> ManagedSurfacePresentationEventsIterator - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV swift.struct ManagedSurfacePresentationEventsIterator struct ManagedSurfacePresentationEventsIterator - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV7Elementa swift.typealias ManagedSurfacePresentationEventsIterator.Element typealias Element = SurfacePresentationFeedback - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV7Failurea swift.typealias ManagedSurfacePresentationEventsIterator.Failure typealias Failure = WaylandDisplayError - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV4nextAA0dE8FeedbackOSgyYaAA0A12DisplayErrorOYKF swift.method ManagedSurfacePresentationEventsIterator.next() mutating func next() async throws(WaylandDisplayError) -> SurfacePresentationFeedback? - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV4next9isolationAA0dE8FeedbackOSgScA_pSgYi_tYaAA0A12DisplayErrorOYKF swift.method ManagedSurfacePresentationEventsIterator.next(isolation:) mutating func next(isolation actor: isolated (any Actor)?) async throws(WaylandDisplayError) -> SurfacePresentationFeedback? - s:13WaylandClient12MillisecondsV swift.struct Milliseconds struct Milliseconds - s:13WaylandClient12MillisecondsV1loiySbAC_ACtFZ swift.func.op Milliseconds.<(_:_:) static func < (lhs: Milliseconds, rhs: Milliseconds) -> Bool - s:13WaylandClient12MillisecondsV11descriptionSSvp swift.property Milliseconds.description var description: String { get } - @@ -1303,11 +1319,14 @@ s:13WaylandClient12PopupSurfaceV8isClosedSbvp swift.property PopupSurface.isClos s:13WaylandClient12PopupSurfaceV11needsRedrawSbvp swift.property PopupSurface.needsRedraw var needsRedraw: Bool { get async throws } - s:13WaylandClient12PopupSurfaceV14parentWindowIDAA0fG0Vvp swift.property PopupSurface.parentWindowID let parentWindowID: WindowID - s:13WaylandClient12PopupSurfaceV9placementAA0C9PlacementVvp swift.property PopupSurface.placement var placement: PopupPlacement { get async throws } - -s:13WaylandClient12PopupSurfaceV6redrawyyyAA13SoftwareFrameVYbKXEYaKF swift.method PopupSurface.redraw(_:) func redraw(_ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - +s:13WaylandClient12PopupSurfaceV18presentationEventsAA07Managedd12PresentationF0Vvp swift.property PopupSurface.presentationEvents var presentationEvents: ManagedSurfacePresentationEvents { get } - +s:13WaylandClient12PopupSurfaceV6redraw8metadata27requestPresentationFeedback_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_SbyAA0jL0VYbKXEtYaKF swift.method PopupSurface.redraw(metadata:requestPresentationFeedback:_:) @discardableResult func redraw(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} +s:13WaylandClient12PopupSurfaceV6redraw8metadata27requestPresentationFeedback9preparing_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_SbxAA0kM11ReservationVYaYbKYCXEyx_AA0kM0VtYbKXEtYaKs8SendableRzlF swift.method PopupSurface.redraw(metadata:requestPresentationFeedback:preparing:_:) @discardableResult func redraw(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome where Prepared : Sendable {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"},"swiftGenerics":{"constraints":[{"kind":"conformance","lhs":"Prepared","rhs":"Sendable","rhsPrecise":"s:s8SendableP"}],"parameters":[{"depth":0,"index":0,"name":"Prepared"}]}} s:13WaylandClient12PopupSurfaceV13requestRedrawyyYaKF swift.method PopupSurface.requestRedraw() func requestRedraw() async throws - s:13WaylandClient12PopupSurfaceV14setInputRegionyyAA0dG0VSgYaKF swift.method PopupSurface.setInputRegion(_:) func setInputRegion(_ region: SurfaceRegion?) async throws - s:13WaylandClient12PopupSurfaceV15setOpaqueRegionyyAA0dG0VSgYaKF swift.method PopupSurface.setOpaqueRegion(_:) func setOpaqueRegion(_ region: SurfaceRegion?) async throws - -s:13WaylandClient12PopupSurfaceV4show19timeoutMilliseconds_ys5Int32V_yAA13SoftwareFrameVYbKXEtYaKF swift.method PopupSurface.show(timeoutMilliseconds:_:) func show(timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - +s:13WaylandClient12PopupSurfaceV4show8metadata27requestPresentationFeedback19timeoutMilliseconds_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_Sbs5Int32VyAA0lN0VYbKXEtYaKF swift.method PopupSurface.show(metadata:requestPresentationFeedback:timeoutMilliseconds:_:) @discardableResult func show(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} +s:13WaylandClient12PopupSurfaceV4show8metadata27requestPresentationFeedback19timeoutMilliseconds9preparing_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_Sbs5Int32VxAA0mO11ReservationVYaYbKYCXEyx_AA0mO0VtYbKXEtYaKs8SendableRzlF swift.method PopupSurface.show(metadata:requestPresentationFeedback:timeoutMilliseconds:preparing:_:) @discardableResult func show(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome where Prepared : Sendable {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"},"swiftGenerics":{"constraints":[{"kind":"conformance","lhs":"Prepared","rhs":"Sendable","rhsPrecise":"s:s8SendableP"}],"parameters":[{"depth":0,"index":0,"name":"Prepared"}]}} s:13WaylandClient20PopupSurfaceIdentityV swift.struct PopupSurfaceIdentity struct PopupSurfaceIdentity - s:13WaylandClient20PopupSurfaceIdentityV11descriptionSSvp swift.property PopupSurfaceIdentity.description var description: String { get } - s:13WaylandClient11PositiveIntV swift.struct PositiveInt struct PositiveInt - @@ -1513,16 +1532,17 @@ s:13WaylandClient10SubsurfaceV11needsRedrawSbvp swift.property Subsurface.needsR s:13WaylandClient10SubsurfaceV14parentWindowIDAA0eF0Vvp swift.property Subsurface.parentWindowID let parentWindowID: WindowID - s:13WaylandClient10SubsurfaceV10placeAboveyyACYaKF swift.method Subsurface.placeAbove(_:) func placeAbove(_ sibling: Subsurface) async throws - s:13WaylandClient10SubsurfaceV10placeBelowyyACYaKF swift.method Subsurface.placeBelow(_:) func placeBelow(_ sibling: Subsurface) async throws - -s:13WaylandClient10SubsurfaceV6redrawyyyAA13SoftwareFrameVYbKXEYaKF swift.method Subsurface.redraw(_:) func redraw(_ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - -s:13WaylandClient10SubsurfaceV6redraw6damage_yAA19SurfaceDamageRegionVSg_yAA13SoftwareFrameVYbKXEtYaKF swift.method Subsurface.redraw(damage:_:) func redraw(damage: SurfaceDamageRegion?, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - +s:13WaylandClient10SubsurfaceV18presentationEventsAA026ManagedSurfacePresentationE0Vvp swift.property Subsurface.presentationEvents var presentationEvents: ManagedSurfacePresentationEvents { get } - +s:13WaylandClient10SubsurfaceV6redraw8metadata27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbyAA0iL0VYbKXEtYaKF swift.method Subsurface.redraw(metadata:requestPresentationFeedback:_:) @discardableResult func redraw(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} +s:13WaylandClient10SubsurfaceV6redraw8metadata27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbxAA0jM11ReservationVYaYbKYCXEyx_AA0jM0VtYbKXEtYaKs8SendableRzlF swift.method Subsurface.redraw(metadata:requestPresentationFeedback:preparing:_:) @discardableResult func redraw(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, preparing: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome where Prepared : Sendable {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"},"swiftGenerics":{"constraints":[{"kind":"conformance","lhs":"Prepared","rhs":"Sendable","rhsPrecise":"s:s8SendableP"}],"parameters":[{"depth":0,"index":0,"name":"Prepared"}]}} s:13WaylandClient10SubsurfaceV13requestRedrawyyYaKF swift.method Subsurface.requestRedraw() func requestRedraw() async throws - s:13WaylandClient10SubsurfaceV17setDesynchronizedyyYaKF swift.method Subsurface.setDesynchronized() func setDesynchronized() async throws - s:13WaylandClient10SubsurfaceV14setInputRegionyyAA07SurfaceF0VSgYaKF swift.method Subsurface.setInputRegion(_:) func setInputRegion(_ region: SurfaceRegion?) async throws - s:13WaylandClient10SubsurfaceV15setOpaqueRegionyyAA07SurfaceF0VSgYaKF swift.method Subsurface.setOpaqueRegion(_:) func setOpaqueRegion(_ region: SurfaceRegion?) async throws - s:13WaylandClient10SubsurfaceV11setPositionyyAA13LogicalOffsetVYaKF swift.method Subsurface.setPosition(_:) func setPosition(_ position: LogicalOffset) async throws - s:13WaylandClient10SubsurfaceV15setSynchronizedyyYaKF swift.method Subsurface.setSynchronized() func setSynchronized() async throws - -s:13WaylandClient10SubsurfaceV4showyyyAA13SoftwareFrameVYbKXEYaKF swift.method Subsurface.show(_:) func show(_ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - -s:13WaylandClient10SubsurfaceV4show6damage_yAA19SurfaceDamageRegionVSg_yAA13SoftwareFrameVYbKXEtYaKF swift.method Subsurface.show(damage:_:) func show(damage: SurfaceDamageRegion?, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - +s:13WaylandClient10SubsurfaceV4show8metadata27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbyAA0iL0VYbKXEtYaKF swift.method Subsurface.show(metadata:requestPresentationFeedback:_:) @discardableResult func show(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} +s:13WaylandClient10SubsurfaceV4show8metadata27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbxAA0jM11ReservationVYaYbKYCXEyx_AA0jM0VtYbKXEtYaKs8SendableRzlF swift.method Subsurface.show(metadata:requestPresentationFeedback:preparing:_:) @discardableResult func show(metadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, preparing: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome where Prepared : Sendable {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"},"swiftGenerics":{"constraints":[{"kind":"conformance","lhs":"Prepared","rhs":"Sendable","rhsPrecise":"s:s8SendableP"}],"parameters":[{"depth":0,"index":0,"name":"Prepared"}]}} s:13WaylandClient23SubsurfaceConfigurationV swift.struct SubsurfaceConfiguration struct SubsurfaceConfiguration - s:13WaylandClient23SubsurfaceConfigurationV11bufferCountAA11PositiveIntVvp swift.property SubsurfaceConfiguration.bufferCount let bufferCount: PositiveInt - s:13WaylandClient23SubsurfaceConfigurationV18defaultBufferCountAA11PositiveIntVvpZ swift.type.property SubsurfaceConfiguration.defaultBufferCount static let defaultBufferCount: PositiveInt - @@ -2143,7 +2163,7 @@ s:13WaylandClient6WindowV24inhibitKeyboardShortcuts6seatIDAA0eF9InhibitorVAA04Se s:13WaylandClient6WindowV8isClosedSbvp swift.property Window.isClosed var isClosed: Bool { get async throws } - s:13WaylandClient6WindowV11lockPointer6seatID10cursorHint6region8lifetimeAA0E10ConstraintVAA04SeatG0V_AA0E8LocationVSgAA0eL6RegionVSgAA0eL8LifetimeOtYaKF swift.method Window.lockPointer(seatID:cursorHint:region:lifetime:) func lockPointer(seatID: SeatID, cursorHint: PointerLocation? = nil, region: PointerConstraintRegion? = nil, lifetime: PointerConstraintLifetime = .oneShot) async throws -> PointerConstraint - s:13WaylandClient6WindowV11needsRedrawSbvp swift.property Window.needsRedraw var needsRedraw: Bool { get async throws } - -s:13WaylandClient6WindowV18presentationEventsAA0c12PresentationE0Vvp swift.property Window.presentationEvents var presentationEvents: WindowPresentationEvents { get } - +s:13WaylandClient6WindowV18presentationEventsAA026ManagedSurfacePresentationE0Vvp swift.property Window.presentationEvents var presentationEvents: ManagedSurfacePresentationEvents { get } - s:13WaylandClient6WindowV6redraw8metadata27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbyAA0iL0VYbKXEtYaKF swift.method Window.redraw(metadata:requestPresentationFeedback:_:) @discardableResult func redraw(metadata frameMetadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} s:13WaylandClient6WindowV6redraw8metadata27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbxAA0jM11ReservationVYaYbKYCXEyx_AA0jM0VtYbKXEtYaKs8SendableRzlF swift.method Window.redraw(metadata:requestPresentationFeedback:preparing:_:) @discardableResult func redraw(metadata frameMetadata: SurfaceFrameMetadata = .default, requestPresentationFeedback: Bool = false, preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome where Prepared : Sendable {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"},"swiftGenerics":{"constraints":[{"kind":"conformance","lhs":"Prepared","rhs":"Sendable","rhsPrecise":"s:s8SendableP"}],"parameters":[{"depth":0,"index":0,"name":"Prepared"}]}} s:13WaylandClient6WindowV15relativePointer6seatIDAA08RelativeE12SubscriptionVAA04SeatG0V_tYaKF swift.method Window.relativePointer(seatID:) func relativePointer(seatID: SeatID) async throws -> RelativePointerSubscription - @@ -2338,19 +2358,6 @@ s:13WaylandClient28WindowPresentationDiagnosticV11descriptionSSvp swift.property s:13WaylandClient28WindowPresentationDiagnosticV5errorAA0D5ErrorOvp swift.property WindowPresentationDiagnostic.error let error: PresentationError - s:13WaylandClient28WindowPresentationDiagnosticV9operation5errorAcA0cD9OperationO_AA0D5ErrorOtcfc swift.init WindowPresentationDiagnostic.init(operation:error:) init(operation diagnosticOperation: WindowPresentationOperation, error presentationError: PresentationError) - s:13WaylandClient28WindowPresentationDiagnosticV9operationAA0cD9OperationOvp swift.property WindowPresentationDiagnostic.operation let operation: WindowPresentationOperation - -s:13WaylandClient23WindowPresentationEventV swift.struct WindowPresentationEvent struct WindowPresentationEvent - -s:13WaylandClient23WindowPresentationEventV8feedbackAA07SurfaceD8FeedbackOvp swift.property WindowPresentationEvent.feedback let feedback: SurfacePresentationFeedback - -s:13WaylandClient23WindowPresentationEventV8windowID8feedbackAcA0cG0V_AA07SurfaceD8FeedbackOtcfc swift.init WindowPresentationEvent.init(windowID:feedback:) init(windowID eventWindowID: WindowID, feedback eventFeedback: SurfacePresentationFeedback) - -s:13WaylandClient23WindowPresentationEventV8windowIDAA0cG0Vvp swift.property WindowPresentationEvent.windowID let windowID: WindowID - -s:13WaylandClient24WindowPresentationEventsV swift.struct WindowPresentationEvents struct WindowPresentationEvents - -s:13WaylandClient24WindowPresentationEventsV7Elementa swift.typealias WindowPresentationEvents.Element typealias Element = SurfacePresentationFeedback - -s:13WaylandClient24WindowPresentationEventsV7Failurea swift.typealias WindowPresentationEvents.Failure typealias Failure = WaylandDisplayError - -s:13WaylandClient24WindowPresentationEventsV17makeAsyncIteratorAA0cdeH0VyF swift.method WindowPresentationEvents.makeAsyncIterator() func makeAsyncIterator() -> WindowPresentationEventsIterator - -s:13WaylandClient32WindowPresentationEventsIteratorV swift.struct WindowPresentationEventsIterator struct WindowPresentationEventsIterator - -s:13WaylandClient32WindowPresentationEventsIteratorV7Elementa swift.typealias WindowPresentationEventsIterator.Element typealias Element = SurfacePresentationFeedback - -s:13WaylandClient32WindowPresentationEventsIteratorV7Failurea swift.typealias WindowPresentationEventsIterator.Failure typealias Failure = WaylandDisplayError - -s:13WaylandClient32WindowPresentationEventsIteratorV4nextAA07SurfaceD8FeedbackOSgyYaAA0A12DisplayErrorOYKF swift.method WindowPresentationEventsIterator.next() mutating func next() async throws(WaylandDisplayError) -> SurfacePresentationFeedback? - -s:13WaylandClient32WindowPresentationEventsIteratorV4next9isolationAA07SurfaceD8FeedbackOSgScA_pSgYi_tYaAA0A12DisplayErrorOYKF swift.method WindowPresentationEventsIterator.next(isolation:) mutating func next(isolation actor: isolated (any Actor)?) async throws(WaylandDisplayError) -> SurfacePresentationFeedback? - s:13WaylandClient27WindowPresentationOperationO swift.enum WindowPresentationOperation enum WindowPresentationOperation - s:13WaylandClient27WindowPresentationOperationO11descriptionSSvp swift.property WindowPresentationOperation.description var description: String { get } - s:13WaylandClient27WindowPresentationOperationO18presentationFailedyA2CmF swift.enum.case WindowPresentationOperation.presentationFailed case presentationFailed - @@ -2725,14 +2732,15 @@ s:13WaylandClient10SubsurfaceV14setInputRegionyyAA07SurfaceF0VSgYaKF memberOf s: s:13WaylandClient10SubsurfaceV15setOpaqueRegionyyAA07SurfaceF0VSgYaKF memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV15setSynchronizedyyYaKF memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV17setDesynchronizedyyYaKF memberOf s:13WaylandClient10SubsurfaceV - +s:13WaylandClient10SubsurfaceV18presentationEventsAA026ManagedSurfacePresentationE0Vvp memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV2eeoiySbAC_ACtFZ memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV2idAA0C8IdentityVvp memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV4hash4intoys6HasherVz_tF memberOf s:13WaylandClient10SubsurfaceV - -s:13WaylandClient10SubsurfaceV4show6damage_yAA19SurfaceDamageRegionVSg_yAA13SoftwareFrameVYbKXEtYaKF memberOf s:13WaylandClient10SubsurfaceV - -s:13WaylandClient10SubsurfaceV4showyyyAA13SoftwareFrameVYbKXEYaKF memberOf s:13WaylandClient10SubsurfaceV - +s:13WaylandClient10SubsurfaceV4show8metadata27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbxAA0jM11ReservationVYaYbKYCXEyx_AA0jM0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient10SubsurfaceV - +s:13WaylandClient10SubsurfaceV4show8metadata27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbyAA0iL0VYbKXEtYaKF memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV5closeyyYaF memberOf s:13WaylandClient10SubsurfaceV - -s:13WaylandClient10SubsurfaceV6redraw6damage_yAA19SurfaceDamageRegionVSg_yAA13SoftwareFrameVYbKXEtYaKF memberOf s:13WaylandClient10SubsurfaceV - -s:13WaylandClient10SubsurfaceV6redrawyyyAA13SoftwareFrameVYbKXEYaKF memberOf s:13WaylandClient10SubsurfaceV - +s:13WaylandClient10SubsurfaceV6redraw8metadata27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbxAA0jM11ReservationVYaYbKYCXEyx_AA0jM0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient10SubsurfaceV - +s:13WaylandClient10SubsurfaceV6redraw8metadata27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA20SurfaceFrameMetadataV_SbyAA0iL0VYbKXEtYaKF memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV8geometryAA15SurfaceGeometryVvp memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV8identityAA0C8IdentityVvp memberOf s:13WaylandClient10SubsurfaceV - s:13WaylandClient10SubsurfaceV8isClosedSbvp memberOf s:13WaylandClient10SubsurfaceV - @@ -2879,13 +2887,12 @@ s:13WaylandClient12DisplayEventO conformsTo s:s8SendableP Swift.Sendable s:13WaylandClient12DisplayEventO10diagnosticyAcA0C10DiagnosticVcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO11popupClosedyAcA014PopupLifecycleD0VcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO12dataTransferyAcA04DatafD0OcACmF memberOf s:13WaylandClient12DisplayEventO - -s:13WaylandClient12DisplayEventO12presentationyAcA018WindowPresentationD0VcACmF memberOf s:13WaylandClient12DisplayEventO - +s:13WaylandClient12DisplayEventO12presentationyAcA026ManagedSurfacePresentationD0VcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO12windowClosedyAcA8WindowIDVcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO13outputChangedyAcA14OutputSnapshotVcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO13outputRemovedyAcA8OutputIDVcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO14popupDismissedyAcA014PopupLifecycleD0VcACmF memberOf s:13WaylandClient12DisplayEventO - -s:13WaylandClient12DisplayEventO15redrawRequestedyAcA8WindowIDVcACmF memberOf s:13WaylandClient12DisplayEventO - -s:13WaylandClient12DisplayEventO20popupRedrawRequestedyAcA014PopupLifecycleD0VcACmF memberOf s:13WaylandClient12DisplayEventO - +s:13WaylandClient12DisplayEventO15redrawRequestedyAcA22ManagedSurfaceIdentityOcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO20windowCloseRequestedyAcA8WindowIDVcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO20windowOutputsChangedyAcA022WindowOutputMembershipD0VcACmF memberOf s:13WaylandClient12DisplayEventO - s:13WaylandClient12DisplayEventO33keyboardShortcutsInhibitorChangedyAcA08KeyboardfgD0VcACmF memberOf s:13WaylandClient12DisplayEventO - @@ -2956,12 +2963,15 @@ s:13WaylandClient12PopupSurfaceV13requestRedrawyyYaKF memberOf s:13WaylandClient s:13WaylandClient12PopupSurfaceV14parentWindowIDAA0fG0Vvp memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV14setInputRegionyyAA0dG0VSgYaKF memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV15setOpaqueRegionyyAA0dG0VSgYaKF memberOf s:13WaylandClient12PopupSurfaceV - +s:13WaylandClient12PopupSurfaceV18presentationEventsAA07Managedd12PresentationF0Vvp memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV2eeoiySbAC_ACtFZ memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV2idAA0cD8IdentityVvp memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV4hash4intoys6HasherVz_tF memberOf s:13WaylandClient12PopupSurfaceV - -s:13WaylandClient12PopupSurfaceV4show19timeoutMilliseconds_ys5Int32V_yAA13SoftwareFrameVYbKXEtYaKF memberOf s:13WaylandClient12PopupSurfaceV - +s:13WaylandClient12PopupSurfaceV4show8metadata27requestPresentationFeedback19timeoutMilliseconds9preparing_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_Sbs5Int32VxAA0mO11ReservationVYaYbKYCXEyx_AA0mO0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient12PopupSurfaceV - +s:13WaylandClient12PopupSurfaceV4show8metadata27requestPresentationFeedback19timeoutMilliseconds_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_Sbs5Int32VyAA0lN0VYbKXEtYaKF memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV5closeyyYaF memberOf s:13WaylandClient12PopupSurfaceV - -s:13WaylandClient12PopupSurfaceV6redrawyyyAA13SoftwareFrameVYbKXEYaKF memberOf s:13WaylandClient12PopupSurfaceV - +s:13WaylandClient12PopupSurfaceV6redraw8metadata27requestPresentationFeedback9preparing_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_SbxAA0kM11ReservationVYaYbKYCXEyx_AA0kM0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient12PopupSurfaceV - +s:13WaylandClient12PopupSurfaceV6redraw8metadata27requestPresentationFeedback_AA08SoftwareH7OutcomeOAA0D13FrameMetadataV_SbyAA0jL0VYbKXEtYaKF memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV8geometryAA0D8GeometryVvp memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV8identityAA0cD8IdentityVvp memberOf s:13WaylandClient12PopupSurfaceV - s:13WaylandClient12PopupSurfaceV8isClosedSbvp memberOf s:13WaylandClient12PopupSurfaceV - @@ -4655,6 +4665,13 @@ s:13WaylandClient22InputDiagnosticPayloadO6cursoryAcA06CursorD0OcACmF memberOf s s:13WaylandClient22InputDiagnosticPayloadO6keymapyAcA06KeymapD0OcACmF memberOf s:13WaylandClient22InputDiagnosticPayloadO - s:13WaylandClient22InputDiagnosticPayloadO8listeneryAcA0c8ListenerD0VcACmF memberOf s:13WaylandClient22InputDiagnosticPayloadO - s:13WaylandClient22InputDiagnosticPayloadO9operationAA0cD9OperationOvp memberOf s:13WaylandClient22InputDiagnosticPayloadO - +s:13WaylandClient22ManagedSurfaceIdentityO conformsTo s:SH Swift.Hashable +s:13WaylandClient22ManagedSurfaceIdentityO conformsTo s:SQ Swift.Equatable +s:13WaylandClient22ManagedSurfaceIdentityO conformsTo s:s16SendableMetatypeP Swift.SendableMetatype +s:13WaylandClient22ManagedSurfaceIdentityO conformsTo s:s8SendableP Swift.Sendable +s:13WaylandClient22ManagedSurfaceIdentityO10subsurfaceyAcA010SubsurfaceE0VcACmF memberOf s:13WaylandClient22ManagedSurfaceIdentityO - +s:13WaylandClient22ManagedSurfaceIdentityO5popupyAcA05PopupdE0VcACmF memberOf s:13WaylandClient22ManagedSurfaceIdentityO - +s:13WaylandClient22ManagedSurfaceIdentityO6windowyAcA8WindowIDVcACmF memberOf s:13WaylandClient22ManagedSurfaceIdentityO - s:13WaylandClient22OutputManagementHeadIDV conformsTo s:SH Swift.Hashable s:13WaylandClient22OutputManagementHeadIDV conformsTo s:SQ Swift.Equatable s:13WaylandClient22OutputManagementHeadIDV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype @@ -4866,12 +4883,6 @@ s:13WaylandClient23WindowManagerCapabilityO7unknownyACs6UInt32VcACmF memberOf s: s:13WaylandClient23WindowManagerCapabilityO8maximizeyA2CmF memberOf s:13WaylandClient23WindowManagerCapabilityO - s:13WaylandClient23WindowManagerCapabilityO8minimizeyA2CmF memberOf s:13WaylandClient23WindowManagerCapabilityO - s:13WaylandClient23WindowManagerCapabilityO8rawValues6UInt32Vvp memberOf s:13WaylandClient23WindowManagerCapabilityO - -s:13WaylandClient23WindowPresentationEventV conformsTo s:SQ Swift.Equatable -s:13WaylandClient23WindowPresentationEventV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype -s:13WaylandClient23WindowPresentationEventV conformsTo s:s8SendableP Swift.Sendable -s:13WaylandClient23WindowPresentationEventV8feedbackAA07SurfaceD8FeedbackOvp memberOf s:13WaylandClient23WindowPresentationEventV - -s:13WaylandClient23WindowPresentationEventV8windowID8feedbackAcA0cG0V_AA07SurfaceD8FeedbackOtcfc memberOf s:13WaylandClient23WindowPresentationEventV - -s:13WaylandClient23WindowPresentationEventV8windowIDAA0cG0Vvp memberOf s:13WaylandClient23WindowPresentationEventV - s:13WaylandClient24CursorConfigurationErrorO conformsTo s:SQ Swift.Equatable s:13WaylandClient24CursorConfigurationErrorO conformsTo s:s16SendableMetatypeP Swift.SendableMetatype s:13WaylandClient24CursorConfigurationErrorO conformsTo s:s23CustomStringConvertibleP Swift.CustomStringConvertible @@ -5038,12 +5049,6 @@ s:13WaylandClient24WindowLifecycleOperationO conformsTo s:s23CustomStringConvert s:13WaylandClient24WindowLifecycleOperationO conformsTo s:s8SendableP Swift.Sendable s:13WaylandClient24WindowLifecycleOperationO11descriptionSSvp memberOf s:13WaylandClient24WindowLifecycleOperationO - s:13WaylandClient24WindowLifecycleOperationO5closeyA2CmF memberOf s:13WaylandClient24WindowLifecycleOperationO - -s:13WaylandClient24WindowPresentationEventsV conformsTo s:Sci _Concurrency.AsyncSequence -s:13WaylandClient24WindowPresentationEventsV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype -s:13WaylandClient24WindowPresentationEventsV conformsTo s:s8SendableP Swift.Sendable -s:13WaylandClient24WindowPresentationEventsV17makeAsyncIteratorAA0cdeH0VyF memberOf s:13WaylandClient24WindowPresentationEventsV - -s:13WaylandClient24WindowPresentationEventsV7Elementa memberOf s:13WaylandClient24WindowPresentationEventsV - -s:13WaylandClient24WindowPresentationEventsV7Failurea memberOf s:13WaylandClient24WindowPresentationEventsV - s:13WaylandClient25CursorDiagnosticOperationO conformsTo s:SH Swift.Hashable s:13WaylandClient25CursorDiagnosticOperationO conformsTo s:SQ Swift.Equatable s:13WaylandClient25CursorDiagnosticOperationO conformsTo s:s16SendableMetatypeP Swift.SendableMetatype @@ -5604,6 +5609,12 @@ s:13WaylandClient31KeyboardShortcutsInhibitorEventV11inhibitorIDAA0cdeH0Vvp memb s:13WaylandClient31KeyboardShortcutsInhibitorEventV6seatIDAA04SeatH0Vvp memberOf s:13WaylandClient31KeyboardShortcutsInhibitorEventV - s:13WaylandClient31KeyboardShortcutsInhibitorEventV8activityAA0cdE8ActivityOvp memberOf s:13WaylandClient31KeyboardShortcutsInhibitorEventV - s:13WaylandClient31KeyboardShortcutsInhibitorEventV8windowIDAA06WindowH0Vvp memberOf s:13WaylandClient31KeyboardShortcutsInhibitorEventV - +s:13WaylandClient31ManagedSurfacePresentationEventV conformsTo s:SQ Swift.Equatable +s:13WaylandClient31ManagedSurfacePresentationEventV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype +s:13WaylandClient31ManagedSurfacePresentationEventV conformsTo s:s8SendableP Swift.Sendable +s:13WaylandClient31ManagedSurfacePresentationEventV7surface8feedbackAcA0cD8IdentityO_AA0dE8FeedbackOtcfc memberOf s:13WaylandClient31ManagedSurfacePresentationEventV - +s:13WaylandClient31ManagedSurfacePresentationEventV7surfaceAA0cD8IdentityOvp memberOf s:13WaylandClient31ManagedSurfacePresentationEventV - +s:13WaylandClient31ManagedSurfacePresentationEventV8feedbackAA0dE8FeedbackOvp memberOf s:13WaylandClient31ManagedSurfacePresentationEventV - s:13WaylandClient31PointerConstraintLifecycleEventO conformsTo s:SQ Swift.Equatable s:13WaylandClient31PointerConstraintLifecycleEventO conformsTo s:s16SendableMetatypeP Swift.SendableMetatype s:13WaylandClient31PointerConstraintLifecycleEventO conformsTo s:s8SendableP Swift.Sendable @@ -5632,11 +5643,12 @@ s:13WaylandClient32DataTransferCallbackFailureCauseO conformsTo s:s23CustomStrin s:13WaylandClient32DataTransferCallbackFailureCauseO conformsTo s:s8SendableP Swift.Sendable s:13WaylandClient32DataTransferCallbackFailureCauseO11descriptionSSvp memberOf s:13WaylandClient32DataTransferCallbackFailureCauseO - s:13WaylandClient32DataTransferCallbackFailureCauseO7backendyACSS_SStcACmF memberOf s:13WaylandClient32DataTransferCallbackFailureCauseO - -s:13WaylandClient32WindowPresentationEventsIteratorV conformsTo s:ScI _Concurrency.AsyncIteratorProtocol -s:13WaylandClient32WindowPresentationEventsIteratorV4next9isolationAA07SurfaceD8FeedbackOSgScA_pSgYi_tYaAA0A12DisplayErrorOYKF memberOf s:13WaylandClient32WindowPresentationEventsIteratorV - -s:13WaylandClient32WindowPresentationEventsIteratorV4nextAA07SurfaceD8FeedbackOSgyYaAA0A12DisplayErrorOYKF memberOf s:13WaylandClient32WindowPresentationEventsIteratorV - -s:13WaylandClient32WindowPresentationEventsIteratorV7Elementa memberOf s:13WaylandClient32WindowPresentationEventsIteratorV - -s:13WaylandClient32WindowPresentationEventsIteratorV7Failurea memberOf s:13WaylandClient32WindowPresentationEventsIteratorV - +s:13WaylandClient32ManagedSurfacePresentationEventsV conformsTo s:Sci _Concurrency.AsyncSequence +s:13WaylandClient32ManagedSurfacePresentationEventsV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype +s:13WaylandClient32ManagedSurfacePresentationEventsV conformsTo s:s8SendableP Swift.Sendable +s:13WaylandClient32ManagedSurfacePresentationEventsV17makeAsyncIteratorAA0cdefI0VyF memberOf s:13WaylandClient32ManagedSurfacePresentationEventsV - +s:13WaylandClient32ManagedSurfacePresentationEventsV7Elementa memberOf s:13WaylandClient32ManagedSurfacePresentationEventsV - +s:13WaylandClient32ManagedSurfacePresentationEventsV7Failurea memberOf s:13WaylandClient32ManagedSurfacePresentationEventsV - s:13WaylandClient32WindowPresentationRequestSummaryV conformsTo s:SQ Swift.Equatable s:13WaylandClient32WindowPresentationRequestSummaryV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype s:13WaylandClient32WindowPresentationRequestSummaryV conformsTo s:s23CustomStringConvertibleP Swift.CustomStringConvertible @@ -5742,6 +5754,11 @@ s:13WaylandClient39KeyboardInterpretationUnavailableReasonO23unsupportedKeymapFo s:13WaylandClient39KeyboardInterpretationUnavailableReasonO26composeStateCreationFailedyA2CmF memberOf s:13WaylandClient39KeyboardInterpretationUnavailableReasonO - s:13WaylandClient39KeyboardInterpretationUnavailableReasonO27invalidComposeConfigurationyA2CmF memberOf s:13WaylandClient39KeyboardInterpretationUnavailableReasonO - s:13WaylandClient39KeyboardInterpretationUnavailableReasonO8noKeymapyA2CmF memberOf s:13WaylandClient39KeyboardInterpretationUnavailableReasonO - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV conformsTo s:ScI _Concurrency.AsyncIteratorProtocol +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV4next9isolationAA0dE8FeedbackOSgScA_pSgYi_tYaAA0A12DisplayErrorOYKF memberOf s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV4nextAA0dE8FeedbackOSgyYaAA0A12DisplayErrorOYKF memberOf s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV7Elementa memberOf s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV - +s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV7Failurea memberOf s:13WaylandClient40ManagedSurfacePresentationEventsIteratorV - s:13WaylandClient6SeatIDV conformsTo s:SH Swift.Hashable s:13WaylandClient6SeatIDV conformsTo s:SQ Swift.Equatable s:13WaylandClient6SeatIDV conformsTo s:s16SendableMetatypeP Swift.SendableMetatype @@ -5779,7 +5796,7 @@ s:13WaylandClient6WindowV16createSubsurface13configurationAA0E0VAA0E13Configurat s:13WaylandClient6WindowV17requestFullscreen6outputyAA8OutputIDVSg_tYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV17requestUnmaximizeyyYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV17startToplevelDrag6source6seatID6serial4icon6offsetAA07StartedeF0VAA0F19SourceConfigurationV_AA04SeatI0VAA11InputSerialVAA0F4IconOAA13LogicalOffsetVtYaKF memberOf s:13WaylandClient6WindowV - -s:13WaylandClient6WindowV18presentationEventsAA0c12PresentationE0Vvp memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV18presentationEventsAA026ManagedSurfacePresentationE0Vvp memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV18requestPointerWarp6seatID8position6serialyAA04SeatH0V_AA13LogicalOffsetVAA11InputSerialVtYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV19restorationSnapshotAA0c11RestorationE0Vvp memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV21requestExitFullscreenyyYaKF memberOf s:13WaylandClient6WindowV - diff --git a/docs/release.md b/docs/release.md index d546b948..5ea308ee 100644 --- a/docs/release.md +++ b/docs/release.md @@ -37,6 +37,16 @@ This checkpoint contains source-breaking `WaylandClient` and same atomic `requestPresentationFeedback` option. Move `damage:` arguments into `SurfaceFrameMetadata.damage`; remove explicit no-op-preparation and legacy `Void` wrappers. Prepared generations are revalidated before drawing. +- Apply the same direct/prepared contract to `PopupSurface` and `Subsurface`. + Popup configure/redraw/lifecycle changes and subsurface scale, synchronization, + cancellation, and parent-close changes now produce typed latest-wins outcomes. + Remove the subsurface `damage:` overloads and pass damage through metadata. +- Replace `DisplayEvent.redrawRequested(WindowID)`, `popupRedrawRequested`, and + `presentation(WindowPresentationEvent)` with role-qualified + `redrawRequested(ManagedSurfaceIdentity)` and + `presentation(ManagedSurfacePresentationEvent)`. Replace + `WindowPresentationEvents` with `ManagedSurfacePresentationEvents`; all three + managed handles expose a filtered presentation sequence. - Replace `WaylandGraphicsFrameMetadata`, `WaylandGraphicsDamageRegion`, `WaylandGraphicsContentType`, `WaylandGraphicsPresentationHint`, `WaylandGraphicsAlphaModifier`, `WaylandGraphicsColorAlphaMode`, and diff --git a/docs/support-matrix.md b/docs/support-matrix.md index dc55d02a..0d845980 100644 --- a/docs/support-matrix.md +++ b/docs/support-matrix.md @@ -25,6 +25,7 @@ This matrix describes the current experimental baseline. Status labels: | --- | --- | --- | | `xdg_wm_base`, `xdg_surface`, `xdg_toplevel` | Public | Toplevel windows and configure lifecycle. | | `xdg_popup`, `xdg_positioner` | Public | Popup placement and dismissal. | +| `wl_subcompositor`, `wl_subsurface` | Public | Managed synchronized and desynchronized child surfaces. | | `zxdg_decoration_manager_v1`, `zxdg_toplevel_decoration_v1` | Public | Server-side decoration negotiation. | | `xdg_wm_dialog_v1`, `xdg_dialog_v1` | Public | Dialog hints when advertised. | | `xdg_toplevel_drag_manager_v1`, `xdg_toplevel_drag_v1` | Public | Toplevel drag start. | @@ -37,7 +38,7 @@ This matrix describes the current experimental baseline. Status labels: | --- | --- | --- | | `wp_viewporter`, `wp_viewport` | Public | Scale-aware SHM buffer presentation. | | `wp_fractional_scale_manager_v1`, `wp_fractional_scale_v1` | Public | Fractional scale facts. | -| `wp_presentation`, `wp_presentation_feedback` | Public | Presentation feedback requests and events. | +| `wp_presentation`, `wp_presentation_feedback` | Public | Window, popup, and subsurface feedback requests and events. | | `wp_content_type_manager_v1`, `wp_content_type_v1` | Internal preview | Surface commit metadata. | | `wp_alpha_modifier_v1`, `wp_alpha_modifier_surface_v1` | Internal preview | Surface commit metadata. | | `wp_tearing_control_manager_v1`, `wp_tearing_control_v1` | Internal preview | Surface commit metadata. | diff --git a/docs/which-api-should-i-use.md b/docs/which-api-should-i-use.md index 53bb7aa1..001dd47e 100644 --- a/docs/which-api-should-i-use.md +++ b/docs/which-api-should-i-use.md @@ -8,11 +8,12 @@ tasks to public APIs and examples. | Open a display connection | `WaylandDisplay.withConnection` | Wayland display availability | [WaylandClientKitDemo](../Examples/WaylandClientKitDemo/main.swift) | | Observe ordering across all event families | `display.events` | Wayland display availability | [WaylandClientKitDemo](../Examples/WaylandClientKitDemo/main.swift) | | Create a window | `WaylandDisplay.createTopLevelWindow` | `xdg_wm_base` | [WaylandClientKitDemo](../Examples/WaylandClientKitDemo/main.swift) | -| Draw pixels | `Window.show`, `Window.redraw`, `SoftwareFrame` | `wl_shm` and xdg configure | [WaylandClientKitDemo](../Examples/WaylandClientKitDemo/main.swift) | +| Draw pixels on a managed surface | `Window`, `PopupSurface`, or `Subsurface` `show`/`redraw` with `SoftwareFrame` | `wl_shm` plus the role's lifecycle gate | [ManagedSurfacePresentationSmoke](../Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift) | | Inspect outputs and window output membership | `WaylandDisplay.outputTopology()`, `Window.stateSnapshot.outputs` | `wl_output`, optional `zxdg_output_manager_v1` | [OutputTopologySmoke](../Examples/OutputTopologySmoke/main.swift) | -| Redraw part of a window | `SurfaceDamageRegion` with `Window.redraw(damage:_:)` | surface size and scale must validate | [DamageRegionSmoke](../Examples/DamageRegionSmoke/main.swift) | +| Redraw part of a managed surface | `SurfaceFrameMetadata(damage:)` with the role's `redraw` method | surface size and scale must validate | [Managed surface drawing](../Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md) | | Shape input or opaque regions | `Window.setInputRegion`, `Window.setOpaqueRegion`, `SurfaceRegion` | `wl_compositor` regions | [SurfaceRegionSmoke](../Examples/SurfaceRegionSmoke/main.swift) | -| Create a child surface | `Window.createSubsurface`, `Subsurface` | `wl_subcompositor` | [SubsurfaceSmoke](../Examples/SubsurfaceSmoke/main.swift) | +| Create and present a child surface | `Window.createSubsurface`, `Subsurface.show`, `Subsurface.redraw` | `wl_subcompositor` | [ManagedSurfacePresentationSmoke](../Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift) | +| Create and present an xdg popup | `Window.createPopup`, `PopupSurface.show`, `PopupSurface.redraw` | `xdg_wm_base` popup configure | [ManagedSurfacePresentationSmoke](../Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift) | | Use a custom cursor image | `PointerCursor.image(_:)`, `WaylandDisplay.setPointerCursor(_:)` | cursor surface support and pointer focus | [CustomCursorSmoke](../Examples/CustomCursorSmoke/main.swift) | | Use an animated custom cursor | `PointerCursorFrame`, `AnimatedPointerCursor`, `PointerCursor.animated(_:)` | cursor surface support and pointer focus | [CursorAnimationSmoke](../Examples/CursorAnimationSmoke/main.swift) | | Request compositor cursor shapes | `PointerCursor.shape(_:)` through `WaylandDisplay.setPointerCursor(_:)` | `wp_cursor_shape_manager_v1` | [CursorPolicySmoke](../Examples/CursorPolicySmoke/main.swift) | @@ -34,7 +35,7 @@ tasks to public APIs and examples. | Inspect compositor session support | `WaylandDisplay.capabilities().compositorSessionManagement` | `xdg_session_manager_v1`, no public session handle yet | [CompositorSessionSmoke](../Examples/CompositorSessionSmoke/main.swift) | | Inspect read-only foreign toplevel facts | `WaylandDisplay.foreignToplevelListSnapshot()` | `ext_foreign_toplevel_list_v1`, titles/app IDs are optional and privacy-sensitive | [ForeignToplevelListSmoke](../Examples/ForeignToplevelListSmoke/main.swift) | | Inspect output-management facts | `WaylandDisplay.outputManagementSnapshot()` | `zwlr_output_manager_v1`, mutation is not public | [OutputManagementSmoke](../Examples/OutputManagementSmoke/main.swift) | -| Time animation to real presentation | `Window.requestPresentationFeedback`, `Window.presentationEvents` | `wp_presentation` | [PresentationFeedbackAnimation](../Examples/PresentationFeedbackAnimation/main.swift) | +| Observe real presentation for any managed role | `requestPresentationFeedback: true`, each handle's `presentationEvents`, and `DisplayEvent.presentation` | `wp_presentation` | [ManagedSurfacePresentationSmoke](../Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift) | | Inspect optional protocol support | `WaylandDisplay.capabilities()` | registry discovery | [Capabilities DocC](../Sources/WaylandClient/WaylandClient.docc/CapabilitiesAndOptionalProtocols.md) | | Try renderer-neutral GPU preview | `WaylandGraphicsPreview` values, `WaylandGraphicsWindowBacking`, frame leases | `zwp_linux_dmabuf_v1` plus surface-specific runtime setup | [Graphics preview docs](../Sources/WaylandGraphicsPreview/WaylandGraphicsPreview.docc/WaylandGraphicsPreview.md) | | Request preview frame scheduling | `WaylandGraphicsFrameSchedule` | explicit sync, FIFO, commit timing, or presentation protocols as requested | [GraphicsPreviewManagedGPUClear](../Examples/GraphicsPreviewManagedGPUClear/main.swift) | diff --git a/identities/manual-identity-categories.json b/identities/manual-identity-categories.json index c9e4ab8d..088385f9 100644 --- a/identities/manual-identity-categories.json +++ b/identities/manual-identity-categories.json @@ -14,6 +14,18 @@ "storage": "value", "storageVisibility": "public" }, + { + "type": "DataTransferOfferIdentity", + "category": "composite identity" + }, + { + "type": "EventStreamIdentity", + "category": "public value identity" + }, + { + "type": "ManagedSurfaceIdentity", + "category": "composite identity" + }, { "type": "PointerConstraintID", "category": "client identity", @@ -34,6 +46,10 @@ "constructor": "package", "storage": "submissionID", "storageVisibility": "public" + }, + { + "type": "WindowStateToken", + "category": "public value identity" } ] }