EmergeSkia (Emerge v0.5.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(%Emerge.Config{
    output: %Emerge.Config.Window{title: "My App"},
    assets: [otp_app: :my_app]
  })

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

start/1 accepts only Emerge.Config; see the viewport configuration documentation and 0.5 migration guide. 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} independently of config.input.events. Native payload names/actions differ from the typed Emerge.Input values passed to a viewport's handle_input/2.

Summary

Types

Native live-control progress, distinct from rendering or presentation.

Session-scoped native live-control submission token.

Renderer handle returned by start/1.

Functions

Reports native live-control revisions for this renderer session.

Lists DRM connectors and their advertised modes without changing display configuration.

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.

Returns native session health and cleanup status.

Convert RGB values to a color integer.

Convert RGBA values to a color integer.

Check if the renderer window is still open.

Set the target process to receive renderer events.

Set the target process to receive native renderer log messages.

Starts a renderer using %Emerge.Config{}.

Fetch renderer counters and timings as a structured snapshot.

Stop the renderer and close its window or output session.

Requests native shutdown and waits at most timeout milliseconds (default 5,000).

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()

config_control_status()

@type config_control_status() :: %{
  session_id: non_neg_integer(),
  desired_revision: non_neg_integer(),
  pending_revision: pos_integer() | nil,
  applied_revision: pos_integer() | nil,
  failed_revision: pos_integer() | nil,
  error: String.t() | nil
}

Native live-control progress, distinct from rendering or presentation.

config_token()

@type config_token() :: {non_neg_integer(), pos_integer()}

Session-scoped native live-control submission token.

drm_mode()

@type drm_mode() :: %{
  id: String.t(),
  name: String.t(),
  width: pos_integer(),
  height: pos_integer(),
  refresh_hz: float(),
  preferred: boolean(),
  interlaced: boolean()
}

drm_output()

@type drm_output() :: %{
  name: String.t(),
  connector_id: non_neg_integer(),
  connected: boolean(),
  modes: [drm_mode()]
}

renderer()

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

Renderer handle returned by start/1.

Functions

config_status(renderer)

@spec config_status(renderer()) ::
  {:ok, config_control_status()} | {:error, :unsupported}

Reports native live-control revisions for this renderer session.

desired_revision is the latest accepted submission, pending_revision awaits owner application, and applied_revision is the last confirmed live-control revision. Failures include failed_revision and error; a failed update may have partial effects, so the last applied revision is not a rollback guarantee. Revision zero means no live submission (startup settings are not counted). This reports native owner processing, not independent verification of physical display state. macOS host revision reporting is not implemented.

Native configure/3 submissions send {:emerge_skia_config, {session_id, revision}, :ok | {:error, reason}} to the submitting process. Session shutdown can end delivery; use renderer status/recovery as the terminal ownership authority.

drm_outputs(opts \\ [])

@spec drm_outputs(keyword()) :: {:ok, [drm_output()]} | {:error, String.t()}

Lists DRM connectors and their advertised modes without changing display configuration.

Accepts drm_card: "/dev/dri/card0" (the default), including stable /dev/dri/by-path/...-card paths. Returns disconnected outputs too, with connected: false. Requires Linux, compiled DRM support and read access to the card.

Pass an output's name as Config.DRM.connector and a mode map (or its exact id) as Config.DRM.mode. IDs describe exact timings, not indices or rounded refresh rates. Discovery is a snapshot; startup checks availability again. Explicit selections never fall back to another output/mode.

{:ok, outputs} = EmergeSkia.drm_outputs(drm_card: "/dev/dri/card0")
output = Enum.find(outputs, &(&1.connected and &1.name == "HDMI-A-1"))
mode = Enum.find(output.modes, & &1.preferred)
%Emerge.Config{output: %Emerge.Config.DRM{connector: output.name, mode: mode}}

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.

renderer_status(renderer)

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

Returns native session health and cleanup status.

cleanup_complete is true only after safe teardown. running?/1 == false alone does not mean a DRM output is available for a replacement renderer.

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_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. Subscribe with config.input.events at startup; the default empty list forwards no raw input. Element events and close notifications remain independent.

This is the low-level native payload contract, not the typed first argument to Emerge.handle_input/2. A viewport translates names/actions through Emerge.Input; do not match its callbacks against this table.

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}}
  • {:text_commit, {text, mods}}
  • {:text_preedit, {text, cursor}}
  • :text_preedit_clear
  • {:delete_surrounding, {before_bytes, after_bytes}} (where the backend supports it)
  • {: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(config)

@spec start(Emerge.Config.t()) :: {:ok, renderer()} | {:error, term()}

Starts a renderer using %Emerge.Config{}.

Outside a viewport, assets.otp_app is required to resolve logical assets. See Emerge.Config and its nested records for defaults and types. Legacy keyword configuration and positional startup arities have been removed.

output = %Emerge.Config.Headless{target: self(), size: {640, 480}}
config = %Emerge.Config{output: output, renderer: [api: :raster], assets: [otp_app: :my_app]}
{:ok, renderer} = EmergeSkia.start(config)

Native logs go to the starting process as {:emerge_skia_log, level, source, message}. Use set_log_target/2 to change the destination. Unsupported controls raise before native startup.

stats(renderer, command \\ :peek)

Fetch renderer counters and timings as a structured snapshot.

On native Wayland, DRM and headless renderers, collection is disabled by default. Set diagnostics: [stats: true] in Emerge.Config to collect data without periodic stats output, or diagnostics: [logs: [:stats]] to enable both collection and periodic summaries. See Emerge.Config.Diagnostics for their shared measurement-window behavior and log destinations.

{: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.

When collection is disabled, the snapshot reports enabled: false and empty counters. Renderer collection is startup-only; {:configure, %{enabled: boolean}} is not supported for renderer handles.

:take and :reset may return a draining error while asynchronous GPU samples finish; retry the same command to complete the closed window. The stats schema version is 25. The macOS host returns {:error, :not_supported} for this API; use diagnostics: [logs: [:stats]] for its periodic summaries instead.

stop(renderer)

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

Stop the renderer and close its window or output session.

Most sessions return :ok. Native sessions, including headless PRIME, can return {:error, reason} when ownership-safe shutdown cannot finish. Do not treat an error as successful cleanup. Use renderer_status/1 to check native cleanup completion; a timeout does not make a DRM output reusable. Quarantined sessions require a BEAM process restart.

stop(renderer, opts)

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

Requests native shutdown and waits at most timeout milliseconds (default 5,000).

A timeout does not release ownership or make an output reusable. Native cleanup continues; renderer_status/1 distinguishes pending cleanup from completion. Quarantined sessions require a VM restart. macOS currently supports only stop/1.

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).