diff --git a/.github/workflows/build-full.yml b/.github/workflows/build-full.yml
index 416d5a8..2cb6eec 100644
--- a/.github/workflows/build-full.yml
+++ b/.github/workflows/build-full.yml
@@ -33,6 +33,8 @@ jobs:
sudo apt-get install -y --no-install-recommends \
xvfb \
xauth \
+ weston \
+ libdecor-0-0 \
mesa-utils \
vulkan-tools \
mesa-vulkan-drivers \
@@ -118,6 +120,42 @@ jobs:
run: |
xvfb-run -a -s "-screen 0 1280x1024x24" "$NIMBLE_BIN" --features:dev test
+ - name: Test Wayland event loop
+ if: runner.os == 'Linux'
+ env:
+ GALLIUM_DRIVER: "llvmpipe"
+ LIBGL_ALWAYS_SOFTWARE: "1"
+ run: |
+ export XDG_RUNTIME_DIR="$RUNNER_TEMP/siwin-wayland-runtime"
+ mkdir -p "$XDG_RUNTIME_DIR"
+ chmod 700 "$XDG_RUNTIME_DIR"
+ weston \
+ --backend=headless-backend.so \
+ --renderer=pixman \
+ --socket=wayland-siwin \
+ --idle-time=0 \
+ --no-config \
+ --log="$RUNNER_TEMP/siwin-weston.log" &
+ weston_pid=$!
+ trap 'kill "$weston_pid" 2>/dev/null || true' EXIT
+
+ for attempt in $(seq 1 50); do
+ if [[ -S "$XDG_RUNTIME_DIR/wayland-siwin" ]]; then
+ break
+ fi
+ if [[ "$attempt" == 50 ]]; then
+ cat "$RUNNER_TEMP/siwin-weston.log"
+ exit 1
+ fi
+ sleep 0.1
+ done
+
+ XDG_SESSION_TYPE=wayland \
+ WAYLAND_DISPLAY=wayland-siwin \
+ DISPLAY= \
+ nim c -r -d:siwin.vulkan=off --threads:on --hints:off \
+ tests/t_event_loop.nim
+
# - name: Build Tests (Vulkan Compile Check)
# if: runner.os == 'Linux'
# env:
diff --git a/README.md b/README.md
index cc02db8..bcf33bc 100644
--- a/README.md
+++ b/README.md
@@ -16,24 +16,47 @@ Can be used as an alternative to GLFW/GLUT/windy
* works on: Linux(X11 and Wayland), Windows, MacOS
* handles events from: mouse, keyboard
* and also supports: clipboard, offscreen rendering, interactive move/resize, etc.
+* can block efficiently for native input, animation deadlines, or cross-thread work
Examples
## simple window
+Create a window with continous event polling:
+
+```nim
+import siwin, opengl
+
+let window = newOpenglWindow()
+opengl.loadExtensions() # load opengl functions
+
+window.eventsHandler.onRender = proc(e: RenderEvent) =
+ glClearColor(0.1, 0.1, 0.1, 1)
+ glClear(GlColorBufferBit or GlDepthBufferBit)
+
+run window
+```
+
+## event loop window
+
+Run an application with a more efficient blocking event loop:
+
```nim
import siwin, opengl
-let win = newOpenglWindow()
+let globals = newSiwinGlobals()
+let window = globals.newOpenglWindow()
opengl.loadExtensions() # load opengl functions
-win.eventsHandler.onRender = proc(e: RenderEvent) =
+window.eventsHandler.onRender = proc(e: RenderEvent) =
glClearColor(0.1, 0.1, 0.1, 1)
glClear(GlColorBufferBit or GlDepthBufferBit)
-run win
+globals.runEventDriven(window)
```
+This approach handles many windows with a shared event loop.
+
## software-rendering window
```nim
import siwin, vmath
@@ -240,23 +263,89 @@ loadExtensions()
manual main cycle
+```nim
+import std/times
+import siwin, opengl
+
+let globals = newSiwinGlobals()
+let window = globals.newOpenglWindow()
+opengl.loadExtensions()
+
+var elapsed = initDuration()
+window.eventsHandler = WindowEventsHandler(
+ onTick: proc(event: TickEvent) =
+ elapsed += event.deltaTime
+ event.window.redraw()
+ ,
+ onRender: proc(event: RenderEvent) =
+ let brightness = (elapsed.inMilliseconds mod 1000).float32 / 1000
+ glClearColor(brightness, brightness, brightness, 1)
+ glClear(GlColorBufferBit or GlDepthBufferBit)
+)
+
+window.firstStep(makeVisible = true)
+while window.opened:
+ window.step()
+```
+
+manual event loop cycle
+
+The blocking event loop approach is recommended when apps don't need continuous `onTick` events. Wake events can be added for short lived animations or other needs. This can significantly reduce CPU usage over the polling approach.
+
+Switch applications from `run` to `runEvenDrive` which waits once for native input or an explicit wake event and then services every window. This means `onTick` callbacks and others will only be called on wake events or when there's new events.
+
```nim
import siwin
-let siwinGlobals = newSiwinGlobals()
-let window = siwinGlobals.newOenglWindow()
-loadExtensions()
+let globals = newSiwinGlobals()
+let window = globals.newSoftwareRenderingWindow(title = "Siwin event loop")
-let eventsHandler = WindowEventsHandler(
- # ...
+window.eventsHandler = WindowEventsHandler(
+ onKey: proc(event: KeyEvent) =
+ if not event.pressed and event.key == Key.escape:
+ event.window.close()
)
-window.firstStep(eventsHandler, makeVisible=true)
-while window.opened:
- window.step(eventsHandler)
+globals.runEventDriven(window)
+```
+
+`runEventDriven` is a convenience runner built from lower-level event-loop APIs. Use them directly when integrating Siwin with another event loop, scheduler, or application queue:
+* `globals.pollEvents()` dispatches available native events and returns immediately.
+* `globals.waitEvents()` waits for native input or an explicit application wakeup.
+* `globals.waitEvents(timeout)` also accepts a deadline and returns `eventActivity`
+ or `eventTimeout`.
+* `window.serviceWindow()` performs one nonblocking tick, render, and presentation
+ pass after the application has handled the dispatched work.
+
+Applications that need to drain another event loop or queue can own the wait directly. Install one copied `EventLoopWaker` on each application-thread destination queue instead of sharing all of `SiwinGlobals`. Every producer must enqueue its message before waking the application thread:
+
+The queue names below are illustrative; use the queue owned by your runtime:
+
+```nim
+let waker = globals.eventLoopWaker()
+
+# On a producer thread:
+destinationQueue.send(message)
+waker.wake()
+
+# On the application thread, after waitEvents returns:
+destinationQueue.drain()
```
+Wakeups carry no data and may be coalesced, so the destination queue remains the source of truth. Drain every relevant queue after each `waitEvents` return, then call `serviceWindow` for every open window. A copied waker is safe to retain and becomes harmless after its event loop shuts down.
+
+The C ABI provides the same lifetime model through the independently retained opaque `SiwinEventLoopWaker` handle. Create it with `siwin_event_loop_waker`, wake it from a producer with `siwin_event_loop_waker_wake`, and release it with `siwin_destroy_event_loop_waker`; the handle does not require the producer to retain `SiwinGlobals`.
+
+For animation, pass the next real deadline instead of scheduling an unconditional 16 ms wake:
+
+```nim
+discard globals.waitEvents(timeUntilNextAnimation)
+window.serviceWindow()
+```
+
+See [text_input_demo.nim](examples/text_input_demo.nim) for a complete loop that combines native input, cursor blinking, and scroll-decay deadlines.
+
running multiple windows
```nim
@@ -293,6 +382,15 @@ runMultiple(
)
```
+Use `runMultipleEventDriven` instead when the application doesn't need continous `onTick` events and can use the more efficient blocking call:
+
+```nim
+siwinGlobals.runMultipleEventDriven(
+ (window: win1, eventsHandler: win1_eventsHandler, makeVisible: true),
+ (window: win2, eventsHandler: win2_eventsHandler, makeVisible: true),
+)
+```
+
client-side decorations
```nim
diff --git a/TODO.md b/TODO.md
index 02cad45..5973e9a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,8 +3,67 @@
## DPI / Coordinate Consistency
- [x] Cocoa: convert mouse move/click coordinates from points to backing pixels and expose `window.uiScale`.
- [ ] Winapi: apply per-window DPI scaling to mouse move/click coordinates so `MouseMoveEvent`/`ClickEvent` stay in physical pixel space (`window.size`/pixel buffer space).
-- [ ] Wayland: apply surface/output scale (including fractional scale support when available) to mouse move/click coordinates so input coordinates match physical pixel size.
-- [ ] X11: define and implement DPI-scaling policy for mouse move/click coordinates (for example using `Xft.dpi`) so coordinates are consistent with the physical-pixel model.
+- [x] Wayland: apply surface/output scale (including fractional scale support when available) to mouse move/click coordinates so input coordinates match physical pixel size.
+- [x] X11: keep native X11 window and pointer coordinates in physical pixels while exposing `Xft.dpi` through `window.uiScale`.
+
+## Event-Driven Application Loop
+
+- [x] Define `pollEvents` and `waitEvents` as application-thread-only operations that synchronously dispatch pending native callbacks from the platform application queue.
+- [x] Define `EventLoopWaker` as a narrow, opaque, copyable capability so worker threads do not need to retain or access the complete `SiwinGlobals` object.
+- [x] Define `EventLoopWaker.wake` as thread-safe, data-free notification: producers must enqueue their work first and then wake the loop; wakeups may be coalesced.
+- [x] Keep `wakeEventLoop(globals)` as a convenience wrapper around `globals.eventLoopWaker().wake()` for callers already on or otherwise holding the globals owner.
+- [x] Define waker lifetime and shutdown semantics explicitly, including what happens when a copied waker outlives its `SiwinGlobals`; waking a stopped/destroyed loop must be harmless and must not access a closed OS handle.
+- [x] Document the integration pattern for other event loops or queues: install one waker on the application-thread destination queue, enqueue each message before calling `wake`, then drain that queue after `waitEvents` returns.
+- [x] Support animation scheduling either by having a timer or queue producer enqueue a tick and wake the loop, or by passing the next animation deadline to timed `waitEvents`; `examples/text_input_demo.nim` demonstrates the deadline-based form and individual animations do not register with Siwin.
+- [x] Keep arbitrary external FD/source registration outside this initial API; enqueue-plus-wake is sufficient for renderer completions, image loading, animation schedulers, and other application queues.
+- [x] Make `serviceWindow` nonblocking and responsible only for per-window tick, redraw/render, buffer swap, and presentation work on Cocoa, Winapi, X11, and Wayland.
+- [x] Preserve the existing source and C ABI: keep no-argument `Window.step()` and `siwin_window_step` behavior available as compatibility wrappers while embedders opt into `pollEvents`/`waitEvents` plus `serviceWindow`.
+- [x] Do not add a wake callback to `WindowEventsHandler`, because wakeup is application-loop control rather than a window event and changing the handler layout could break ABI consumers.
+- [x] Add corresponding additive C ABI functions: `siwin_poll_events`, `siwin_wait_events`, `siwin_wake_event_loop`, and `siwin_window_service`.
+- [x] Give non-Nim consumers an independently retained opaque `SiwinEventLoopWaker` handle so producer threads need not retain `SiwinGlobals`; waking it after globals shutdown is harmless.
+- [x] Add `runEventDriven` and `runMultipleEventDriven`, which wait once and then service every window; keep `run` and `runMultiple` unchanged so their continuous `onTick` behavior remains compatible.
+- [x] Support an infinite idle wait and immediate/nonblocking `pollEvents` and zero-timeout paths.
+- [x] Ensure every backend's finite wait and internal scheduling deadlines remain monotonic across wall-clock changes.
+- [x] Add cross-platform tests for idle blocking/CPU use, zero timeout, copied cross-thread wakers, shutdown safety, wakeup coalescing, nonblocking window service, and existing `step()` compatibility.
+- [x] Exercise the X11 wait path under Xvfb and the Wayland wait path under headless Weston in Linux CI.
+- [x] Extend global-loop coverage to multiple windows, redraw scheduled by a worker wake, and repeated producer/consumer races that stress no-lost-wake behavior.
+
+### Cocoa (macOS)
+
+- [x] Move the application-global `NSApp` event draining out of `WindowCocoa.step`; the compatibility wrapper now delegates to the global pump before servicing its window.
+- [x] Implement `pollEvents` by draining immediately available events in default, event-tracking/live-resize, and modal-panel run-loop modes.
+- [x] Implement `waitEvents` with `nextEventMatchingMask`, using `distantFuture` plus a monotonic dispatch-timer sentinel for finite waits, then drain immediately available events before returning.
+- [x] Implement `wakeEventLoop` by posting a coalesced application-defined `NSEvent`, which AppKit permits from a secondary thread.
+- [x] Recognize and consume the Siwin wake sentinel without forwarding it to a window or invoking a `WindowEventsHandler` callback; clear the coalescing flag before the application drains its work queues.
+- [x] Keep all AppKit dispatch and window callbacks on the application thread.
+
+### X11
+
+- [x] Wait with `poll()` on both `ConnectionNumber(display)` and a self-pipe owned by `SiwinGlobalsX11`.
+- [x] Implement `wakeEventLoop` by signaling only the wake FD; do not call Xlib from the producer thread or require `XInitThreads`.
+- [x] Add a window registry to `SiwinGlobalsX11` mapping X11 window IDs to `WindowX11` objects.
+- [x] Replace the current per-window `XCheckIfEvent` loop with direct global `XPending`/`XNextEvent` dispatch that routes each event through the registry while preserving key-repeat lookahead, clipboard, drag-and-drop, and sync-request behavior.
+- [x] Drain/reset the wake FD without losing a wake that races with event dispatch.
+- [x] Flush X11 output at the appropriate global/per-window boundaries without introducing a blocking `XNextEvent` call after the readiness check.
+
+### Wayland
+
+- [x] Wait with `poll()` on `wl_display_get_fd()` and a self-pipe owned by `SiwinGlobalsWayland`.
+- [x] Add bindings for `wl_display_prepare_read`, `wl_display_read_events`, and `wl_display_cancel_read`.
+- [x] Implement the required race-free sequence: dispatch pending events until `prepare_read` succeeds, flush, poll, call `read_events` when the display is readable or `cancel_read` when another source wakes the loop, then dispatch pending events.
+- [x] Replace the per-window `wl_display_roundtrip()` in `WindowWayland.step` with the application-global event pump; retain roundtrips only where synchronous initialization/configuration genuinely requires them.
+- [x] Continue routing callbacks through the existing `SiwinGlobalsWayland.associatedWindows` and Wayland proxy state.
+- [x] Include keyboard-repeat deadlines in the next wait timeout so held keys continue repeating without idle polling.
+- [x] Verify that libdecor dispatch and flushing are integrated with the same display wait without adding a second polling loop.
+
+### Windows (Winapi)
+
+- [x] Store the application thread/event-loop state in a Winapi-specific `SiwinGlobals`, including an auto-reset Win32 event used for cross-thread wakeups.
+- [x] Implement `waitEvents` with `MsgWaitForMultipleObjectsEx`, monitoring both the wake handle and the thread message queue with `QS_ALLINPUT` and `MWMO_INPUTAVAILABLE`.
+- [x] Implement `wakeEventLoop` with `SetEvent`; signaling before the wait remains observable and repeated signals may be coalesced.
+- [x] Move the global `PeekMessage`/`TranslateMessage`/`DispatchMessage` loop out of `WindowWinapi.step` and remove its idle `sleep(1)` path.
+- [x] Let normal Win32 dispatch continue routing messages to the correct `HWND` window procedure, while `serviceWindow` handles only per-window tick/render/presentation work.
+- [x] Map finite `Duration` values safely to the millisecond timeout accepted by `MsgWaitForMultipleObjectsEx`, including zero and infinite waits.
## Wayland
- [x] Make `KeyEvent.modifiers` reflect effective xkb modifier state (including remaps like Caps-as-Ctrl), not only raw pressed key symbols.
@@ -37,7 +96,7 @@
- [ ] Implement custom image cursor support on Cocoa.
- [x] Replace deprecated activation calls (`activateIgnoringOtherApps`) with the current AppKit approach.
- [x] Revisit `WindowCocoaMetal` implementation so it uses a true Metal-backed view/path instead of `NSOpenGLView`.
-- [ ] Add macOS branches in top-level window/screen wrappers where missing (for example `screenCount`/`screen`/`defaultScreen` in `src/siwin/window.nim`).
+- [x] Add macOS branches in top-level window/screen wrappers where missing (for example `screenCount`/`screen`/`defaultScreen` in `src/siwin/window.nim`).
## IME / Text Input
- [ ] Add a cross-platform API for enabling/disabling text input mode (similar to `runeInputEnabled` semantics).
diff --git a/bindings/siwin.h b/bindings/siwin.h
index a65e6c9..97d1cad 100644
--- a/bindings/siwin.h
+++ b/bindings/siwin.h
@@ -7,6 +7,7 @@ typedef struct {} *SiwinGlobals;
typedef struct {} *Window;
typedef struct {} *Screen;
typedef struct {} *Clipboard;
+typedef struct {} *SiwinEventLoopWaker;
typedef struct NimRtti {
@@ -322,8 +323,20 @@ typedef struct WindowEventHandler {
extern Platform siwin_default_platform();
extern SiwinGlobals siwin_new_globals(Platform platform);
extern void siwin_destroy_globals(SiwinGlobals globals);
+ /* Returns nonzero when native activity or an explicit wake was consumed. */
+ extern char siwin_poll_events(SiwinGlobals globals);
+ /* A negative timeout waits indefinitely. Returns 0 for activity, 1 for timeout. */
+ extern char siwin_wait_events(SiwinGlobals globals, int timeout_milliseconds);
+ extern void siwin_wake_event_loop(SiwinGlobals globals);
+ /* Retained independently of globals; destroy it when the worker is done.
+ * Enqueue application data before waking. Wakes carry no data and may be
+ * coalesced; drain the destination queue after siwin_wait_events returns. */
+ extern SiwinEventLoopWaker siwin_event_loop_waker(SiwinGlobals globals);
+ extern void siwin_event_loop_waker_wake(SiwinEventLoopWaker waker);
+ extern void siwin_destroy_event_loop_waker(SiwinEventLoopWaker waker);
extern void siwin_destroy_window(Window window);
+ extern void siwin_window_service(Window window);
extern Window siwin_new_software_rendering_window(
SiwinGlobals globals,
@@ -441,4 +454,3 @@ typedef struct WindowEventHandler {
cmdCount = argc; \
cmdLine = argv; \
siwin_main();
-
diff --git a/examples/text_input_demo.nim b/examples/text_input_demo.nim
index 1d5cfd7..299eb77 100644
--- a/examples/text_input_demo.nim
+++ b/examples/text_input_demo.nim
@@ -2,7 +2,10 @@ import std/[os, strutils, unicode, times]
import pixie
import siwin
-const PasteMaxChars = 64
+const
+ CursorBlinkMilliseconds = 5_000
+ PasteMaxChars = 64
+ ScrollIdleMilliseconds = 150
when defined(macosx):
const CopyPasteHint = "Cmd+C/Cmd+V"
@@ -20,6 +23,8 @@ type
cursorElapsedMs: float32
scrollSpeedX: float32
scrollSpeedY: float32
+ scrollElapsedMs: float32
+ scrollUpdated: bool
modifiers: set[ModifierKey]
mousePos: Vec2
mouseInside: bool
@@ -108,6 +113,15 @@ proc formatClickPos(pos: Vec2, hasPos: bool): string =
proc formatUiScale(scale: float32): string =
formatFloat(scale, ffDecimal, 2)
+proc nextAnimationWait(state: TextInputDemoState): Duration =
+ let cursorWaitMs = max(1, CursorBlinkMilliseconds - state.cursorElapsedMs.int)
+ let waitMs =
+ if abs(state.scrollSpeedX) >= 0.05 or abs(state.scrollSpeedY) >= 0.05:
+ min(cursorWaitMs, max(1, ScrollIdleMilliseconds - state.scrollElapsedMs.int))
+ else:
+ cursorWaitMs
+ initDuration(milliseconds = waitMs)
+
when defined(macosx):
proc isCopyShortcut(key: Key, modifiers: set[ModifierKey]): bool =
key == Key.c and hasGuiMod(modifiers)
@@ -304,7 +318,7 @@ proc main() =
updateWindowTitle(window, demo.currentText)
- run window, WindowEventsHandler(
+ window.eventsHandler = WindowEventsHandler(
onResize: proc(e: ResizeEvent) =
demo.ensureImage(e.size.x, e.size.y)
demo.uiScale = e.window.uiScale
@@ -325,6 +339,8 @@ proc main() =
onScroll: proc(e: ScrollEvent) =
demo.scrollSpeedX = e.deltaX.float32 * 60'f32
demo.scrollSpeedY = e.delta.float32 * 60'f32
+ demo.scrollElapsedMs = 0
+ demo.scrollUpdated = true
redraw e.window
,
onMouseMove: proc(e: MouseMoveEvent) =
@@ -388,24 +404,34 @@ proc main() =
,
onTick: proc(e: TickEvent) =
demo.uiScale = e.window.uiScale
- var dtMs = e.deltaTime.inMilliseconds.float32
- if dtMs <= 0:
- dtMs = 16
-
- let decay = max(0'f32, 1'f32 - (dtMs / 1000'f32) * 8'f32)
- demo.scrollSpeedX *= decay
- demo.scrollSpeedY *= decay
- if abs(demo.scrollSpeedX) < 0.05:
- demo.scrollSpeedX = 0
- if abs(demo.scrollSpeedY) < 0.05:
- demo.scrollSpeedY = 0
+ let dtMs = max(0'f32, e.deltaTime.inNanoseconds.float32 / 1_000_000'f32)
+
+ var animationChanged = false
+ if demo.scrollUpdated:
+ demo.scrollUpdated = false
+ elif abs(demo.scrollSpeedX) >= 0.05 or abs(demo.scrollSpeedY) >= 0.05:
+ demo.scrollElapsedMs += dtMs
+ if demo.scrollElapsedMs >= ScrollIdleMilliseconds.float32:
+ demo.scrollSpeedX = 0
+ demo.scrollSpeedY = 0
+ demo.scrollElapsedMs = 0
+ animationChanged = true
demo.cursorElapsedMs += dtMs
- if demo.cursorElapsedMs >= 500:
+ if demo.cursorElapsedMs >= CursorBlinkMilliseconds.float32:
demo.cursorElapsedMs = 0
demo.cursorVisible = not demo.cursorVisible
+ animationChanged = true
- redraw e.window
+ if animationChanged:
+ redraw e.window
)
+ window.firstStep()
+ window.serviceWindow()
+ while window.opened:
+ discard globals.waitEvents(demo.nextAnimationWait())
+ if window.opened:
+ window.serviceWindow()
+
main()
diff --git a/src/siwin/build_utils/tasks.nim b/src/siwin/build_utils/tasks.nim
index bdd8e39..4c64749 100644
--- a/src/siwin/build_utils/tasks.nim
+++ b/src/siwin/build_utils/tasks.nim
@@ -59,7 +59,10 @@ task installTestDeps, "install test dependencies":
exec "nimble install pixie"
-const testTargets = ["t_opengl_es", "t_opengl", "t_swrendering", "t_multiwindow", "t_vulkan", "t_offscreen", "t_macos_live_resize"]
+const testTargets = [
+ "t_opengl_es", "t_opengl", "t_swrendering", "t_multiwindow", "t_vulkan",
+ "t_offscreen", "t_macos_live_resize", "t_event_loop",
+]
proc shouldSkipTarget(target, args: string): bool =
let targetingMacos =
diff --git a/src/siwin/platforms.nim b/src/siwin/platforms.nim
index de2a15e..56fb87e 100644
--- a/src/siwin/platforms.nim
+++ b/src/siwin/platforms.nim
@@ -9,9 +9,9 @@ when not siwin_use_lib:
import ./platforms/wayland/siwinGlobals as waylandGlobals
import ./platforms/x11/siwinGlobals as x11Globals
when defined(windows):
- ##
+ import ./platforms/winapi/window as winapiWindow
when defined(macosx):
- ##
+ import ./platforms/cocoa/window as cocoaWindow
const siwin_use_wayland* {.booldefine: "siwin.wayland".} = defined(linux) or defined(bsd)
const siwin_use_x11* {.booldefine: "siwin.x11".} = defined(linux) or defined(bsd)
@@ -110,10 +110,10 @@ when not siwin_use_lib:
raise SiwinPlatformSupportDefect.newException("Unsupported platform")
elif defined(windows):
- result = SiwinGlobals()
+ result = newWinapiGlobals()
elif defined(macosx):
- result = SiwinGlobals()
+ result = newCocoaGlobals()
else:
{.error.}
diff --git a/src/siwin/platforms/android/window.nim b/src/siwin/platforms/android/window.nim
index 9c15d32..0626849 100644
--- a/src/siwin/platforms/android/window.nim
+++ b/src/siwin/platforms/android/window.nim
@@ -1,7 +1,7 @@
when not (compiles do: import jnim):
{.error: "jnim library not installed, required to cross compile to android\n please run `nimble install jnim`".}
-import std/[strutils, macros, importutils, times, os, locks, deques, tables]
+import std/[strutils, macros, importutils, times, monotimes, os, locks, deques, tables]
import pkg/[jnim, vmath]
import ../../[siwindefs]
import ../any/[window]
@@ -320,12 +320,13 @@ proc newOpenglWindowAndroid*(
method firstStep*(window: WindowAndroid, makeVisible = true) =
if makeVisible:
window.visible = true
-
+
+ window.lastTickTime = getMonoTime()
redraw window
method step*(window: WindowAndroid) =
- let time = getTime()
+ let time = getMonoTime()
window.eventsHandler.onTick.pushEventImpl TickEvent(window: window, deltaTime: time - window.lastTickTime)
window.lastTickTime = time
let timeToSleep =
@@ -346,4 +347,3 @@ method step*(window: WindowAndroid) =
release drawLock
sleep timeToSleep
acquire drawLock
-
diff --git a/src/siwin/platforms/any/eventLoop.nim b/src/siwin/platforms/any/eventLoop.nim
new file mode 100644
index 0000000..1d69be6
--- /dev/null
+++ b/src/siwin/platforms/any/eventLoop.nim
@@ -0,0 +1,21 @@
+import std/times
+
+const nanosecondsPerMillisecond = 1_000_000'i64
+
+func inTimeoutMilliseconds*[T: SomeInteger](
+ timeout: Duration, infinite, maxFinite: T
+): T =
+ ## Converts `timeout` to a native whole-millisecond timeout.
+ ##
+ ## Infinite waits map to `infinite`, nonpositive waits map to zero, positive
+ ## fractional milliseconds round up to avoid early timeouts, and oversized
+ ## finite waits clamp to `maxFinite`.
+ if timeout == Duration.high:
+ return infinite
+ if timeout <= DurationZero:
+ return 0
+ if timeout >= initDuration(milliseconds = maxFinite.int64):
+ return maxFinite
+
+ let nanoseconds = timeout.inNanoseconds
+ result = T(nanoseconds div 1_000_000'i64 + int64(nanoseconds mod 1_000_000'i64 != 0))
diff --git a/src/siwin/platforms/any/window.nim b/src/siwin/platforms/any/window.nim
index 624939f..a781499 100644
--- a/src/siwin/platforms/any/window.nim
+++ b/src/siwin/platforms/any/window.nim
@@ -1,4 +1,4 @@
-import std/[times, options, sequtils, tables]
+import std/[atomics, times, monotimes, options, sequtils, tables]
import pkg/[vmath]
import ../../[siwindefs, colorutils]
import ./[clipboards]
@@ -11,6 +11,32 @@ else:
type
+ EventWaitResult* = enum
+ ## The event loop dispatched native activity, including a wake notification.
+ eventActivity
+ ## A timed wait reached its deadline without native activity.
+ eventTimeout
+
+ EventLoopWakeProc = proc(data: pointer) {.nimcall, gcsafe, raises: [].}
+
+ EventLoopWakeStateObj = object
+ ## Shared by copies of an EventLoopWaker and owns the backend wake resource.
+ owners: Atomic[int]
+ alive: Atomic[bool]
+ pending: Atomic[bool]
+ backendData: pointer
+ wakeProc: EventLoopWakeProc
+ closeProc: EventLoopWakeProc
+
+ EventLoopWakeState = ptr EventLoopWakeStateObj
+
+ EventLoopWaker* = object
+ ## A narrow, copyable capability for waking an event loop from another thread.
+ state: EventLoopWakeState
+
+ EventLoopUnsupportedDefect* = object of Defect
+ ## Raised on platforms whose global event loop is not implemented yet.
+
MouseButton* {.siwin_enum.} = enum
left right middle forward backward
@@ -105,12 +131,14 @@ type
## raised when trying to get pixel buffer from non-softwareRendering window
- SiwinGlobals* = ref object of RootObj
+ SiwinGlobalsObj = object of RootObj
+ eventLoopState: EventLoopWakeState
+
+ SiwinGlobals* = ref SiwinGlobalsObj
Screen* = ref object of RootObj
-
MouseMoveKind* {.siwin_enum.} = enum
move
enter
@@ -301,7 +329,7 @@ type
redrawRequested: bool
- lastTickTime: times.Time
+ lastTickTime: MonoTime
m_closed: bool
@@ -337,6 +365,43 @@ type
borderWidth: Option[tuple[innerWidth, outerWidrth, diagonalSize: float32]]
+proc retainEventLoopWakeState(state: EventLoopWakeState) {.inline.} =
+ if state != nil:
+ discard state.owners.fetchAdd(1, moRelaxed)
+
+proc releaseEventLoopWakeState(state: EventLoopWakeState) {.inline.} =
+ if state != nil and state.owners.fetchSub(1, moAcquireRelease) == 1:
+ if state.closeProc != nil:
+ state.closeProc(state.backendData)
+ deallocShared(state)
+
+proc `=destroy`(waker: EventLoopWaker) {.siwin_destructor.} =
+ releaseEventLoopWakeState(waker.state)
+
+proc `=wasMoved`(waker: var EventLoopWaker) =
+ waker.state = nil
+
+proc `=dup`(waker: EventLoopWaker): EventLoopWaker =
+ retainEventLoopWakeState(waker.state)
+ result.state = waker.state
+
+proc `=copy`(dest: var EventLoopWaker, source: EventLoopWaker) =
+ retainEventLoopWakeState(source.state)
+ `=destroy`(dest)
+ dest.state = source.state
+
+proc `=destroy`(globals: SiwinGlobalsObj) {.siwin_destructor.} =
+ if globals.eventLoopState != nil:
+ globals.eventLoopState.alive.store(false)
+ releaseEventLoopWakeState(globals.eventLoopState)
+
+proc shutdownEventLoopWakeState*(globals: SiwinGlobals) {.raises: [].} =
+ ## Backend destructor hook for globals types that define a custom destructor.
+ if globals != nil and globals.eventLoopState != nil:
+ globals.eventLoopState.alive.store(false)
+ releaseEventLoopWakeState(globals.eventLoopState)
+ globals.eventLoopState = nil
+
method number*(screen: Screen): int32 {.base.} = discard
method width*(screen: Screen): int32 {.base.} = discard
@@ -701,6 +766,102 @@ method step*(window: Window) {.base.} = discard
## make window main loop step
## ! don't forget to call firstStep()
+method serviceWindow*(window: Window) {.base.} =
+ ## Run per-window tick, rendering, and presentation without waiting for input.
+ discard window
+ raise EventLoopUnsupportedDefect.newException(
+ "Nonblocking window service is not implemented on this platform",
+ )
+
+method pollEventsImpl(globals: SiwinGlobals): bool {.base.} =
+ discard globals
+ raise EventLoopUnsupportedDefect.newException(
+ "Global event-loop pumping is not implemented on this platform",
+ )
+
+method waitEventsImpl(
+ globals: SiwinGlobals, timeout: Duration,
+): EventWaitResult {.base.} =
+ discard globals
+ discard timeout
+ raise EventLoopUnsupportedDefect.newException(
+ "Global event-loop waiting is not implemented on this platform",
+ )
+
+proc eventLoopWaker*(globals: SiwinGlobals): EventLoopWaker =
+ ## Returns a thread-safe capability that remains harmless after loop shutdown.
+ if globals.eventLoopState == nil:
+ globals.eventLoopState = cast[EventLoopWakeState](
+ allocShared0(sizeof(EventLoopWakeStateObj))
+ )
+ # The globals object owns the initial reference. EventLoopWaker copies add
+ # further owners through their copy and duplication hooks.
+ globals.eventLoopState.owners.store(1, moRelaxed)
+ globals.eventLoopState.alive.store(true)
+ retainEventLoopWakeState(globals.eventLoopState)
+ result.state = globals.eventLoopState
+
+proc installEventLoopWakeProc*(
+ globals: SiwinGlobals,
+ wakeProc: EventLoopWakeProc,
+ backendData: pointer = nil,
+ closeProc: EventLoopWakeProc = nil,
+) =
+ ## Installs a backend wake primitive and transfers any resource during startup.
+ discard globals.eventLoopWaker()
+ globals.eventLoopState.backendData = backendData
+ globals.eventLoopState.wakeProc = wakeProc
+ globals.eventLoopState.closeProc = closeProc
+
+proc consumeEventLoopWake*(waker: EventLoopWaker): bool =
+ ## Clears this waker's coalesced notification after the backend consumes it.
+ ##
+ ## Platform event pumps call this on the application thread before returning
+ ## control to the application, so work enqueued by a racing producer is either
+ ## observed in the current drain or causes a later wake. Returns `false` when
+ ## `waker` has no state or its owning event loop is no longer alive.
+ if waker.state != nil and waker.state.alive.load():
+ waker.state.pending.store(false)
+ result = true
+
+proc consumeEventLoopWake*(globals: SiwinGlobals) =
+ ## Clears the pending notification for `globals`, if it has a waker.
+ ##
+ ## This is a backend event-pump hook. Applications normally call
+ ## `pollEvents` or `waitEvents`, which consume wake notifications themselves.
+ if globals.eventLoopState != nil:
+ globals.eventLoopState.pending.store(false)
+
+proc wake*(waker: EventLoopWaker) {.gcsafe, raises: [].} =
+ ## Notify the owning event loop after enqueuing application work.
+ if waker.state == nil or not waker.state.alive.load():
+ return
+
+ if not waker.state.pending.exchange(true):
+ # A backend may coalesce repeated notifications while this sentinel is pending.
+ if waker.state.wakeProc != nil:
+ waker.state.wakeProc(waker.state.backendData)
+
+proc wakeEventLoop*(globals: SiwinGlobals) {.gcsafe, raises: [].} =
+ ## Wakes the event loop owned by `globals` from any thread.
+ ##
+ ## Enqueue application work before calling this procedure. The notification
+ ## carries no data and repeated calls may be coalesced. Prefer copying an
+ ## `EventLoopWaker` when a worker should not retain the complete globals owner.
+ globals.eventLoopWaker().wake()
+
+proc pollEvents*(globals: SiwinGlobals): bool =
+ ## Dispatch all immediately available native events on the application thread.
+ globals.pollEventsImpl()
+
+proc waitEvents*(globals: SiwinGlobals) =
+ ## On the application thread, wait for native input or a waker notification.
+ discard globals.waitEventsImpl(high(Duration))
+
+proc waitEvents*(globals: SiwinGlobals, timeout: Duration): EventWaitResult =
+ ## On the application thread, wait up to `timeout` for input or a notification.
+ globals.waitEventsImpl(timeout)
+
proc run*(window: sink Window, makeVisible = true) =
## run whole window main loops
@@ -714,6 +875,51 @@ proc run*(window: sink Window, eventsHandler: WindowEventsHandler, makeVisible =
window.eventsHandler = eventsHandler
run(window, makeVisible)
+proc serviceEventDrivenWindows(
+ globals: SiwinGlobals,
+ windows: sink seq[Window],
+) =
+ proc serviceOpenWindows(windows: var seq[Window]) =
+ var index = 0
+ while index < windows.len:
+ let window = windows[index]
+ if window.closed:
+ windows.delete(index)
+ else:
+ window.serviceWindow()
+ if window.closed:
+ windows.delete(index)
+ else:
+ inc index
+
+ windows.serviceOpenWindows()
+ while windows.len > 0:
+ globals.waitEvents()
+ windows.serviceOpenWindows()
+
+proc runEventDriven*(
+ globals: SiwinGlobals,
+ window: sink Window,
+ makeVisible = true,
+) =
+ ## Run one window with an efficiently blocking application-global event loop.
+ ##
+ ## `globals` must own `window`. Unlike the compatibility `run` loop, ticks
+ ## occur when native activity or an explicit event-loop wake is serviced.
+ window.firstStep(makeVisible)
+ globals.serviceEventDrivenWindows(@[window])
+
+proc runEventDriven*(
+ globals: SiwinGlobals,
+ window: sink Window,
+ eventsHandler: WindowEventsHandler,
+ makeVisible = true,
+) =
+ ## Install `eventsHandler` and run one window with the global event loop.
+ if eventsHandler != WindowEventsHandler():
+ window.eventsHandler = eventsHandler
+ globals.runEventDriven(window, makeVisible)
+
proc runMultiple*(windows: varargs[tuple[window: Window, makeVisible: bool]]) =
## run for multiple windows
for (window, makeVisible) in windows:
@@ -747,3 +953,30 @@ proc runMultiple*(windows: varargs[tuple[window: Window, eventsHandler: WindowEv
continue
window.step()
inc i
+
+proc runMultipleEventDriven*(
+ globals: SiwinGlobals,
+ windows: varargs[tuple[window: Window, makeVisible: bool]],
+) =
+ ## Run multiple windows with one efficiently blocking global event wait.
+ ##
+ ## Every window must be owned by `globals`. Each wake dispatches native
+ ## events once and then services every open window once.
+ for (window, makeVisible) in windows:
+ window.firstStep(makeVisible)
+ globals.serviceEventDrivenWindows(windows.mapIt(it.window))
+
+proc runMultipleEventDriven*(
+ globals: SiwinGlobals,
+ windows: varargs[tuple[
+ window: Window,
+ eventsHandler: WindowEventsHandler,
+ makeVisible: bool,
+ ]],
+) =
+ ## Install handlers and run multiple windows with one global event wait.
+ for (window, eventsHandler, makeVisible) in windows:
+ if eventsHandler != WindowEventsHandler():
+ window.eventsHandler = eventsHandler
+ window.firstStep(makeVisible)
+ globals.serviceEventDrivenWindows(windows.mapIt(it.window))
diff --git a/src/siwin/platforms/cocoa/extras.nim b/src/siwin/platforms/cocoa/extras.nim
index 0f3f47d..0d0ac07 100644
--- a/src/siwin/platforms/cocoa/extras.nim
+++ b/src/siwin/platforms/cocoa/extras.nim
@@ -6,6 +6,25 @@ export app_kit, foundation, runtime
when not declared(activateIgnoringOtherApps):
proc activateIgnoringOtherApps*(self: NSApplication, x: bool) {.objc: "activateIgnoringOtherApps:".}
+when not declared(applicationEventWithType):
+ proc applicationEventWithType*(
+ self: typedesc[NSEvent], eventType: NSEventKind, location: NSPoint,
+ modifierFlags: NSUInteger, timestamp: NSTimeInterval, windowNumber: NSInteger,
+ context: ID, subtype: int16, data1, data2: NSInteger,
+ ): NSEvent {.objc: "otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:".}
+
+when not declared(data1):
+ proc data1*(self: NSEvent): NSInteger {.objc: "data1".}
+
+when not declared(data2):
+ proc data2*(self: NSEvent): NSInteger {.objc: "data2".}
+
+when not declared(subtype):
+ proc subtype*(self: NSEvent): int16 {.objc: "subtype".}
+
+when not declared(distantFuture):
+ proc distantFuture*(t: typedesc[NSDate]): NSDate {.objc.}
+
when not compiles(screens(NSScreen)):
proc screens*(n: typedesc[NSScreen]): NSArray[NSScreen] {.objc: "screens".}
proc registerForDraggedTypes*(self: NSView, types: NSArray[NSString]): NSArray[NSString] {.objc: "registerForDraggedTypes:".}
diff --git a/src/siwin/platforms/cocoa/window.nim b/src/siwin/platforms/cocoa/window.nim
index 413f3d4..c6706ae 100644
--- a/src/siwin/platforms/cocoa/window.nim
+++ b/src/siwin/platforms/cocoa/window.nim
@@ -1,4 +1,4 @@
-import std/[importutils, tables, times, os, unicode, uri, sequtils, strutils, strformat, math]
+import std/[atomics, importutils, tables, times, monotimes, os, unicode, uri, sequtils, strutils, strformat, math]
import pkg/[vmath]
from pkg/darwin/quartz_core/calayer import CALayer
from pkg/darwin/quartz_core/cametal_layer import CAMetalLayer
@@ -11,6 +11,21 @@ import ./[modifierstate, extras]
{.passL: "-framework Cocoa".}
privateAccess Window
+privateAccess SiwinGlobals
+privateAccess EventLoopWaker
+
+type DispatchQueue = pointer
+
+proc dispatchTime(baseTime: uint64, delta: int64): uint64
+ {.importc: "dispatch_time", header: "".}
+proc dispatchGlobalQueue(identifier: int, flags: uint): DispatchQueue
+ {.importc: "dispatch_get_global_queue", header: "".}
+proc dispatchAfter(
+ deadline: uint64,
+ queue: DispatchQueue,
+ context: pointer,
+ callback: proc(context: pointer) {.cdecl.},
+) {.importc: "dispatch_after_f", header: "".}
template autoreleasepool(body: untyped) =
let pool = NSAutoreleasePool.alloc().init()
@@ -57,6 +72,9 @@ proc setBlendingMode(view: NSVisualEffectView, mode: NSVisualEffectBlendingMode)
proc setState(view: NSVisualEffectView, state: NSVisualEffectState) {.objc: "setState:".}
type
+ SiwinGlobalsCocoa* = ref SiwinGlobalsCocoaObj
+ SiwinGlobalsCocoaObj* = object of SiwinGlobals
+
ScreenCocoa* = ref object of Screen
id: int32
handle: NSScreen
@@ -69,7 +87,7 @@ type
trackingArea: NSTrackingArea
updatingTrackingAreas: bool
markedText: NSString
- lastClickTime: array[MouseButton, Time]
+ lastClickTime: array[MouseButton, MonoTime]
lastDragStatus: DragStatus
m_canBecomeKeyWindow: bool
m_canBecomeMainWindow: bool
@@ -90,13 +108,124 @@ type
ClipboardCocoaDnd* = ref object of Clipboard
activePasteboard: NSPasteboard
+var cocoaWakeWakers: seq[EventLoopWaker]
+
+proc `=destroy`(globals: SiwinGlobalsCocoaObj) {.siwin_destructor.} =
+ let wakeState = globals.eventLoopState
+ cast[SiwinGlobals](globals.addr).shutdownEventLoopWakeState()
+
+ var index = 0
+ while index < cocoaWakeWakers.len:
+ if cocoaWakeWakers[index].state == wakeState:
+ cocoaWakeWakers.delete(index)
+ else:
+ inc index
var
initialized: bool
appDelegateClass, windowClass, softwareViewClass, openglViewClass, metalViewClass: ObjcClass
windows: seq[WindowCocoa]
+ legacyCocoaGlobals: SiwinGlobalsCocoa
+ cocoaTimeoutGeneration: Atomic[int64]
proc init
+const
+ wakeEventSubtype = 0x5349'i16 # "SI"
+ wakeEventMarker = 0x534957494E57414B'i64 # "SIWINWAK"
+ timeoutEventMarker = 0x534957494E54494D'i64 # "SIWINTIM"
+
+type CocoaTimeoutRequest = object
+ generation: int64
+
+proc isWakeEvent(event: NSEvent): bool =
+ event.kind == NSApplicationDefined and
+ event.subtype == wakeEventSubtype and
+ event.data1 == wakeEventMarker.NSInteger
+
+proc isTimeoutEvent(event: NSEvent): bool =
+ event.kind == NSApplicationDefined and
+ event.subtype == wakeEventSubtype and
+ event.data1 == timeoutEventMarker.NSInteger
+
+proc postWakeEvent(_: pointer) {.gcsafe, raises: [].} =
+ if not initialized or NSApp == nil:
+ return
+
+ try:
+ autoreleasepool:
+ let event = NSEvent.applicationEventWithType(
+ NSApplicationDefined,
+ NSMakePoint(0, 0),
+ 0,
+ 0,
+ 0,
+ nil,
+ wakeEventSubtype,
+ wakeEventMarker.NSInteger,
+ 0,
+ )
+ if event != nil:
+ NSApp.postEvent(event, false)
+ except:
+ discard
+
+proc postTimeoutEvent(data: pointer) {.cdecl, gcsafe, raises: [].} =
+ let request = cast[ptr CocoaTimeoutRequest](data)
+ let generation = request.generation
+ deallocShared(request)
+
+ if not initialized or NSApp == nil:
+ return
+
+ try:
+ autoreleasepool:
+ let event = NSEvent.applicationEventWithType(
+ NSApplicationDefined,
+ NSMakePoint(0, 0),
+ 0,
+ 0,
+ 0,
+ nil,
+ wakeEventSubtype,
+ timeoutEventMarker.NSInteger,
+ generation.NSInteger,
+ )
+ if event != nil:
+ NSApp.postEvent(event, false)
+ except:
+ discard
+
+proc scheduleCocoaTimeout(timeout: Duration): int64 =
+ result = cocoaTimeoutGeneration.fetchAdd(1) + 1
+ let request = cast[ptr CocoaTimeoutRequest](
+ allocShared0(sizeof(CocoaTimeoutRequest))
+ )
+ request.generation = result
+ dispatchAfter(
+ dispatchTime(0, timeout.inNanoseconds),
+ dispatchGlobalQueue(0, 0),
+ request,
+ postTimeoutEvent,
+ )
+
+proc newCocoaGlobals*(): SiwinGlobalsCocoa =
+ init()
+ result = SiwinGlobalsCocoa()
+ result.installEventLoopWakeProc(postWakeEvent)
+ cocoaWakeWakers.add(result.eventLoopWaker())
+
+proc legacyGlobals(): SiwinGlobalsCocoa =
+ if legacyCocoaGlobals == nil:
+ legacyCocoaGlobals = newCocoaGlobals()
+ legacyCocoaGlobals
+
+proc consumeCocoaWake() =
+ var activeWakers = newSeqOfCap[EventLoopWaker](cocoaWakeWakers.len)
+ for waker in cocoaWakeWakers:
+ if waker.consumeEventLoopWake():
+ activeWakers.add(waker)
+ cocoaWakeWakers = move(activeWakers)
+
proc currentStyleMask(window: WindowCocoa): NSWindowStyleMask =
result =
if window.m_frameless:
@@ -1454,7 +1583,7 @@ proc init =
window.mouse.pressed.incl button
window.clicking.incl button
else:
- let nows = getTime()
+ let nows = getMonoTime()
window.mouse.pressed.excl button
if button in window.clicking:
@@ -1920,6 +2049,68 @@ proc init =
NSApp.finishLaunching()
+proc sendCocoaEvent(event: NSEvent): bool =
+ if event.isWakeEvent():
+ consumeCocoaWake()
+ elif event.isTimeoutEvent():
+ return false
+ else:
+ NSApp.sendEvent(event)
+ true
+
+proc drainCocoaEvents(globals: SiwinGlobalsCocoa, mode: NSRunLoopMode): bool =
+ while true:
+ let event = NSApp.nextEventMatchingMask(
+ NSEventMaskAny,
+ NSDate.distantPast,
+ mode,
+ true,
+ )
+ if event == nil:
+ return
+ result = sendCocoaEvent(event) or result
+
+proc drainCocoaEvents(globals: SiwinGlobalsCocoa): bool =
+ let trackingMode = cast[NSRunLoopMode](@"NSEventTrackingRunLoopMode")
+ let modalMode = cast[NSRunLoopMode](@"NSModalPanelRunLoopMode")
+ result = globals.drainCocoaEvents(NSDefaultRunLoopMode)
+ result = globals.drainCocoaEvents(trackingMode) or result
+ result = globals.drainCocoaEvents(modalMode) or result
+
+method pollEventsImpl(globals: SiwinGlobalsCocoa): bool =
+ autoreleasepool:
+ result = globals.drainCocoaEvents()
+
+method waitEventsImpl(
+ globals: SiwinGlobalsCocoa, timeout: Duration,
+): EventWaitResult =
+ autoreleasepool:
+ if globals.drainCocoaEvents():
+ return eventActivity
+
+ if timeout != Duration.high and timeout.inNanoseconds <= 0:
+ return eventTimeout
+
+ let timeoutGeneration =
+ if timeout == Duration.high: 0'i64
+ else: scheduleCocoaTimeout(timeout)
+
+ while true:
+ let event = NSApp.nextEventMatchingMask(
+ NSEventMaskAny,
+ NSDate.distantFuture,
+ NSDefaultRunLoopMode,
+ true,
+ )
+ if event == nil:
+ continue
+ if event.isTimeoutEvent() and
+ event.data2.int64 == timeoutGeneration:
+ return eventTimeout
+ if sendCocoaEvent(event):
+ discard globals.drainCocoaEvents()
+ return eventActivity
+
method firstStep*(window: WindowCocoa, makeVisible = true) =
if makeVisible:
window.visible = true
@@ -1935,7 +2126,10 @@ method firstStep*(window: WindowCocoa, makeVisible = true) =
)
if event == nil:
break
- NSApp.sendEvent(event)
+ if event.isWakeEvent():
+ consumeCocoaWake()
+ else:
+ NSApp.sendEvent(event)
window.syncPosFromHandle()
# Ensure all visible windows are present in the initial z-order. Without
# this, some macOS setups only show the most recently shown window until
@@ -1945,40 +2139,18 @@ method firstStep*(window: WindowCocoa, makeVisible = true) =
w.handle.orderFront(cast[ID](nil))
if window.canBecomeKeyWindow:
window.handle.makeKeyAndOrderFront(cast[ID](nil))
+ window.lastTickTime = getMonoTime()
-method step*(window: WindowCocoa) =
- proc pumpEvents(mode: NSRunLoopMode, firstUntilDate: NSDate): bool =
- var first = true
- while true:
- let event = NSApp.nextEventMatchingMask(
- NSEventMaskAny,
- (if first: firstUntilDate else: NSDate.distantPast),
- mode,
- true
- )
- if event == nil:
- break
- first = false
- result = true
- NSApp.sendEvent(event)
-
- let
- defaultMode = NSDefaultRunLoopMode
- trackingMode = cast[NSRunLoopMode](@"NSEventTrackingRunLoopMode")
- modalMode = cast[NSRunLoopMode](@"NSModalPanelRunLoopMode")
-
- autoreleasepool:
- # Wait briefly for regular events, then drain all immediate events including
- # tracking/live-resize and modal-panel modes.
- discard pumpEvents(defaultMode, NSDate.withTimeIntervalSinceNow(0.001))
- discard pumpEvents(defaultMode, NSDate.distantPast)
- discard pumpEvents(trackingMode, NSDate.distantPast)
- discard pumpEvents(modalMode, NSDate.distantPast)
-
+method serviceWindow*(window: WindowCocoa) =
window.refreshModifiers()
- window.eventsHandler.pushEvent onTick, TickEvent(window: window) # todo: lastTickTime
+ let now = getMonoTime()
+ window.eventsHandler.pushEvent onTick, TickEvent(
+ window: window,
+ deltaTime: now - window.lastTickTime,
+ )
+ window.lastTickTime = now
if window.redrawRequested:
window.redrawRequested = false
@@ -1991,6 +2163,12 @@ method step*(window: WindowCocoa) =
elif window of WindowCocoaOpengl:
window.WindowCocoaOpengl.swapBuffers()
+method step*(window: WindowCocoa) =
+ ## Compatibility path: retain the old short global wait followed by one window tick.
+ let globals = legacyGlobals()
+ discard globals.waitEvents(initDuration(milliseconds = 1))
+ window.serviceWindow()
+
method pixelBuffer*(window: WindowCocoaSoftwareRendering): PixelBuffer =
if window.softwareRep == nil:
window.resizeSoftwarePixelBuffer(window.m_size)
diff --git a/src/siwin/platforms/wayland/libdecor.nim b/src/siwin/platforms/wayland/libdecor.nim
index 5a57b9e..893136e 100644
--- a/src/siwin/platforms/wayland/libdecor.nim
+++ b/src/siwin/platforms/wayland/libdecor.nim
@@ -59,6 +59,7 @@ if libdecorHandle == nil:
siwin_loadDynlibIfExists libdecorHandle:
proc libdecor_new*(display: pointer, iface: ptr LibdecorInterface): LibdecorContext
proc libdecor_unref*(context: LibdecorContext)
+ proc libdecor_get_fd*(context: LibdecorContext): cint
proc libdecor_dispatch*(context: LibdecorContext, timeout: cint): cint
proc libdecor_decorate*(context: LibdecorContext, surface: pointer,
diff --git a/src/siwin/platforms/wayland/libwayland.nim b/src/siwin/platforms/wayland/libwayland.nim
index 7c52b66..ab2f6ed 100644
--- a/src/siwin/platforms/wayland/libwayland.nim
+++ b/src/siwin/platforms/wayland/libwayland.nim
@@ -82,6 +82,9 @@ let
var
libwaylandclientHandle = loadLib("libwayland-client.so")
+if libwaylandclientHandle == nil:
+ libwaylandclientHandle = loadLib("libwayland-client.so.0")
+
siwin_loadDynlibIfExists libwaylandclientHandle:
proc wl_display_disconnect*(this: Wl_display)
@@ -91,7 +94,10 @@ siwin_loadDynlibIfExists libwaylandclientHandle:
proc wl_display_get_fd*(this: Wl_display): FileHandle
- proc wl_display_flush*(this: Wl_display)
+ proc wl_display_flush*(this: Wl_display): int32
+ proc wl_display_prepare_read*(this: Wl_display): int32
+ proc wl_display_read_events*(this: Wl_display): int32
+ proc wl_display_cancel_read*(this: Wl_display)
proc wl_display_roundtrip*(this: Wl_display): int32
@@ -145,12 +151,16 @@ proc destroy*(this: Wl_proxy) =
# this.raw = v.raw
-proc dispatch*(this: Wl_display): int32 =
+proc dispatchPending*(this: Wl_display): int32 =
proc impl(this: Wl_display): int32 {.importc: "wl_display_dispatch_pending", importwayland.}
result = impl(this)
if result == -1:
raise WaylandProtocolError.newException("failed to dispatch events")
+proc dispatch*(this: Wl_display): int32 {.deprecated: "Use dispatchPending".} =
+ ## Compatibility alias for the original public wrapper name.
+ this.dispatchPending()
+
proc newWlMessage*(name: cstring, signature: cstring, types: openarray[ptr Wl_interface]): WlMessage =
result.name = name
result.signature = signature
diff --git a/src/siwin/platforms/wayland/sharedBuffer.nim b/src/siwin/platforms/wayland/sharedBuffer.nim
index f45e16a..7744a12 100644
--- a/src/siwin/platforms/wayland/sharedBuffer.nim
+++ b/src/siwin/platforms/wayland/sharedBuffer.nim
@@ -1,4 +1,4 @@
-import std/[memfiles, os, oserrors, times, sequtils]
+import std/[memfiles, os, oserrors, times, monotimes, sequtils]
import std/posix
import pkg/[vmath]
import ../../[siwindefs]
@@ -92,10 +92,10 @@ proc swapBuffers*(
# if unlocked buffer was not found, wait up to timeout while trying again
if stable.buffers[stable.currentBuffer].locked:
- let deadline = now() + timeout
+ let deadline = getMonoTime() + timeout
block waiting_for_unlocked_buffer:
- while now() < deadline:
+ while getMonoTime() < deadline:
if stable.buffers.len == 0 or stable.buffers.allIt(it.buffer.proxy.raw == nil):
return
@@ -213,8 +213,8 @@ proc resize*(buffer: var SharedBuffer, size: IVec2, timeout: Duration = initDura
buffer.size = size
let newSizeInBytes = size.x * size.y * buffer.bytesPerPixel * buffer.buffers.len.int32
- let deadline = now() + timeout
- while now() < deadline and buffer.buffers.anyIt(it.locked):
+ let deadline = getMonoTime() + timeout
+ while getMonoTime() < deadline and buffer.buffers.anyIt(it.locked):
discard wl_display_roundtrip buffer.globals.display
if newSizeInBytes > buffer.file.size:
diff --git a/src/siwin/platforms/wayland/siwinGlobals.nim b/src/siwin/platforms/wayland/siwinGlobals.nim
index 5f895ab..d5eb6f6 100644
--- a/src/siwin/platforms/wayland/siwinGlobals.nim
+++ b/src/siwin/platforms/wayland/siwinGlobals.nim
@@ -1,6 +1,6 @@
-import std/[tables, os, posix]
+import std/[tables, os, posix, times, monotimes]
import ../../[siwindefs]
-import ../any/[window, clipboards]
+import ../any/[window, clipboards, eventLoop]
import ./[libwayland, protocol, bitfields, libdecor]
type
@@ -10,6 +10,9 @@ type
registryName*: uint32
output*: Wl_output
+ WaylandWakeFd = object
+ readFd, writeFd: cint
+
SiwinGlobalsWayland* = ref SiwinGlobalsWaylandObj
SiwinGlobalsWaylandObj* = object of SiwinGlobals
seatEventsInitialized*: bool
@@ -80,9 +83,12 @@ type
libdecorCtx*: LibdecorContext
libdecorIface*: LibdecorInterface
-
+ wake: ptr WaylandWakeFd
+ repeatWakeDeadline*: MonoTime
+ repeatWakeWindow*: Window
proc `=destroy`*(globals: SiwinGlobalsWaylandObj) {.siwin_destructor.} =
+ cast[SiwinGlobals](globals.addr).shutdownEventLoopWakeState()
try:
if globals.libdecorCtx != nil and libdecor_unref != nil:
libdecor_unref(globals.libdecorCtx)
@@ -90,6 +96,72 @@ proc `=destroy`*(globals: SiwinGlobalsWaylandObj) {.siwin_destructor.} =
except: discard
+proc signalWaylandWake(data: pointer) {.gcsafe, raises: [].} =
+ let wake = cast[ptr WaylandWakeFd](data)
+ if wake != nil and wake.writeFd >= 0:
+ var byte = '\x01'
+ {.cast(gcsafe).}:
+ while write(wake.writeFd, byte.addr, 1) < 0 and errno == EINTR:
+ discard
+
+proc closeWaylandWake(data: pointer) {.gcsafe, raises: [].} =
+ let wake = cast[ptr WaylandWakeFd](data)
+ if wake != nil:
+ if wake.readFd >= 0:
+ discard close(wake.readFd)
+ if wake.writeFd >= 0:
+ discard close(wake.writeFd)
+ dealloc(wake)
+
+proc configureWakeFd(fd: cint) =
+ let flags = fcntl(fd, F_GETFL)
+ if flags < 0 or fcntl(fd, F_SETFL, flags or O_NONBLOCK) < 0:
+ raiseOSError(osLastError())
+ let fdFlags = fcntl(fd, F_GETFD)
+ if fdFlags < 0 or fcntl(fd, F_SETFD, fdFlags or FD_CLOEXEC) < 0:
+ raiseOSError(osLastError())
+
+proc drainWaylandWake(globals: SiwinGlobalsWayland): bool =
+ let wake = globals.wake
+ if wake == nil:
+ return false
+ var buffer: array[64, char]
+ while true:
+ let count = read(wake.readFd, buffer[0].addr, buffer.len)
+ if count > 0:
+ result = true
+ elif count < 0 and errno == EINTR:
+ continue
+ else:
+ break
+ if result:
+ globals.consumeEventLoopWake()
+
+proc waitTimeout(globals: SiwinGlobalsWayland, timeout: Duration): Duration =
+ result = timeout
+ if globals.repeatWakeDeadline != MonoTime.default:
+ let remaining = globals.repeatWakeDeadline - getMonoTime()
+ if remaining <= initDuration():
+ return initDuration()
+ if result == Duration.high or remaining < result:
+ result = remaining
+
+proc repeatWakeIsDue(globals: SiwinGlobalsWayland): bool =
+ globals.repeatWakeDeadline != MonoTime.default and
+ globals.repeatWakeDeadline <= getMonoTime()
+
+proc libdecorFd(globals: SiwinGlobalsWayland): cint =
+ if globals.libdecorCtx != nil and libdecor_get_fd != nil:
+ result = libdecor_get_fd(globals.libdecorCtx)
+ else:
+ result = -1
+
+proc dispatchLibdecor(globals: SiwinGlobalsWayland) =
+ if globals.libdecorCtx != nil and libdecor_dispatch != nil and
+ libdecor_dispatch(globals.libdecorCtx, 0) < 0:
+ raise WaylandProtocolError.newException("failed to dispatch libdecor events")
+
+
proc initRegistryCallbacks(globals: SiwinGlobalsWayland) =
template addRegistry(target: type, body) =
globals.registryCallbacks[ifaceName(target)] = proc(registry {.inject.}: Wl_registry, name {.inject.}: uint32, version {.inject.}: uint32) =
@@ -202,6 +274,22 @@ proc newWaylandGlobals*(): SiwinGlobalsWayland =
if result.display == nil:
raise OSError.newException("Wayland is not available")
+ result.wake = cast[ptr WaylandWakeFd](alloc0(sizeof(WaylandWakeFd)))
+ var wakeFds: array[2, cint]
+ if pipe(wakeFds) != 0:
+ dealloc(result.wake)
+ raiseOSError(osLastError())
+ result.wake.readFd = wakeFds[0]
+ result.wake.writeFd = wakeFds[1]
+ try:
+ configureWakeFd(result.wake.readFd)
+ configureWakeFd(result.wake.writeFd)
+ except:
+ closeWaylandWake(result.wake)
+ result.wake = nil
+ raise
+ result.installEventLoopWakeProc(signalWaylandWake, result.wake, closeWaylandWake)
+
result.interfaces.initInterfaces()
result.registry = result.display.get_registry(result.interfaces.addr)
@@ -226,6 +314,176 @@ proc newWaylandGlobals*(): SiwinGlobalsWayland =
globals.outputs.delete(idx)
+method pollEventsImpl(globals: SiwinGlobalsWayland): bool =
+ result = globals.drainWaylandWake()
+ if globals.display.dispatchPending() > 0:
+ result = true
+
+ while wl_display_prepare_read(globals.display) != 0:
+ if globals.display.dispatchPending() > 0:
+ result = true
+
+ let
+ displayFd = globals.display.wl_display_get_fd().cint
+ decorationFd = globals.libdecorFd()
+ var fds = [
+ TPollfd(fd: displayFd, events: POLLIN),
+ TPollfd(fd: globals.wake.readFd, events: POLLIN),
+ TPollfd(
+ fd: (if decorationFd != displayFd: decorationFd else: -1),
+ events: POLLIN,
+ ),
+ ]
+ let flushResult = wl_display_flush(globals.display)
+ if flushResult < 0:
+ if errno == EAGAIN:
+ fds[0].events = fds[0].events or POLLOUT
+ else:
+ wl_display_cancel_read(globals.display)
+ raise WaylandProtocolError.newException("failed to flush Wayland requests")
+
+ let count = poll(fds[0].addr, fds.len.Tnfds, 0)
+ if count < 0:
+ wl_display_cancel_read(globals.display)
+ if errno == EINTR:
+ return
+ raiseOSError(osLastError())
+ if count == 0:
+ wl_display_cancel_read(globals.display)
+ return result or globals.repeatWakeIsDue()
+
+ if (fds[0].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ wl_display_cancel_read(globals.display)
+ raise OSError.newException("Wayland display connection closed while polling")
+ if (fds[1].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ wl_display_cancel_read(globals.display)
+ raise OSError.newException("Wayland event-loop wake pipe closed while polling")
+ if (fds[2].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ wl_display_cancel_read(globals.display)
+ raise OSError.newException("libdecor connection closed while polling")
+
+ let
+ displayReadable = (fds[0].revents and POLLIN) != 0
+ decorationReadable = (fds[2].revents and POLLIN) != 0
+ if displayReadable:
+ if wl_display_read_events(globals.display) < 0:
+ raise WaylandProtocolError.newException("failed to read Wayland events")
+ result = true
+ else:
+ wl_display_cancel_read(globals.display)
+
+ if (fds[1].revents and POLLIN) != 0:
+ result = globals.drainWaylandWake() or result
+ if (fds[0].revents and POLLOUT) != 0:
+ let retryFlushResult = wl_display_flush(globals.display)
+ if retryFlushResult < 0 and errno != EAGAIN:
+ raise WaylandProtocolError.newException("failed to flush Wayland requests")
+
+ if displayReadable:
+ result = globals.display.dispatchPending() > 0 or result
+ if displayReadable or decorationReadable:
+ globals.dispatchLibdecor()
+ result = true
+ result = globals.repeatWakeIsDue() or result
+
+method waitEventsImpl(
+ globals: SiwinGlobalsWayland,
+ timeout: Duration,
+): EventWaitResult =
+ if globals.pollEventsImpl():
+ return eventActivity
+
+ let started = getMonoTime()
+ while true:
+ while wl_display_prepare_read(globals.display) != 0:
+ if globals.display.dispatchPending() > 0:
+ return eventActivity
+
+ let
+ displayFd = globals.display.wl_display_get_fd().cint
+ decorationFd = globals.libdecorFd()
+ var fds = [
+ TPollfd(fd: displayFd, events: POLLIN),
+ TPollfd(fd: globals.wake.readFd, events: POLLIN),
+ TPollfd(
+ fd: (if decorationFd != displayFd: decorationFd else: -1),
+ events: POLLIN,
+ ),
+ ]
+ let flushResult = wl_display_flush(globals.display)
+ if flushResult < 0:
+ if errno == EAGAIN:
+ fds[0].events = fds[0].events or POLLOUT
+ else:
+ wl_display_cancel_read(globals.display)
+ raise WaylandProtocolError.newException("failed to flush Wayland requests")
+
+ let callerRemaining =
+ if timeout == Duration.high:
+ Duration.high
+ else:
+ max(initDuration(), timeout - (getMonoTime() - started))
+ let count = poll(
+ fds[0].addr,
+ fds.len.Tnfds,
+ globals.waitTimeout(callerRemaining).inTimeoutMilliseconds(
+ infinite = -1.cint,
+ maxFinite = cint.high,
+ ),
+ )
+ if count == 0:
+ wl_display_cancel_read(globals.display)
+ if globals.repeatWakeIsDue():
+ return eventActivity
+ if timeout != Duration.high and getMonoTime() - started >= timeout:
+ return eventTimeout
+ continue
+ if count < 0:
+ wl_display_cancel_read(globals.display)
+ if errno == EINTR:
+ if timeout != Duration.high and getMonoTime() - started >= timeout:
+ return eventTimeout
+ continue
+ raiseOSError(osLastError())
+
+ if (fds[0].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ wl_display_cancel_read(globals.display)
+ raise OSError.newException("Wayland display connection closed while waiting")
+ if (fds[1].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ wl_display_cancel_read(globals.display)
+ raise OSError.newException("Wayland event-loop wake pipe closed while waiting")
+ if (fds[2].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ wl_display_cancel_read(globals.display)
+ raise OSError.newException("libdecor connection closed while waiting")
+
+ let
+ displayReadable = (fds[0].revents and POLLIN) != 0
+ decorationReadable = (fds[2].revents and POLLIN) != 0
+ wakeReadable = (fds[1].revents and POLLIN) != 0
+ if displayReadable:
+ if wl_display_read_events(globals.display) < 0:
+ raise WaylandProtocolError.newException("failed to read Wayland events")
+ else:
+ wl_display_cancel_read(globals.display)
+
+ if wakeReadable:
+ discard globals.drainWaylandWake()
+ if (fds[0].revents and POLLOUT) != 0:
+ let retryFlushResult = wl_display_flush(globals.display)
+ if retryFlushResult < 0 and errno != EAGAIN:
+ raise WaylandProtocolError.newException("failed to flush Wayland requests")
+
+ if displayReadable:
+ discard globals.display.dispatchPending()
+ if displayReadable or decorationReadable:
+ globals.dispatchLibdecor()
+
+ if displayReadable or decorationReadable or wakeReadable:
+ return eventActivity
+ # POLLOUT only means queued protocol output can proceed; keep waiting for
+ # application-visible activity without turning writability into a busy loop.
+
+
proc roundtrip*(globals: SiwinGlobalsWayland) =
discard wl_display_roundtrip globals.display
diff --git a/src/siwin/platforms/wayland/window.nim b/src/siwin/platforms/wayland/window.nim
index 23cbb53..abd507b 100644
--- a/src/siwin/platforms/wayland/window.nim
+++ b/src/siwin/platforms/wayland/window.nim
@@ -1,4 +1,4 @@
-import std/[times, importutils, strformat, options, tables, os, uri, sequtils, strutils, math]
+import std/[times, monotimes, importutils, strformat, options, tables, os, uri, sequtils, strutils, math]
from std/posix import pipe, close, write, read
import pkg/[vmath]
import ../../[colorutils, siwindefs]
@@ -99,8 +99,8 @@ type
lastPressedRawKeycode: uint32
lastPressedRawKeyDown: bool
lastTextEntered: string
- lastPressedKeyTime: Time
- lastKeyRepeatedTime: Time
+ lastPressedKeyTime: MonoTime
+ lastKeyRepeatedTime: MonoTime
initialConfigureReceived: bool
bufferScaleFactor: int32
fractionalScaleFactor: float32
@@ -363,6 +363,10 @@ method release(window: WindowWayland) {.base, raises: [].} =
return
window.releasing = true
+ if window.globals != nil and window.globals.repeatWakeWindow == window:
+ window.globals.repeatWakeDeadline = MonoTime.default
+ window.globals.repeatWakeWindow = nil
+
if window.surface != nil:
if window.globals.associatedWindows_queueRemove_insteadOf_removingInstantly:
window.globals.associatedWindows_removeQueue.add window.surface.proxy.raw.id
@@ -1379,7 +1383,7 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) =
)
window.lastPressedKey = siwinKey
window.lastTextEntered = ""
- window.lastPressedKeyTime = getTime()
+ window.lastPressedKeyTime = getMonoTime()
globals.seat_keyboard.onLeave:
@@ -1400,7 +1404,7 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) =
if pressed:
window.lastPressedRawKeycode = key
window.lastPressedRawKeyDown = true
- window.lastPressedKeyTime = getTime()
+ window.lastPressedKeyTime = getMonoTime()
window.lastTextEntered = ""
elif key == window.lastPressedRawKeycode:
window.lastPressedRawKeyDown = false
@@ -2110,7 +2114,7 @@ method content*(
clipboard.globals.current_dnd_data_offer.receive(mimeType.cstring, fds[1])
discard close fds[1]
- wl_display_flush clipboard.globals.display
+ discard wl_display_flush clipboard.globals.display
var data: string
var cbuffer: array[1024, char]
@@ -2204,13 +2208,12 @@ method firstStep*(window: WindowWayland, makeVisible = true) =
window.configureSurface()
if window.opened: window.eventsHandler.onResize.pushEvent ResizeEvent(window: window, size: window.size, initial: true)
- window.lastTickTime = getTime()
+ window.lastTickTime = getMonoTime()
redraw window
-method step*(window: WindowWayland) =
- ## make window main loop step
- ## ! don't forget to call firstStep()
+method serviceWindow*(window: WindowWayland) =
+ ## Run one nonblocking per-window tick and presentation pass.
template closeIfNeeded =
if window.m_closed:
@@ -2220,31 +2223,20 @@ method step*(window: WindowWayland) =
closeIfNeeded()
- if window.globals.libdecorCtx != nil:
- discard libdecor_dispatch(window.globals.libdecorCtx, 0)
-
- let eventCount = wl_display_roundtrip(window.globals.display)
- if eventCount < 0:
- raise newException(RoundtripFailed, "wl_display_roundtrip() returned " & $eventCount)
-
- closeIfNeeded()
- if eventCount <= 2: # seems like idle event count is 2
- sleep(1)
-
# repeat keys if needed
if (
window.globals.seat_keyboard_repeatSettings.rate > 0 and
window.lastPressedRawKeyDown
):
let repeatStartTime = window.lastPressedKeyTime + initDuration(milliseconds = window.globals.seat_keyboard_repeatSettings.delay)
- let nows = getTime()
+ let nows = getMonoTime()
let interval = initDuration(milliseconds = max(1'i64, (1000 div window.globals.seat_keyboard_repeatSettings.rate).int64))
if repeatStartTime <= nows and window.lastKeyRepeatedTime < repeatStartTime - interval:
window.lastKeyRepeatedTime = repeatStartTime - interval
while repeatStartTime <= nows and window.lastKeyRepeatedTime + interval <= nows:
- window.lastKeyRepeatedTime += interval
+ window.lastKeyRepeatedTime = window.lastKeyRepeatedTime + interval
let repeatedKey = waylandKeyToKey(window.lastPressedRawKeycode)
var repeatedText = waylandKeyToString(window.lastPressedRawKeycode)
@@ -2271,7 +2263,15 @@ method step*(window: WindowWayland) =
window: window, text: repeatedText, repeated: true
)
- let nows = getTime()
+ window.globals.repeatWakeDeadline =
+ if nows < repeatStartTime: repeatStartTime
+ else: window.lastKeyRepeatedTime + interval
+ window.globals.repeatWakeWindow = window
+ elif window.globals.repeatWakeWindow == window:
+ window.globals.repeatWakeDeadline = MonoTime.default
+ window.globals.repeatWakeWindow = nil
+
+ let nows = getMonoTime()
if window.opened: window.eventsHandler.onTick.pushEvent TickEvent(window: window, deltaTime: nows - window.lastTickTime)
closeIfNeeded()
window.lastTickTime = nows
@@ -2285,7 +2285,13 @@ method step*(window: WindowWayland) =
window.swapBuffers()
- wl_display_flush window.globals.display
+ discard wl_display_flush window.globals.display
+
+
+method step*(window: WindowWayland) =
+ let globals = window.globals
+ discard globals.waitEvents(initDuration(milliseconds = 1))
+ window.serviceWindow()
proc newSoftwareRenderingWindowWayland*(
diff --git a/src/siwin/platforms/winapi/winapi.nim b/src/siwin/platforms/winapi/winapi.nim
index 4eff303..a9142a2 100644
--- a/src/siwin/platforms/winapi/winapi.nim
+++ b/src/siwin/platforms/winapi/winapi.nim
@@ -2,8 +2,8 @@ when not (compiles do: import winim/inc/windef):
{.error: "winim library not installed, required to cross compile to windows\n please run `nimble install winim`".}
import ../../[siwindefs]
-import winim/inc/[windef, winbase, wingdi, winuser, dwmapi], winim/winstr
-export windef, winbase, wingdi, winuser, winstr, dwmapi
+import winim/inc/[windef, winbase, winerror, wingdi, winuser, dwmapi], winim/winstr
+export windef, winbase, winerror, wingdi, winuser, winstr, dwmapi
proc rtlGetVersionSiwin*(versionInfo: ptr OSVERSIONINFOW): LONG {.
stdcall, dynlib: "ntdll", importc: "RtlGetVersion"
diff --git a/src/siwin/platforms/winapi/window.nim b/src/siwin/platforms/winapi/window.nim
index c26af9a..101358d 100644
--- a/src/siwin/platforms/winapi/window.nim
+++ b/src/siwin/platforms/winapi/window.nim
@@ -1,8 +1,8 @@
-import std/[times, os, options, importutils, sequtils]
+import std/[times, monotimes, os, options, importutils, sequtils]
import pkg/[vmath]
import ./[winapi]
import ../../[colorutils, siwindefs]
-import ../any/[window, clipboards]
+import ../any/[window, clipboards, eventLoop]
import ../any/[windowUtils]
privateAccess Window
@@ -10,6 +10,9 @@ privateAccess Window
{.experimental: "overloadableEnums".}
type
+ SiwinGlobalsWinapi* = ref object of SiwinGlobals
+ wakeEvent: Handle
+
ScreenWinapi* = ref object of Screen
Buffer = object
@@ -24,6 +27,7 @@ type
WindowWinapi* = ref WindowWinapiObj
WindowWinapiObj* = object of Window
+ globals: SiwinGlobalsWinapi
handle: HWnd
wicon: HIcon
hdc: Hdc
@@ -34,6 +38,38 @@ type
buffer: Buffer
+var legacyGlobals: SiwinGlobalsWinapi
+
+proc signalWinapiWake(data: pointer) {.gcsafe, raises: [].} =
+ let wakeEvent = cast[Handle](data)
+ if wakeEvent != 0:
+ {.cast(gcsafe).}:
+ discard SetEvent(wakeEvent)
+
+proc closeWinapiWake(data: pointer) {.gcsafe, raises: [].} =
+ let wakeEvent = cast[Handle](data)
+ if wakeEvent != 0:
+ {.cast(gcsafe).}:
+ discard CloseHandle(wakeEvent)
+
+proc newWinapiGlobals*(): SiwinGlobalsWinapi =
+ let wakeEvent = CreateEvent(nil, False, False, nil)
+ if wakeEvent == 0:
+ raiseOSError(osLastError())
+
+ result = SiwinGlobalsWinapi(wakeEvent: wakeEvent)
+ result.installEventLoopWakeProc(
+ signalWinapiWake,
+ cast[pointer](wakeEvent),
+ closeWinapiWake,
+ )
+
+proc getLegacyGlobals(): SiwinGlobalsWinapi =
+ if legacyGlobals == nil:
+ legacyGlobals = newWinapiGlobals()
+ legacyGlobals
+
+
proc wkeyToKey(key: WParam): Key =
case key
of Vk_lshift: Key.lshift
@@ -290,7 +326,15 @@ method trySetBackdrop*(window: WindowWinapi, config: WindowBackdropConfig): bool
true
-proc initWindow(window: WindowWinapi; size: IVec2; screen: ScreenWinapi, fullscreen, frameless, transparent: bool, class = wClassName) =
+proc initWindow(
+ window: WindowWinapi,
+ size: IVec2,
+ screen: ScreenWinapi,
+ fullscreen, frameless, transparent: bool,
+ class = wClassName,
+ globals: SiwinGlobalsWinapi = nil,
+) =
+ window.globals = if globals == nil: getLegacyGlobals() else: globals
window.handle = CreateWindow(
class,
"",
@@ -582,18 +626,13 @@ method `content=`*(clipboard: ClipboardWinapi, content: ClipboardConvertableCont
method displayImpl(window: WindowWinapi) {.base.} =
- var ps: PaintStruct
- window.handle.BeginPaint(ps.addr)
-
window.eventsHandler.pushEvent onRender, RenderEvent(window: window)
-
+
if window of WindowWinapiSoftwareRendering:
BitBlt(
window.hdc, 0, 0, window.m_size.x, window.m_size.y,
window.WindowWinapiSoftwareRendering.buffer.hdc, 0, 0, SrcCopy
)
-
- window.handle.EndPaint(ps.addr)
method firstStep*(window: WindowWinapi, makeVisible = true) =
@@ -608,7 +647,7 @@ method firstStep*(window: WindowWinapi, makeVisible = true) =
window.handle.UpdateWindow()
- window.lastTickTime = getTime()
+ window.lastTickTime = getMonoTime()
proc updateWindowState(window: WindowWinapi) =
@@ -628,28 +667,87 @@ proc updateWindowState(window: WindowWinapi) =
window.m_pos = ivec2(p.rcNormalPosition.left, p.rcNormalPosition.top)
-method step*(window: WindowWinapi) =
+proc dispatchPendingWinapiMessages(): bool =
var msg: Msg
- var catched = false
-
while PeekMessage(msg.addr, 0, 0, 0, PmRemove).bool:
- # make tick if windows sent us WmPaint, it does it when the event queue is empty
- if msg.message == WmPaint:
- let nows = getTime()
- window.eventsHandler.pushEvent onTick, TickEvent(window: window, deltaTime: nows - window.lastTickTime)
- window.lastTickTime = nows
+ result = true
+ if msg.message != WmQuit:
+ TranslateMessage(msg.addr)
+ DispatchMessage(msg.addr)
+
+proc waitForWinapiActivity(globals: SiwinGlobalsWinapi, timeout: DWord): DWord =
+ var wakeEvent = globals.wakeEvent
+ MsgWaitForMultipleObjectsEx(
+ 1,
+ wakeEvent.addr,
+ timeout,
+ QsAllInput,
+ MwmoInputAvailable,
+ )
- TranslateMessage(msg.addr)
- DispatchMessage(msg.addr)
+method pollEventsImpl(globals: SiwinGlobalsWinapi): bool =
+ let waitResult = globals.waitForWinapiActivity(0)
+ if waitResult == WaitObject0:
+ globals.consumeEventLoopWake()
+ result = true
+ elif waitResult == WaitObject0 + 1:
+ result = true
+ elif waitResult == WaitFailed:
+ raiseOSError(osLastError())
+
+ result = dispatchPendingWinapiMessages() or result
+
+method waitEventsImpl(
+ globals: SiwinGlobalsWinapi,
+ timeout: Duration,
+): EventWaitResult =
+ if globals.pollEventsImpl():
+ return eventActivity
+
+ # Winim represents DWORD as int32, so convert against its unsigned range
+ # before preserving that value's bits in the signed binding type.
+ let timeoutMilliseconds = timeout.inTimeoutMilliseconds(
+ infinite = uint32.high,
+ maxFinite = uint32.high - 1,
+ )
+ let waitResult = globals.waitForWinapiActivity(cast[DWord](timeoutMilliseconds))
+ if waitResult == WaitObject0:
+ globals.consumeEventLoopWake()
+ elif waitResult == WaitObject0 + 1:
+ discard
+ elif waitResult == WaitTimeout:
+ return eventTimeout
+ elif waitResult == WaitFailed:
+ raiseOSError(osLastError())
+ else:
+ raise OSError.newException("Unexpected Win32 event-loop wait result")
- if msg.message == WmPaint:
- break
- else:
- catched = true
+ discard dispatchPendingWinapiMessages()
+ eventActivity
+
+method serviceWindow*(window: WindowWinapi) =
+ if window.closed:
+ return
+
+ let now = getMonoTime()
+ window.eventsHandler.pushEvent onTick, TickEvent(
+ window: window,
+ deltaTime: now - window.lastTickTime,
+ )
+ window.lastTickTime = now
+
+ if window.closed:
+ return
- if window.m_closed: return
+ if window.redrawRequested:
+ window.redrawRequested = false
+ if window.m_size.x > 0 and window.m_size.y > 0:
+ window.displayImpl()
- if not catched: sleep(1)
+method step*(window: WindowWinapi) =
+ let globals = if window.globals == nil: getLegacyGlobals() else: window.globals
+ discard globals.waitEvents(initDuration(milliseconds = 1))
+ window.serviceWindow()
proc poolEvent(window: WindowWinapi, message: Uint, wParam: WParam, lParam: LParam): LResult =
@@ -672,6 +770,10 @@ proc poolEvent(window: WindowWinapi, message: Uint, wParam: WParam, lParam: LPar
case message
of WmPaint:
+ var paint: PaintStruct
+ window.handle.BeginPaint(paint.addr)
+ window.handle.EndPaint(paint.addr)
+
let rect = window.handle.clientRect
if rect.right != window.m_size.x or rect.bottom != window.m_size.y:
window.m_size = ivec2(rect.right, rect.bottom)
@@ -682,11 +784,6 @@ proc poolEvent(window: WindowWinapi, message: Uint, wParam: WParam, lParam: LPar
window.eventsHandler.pushEvent onResize, ResizeEvent(window: window, size: window.m_size, initial: false)
window.redrawRequested = true
- if window.redrawRequested:
- window.redrawRequested = false
- if window.m_size.x * window.m_size.y > 0:
- window.displayImpl()
-
of WmDestroy:
window.m_closed = true
@@ -830,9 +927,13 @@ proc newSoftwareRenderingWindowWinapi*(
fullscreen = false,
frameless = false,
transparent = false,
+ globals: SiwinGlobalsWinapi = nil,
): WindowWinapiSoftwareRendering =
new result
- result.initWindow(size, screen, fullscreen, frameless, transparent)
+ result.initWindow(
+ size, screen, fullscreen, frameless, transparent,
+ globals = globals,
+ )
result.title = title
if not resizable: result.resizable = false
@@ -849,6 +950,7 @@ proc newPopupWindowWinapi*(
fullscreen = false,
frameless = true,
transparent = transparent,
+ globals = parent.globals,
)
result.initPopupState(parent, placement, grab)
result.pos = parent.pos + placement.popupRelativePos()
diff --git a/src/siwin/platforms/winapi/windowOpengl.nim b/src/siwin/platforms/winapi/windowOpengl.nim
index f290c66..cdcf56f 100644
--- a/src/siwin/platforms/winapi/windowOpengl.nim
+++ b/src/siwin/platforms/winapi/windowOpengl.nim
@@ -11,8 +11,16 @@ type
ctx: WglContext
-proc initWindowWinapiOpengl(window: WindowWinapiOpengl; size: IVec2; screen: ScreenWinapi, fullscreen, frameless, transparent: bool) =
- window.initWindow size, screen, fullscreen, frameless, transparent, woClassName
+proc initWindowWinapiOpengl(
+ window: WindowWinapiOpengl,
+ size: IVec2,
+ screen: ScreenWinapi,
+ fullscreen, frameless, transparent: bool,
+ globals: SiwinGlobalsWinapi,
+) =
+ window.initWindow(
+ size, screen, fullscreen, frameless, transparent, woClassName, globals,
+ )
var pfd = PixelFormatDescriptor(
nSize: Word PixelFormatDescriptor.sizeof,
@@ -54,9 +62,12 @@ proc newOpenglWindowWinapi*(
frameless = false,
transparent = false,
vsync = true,
+ globals: SiwinGlobalsWinapi = nil,
): WindowWinapiOpengl =
new result
- result.initWindowWinapiOpengl(size, screen, fullscreen, frameless, transparent)
+ result.initWindowWinapiOpengl(
+ size, screen, fullscreen, frameless, transparent, globals,
+ )
result.title = title
result.`vsync=`(vsync, silent=true)
if not resizable: result.resizable = false
diff --git a/src/siwin/platforms/winapi/windowVulkan.nim b/src/siwin/platforms/winapi/windowVulkan.nim
index 1f3c18c..dcd6755 100644
--- a/src/siwin/platforms/winapi/windowVulkan.nim
+++ b/src/siwin/platforms/winapi/windowVulkan.nim
@@ -26,8 +26,17 @@ method vulkanSurface*(window: WindowWinapiVulkan): pointer =
window.surface.raw
-proc initWindowWinapiVulkan(window: WindowWinapiVulkan; vkInstance: pointer, size: IVec2; screen: ScreenWinapi, fullscreen, frameless, transparent: bool) =
- window.initWindow size, screen, fullscreen, frameless, transparent, woClassName
+proc initWindowWinapiVulkan(
+ window: WindowWinapiVulkan,
+ vkInstance: pointer,
+ size: IVec2,
+ screen: ScreenWinapi,
+ fullscreen, frameless, transparent: bool,
+ globals: SiwinGlobalsWinapi,
+) =
+ window.initWindow(
+ size, screen, fullscreen, frameless, transparent, woClassName, globals,
+ )
var pfd = PixelFormatDescriptor(
nSize: Word PixelFormatDescriptor.sizeof,
@@ -70,8 +79,11 @@ proc newVulkanWindowWinapi*(
fullscreen = false,
frameless = false,
transparent = false,
+ globals: SiwinGlobalsWinapi = nil,
): WindowWinapiVulkan =
new result
- result.initWindowWinapiVulkan(vkInstance, size, screen, fullscreen, frameless, transparent)
+ result.initWindowWinapiVulkan(
+ vkInstance, size, screen, fullscreen, frameless, transparent, globals,
+ )
result.title = title
if not resizable: result.resizable = false
diff --git a/src/siwin/platforms/x11/siwinGlobals.nim b/src/siwin/platforms/x11/siwinGlobals.nim
index 99f0f04..9ba78fa 100644
--- a/src/siwin/platforms/x11/siwinGlobals.nim
+++ b/src/siwin/platforms/x11/siwinGlobals.nim
@@ -1,4 +1,4 @@
-import os, tables
+import std/[os, tables, posix]
import ../../[siwindefs]
import ../any/[window]
import x11/[xlib, x]
@@ -13,6 +13,8 @@ type
SiwinGlobalsX11* = ref SiwinGlobalsX11Obj
SiwinGlobalsX11Obj* = object of SiwinGlobals
display*: ptr Display
+ windows*: Table[uint, window.Window]
+ wake*: ptr X11WakeFd
wmForFramelessKind*: WmForFramelessKind
atoms*: tuple[
frameless, wmDeleteWindow, utf8String, netWmName, netWmIconName,
@@ -25,16 +27,70 @@ type
: Atom
]
+ X11WakeFd = object
+ readFd*, writeFd*: cint
+
proc `=destroy`(x: SiwinGlobalsX11Obj) {.siwin_destructor.} =
+ cast[SiwinGlobals](x.addr).shutdownEventLoopWakeState()
if x.display != nil:
discard XCloseDisplay(x.display)
+proc signalX11Wake(data: pointer) {.gcsafe, raises: [].} =
+ let wake = cast[ptr X11WakeFd](data)
+ if wake != nil and wake.writeFd >= 0:
+ var byte = '\x01'
+ {.cast(gcsafe).}:
+ while write(wake.writeFd, byte.addr, 1) < 0 and errno == EINTR:
+ discard
+
+proc closeX11Wake(data: pointer) {.gcsafe, raises: [].} =
+ let wake = cast[ptr X11WakeFd](data)
+ if wake != nil:
+ if wake.readFd >= 0: discard close(wake.readFd)
+ if wake.writeFd >= 0: discard close(wake.writeFd)
+ dealloc(wake)
+
+proc configureWakeFd(fd: cint) =
+ let flags = fcntl(fd, F_GETFL)
+ if flags < 0 or fcntl(fd, F_SETFL, flags or O_NONBLOCK) < 0:
+ raiseOSError(osLastError())
+ let fdFlags = fcntl(fd, F_GETFD)
+ if fdFlags < 0 or fcntl(fd, F_SETFD, fdFlags or FD_CLOEXEC) < 0:
+ raiseOSError(osLastError())
+
+proc drainX11Wake*(globals: SiwinGlobalsX11): bool =
+ if globals.wake == nil: return false
+ var buffer: array[64, char]
+ while true:
+ let count = read(globals.wake.readFd, buffer[0].addr, buffer.len)
+ if count > 0:
+ result = true
+ elif count < 0 and errno == EINTR:
+ continue
+ else:
+ break
+ if result: globals.consumeEventLoopWake()
proc newX11Globals*: SiwinGlobalsX11 {.raises: [OsError].} =
new result
result.display = XOpenDisplay(getEnv("DISPLAY").cstring)
if result.display == nil: raise OsError.newException("failed to open X11 display, make sure the DISPLAY environment variable is set correctly")
+ result.wake = cast[ptr X11WakeFd](alloc0(sizeof(X11WakeFd)))
+ var wakeFds: array[2, cint]
+ if pipe(wakeFds) != 0:
+ dealloc(result.wake)
+ raiseOSError(osLastError())
+ result.wake.readFd = wakeFds[0]
+ result.wake.writeFd = wakeFds[1]
+ try:
+ configureWakeFd(result.wake.readFd)
+ configureWakeFd(result.wake.writeFd)
+ except OSError:
+ closeX11Wake(result.wake)
+ result.wake = nil
+ raise
+ result.installEventLoopWakeProc(signalX11Wake, result.wake, closeX11Wake)
result.wmForFramelessKind =
if (result.atoms.frameless = result.display.XInternAtom("_MOTIF_WM_HINTS", 1); result.atoms.frameless != 0):
diff --git a/src/siwin/platforms/x11/window.nim b/src/siwin/platforms/x11/window.nim
index fffe3f4..c6bd88e 100644
--- a/src/siwin/platforms/x11/window.nim
+++ b/src/siwin/platforms/x11/window.nim
@@ -1,13 +1,15 @@
when not (compiles do: import pkg/x11/xutil):
{.error: "x11 library not installed, required to cross compile to linux\n please run `nimble install x11`".}
-import std/[times, importutils, strformat, sequtils, os, options, tables, uri, strutils, dynlib]
+import std/[times, monotimes, importutils, strformat, sequtils, os, options, tables, uri, strutils, dynlib]
+from std/posix import
+ TPollfd, Tnfds, POLLIN, POLLERR, POLLHUP, POLLNVAL, EINTR, errno, poll
import pkg/[vmath, chroma]
import pkg/x11/xlib except Screen
import pkg/x11/x except Window, Cursor, Time
import pkg/x11/[xutil, xatom, cursorfont, keysym]
import ../../[colorutils, siwindefs]
-import ../any/[window, clipboards]
+import ../any/[window, clipboards, eventLoop]
import ../any/[windowUtils]
import ./[siwinGlobals]
@@ -68,7 +70,7 @@ type
syncState: SyncState
lastSync: XSyncValue
- lastClickTime: Time
+ lastClickTime: MonoTime
doubleClickHandled: bool
temporaryCursor: Option[BuiltinCursor]
@@ -78,6 +80,7 @@ type
dragPositionTimestamp: x.Time
lastDragStatus: DragStatus
dragStatusSent: bool
+ prevEventIsKeyUpRepeated: bool
closeEventSent: bool
@@ -478,6 +481,7 @@ proc basicInitWindow(window: WindowX11; size: IVec2; screen: ScreenX11) =
window.m_focused = true
proc setupWindow(window: WindowX11, fullscreen, frameless: bool, class: string) =
+ window.globals.windows[window.handle.uint] = window
discard window.globals.display.XSelectInput(
window.handle,
ExposureMask or KeyPressMask or KeyReleaseMask or PointerMotionMask or ButtonPressMask or
@@ -595,6 +599,7 @@ method close*(window: WindowX11) =
if window.m_closed:
return
window.m_closed = true
+ window.globals.windows.del(window.handle.uint)
window.pushCloseEvent()
proc backdropBlurSupported(window: WindowX11): bool =
@@ -1210,35 +1215,16 @@ method firstStep*(window: WindowX11, makeVisible = true) =
if window of WindowX11SoftwareRendering:
window.WindowX11SoftwareRendering.resizePixelBuffer(window.m_size)
window.eventsHandler.onResize.pushEvent ResizeEvent(window: window, size: window.m_size, initial: true)
- window.lastTickTime = getTime()
+ window.lastTickTime = getMonoTime()
-method step*(window: WindowX11) =
- ## make window main loop step
- ## ! don't forget to call firstStep()
- template button: MouseButton =
- case ev.xbutton.button
- of 1: MouseButton.left
- of 2: MouseButton.middle
- of 3: MouseButton.right
- of 8: MouseButton.backward
- of 9: MouseButton.forward
- else: MouseButton.left
-
- template isScroll: bool = ev.xbutton.button.int in 4..7
-
- template scrollDeltaY: float =
- case ev.xbutton.button
- of 4: 1
- of 5: -1
- else: 0
-
- template scrollDeltaX: float =
- case ev.xbutton.button
- of 6: 1
- of 7: -1
- else: 0
-
+proc dispatchWindowEvent(
+ window: WindowX11,
+ event, followingEvent: XEvent,
+ hasFollowingEvent: bool,
+) =
+ var ev = event
+
proc extractKey(xkey: XKeyEvent): Key =
var i = 0
while i < 4 and result == Key.unknown:
@@ -1265,10 +1251,32 @@ method step*(window: WindowX11) =
# todo: press pressed in system mouse buttons
- var prevEventIsKeyUpRepeated = false
- proc handleEvent(ev: var XEvent, nextEv: var XEvent, hasNextEvent: bool) =
- let repeated = prevEventIsKeyUpRepeated
- prevEventIsKeyUpRepeated = false
+ proc handleEvent(ev: var XEvent, nextEv: XEvent, hasNextEvent: bool) =
+ template button: MouseButton =
+ case ev.xbutton.button
+ of 1: MouseButton.left
+ of 2: MouseButton.middle
+ of 3: MouseButton.right
+ of 8: MouseButton.backward
+ of 9: MouseButton.forward
+ else: MouseButton.left
+
+ template isScroll: bool = ev.xbutton.button.int in 4..7
+
+ template scrollDeltaY: float =
+ case ev.xbutton.button
+ of 4: 1
+ of 5: -1
+ else: 0
+
+ template scrollDeltaX: float =
+ case ev.xbutton.button
+ of 6: 1
+ of 7: -1
+ else: 0
+
+ let repeated = window.prevEventIsKeyUpRepeated
+ window.prevEventIsKeyUpRepeated = false
case ev.theType
of Expose:
@@ -1391,7 +1399,7 @@ method step*(window: WindowX11) =
of ButtonPress:
if not isScroll:
- let nows = getTime()
+ let nows = getMonoTime()
window.mouse.pressed.incl button
window.clicking.incl button
@@ -1421,7 +1429,7 @@ method step*(window: WindowX11) =
window.onInteractiveResizeOrMoveFinished()
if not isScroll:
- let nows = getTime()
+ let nows = getMonoTime()
window.mouse.pressed.excl button
if button in window.clicking:
@@ -1498,7 +1506,7 @@ method step*(window: WindowX11) =
var key = ev.xkey.extractKey
if key != Key.unknown:
let repeated = hasNextEvent and nextEv.theType == KeyPress and nextEv.xkey.extractKey == key
- if repeated: prevEventIsKeyUpRepeated = true
+ if repeated: window.prevEventIsKeyUpRepeated = true
window.keyboard.pressed.excl key
window.refreshKeyboardModifiers()
@@ -1630,44 +1638,15 @@ method step*(window: WindowX11) =
else: discard
- block nextEvent:
- template closeAndExit =
- window.pushCloseEvent()
- return
-
- var
- ev: XEvent
- nextEv: XEvent
- catched = false
-
- proc checkEvent(_: PDisplay, event: PXEvent, userData: XPointer): XBool {.cdecl.} =
- if cast[int](event.xany.window) == cast[int](userData): 1 else: 0
-
- while window.globals.display.XCheckIfEvent(nextEv.addr, checkEvent, cast[XPointer](window.handle)) == 1:
- if not catched:
- ev = nextEv
- catched = true
- continue
-
- handleEvent(ev, nextEv, true)
- ev = nextEv
-
- if window.closed: closeAndExit()
-
- # force make tick if server decided to spam events to us
- if (getTime() - window.lastTickTime) > initDuration(milliseconds=10):
- break
-
- discard XFlush window.globals.display
-
- if catched:
- handleEvent(ev, nextEv, false)
-
- if window.closed: closeAndExit()
- if not catched: sleep(1)
+ handleEvent(ev, followingEvent, hasFollowingEvent)
+ if window.closed:
+ window.pushCloseEvent()
- let nows = getTime()
+method serviceWindow*(window: WindowX11) =
+ if window.closed:
+ return
+ let nows = getMonoTime()
window.eventsHandler.onTick.pushEvent TickEvent(window: window, deltaTime: nows - window.lastTickTime)
window.lastTickTime = nows
@@ -1686,6 +1665,86 @@ method step*(window: WindowX11) =
discard XFlush window.globals.display
+method pollEventsImpl(globals: SiwinGlobalsX11): bool =
+ result = globals.drainX11Wake()
+ while globals.display.XPending() > 0:
+ var events = newSeqOfCap[XEvent](globals.display.XPending().int)
+ while globals.display.XPending() > 0:
+ var event: XEvent
+ discard globals.display.XNextEvent(event.addr)
+ events.add(event)
+
+ var
+ nextForWindow = initTable[uint, int]()
+ nextEventIndices = newSeq[int](events.len)
+ for i in countdown(events.high, 0):
+ let windowId = events[i].xany.window.uint
+ nextEventIndices[i] = nextForWindow.getOrDefault(windowId, -1)
+ nextForWindow[windowId] = i
+
+ for i, event in events:
+ let window = globals.windows.getOrDefault(event.xany.window.uint)
+ if window != nil and not window.closed:
+ let nextIndex = nextEventIndices[i]
+ window.WindowX11.dispatchWindowEvent(
+ event,
+ if nextIndex >= 0: events[nextIndex] else: XEvent(),
+ nextIndex >= 0,
+ )
+
+ result = true
+ discard XFlush(globals.display)
+
+method waitEventsImpl(
+ globals: SiwinGlobalsX11,
+ timeout: Duration,
+): EventWaitResult =
+ if globals.pollEventsImpl():
+ return eventActivity
+ discard XFlush(globals.display)
+ let started = getMonoTime()
+ while true:
+ var fds = [
+ TPollfd(fd: globals.display.XConnectionNumber(), events: POLLIN),
+ TPollfd(fd: globals.wake.readFd, events: POLLIN),
+ ]
+ let remaining =
+ if timeout == Duration.high:
+ Duration.high
+ else:
+ max(initDuration(), timeout - (getMonoTime() - started))
+ let count = poll(
+ fds[0].addr,
+ fds.len.Tnfds,
+ remaining.inTimeoutMilliseconds(
+ infinite = -1.cint,
+ maxFinite = cint.high,
+ ),
+ )
+ if count == 0:
+ return eventTimeout
+ if count < 0:
+ if errno == EINTR:
+ if timeout != Duration.high and getMonoTime() - started >= timeout:
+ return eventTimeout
+ continue
+ raiseOSError(osLastError())
+
+ if (fds[0].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ raise OSError.newException("X11 display connection closed while waiting")
+ if (fds[1].revents and (POLLERR or POLLHUP or POLLNVAL)) != 0:
+ raise OSError.newException("X11 event-loop wake pipe closed while waiting")
+ if (fds[1].revents and POLLIN) != 0:
+ discard globals.drainX11Wake()
+ if (fds[0].revents and POLLIN) != 0:
+ discard globals.pollEventsImpl()
+ return eventActivity
+
+method step*(window: WindowX11) =
+ discard window.globals.waitEvents(initDuration(milliseconds = 1))
+ window.serviceWindow()
+
+
proc newSoftwareRenderingWindowX11*(
globals: SiwinGlobalsX11,
size = ivec2(1280, 720),
diff --git a/src/siwin/window.nim b/src/siwin/window.nim
index 763b12b..71221ac 100644
--- a/src/siwin/window.nim
+++ b/src/siwin/window.nim
@@ -1,5 +1,7 @@
import vmath
import ./[siwindefs]
+when siwin_build_lib:
+ import std/times
import ./platforms
import ./platforms/any/[window as anyWindow]
@@ -108,10 +110,13 @@ when not siwin_use_lib:
raise SiwinPlatformSupportDefect.newException("Unsupported platform")
elif defined(windows):
- newSoftwareRenderingWindowWinapi(
+ if not (globals of SiwinGlobalsWinapi):
+ raise SiwinPlatformSupportDefect.newException("Unsupported platform")
+ result = newSoftwareRenderingWindowWinapi(
size, title,
(if screen == -1: defaultScreenWinapi() else: screenWinapi(screen)),
- resizable, fullscreen, frameless, transparent
+ resizable, fullscreen, frameless, transparent,
+ globals = globals.SiwinGlobalsWinapi,
)
elif defined(macosx):
@@ -263,6 +268,11 @@ when siwin_build_lib:
import ./colorutils
import ./platforms/any/[clipboards]
+ type
+ CEventLoopWakerHandleObj = object
+ waker: EventLoopWaker
+ CEventLoopWakerHandle = ptr CEventLoopWakerHandleObj
+
{.push, exportc, cdecl, dynlib.}
proc siwin_destroy_window(window: Window) = GC_unref(window)
@@ -355,8 +365,39 @@ when siwin_build_lib:
proc siwin_window_set_drag_status(window: Window, v: DragStatus) = window.dragStatus = v
proc siwin_window_first_step(window: Window, makeVisible: cchar) = window.firstStep(makeVisible.bool)
proc siwin_window_step(window: Window) = window.step()
+ proc siwin_window_service(window: Window) = window.serviceWindow()
proc siwin_window_run(window: Window, makeVisible: cchar) = window.run(makeVisible.bool)
+ proc siwin_poll_events(globals: SiwinGlobals): cchar = globals.pollEvents().cchar
+ proc siwin_wait_events(globals: SiwinGlobals, timeoutMilliseconds: cint): cchar =
+ if timeoutMilliseconds < 0:
+ globals.waitEvents()
+ return 0.cchar
+ let timeout = initDuration(milliseconds = timeoutMilliseconds.int)
+ result = (if globals.waitEvents(timeout) == eventActivity: 0 else: 1).cchar
+ proc siwin_wake_event_loop(globals: SiwinGlobals) = globals.wakeEventLoop()
+
+ proc siwin_event_loop_waker(
+ globals: SiwinGlobals,
+ ): CEventLoopWakerHandle =
+ result = cast[CEventLoopWakerHandle](
+ allocShared0(sizeof(CEventLoopWakerHandleObj))
+ )
+ result.waker = globals.eventLoopWaker()
+
+ proc siwin_event_loop_waker_wake(
+ handle: CEventLoopWakerHandle,
+ ) {.gcsafe, raises: [], nodestroy.} =
+ if handle != nil:
+ handle.waker.wake()
+
+ proc siwin_destroy_event_loop_waker(
+ handle: CEventLoopWakerHandle,
+ ) =
+ if handle != nil:
+ `=destroy`(handle.waker)
+ deallocShared(handle)
+
proc siwin_window_set_event_handler(window: Window, eventHandler: ptr WindowEventsHandler) = window.eventsHandler = eventHandler[]
{.pop.}
diff --git a/src/siwin/windowOpengl.nim b/src/siwin/windowOpengl.nim
index dc4fcd0..bb54821 100644
--- a/src/siwin/windowOpengl.nim
+++ b/src/siwin/windowOpengl.nim
@@ -65,10 +65,13 @@ when not siwin_use_lib:
raise SiwinPlatformSupportDefect.newException("Unsupported platform")
elif defined(windows):
- newOpenglWindowWinapi(
+ if not (globals of SiwinGlobalsWinapi):
+ raise SiwinPlatformSupportDefect.newException("Unsupported platform")
+ result = newOpenglWindowWinapi(
size, title,
(if screen == -1: defaultScreenWinapi() else: screenWinapi(screen)),
- resizable, fullscreen, frameless, transparent, vsync
+ resizable, fullscreen, frameless, transparent, vsync,
+ globals = globals.SiwinGlobalsWinapi,
)
elif defined(macosx):
diff --git a/src/siwin/windowVulkan.nim b/src/siwin/windowVulkan.nim
index 206d628..60cffbb 100644
--- a/src/siwin/windowVulkan.nim
+++ b/src/siwin/windowVulkan.nim
@@ -65,11 +65,14 @@ when not siwin_use_lib:
raise SiwinPlatformSupportDefect.newException("Unsupported platform")
elif defined(windows):
- newVulkanWindowWinapi(
+ if not (globals of SiwinGlobalsWinapi):
+ raise SiwinPlatformSupportDefect.newException("Unsupported platform")
+ result = newVulkanWindowWinapi(
vkInstance,
size, title,
(if screen == -1: defaultScreenWinapi() else: screenWinapi(screen)),
- resizable, fullscreen, frameless, transparent
+ resizable, fullscreen, frameless, transparent,
+ globals = globals.SiwinGlobalsWinapi,
)
when defined(linux) or defined(bsd):
diff --git a/tests/et_bindings.c b/tests/et_bindings.c
index a6e5236..4508478 100644
--- a/tests/et_bindings.c
+++ b/tests/et_bindings.c
@@ -35,6 +35,13 @@ int main(int argc, char *argv[]) {
Platform platform = siwin_default_platform();
SiwinGlobals globals = siwin_new_globals(platform);
+ SiwinEventLoopWaker waker = siwin_event_loop_waker(globals);
+
+ /* A retained waker can be handed to a producer without sharing globals. */
+ siwin_event_loop_waker_wake(waker);
+ if (siwin_wait_events(globals, 0) != 0) {
+ return 1;
+ }
Window win = siwin_new_software_rendering_window(
globals,
@@ -55,5 +62,8 @@ int main(int argc, char *argv[]) {
siwin_destroy_window(win);
siwin_destroy_globals(globals);
+ /* Retaining the waker past loop shutdown is safe; waking becomes a no-op. */
+ siwin_event_loop_waker_wake(waker);
+ siwin_destroy_event_loop_waker(waker);
return 0;
}
diff --git a/tests/t_event_loop.nim b/tests/t_event_loop.nim
new file mode 100644
index 0000000..aa8d52a
--- /dev/null
+++ b/tests/t_event_loop.nim
@@ -0,0 +1,360 @@
+import std/[assertions, times]
+
+import siwin/platforms/any/eventLoop
+
+
+const
+ signedInfiniteTimeout = -1'i32
+ signedMaxFiniteTimeout = int32.high
+
+
+block timeout_milliseconds_round_up:
+ doAssert initDuration(nanoseconds = -1).inTimeoutMilliseconds(
+ signedInfiniteTimeout, signedMaxFiniteTimeout,
+ ) == 0
+ doAssert initDuration().inTimeoutMilliseconds(
+ signedInfiniteTimeout, signedMaxFiniteTimeout,
+ ) == 0
+ doAssert initDuration(nanoseconds = 1).inTimeoutMilliseconds(
+ signedInfiniteTimeout, signedMaxFiniteTimeout,
+ ) == 1
+ doAssert initDuration(milliseconds = 1).inTimeoutMilliseconds(
+ signedInfiniteTimeout, signedMaxFiniteTimeout,
+ ) == 1
+ doAssert initDuration(
+ milliseconds = 1,
+ nanoseconds = 1,
+ ).inTimeoutMilliseconds(signedInfiniteTimeout, signedMaxFiniteTimeout) == 2
+
+block timeout_milliseconds_handle_native_bounds:
+ doAssert Duration.high.inTimeoutMilliseconds(
+ signedInfiniteTimeout, signedMaxFiniteTimeout,
+ ) == signedInfiniteTimeout
+ doAssert initDuration(seconds = int64.high - 1).inTimeoutMilliseconds(
+ signedInfiniteTimeout,
+ signedMaxFiniteTimeout,
+ ) == signedMaxFiniteTimeout
+ doAssert Duration.high.inTimeoutMilliseconds(uint32.high, uint32.high - 1) ==
+ uint32.high
+ doAssert initDuration(milliseconds = 1).inTimeoutMilliseconds(
+ uint32.high, uint32.high - 1,
+ ) == 1'u32
+ doAssert initDuration(seconds = int64.high - 1).inTimeoutMilliseconds(
+ uint32.high, uint32.high - 1,
+ ) == uint32.high - 1
+
+
+const eventLoopIntegrationSupported =
+ # Add platforms here as their global event-loop backends are implemented.
+ when defined(macosx) or defined(windows) or defined(linux) or defined(bsd): true
+ else: false
+
+const delayedWakeMilliseconds = 500
+const shortWakeMilliseconds = 100
+const wakeRaceIterations = 200
+const serviceWindowNeedsVisibleSurface =
+ when defined(linux) or defined(bsd): true
+ else: false
+
+when eventLoopIntegrationSupported:
+ import std/[atomics, monotimes, os]
+
+ import pkg/vmath
+
+ import siwin
+
+ when defined(windows):
+ import siwin/platforms/winapi/winapi
+
+ # Nim's `cpuTime` uses Microsoft's wall-clock `clock()`. Query actual
+ # thread execution time so this assertion can distinguish waiting from spin.
+ func fileTimeTicks(value: FileTime): uint64 =
+ value.dwLowDateTime.uint64 or (value.dwHighDateTime.uint64 shl 32)
+
+ proc threadCpuTime(): float64 =
+ var creationTime, exitTime, kernelTime, userTime: FileTime
+ doAssert GetThreadTimes(
+ GetCurrentThread(),
+ creationTime.addr,
+ exitTime.addr,
+ kernelTime.addr,
+ userTime.addr,
+ ).bool
+ (kernelTime.fileTimeTicks + userTime.fileTimeTicks).float64 / 10_000_000.0
+ else:
+ proc threadCpuTime(): float64 = cpuTime()
+
+ proc wakeFromWorker(waker: EventLoopWaker) {.thread.} =
+ waker.wake()
+
+ type DelayedWakeRequest = object
+ waker: EventLoopWaker
+ signaled: ptr Atomic[bool]
+ delayMilliseconds: int
+
+ proc wakeAfterDelay(request: DelayedWakeRequest) {.thread.} =
+ sleep(request.delayMilliseconds)
+ request.signaled[].store(true)
+ request.waker.wake()
+
+ type WakeRaceRequest = object
+ waker: EventLoopWaker
+ produced: ptr Atomic[int]
+ consumed: ptr Atomic[int]
+
+ proc produceWakeRace(request: WakeRaceRequest) {.thread.} =
+ for sequence in 1..wakeRaceIterations:
+ while request.consumed[].load() != sequence - 1:
+ sleep(0)
+ request.produced[].store(sequence)
+ request.waker.wake()
+
+ let globals = newSiwinGlobals()
+
+ block poll_is_nonblocking:
+ discard globals.pollEvents()
+
+ block zero_timeout_reports_timeout:
+ discard globals.pollEvents()
+ doAssert globals.waitEvents(initDuration()) == eventTimeout
+
+ block queued_wake_returns_activity:
+ let waker = globals.eventLoopWaker()
+ waker.wake()
+ doAssert globals.waitEvents(initDuration(milliseconds = 50)) == eventActivity
+
+ block repeated_wakes_are_coalesced:
+ let waker = globals.eventLoopWaker()
+ waker.wake()
+ waker.wake()
+ doAssert globals.waitEvents(initDuration(milliseconds = 50)) == eventActivity
+
+ block copied_waker_can_wake_from_a_worker:
+ var worker: Thread[EventLoopWaker]
+ createThread(worker, wakeFromWorker, globals.eventLoopWaker())
+ doAssert globals.waitEvents(initDuration(milliseconds = 50)) == eventActivity
+ joinThread(worker)
+
+ block finite_wait_uses_a_monotonic_deadline:
+ discard globals.pollEvents()
+ let
+ started = getMonoTime()
+ target = initDuration(milliseconds = 100)
+
+ while true:
+ let remaining = target - (getMonoTime() - started)
+ let result = globals.waitEvents(
+ if remaining.inNanoseconds > 0: remaining else: initDuration()
+ )
+ if result == eventTimeout:
+ break
+
+ let elapsed = getMonoTime() - started
+ doAssert elapsed >= initDuration(milliseconds = 80),
+ "the finite event wait returned before its monotonic deadline"
+ doAssert elapsed < initDuration(seconds = 2),
+ "the finite event wait did not return near its deadline"
+
+ block idle_wait_blocks_without_busy_spinning:
+ discard globals.pollEvents()
+
+ var signaled: Atomic[bool]
+ signaled.store(false)
+
+ var worker: Thread[DelayedWakeRequest]
+ let
+ wallStarted = getMonoTime()
+ cpuStarted = threadCpuTime()
+ createThread(
+ worker,
+ wakeAfterDelay,
+ DelayedWakeRequest(
+ waker: globals.eventLoopWaker(),
+ signaled: signaled.addr,
+ delayMilliseconds: delayedWakeMilliseconds,
+ ),
+ )
+
+ var waitResult = eventTimeout
+ while not signaled.load():
+ waitResult = globals.waitEvents(initDuration(seconds = 3))
+ if waitResult == eventTimeout:
+ doAssert signaled.load(), "event wait timed out before the worker wake"
+
+ joinThread(worker)
+ discard globals.pollEvents()
+
+ let
+ wallSeconds = (getMonoTime() - wallStarted).inNanoseconds.float64 /
+ 1_000_000_000.0
+ cpuSeconds = threadCpuTime() - cpuStarted
+
+ doAssert waitResult == eventActivity,
+ "the delayed worker wake should interrupt the native wait"
+ doAssert wallSeconds >= delayedWakeMilliseconds.float64 / 1_000.0 * 0.8,
+ "the event loop returned before the delayed wake: " & $wallSeconds & "s"
+ doAssert cpuSeconds < 0.2,
+ "the idle wait may be polling: " & $cpuSeconds & " CPU seconds over " &
+ $wallSeconds & " wall seconds"
+
+ block repeated_queue_before_wake_races_lose_no_wakeups:
+ discard globals.pollEvents()
+
+ var produced, consumed: Atomic[int]
+ produced.store(0)
+ consumed.store(0)
+
+ var worker: Thread[WakeRaceRequest]
+ let raceWaker = globals.eventLoopWaker()
+ createThread(
+ worker,
+ produceWakeRace,
+ WakeRaceRequest(
+ waker: raceWaker,
+ produced: produced.addr,
+ consumed: consumed.addr,
+ ),
+ )
+
+ while consumed.load() < wakeRaceIterations:
+ while produced.load() <= consumed.load():
+ doAssert globals.waitEvents(initDuration(seconds = 2)) == eventActivity,
+ "an enqueued producer wake was lost (produced=" & $produced.load() &
+ ", consumed=" & $consumed.load() & ")"
+
+ # This atomic counter stands in for draining an application queue. The
+ # producer publishes it before waking and waits for the drain before the
+ # next iteration, repeatedly exercising the wait boundary race.
+ consumed.store(produced.load())
+
+ joinThread(worker)
+
+ block copied_waker_is_harmless_after_shutdown:
+ var temporaryGlobals = newSiwinGlobals()
+ let survivingWaker = temporaryGlobals.eventLoopWaker()
+ temporaryGlobals = nil
+ GC_fullCollect()
+ survivingWaker.wake()
+
+ block service_window_is_nonblocking:
+ var ticks, renders: int
+ let window = globals.newSoftwareRenderingWindow(
+ size = ivec2(32, 32),
+ title = "Siwin event loop test",
+ )
+ defer:
+ if window.opened:
+ window.close()
+
+ window.eventsHandler = WindowEventsHandler(
+ onTick: proc(event: TickEvent) =
+ discard event
+ inc ticks
+ ,
+ onRender: proc(event: RenderEvent) =
+ discard event
+ inc renders
+ ,
+ )
+ window.firstStep(makeVisible = serviceWindowNeedsVisibleSurface)
+ window.redraw()
+ window.serviceWindow()
+
+ doAssert ticks == 1
+ doAssert renders == 1
+
+ block worker_wake_schedules_redraw:
+ ticks = 0
+ renders = 0
+
+ var redrawQueued: Atomic[bool]
+ redrawQueued.store(false)
+ var worker: Thread[DelayedWakeRequest]
+ createThread(
+ worker,
+ wakeAfterDelay,
+ DelayedWakeRequest(
+ waker: globals.eventLoopWaker(),
+ signaled: redrawQueued.addr,
+ delayMilliseconds: shortWakeMilliseconds,
+ ),
+ )
+
+ while not redrawQueued.load():
+ doAssert globals.waitEvents(initDuration(seconds = 2)) == eventActivity,
+ "the redraw request did not wake the application loop"
+
+ # Drain the simulated destination queue before servicing the window.
+ redrawQueued.store(false)
+ window.redraw()
+ window.serviceWindow()
+ joinThread(worker)
+
+ doAssert ticks == 1
+ doAssert renders == 1
+
+ ticks = 0
+ renders = 0
+ window.redraw()
+ window.step()
+
+ doAssert ticks == 1
+ doAssert renders == 1
+
+ block event_driven_runner_services_every_window_after_one_wait:
+ var wakeQueued: Atomic[bool]
+ wakeQueued.store(false)
+ var worker: Thread[DelayedWakeRequest]
+ createThread(
+ worker,
+ wakeAfterDelay,
+ DelayedWakeRequest(
+ waker: globals.eventLoopWaker(),
+ signaled: wakeQueued.addr,
+ delayMilliseconds: shortWakeMilliseconds,
+ ),
+ )
+
+ var ticks = [0, 0]
+ var rendersAfterWake = [0, 0]
+ let
+ firstWindow = globals.newSoftwareRenderingWindow(
+ size = ivec2(32, 32),
+ title = "Siwin event loop multi-window test 1",
+ )
+ secondWindow = globals.newSoftwareRenderingWindow(
+ size = ivec2(32, 32),
+ title = "Siwin event loop multi-window test 2",
+ )
+
+ proc handler(index: int): WindowEventsHandler =
+ WindowEventsHandler(
+ onTick: proc(event: TickEvent) =
+ inc ticks[index]
+ if wakeQueued.load():
+ event.window.redraw()
+ ,
+ onRender: proc(event: RenderEvent) =
+ if wakeQueued.load():
+ inc rendersAfterWake[index]
+ event.window.close()
+ ,
+ )
+
+ globals.runMultipleEventDriven(
+ (
+ window: firstWindow,
+ eventsHandler: handler(0),
+ makeVisible: serviceWindowNeedsVisibleSurface,
+ ),
+ (
+ window: secondWindow,
+ eventsHandler: handler(1),
+ makeVisible: serviceWindowNeedsVisibleSurface,
+ ),
+ )
+ joinThread(worker)
+
+ doAssert ticks[0] >= 2
+ doAssert ticks[1] >= 2
+ doAssert rendersAfterWake == [1, 1]