EmergeSkia (Emerge v0.4.0-beta.1)

Copy Markdown View Source

Low-level renderer API used by Emerge viewports.

Most applications should define a viewport with use Emerge. Use EmergeSkia directly when integrating the renderer without the viewport process or when using renderer-specific APIs such as capture, statistics, or video targets.

Example

{:ok, renderer} =
  EmergeSkia.start(
    otp_app: :my_app,
    backend: :wayland,
    title: "My App",
    width: 800,
    height: 600
  )

{_state, _assigned_tree} = EmergeSkia.upload_tree(renderer, tree)
:ok = EmergeSkia.stop(renderer)

start/1 documents renderer and output options. renderer_info/1 reports the backend and rendering API actually selected. stats/2 and the capture functions provide on-demand diagnostics for a running renderer.

For UI colors, prefer Emerge.UI.Color. rgb/3 and rgba/4 return packed unsigned 0xRRGGBBAA values for low-level interfaces.

When an input target is registered with set_input_target/2, Wayland close requests arrive as {:emerge_skia_close, :window_close_requested} and are not filtered by the input mask.

Summary

Types

Renderer handle returned by start/1.

Functions

Returns the input mask for all events.

Returns the input mask for text input events.

Returns the input mask for cursor button events.

Returns the input mask for cursor enter/exit events.

Returns the input mask for cursor position events.

Returns the input mask for cursor scroll events.

Returns the input mask for window focus events.

Returns the input mask for key events.

Returns the input mask for window resize events.

Measure text dimensions for layout purposes.

Apply patches for a new tree, run layout, and render.

Return pixels from the renderer's latest already-presented frame.

Return an encoded PNG from the renderer's latest already-presented frame.

Fetch normalized renderer/backend information for a running renderer.

Convert RGB values to a color integer.

Convert RGBA values to a color integer.

Check if the renderer window is still open.

Set the input event mask to filter which events are sent.

Set the target process to receive renderer events.

Set the target process to receive native renderer log messages.

Starts a renderer session.

Fetch renderer stats.

Stop the renderer and close its window or output session.

Submits one VideoInterop frame to a renderer-local video target.

Upload a full EMRG tree, run layout, and render.

Types

color()

@type color() :: non_neg_integer()

renderer()

@type renderer() :: reference() | struct()

Renderer handle returned by start/1.

Functions

input_mask_all()

@spec input_mask_all() :: non_neg_integer()

Returns the input mask for all events.

input_mask_codepoint()

@spec input_mask_codepoint() :: non_neg_integer()

Returns the input mask for text input events.

input_mask_cursor_button()

@spec input_mask_cursor_button() :: non_neg_integer()

Returns the input mask for cursor button events.

input_mask_cursor_enter()

@spec input_mask_cursor_enter() :: non_neg_integer()

Returns the input mask for cursor enter/exit events.

input_mask_cursor_pos()

@spec input_mask_cursor_pos() :: non_neg_integer()

Returns the input mask for cursor position events.

input_mask_cursor_scroll()

@spec input_mask_cursor_scroll() :: non_neg_integer()

Returns the input mask for cursor scroll events.

input_mask_focus()

@spec input_mask_focus() :: non_neg_integer()

Returns the input mask for window focus events.

input_mask_key()

@spec input_mask_key() :: non_neg_integer()

Returns the input mask for key events.

input_mask_resize()

@spec input_mask_resize() :: non_neg_integer()

Returns the input mask for window resize events.

load_font_file(renderer, name, weight, italic, path)

@spec load_font_file(renderer(), String.t(), non_neg_integer(), boolean(), Path.t()) ::
  :ok | {:error, term()}

Load a font from a file path.

The font is registered by name and can be used with Font.family/1 in elements. Load different variants (bold, italic) with separate calls using appropriate weight/italic params.

The registration belongs only to renderer and does not change any other renderer or offscreen render.

