This file collects contributor-facing details for Umbriel: design goals, stack notes, testing, contribution rules, code style, source layout, commits, and debugging helpers. Umbriel shares its conventions with noctalia: same team, same style. If in doubt, match what noctalia does.
For dependencies and normal build commands, start with the installation guide. For what the project accepts and declines, read SCOPE.md: it is the reference used when triaging feature requests and unsolicited pull requests.
- Thin layer over wlroots 0.20 +
umbrielfx: lean on the libraries, do not reimplement them. - Domain-oriented C++23: one domain per directory, headers beside their sources, and
src/as the include root. - Effects (blur, shadows, rounded corners, animations) go through
umbrielfx; new visuals land there rather than as ad-hoc scene hacks. - Mechanism and policy stay separate. Example:
View::applySeatFocusis mechanism; focus policy lives inServer::focusView. - Keep the compositor event loop single-threaded.
Server::spawnrelies on that property to make itsforkand environment setup safe. - Packaging targets Nix first, plus plain system packages via pkg-config.
Direct project dependencies. Transitive dependencies are owned by their providing system packages.
| Layer | Library |
|---|---|
| Compositor framework | wlroots-0.20 |
| Scene graph and effects | umbrielfx (blur, shadows, rounded corners; maintained in-tree) |
| Wayland core | wayland-server, wayland-client, wayland-protocols, wayland-scanner |
| Input | libinput, xkbcommon >= 1.10.0, libeis >= 1.3, libsystemd (sd-bus, optional: the systemd-localed keymap) |
| Graphics | pixman, libdrm, OpenGL via wlroots |
| Text | cairo, pangocairo |
| Memory allocation | jemalloc (optional, glibc) |
| Config | tomlplusplus |
| JSON (IPC) | nlohmann/json |
| Xwayland | wlroots' Xwayland server (Xwayland spawned on demand), xcb, xcb-icccm, xcb-ewmh, xcb-shape; test-only xcb-render, xcb-xfixes, xcb-xtest, and libei >= 1.3 |
The README covers routine builds and running Umbriel. Contributor checks and specialized builds use:
| Command | Purpose |
|---|---|
just configure <mode> [prefix] |
Create or reconfigure a build directory and symlink compile_commands.json to it |
just asan |
Build with AddressSanitizer (see AddressSanitizer) |
just run <mode> [startup] |
Build and run a nested session, optionally spawning a command |
just test |
Run the Meson test suite: unit tests plus the umbrielfx suites |
just gpu-test |
Run the umbrielfx renderer ownership check against this machine's DRM render nodes; it needs a GPU, so just test does not cover it |
just check [filter ...] |
Run the headless compositor harness (tests/harness/check.sh), every check or the ones whose names contain a fragment: just check render/, just check drag, just check render/ -v. Checks run several at a time; -j16 or CHECK_JOBS=16 changes how many. Another build directory is mode=, as in just mode=asan check render/ |
just check-stress <name> [n] |
Run n copies (default 32) of one harness check at once, to expose races that load reveals |
just check-names |
List every harness check name. Builds nothing |
just lint [file ...] |
Build the compositor, unit tests, and harness clients, then run clang-tidy on everything or only the given files |
just format |
Format code (just format-code: the C++ and umbrielfx C sources, tests, and GLSL shaders, including examples/effects) and Markdown (just format-docs) |
just format-docs-check |
Report tracked Markdown and MDX files that just format-docs would change |
just install |
Build a release binary and install it with meson install |
just clean <mode> |
Remove a build directory |
just rebuild <mode> |
Clean and rebuild a build directory |
Set installation paths with just configure <mode> <prefix> before building. Build and install recipes reuse Meson's stored prefix. Their automatic configure call does not forward prefix=... overrides. For the system-wide prefix, build as your user and install with sudo meson install -C build-release --no-rebuild; just install also runs the release build and is suitable without sudo for a writable prefix.
The Nix development shell includes the clients and command-line tools used by the test suite:
nix develop
just test # unit and umbrielfx suites, through Meson
just check # every harness checkSelect harness checks by name or name fragment:
just check protocol/color_management # one check
just check protocol/color_management protocol/tearing # several
just check overview/ # every check in a group
just check protocol/color_management -v # keep passing-check output
just mode=asan check protocol/color_management # use build-asanTests live in three places, and which one a change belongs in follows from what it can observe:
tests/unit/ C++ unit tests, one binary per test, run by `just test`
tests/meson.build the unit test table and the harness client targets
tests/harness/check.sh the headless compositor harness, run by `just check`
tests/harness/lib.sh helpers a check sources with `source "$UMBRIEL_HARNESS_LIB"`
tests/harness/checks/ one script per behaviour it asserts
tests/harness/clients/ Wayland helper clients the checks drive
A unit test covers math and pure decisions (layout geometry, config classification, keybind parsing) and never needs a compositor. A harness check covers anything that only exists in a running compositor: real clients, real framebuffers, seat grabs, live reloads. Every unit test gets umbriel_pure_dep, and one that needs compositor code adds umbriel_core_dep in the third field of the unit_tests table in tests/meson.build. A test that is not in that table is not built and will rot unnoticed.
Test targets exist only where the tests feature option resolves to enabled. just configure leaves it at auto: unsanitized debug builds get the targets, release and asan builds get none. just test, just gpu-test, just check, just check-stress, and just lint switch the selected build directory to -Dtests=enabled when it lacks them, so they work in every mode; a plain just build of a release directory never compiles a test. Test binaries land in the build directory's tests subdir. Checks also drive IPC commands that exist only with the harness option (auto: debug builds, which includes asan), so just check and just check-stress also switch a release directory to -Dharness=enabled, and check.sh refuses a binary built without it.
check.sh runs every script in tests/harness/checks/ against its own dedicated compositor: one contained headless instance is booted per check, the check runs in its own process group with XDG_RUNTIME_DIR and WAYLAND_DISPLAY already pointing at that instance, and the harness kills the group and asserts the instance exited cleanly. Boot plus teardown costs about 80ms, so isolation is cheaper than the cleanup it replaces. That isolation is also what lets the harness run several checks at once, bounded by -j (default: the core count), starting the slowest first by the durations the previous run recorded. Reports stay in declaration order whatever order the checks finish in. Seven rules follow:
- A check must pass in a plain
just checkrun, with no environment overrides. - A check starts from a pristine instance (no windows, overview closed, workspace 1 focused,
$UMBRIEL_CONFIGholding the harness default) and owes nothing to whatever runs next. It appends the config it needs, spawns what it needs, and asserts. It must not restore config, close the overview, return to workspace 1, or reap its clients at exit: the harness owns all of that. A check that needs a different compositor lifecycle boots a private instance and tears it down itself, assession/quitdoes. - Never re-apply
XDG_RUNTIME_DIR,WAYLAND_DISPLAY, or-u DBUS_SESSION_BUS_ADDRESSper command. The harness already put the body in that environment. This is containment, not convenience: only IPC subcommands honourUMBRIEL_SOCKET, while every helper client is a Wayland client resolvingXDG_RUNTIME_DIRandWAYLAND_DISPLAY, so a missing prefix used to query the developer's live session instead of the instance. - Never retain
$!from a backgrounded shell function. Bash forks a subshell, so the captured pid is the wrapper and a signal to it leaves the client running. Background the client binary directly when a pid must be kept. - Never time a check with the wall clock. Wait for an end state with
umbriel settle: it replies once no animation runs, no arrange is pending, every mapped window has acknowledged and committed its latest configure, and every output has drawn a frame. Sample mid-animation on the animation clock:umbriel clock-freezestops animation time,umbriel clock-advance <ms>moves it and replies once every output has drawn that instant, and an animation started while frozen counts from the frozen instant, so long configured durations cost nothing (animation/builtin_window_stylesis the reference). Poll a client's log or IPC state for anything a client does. The pointer helper'smark <label>prints once every earlier command has been processed, andholdkeeps buttons and modifiers pressed until a line arrives on its stdin;pointer_hold,pointer_step, andpointer_releaseinlib.shwrap both, so a drag stays held across screenshots without a timedpause(drag/overhanging_card).check.shrefuses a fixedsleepof 0.2s or more outside a polling loop unless the line ends with# real time: <reason>, which is for compositor timers, helper-client pauses, and proofs that nothing reacts within a window.windows --jsonreports layout targets, not what is on screen, so it is not proof that motion has been drawn. - Analyse screenshots with
$UMBRIEL_PIXEL_PROBE(count,bbox,mean,max,pixel,size; its source documents the predicate syntax) rather than ImageMagick. Read pixels asgrimencodes them, which is what the compositor blended and shows: a colourspace conversion such asmagick -colorspace RGBonly moves blended values and makes thresholds harder to reason about. Pass a new or materially changed check throughjust check-stress <name>before relying on it. - Never depend on a machine-wide resource a sibling check could be using at the same time: a fixed port, a shared path outside
$UMBRIEL_RUNTIME_DIR, a named process matched withpkill, or the wall-clock cost of a neighbour. Everything a check needs lives in its own instance and its own runtime directory.
Checks live in topic directories under tests/harness/checks/, and a check's name is its path without .sh: overview/wheel. just check overview/ runs a topic. The topics are session (IPC, config, actions, spawn and activation), protocol (protocol globals and session lock), layout, workspace, scratchpad, transient, rule (window rules), overview, drag, focus, input (keyboard, input method, pointer grabs), output, render, effect, and animation (geometry motion and window lifecycle timing). File a check under the behavior it asserts, not under what it sets up: a scratchpad focus regression belongs in focus/. Name it without repeating its topic: scratchpad/named, not scratchpad/named_scratchpads. Checks reach repository files through $UMBRIEL_REPO.
A boot costs about 80ms and the pool runs checks side by side, so the suite's wall time is bounded below by its longest check: three six-second siblings finish in six seconds, and folding them into one check makes the whole suite wait eighteen. Split a check that grows past a few seconds instead of merging relatives to save a boot. After a full run the summary names the slowest checks and any over CHECK_BUDGET (default 8s).
A harness check that asserts a value a unit test computes is coverage in the wrong tier: it costs a compositor to re-derive what tests/unit already pins, and it fails a second time for the same bug. Assert layout arithmetic, config resolution, and parse results in tests/unit, and keep the check for what only the live compositor shows: that a real client's first configure agrees with the arrangement, that a reload reaches a mapped workspace, that the pixels land where the geometry said.
An instance has one output unless the check asks for more with a # harness: outputs=N directive in its header, which output/disable, output/dpms, and output/two_output_containment use. Output count is fixed when the compositor starts, so it cannot be a runtime config change. Single-output instances are what session/action_dispatch relies on to assert that directional output actions are rejected when there is nowhere to move.
A check that needs deterministic Xcursor images declares # harness: xcursor-theme=true. The harness creates an isolated theme with distinct nominal sizes 24 and 48, exports its path and name, and writes the matching cursor configuration before the compositor starts.
The compositor's config file watcher is off in harness instances, so a check applies a config change with umbriel msg config-reload. A check about automatic reloads declares # harness: config-watch=true, as session/config_reload and effect/animation do.
A headless session starts with no keyboard, so the harness connects a keyboard-only helper to each instance before its check runs and keeps it through teardown, the way a real session always has one. Without it the seat's keyboard capability would come and go with each pointer-client run, and clients would bind wl_keyboard too late for the first keys. A check about how keyboards themselves arrive opts out with # harness: keyboard=none in its header, as input/input_method_wheel and input/keyboard_keymap do.
A check that stops making progress is killed after 120 seconds, so the suite reports instead of hanging. Set CHECK_TIMEOUT to change the cap, and CHECK_VERBOSE=1 (or -v) to keep the full output of passing checks. Passing checks emit completion messages, summarized to a single dimmed line unless verbose output is enabled. Failing checks print their whole output and keep their runtime directory (compositor log, config, and per-client logs), with its path in the report.
- Before relying on a new or materially changed check, temporarily break the behavior it covers and confirm the check fails for the intended reason.
- Prefer assertions that observe a specific transition over end-state assertions that can pass through an unrelated fallback.
- Every file in
tests/harness/checks/boots its own compositor in every future run. Add one only for behavior that exists solely in a running compositor and that no existing check covers; otherwise extend an existing check or a harness client. - Scratch probes, measurements, and bug reproductions stay out of
tests/harness/checks/. Run them from a temporary path and delete them once they have served their purpose. - A check that pins wording, an implementation detail, or incidental behavior is deleted, not re-pinned to the new text.
Rendering, layout, animation, Wayland lifecycle, and compositor integration changes also deserve a functional or visual check in a running Umbriel session when practical. Do not write brittle pointer-event or mock-based tests to avoid that check. Two known limits:
zwlr_virtual_pointer_v1cannot generate touchpad gesture events, so changes to gesture state or gesture feel need a real input device in a running session.- The harness
layer-clientmaps a real exclusive zone (seelayout/struts). Extend it for layer arrangement and usable-area changes; interaction with real bars and panels still needs a running session.
VERSION is the canonical build version and Meson reads it directly. A release remains an explicit tag push: after the version bump is committed to the intended release commit, create and push the matching tag:
git tag -a "v$(<VERSION)" -m "Umbriel $(<VERSION)"
git push origin "v$(<VERSION)"The release workflow runs only for v* tags. It rejects a tag that does not exactly match VERSION at its target and creates the GitHub release when one does not already exist. It never creates or moves tags.
Keep each pull request to a single feature, fix, or refactor. Unrelated changes, including drive-by cleanups and formatting of untouched code, go in separate pull requests so each can be reviewed and reverted on its own.
Never add deprecated keys, migration readers, compatibility aliases, shims, or silent fallbacks to old names or forms. Removed configuration becomes unknown through normal validation. Alternate vocabulary is allowed only as a deliberate, documented first-class interface, never as compatibility behavior.
User documentation (docs/user/ and examples/config.toml) covers what a user configures or invokes: config keys, actions, IPC, and the behavior a user must know to use them. Update it in the same change when one of those is added, removed, renamed, or changes meaning. Bug fixes that make behavior match what users already expect, edge cases, and internal mechanics need no user documentation. Maintainer design notes live in docs/design/.
Comments and documentation explain what the code currently does and any non-obvious constraint a reader needs. Do not narrate history, migrations, rejected alternatives, or "why we don't do X"; git holds that. This applies to Markdown, meson.build, and code comments alike. If a comment is longer than the code it describes, cut it down.
This project uses clang-format for formatting, with the same .clang-format as noctalia-shell (LLVM base, 2-space indent, 120 columns, left pointer alignment, regrouped includes). The same file covers the umbrielfx C sources and the GLSL shaders (.frag, .vert, .glsl). Wrap hand-aligned tables such as matrices in // clang-format off and // clang-format on. Run just format before committing.
Markdown and MDX files are formatted by tools/remark/format.mjs, which parses them with the same remark plugins as the docs site (MDX, GFM, directives, custom heading ids, frontmatter) plus math, so it reads every file as the site and GitHub render it. Its dependencies are pinned in tools/remark/package-lock.json; the docs recipes need Node.js and npm and install them into tools/remark/node_modules on first use. Do not hard-wrap prose: the formatter writes each paragraph, list item, and table cell on one line, keeping GitHub alert markers (> [!NOTE]) on their own line. It refuses to write a result whose parsed document differs from the input beyond whitespace. CI runs just format-docs-check.
Static analysis uses clang-tidy with the same .clang-tidy check set as noctalia-shell. Run just lint (warnings are errors). Prefer the modern idioms the checks enforce: auto, ranges, std::print/std::format, make_unique, scoped locks, no C-style casts, uppercase literal suffixes (1.0F).
just configure <mode> creates a root compile_commands.json symlink to the selected Meson build directory, so clangd and clang-tidy see the build you are working in.
The repo also includes lefthook.yml. Run lefthook install to install the pre-commit hook; it runs just format-code when C, C++, or shader files are staged and just format-docs when Markdown or MDX files are staged, then refreshes the git index for tracked formatting changes.
| Convention | Example | |
|---|---|---|
| Files | snake_case | session_lock.cpp |
| Directories | snake_case | input/, workspace/ |
| Types / Classes | PascalCase | SessionLock |
| Functions / Methods | camelCase | focusView() |
| Variables / Parameters | camelCase | startupCmd |
| Private members | m_camelCase | m_sceneTree |
| Constants | k-prefixed constexpr | kLayerCount |
| Enumerators | PascalCase | FocusReason::Directional |
| Macros | SCREAMING_SNAKE_CASE | UMBRIEL_VERSION |
clang-format inserts braces around every control statement body and clang-tidy enforces the naming table for enumerators and private members.
Scoped enums (enum class) use an explicit std::uint8_t underlying type where it makes sense.
Getters are the noun, without a get prefix: toplevel(), mapped(), workspace().
Use [[nodiscard]] only where ignoring the result is likely a bug: a call with side effects that reports failure or hands back something the caller must own, consume or keep (a token, id or claimed node), and && builders. Mark result and status types with struct [[nodiscard]] Name instead of each function that returns them. Getters and pure functions go without it.
| Convention | Example | |
|---|---|---|
| TOML keys | snake_case | scroll_wheel_step, click_method |
| Enumerated values Umbriel defines | snake_case | top_left, button_areas, on |
| Action and IPC identifiers | kebab-case | window-close, layout-scroll-left |
| Identifiers owned by another system | verbatim | variant = "altgr-intl", output DP-1, app_id |
If the reader checks a value against a list, the value is ours and gets our spelling. click_method is one: libinput offers only a C enum, and its own tools disagree anyway (list-devices prints button-areas, debug-events accepts buttonareas).
If the value goes to another library untouched, keep that library's spelling.
- Headers use
#pragma once. - Forward-declare
wlr_*structs in headers; include the wlroots headers only in the.cpp. Wrap C includes inextern "C" { ... }when the header is not already C++-safe. - Wire wlroots signals with the paired-handler pattern: a
static void onEvent(wl_listener*, void*)trampoline that recoversthisviawl_container_ofand forwards to avoid handleEvent()member. Store thewl_listeneras anm_event{}member. - Include ordering follows clang-format regrouping: project
"..."headers first, then system<...>headers.
- Output and layout geometry are logical coordinates. Never derive compositor geometry from physical pixel dimensions on a scaled output.
- Fixed-vocabulary TOML sections use
Sectionreaders (src/config/section.h). When direct boolean parsing is required, checkis_boolean()before callingvalue<bool>(). Claim every recognized key before semantic validation can return or continue, so valid keys are never reported as unknown.
Pull request descriptions are checked automatically when they are opened, edited, reopened, or marked ready for review. Keep the ## Summary, ## Motivation, ## Type of Change, ## Testing, and ## Checklist headings and the Checklist wording from .github/PULL_REQUEST_TEMPLATE.md. The remaining sections are context only: fill them in, leave them empty, or delete them. In Type of Change, keep only the lines that apply.
Draft pull requests may leave checkboxes incomplete. Before marking a pull request ready for review, check exactly one change type other than Breaking change (add Breaking change alongside it when it applies), and check every item in the Checklist section. A pull request that is missing required template structure is commented on and converted back to a draft; add the missing content and mark it ready for review to run the check again. The check never closes a pull request.
src/
main.cpp
wlr.h
server/ display, backend, scene, protocol wiring, focus, and IPC
output/ per-output lifecycle and frame commits
input/ seat, keyboard, cursor, gestures, constraints, and IME relay
view/ XDG toplevels and popups, window rules, and decoration
layer/ layer-shell surfaces
lock/ ext-session-lock surfaces
xwayland/ Xwayland server, X11 window lifecycle, override-redirect windows
workspace/ per-output workspaces and scratchpads
layout/ scrolling, dwindle, master, and floating layouts, insert and drop targets
overview/ overview lifecycle and presentation
scene/ blur, shadows, text, banners, and internal overlays
config/ TOML parsing, resolution, reloads, and diagnostics
core/ animation, logging, process, and resource helpers
cli/ runtime inspection and command-line entry points
umbrielfx/ in-tree scene graph and GLES2 renderer (C, hard fork of SceneFX)
protocols/ vendored Wayland protocol XML
data/ session desktop entry
nix/ package and system integration modules
Conventions:
src/is the include root; headers live next to their sources.- Each directory owns one domain. Add new sources to the matching directory and register them in
meson.build. - Vendored Wayland protocol XML lives in
protocols/and is code-generated viawayland-scannerinmeson.build. - User-facing configuration documentation lives in
docs/user/; see User documentation. The reference pages are linked fromexamples/config.tomland the README.
umbrielfx/ is a hard fork of SceneFX, built by subdir('umbrielfx') from the root meson.build. Edit it like any other directory and commit alongside the compositor change that needs it. Never rebase it onto upstream SceneFX.
It is C compiled against wlroots' private struct layouts (-DWLR_PRIVATE=), so its compiler flags stay on its own target and never reach the compositor's C++23 units. Public headers are umbrielfx/include/umbrielfx/; private ones live in umbrielfx/internal/ and stay off the compositor's include path. See umbrielfx/README.md.
It extends wlroots' scene structs, so a scene node is only valid if umbrielfx allocated it. Every scene helper that creates or destroys nodes therefore lives in umbrielfx/types/scene/, including the thin wrappers taken from wlroots (surface.c, subsurface_tree.c, xdg_shell.c, layer_shell_v1.c, drag_icon.c, output_layout.c). Never call a scene helper from libwlroots; umbrielfx_scene_check_helpers() aborts startup if one resolves there. See docs/design/scene-helper-ownership.md.
- Every struct in
umbrielfx/include/umbrielfx/types/wlr_scene.hthat wlroots also declares stays a strict prefix extension: fields Umbriel adds go after every wlroots field, never interleaved.umbrielfx/tests/abi.cenforces this. - The
wlr_scene_*,wlr_egl_*,wlr_color_transform_*, andwlr_matrix_*symbols umbrielfx exports intentionally shadowlibwlroots. Never rename them to anumbrielfx_prefix: calls under the wlroots names would then resolve tolibwlrootsand allocate wlroots-sized nodes that umbrielfx reads as larger ones. - umbrielfx is pinned to one wlroots minor series. A wlroots bump means porting upstream
wlr_scene.cchanges by hand and re-reading thewlr_scene.hdiff for new or moved struct fields.
Its regressions run in their own suite:
just configure debug
meson test -C build-debug --suite umbrielfxFor a configured release build, enable tests first with meson configure build-release -Dtests=enabled.
Renderer ownership and descriptor stability need a real GPU, so that check is a tool rather than a suite entry: umbrielfx-renderer-test takes DRM render nodes as arguments, and just gpu-test passes every node the machine has.
just gpu-test # every /dev/dri/renderD*
just mode=release gpu-test # from another build directory
build-release/umbrielfx/umbrielfx-renderer-test /dev/dri/renderD128 /dev/dri/renderD129Every selected node must open and initialize or the run fails. The EGL and scene ABI checks need no GPU access. The compositor's backend-manager test checks native startup and hotplug filtering with simulated device I/O, without taking control of a seat.
- Every build writes every level, debug included, to
$XDG_CACHE_HOME/umbriel/umbriel.log(fallback~/.cache/umbriel/umbriel.log). The file is deliberately unfiltered (src/core/log.cpp:270); only the console honours the build's minimum level, which is debug for debug and ASan builds and info for release (src/core/log.cpp:21-25). So a release session still records debug diagnostics to the file while keeping stderr quiet. wlroots messages ([wlr]) are recorded from info up; setUMBRIEL_WLR_DEBUG=1before starting the compositor to include wlroots debug output as well. The first startup record includes the release version and commit revision, which helps identify the exact binary behind a report.
just configure asan creates build-asan as a debug build with -Db_sanitize=address. The recipes below enable the test targets in that directory on first use, so every workflow runs there:
just asan # build the instrumented binary
just run asan [startup] # nested session under ASan
just test asan # unit tests plus the umbrielfx suites
just mode=asan check [filter] # harness checks, one instrumented compositor per checkb_sanitize is a global Meson option, so umbrielfx's C sources are instrumented alongside the compositor and a report points into them with file and line. just configure also repoints the compile_commands.json symlink, so returning to unsanitized work needs just configure debug.
In asan mode those recipes prepend abort_on_error=1:detect_leaks=0:halt_on_error=1 to ASAN_OPTIONS. A later key wins, so ASAN_OPTIONS=detect_leaks=1 just mode=asan check overview/wheel overrides one default and keeps the others.
Leak detection is off because Mesa leaks its EGL display setup on every renderer teardown, under dri2_initialize below wlr_egl_create_with_drm_fd: about 400 KB in 7900 allocations when the compositor exits, and 115 KB in the umbrielfx color tests. Left on, every harness check fails at teardown with compositor exited with status 1 and the 14 color tests abort. Enable it for the one scope you are chasing a leak in, and read past the libEGL_mesa frames.
Reports go to stderr, which for just run asan is the parent terminal. Where stderr is not readable, as in a session started by a display manager, redirect them: ASAN_OPTIONS=log_path=/var/tmp/umbriel-asan just run asan writes /var/tmp/umbriel-asan.<pid> instead.
An ASan build still links jemalloc when it is installed, since -Djemalloc is independent of b_sanitize, but libasan precedes it in the link order and services every allocation. The jemalloc: narenas=... startup record in an ASan build therefore describes an allocator nothing uses; configure with -Djemalloc=disabled to drop it.
tracy mode is a release build with -Dtracy=enabled, debug info, and frame pointers, so Tracy's sampling can walk call stacks. It compiles the Tracy zones in umbrielfx's render pass and in the compositor's per-frame path; every other build mode compiles them to nothing.
just tracy # build build-tracy/umbriel
just mode=tracy run # nested instrumented session
tracy-capture -o /tmp/umbriel.tracy -s 10 -f # 10 seconds, overwrite
tracy-csvexport /tmp/umbriel.tracy # name, counts, mean_ns, max_ns per zoneThe client half of Tracy has to be built rather than installed. Both of its upstream build systems default TRACY_ENABLE and TRACY_ON_DEMAND to off, and distributions package those defaults, so the packaged libtracy.so links successfully and records nothing. -Dtracy=enabled probes for ___tracy_connected and fails configuration rather than producing a silently empty trace. The GUI and the CLI tools can stay the packaged ones.
git clone --depth 1 --branch v0.14.1 https://github.com/wolfpld/tracy
cmake -S tracy -B tracy/b -GNinja -DCMAKE_BUILD_TYPE=Release \
-DTRACY_ENABLE=ON -DTRACY_ON_DEMAND=ON \
-DCMAKE_INSTALL_PREFIX="$HOME/.local" -DCMAKE_INSTALL_LIBDIR=lib
cmake --build tracy/b && cmake --install tracy/bThat installs libTracyClient.a, which links statically, so the instrumented binary needs no library path at runtime. tracy mode adds $HOME/.local/lib/pkgconfig to the pkg-config path; put a tracy.pc there whose Libs is -L${libdir} -lTracyClient and whose Cflags points at ${prefix}/include/tracy.
Which zones exist, what a capture can and cannot answer, and which workloads to measure with are in Render performance.
The CLI doubles as a runtime inspection and IPC surface against a running compositor:
umbriel -v | --version # print the release version and commit revision
umbriel config validate [-c <config>] # check a config file without starting
umbriel config schema [--json] # list every config key with its type and default
umbriel outputs # list connectors and modes
umbriel windows # list windows (focused *, urgent !)
umbriel focused-window # print the focused window (app id and title)
umbriel effects # inspect effect programs, pools, and owner selections
umbriel workspaces # list workspaces and their layouts
umbriel subscribe <events> # stream events as JSON lines until closed
umbriel layers # list layer-shell surfaces
umbriel keyboard-layouts # list configured keyboard layouts
umbriel msg --help # list actions available to `msg` and keybinds
umbriel msg <action> [args...] # send an action to the running compositorwindows, focused-window, effects, workspaces, layers, outputs, keyboard-layouts, and msg accept --json / -j for machine-readable output. subscribe is always JSON; see docs/user/ipc.md for the families and payloads.
Use Conventional Commits: type(scope): imperative summary, with lowercase types such as feat, fix, refactor, chore, test, or docs and a focused subsystem scope when it helps.
Default to the summary line alone. Add a body only when it carries information a reader would otherwise miss: a non-obvious root cause, a constraint that forced the approach, a behavior change users will notice, or a Fixes #N reference. Do not restate the diff or list touched files.