compose

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 19, 2026 License: MIT Imports: 12 Imported by: 0

README

GoGPU Logo

compose

Pure Go multi-process composition library
Independent processes render UI content into offscreen buffers, and a single
compositor process composes them onto one display. Zero CGO.
First-class for gogpu/gg and gogpu/ui — works with any Go rendering library, or even non-Go modules speaking the wire protocol.

CI Coverage Go Reference Go Report Card License Zero CGO


Installation

go get github.com/gogpu/compose

What is compose?

The compose library lets Go applications combine content from several independent processes onto a single display.

Each module is a separate OS binary that renders into an offscreen buffer using gogpu/gg primitives or gogpu/ui widgets, then ships pixels over a Unix socket or shared memory ring buffer to a compositor process. The compositor positions and blits each module's frame onto the screen at its assigned slot.

This gives you:

  • Process isolation — a crash in one module never takes down the display
  • Hot-pluggable modules — start, stop, replace, and update individual modules without restarting the compositor
  • Cross-language modules — anything that can write RGBA to a Unix socket can participate, not just Go
  • Independent module lifecycles — each module ships, releases, and updates on its own schedule
  • Cross-platform — Linux, macOS, Windows, FreeBSD as first-class targets. Windows supports the Unix domain socket transport natively via AF_UNIX (since Windows 10 1803, April 2018), so the same net.Listen("unix", ...) works on every desktop OS. Future: Redox OS.
  • Zero CGO — Pure Go on the wire and on every supported platform. Shared memory uses POSIX mmap on Unix-likes and CreateFileMapping on Windows, behind a unified Go interface
package main

import (
    "github.com/gogpu/compose"
    "github.com/gogpu/gg"
)

func main() {
    // Module side: render a 400x120 frame, ship it to the compositor
    client, _ := compose.Dial("/tmp/compose.sock")
    defer client.Close()

    dc := gg.NewContext(400, 120)
    dc.ClearWithColor(gg.Transparent)
    dc.SetRGB(1, 1, 1)
    dc.DrawString("Hello from module", 20, 60)

    client.PublishFrame(compose.Frame{
        Name:   "hello-module", // human-readable; the wire ID is assigned at handshake
        Pixels: dc.Image(),
    })
}
// Compositor side: listen for module frames, place them onto a gogpu window
srv, _ := compose.Listen("/tmp/compose.sock")
srv.OnFrame(func(f compose.Frame) {
    // 'layout' is your application's slot-assignment helper that
    // decides where each module's pixels go on the screen.
    layout.Place(f.Name, f.Pixels)
})

Unix socket transport, wire protocol v1, LZ4 compression, pull-based flow control. See the Roadmap for current progress.

Why a separate library?

The compose library deliberately lives outside gogpu/ui and gogpu/gogpu:

  • Not UI-specific. A module can render with gg, with ui, or with any third-party Go rendering code. It can even be written in another language and pipe raw RGBA into the socket. Anchoring this in a UI library would be the wrong scope.
  • Not app-framework specific. gogpu is about "one process renders one window". Multi-process composition is a different problem area with different lifecycle, trust, and protocol concerns.
  • Platform-specific dependencies belong in their own module. Unix domain sockets, shared memory primitives (mmap on Unix, CreateFileMapping on Windows), ring buffers, build-tagged transport implementations — users of the UI framework should not pay for them transitively.
  • The IPC protocol is a stable compatibility surface. It needs its own versioning discipline and release cadence so that protocol changes do not force a UI framework release and vice versa.
  • Precedent. Qt ships Qt Remote Objects as a separate module. GTK ships Broadway as a separate display backend. Flutter separates Engine from Framework. Mature UI ecosystems consistently separate infrastructure layers from widget layers.

How is this different from multi-window?

Concern Multi-window in gogpu Multi-process compositor (this library)
Processes 1 N + 1
GPU Devices 1 shared N independent
Resource sharing pipelines, textures, buffers only pixels via IPC
Trust model full trust between windows process isolation
Crash isolation whole app exits one module dies, compositor lives
Cross-language Go only any language with POSIX sockets
Hot-reload not needed (one binary) first-class concern
Communication function calls (zero cost) IPC framing (small latency)
Use cases IDEs, dialogs, multi-doc editors smart mirrors, kiosks, modular dashboards, plugin hosts

