Capability-driven system-webview adapter — scry into WebView2/WKWebView/WPE/WebKitGTK and surface frames the host renderer can consume.
The name comes from scrying — gazing into a reflective surface for visions. The webview is the surface; the captured frame is the vision; this crate is the lens.
This crate is the home for system-webview-backed frame production. It is deliberately separate from grafting (sibling repo): Graft imports native GPU resources, while this adapter owns system-webview probing, fallback selection, synchronization policy, and platform-specific frame-source integration.
The crate defaults to wgpu 30 and also carries wgpu-28 and wgpu-29
features. Pick the row matching the host, with default features disabled for
28 or 29. scrying::wgpu re-exports the selected version so public device and
texture types cannot silently come from a different major.
The published library supports Rust 1.92 and has an exact 1.92.0 compile gate
on Windows, macOS, and Linux for each wgpu 28/29/30 public API row. Linux's
native-engine families are checked independently: WebKitGTK 4.1 fallback,
WPE, and WebKitGTK 6.0 each build in their own system-library environment.
They are deliberately not combined through --all-features, since WebKitGTK
6.0 selects an incompatible GTK/glib dependency family. This is a
library-only compatibility promise; the headed hardware workflows remain on
their Rust 1.97.1 lane.
The shared contract:
WebSurfaceMode— imported texture, native child overlay, CPU snapshot, or unsupported.WebSurfaceCapabilities— platform/backend capability reporting.WebSurfaceFrame— imported native frame, CPU RGBA frame, PNG snapshot, or overlay-only state.WebSurfaceProducer— producer trait that platform implementations satisfy.PlatformWebSurfaceProducer/PlatformWebSurfaceConfig— cfg-selected aliases for the current target platform's primary concrete producer and config. Linux selects the WPE producer type; thewpefeature enables its runtime FFI. Its shared-fd DCC DMABUF path is pixel-verified on AMD Renoir/RADV.OverlayOnlyProducer— conservative fallback when no capture backend is available.
Platform selection is intentionally split:
- scrying owns backend selection. Platform modules, concrete producer aliases, and engine dependencies are
cfg(target_os = ...)gated, so a Windows build selects WebView2, a macOS build selects WKWebView, and a Linux build selects the WPE type without compiling the other engine paths. Addwpefor the live WPEPlatform producer; without it the alias is a compile-only shell. WebKitGTK 4.1 and 6.0 remain opt-in Linux alternatives. - the host owns embedding. The host still creates the window/event loop, supplies the native parent handle, chooses size/data-dir policy, and forwards native input/lifecycle events. Those responsibilities are application-specific and cannot be guessed reliably inside the library.
- runtime capability probing stays layered on top.
WebSurfaceCapabilities::probeanswers which surface modes are viable for the current GPU/OS/runtime after the target backend has been selected at compile time.
WebSurfaceProducer covers the full embeddable-webview surface, not just frame production:
- Frame acquisition —
acquire_frame, plus producer-specific fast paths. - Layout —
resize,set_offset. - Navigation —
navigate_to_string,navigate_to_url. Both block untilNavigationCompleted. - History —
reload,stop,go_back,go_forward,can_go_back,can_go_forward. - Input —
send_mouse_input(mouse + scroll + leave),send_pointer_input(touch / pen with pressure + tilt),move_focus(Programmatic / Next / Previous tab order). Drag-and-drop is implemented on the Windows producer's concrete type asdrag_enter/drag_over/drag_leave/drop_data— the host supplies anIDataObjectfrom its OLE drop-target callbacks. The trait-levelsend_drag_inputstays platform-abstract; full cross-platform DnD waits for a unified data-carrier abstraction. - Ordered events —
poll_web_surface_eventis the authoritative FIFO for navigation, page messages, and correlated command completions.poll_navigation_eventandpoll_web_messageremain compatibility views; consumers choose one polling API, and draining either view consumes its mirror so an unused queue does not grow without bound. - Cursor reporting —
poll_cursor_shapereturns the next [CursorShape] the engine wants the host to display (Pointer over a link, Text in an input, etc.). - JS and messaging —
request_script_result(WebRequestId, script)completes asynchronously asWebSurfaceEvent::ScriptCompleted;post_web_messageand the page bridge cover host/page messages. - Cookies —
request_cookies_for_url(WebRequestId, url)completes asynchronously on WebView2 and WPE. WKWebView retains its direct all-cookie API and refuses URL-scoped reads because its translated cookie shape cannot preserve host-only matching. - DevTools —
open_devtools_windowopens the engine's developer-tools UI. - Settings —
apply_settings(&WebSurfaceSettings)accepts a partial update of zoom factor, user-agent string, JS-enabled, devtools-enabled, default-context-menus, and built-in accelerator keys.Nonefields are left at the producer's current value. - Profiles — platform configs take a persistent data directory.
non_persistent()switches supported producers into incognito/private mode so browser-shaped hosts can create temporary tiles without touching the persistent profile. - Snapshots —
capture_snapshot_pngreturns encoded PNG bytes via the underlying engine's preview API.
Methods that aren't yet implemented on a given platform return [WebSurfaceError::Unsupported] rather than panicking, so consumers can probe the surface incrementally.
Per-platform producer modules:
| Platform | Module | Status | Capture path |
|---|---|---|---|
| Windows | [webview2_composition_producer] |
Implemented. Reference implementation; runtime-driven by [demo-scrying-winit]. |
WebView2 CompositionController → Windows.UI.Composition.Visual → Windows.Graphics.Capture → shared D3D11 NT-handle texture → wgpu D3D12 import. |
| macOS | [wkwebview_producer] |
Implemented. Runtime-driven by [demo-mac]. Slices A–N + the MetalTextureRef import path all exercised end-to-end. See design_docs/2026-05-07_platform_ceilings.md. |
WKWebView hosted in NSView → ScreenCaptureKit stream bound to the host window → CMSampleBuffer → IOSurfaceRef → MTLTexture (via MTLDevice::newTextureWithDescriptor:iosurface:plane:) → wgpu Metal import (via wgpu::hal::metal::Device::texture_from_raw). |
| Linux | [wpe_producer], [webkitgtk_producer], [webkit6_producer] |
Implemented behind backend features. wpe enables the headless WPEPlatform producer; its wgpu 30 DMABUF path is pixel-verified on AMD Renoir/RADV. WebKitGTK 4.1 and 6.0 are CPU-snapshot alternatives. Their try_acquire_frame path is deliberately non-blocking and returns None; call acquire_frame when a blocking CPU snapshot is acceptable. |
WPEWebView + WPEViewBackendDMABuf → DmaBufImage → Graft's host-side Vulkan import. |
WebSurfaceCapabilities::features is the host-facing contract for browser
operations. Every field is an explicit Supported, Partial (with a caveat),
or Unsupported status. Hosts should inspect it before selecting a fallback.
The matrix covers cookie read/write/delete/change events and attributes,
script execution/results/exceptions and timeout behavior, page capture,
developer tools, downloads, popups, drag/drop, pointer input, IME, and
accessibility. degradation_reasons carries stable explanations for backend
limits such as host API mismatches, reduced pointer metadata, and GTK's
blocking CPU snapshot path.
The current honest limits are important: the two optional WebKitGTK producers
do not expose correlated script results; WKWebView cannot perform an exact
URL-scoped cookie read, open
Safari Web Inspector or synthesize capture-mode drag payloads; WebView2's
portable drag method cannot carry its required IDataObject; and none of the
four producers exports an accessibility tree. Cookie SameSite and
Partitioned fields are explicitly reported as unsupported on every current
backend, while secure, HttpOnly, and expiry attributes are reported
individually.
The Windows and macOS producers cover the producer/consumer split, lazy capture standup, lifecycle teardown, and platform-appropriate cross-API sync (D3D11 keyed-mutex on Windows; implicit IOSurface coherence + MTLSharedEvent scaffolding on macOS). The WPE producer exposes owned DMABUF frame metadata and duplicates its fds before releasing the WPE buffer back to the producer pool.
The Windows producer ([webview2_composition_producer::WebView2CompositionProducer]) owns the full WebView2 composition + WGC capture lifecycle:
- WebView2 environment +
ICoreWebView2CompositionController+ICoreWebView2Controller Windows.UI.Compositioncompositor + desktop-window-target + root + WebView visualsWindows.Graphics.Captureitem, frame pool, session- A fresh NT-handle-shareable D3D11 destination texture for each delivered frame, ordered by the explicit producer fence. A texture still referenced by a host submission is not overwritten by later capture.
- Lazy
start_capture+ bounded first-frame block + post-resize tear-down/rebuild + stall-detection escape hatch (force_restart_capture) navigate_to_stringandnavigate_to_urlwait for navigation completion. Callwait_for_render_tick(timeout)explicitly when two animation-frame callbacks are needed; hidden pages can pause those callbacks. The checked callback result does not guarantee a captured compositor paint.- Optional
WebView2CompositionConfig::non_persistent()InPrivate mode for producers whose cookie, local-storage, and IndexedDB state should die with the controller instead of persisting intouser_data_dir NewWindowRequestedevent routing fortarget="_blank"/window.open(...), with the default WebView2 popup suppressed so the host owns tab creationProcessFailedrouting toNavigationEvent::ContentProcessTerminated, plus DevTools-protocol diagnostic calls for bounded crash/recovery smokesregister_virtual_host_handler(host, handler)for app-ownedhttps://{host}/...content via WebView2WebResourceRequested, using the sameUrlSchemeResponsebody/header shape as macOS custom schemesDownloadStartingrouting toNavigationEvent::DownloadStarted/DownloadProgress/DownloadFinished/DownloadCancelled, withset_download_handler,cancel_download,pause_download,resume_download, andcan_resume_downloadfor host-owned destinations and live WebView2 operation control. WebView2 does not expose a portable offline resume-data blob through this path, so cancelled/interrupted events still carryresume_data: None.BasicAuthenticationRequestedrouting toNavigationEvent::AuthChallengedplusset_auth_handlerfor host-supplied HTTP Basic credentials. Challenges matching an active download URL are reported asAuthSource::Download; WebView2 otherwise surfaces Basic auth at the WebView level.PermissionRequestedrouting throughset_permission_handlerfor camera, microphone, and sensor-like prompts- Browser-convenience APIs: native
find_in_page/poll_find_match, nativerequest_pdf/poll_pdfusingPrintToPdfStream, andprint()via WebView2's print UI. ContextMenuRequestedrouting through both WebView2's nativeContextMenuRequestedevent and a document-start context-menu bridge, with default-menu suppression tied toWebSurfaceSettings::default_context_menus_enabled.DropDetectedrouting through a document-startDataTransferbridge; real page delivery still uses the concrete OLEIDataObjectdrag/drop helpers.MediaCaptureStateChangedrouting through a document-startgetUserMediaobserver that tracks active audio/video tracks.- Cookie-change callbacks for host cookie writes/deletes, page-side
document.cookiewrites, and nativeSet-Cookieresponse headers observed throughWebResourceResponseReceived.
WebView2 TextureStream is not treated as the primary path because it is a page/media texture stream API, not a whole-webview compositor-output API.
The Windows native battery maintains a hidden-navigation control in its Core
suite and --pixel-test in its Capture suite. The pixel control checks the first
imported frame against a dedicated fixture with background corners and an
opaque contrasting center. The general form-control probe can cover those
corners at high DPI and is not the pixel fixture.
The lower-level building blocks live in [windows_capture]:
D3D11SharedTextureFactory::create_shared_texture_frame(...)allocates an NT-handle-shareable D3D11 texture.D3D11SharedTextureFactory::copy_capture_into_existing_target(...)is the explicit-fence-only internal copy used by the composition producer. One-shot diagnostics usecopy_capture_into_shared_frame(...), which waits for the D3D11 copy before returning a fresh resource.capture_graphics_item_frame_once(...)andcapture_visual_frame_once(...)are one-shot capture helpers used by the demo's startup probes.DxgiSharedHandleBridgewraps theWebView2DxgiSharedHandleFrame→WebView2Dx12SharedFrame→WebSurfaceFrame::Native(NativeFrame::Dx12SharedTexture)handoff.
NativeChildOverlay remains the normal native-overlay fallback on every platform. macOS supports CpuSnapshot end-to-end via WKWebView.takeSnapshot (synchronous via capture_cpu_snapshot, non-blocking via request_snapshot / poll_snapshot). WPE does not provide that tier; the WebKitGTK 4.1 and 6.0 alternatives do.
CpuSnapshot is useful for diagnostics, thumbnails, and low-frequency preview paths, but it is not the target for interactive composited web surfaces.
Minimum macOS: 14.0 (Sonoma). The producer hard-depends on WKWebsiteDataStore::dataStoreForIdentifier: (per-profile storage, macOS 14+) and WKWebView::setInspectable: (macOS 13.3+). It also uses ScreenCaptureKit (macOS 12.3+), WKDownloadDelegate (macOS 11.3+), and WKWebView::interactionState (macOS 12+). All of these are called unconditionally — there are no runtime-availability guards — so building or running against an older SDK / OS is unsupported. CI targets macos-latest (Apple Silicon, currently 14+) which matches.
The macOS producer ([wkwebview_producer::WkWebViewProducer]) was developed in slice-by-slice fashion. Slices A–N cover the core surface (lifecycle, SCK pipeline, navigation, mouse / scroll / keyboard, JS messaging, snapshots, KVO, cursor reporting, profile data store, MTLSharedEvent scaffolding, resize-applies-to-stream). Items 1–9 of the browser-class roadmap (history controls, new-window intercept, settings, custom URL schemes, process-failure recovery, auth pass-through, multi-instance, downloads, find + PDF) build on top to make scrying usable for browser-shape consumers. Both rosters are tracked in design_docs/2026-05-07_platform_ceilings.md with API hooks and known limitations.
Browser-class additions on top of WebSurfaceProducer:
- History.
reload,stop,go_back,go_forward,can_go_back,can_go_forward— straightWKWebViewmappings. - New-window intercept.
NavigationEvent::NewWindowRequested { url }fires when a page tries to open a popup; the producer suppresses the engine-level popup so browser-shape consumers can route the URL into a new tab. - Settings.
apply_settings(&WebSurfaceSettings)applies zoom factor, custom user-agent, JS-enabled, and devtools (viasetInspectable, macOS 13.3+). - Custom URL schemes.
WkWebViewProducer::new_with_url_schemes(parent, config, schemes)registersWKURLSchemeHandlers on the configuration. Each scheme handler is a closureFn(&str) -> UrlSchemeResponse + Send + Sync. - Process-failure recovery.
NavigationEvent::ContentProcessTerminatedfires when the WebKit content process crashes; the WKWebView is reusable viaproducer.reload()or anotherload_url. - Auth.
NavigationEvent::AuthChallenged { url, host, auth_method }fires when the engine receives an auth challenge. With no handler the producer responds withPerformDefaultHandling(system keychain / interactive prompts); register aFn(AuthChallenge) -> AuthDispositionviaset_auth_handlerto drive the disposition yourself (HTTP basic viaAuthDisposition::UseCredential { username, password }, server-trust override, etc.). The same handler also coversWKDownloadDelegate::download:didReceiveAuthenticationChallenge:for both promotion-driven andstart_download-initiated transfers. - Permissions.
set_permission_handlerregisters aFn(PermissionRequest) -> PermissionDecisionfor camera / microphone / device-orientation requests; default with no handler isPrompt(system UI). - Cookies.
request_all_cookies+poll_cookies(async fetch),set_cookie(&Cookie)/delete_cookie(name, domain, path)(fire-and-forget). Wraps the producer'sWKHTTPCookieStore. - Incognito.
WkWebViewProducerConfig::non_persistent(or.non_persistent()builder) wiresWKWebsiteDataStore::nonPersistentDataStore— cookies / local storage / IndexedDB live only for the producer's lifetime. - Tab restoration. macOS
serialize_interaction_state() -> Option<Vec<u8>>+restore_interaction_state(&[u8])round-trip WebKit'sinteractionStateblob (back-forward list, scroll position, form data). Windows exposes same-named methods for cross-platform call sites, but WebView2 has no opaque blob equivalent: serialize returnsNone, restore returnsUnsupported. - Downloads.
NavigationEvent::DownloadStarted/DownloadProgress/DownloadFinished/DownloadCancelledcarry aDownloadIdso concurrent downloads correlate cleanly. Progress is throttled (100ms / 1MiB per download); a final emit on completion always lands.set_download_handlerlets the host pick destinations or cancel viaDownloadDecision;cancel_download(id)cancels in-flight transfers. macOS surfaces WebKitresume_dataon resumable cancellations andresume_download(&[u8], PathBuf)restarts fromresumeDownloadFromResumeData:. Windows WebView2 exposes livepause_download(id)/resume_download(id)/can_resume_download(id)while the operation exists, but cancelled downloads reportresume_data: Nonebecause WebView2 exposes no portable offline resume-data blob. Defaults:<config.download_dir>/<suggested_filename>with-Ncollision suffixing. - Find / PDF.
find_in_page(query, FindOptions)+poll_find_match() -> Option<bool>andrequest_pdf()+poll_pdf() -> Option<Result<Vec<u8>, String>>are async, mirroring the snapshot pattern. - DPI awareness. An
NSWindowDidChangeBackingPropertiesNotificationobserver re-appliesconfig.sizeon the nexttry_acquire_frame/resizeso points/pixels stay coherent across monitor moves. No host-side wiring needed. - Cursors.
set_cursor_handlerregisters aFn(CursorShape) + Send + Synccallback invoked synchronously on every system-cursor change observed after a forwarded input event. Coexists with the pull-modelpoll_cursor_shapequeue — both fire on the same change so hosts can mix push and pull. - Pointer input.
WebSurfaceProducer::send_pointer_inputsynthesizes touch / pen events through the same path assend_mouse_input; WebKit's pointer-events JS API observes them aspointerType: "mouse"because macOS has no public direct-touch synthesis API.
Key cross-API GPU-sync notes:
- The
MetalTextureRefimport path is the analog of the Windows D3D12 shared-handle path. Scry creates the producer texture and delegates the rawMTLTexture *→wgpu::Textureboundary to Graft. - IOSurface has implicit cross-API cache coherence on Apple silicon and via IOSurface locks on Intel, so today's correctness model doesn't require an explicit fence. A
MetalSharedEventSynchronizer(parallel toDx12FenceSynchronizer) is scaffolded but inert; ScreenCaptureKit doesn't expose its render queue, so there's no producer-side hook to drive a signal from. The infrastructure is ready for when SCK extends or a downstream consumer wires manual signal points.
Critical caveat for event-loop hosts: blocking entry points (navigate_to_url, navigate_to_string, start_capture, capture_cpu_snapshot) pump the main NSRunLoop and must not be called from inside a host event-loop callback (winit's resumed / window_event etc.) — the pump re-enters the host's dispatch and panics. Each blocking method's docstring carries a ⚠️ warning and a pointer to the non-blocking equivalent (load_url / load_html, start_capture_async + capture_status, request_snapshot + poll_snapshot).
The composition producer allocates a new D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED destination for every emitted frame. Its host must create a Dx12FenceSynchronizer, configure the producer with with_dx12_fence_synchronizer, and import using that same synchronizer.
- The producer copies the WGC capture output and signals the host-created
D3D12_FENCE_FLAG_SHAREDfence with a monotonic value. - Before importing, the host synchronizer queues
ID3D12CommandQueue::Wait(fence, value). - The producer never writes that destination again, so an in-flight host render cannot race the next capture copy.
The keyed-mutex path remains only for one-shot diagnostics that complete their D3D11 copy before returning their fresh resource. It is not a live composition-texture fallback.