atagen

monolith

public
Star0
Watch0
867b15d7c12f67caa1be52bad6353ec5d2584543
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
research/round2-reports.mdMarkdown371 lines44.3 KB

Round 2 research reports

Raw reports gathered while the tag/layout/animation design is being hardened. Paths prefixed smithay/ and niri/ refer to shallow clones at HEAD 2026-10-15 (smithay 023b78b, niri 45739bc).

Status

  • Smithay and niri internals
  • NVIDIA driver state
  • ext-workspace and decoration semantics
  • Protocol inventory

1. Smithay and niri internals

Smithay

1.1 Tearing. No async flip or tearing control. No match for PAGE_FLIP_ASYNC, ASYNC, tearing_control or tearing-control-v1 anywhere under smithay/src. The only atomic flags used are ALLOW_MODESET, TEST_ONLY, PAGE_FLIP_EVENT and NONBLOCK (smithay/src/backend/drm/surface/atomic.rs:830, :900, :943). Page flips go through DrmSurface::page_flip (smithay/src/backend/drm/surface/atomic.rs:868). DrmCompositor submit picks commit versus page_flip (smithay/src/backend/drm/compositor/mod.rs:2541). The narrowest patch point is page_flip plus the FrameState commit helpers (smithay/src/backend/drm/compositor/mod.rs:697), with a new FrameFlags bit plumbed from render_frame (:1686) into the try_assign paths. A custom commit path would also need drm-rs AtomicCommitFlags to gain an ASYNC bit first.

1.2 Deferred submit in Frame::finish. Allowed by design. Frame is defined at smithay/src/backend/renderer/mod.rs:270, with wait at :349 and finish returning SyncPoint at :362. OutputDamageTracker::damage_output_internal (smithay/src/backend/renderer/damage/mod.rs:475) only calls geometry, src, opaque_regions and damage_since, so there is no GPU readback between draws. render_output_internal draws elements, finishes once and returns the sync in RenderOutputResult. DrmCompositor::render_frame binds the swapchain dmabuf and calls render_output (smithay/src/backend/drm/compositor/mod.rs:2241), then stores the result sync into the plane config (:2316, :2337). GlesFrame::finish_internal (smithay/src/backend/renderer/gles/mod.rs:2552) exports an EGL fence when available (:2588), else glFinish and signaled (:2597). SyncPoint is an optional fence with wait, is_exportable and export (smithay/src/backend/renderer/sync/mod.rs:81, :96, :104). DrmCompositor attaches the exported fd as in-fence and reports needs_sync when fencing is unsupported or not exportable (smithay/src/backend/drm/compositor/mod.rs:230, :774). niri does the consumer CPU wait at niri/src/backend/tty.rs:1965. A Vulkan renderer can record into an op list and submit in finish, as long as wait is honored and finish returns a real fence when work is pending.

1.3 xdg configure/ack. ToplevelSurface::send_configure (smithay/src/wayland/shell/xdg/mod.rs:1489) allocates a SERIAL_COUNTER serial, pushes ToplevelConfigure to pending_configures and sends it. send_pending_configure (:1472) only sends on has_pending_changes. with_pending_state (:1698) seeds server_pending from current_server_state. Ack handling (smithay/src/wayland/shell/xdg/handlers/surface.rs:293) calls ack_configure (smithay/src/wayland/shell/xdg/mod.rs:221), which sets last_acked and retains newer configures. The toplevel pre-commit hook (:1585) copies last_acked into cached pending state when a buffer is present (:1617). The committed serial is readable via with_cached_state or with_committed_state (:1736, :1746), and niri reads it that way (niri/src/handlers/compositor.rs:322). A transaction hold works through add_pre_commit_hook (smithay/src/wayland/compositor/mod.rs:454, invoked at smithay/src/wayland/compositor/handlers.rs:276) plus add_blocker (smithay/src/wayland/compositor/mod.rs:554). Blocker states are at smithay/src/wayland/compositor/transaction.rs:61, and TransactionQueue::take_ready (:293) enforces readiness and per-surface ordering. blocker_cleared (smithay/src/wayland/compositor/mod.rs:641) wakes the queue.

1.4 Plane assignment. Tries primary, then cursor, then overlay (smithay/src/backend/drm/compositor/mod.rs:2851). Primary scanout is tried only for the single last element when no underlay overlaps and either the clear is black/transparent or the element covers the output and is opaque (:1995). Overlay requires ALLOW_OVERLAY_PLANE_SCANOUT (:3728), element kind ScanoutCandidate or Cursor (:3733), a free plane, compatible zpos and properties, and an atomic TEST_ONLY pass (:3809). Cursor assignment (:3022) checks kind, single assignment, sticky plane reuse, size hints and re-render on move, and falls back to the primary composite when too big. RenderFrameResult exposes primary_element, overlay_elements and cursor_element (smithay/src/backend/drm/compositor/frame_result.rs:40). Overlay assignment prefers sticky planes by z-index (:3791), which can block a later higher element if ordering shifts.

1.5 Vblank timing. No CRTC_SEQUENCE or vblank timestamp ioctl. Smithay only consumes page flip events carrying sequence and monotonic time, as DrmEventMetadata (smithay/src/backend/drm/device/mod.rs:493). Callers must invoke frame_submitted after the event (smithay/src/backend/drm/compositor/mod.rs:2614, smithay/src/backend/drm/output.rs:676). Prediction is the compositor’s job. Anvil estimates from last presentation plus frame duration (smithay/anvil/src/udev.rs:1262).