Parameters

  • renderer - Renderer returned by start/1
  • name - Font family name to register (e.g., "my-font")
  • weight - Font weight (100-900, 400=normal, 700=bold)
  • italic - Whether this is an italic variant
  • path - Path to the TTF font file

Example

# Load font variants into one renderer
:ok = EmergeSkia.load_font_file(renderer, "my-font", 400, false, "priv/fonts/MyFont-Regular.ttf")
:ok = EmergeSkia.load_font_file(renderer, "my-font", 700, false, "priv/fonts/MyFont-Bold.ttf")
:ok = EmergeSkia.load_font_file(renderer, "my-font", 400, true, "priv/fonts/MyFont-Italic.ttf")

# Use in elements
el([Font.family("my-font"), Font.size(16)], text("Hello"))
el([Font.family("my-font"), Font.bold()], text("Bold text"))

measure_text(text, font_size)

@spec measure_text(String.t(), number()) :: {float(), float(), float(), float()}

Measure text dimensions for layout purposes.

Returns {width, line_height, ascent, descent} where:

  • width - Horizontal extent of the text
  • line_height - Total line height (ascent + descent)
  • ascent - Distance from baseline to top (positive)
  • descent - Distance from baseline to bottom (positive)

patch_tree(renderer, state, tree)

Apply patches for a new tree, run layout, and render.

Window dimensions come from the initial start config and are updated automatically when the window is resized (handled on the Rust side).

render_to_pixels(renderer, opts \\ [])

@spec render_to_pixels(
  renderer(),
  keyword()
) :: {:ok, binary()} | {:error, term()}

Return pixels from the renderer's latest already-presented frame.

This API captures retained renderer state. It no longer accepts an Emerge tree; pass a renderer handle returned by start/1.

{:ok, pixels} =
  EmergeSkia.render_to_pixels(renderer,
    region: {0, 0, 320, 240},
    pixel_format: :rgba8888,
    timeout: 5_000
  )

Supported options are region: {x, y, width, height}, pixel_format: :rgba8888 | :rgb888, timeout, scale: 1.0, and background: :transparent. macOS and headless PRIME do not support capture.

render_to_png(renderer, opts \\ [])

@spec render_to_png(
  renderer(),
  keyword()
) :: {:ok, binary()} | {:error, term()}

Return an encoded PNG from the renderer's latest already-presented frame.

This API captures retained renderer state. It no longer accepts an Emerge tree; pass a renderer handle returned by start/1.

{:ok, png} = EmergeSkia.render_to_png(renderer, png: [compression: :default])
File.write!("capture.png", png)

PNG capture accepts the same region, timeout, scale, and background options as render_to_pixels/2, plus png: [compression: :default]. macOS and headless PRIME do not support capture.

renderer_info(renderer)

@spec renderer_info(renderer()) ::
  {:ok, EmergeSkia.Native.renderer_info()} | {:error, term()}

Fetch normalized renderer/backend information for a running renderer.

{:ok, info} = EmergeSkia.renderer_info(renderer)
info.rendering_api.selected
info.capabilities.screenshot

Explicit Vulkan renderers include the physical device that won startup selection. When selection used an exact DRM node, vulkan_device.drm_node contains its path, match field, and major/minor identity. The KMS card is not reported as the Vulkan node on split-device systems. Non-Vulkan renderers return vulkan_device: nil.

Use capabilities to check capture, raster presentation, and PRIME video support before starting an optional operation.

rgb(r, g, b)

@spec rgb(0..255, 0..255, 0..255) :: color()

Convert RGB values to a color integer.

Examples

iex> EmergeSkia.rgb(255, 0, 0)
0xFF0000FF

iex> EmergeSkia.rgb(0, 255, 0)
0x00FF00FF

rgba(r, g, b, a)

@spec rgba(0..255, 0..255, 0..255, 0..255) :: color()

Convert RGBA values to a color integer.

Examples

iex> EmergeSkia.rgba(255, 0, 0, 128)
0xFF000080

