Skip to content

Frust 0.6.2

Choose a tag to compare

@github-actions github-actions released this 16 Oct 14:29
· 570 commits to main since this release
ad0c82b

A patch release (the 0.6.x line continues) that carries source changes for code that implements
PaintScene, builds or matches frust_scene::Command, or constructs frust-gpu pipeline descriptors
directly (the Changed list marks them), and two behaviour changes every widget author should read:
layout skipping is on by default and pattern_switcher tears its outgoing child down at the switch.

The render model gained the pieces a mobile-first framework needs to keep frames cheap and to mix
native content with its own paint. Layout now skips pods whose constraints are unchanged and can
audit itself (FRUST_LAYOUT_AUDIT); slivers (custom_scroll_view, sliver_list,
sliver_persistent_header) give scrolling a lazy, composable shape; retained layers
(repaint_boundary, AnimatedOpacity::cached) keep rasterized pages across frames, with path
clips and backdrop blur as engine primitives. Partial repaint was measured and rejected, and was
removed. Platform views can sit between scene slots through an overlay host on Android, iOS and
macOS (FRUST_OVERLAYS, default off), and video and camera frames can enter the scene as textures
through frust::texture and texture_view, with zero-copy import on Apple and, behind
FRUST_ANDROID_HWBUFFER_IMPORT (default off), on Android. Every device leg has run: iPhone SE, iOS
Simulator and macOS for the Apple half, a OnePlus 9 for the Android overlays, the hardware-buffer import and the
Phase 3 retained-layer numbers. Gates passed: workspace tests, clippy, the five engine golden arms byte-unchanged, and both
Android app Kotlin compiles. How the model maps onto Flutter's is in
docs/learning/17-render-model-vs-flutter.md; the
limits are in docs/LIMITATIONS.md.