The two are layers, not alternatives. A compose-based application can use multi-window internally if its compositor process needs to span multiple physical monitors. A module can use multi-window internally if it wants sub-windows. They compose cleanly.

Use cases

  • Smart mirrors and kiosks — modular displays where third-party modules render time, weather, calendars, notifications, transit, news
  • Modular dashboards — independent data sources (each as its own process, possibly on different teams or repos) feeding one operations display
  • Plugin hosts — applications that load untrusted third-party plugins and need crash containment
  • Cross-language UIs — Go compositor with modules written in Rust, Python, or C
  • Embedded HMIs — industrial control panels with hot-pluggable functional blocks
  • Live event displays — concert visuals, sports broadcasts, conference signage with independently-developed segments

Architecture

                 ┌────────────────────────────────────┐
                 │   Display  (one physical screen)   │
                 └─────────────────┬──────────────────┘
                                   │
                 ┌─────────────────┴──────────────────┐
                 │   Compositor process               │
                 │   (gogpu window owns the surface)  │
                 │                                    │
                 │   • accepts module connections     │
                 │   • assigns slots in the layout    │
                 │   • blits incoming frames          │
                 │   • handles hot-plug & lifecycles  │
                 └────┬────────────┬────────────┬─────┘
                      │            │            │
                Unix socket   Unix socket   shared memory
                      │            │            │
        ┌─────────────┴──┐ ┌───────┴──────┐ ┌───┴───────────┐
        │  Clock module  │ │ Weather mod. │ │ Notification  │
        │                │ │              │ │   module      │
        │  gg primitives │ │ gg primitives│ │ ui widgets    │
        │  1 Hz · static │ │ 0.1 Hz       │ │ 60 Hz · anim. │
        │  own process   │ │ own process  │ │ own process   │
        │  own GPU       │ │ own GPU      │ │ own GPU       │
        └────────────────┘ └──────────────┘ └───────────────┘

Each module owns its own process, its own GPU device (if it uses GPU at all), its own crash domain, and its own release lifecycle. The compositor is the only process that touches the actual display surface.

IPC transports

Pixels travel through one of two transports, chosen per module:

Transport Best for Throughput Setup
Unix domain socket Static / low-rate modules (clocks, weather, calendars) Hundreds of MB/s on Pi-class hardware Zero — just connect
Shared memory ring buffer Animated / high-rate modules (notifications, video, charts) Limited only by memory bandwidth, zero copy mmap (POSIX) or CreateFileMapping (Windows) behind a unified interface