iex> EmergeSkia.rgba(0, 0, 0, 255)
0x000000FF

running?(renderer)

@spec running?(renderer()) :: boolean()

Check if the renderer window is still open.

set_input_mask(renderer, mask)

@spec set_input_mask(renderer(), non_neg_integer()) :: :ok

Set the input event mask to filter which events are sent.

Wayland close notifications are always delivered to the input target as {:emerge_skia_close, :window_close_requested} and are not filtered by this mask.

Example

# Only capture mouse button and key events
import Bitwise
mask = EmergeSkia.input_mask_cursor_button() ||| EmergeSkia.input_mask_key()
EmergeSkia.set_input_mask(renderer, mask)

set_input_target(renderer, pid)

@spec set_input_target(renderer(), pid() | nil) :: :ok

Set the target process to receive renderer events.

Events are sent directly to the target process as {:emerge_skia_event, event} messages.

Raw input event payloads include:

  • {:cursor_pos, {x, y}}
  • {:cursor_button, {button, action, mods, {x, y}}}
  • {:cursor_scroll, {{dx, dy}, {x, y}}}
  • {:key, {key, action, mods}}
  • {:codepoint, {char, mods}}
  • {:text_commit, {text, mods}}
  • {:text_preedit, {text, cursor}}
  • :text_preedit_clear
  • {:cursor_entered, entered}
  • {:resized, {width, height, scale}}
  • {:focused, focused}

On Wayland, close notifications are sent separately as:

  • {:emerge_skia_close, :window_close_requested}

This lifecycle message bypasses the input mask so close requests are still delivered when other raw input categories are disabled.

On DRM, raw {:cursor_pos, {x, y}} delivery is latest-wins under load so pointer motion does not stall rendering. Button, scroll, key, and text events remain ordered.

Element event payloads include:

  • {id_bin, event_type}
  • {id_bin, event_type, payload}

where id_bin is an opaque element id and event_type is an atom such as :press, :click, :swipe_up, :swipe_down, :swipe_left, :swipe_right, :change, :key_down, :key_up, or :key_press.

Text-input :change payloads are binaries. Slider :change payloads are floats.

Routed :key_down, :key_up, and :key_press payloads currently carry an opaque binding route id used by higher-level runtimes.

Higher-level runtimes should route element events with Emerge.Engine.lookup_event/3 or Emerge.Engine.dispatch_event/3/4.

Where:

  • button is an atom like :left, :right, :middle
  • action is 0 for release, 1 for press
  • mods is a list of modifier atoms like [:shift, :ctrl]
  • key is a canonical atom like :escape, :enter, :a, :digit_1, :arrow_left, or :plus

Raw key events stay layout-independent. Text-producing input is delivered separately through text commit/preedit events. For example, Shift+= reports raw key :equal with [:shift] and still commits the text "+".

Example

EmergeSkia.set_input_target(renderer, self())

receive do
  {:emerge_skia_event, {:cursor_button, {button, 1, _mods, {x, y}}}} ->
    IO.puts("Clicked #{button} at #{x}, #{y}")

  {:emerge_skia_event, {:key, {key, 1, _mods}}} ->
    IO.puts("Key pressed: #{key}")
end

set_log_target(renderer, pid)

@spec set_log_target(renderer(), pid() | nil) :: :ok

Set the target process to receive native renderer log messages.

Native logs are sent directly to the target process as {:emerge_skia_log, level, source, message} messages.

start()

@spec start() :: no_return()

start(opts)

@spec start(keyword()) :: {:ok, renderer()} | {:error, term()}
@spec start(String.t()) :: no_return()

Starts a renderer session.

otp_app is required and identifies the application whose priv/ directory contains logical assets.

{:ok, renderer} =
  EmergeSkia.start(
    otp_app: :my_app,
    backend: :wayland,
    rendering_api: :auto,
    title: "My App",
    width: 800,
    height: 600
  )

Backend and rendering API

