Migrate from Emerge 0.4.2 to 0.5.0

Copy Markdown View Source

The main change is configuration: return an Emerge.Config struct instead of a renderer keyword list. UI construction, element events and viewport state work as before. Raw input callbacks are now opt-in and use clearer event names.

1. Update the dependency

In mix.exs:

{:emerge, "~> 0.5.0"}

Then run:

mix deps.update emerge

Emerge fetches the matching precompiled renderer for supported targets. There is no new source-build step for a normal upgrade. Existing compile-time backend selection stays unchanged.

2. Return a config from mount

0.4.2:

@impl Viewport
def mount(opts) do
  {:ok, %{count: 0}, Keyword.merge([title: "Counter", width: 800, height: 600], opts)}
end

0.5.0:

@impl Viewport
def mount(opts) do
  output = %Emerge.Config.Window{
    title: Keyword.get(opts, :title, "Counter"),
    size: Keyword.get(opts, :size, {800, 600})
  }

  {:ok, %{count: 0}, %Emerge.Config{output: output}}
end

The state and render/1 do not change. A stateless viewport returns {:ok, %Emerge.Config{}} and still uses render/0.

Arguments to start_link/1 are still keywords, but your mount callback decides which ones to use. In this example, pass size: {1024, 768} instead of separate width and height arguments. Process options such as name: stay unchanged.

If you start EmergeSkia directly

Replace keyword startup with a config, and move otp_app into assets:

# 0.4.2
EmergeSkia.start(otp_app: :my_app, title: "Preview", rendering_api: :raster)
# 0.5.0
EmergeSkia.start(%Emerge.Config{
  output: %Emerge.Config.Window{title: "Preview"},
  renderer: %Emerge.Config.Renderer{api: :raster},
  assets: %Emerge.Config.Assets{otp_app: :my_app}
})

Viewports infer their asset application when possible. Standalone callers must supply it. Old positional startup calls and keyword renderer configs are removed.

3. Move settings into their groups

The config has six groups. You only need to supply the ones you customize; all others use defaults. Remove the old emerge_skia: and viewport: wrappers.

Window and renderer

This is a typical replacement for a window's flat options:

%Emerge.Config{
  output: %Emerge.Config.Window{title: "My App", size: {1024, 768}},
  renderer: %Emerge.Config.Renderer{api: :opengl}
}
  • backend: :wayland or :macos becomes Window.backend; usually leave it automatic.
  • title and always_on_top move into Window.
  • width and height become the size tuple.
  • rendering_api (or the old backend_renderer alias) becomes Renderer.api. Use :opengl, not the old :gl alias.
  • Raster's nested present setting becomes Renderer.raster_present.
  • vulkan_drm_node becomes Renderer.device.
  • renderer_cache becomes Renderer.cache; its field names are unchanged.
  • headless.target_fps becomes Renderer.target_fps.

See Emerge.Config.Window and Emerge.Config.Renderer for field descriptions.

DRM display

Replace backend: :drm, drm_card, drm_output and drm_mode with a DRM record:

%Emerge.Config{
  output: %Emerge.Config.DRM{card: "/dev/dri/card0", connector: "DSI-1"},
  renderer: %Emerge.Config.Renderer{api: :opengl}
}

drm_mode becomes mode. Old width/height preferences become preferred_size: {width, height}; the selected display mode determines the actual size. Display discovery still uses EmergeSkia.drm_outputs(drm_card: path). See Emerge.Config.DRM for display-off and brightness settings.

Headless output

Replace the backend flag and nested headless options with a headless record:

%Emerge.Config{
  output: %Emerge.Config.Headless{
    target: self(),
    size: {400, 300},
    frames: %Emerge.Config.BinaryFrames{pixel_format: :bw1, dither: true}
  },
  renderer: %Emerge.Config.Renderer{api: :raster},
  assets: %Emerge.Config.Assets{otp_app: :my_app}
}

target and frame_message keep their names. Binary pixel_format, bw1_polarity and dither move into BinaryFrames. For PRIME output, use PrimeFrames instead; prime.drm_node becomes PrimeFrames.device, while max_in_flight and on_backpressure keep their names. The explicit frame struct replaces the old :binary/:prime mode flag. Frame messages still contain VideoInterop.Frame.

Assets

Move the root otp_app into assets. Other asset field names stay the same:

%Emerge.Config{
  assets: %Emerge.Config.Assets{
    otp_app: :my_app,
    fonts: [%Emerge.Config.Font{family: "Inter", source: "fonts/Inter-Regular.ttf"}]
  }
}

Use RuntimePaths for assets.runtime_paths and AssetCache for assets.cache. See Use assets for the full examples.

Input and cursor

  • viewport.input_mask is replaced by Input.events; see the next step.
  • scroll_line_pixels moves into Input.
  • hw_cursor becomes Input.cursor.hardware.
  • Cursor image overrides move from drm_cursor into Input.cursor, using CursorIcon records. source and hotspot keep their names.

Diagnostics

Instead of individual logging booleans, choose categories in Diagnostics.logs:

  • input_log: true → :input
  • render_log: true → :render
  • close_signal_log: true → :close
  • renderer_stats_log: true → :stats
  • renderer_animation_log: true → :animation

For example, to keep automatic performance summaries:

%Emerge.Config{
  diagnostics: %Emerge.Config.Diagnostics{logs: [:stats]}
}

stats: true collects data for EmergeSkia.stats/2 without printing summaries. Use logs: [:stats] when you want printed summaries; specifying both is unnecessary. macOS supports printed summaries, not the stats API. The old stats field moves into Diagnostics.stats; drm_force_gpu_finish becomes Diagnostics.force_gpu_finish.

Runtime

Most applications can leave this group at its defaults. If you customized it:

  • drm_startup_retries → Runtime.startup_retries
  • drm_retry_interval_ms → Runtime.retry_interval_ms
  • viewport.renderer_check_interval_ms → Runtime.health_check_interval_ms
  • viewport.renderer_module → Runtime.adapter
  • viewport.renderer_opts → Runtime.adapter_opts

Custom adapter authors need to update to the new config contract. Ordinary applications use the default adapter and need no adapter changes.

4. Update raw input callbacks, if you use them

Buttons, text inputs, scrolling, element key bindings and window close callbacks continue to work without raw subscriptions. If you use handle_input/2, explicitly choose its events; the new default is none:

%Emerge.Config{
  input: %Emerge.Config.Input{events: [:key, :pointer_button, :resize]}
}

Use Emerge.Config.Input.all_events/0 for all categories. Numeric masks and the :all/:none shortcuts are no longer accepted.

Update callback patterns:

  • Key/button actions 1 and 0 become :press and :release.
  • :cursor_pos becomes :pointer_move; :cursor_button becomes :pointer_button.
  • :cursor_scroll becomes :pointer_scroll; :cursor_entered becomes :pointer_enter.
  • :resized becomes :resize; :focused becomes :focus.
  • Subscribe to :text for committed text and IME updates.

For example, {:key, {:escape, 1, modifiers}} becomes {:key, {:escape, :press, modifiers}}. See the input guide for complete callback values. These changes apply to viewport callbacks, not standalone native event messages.

DRM touchscreen observers now subscribe to :touch and receive {:touch, {contact_id, :down | :move | :up | :cancel, {x, y}}} instead of emulated mouse-button callbacks. Each contact has an independent lifetime. Virtual keys activate on that contact's release; cancellation never activates a key. This does not change mouse observations or require raw subscriptions for ordinary UI input.

Optional: change settings while running

Existing mount-only configuration needs no additional callback. To make a supported setting follow application state, add config(previous, state) (or config(previous) for a stateless viewport). Implement only one arity:

@impl Viewport
def config(previous, state) do
  output = %{previous.output | title: state.title}
  %{previous | output: output}
end

Call Viewport.rerender(state) as usual after changing state. The callback receives the previous config with all nested records expanded into structs. Update those records to keep other settings; supplying a new record uses its defaults, not values from the previous one. Lists also replace rather than append.

Settings you can change while running

  • Raw input subscriptions and the renderer health-check interval.
  • Window titles on Wayland and macOS.
  • Frame-rate limits on headless and Wayland outputs.
  • DRM display on/off and brightness with explicit OpenGL, Vulkan or raster (CPU or GPU-upload presentation).

Settings that need a new viewport

Output selection, size, renderer API/device, caches, cursor appearance, assets, diagnostics and adapter settings cannot be changed by the callback.

Backend limitations

Frame-rate limits are not available on DRM or macOS. DRM power/brightness controls require an explicit OpenGL, Vulkan or raster renderer; automatic API selection remains unsupported. Raster GPU upload requires an OpenGL-enabled DRM build. See configuration records and each config module for the settings relevant to your output.