Emerge.Runtime.Viewport (Emerge v0.4.0-beta.1)

Copy Markdown View Source

Configuration reference for viewports defined with use Emerge.

You normally start the generated function on your viewport module rather than calling this module directly:

{:ok, viewport} = MyApp.Viewport.start_link(title: "My App")

The options passed to start_link/1 are received by Emerge.mount/1. The options returned from mount/1 configure the viewport.

Option locations

Renderer options may be returned directly:

{:ok, Keyword.merge([title: "My App", width: 800, height: 600], opts)}

They may also be placed under emerge_skia: [...]. Nested values override duplicate top-level renderer values:

{:ok,
 [
   title: "My App",
   emerge_skia: [scroll_line_pixels: 45]
 ]}

Runtime-only options belong under viewport: [...]:

OptionDefaultUse
input_maskrenderer defaultBitmask selecting raw events delivered to Emerge.handle_input/2
renderer_check_interval_ms500Renderer liveness-check interval; use nil to disable it

viewport.renderer_module and viewport.renderer_opts are advanced adapter options and are not needed when using EmergeSkia.

Common renderer options

OptionDefaultUse
otp_appinferredApplication whose priv/ directory contains assets
title"Emerge"Window title
width, height800, 600Initial window size or fixed headless size
backendplatform default:macos, :wayland, :drm, or :headless
rendering_api:auto:metal, :opengl, :raster, or :vulkan where supported
assetsrestrictive defaultsFonts, image paths, raster decode, and cache limits
statsfalseCollect renderer statistics
renderer_stats_logfalseLog a renderer summary every five seconds
headlessbinary defaultsHeadless binary or PRIME output options

backend_renderer and :gl are deprecated compatibility names. macos_backend and dispatch_mode were removed in 0.4.

Windowed and display viewports

Most desktop viewports need no backend option. Emerge defaults to macOS on Darwin and Wayland on Linux desktop:

def mount(opts), do: {:ok, Keyword.merge([title: "My App"], opts)}

Use DRM for direct display output on Linux or Nerves:

def mount(opts) do
  {:ok,
   Keyword.merge(
     [backend: :drm, drm_card: "/dev/dri/card0"],
     opts
   )}
end

rendering_api: :auto is recommended unless the application requires a specific path. Raster is a rendering API, not a backend. Vulkan support is selected explicitly at build time.

Headless binary viewports

Headless is another viewport mode. It uses the same mount, render, event, rerender, and supervision APIs without opening a window.

Binary mode sends frames directly to a live local PID. A sink can be an ordinary GenServer:

defmodule MyApp.FrameSink do
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)
  end

  @impl true
  def init(_opts), do: {:ok, nil}

  @impl true
  def handle_info(
        {:emerge_skia_frame,
         %VideoInterop.Frame{
           coded_height: height,
           storage: %VideoInterop.Binary{
             data: data,
             planes: [%VideoInterop.Binary.Plane{stride: stride}]
           }
         } = frame},
        last_frame
      ) do
    :ok = VideoInterop.validate(frame)

    if byte_size(data) != stride * height do
      raise "invalid headless frame"
    end

    if data != last_frame do
      MyApp.Display.draw(frame)
    end

    {:noreply, data}
  end
end

Configure the viewport with that process as its direct target:

defmodule MyApp.DisplayViewport do
  use Emerge

  @impl Viewport
  def mount(opts) do
    target = Process.whereis(MyApp.FrameSink) || raise "frame sink is not running"

    {:ok,
     Keyword.merge(
       [
         backend: :headless,
         rendering_api: :raster,
         width: 400,
         height: 300,
         headless: [
           mode: :binary,
           target: target,
           pixel_format: :gray2,
           dither: true,
           bw1_polarity: :one_is_white
         ]
       ],
       opts
     )}
  end

  @impl Viewport
  def render, do: MyApp.UI.display()
end

Start the frame sink before the viewport. Use :rest_for_one when a sink restart must also restart the viewport holding its PID:

children = [MyApp.FrameSink, MyApp.DisplayViewport]
Supervisor.start_link(children, strategy: :rest_for_one)

The target receives {:emerge_skia_frame, %VideoInterop.Frame{}} by default. See EmergeSkia.start/1 for packed BW1/Gray2 rules and all headless options.

Headless PRIME viewports

PRIME mode produces leased Linux DMA-BUF frames. Its target is usually a Membrane.VideoInterop.Source process:

def mount(opts) do
  {:ok,
   Keyword.merge(
     [
       backend: :headless,
       rendering_api: :opengl,
       width: 640,
       height: 420,
       headless: [
         mode: :prime,
         target: frame_source,
         target_fps: 30,
         prime: [max_in_flight: 3, on_backpressure: :drop_new]
       ]
     ],
     opts
   )}
end

Route those frames through membrane_video_interop and submit them to a visible atom target with Emerge.submit_video_frame/3. PRIME is Linux-only; both OpenGL and Vulkan PRIME are supported when compiled and available.

Raw input

Set viewport.input_mask with the bitmasks returned by EmergeSkia.input_mask_*. Selected messages are delivered to Emerge.handle_input/2, for example:

@impl Viewport
def handle_input({:resized, {width, height, scale}}, state) do
  IO.inspect({width, height, scale})
  {:noreply, state}
end

Element events still use Emerge.handle_info/2.

Summary

Types

t()

@type t() :: map()

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

default_wrap_payload(message, payload, event_type)

@spec default_wrap_payload(term(), term(), term()) :: term()

notify_source_reloaded(meta \\ %{})

@spec notify_source_reloaded(term()) :: :ok

renderer(pid)

@spec renderer(pid()) :: term()

rerender(state)

@spec rerender(map()) :: map()