niri

1.6 Transactions. A refcounted Inner with AtomicBool completed plus Weak TransactionBlocker (niri/src/utils/transaction.rs:48, :159). Timeout is 300 ms (:18) with a calloop deadline timer (:86). Drop of the last Transaction completes it and removes the timer (:136). Per-window state is transaction_for_next_configure plus a pending_transactions vec of (serial, Transaction) (niri/src/window/mapped.rs:125). send (:1104) pairs serial to transaction, and take_pending_transaction (:460) completes all serials up to the committed one so fast resizes collapse to the latest. The comment at :466 acknowledges that a window waiting on a skipped intermediate size can look inconsistent. The close path creates a Transaction, passes the blocker into start_close_animation, and arms the timer only when not last (niri/src/handlers/compositor.rs:272). The Blocker impl maps live-and-incomplete to Pending (niri/src/utils/transaction.rs:159).

1.7 Custom shaders. Config keys are animations.window_resize, window_close and window_open custom_shader, consumed at niri/src/niri.rs:1800, :1810, :1820. Compilation wraps the user body with prelude, epilogue and rounding alpha (niri/src/render_helpers/shaders/mod.rs:222, :270, :313). Resize uniforms (:235) are niri_progress, niri_clamped_progress, geometry matrices, corner radius, clip flag, niri_tex_prev and niri_tex_next. Open and close expose input_to_geo, geo_size, geo_to_tex, progress, clamped_progress, random_seed and niri_tex (:281, :324). On compile error the setter warns and keeps the prior program (:250). Resize falls back to the built-in shader when no custom one is set (:199), while open and close skip the custom path.

1.8 Resize rendering. Mid-resize renders a crossfade between snapshots. Tile stores a ResizeAnimation with snapshot plus anim and fullscreen/expanded progress (niri/src/layout/tile.rs:143). A new size triggers take_animation_snapshot and a new ResizeAnimation (:272). Render builds ResizeRenderElement from texture_prev and texture_next with progress (niri/src/render_helpers/resize.rs:24, niri/src/layout/tile.rs:1127). Snapshots are lazy encompassing textures (niri/src/render_helpers/snapshot.rs:54). If the client never commits, the 300 ms deadline completes the blocker anyway, and later commits collapse via take_pending_transaction.

1.9 Animation model. Kinds are Easing, Spring and Deceleration (niri/src/animation/mod.rs:32), curves Linear, EaseOutQuad, EaseOutCubic, EaseOutExpo and CubicBezier (:44). SpringParams derive damping from damping ratio and stiffness (niri/src/animation/spring.rs:19), with analytic under-, critically and overdamped solutions (:141) and durations (:48, :110). Retarget via restarted (niri/src/animation/mod.rs:111) reuses the prior config, but the spring branch copies self.initial_velocity rather than the passed velocity (:128), so spring retargets do not carry fresh velocity while easing does after rate scaling. Likely a bug. Time comes from a lazy adjustable Clock (niri/src/animation/clock.rs:12) with rate and complete-instantly flags. Sampling is value_at(now) (niri/src/animation/mod.rs:249). niri advances animations at current time (niri/src/niri.rs:816), then freezes the clock at predicted presentation time before rendering (:4869), so rendered values sample predicted presentation time.

1.10 Frame scheduling. niri renders right after vblank or after an estimated-vblank timer. on_vblank (niri/src/backend/tty.rs:1630) feeds frame_clock.presented (:1804), then queues a redraw when needed or animations remain, else sends frame callbacks (:1806). Empty frames arm queue_estimated_vblank_timer for the target presentation time (:2032, implementation :3056). FrameClock::next_presentation_time (niri/src/frame_clock.rs:56) rounds up from the last presentation and returns now when VRR skipped more than one interval (:92). VRR toggling and on-demand VRR are at niri/src/backend/tty.rs:1354 and :2295. Cursor-only updates are skipped during VRR via SKIP_CURSOR_ONLY_UPDATES (:1951, smithay logic at smithay/src/backend/drm/compositor/mod.rs:2379). Early rogue vblanks arriving in under half a refresh are throttled (niri/src/utils/vblank_throttle.rs:33). No deadline scheduling.

1.11 Direct scanout and tearing. Scanout requires a fullscreen ScanoutCandidate with matching dmabuf feedback modifiers, enabled through ALLOW_PRIMARY_PLANE_SCANOUT_ANY by default (niri/src/backend/tty.rs:1934), cursor plane only, overlays off unless the debug flag enable_overlay_planes is set (:1941). Feedback tranches restrict to the Linear modifier for multi-GPU (:2952). No tearing path exists. No ASYNC commit and no tearing-control protocol.


4. Protocol inventory

wry paths are under src/ifs/ in /home/bolt/code/wry. smithay helpers are modules under smithay/src/wayland/. niri globals are created around niri/src/niri.rs:2500 plus custom implementations in niri/src/protocols/. Hand-verified that niri has no syncobj code and wry has no keyboard-shortcuts-inhibit.

