rendering.md
January 26, 2026 · View on GitHub
Rendering pipeline overview
This document explains (at a high level) how Otto renders frames, starting from the underlying architecture and ending with the practical “where do I hook in?” points.
If you want to follow the code while reading:
- Element building / composition:
src/render.rs - Skia wrapper over Smithay GL renderer:
src/skia_renderer.rs - DRM/udev backend and frame submission:
src/udev.rs - winit backend (dev path):
src/winit.rs
If you are new to this codebase, a good reading order is:
- Skim “Rendering architecture” for the mental model.
- Read “Frame flow” to understand the per-frame execution.
- Jump to “Screenshare integration points” if you are working on capture/streaming.
Rendering architecture (layered mental model)
Think of Otto rendering as a few layers stacked on top of each other:
-
Smithay provides the backend plumbing
- Smithay owns the low-level rendering machinery (a
GlesRenderer) and the output infrastructure. - Conceptually, Smithay renders “into a buffer” that will later be presented on screen.
- Smithay owns the low-level rendering machinery (a
-
The backend decides what “buffer” means
- On winit, the “output” is a window inside another compositor; the buffer is presented to that host window.
- On udev/DRM, the “output” is a real connector/CRTC/plane pipeline; buffers are submitted to KMS.
-
Otto wraps Smithay GL with Skia
- Otto’s renderer uses Smithay’s
GlesRendererinternally. - It wraps the current EGL framebuffer (from the GL context) into a Skia surface.
- Otto then uses a Skia canvas to draw.
- Otto’s renderer uses Smithay’s
-
Otto draws mostly via a retained scene graph (
lay-rs)- The UI/scene is built as a retained scene graph managed by
lay-rs. - In rendering terms, one of the output render elements is a
SceneElement, which encapsulates “all our graphics”.
- The UI/scene is built as a retained scene graph managed by
Key pieces (what lives where)
- Backend: udev/DRM+GBM+EGL (primary), plus optional winit/x11 paths.
- Buffers & presentation: handled by Smithay + the backend (winit presents into a host window; udev submits to DRM/KMS).
- Renderer: Smithay
GlesRendererwrapped by Skia (src/skia_renderer.rs). This draws, imports dmabufs, and manages textures/surfaces. - Elements:
src/render.rsproducesOutputRenderElementsper output; it includes aSceneElement(fromsrc/render_elements/scene_element.rs) that renders thelay-rsscene. - Damage tracking:
smithay::backend::renderer::damage::OutputDamageTrackeris used per output so only damaged regions are rendered.
Backends in practice (winit vs udev)
-
winit backend
- Best for development.
- Output is a regular window; there is no hardware cursor plane.
- The cursor is rendered as part of normal composition.
- Does not offer all the functionalities of the udev/DRM path (no real outputs, no DMAbuffer, no screensharing).
- no touch gestures support.
-
udev/DRM backend
- Production/bare-metal path.
- Smithay/DRM manages connectors/CRTCs/planes, swapchains, and submission.
- The cursor is handled through Smithay so it can be optimized (including being placed on a dedicated DRM plane when available).
Frame flow (common path)
At a high level, rendering a frame looks like:
- Build the list of renderable elements for each output (
OutputRenderElementsinsrc/render.rs).- This includes the
SceneElementwhich renders thelay-rsscene graph.
- This includes the
- Hand those elements to Smithay’s
OutputDamageTracker. - Smithay drives the render pass for damaged regions.
- Otto’s
SkiaRendereruses Smithay’s GL renderer under the hood. - The current framebuffer is wrapped into a Skia surface, and Otto renders the scene (via
SceneElement) into the Skia canvas.
- Otto’s
- The backend presents the resulting buffer.
Screenshare integration points
Screenshar is available only in udev backend
After a successful normal render, the udev render loop blits a compositor Output (Screen) buffer into PipeWire-provided buffers.
- This happens in
src/udev.rsinrender_surface(...): whenoutcome.rendered && !screenshare_sessions.is_empty(), Otto dequeues an available PipeWire buffer and callscrate::screenshare::fullscreen_to_dmabuf(...), then triggersPipeWireStreamto queue the buffer. - Damage is forwarded when possible; the first frame (or buffer changes) forces a full-frame blit.