densa

Platform

Independent crates. One stack.

densa's crates fall into three lanes: data that keeps itself in order, an interface that draws it on the GPU, and a runtime that moves it between threads without waiting. Use one crate, or all of them.

01 — Architecture

Data flows down; dependencies too.

Data flows from Arrow sources through live tables into the grid and charts. A tree of widgets, laid out with flexbox, draws into a display list that is independent of any GPU API. Only densa-render and densa-window touch the GPU or a window, and no third-party types appear in densa's APIs beyond Arrow's.

Your applicationDATAINTERFACERUNTIMEArrow sourcesFiles, streams, query enginesdensa-filesCSV and Parquet, in the backgrounddensa-dataLive tables, views, aggregatesdensa-arrowTables and views on screendensa-gridData grid painterdensa-chartLines, candles, barsdensa-widgetsGrid, chart, inputs, splitters, dockingdensa-paragraphText layout and editingdensa-uiWidget tree, layout, eventsdensa-textFonts and glyph atlasdensa-drawFrame display listdensa-renderwgpu: one draw calldensa-windowNative window, inputGPUVulkan, Metal, DirectX 12, OpenGLdensa-syncWait-free data hand-offdensa-platformThreads: placement, prioritydensa-fmtFormatting without allocationdensa-allocProves frames never allocatedensa-coreColor, Point, Rect: the primitives every crate shares

02 — Crates

Each crate does one thing, and can stand alone.

Every densa crate is a library. Applications and demos live apart from them, and connectors with heavy dependencies live in their own repository.

Data6 crates

  • densa-dataLive Arrow tables: keyed upserts and deletes, sorted and filtered views and grouped aggregates kept up to date incrementally
  • densa-arrowArrow tables and live views in the grid, the grid widget and charts, with typed, allocation-free formatting
  • densa-filesCSV and Parquet files as Arrow batches, read on a background thread (in densa-connectors)
  • densa-wsWebSocket streams of JSON rows or Arrow IPC as Arrow batches, reconnecting on its own (in densa-connectors)
  • densa-flightArrow Flight and Flight SQL results as Arrow batches, every endpoint read as it arrives (in densa-connectors)
  • densa-datafusionSQL over Parquet, CSV, record batches and snapshots of live tables with Apache DataFusion, results handed over as they are computed (in densa-connectors)

Interface9 crates

  • densa-widgetsLabels, buttons, text inputs with undo and the clipboard, scroll views, splitters, panels, docking with saved layouts, menus and context menus, a data grid and a chart
  • densa-uiThe widget tree: flexbox layout, events, focus, themes and an accessibility tree, without a window
  • densa-gridA data grid over your own data: columns, alignment, cell styles
  • densa-chartLines, candlesticks and bars on number and time axes, with a crosshair
  • densa-paragraphShaping, wrapping, bidirectional text, selection and editing with input methods
  • densa-textFonts, rasterization and a glyph atlas that fits the GPU's limits
  • densa-drawA frame's display list: rectangles and glyphs, independent of any GPU API
  • densa-renderThe display list drawn with wgpu in one draw call
  • densa-windowA native window: input in, a frame out, screen readers through AccessKit

Runtime4 crates

  • densa-syncA wait-free triple buffer: the latest data from a producer thread to the interface
  • densa-platformCPU topology, thread placement and priority (Windows today)
  • densa-fmtNumber and date formatting into stack buffers, without allocating
  • densa-allocA counting allocator, to prove a code path allocates nothing

Foundation1 crate

  • densa-coreShared primitives every crate agrees on: Color, Point, Rect, Size

03 — Capabilities

From the data stream to the pixel.

Live tables

Upsert Arrow record batches by key. Views sort and filter the rows, groups aggregate them, and both follow every change incrementally instead of being recomputed.

  • Batches are kept as they arrive, shared rather than copied
  • Filters follow SQL's three-valued logic: comparisons with null are unknown
  • Each key's row keeps a record id, so a selection follows it as it moves
  • Grouped count, sum, min, max and mean, exact for integers and decimals
  • Files in CSV and Parquet load in the background as batches
movers.rs
use densa_data::{
    Aggregate, CompareOp, Filter, GroupSpec, LiveTable, Order, Scalar, ViewSpec,
};

let mut table = LiveTable::new(schema, "symbol")?;
let movers = table.add_view(
    ViewSpec::new()
        .filter(Filter::compare("volume", CompareOp::Gt, Scalar::Int(1_000)))
        .sort_by("change_pct", Order::Descending),
)?;
let sectors = table.add_groups(
    GroupSpec::new().group_by("sector").aggregate(Aggregate::sum("volume")),
)?;

table.upsert(ticks)?;      // new symbols insert, known ones update
table.delete(&delisted)?;  // an array of keys