protocolwrysmithay helperniripurpose
wl_compositor, subcompositor, shm, wl_output, wl_seat, wl_data_deviceyy (compositor, shm, output, seat, selection)ycore
xdg-shelly xdg_wm_base.rsy shell::xdgyapp windows
xdg-decoration, kde-decoration, xdg-dialogyy shell::xdg, shell::kdeydecoration mode, modal dialogs
wlr-layer-shellyy shell::wlr_layerypanels, wallpapers, lockers
ext-session-lockyy session_lockyscreen locker
linux-dmabufyy dmabufyGPU buffers
linux-drm-syncobjyy drm_syncobjnexplicit sync
drm-leaseyy drm_leasey (tty)VR headsets
ext-image-copy-capture plus capture sourcesyy image_copy_capture, image_capture_sourceymodern screen capture
wlr-screencopyyny customlegacy screen capture
wp-tearing-controlynntearing for games
wp-fractional-scaleyyyfractional scaling
viewporteryyycrop and scale
single-pixel-bufferyyy (test only)solid color buffers
presentation-timeyyypresentation timestamps
content-typeyyngame/video/photo hint
alpha-modifieryynper-surface alpha
wp-fifoyynFIFO present barrier
wp-commit-timingyyntimed commits
wp-color-management, wp-color-representationynnHDR and color spaces
wp-cursor-shapeyyynamed cursors
pointer-constraintsyyypointer lock for games
relative-pointeryyyraw motion for games
pointer-gesturesyyytouchpad gestures
wp-pointer-warpyynpointer warp
tabletyy tablet_managerystylus
text-input, input-methodyyyIME
virtual-keyboardyyysynthetic keys
wlr virtual-pointeryyysynthetic pointer
idle-inhibityyyblock idle
ext-idle-notifyyyyidle notifications
wp-security-contextyyysandbox identity
keyboard-shortcuts-inhibitnyygames/VMs grab hotkeys
xdg-activationyyyfocus tokens
ext-foreign-toplevel-listyyy customwindow lists
wlr-foreign-toplevel-managementyny customcontrol other windows
ext-workspaceyny customworkspaces for bars
wlr-output-managementyny customruntime output config
wlr-gamma-controlyny customnight light
wlr and ext data-controlyy selectionyclipboard managers
primary-selectionyy selectionymiddle-click paste
ext-background-effectnyyblur behind surfaces
xdg-foreignnyycross-client parenting
xdg-system-bellnynbell
xdg-toplevel-iconyynwindow icons
xdg-toplevel-tagyynstable window tag string
xdg-toplevel-dragynntab drag-out
ext-transient-seatynnremote desktop seats
xwayland-shellyy (xwayland feature)nXwayland association
xwayland-keyboard-grabny (xwayland feature)nXwayland grabs
xdg-outputyyyoutput geometry
wl-fixes, wl-drmyy fixesn (wl-drm)registry fixes, legacy drm
xdg-session-managementynnsession restore
jay-private (compositor control, tree query, tray, sync-file, screencast, xx-geometry)ynnjay IPC
hyprland-focus-grabynnfocus grab for popups
mutter-x11-interopnny customGNOME Xwayland interop

No smithay helper, so monolith writes these itself if wanted. wp-tearing-control, wp-color-management and wp-color-representation, ext-transient-seat, xdg-toplevel-drag, wlr-foreign-toplevel-management, ext-workspace, wlr-output-management, wlr-screencopy, wlr-gamma-control, xdg-session-management.


3. ext-workspace and decoration semantics

Spec at /tmp/monolith-research/ext-workspace-v1.xml. Source revisions used by the reader include gtk main ce6b23c, gtk-3-24 0f3aa45, libadwaita main 0531b4c, niri 45739bc. Two early unsourced sub-reports (claiming GTK4 supports xdg-decoration and river 0.4 keeps tags) were discarded as wrong after checking source.

3.1 ext-workspace-v1

Several active per group. Allowed. active is a per-workspace state bit (xml:313), an unset bit means inactive (xml:330), activate “may or may not deactivate all other workspaces in the same group” (xml:391-393), and commit exists so deactivate-plus-activate is atomic (xml:85-86).

Group membership. A workspace “may only ever be assigned to a single group at a single point in time” (xml:186-187, :248). Workspaces start unassigned (xml:72). Groups carry a set of outputs via output_enter/output_leave (xml:130-135), and the spec allows per-output sets or “all outputs (or any other arbitrary grouping)” sharing (xml:39-41). A zero-output group is not forbidden.

Capabilities. Group has create_workspace with no guarantees (xml:144, :217-218). Workspace has activate, deactivate, remove (only for ungrouped workspaces, xml:371-372) and assign(group) (xml:337-340). Unsupported requests are silently ignored (xml:155-161, :351-357). Per-workspace id (stable across sessions), name, N-dimensional coordinates, states urgent and hidden.

Mapping tags.

  • Per-output tags map to one group per output, one workspace per tag, every viewed tag active, coordinates=[n] for order, hidden for empty tags if wanted.
  • Global tags map to one group whose outputs are all outputs. Switching modes at runtime regroups inside one done batch (xml:98-102).

What it cannot express. A primary tag among the active ones (only active, urgent, hidden exist). Focused output. Which window has which tag (no toplevel linkage). Per-tag client counts. Window-to-output binding. Whether activate is exclusive or toggling. Whether the mode is global or per-output.

Client handling of several active workspaces.

  • Waybar handles it, per-button .active class (src/modules/ext/workspace_manager.cpp:387-410, :436 @4e207a0), has a deactivate action (:475-476), and also ships native river/dwl/mango tag modules.
  • quickshell handles it, and its docs explicitly mention tag-based WMs (src/windowmanager/windowset.hpp:38-53 @4f508be).
  • astal handles it, “any combination of these is allowed” (lib/workspace/src/workspace.vala:3-8 @cbcd9f4).
  • ironbar main has no ext-workspace backend and assumes exactly one focused monitor (src/modules/workspaces/mod.rs:503-508 @149c0d0). PR #1465 adds multi-focus support.
  • sfwbar renders per-item focus fine but keeps a single global focus pointer (src/workspace.c:11, 248-249 @a3594e5). Issue #389 open.
  • eww has no ext-workspace support.

Protocols with tag semantics.

  • river-status-unstable-v1 (river 0.3.x). Per-output focused_tags u32 mask, view_tags masks without window identity, urgent_tags. Seat sends focused_output and focused_view. Control is stringly typed run_command.
  • river 0.4 and later (0.5.0 released 2026-10-10). river-status and river-control are gone. river-window-management-v1 has no tags, the external WM owns them.
  • dwl-ipc-unstable-v2. Per-output tag(index, state{active,urgent}, clients, focused) events, active output flag, layout, title, appid, frame batching. Requests set_tags(mask, toggle) and set_client_tags(and, xor) on the focused client.
  • cosmic-workspace-unstable-v2. Extends ext-workspace with rename, tiling state, move_before/after, pin. Still no window mapping or counts.
  • Hyprland socket2 text events (workspacev2, focusedmonv2, activespecialv2, movewindowv2). One workspace per window, one regular workspace per monitor plus a special overlay (inferred).

Reader recommendation. Model a custom protocol on dwl-ipc v2, keyed on ext-foreign-toplevel handles with set_window_tag(toplevel, tag), a manager event for per-output vs global mode, a seat-level focused-output event, per-tag (state, clients, focused) events and frame batching. Keep ext-workspace-v1 as the compatibility layer for generic bars.

3.2 Client-side decorations

GTK3, GTK4, libadwaita.

  • Neither GTK3 nor GTK4 implements xdg-decoration. Both bind only org_kde_kwin_server_decoration_manager (gtk4 gdk/wayland/gdkdisplay-wayland.c:667, gtk3 :528).
  • gdk_wayland_display_prefers_ssd() is true only when that KDE global defaults to SERVER (gtk4 gdkdisplay-wayland.c:1270-1282). gtk_window_should_use_csd() returns !prefers_ssd on Wayland (gtk4 gtk/gtkwindow.c:3976-3996, gtk3 gtkwindow.c:6152-6177). GTK_CSD is ignored on Wayland.
  • gtk_window_set_titlebar forces CSD unconditionally (gtk4 gtkwindow.c:3195). AdwWindow/AdwApplicationWindow always install a titlebar gizmo (libadwaita src/adw-window.c:174-176, 325-328). So every libadwaita and headerbar app keeps its headerbar no matter what.

Tiled and constrained states.

  • GTK4 maps tiled_* to .tiled-* classes (gtkwindow.c:4674-4687) and parses xdg-shell v7 constrained_* into per-edge resizability (gdktoplevel-wayland.c:762-773, landed 4.19.1 via MR !8445). Without v7, non-tiled edges count as resizable (:565-586).
  • libadwaita on any .tiled* sets window radius 0 and drops the outline, but keeps a 1px $border_color line plus a transparent 20px shadow (src/stylesheet/widgets/_window.scss:49-63). Only maximized/fullscreen remove everything (:66-71). Whether that 20px becomes input area is unverified.
  • GTK3 binds at most xdg_wm_base v6. Adwaita squares corners on .tiled*.
  • Qt records tiled edges only (qtbase qwaylandxdgshell.cpp:169-180 @a4b1c40). Chromium tiled edges only (ui/ozone/platform/wayland/host/xdg_toplevel.cc:256-264).