Added

  • Backdrop blur: frust_scene::Command::BackdropBlur, SceneBuilder::backdrop_blur and
    PaintScene::backdrop_blur(origin, size, radius) (a no-op by default) blur whatever is already
    painted beneath a rectangle, masked by any open clip. The engine cuts the frame there, copies the
    region into a page, blurs it and draws the panel from it; at most four per frame
    (EngineError::TooManyBackdrops), none inside a translucent layer, never retained. A swapchain
    frame carrying one renders via an intermediate plus one present copy (CopyPresentPass).
  • Path clips: frust_scene::Command::PushClipPath, SceneBuilder::push_clip_path and
    PaintScene::push_clip_path(origin, path) clip to an arbitrary outline (nonzero winding), popped
    by the shared pop_clip. The engine lowers it to a coverage mask, never a layer page, so a
    retained body under one paints inline; a recorder without its own override clips to the path's
    bounding box.
  • Retained layers. repaint_boundary(child) (with .alpha(f32)) and AnimatedOpacity::cached(bool)
    keep a subtree's rasterized page across frames and composite it while nothing under it changes;
    navigator pages and pattern_switcher children carry a boundary, so transitions composite
    retained pages while a page at rest paints plain. The engine owns the pages in a budgeted LRU store
    outside the intermediate pool (FRUST_ENGINE_RETAINED_BUDGET_MB=<n> overrides the tier budget); a
    page it cannot serve refuses the frame and the shell repaints without pages. Kill switches:
    FRUST_NO_RETAINED_LAYERS (widgets) and FRUST_ENGINE_NO_LAYERS (engine, now consulted).
    A page is drawn only when the published ledger covers the paint serial it was recorded under;
    re-recording a key whose new recording does not reach its page (refused, nested or unrendered)
    evicts the old page; a hover-link change or focus-epoch advance re-records every boundary once.
    PaintScene::current_transform() (default None) reports the transform a command would record
    under. Limits are in docs/LIMITATIONS.md (retained-pages-budget-refusals).
  • Per-paint-class strip programs: strip.wgsl is specialised per PaintClass (solid + coverage,
    gradient, image, image-filtered, blurred-rect, layer-composite, and the whole Uber program), the
    variants created lazily against one explicit strip pipeline layout. FRUST_ENGINE_UBER_PROGRAM=1
    draws every run with Uber again; FRUST_ENGINE_RUN_SPLIT_MIN=<n> (default 16) sets the shortest
    run worth a pipeline switch. EngineRenderer::set_uber_program / set_run_split_min are the
    programmatic counterparts.
  • One-pass surface opening: the clear, the opaque strips and the first drawing surface round record
    as a single render pass. FRUST_ENGINE_NO_PASS_MERGE=1 records them apart again;
    EngineRenderer::set_pass_merge is its counterpart and last_frame_passes lists a frame's
    FramePasses.
  • Atlas tier rule: an adapter-family name table places a mobile GPU on the mobile budget whatever
    its capability flags say, and a mobile family with at least 4 GiB of MemTotal (Linux/Android)
    earns the new mobile_large tier, 2048x2048x4 (1024x1024x16 when the wide shape is refused).
    The tier log line gains rule= and memory_mib=. An eviction clear that a pending upload
    covers exactly is no longer issued.
  • Hardware image sampler program family, on by default: bilinear atlas images are read with one
    filtered fetch instead of four textureLoad taps. FRUST_ENGINE_IMAGE_SAMPLER=0 selects the
    manual family; EngineRenderer::with_image_sampler chooses per renderer.
  • frust-gpu: RenderPipelineDesc gains an optional explicit layout, the sixth field of the
    pipeline key.
  • EventCtx::request_layout / needs_layout, ChildPod layout-skip state,
    RenderRoot::{set_layout_skip, layout_skip} and WidgetTree::inspect_layout_dirty.
  • Layout-audit mode: RenderRoot::set_layout_audit or FRUST_LAYOUT_AUDIT (any value but 0)
    compares each skipped pass with a forced one and logs layout-audit: warnings;
    layout_audit_mismatches() counts them. Shells log layout skip=.. audit=.. at startup.
  • frust-network-image (plugins/network-image): a network_image(url) view with placeholder,
    error and fade-in slots, an ImageFetcher seam (default ReqwestFetcher: rustls on the ring
    provider with bundled webpki-roots), a byte-budgeted memory cache, an LRU disk cache under the
    app cache directory, single-flight fetches, and an opt-in webp feature. Install with
    NetworkImages::install() in the app! setup block. Its Android compile gate needs
    cargo ndk (ring compiles C).
  • frust-network-image: DecodeLimits::max_alloc_bytes is now the budget for the header-predictable
    decode-and-fit working set (resize working set and a progressive JPEG's coefficient store,
    counted from its header, included; default 512 MiB); over-budget images fail with
    DecodeError::OverBudget.
  • frust add plugin network-image and the TUI's Add Plugin list offer the new plugin (the registry
    now has nineteen entries).
  • The playground's Images section: six network_image views, a local-fixtures loopback toggle, a
    cache stats row and a Clear caches button.
  • IndexedStack / indexed_stack in frust-widgets, re-exported by the facade: it keeps every
    child mounted, and only the selected child is painted, routed input and reachable for back, so
    per-tab navigator stacks route Android back to the selected tab.
  • frust::BackFallback and frust::back_fallback(&fallback, child): a non-navigator back
    registrant ranked after every navigator (for example back-to-home-tab at a tab root). It is live
    while its wrapper is mounted.
  • The playground's Tabs section: per-tab navigators under an IndexedStack with a home-tab
    back_fallback, a device-gate surface for Android back routing.
  • frust-share (plugins/share): Share::text / text_anchored / url open the Android chooser or
    the iOS activity sheet (an iPad popover anchored to a ShareAnchor rect, or centred); desktop
    copies to the clipboard through frust-clipboard (ShareOutcome::CopiedToClipboard). Device
    gates have not run yet.
  • frust-qr (plugins/qr): a qr_code(payload) view painted on a canvas with .ecc,
    .quiet_zone, .colors, .size and .semantic_label, plus encode / QrMatrix. frust add plugin and the TUI's Add Plugin list offer share and qr (the registry now has twenty-one
    entries).
  • frust-url-launcher Maps helpers: Maps::directions / directions_to / show (Apple Maps on
    iOS/macOS, Google Maps elsewhere) and UrlLauncher::open_maps_native for geo: / maps: /
    comgooglemaps: / google.navigation: URIs (NoHandler on desktop).
  • frust-secure-storage: a debug-only, desktop-only plaintext insecure_file fallback for hosts
    without a Secret Service, selected by StoreOptions::with_insecure_file_fallback() or
    FRUST_SECURE_STORAGE_INSECURE_FILE=1; release builds refuse it.
  • The playground's URL page gains Share, QR and Maps groups.
  • Sliver scrolling: custom_scroll_view (CustomScrollView, .axis(ScrollAxis), .cache_extent,
    .controller) composes slivers under a viewport with a cache window and correction-based anchoring,
    with sliver_box, sliver_padding, sliver_fill_remaining, sliver_fixed_extent_list / _keyed,
    sliver_list, sliver_grid / _keyed (SliverGridDelegate), sliver_persistent_header
    (pinned and floating) and keep_alive / KeepAliveHandle. Transparent wrappers (Either,
    EitherOf3..8, hero, shield, auto_scroll_zone, GestureDetector) forward slivers.
  • The SliverWidget seam in frust-core: Widget::as_sliver, ChildPod::layout_sliver_child,
    SliverConstraints / SliverGeometry, re-exported by frust::authoring with ScrollAxis
    (also flat in the facade).
  • Material sliver_app_bar works as a sliver inside a custom_scroll_view (pinned by default,
    .floating(bool) opt-in); a set collapse / scroll_offset prop keeps the box form. The
    playground gains a Slivers page.
  • ScrollSurfaceCore and the ScrollSurface trait: one shared scroll core under ScrollView,
    ListView and the viewport.
  • Native-view z-order and overlays: Command::is_drawing, PlatformViewFrame::command_index,
    ViewCommand::Update.z_index and the ViewCommand::{Overlay, DisposeOverlay} commands, emitted
    by PlatformViewState::ingest_scene(frames, shields, commands, viewport) (plain ingest never
    emits overlays). Z-shields are now input-only.
  • Segmented rendering: frust_render::overlay::OverlaySurface and SurfaceRenderer::encode_segmented,
    submit_segmented and submit_segmented_deferred draw one compiled frame as a main segment plus
    one segment per cut with a single submit.
  • Android overlay host behind FRUST_OVERLAYS (default off; frust paint above a native view on an
    opaque surface; media selects the setZOrderMediaOverlay arm). Two additive JSON ops,
    overlay and disposeOverlay, and a debuggable-build intent extra dev.frust.translucentSurface
    to force the surface mode for one launch. The native-widgets demo gains a sandwich page.
    Interleaved content covers later slots (android-overlays-cover-later-slots).
  • Texture bus: frust::texture (register_texture_source, NativeImporter), the leaf
    frust_plugin::texture::TextureSink mailbox and the texture_view widget put plugin-produced
    frames in the scene as ordinary paint.
  • Android hardware-buffer import behind FRUST_ANDROID_HWBUFFER_IMPORT (or
    debug.frust.hwbuffer_import; default off): frust_gpu::import::android::import_hardware_buffer
    and frust_render::native_import::install.
  • frust-video-player: VideoOutput::Texture with video_texture_view, texture_sink and
    texture_output. frust-camera: CameraSession::preview_texture and preview_texture_output,
    and a default-on frust-api feature. Both keep the native view as the default; the playground
    gains Texture toggles.
  • Apple overlay hosts behind FRUST_OVERLAYS (default off): iOS FrustOverlayView per cut (Mode A
    only; additive frust_overlay_mode/frust_overlay_attach/frust_overlay_detach exports) and
    macOS FrustOverlaySurfaceView, presenting main then overlays from one segmented submit. Each
    overlay sits directly above its own slot, so interleaved content stays under a later native
    control. Limits: apple-overlay-limits.
  • Apple zero-copy import: frust_gpu::import::apple::MetalImporter wraps a 32BGRA IOSurface-backed
    CVPixelBuffer as a texture (NV12 stays on the CPU path), installed by
    frust_render::native_import::install on iOS and macOS.
  • Apple texture producers: VideoOutput::Texture (an AVPlayerItemVideoOutput) and the camera's
    preview_texture (a second 32BGRA data output) hand frames on zero-copy when the importer is
    installed.