Cross-platform support: Linux, macOS, FreeBSD, and Windows 10+ (which gained AF_UNIX support in the 1803 release, April 2018, so Go's net.Listen("unix", ...) works natively on Windows). Future: Redox OS when its Go toolchain matures.

The library deliberately avoids Linux-specific kernel APIs (io_uring, eventfd, Linux-specific shm segments) so the design ports cleanly across all desktop OSes. Where POSIX and Windows diverge — fd passing for shared memory is SCM_RIGHTS on POSIX and named handles on Windows — a thin Go interface with build tags hides the difference from module authors.

Wire protocol v1

64-byte fixed header, little-endian, cache-line aligned:

Offset Field Size Description
0 Magic 4B 0x434F4D50 ("COMP")
4 Version 2B Protocol version
6 MsgType 1B Frame, Handshake, Ack, FrameRequest, Resize, Disconnect
7 Flags 1B DirtyValid, Compressed, Keyframe
8 ModuleID 8B Compositor-assigned
16 Sequence 8B Monotonic frame counter
24 Timestamp 8B Monotonic nanoseconds
32 Width 2B Frame width
34 Height 2B Frame height
36 Stride 4B Bytes per row
40 DirtyRect 8B x, y, w, h (2B each)
48 PixelFormat 1B RGBA8, BGRA8
49 Compression 1B None, LZ4, Zstd
50 Reserved 6B Future use
56 PayloadSize 4B Compressed payload bytes
60 UncompressedSize 4B Original pixel bytes

Followed by PayloadSize bytes of pixel data (RGBA premultiplied, optionally LZ4-compressed).

The protocol is the stable compatibility surface of compose. Versioning is independent from the rest of the gogpu ecosystem so that protocol changes do not force a UI framework release.

Roadmap

Phase Features Status
Phase 1 Wire protocol v1, Unix socket transport, LZ4 compression, public API Complete
Phase 2 Reference examples: compositor + clock + notification (multi-process) Next
Phase 3 Shared memory ring buffer transport (zero-copy) Planned
Phase 4 Delta frames, compression negotiation Planned
Phase 5 Multi-screen layout, layered z-order, fade transitions Future
Phase 6 Cross-language module SDK (C header, Rust crate, Python) Future

Design principles

  1. Example first, library second. The reference example proves the pattern with real users before any API freezes. Library extraction happens after at least two real use cases agree on the shape.
  2. POSIX only. No Linux-specific kernel APIs. Portable to Redox OS, FreeBSD, and other POSIX systems.
  3. Zero CGO. Pure Go transports, pure Go protocol, single binary deployment per module.
  4. Process isolation as a feature, not an accident. Crashes contained, modules hot-swappable, third-party modules sandboxed by default.
  5. Independent releases. The compose library versions independently from gogpu, ui, and gg. The wire protocol has its own versioning discipline.
  6. Language-agnostic at the wire. A module can be a Rust binary or a Python script as long as it speaks the protocol.

Inspiration

The design of compose is informed by:

  • Android SurfaceFlinger — the system compositor that combines layers from independent processes onto Android displays
  • Wayland compositorswlroots, smithay, KDE KWin, GNOME Mutter
  • MagicMirror² — the JavaScript / Electron smart mirror project that the first user of compose is rewriting in Go
  • Qt Remote Objects — Qt's process-isolated component framework
  • GTK Broadway — GTK's HTML5 display backend, which composes widget pixels remotely
  • Flutter Engine vs Framework split — the architectural separation between the rendering substrate and the UI toolkit

Status and contributing

The compose library ships a working Unix socket transport with wire protocol v1, LZ4 compression, pull-based flow control, and hot-plug module lifecycle.

Known users: KiGo (modular Go application using offscreen rendering + multi-process composition).

If you have a use case for multi-process composition in Go, please join the compose RFC discussion or open an issue describing your scenario.

Part of the GoGPU Ecosystem

The compose library is part of GoGPU — a Pure Go GPU computing ecosystem.

Library Purpose
gogpu Application framework, windowing, multi-window
wgpu Pure Go WebGPU (Vulkan/Metal/DX12/GLES)
naga Shader compiler (WGSL → SPIR-V/MSL/GLSL/HLSL/DXIL)
gg 2D graphics with GPU acceleration
g3d 3D rendering (scene graph, PBR, GLTF)
ui GUI toolkit (22+ widgets, 4 themes)
compose Multi-process composition (this library)
systray System tray (Win32/macOS/Linux)

License

MIT License — see LICENSE for details.

Documentation

Overview

Package compose is a Pure Go multi-process composition library for the gogpu ecosystem. It lets independent OS processes render UI content into offscreen buffers and ship pixels to a single compositor process that blits them onto the screen.

The compositor model provides process isolation (a crash in one module does not take down the display), hot-pluggable third-party modules, and the possibility of cross-language modules — anything that can write RGBA to a Unix socket or shared memory segment can participate.

Quick Start

Compositor (server) side:

srv, err := compose.Listen("/tmp/compose.sock",
    compose.WithMaxModules(8),
)
if err != nil {
    log.Fatal(err)
}
defer srv.Close()

srv.OnFrame(func(f compose.Frame) {
    // Blit f.Pixels onto the compositor window.
})

Module (client) side:

client, err := compose.Dial("/tmp/compose.sock",
    compose.WithName("clock"),
    compose.WithFrameSize(400, 120),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

client.OnFrameRequest(func() {
    // Render and publish a frame.
    _ = client.PublishFrame(compose.Frame{
        Pixels: renderClock(),
        Width:  400,
        Height: 120,
    })
})

Architecture

Multi-window in gogpu (ADR-010) and the compose model are different concepts. Multi-window shares one GPU Device across N native windows inside a single process. The compose model is the opposite: N independent processes, each with its own GPU Device, cooperating over IPC to produce a single composed display.

The public API consists of five types (Frame, Server, Client, ServerOption, ClientOption) and three constructor functions (Listen, Dial, and With* options). All implementation details live behind internal/ boundaries.

Part of the GoGPU ecosystem: https://github.com/gogpu

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrClosed is returned when an operation is attempted on a closed
	// Server or Client.
	ErrClosed = errors.New("compose: server/client closed")

	// ErrNotAccepted is returned by Dial when the compositor rejects the
	// connection (e.g., due to capacity limits or policy).
	ErrNotAccepted = errors.New("compose: connection not accepted by compositor")

	// ErrModuleNotFound is returned by RequestFrame when the specified
	// module ID does not exist in the server's module table.
	ErrModuleNotFound = errors.New("compose: module not found")

	// ErrMaxModules is returned when the server cannot accept a new module
	// because the maximum module count has been reached.
	ErrMaxModules = conn.ErrMaxModules

	// ErrNameTaken is returned when a module with the same name is already
	// connected to the server.
	ErrNameTaken = conn.ErrNameTaken
)

Sentinel errors returned by Server and Client.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the module-side endpoint that connects to a compositor and publishes frames. All methods are safe for concurrent use.

func Dial

func Dial(addr string, opts ...ClientOption) (*Client, error)

Dial creates a Client that connects to a compositor at the given Unix domain socket address. Dial performs the handshake immediately: it sends a HelloMsg and reads the WelcomeMsg. If the compositor rejects the connection, ErrNotAccepted is returned.

Use ClientOption functions to configure the module:

client, err := compose.Dial("/tmp/compose.sock",
    compose.WithName("clock"),
    compose.WithFrameSize(400, 120),
    compose.WithFPS(1),
)

func (*Client) Close

func (c *Client) Close() error

Close disconnects from the compositor and releases resources. After Close returns, no more callbacks will be invoked.

func (*Client) ModuleID

func (c *Client) ModuleID() uint64

ModuleID returns the compositor-assigned module identifier. This is valid after Dial returns successfully.

func (*Client) OnFrameRequest

func (c *Client) OnFrameRequest(fn func())

OnFrameRequest registers a callback invoked when the compositor requests a frame. This enables pull-based rendering: the module renders only when asked. The callback is called on an internal goroutine; it must not block for extended periods. Only one callback can be active; subsequent calls replace the previous one. Pass nil to remove.

func (*Client) PublishFrame

func (c *Client) PublishFrame(f Frame) error

PublishFrame sends a frame to the compositor. The frame's ModuleID is automatically set to this client's assigned ID.

Returns ErrClosed if the client has been shut down.

func (*Client) SetCompression

func (c *Client) SetCompression(algo string)

SetCompression sets the codec used for frame payload compression. This allows the compositor to negotiate compression during or after handshake. Supported values: "lz4". Any other value uses raw.

type ClientOption

type ClientOption func(*clientConfig)

ClientOption configures a Client. Use With* functions to create options.

func WithFPS

func WithFPS(fps uint16) ClientOption

WithFPS sets the module's preferred frame rate. A value of 0 defaults to 1 FPS (suitable for static content). Default: 1.

func WithFrameSize

func WithFrameSize(width, height uint32) ClientOption

WithFrameSize sets the initial frame dimensions in pixels. The compositor may acknowledge different dimensions during handshake. Default: 400x300.

func WithName

func WithName(name string) ClientOption

WithName sets the human-readable module name sent during the handshake. The name is used for logging, slot assignment, and module identification. Names longer than 63 bytes are silently truncated. Default: "module".

type Frame

type Frame struct {
	// ModuleID is the compositor-assigned identifier.
	// Ignored on publish (server assigns).
	ModuleID uint64

	// Name is a human-readable module name (e.g., "clock", "weather").
	// Set during handshake, included in received frames for convenience.
	Name string

	// Pixels is the RGBA premultiplied pixel buffer.
	// Stride is always Width * 4.
	Pixels []byte

	// Width and Height of the frame in pixels.
	Width  uint32
	Height uint32

	// DirtyRect is the sub-region that changed since the last frame.
	// Zero value means the entire frame is dirty (keyframe).
	DirtyRect image.Rectangle

	// Timestamp is a monotonic nanosecond timestamp from the module's clock.
	Timestamp int64

	// Sequence is the monotonically increasing frame counter assigned by
	// the publishing client. It is carried from the wire protocol header
	// and can be used for change detection (e.g., comparing against a
	// previously seen sequence number in Snapshot).
	Sequence uint64
}

Frame is the fundamental data unit: a rectangular pixel buffer from a module. Users construct it to publish (module side) or receive it in callbacks (compositor side).

On the module side, ModuleID is ignored in PublishFrame — the server assigns the module's ID automatically. On the compositor side, OnFrame delivers a Frame with ModuleID populated.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the compositor-side endpoint that accepts module connections and delivers frames. All methods are safe for concurrent use.

func Listen

func Listen(addr string, opts ...ServerOption) (*Server, error)

Listen creates a Server that accepts module connections on the given Unix domain socket address. The server immediately begins accepting connections in a background goroutine.

Use ServerOption functions to configure behavior:

srv, err := compose.Listen("/tmp/compose.sock",
    compose.WithMaxModules(8),
    compose.WithCompression("lz4"),
)

func (*Server) Close

func (s *Server) Close() error

Close shuts down the server, disconnects all modules, and releases resources. After Close returns, no more callbacks will be invoked.

func (*Server) OnConnect

func (s *Server) OnConnect(fn func(id uint64, name string))

OnConnect registers a callback invoked when a module completes the handshake and becomes active. Only one callback can be active; subsequent calls replace the previous one. Pass nil to remove.

func (*Server) OnDisconnect

func (s *Server) OnDisconnect(fn func(id uint64, name string))

OnDisconnect registers a callback invoked when a module disconnects or crashes. Only one callback can be active; subsequent calls replace the previous one. Pass nil to remove.

func (*Server) OnFrame

func (s *Server) OnFrame(fn func(Frame))

OnFrame registers a callback invoked when a module delivers a frame. The callback is called on an internal goroutine; it must not block for extended periods. Only one callback can be active; subsequent calls replace the previous one. Pass nil to remove.

func (*Server) RequestFrame

func (s *Server) RequestFrame(moduleID uint64) error

RequestFrame sends a frame-request signal to the specified module (pull model). The module should respond with its next frame via PublishFrame.

Returns ErrModuleNotFound if the module ID is not connected. Returns ErrClosed if the server has been shut down.

func (*Server) Snapshot added in v0.2.0

func (s *Server) Snapshot() map[uint64]*Frame

Snapshot returns the latest frame from each connected module. Returns nil entries for modules that haven't sent any frames yet. The returned map is keyed by module ID.

Snapshot does NOT clear the mailbox — the frame stays until overwritten by a newer one from the same module. Compositors may call Snapshot multiple times per render tick; each call returns the same frame until the module publishes a new one. Use Frame.Sequence for change detection.

Thread-safe. Designed to be called once per compositor render tick.

type ServerOption

type ServerOption func(*serverConfig)

ServerOption configures a Server. Use With* functions to create options.

func WithCompression

func WithCompression(algo string) ServerOption

WithCompression enables frame payload compression on the Server. Supported values: "lz4". Any other value (including "") uses raw pass-through (no compression). Default: no compression.

func WithMaxModules

func WithMaxModules(n int) ServerOption

WithMaxModules sets the maximum number of concurrent module connections the Server will accept. Values less than 1 are clamped to 1. Default: 16.

Directories

Path Synopsis
internal
codec
Package codec provides frame payload compression and decompression for the compose protocol.
Package codec provides frame payload compression and decompression for the compose protocol.
conn
Package conn provides connection lifecycle management for the compose library.
Package conn provides connection lifecycle management for the compose library.
flow
Package flow provides pull-based frame pacing for the compose library.
Package flow provides pull-based frame pacing for the compose library.
protocol
Package protocol defines the wire format for gogpu/compose inter-process communication.
Package protocol defines the wire format for gogpu/compose inter-process communication.
transport/socket
Package socket provides a Unix domain socket transport for the compose protocol.
Package socket provides a Unix domain socket transport for the compose protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL