diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift index c4d9aed4..d30bb36e 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPresentationIntegrationTests.swift @@ -2,6 +2,226 @@ import Testing import WaylandClient extension WaylandDisplayPublicIntegrationTests { + @Test + func asyncSoftwarePresentationSupersedesStalePreparationAndCommitsReplacement() async throws { + try await withPublicConnection { display in + try await exerciseStaleSoftwarePresentation(on: display) + } + } + + @Test + func canceledAsyncSoftwarePreparationIsSupersededAndRetryable() async throws { + try await withPublicConnection { display in + try await exerciseCanceledSoftwarePresentation(on: display) + } + } + + @Test + func closingWindowDuringAsyncSoftwarePreparationReturnsClosedWithoutDrawing() async throws { + try await withPublicConnection { display in + try await exerciseWindowCloseDuringSoftwarePreparation(on: display) + } + } + + @Test + func closingDisplayDuringAsyncSoftwarePreparationReturnsClosedWithoutDrawing() async throws { + try await withPublicConnection { display in + try await exerciseDisplayCloseDuringSoftwarePreparation(on: display) + } + } + + private func exerciseStaleSoftwarePresentation(on display: WaylandDisplay) async throws { + let displayEvents = display.events + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + + let initialOutcome = try await window.show( + timeoutMilliseconds: publicIntegrationTimeoutMilliseconds, + preparing: { $0.id }, + { preparedID, frame in + #expect(preparedID == frame.id) + fill(frame, color: 0x0014_2434) + } + ) + #expect(initialOutcome == .presented) + #expect( + try await window.redraw( + preparing: { $0.id }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) == .deferred + ) + try await waitForRedrawRequest(for: window, in: displayEvents) + + let gate = AsyncSoftwarePreparationGate() + async let staleOutcome = window.redraw( + requestPresentationFeedback: true, + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) + await gate.waitUntilSuspended() + + async let replacementEvent = displayEvent( + in: displayEvents, + matching: { $0 == .redrawRequested(window.id) }, + after: { + try await window.requestRedraw() + await gate.resumePreparation() + } + ) + + #expect(try await staleOutcome == .superseded) + #expect(try await replacementEvent == .redrawRequested(window.id)) + #expect(try await window.needsRedraw) + let replacementOutcome = try await window.redraw( + preparing: { $0.id }, + { preparedID, frame in + #expect(preparedID == frame.id) + fill(frame, color: 0x0044_2414) + } + ) + #expect(replacementOutcome == .presented) + await window.close() + } + + private func exerciseCanceledSoftwarePresentation(on display: WaylandDisplay) async throws { + let displayEvents = display.events + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await show(window, color: 0x0024_1424) + try await waitForRedrawRequest(for: window, in: displayEvents) + + let gate = AsyncSoftwarePreparationGate() + let outcome = try await withThrowingTaskGroup( + of: SoftwarePresentationOutcome.self + ) { group in + group.addTask { + try await window.redraw( + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) + } + await gate.waitUntilSuspended() + group.cancelAll() + + async let replacementEvent = displayEvent( + in: displayEvents, + matching: { $0 == .redrawRequested(window.id) }, + after: { await gate.resumePreparation() } + ) + let nextOutcome = try await group.next() + #expect(try await replacementEvent == .redrawRequested(window.id)) + return try #require(nextOutcome) + } + #expect(outcome == .superseded) + #expect( + try await window.redraw( + preparing: { $0.id }, + { _, frame in fill(frame, color: 0x0034_1434) } + ) == .presented + ) + await window.close() + } + + private func exerciseWindowCloseDuringSoftwarePreparation( + on display: WaylandDisplay + ) async throws { + let displayEvents = display.events + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await show(window, color: 0x0011_2233) + try await waitForRedrawRequest(for: window, in: displayEvents) + + let gate = AsyncSoftwarePreparationGate() + async let outcome = window.redraw( + 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) + #expect( + try await window.redraw( + preparing: { $0.id }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) == .closed + ) + } + + private func exerciseDisplayCloseDuringSoftwarePreparation( + on display: WaylandDisplay + ) async throws { + let displayEvents = display.events + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await show(window, color: 0x0011_3344) + try await waitForRedrawRequest(for: window, in: displayEvents) + + let gate = AsyncSoftwarePreparationGate() + async let outcome = window.redraw( + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) + await gate.waitUntilSuspended() + await display.close() + await gate.resumePreparation() + + #expect(try await outcome == .closed) + } + + @Test + func throwingAsyncSoftwarePreparationReleasesReservationAndRepublishesRedraw() async throws { + try await withPublicConnection { display in + let displayEvents = display.events + let window = try await display.createTopLevelWindow( + configuration: testWindowConfiguration() + ) + try await show(window, color: 0x0033_2211) + try await waitForRedrawRequest(for: window, in: displayEvents) + + let replacementEvent = try await displayEvent( + in: displayEvents, + matching: { $0 == .redrawRequested(window.id) }, + after: { + do { + _ = try await window.redraw( + preparing: { _ in throw InjectedSoftwarePreparationFailure() }, + { _, _ in throw UnexpectedStaleSoftwareDraw() } + ) + Issue.record("expected software preparation failure") + } catch is InjectedSoftwarePreparationFailure { + // The original preparation failure remains observable. + } + } + ) + #expect(replacementEvent == .redrawRequested(window.id)) + #expect( + try await window.redraw( + preparing: { $0.id }, + { _, frame in fill(frame, color: 0x0022_3344) } + ) == .presented + ) + await window.close() + } + } + @Test func presentationFeedbackReportsUnavailableOrPublishesResult() async throws { try await withPublicConnection { display in @@ -53,10 +273,10 @@ private func expectPresentationFeedback( try await window.requestRedraw() } ) - try await window.requestPresentationFeedback() - try await window.redraw { frame in + let outcome = try await window.redraw(requestPresentationFeedback: true) { frame in fill(frame, color: 0x0044_2414) } + #expect(outcome == .presented) let feedback: SurfacePresentationFeedback? do { @@ -98,3 +318,48 @@ private func noteOptionalProtocolRuntimeSkip(test: String, interfaceName: String "Compositor advertised \(interfaceName) for \(test) but did not deliver a terminal event." ) } + +private func waitForRedrawRequest( + for window: Window, + in displayEvents: DisplayEvents +) async throws { + _ = try await displayEvent( + in: displayEvents, + matching: { $0 == .redrawRequested(window.id) }, + after: { try await window.requestRedraw() } + ) +} + +private actor AsyncSoftwarePreparationGate { + 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 { continuation in + resumeContinuation = continuation + } + } + + func waitUntilSuspended() async { + guard !isSuspended else { return } + await withCheckedContinuation { continuation in + suspensionWaiters.append(continuation) + } + } + + func resumePreparation() { + resumeContinuation?.resume() + resumeContinuation = nil + } +} + +private struct UnexpectedStaleSoftwareDraw: Error {} +private struct InjectedSoftwarePreparationFailure: Error {} diff --git a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift index 59026363..b4eadda6 100644 --- a/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift +++ b/IntegrationTests/PublicAPIClient/Tests/WaylandPublicIntegrationClientTests/WaylandDisplayPublicAPISurfaceTests.swift @@ -257,6 +257,68 @@ struct WaylandPresentationAPISurfaceTests { _ = usePresentationFeedbackAPI } + + @Test + func softwarePresentationOutcomesCompileForExternalClients() { + let outcomes: [SoftwarePresentationOutcome] = [ + .presented, + .superseded, + .deferred, + .closed, + ] + + #expect(Set(outcomes).count == 4) + _ = useAtomicSoftwarePresentationAPI + } + + private func useAtomicSoftwarePresentationAPI( + _ window: Window + ) async throws -> [SoftwarePresentationOutcome] { + let simpleShow = try await window.show(requestPresentationFeedback: true) { _ in () } + let simpleRedraw = try await window.redraw(requestPresentationFeedback: true) { _ in () } + let simpleDamagedShow = try await window.show( + damage: nil, + requestPresentationFeedback: true, + timeoutMilliseconds: 1_000 + ) { _ in () } + let simpleDamagedRedraw = try await window.redraw( + damage: nil, + requestPresentationFeedback: true + ) { _ in () } + let shown = try await window.show( + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + let redrawn = try await window.redraw( + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + let damagedShow = try await window.show( + damage: nil, + timeoutMilliseconds: 1_000, + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + let damagedRedraw = try await window.redraw( + damage: nil, + requestPresentationFeedback: true, + preparing: { reservation in reservation.id }, + { _, _ in () } + ) + return [ + simpleShow, + simpleRedraw, + simpleDamagedShow, + simpleDamagedRedraw, + shown, + redrawn, + damagedShow, + damagedRedraw, + ] + } } @Suite("WaylandDisplay data transfer public API surface") diff --git a/Sources/CWaylandProtocols/generated/shims/request-bridges.c b/Sources/CWaylandProtocols/generated/shims/request-bridges.c index 29e63dd4..ffd78b65 100644 --- a/Sources/CWaylandProtocols/generated/shims/request-bridges.c +++ b/Sources/CWaylandProtocols/generated/shims/request-bridges.c @@ -92,12 +92,6 @@ void swl_seat_release( wl_seat_release(object); } -struct wl_callback *swl_surface_frame( - struct wl_surface *object) -{ - return wl_surface_frame(object); -} - void swl_surface_set_buffer_scale( struct wl_surface *object, int32_t request_scale) diff --git a/Sources/CWaylandProtocols/include/generated/shims/request-bridges.h b/Sources/CWaylandProtocols/include/generated/shims/request-bridges.h index 7612faf5..e9f12664 100644 --- a/Sources/CWaylandProtocols/include/generated/shims/request-bridges.h +++ b/Sources/CWaylandProtocols/include/generated/shims/request-bridges.h @@ -8,7 +8,6 @@ #include #include -struct wl_callback; struct wl_compositor; struct wl_data_device; struct wl_data_device_manager; @@ -97,9 +96,6 @@ struct wl_touch *swl_seat_get_touch( void swl_seat_release( struct wl_seat *object); -struct wl_callback *swl_surface_frame( - struct wl_surface *object); - void swl_surface_set_buffer_scale( struct wl_surface *object, int32_t request_scale); diff --git a/Sources/CWaylandProtocols/include/wayland-client-kit-shims.h b/Sources/CWaylandProtocols/include/wayland-client-kit-shims.h index bd7a0ed6..94ce2a78 100644 --- a/Sources/CWaylandProtocols/include/wayland-client-kit-shims.h +++ b/Sources/CWaylandProtocols/include/wayland-client-kit-shims.h @@ -129,6 +129,7 @@ struct wl_buffer *swl_shm_pool_create_buffer( void swl_surface_attach( struct wl_surface *surface, struct wl_buffer *buffer, int32_t x, int32_t y); +struct wl_callback *swl_surface_frame(struct wl_surface *surface); void swl_surface_commit(struct wl_surface *surface); void swl_surface_damage_buffer( struct wl_surface *surface, int32_t x, int32_t y, @@ -837,6 +838,7 @@ enum swl_test_core_request_kind { SWL_TEST_CORE_SUBSURFACE_SET_SYNC = 18, SWL_TEST_CORE_SUBSURFACE_SET_DESYNC = 19, SWL_TEST_CORE_SUBSURFACE_DESTROY = 20, + SWL_TEST_CORE_SURFACE_FRAME = 21, }; enum swl_test_metadata_request_kind { @@ -946,6 +948,7 @@ struct swl_test_core_request_record { int32_t y; uint32_t latest_sequence; uint32_t attach_sequence; + uint32_t frame_sequence; uint32_t damage_sequence; uint32_t commit_sequence; uint32_t buffer_destroy_sequence; @@ -1970,6 +1973,7 @@ struct swl_test_commit_timing_destroy_record swl_test_commit_timing_destroy_record(void); void swl_test_presentation_request_recording_begin(void); +void swl_test_presentation_request_recording_begin_forwarding(void); void swl_test_presentation_request_recording_end(void); struct swl_test_presentation_request_record swl_test_presentation_request_record(void); diff --git a/Sources/CWaylandProtocols/shims/requests-core.c b/Sources/CWaylandProtocols/shims/requests-core.c index c08f7d73..ea0a626b 100644 --- a/Sources/CWaylandProtocols/shims/requests-core.c +++ b/Sources/CWaylandProtocols/shims/requests-core.c @@ -58,6 +58,12 @@ static void swl_surface_attach_default( wl_surface_attach(surface, buffer, x, y); } +static struct wl_callback *swl_surface_frame_default( + struct wl_surface *surface) +{ + return wl_surface_frame(surface); +} + static void swl_surface_commit_default(struct wl_surface *surface) { wl_surface_commit(surface); @@ -199,6 +205,8 @@ static void (*swl_surface_attach_impl)( struct wl_buffer *buffer, int32_t x, int32_t y) = swl_surface_attach_default; +static struct wl_callback *(*swl_surface_frame_impl)( + struct wl_surface *surface) = swl_surface_frame_default; static void (*swl_surface_commit_impl)(struct wl_surface *surface) = swl_surface_commit_default; static void (*swl_surface_damage_impl)( @@ -322,6 +330,17 @@ static void swl_test_surface_attach_record( swl_surface_attach_default(surface, buffer, x, y); } +static struct wl_callback *swl_test_surface_frame_record( + struct wl_surface *surface) +{ + swl_test_record_core_request(SWL_TEST_CORE_SURFACE_FRAME, surface); + swl_test_core_request_latest.frame_sequence = + swl_test_core_request_latest.latest_sequence; + if (swl_test_core_request_forwards_requests) + return swl_surface_frame_default(surface); + return (struct wl_callback *)0x5404; +} + static void swl_test_surface_commit_record(struct wl_surface *surface) { swl_test_record_core_request(SWL_TEST_CORE_SURFACE_COMMIT, surface); @@ -565,6 +584,7 @@ static uint32_t swl_test_proxy_get_id(void *proxy) #define swl_shm_create_pool_impl wl_shm_create_pool #define swl_shm_pool_create_buffer_impl wl_shm_pool_create_buffer #define swl_surface_attach_impl wl_surface_attach +#define swl_surface_frame_impl wl_surface_frame #define swl_surface_commit_impl wl_surface_commit #define swl_surface_damage_impl wl_surface_damage #define swl_surface_damage_buffer_impl wl_surface_damage_buffer @@ -611,6 +631,14 @@ void swl_surface_attach( SWL_CORE_REQUEST_UNLOCK(); } +struct wl_callback *swl_surface_frame(struct wl_surface *surface) +{ + SWL_CORE_REQUEST_LOCK(); + struct wl_callback *callback = swl_surface_frame_impl(surface); + SWL_CORE_REQUEST_UNLOCK(); + return callback; +} + void swl_surface_commit(struct wl_surface *surface) { SWL_CORE_REQUEST_LOCK(); @@ -826,6 +854,7 @@ static void swl_test_core_request_recording_start(int forwards_requests) swl_shm_create_pool_impl = swl_test_shm_create_pool_record; swl_shm_pool_create_buffer_impl = swl_test_shm_pool_create_buffer_record; swl_surface_attach_impl = swl_test_surface_attach_record; + swl_surface_frame_impl = swl_test_surface_frame_record; swl_surface_commit_impl = swl_test_surface_commit_record; swl_surface_damage_impl = swl_test_surface_damage_legacy_record; swl_surface_damage_buffer_impl = swl_test_surface_damage_buffer_record; @@ -869,6 +898,7 @@ void swl_test_core_request_recording_end(void) swl_shm_create_pool_impl = swl_shm_create_pool_default; swl_shm_pool_create_buffer_impl = swl_shm_pool_create_buffer_default; swl_surface_attach_impl = swl_surface_attach_default; + swl_surface_frame_impl = swl_surface_frame_default; swl_surface_commit_impl = swl_surface_commit_default; swl_surface_damage_impl = swl_surface_damage_default; swl_surface_damage_buffer_impl = swl_surface_damage_buffer_default; diff --git a/Sources/CWaylandProtocols/shims/requests-presentation.c b/Sources/CWaylandProtocols/shims/requests-presentation.c index 91b8ea41..55258c66 100644 --- a/Sources/CWaylandProtocols/shims/requests-presentation.c +++ b/Sources/CWaylandProtocols/shims/requests-presentation.c @@ -2,6 +2,19 @@ #include "generated/stable/presentation-time/presentation-time-client-protocol.h" #ifdef SWL_ENABLE_TESTING +#include + +// Live request tests switch these hooks from the test task while requests run +// on the display thread. This mutex keeps hook changes and captured records in +// one order that ThreadSanitizer can observe. +static pthread_mutex_t swl_test_presentation_request_mutex = + PTHREAD_MUTEX_INITIALIZER; + +#define SWL_PRESENTATION_REQUEST_LOCK() \ + ((void)pthread_mutex_lock(&swl_test_presentation_request_mutex)) +#define SWL_PRESENTATION_REQUEST_UNLOCK() \ + ((void)pthread_mutex_unlock(&swl_test_presentation_request_mutex)) + static struct swl_test_presentation_request_record swl_test_presentation_request_latest; @@ -59,6 +72,18 @@ static struct wp_presentation_feedback *swl_test_presentation_feedback_record( return feedback; } +static struct wp_presentation_feedback * +swl_test_presentation_feedback_record_forwarding( + struct wp_presentation *presentation, + struct wl_surface *surface) +{ + struct wp_presentation_feedback *feedback = + swl_presentation_feedback_default(presentation, surface); + swl_test_record_presentation_request( + SWL_TEST_PRESENTATION_FEEDBACK, presentation, surface, feedback); + return feedback; +} + static void swl_test_presentation_destroy_record( struct wp_presentation *presentation) { @@ -73,6 +98,8 @@ static void swl_test_presentation_feedback_destroy_record( SWL_TEST_PRESENTATION_FEEDBACK_DESTROY, feedback, NULL, feedback); } #else +#define SWL_PRESENTATION_REQUEST_LOCK() ((void)0) +#define SWL_PRESENTATION_REQUEST_UNLOCK() ((void)0) #define swl_presentation_feedback_impl wp_presentation_feedback #define swl_presentation_destroy_impl wp_presentation_destroy #define swl_presentation_feedback_destroy_impl wp_presentation_feedback_destroy @@ -82,42 +109,71 @@ struct wp_presentation_feedback *swl_wp_presentation_feedback( struct wp_presentation *presentation, struct wl_surface *surface) { - return swl_presentation_feedback_impl(presentation, surface); + SWL_PRESENTATION_REQUEST_LOCK(); + struct wp_presentation_feedback *feedback = + swl_presentation_feedback_impl(presentation, surface); + SWL_PRESENTATION_REQUEST_UNLOCK(); + return feedback; } void swl_wp_presentation_destroy(struct wp_presentation *presentation) { + SWL_PRESENTATION_REQUEST_LOCK(); swl_presentation_destroy_impl(presentation); + SWL_PRESENTATION_REQUEST_UNLOCK(); } void swl_wp_presentation_feedback_destroy( struct wp_presentation_feedback *feedback) { + SWL_PRESENTATION_REQUEST_LOCK(); swl_presentation_feedback_destroy_impl(feedback); + SWL_PRESENTATION_REQUEST_UNLOCK(); } #ifdef SWL_ENABLE_TESTING void swl_test_presentation_request_recording_begin(void) { + SWL_PRESENTATION_REQUEST_LOCK(); swl_test_presentation_request_latest = (struct swl_test_presentation_request_record){0}; swl_presentation_feedback_impl = swl_test_presentation_feedback_record; swl_presentation_destroy_impl = swl_test_presentation_destroy_record; swl_presentation_feedback_destroy_impl = swl_test_presentation_feedback_destroy_record; + SWL_PRESENTATION_REQUEST_UNLOCK(); +} + +void swl_test_presentation_request_recording_begin_forwarding(void) +{ + SWL_PRESENTATION_REQUEST_LOCK(); + swl_test_presentation_request_latest = + (struct swl_test_presentation_request_record){0}; + swl_presentation_feedback_impl = + swl_test_presentation_feedback_record_forwarding; + swl_presentation_destroy_impl = swl_presentation_destroy_default; + swl_presentation_feedback_destroy_impl = + swl_presentation_feedback_destroy_default; + SWL_PRESENTATION_REQUEST_UNLOCK(); } void swl_test_presentation_request_recording_end(void) { + SWL_PRESENTATION_REQUEST_LOCK(); swl_presentation_feedback_impl = swl_presentation_feedback_default; swl_presentation_destroy_impl = swl_presentation_destroy_default; swl_presentation_feedback_destroy_impl = swl_presentation_feedback_destroy_default; + SWL_PRESENTATION_REQUEST_UNLOCK(); } struct swl_test_presentation_request_record swl_test_presentation_request_record(void) { - return swl_test_presentation_request_latest; + SWL_PRESENTATION_REQUEST_LOCK(); + struct swl_test_presentation_request_record record = + swl_test_presentation_request_latest; + SWL_PRESENTATION_REQUEST_UNLOCK(); + return record; } #endif diff --git a/Sources/WaylandClient/Internal/Display/DisplayCore.swift b/Sources/WaylandClient/Internal/Display/DisplayCore.swift index d72ab1ff..27b7fd9e 100644 --- a/Sources/WaylandClient/Internal/Display/DisplayCore.swift +++ b/Sources/WaylandClient/Internal/Display/DisplayCore.swift @@ -124,9 +124,12 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { func reserveSoftwareFrameForShow( _ windowID: WindowID, timeoutMilliseconds: Int32 - ) throws -> SoftwareFrameReservation? { + ) throws -> WindowSoftwareFrameReservationOutcome { try withFatalFailureFinalization { - try requireOpenWindow(windowID).reserveShowSoftwareFrameOnOwnerThread( + guard !isClosed, let window = surfaces.window(windowID) else { + return .closed + } + return try window.reserveShowSoftwareFrameOnOwnerThread( timeoutMilliseconds: timeoutMilliseconds ) } @@ -163,9 +166,12 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { func reserveSoftwareFrameForRedraw( _ windowID: WindowID - ) throws -> SoftwareFrameReservation? { + ) throws -> WindowSoftwareFrameReservationOutcome { try withFatalFailureFinalization { - try requireOpenWindow(windowID).reserveRedrawSoftwareFrameOnOwnerThread() + guard !isClosed, let window = surfaces.window(windowID) else { + return .closed + } + return try window.reserveRedrawSoftwareFrameOnOwnerThread() } } @@ -178,24 +184,28 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { requestPresentationFeedback: Bool, damage: SurfaceDamageRegion?, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { + ) throws -> SoftwarePresentationOutcome { try withFatalFailureFinalization { - let window = try requireOpenWindow(windowID) - let presentationFeedback = try presentationFeedbackCommitRequest( - for: window, - windowID: windowID, - isRequested: requestPresentationFeedback - ) - try window.submitReservedSoftwareFrameOnOwnerThread( + guard !isClosed, let window = surfaces.window(windowID) else { + return .closed + } + let outcome = try window.submitReservedSoftwareFrameOnOwnerThread( reservation, submitConstraints: submitConstraints, metadata: metadata, damage: damage, - presentationFeedback: presentationFeedback, + makePresentationFeedback: { [self] in + try presentationFeedbackCommitRequest( + for: window, + windowID: windowID, + isRequested: requestPresentationFeedback + ) + }, draw ) - guard !isClosed, let activeSession else { return } + guard !isClosed, let activeSession else { return .closed } publishSessionEvents(activeSession) + return outcome } } @@ -203,10 +213,9 @@ final class DisplayCore: RawInvariantFailureReporter, WindowFailureSink { _ windowID: WindowID, reservation: SoftwareFrameReservation ) throws { - try withFatalFailureFinalization { - try requireOpenWindow(windowID).cancelSoftwareFrameReservationOnOwnerThread( - reservation - ) + withFatalFailureFinalization { + guard !isClosed, let window = surfaces.window(windowID) else { return } + window.cancelSoftwareFrameReservationOnOwnerThread(reservation) } } diff --git a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift index 61e235f0..5d5b4976 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface+Presentation.swift @@ -1,3 +1,52 @@ +struct StagedPopupPresentationSuccess { + let model: PopupModel + let publishesRedrawRequest: Bool +} + +struct PopupPresentationSuccessStagingContext { + let model: PopupModel + let parentWindowID: WindowID + + func stage( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) throws -> StagedPopupPresentationSuccess { + var stagedModel = model + let effects = try stagedModel.reduce( + .presentationSucceeded( + generation: generation, + bufferAvailability: bufferAvailability + ) + ) + let expectedEvent = PopupLifecycleEvent( + popup: stagedModel.id, + parentWindowID: parentWindowID + ) + var publishesRedrawRequest = false + for effect in effects { + guard case .publishRedrawRequested(let event) = effect, + event == expectedEvent, + !publishesRedrawRequest + else { + throw ClientError.window( + parentWindowID, + .invalidLifecycleTransition( + .invalidTransition( + from: "popup presentation success staging", + event: "unexpected effect \(effect)" + ) + ) + ) + } + publishesRedrawRequest = true + } + return StagedPopupPresentationSuccess( + model: stagedModel, + publishesRedrawRequest: publishesRedrawRequest + ) + } +} + extension PopupRoleSurface { // swiftlint:disable:next function_body_length func performSoftwarePresent( @@ -70,11 +119,7 @@ extension PopupRoleSurface { } do { - pendingFrameRegistration = try requestSurfaceFrameCallback( - generation: request.generation - ) { [weak self] in - self?.handleFrameDone() - } + try reserveSurfaceFrameCallback(generation: request.generation) } catch { failActivePresentation( generation: request.generation, @@ -84,9 +129,17 @@ extension PopupRoleSurface { throw error } + let stagedCommit: StagedSurfaceFrameCommit + let stagedSuccess: StagedPopupPresentationSuccess do { - _ = drawingBuffer.markBusy(commitGeneration: request.generation) - try commitSurfaceFrame(preparedCommit) + stagedCommit = try stageSurfaceFrameCommit(preparedCommit) + stagedSuccess = try PopupPresentationSuccessStagingContext( + model: model, + parentWindowID: parentWindowID + ).stage( + generation: request.generation, + bufferAvailability: try redrawBufferAvailability() + ) } catch { pendingFrameRegistration = nil cancelSurfaceFrameCallback() @@ -94,14 +147,15 @@ extension PopupRoleSurface { throw error } - try interpretPopupEffects( - model.reduce( - .presentationSucceeded( - generation: request.generation, - bufferAvailability: try redrawBufferAvailability() - ) - ) - ) + _ = drawingBuffer.markBusy(commitGeneration: request.generation) + pendingFrameRegistration = requestReservedSurfaceFrameCallback { [weak self] in + self?.handleFrameDone() + } + commitSurfaceFrame(stagedCommit) + model = stagedSuccess.model + if stagedSuccess.publishesRedrawRequest { + onRedrawRequested?() + } return .presented } catch { failPresentationIfStillActive( diff --git a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift index 8f49b3b5..d15cc58a 100644 --- a/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift +++ b/Sources/WaylandClient/Internal/Popup/PopupRoleSurface.swift @@ -339,14 +339,18 @@ extension PopupRoleSurface { try surfaceRuntime.acknowledgeConfigure(serial: serial) } - package func requestSurfaceFrameCallback( - generation: UInt64, + package func reserveSurfaceFrameCallback(generation: UInt64) throws { + try SurfaceFrameCommitter.reserveFrameCallback( + runtime: &surfaceRuntime, + generation: generation + ) + } + + package func requestReservedSurfaceFrameCallback( onFrame: @escaping () -> Void - ) throws -> FrameCallbackRegistration { - try SurfaceFrameCommitter.requestFrameCallback( + ) -> FrameCallbackRegistration { + SurfaceFrameCommitter.requestReservedFrameCallback( on: surface, - runtime: &surfaceRuntime, - generation: generation, onFrame: onFrame ) } @@ -376,15 +380,22 @@ extension PopupRoleSurface { ) } - package func commitSurfaceFrame( + package func stageSurfaceFrameCommit( _ preparedCommit: PreparedSurfaceFrameCommit - ) throws { - try SurfaceFrameCommitter.commit( + ) throws -> StagedSurfaceFrameCommit { + try SurfaceFrameCommitter.stage( preparedCommit, runtime: &surfaceRuntime ) } + package func commitSurfaceFrame(_ stagedCommit: StagedSurfaceFrameCommit) { + SurfaceFrameCommitter.commit( + stagedCommit, + runtime: &surfaceRuntime + ) + } + package func resetTransientSurfaceTransactionState() { surfaceRuntime.resetTransientTransactionState() } diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceFrameCommitter.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceFrameCommitter.swift index b911f609..baabee17 100644 --- a/Sources/WaylandClient/Internal/Surfaces/SurfaceFrameCommitter.swift +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceFrameCommitter.swift @@ -59,12 +59,35 @@ package struct PreparedSurfaceFrameCommit { let scaleInstallation: SurfaceScaleInstallation let generation: UInt64 let plan: SurfaceCommitPlan - let submitConstraints: SurfaceSubmitConstraints - let metadata: SurfaceCommitMetadata + let submitConstraints: ResolvedSurfaceSubmitConstraints? + let metadata: ResolvedSurfaceCommitMetadata? let payload: SurfaceCommitPayload } +package struct StagedSurfaceFrameCommit { + let preparedCommit: PreparedSurfaceFrameCommit + let committedFrame: SurfaceCommittedFrame +} + enum SurfaceFrameCommitter { + static func reserveFrameCallback( + runtime: inout SurfaceRuntime, + generation: UInt64 + ) throws { + try runtime.requestFrameCallback(generation: generation) + } + + static func requestReservedFrameCallback( + on surface: RawSurface, + onFrame: @escaping () -> Void + ) -> FrameCallbackRegistration { + do { + return try surface.requestFrame(onDone: onFrame) + } catch { + preconditionFailure("Reserved frame callback request failed: \(error)") + } + } + static func requestFrameCallback( on surface: RawSurface, runtime: inout SurfaceRuntime, @@ -107,24 +130,50 @@ enum SurfaceFrameCommitter { payload: request.payload ) try request.metadata.validate(capabilities: runtime.capabilitySnapshot()) + let submitConstraints = try runtime.resolvedSubmitConstraints(request.submitConstraints) + let metadata = try runtime.resolvedCommitMetadata(request.metadata) return PreparedSurfaceFrameCommit( surface: request.surface, scaleInstallation: request.scaleInstallation, generation: request.generation, plan: plan, - submitConstraints: request.submitConstraints, - metadata: request.metadata, + submitConstraints: submitConstraints, + metadata: metadata, payload: request.payload ) } + static func stage( + _ preparedCommit: PreparedSurfaceFrameCommit, + runtime: inout SurfaceRuntime + ) throws -> StagedSurfaceFrameCommit { + let committedFrame = try runtime.committedFrameCandidate( + generation: preparedCommit.generation, + plan: preparedCommit.plan, + payload: preparedCommit.payload.committedPayload + ) + return StagedSurfaceFrameCommit( + preparedCommit: preparedCommit, + committedFrame: committedFrame + ) + } + @discardableResult static func commit( _ preparedCommit: PreparedSurfaceFrameCommit, runtime: inout SurfaceRuntime ) throws -> SurfaceCommitPlan { - try runtime.applySubmitConstraints(preparedCommit.submitConstraints) - try runtime.applyCommitMetadata(preparedCommit.metadata) + let stagedCommit = try stage(preparedCommit, runtime: &runtime) + return commit(stagedCommit, runtime: &runtime) + } + + @discardableResult + static func commit( + _ stagedCommit: StagedSurfaceFrameCommit, + runtime: inout SurfaceRuntime + ) -> SurfaceCommitPlan { + let preparedCommit = stagedCommit.preparedCommit + applyPreflightedCommitState(preparedCommit) preparedCommit.surface.setBufferScale(preparedCommit.plan.bufferScale) preparedCommit.scaleInstallation.applyViewportDestinationIfNeeded( preparedCommit.plan.viewportDestination @@ -137,15 +186,22 @@ enum SurfaceFrameCommitter { break } preparedCommit.surface.commit() - try runtime.prepareCommittedFrame( - generation: preparedCommit.generation, - plan: preparedCommit.plan, - payload: preparedCommit.payload.committedPayload - ) + runtime.recordValidatedCommittedFrame(stagedCommit.committedFrame) runtime.markSubmitConstraintsCommitted() return preparedCommit.plan } + private static func applyPreflightedCommitState( + _ preparedCommit: PreparedSurfaceFrameCommit + ) { + do { + try preparedCommit.submitConstraints?.apply() + } catch { + preconditionFailure("Preflighted surface submit constraints failed: \(error)") + } + preparedCommit.metadata?.apply() + } + private static func apply(_ damage: SurfaceDamageExtent, to surface: RawSurface) { switch damage { case .buffer(let rectangles): diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Metadata.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Metadata.swift index e7706d91..35d889af 100644 --- a/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Metadata.swift +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Metadata.swift @@ -127,7 +127,13 @@ extension SurfaceRuntime { func preflightCommitMetadata( _ metadata: SurfaceCommitMetadata ) throws(SurfaceCommitMetadataError) { - guard let objects = surfaceObjects else { return } - _ = try objects.metadataObjects.preflight(metadata) + _ = try resolvedCommitMetadata(metadata) + } + + func resolvedCommitMetadata( + _ metadata: SurfaceCommitMetadata + ) throws(SurfaceCommitMetadataError) -> ResolvedSurfaceCommitMetadata? { + guard let objects = surfaceObjects else { return nil } + return try objects.metadataObjects.preflight(metadata) } } diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Submit.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Submit.swift index b3250b38..7672d094 100644 --- a/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Submit.swift +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime+Submit.swift @@ -73,6 +73,13 @@ extension SurfaceRuntime { } } + func resolvedSubmitConstraints( + _ constraints: SurfaceSubmitConstraints + ) throws(SurfaceSubmitConstraintError) -> ResolvedSurfaceSubmitConstraints? { + guard let objects = surfaceObjects else { return nil } + return try objects.submitConstraintObjects.preflight(constraints) + } + mutating func markSubmitConstraintsCommitted() { updateSurfaceObjects { objects in objects.submitConstraintObjects.markCommitted() diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime.swift index 2d683fbe..027a7d0a 100644 --- a/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime.swift +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceRuntime.swift @@ -448,6 +448,25 @@ extension SurfaceRuntime { try transactionState.validateCommittedFrameCandidate(generation: generation) } + func committedFrameCandidate( + generation: UInt64, + plan: SurfaceCommitPlan, + payload: SurfaceCommittedPayload = .buffer + ) throws -> SurfaceCommittedFrame { + let transactionState = surfaceObjects?.transactionState ?? SurfaceTransactionState() + return try transactionState.committedFrameCandidate( + generation: generation, + plan: plan, + payload: payload + ) + } + + mutating func recordValidatedCommittedFrame(_ committedFrame: SurfaceCommittedFrame) { + updateSurfaceObjects { objects in + objects.transactionState.recordValidatedCommittedFrame(committedFrame) + } + } + mutating func prepareCommittedFrame( generation: UInt64, plan: SurfaceCommitPlan, diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceSubmitConstraintObjects.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceSubmitConstraintObjects.swift index a5081ce9..4f531579 100644 --- a/Sources/WaylandClient/Internal/Surfaces/SurfaceSubmitConstraintObjects.swift +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceSubmitConstraintObjects.swift @@ -89,6 +89,7 @@ struct SurfaceSubmitConstraintObjects { case .implicit: return nil case .explicit(let acquire, let release): + guard acquire != nil || release != nil else { return nil } guard let synchronization else { throw .explicitSyncUnavailable } diff --git a/Sources/WaylandClient/Internal/Surfaces/SurfaceTransactionState.swift b/Sources/WaylandClient/Internal/Surfaces/SurfaceTransactionState.swift index d1474b7b..f8bb9b06 100644 --- a/Sources/WaylandClient/Internal/Surfaces/SurfaceTransactionState.swift +++ b/Sources/WaylandClient/Internal/Surfaces/SurfaceTransactionState.swift @@ -135,6 +135,20 @@ package struct SurfaceTransactionState: Equatable, Sendable { plan: SurfaceCommitPlan, payload: SurfaceCommittedPayload = .buffer ) throws { + recordValidatedCommittedFrame( + try committedFrameCandidate( + generation: generation, + plan: plan, + payload: payload + ) + ) + } + + package func committedFrameCandidate( + generation: UInt64, + plan: SurfaceCommitPlan, + payload: SurfaceCommittedPayload = .buffer + ) throws -> SurfaceCommittedFrame { let acknowledgedConfigureSerial = try validateCommittedFrameCandidate( generation: generation ) @@ -147,20 +161,26 @@ package struct SurfaceTransactionState: Equatable, Sendable { actual: generation ) } - if let lastGeneration = lastCommittedFrame?.generation, generation <= lastGeneration { - throw SurfaceTransactionError.commitGenerationDidNotAdvance( - previous: lastGeneration, - actual: generation - ) - } - lastCommittedFrame = SurfaceCommittedFrame( + return SurfaceCommittedFrame( generation: generation, configureSerial: acknowledgedConfigureSerial, plan: plan, payload: payload ) - if payload == .buffer { + } + + package mutating func recordValidatedCommittedFrame( + _ committedFrame: SurfaceCommittedFrame + ) { + precondition(acknowledgedConfigureSerial == committedFrame.configureSerial) + precondition(pendingFrameCallbackGeneration == committedFrame.generation) + precondition( + lastCommittedFrame.map { committedFrame.generation > $0.generation } ?? true + ) + + lastCommittedFrame = committedFrame + if committedFrame.payload == .buffer { hasCommittedBufferContent = true } } diff --git a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift index 4722806c..fc84ff9b 100644 --- a/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift +++ b/Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift @@ -35,6 +35,109 @@ struct PendingSoftwareFrameReservation { let reservedFrame: WindowReservedSoftwareFrame } +private struct StagedSoftwarePresentationSuccess { + let model: WindowModel + let publishesRedrawRequest: Bool +} + +private struct SoftwarePresentationSuccessStagingContext { + let model: WindowModel + let windowID: WindowID + let hasPendingSurfaceConfigure: Bool + + func stage( + generation: UInt64, + currentBufferAvailability: RedrawBufferAvailability + ) throws -> StagedSoftwarePresentationSuccess { + var stagedModel = model + let bufferAvailability = RedrawBufferAvailability.resolvingPendingConfigure( + hasPendingSurfaceConfigure, + currentBufferAvailability: currentBufferAvailability + ) + let effects = try stagedModel.reduce( + .presentationSucceeded( + generation: generation, + bufferAvailability: bufferAvailability + ) + ) + var publishesRedrawRequest = false + for effect in effects { + guard case .publishRedrawRequested(let effectWindowID) = effect, + effectWindowID == windowID, + !publishesRedrawRequest + else { + throw ClientError.window( + windowID, + .invalidLifecycleTransition( + .invalidTransition( + from: "software presentation success staging", + event: "unexpected effect \(effect)" + ) + ) + ) + } + publishesRedrawRequest = true + } + return StagedSoftwarePresentationSuccess( + model: stagedModel, + publishesRedrawRequest: publishesRedrawRequest + ) + } +} + +private struct StagedExternalBufferPresentationSuccess { + let model: WindowModel + let result: PreviewBufferPresentationResult + let publishesRedrawRequest: Bool +} + +private struct ExternalSuccessStagingContext { + let model: WindowModel + let windowID: WindowID + let bufferAvailability: RedrawBufferAvailability + let capabilities: SurfaceCapabilitySnapshot + + func stage( + generation: UInt64, + commitPlan: SurfaceCommitPlan + ) throws -> StagedExternalBufferPresentationSuccess { + var stagedModel = model + let effects = try stagedModel.reduce( + .externalPresentationSucceeded( + generation: generation, + bufferAvailability: bufferAvailability + ) + ) + var publishesRedrawRequest = false + for effect in effects { + guard case .publishRedrawRequested(let effectWindowID) = effect, + effectWindowID == windowID, + !publishesRedrawRequest + else { + throw ClientError.window( + windowID, + .invalidLifecycleTransition( + .invalidTransition( + from: "external presentation success staging", + event: "unexpected effect \(effect)" + ) + ) + ) + } + publishesRedrawRequest = true + } + return try StagedExternalBufferPresentationSuccess( + model: stagedModel, + result: PreviewBufferPresentationResult( + generation: generation, + commitPlan: commitPlan, + capabilities: capabilities + ), + publishesRedrawRequest: publishesRedrawRequest + ) + } +} + // swiftlint:disable:next type_body_length package final class TopLevelWindow { package static let defaultConfigureTimeoutMS: Int32 = 1_000 @@ -430,9 +533,11 @@ package final class TopLevelWindow { return presentationRequest } - private func reserveSoftwareFrameForCurrentRedraw() throws -> SoftwareFrameReservation? { - guard !model.isClosed else { return nil } - guard let request = try consumeSoftwarePresentationRequest() else { return nil } + private func reserveSoftwareFrameForCurrentRedraw() + throws -> WindowSoftwareFrameReservationOutcome + { + guard !model.isClosed else { return .closed } + guard let request = try consumeSoftwarePresentationRequest() else { return .deferred } try interpretWindowEffects( model.reduce(.presentationStarted(request)) @@ -454,7 +559,9 @@ package final class TopLevelWindow { hasPendingFrameRegistration: pendingFrameRegistration != nil ) try interpretSoftwarePresentationFollowUp(result.followUp) - guard let reservedFrame = result.reservedFrame else { return nil } + guard let reservedFrame = result.reservedFrame else { + return model.isClosed ? .closed : .deferred + } softwarePresentationCoordinator.register( PendingSoftwareFrameReservation( @@ -464,12 +571,9 @@ package final class TopLevelWindow { ), for: reservedFrame.reservation.reservationID ) - return reservedFrame.reservation + return .reserved(reservedFrame.reservation) } catch let failure as WindowSoftwarePresentationFailure { - failActivePresentation( - generation: request.generation, - error: failure.presentationError - ) + failSoftwarePresentationIfStillActive(generation: request.generation) throw failure.underlying } catch { failPresentationIfStillActive( @@ -581,9 +685,12 @@ package final class TopLevelWindow { model.reduce(.presentationStarted(request)) ) + let successStagingContext = softwarePresentationSuccessStagingContext() + var stagedSuccess: StagedSoftwarePresentationSuccess? + let result: WindowSoftwarePresentationResult do { let geometry = try surfaceGeometry(logicalSize: request.configuration.size) - let result = try softwarePresenter().present( + result = try softwarePresenter().present( context: WindowSoftwarePresentationContext( request: request, geometry: geometry, @@ -593,16 +700,17 @@ package final class TopLevelWindow { presentationFeedback: presentationFeedback ), draw: draw, + stageSuccess: { currentBufferAvailability in + stagedSuccess = try successStagingContext.stage( + generation: request.generation, + currentBufferAvailability: currentBufferAvailability + ) + }, runtime: &surfaceRuntime, pendingFrameRegistration: &pendingFrameRegistration ) - try interpretSoftwarePresentationFollowUp(result.followUp) - return result.outcome } catch let failure as WindowSoftwarePresentationFailure { - failActivePresentation( - generation: request.generation, - error: failure.presentationError - ) + failSoftwarePresentationIfStillActive(generation: request.generation) if case .userDraw = failure.presentationError { throw WindowSoftwareDrawFailure(underlying: failure.underlying) } @@ -614,6 +722,22 @@ package final class TopLevelWindow { ) throw error } + + if result.outcome == .presented { + installSoftwarePresentationSuccess(stagedSuccess) + return .presented + } + + do { + try interpretSoftwarePresentationFollowUp(result.followUp) + return result.outcome + } catch { + failPresentationIfStillActive( + generation: request.generation, + error: .surfaceCommit(String(describing: error)) + ) + throw error + } } private func softwarePresenter() -> WindowSoftwarePresenter { @@ -646,33 +770,27 @@ package final class TopLevelWindow { submitConstraints: SurfaceSubmitConstraints, metadata: SurfaceCommitMetadata, damage: SurfaceDamageRegion?, - presentationFeedback: WindowPresentationFeedbackCommitRequest?, + makePresentationFeedback: () throws -> WindowPresentationFeedbackCommitRequest?, _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws -> RedrawOutcome { + ) throws -> SoftwarePresentationOutcome { guard !model.isClosed else { cancelSoftwareFrameReservation(reservation) - return .skippedClosed + return .closed } - guard - let pendingReservation = softwarePresentationCoordinator.take( - reservation.reservationID - ) - else { - throw ClientError.invalidWindowState( - .message("software frame reservation is not active") - ) + guard let pendingReservation = softwarePresentationCoordinator.take(reservation) else { + return model.isClosed ? .closed : .superseded } + var stagedSuccess: StagedSoftwarePresentationSuccess? + let result: WindowSoftwarePresentationResult do { - _ = try consumeLatestConfigureIfAvailable() - guard try currentSurfaceGeometry() == pendingReservation.geometry else { - pendingReservation.reservedFrame.drawingBuffer.discard() - resetTransientState() - try markNeedsRedraw(bufferAvailability: try redrawBufferAvailability()) - return .skippedPendingFrame + guard try isCurrentSoftwarePresentation(pendingReservation) else { + return try supersedeSoftwarePresentation(pendingReservation) } + let successStagingContext = softwarePresentationSuccessStagingContext() - let result = try softwarePresenter().presentReserved( + let presentationFeedback = try makePresentationFeedback() + result = try softwarePresenter().presentReserved( pendingReservation.reservedFrame, context: WindowSoftwarePresentationContext( request: pendingReservation.request, @@ -683,16 +801,19 @@ package final class TopLevelWindow { presentationFeedback: presentationFeedback ), draw: draw, + stageSuccess: { currentBufferAvailability in + stagedSuccess = try successStagingContext.stage( + generation: pendingReservation.request.generation, + currentBufferAvailability: currentBufferAvailability + ) + }, runtime: &surfaceRuntime, pendingFrameRegistration: &pendingFrameRegistration ) - try interpretSoftwarePresentationFollowUp(result.followUp) - return result.outcome } catch let failure as WindowSoftwarePresentationFailure { pendingReservation.reservedFrame.drawingBuffer.discard() - failActivePresentation( - generation: pendingReservation.request.generation, - error: failure.presentationError + failSoftwarePresentationIfStillActive( + generation: pendingReservation.request.generation ) if case .userDraw = failure.presentationError { throw WindowSoftwareDrawFailure(underlying: failure.underlying) @@ -700,18 +821,113 @@ package final class TopLevelWindow { throw failure.underlying } catch { pendingReservation.reservedFrame.drawingBuffer.discard() - failPresentationIfStillActive( - generation: pendingReservation.request.generation, - error: .surfaceCommit(String(describing: error)) + failSoftwarePresentationIfStillActive( + generation: pendingReservation.request.generation ) throw error } + + if result.outcome == .presented { + installSoftwarePresentationSuccess(stagedSuccess) + return .presented + } + + return try interpretDeferredSoftwarePresentation( + result, + pendingReservation: pendingReservation + ) + } + + private func isCurrentSoftwarePresentation( + _ pendingReservation: PendingSoftwareFrameReservation + ) throws -> Bool { + _ = try consumeLatestConfigureIfAvailable() + let currentGeometry = try currentSurfaceGeometry() + return !model.isClosed + && model.isCurrentSoftwarePresentation(pendingReservation.request) + && currentGeometry == pendingReservation.geometry + && !Task.isCancelled + } + + private func softwarePresentationSuccessStagingContext() + -> SoftwarePresentationSuccessStagingContext + { + SoftwarePresentationSuccessStagingContext( + model: model, + windowID: id, + hasPendingSurfaceConfigure: configureState.hasPendingSurfaceConfigure + ) + } + + private func interpretDeferredSoftwarePresentation( + _ result: WindowSoftwarePresentationResult, + pendingReservation: PendingSoftwareFrameReservation + ) throws -> SoftwarePresentationOutcome { + do { + try interpretSoftwarePresentationFollowUp(result.followUp) + switch result.outcome { + case .presented: + preconditionFailure("Presented software frames require staged success") + case .skippedClosed: + return .closed + case .skippedPendingFrame, .waitingForBuffer: + return .deferred + } + } catch { + pendingReservation.reservedFrame.drawingBuffer.discard() + failSoftwarePresentationIfStillActive( + generation: pendingReservation.request.generation + ) + throw error + } + } + + private func supersedeSoftwarePresentation( + _ pendingReservation: PendingSoftwareFrameReservation + ) throws -> SoftwarePresentationOutcome { + pendingReservation.reservedFrame.drawingBuffer.discard() + guard !model.isClosed, + case .drawing(let activeRequest) = model.presentation, + activeRequest == pendingReservation.request + else { + return model.isClosed ? .closed : .superseded + } + + try interpretWindowEffects( + model.reduce( + .softwarePresentationSuperseded( + generation: pendingReservation.request.generation, + bufferAvailability: .available + ) + ) + ) + return .superseded } private func cancelSoftwareFrameReservation(_ reservation: SoftwareFrameReservation) { - guard softwarePresentationCoordinator.cancel(reservation.reservationID) else { return } - resetTransientState() - publishDeferredRedrawAfterReservationCancellation() + guard let pendingReservation = softwarePresentationCoordinator.cancel(reservation) else { + return + } + pendingReservation.reservedFrame.drawingBuffer.discard() + guard !model.isClosed, + case .drawing(let activeRequest) = model.presentation, + activeRequest == pendingReservation.request + else { + return + } + + do { + try interpretWindowEffects( + model.reduce( + .softwarePresentationSuperseded( + generation: pendingReservation.request.generation, + bufferAvailability: .available + ) + ) + ) + } catch { + preconditionFailure("Unexpected software-presentation cancellation error: \(error)") + } } private func cancelAllSoftwareFrameReservations() { @@ -724,24 +940,26 @@ package final class TopLevelWindow { guard let followUp else { return } switch followUp { - case .fail(let generation, let error): - failActivePresentation( - generation: generation, - error: error - ) + case .fail(let generation, _): + failSoftwarePresentationIfStillActive(generation: generation) case .blockedByBuffer: try interpretWindowEffects(model.reduce(.presentationBlockedByBuffer)) case .resetTransientState: try interpretWindowEffects(model.reduce(.transientStateReset)) - case .succeeded(let generation): - try interpretWindowEffects( - model.reduce( - .presentationSucceeded( - generation: generation, - bufferAvailability: try redrawBufferAvailability() - ) - ) - ) + case .succeeded: + preconditionFailure("Software presentation success must be installed from staging") + } + } + + private func installSoftwarePresentationSuccess( + _ stagedSuccess: StagedSoftwarePresentationSuccess? + ) { + guard let stagedSuccess else { + preconditionFailure("Committed software presentation has no staged success") + } + model = stagedSuccess.model + if stagedSuccess.publishesRedrawRequest { + onRedrawRequested?() } } @@ -761,6 +979,27 @@ package final class TopLevelWindow { ) } + private func failSoftwarePresentationIfStillActive(generation: UInt64) { + guard case .drawing(let request) = model.presentation, + request.generation == generation + else { + return + } + + do { + try interpretWindowEffects( + model.reduce( + .softwarePresentationFailed( + generation: generation, + bufferAvailability: .available + ) + ) + ) + } catch { + preconditionFailure("Unexpected software-presentation failure error: \(error)") + } + } + private func failActivePresentation( generation: UInt64, error: PresentationError @@ -1414,6 +1653,13 @@ extension TopLevelWindow { try ensureSubmitConstraintObjectsInstalled(for: submitConstraints) try ensureMetadataObjectsInstalled(for: metadata) try surfaceRuntime.preflightCommitMetadata(metadata) + let successStagingContext = ExternalSuccessStagingContext( + model: model, + windowID: id, + bufferAvailability: bufferAvailability, + capabilities: surfaceRuntime.capabilitySnapshot() + ) + var stagedSuccess: StagedExternalBufferPresentationSuccess? let presentationRequest = WindowExternalBufferPresentationRequest( buffer: buffer, surface: surface, @@ -1428,32 +1674,37 @@ extension TopLevelWindow { } let presentation = try WindowExternalBufferPresenter.present( presentationRequest, + stageSuccess: { commitPlan in + stagedSuccess = try successStagingContext.stage( + generation: generation, + commitPlan: commitPlan + ) + }, runtime: &surfaceRuntime, pendingFrameRegistration: &pendingFrameRegistration ) - do { - try interpretWindowEffects( - model.reduce( - .externalPresentationSucceeded( - generation: generation, - bufferAvailability: bufferAvailability - ) - ) - ) - } catch { - pendingFrameRegistration = nil - surfaceRuntime.cancelFrameCallback() - throw error + guard let stagedSuccess else { + preconditionFailure("Committed external presentation is missing staged success state") } - return try PreviewBufferPresentationResult( - generation: generation, - commitPlan: presentation.commitPlan, - capabilities: surfaceRuntime.capabilitySnapshot(), - presentationFeedbackIdentity: presentation.presentationFeedbackIdentity + return installExternalPresentationSuccess( + stagedSuccess, + feedbackIdentity: presentation.presentationFeedbackIdentity ) } + private func installExternalPresentationSuccess( + _ stagedSuccess: StagedExternalBufferPresentationSuccess, + feedbackIdentity: SurfacePresentationIdentity? + ) -> PreviewBufferPresentationResult { + let result = stagedSuccess.result.withPresentationFeedbackIdentity(feedbackIdentity) + model = stagedSuccess.model + if stagedSuccess.publishesRedrawRequest { + onRedrawRequested?() + } + return result + } + package func importPreviewSynchronizationTimelineOnOwnerThread( _ fileDescriptor: inout RawDrmSyncobjTimelineFD, identity: SurfaceSyncTimelineIdentity @@ -1920,7 +2171,7 @@ extension TopLevelWindow { package func reserveShowSoftwareFrameOnOwnerThread( timeoutMilliseconds: Int32 = defaultConfigureTimeoutMS - ) throws -> SoftwareFrameReservation? { + ) throws -> WindowSoftwareFrameReservationOutcome { connection.preconditionIsOwnerThread() if model.currentConfiguration == nil { @@ -1974,10 +2225,12 @@ extension TopLevelWindow { ) } - package func reserveRedrawSoftwareFrameOnOwnerThread() throws -> SoftwareFrameReservation? { + package func reserveRedrawSoftwareFrameOnOwnerThread() + throws -> WindowSoftwareFrameReservationOutcome + { connection.preconditionIsOwnerThread() - guard !model.isClosed else { return nil } + guard !model.isClosed else { return .closed } _ = try consumeLatestConfigureIfAvailable() return try reserveSoftwareFrameForCurrentRedraw() @@ -1988,17 +2241,17 @@ extension TopLevelWindow { submitConstraints: SurfaceSubmitConstraints = .default, metadata: SurfaceCommitMetadata = .default, damage: SurfaceDamageRegion? = nil, - presentationFeedback: WindowPresentationFeedbackCommitRequest? = nil, + makePresentationFeedback: () throws -> WindowPresentationFeedbackCommitRequest? = { nil }, _ draw: (borrowing SoftwareFrame) throws -> Void - ) throws { + ) throws -> SoftwarePresentationOutcome { connection.preconditionIsOwnerThread() - _ = try submitReservedSoftwareFrame( + return try submitReservedSoftwareFrame( reservation, submitConstraints: submitConstraints, metadata: metadata, damage: damage, - presentationFeedback: presentationFeedback, + makePresentationFeedback: makePresentationFeedback, draw ) } diff --git a/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift b/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift index 3f2295ea..40c4494e 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowExternalBufferPresenter.swift @@ -13,12 +13,9 @@ struct WindowExternalBufferPresentationRequest { } enum WindowExternalBufferPresenter { - private enum PresentationError: Error { - case missingCommitPlan - } - static func present( _ request: WindowExternalBufferPresentationRequest, + stageSuccess: (SurfaceCommitPlan) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration? ) throws -> ( @@ -37,63 +34,71 @@ enum WindowExternalBufferPresenter { ), runtime: &runtime, ) - - return try performCommitSequence( - requestFrameCallback: { - pendingFrameRegistration = try SurfaceFrameCommitter.requestFrameCallback( - on: request.surface, - runtime: &runtime, - generation: request.generation, - onFrame: request.onFrameDone - ) - }, - requestPresentationFeedback: { - try request.presentationFeedback?.request() - }, - commit: { - try SurfaceFrameCommitter.commit( - preparedCommit, - runtime: &runtime - ) - }, - cancelFrameCallback: { - pendingFrameRegistration = nil - runtime.cancelFrameCallback() - }, - cancelPresentationFeedback: { feedbackIdentity in - request.presentationFeedback?.cancel(feedbackIdentity) - } + try SurfaceFrameCommitter.reserveFrameCallback( + runtime: &runtime, + generation: request.generation ) + + do { + return try performCommitSequence( + { + let stagedCommit = try SurfaceFrameCommitter.stage( + preparedCommit, + runtime: &runtime + ) + try stageSuccess(stagedCommit.preparedCommit.plan) + return stagedCommit + }, + requestFrameCallback: { + pendingFrameRegistration = + SurfaceFrameCommitter.requestReservedFrameCallback( + on: request.surface, + onFrame: request.onFrameDone + ) + }, + requestPresentationFeedback: { + requestPresentationFeedbackAtPointOfNoReturn(request.presentationFeedback) + }, + commit: { stagedCommit in + SurfaceFrameCommitter.commit( + stagedCommit, + runtime: &runtime + ) + } + ) + } catch { + pendingFrameRegistration = nil + runtime.cancelFrameCallback() + throw error + } } - static func performCommitSequence( - requestFrameCallback: () throws -> Void, - requestPresentationFeedback: () throws -> SurfacePresentationIdentity?, - commit: () throws -> SurfaceCommitPlan, - cancelFrameCallback: () -> Void, - cancelPresentationFeedback: (SurfacePresentationIdentity) -> Void - ) throws -> ( + static func performCommitSequence( + _ stageSuccess: () throws -> StagedCommit, + requestFrameCallback: () -> Void, + requestPresentationFeedback: () -> SurfacePresentationIdentity?, + commit: (StagedCommit) -> SurfaceCommitPlan + ) rethrows -> ( commitPlan: SurfaceCommitPlan, presentationFeedbackIdentity: SurfacePresentationIdentity? ) { - var committedPlan: SurfaceCommitPlan? - let feedbackIdentity = try WindowSoftwarePresentationCommitSequence.perform { - try requestFrameCallback() - } requestPresentationFeedback: { - try requestPresentationFeedback() - } commit: { - committedPlan = try commit() - } cancelFrameCallback: { - cancelFrameCallback() - } cleanupAfterFailure: { feedbackIdentity in - if let feedbackIdentity { - cancelPresentationFeedback(feedbackIdentity) - } - } + let stagedCommit = try stageSuccess() + requestFrameCallback() + let feedbackIdentity = requestPresentationFeedback() + return (commit(stagedCommit), feedbackIdentity) + } + + private static func requestPresentationFeedbackAtPointOfNoReturn( + _ presentationFeedback: WindowPresentationFeedbackCommitRequest? + ) -> SurfacePresentationIdentity? { + guard let presentationFeedback else { return nil } - guard let committedPlan else { - throw PresentationError.missingCommitPlan + do { + return try presentationFeedback.request() + } catch { + preconditionFailure( + "Prepared presentation feedback request failed: \(error)" + ) } - return (committedPlan, feedbackIdentity) } } diff --git a/Sources/WaylandClient/Internal/Windowing/WindowModel+SoftwarePresentation.swift b/Sources/WaylandClient/Internal/Windowing/WindowModel+SoftwarePresentation.swift new file mode 100644 index 00000000..8acf7da3 --- /dev/null +++ b/Sources/WaylandClient/Internal/Windowing/WindowModel+SoftwarePresentation.swift @@ -0,0 +1,48 @@ +extension WindowModel { + func isCurrentSoftwarePresentation(_ request: PresentationRequest) -> Bool { + guard case .active(let activeState) = lifecycle else { return false } + + return activeState.presentation == .drawing(request: request) + && activeState.configure == request.configuration + && activeState.redraw.matchesCurrentContent( + generation: request.generation, + identity: request.redrawIdentity + ) + } + + mutating func reduceSoftwarePresentationSuperseded( + _ generation: UInt64, + _ bufferAvailability: RedrawBufferAvailability + ) throws -> [WindowEffect] { + try reduceUncommittedSoftwarePresentation(generation, bufferAvailability) + } + + mutating func reduceSoftwarePresentationFailed( + _ generation: UInt64, + _ bufferAvailability: RedrawBufferAvailability + ) throws -> [WindowEffect] { + try reduceUncommittedSoftwarePresentation(generation, bufferAvailability) + } + + private mutating func reduceUncommittedSoftwarePresentation( + _ generation: UInt64, + _ bufferAvailability: RedrawBufferAvailability + ) throws -> [WindowEffect] { + let windowID = id + return try transitionActiveWindowState { activeState in + try Self.requireActivePresentation( + generation: generation, + in: activeState, + windowID: windowID + ) + activeState.presentation = .idle + return Self.mapRedrawEffects( + activeState.redraw.supersedeSoftwarePresentation( + bufferAvailability: bufferAvailability + ), + in: activeState, + windowID: windowID + ) + } + } +} diff --git a/Sources/WaylandClient/Internal/Windowing/WindowModel.swift b/Sources/WaylandClient/Internal/Windowing/WindowModel.swift index 956340f3..055a142c 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowModel.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowModel.swift @@ -77,8 +77,7 @@ package struct WindowModel: Equatable, Sendable { return try reduceInitialCommitSent() case .published: return try reducePublished() - case .configureReceived(let sequence): - return try reduceConfigureReceived(sequence) + case .configureReceived(let sequence): return try reduceConfigureReceived(sequence) case .contentInvalidated(let bufferAvailability): return reduceRedraw(.contentInvalidated, bufferAvailability: bufferAvailability) case .frameBecameReady(let bufferAvailability): @@ -87,13 +86,15 @@ package struct WindowModel: Equatable, Sendable { return reduceRedraw(.bufferBecameAvailable, bufferAvailability: bufferAvailability) case .redrawRequestConsumed(let bufferAvailability): return try reduceRedrawRequestConsumed(bufferAvailability: bufferAvailability) - case .graphicsPreviewPresentationCanceled(let bufferAvailability): - return reduceGraphicsPreviewPresentationCanceled( - bufferAvailability: bufferAvailability - ) + case .graphicsPreviewPresentationCanceled(let availability): + return reduceGraphicsPreviewPresentationCanceled(bufferAvailability: availability) case .presentationStarted(let request): return try reducePresentationStarted(request) case .presentationBlockedByBuffer: return try reducePresentationBlockedByBuffer() + case .softwarePresentationFailed(let generation, let availability): + return try reduceSoftwarePresentationFailed(generation, availability) + case .softwarePresentationSuperseded(let generation, let availability): + return try reduceSoftwarePresentationSuperseded(generation, availability) case .presentationSucceeded(let generation, let bufferAvailability): return try reducePresentationSucceeded( generation: generation, @@ -135,7 +136,7 @@ extension WindowModel { return state } - private mutating func transitionActiveWindowState( + mutating func transitionActiveWindowState( _ update: (inout ActiveWindowState) throws -> [WindowEffect] ) throws -> [WindowEffect] { var activeState = try requireActiveWindowState() @@ -251,7 +252,8 @@ extension WindowModel { let generation = activeState.redraw.generationForCurrentDraw let request = PresentationRequest( generation: generation, - configuration: activeState.configure + configuration: activeState.configure, + redrawIdentity: activeState.redraw.identityForCurrentDraw ) activeState.presentation = .requested(request: request) return [.performSoftwarePresent(request)] @@ -405,7 +407,7 @@ extension WindowModel { Self.mapRedrawEffects(effects, windowID: id) } - private static func mapRedrawEffects( + static func mapRedrawEffects( _ effects: [WindowRedrawEffect], in activeState: ActiveWindowState, windowID: WindowID @@ -439,7 +441,7 @@ extension WindowModel { return generation } - private static func requireActivePresentation( + static func requireActivePresentation( generation actualGeneration: UInt64, in activeState: ActiveWindowState, windowID: WindowID diff --git a/Sources/WaylandClient/Internal/Windowing/WindowModelTypes.swift b/Sources/WaylandClient/Internal/Windowing/WindowModelTypes.swift index 08eb4279..843caa06 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowModelTypes.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowModelTypes.swift @@ -17,6 +17,14 @@ package enum WindowEvent: Equatable, Sendable { ) case presentationStarted(PresentationRequest) case presentationBlockedByBuffer + case softwarePresentationFailed( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) + case softwarePresentationSuperseded( + generation: UInt64, + bufferAvailability: RedrawBufferAvailability + ) case presentationSucceeded(generation: UInt64, bufferAvailability: RedrawBufferAvailability) case externalPresentationSucceeded( generation: UInt64, @@ -45,6 +53,17 @@ package enum WindowEffect: Equatable, Sendable { package struct PresentationRequest: Equatable, Sendable { let generation: UInt64 let configuration: ResolvedWindowConfiguration + let redrawIdentity: WindowRedrawContentIdentity + + init( + generation: UInt64, + configuration: ResolvedWindowConfiguration, + redrawIdentity: WindowRedrawContentIdentity = .initial + ) { + self.generation = generation + self.configuration = configuration + self.redrawIdentity = redrawIdentity + } var summary: WindowPresentationRequestSummary { WindowPresentationRequestSummary( @@ -75,11 +94,36 @@ package struct PreviewBufferPresentationResult: Equatable, Sendable { throw PreviewBufferPresentationResultError.invalidGeneration(commitGeneration) } + self.init( + validatedGeneration: commitGeneration, + commitPlan: surfaceCommitPlan, + capabilities: surfaceCapabilities, + presentationFeedbackIdentity: feedbackIdentity + ) + } + + private init( + validatedGeneration commitGeneration: UInt64, + commitPlan surfaceCommitPlan: SurfaceCommitPlan, + capabilities surfaceCapabilities: SurfaceCapabilitySnapshot, + presentationFeedbackIdentity feedbackIdentity: SurfacePresentationIdentity? + ) { generation = commitGeneration commitPlan = surfaceCommitPlan capabilities = surfaceCapabilities presentationFeedbackIdentity = feedbackIdentity } + + package func withPresentationFeedbackIdentity( + _ feedbackIdentity: SurfacePresentationIdentity? + ) -> Self { + Self( + validatedGeneration: generation, + commitPlan: commitPlan, + capabilities: capabilities, + presentationFeedbackIdentity: feedbackIdentity + ) + } } package enum PreviewBufferPresentationResultError: Error, Equatable, Sendable { diff --git a/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift b/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift index df05fe99..826e5f1a 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowOwnedResourceLedger.swift @@ -26,6 +26,14 @@ final class WindowOwnedResourceLedger { 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) @@ -93,14 +101,14 @@ final class WindowSoftwareReservationCoordinator { _ = resources.insert(reservation, for: identity) } - func take(_ identity: SoftwareFrameReservationToken) -> PendingSoftwareFrameReservation? { - resources.take(identity) + func take(_ reservation: SoftwareFrameReservation) -> PendingSoftwareFrameReservation? { + resources.take(reservation.reservationID) { pendingReservation in + pendingReservation.reservedFrame.reservation == reservation + } } - func cancel(_ identity: SoftwareFrameReservationToken) -> Bool { - guard let reservation = resources.take(identity) else { return false } - reservation.reservedFrame.drawingBuffer.discard() - return true + func cancel(_ reservation: SoftwareFrameReservation) -> PendingSoftwareFrameReservation? { + take(reservation) } func close() { diff --git a/Sources/WaylandClient/Internal/Windowing/WindowRedrawState.swift b/Sources/WaylandClient/Internal/Windowing/WindowRedrawState.swift index d5176183..5975c630 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowRedrawState.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowRedrawState.swift @@ -20,6 +20,18 @@ enum WindowRedrawEffect: Equatable, Sendable { case publishRedrawRequested } +struct WindowRedrawContentIdentity: Equatable, Sendable { + static let initial = Self(rawValue: 0) + + let rawValue: UInt64 + + func incremented() -> Self { + let (nextValue, overflowed) = rawValue.addingReportingOverflow(1) + precondition(!overflowed, "Window redraw content identity exhausted") + return Self(rawValue: nextValue) + } +} + package enum RedrawBufferAvailability: Equatable, Sendable { case available case unavailable @@ -72,6 +84,7 @@ struct WindowRedrawState: Equatable, Sendable { } private var contentGeneration: UInt64 = 0 + private var contentIdentity = WindowRedrawContentIdentity.initial private var presentedGeneration: UInt64 = 0 private var presentationStatus = PresentationStatus.current private var pacing = Pacing.frameReady(.none) @@ -92,6 +105,17 @@ struct WindowRedrawState: Equatable, Sendable { contentGeneration } + var identityForCurrentDraw: WindowRedrawContentIdentity { + contentIdentity + } + + func matchesCurrentContent( + generation: UInt64, + identity: WindowRedrawContentIdentity + ) -> Bool { + generation == contentGeneration && identity == contentIdentity + } + mutating func reduce( _ event: WindowRedrawEvent, bufferAvailability: RedrawBufferAvailability @@ -130,7 +154,9 @@ struct WindowRedrawState: Equatable, Sendable { extension WindowRedrawState { private mutating func invalidateContent() { + let nextIdentity = contentIdentity.incremented() contentGeneration &+= 1 + contentIdentity = nextIdentity presentationStatus = .invalidated } @@ -163,6 +189,19 @@ extension WindowRedrawState { return publishIfNeeded(bufferAvailability: bufferAvailability) } + mutating func supersedeSoftwarePresentation( + bufferAvailability: RedrawBufferAvailability + ) -> [WindowRedrawEffect] { + switch pacing { + case .frameReady(.outstanding), .waitingForBuffer: + pacing = .frameReady(.none) + case .frameReady(.none), .waitingForFrame: + break + } + + return publishIfNeeded(bufferAvailability: bufferAvailability) + } + private mutating func markDrawBlockedByBuffer() { guard isDirty else { return } pacing = .waitingForBuffer diff --git a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift b/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift new file mode 100644 index 00000000..0fe7e221 --- /dev/null +++ b/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresentationTypes.swift @@ -0,0 +1,82 @@ +import WaylandRaw + +struct WindowSoftwarePresentationResult { + let outcome: RedrawOutcome + let followUp: WindowSoftwarePresentationFollowUp? +} + +struct WindowReservedSoftwareFrame { + let reservation: SoftwareFrameReservation + let drawingBuffer: RawBuffer.ReservedDrawingBuffer +} + +struct WindowSoftwareFrameReservationResult { + let reservedFrame: WindowReservedSoftwareFrame? + let followUp: WindowSoftwarePresentationFollowUp? +} + +package enum WindowSoftwareFrameReservationOutcome: Equatable, Sendable { + case reserved(SoftwareFrameReservation) + case deferred + case closed +} + +enum WindowSoftwarePresentationFollowUp { + case fail(generation: UInt64, PresentationError) + case blockedByBuffer + case resetTransientState + case succeeded(generation: UInt64) +} + +struct WindowSoftwarePresentationFailure: Error { + let presentationError: PresentationError + let underlying: any Error +} + +package struct WindowSoftwareDrawFailure: Error { + package let underlying: any Error + + package init(underlying drawError: any Error) { + underlying = drawError + } +} + +package struct WindowPresentationFeedbackCommitRequest { + let request: () throws -> SurfacePresentationIdentity + let cancel: (SurfacePresentationIdentity) -> Void + + package init( + request feedbackRequest: @escaping () throws -> SurfacePresentationIdentity, + cancel cancelFeedback: @escaping (SurfacePresentationIdentity) -> Void + ) { + request = feedbackRequest + cancel = cancelFeedback + } +} + +package enum WindowSoftwarePresentationCommitSequence { + @discardableResult + package static func perform( + stageSuccess: () throws -> Void, + markDrawingBufferBusy: () -> Void, + requestFrameCallback: () -> Void, + requestPresentationFeedback: () -> SurfacePresentationIdentity?, + commit: () -> Void + ) rethrows -> SurfacePresentationIdentity? { + try stageSuccess() + markDrawingBufferBusy() + requestFrameCallback() + let feedbackIdentity = requestPresentationFeedback() + commit() + return feedbackIdentity + } +} + +struct WindowSoftwarePresentationContext { + let request: PresentationRequest + let geometry: SurfaceGeometry + let submitConstraints: SurfaceSubmitConstraints + let metadata: SurfaceCommitMetadata + let damage: SurfaceDamageRegion? + let presentationFeedback: WindowPresentationFeedbackCommitRequest? +} diff --git a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift b/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift index 07b5906f..285c666c 100644 --- a/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift +++ b/Sources/WaylandClient/Internal/Windowing/WindowSoftwarePresenter.swift @@ -1,103 +1,9 @@ -// swiftlint:disable file_length - import WaylandRaw -struct WindowSoftwarePresentationResult { - let outcome: RedrawOutcome - let followUp: WindowSoftwarePresentationFollowUp? -} - -struct WindowReservedSoftwareFrame { - let reservation: SoftwareFrameReservation - let drawingBuffer: RawBuffer.ReservedDrawingBuffer -} - -struct WindowSoftwareFrameReservationResult { - let reservedFrame: WindowReservedSoftwareFrame? - let followUp: WindowSoftwarePresentationFollowUp? -} - -enum WindowSoftwarePresentationFollowUp { - case fail(generation: UInt64, PresentationError) - case blockedByBuffer - case resetTransientState - case succeeded(generation: UInt64) -} - -struct WindowSoftwarePresentationFailure: Error { - let presentationError: PresentationError - let underlying: any Error -} - -package struct WindowSoftwareDrawFailure: Error { - package let underlying: any Error - - package init(underlying drawError: any Error) { - underlying = drawError - } -} - -package struct WindowPresentationFeedbackCommitRequest { - let request: () throws -> SurfacePresentationIdentity - let cancel: (SurfacePresentationIdentity) -> Void - - package init( - request feedbackRequest: @escaping () throws -> SurfacePresentationIdentity, - cancel cancelFeedback: @escaping (SurfacePresentationIdentity) -> Void - ) { - request = feedbackRequest - cancel = cancelFeedback - } -} - -package enum WindowSoftwarePresentationCommitSequence { - @discardableResult - package static func perform( - requestFrameCallback: () throws -> Void, - requestPresentationFeedback: () throws -> SurfacePresentationIdentity?, - commit: () throws -> Void, - cancelFrameCallback: () -> Void, - cleanupAfterFailure: (SurfacePresentationIdentity?) -> Void - ) throws -> SurfacePresentationIdentity? { - do { - try requestFrameCallback() - } catch { - cleanupAfterFailure(nil) - throw error - } - - let feedbackIdentity: SurfacePresentationIdentity? - do { - feedbackIdentity = try requestPresentationFeedback() - } catch { - cancelFrameCallback() - cleanupAfterFailure(nil) - throw error - } - - do { - try commit() - return feedbackIdentity - } catch { - cancelFrameCallback() - cleanupAfterFailure(feedbackIdentity) - throw error - } - } -} - -struct WindowSoftwarePresentationContext { - let request: PresentationRequest - let geometry: SurfaceGeometry - let submitConstraints: SurfaceSubmitConstraints - let metadata: SurfaceCommitMetadata - let damage: SurfaceDamageRegion? - let presentationFeedback: WindowPresentationFeedbackCommitRequest? -} - private struct WindowSoftwareCommitContext { let preparedCommit: PreparedSurfaceFrameCommit let request: PresentationRequest + let bufferSize: TopLevelSize let presentationFeedback: WindowPresentationFeedbackCommitRequest? } @@ -112,6 +18,7 @@ struct WindowSoftwarePresenter { func present( context: WindowSoftwarePresentationContext, draw: (borrowing SoftwareFrame) throws -> Void, + stageSuccess: (RedrawBufferAvailability) throws -> Void = { _ in () }, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration? ) throws -> WindowSoftwarePresentationResult { @@ -154,8 +61,10 @@ struct WindowSoftwarePresenter { context: WindowSoftwareCommitContext( preparedCommit: preparedCommit, request: context.request, + bufferSize: context.geometry.bufferSize.rawSize, presentationFeedback: context.presentationFeedback ), + stageSuccess: stageSuccess, runtime: &runtime, pendingFrameRegistration: &pendingFrameRegistration, drawingBuffer: &drawingBuffer @@ -218,6 +127,7 @@ struct WindowSoftwarePresenter { _ reservedFrame: WindowReservedSoftwareFrame, context: WindowSoftwarePresentationContext, draw: (borrowing SoftwareFrame) throws -> Void, + stageSuccess: (RedrawBufferAvailability) throws -> Void = { _ in () }, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration? ) throws -> WindowSoftwarePresentationResult { @@ -252,8 +162,10 @@ struct WindowSoftwarePresenter { context: WindowSoftwareCommitContext( preparedCommit: preparedCommit, request: context.request, + bufferSize: context.geometry.bufferSize.rawSize, presentationFeedback: context.presentationFeedback ), + stageSuccess: stageSuccess, runtime: &runtime, pendingFrameRegistration: &pendingFrameRegistration, drawingBuffer: reservedFrame.drawingBuffer @@ -267,39 +179,52 @@ struct WindowSoftwarePresenter { private func performPreparedCommit( context: WindowSoftwareCommitContext, + stageSuccess: (RedrawBufferAvailability) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration?, drawingBuffer: inout RawBuffer.DrawingBuffer ) throws { - _ = try WindowSoftwarePresentationCommitSequence.perform( - requestFrameCallback: { - try requestFrameCallback( - request: context.request, - runtime: &runtime, - pendingFrameRegistration: &pendingFrameRegistration - ) - }, - requestPresentationFeedback: { - try requestPresentationFeedback(context.presentationFeedback) - }, - commit: { - try recordAndCommit( - context: context, - runtime: &runtime, - drawingBuffer: &drawingBuffer - ) - }, - cancelFrameCallback: { - pendingFrameRegistration = nil - runtime.cancelFrameCallback() - }, - cleanupAfterFailure: { identity in - if let identity { - context.presentationFeedback?.cancel(identity) - } - drawingBuffer.discard() - } + let stagedCommit: StagedSurfaceFrameCommit + do { + stagedCommit = try stageCommit(context: context, runtime: &runtime) + } catch { + drawingBuffer.discard() + throw error + } + + let currentBufferAvailability = runtime.redrawBufferAvailability( + matching: context.bufferSize ) + do { + _ = try WindowSoftwarePresentationCommitSequence.perform( + stageSuccess: { + try stageSuccess(currentBufferAvailability) + }, + markDrawingBufferBusy: { + _ = drawingBuffer.markBusy(commitGeneration: context.request.generation) + }, + requestFrameCallback: { + requestReservedFrameCallback( + pendingFrameRegistration: &pendingFrameRegistration + ) + }, + requestPresentationFeedback: { + requestPresentationFeedbackAtPointOfNoReturn( + context.presentationFeedback + ) + }, + commit: { + SurfaceFrameCommitter.commit( + stagedCommit, + runtime: &runtime, + ) + } + ) + } catch { + runtime.cancelFrameCallback() + drawingBuffer.discard() + throw error + } } private func drawFrame( @@ -420,53 +345,52 @@ struct WindowSoftwarePresenter { } } - private func requestFrameCallback( - request: PresentationRequest, - runtime: inout SurfaceRuntime, + private func requestReservedFrameCallback( pendingFrameRegistration: inout FrameCallbackRegistration? - ) throws { - do { - pendingFrameRegistration = try SurfaceFrameCommitter.requestFrameCallback( - on: surface, - runtime: &runtime, - generation: request.generation, - onFrame: onFrame - ) - } catch { - throw WindowSoftwarePresentationFailure( - presentationError: .frameCallbackRequest(String(describing: error)), - underlying: error - ) - } + ) { + pendingFrameRegistration = SurfaceFrameCommitter.requestReservedFrameCallback( + on: surface, + onFrame: onFrame + ) } - private func requestPresentationFeedback( + private func requestPresentationFeedbackAtPointOfNoReturn( _ presentationFeedback: WindowPresentationFeedbackCommitRequest? - ) throws -> SurfacePresentationIdentity? { + ) -> SurfacePresentationIdentity? { guard let presentationFeedback else { return nil } do { return try presentationFeedback.request() } catch { - throw WindowSoftwarePresentationFailure( - presentationError: .presentationFeedbackRequest(String(describing: error)), - underlying: error + preconditionFailure( + "Prepared presentation feedback request failed: \(error)" ) } } - private func recordAndCommit( + private func stageCommit( context: WindowSoftwareCommitContext, - runtime: inout SurfaceRuntime, - drawingBuffer: inout RawBuffer.DrawingBuffer - ) throws { + runtime: inout SurfaceRuntime + ) throws -> StagedSurfaceFrameCommit { + do { + try SurfaceFrameCommitter.reserveFrameCallback( + runtime: &runtime, + generation: context.request.generation + ) + } catch { + throw WindowSoftwarePresentationFailure( + presentationError: .frameCallbackRequest(String(describing: error)), + underlying: error + ) + } + do { - _ = drawingBuffer.markBusy(commitGeneration: context.request.generation) - try SurfaceFrameCommitter.commit( + return try SurfaceFrameCommitter.stage( context.preparedCommit, - runtime: &runtime + runtime: &runtime, ) } catch { + runtime.cancelFrameCallback() throw WindowSoftwarePresentationFailure( presentationError: .surfaceCommit(String(describing: error)), underlying: error @@ -476,57 +400,51 @@ struct WindowSoftwarePresenter { private func performPreparedReservedCommit( context: WindowSoftwareCommitContext, + stageSuccess: (RedrawBufferAvailability) throws -> Void, runtime: inout SurfaceRuntime, pendingFrameRegistration: inout FrameCallbackRegistration?, drawingBuffer: RawBuffer.ReservedDrawingBuffer ) throws { - _ = try WindowSoftwarePresentationCommitSequence.perform( - requestFrameCallback: { - try requestFrameCallback( - request: context.request, - runtime: &runtime, - pendingFrameRegistration: &pendingFrameRegistration - ) - }, - requestPresentationFeedback: { - try requestPresentationFeedback(context.presentationFeedback) - }, - commit: { - try recordAndCommitReserved( - context: context, - runtime: &runtime, - drawingBuffer: drawingBuffer - ) - }, - cancelFrameCallback: { - pendingFrameRegistration = nil - runtime.cancelFrameCallback() - }, - cleanupAfterFailure: { identity in - if let identity { - context.presentationFeedback?.cancel(identity) - } - drawingBuffer.discard() - } - ) - } + let stagedCommit: StagedSurfaceFrameCommit + do { + stagedCommit = try stageCommit(context: context, runtime: &runtime) + } catch { + drawingBuffer.discard() + throw error + } - private func recordAndCommitReserved( - context: WindowSoftwareCommitContext, - runtime: inout SurfaceRuntime, - drawingBuffer: RawBuffer.ReservedDrawingBuffer - ) throws { + let currentBufferAvailability = runtime.redrawBufferAvailability( + matching: context.bufferSize + ) do { - _ = drawingBuffer.markBusy(commitGeneration: context.request.generation) - try SurfaceFrameCommitter.commit( - context.preparedCommit, - runtime: &runtime + _ = try WindowSoftwarePresentationCommitSequence.perform( + stageSuccess: { + try stageSuccess(currentBufferAvailability) + }, + markDrawingBufferBusy: { + _ = drawingBuffer.markBusy(commitGeneration: context.request.generation) + }, + requestFrameCallback: { + requestReservedFrameCallback( + pendingFrameRegistration: &pendingFrameRegistration + ) + }, + requestPresentationFeedback: { + requestPresentationFeedbackAtPointOfNoReturn( + context.presentationFeedback + ) + }, + commit: { + SurfaceFrameCommitter.commit( + stagedCommit, + runtime: &runtime, + ) + } ) } catch { - throw WindowSoftwarePresentationFailure( - presentationError: .surfaceCommit(String(describing: error)), - underlying: error - ) + runtime.cancelFrameCallback() + drawingBuffer.discard() + throw error } } } diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift index 9bca12bc..37d49037 100644 --- a/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay+SoftwarePresentation.swift @@ -39,8 +39,9 @@ extension WaylandDisplay { package func reserveSoftwareFrameForShow( _ windowID: WindowID, timeoutMilliseconds: Int32 - ) throws -> SoftwareFrameReservation? { - try requireCore().reserveSoftwareFrameForShow( + ) throws -> WindowSoftwareFrameReservationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.reserveSoftwareFrameForShow( windowID, timeoutMilliseconds: timeoutMilliseconds ) @@ -80,8 +81,9 @@ extension WaylandDisplay { package func reserveSoftwareFrameForRedraw( _ windowID: WindowID - ) throws -> SoftwareFrameReservation? { - try requireCore().reserveSoftwareFrameForRedraw(windowID) + ) throws -> WindowSoftwareFrameReservationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.reserveSoftwareFrameForRedraw(windowID) } // swiftlint:disable:next function_parameter_count @@ -93,8 +95,9 @@ extension WaylandDisplay { requestPresentationFeedback: Bool, damage: SurfaceDamageRegion? = nil, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void - ) throws { - try requireCore().submitReservedSoftwareFrame( + ) throws -> SoftwarePresentationOutcome { + guard let core = coreIfActive() else { return .closed } + return try core.submitReservedSoftwareFrame( windowID, reservation: reservation, submitConstraints: submitConstraints, @@ -109,6 +112,7 @@ extension WaylandDisplay { _ windowID: WindowID, reservation: SoftwareFrameReservation ) throws { - try requireCore().cancelSoftwareFrameReservation(windowID, reservation: reservation) + guard let core = coreIfActive() else { return } + try core.cancelSoftwareFrameReservation(windowID, reservation: reservation) } } diff --git a/Sources/WaylandClient/Public/Display/WaylandDisplay.swift b/Sources/WaylandClient/Public/Display/WaylandDisplay.swift index 33499197..737affdd 100644 --- a/Sources/WaylandClient/Public/Display/WaylandDisplay.swift +++ b/Sources/WaylandClient/Public/Display/WaylandDisplay.swift @@ -314,6 +314,11 @@ public actor WaylandDisplay { return core } + + func coreIfActive() -> DisplayCore? { + guard case .active(let core, _) = lifecycle else { return nil } + return core + } } extension WaylandDisplay { diff --git a/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift index 999c4b3b..aa39c468 100644 --- a/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift +++ b/Sources/WaylandClient/Public/Window/Window+SoftwareFramePreparation.swift @@ -1,95 +1,190 @@ +/// The terminal disposition of an asynchronously prepared software frame. +public enum SoftwarePresentationOutcome: Equatable, Sendable { + /// The frame was committed to the window's Wayland surface. + 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. + case closed +} + extension Window { + @discardableResult + public func show( + requestPresentationFeedback: Bool, + timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await show( + damage: nil, + requestPresentationFeedback: requestPresentationFeedback, + timeoutMilliseconds: timeoutMilliseconds, + draw + ) + } + + @discardableResult + public func show( + damage: SurfaceDamageRegion?, + requestPresentationFeedback: Bool, + timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await show( + damage: damage, + timeoutMilliseconds: timeoutMilliseconds, + requestPresentationFeedback: requestPresentationFeedback, + preparing: { _ in () }, + { _, frame in + try draw(frame) + } + ) + } + + @discardableResult public func show( timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, + requestPresentationFeedback: Bool = false, preparing prepare: sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void - ) async throws { + ) async throws -> SoftwarePresentationOutcome { try await show( damage: nil, timeoutMilliseconds: timeoutMilliseconds, + requestPresentationFeedback: requestPresentationFeedback, preparing: prepare, draw ) } + @discardableResult public func show( damage: SurfaceDamageRegion?, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, + requestPresentationFeedback: Bool = false, preparing prepare: sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void - ) async throws { - guard - let reservation = try await display.reserveSoftwareFrameForShow( - id, - timeoutMilliseconds: timeoutMilliseconds - ) - else { - return + ) async throws -> SoftwarePresentationOutcome { + let reservationOutcome = try await display.reserveSoftwareFrameForShow( + id, + timeoutMilliseconds: timeoutMilliseconds + ) + let reservation: SoftwareFrameReservation + switch reservationOutcome { + case .reserved(let reservedFrame): + reservation = reservedFrame + case .deferred: + return .deferred + case .closed: + return .closed } do { let prepared = try await prepare(reservation) - try await display.submitReservedSoftwareFrame( + return try await display.submitReservedSoftwareFrame( id, reservation: reservation, submitConstraints: .default, metadata: .default, - requestPresentationFeedback: false, + requestPresentationFeedback: requestPresentationFeedback, damage: damage ) { frame in try draw(prepared, frame) } } catch { - let submitError = error + let submissionError = error do { try await display.cancelSoftwareFrameReservation(id, reservation: reservation) } catch { - throw submitError + throw submissionError } - throw submitError + throw submissionError } } + @discardableResult + public func redraw( + requestPresentationFeedback: Bool, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await redraw( + damage: nil, + requestPresentationFeedback: requestPresentationFeedback, + draw + ) + } + + @discardableResult + public func redraw( + damage: SurfaceDamageRegion?, + requestPresentationFeedback: Bool, + _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void + ) async throws -> SoftwarePresentationOutcome { + try await redraw( + damage: damage, + requestPresentationFeedback: requestPresentationFeedback, + preparing: { _ in () }, + { _, frame in + try draw(frame) + } + ) + } + + @discardableResult public func redraw( + requestPresentationFeedback: Bool = false, preparing prepare: sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void - ) async throws { + ) async throws -> SoftwarePresentationOutcome { try await redraw( damage: nil, + requestPresentationFeedback: requestPresentationFeedback, preparing: prepare, draw ) } + @discardableResult public func redraw( damage: SurfaceDamageRegion?, + requestPresentationFeedback: Bool = false, preparing prepare: sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void - ) async throws { - guard let reservation = try await display.reserveSoftwareFrameForRedraw(id) else { - return + ) async throws -> SoftwarePresentationOutcome { + let reservationOutcome = try await display.reserveSoftwareFrameForRedraw(id) + let reservation: SoftwareFrameReservation + switch reservationOutcome { + case .reserved(let reservedFrame): + reservation = reservedFrame + case .deferred: + return .deferred + case .closed: + return .closed } do { let prepared = try await prepare(reservation) - try await display.submitReservedSoftwareFrame( + return try await display.submitReservedSoftwareFrame( id, reservation: reservation, submitConstraints: .default, metadata: .default, - requestPresentationFeedback: false, + requestPresentationFeedback: requestPresentationFeedback, damage: damage ) { frame in try draw(prepared, frame) } } catch { - let submitError = error + let submissionError = error do { try await display.cancelSoftwareFrameReservation(id, reservation: reservation) } catch { - throw submitError + throw submissionError } - throw submitError + throw submissionError } } } diff --git a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md index b6bc021e..9e1641e2 100644 --- a/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md +++ b/Sources/WaylandClient/WaylandClient.docc/PresentationFeedbackAndFrameCallbacks.md @@ -24,6 +24,15 @@ presentation feedback. WaylandClientKit owns requests, event correlation, and stream termination. Frameworks own animation, frame budgeting, and fallback scheduling. +For asynchronously prepared software frames, pass +`requestPresentationFeedback: true` to +``Window/show(damage:timeoutMilliseconds:requestPresentationFeedback:preparing:_:)`` +or ``Window/redraw(damage: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. + ## Example See `PresentationFeedbackAnimation` in `Examples/PresentationFeedbackAnimation`. diff --git a/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md b/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md index 773553a8..b45dbc37 100644 --- a/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md +++ b/Sources/WaylandClient/WaylandClient.docc/WaylandClient.md @@ -48,6 +48,8 @@ targets. ### Rendering - ``SoftwareFrame`` +- ``SoftwareFrameReservation`` +- ``SoftwarePresentationOutcome`` - ``SurfaceGeometry`` - ``SurfaceScale`` - ``OutputSnapshot`` diff --git a/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md b/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md index f363cef8..1698dbc8 100644 --- a/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md +++ b/Sources/WaylandClient/WaylandClient.docc/WindowDrawing.md @@ -9,14 +9,51 @@ 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(damage:timeoutMilliseconds:preparing:_:)`` and -``Window/redraw(damage:preparing:_:)`` when expensive scene preparation should +Use +``Window/show(damage:timeoutMilliseconds:requestPresentationFeedback:preparing:_:)`` +and ``Window/redraw(damage: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. +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(damage:requestPresentationFeedback:timeoutMilliseconds:_:)`` or +``Window/redraw(damage:requestPresentationFeedback:_:)`` to request feedback +atomically without a no-op preparation closure. These overloads return the same +``SoftwarePresentationOutcome``. + +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. diff --git a/Sources/WaylandClientKitTool/main.swift b/Sources/WaylandClientKitTool/main.swift index d900d10c..7980fbc0 100644 --- a/Sources/WaylandClientKitTool/main.swift +++ b/Sources/WaylandClientKitTool/main.swift @@ -1183,6 +1183,7 @@ private func runRequestPathTests(context: ToolContext, sanitizer: RequestPathSan let filters = [ "WindowControlPublicRequestTests", + "WindowSoftwarePresentationPublicRequestTests", "WindowDragSourcePublicRequestTests", "SubsurfacePublicRequestTests", "DesktopIntegrationPublicRequestTests", @@ -1436,6 +1437,7 @@ private func runFoundationCheck(context: ToolContext) throws { private func requestTestEnvironment() -> [String: String] { [ "WAYLAND_CLIENT_KIT_ENABLE_WINDOW_CONTROL_REQUEST_TESTS": "1", + "WAYLAND_CLIENT_KIT_ENABLE_SOFTWARE_PRESENTATION_REQUEST_TESTS": "1", "WAYLAND_CLIENT_KIT_ENABLE_DND_SOURCE_REQUEST_TESTS": "1", "WAYLAND_CLIENT_KIT_ENABLE_SUBSURFACE_REQUEST_TESTS": "1", "WAYLAND_CLIENT_KIT_ENABLE_DESKTOP_REQUEST_TESTS": "1", diff --git a/Tests/WaylandClientKitToolTests/RequestBridgeGenerationTests.swift b/Tests/WaylandClientKitToolTests/RequestBridgeGenerationTests.swift index bafe1e46..0854d15c 100644 --- a/Tests/WaylandClientKitToolTests/RequestBridgeGenerationTests.swift +++ b/Tests/WaylandClientKitToolTests/RequestBridgeGenerationTests.swift @@ -40,14 +40,15 @@ struct RequestBridgeGenerationTests { let names = Set(bridges.map(\.functionName)) #expect(policy.interfaces.count == 38) - #expect(bridges.count == 61) + #expect(bridges.count == 60) #expect(names.contains("swl_compositor_create_surface")) - #expect(names.contains("swl_surface_frame")) + #expect(!names.contains("swl_surface_frame")) #expect(names.contains("swl_seat_get_pointer")) #expect(names.contains("swl_data_device_manager_create_data_source")) #expect(names.contains("swl_primary_selection_device_manager_get_device")) #expect(!names.contains("swl_wl_compositor_create_surface")) #expect(!names.contains("swl_wl_data_device_manager_create_data_source")) + #expect(policy.interfaces["wl_surface"]?.handwrittenByDefault == .testRecording) #expect( policy.interfaces["wp_linux_drm_syncobj_manager_v1"]? .handwrittenRequests["import_timeline"] == .failureInjection diff --git a/Tests/WaylandClientTests/Popup/PopupModelPresentationTests.swift b/Tests/WaylandClientTests/Popup/PopupModelPresentationTests.swift index 177dbcb3..50231ff4 100644 --- a/Tests/WaylandClientTests/Popup/PopupModelPresentationTests.swift +++ b/Tests/WaylandClientTests/Popup/PopupModelPresentationTests.swift @@ -101,6 +101,50 @@ struct PopupModelPresentationTests { #expect(model.presentation == .idle) } + @Test + func presentationSuccessStagesOnCopiedModel() throws { + let (model, request) = try activeModelWithStartedPresentation() + + let stagedSuccess = try PopupPresentationSuccessStagingContext( + model: model, + parentWindowID: parentWindowID + ).stage( + generation: request.generation, + bufferAvailability: .available + ) + + #expect(model.presentation == .drawing(request: request)) + #expect(stagedSuccess.model.presentation == .idle) + #expect(!stagedSuccess.publishesRedrawRequest) + } + + @Test + func presentationSuccessStagingFailureLeavesSourceModelDrawing() throws { + let (model, request) = try activeModelWithStartedPresentation() + + #expect( + throws: ClientError.window( + parentWindowID, + .invalidLifecycleTransition( + .presentationGenerationMismatch( + expected: request.generation, + actual: request.generation + 1 + ) + ) + ) + ) { + _ = try PopupPresentationSuccessStagingContext( + model: model, + parentWindowID: parentWindowID + ).stage( + generation: request.generation + 1, + bufferAvailability: .available + ) + } + + #expect(model.presentation == .drawing(request: request)) + } + private func popupModel() -> PopupModel { PopupModel( id: popupID, @@ -124,6 +168,17 @@ struct PopupModelPresentationTests { return model } + private func activeModelWithStartedPresentation() throws -> ( + model: PopupModel, + request: PopupPresentationRequest + ) { + var model = try activeModel() + let effects = try model.reduce(.redrawRequestConsumed(bufferAvailability: .available)) + let request = try #require(presentationRequest(from: effects)) + _ = try model.reduce(.presentationStarted(request)) + return (model, request) + } + private func configure(serial: UInt32) -> PopupConfigureSequence { PopupConfigureSequence( serial: serial, diff --git a/Tests/WaylandClientTests/Rendering/SurfaceRuntimeSubmitTests.swift b/Tests/WaylandClientTests/Rendering/SurfaceRuntimeSubmitTests.swift index a56f0269..04dcbf50 100644 --- a/Tests/WaylandClientTests/Rendering/SurfaceRuntimeSubmitTests.swift +++ b/Tests/WaylandClientTests/Rendering/SurfaceRuntimeSubmitTests.swift @@ -12,6 +12,20 @@ @Suite(.serialized) struct SurfaceRuntimeSubmitTests { // swiftlint:disable:this type_body_length + @Test + func reservingPendingFrameCallbackFailsBeforeRequestCreation() throws { + var runtime = SurfaceRuntime(role: .toplevelWindow) + try runtime.requestFrameCallback(generation: 17) + + #expect(throws: SurfaceTransactionError.frameCallbackAlreadyPending(generation: 17)) { + try SurfaceFrameCommitter.reserveFrameCallback( + runtime: &runtime, + generation: 18 + ) + } + #expect(runtime.transactionSnapshot.pendingFrameCallbackGeneration == 17) + } + @Test func submitCapabilitySnapshotPublishesSynchronizationAndPacingFacts() { var runtime = SurfaceRuntime(role: .toplevelWindow) @@ -134,31 +148,31 @@ } @Test - func externalBufferCommitSequenceRequestsPresentationFeedbackBeforeCommit() + func externalBufferCommitSequenceStagesSuccessBeforeProtocolRequests() throws { var events: [ExternalBufferPresentationEvent] = [] let identity = SurfacePresentationIdentity(rawValue: 31) let expectedPlan = try testSurfaceCommitPlan() - let presentation = try WindowExternalBufferPresenter.performCommitSequence { + let presentation = WindowExternalBufferPresenter.performCommitSequence { + events.append(.stageSuccess) + return expectedPlan + } requestFrameCallback: { events.append(.frameCallback) } requestPresentationFeedback: { events.append(.presentationFeedback) return identity - } commit: { + } commit: { stagedPlan in events.append(.commit) - return expectedPlan - } cancelFrameCallback: { - events.append(.cancelFrameCallback) - } cancelPresentationFeedback: { cancelledIdentity in - events.append(.cancelPresentationFeedback(cancelledIdentity)) + return stagedPlan } #expect(presentation.commitPlan == expectedPlan) #expect(presentation.presentationFeedbackIdentity == identity) #expect( events == [ + .stageSuccess, .frameCallback, .presentationFeedback, .commit, @@ -167,36 +181,78 @@ } @Test - func externalBufferFeedbackFailureDoesNotCommit() throws { + func externalBufferSuccessStagingFailureEmitsNoProtocolOperations() throws { var events: [ExternalBufferPresentationEvent] = [] + let expectedPlan = try testSurfaceCommitPlan() - #expect(throws: InjectedExternalFeedbackFailure.self) { + #expect(throws: InjectedExternalStagingFailure.self) { _ = try WindowExternalBufferPresenter.performCommitSequence { + events.append(.stageSuccess) + throw InjectedExternalStagingFailure() + } requestFrameCallback: { events.append(.frameCallback) } requestPresentationFeedback: { events.append(.presentationFeedback) - throw InjectedExternalFeedbackFailure() - } commit: { + return SurfacePresentationIdentity(rawValue: 32) + } commit: { _ in events.append(.commit) - return try testSurfaceCommitPlan() - } cancelFrameCallback: { - events.append(.cancelFrameCallback) - } cancelPresentationFeedback: { cancelledIdentity in - events.append(.cancelPresentationFeedback(cancelledIdentity)) + return expectedPlan } } - #expect( - events == [ - .frameCallback, - .presentationFeedback, - .cancelFrameCallback, - ] - ) + #expect(events == [.stageSuccess]) } @Test - func frameCommitterDoesNotRecordFrameWhenSubmitConstraintsFail() async throws { + func externalBufferSuccessStagingFailureRecordsNoRequests() async throws { + try await CoreRequestRecordingGate.withExclusiveRecording { + swl_test_core_request_recording_begin() + defer { swl_test_core_request_recording_end() } + + let surface = try testSurface(pointer: 0x5651) + defer { surface.destroy() } + var runtime = try configuredRuntimeWithoutPendingFrame() + var pendingFrameRegistration: FrameCallbackRegistration? + let request = WindowExternalBufferPresentationRequest( + buffer: try testSurfaceBuffer(pointer: 0x5652), + surface: surface, + scaleInstallation: SurfaceScaleInstallation(), + generation: 1, + geometry: try testSurfaceGeometry(), + submitConstraints: .default, + metadata: .default, + presentationFeedback: nil + ) { + _ = () + } + + #expect(throws: InjectedExternalStagingFailure.self) { + _ = try WindowExternalBufferPresenter.present( + request, + stageSuccess: { _ in + throw InjectedExternalStagingFailure() + }, + runtime: &runtime, + pendingFrameRegistration: &pendingFrameRegistration + ) + } + + #expect(runtime.transactionSnapshot.pendingFrameCallbackGeneration == nil) + #expect(runtime.transactionSnapshot.lastCommittedFrame == nil) + #expect(unsafe swl_test_core_request_record().call_count == 0) + let hasPendingFrameRegistration: Bool + switch consume pendingFrameRegistration { + case .none: + hasPendingFrameRegistration = false + case .some: + hasPendingFrameRegistration = true + } + #expect(!hasPendingFrameRegistration) + } + } + + @Test + func frameCommitterRejectsUnavailableSubmitConstraintsDuringPreparation() async throws { try await CoreRequestRecordingGate.withExclusiveRecording { swl_test_core_request_recording_begin() defer { swl_test_core_request_recording_end() } @@ -205,17 +261,12 @@ defer { surface.destroy() } var runtime = try configuredRuntime() runtime.setExplicitSynchronizationActive() - let preparedCommit = try preparedCommit( - surface: surface, - runtime: &runtime, - constraints: explicitConstraints(timeline: 77, acquire: 2, release: 3), - payload: .buffer(try testSurfaceBuffer(pointer: 0x5702)) - ) - #expect(throws: SurfaceSubmitConstraintError.explicitSyncUnavailable) { - try SurfaceFrameCommitter.commit( - preparedCommit, - runtime: &runtime + try preparedCommit( + surface: surface, + runtime: &runtime, + constraints: explicitConstraints(timeline: 77, acquire: 2, release: 3), + payload: .buffer(try testSurfaceBuffer(pointer: 0x5702)) ) } #expect(runtime.transactionSnapshot.lastCommittedFrame == nil) @@ -630,14 +681,13 @@ } private enum ExternalBufferPresentationEvent: Equatable { + case stageSuccess case frameCallback case presentationFeedback case commit - case cancelFrameCallback - case cancelPresentationFeedback(SurfacePresentationIdentity) } - private struct InjectedExternalFeedbackFailure: Error {} + private struct InjectedExternalStagingFailure: Error {} // swiftlint:enable closure_body_length #endif diff --git a/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift b/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift index b75adbab..b93516fb 100644 --- a/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift +++ b/Tests/WaylandClientTests/Rendering/WindowSoftwarePresenterTests.swift @@ -9,31 +9,36 @@ @Suite struct WindowSoftwarePresentationCommitSequenceTests { @Test(arguments: ManagedPresentationOperation.allCases) - func managedSubmissionRequestsPresentationFeedbackBeforeCommit( + func pointOfNoReturnMarksBufferBusyBeforeProtocolRequests( operation: ManagedPresentationOperation - ) throws { + ) { var events: [CommitSequenceEvent] = [] let identity = SurfacePresentationIdentity(rawValue: 7) - let returnedIdentity = try WindowSoftwarePresentationCommitSequence.perform { - events.append(.frameCallback) - } requestPresentationFeedback: { - events.append(.presentationFeedback(operation)) - return identity - } commit: { - events.append(.commit(operation)) - } cancelFrameCallback: { - events.append(.cancelFrameCallback) - } cleanupAfterFailure: { identity in - if let identity { - events.append(.cancelPresentationFeedback(identity)) + let returnedIdentity = WindowSoftwarePresentationCommitSequence.perform( + stageSuccess: { + events.append(.stageSuccess) + }, + markDrawingBufferBusy: { + events.append(.markDrawingBufferBusy) + }, + requestFrameCallback: { + events.append(.frameCallback) + }, + requestPresentationFeedback: { + events.append(.presentationFeedback(operation)) + return identity + }, + commit: { + events.append(.commit(operation)) } - events.append(.discardDrawingBuffer) - } + ) #expect(returnedIdentity == identity) #expect( events == [ + .stageSuccess, + .markDrawingBufferBusy, .frameCallback, .presentationFeedback(operation), .commit(operation), @@ -42,82 +47,42 @@ } @Test - func presentationFeedbackRequestFailureDoesNotCommitFrame() { + func successStagingFailureStopsBeforePointOfNoReturn() { var events: [CommitSequenceEvent] = [] do { - try WindowSoftwarePresentationCommitSequence.perform { - events.append(.frameCallback) - } requestPresentationFeedback: { - events.append(.presentationFeedback(.show)) - throw InjectedPresentationFeedbackFailure() - } commit: { - events.append(.commit(.show)) - } cancelFrameCallback: { - events.append(.cancelFrameCallback) - } cleanupAfterFailure: { identity in - if let identity { - events.append(.cancelPresentationFeedback(identity)) + _ = try WindowSoftwarePresentationCommitSequence.perform( + stageSuccess: { + events.append(.stageSuccess) + throw InjectedSuccessStagingFailure() + }, + markDrawingBufferBusy: { + events.append(.markDrawingBufferBusy) + }, + requestFrameCallback: { + events.append(.frameCallback) + }, + requestPresentationFeedback: { + events.append(.presentationFeedback(.redraw)) + return nil + }, + commit: { + events.append(.commit(.redraw)) } - events.append(.discardDrawingBuffer) - } - Issue.record("expected presentation feedback request failure") - } catch is InjectedPresentationFeedbackFailure { - #expect( - events == [ - .frameCallback, - .presentationFeedback(.show), - .cancelFrameCallback, - .discardDrawingBuffer, - ] ) + Issue.record("expected success staging failure") + } catch is InjectedSuccessStagingFailure { + // The injected failure must remain observable before the point of no return. } catch { Issue.record("unexpected error: \(error)") } - } - @Test - func commitFailureCancelsPresentationFeedback() { - var events: [CommitSequenceEvent] = [] - let identity = SurfacePresentationIdentity(rawValue: 9) - - do { - try WindowSoftwarePresentationCommitSequence.perform { - events.append(.frameCallback) - } requestPresentationFeedback: { - events.append(.presentationFeedback(.redraw)) - return identity - } commit: { - events.append(.commit(.redraw)) - throw InjectedCommitFailure() - } cancelFrameCallback: { - events.append(.cancelFrameCallback) - } cleanupAfterFailure: { cancelledIdentity in - if let cancelledIdentity { - events.append(.cancelPresentationFeedback(cancelledIdentity)) - } - events.append(.discardDrawingBuffer) - } - Issue.record("expected commit failure") - } catch is InjectedCommitFailure { - #expect( - events == [ - .frameCallback, - .presentationFeedback(.redraw), - .commit(.redraw), - .cancelFrameCallback, - .cancelPresentationFeedback(identity), - .discardDrawingBuffer, - ] - ) - } catch { - Issue.record("unexpected error: \(error)") - } + #expect(events == [.stageSuccess]) } } @Suite(.serialized) - struct WindowSoftwarePresenterTests { + struct WindowSoftwarePresenterTests { // swiftlint:disable:this type_body_length private struct RoleToken {} private struct FrameIDCaptureComplete: Error {} @@ -147,6 +112,135 @@ } } + @Test + func reservedBufferCanBeDiscardedAfterPoolRetirement() async throws { + try await withSoftwarePresentationRecording { + let sharedMemory = try testSharedMemory(pointer: 0x6A22) + let pool = try sharedMemory.createPool(width: 64, height: 48, bufferCount: 1) + let reservedBuffer = try #require(pool.acquireReservedDrawingBuffer()) + + pool.retire(reason: .resized) + #expect(unsafe swl_test_core_request_record().buffer_destroy_sequence == 0) + + reservedBuffer.discard() + #expect(unsafe swl_test_core_request_record().buffer_destroy_sequence == 0) + #expect(!pool.hasBusyBuffers) + #expect(!pool.hasFreeBuffers) + + pool.destroy() + #expect(unsafe swl_test_core_request_record().buffer_destroy_sequence > 0) + #expect(unsafe swl_test_core_request_record().shm_pool_destroy_sequence > 0) + } + } + + @Test + func submitConstraintFailureLeavesDrawingBufferReusable() async throws { + try await withSoftwarePresentationRecording { + try exerciseSubmitConstraintFailureLeavesDrawingBufferReusable() + } + } + + @Test + func successStagingFailureLeavesDrawingBufferReusable() async throws { + try await withSoftwarePresentationRecording { + try exerciseSuccessStagingFailureLeavesDrawingBufferReusable() + } + } + + private func exerciseSuccessStagingFailureLeavesDrawingBufferReusable() throws { + let surface = try testSurface(pointer: 0x6A41) + let sharedMemory = try testSharedMemory(pointer: 0x6A42) + let pool = try sharedMemory.createPool(width: 64, height: 48, bufferCount: 1) + var runtime = SurfaceRuntime(role: .toplevelWindow) + runtime.recordConfigureReceived(serial: 1) + try runtime.acknowledgeConfigure(serial: 1) + var pendingFrameRegistration: FrameCallbackRegistration? + let presenter = softwarePresenter(surface: surface, pool: pool) + + do { + _ = try presenter.present( + context: try softwarePresentationContext(), + draw: { _ in () }, + stageSuccess: { currentBufferAvailability in + #expect(currentBufferAvailability == .unavailable) + throw InjectedSuccessStagingFailure() + }, + runtime: &runtime, + pendingFrameRegistration: &pendingFrameRegistration + ) + Issue.record("expected success staging failure") + } catch is InjectedSuccessStagingFailure { + // Success bookkeeping remains fallible only before the point of no return. + } catch { + Issue.record("unexpected error: \(error)") + } + + let hasPendingRegistration = hasPendingFrameRegistration(pendingFrameRegistration) + #expect(!hasPendingRegistration) + #expect(!pool.hasBusyBuffers) + #expect(pool.hasFreeBuffers) + #expect(unsafe swl_test_core_request_record().frame_sequence == 0) + #expect(unsafe swl_test_core_request_record().attach_sequence == 0) + #expect(unsafe swl_test_core_request_record().damage_sequence == 0) + #expect(unsafe swl_test_presentation_request_record().call_count == 0) + #expect(unsafe swl_test_core_request_record().commit_sequence == 0) + } + + private func exerciseSubmitConstraintFailureLeavesDrawingBufferReusable() throws { + let surface = try testSurface(pointer: 0x6A31) + let sharedMemory = try testSharedMemory(pointer: 0x6A32) + let pool = try sharedMemory.createPool(width: 64, height: 48, bufferCount: 1) + let acquirePoint = SurfaceSyncPoint( + timeline: SurfaceSyncTimelineIdentity(77), + point: RawSyncobjTimelinePoint(2) + ) + let releasePoint = SurfaceSyncPoint( + timeline: SurfaceSyncTimelineIdentity(77), + point: RawSyncobjTimelinePoint(3) + ) + var runtime = SurfaceRuntime(role: .toplevelWindow) + runtime.setExplicitSynchronizationActive() + runtime.recordConfigureReceived(serial: 1) + try runtime.acknowledgeConfigure(serial: 1) + var pendingFrameRegistration: FrameCallbackRegistration? + let presenter = softwarePresenter(surface: surface, pool: pool) + + do { + _ = try presenter.present( + context: try softwarePresentationContext( + submitConstraints: SurfaceSubmitConstraints( + synchronization: .explicit( + acquire: acquirePoint, + release: releasePoint + ), + pacing: .none + ) + ), + draw: { _ in () }, + runtime: &runtime, + pendingFrameRegistration: &pendingFrameRegistration + ) + Issue.record("expected submit constraint failure") + } catch let failure as WindowSoftwarePresentationFailure { + #expect( + failure.underlying as? SurfaceSubmitConstraintError + == .explicitSyncUnavailable + ) + } catch { + Issue.record("unexpected error: \(error)") + } + + let hasPendingRegistration = hasPendingFrameRegistration(pendingFrameRegistration) + #expect(!hasPendingRegistration) + #expect(!pool.hasBusyBuffers) + #expect(pool.hasFreeBuffers) + #expect(unsafe swl_test_core_request_record().frame_sequence == 0) + #expect(unsafe swl_test_core_request_record().attach_sequence == 0) + #expect(unsafe swl_test_core_request_record().damage_sequence == 0) + #expect(unsafe swl_test_presentation_request_record().call_count == 0) + #expect(unsafe swl_test_core_request_record().commit_sequence == 0) + } + private func exerciseDrawFailureBeforePresentationRequests() throws { let surface = try testSurface(pointer: 0x6A01) let sharedMemory = try testSharedMemory(pointer: 0x6A02) @@ -226,7 +320,18 @@ } } - private func softwarePresentationContext() throws + private func hasPendingFrameRegistration( + _ registration: borrowing FrameCallbackRegistration? + ) -> Bool { + switch registration { + case .some: true + case .none: false + } + } + + private func softwarePresentationContext( + submitConstraints: SurfaceSubmitConstraints = .default + ) throws -> WindowSoftwarePresentationContext { let geometry = try SurfaceGeometry( @@ -249,7 +354,7 @@ configuration: configure.configuration ), geometry: geometry, - submitConstraints: .default, + submitConstraints: submitConstraints, metadata: .default, damage: nil, presentationFeedback: nil @@ -305,15 +410,13 @@ } private enum CommitSequenceEvent: Equatable { + case stageSuccess + case markDrawingBufferBusy case frameCallback case presentationFeedback(ManagedPresentationOperation) case commit(ManagedPresentationOperation) - case cancelFrameCallback - case cancelPresentationFeedback(SurfacePresentationIdentity) - case discardDrawingBuffer } - private struct InjectedPresentationFeedbackFailure: Error {} - private struct InjectedCommitFailure: Error {} private struct InjectedDrawFailure: Error {} + private struct InjectedSuccessStagingFailure: Error {} #endif diff --git a/Tests/WaylandClientTests/Windowing/DisplayCoreSoftwarePresentationTests.swift b/Tests/WaylandClientTests/Windowing/DisplayCoreSoftwarePresentationTests.swift new file mode 100644 index 00000000..3398f059 --- /dev/null +++ b/Tests/WaylandClientTests/Windowing/DisplayCoreSoftwarePresentationTests.swift @@ -0,0 +1,48 @@ +import Synchronization +import Testing + +@testable import WaylandClient + +@Suite +struct DisplayCoreSoftwarePresentationTests { + @Test + func reservationForClosedWindowReturnsClosed() throws { + let core = DisplayCore(eventHub: DisplayEventHub()) + + #expect( + try core.reserveSoftwareFrameForRedraw(WindowID(rawValue: 42)) == .closed + ) + } + + @Test + func submissionForClosedWindowReturnsClosedWithoutDrawing() throws { + let core = DisplayCore(eventHub: DisplayEventHub()) + let surfaceGeometry = try SurfaceGeometry( + logicalSize: PositiveLogicalSize(width: 1, height: 1), + scale: .one + ) + let reservation = SoftwareFrameReservation( + reservationID: SoftwareFrameReservationToken(rawValue: 1), + id: SoftwareFrameBufferID(rawValue: 1), + width: 1, + height: 1, + stride: 4, + geometry: SoftwareFrameGeometry(surface: surfaceGeometry) + ) + let didDraw = Mutex(false) + + let outcome = try core.submitReservedSoftwareFrame( + WindowID(rawValue: 42), + reservation: reservation, + submitConstraints: .default, + metadata: .default, + requestPresentationFeedback: true, + damage: nil + ) { _ in + didDraw.withLock { $0 = true } + } + + #expect(outcome == .closed) + #expect(!didDraw.withLock { $0 }) + } +} diff --git a/Tests/WaylandClientTests/Windowing/WindowModelPresentationTests.swift b/Tests/WaylandClientTests/Windowing/WindowModelPresentationTests.swift index 6b07137d..e48178ae 100644 --- a/Tests/WaylandClientTests/Windowing/WindowModelPresentationTests.swift +++ b/Tests/WaylandClientTests/Windowing/WindowModelPresentationTests.swift @@ -5,7 +5,7 @@ import WaylandRaw @Suite struct WindowModelPresentationTests { // swiftlint:disable:this type_body_length - private let windowID = WindowID(rawValue: 42) + let windowID = WindowID(rawValue: 42) @Test func redrawRequestConsumedProducesPresentationEffect() throws { @@ -14,7 +14,8 @@ struct WindowModelPresentationTests { // swiftlint:disable:this type_body_lengt let effects = try model.reduce(.redrawRequestConsumed(bufferAvailability: .available)) let request = PresentationRequest( generation: 1, - configuration: try #require(model.currentConfiguration) + configuration: try #require(model.currentConfiguration), + redrawIdentity: model.redraw.identityForCurrentDraw ) #expect(effects == [.performSoftwarePresent(request)]) @@ -363,21 +364,21 @@ struct WindowModelPresentationTests { // swiftlint:disable:this type_body_lengt } extension WindowModelPresentationTests { - private func configuredModelReadyForConfigure() throws -> WindowModel { + func configuredModelReadyForConfigure() throws -> WindowModel { var model = WindowModel(id: windowID, fallbackSize: .default) _ = try model.reduce(.roleObjectsCreated) _ = try model.reduce(.initialCommitSent) return model } - private func activePublishedModel() throws -> WindowModel { + func activePublishedModel() throws -> WindowModel { var model = try configuredModelReadyForConfigure() _ = try model.reduce(.published) _ = try model.reduce(.configureReceived(configure(width: 800, height: 600, serial: 1))) return model } - private func activeModelWithStartedPresentation() throws -> ( + func activeModelWithStartedPresentation() throws -> ( model: WindowModel, request: PresentationRequest ) { @@ -388,7 +389,7 @@ extension WindowModelPresentationTests { return (model, request) } - private func presentationRequest(from effects: [WindowEffect]) throws -> PresentationRequest { + func presentationRequest(from effects: [WindowEffect]) throws -> PresentationRequest { guard case .performSoftwarePresent(let request) = try #require(effects.first) else { Issue.record("expected presentation effect") throw ClientError.window( @@ -400,7 +401,7 @@ extension WindowModelPresentationTests { return request } - private func configure( + func configure( width: Int32, height: Int32, serial: UInt32 = 1 diff --git a/Tests/WaylandClientTests/Windowing/WindowModelSoftwarePresentationTests.swift b/Tests/WaylandClientTests/Windowing/WindowModelSoftwarePresentationTests.swift new file mode 100644 index 00000000..517a2454 --- /dev/null +++ b/Tests/WaylandClientTests/Windowing/WindowModelSoftwarePresentationTests.swift @@ -0,0 +1,223 @@ +import Testing +import WaylandRaw + +@testable import WaylandClient + +extension WindowModelPresentationTests { + @Test + func configureDuringSoftwarePreparationSupersedesAndPublishesReplacementImmediately() throws { + var (model, request) = try activeModelWithStartedPresentation() + let replacementConfigure = try configure(width: 1_024, height: 768, serial: 2) + + #expect( + try model.reduce(.configureReceived(replacementConfigure)) + == [.ackConfigure(2)] + ) + #expect(!model.isCurrentSoftwarePresentation(request)) + + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested(windowID)] + ) + #expect(model.presentation == .idle) + #expect(model.currentConfiguration == replacementConfigure.configuration) + #expect(model.redraw.isDirty) + #expect(model.redraw.hasOutstandingRedrawRequest) + } + + @Test + func supersededSoftwarePresentationRepublishesNewestGenerationExactlyOnce() throws { + var (model, request) = try activeModelWithStartedPresentation() + + #expect( + try model.reduce(.contentInvalidated(bufferAvailability: .available)).isEmpty + ) + #expect(model.redraw.generationForCurrentDraw == request.generation + 1) + #expect(model.redraw.hasOutstandingRedrawRequest) + + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested(windowID)] + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + #expect(model.redraw.generationForCurrentDraw == request.generation + 1) + #expect(model.redraw.hasOutstandingRedrawRequest) + + #expect( + try model.reduce(.contentInvalidated(bufferAvailability: .available)).isEmpty + ) + } + + @Test + func canceledSoftwarePreparationRepublishesItsUncommittedGeneration() throws { + var (model, request) = try activeModelWithStartedPresentation() + + #expect(!model.redraw.hasOutstandingRedrawRequest) + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested(windowID)] + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + #expect(model.redraw.generationForCurrentDraw == request.generation) + #expect(model.redraw.hasOutstandingRedrawRequest) + } + + @Test + func failedSoftwarePresentationRepublishesItsUncommittedGeneration() throws { + var (model, request) = try activeModelWithStartedPresentation() + + #expect( + try model.reduce( + .softwarePresentationFailed( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested(windowID)] + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + #expect(model.redraw.generationForCurrentDraw == request.generation) + #expect(model.redraw.hasOutstandingRedrawRequest) + } + + @Test + func supersededSoftwarePresentationWaitsWhenNoBufferIsAvailable() throws { + var (model, request) = try activeModelWithStartedPresentation() + + #expect( + try model.reduce(.contentInvalidated(bufferAvailability: .unavailable)).isEmpty + ) + #expect(model.redraw.isWaitingForBuffer) + + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .unavailable + ) + ).isEmpty + ) + #expect(model.presentation == .idle) + #expect(model.redraw.isDirty) + #expect(model.redraw.isWaitingForBuffer) + #expect( + try model.reduce(.bufferBecameAvailable(bufferAvailability: .available)) + == [.publishRedrawRequested(windowID)] + ) + } + + @Test + func supersededSoftwarePresentationRejectsMismatchedGeneration() throws { + var (model, request) = try activeModelWithStartedPresentation() + + #expect( + throws: ClientError.window( + windowID, + .invalidLifecycleTransition( + .presentationGenerationMismatch( + expected: request.generation, + actual: request.generation + 1 + ) + ) + ) + ) { + _ = try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation + 1, + bufferAvailability: .available + ) + ) + } + #expect(model.presentation == .drawing(request: request)) + } + + @Test + func softwarePresentationCurrencyRequiresActiveRequestConfigurationAndGeneration() throws { + var (model, request) = try activeModelWithStartedPresentation() + + #expect(model.isCurrentSoftwarePresentation(request)) + let collidingRequest = PresentationRequest( + generation: request.generation, + configuration: request.configuration + ) + #expect(collidingRequest.generation == request.generation) + #expect(collidingRequest.configuration == request.configuration) + #expect(!model.isCurrentSoftwarePresentation(collidingRequest)) + + _ = try model.reduce(.contentInvalidated(bufferAvailability: .available)) + #expect(!model.isCurrentSoftwarePresentation(request)) + + _ = try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) + #expect(!model.isCurrentSoftwarePresentation(request)) + } + + @Test + func wrappedGenerationDoesNotReuseContentIdentity() { + var redraw = WindowRedrawState() + let previousIdentity = redraw.identityForCurrentDraw + + _ = redraw.reduce(.presented(generation: .max), bufferAvailability: .available) + _ = redraw.reduce(.contentInvalidated, bufferAvailability: .unavailable) + + #expect(redraw.generationForCurrentDraw == 0) + #expect( + !redraw.matchesCurrentContent( + generation: 0, + identity: previousIdentity + ) + ) + } + + @Test + func manyConsecutiveSoftwareSupersessionsDoNotLoseOrDuplicateRedraws() throws { + var (model, request) = try activeModelWithStartedPresentation() + + for expectedGeneration in 2...101 { + #expect( + try model.reduce( + .contentInvalidated(bufferAvailability: .available) + ).isEmpty + ) + #expect(model.redraw.generationForCurrentDraw == UInt64(expectedGeneration)) + #expect( + try model.reduce( + .softwarePresentationSuperseded( + generation: request.generation, + bufferAvailability: .available + ) + ) == [.publishRedrawRequested(windowID)] + ) + #expect(model.presentation == .idle) + + let effects = try model.reduce( + .redrawRequestConsumed(bufferAvailability: .available) + ) + guard case .performSoftwarePresent(let nextRequest) = try #require(effects.first) else { + Issue.record("expected replacement software presentation") + return + } + #expect(effects.count == 1) + _ = try model.reduce(.presentationStarted(nextRequest)) + request = nextRequest + } + } +} diff --git a/Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift b/Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift index d9ed00ee..88b15887 100644 --- a/Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift +++ b/Tests/WaylandClientTests/Windowing/WindowOwnedResourceLedgerTests.swift @@ -56,6 +56,19 @@ struct WindowOwnedResourceLedgerTests { #expect(ledger.count == 1) } + @Test + func mismatchedResourceDoesNotConsumeMatchingIdentity() throws { + let log = RetirementLog() + let ledger = makeLedger(log) + ledger.insert(Resource(id: 1), for: 7) + + #expect(ledger.take(7) { $0.id == 2 } == nil) + #expect(ledger.count == 1) + #expect(try #require(ledger.take(7) { $0.id == 1 }).id == 1) + #expect(ledger.isEmpty) + #expect(log.ids.isEmpty) + } + @Test func connectionFailureCleanupRetiresAllResourcesOnce() { let log = RetirementLog() diff --git a/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift new file mode 100644 index 00000000..3c302614 --- /dev/null +++ b/Tests/WaylandClientTests/Windowing/WindowSoftwarePresentationPublicRequestTests.swift @@ -0,0 +1,390 @@ +#if SWL_ENABLE_TESTING + import CWaylandProtocols + import Foundation + import Testing + import WaylandClient + import WaylandTestSupport + + @Suite( + .enabled( + if: SoftwarePresentationRequestTestEnvironment.isEnabled, + """ + Set WAYLAND_DISPLAY and + WAYLAND_CLIENT_KIT_ENABLE_SOFTWARE_PRESENTATION_REQUEST_TESTS=1 + """ + ), + .timeLimit(.minutes(1)), + .tags(.linux, .integration, .liveWayland, .publicAPI), + .serialized + ) + struct WindowSoftwarePresentationPublicRequestTests { + @Test + func forwardingPresentationRecorderRecordsAndRestores() async throws { + try await withReadySoftwarePresentationConnection { window, _ in + swl_test_core_request_recording_begin_forwarding() + defer { swl_test_core_request_recording_end() } + + swl_test_presentation_request_recording_begin_forwarding() + let outcome: SoftwarePresentationOutcome + do { + defer { swl_test_presentation_request_recording_end() } + outcome = try await window.redraw( + requestPresentationFeedback: true, + preparing: { _ in () }, + { _, frame in + fillSoftwareFrame(frame, color: 0x0034_4424) + } + ) + let record = unsafe swl_test_presentation_request_record() + #expect(unsafe record.call_count == 1) + #expect(unsafe record.kind == SWL_TEST_PRESENTATION_FEEDBACK) + #expect(unsafe record.feedback != nil) + } + #expect(outcome == .presented) + + let coreRecord = unsafe swl_test_core_request_record() + #expect(unsafe coreRecord.frame_sequence > 0) + #expect(unsafe coreRecord.frame_sequence < coreRecord.attach_sequence) + #expect(unsafe coreRecord.attach_sequence < coreRecord.damage_sequence) + #expect(unsafe coreRecord.damage_sequence < coreRecord.commit_sequence) + + let recordedCallCount = unsafe swl_test_presentation_request_record().call_count + try await window.requestPresentationFeedback() + #expect( + unsafe swl_test_presentation_request_record().call_count + == recordedCallCount + ) + } + } + + @Test + func rejectedPreparationsIssueNoSurfaceTransactionRequests() async throws { + try await withRecordedSoftwarePresentationConnection { window, displayEvents in + try await exerciseRepeatedSupersessions(window, displayEvents) + try await exerciseCancellation(window, displayEvents) + try await exercisePreparationFailure(window, displayEvents) + try await exerciseDrawFailure(window, displayEvents) + #expect(try await window.needsRedraw) + try await exerciseClose(window) + } + } + } + + private func withRecordedSoftwarePresentationConnection( + _ operation: @Sendable (Window, DisplayEvents) async throws -> Void + ) async throws { + try await withReadySoftwarePresentationConnection { window, displayEvents in + swl_test_core_request_recording_begin_forwarding() + swl_test_presentation_request_recording_begin_forwarding() + defer { swl_test_presentation_request_recording_end() } + defer { swl_test_core_request_recording_end() } + + try await operation(window, displayEvents) + expectNoSurfaceTransactionRequests() + } + } + + private func withReadySoftwarePresentationConnection( + _ operation: @Sendable (Window, DisplayEvents) async throws -> Void + ) async throws { + try await CoreRequestRecordingGate.withExclusiveRecording { + try await PresentationRequestRecordingGate.withExclusiveRecording { + try await withSoftwarePresentationConnection { display, window in + let displayEvents = display.events + try await window.show { frame in + fillSoftwareFrame(frame, color: 0x0014_2434) + } + try await waitForSoftwareRedrawRequest( + for: window, + in: displayEvents, + phase: "initial redraw request" + ) + try await operation(window, displayEvents) + } + } + } + } + + private func exerciseRepeatedSupersessions( + _ window: Window, + _ displayEvents: DisplayEvents + ) async throws { + for _ in 0..<16 { + let gate = SoftwarePreparationRequestGate() + async let staleOutcome = window.redraw( + requestPresentationFeedback: true, + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { preparedID, frame in + try rejectSoftwareDraw(preparedID, frame) + } + ) + await gate.waitUntilSuspended() + + async let replacementEvent = softwareRedrawEvent( + for: window, + in: displayEvents, + phase: "repeated supersession replacement" + ) { + do { + try await window.requestRedraw() + } catch { + await gate.resumePreparation() + throw error + } + await gate.resumePreparation() + } + #expect(try await staleOutcome == .superseded) + _ = try await replacementEvent + } + } + + private func exerciseCancellation( + _ window: Window, + _ displayEvents: DisplayEvents + ) async throws { + let gate = SoftwarePreparationRequestGate() + let outcome = try await withThrowingTaskGroup( + of: SoftwarePresentationOutcome.self + ) { group in + group.addTask { + try await window.redraw( + requestPresentationFeedback: true, + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { preparedID, frame in + try rejectSoftwareDraw(preparedID, frame) + } + ) + } + await gate.waitUntilSuspended() + group.cancelAll() + + async let replacementEvent = softwareRedrawEvent( + for: window, + in: displayEvents, + phase: "cancellation replacement" + ) { + await gate.resumePreparation() + } + let nextOutcome = try await group.next() + _ = try await replacementEvent + return try #require(nextOutcome) + } + #expect(outcome == .superseded) + } + + private func exercisePreparationFailure( + _ window: Window, + _ displayEvents: DisplayEvents + ) async throws { + _ = try await softwareRedrawEvent( + for: window, + in: displayEvents, + phase: "preparation-failure replacement" + ) { + do { + _ = try await window.redraw( + requestPresentationFeedback: true, + preparing: { _ in + throw InjectedSoftwarePreparationFailure() + }, + { preparedID, frame in + try rejectSoftwareDraw(preparedID, frame) + } + ) + Issue.record("expected preparation failure") + } catch is InjectedSoftwarePreparationFailure { + // The original preparation error must remain observable. + } + } + } + + private func exerciseDrawFailure( + _ window: Window, + _ displayEvents: DisplayEvents + ) async throws { + _ = try await softwareRedrawEvent( + for: window, + in: displayEvents, + phase: "draw-failure replacement" + ) { + do { + _ = try await window.redraw( + requestPresentationFeedback: true, + preparing: { _ in () }, + { _, _ in + throw InjectedSoftwareDrawFailure() + } + ) + Issue.record("expected draw failure") + } catch let failure as WindowSoftwareDrawFailure { + #expect(failure.underlying is InjectedSoftwareDrawFailure) + } + } + } + + private func exerciseClose(_ window: Window) async throws { + let gate = SoftwarePreparationRequestGate() + async let outcome = window.redraw( + requestPresentationFeedback: true, + preparing: { reservation in + await gate.suspendPreparation() + return reservation.id + }, + { preparedID, frame in + try rejectSoftwareDraw(preparedID, frame) + } + ) + await gate.waitUntilSuspended() + await window.close() + await gate.resumePreparation() + #expect(try await outcome == .closed) + } + + private func expectNoSurfaceTransactionRequests() { + let coreRecord = unsafe swl_test_core_request_record() + let presentationRecord = unsafe swl_test_presentation_request_record() + + #expect(unsafe coreRecord.attach_sequence == 0) + #expect(unsafe coreRecord.frame_sequence == 0) + #expect(unsafe coreRecord.damage_sequence == 0) + #expect(unsafe coreRecord.commit_sequence == 0) + #expect(unsafe presentationRecord.call_count == 0) + } + + private func withSoftwarePresentationConnection( + _ body: @Sendable (WaylandDisplay, Window) async throws -> Void + ) async throws { + try await WaylandDisplay.withConnection( + applicationID: "org.waylandclientkit.SoftwarePresentationRequestTests", + cursorConfiguration: CursorConfiguration(fallbackCursor: .hidden), + discoveryTimeoutMilliseconds: 5_000 + ) { display in + let window = try await display.createTopLevelWindow( + configuration: try WindowConfiguration( + title: "WaylandClientKit Software Presentation Request Test", + appID: "wayland-client-kit-software-presentation-request-test", + initialWidth: 160, + initialHeight: 120, + bufferCount: 3, + closeRequestPolicy: .requestOnly, + decorationPreference: .preferServerSide + ) + ) + try await body(display, window) + } + } + + private func waitForSoftwareRedrawRequest( + for window: Window, + in displayEvents: DisplayEvents, + phase: String + ) async throws { + _ = try await softwareRedrawEvent( + for: window, + in: displayEvents, + phase: phase + ) { + try await window.requestRedraw() + } + } + + private func softwareRedrawEvent( + for window: Window, + in displayEvents: DisplayEvents, + phase: String, + after trigger: @escaping @Sendable () async throws -> Void + ) async throws -> DisplayEvent { + try await withThrowingTaskGroup(of: DisplayEvent.self) { group in + group.addTask { + var iterator = displayEvents.makeAsyncIterator() + try await trigger() + while let event = try await iterator.next() { + if event == .redrawRequested(window.id) { + return event + } + } + throw SoftwarePresentationRequestTestError.streamEnded + } + group.addTask { + try await Task.sleep(for: .seconds(5)) + throw SoftwarePresentationRequestTestError.timeout(phase) + } + + let event = try await group.next() + group.cancelAll() + return try #require(event) + } + } + + private func fillSoftwareFrame(_ frame: borrowing SoftwareFrame, color: UInt32) { + frame.withXRGB8888Rows { _, pixels in + for index in 0..] = [] + private var resumeContinuation: CheckedContinuation? + + func suspendPreparation() async { + isSuspended = true + let waiters = suspensionWaiters + suspensionWaiters.removeAll() + for waiter in waiters { + waiter.resume() + } + await withCheckedContinuation { continuation in + resumeContinuation = continuation + } + } + + func waitUntilSuspended() async { + guard !isSuspended else { return } + await withCheckedContinuation { continuation in + suspensionWaiters.append(continuation) + } + } + + func resumePreparation() { + resumeContinuation?.resume() + resumeContinuation = nil + } + } + + private enum SoftwarePresentationRequestTestError: Error { + case streamEnded + case timeout(String) + } + + private enum SoftwarePresentationRequestTestEnvironment { + static var isEnabled: Bool { + let environment = ProcessInfo.processInfo.environment + return environment["WAYLAND_DISPLAY"]?.isEmpty == false + && environment[ + "WAYLAND_CLIENT_KIT_ENABLE_SOFTWARE_PRESENTATION_REQUEST_TESTS" + ] == "1" + } + } + + private struct UnexpectedSoftwarePresentationDraw: Error {} + private struct InjectedSoftwarePreparationFailure: Error {} + private struct InjectedSoftwareDrawFailure: Error {} +#endif diff --git a/docs/documentation-symbol-coverage.json b/docs/documentation-symbol-coverage.json index c6f15606..f3459089 100644 --- a/docs/documentation-symbol-coverage.json +++ b/docs/documentation-symbol-coverage.json @@ -1,8 +1,8 @@ { "products" : { "WaylandClient" : { - "documented" : 146, - "eligible" : 1521 + "documented" : 151, + "eligible" : 1530 }, "WaylandGraphicsPreview" : { "documented" : 32, diff --git a/docs/public-api-audit.md b/docs/public-api-audit.md index 1921c0fb..b91c5f21 100644 --- a/docs/public-api-audit.md +++ b/docs/public-api-audit.md @@ -40,6 +40,7 @@ Intentionally public: - `SoftwareFrameBufferID` - `SoftwareFrameBuffer` - `SoftwareFrameReservation` +- `SoftwarePresentationOutcome` - `SoftwareFrameGeometry` - `PositivePixelSize` - `SoftwareFrame` @@ -210,6 +211,16 @@ 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. + Async `Window.show(preparing:_:)` and `Window.redraw(preparing:_:)` calls are + latest-wins transactions and return `SoftwarePresentationOutcome`. Matching + overloads without a preparation closure provide the same outcome and atomic + presentation-feedback option. After preparation resumes, the library + revalidates the exact reservation, window, configure, authoritative geometry, + redraw generation, and task cancellation. A stale attempt is discarded + without drawing, requesting a frame callback or presentation feedback, or + committing the surface. Its replacement redraw is published once through + normal pacing. Presentation feedback requested by these APIs belongs to the + same eventual surface commit. - 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 dc5a85ad..2e688d50 100644 --- a/docs/public-api-baseline.md +++ b/docs/public-api-baseline.md @@ -1492,6 +1492,11 @@ s:13WaylandClient24SoftwareFrameReservationV6heights5Int32Vvp swift.property Sof s:13WaylandClient24SoftwareFrameReservationV2idAA0cD8BufferIDVvp swift.property SoftwareFrameReservation.id let id: SoftwareFrameBufferID - s:13WaylandClient24SoftwareFrameReservationV6strides5Int32Vvp swift.property SoftwareFrameReservation.stride let stride: Int32 - s:13WaylandClient24SoftwareFrameReservationV5widths5Int32Vvp swift.property SoftwareFrameReservation.width let width: Int32 - +s:13WaylandClient27SoftwarePresentationOutcomeO swift.enum SoftwarePresentationOutcome enum SoftwarePresentationOutcome - +s:13WaylandClient27SoftwarePresentationOutcomeO6closedyA2CmF swift.enum.case SoftwarePresentationOutcome.closed case closed - +s:13WaylandClient27SoftwarePresentationOutcomeO8deferredyA2CmF swift.enum.case SoftwarePresentationOutcome.deferred case deferred - +s:13WaylandClient27SoftwarePresentationOutcomeO9presentedyA2CmF swift.enum.case SoftwarePresentationOutcome.presented case presented - +s:13WaylandClient27SoftwarePresentationOutcomeO10supersededyA2CmF swift.enum.case SoftwarePresentationOutcome.superseded case superseded - s:13WaylandClient19StartedToplevelDragV swift.struct StartedToplevelDrag struct StartedToplevelDrag - s:13WaylandClient19StartedToplevelDragV4dragAA0dE0Vvp swift.property StartedToplevelDrag.drag let drag: ToplevelDrag - s:13WaylandClient19StartedToplevelDragV6sourceAA0E6SourceVvp swift.property StartedToplevelDrag.source let source: DragSource - @@ -2104,8 +2109,10 @@ s:13WaylandClient6WindowV11needsRedrawSbvp swift.property Window.needsRedraw var s:13WaylandClient6WindowV18presentationEventsAA0c12PresentationE0Vvp swift.property Window.presentationEvents var presentationEvents: WindowPresentationEvents { get } - s:13WaylandClient6WindowV6redrawyyyAA13SoftwareFrameVYbKXEYaKF swift.method Window.redraw(_:) func redraw(_ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - s:13WaylandClient6WindowV6redraw6damage_yAA19SurfaceDamageRegionVSg_yAA13SoftwareFrameVYbKXEtYaKF swift.method Window.redraw(damage:_:) func redraw(damage: SurfaceDamageRegion?, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - -s:13WaylandClient6WindowV6redraw6damage9preparing_yAA19SurfaceDamageRegionVSg_xAA24SoftwareFrameReservationVYaYbKYCXEyx_AA0jK0VtYbKXEtYaKs8SendableRzlF swift.method Window.redraw(damage:preparing:_:) func redraw(damage: SurfaceDamageRegion?, preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws 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:13WaylandClient6WindowV6redraw9preparing_yxAA24SoftwareFrameReservationVYaYbKYCXE_yx_AA0fG0VtYbKXEtYaKs8SendableRzlF swift.method Window.redraw(preparing:_:) func redraw(preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws 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:13WaylandClient6WindowV6redraw6damage27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA19SurfaceDamageRegionVSg_SbyAA0I5FrameVYbKXEtYaKF swift.method Window.redraw(damage:requestPresentationFeedback:_:) @discardableResult func redraw(damage: SurfaceDamageRegion?, requestPresentationFeedback: Bool, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} +s:13WaylandClient6WindowV6redraw6damage27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA19SurfaceDamageRegionVSg_SbxAA0J16FrameReservationVYaYbKYCXEyx_AA0jO0VtYbKXEtYaKs8SendableRzlF swift.method Window.redraw(damage:requestPresentationFeedback:preparing:_:) @discardableResult func redraw(damage: SurfaceDamageRegion?, 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:13WaylandClient6WindowV6redraw27requestPresentationFeedback_AA08SoftwareF7OutcomeOSb_yAA0H5FrameVYbKXEtYaKF swift.method Window.redraw(requestPresentationFeedback:_:) @discardableResult func redraw(requestPresentationFeedback: Bool, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} +s:13WaylandClient6WindowV6redraw27requestPresentationFeedback9preparing_AA08SoftwareF7OutcomeOSb_xAA0I16FrameReservationVYaYbKYCXEyx_AA0iK0VtYbKXEtYaKs8SendableRzlF swift.method Window.redraw(requestPresentationFeedback:preparing:_:) @discardableResult func redraw(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 - s:13WaylandClient6WindowV22requestActivationToken5appID13serialContext19timeoutMillisecondsAA0eF0VSSSg_AA0e6SerialJ0VSgs5Int32VtYaKF swift.method Window.requestActivationToken(appID:serialContext:timeoutMilliseconds:) func requestActivationToken(appID: String? = nil, serialContext: ActivationSerialContext? = nil, timeoutMilliseconds: Int32 = WaylandDisplay.defaultActivationTokenTimeoutMilliseconds) async throws -> ActivationToken - s:13WaylandClient6WindowV21requestExitFullscreenyyYaKF swift.method Window.requestExitFullscreen() func requestExitFullscreen() async throws - @@ -2130,10 +2137,12 @@ s:13WaylandClient6WindowV14setMinimumSizeyyAA015PositiveLogicalF0VSgYaKF swift.m s:13WaylandClient6WindowV15setOpaqueRegionyyAA07SurfaceF0VSgYaKF swift.method Window.setOpaqueRegion(_:) func setOpaqueRegion(_ region: SurfaceRegion?) async throws - s:13WaylandClient6WindowV8setTitleyyAA0A6StringVYaKF swift.method Window.setTitle(_:) func setTitle(_ title: WaylandString) async throws - s:13WaylandClient6WindowV8setTitleyySSYaKF swift.method Window.setTitle(_:) func setTitle(_ title: String) async throws - +s:13WaylandClient6WindowV4show6damage27requestPresentationFeedback19timeoutMilliseconds_AA08SoftwareG7OutcomeOAA19SurfaceDamageRegionVSg_Sbs5Int32VyAA0K5FrameVYbKXEtYaKF swift.method Window.show(damage:requestPresentationFeedback:timeoutMilliseconds:_:) @discardableResult func show(damage: SurfaceDamageRegion?, requestPresentationFeedback: Bool, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} s:13WaylandClient6WindowV4show6damage19timeoutMilliseconds_yAA19SurfaceDamageRegionVSg_s5Int32VyAA13SoftwareFrameVYbKXEtYaKF swift.method Window.show(damage:timeoutMilliseconds:_:) func show(damage: SurfaceDamageRegion?, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - -s:13WaylandClient6WindowV4show6damage19timeoutMilliseconds9preparing_yAA19SurfaceDamageRegionVSg_s5Int32VxAA24SoftwareFrameReservationVYaYbKYCXEyx_AA0mN0VtYbKXEtYaKs8SendableRzlF swift.method Window.show(damage:timeoutMilliseconds:preparing:_:) func show(damage: SurfaceDamageRegion?, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws 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:13WaylandClient6WindowV4show6damage19timeoutMilliseconds27requestPresentationFeedback9preparing_AA08SoftwareI7OutcomeOAA19SurfaceDamageRegionVSg_s5Int32VSbxAA0L16FrameReservationVYaYbKYCXEyx_AA0lR0VtYbKXEtYaKs8SendableRzlF swift.method Window.show(damage:timeoutMilliseconds:requestPresentationFeedback:preparing:_:) @discardableResult func show(damage: SurfaceDamageRegion?, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, 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:13WaylandClient6WindowV4show27requestPresentationFeedback19timeoutMilliseconds_AA08SoftwareF7OutcomeOSb_s5Int32VyAA0J5FrameVYbKXEtYaKF swift.method Window.show(requestPresentationFeedback:timeoutMilliseconds:_:) @discardableResult func show(requestPresentationFeedback: Bool, timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws -> SoftwarePresentationOutcome {"swiftExtension":{"extendedModule":"WaylandClient","typeKind":"swift.struct"}} s:13WaylandClient6WindowV4show19timeoutMilliseconds_ys5Int32V_yAA13SoftwareFrameVYbKXEtYaKF swift.method Window.show(timeoutMilliseconds:_:) func show(timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, _ draw: sending @Sendable (borrowing SoftwareFrame) throws -> Void) async throws - -s:13WaylandClient6WindowV4show19timeoutMilliseconds9preparing_ys5Int32V_xAA24SoftwareFrameReservationVYaYbKYCXEyx_AA0iJ0VtYbKXEtYaKs8SendableRzlF swift.method Window.show(timeoutMilliseconds:preparing:_:) func show(timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, preparing prepare: nonisolated(nonsending) sending @Sendable (SoftwareFrameReservation) async throws -> Prepared, _ draw: sending @Sendable (Prepared, borrowing SoftwareFrame) throws -> Void) async throws 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:13WaylandClient6WindowV4show19timeoutMilliseconds27requestPresentationFeedback9preparing_AA08SoftwareH7OutcomeOs5Int32V_SbxAA0K16FrameReservationVYaYbKYCXEyx_AA0kN0VtYbKXEtYaKs8SendableRzlF swift.method Window.show(timeoutMilliseconds:requestPresentationFeedback:preparing:_:) @discardableResult func show(timeoutMilliseconds: Int32 = WaylandDisplay.defaultConfigureTimeoutMilliseconds, 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:13WaylandClient6WindowV9startDrag6source6seatID6serial4iconAA0E6SourceVAA0eK13ConfigurationV_AA04SeatH0VAA11InputSerialVAA0E4IconOtYaKF swift.method Window.startDrag(source:seatID:serial:icon:) func startDrag(source configuration: DragSourceConfiguration, seatID: SeatID, serial: InputSerial, icon: DragIcon = .none) async throws -> DragSource - s:13WaylandClient6WindowV17startToplevelDrag6source6seatID6serial4icon6offsetAA07StartedeF0VAA0F19SourceConfigurationV_AA04SeatI0VAA11InputSerialVAA0F4IconOAA13LogicalOffsetVtYaKF swift.method Window.startToplevelDrag(source:seatID:serial:icon:offset:) func startToplevelDrag(source configuration: DragSourceConfiguration, seatID: SeatID, serial: InputSerial, icon: DragIcon = .none, offset: LogicalOffset = .zero) async throws -> StartedToplevelDrag - s:13WaylandClient6WindowV13stateSnapshotAA0c5StateE0Vvp swift.property Window.stateSnapshot var stateSnapshot: WindowStateSnapshot { get async throws } - @@ -5291,6 +5300,14 @@ s:13WaylandClient27RelativePointerSubscriptionV2eeoiySbAC_ACtFZ memberOf s:13Way s:13WaylandClient27RelativePointerSubscriptionV2idAA0cdE2IDVvp memberOf s:13WaylandClient27RelativePointerSubscriptionV - s:13WaylandClient27RelativePointerSubscriptionV4hash4intoys6HasherVz_tF memberOf s:13WaylandClient27RelativePointerSubscriptionV - s:13WaylandClient27RelativePointerSubscriptionV7destroyyyYaKF memberOf s:13WaylandClient27RelativePointerSubscriptionV - +s:13WaylandClient27SoftwarePresentationOutcomeO conformsTo s:SH Swift.Hashable +s:13WaylandClient27SoftwarePresentationOutcomeO conformsTo s:SQ Swift.Equatable +s:13WaylandClient27SoftwarePresentationOutcomeO conformsTo s:s16SendableMetatypeP Swift.SendableMetatype +s:13WaylandClient27SoftwarePresentationOutcomeO conformsTo s:s8SendableP Swift.Sendable +s:13WaylandClient27SoftwarePresentationOutcomeO10supersededyA2CmF memberOf s:13WaylandClient27SoftwarePresentationOutcomeO - +s:13WaylandClient27SoftwarePresentationOutcomeO6closedyA2CmF memberOf s:13WaylandClient27SoftwarePresentationOutcomeO - +s:13WaylandClient27SoftwarePresentationOutcomeO8deferredyA2CmF memberOf s:13WaylandClient27SoftwarePresentationOutcomeO - +s:13WaylandClient27SoftwarePresentationOutcomeO9presentedyA2CmF memberOf s:13WaylandClient27SoftwarePresentationOutcomeO - s:13WaylandClient27SurfacePresentationFeedbackO conformsTo s:SQ Swift.Equatable s:13WaylandClient27SurfacePresentationFeedbackO conformsTo s:s16SendableMetatypeP Swift.SendableMetatype s:13WaylandClient27SurfacePresentationFeedbackO conformsTo s:s8SendableP Swift.Sendable @@ -5690,14 +5707,18 @@ s:13WaylandClient6WindowV27requestPresentationFeedbackyyYaKF memberOf s:13Waylan s:13WaylandClient6WindowV2eeoiySbAC_ACtFZ memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV2idAA0C2IDVvp memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV4hash4intoys6HasherVz_tF memberOf s:13WaylandClient6WindowV - -s:13WaylandClient6WindowV4show19timeoutMilliseconds9preparing_ys5Int32V_xAA24SoftwareFrameReservationVYaYbKYCXEyx_AA0iJ0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV4show19timeoutMilliseconds27requestPresentationFeedback9preparing_AA08SoftwareH7OutcomeOs5Int32V_SbxAA0K16FrameReservationVYaYbKYCXEyx_AA0kN0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV4show19timeoutMilliseconds_ys5Int32V_yAA13SoftwareFrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - -s:13WaylandClient6WindowV4show6damage19timeoutMilliseconds9preparing_yAA19SurfaceDamageRegionVSg_s5Int32VxAA24SoftwareFrameReservationVYaYbKYCXEyx_AA0mN0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV4show27requestPresentationFeedback19timeoutMilliseconds_AA08SoftwareF7OutcomeOSb_s5Int32VyAA0J5FrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV4show6damage19timeoutMilliseconds27requestPresentationFeedback9preparing_AA08SoftwareI7OutcomeOAA19SurfaceDamageRegionVSg_s5Int32VSbxAA0L16FrameReservationVYaYbKYCXEyx_AA0lR0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV4show6damage19timeoutMilliseconds_yAA19SurfaceDamageRegionVSg_s5Int32VyAA13SoftwareFrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV4show6damage27requestPresentationFeedback19timeoutMilliseconds_AA08SoftwareG7OutcomeOAA19SurfaceDamageRegionVSg_Sbs5Int32VyAA0K5FrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV5closeyyYaF memberOf s:13WaylandClient6WindowV - -s:13WaylandClient6WindowV6redraw6damage9preparing_yAA19SurfaceDamageRegionVSg_xAA24SoftwareFrameReservationVYaYbKYCXEyx_AA0jK0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV6redraw27requestPresentationFeedback9preparing_AA08SoftwareF7OutcomeOSb_xAA0I16FrameReservationVYaYbKYCXEyx_AA0iK0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV6redraw27requestPresentationFeedback_AA08SoftwareF7OutcomeOSb_yAA0H5FrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV6redraw6damage27requestPresentationFeedback9preparing_AA08SoftwareG7OutcomeOAA19SurfaceDamageRegionVSg_SbxAA0J16FrameReservationVYaYbKYCXEyx_AA0jO0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - +s:13WaylandClient6WindowV6redraw6damage27requestPresentationFeedback_AA08SoftwareG7OutcomeOAA19SurfaceDamageRegionVSg_SbyAA0I5FrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV6redraw6damage_yAA19SurfaceDamageRegionVSg_yAA13SoftwareFrameVYbKXEtYaKF memberOf s:13WaylandClient6WindowV - -s:13WaylandClient6WindowV6redraw9preparing_yxAA24SoftwareFrameReservationVYaYbKYCXE_yx_AA0fG0VtYbKXEtYaKs8SendableRzlF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV6redrawyyyAA13SoftwareFrameVYbKXEYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV7setIconyyAA0cE0OYaKF memberOf s:13WaylandClient6WindowV - s:13WaylandClient6WindowV8activate5usingyAA15ActivationTokenV_tYaKF memberOf s:13WaylandClient6WindowV - diff --git a/docs/release.md b/docs/release.md index 6c5f2cd9..35ab0a50 100644 --- a/docs/release.md +++ b/docs/release.md @@ -30,6 +30,13 @@ This checkpoint contains source-breaking `WaylandClient` changes: `dataTransfer`, and `presentation` cases. `WaylandDisplay.events` is now the complete cross-family ordered feed; specialized streams remain family-ordered convenience views. +- `Window.show(preparing:_:)` and `Window.redraw(preparing:_:)` now return + `SoftwarePresentationOutcome`, distinguishing presented, superseded, deferred, + and closed attempts. Calls that ignore the result remain valid because it is + discardable. Explicit `Void` bindings, typed function references, and wrappers + must adopt or discard the returned outcome. Matching overloads without a + preparation closure expose the same atomic `requestPresentationFeedback` + option and outcome. Prepared generations are revalidated before drawing. ## Required Gates diff --git a/protocols/request-bridge-policy.json b/protocols/request-bridge-policy.json index 5b6990cd..1db4766c 100644 --- a/protocols/request-bridge-policy.json +++ b/protocols/request-bridge-policy.json @@ -52,7 +52,6 @@ "wl_surface": { "wrapperPrefix": "surface", "generatedRequests": [ - "frame", "set_buffer_scale" ], "handwrittenByDefault": "test_recording" diff --git a/safety/unsafe-token-allowlist.tsv b/safety/unsafe-token-allowlist.tsv index 4a2930dd..679e6547 100644 --- a/safety/unsafe-token-allowlist.tsv +++ b/safety/unsafe-token-allowlist.tsv @@ -53,6 +53,7 @@ Sources/CWaylandProtocols/shims/display-core.c wl_proxy_set_queue Queue-at-creat Sources/CWaylandProtocols/shims/display-core.c wl_proxy_wrapper_destroy Queue-at-creation wrapper shim. Sources/CWaylandProtocols/shims/requests-core.c pthread_* Testing-only core request recording lock used to serialize hook globals under ThreadSanitizer. Sources/CWaylandProtocols/shims/requests-desktop.c pthread_* Testing-only desktop request recording lock used to serialize hook globals under ThreadSanitizer. +Sources/CWaylandProtocols/shims/requests-presentation.c pthread_* Testing-only presentation request recording lock used to serialize hook globals under ThreadSanitizer. Sources/CWaylandRuntimeShims/fd-shim.c pthread_* C shim blocks SIGPIPE around fd writes without changing process-global signal disposition. Sources/CWaylandProtocols/shims/requests-core.c wl_proxy_get_queue Queue assertion shim implementation. Sources/CWaylandProtocols/shims/requests-core.c swl_proxy_get_queue_raw Queue assertion shim implementation.