Changed

  • Android video/camera texture output logs the path frames take (AHardwareBuffer zero-copy or CPU
    copy) once per session at Info.
  • NativeHandle is move-only (no Clone/Copy) and FramePayload/SourcePayload are no longer
    Clone, so a handle cannot be copied. TextureFrame and SourceFrame keep their payload and
    release callback private (new, payload(), meta(), has_release()), so outside unsafe a
    handle cannot leave the frame that carries its release; the unsafe into_parts
    (FrameMeta/SourceMeta) makes its caller run the returned callback exactly once. Release
    callbacks run under catch_unwind in panic=unwind builds (dev and test); the shipped release
    profile sets panic = "abort", where a panicking callback aborts and callbacks must not panic.
  • NativeHandle (in frust-render and frust-plugin) keeps its fields private: build one
    through the unsafe NativeHandle::new(kind, ptr) and read it with kind()/ptr(). Texture
    frames run their release callback on drop, so it runs exactly once on every path.
  • BREAKING: frust_scene::Command::PushSnapshot / PopSnapshot are replaced by PushRetained { key, rect, alpha, scale, transform } / PopRetained, plus the new Command::DrawRetained;
    PaintScene::push_snapshot / pop_snapshot are removed in favour of push_retained /
    pop_retained / draw_retained (rename the calls). A renderer that does not retain paints a
    PushRetained body inline exactly as the old snapshot bracket did and is never sent
    DrawRetained; a renderer that publishes a ledger must serve every key it lists or refuse the
    frame. PaintCtx::retained_available takes a fourth recorded_serial parameter.
  • PaintOutcome::paint_serial reports the monotonic serial RenderRoot::paint minted for the pass.
  • Host paint cost with retained layers on versus off (release, i5-12600 Linux, 300 frames, mean
    us; relayout_cost_bench): cached opacity 0.11 vs 49.00, navigator push/pop 32.63 vs 111.52,
    navigator page at rest 48.53 vs 48.54. Device legs (Pixel 5, iPhone SE) are owed. A backdrop
    frame on the direct present arm pays one present copy, about 0.239 ms at 1080x2400 and 0.189 ms
    at 1920x1080 (T400).
  • Measured gpu_main p50 on the S1 scene, before and after the FAST Phase 1 engine work: Pixel 4a
    32.70 to 12.75 ms, Pixel 4 11.15 to 8.12 ms. Method and the other scenes are in
    docs/PERFORMANCE_BASELINES.md § After Phase 1.
  • Layout is skipped per pod by default: a pod laid out before returns its recorded size unless it
    is dirty, its constraints, the window insets or the root layout epoch changed (theme, insets,
    window size, a dirty floated overlay). Widgets must signal layout-affecting changes through
    LAYOUT from rebuild, paint request_layout/request_frame, or a consumed event. Unoptimized
    bench (relayout_cost_bench), skip off to on, mean layout us: static tree 76.3 to 0.52,
    scroll-only 75.2 to 1.46, one changed leaf 160.1 to 93.6 (one-leaf vs no-change ratio 2.10x to
    179.9x); paint unchanged. Set FRUST_NO_LAYOUT_SKIP to opt out.
  • ScrollView's rebuild reports LAYOUT while a ScrollController command is queued, as
    ListView already did.
  • pattern_switcher tears its outgoing child down when the switch starts, not when the exit
    animation ends; the outgoing child still paints its exit. A child that owns resources (a native
    widget, a subscription, a timer) releases them at the switch.
  • NavigatorController::back_interest() is false for a navigator that is not mounted.
  • A back press that no registrant claims at delivery is dropped instead of being handed to the
    navigator whose wire consumed it.
  • Codemod: the accumulator // erasure: keep marker now also covers an untyped let, one seeded
    with a direct (unwrapped) any(..) element (let mut body = vec![any(..)]) or started as
    exactly Vec::new(), Vec::with_capacity(..) or vec![] and later pushed or inserted a direct
    any(..) in the same fn, so idiomatic accumulators no longer need a : Vec<AnyView<_>>
    annotation for rule U.
  • The Android overlay spike's JNI symbols and single-overlay path are removed; the overlay host
    uses nativeOverlayMode, nativeOverlaySurfaceChanged and nativeOverlaySurfaceDestroyed.