Backend/modeRendering APIsFallbackCaptureVideo
macOS:auto, :metal, :raster:auto falls back from Metal to rasterNoNo
Wayland:auto, :opengl, :raster, :vulkan:auto falls back from OpenGL to rasterYesOpenGL/Vulkan when supported
DRM:auto, :opengl, :raster, :vulkan:auto falls back from OpenGL to raster GPU uploadYesOpenGL/Vulkan when supported
headless binary:auto, :opengl, :raster:auto falls back from OpenGL to rasterYesNo
headless PRIME:auto, :opengl, :vulkanNoneNoProduces ABGR8888 DMA-BUF frames

Explicit Vulkan never falls back. DRM Vulkan requires vulkan_drm_node in addition to the KMS drm_card.

Raster presentation may be selected explicitly:

rendering_api: [raster: [present: :cpu]]
rendering_api: [raster: [present: :gpu_upload]]
rendering_api: [auto: [raster: [present: :cpu]]]

Wayland supports :cpu and :gpu_upload. DRM supports :gpu_upload.

Runtime backends and their GPU APIs must be compiled into the NIF:

config :emerge,
  compiled_backends: [
    wayland: [:opengl],
    drm: [:opengl, :vulkan],
    headless: [:vulkan]
  ]

Use :all to compile every GPU API supported by one backend, for example [drm: :all]. Use an exact list such as [drm: [:vulkan]] to omit OpenGL. Atom entries remain the compatibility shorthand, selecting OpenGL on Linux and Metal on macOS: [:wayland, :drm]. Headless raster rendering requires no matrix entry.

Options

OptionDefaultDescription
otp_apprequiredApplication used to resolve logical assets
backendplatform default:macos, :wayland, :drm, or :headless
rendering_api:autoRenderer selection described above
title"Emerge"Window title
width, height800, 600Initial window size or fixed headless size
scroll_line_pixels30.0Pixels for one discrete wheel step
drm_card/dev/dri/card0KMS primary-node path
vulkan_drm_nodenoneRequired Vulkan device node for DRM Vulkan
drm_startup_retries40DRM startup retry count
drm_retry_interval_ms250Delay between DRM retries
drm_force_gpu_finishfalseOpenGL-only DRM diagnostic
hw_cursortrueUse a DRM hardware cursor when available
drm_cursorbuilt-in themeDRM cursor source and hotspot overrides
input_logfalseLog DRM input-device discovery
render_logfalseLog detailed renderer and presentation messages
close_signal_logfalseLog detailed Wayland close handling
statsfalseCollect renderer stats
renderer_stats_logfalseCollect and log renderer stats every five seconds
renderer_animation_logfalseLog Wayland animation cadence
renderer_cacheenabled on GPU routesPaint-layer cache settings
assetsrestrictive defaultsAsset paths, fonts, decode, and raster cache settings
headlessbinary defaultsHeadless output settings

backend_renderer and :gl remain deprecated aliases. macos_backend and dispatch_mode were removed. See Migrating to 0.4.

Asset options are documented in Use assets.

Headless options

Both binary and PRIME modes require a live local target PID. The renderer sends frames directly to that process.

OptionDefaultDescription
mode:binary:binary or :prime
targetnoneRequired live local frame recipient PID
pixel_format:rgba8888Binary output format
bw1_polarity:one_is_black:one_is_black or :one_is_white
ditherfalseAtkinson dithering for raster BW1/Gray2
target_fpsnonePositive requested cadence
frame_message:emerge_skia_frameFrame tuple tag
prime.drm_nodenoneOptional absolute DRM allocation node
prime.max_in_flight2Maximum unreleased PRIME frames
prime.on_backpressure:drop_newPRIME backpressure policy

The target receives {message_tag, %VideoInterop.Frame{}}. The default tag is :emerge_skia_frame; custom tags are delivered as strings. Binary mode uses %VideoInterop.Binary{} storage with no lease. PRIME mode uses DMA-BUF storage with a lease that must be released after the recipient finishes with it.

