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: :openglbackend_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)}")
endDo 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)
endConsumers 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.