diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27b6ab84..8fa63f49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,10 @@ jobs: if: matrix.os == 'macos-latest' - run: nim r -d:useCpu tests/test_cpu_pixels.nim if: matrix.os == 'windows-latest' + - run: | + sudo apt-get install -y xvfb libgl1-mesa-dri + LIBGL_ALWAYS_SOFTWARE=1 xvfb-run -a nim r tests/test_x11.nim + if: matrix.os == 'ubuntu-latest' # Build native examples. - run: nim c examples/basic.nim diff --git a/src/windy/platforms/linux/x11.nim b/src/windy/platforms/linux/x11.nim index 5b29e3cd..84f952dd 100644 --- a/src/windy/platforms/linux/x11.nim +++ b/src/windy/platforms/linux/x11.nim @@ -42,6 +42,8 @@ type im: XIM xSyncCounter: XSyncCounter lastSync: XSyncValue + syncState: SyncState + syncDeadline: float64 closeRequested, closed: bool innerDecorated: bool @@ -61,6 +63,16 @@ type kwm other + SyncState = enum + ## Where a window is in the _NET_WM_SYNC_REQUEST handshake. + SyncIdle ## No request outstanding. + SyncRequested ## Request received, waiting for its ConfigureNotify. + SyncConfigured ## ConfigureNotify handled, acknowledge after the next frame. + +const + syncConfigureTimeout = 0.010 + ## Seconds to wait for the ConfigureNotify that follows a sync request. + var quitRequested*: bool onQuitRequest*: Callback @@ -856,8 +868,13 @@ proc pollEvents(window: Window) = window.buttonPressed = {} window.buttonReleased = {} - # signal that frame was drawn - display.XSyncSetCounter(window.xSyncCounter, window.lastSync) + # Signal that the frame for the last _NET_WM_SYNC_REQUEST was drawn. This + # only happens once the matching ConfigureNotify was handled and a frame has + # gone by, otherwise a compositor such as KWin shows a frame that is still + # the old size. + if window.syncState == SyncConfigured: + display.XSyncSetCounter(window.xSyncCounter, window.lastSync) + window.syncState = SyncIdle var ev: XEvent @@ -888,7 +905,16 @@ proc pollEvents(window: Window) = proc handleRune(window: Window, rune: Rune) = handleRuneTemplate() - while display.XCheckIfEvent(ev.addr, checkEvent, cast[pointer](window)): + while true: + if not display.XCheckIfEvent(ev.addr, checkEvent, cast[pointer](window)): + if window.syncState == SyncRequested and + epochTime() < window.syncDeadline: + # The window manager sends the ConfigureNotify right after the sync + # request. Wait for it so the next frame is already the new size. + sleep(1) + continue + break + case ev.kind of xeClientMessage: @@ -903,6 +929,8 @@ proc pollEvents(window: Window) = lo: cast[uint32](ev.client.data.l[2]), hi: cast[int32](ev.client.data.l[3]) ) + window.syncState = SyncRequested + window.syncDeadline = epochTime() + syncConfigureTimeout elif ev.client.messageType == xaXdndEnter: # XDnD drag entered. @@ -1035,6 +1063,9 @@ proc pollEvents(window: Window) = if window.onFrame != nil: window.onFrame() + if window.syncState == SyncRequested: + window.syncState = SyncConfigured + of xeMotion: window.mousePrevPos = window.mousePos window.mousePos = ev.motion.pos diff --git a/src/windy/platforms/linux/x11/xlib.nim b/src/windy/platforms/linux/x11/xlib.nim index c84b559e..91c3254b 100644 --- a/src/windy/platforms/linux/x11/xlib.nim +++ b/src/windy/platforms/linux/x11/xlib.nim @@ -354,5 +354,6 @@ proc XSyncCreateCounter*(d: Display, v: XSyncValue): XSyncCounter proc XSyncDestroyCounter*(d: Display, c: XSyncCounter) proc XSyncSetCounter*(d: Display, c: XSyncCounter; v: XSyncValue) +proc XSyncQueryCounter*(d: Display, c: XSyncCounter; v: ptr XSyncValue): cint {.pop.} diff --git a/tests/test_x11.nim b/tests/test_x11.nim new file mode 100644 index 00000000..07c23f01 --- /dev/null +++ b/tests/test_x11.nim @@ -0,0 +1,92 @@ +when defined(linux): + include ../src/windy/platforms/linux/x11 + + ## Plays the window manager side of the _NET_WM_SYNC_REQUEST handshake so + ## it runs under a bare Xvfb. The rule under test: the sync counter is only + ## updated after the ConfigureNotify for the request was handled and a + ## frame has gone by, never before, and never without a request. + + proc counterValue(window: Window): XSyncValue = + doAssert display.XSyncQueryCounter(window.xSyncCounter, result.addr) != 0 + + proc sendSyncRequest(window: Window, lo: uint32, hi: int32) = + ## Sends what a window manager sends right before resizing the window. + let xaWMProtocols = display.XInternAtom("WM_PROTOCOLS", 0) + let message = newClientMessage( + window.handle, + xaWMProtocols, + [xaNetWMSyncRequest.clong, 0.clong, lo.clong, hi.clong] + ) + window.handle.send(message) + display.XSync() + + proc resize(window: Window, size: IVec2) = + display.XResizeWindow(window.handle, size.x.uint32, size.y.uint32) + display.XSync() + + proc testSyncRequestAck() = + let window = newWindow("Sync", ivec2(200, 100), visible = false) + doAssert window.xSyncCounter.int != 0, "Xvfb lacks the SYNC extension" + + var resizes: seq[IVec2] + window.onResize = proc() = + resizes.add window.size + + doAssert window.counterValue.lo == 0 + doAssert window.syncState == SyncIdle + + # A resize without a request never touches the counter. + window.resize(ivec2(210, 110)) + pollEvents() + doAssert resizes == @[ivec2(210, 110)] + doAssert window.counterValue.lo == 0 + doAssert window.syncState == SyncIdle + + # Request and ConfigureNotify in one poll: the ack waits for a frame. + window.sendSyncRequest(1, 0) + window.resize(ivec2(300, 200)) + pollEvents() + doAssert resizes[^1] == ivec2(300, 200) + doAssert window.syncState == SyncConfigured + doAssert window.counterValue.lo == 0, "acked before the resized frame" + pollEvents() + doAssert window.syncState == SyncIdle + doAssert window.counterValue.lo == 1 + + # Request in one poll, ConfigureNotify in a later one. The poll that saw + # the request must not ack, and must not wait longer than the timeout. + window.sendSyncRequest(2, 0) + let start = epochTime() + pollEvents() + doAssert epochTime() - start < 1.0, "poll did not give up waiting" + doAssert window.syncState == SyncRequested + doAssert window.counterValue.lo == 1, "acked without a ConfigureNotify" + pollEvents() + doAssert window.counterValue.lo == 1 + window.resize(ivec2(400, 300)) + pollEvents() + doAssert resizes[^1] == ivec2(400, 300) + doAssert window.syncState == SyncConfigured + doAssert window.counterValue.lo == 1 + pollEvents() + doAssert window.syncState == SyncIdle + doAssert window.counterValue.lo == 2 + + # The full 64-bit value round-trips and a newer request wins. + window.sendSyncRequest(3, 0) + window.sendSyncRequest(5, 1) + window.resize(ivec2(500, 400)) + pollEvents() + pollEvents() + let value = window.counterValue + doAssert value.lo == 5 and value.hi == 1 + + # Once idle, polling leaves the counter alone. + pollEvents() + doAssert window.counterValue.lo == 5 + doAssert window.syncState == SyncIdle + + window.close() + + testSyncRequestAck() + echo "test_x11 passed"