let view = table.view(movers).unwrap();  // in order, after every change
let totals = table.groups(sectors).unwrap().to_batch()?;  // one row per sector

Widgets

A retained tree of widgets, laid out with flexbox, that takes pointer, keyboard and input method events. It runs without a window, so an interface is built, driven and tested headless.

  • Labels, buttons, text inputs, scroll views, splitters, panels and docking
  • A data grid that sorts by a click and keeps its selection on its row
  • Screen readers read every widget and press, type and scroll through them
  • Dark and light themes from one set of colors and sizes
Panels in tab groups: drag a tab beside a group to split it.

Charts

Lines, candlesticks and volume in stacked panes that follow the newest data, zoom around the pointer and pan. Fed from your own data or from a live table's view.

  • A line is drawn one span per pixel column: a million points cost a chart's width
  • Candles and bars denser than the pixels merge per column
  • Number and time axes with round ticks, and a crosshair with values
  • Charts and grids read the same live tables
1,000,000 pointsone span per pixel columnhowever many points fall in a columnits lowest to its highest
However many points fall in a pixel column, the line draws one span there.

Rendering

A canvas batches every rectangle and glyph of a frame into one instanced draw call. The data grid reads cells straight from your own data, so there is no model to copy into.

  • Implement GridSource for your data; the grid asks for visible cells only
  • Cells format into a stack buffer: drawing a frame allocates nothing
  • The glyph atlas grows a page at a time, capped to the GPU's texture limit
  • Pixel output verified in tests by rendering offscreen and reading it back
sensors.rs
use densa_fmt::write_fixed;
use densa_grid::{CellStyle, CellText, Color, GridSource};
use std::fmt::Write as _;

/// A monitoring table: one row per sensor.
struct Sensors<'a> {
    names: &'a [&'a str],
    celsius: &'a [f64],
}

impl GridSource for Sensors<'_> {
    fn rows(&self) -> usize {
        self.celsius.len()
    }

    // Called for every visible cell, every frame.
    fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle {
        let value = self.celsius[row];
        if column == 0 {
            let _ = text.write_str(self.names[row]);
        } else {
            let _ = write_fixed(text, value, 1);
        }
        let hex = if value > 80.0 { 0xF87171 } else { 0xE2E8F0 };
        CellStyle { color: Color::srgb(hex), background: None }
    }
}

Text

Paragraphs are shaped with parley, wrapped and aligned, and share one glyph atlas with the grid. Changing the width re-wraps without reshaping.

  • Bidirectional text, colored and sized spans, six alignments including justify
  • Click-to-caret, drag, word and line selection, keyboard navigation
  • Editing with input methods: preedit shown in place, candidates beside it
  • The system's fonts for Korean, Japanese, Chinese and more
Dense by design.Shaped, wrapped and justified text in every script:نص من اليمين إلى اليسار漢字とかな、한국어, ελληνικά.selection · input methods · right to left
One text engine for every script, with selection and input methods.

Data hand-off

A producer thread publishes the latest data; the interface picks up the newest snapshot once per frame. Neither waits for the other.

  • Wait-free on both sides: one atomic swap to publish, one to read
  • Every memory ordering weaker than the one used fails the loom model
  • Unsafe code checked under Miri; slots on separate cache lines
  • Thread pinning and priority for producers on hybrid CPUs (Windows)
handoff.rs
use densa_sync::triple_buffer;

let (mut writer, mut reader) = triple_buffer([0.0_f64; 64]);

// Producer thread: fill the back slot, then publish it.
std::thread::spawn(move || {
    writer.write(|rows| rows[0] = 101.25);
    writer.publish();
});

// UI thread, once per frame: switch to the newest snapshot.
if reader.update() {
    reader.read(|rows| draw(rows));
}

04 — Guarantees

Guarantees are tests, not promises.

Each guarantee below is a test or a check in the repository, run in continuous integration on Linux, macOS and Windows.

No allocation per frame

Drawing grids, charts and paragraphs, hit-testing and moving a selection run under a counting allocator; any heap operation fails the test.

No locks between threads

The triple buffer is model-checked with loom, and a mutation script proves every weaker atomic ordering is caught.

Correct pixels

GPU tests render offscreen and read the pixels back, including the glyph atlas as it grows and evicts.

Sound unsafe code

Every unsafe block carries a safety comment, enforced by Clippy; the concurrency and allocator crates run under Miri.

Bounded memory

Glyph memory has a hard limit. When it is full, the least recently used glyphs are reused, never one drawn in the current frame.

Tests that catch bugs

Tests are checked with deliberate mutations of the code: a test that lets a planted bug through is not a test.

Get started

Run the examples in a minute.