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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/build-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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:
Expand Down
120 changes: 109 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<h2 align="center">Examples</h2>

## 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
Expand Down Expand Up @@ -240,23 +263,89 @@ loadExtensions()

<h2 align="center">manual main cycle</h2>

```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()
```

<h2 align="center">manual event loop cycle</h2>

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.

<h2 align="center">running multiple windows</h2>

```nim
Expand Down Expand Up @@ -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),
)
```

<h2 align="center">client-side decorations</h2>

```nim
Expand Down
65 changes: 62 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down
14 changes: 13 additions & 1 deletion bindings/siwin.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ typedef struct {} *SiwinGlobals;
typedef struct {} *Window;
typedef struct {} *Screen;
typedef struct {} *Clipboard;
typedef struct {} *SiwinEventLoopWaker;


typedef struct NimRtti {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -441,4 +454,3 @@ typedef struct WindowEventHandler {
cmdCount = argc; \
cmdLine = argv; \
siwin_main();

Loading
Loading