Replies: 1 comment
Implemented in v0.28.0Multi-window support is now shipped in v0.28.0. What's available
Exampleapp := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("Primary").WithSize(600, 400))
app.OnDraw(func(ctx *gogpu.Context) {
ctx.Clear(0.2, 0.3, 0.8, 1.0) // Blue
})
var created bool
app.OnUpdate(func(dt float64) {
if created { return }
created = true
w2, _ := app.NewWindow(gogpu.DefaultConfig().
WithTitle("Second").WithSize(400, 300))
w2.SetOnDraw(func(ctx *gogpu.Context) {
ctx.Clear(0.8, 0.2, 0.3, 1.0) // Red
})
})
app.Run()Remaining work (separate issues)
Thank you to everyone who reviewed the RFC. The architecture followed the design proposed here with minor adjustments (removed backward-compat adapter layers in favor of direct PlatformManager usage). Closing as implemented. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC: Multi-window architecture for gogpu
TL;DR
We propose adding multi-window support to
gogpuso a single application can open and manage N native windows from one process. The architecture follows the unanimously-validated pattern used by every mature desktop UI framework: one shared GPUDevice, oneSurfaceper window, monotonicWindowID, centralized event loop with per-window event routing. Implementation is approximately 1500 LOC of platform code plus a renderer split, with zero breaking changes for existing single-window applications.This RFC is the public draft of internal ADR-010. We are asking for community feedback — on API shape, window types, quit semantics, edge cases — before writing 1500 lines of platform code.
Why this matters
gogpucurrently allows exactly one window perApp. That is a hard ceiling on what you can build:Every other modern UI framework (Qt, GTK, SDL, winit, Cocoa, Win32 directly) treats multi-window as a baseline feature, not an opt-in. Until
gogpudoes the same, building a serious desktop application on top of it requires custom workarounds that bypass the window manager — which means no native chrome, no native window-manager integration, no native focus handling, and no native multi-monitor placement.Research summary
Before drafting this, we read the relevant code in seven mature multi-window implementations and wrote internal research reports for each. The conclusions converge cleanly:
SurfaceAboutToBeDestroyedlifecycle, close-as-request (window can reject), shared QRhi for resourcesVkDeviceperGdkDisplay,GskGpuDevicesingleton, per-surfaceGskRenderer,g_application_hold/releasequit refcountSDL_ClaimWindowForGPUDevice)WindowID,SetPropfor native-handle-to-Window routing, event dedup for resize/move eventsWindowIdas a stable handle,EventLooppolls all windows, per-window state in side maps keyed by IDGetPropW/XFindContextfor native-handle routing, polling at the platform layer (process-level)Arc<Device>shared across surfaces, no per-surface device, no per-window threadLockOSThread(Gio); we evaluated both and prefer centralized for our platform constraintsThe unanimous answer across all seven projects: shared GPU device + per-window swapchain is the right architecture for desktop applications. Per-window devices are at most a v2 optimization for performance-critical workloads, never a starting point.
Proposed architecture
Shared GPU context, per-window surface
graph TD GPU["<b>GPUContext (shared)</b><br/>Instance / Adapter / Device<br/>Queue / Shared Pipelines<br/>Shared Textures & Buffers"] GPU --> W1 GPU --> W2 GPU --> W3 W1["<b>Window 1</b><br/>Surface / Swapchain<br/>FrameState / Callbacks"] W2["<b>Window 2</b><br/>Surface / Swapchain<br/>FrameState / Callbacks"] W3["<b>Window 3</b><br/>Surface / Swapchain<br/>FrameState / Callbacks"] style GPU fill:#2d5a3d,stroke:#4a9,color:#fff style W1 fill:#1a3a5c,stroke:#4a9,color:#fff style W2 fill:#1a3a5c,stroke:#4a9,color:#fff style W3 fill:#1a3a5c,stroke:#4a9,color:#fffOne
Instance/Adapter/Device/Queuelives inGPUContext. Each window owns its ownSurface, swapchain, per-frame state, and callbacks. Pipelines and textures are shared viaGPUContext— create a texture once, use it from any window.WindowIDis a monotonicuint32Why monotonic, not the native handle:
SDL_GetNextObjectID)Window registry: a Go map
Pre-allocated to size 8 — zero allocations for ≤8 windows, which covers virtually every real desktop application. Go maps give O(1) lookup, superior to GLFW/SDL3's linked lists.
Platform interface split:
PlatformManager+PlatformWindowThe current
Platforminterface bundles process-level concerns (event loop, clipboard, system preferences) with per-window concerns (size, title, cursor, present). For multi-window, we split it into two:This mirrors Qt6's split between
QPlatformIntegrationandQPlatformWindow.WindowEventis tagged withWindowIDValue type returned by value from
PollEvents(). Zero allocations per event. Stale resize/move events are deduped before enqueuing (SDL3 pattern) so rapid window dragging cannot overflow the queue.Per-window callbacks: a struct, not N setters
Set once via
SetWindowCallbacks(id, callbacks).OnClosereturningboolis the close-as-request pattern from Qt6 — the application may reject a close if there is unsaved work.Surface destruction must precede native window destruction
GPU swapchain MUST be destroyed BEFORE the native window (HWND, NSView, X11 Window). Reversing the order produces driver crashes on every backend — this is the Qt6
SurfaceAboutToBeDestroyedlesson.WindowManagerenforces this by listening forEventSurfaceAboutToBeDestroyedand releasing theWindowSurfacebefore allowing the native window to die.VSync strategy
Sequential
Presentto N VSync surfaces causes N × VBlank delay. A 3-window IDE on a 60Hz display would render at 20Hz if every window usedFifo. Strategy:Fifo(vsync on)ImmediateorMailboxMatches Qt6 and Chromium.
Window types
Mirrors
Qt::WindowFlags, GTK'sGdkToplevel/GdkPopup, and SDL3 parent/child hierarchies.Quit logic
By default tool windows and popups do not participate in the "last window" count — closing them does not quit the application (Qt6 pattern). Apps can override.
Backward compatibility
Existing single-window code continues to work unchanged.
NewApp(config)still creates one window automatically.App.NewWindow(config)is the new API for additional windows. Zero breaking changes.Implementation phases
GPUContext/WindowSurfacesplit,WindowManager, backward-compat wrappergogpu/uiTotal estimate: ~1500 LOC across
gogpu, ~100 LOC ingpucontext. No changes needed inwgpufor Vulkan / Metal / DX12 — already multi-surface ready. GLES needs ~50 LOC forMakeCurrentswitching.Out of scope
Multi-process composition (
gogpu/compose)This RFC is about one process opening many native windows. It is not about many independent processes feeding pixels into one display — that is the opposite problem, and it is being addressed by a separate library,
gogpu/compose, currently in design phase.The two are layers, not alternatives. Every row of this comparison shows a fundamental difference:
DevicesNo row in this table is the same. They are mathematically opposite: this RFC is
1 process → N windows, whilecomposeisN processes → 1 display.The two stack cleanly. A
composecompositor process can use multi-window from this RFC internally if it wants to span multiple physical monitors (one OS window per monitor). Acomposemodule can use multi-window internally if it wants debug sub-windows. They compose, but they are not interchangeable: trying to make this RFC's multi-window cover the multi-process case would force every gogpu user to pay the cost of IPC and protocol versioning even for a simple dialog box; trying to makecomposecover the single-process case would force an IDE to spawn a separate process per panel and pay milliseconds of latency for what should be a function call.Per-window threads
Gio spawns a goroutine per window with
runtime.LockOSThread. We evaluated this and chose centralized polling instead, because macOS forces all AppKit calls onto the main thread and GL forcesMakeCurrentper thread anyway. Per-window threads do not buy enough on the platforms we support to justify the extra complexity. We may revisit for Vulkan/DX12 multi-threaded command encoding later.Per-window devices
Qt6 recommends per-window QRhi for performance-critical workloads, but it doubles GPU memory usage and breaks resource sharing between windows. Our default for v1 is shared device. Per-window devices are a v2 conversation if and when benchmarks show shared device is a real bottleneck.
Multi-GPU presentation
All windows share one
Deviceon one physical GPU for v1. Cross-GPU presentation is handled by the OS (Windows WDDM does this transparently with some latency). Per-GPUDevices are a v2 question if anyone has multi-monitor setups with monitors connected to different GPUs and shows a real workload that suffers from cross-GPU copies.What we want from this discussion
Before we begin implementation, we want feedback on:
API shape. Is
App.NewWindow(config)the right entry point, or should windows be created independently (Gio style: each window is a standalone object)? IsWindowIDasuint32the right level of typing, or should it be a struct with a name field?Window types. Is
Normal / Dialog / Tool / Popupcomplete? Do you have a use case for splash screens, child-of-parent windows, undecorated overlays, tooltips with their own lifecycle, that does not fit one of these four?Quit semantics. Is
QuitOnLastPrimaryWindowClosedthe right default, or should we follow Qt'squitOnLastWindowClosed(any window type counts) or GTK's explicithold/releaserefcount?Backward compatibility constraints we missed. If you have a
gogpuapplication today and would be affected by this work, please describe how — we want to know before shipping anything that breaks you.VSync edge cases on multi-monitor. Mixed refresh rates (60Hz + 144Hz, 60Hz + ProMotion 120Hz) — does our "primary
Fifo, secondaryImmediate" strategy work for you, or do you need per-monitor VSync handling?Anything we missed entirely. This RFC is the result of three weeks of research, but research only goes so far without real users testing the design. If you have built multi-window in Go before — in Fyne, Gio, walk, gioui, or anything else — tell us what worked and what didn't.
Status
gogpu/docs/dev/research/)If feedback in this discussion changes the design materially, ADR-010 will be amended and the change recorded here. If feedback confirms the direction, implementation begins in the next sprint.
Thank you for reading this far. Multi-window is one of the larger architectural commitments we have made on
gogpu, and we want to get the design right before writing 1500 lines of platform code.All reactions