From ccd6cbfb52b5986c47b3455616e2021706ba7070 Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:32:00 -0400 Subject: [PATCH 1/7] Route redraw and feedback events by managed surface --- Examples/ClientSideResizeChrome/main.swift | 2 +- Examples/CursorAnimationSmoke/main.swift | 2 +- Examples/CursorPolicySmoke/main.swift | 2 +- Examples/CustomCursorSmoke/main.swift | 2 +- Examples/DataTransferSmoke/main.swift | 2 +- Examples/FrameworkHostSmoke/main.swift | 4 +- .../GraphicsPreviewManagedGPUClear/main.swift | 2 +- .../KeyboardShortcutsInhibitSmoke/main.swift | 2 +- Examples/PointerCaptureSmoke/main.swift | 2 +- Examples/PointerGesturesSmoke/main.swift | 2 +- Examples/PointerWarpSmoke/main.swift | 2 +- .../PresentationFeedbackAnimation/main.swift | 4 +- Examples/SerialActionsProbe/main.swift | 2 +- Examples/SessionStateSmoke/main.swift | 2 +- Examples/SurfaceRegionSmoke/main.swift | 2 +- Examples/TabletInputSmoke/main.swift | 2 +- Examples/TextInputSmoke/main.swift | 2 +- Examples/ToplevelDragSmoke/main.swift | 2 +- Examples/TwoWindowFrameworkHost/main.swift | 2 +- Examples/TwoWindowOrderStress/main.swift | 2 +- Examples/WaylandClientKitDemo/main.swift | 4 +- ...dDisplayPresentationIntegrationTests.swift | 18 +++--- .../WaylandDisplayPublicAPISurfaceTests.swift | 19 ++++-- ...WaylandDisplayPublicIntegrationTests.swift | 37 +++-------- .../Internal/Display/DisplayCore+Events.swift | 1 - .../Internal/Display/DisplayCore+Popup.swift | 15 ++--- .../DisplayCore+PresentationFeedback.swift | 9 ++- .../Internal/Display/DisplayCore.swift | 2 +- .../Internal/Display/DisplayEventHub.swift | 16 +++-- .../Public/Display/DisplayEvent.swift | 5 +- .../Display/WaylandDisplay+Presentation.swift | 8 +-- .../Public/Popup/PopupSurface.swift | 4 ++ .../Presentation/PresentationFeedback.swift | 45 ++++++------- .../Rendering/ManagedSurfaceIdentity.swift | 6 ++ .../Public/Subsurface/Subsurface.swift | 4 ++ .../WaylandClient/Public/Window/Window.swift | 4 +- .../Events/DisplayEventHubPopupTests.swift | 16 ++--- .../DisplayEventHubPresentationTests.swift | 41 +++++++----- ...isplayEventHubStreamTerminationTests.swift | 14 ++-- .../Events/DisplayEventHubTests.swift | 11 ++-- .../Events/ManagedSurfaceEventsTests.swift | 64 +++++++++++++++++++ ...ftwarePresentationPublicRequestTests.swift | 2 +- 42 files changed, 230 insertions(+), 159 deletions(-) create mode 100644 Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift create mode 100644 Tests/WaylandClientTests/Events/ManagedSurfaceEventsTests.swift 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/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/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift index d30bb36e..27b413f9 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift @@ -66,7 +66,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 +74,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 +113,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 +198,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 +211,7 @@ extension WaylandDisplayPublicIntegrationTests { } } ) - #expect(replacementEvent == .redrawRequested(window.id)) + #expect(replacementEvent == .redrawRequested(.window(window.id))) #expect( try await window.redraw( preparing: { $0.id }, @@ -267,7 +267,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 +307,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 +325,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..f561c4b5 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,7 +257,16 @@ struct WaylandPresentationAPISurfaceTests { try await window.requestPresentationFeedback() } + func useManagedSurfacePresentationStreams( + popup: PopupSurface, + subsurface: Subsurface + ) { + _ = popup.presentationEvents.makeAsyncIterator() + _ = subsurface.presentationEvents.makeAsyncIterator() + } + _ = usePresentationFeedbackAPI + _ = useManagedSurfacePresentationStreams } @Test diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift index a3199eb2..7c319858 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 @@ -270,24 +270,18 @@ private func redraw( 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 @@ -321,9 +315,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 +336,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/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..063fb326 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift @@ -187,11 +187,8 @@ extension DisplayCore { core?.handlePopupClosed(popupID) }, onRedrawRequested: { [weak core = self] in - core?.publishPopupRedrawRequested( - popupID: popupID, - parentWindowID: parentWindowID - ) - } + core?.publishPopupRedrawRequested(popupID: popupID) + }, ) } @@ -230,13 +227,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..031f6194 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 + ) ) } ) @@ -37,8 +40,8 @@ extension DisplayCore { }, onFeedback: { [weak self] feedback in self?.eventHub.publishPresentation( - WindowPresentationEvent( - windowID: windowID, + ManagedSurfacePresentationEvent( + surface: .window(windowID), feedback: feedback ) ) diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore.swift b/Sources/WaylandClient/Internal/Display/DisplayCore.swift index 46d46ade..65d86687 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore.swift @@ -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( diff --git a/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift b/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift index 5d06353a..df102c29 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,9 +77,11 @@ final class DisplayEventHub: Sendable { TextInputEvents(InternalEventSubscriptionFactory(textInputBroker)) } - func windowPresentationEvents(windowID: WindowID) -> WindowPresentationEvents { - WindowPresentationEvents( - windowID: windowID, + func managedSurfacePresentationEvents( + surface: ManagedSurfaceIdentity + ) -> ManagedSurfacePresentationEvents { + ManagedSurfacePresentationEvents( + surface: surface, subscriptions: InternalEventSubscriptionFactory(presentationBroker) ) } @@ -101,7 +103,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 +145,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/Public/Display/DisplayEvent.swift b/Sources/WaylandClient/Public/Display/DisplayEvent.swift index 18b2377a..1c55b617 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) } 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/Popup/PopupSurface.swift b/Sources/WaylandClient/Public/Popup/PopupSurface.swift index e6c0eb03..c77a59ab 100644 --- a/Sources/WaylandClient/Public/Popup/PopupSurface.swift +++ b/Sources/WaylandClient/Public/Popup/PopupSurface.swift @@ -39,6 +39,10 @@ public struct PopupSurface: Sendable, Hashable, Identifiable { 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..e9c3b568 100644 --- a/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift +++ b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift @@ -68,61 +68,62 @@ public struct PresentationFeedback: Equatable, Sendable { } } -public struct WindowPresentationEvent: Equatable, Sendable { - public let windowID: WindowID +public struct ManagedSurfacePresentationEvent: Equatable, Sendable { + public let surface: ManagedSurfaceIdentity public let feedback: SurfacePresentationFeedback 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. @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() ) } } @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 } @@ -134,7 +135,7 @@ public struct WindowPresentationEventsIterator: AsyncIteratorProtocol { 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..9606e676 --- /dev/null +++ b/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift @@ -0,0 +1,6 @@ +/// Identifies a managed software surface independently of a presentation request. +public enum ManagedSurfaceIdentity: Hashable, Sendable { + case window(WindowID) + case popup(PopupSurfaceIdentity) + case subsurface(SubsurfaceIdentity) +} diff --git a/Sources/WaylandClient/Public/Subsurface/Subsurface.swift b/Sources/WaylandClient/Public/Subsurface/Subsurface.swift index 4c2b6758..8483b184 100644 --- a/Sources/WaylandClient/Public/Subsurface/Subsurface.swift +++ b/Sources/WaylandClient/Public/Subsurface/Subsurface.swift @@ -56,6 +56,10 @@ public struct Subsurface: Sendable, Hashable, Identifiable { 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.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/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..82ecd34b 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,29 +17,32 @@ 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)") } @@ -51,18 +54,22 @@ struct DisplayEventHubPresentationTests { 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/Windowing/WindowSoftwarePresentationPublicRequestTests.swift b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift index 3c302614..975bb361 100644 --- a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift +++ b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift @@ -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 } } From ec705d343e2efd696cc3b1d68eb9984f155eee5f Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:36:22 -0400 Subject: [PATCH 2/7] Detach software presenter from window requests --- .../DisplayCore+PresentationFeedback.swift | 4 +- .../Internal/Display/DisplayCore.swift | 4 +- .../Subsurface/SubsurfaceRoleSurface.swift | 16 ++-- ...=> SoftwareSurfacePresentationTypes.swift} | 30 +++--- ...r.swift => SoftwareSurfacePresenter.swift} | 96 +++++++++---------- .../Internal/Windowing/TopLevelWindow.swift | 62 ++++++------ .../WindowExternalBufferPresenter.swift | 4 +- .../WaylandDisplay+SoftwarePresentation.swift | 4 +- .../WaylandGraphicsSubmissionState.swift | 2 +- ...ft => SoftwareSurfacePresenterTests.swift} | 43 +++------ ...ftwarePresentationPublicRequestTests.swift | 2 +- ...aylandGraphicsSubmissionFailureTests.swift | 4 +- 12 files changed, 129 insertions(+), 142 deletions(-) rename Sources/WaylandClient/Internal/Windowing/{WindowSoftwarePresentationTypes.swift => SoftwareSurfacePresentationTypes.swift} (68%) rename Sources/WaylandClient/Internal/Windowing/{WindowSoftwarePresenter.swift => SoftwareSurfacePresenter.swift} (85%) rename Tests/WaylandClientTests/Rendering/{WindowSoftwarePresenterTests.swift => SoftwareSurfacePresenterTests.swift} (92%) diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift index 031f6194..a5bee5e0 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift @@ -26,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, diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore.swift b/Sources/WaylandClient/Internal/Display/DisplayCore.swift index 65d86687..a02f52c8 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 diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift index 2622fc67..b4819425 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift @@ -296,9 +296,9 @@ extension SubsurfaceRoleSurface { generation: generation, configuration: resolvedConfiguration() ) - let result: WindowSoftwarePresentationResult + let result: SoftwareSurfacePresentationResult do { - result = try WindowSoftwarePresenter( + result = try SoftwareSurfacePresenter( surface: surface, scaleInstallation: scaleInstallation, createSharedMemoryPool: { [self] bufferSize in @@ -314,13 +314,13 @@ extension SubsurfaceRoleSurface { self?.handleBufferReleased() } }, - isWindowClosed: { [self] in isClosed }, + isSurfaceClosed: { [self] in isClosed }, onFrame: { [weak self] in self?.handleFrameDone() } ).present( - context: WindowSoftwarePresentationContext( - request: request, + context: SoftwareSurfacePresentationContext( + generation: request.generation, geometry: try currentGeometry(), submitConstraints: .default, metadata: .default, @@ -338,7 +338,7 @@ extension SubsurfaceRoleSurface { } private func handlePresentationFollowUp( - _ followUp: WindowSoftwarePresentationFollowUp? + _ followUp: SoftwareSurfacePresentationFollowUp? ) throws -> SubsurfaceParentCommitRequirement? { guard let followUp else { return nil } @@ -366,9 +366,9 @@ extension SubsurfaceRoleSurface { private func mapPresentationFailure(_ error: any Error) -> ClientError { let cause: SubsurfacePresentationFailureCause - if let failure = error as? WindowSoftwarePresentationFailure { + if let failure = error as? SoftwareSurfacePresentationFailure { cause = .presentation(failure.presentationError) - } else if let failure = error as? WindowSoftwareDrawFailure { + } else if let failure = error as? SoftwareSurfaceDrawFailure { cause = .draw(String(describing: failure.underlying)) } else { cause = .operation(String(describing: error)) diff --git a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift similarity index 68% rename from Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift rename to Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift index 0fe7e221..9d89bac5 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift +++ b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift @@ -1,39 +1,39 @@ import WaylandRaw -struct WindowSoftwarePresentationResult { +struct SoftwareSurfacePresentationResult { let outcome: RedrawOutcome - let followUp: WindowSoftwarePresentationFollowUp? + let followUp: SoftwareSurfacePresentationFollowUp? } -struct WindowReservedSoftwareFrame { +struct ReservedSoftwareSurfaceFrame { let reservation: SoftwareFrameReservation let drawingBuffer: RawBuffer.ReservedDrawingBuffer } -struct WindowSoftwareFrameReservationResult { - let reservedFrame: WindowReservedSoftwareFrame? - let followUp: WindowSoftwarePresentationFollowUp? +struct SoftwareSurfaceFrameReservationResult { + let reservedFrame: ReservedSoftwareSurfaceFrame? + let followUp: SoftwareSurfacePresentationFollowUp? } -package enum WindowSoftwareFrameReservationOutcome: Equatable, Sendable { +package enum SoftwareSurfaceFrameReservationOutcome: Equatable, Sendable { case reserved(SoftwareFrameReservation) case deferred case closed } -enum WindowSoftwarePresentationFollowUp { +enum SoftwareSurfacePresentationFollowUp { case fail(generation: UInt64, PresentationError) case blockedByBuffer case resetTransientState case succeeded(generation: UInt64) } -struct WindowSoftwarePresentationFailure: Error { +struct SoftwareSurfacePresentationFailure: Error { let presentationError: PresentationError let underlying: any Error } -package struct WindowSoftwareDrawFailure: Error { +package struct SoftwareSurfaceDrawFailure: Error { package let underlying: any Error package init(underlying drawError: any Error) { @@ -41,7 +41,7 @@ package struct WindowSoftwareDrawFailure: Error { } } -package struct WindowPresentationFeedbackCommitRequest { +package struct SurfacePresentationFeedbackCommitRequest { let request: () throws -> SurfacePresentationIdentity let cancel: (SurfacePresentationIdentity) -> Void @@ -54,7 +54,7 @@ package struct WindowPresentationFeedbackCommitRequest { } } -package enum WindowSoftwarePresentationCommitSequence { +package enum SoftwareSurfacePresentationCommitSequence { @discardableResult package static func perform( stageSuccess: () throws -> Void, @@ -72,11 +72,11 @@ package enum WindowSoftwarePresentationCommitSequence { } } -struct WindowSoftwarePresentationContext { - let request: PresentationRequest +struct SoftwareSurfacePresentationContext { + let generation: UInt64 let geometry: SurfaceGeometry let submitConstraints: SurfaceSubmitConstraints let metadata: SurfaceCommitMetadata let damage: SurfaceDamageRegion? - let presentationFeedback: WindowPresentationFeedbackCommitRequest? + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? } diff --git a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift similarity index 85% rename from Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift rename to Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift index 25649f5b..e2cc62de 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift +++ b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift @@ -1,32 +1,32 @@ 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? } // 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 +44,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,9 +59,9 @@ 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 ), @@ -73,21 +73,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 +102,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 +116,7 @@ struct WindowSoftwarePresenter { geometry: SoftwareFrameGeometry(surface: context.geometry) ) return .init( - reservedFrame: WindowReservedSoftwareFrame( + reservedFrame: ReservedSoftwareSurfaceFrame( reservation: reservation, drawingBuffer: drawingBuffer ), @@ -125,19 +125,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 +146,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,9 +161,9 @@ 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 ), @@ -175,12 +175,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 +188,7 @@ struct WindowSoftwarePresenter { } private func performPreparedCommit( - context: WindowSoftwareCommitContext, + context: SoftwareSurfaceCommitContext, stageSuccess: (RedrawBufferAvailability) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration?, @@ -206,12 +206,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( @@ -256,7 +256,7 @@ struct WindowSoftwarePresenter { } } catch { drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .userDraw(String(describing: error)), underlying: error ) @@ -264,7 +264,7 @@ struct WindowSoftwarePresenter { } private func drawReservedFrame( - _ reservedFrame: WindowReservedSoftwareFrame, + _ reservedFrame: ReservedSoftwareSurfaceFrame, geometry: SurfaceGeometry, draw: (borrowing SoftwareFrame) throws -> Void ) throws { @@ -282,7 +282,7 @@ struct WindowSoftwarePresenter { } } catch { reservedFrame.drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .userDraw(String(describing: error)), underlying: error ) @@ -291,7 +291,7 @@ struct WindowSoftwarePresenter { // swiftlint:disable:next function_parameter_count private func prepareCommit( - request: PresentationRequest, + generation: UInt64, geometry: SurfaceGeometry, submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, @@ -304,7 +304,7 @@ struct WindowSoftwarePresenter { SurfaceFrameCommitRequest( surface: surface, scaleInstallation: scaleInstallation, - generation: request.generation, + generation: generation, geometry: geometry, payload: .buffer(drawingBuffer.surfaceBuffer), submitConstraints: submitConstraints, @@ -315,7 +315,7 @@ struct WindowSoftwarePresenter { ) } catch { drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error ) @@ -324,7 +324,7 @@ struct WindowSoftwarePresenter { // swiftlint:disable:next function_parameter_count private func prepareReservedCommit( - request: PresentationRequest, + generation: UInt64, geometry: SurfaceGeometry, submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, @@ -337,7 +337,7 @@ struct WindowSoftwarePresenter { SurfaceFrameCommitRequest( surface: surface, scaleInstallation: scaleInstallation, - generation: request.generation, + generation: generation, geometry: geometry, payload: .buffer(drawingBuffer.surfaceBuffer), submitConstraints: submitConstraints, @@ -348,7 +348,7 @@ struct WindowSoftwarePresenter { ) } catch { drawingBuffer.discard() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error ) @@ -365,7 +365,7 @@ struct WindowSoftwarePresenter { } private func requestPresentationFeedbackAtPointOfNoReturn( - _ presentationFeedback: WindowPresentationFeedbackCommitRequest? + _ presentationFeedback: SurfacePresentationFeedbackCommitRequest? ) -> SurfacePresentationIdentity? { guard let presentationFeedback else { return nil } @@ -379,16 +379,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 +401,7 @@ struct WindowSoftwarePresenter { ) } catch { runtime.cancelFrameCallback() - throw WindowSoftwarePresentationFailure( + throw SoftwareSurfacePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error ) @@ -409,7 +409,7 @@ struct WindowSoftwarePresenter { } private func performPreparedReservedCommit( - context: WindowSoftwareCommitContext, + context: SoftwareSurfaceCommitContext, stageSuccess: (RedrawBufferAvailability) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration?, @@ -427,12 +427,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( diff --git a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift index 0a14c085..4d27c4f8 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 { @@ -497,7 +497,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,7 +536,7 @@ package final class TopLevelWindow { private func reserveSoftwareFrameForCurrentRedraw( metadata frameMetadata: SurfaceFrameMetadata = .default ) - throws -> WindowSoftwareFrameReservationOutcome + throws -> SoftwareSurfaceFrameReservationOutcome { guard !model.isClosed else { return .closed } try validateSurfaceFrameMetadataSupport(frameMetadata) @@ -550,8 +550,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 +576,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 +682,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 +692,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 +714,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 +745,8 @@ package final class TopLevelWindow { } } - private func softwarePresenter() -> WindowSoftwarePresenter { - WindowSoftwarePresenter( + private func softwarePresenter() -> SoftwareSurfacePresenter { + SoftwareSurfacePresenter( surface: surface, scaleInstallation: scaleInstallation, createSharedMemoryPool: { [self] bufferSize in @@ -762,7 +762,7 @@ package final class TopLevelWindow { self?.handleBufferReleased() } }, - isWindowClosed: { [self] in model.isClosed }, + isSurfaceClosed: { [self] in model.isClosed }, onFrame: { [weak self] in self?.handleFrameDone() } @@ -775,7 +775,7 @@ package final class TopLevelWindow { 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,7 +787,7 @@ package final class TopLevelWindow { } var stagedSuccess: StagedSoftwarePresentationSuccess? - let result: WindowSoftwarePresentationResult + let result: SoftwareSurfacePresentationResult do { guard try isCurrentSoftwarePresentation(pendingReservation) else { return try supersedeSoftwarePresentation(pendingReservation) @@ -798,8 +798,8 @@ package final class TopLevelWindow { 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 +816,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 +866,7 @@ package final class TopLevelWindow { } private func interpretDeferredSoftwarePresentation( - _ result: WindowSoftwarePresentationResult, + _ result: SoftwareSurfacePresentationResult, pendingReservation: PendingSoftwareFrameReservation ) throws -> SoftwarePresentationOutcome { do { @@ -941,7 +941,7 @@ package final class TopLevelWindow { } private func interpretSoftwarePresentationFollowUp( - _ followUp: WindowSoftwarePresentationFollowUp? + _ followUp: SoftwareSurfacePresentationFollowUp? ) throws { guard let followUp else { return } @@ -1465,7 +1465,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 +1633,7 @@ extension TopLevelWindow { _ buffer: RawSurfaceBuffer, submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata = .default, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil + presentationFeedback: SurfacePresentationFeedbackCommitRequest? = nil ) throws -> PreviewBufferPresentationResult { connection.preconditionIsOwnerThread() @@ -2171,7 +2171,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 +2193,7 @@ extension TopLevelWindow { package func reserveShowSoftwareFrameOnOwnerThread( timeoutMilliseconds: Int32 = defaultConfigureTimeoutMS, metadata frameMetadata: SurfaceFrameMetadata = .default - ) throws -> WindowSoftwareFrameReservationOutcome { + ) throws -> SoftwareSurfaceFrameReservationOutcome { connection.preconditionIsOwnerThread() try validateSurfaceFrameMetadataSupport(frameMetadata) @@ -2231,7 +2231,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 +2252,7 @@ extension TopLevelWindow { package func reserveRedrawSoftwareFrameOnOwnerThread( metadata frameMetadata: SurfaceFrameMetadata = .default ) - throws -> WindowSoftwareFrameReservationOutcome + throws -> SoftwareSurfaceFrameReservationOutcome { connection.preconditionIsOwnerThread() @@ -2268,7 +2268,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 +2321,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( 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/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/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/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift similarity index 92% rename from Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift rename to Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift index 15027d12..64ddf139 100644 --- a/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift +++ b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift @@ -7,7 +7,7 @@ @testable import WaylandClient @Suite - struct WindowSoftwarePresentationCommitSequenceTests { + struct SoftwareSurfacePresentationCommitSequenceTests { @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) }, @@ -51,7 +51,7 @@ var events: [CommitSequenceEvent] = [] do { - _ = try WindowSoftwarePresentationCommitSequence.perform( + _ = try SoftwareSurfacePresentationCommitSequence.perform( stageSuccess: { events.append(.stageSuccess) throw InjectedSuccessStagingFailure() @@ -82,7 +82,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 +105,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 +258,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 +296,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 +330,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 +371,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 +390,12 @@ private func softwarePresenter( surface: RawSurface, pool: RawSharedMemoryPool - ) -> WindowSoftwarePresenter { - WindowSoftwarePresenter( + ) -> SoftwareSurfacePresenter { + SoftwareSurfacePresenter( surface: surface, scaleInstallation: SurfaceScaleInstallation(), createSharedMemoryPool: { _ in pool }, - isWindowClosed: { false }, + isSurfaceClosed: { false }, onFrame: { _ = () } diff --git a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift index 975bb361..bbab6822 100644 --- a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift +++ b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift @@ -222,7 +222,7 @@ } ) Issue.record("expected draw failure") - } catch let failure as WindowSoftwareDrawFailure { + } catch let failure as SoftwareSurfaceDrawFailure { #expect(failure.underlying is InjectedSoftwareDrawFailure) } } 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()) } } From 365c527e223191ec226be8faf4685eb49cb032b1 Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:11:11 -0400 Subject: [PATCH 3/7] Add latest-wins popup software presentation --- ...edSurfacePresentationAPISurfaceTests.swift | 86 ++++ .../WaylandDisplayPublicAPISurfaceTests.swift | 52 -- ...WaylandDisplayPublicIntegrationTests.swift | 19 +- .../Internal/Display/DisplayCore+Popup.swift | 73 ++- .../DisplayCore+PresentationFeedback.swift | 32 ++ .../Internal/Popup/PopupModel.swift | 100 ++-- .../Internal/Popup/PopupModelTypes.swift | 68 +++ .../Popup/PopupRoleSurface+Effects.swift | 64 +++ .../Popup/PopupRoleSurface+Lifecycle.swift | 11 - .../Popup/PopupRoleSurface+Presentation.swift | 446 ++++++++++++------ .../Internal/Popup/PopupRoleSurface.swift | 32 +- .../Surfaces/SurfaceOwnedResourceLedger.swift | 178 +++++++ .../Internal/Windowing/TopLevelWindow.swift | 66 +-- .../Windowing/WindowOwnedResourceLedger.swift | 117 ----- ...andDisplay+PopupSoftwarePresentation.swift | 47 ++ .../Public/Display/WaylandDisplay.swift | 20 - ...opupSurface+SoftwareFramePreparation.swift | 117 +++++ .../Public/Popup/PopupSurface.swift | 15 +- .../PopupPreparedPresentationModelTests.swift | 132 ++++++ .../SoftwareSurfacePresenterTests.swift | 2 +- .../SurfaceOwnedResourceLedgerTests.swift} | 10 +- 21 files changed, 1158 insertions(+), 529 deletions(-) create mode 100644 IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift create mode 100644 Sources/WaylandClient/Internal/Popup/PopupModelTypes.swift create mode 100644 Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Effects.swift create mode 100644 Sources/WaylandClient/Internal/Surfaces/SurfaceOwnedResourceLedger.swift delete mode 100644 Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift create mode 100644 Sources/WaylandClient/Public/Display/WaylandDisplay+PopupSoftwarePresentation.swift create mode 100644 Sources/WaylandClient/Public/Popup/PopupSurface+SoftwareFramePreparation.swift create mode 100644 Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift rename Tests/WaylandClientTests/{Windowing/WindowOwnedResourceLedgerTests.swift => Rendering/SurfaceOwnedResourceLedgerTests.swift} (89%) diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift new file mode 100644 index 00000000..0cdd9ec7 --- /dev/null +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift @@ -0,0 +1,86 @@ +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 + } + + 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] + } +} diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift index f561c4b5..95fb1516 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift @@ -268,58 +268,6 @@ struct WaylandPresentationAPISurfaceTests { _ = usePresentationFeedbackAPI _ = useManagedSurfacePresentationStreams } - - @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]) - ) - - 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] - } } @Suite("WaylandDisplay data transfer public API surface") diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift index 7c319858..6c108b0c 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift @@ -175,7 +175,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,7 +270,7 @@ 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( @@ -284,9 +290,12 @@ private func redraw( #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( diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+Popup.swift index 063fb326..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) } } diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift index a5bee5e0..27d51aa1 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift @@ -54,4 +54,36 @@ 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 SurfacePresentationFeedbackCommitRequest( + request: { + try popup.requestPresentationFeedbackOnOwnerThread( + presentation: presentation, + outputIDForPresentationSyncOutput: { output in + try session.outputIDForPresentationSyncOutput(output) + }, + onFeedback: { [weak self] feedback in + self?.eventHub.publishPresentation( + ManagedSurfacePresentationEvent( + surface: .popup(PopupSurfaceIdentity(popupID)), + feedback: feedback + ) + ) + } + ) + }, + cancel: { identity in + popup.cancelPresentationFeedbackOnOwnerThread(identity) + } + ) + } } diff --git a/Sources/WaylandClient/Internal/Popup/PopupModel.swift b/Sources/WaylandClient/Internal/Popup/PopupModel.swift index 5e081591..3912f151 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, @@ -315,6 +263,28 @@ 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 replacementAlreadyPublished = activeState.redraw.hasOutstandingRedrawRequest + let redrawEffects = activeState.redraw.supersedeSoftwarePresentation( + bufferAvailability: bufferAvailability + ) + guard !replacementAlreadyPublished else { return [] } + return Self.mapRedrawEffects(redrawEffects, event: event) + } + } + private mutating func reducePresentationSucceeded( generation: UInt64, bufferAvailability: RedrawBufferAvailability 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..b0982498 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,333 @@ 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, - _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> RedrawOutcome { - try interpretPopupEffects( - model.reduce(.presentationStarted(request)) - ) + package func reserveSoftwareFrameForShowOnOwnerThread( + timeoutMilliseconds: Int32, + metadata: SurfaceFrameMetadata + ) throws -> SoftwareSurfaceFrameReservationOutcome { + connection.preconditionIsOwnerThread() + guard !model.isClosed else { return .closed } + try validateSurfaceFrameMetadataSupport(metadata) + _ = try waitForInitialConfigure(timeoutMilliseconds: timeoutMilliseconds) + return try reserveSoftwareFrameForCurrentRedraw(metadata: metadata) + } - do { - guard pendingFrameRegistration == nil else { - failActivePresentation( - generation: request.generation, - error: .frameCallbackRequest("frame callback already pending") - ) - return .skippedPendingFrame - } + 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) + } - let geometry = try surfaceGeometry(logicalSize: request.placement.size) - let pool = try bufferPool(for: geometry.bufferSize) - dropReleasedRetiredPools() + // swiftlint:disable:next cyclomatic_complexity function_body_length + package func submitReservedSoftwareFrameOnOwnerThread( + _ reservation: SoftwareFrameReservation, + metadata: SurfaceFrameMetadata, + makePresentationFeedback: () throws -> SurfacePresentationFeedbackCommitRequest?, + _ draw: (borrowing SoftwareFrame) throws -> Void + ) throws -> SoftwarePresentationOutcome { + connection.preconditionIsOwnerThread() - guard var drawingBuffer = pool.acquireDrawingBuffer() else { - try interpretPopupEffects(model.reduce(.presentationBlockedByBuffer)) - return .waitingForBuffer - } + guard !model.isClosed else { return .closed } + guard let pendingReservation = softwarePresentationCoordinator.take(reservation) else { + return model.isClosed ? .closed : .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( - generation: request.generation, - error: .userDraw(String(describing: error)) - ) - drawingBuffer.discard() - throw error - } + let request = pendingReservation.request + do { + _ = try consumeLatestConfigureIfAvailable() + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } - guard !model.isClosed else { - try interpretPopupEffects(model.reduce(.transientStateReset)) - drawingBuffer.discard() - return .skippedClosed - } + guard !model.isClosed else { + pendingReservation.reservedFrame.drawingBuffer.discard() + return .closed + } - 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 - } + let currentGeometry = try currentGeometry() + guard + !Task.isCancelled, + model.isCurrentSoftwarePresentation(request), + currentGeometry == pendingReservation.geometry + else { + pendingReservation.reservedFrame.drawingBuffer.discard() + supersedeSoftwarePresentationIfStillActive(generation: request.generation) + return .superseded + } - do { - try reserveSurfaceFrameCallback(generation: request.generation) - } catch { - failActivePresentation( - generation: request.generation, - error: .frameCallbackRequest(String(describing: error)) - ) - drawingBuffer.discard() - throw error - } + let presentationFeedback: SurfacePresentationFeedbackCommitRequest? + do { + try validateSurfaceFrameMetadataSupport(metadata) + try metadata.damage?.validate(within: currentGeometry) + presentationFeedback = try makePresentationFeedback() + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive(generation: request.generation) + throw error + } - let stagedCommit: StagedSurfaceFrameCommit - let stagedSuccess: StagedPopupPresentationSuccess - do { - stagedCommit = try stageSurfaceFrameCommit(preparedCommit) - stagedSuccess = try PopupPresentationSuccessStagingContext( - model: model, - parentWindowID: parentWindowID - ).stage( + 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, - bufferAvailability: try redrawBufferAvailability() - ) - } catch { - pendingFrameRegistration = nil - cancelSurfaceFrameCallback() - 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 + } - _ = drawingBuffer.markBusy(commitGeneration: request.generation) - pendingFrameRegistration = requestReservedSurfaceFrameCallback { [weak self] in - self?.handleFrameDone() + if result.outcome == .presented { + guard let stagedSuccess else { + preconditionFailure("Popup presentation committed without staged success") } - commitSurfaceFrame(stagedCommit) model = stagedSuccess.model if stagedSuccess.publishesRedrawRequest { onRedrawRequested?() } return .presented - } catch { - failPresentationIfStillActive( - generation: request.generation, - error: .surfaceCommit(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 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 currentBufferAvailability = try redrawBufferAvailability() + var presentationRequest: PopupPresentationRequest? + try interpretPopupEffects( + model.reduce( + .redrawRequestConsumed(bufferAvailability: currentBufferAvailability) ) + ) { request in + presentationRequest = request + return .presented + } + + guard let request = presentationRequest else { + return model.isClosed ? .closed : .deferred + } + + try interpretPopupEffects(model.reduce(.presentationStarted(request))) + let geometry = try currentGeometry() + try metadata.damage?.validate(within: geometry) + + do { + let reservationID = softwarePresentationCoordinator.allocateIdentity() + let reservationResult = try softwarePresenter().reserve( + context: SoftwareSurfacePresentationContext( + generation: request.generation, + geometry: geometry, + submitConstraints: .default, + metadata: metadata.surfaceCommitMetadata, + damage: metadata.damage, + presentationFeedback: nil + ), + reservationID: reservationID, + runtime: &surfaceRuntime, + hasPendingFrameRegistration: pendingFrameRegistration != nil + ) + + if let followUp = reservationResult.followUp { + try interpretSoftwarePresentationFollowUp(followUp) + } + guard let reservedFrame = reservationResult.reservedFrame else { + return model.isClosed ? .closed : .deferred + } + + 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 + try bufferPool(for: bufferSize) + }, + 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 softwarePresentationOutcome( + for outcome: RedrawOutcome + ) -> SoftwarePresentationOutcome { + switch outcome { + case .presented: + return .presented + case .skippedClosed: + return .closed + case .skippedPendingFrame, .waitingForBuffer: + return .deferred + } } - private func destroyRoleObjects() throws { - onClose?() - onClose = nil - onRedrawRequested = nil + private func validateSurfaceFrameMetadataSupport( + _ metadata: SurfaceFrameMetadata + ) throws { + refreshMetadataCapabilitiesFromBoundGlobals() + try metadata.validate(capabilities: surfaceRuntime.capabilitySnapshot()) + } - destroyScaleResources() - try destroyRoleResources() + 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) } } 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/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/TopLevelWindow.swift b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift index 4d27c4f8..b3b12740 100644 --- a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift +++ b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift @@ -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 @@ -1960,17 +1964,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( @@ -2345,47 +2346,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/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/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.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 c77a59ab..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,19 +22,6 @@ 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) } diff --git a/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift b/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift new file mode 100644 index 00000000..83bde7e0 --- /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.contains(.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 == [.publishRedrawRequested(lifecycleEvent)]) + + let repeatedInvalidationEffects = try model.reduce( + .contentInvalidated(bufferAvailability: .available) + ) + #expect(repeatedInvalidationEffects.isEmpty) + + let supersessionEffects = try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) + #expect(supersessionEffects.isEmpty) + #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/SoftwareSurfacePresenterTests.swift b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift index 64ddf139..3631df1b 100644 --- a/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift +++ b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift @@ -7,7 +7,7 @@ @testable import WaylandClient @Suite - struct SoftwareSurfacePresentationCommitSequenceTests { + struct SoftwareSurfaceCommitSequenceTests { @Test(arguments: ManagedPresentationOperation.allCases) func pointOfNoReturnMarksBufferBusyBeforeProtocolRequests( operation: ManagedPresentationOperation 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? } From 16034a7d49ae90aec3e2bffe37b657f7b060421f Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:22:11 -0400 Subject: [PATCH 4/7] Add transactional subsurface presentation --- ...edSurfacePresentationAPISurfaceTests.swift | 27 ++ ...PresentationFeedbackIntegrationTests.swift | 192 +++++++++ ...dDisplayPresentationIntegrationTests.swift | 26 ++ ...WaylandDisplayPublicIntegrationTests.swift | 2 +- .../DisplayCore+PresentationFeedback.swift | 52 ++- .../Display/DisplayCore+Subsurface.swift | 99 +++-- .../Internal/Display/DisplayCore.swift | 4 +- .../Popup/PopupRoleSurface+Presentation.swift | 76 +++- .../Internal/Subsurface/SubsurfaceModel.swift | 252 ++++++++++++ .../SubsurfaceRoleSurface+Effects.swift | 41 ++ .../SubsurfaceRoleSurface+Presentation.swift | 386 ++++++++++++++++++ .../Subsurface/SubsurfaceRoleSurface.swift | 281 +++++-------- .../Surfaces/SurfaceMetadataSupport.swift | 161 ++++++++ .../PresentationState+Requests.swift | 1 + .../SoftwareSurfacePresentationTypes.swift | 29 +- .../Windowing/SoftwareSurfacePresenter.swift | 17 +- .../Internal/Windowing/TopLevelWindow.swift | 136 +----- .../Display/WaylandDisplay+Subsurface.swift | 16 - ...splay+SubsurfaceSoftwarePresentation.swift | 48 +++ .../Subsurface+SoftwareFramePreparation.swift | 133 ++++++ .../Public/Subsurface/Subsurface.swift | 28 +- .../Window+SoftwareFramePreparation.swift | 14 +- .../SoftwareSurfacePresenterTests.swift | 20 +- .../Subsurface/SubsurfaceModelTests.swift | 183 +++++++++ .../SubsurfacePublicRequestTests.swift | 66 ++- ...ftwarePresentationPublicRequestTests.swift | 4 +- 26 files changed, 1858 insertions(+), 436 deletions(-) create mode 100644 IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift create mode 100644 Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift create mode 100644 Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Effects.swift create mode 100644 Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift create mode 100644 Sources/WaylandClient/Internal/Surfaces/SurfaceMetadataSupport.swift create mode 100644 Sources/WaylandClient/Public/Display/WaylandDisplay+SubsurfaceSoftwarePresentation.swift create mode 100644 Sources/WaylandClient/Public/Subsurface/Subsurface+SoftwareFramePreparation.swift create mode 100644 Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift index 0cdd9ec7..4f71c80f 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationAPISurfaceTests.swift @@ -15,6 +15,7 @@ struct ManagedSurfacePresentationAPISurfaceTests { #expect(Set(outcomes).count == 4) _ = useAtomicWindowPresentationAPI _ = useAtomicPopupPresentationAPI + _ = useAtomicSubsurfacePresentationAPI } private func useAtomicWindowPresentationAPI( @@ -83,4 +84,30 @@ struct ManagedSurfacePresentationAPISurfaceTests { ) 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..8d1d1748 --- /dev/null +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift @@ -0,0 +1,192 @@ +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 popupAndSubsurfaceFeedbackReachRootAndFilteredStreams() 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() + ) + let windowOutcome = try await window.show { frame in + fill(frame, color: 0x0012_2436) + } + #expect(windowOutcome == .presented) + + 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 27b413f9..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 diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift index 6c108b0c..521fdde6 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift @@ -151,7 +151,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) diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift index 27d51aa1..513822af 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+PresentationFeedback.swift @@ -64,25 +64,43 @@ extension DisplayCore { let session = try requireSession() let presentation = try session.presentationOnOwnerThread() - return SurfacePresentationFeedbackCommitRequest( - request: { - try popup.requestPresentationFeedbackOnOwnerThread( - presentation: presentation, - outputIDForPresentationSyncOutput: { output in - try session.outputIDForPresentationSyncOutput(output) - }, - onFeedback: { [weak self] feedback in - self?.eventHub.publishPresentation( - ManagedSurfacePresentationEvent( - surface: .popup(PopupSurfaceIdentity(popupID)), - feedback: feedback - ) - ) - } + 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) }, - cancel: { identity in - popup.cancelPresentationFeedbackOnOwnerThread(identity) + 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..ab8ee96f 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift @@ -13,6 +13,10 @@ extension DisplayCore { 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 +24,82 @@ 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.commitSubsurfaceParentStateOnOwnerThread() + }, + { 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 +182,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 a02f52c8..e7fece31 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore.swift @@ -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) @@ -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/Popup/PopupRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift index b0982498..93835abf 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift @@ -120,6 +120,12 @@ extension PopupRoleSurface { 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() @@ -191,6 +197,35 @@ extension PopupRoleSurface { ) } + 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 + ) + }, + cancel: { identity in + coordinator.cancel(identity) + } + ) + } + package func requestPresentationFeedbackOnOwnerThread( presentation: RawPresentation, outputIDForPresentationSyncOutput: @@ -222,6 +257,12 @@ extension PopupRoleSurface { private func reserveSoftwareFrameForCurrentRedraw( metadata: SurfaceFrameMetadata ) throws -> SoftwareSurfaceFrameReservationOutcome { + try SurfaceMetadataSupport.ensureObjectsInstalled( + for: metadata.surfaceCommitMetadata, + connection: connection, + surface: surface, + runtime: &surfaceRuntime + ) let currentBufferAvailability = try redrawBufferAvailability() var presentationRequest: PopupPresentationRequest? try interpretPopupEffects( @@ -287,7 +328,16 @@ extension PopupRoleSurface { surface: surface, scaleInstallation: scaleInstallation, createSharedMemoryPool: { [self] bufferSize in - try bufferPool(for: bufferSize) + 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() } @@ -356,26 +406,10 @@ extension PopupRoleSurface { private func validateSurfaceFrameMetadataSupport( _ metadata: SurfaceFrameMetadata ) throws { - refreshMetadataCapabilitiesFromBoundGlobals() - try metadata.validate(capabilities: surfaceRuntime.capabilitySnapshot()) - } - - 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 + try SurfaceMetadataSupport.validate( + metadata, + connection: connection, + runtime: &surfaceRuntime ) - surfaceRuntime.setColorCapability(extensions.surfaceColorCapability) } } diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift new file mode 100644 index 00000000..9215fd57 --- /dev/null +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift @@ -0,0 +1,252 @@ +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 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 .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 [] } + return map( + redraw.reduce(.contentInvalidated, bufferAvailability: bufferAvailability) + ) + } + + 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 replacementAlreadyPublished = redraw.hasOutstandingRedrawRequest + let effects = redraw.supersedeSoftwarePresentation( + bufferAvailability: bufferAvailability + ) + return replacementAlreadyPublished ? [] : 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..3ebfcf47 --- /dev/null +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift @@ -0,0 +1,386 @@ +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 + } + 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] _ in + self?.surfaceRuntime.resetTransientTransactionState() + } + 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] _ in + self?.surfaceRuntime.resetTransientTransactionState() + } + } + + package func cancelPresentationFeedbackOnOwnerThread(_ identity: SurfacePresentationIdentity) { + connection.preconditionIsOwnerThread() + presentationFeedbackCoordinator.cancel(identity) + } +} + +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 b4819425..064864db 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,14 +14,19 @@ 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 + 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( @@ -37,7 +40,9 @@ package final class SubsurfaceRoleSurface { connection = rawConnection bufferCount = subsurfaceConfiguration.bufferCount size = subsurfaceConfiguration.size - synchronizationMode = subsurfaceConfiguration.synchronizationMode + model = SubsurfaceModel( + synchronizationMode: subsurfaceConfiguration.synchronizationMode + ) let globals = try rawConnection.bindRequiredGlobals() let rawObjects = try rawConnection.createManagedSubsurface( @@ -72,17 +77,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 +97,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 +129,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 +139,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 +152,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 +215,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 +276,6 @@ extension SubsurfaceRoleSurface { } private func applySynchronizationMode(_ mode: SubsurfaceSynchronizationMode) { - synchronizationMode = mode switch mode { case .synchronized: subsurface.setSynchronized() @@ -270,7 +288,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 +303,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: SoftwareSurfacePresentationResult - do { - result = try 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: { [self] in isClosed }, - onFrame: { [weak self] in - self?.handleFrameDone() - } - ).present( - context: SoftwareSurfacePresentationContext( - generation: request.generation, - 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: SoftwareSurfacePresentationFollowUp? - ) 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? SoftwareSurfacePresentationFailure { - cause = .presentation(failure.presentationError) - } else if let failure = error as? SoftwareSurfaceDrawFailure { - 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 +322,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 +364,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 +383,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 +403,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 +443,7 @@ extension SubsurfaceRoleSurface { SubsurfaceParentCommitPolicy.requirement( parentWindowID: parentWindowID, subsurfaceID: id, - event: .surfaceStateCommitted(synchronizationMode) + event: .surfaceStateCommitted(model.synchronizationMode) ) } @@ -520,21 +456,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/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 index 9d89bac5..95e5f313 100644 --- a/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift +++ b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresentationTypes.swift @@ -61,13 +61,17 @@ package enum SoftwareSurfacePresentationCommitSequence { markDrawingBufferBusy: () -> Void, requestFrameCallback: () -> Void, requestPresentationFeedback: () -> SurfacePresentationIdentity?, - commit: () -> Void + commitSurface: () -> Void, + commitFollowUp: () -> Void = { + // No role-level commit is required. + } ) rethrows -> SurfacePresentationIdentity? { try stageSuccess() markDrawingBufferBusy() requestFrameCallback() let feedbackIdentity = requestPresentationFeedback() - commit() + commitSurface() + commitFollowUp() return feedbackIdentity } } @@ -79,4 +83,25 @@ struct SoftwareSurfacePresentationContext { 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/SoftwareSurfacePresenter.swift b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift index e2cc62de..a11b9322 100644 --- a/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift +++ b/Sources/WaylandClient/Internal/Windowing/SoftwareSurfacePresenter.swift @@ -5,6 +5,7 @@ private struct SoftwareSurfaceCommitContext { let generation: UInt64 let bufferSize: TopLevelSize let presentationFeedback: SurfacePresentationFeedbackCommitRequest? + let commitFollowUp: () -> Void } // swiftlint:disable:next type_body_length @@ -63,7 +64,8 @@ struct SoftwareSurfacePresenter { preparedCommit: preparedCommit, generation: context.generation, bufferSize: context.geometry.bufferSize.rawSize, - presentationFeedback: context.presentationFeedback + presentationFeedback: context.presentationFeedback, + commitFollowUp: context.commitFollowUp ), stageSuccess: stageSuccess, runtime: &runtime, @@ -165,7 +167,8 @@ struct SoftwareSurfacePresenter { preparedCommit: preparedCommit, generation: context.generation, bufferSize: context.geometry.bufferSize.rawSize, - presentationFeedback: context.presentationFeedback + presentationFeedback: context.presentationFeedback, + commitFollowUp: context.commitFollowUp ), stageSuccess: stageSuccess, runtime: &runtime, @@ -223,12 +226,13 @@ struct SoftwareSurfacePresenter { context.presentationFeedback ) }, - commit: { + commitSurface: { SurfaceFrameCommitter.commit( stagedCommit, runtime: &runtime, ) - } + }, + commitFollowUp: context.commitFollowUp ) } catch { runtime.cancelFrameCallback() @@ -444,12 +448,13 @@ struct SoftwareSurfacePresenter { 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 b3b12740..ef72a398 100644 --- a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift +++ b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift @@ -544,6 +544,7 @@ package final class TopLevelWindow { { guard !model.isClosed else { return .closed } try validateSurfaceFrameMetadataSupport(frameMetadata) + try ensureMetadataObjectsInstalled(for: frameMetadata.surfaceCommitMetadata) guard let request = try consumeSoftwarePresentationRequest() else { return .deferred } try interpretWindowEffects( @@ -773,7 +774,7 @@ package final class TopLevelWindow { ) } - // swiftlint:disable:next function_parameter_count + // swiftlint:disable:next function_parameter_count function_body_length private func submitReservedSoftwareFrame( _ reservation: SoftwareFrameReservation, submitConstraints: SurfaceSubmitConstraints, @@ -797,6 +798,7 @@ package final class TopLevelWindow { return try supersedeSoftwarePresentation(pendingReservation) } try validateSurfaceFrameMetadataSupport(frameMetadata) + try ensureMetadataObjectsInstalled(for: frameMetadata.surfaceCommitMetadata) let successStagingContext = softwarePresentationSuccessStagingContext() let presentationFeedback = try makePresentationFeedback() @@ -1802,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( @@ -1835,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 ) } 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/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 8483b184..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,32 +26,6 @@ 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) } diff --git a/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift index 567e9b66..60c5bea2 100644 --- a/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift +++ b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift @@ -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/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift index 3631df1b..4764bb49 100644 --- a/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift +++ b/Tests/WaylandClientTests/Rendering/SoftwareSurfacePresenterTests.swift @@ -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), ] ) } @@ -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") @@ -440,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/Subsurface/SubsurfaceModelTests.swift b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift new file mode 100644 index 00000000..70e56371 --- /dev/null +++ b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift @@ -0,0 +1,183 @@ +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 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)) + == [.publishRedrawRequested] + ) + #expect(!model.isCurrentSoftwarePresentation(request, geometry: replacementGeometry)) + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ).isEmpty + ) + #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 + ) + ) == [.publishRedrawRequested] + ) + #expect(model.synchronizationMode == .desynchronized) + #expect(!model.isCurrentSoftwarePresentation(request, geometry: geometry)) + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ).isEmpty + ) + } + + @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..96bc0bb1 100644 --- a/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift +++ b/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift @@ -38,8 +38,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 +58,43 @@ 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 setPositionCommitsParentAfterParentWasDrawn() async throws { try await withSubsurfaceConnection { display, window in @@ -381,6 +415,34 @@ } } + 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 enum SubsurfaceRequestTestEnvironment { static var isEnabled: Bool { let environment = ProcessInfo.processInfo.environment diff --git a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift index bbab6822..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 SoftwareSurfaceDrawFailure { - #expect(failure.underlying is InjectedSoftwareDrawFailure) + } catch is InjectedSoftwareDrawFailure { + // The original draw error must remain observable. } } } From 7f5fee314d76409d09ba495429cca906ad1e852d Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:44:31 -0400 Subject: [PATCH 5/7] Add managed surface smoke and migration guide --- .../ManagedSurfacePresentationSmoke.swift | 19 +++ .../ManagedSurfaceSmokePresentations.swift | 95 ++++++++++++++ .../ManagedSurfaceSmokeRedraw.swift | 111 +++++++++++++++++ .../ManagedSurfaceSmokeRun.swift | 101 +++++++++++++++ Examples/Package.swift | 6 + .../WaylandFrameworkHostClientTests.swift | 55 ++++++++ README.md | 4 +- .../Presentation/PresentationFeedback.swift | 7 ++ .../Rendering/ManagedSurfaceIdentity.swift | 3 + .../Window+SoftwareFramePreparation.swift | 6 +- .../EventStreamsAndOverflow.md | 7 +- .../ManagedSurfaceDrawing.md | 80 ++++++++++++ .../ManagedSurfacePresentationMigration.md | 50 ++++++++ .../PresentationFeedbackAndFrameCallbacks.md | 48 ++++--- .../WaylandClient.docc/Subsurfaces.md | 45 ++++--- .../SurfaceRegionsAndDamage.md | 10 +- .../WaylandClient.docc/WaylandClient.md | 14 ++- .../WaylandClient.docc/WindowDrawing.md | 73 ----------- .../DocumentationStructureVerifier.swift | 3 +- .../ExampleBuilder.swift | 1 + .../ToolingConvergenceTests.swift | 3 +- docs/compositor-matrix.md | 16 ++- docs/documentation-symbol-coverage.json | 4 +- docs/public-api-audit.md | 30 +++-- docs/public-api-baseline.md | 117 ++++++++++-------- docs/support-matrix.md | 3 +- docs/which-api-should-i-use.md | 9 +- 27 files changed, 718 insertions(+), 202 deletions(-) create mode 100644 Examples/ManagedSurfacePresentationSmoke/ManagedSurfacePresentationSmoke.swift create mode 100644 Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift create mode 100644 Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift create mode 100644 Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRun.swift create mode 100644 Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md create mode 100644 Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md delete mode 100644 Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md 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..8eb2e772 --- /dev/null +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift @@ -0,0 +1,95 @@ +import WaylandClient + +extension ManagedSurfaceSmokeRun { + func presentWindow(_ window: Window, requestsFeedback: Bool) async throws { + 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 requireFeedback(from: &feedback, label: "window") + } + print("window initial presentation: presented") + } + + func presentPopup(_ popup: PopupSurface, requestsFeedback: Bool) async throws { + 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 requireFeedback(from: &feedback, label: "popup") + } + print("popup initial presentation: presented") + } + + func presentSubsurface( + _ subsurface: Subsurface, + label: String, + color: UInt32, + requestsFeedback: Bool + ) async throws { + 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 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)") + } +} diff --git a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift new file mode 100644 index 00000000..c18478b4 --- /dev/null +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift @@ -0,0 +1,111 @@ +import WaylandClient + +extension ManagedSurfaceSmokeRun { + 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/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/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/Public/Presentation/PresentationFeedback.swift b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift index e9c3b568..2b68f808 100644 --- a/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift +++ b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift @@ -68,10 +68,14 @@ public struct PresentationFeedback: Equatable, Sendable { } } +/// 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( surface eventSurface: ManagedSurfaceIdentity, feedback eventFeedback: SurfacePresentationFeedback @@ -111,6 +115,7 @@ public struct ManagedSurfacePresentationEvents: AsyncSequence, Sendable { } } +/// An iterator over presentation feedback for one managed surface. @safe public struct ManagedSurfacePresentationEventsIterator: AsyncIteratorProtocol { public typealias Element = SurfacePresentationFeedback @@ -127,10 +132,12 @@ public struct ManagedSurfacePresentationEventsIterator: AsyncIteratorProtocol { 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? { diff --git a/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift b/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift index 9606e676..e6db4b86 100644 --- a/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift +++ b/Sources/WaylandClient/Public/Rendering/ManagedSurfaceIdentity.swift @@ -1,6 +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/Window/Window+SoftwareFramePreparation.swift b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift index 60c5bea2..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 } diff --git a/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md b/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md index 2a69935d..2a397432 100644 --- a/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md +++ b/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md @@ -5,11 +5,16 @@ 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(_:)`` with a ``ManagedSurfaceIdentity``. Presentation +feedback for all three roles uses ``DisplayEvent/presentation(_:)`` with a +``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..321cd4c6 --- /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(_:)`` 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..92f6e526 --- /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..642a8562 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(_:)``: -`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 or display close terminates outstanding feedback and eventually 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/Subsurfaces.md b/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md index 831c2ead..771d3490 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(_:)`` with ``ManagedSurfaceIdentity/subsurface(_:)``. + +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/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/docs/compositor-matrix.md b/docs/compositor-matrix.md index 75286e4b..587c76d4 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,18 @@ 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 default smoke. + ## 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..ad2b0339 100644 --- a/docs/documentation-symbol-coverage.json +++ b/docs/documentation-symbol-coverage.json @@ -1,8 +1,8 @@ { "products" : { "WaylandClient" : { - "documented" : 176, - "eligible" : 1547 + "documented" : 182, + "eligible" : 1552 }, "WaylandGraphicsPreview" : { "documented" : 32, diff --git a/docs/public-api-audit.md b/docs/public-api-audit.md index c5d5681b..36c779f6 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. 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/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) | From cafdbc0e245f6be9ba54ce72fa5ff4ad578da2d3 Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:16:19 -0400 Subject: [PATCH 6/7] Close managed presentation review gaps --- .../ManagedSurfaceSmokePresentations.swift | 34 +++++ .../ManagedSurfaceSmokeRedraw.swift | 22 +++ .../ManagedSurfaceSmokeRun.swift | 1 + ...PresentationFeedbackIntegrationTests.swift | 13 +- ...WaylandDisplayPublicIntegrationTests.swift | 30 ++++ .../Display/DisplayCore+Subsurface.swift | 3 +- .../Internal/Popup/PopupModel.swift | 17 ++- .../Popup/PopupRoleSurface+Presentation.swift | 13 +- .../Internal/Subsurface/SubsurfaceModel.swift | 7 +- .../SubsurfaceRoleSurface+Presentation.swift | 1 + .../Internal/Windowing/TopLevelWindow.swift | 13 ++ .../EventStreamsAndOverflow.md | 5 +- .../ManagedSurfaceDrawing.md | 2 +- .../ManagedSurfacePresentationMigration.md | 2 +- .../PresentationFeedbackAndFrameCallbacks.md | 2 +- .../WaylandClient.docc/SessionReadiness.md | 2 +- .../WaylandClient.docc/Subsurfaces.md | 2 +- .../IdentityGeneration.swift | 4 + .../IdentityGenerationModels.swift | 8 +- .../PublicIdentityAuditor.swift | 143 ++++++++++++------ .../PublicIdentityAuditorTests.swift | 9 ++ .../PopupPreparedPresentationModelTests.swift | 8 +- .../Subsurface/SubsurfaceModelTests.swift | 11 +- .../SubsurfacePublicRequestTests.swift | 33 ++++ docs/documentation-symbol-coverage.json | 2 +- docs/identity-categories.json | 6 +- docs/identity-visibility.md | 6 + docs/public-api-audit.md | 8 +- docs/release.md | 10 ++ identities/manual-identity-categories.json | 16 ++ 30 files changed, 338 insertions(+), 95 deletions(-) diff --git a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift index 8eb2e772..7a8353bb 100644 --- a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokePresentations.swift @@ -2,6 +2,7 @@ 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 @@ -10,12 +11,18 @@ extension ManagedSurfaceSmokeRun { } 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( @@ -26,6 +33,11 @@ extension ManagedSurfaceSmokeRun { } 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") @@ -37,6 +49,7 @@ extension ManagedSurfaceSmokeRun { 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( @@ -47,6 +60,11 @@ extension ManagedSurfaceSmokeRun { } 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") @@ -92,4 +110,20 @@ extension ManagedSurfaceSmokeRun { } 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 index c18478b4..c65cdc5a 100644 --- a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRedraw.swift @@ -1,6 +1,28 @@ 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() diff --git a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRun.swift b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRun.swift index 3836c8fc..54c1414d 100644 --- a/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRun.swift +++ b/Examples/ManagedSurfacePresentationSmoke/ManagedSurfaceSmokeRun.swift @@ -45,6 +45,7 @@ struct ManagedSurfaceSmokeRun { if CommandLine.arguments.contains("--skip-redraw-routing") { print("redraw routing: skipped for compositor evidence run") } else { + try await routeRedraw(for: window, color: 0x0040_6080) try await routeRedraw(for: popup, color: 0x0080_50A0) try await routeRedraw(for: synchronized, color: 0x0050_A080) try await routeRedraw(for: desynchronized, color: 0x00A0_8050) diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift index 8d1d1748..61af5fd0 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/ManagedSurfacePresentationFeedbackIntegrationTests.swift @@ -43,7 +43,7 @@ struct ManagedPresentationFeedbackTests { } @Test - func popupAndSubsurfaceFeedbackReachRootAndFilteredStreams() async throws { + func allManagedFeedbackReachesRootAndFilteredStreams() async throws { try await withPublicConnection { display in try await exerciseManagedFeedback(on: display) } @@ -62,10 +62,15 @@ struct ManagedPresentationFeedbackTests { let window = try await display.createTopLevelWindow( configuration: testWindowConfiguration() ) - let windowOutcome = try await window.show { frame in - fill(frame, color: 0x0012_2436) + 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) + } } - #expect(windowOutcome == .presented) let popup = try await window.createPopup(configuration: testPopupConfiguration()) try await expectFeedback( diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift index 521fdde6..952b9ffe 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicIntegrationTests.swift @@ -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( diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift index ab8ee96f..7c7e8cb9 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift @@ -84,7 +84,8 @@ extension DisplayCore { ) }, commitSynchronizedParent: { - parentWindow.commitSubsurfaceParentStateOnOwnerThread() + parentWindow + .commitSynchronizedSubsurfacePresentationParentStateOnOwnerThread() }, { frame in try draw(prepared, frame) diff --git a/Sources/WaylandClient/Internal/Popup/PopupModel.swift b/Sources/WaylandClient/Internal/Popup/PopupModel.swift index 3912f151..1ff690e9 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupModel.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupModel.swift @@ -165,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) @@ -276,11 +281,9 @@ extension PopupModel { windowID: windowID ) activeState.presentation = .idle - let replacementAlreadyPublished = activeState.redraw.hasOutstandingRedrawRequest let redrawEffects = activeState.redraw.supersedeSoftwarePresentation( bufferAvailability: bufferAvailability ) - guard !replacementAlreadyPublished else { return [] } return Self.mapRedrawEffects(redrawEffects, event: event) } } @@ -331,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/PopupRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift index 93835abf..de40bb79 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift @@ -63,7 +63,11 @@ extension PopupRoleSurface { connection.preconditionIsOwnerThread() guard !model.isClosed else { return .closed } try validateSurfaceFrameMetadataSupport(metadata) - _ = try waitForInitialConfigure(timeoutMilliseconds: timeoutMilliseconds) + if model.currentPlacement == nil { + _ = try waitForInitialConfigure(timeoutMilliseconds: timeoutMilliseconds) + } else { + _ = try consumeLatestConfigureIfAvailable() + } return try reserveSoftwareFrameForCurrentRedraw(metadata: metadata) } @@ -92,8 +96,10 @@ extension PopupRoleSurface { } let request = pendingReservation.request + let currentGeometry: SurfaceGeometry do { _ = try consumeLatestConfigureIfAvailable() + currentGeometry = try self.currentGeometry() } catch { pendingReservation.reservedFrame.drawingBuffer.discard() failSoftwarePresentationIfStillActive(generation: request.generation) @@ -105,7 +111,6 @@ extension PopupRoleSurface { return .closed } - let currentGeometry = try currentGeometry() guard !Task.isCancelled, model.isCurrentSoftwarePresentation(request), @@ -257,6 +262,8 @@ extension PopupRoleSurface { 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, @@ -279,8 +286,6 @@ extension PopupRoleSurface { } try interpretPopupEffects(model.reduce(.presentationStarted(request))) - let geometry = try currentGeometry() - try metadata.damage?.validate(within: geometry) do { let reservationID = softwarePresentationCoordinator.allocateIdentity() diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift index 9215fd57..ab7cc394 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift @@ -133,8 +133,10 @@ extension SubsurfaceModel { bufferAvailability: RedrawBufferAvailability ) -> [SubsurfaceEffect] { guard lifecycle == .active else { return [] } + let redrawAvailability = + presentation.isIdle ? bufferAvailability : .unavailable return map( - redraw.reduce(.contentInvalidated, bufferAvailability: bufferAvailability) + redraw.reduce(.contentInvalidated, bufferAvailability: redrawAvailability) ) } @@ -200,11 +202,10 @@ extension SubsurfaceModel { ) throws -> [SubsurfaceEffect] { try requireDrawingGeneration(generation) presentation = .idle - let replacementAlreadyPublished = redraw.hasOutstandingRedrawRequest let effects = redraw.supersedeSoftwarePresentation( bufferAvailability: bufferAvailability ) - return replacementAlreadyPublished ? [] : map(effects) + return map(effects) } private mutating func completePresentation( diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift index 3ebfcf47..72901e0d 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift @@ -167,6 +167,7 @@ extension SubsurfaceRoleSurface { guard let pendingReservation = softwarePresentationCoordinator.cancel(reservation) else { return } + pendingReservation.reservedFrame.drawingBuffer.discard() supersedeSoftwarePresentationIfStillActive( generation: pendingReservation.request.generation ) diff --git a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift index ef72a398..5986cbb0 100644 --- a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift +++ b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift @@ -2110,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?() diff --git a/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md b/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md index 2a397432..0ca926aa 100644 --- a/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md +++ b/Sources/WaylandClient/WaylandClient.docc/EventStreamsAndOverflow.md @@ -11,9 +11,8 @@ preserves ordering across event families. A specialized stream preserves order only within its own family. Window, popup, and subsurface redraws all use -``DisplayEvent/redrawRequested(_:)`` with a ``ManagedSurfaceIdentity``. Presentation -feedback for all three roles uses ``DisplayEvent/presentation(_:)`` with a -``ManagedSurfacePresentationEvent``. +`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 diff --git a/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md index 321cd4c6..1a761084 100644 --- a/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md +++ b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfaceDrawing.md @@ -30,7 +30,7 @@ Prepared presentation reserves one exact redraw generation. After preparation re - 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(_:)`` when pacing permits. +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: diff --git a/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md index 92f6e526..36c1978d 100644 --- a/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md +++ b/Sources/WaylandClient/WaylandClient.docc/ManagedSurfacePresentationMigration.md @@ -26,7 +26,7 @@ Popup lifecycle events continue to carry ``PopupLifecycleEvent`` through `popupD ## 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. +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. diff --git a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md index 642a8562..4c612e0d 100644 --- a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md +++ b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md @@ -4,7 +4,7 @@ Frame callbacks and presentation feedback serve different purposes. Frame callba 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. -Every feedback result enters the root stream as ``DisplayEvent/presentation(_:)``: +Every feedback result enters the root stream as `DisplayEvent.presentation(ManagedSurfacePresentationEvent)`: ```swift case .presentation(let event): 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 771d3490..4ffb4ecd 100644 --- a/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md +++ b/Sources/WaylandClient/WaylandClient.docc/Subsurfaces.md @@ -6,7 +6,7 @@ Use ``Window/createSubsurface(configuration:)`` when a child needs its own conte ## 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(_:)`` with ``ManagedSurfaceIdentity/subsurface(_:)``. +``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. 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/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/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift b/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift index 83bde7e0..dc482518 100644 --- a/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift +++ b/Tests/WaylandClientTests/Popup/PopupPreparedPresentationModelTests.swift @@ -16,7 +16,7 @@ struct PopupPreparedPresentationModelTests { .configureReceived(configure(serial: 2, originX: 99)) ) #expect(!model.isCurrentSoftwarePresentation(request)) - #expect(configureEffects.contains(.publishRedrawRequested(lifecycleEvent))) + #expect(!configureEffects.contains(.publishRedrawRequested(lifecycleEvent))) let supersessionEffects = try model.reduce( .softwarePresentationSuperseded( @@ -26,7 +26,7 @@ struct PopupPreparedPresentationModelTests { ) #expect(model.presentation == .idle) #expect(model.redraw.isDirty) - #expect(!supersessionEffects.contains(.publishRedrawRequested(lifecycleEvent))) + #expect(supersessionEffects == [.publishRedrawRequested(lifecycleEvent)]) } @Test @@ -38,7 +38,7 @@ struct PopupPreparedPresentationModelTests { .contentInvalidated(bufferAvailability: .available) ) #expect(!model.isCurrentSoftwarePresentation(request)) - #expect(invalidationEffects == [.publishRedrawRequested(lifecycleEvent)]) + #expect(invalidationEffects.isEmpty) let repeatedInvalidationEffects = try model.reduce( .contentInvalidated(bufferAvailability: .available) @@ -51,7 +51,7 @@ struct PopupPreparedPresentationModelTests { bufferAvailability: .available ) ) - #expect(supersessionEffects.isEmpty) + #expect(supersessionEffects == [.publishRedrawRequested(lifecycleEvent)]) #expect(model.redraw.isDirty) } diff --git a/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift index 70e56371..9a4919ec 100644 --- a/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift +++ b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift @@ -45,10 +45,7 @@ struct SubsurfaceModelTests { let replacementGeometry = try geometry(width: 96) let request = try startPresentation(in: &model, geometry: originalGeometry) - #expect( - try model.reduce(.scaleChanged(bufferAvailability: .available)) - == [.publishRedrawRequested] - ) + #expect(try model.reduce(.scaleChanged(bufferAvailability: .available)).isEmpty) #expect(!model.isCurrentSoftwarePresentation(request, geometry: replacementGeometry)) #expect( try model.reduce( @@ -56,7 +53,7 @@ struct SubsurfaceModelTests { generation: request.generation, bufferAvailability: .available ) - ).isEmpty + ) == [.publishRedrawRequested] ) #expect(model.presentation == .idle) #expect(model.redraw.isDirty) @@ -74,7 +71,7 @@ struct SubsurfaceModelTests { .desynchronized, bufferAvailability: .available ) - ) == [.publishRedrawRequested] + ).isEmpty ) #expect(model.synchronizationMode == .desynchronized) #expect(!model.isCurrentSoftwarePresentation(request, geometry: geometry)) @@ -84,7 +81,7 @@ struct SubsurfaceModelTests { generation: request.generation, bufferAvailability: .available ) - ).isEmpty + ) == [.publishRedrawRequested] ) } diff --git a/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift b/Tests/WaylandClientTests/Subsurface/SubsurfacePublicRequestTests.swift index 96bc0bb1..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, @@ -95,6 +98,31 @@ } } + @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 @@ -321,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 ) } @@ -442,6 +472,7 @@ } private struct UnexpectedSubsurfaceDraw: Error {} + private struct InjectedPreparationFailure: Error {} private enum SubsurfaceRequestTestEnvironment { static var isEnabled: Bool { @@ -452,4 +483,6 @@ } } +// swiftlint:enable type_body_length + #endif diff --git a/docs/documentation-symbol-coverage.json b/docs/documentation-symbol-coverage.json index ad2b0339..2a1409ee 100644 --- a/docs/documentation-symbol-coverage.json +++ b/docs/documentation-symbol-coverage.json @@ -1,7 +1,7 @@ { "products" : { "WaylandClient" : { - "documented" : 182, + "documented" : 183, "eligible" : 1552 }, "WaylandGraphicsPreview" : { 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 36c779f6..e4e31e41 100644 --- a/docs/public-api-audit.md +++ b/docs/public-api-audit.md @@ -497,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 @@ -532,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/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/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" } ] } From bd6ae41e2499da7bb97af9cff7c0ab699934c377 Mon Sep 17 00:00:00 2001 From: conner <83147518+conxlgtm@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:48:11 -0400 Subject: [PATCH 7/7] Close managed presentation review findings --- .../Display/DisplayCore+Subsurface.swift | 3 +- .../Internal/Display/DisplayEventHub.swift | 4 ++- .../Display/DisplaySession+Subsurface.swift | 6 ++-- .../Internal/Events/EventBroker.swift | 14 +++++--- .../Internal/Subsurface/SubsurfaceModel.swift | 3 ++ .../SubsurfaceRoleSurface+Presentation.swift | 32 ++++++++++++++--- .../Subsurface/SubsurfaceRoleSurface.swift | 5 ++- .../Public/Display/DisplayEvent.swift | 9 +++-- .../Presentation/PresentationFeedback.swift | 4 ++- .../PresentationFeedbackAndFrameCallbacks.md | 2 +- .../DisplayEventHubPresentationTests.swift | 35 +++++++++++++++++++ .../Subsurface/SubsurfaceModelTests.swift | 26 ++++++++++++++ docs/compositor-matrix.md | 3 +- 13 files changed, 128 insertions(+), 18 deletions(-) diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift index 7c7e8cb9..477ddc20 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore+Subsurface.swift @@ -7,7 +7,8 @@ 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() diff --git a/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift b/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift index df102c29..58c087cb 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayEventHub.swift @@ -82,7 +82,9 @@ final class DisplayEventHub: Sendable { ) -> ManagedSurfacePresentationEvents { ManagedSurfacePresentationEvents( surface: surface, - subscriptions: InternalEventSubscriptionFactory(presentationBroker) + subscriptions: InternalEventSubscriptionFactory(presentationBroker) { event in + event.surface == surface + } ) } 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/Subsurface/SubsurfaceModel.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift index ab7cc394..68baf195 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceModel.swift @@ -33,6 +33,7 @@ enum SubsurfaceEvent: Equatable, Sendable { generation: UInt64, bufferAvailability: RedrawBufferAvailability ) + case presentationFeedbackFailed case transientStateReset case explicitClose case parentWindowClosed @@ -119,6 +120,8 @@ struct SubsurfaceModel: Equatable, Sendable { generation: generation, bufferAvailability: bufferAvailability ) + case .presentationFeedbackFailed: + return [] case .transientStateReset: guard lifecycle == .active else { return [] } presentation = .idle diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift index 72901e0d..ec1d82a9 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface+Presentation.swift @@ -183,8 +183,8 @@ extension SubsurfaceRoleSurface { let feedbackSurface = surface let coordinator = presentationFeedbackCoordinator - let onFailure: (any Error) -> Void = { [weak self] _ in - self?.surfaceRuntime.resetTransientTransactionState() + let onFailure: (any Error) -> Void = { [weak self] error in + self?.reportPresentationFeedbackFailure(error) } return SurfacePresentationFeedbackCommitRequest( request: { @@ -215,8 +215,8 @@ extension SubsurfaceRoleSurface { surface: surface, outputIDForPresentationSyncOutput: outputIDForPresentationSyncOutput, onFeedback: onFeedback - ) { [weak self] _ in - self?.surfaceRuntime.resetTransientTransactionState() + ) { [weak self] error in + self?.reportPresentationFeedbackFailure(error) } } @@ -224,6 +224,30 @@ extension SubsurfaceRoleSurface { 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 { diff --git a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift index 064864db..d35d5483 100644 --- a/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift +++ b/Sources/WaylandClient/Internal/Subsurface/SubsurfaceRoleSurface.swift @@ -16,6 +16,7 @@ package final class SubsurfaceRoleSurface { let connection: RawDisplayConnection let bufferCount: PositiveInt + let failureSink: any WindowFailureSink var size: PositiveLogicalSize var model: SubsurfaceModel let softwarePresentationCoordinator = @@ -33,12 +34,14 @@ package final class SubsurfaceRoleSurface { 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 model = SubsurfaceModel( synchronizationMode: subsurfaceConfiguration.synchronizationMode diff --git a/Sources/WaylandClient/Public/Display/DisplayEvent.swift b/Sources/WaylandClient/Public/Display/DisplayEvent.swift index 1c55b617..b4735bd5 100644 --- a/Sources/WaylandClient/Public/Display/DisplayEvent.swift +++ b/Sources/WaylandClient/Public/Display/DisplayEvent.swift @@ -117,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/Presentation/PresentationFeedback.swift b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift index 2b68f808..efc333e8 100644 --- a/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift +++ b/Sources/WaylandClient/Public/Presentation/PresentationFeedback.swift @@ -89,7 +89,9 @@ public struct ManagedSurfacePresentationEvent: Equatable, Sendable { /// /// 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 ManagedSurfacePresentationEvents: AsyncSequence, Sendable { public typealias Element = SurfacePresentationFeedback diff --git a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md index 4c612e0d..d82ca401 100644 --- a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md +++ b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md @@ -19,7 +19,7 @@ A synchronized subsurface requests feedback for the child surface commit. The fo ``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 or display close terminates outstanding feedback and eventually finishes both root and filtered streams after already-published events are delivered. +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 diff --git a/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift b/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift index 82ecd34b..3dc33636 100644 --- a/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift +++ b/Tests/WaylandClientTests/Events/DisplayEventHubPresentationTests.swift @@ -48,6 +48,41 @@ struct DisplayEventHubPresentationTests { } } + @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( diff --git a/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift index 9a4919ec..31043e3b 100644 --- a/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift +++ b/Tests/WaylandClientTests/Subsurface/SubsurfaceModelTests.swift @@ -38,6 +38,32 @@ struct SubsurfaceModelTests { #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) diff --git a/docs/compositor-matrix.md b/docs/compositor-matrix.md index 587c76d4..ea421f7f 100644 --- a/docs/compositor-matrix.md +++ b/docs/compositor-matrix.md @@ -124,7 +124,8 @@ active. `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 default smoke. +additional desktop observation without changing the bounded evidence mode that +uses `--skip-feedback --skip-redraw-routing`. ## External Graphics Buffer Evidence