Repository navigation
Frust 0.6.2
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_blurand
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_pathand
PaintScene::push_clip_path(origin, path)clip to an arbitrary outline (nonzero winding), popped
by the sharedpop_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)) andAnimatedOpacity::cached(bool)
keep a subtree's rasterized page across frames and composite it while nothing under it changes;
navigator pages andpattern_switcherchildren 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) andFRUST_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()(defaultNone) reports the transform a command would record
under. Limits are in docs/LIMITATIONS.md (retained-pages-budget-refusals). - Per-paint-class strip programs:
strip.wgslis specialised perPaintClass(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_minare 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=1records them apart again;
EngineRenderer::set_pass_mergeis its counterpart andlast_frame_passeslists 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 ofMemTotal(Linux/Android)
earns the newmobile_largetier, 2048x2048x4 (1024x1024x16 when the wide shape is refused).
The tier log line gainsrule=andmemory_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 fourtextureLoadtaps.FRUST_ENGINE_IMAGE_SAMPLER=0selects the
manual family;EngineRenderer::with_image_samplerchooses per renderer. frust-gpu:RenderPipelineDescgains an optional explicitlayout, the sixth field of the
pipeline key.EventCtx::request_layout/needs_layout,ChildPodlayout-skip state,
RenderRoot::{set_layout_skip, layout_skip}andWidgetTree::inspect_layout_dirty.- Layout-audit mode:
RenderRoot::set_layout_auditorFRUST_LAYOUT_AUDIT(any value but0)
compares each skipped pass with a forced one and logslayout-audit:warnings;
layout_audit_mismatches()counts them. Shells loglayout skip=.. audit=..at startup. frust-network-image(plugins/network-image): anetwork_image(url)view with placeholder,
error and fade-in slots, anImageFetcherseam (defaultReqwestFetcher: rustls on the ring
provider with bundledwebpki-roots), a byte-budgeted memory cache, an LRU disk cache under the
app cache directory, single-flight fetches, and an opt-inwebpfeature. Install with
NetworkImages::install()in theapp!setup block. Its Android compile gate needs
cargo ndk(ring compiles C).frust-network-image:DecodeLimits::max_alloc_bytesis 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-imageand the TUI's Add Plugin list offer the new plugin (the registry
now has nineteen entries).- The playground's Images section: six
network_imageviews, a local-fixtures loopback toggle, a
cache stats row and a Clear caches button. IndexedStack/indexed_stackinfrust-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::BackFallbackandfrust::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
IndexedStackwith a home-tab
back_fallback, a device-gate surface for Android back routing. frust-share(plugins/share):Share::text/text_anchored/urlopen the Android chooser or
the iOS activity sheet (an iPad popover anchored to aShareAnchorrect, or centred); desktop
copies to the clipboard throughfrust-clipboard(ShareOutcome::CopiedToClipboard). Device
gates have not run yet.frust-qr(plugins/qr): aqr_code(payload)view painted on a canvas with.ecc,
.quiet_zone,.colors,.sizeand.semantic_label, plusencode/QrMatrix.frust add pluginand the TUI's Add Plugin list offershareandqr(the registry now has twenty-one
entries).frust-url-launcherMaps helpers:Maps::directions/directions_to/show(Apple Maps on
iOS/macOS, Google Maps elsewhere) andUrlLauncher::open_maps_nativeforgeo:/maps:/
comgooglemaps:/google.navigation:URIs (NoHandleron desktop).frust-secure-storage: a debug-only, desktop-only plaintextinsecure_filefallback for hosts
without a Secret Service, selected byStoreOptions::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,
withsliver_box,sliver_padding,sliver_fill_remaining,sliver_fixed_extent_list/_keyed,
sliver_list,sliver_grid/_keyed(SliverGridDelegate),sliver_persistent_header
(pinned and floating) andkeep_alive/KeepAliveHandle. Transparent wrappers (Either,
EitherOf3..8,hero,shield,auto_scroll_zone,GestureDetector) forward slivers. - The
SliverWidgetseam infrust-core:Widget::as_sliver,ChildPod::layout_sliver_child,
SliverConstraints/SliverGeometry, re-exported byfrust::authoringwithScrollAxis
(also flat in the facade). - Material
sliver_app_barworks as a sliver inside acustom_scroll_view(pinned by default,
.floating(bool)opt-in); a setcollapse/scroll_offsetprop keeps the box form. The
playground gains a Slivers page. ScrollSurfaceCoreand theScrollSurfacetrait: one shared scroll core underScrollView,
ListViewand the viewport.- Native-view z-order and overlays:
Command::is_drawing,PlatformViewFrame::command_index,
ViewCommand::Update.z_indexand theViewCommand::{Overlay, DisposeOverlay}commands, emitted
byPlatformViewState::ingest_scene(frames, shields, commands, viewport)(plainingestnever
emits overlays). Z-shields are now input-only. - Segmented rendering:
frust_render::overlay::OverlaySurfaceandSurfaceRenderer::encode_segmented,
submit_segmentedandsubmit_segmented_deferreddraw 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;mediaselects thesetZOrderMediaOverlayarm). Two additive JSON ops,
overlayanddisposeOverlay, and a debuggable-build intent extradev.frust.translucentSurface
to force the surface mode for one launch. The native-widgets demo gains asandwichpage.
Interleaved content covers later slots (android-overlays-cover-later-slots). - Texture bus:
frust::texture(register_texture_source,NativeImporter), the leaf
frust_plugin::texture::TextureSinkmailbox and thetexture_viewwidget 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
andfrust_render::native_import::install. frust-video-player:VideoOutput::Texturewithvideo_texture_view,texture_sinkand
texture_output.frust-camera:CameraSession::preview_textureandpreview_texture_output,
and a default-onfrust-apifeature. Both keep the native view as the default; the playground
gains Texture toggles.- Apple overlay hosts behind
FRUST_OVERLAYS(default off): iOSFrustOverlayViewper cut (Mode A
only; additivefrust_overlay_mode/frust_overlay_attach/frust_overlay_detachexports) and
macOSFrustOverlaySurfaceView, 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::MetalImporterwraps a 32BGRA IOSurface-backed
CVPixelBufferas a texture (NV12 stays on the CPU path), installed by
frust_render::native_import::installon iOS and macOS. - Apple texture producers:
VideoOutput::Texture(anAVPlayerItemVideoOutput) 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. NativeHandleis move-only (noClone/Copy) andFramePayload/SourcePayloadare no longer
Clone, so a handle cannot be copied.TextureFrameandSourceFramekeep their payload and
release callback private (new,payload(),meta(),has_release()), so outsideunsafea
handle cannot leave the frame that carries its release; theunsafeinto_parts
(FrameMeta/SourceMeta) makes its caller run the returned callback exactly once. Release
callbacks run undercatch_unwindin panic=unwind builds (dev and test); the shipped release
profile setspanic = "abort", where a panicking callback aborts and callbacks must not panic.NativeHandle(infrust-renderandfrust-plugin) keeps its fields private: build one
through the unsafeNativeHandle::new(kind, ptr)and read it withkind()/ptr(). Texture
frames run their release callback on drop, so it runs exactly once on every path.- BREAKING:
frust_scene::Command::PushSnapshot/PopSnapshotare replaced byPushRetained { key, rect, alpha, scale, transform }/PopRetained, plus the newCommand::DrawRetained;
PaintScene::push_snapshot/pop_snapshotare removed in favour ofpush_retained/
pop_retained/draw_retained(rename the calls). A renderer that does not retain paints a
PushRetainedbody 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_availabletakes a fourthrecorded_serialparameter. PaintOutcome::paint_serialreports the monotonic serialRenderRoot::paintminted 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_mainp50 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
LAYOUTfrom rebuild, paintrequest_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. SetFRUST_NO_LAYOUT_SKIPto opt out. ScrollView's rebuild reportsLAYOUTwhile aScrollControllercommand is queued, as
ListViewalready did.pattern_switchertears 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: keepmarker now also covers an untypedlet, one seeded
with a direct (unwrapped)any(..)element (let mut body = vec![any(..)]) or started as
exactlyVec::new(),Vec::with_capacity(..)orvec![]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
usesnativeOverlayMode,nativeOverlaySurfaceChangedandnativeOverlaySurfaceDestroyed.
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_switcherchild 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_tonow flusheson_scroll(andListViewnear-edge callbacks) on
every frame the tween moves and the frame it lands on, with no input event needed.- A
scroll_to_itemrequest that moves nothing (unknown key, orNeareston a fully visible row)
now supersedes an in-flight controller tween instead of leaving it running. VideoFit::Coverno 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).