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]