x.multiwindow #
x.multiwindow
x.multiwindow is the low-level multi-window layer used by the experimental gg multi-window facade. It owns native window lifetimes, backend selection, per-window events, owner-thread dispatch, and explicit render swapchains.
Most application code should use gg with -d gg_multiwindow. This module is intended for the gg facade, backend work, and callers that need direct control over native windows and rendering setup.
Scope
The module provides:
- an
Appregistry for multiple native windows; - generation-checked
WindowIdhandles; - backend capability reporting and backend selection;
- lifecycle events routed to a specific window;
- an owner-thread job queue for cross-thread work;
- explicit swapchain handoff for
sokol.gfxrendering.
It does not provide high-level input handling, layout, widgets, text rendering, or a default event loop. The gg facade supplies the higher-level loop and drawing API.
Creating an App
import x.multiwindow
mut app := multiwindow.new_app(backend: .mock, queue_size: 128)!
defer {
app.stop() or {}
}
win := app.create_window(title: 'Tool', width: 320, height: 200)!
info := app.window_info(win)!
println('${info.title}: ${info.width}x${info.height}')
multiwindow.new_app() uses the values in Config. The low-level default backend is .mock; .auto must be requested explicitly. The gg facade has its own configuration and defaults to .auto.
Config.require_renderer: true asks the selected backend to initialize its renderer during new_app(). The render API requires that creation-time request and Capabilities.explicit_swapchain; x.multiwindow does not lazily initialize a renderer later.
Backend Selection
BackendKind values are:
.mock: deterministic in-process backend for tests and event-only code;.x11: Linux X11 backend, compiled only with-d x_multiwindow_x11;.wayland: Linux Wayland backend, compiled only with-d sokol_wayland;.appkit: macOS AppKit backend;.win32: Windows backend;.auto: resolve to a concrete backend.
The .auto policy is platform and environment dependent:
- Windows selects
.win32. - macOS selects
.appkit. - Linux with
require_renderer: trueprefers X11 only when compiled with-d x_multiwindow_x11andDISPLAYis set, then Wayland when compiled with-d sokol_waylandandWAYLAND_DISPLAYis set. - Linux without
require_rendererprefers Wayland when compiled with-d sokol_waylandandWAYLAND_DISPLAYis set, then X11 only when compiled with-d x_multiwindow_x11andDISPLAYis set. - If no native backend is selected,
.autofalls back to.mock.
Plain capability probes do not necessarily open a display or create a device. Renderer capability probes and new_app(require_renderer: true) may fail if the display server, graphics device, or platform API is unavailable.
Backend Capabilities
Capabilities describes the selected backend contract:
multi_window: backend can manage more than one window;owner_queue: the owner-thread queue is available;explicit_swapchain: render calls can return agfx.Swapchain;mock,native,x11,wayland,win32: selected platform flags;gl,metal,d3d11: active renderer API flags;readback: reserved for backends that expose readback support.
Backend notes:
- Mock supports lifecycle, events, min-size clamping, and the owner queue, but it has no renderer.
- X11 is Linux-only and exists only in builds compiled with
-d x_multiwindow_x11. It supports native lifecycle, title updates, X11 size hints, borderless/fullscreen hints, optional EGL/OpenGL rendering, and native size queries after create/resize. Programmatic resize is rejected for non-resizable windows. - Wayland is Linux-only and exists only in builds compiled with
-d sokol_wayland. It requireswl_compositorandxdg_wm_base, rejectsvisible: false, and currently rejects programmatic resize. Rendering uses Wayland EGL/OpenGL when initialized. - AppKit is macOS-only. It must start on the main thread and uses Metal when rendering is required.
- Win32 is Windows-only and supports native lifecycle and min-size enforcement. D3D11 rendering requires a Windows build with
-d sokol_d3d11; without that flag, lifecycle works but renderer/swapchain calls are unsupported. Renderer startup can still fail if D3D11 device or swapchain creation is unavailable, and DXGI occlusion during present is treated as a skipped frame.
Window Lifecycle
create_window() creates the native/backend window and returns a generation-checked WindowId. The stored WindowInfo uses the actual size reported by the backend after clamping or native size queries, not just the requested WindowConfig.
destroy_window() destroys one live window and emits a destroy event. Destroying the last window does not stop the app. stop() destroys all remaining live windows, marks the app stopped, stops the backend, and closes owner-queue admission.
Window handles are generation checked. A handle for a destroyed slot becomes stale if that slot is later reused.
Owner-Thread Rule
The thread that calls new_app() is the App owner thread. Mutating operations, event draining, registry enumeration, owner-queue draining, and rendering must run on that thread. Calls from another thread fail with:
multiwindow: operation requires the owner thread
Use post() or try_post() to enqueue short callbacks from other threads, then call drain_pending() on the owner thread. drain_pending() runs at most the requested number of jobs and rechecks app status between jobs; if a job stops the app, later queued jobs are not run.
The simple read helpers status(), capabilities(), window_exists(), and window_status() do not enforce the owner-thread check.
Events
Events are explicit. Native events are not delivered to user code until the owner thread calls:
poll_events()to collect backend/native events into the App queue;drain_events()to retrieve and clear queued events.
Event kinds are:
.window_created: emitted bycreate_window()with the actual initial size;.window_destroyed: emitted bydestroy_window(),stop(), or accepted backend destroy notifications;.window_close_requested: emitted when the backend reports a close request;.window_resized: emitted afterresize_window()or accepted backend resize notifications with the actual size.
Backend events for stale or already-destroyed window handles are filtered.
Rendering
Rendering is explicit and optional. The render-facing API is compiled when the program uses the gg facade with -d gg_multiwindow, or when direct x.multiwindow callers opt in with -d x_multiwindow_render. Plain lifecycle and .mock imports remain dependency-light and do not pull sokol.gfx, X11, EGL, or OpenGL by default.
Backends that have initialized a renderer set Capabilities.explicit_swapchain and implement:
render_environment(window)for thegfx.setup()environment;begin_render(window)to make the window current and return aRenderFrame;end_render(frame)to present the frame;abort_render(frame)for error paths before presentation.
The low-level module does not call gfx.setup(), does not build passes, and does not draw. A direct caller is responsible for using RenderFrame.swapchain in a gfx.Pass, committing the frame, and then calling end_render(). The gg facade handles this for normal gg applications.
Relationship With gg
gg.App is the user-facing multi-window facade and is enabled with -d gg_multiwindow. It maps gg types to x.multiwindow, owns the sokol.gfx and sokol.sgl setup, maintains per-window draw contexts, runs the event/frame loop, and exposes draw_window() and run().
The checked-in example is:
./v -d gg_multiwindow run examples/gg/multiwindow.v
For Linux X11 native rendering, including Xvfb runs, add the X11 backend flag:
xvfb-run -a ./v -d gg_multiwindow -d x_multiwindow_x11 run examples/gg/multiwindow.v
For Wayland, build with -d sokol_wayland. The default build can still fall back to .mock when no enabled native backend is available. The example creates two gg windows, handles lifecycle events, and tolerates backends that reject programmatic resize.
Limitations
- X11 support is compiled only with
-d x_multiwindow_x11; without that flag, the X11 backend is unsupported and X11/EGL/OpenGL libraries are not linked by low-level lifecycle or.mockimports. - Wayland support is compiled only with
-d sokol_wayland; without that flag, the Wayland backend is unsupported and Wayland libraries are not linked. - Wayland hidden window creation (
visible: false) is rejected. - Wayland programmatic resize is currently unsupported.
- X11 programmatic resize is rejected for non-resizable windows.
- Native app creation can still fail even when plain capabilities report that a backend is supported, for example when a display cannot be opened.
- The mock backend is not a renderer and cannot produce swapchains.
- The module has no high-level input, layout, or drawing abstraction.
Validation
Useful checks while working on this module:
./v test vlib/x/multiwindow/multiwindow_test.v
./v -d gg_multiwindow test vlib/x/multiwindow
./v -d gg_multiwindow -d x_multiwindow_x11 test vlib/x/multiwindow
fn capabilities_for_backend #
fn capabilities_for_backend(kind BackendKind) !Capabilities
capabilities_for_backend reports capabilities without creating an App.
fn capabilities_for_backend_with_renderer #
fn capabilities_for_backend_with_renderer(kind BackendKind, require_renderer bool) !Capabilities
capabilities_for_backend_with_renderer reports capabilities with the same backend resolution policy used by new_app(...).
fn capabilities_for_config #
fn capabilities_for_config(config Config) !Capabilities
capabilities_for_config reports capabilities without starting an App while respecting Config.require_renderer for .auto backend selection.
fn new_app #
fn new_app(config Config) !&App
new_app creates a low-level multi-window App with the mock backend by default.
fn AppStatus.from #
fn AppStatus.from[W](input W) !AppStatus
fn BackendKind.from #
fn BackendKind.from[W](input W) !BackendKind
fn EventKind.from #
fn EventKind.from[W](input W) !EventKind
fn WindowStatus.from #
fn WindowStatus.from[W](input W) !WindowStatus
type AppJobFn #
type AppJobFn = fn (mut app App) !
AppJobFn is executed later by the owner queue while App is being pumped.
enum AppStatus #
enum AppStatus {
running
stopped
}
AppStatus describes the application lifecycle owned by App.
enum BackendKind #
enum BackendKind {
auto
mock
x11
wayland
appkit
win32
}
BackendKind selects the platform implementation. The .auto policy resolves to a concrete backend before startup/capabilities are reported.
enum EventKind #
enum EventKind {
window_created
window_destroyed
window_close_requested
window_resized
}
EventKind describes core window lifecycle events emitted by the backend.
enum WindowStatus #
enum WindowStatus {
invalid
alive
destroyed
}
WindowStatus describes the lifecycle of a registered window slot.
struct App #
struct App {
mut:
config Config
status AppStatus = .running
backend Backend
windows []WindowSlot
events []Event
owner_thread_id u64
state_mutex &sync.Mutex = sync.new_mutex()
owner &executor.Executor = unsafe { nil }
}
App owns the low-level multi-window registry and owner queue.
fn (App) status #
fn (app &App) status() AppStatus
status reports the application lifecycle state.
fn (App) capabilities #
fn (app &App) capabilities() Capabilities
capabilities reports the selected backend capabilities.
fn (App) create_window #
fn (mut app App) create_window(config WindowConfig) !WindowId
create_window creates a backend window and returns its generation-checked id.
fn (App) destroy_window #
fn (mut app App) destroy_window(id WindowId) !
destroy_window destroys one live window. The app remains alive when any window, including the last one, is destroyed.
fn (App) set_window_title #
fn (mut app App) set_window_title(id WindowId, title string) !
set_window_title updates the native title and then the authoritative App state.
fn (App) resize_window #
fn (mut app App) resize_window(id WindowId, width int, height int) !
resize_window requests a native resize and then updates the authoritative App state.
fn (App) window_info #
fn (app &App) window_info(id WindowId) !WindowInfo
window_info returns a snapshot of the authoritative App-side window state.
fn (App) window_ids #
fn (app &App) window_ids() ![]WindowId
window_ids returns live window ids in stable slot order.
fn (App) window_infos #
fn (app &App) window_infos() ![]WindowInfo
window_infos returns live window snapshots in stable slot order.
fn (App) window_exists #
fn (app &App) window_exists(id WindowId) bool
window_exists reports whether id currently points to a live window.
fn (App) window_status #
fn (app &App) window_status(id WindowId) !WindowStatus
window_status returns the lifecycle status for a valid generation.
fn (App) drain_events #
fn (mut app App) drain_events() ![]Event
drain_events returns and clears pending lifecycle events.
fn (App) poll_events #
fn (mut app App) poll_events() !int
poll_events lets the backend route native lifecycle events into App events.
fn (App) post #
fn (mut app App) post(f AppJobFn) !
post submits a short owner-thread callback if queue capacity is available.
fn (App) try_post #
fn (mut app App) try_post(f AppJobFn) !
try_post submits a short owner-thread callback without waiting for capacity. It follows x.executor's ! contract: queue-full and closed-queue states are returned as errors instead of a bool.
fn (App) drain_pending #
fn (mut app App) drain_pending(max_jobs int) !int
drain_pending executes up to max_jobs queued owner callbacks while the app is running. After stop(), accepted-but-pending callbacks are not executed and this returns multiwindow: app is stopped.
fn (App) stop #
fn (mut app App) stop() !
stop destroys live windows, closes owner-queue admission and marks the app stopped. Pending owner callbacks are canceled logically: public drain_pending() refuses to run them after stop().
struct Capabilities #
struct Capabilities {
pub:
backend BackendKind
mock bool
native bool
multi_window bool
owner_queue bool
explicit_swapchain bool
readback bool
d3d11 bool
metal bool
x11 bool
wayland bool
win32 bool
gl bool
}
Capabilities reports what the selected backend can do.
struct Config #
struct Config {
pub:
backend BackendKind = .mock
queue_size int = 128
require_renderer bool
}
Config configures a multi-window App.
struct Event #
struct Event {
pub:
kind EventKind
window_id WindowId
width int
height int
}
Event is always routed to a specific WindowId.
struct WindowConfig #
struct WindowConfig {
pub:
title string = 'V Window'
width int = 800
height int = 600
min_width int
min_height int
resizable bool = true
visible bool = true
high_dpi bool = true
borderless bool
fullscreen bool
}
WindowConfig describes one window at creation time.
struct WindowId #
struct WindowId {
slot int
generation u32
}
WindowId is an opaque generation-checked handle to a window.
fn (WindowId) str #
fn (id WindowId) str() string
str returns a diagnostic representation of a WindowId without exposing its fields as public mutable state.
struct WindowInfo #
struct WindowInfo {
pub:
id WindowId
status WindowStatus
title string
width int
height int
min_width int
min_height int
resizable bool
visible bool
high_dpi bool
borderless bool
fullscreen bool
}
WindowInfo is a snapshot of the authoritative App-side window state.
- README
- fn capabilities_for_backend
- fn capabilities_for_backend_with_renderer
- fn capabilities_for_config
- fn new_app
- fn AppStatus.from
- fn BackendKind.from
- fn EventKind.from
- fn WindowStatus.from
- type AppJobFn
- enum AppStatus
- enum BackendKind
- enum EventKind
- enum WindowStatus
- struct App
- struct Capabilities
- struct Config
- struct Event
- struct WindowConfig
- struct WindowId
- struct WindowInfo