Other toolkits.

  • Qt6 requests server-side unless frameless (qwaylandxdgshell.cpp:197-211), draws CSD only if refused.
  • Chromium/Electron default to CSD. SSD is a user opt-in that appears only when the xdg-decoration manager exists (crbug 40817442). Electron Wayland CSD default since v28 (electron#39582).
  • Firefox forces CLIENT on Wayland unless MOZ_GTK_TITLEBAR_DECORATION is set (widget/gtk/nsWindow.cpp:7224-7244), honours prefers_ssd when tabs are not in the titlebar (:855-882).
  • SDL2/SDL3 prefer xdg-decoration SERVER and fall back to libdecor only when the manager is absent (SDL3 SDL_waylandwindow.c:2451-2456, SDL#8173).
  • GLFW 3.4 prefers libdecor (init.c:67), else requests SERVER.
  • winit requests Server unless prefer_csd (winit-wayland frame.rs:176-195).
  • Java JBR WLToolkit defaults to GTK-drawn CSD, SSD with -Dsun.awt.wl.WindowDecorationStyle=server (WLDecoratedPeer.java:50-68).

Other compositors.

  • niri gates visibility of decoration globals on prefer-no-csd (src/niri.rs:2917) and sends all four tiled_* states as the “no CSD” signal (src/utils/mod.rs:421-475), with a tiled-state window rule override.
  • sway forces SERVER for tiled views and honours client mode only when floating (xdg_decoration.c:64-91 @1652c54). KDE manager defaults to SERVER (server.c:386-394).
  • river 0.4 forwards the client hint to the WM, CSD by default (river-window-management-v1.xml:552-600), no KDE protocol.

Reader takeaway. Advertise both xdg-decoration and KDE server-decoration defaulting to SERVER, always send all four tiled_* plus v7 constrained states. GTK/libadwaita windows become square and shadowless, headerbars and libadwaita’s 1px line remain.


2. NVIDIA driver state

Primary sources where available. nvidia-drm read from open-gpu-kernel-modules main (version.mk 615.78.08), lavapipe from Mesa main. URLs inline.

2.1 Current drivers

  • Latest is 615.78.08, New Feature Branch, 2026-10-07 (linuxiac).
  • Production branch R595, latest 595.104.02 (9to5Linux). R595 made nvidia-drm modeset=1 the default.
  • R610 (beta 610.43.02, May 2026) added the per-plane DRM color pipeline, FP16 EGL on Wayland, multi-planar YCbCr modifiers, mmap of dGPU dmabufs (Phoronix).

2.2 NVIDIA workarounds in local trees

  • wry src/state.rs:883 skips nvidia DRM devices when picking the initial render device unless nothing else works (likely a hybrid-laptop iGPU preference, uncommented).
  • wry src/backends/metal/video.rs:2010-2016 detects nvidia by driver name. src/backends/metal/present.rs:549, :582 never set IN_FENCE_FD on nvidia, any version.
  • wry src/backends/metal/video/metal_cm/metal_cm_plane.rs:347 treats nvidia colorop 1D LUTs as low precision unless WRY_MCM_NVIDIA_USE_LUTS (env.rs:232). metal_cm.rs:52 notes LUT size 1024.
  • smithay src/backend/drm/compositor/mod.rs:1183-1206, :1366-1389 disables IN_FENCE_FD below 560.35.03, version read from /sys/module/nvidia_drm/version (:4424), for ogkm#622.
  • smithay src/backend/egl/display.rs:932 ignores EGL_BAD_PARAMETER from eglQueryDmaBufModifiersEXT (nvidia since 520).
  • smithay src/backend/winit/mod.rs:317 resize-before-make_current ordering note.
  • smithay src/wayland/drm_lease/mod.rs:372 tolerates EINVAL on drop-master.
  • niri src/backend/tty.rs:73 orders Abgr2101010 first because nvidia dGPU to iGPU copies only handle X/ABGR.

2.3 Explicit sync

  • nvidia-drm sets DRIVER_SYNCOBJ | DRIVER_SYNCOBJ_TIMELINE (nvidia-drm-drv.c:1938). linux-drm-syncobj-v1 client support since 555.58 (2024-06-27), needs kernel 6.8+.
  • NVIDIA does not participate in dmabuf implicit sync. Undocumented what DMA_BUF_IOCTL_EXPORT_SYNC_FILE returns on its buffers. Assume empty/signalled and require syncobj from clients.
  • Importing a timeline syncobj fd as an OPAQUE_FD timeline semaphore fails on NVIDIA by design (“would never”, gamescope#1662). The portable path is syncobj ioctls on the DRM fd plus binary SYNC_FD semaphore import/export, checking the importable feature bit at runtime (forum). This is what smithay does. VK_EXT_external_semaphore_drm_syncobj (Vulkan-Docs#2692) appears unmerged.

2.4 Vulkan extensions on NVIDIA

extensionstate
VK_EXT_image_drm_format_modifiersince 515.48.07, YCbCr fixes in 580.94.18 and 610
VK_EXT_external_memory_dma_bufsince 515.48.07, needs modeset=1
VK_KHR_external_semaphore_fd SYNC_FDpresent, check feature bits
timeline semaphore as syncobjnot supported, by design
global priorityEXT since 455, KHR since 470. gamescope with CAP_SYS_NICE failed vkCreateDevice on 515 (gamescope#521). Query and fall back.
VK_EXT_descriptor_buffersince 515.49.25
VK_EXT_descriptor_heap595.58.03 stable
VK_EXT_queue_family_foreignsince 470.56.05
BDA, dynamic renderingcore 1.2/1.3

2.5 KMS

  • Async flips. async_page_flip = true (nvidia-drm-drv.c:364), atomic PAGE_FLIP_ASYNC maps to tearing (nvidia-drm-crtc.c:1683-1795). Kernel core only lets async commits change FB_ID, so expect fallbacks to sync.
  • VRR. VRR_ENABLED honoured (crtc.c:2740). The driver forces VRR-capable displays into VRR mode by default, conceal_vrr_caps hides the capability (README).
  • Vblank events. nvidia_drm vblank module parameter defaults to 0, so enable_vblank returns -ENODEV (nvidia-drm-os-interface.c:47, crtc.c:2587). WAIT_VBLANK and CRTC_QUEUE_SEQUENCE are off by default. Page-flip events still work, so timing must come from flip timestamps.
  • IN_FENCE_FD since 560.28.03 beta, multi-monitor freeze fixed in 560.35.03 (ogkm#622).
  • OUT_FENCE_PTR. Standard property unconfirmed. Vendor NV_DRM_OUT_FENCE_PTR signals when the buffer is flipped away, not on display (drv.c:467).
  • Overlay planes exist per hardware layer (crtc.c:3426-3437), count per GPU unknown, direct-scanout usability undocumented.
  • Cursor plane sized from maxCursorSizeInPixels (drv.c:351).
  • COLOR_PIPELINE on by default with R610+ and kernel 6.19+. Pipelines can contain non-bypassable colorops, which blanks the screen on HDR enable for compositors that don’t handle them. Escape hatch color_pipeline=0 (README).
  • HDR_OUTPUT_METADATA on connector (nvidia-drm-connector.c:56, :569), plus vendor NV_HDR_STATIC_METADATA.
  • DEGAMMA_LUT, CTM, GAMMA_LUT supported. COLOR_ENCODING, COLOR_RANGE not.

2.6 GBM and scanout

  • GBM since 495.44 via nvidia-drm_gbm.so, needs Mesa libgbm 21.2+.
  • Native buffers use block-linear modifiers. Without a modifier NVIDIA assumes pitch-linear. Intersect plane IN_FORMATS with renderer modifiers and use dmabuf feedback scanout tranches.
  • Same-GPU direct scanout on the primary plane works (smithay, KWin). No source on scanout from sysmem or iGPU buffers, assume a copy.

2.7 Hybrid laptops

  • Internal panel usually on the iGPU, HDMI often on the dGPU (zamundaaa 2026-07).
  • Compositors usually render on the iGPU and copy to nvidia outputs. Known slow, high CPU, ~50 fps clamping, regressions as recent as 590 (KDE 452219).
  • linux-dmabuf v6 per-output target device exists, NVIDIA’s side unreleased as of July 2026. No automatic mux switching, no public API to power down VRAM.
  • Requirements are a per-output render GPU, cross-GPU copies with format quirks, and not keeping the dGPU awake.

2.8 Known pitfalls 2025-2026

  • VRR plus cursor-only commits swing refresh and flicker OLEDs (niri#1214). Throttle cursor to the game’s frame rate under fullscreen VRR.
  • “Pageflip timed out” persists on 595.58.03 with Blackwell (forum). Freeze on monitor wake regressed in 570.124.04.
  • Suspend VRAM preservation via kernel notifiers is default in 615.78.08. Mixing with PreserveVideoMemoryAllocations and systemd units breaks resume. Treat lost device state after resume as recoverable.
  • NVIDIA heap doesn’t return freed buffers, niri climbs toward 1 GiB without GLVidHeapReuseRatio=0 (niri wiki). GL-specific, Vulkan unknown.
  • simpledrm alongside nvidia-drm needs fbdev=1, now default.
  • Xwayland and Electron need explicit sync (WaylandLinuxDrmSyncobj for Electron).

2.9 lavapipe for CI

  • dmabuf since Mesa 24.1. EXT_external_memory_dma_buf and EXT_image_drm_format_modifier need libdrm at build time and a winsys fd or writable /dev/udmabuf at runtime (lp_screen.c:200-207, lvp_device.c:1541-1556). Without udmabuf only import works and the modifier extension is hidden.
  • Only DRM_FORMAT_MOD_LINEAR is offered (lvp_formats.c:264, :278, :297). Tiled modifiers, multi-modifier negotiation and KMS are untestable.
  • Advertises descriptor_buffer, descriptor_heap, queue_family_foreign, timeline semaphores, global priority up to REALTIME (lvp_device.c:173-302, :1763).
  • Accepts a pipe as a sync_file where ANV rejects it, so fence bugs can pass CI. Treat it as a rendering-correctness test only.

2.10 Uncertainties

Overlay counts per GPU, standard OUT_FENCE_PTR, which version added the vblank parameter, CAP_SYS_NICE and global priority today, NVIDIA’s dmabuf implicit fences, direct scanout from sysmem. The README kms.html is stale against source in several places, source was trusted.


5. Smithay tearing status

GitHub data pulled unauthenticated (rate limit hit partway), line numbers against the local smithay clone 023b78b.

5.1 Upstream PRs and issues

  • #1325 wp_tearing_control_v1 helper (PolyMeilex). Open since 2024-02, mergeable. Drakulix “Wayland side LGTM”. YaLTeR wants compositor-decided policy. Unmerged only because the DRM side never landed.
  • #1335 initial async flip (PolyMeilex). Draft, conflicting, last activity 2026-02-28. cmeissl on 2026-02-16 “imo it does not make sense without the atomic backend”.
  • #1948 async page flip v2 (cmeissl). Open, conflicting, no maintainer activity since March 2026. Compositor passes desired mode into render_frame, overlays disabled while async, async only when the primary plane alone changed compatibly, async_flip_failed triggers sync retry. Testers report it working on AMD, niri on AMD, NVIDIA Turing (nvidia-open) and RTX 4060 (610.43, kernel 6.18). Design intent is to keep the decision compositor policy.
  • #1949 async formats (cmeissl, draft). On top of #1948, adds IN_FORMATS_ASYNC per plane and an async dmabuf feedback tranche, needed for Intel.
  • #2099 closed 2026-07-17, Drakulix redirected to #1325 and #1948.
  • Tracker #781 lists tearing-control as not implemented. No CHANGELOG entries.

5.2 drm-rs

AtomicCommitFlags::PAGE_FLIP_ASYNC exists in 0.14.0, 0.14.1 and develop (src/control/mod.rs:1524). DriverCapability::AtomicASyncPageFlip and PageFlipFlags::ASYNC exist too. atomic_commit passes bits straight to the ioctl. No drm-rs patch needed. The gap is smithay’s AtomicDrmSurface::page_flip hardcoding PAGE_FLIP_EVENT | NONBLOCK (smithay/src/backend/drm/surface/atomic.rs:898-903).

5.3 Other compositors

  • cosmic-comp pins upstream smithay cf43a02, no tearing. #248 open, “not a very high priority”. PR #2588 closed by Drakulix, who said it belongs in smithay and that composited tearing still syncs on the cross-GPU copy, gaining little latency.
  • niri #844 open. YaLTeR “We’re mainly waiting for Smithay/smithay#1335”, and in discussion #2450 “There’s nobody driving the Smithay PR… just someone to finish the Smithay PR.” No timeline.
  • Forks carrying #1948 plus #1949. urayde/niri (AUR niri-tearing-git, pins urayde/smithay@6df9a643, users report busy-looping frame callbacks while tearing), niri-spicy (losnoco/smithay spicy-master), alexparlett/smithay#4 rebase from 2026-10-10. Pinnacle, MagmaWM, StrataWM, fht-compositor have nothing.

5.4 Kernel constraints (torvalds master)

  • 6.8 added atomic async flips and DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. 6.11 allowed changing IN_FENCE_FD and FB_DAMAGE_CLIPS. 6.15 let drivers accept non-primary planes via atomic_async_check. 6.18 fixed a 6.15-6.17 regression for unchanged values on non-primary planes.
  • drm_atomic_set_property (drm_atomic_uapi.c:1190-1297). Connector and CRTC props only with unchanged values. Plane props unchanged except FB_ID, IN_FENCE_FD, FB_DAMAGE_CLIPS, on primary always and on others if the driver allows. smithay writes VRR_ENABLED each flip (atomic.rs:1198), fine since unchanged.
  • amdgpu fast updates only, overlays from about 6.15, cursor rejected, async disables PSR, IN_FORMATS_ASYNC in 7.2.
  • i915/xe require unchanged stride, modifier, format, rects and plane set. The first async request is silently done as sync. IN_FORMATS_ASYNC in 6.16.
  • nouveau primary only. nvidia-drm maps async to tearing (515-615), primary only, format handling unknown.
  • Async flips with NONBLOCK still deliver a flip event, at latch time not vblank. On failure userspace must retry sync. Async plus VRR unverified on all drivers.

5.5 Options

  • Wait. Nothing is moving. #1948 has had no maintainer action for about seven months.
  • Contribute upstream, carry #1948 meanwhile (reader’s recommendation). Rebase onto HEAD, which conflicts with commit_frame and handle_flip(&PreparedFrame) (smithay/src/backend/drm/compositor/mod.rs:2484-2600). Touch points FrameState::commit/page_flip (:732, :747), submit (:2541), handle_flip (:2570), overlay and cursor assignment (:3709, :3022), the render_frame diff base (:1784), DrmSurface::page_flip (surface/mod.rs:438), AtomicDrmSurface::page_flip (atomic.rs:868), capability probing in device/mod.rs. Then #1949 and #1325. About 400 lines across 12 files on a fast-moving file.
  • Side-channel commit, not viable. current_frame/pending_frame/queued_frame (:1069-1072) go stale and render_frame diffs against them (:1784). DrmDevice reports every flip as an anonymous DrmEvent::VBlank(crtc) (device/mod.rs:544-554), so frame_submitted (:2618) would retire the wrong frame and could re-render into a buffer still on screen. used_planes (atomic.rs:171) and swapchain accounting drift. Concurrent commits likely EBUSY (unverified).
  • Composited tearing (async-flip the compositor’s own buffer) works on primary-only drivers, gains little where a cross-GPU copy is involved, reasonable fallback on single GPU when the game can’t be scanned out.

6. wry master animation system (the reference design)

Snapshot of wry master (8b7ad7e6, 2026-08-11) extracted to /tmp/monolith-research/wry-master. Paths relative to that root. This supersedes wry-reports.md section 5 for animation semantics, which described the later jay-port branch.

6.1 Data model

Animations interpolate integer Rects per tree node, nothing else. AnimationState (src/animation.rs:28) holds windows (WindowAnimation), phased (PhasedWindowAnimation) and exits (ExitAnimation) plus a tick handle. WindowAnimation stores from, to, start_nsec, duration_nsec, curve, last_damage, retained (:480). PhasedWindowAnimation stores segments, start_nsec, per-phase duration_nsec, curve, last_damage, final_rect, route_edges, retained (:506). ExitAnimation adds a retained toplevel, frame_inset, source_body_size, active, layer (:666). Render queries visual_rect(node, layout, now) (src/renderer.rs:218, :481, :1090). Containers offset children through presentation_child_body (src/renderer.rs:471). Floats animate outer frame and body as two nodes (src/tree/float.rs:401).

6.2 Multiphase planning

A phase is a synchronized step list where every step is a pure horizontal or vertical move or scale (crates/layout-animation/src/lib.rs:1650). Diagonal or mixed-axis motion of a single rect is rejected. A plan is Vec<MultiphasePhase> (:362).

plan_forward (:696) tries single-action, then SpaceThenOrthogonalGrowth per axis, then HierarchyOrderedScales, then OrientationChange per axis, then SwapLanes per axis. plan_no_overlap_explained (:522) rejects empty bounds, empty windows, duplicate nodes, initial or final overlap (:651), and undersized targets via sane_min_size of bounds/8 min 1 (:674, :1777). It tries forward, then the reversed request with the plan reversed (:1682, :1699). Strategy enum NoOp, SingleAction, MixedSinglePhase, HierarchyOrderedScales, OrientationChange, SwapLanes, SpaceThenOrthogonalGrowth, ReversedForwardPlan (:317).

Batching partitions windows into motion groups by intersecting from.union(to) expanded by clearance (:618, :1750). Clearance is border or gap + 2*border (src/state/animations.rs:274). Groups with any Plain-style node run plain (:259), otherwise phased with plain fallback on diagnostic failure (:280).

validate_plan_continuous_diagnostic (crates/layout-animation/src/lib.rs:1444) checks each step starts at the current rect, no duplicate or unknown node per phase, pairwise linear motion overlap-free across the whole phase via rational time-interval intersection (:1540), and every node ends at its target. validate_phase_paths does the same for retarget paths (:572). Swap lanes snap sub-pixel remainders within SWAP_AXIS_SNAP_PX = 1 (:169).

6.3 Interruption

Plain set_target restarts from the current visual rect (src/animation.rs:190). Same target keeps running, from == new cancels (:206). No velocity stored or used, sampling is lerp_rect(from, to, curve(t)) (:751).

Batched multiphase interruption builds requests from visual_rect(node, old, now) (src/state/animations.rs:246) and tries, in order, existing phased retarget, bridged retarget, fresh plan (:280).

  • Existing retarget routes each node through its stored route_edges graph to the new target with BFS rect_graph_route and validates the combined paths (src/animation.rs:554, :599, src/state/animations.rs:320).
  • Bridged retarget routes from the current visual position back to the stale candidate.old, pads shorter bridges with holds, then appends a fresh plan from settled rects (src/state/animations.rs:355, :64).
  • Duplicate candidates within one layout pass coalesce to first old and last new with chained hierarchy (:32).

Repeated interruption re-roots every plan at displayed rects while route_edges accumulate history, so paths can reverse along prior lanes (tests phased_route_remembers_original_path_after_retarget at src/animation.rs:964, bridged_retarget_handles_second_rotation_interrupt at src/state/animations.rs:698).

6.4 Curves and timing

Linear or pregenerated piecewise cubic bezier (crates/layout-animation/src/lib.rs:8). Presets linear, ease, ease-in, ease-out, ease-in-out, unknown falls back to ease-out (:30). Custom beziers need finite values and x1, x2 in 0..=1 (:40). Sampling flattens to at most 33 points and lerps (:71, :86).

duration_ms is total for plain and exit, per phase for phased, so phased total is duration * phases (src/animation.rs:523). Zero duration or empty rect disables (:200, :255). Time is eng.now() (src/state.rs:598), sampled at queue, render and latch time, no predicted presentation time (src/animation.rs:731, src/renderer.rs:209).

Ticks are driven by output latch events. ensure_animation_tick attaches a latch listener to every output and damages full output extents (src/state/animations.rs:623). after_latch re-damages active unions and detaches when idle (src/animation.rs:725).

6.5 Client buffers

No resize transaction or blocking. tl_change_extents queues the animation from prev to rect and configures the client immediately (src/tree/toplevel.rs:193). The renderer stretches the live surface into the current visual body with a scissor equal to it, stretch set whenever content size differs (src/renderer.rs:774, :836, :1111). A new buffer mid-animation is simply stretched into the next sampled rect. Moves never snapshot, retained is None for plain, phased and spawn-in (src/animation.rs:230, :280, :296). Only close captures a RetainedToplevel (:87).

6.6 Spawn, close, other

Spawn-in grows from an empty rect at the target centre (src/animation.rs:739, test :1057). Spawn-out shrinks from the current visual rect to centre, rendering the retained texture plus border and background (:317, src/renderer.rs:530). Tiled resize/move use multiphase when plannable, else plain. Floating map uses spawn-in (src/tree/float.rs:164), floating move/resize queue plain animations for outer and body (:401), tile-to-float is linear (src/tree/toplevel.rs:1252). Mono collapse and child removal set animate_next_layout or wrap in with_layout_animations (src/tree/container.rs:1301, :1977).

6.7 Scheduling and damage

Start damages old.union(new) plus border (src/state/animations.rs:208). Multiphase start damages the union of all phase endpoints (:446). Each latch tick runs damage_active over last ∪ current ∪ target per animation, drops finished ones, returns liveness (src/animation.rs:419). Layout batching wraps perform_layout in begin_layout_animation_batch / finish_layout_animation_batch inside the container_layout task (src/tree/container/tasks.rs:14).

6.8 Config

[animations] with enabled (default false), duration-ms (160), style plain or multiphase (default multiphase), curve preset or [x1,y1,x2,y2] (default ease-out) (crates/toml-spec/spec/spec.generated.md:1081, crates/jay-config-schema/src/animations.rs:1). Runtime IPC mirrors these, duration clamped to 10000 (src/config/handler/options.rs:61, src/config/handler/dispatch.rs:395). Disabling clears state and damages root (src/state/animations.rs:564). Scoped overrides with_layout_animations, with_linear_layout_animations, suppress_animations_for_next_layout (:597, :606, src/state.rs:253).

6.9 Tests

About 40 planner tests in crates/layout-animation (swaps, rotations, lane direction and clearance, uneven splits, orientation changes, hierarchy-ordered scales, generated sibling swaps and redistributions, mixed single phase vs diagonal rejection, reverse equivalence, diagnostics, continuous validation). src/animation.rs:772 onward covers lerp, bezier, spawn/exit, per-phase duration, route memory, interruption and retargeting. src/state/animations.rs:643 onward covers plain-style groups, bridged interrupts and candidate coalescing.

6.10 Limitations visible in code

  • Only single-axis move or scale steps are expressible. Diagonal motion falls back to overlapping plain interpolation.
  • No-overlap holds only within a clearance-expanded motion group. Cross-output or cross-scale moves skip animation (src/state/animations.rs:177).
  • Durations are per phase, so complex plans exceed duration-ms.
  • Interruption is position-based, no velocity continuity.
  • Retained snapshots only for exits. Live resize visibly stretches the client buffer.
  • 1px swap snap and bounds/8 minimum shrink are deliberate imperceptibility heuristics.