Migrate to Emerge 0.4

Copy Markdown View Source

Historical guide: startup/config examples below describe 0.4, not the current 0.5 API. After applying these changes, follow Migrate to Emerge 0.5 for typed config, input subscriptions and callback migration.

Emerge 0.4 unifies renderer selection, changes screenshot capture to retained renderer state, and moves video submission to viewport-local atom targets.

Move gradients to Color

Emerge.UI.Background.gradient/2,3 has been removed. Pass a gradient color to Background.color/1 instead:

# Removed
Background.gradient(from, to, angle)

# Replacement (with use Emerge.UI)
Background.color(gradient([from, to], angle))

# Additional colors are evenly spaced; the angle defaults to 0 degrees
Background.color(gradient([color(:sky, 400), color(:violet, 500), color(:rose, 600)]))

The fully qualified constructor is Emerge.UI.Color.gradient/2. Gradients need at least two solid colors and cannot contain nested gradients. The same value works with Font.color/1, Border.color/1, shadow colors and Svg.color/1:

paint = gradient([color(:sky, 400), color(:rose, 600)], 45)
Font.color(paint)
Border.color(paint)
Border.shadow(color: paint)
Svg.color(paint)

SVG tint preserves source alpha and multiplies it by gradient alpha. Gradients span the element's border box for backgrounds/borders/shadows, and content box for text/SVG. Paragraph fragments share the paragraph content box; glyphs, words, lines, border edges and SVG tiles do not independently restart the gradient. Animated gradients keep the same stop count; solid endpoints are lifted to the gradient endpoint's count and angle before interpolation.

Raw {:gradient, from, to, angle} attributes are no longer accepted either.

Review animation dimensions

Width and height animations now support pixel, content, fill, and weighted-fill sizes. min/2 and max/2 remain valid for static layout, but cannot be animation endpoints or the starting value of a change transition. Keep those bounds on static elements, or resolve them to pixel sizes before constructing an animation.

Use Animation.change/3 with a list of attributes to animate later value changes:

Animation.change([width(fill()), height(px(60))], 300, :ease_out)

Each attribute uses the shared timing. First mount is immediate; later changes start from the current displayed value. A plain attribute removes its change policy. Animation.change([width(content())], 300, :ease_out) also animates size changes caused by text or children. See Emerge.UI.Animation for examples.

Review shadows on transparent elements

Border.shadow and Border.glow now paint the full box behind the element, including text and other content. A transparent interior reveals the shadow instead of cutting it out. Add an opaque background if you want to cover the interior. Inline paragraph wrappers use the same behavior; inset shadows are unchanged.

Use rendering_api

Replace macOS-specific selection:

# 0.3
EmergeSkia.start(otp_app: :my_app, backend: :macos, macos_backend: :metal)

# 0.4
EmergeSkia.start(otp_app: :my_app, backend: :macos, rendering_api: :metal)

Replace the compatibility renderer name:

# Deprecated in 0.4
backend_renderer: :gl

# Current
rendering_api: :opengl

backend_renderer and :gl still warn and work in 0.4, but should not be used in new configuration. macos_backend and dispatch_mode have been removed and raise during option validation.

See EmergeSkia.start/1 for supported backend/rendering API combinations and fallback rules.

Capture a retained renderer

In 0.3, render_to_pixels/2 and render_to_png/2 accepted a tree, created a one-shot raster surface, and returned a binary:

# 0.3
png =
  EmergeSkia.render_to_png(tree,
    otp_app: :my_app,
    width: 800,
    height: 600
  )

In 0.4, they capture the latest already-presented frame from a running renderer and return a result tuple:

# 0.4
{:ok, renderer} = EmergeSkia.start(otp_app: :my_app, backend: :wayland)
{_diff_state, _assigned_tree} = EmergeSkia.upload_tree(renderer, tree)

{:ok, png} = EmergeSkia.render_to_png(renderer, timeout: 5_000)

Update both the first argument and return-value matching. macOS host and headless PRIME sessions currently return a not-supported error for capture.

See EmergeSkia.render_to_pixels/2 and EmergeSkia.render_to_png/2 for the current capture options and backend support.

Submit video to an atom target

