Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ extension DisplayCore {
}
}

func cancelGraphicsPreviewPresentation(for windowID: WindowID) {
withFatalFailureFinalization {
guard let window = surfaces.window(windowID) else { return }

window.cancelGraphicsPreviewPresentationOnOwnerThread()
}
}

func presentGraphicsPreviewBuffer(
_ buffer: RawSurfaceBuffer,
on windowID: WindowID,
Expand Down
13 changes: 13 additions & 0 deletions Sources/WaylandClient/Internal/Popup/PopupConfigureState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ package final class PopupConfigureState {
recoverablePhase.hasReceivedInitialConfigure
}

/// Whether a complete surface configure is waiting to be consumed.
package var hasPendingSurfaceConfigure: Bool {
switch recoverablePhase {
case .ready:
true
case .waitingForInitialPopupConfigure,
.pendingInitialRolePayload,
.waitingForPopupConfigure,
.pendingRolePayload:
false
}
}

package init() {
// Starts with no popup configure payload.
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/WaylandClient/Internal/Popup/PopupModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package enum PopupEvent: Equatable, Sendable {
case frameBecameReady(bufferAvailability: RedrawBufferAvailability)
case bufferBecameAvailable(bufferAvailability: RedrawBufferAvailability)
case redrawRequestConsumed(bufferAvailability: RedrawBufferAvailability)
case redrawRequestCanceled(bufferAvailability: RedrawBufferAvailability)
case presentationStarted(PopupPresentationRequest)
case presentationBlockedByBuffer
case presentationSucceeded(generation: UInt64, bufferAvailability: RedrawBufferAvailability)
Expand Down Expand Up @@ -122,6 +123,8 @@ package struct PopupModel: Equatable, Sendable {
return reduceRedraw(.bufferBecameAvailable, bufferAvailability: bufferAvailability)
case .redrawRequestConsumed(let bufferAvailability):
return try reduceRedrawRequestConsumed(bufferAvailability: bufferAvailability)
case .redrawRequestCanceled(let bufferAvailability):
return reduceRedraw(.redrawRequestCanceled, bufferAvailability: bufferAvailability)
case .presentationStarted(let request):
return try reducePresentationStarted(request)
case .presentationBlockedByBuffer:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,11 @@ extension PopupRoleSurface {
}

package func redrawBufferAvailability() throws -> RedrawBufferAvailability {
surfaceRuntime.redrawBufferAvailability(
matching: try currentSurfaceGeometry().bufferSize.rawSize
try RedrawBufferAvailability.resolvingPendingConfigure(
configureState.hasPendingSurfaceConfigure,
currentBufferAvailability: surfaceRuntime.redrawBufferAvailability(
matching: try currentSurfaceGeometry().bufferSize.rawSize
)
)
}

Expand Down Expand Up @@ -315,6 +318,8 @@ extension PopupRoleSurface {
.bufferBecameAvailable(bufferAvailability: bufferAvailability)
case .redrawRequestConsumed:
.redrawRequestConsumed(bufferAvailability: bufferAvailability)
case .redrawRequestCanceled:
.redrawRequestCanceled(bufferAvailability: bufferAvailability)
case .drawBlockedByBuffer:
.presentationBlockedByBuffer
case .presented(let generation):
Expand Down
35 changes: 32 additions & 3 deletions Sources/WaylandClient/Internal/Windowing/TopLevelWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -788,8 +788,12 @@ package final class TopLevelWindow {
}

private func resetTransientState() {
surfaceRuntime.resetTransientTransactionState()
resetTransientPresentationState()
}

private func resetTransientPresentationState() {
do {
surfaceRuntime.resetTransientTransactionState()
_ = try model.reduce(.transientStateReset)
} catch let error as ClientError {
reportCallbackFailure(operation: .transientStateReset, error: error)
Expand Down Expand Up @@ -1154,8 +1158,11 @@ extension TopLevelWindow {
}

private func redrawBufferAvailability() throws -> RedrawBufferAvailability {
surfaceRuntime.redrawBufferAvailability(
matching: try currentSurfaceGeometry().bufferSize.rawSize
try RedrawBufferAvailability.resolvingPendingConfigure(
configureState.hasPendingSurfaceConfigure,
currentBufferAvailability: surfaceRuntime.redrawBufferAvailability(
matching: try currentSurfaceGeometry().bufferSize.rawSize
)
)
}

Expand Down Expand Up @@ -1302,6 +1309,28 @@ extension TopLevelWindow {
return try currentSurfaceGeometry()
}

/// Cancels the redraw request held by a graphics frame that wasn't submitted.
///
/// A newer invalidation is republished. Surface transaction state and software
/// presentations are left intact because they may belong to other frames.
package func cancelGraphicsPreviewPresentationOnOwnerThread() {
connection.preconditionIsOwnerThread()

do {
try interpretWindowEffects(
model.reduce(
.graphicsPreviewPresentationCanceled(
bufferAvailability: try redrawBufferAvailability()
)
)
)
} catch let error as ClientError {
reportCallbackFailure(operation: .markNeedsRedraw, error: error)
} catch {
reportCallbackFailure(operation: .markNeedsRedraw, error: error)
}
}

package var stateSnapshotOnOwnerThread: WindowStateSnapshot {
get throws {
connection.preconditionIsOwnerThread()
Expand Down
25 changes: 25 additions & 0 deletions Sources/WaylandClient/Internal/Windowing/WindowModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ 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 .presentationStarted(let request):
return try reducePresentationStarted(request)
case .presentationBlockedByBuffer: return try reducePresentationBlockedByBuffer()
Expand Down Expand Up @@ -376,6 +380,27 @@ extension WindowModel {
}
}

private mutating func reduceGraphicsPreviewPresentationCanceled(
bufferAvailability: RedrawBufferAvailability
) -> [WindowEffect] {
guard !isClosed else { return [] }
let windowID = id
return updateActiveWindowStateIfPresent { activeState in
guard activeState.presentation.isIdle else {
return []
}

return Self.mapRedrawEffects(
activeState.redraw.reduce(
.redrawRequestCanceled,
bufferAvailability: bufferAvailability
),
in: activeState,
windowID: windowID
)
}
}

private func mapRedrawEffects(_ effects: [WindowRedrawEffect]) -> [WindowEffect] {
Self.mapRedrawEffects(effects, windowID: id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ package enum WindowEvent: Equatable, Sendable {
case frameBecameReady(bufferAvailability: RedrawBufferAvailability)
case bufferBecameAvailable(bufferAvailability: RedrawBufferAvailability)
case redrawRequestConsumed(bufferAvailability: RedrawBufferAvailability)
case graphicsPreviewPresentationCanceled(
bufferAvailability: RedrawBufferAvailability
)
case presentationStarted(PresentationRequest)
case presentationBlockedByBuffer
case presentationSucceeded(generation: UInt64, bufferAvailability: RedrawBufferAvailability)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum WindowRedrawEvent: Equatable, Sendable {
case frameBecameReady
case bufferBecameAvailable
case redrawRequestConsumed
case redrawRequestCanceled
case drawBlockedByBuffer
case presented(generation: UInt64)
case transientStateReset
Expand All @@ -30,6 +31,21 @@ package enum RedrawBufferAvailability: Equatable, Sendable {
var isAvailable: Bool {
self == .available
}

/// Resolves whether a redraw can start before it consumes a pending configure.
///
/// A complete configure supersedes the current geometry, so the old buffer pool
/// cannot decide whether the next frame can be drawn.
package static func resolvingPendingConfigure(
_ hasPendingSurfaceConfigure: Bool,
currentBufferAvailability: @autoclosure () throws -> Self
) rethrows -> Self {
guard !hasPendingSurfaceConfigure else {
return .available
}

return try currentBufferAvailability()
}
}

struct WindowRedrawState: Equatable, Sendable {
Expand Down Expand Up @@ -94,6 +110,8 @@ struct WindowRedrawState: Equatable, Sendable {
case .redrawRequestConsumed:
markRedrawRequestConsumed()
effects = []
case .redrawRequestCanceled:
effects = cancelRedrawRequest(bufferAvailability: bufferAvailability)
case .drawBlockedByBuffer:
markDrawBlockedByBuffer()
effects = []
Expand Down Expand Up @@ -134,6 +152,17 @@ extension WindowRedrawState {
}
}

private mutating func cancelRedrawRequest(
bufferAvailability: RedrawBufferAvailability
) -> [WindowRedrawEffect] {
guard case .frameReady(.outstanding) = pacing else {
return []
}

pacing = .frameReady(.none)
return publishIfNeeded(bufferAvailability: bufferAvailability)
}

private mutating func markDrawBlockedByBuffer() {
guard isDirty else { return }
pacing = .waitingForBuffer
Expand Down
6 changes: 6 additions & 0 deletions Sources/WaylandClient/Public/Display/WaylandDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ public actor WaylandDisplay {
try requireCore().requestRedraw(windowID)
}

package func cancelGraphicsPreviewPresentation(for windowID: WindowID) {
guard case .active(let core, _) = lifecycle else { return }

core.cancelGraphicsPreviewPresentation(for: windowID)
}

package func requestPresentationFeedback(_ windowID: WindowID) throws {
try requireCore().requestPresentationFeedback(windowID)
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/WaylandClient/Public/Window/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,8 @@ public struct Window: Sendable, Hashable {
timeoutMilliseconds: timeoutMilliseconds
)
}

package func cancelGraphicsPreviewPresentation() async {
await display.cancelGraphicsPreviewPresentation(for: id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ package protocol WaylandGraphicsManagedWindow: Sendable {
timeoutMilliseconds: Int32
) async throws -> SurfaceGeometry

/// Cancels presentation state reserved for a frame that produced no work.
func cancelGraphicsPreviewPresentation() async

func requestGraphicsPreviewSurfaceFeedback(
timeoutMilliseconds: Int32
) async throws -> SurfaceCapabilitySnapshot
Expand Down Expand Up @@ -65,6 +68,10 @@ extension WaylandGraphicsManagedWindow {
try await geometry
}

package func cancelGraphicsPreviewPresentation() async {
// Alternate managed windows may not retain presentation state between calls.
}

package func requestGraphicsPreviewSurfaceFeedback(
timeoutMilliseconds _: Int32
) async throws -> SurfaceCapabilitySnapshot {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1638,7 +1638,7 @@ package actor WaylandGraphicsWindowBackingStorage {
geometry: geometry
)
} catch {
releaseExternalBufferReservationsAndCancelLease(leaseID: leaseID)
await cancel(leaseID: leaseID)
throw error
}
let operation = try leaseState.prepareSubmission(leaseID: leaseID)
Expand Down Expand Up @@ -1709,7 +1709,7 @@ package actor WaylandGraphicsWindowBackingStorage {
geometry: geometry
)
} catch {
releaseExternalBufferReservationsAndCancelLease(leaseID: leaseID)
await cancel(leaseID: leaseID)
throw error
}
let operation = try leaseState.prepareSubmission(leaseID: leaseID)
Expand Down Expand Up @@ -1858,13 +1858,6 @@ package actor WaylandGraphicsWindowBackingStorage {
externalBufferRegistry.releaseReservations(leaseID: leaseID)
}

private func releaseExternalBufferReservationsAndCancelLease(
leaseID: WaylandGraphicsFrameLeaseID
) {
releaseExternalBufferReservations(leaseID: leaseID)
_ = leaseState.cancel(leaseID: leaseID)
}

private func markExternalBufferSubmitted(
_ externalBuffer: WaylandGraphicsExternalBuffer,
leaseID: WaylandGraphicsFrameLeaseID,
Expand Down Expand Up @@ -2047,9 +2040,13 @@ package actor WaylandGraphicsWindowBackingStorage {
)
}

func cancel(leaseID: WaylandGraphicsFrameLeaseID) {
if leaseState.cancel(leaseID: leaseID) {
releaseExternalBufferReservations(leaseID: leaseID)
func cancel(leaseID: WaylandGraphicsFrameLeaseID) async {
let shouldCancelPresentation = leaseState.hasSubmittedFrame
guard leaseState.cancel(leaseID: leaseID) else { return }

releaseExternalBufferReservations(leaseID: leaseID)
if shouldCancelPresentation {
await window.cancelGraphicsPreviewPresentation()
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/WaylandRaw/Internal/Binding/XDGConfigureState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ package final class XDGConfigureState {
recoverablePhase.hasReceivedInitialConfigure
}

/// Whether a complete surface configure is waiting to be consumed.
package var hasPendingSurfaceConfigure: Bool {
switch recoverablePhase {
case .collectingInitial, .collecting:
false
case .ready:
true
}
}

package init(initialSize: TopLevelSize = .unspecified) {
phase = .collectingInitial(
XDGConfigureCollection(
Expand Down
28 changes: 28 additions & 0 deletions Tests/WaylandClientTests/Popup/PopupDomainTypesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,50 @@ struct PopupDomainTypesTests {
func popupConfigureStateLatchesPlacementAndAcksOnlyAfterSurfaceConfigure() throws {
let state = PopupConfigureState()

#expect(!state.hasPendingSurfaceConfigure)
state.handlePopupConfigure(
RawXDGPopupConfigure(x: 4, y: 8, width: 120, height: 64)
)
#expect(!state.hasReceivedInitialConfigure)
#expect(!state.hasPendingSurfaceConfigure)
#expect(state.consumeLatestConfigure() == nil)

let sequence = try #require(state.handleSurfaceConfigure(serial: 99).configure)

#expect(state.hasReceivedInitialConfigure)
#expect(state.hasPendingSurfaceConfigure)
#expect(sequence.serial == 99)
#expect(sequence.placement.origin == LogicalOffset(x: 4, y: 8))
#expect(sequence.placement.size == (try PositiveLogicalSize(width: 120, height: 64)))
#expect(state.consumeLatestConfigure() == sequence)
#expect(!state.hasPendingSurfaceConfigure)
#expect(state.consumeLatestConfigure() == nil)
}

@Test
func pendingPopupConfigurePublishesRedrawWithoutOldBuffer() throws {
let configureState = PopupConfigureState()
configureState.handlePopupConfigure(
RawXDGPopupConfigure(x: 4, y: 8, width: 120, height: 64)
)
_ = try #require(configureState.handleSurfaceConfigure(serial: 99).configure)
var redraw = WindowRedrawState()
let availability = RedrawBufferAvailability.resolvingPendingConfigure(
configureState.hasPendingSurfaceConfigure,
currentBufferAvailability: {
Issue.record("The old popup buffer pool should not be checked.")
return .unavailable
}()
)

let effects = redraw.reduce(
.contentInvalidated,
bufferAvailability: availability
)

#expect(effects == [.publishRedrawRequested])
}

@Test
func popupConfigureStateRejectsSurfaceConfigureWithoutPopupPayload() {
let state = PopupConfigureState()
Expand Down
Loading
Loading