denise-macos 0.22.0

Embeddable NSView backend for Denise, for a panel inside a Cocoa application.
Documentation

denise-macos

crates.io docs.rs Licence

An embeddable Cocoa view backend for Denise, a direct-rendering UI toolkit in Rust for embedded Linux and systems without a desktop environment.

Not a way to ship Denise on a Mac. denise-winit already previews on one, and a desktop application should use a desktop toolkit. This exists for the same reason the Win32 control does: an existing Cocoa application that wants a Denise panel inside it, next to its own views, with the host owning the window and the run loop.

# #[cfg(target_os = "macos")]
# fn demo() -> Result<(), denise_macos::Error> {
use denise::Size;
use denise_macos::ViewSurface;

// The host has a view; Denise has a surface the size of its backing store.
let mut surface = ViewSurface::new(Size::new(800, 480), 2.0)?;
# let _ = &mut surface;
# Ok(())
# }

DeniseView is the NSView subclass, ViewDelegate is what the host implements to drive it, and ViewSurface is the denise::Surface behind a layer-backed context.

What is different from the bare-metal backends

  • The host owns the run loop. There is no run function here. AppKit decides when to draw and Denise answers — the opposite of the DRM backend, where Denise decides and the display follows.
  • Damage is real. setNeedsDisplayInRect: genuinely limits what gets composited, unlike a page flip where the whole buffer goes regardless. So the rectangles the tree produces are worth passing on rather than rounding up to the whole view.
  • Points are not pixels. A Retina view is 2 physical pixels per point. Denise lays out in physical pixels throughout — the conversion happens once, at this edge, and nothing above it needs the scale factor to hit-test.
  • There is already a cursor. The host's window system draws one, so the composited sprite stays off: Ui::show_cursor(false), a decision that sticks rather than one the next mouse move overrides.

How a frame reaches the screen

Two IOSurfaces, shown alternately. The rasteriser draws into whichever one the compositor is not showing, and present swaps them; the view hands the new one to its CALayer as contents, where CoreAnimation reads it in place. Nothing is copied between the tree and the screen.

The pair is not only about tearing. Assigning the same object to contents tells CoreAnimation nothing — the property has not changed, so it has no reason to look at the buffer again, and the window shows its first frame for ever while the application draws happily into memory nobody reads. Two surfaces means every present assigns a different object, which is a change it cannot miss.

Consequences worth knowing:

  • The buffer is two frames old, so acquire reports BufferAge::Frames(2) and DamageTracker widens the repaint to match. That is the case it exists for.
  • present is what publishes. The view calls it after the delegate returns, so a delegate that only paints keeps working — but a host driving a ViewSurface itself must present, or nothing reaches the screen.

The alternative, a CGImage snapshot per frame, is copied whole on every commit however little of it changed. On a 1040×720 view with one spinner: 9.2% of a core against 0.6%.

Platform

macOS only; elsewhere the crate compiles to almost nothing. Built on objc2, so the Objective-C bridging is checked rather than hand-rolled. unsafe is necessarily permitted here; every block carries a // SAFETY: comment.

examples/embed.rs is a complete host — a window, a view and the run loop:

cargo run -p denise-macos --example embed

Where this sits

Wraps denise-ui. The Windows equivalents are denise-win32 and denise-activex.

Status

M5 complete, run on real hardware. Part of Denise — see the repository README for the whole picture.

MIT licensed.