A Rust port of drm-cxx — a layered Linux DRM/KMS display framework covering modesetting, plane allocation, scene composition, and scanout.
Status: phases 0–4 met, phase 5 (the v0.1.0-minimal ship gate) under way. The 0.1 releases ship ahead of that gate's last item, the smoke run on RK3588 and VisionFive 2 boards: 0.1.1 is current, on crates.io and docs.rs, and
CHANGELOG.mdsays what is in each. Twenty-one published crates and a test kit; 1,037 unit and integration tests plus 29 doctests on the host, 221 more that need a card, and the host suite again under qemu on the aarch64 and riscv64 cross lanes. All sixINVARIANTS.mdcontracts are pinned against a real device. A scene can build a frame, allocate planes, commit it, composite what it cannot place, and hand buffers back on the right vblank; EDID, color management, the cursor and capture paths are in and tested.The present spine, cross-output orchestration, GBM surface sources and client-side decorations have since landed. The port matches drm-cxx v5.0.0 (
2b260d0): of the 108 test files upstream has there, 75 are ported, 14 partial and 3 not applicable. Each of the 16 pending waits on hardware or a library this tree does not have: a working GPU render node, thevgem,vividorvicodecmodules, NVIDIA silicon for EGL streams, or the GL, Vulkan, V4L2 andGStreamerproducers that sit on top of them.TEST_PARITY.mdrecords which, per file, anddocs/parity-findings.mdrecords the three findings still open: two wait on hardware (P-30, P-14), and P-42 is by design. Seeplan.mdfor the full porting plan.It has run on more than vkms. amdgpu and vc4 (Raspberry Pi 5) by hand, a rockchip VOP2 for the case that needed it, the SA8155P's display controller for the scene and GBM suites, and — for the assumptions no lab can settle — the drmdb corpus of ~1,200 real devices across 50 drivers, replayed through this tree's own capability logic. Several findings here came from hardware nobody owns.
Phase 0 — met. Workspace, pinned toolchain, cross fragments, all CI lanes
green, drift linters negative-tested, and the dependency gap list decided
(docs/phase0-spikes.md). The one item left open then
— a trivial modeset under vkms — is now met by Phase 1: drmkit-core and
drmkit-dumb allocate, map, and register framebuffers against the vkms card in
the lane on every push.
Phase 1 — met. Its one carry-over closed in phase 4.
| Item | State |
|---|---|
drmkit-core, -modeset, -dumb, -sync, -fmt |
done |
| Ported unit tests for these subsystems pass | done — 10 upstream files ported, 1 partial |
Invariant 3 pinned (PageFlip::dispatch) |
done |
Invariant 6 pinned (Buffer::map) |
done |
Fuzz target seeded for IN_FORMATS |
done — ~1.5M execs per CI run |
| Fuzz target seeded for EDID-adjacent parsers | done in phase 4 — edid_blob, once the parser it targets existed |
The EDID parser landed with drmkit-display in phase 4 and the target was
seeded then, closing phase 1's one carry-over.
Phase 2 — met.
| Item | State |
|---|---|
PlaneRegistry::from_capabilities first |
done — every allocator test runs on synthetic fixtures, no hardware |
| Matching, allocator, warm-start + stability bonus | done |
| Invariant 4 hash-classification table | done — an exhaustive match, so an unclassified property does not compile |
| Externally-bound exclusion hooks | done — is_externally_bound / is_pinned skip placement |
test_matching, test_plane_allocator, test_layer, test_output ported |
done |
| Proptest: cardinality-first | done — against a brute-force oracle |
| Proptest: stability bonus never reduces placements | done |
Every card-dependent test is #[ignore]d so a developer machine without vkms
stays green, and the lane runs them with --include-ignored under
DRMKIT_REQUIRE_MASTER=1 — which turns "another client holds DRM master" from a
tolerated local condition into a lane failure, so a card test cannot pass in CI
by quietly doing nothing.
Phase 3 — met.
| Item | State |
|---|---|
LayerScene: build/finalize split, EAGAIN flow control, commit report |
done |
| Invariants 1, 2 and 5 pinned against a device | done — release_invariants_vkms |
| Invariant 4 pinned end to end | done — reachability and the steady-state write census |
| Real commit path: property map, modeset, emission | done |
| Composition fallback: blend, canvas, plane arming | done |
drmkit-scene-sources tier 1: dumb, external, ring, pool, cache |
done — GbmBufferSource waits on drmkit-gbm |
| T7 differential harness runs its first scenario | done — seven scenarios, six diff-clean; all four the plan requires are present |
Phase 4 — met.
| Item | State |
|---|---|
drmkit-gbm |
done |
drmkit-display: tone mapper and color curves |
done — diffed pixel-for-pixel against the reference |
drmkit-display: EDID via libdisplay-info |
done — behind the edid feature |
| Fuzz target for EDID | done — seeded, closing phase 1's carry-over |
allocator_torture thresholds asserted on vkms |
done, and measured: three of the six pin a caching mechanism and between them cover both; one is about placement rather than caching; one skips here; rapid_churn pins nothing at all (P-14) |
plane_stress measurement tool |
done — benches/plane-stress, CSV per frame |
drmkit-display: DriverProfile |
done — capability probe, never the driver name |
drmkit-display: HDR metadata and cache |
done — byte-identical to the reference's serializer over twelve vectors |
drmkit-capture: plane readback and PNG |
done — including RGB565, which the reference cannot read (#243) |
drmkit-cursor |
done — own .xcursor parser, hardware probe, ping-pong buffers |
drmkit-session |
done — sequencing over a Backend trait, libseat behind a feature |
drmkit-input |
done — xkb keyboard, auto-repeat, libinput seat with its log routed into drmkit's |
drmkit-display: connector and CRTC capabilities |
done — measured on vkms and amdgpu |
drmkit-display: mode lists, hotplug watch, color pipeline, scanout discovery |
done |
drmkit-planes: reading planes off a device |
done — behind a feature, so the crate stays device-free without it |
drmkit-scene-sources: cursor as an ordinary layer |
done — for hardware with no cursor plane |
Phase 5 — under way. The v0.1.0-minimal ship gate.
| Item | State |
|---|---|
| T7 scenario corpus, including the §4.6 stability scenario | done — all four the plan requires; §4.6 only began discriminating its own feature once insertion was added (P-27) |
INVARIANTS.md (Rust) |
done — six contracts, numbering and titles linted against upstream |
| T3: Miri on the pure crates | done — and drmkit-planes added, which the unsafe policy named and the lane had missed |
| T3: ASan on the fd-bound crates | done — 389 cases, including the va_list trampoline docs/phase0-spikes.md promised a run |
| T3: TSan | done, and the plan was right about why it is small. The three modules the C++ used atomics for are ring (no shared-state primitive at all), pool (two Mutexes) and seat (Rc/RefCell, which is not Send); no library code here spawns a thread. What remains is drmkit-modeset, whose tests spawn waker threads for the EINTR case invariant 3 covers — the one place a race could hide, and now checked |
| A validation process for the device pool | done — cargo xtask validate, capability baselines, and coverage attribution per device (docs/validation.md) |
| The drmdb corpus as a standing check | done — five gating rules and ten surveys over ~1,200 devices, run weekly and on the pull requests that touch what it reads |
| HIL smoke on RK3588 and VisionFive 2 | not started. Needs the boards |
| Docs | mostly. Rustdoc is clean under -Dwarnings and stays that way in CI; all 940 public items are documented, missing_docs having been enforced throughout; and each of the six invariants is cross-referenced from the crates that carry it. The crate front pages a consumer lands on have been read and rewritten where they described an earlier version of the library or omitted how to use it. What is left is the long tail — the individual item prose, which no one has read end to end |
The T7 harness runs identical scenario scripts through both implementations against one vkms instance and diffs the property writes that reach the kernel, captured at the ioctl boundary so neither side is instrumented.
Building it found eleven defects in this port — a missing modeset path, a
composition fallback that blinked layers off screen, inverted z-order on
hardware that cannot reorder planes — most of them invisible to every counter
and test in the tree. Six of the seven scenarios produce byte-identical traces; the
remaining one is a reproduction for an open upstream question and is expected
to diverge. The corpus now carries all four the plan requires — the fence-dup
scenario was the missing one, and writing it corrected a documented rationale
that measurement did not support. See docs/parity-findings.md.
Upstream first. drm-cxx is the origin of authority: anything this port surfaces is filed there rather than fixed only here, and the default for a bug is to reproduce the C++ behavior so the two agree byte for byte. Forty issues filed so far, plus three against third-party libraries.
Three drivers, then fifty. Every device test runs on vkms in CI and is
also run against amdgpu and vc4 (Raspberry Pi 5) by hand. They disagree in the
places that matter: rotation masks of 0x3f, 0xf and 0x35 — vc4's planes
do 0/180 and both reflects but not 90 or 270 — and color pipelines of
gamma-only, all three stages, and none at all. Several branches are reachable
on exactly one of them; see docs/parity-findings.md.
Three boards still only answer for three boards. validation/drmdb replays
the drmdb corpus — roughly 1,200 drm_info dumps
across 50 drivers — through drmkit's own capability types and decisions, and
gates on the assumptions this tree acts on. It has already closed a finding
that sat open for want of hardware (P-6, on two Allwinner boards) and found a
live defect on hardware nobody here owns (P-23, Tegra's cursor plane). See
docs/validation.md.
Upstream baseline: jwinarske/drm-cxx @ 2b260d0 (v5.0.0, PR #362).
A behavioral-parity port, not a redesign. The C++ tree is the reference
implementation; its test suite, benchmark thresholds, and INVARIANTS.md are
the contract. Where Rust's type system can turn a documented hazard into a
compile error it does — but the timing contracts still get their vkms pins,
because ownership cannot express "two commits deep".
Two files keep the port honest as upstream moves:
TEST_PARITY.md— every upstream test file mapped to its Rust counterpart and status. A test appearing upstream without a row fails CI.INVARIANTS.md— the six externally-relied-upon contracts, numbered and titled identically to upstream. Divergence fails CI.
Both are enforced by cargo xtask check.
Getting a build onto a board is written down too:
docs/cross-to-a-board.md. The committed cross
fragment assumes a target sysroot the host distribution may not provide, and
the way round it — borrowing one from the Pi itself — installs nothing on
either machine.
A third file records the flow in the other direction:
docs/upstream-findings.md tracks every defect and
improvement the port surfaces, the drm-cxx issue it was filed as, and what the
Rust side does in the meantime. drm-cxx is the origin of authority — a fix that
lived only here would make the two implementations diverge.
| Crate | Purpose | Phase |
|---|---|---|
drmkit-log |
Diagnostic surface: level, installable sink, per-category channels | 0 ✅ |
drmkit-time |
Monotonic clock abstraction over CLOCK_MONOTONIC |
0 ✅ |
xtask |
TEST_PARITY / INVARIANTS drift linters | 0 ✅ |
drmkit-fmt |
Format modifiers, IN_FORMATS tables, bandwidth cost (no_std) |
1 ✅ |
drmkit-sync |
sync_file fences for explicit synchronization |
1 ✅ |
drmkit-core |
Device handles, property cache, atomic requests, property blobs | 1 ✅ |
drmkit-modeset |
Mode selection, page-flip dispatch — invariant 3 | 1 ✅ |
drmkit-dumb |
Dumb buffers, lifetime mmaps — invariant 6 | 1 ✅ |
drmkit-planes |
Registry, layer model, matcher, allocator — invariant 4 | 2 ✅ |
drmkit-scene |
Scene, commit path, release ring, software composition — invariants 1, 2, 5 | 3 ✅ |
drmkit-scene-sources |
Tier-1 sources: dumb, external dma-buf, ring, pool, cache | 3 ✅ |
drmkit-gbm |
GBM allocation and dma-buf export | 4 ✅ |
drmkit |
Umbrella re-export, feature-gated | — |
Plus parity/: the T7 differential harness — an
LD_PRELOAD trace shim, a scenario runner per implementation, and the corpus.
The remaining ~25 crates and the phase that lands each are listed in
plan.md §5 and §8.
Requires Rust 1.94.1 exactly, pinned by rust-toolchain.toml; rustup
installs it automatically.
cargo build --workspace
cargo test --workspaceBoth need a drm-cxx checkout, from --ref or $DRMKIT_DRM_CXX:
export DRMKIT_DRM_CXX=/path/to/drm-cxx
cargo xtask check # parity + invariants
cargo xtask parity
cargo xtask invariantsThey fail on a missing reference rather than passing vacuously — a linter that silently skips is worse than no linter.
Per-target fragments are pulled in through Cargo's include config key:
cargo build --target aarch64-unknown-linux-gnu # RK3588, RK3566, SA8155P, Orin
cargo build --target riscv64gc-unknown-linux-gnu # StarFive VisionFive 2MIT, matching drm-cxx. See LICENSE.