Stable binary formats are:

FormatStrideData
:rgba8888width * 4Premultiplied RGBA
:rgb888width * 3Premultiplied RGB components with alpha omitted
:bw1ceil(width / 8)Eight MSB-first pixels per byte
:gray2ceil(width / 4)Four MSB-first two-bit pixels per byte

BW1 and Gray2 rows are packed independently. Unused low bits in the final byte of each row are zero. Gray2 values are 0..3 from black to white. Gray4 is unavailable; Gray8 is not a stable 0.4 output contract.

Grayscale output is composited over white before quantization. Dithering is available only with headless raster binary BW1/Gray2. Text, SVG/vector coverage, borders, and crisp generated UI are protected from error diffusion.

Validate each received frame before consuming it:

:ok = VideoInterop.validate(frame)

Renderer cache options

OptionDefault
enabledtrue; explicit raster defaults to false
max_new_payloads_per_frame16
paint_layer.max_entries512
paint_layer.max_bytes671_088_640
paint_layer.max_entry_bytes268_435_456
paint_layer.min_visible_before_store1
paint_layer.max_stale_frames120

Set an entry or byte limit to zero to prevent new stores for that limit.

DRM cursor options

drm_cursor accepts default, text, and pointer entries:

drm_cursor: [
  pointer: [source: "cursors/pointer.svg", hotspot: {3, 2}]
]

source must be a .png or .svg logical path, %Emerge.Assets.Ref{}, or an allowed absolute runtime path. hotspot is a pair of non-negative numbers.

Native logs are delivered to the process that starts the renderer as {:emerge_skia_log, level, source, message}. Use set_log_target/2 to change the destination.

start(title, width)

@spec start(String.t(), non_neg_integer()) :: no_return()

start(title, width, height)

stats(renderer, command \\ :peek)

Fetch renderer stats.

Stats collection is disabled by default. Start the renderer with stats: true or renderer_stats_log: true to collect renderer stats.

{:ok, snapshot} = EmergeSkia.stats(renderer, :peek)
{:ok, closed_window} = EmergeSkia.stats(renderer, :take)

Commands are:

  • :peek reads the current window without resetting it;
  • :take returns and resets the current window;
  • :reset discards the current window;
  • {:configure, %{enabled: boolean}} changes collection at runtime.

:take reads and resets the current window. On DRM it may return a draining error while asynchronous GPU samples finish; retry :take to receive the exact closed snapshot. The 0.4 stats schema version is 25.

stop(renderer)

@spec stop(renderer()) :: :ok | {:error, term()}

Stop the renderer and close its window or output session.

Most sessions return :ok. A headless PRIME session returns {:error, reason} when it cannot complete ownership-safe shutdown. Do not treat an error as successful cleanup; stop native video use and cold-restart before loading replacement native code.

submit_video_frame(renderer, target, frame)

@spec submit_video_frame(renderer(), atom(), VideoInterop.Frame.t()) ::
  :ok | {:error, {:caller_owned | :transferred, term()}}

Submits one VideoInterop frame to a renderer-local video target.

The target is the atom passed to Emerge.UI.video/2 in the renderer's tree. Hidden targets consume and drop frames immediately. Visible targets retain only the latest submitted frame.

This low-level function exposes ownership on errors:

  • :ok means the frame was consumed.
  • {:error, {:caller_owned, reason}} means submission did not consume the frame; the caller must release borrowed storage.
  • {:error, {:transferred, reason}} means ownership transferred before the failure and the caller must not release the frame.

Applications submitting through a viewport should normally use Emerge.submit_video_frame/3, which consumes the frame on every normal return and hides these ownership receipts.

upload_tree(renderer, tree)

@spec upload_tree(renderer(), Emerge.tree()) ::
  {Emerge.Engine.diff_state(), Emerge.tree()}

Upload a full EMRG tree, run layout, and render.

Window dimensions come from the initial start config and are updated automatically when the window is resized (handled on the Rust side).