Renderer-owned target handles, direct viewport connections, and submit_prime/2 were removed. Describe the destination with an atom:

video([width(fill()), height(fill()), image_fit(:contain)], :camera)

Submit each owned binary or borrowed DMA-BUF frame to the viewport:

:ok = Emerge.submit_video_frame(viewport, :camera, frame)

Every normal return consumes the frame. Hidden targets drop it immediately; visible targets retain only the latest frame. Use VideoInterop.retain/2 first when another branch also needs the borrowed storage.

macOS now accepts owned, single-plane RGBA8888 binary frames with implicit synchronization and premultiplied, straight, or opaque alpha. DMA-BUF/PRIME input and retained-frame capture remain unsupported there.

For Emerge-to-Emerge transport, configure the headless viewport to send %VideoInterop.Frame{} values into Membrane.VideoInterop.Source, then route them to Membrane.VideoInterop.Sink with an MFA that calls Emerge.submit_video_frame/3. Do not forward frames through an application handle_info/2 bridge.

The native video ownership contract changed in 0.4. Stop producers and consumers and cold-restart the BEAM operating-system process before loading upgraded Emerge, VideoInterop, or producer native libraries.

Handle renderer stop errors

EmergeSkia.stop/1 can now return {:error, reason} when a native session, including headless PRIME, cannot finish ownership-safe shutdown:

case EmergeSkia.stop(renderer) do
  :ok -> :ok
  {:error, reason} -> Logger.error("renderer did not stop cleanly: #{inspect(reason)}")
end

Do not treat an error as successful cleanup. Native callers can use EmergeSkia.stop(renderer, timeout: 5_000) to bound the wait, then inspect EmergeSkia.renderer_status/1. A timeout does not make the output reusable; quarantined sessions require a BEAM process restart. macOS currently supports only stop/1, not the native status/bounded-stop APIs.

Keep viewport identity, refresh renderer handles

Native viewports can recover an unexpectedly stopped renderer after safe cleanup while keeping their callback PID, state, and subscriptions. Applications still supervise the viewport process; callback death is not a renderer-only restart.

Do not cache the result of Emerge.renderer/1 across recovery. It may be nil while unavailable, and the replacement has a new handle. Frames submitted through Emerge.submit_video_frame/3 during that interval return {:error, :viewport_not_ready} and are still consumed; do not resubmit them. macOS and custom renderer adapters do not implement this native recovery contract. See Emerge.Runtime.Viewport for recovery and supervision details.

Check stats schema version

The renderer stats payload changed with renderer, video, Vulkan, and cache diagnostics. The 0.4 schema version is 25.

with {:ok, %{version: 25} = snapshot} <- EmergeSkia.stats(renderer, :peek) do
  inspect_snapshot(snapshot)
end

Consumers that persist or transmit snapshots should branch on version rather than assuming the 0.3 map shape.

Review new memory defaults

Decoded raster assets now default to a 256-entry, 256 MiB cache. Constrained devices should set smaller limits and enable target-sized decode:

assets: [
  decode_at_size: true,
  cache: [max_entries: 8, max_bytes: 2 * 1024 * 1024]
]

See Use assets for configuration and memory examples.

Scope runtime font registration

Runtime font registration now requires the renderer that owns the font:

# 0.3
:ok = EmergeSkia.load_font_file("my-font", 400, false, path)

# 0.4
:ok = EmergeSkia.load_font_file(renderer, "my-font", 400, false, path)

The registration and its text-metrics cache are isolated from every other renderer. Fonts declared through assets.fonts are loaded into the renderer created by that start/1 call.

Review Vulkan configuration

Vulkan is supported and opt-in at build time. Existing OpenGL configurations do not switch to Vulkan automatically.

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

compiled_backends is a backend/API matrix. Use :all for every GPU API on a backend or list the exact APIs to include. For example, [wayland: [:vulkan]] contains Vulkan but no OpenGL. Atom entries retain their 0.3 behavior and select OpenGL. 64-bit Linux has precompiled Vulkan-only Wayland, DRM, and headless variants.

Explicit rendering_api: :vulkan does not fall back. DRM Vulkan also requires an exact vulkan_drm_node path separate from drm_card.