Fixed

  • Android overlay host: a new, resized or withdrawn overlay surface requests a frame, so an idle app
    no longer keeps frust content under a native view until an unrelated repaint.
  • Overlay cuts: content the overlay engine cannot draw (retained pages, external-texture frames,
    backdrop blurs) stays on the main surface under the native view instead of vanishing; plain
    hosting emits no rank-only updates; a cut no longer swallows a slot's hole-punch.
  • frust-shell-ios: the AccessKit tree is published in the view's physical pixels through a root
    transform equal to the display scale, so VoiceOver frames and hit-tests land on the drawn
    elements on 2x/3x devices (they were half-scale before).
  • A navigator inside a pattern_switcher child that had been switched away kept claiming Android
    back, so back was swallowed on every other page and could not exit the app.
  • ScrollController::animate_to now flushes on_scroll (and ListView near-edge callbacks) on
    every frame the tween moves and the frame it lands on, with no input event needed.
  • A scroll_to_item request that moves nothing (unknown key, or Nearest on a fully visible row)
    now supersedes an in-flight controller tween instead of leaving it running.
  • VideoFit::Cover no longer spills outside its box on Android in texture mode, where the frames
    are scene content and crop inside the box, and the native player view now clips on Apple too.
  • iOS: with present-sync armed, the 6 wgpu validation errors per launch are gone, and the
    present-sync black screen on a phone and the Simulator Mode B black screen no longer reproduce.
  • The playground's camera texture preview no longer freezes when its frame requester is culled
    below the fold; the ticker now sits under the picture (texture-bus-no-wake-on-push).