Skip to content

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 App registry for multiple native windows;
  • generation-checked WindowId handles;
  • 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.gfx rendering.

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: true prefers X11 only when compiled with -d x_multiwindow_x11 and DISPLAY is set, then Wayland when compiled with -d sokol_wayland and WAYLAND_DISPLAY is set.
  • Linux without require_renderer prefers Wayland when compiled with -d sokol_wayland and WAYLAND_DISPLAY is set, then X11 only when compiled with -d x_multiwindow_x11 and DISPLAY is set.
  • If no native backend is selected, .auto falls 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 a gfx.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 requires wl_compositor and xdg_wm_base, rejects visible: 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 by create_window() with the actual initial size;
  • .window_destroyed: emitted by destroy_window(), stop(), or accepted backend destroy notifications;
  • .window_close_requested: emitted when the backend reports a close request;
  • .window_resized: emitted after resize_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 the gfx.setup() environment;
  • begin_render(window) to make the window current and return a RenderFrame;
  • 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 .mock imports.
  • 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 #

@[heap]
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 #

@[params]
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 #

@[params]
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.