vsync.rs: - set_state(animating, dirty) replaces set_animating. The Choreographer callback chain re-posts while animating||dirty: gating on animating alone let the chain die during flings (pointer inactivity >200ms) — with EW disabled the loop then froze (measured: 30 frames in 4s, then silence). The dirty mirror is pushed every main-loop iteration (<=8ms stale); the 6ms poll backstop bounds chain-break damage. - EW_ON now gates the WAKE itself: the early waker raced the Choreographer (fires at edge+0.52ms measured) and its independent phase caused the -4..+4ms render-phase wander. Choreographer is the single wake source. - MISS lines carry forensics: sw/sn (wake slot vs last rendered slot), eage (ms since last observed vsync edge), lastage (ms since last render) — distinguishes chain-stall / loop-stuck / guard-reject / no-wake-iteration without a second run. stg.rs: - ctxsw_sample() reads /proc/self/task/<pid>/stat for voluntary/involuntary context switches, CONDITIONAL on iteration >5ms (per-iteration procfs reads were a measured confounder: 20-50us each, pushed phase across the slot boundary, 10x worse drops). vsw/nvol on the IT line. - precpu: CPU time inside Queue::present on the PRES line (pre_us >> precpu = blocked on binder/fence/SF; pre_us ~= precpu = CPU-bound). wgpu_30_surface.rs: EW_ON moved here (vkprobe) so vsync.rs can gate the wake without a new crate dependency. lib.rs: poll backstop uses the dirty state; 6ms cap during active phases.
1012 lines
44 KiB
Rust
1012 lines
44 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
use i_slint_core::api::GraphicsAPI;
|
||
use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window};
|
||
use i_slint_core::graphics::RequestedGraphicsAPI;
|
||
use i_slint_core::partial_renderer::DirtyRegion;
|
||
use i_slint_core::platform::PlatformError;
|
||
use i_slint_core::renderer::DrawOutcome;
|
||
|
||
use std::cell::RefCell;
|
||
use std::rc::Rc;
|
||
use std::sync::Arc;
|
||
|
||
use wgpu_30 as wgpu;
|
||
|
||
use crate::SkiaSharedContext;
|
||
|
||
// Jank-hunt: per-stage timing of the Vulkan/wgpu calls inside render().
|
||
// Stalls reproduce ONLY on the Mali G715 Vulkan driver (never on the
|
||
// SwiftShader emulator), so we time each Vulkan call site and log the slow
|
||
// ones to the perf file (nblog mirrors `VK ` lines). See
|
||
// docs/BUGS/scroll-jank-120hz.md.
|
||
pub mod vkprobe {
|
||
use std::sync::atomic::{AtomicU64, Ordering};
|
||
/// Log stages slower than this (microseconds). 3 ms by default; the
|
||
/// jank-hunt apps lower it (e.g. 300) to see the whole per-frame
|
||
/// breakdown of a ~4 ms render.
|
||
pub static THRESHOLD_US: AtomicU64 = AtomicU64::new(3_000);
|
||
pub static FRAME: AtomicU64 = AtomicU64::new(0);
|
||
pub static ACQ: AtomicU64 = AtomicU64::new(0);
|
||
pub static CLR: AtomicU64 = AtomicU64::new(0);
|
||
pub static SKD: AtomicU64 = AtomicU64::new(0);
|
||
pub static SKF: AtomicU64 = AtomicU64::new(0);
|
||
pub static ORD: AtomicU64 = AtomicU64::new(0);
|
||
pub static PRE: AtomicU64 = AtomicU64::new(0);
|
||
/// 120Hz experiment: when set, skip the separate wgpu CLEAR pass (the
|
||
/// scene's opaque background covers the whole window) and move the wgpu
|
||
/// PRESENT-ordering submit to AFTER Skia's draw, so it only does the
|
||
/// texture transition. Result: 3 submits/frame instead of 4, and no
|
||
/// full-screen GPU clear. Safe while the root background is opaque.
|
||
/// NOTE: DISPROVEN — without the wgpu clear/write pass, `Queue::present`
|
||
/// clears the frame (black screen). Kept for reference; do not enable.
|
||
pub static REORDER_NOCLR: std::sync::atomic::AtomicBool =
|
||
std::sync::atomic::AtomicBool::new(false);
|
||
/// 120Hz experiment: render-ahead pipeline. When set, each `render()`
|
||
/// call first PRESENTS the frame prepared by the previous call (phase =
|
||
/// vsync wake + ~1.1 ms present, i.e. locked to the vsync edge), and
|
||
/// then prepares the NEXT frame (ACQ + CLR + Skia draw + ORD, ~3.5 ms)
|
||
/// without presenting it. The display shows one slot (8.3 ms) older
|
||
/// content at a constant, deterministic latency. Requires the loop to
|
||
/// call render() every vsync while animating (VSYNC_LOCKED_RENDER).
|
||
pub static RENDER_AHEAD: std::sync::atomic::AtomicBool =
|
||
std::sync::atomic::AtomicBool::new(false);
|
||
/// RENDER_AHEAD per-frame activation: the event loop sets this to the
|
||
/// current `locked` state before each `do_render()`, so the render-ahead
|
||
/// pipeline (hold-without-present) is only used while animating. An
|
||
/// unlocked render (idle redraw, model update) presents immediately —
|
||
/// otherwise its content would sit in the held slot forever (no further
|
||
/// renders coming) and never reach the screen.
|
||
pub static RENDER_AHEAD_ACTIVE: std::sync::atomic::AtomicBool =
|
||
std::sync::atomic::AtomicBool::new(false);
|
||
// ------------------------------------------------------------------
|
||
// PRESENT GATE (vsync-grid quantization, the real 120Hz fix).
|
||
//
|
||
// Measured on the phone: the Choreographer vsync callback reaches the
|
||
// UI thread 2.4–6 ms (median 3.3 ms) after the vsync edge, and the
|
||
// render takes 3–9 ms — so each frame's present lands at a RANDOM
|
||
// phase 8–14 ms after the edge against a 13.8 ms latch deadline.
|
||
// 51% of frames land one slot late (judder) and ~4% two slots late
|
||
// (visible hitch), independent of where in the render() call the
|
||
// present happens.
|
||
//
|
||
// Fix: quantize the PRESENT to the vsync grid. The vsync driver hands
|
||
// us the exact edge (Choreographer `frame_time_nanos`, CLOCK_MONOTONIC)
|
||
// via `note_vsync_edge`; before `Queue::present` we sleep (≤ ~2 ms) so
|
||
// the present lands at (next edge + 1.5 ms) — constant phase, ~4 ms of
|
||
// deadline margin, frame-to-frame gaps of exactly one period. See
|
||
// docs/BUGS/scroll-jank-120hz.md §9.
|
||
/// Latest Choreographer `frame_time_nanos` (CLOCK_MONOTONIC ns).
|
||
pub static VSYNC_EDGE_NS: AtomicU64 = AtomicU64::new(0);
|
||
/// Detected vsync period in ns (from consecutive callback deltas).
|
||
pub static VSYNC_PERIOD_NS: AtomicU64 = AtomicU64::new(0);
|
||
/// Gate enabled for the current render (set per-frame by the event
|
||
/// loop; true while animating/locked, false for one-off redraws so
|
||
/// taps and model updates keep minimum latency).
|
||
pub static PRESENT_GATE_ON: std::sync::atomic::AtomicBool =
|
||
std::sync::atomic::AtomicBool::new(false);
|
||
/// Microseconds the most recent `present_gate()` slept (0 = no sleep).
|
||
pub static GATE_US: AtomicU64 = AtomicU64::new(0);
|
||
/// WAKE PROBE: log every early-waker attempt (EW lines) — H1 check.
|
||
pub static EW_ON: std::sync::atomic::AtomicBool =
|
||
std::sync::atomic::AtomicBool::new(false);
|
||
/// CLOCK_MONOTONIC ns when the last vsync-wake (locked) render STARTED.
|
||
/// The event loop uses it to skip a second wake (e.g. the Choreographer's
|
||
/// late fallback wake arriving ~5 ms after the early-waker's edge wake
|
||
/// already triggered a render) instead of double-rendering the slot.
|
||
pub static LAST_RENDER_START_NS: AtomicU64 = AtomicU64::new(0);
|
||
|
||
/// Called from the vsync driver on every Choreographer callback.
|
||
pub fn note_vsync_edge(ftn_ns: u64) {
|
||
let prev = VSYNC_EDGE_NS.swap(ftn_ns, Ordering::Relaxed);
|
||
if prev > 0 {
|
||
let d = ftn_ns.saturating_sub(prev);
|
||
if (4_000_000..20_000_000).contains(&d) {
|
||
VSYNC_PERIOD_NS.store(d, Ordering::Relaxed);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Sleep so the `Queue::present` that follows lands at (latest vsync
|
||
/// edge + one period + 1.5 ms). No-op when disabled, when the grid is
|
||
/// unknown/stale (no recent callbacks), or when already past target.
|
||
pub fn present_gate() {
|
||
if !PRESENT_GATE_ON.load(Ordering::Acquire) {
|
||
return;
|
||
}
|
||
let edge = VSYNC_EDGE_NS.load(Ordering::Relaxed);
|
||
let period = VSYNC_PERIOD_NS.load(Ordering::Relaxed);
|
||
if edge == 0 || period == 0 {
|
||
return;
|
||
}
|
||
let now = monotonic_ns();
|
||
if now == 0 || now > edge + 4 * period {
|
||
return; // stale grid (e.g. after resume) — don't gate
|
||
}
|
||
// Earliest grid phase (a vsync edge + 1.5 ms) strictly after now:
|
||
// T = edge + m*period + 1.5 ms with m = floor((now-edge-1.5ms)
|
||
// / period) + 1.
|
||
// NOTE: a Choreographer callback can fire MID-RENDER (the next
|
||
// edge's callback arrives at edge+~11.6 ms while a slow render is
|
||
// still running), which advances `edge` by one period — the
|
||
// smallest-phase rule above then just yields the same absolute T, so
|
||
// the mid-render callback can never add an extra slot of delay.
|
||
const PHASE_NS: u64 = 1_500_000;
|
||
let rel = now.saturating_sub(edge).saturating_sub(PHASE_NS) / period;
|
||
let target = edge + (rel + 1) * period + PHASE_NS;
|
||
let wait = target.saturating_sub(now);
|
||
// Only sleep for close phases; if the next grid phase is >4 ms out
|
||
// this frame is already running late for it — present immediately
|
||
// (the next frame re-syncs to the grid).
|
||
let slept = if wait > 200_000 && wait <= 4_000_000 { wait } else { 0 };
|
||
GATE_US.store(slept / 1000, Ordering::Relaxed);
|
||
if slept > 0 {
|
||
std::thread::sleep(std::time::Duration::from_nanos(slept));
|
||
}
|
||
}
|
||
|
||
pub fn scn_reset() {
|
||
crate::scn::reset();
|
||
}
|
||
|
||
pub fn scn_report(frame: u64) {
|
||
crate::scn::report(frame);
|
||
}
|
||
|
||
pub fn now_us() -> u64 {
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.map(|d| d.as_micros() as u64)
|
||
.unwrap_or(0)
|
||
}
|
||
|
||
/// CLOCK_THREAD_CPUTIME_ID nanoseconds — CPU time consumed by the
|
||
/// (current) thread. For a slow frame window: wall ≫ cpu ⇒ the thread
|
||
/// was descheduled/blocked between calls; wall ≈ cpu ⇒ pure CPU work.
|
||
pub fn thread_cpu_ns() -> u64 {
|
||
#[repr(C)]
|
||
struct Ts {
|
||
tv_sec: i64,
|
||
tv_nsec: i64,
|
||
}
|
||
unsafe extern "C" {
|
||
fn clock_gettime(clkid: i32, tp: *mut Ts) -> i32;
|
||
}
|
||
const CLOCK_THREAD_CPUTIME_ID: i32 = 3;
|
||
let mut ts = Ts { tv_sec: 0, tv_nsec: 0 };
|
||
if unsafe { clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) } == 0 {
|
||
ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
|
||
} else {
|
||
0
|
||
}
|
||
}
|
||
|
||
/// CLOCK_MONOTONIC nanoseconds — the same clock/epoch as the backend's
|
||
/// VSYNC `mon=` stamp, so PRES and VSYNC can be correlated offline for
|
||
/// frame-alignment (phase of present vs the vsync grid).
|
||
pub fn monotonic_ns() -> u64 {
|
||
#[repr(C)]
|
||
struct Ts {
|
||
tv_sec: i64,
|
||
tv_nsec: i64,
|
||
}
|
||
unsafe extern "C" {
|
||
fn clock_gettime(clkid: i32, tp: *mut Ts) -> i32;
|
||
}
|
||
const CLOCK_MONOTONIC: i32 = 1;
|
||
let mut ts = Ts { tv_sec: 0, tv_nsec: 0 };
|
||
if unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts) } == 0 {
|
||
ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
|
||
} else {
|
||
0
|
||
}
|
||
}
|
||
|
||
// Log one slow stage (>= 3 ms) with its running-max and total so we can
|
||
// see both the worst case and the accumulated cost per call site.
|
||
pub fn report(stage: &str, slot: &AtomicU64, us: u64) {
|
||
let _ = slot.fetch_max(us, Ordering::Relaxed);
|
||
if us >= THRESHOLD_US.load(Ordering::Relaxed) {
|
||
let f = FRAME.load(Ordering::Relaxed);
|
||
log::info!("VK {} {}us f={} max={}us", stage, us, f, slot.load(Ordering::Relaxed));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(target_family = "windows")]
|
||
mod dx12;
|
||
#[cfg(target_vendor = "apple")]
|
||
mod metal;
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
mod vulkan;
|
||
|
||
/// See [`crate::attachment_color_space`].
|
||
pub(crate) fn attachment_color_space(texture: &wgpu::Texture) -> skia_safe::ColorSpace {
|
||
crate::attachment_color_space(crate::TextureEncoding::from_format_is_srgb(
|
||
texture.format().is_srgb(),
|
||
))
|
||
}
|
||
|
||
/// See [`crate::sampled_texture_color_space`].
|
||
#[cfg_attr(not(feature = "unstable-wgpu-30"), allow(dead_code))]
|
||
pub(crate) fn sampled_texture_color_space(texture: &wgpu::Texture) -> skia_safe::ColorSpace {
|
||
crate::sampled_texture_color_space(crate::TextureEncoding::from_format_is_srgb(
|
||
texture.format().is_srgb(),
|
||
))
|
||
}
|
||
|
||
/// Skia rendering surface backed by WGPU. Supports both on-screen rendering (with a
|
||
/// window surface) and offscreen rendering into caller-provided textures.
|
||
pub struct WGPUSurface {
|
||
pub(crate) gr_context: RefCell<skia_safe::gpu::DirectContext>,
|
||
wgpu: Rc<SharedWgpuState>,
|
||
surface_config: RefCell<Option<wgpu::SurfaceConfiguration>>,
|
||
surface: Option<wgpu::Surface<'static>>,
|
||
textures_to_transition_for_sampling: RefCell<Vec<wgpu::Texture>>,
|
||
pub(crate) backend: Backend,
|
||
alpha_modes: Vec<wgpu::CompositeAlphaMode>,
|
||
/// RENDER_AHEAD: the frame prepared by the previous `render()` call,
|
||
/// presented at the start of the next one. `1` = frame index it was
|
||
/// prepared under (for the PRES log line).
|
||
pending_frame: RefCell<Option<(wgpu::SurfaceTexture, u64)>>,
|
||
}
|
||
|
||
impl WGPUSurface {
|
||
pub fn new_with_surface(
|
||
surface_target: impl Into<i_slint_core::graphics::wgpu_30::SurfaceTarget>,
|
||
size: PhysicalWindowSize,
|
||
requested_graphics_api: Option<RequestedGraphicsAPI>,
|
||
) -> Result<Self, PlatformError> {
|
||
let (instance, adapter, device, queue, surface) =
|
||
i_slint_core::graphics::wgpu_30::init_instance_adapter_device_queue_surface(
|
||
surface_target,
|
||
requested_graphics_api,
|
||
i_slint_core::graphics::wgpu_30::default_backends_to_avoid(),
|
||
)?;
|
||
Self::init_with_parts(
|
||
Rc::new(SharedWgpuState { instance, adapter, device, queue }),
|
||
surface,
|
||
size,
|
||
)
|
||
}
|
||
|
||
fn init_with_parts(
|
||
wgpu: Rc<SharedWgpuState>,
|
||
surface: wgpu::Surface<'static>,
|
||
size: PhysicalWindowSize,
|
||
) -> Result<Self, PlatformError> {
|
||
let SharedWgpuState { adapter, device, queue, .. } = &*wgpu;
|
||
#[cfg(target_vendor = "apple")]
|
||
metal::set_layer_contents_gravity(&surface);
|
||
|
||
// On iOS a window has no size until UIKit attaches it to a scene,
|
||
// and wgpu rejects a zero-sized configure.
|
||
// Start at 1x1; `resize_event` applies the real size.
|
||
let mut surface_config = surface
|
||
.get_default_config(adapter, size.width.max(1), size.height.max(1))
|
||
.ok_or_else(|| PlatformError::from("WGPU surface is not compatible with adapter"))?;
|
||
|
||
let swapchain_capabilities = surface.get_capabilities(adapter);
|
||
let swapchain_format = swapchain_capabilities
|
||
.formats
|
||
.iter()
|
||
.find(|f| {
|
||
matches!(f, wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm)
|
||
})
|
||
.copied()
|
||
.unwrap_or_else(|| swapchain_capabilities.formats[0]);
|
||
surface_config.format = swapchain_format;
|
||
|
||
// Prefer Mailbox / Immediate over Fifo/AutoVsync to prevent queue.present
|
||
// from blocking the render thread on vsync backpressure (which causes
|
||
// 17-55 ms tail stalls / dropped frames on 120Hz mobile GPUs).
|
||
surface_config.present_mode = if swapchain_capabilities.present_modes.contains(&wgpu::PresentMode::Mailbox) {
|
||
wgpu::PresentMode::Mailbox
|
||
} else if swapchain_capabilities.present_modes.contains(&wgpu::PresentMode::Immediate) {
|
||
wgpu::PresentMode::Immediate
|
||
} else {
|
||
wgpu::PresentMode::AutoVsync
|
||
};
|
||
|
||
let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||
surface.configure(device, &surface_config);
|
||
if let Some(e) = spin_on::spin_on(error_scope.pop()) {
|
||
return Err(PlatformError::from(format!("Error configuring WGPU surface: {e}")));
|
||
}
|
||
|
||
let backend = Backend::new(adapter, device)?;
|
||
|
||
let gr_context = backend
|
||
.make_context(adapter, device, queue)
|
||
.ok_or_else(|| PlatformError::from("Failed to create Skia context from WGPU"))?;
|
||
|
||
Ok(Self {
|
||
gr_context: gr_context.into(),
|
||
wgpu,
|
||
surface_config: Some(surface_config).into(),
|
||
surface: Some(surface),
|
||
textures_to_transition_for_sampling: RefCell::new(Vec::new()),
|
||
backend,
|
||
alpha_modes: swapchain_capabilities.alpha_modes,
|
||
pending_frame: RefCell::new(None),
|
||
})
|
||
}
|
||
|
||
pub(crate) fn new_offscreen(
|
||
instance: wgpu::Instance,
|
||
adapter: wgpu::Adapter,
|
||
device: wgpu::Device,
|
||
queue: wgpu::Queue,
|
||
backend: Backend,
|
||
gr_context: skia_safe::gpu::DirectContext,
|
||
) -> Self {
|
||
Self {
|
||
gr_context: RefCell::new(gr_context),
|
||
wgpu: Rc::new(SharedWgpuState { instance, adapter, device, queue }),
|
||
surface_config: None.into(),
|
||
surface: None,
|
||
textures_to_transition_for_sampling: RefCell::new(Vec::new()),
|
||
backend,
|
||
alpha_modes: vec![],
|
||
pending_frame: RefCell::new(None),
|
||
}
|
||
}
|
||
|
||
/// RENDER_AHEAD: release a held frame before a reconfigure, where a
|
||
/// stale-size frame would be wrong anyway. PRESENTS it (rather than
|
||
/// just dropping it): wgpu's Presentation tracks the acquired image
|
||
/// independently of the Rust Arc, so a bare drop would leave it
|
||
/// "acquired" and the next get_current_texture would fail with
|
||
/// AlreadyAcquired. The compositor handles the one-frame size change.
|
||
fn discard_pending_frame(&self) {
|
||
if let Some((pf, _)) = self.pending_frame.borrow_mut().take() {
|
||
self.wgpu.queue.present(pf);
|
||
}
|
||
}
|
||
|
||
/// Transitions any imported wgpu textures to sampling state and flushes
|
||
/// the Skia graphics context. Must be called after rendering to ensure
|
||
/// Skia's GPU work is submitted.
|
||
pub(crate) fn flush_and_submit(&self, gr_context: &mut skia_safe::gpu::DirectContext) {
|
||
let textures_to_transition = self.textures_to_transition_for_sampling.take();
|
||
if !textures_to_transition.is_empty() {
|
||
let mut encoder =
|
||
self.wgpu.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||
label: Some("Skia texture transition encoder"),
|
||
});
|
||
encoder.transition_resources(
|
||
std::iter::empty(),
|
||
textures_to_transition.iter().map(|texture| wgpu::TextureTransition {
|
||
texture,
|
||
selector: None,
|
||
state: wgpu::TextureUses::RESOURCE,
|
||
}),
|
||
);
|
||
self.wgpu.queue.submit(Some(encoder.finish()));
|
||
}
|
||
|
||
gr_context.submit(None);
|
||
}
|
||
}
|
||
|
||
/// The wgpu stack a surface renders with.
|
||
///
|
||
/// [`SkiaSharedContext`] caches the one the first window created, so that later windows it can
|
||
/// serve reuse it instead of creating their own.
|
||
/// That cache holds a weak reference and every surface a strong one, so the resources go away
|
||
/// with the last surface using them.
|
||
/// Were they to outlive the surfaces, they'd be destroyed whenever the last renderer happens to
|
||
/// be dropped, and tearing a wgpu device down from a thread-local destructor aborts the process
|
||
/// on macOS, where the Metal backend needs an autorelease pool that is gone by then.
|
||
/// A window that is still shown owns its surface, so that case remains.
|
||
pub(crate) struct SharedWgpuState {
|
||
instance: wgpu::Instance,
|
||
adapter: wgpu::Adapter,
|
||
device: wgpu::Device,
|
||
queue: wgpu::Queue,
|
||
}
|
||
|
||
/// Whether `device` offers everything `settings` asks of it.
|
||
///
|
||
/// The instance and adapter parts of the settings are covered by
|
||
/// [`adapter_matches_graphics_api_request`].
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
fn device_satisfies_settings(
|
||
device: &wgpu::Device,
|
||
settings: &i_slint_core::graphics::wgpu_30::api::WGPUSettings,
|
||
) -> bool {
|
||
device.features().contains(settings.device_required_features)
|
||
&& settings.device_required_limits.check_limits(&device.limits())
|
||
}
|
||
|
||
fn adapter_matches_graphics_api_request(
|
||
adapter: &wgpu::Adapter,
|
||
requested_graphics_api: Option<&RequestedGraphicsAPI>,
|
||
) -> bool {
|
||
let backend = adapter.get_info().backend;
|
||
|
||
#[cfg_attr(slint_nightly_test, allow(non_exhaustive_omitted_patterns))]
|
||
match requested_graphics_api {
|
||
None => true,
|
||
Some(RequestedGraphicsAPI::Metal) => backend == wgpu::Backend::Metal,
|
||
Some(RequestedGraphicsAPI::Vulkan) => backend == wgpu::Backend::Vulkan,
|
||
Some(RequestedGraphicsAPI::Direct3D) => backend == wgpu::Backend::Dx12,
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
Some(RequestedGraphicsAPI::WGPU30(
|
||
i_slint_core::graphics::wgpu_30::api::WGPUConfiguration::Automatic(settings),
|
||
)) => {
|
||
let backend_bit = match backend {
|
||
wgpu::Backend::Vulkan => wgpu::Backends::VULKAN,
|
||
wgpu::Backend::Metal => wgpu::Backends::METAL,
|
||
wgpu::Backend::Dx12 => wgpu::Backends::DX12,
|
||
wgpu::Backend::Gl => wgpu::Backends::GL,
|
||
wgpu::Backend::BrowserWebGpu => wgpu::Backends::BROWSER_WEBGPU,
|
||
_ => return false,
|
||
};
|
||
settings.backends.contains(backend_bit)
|
||
}
|
||
Some(_) => false,
|
||
}
|
||
}
|
||
|
||
impl crate::Surface for WGPUSurface {
|
||
fn new(
|
||
shared_context: &SkiaSharedContext,
|
||
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
|
||
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
|
||
size: PhysicalWindowSize,
|
||
requested_graphics_api: Option<RequestedGraphicsAPI>,
|
||
) -> Result<Self, PlatformError> {
|
||
let make_target = || -> Box<dyn wgpu::DisplayAndWindowHandle + 'static> {
|
||
Box::new(WindowAndDisplayHandle(window_handle.clone(), display_handle.clone()))
|
||
};
|
||
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
let manual_configuration = matches!(
|
||
&requested_graphics_api,
|
||
Some(RequestedGraphicsAPI::WGPU30(
|
||
i_slint_core::graphics::wgpu_30::api::WGPUConfiguration::Manual { .. }
|
||
))
|
||
);
|
||
#[cfg(not(feature = "unstable-wgpu-30"))]
|
||
let manual_configuration = false;
|
||
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
#[cfg_attr(slint_nightly_test, allow(non_exhaustive_omitted_patterns))]
|
||
let requested_settings = match &requested_graphics_api {
|
||
Some(RequestedGraphicsAPI::WGPU30(
|
||
i_slint_core::graphics::wgpu_30::api::WGPUConfiguration::Automatic(settings),
|
||
)) => Some(settings.clone()),
|
||
_ => None,
|
||
};
|
||
|
||
// try to reuse old / shared graphics primitives from previous windows with matching
|
||
// settings
|
||
if !manual_configuration {
|
||
let shared_state = shared_context.0.wgpu_30_state.borrow().upgrade();
|
||
if let Some(shared) = shared_state {
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
let settings_compatible = requested_settings
|
||
.as_ref()
|
||
.is_none_or(|settings| device_satisfies_settings(&shared.device, settings));
|
||
#[cfg(not(feature = "unstable-wgpu-30"))]
|
||
let settings_compatible = true;
|
||
if settings_compatible
|
||
&& adapter_matches_graphics_api_request(
|
||
&shared.adapter,
|
||
requested_graphics_api.as_ref(),
|
||
)
|
||
&& let Ok(surface) = shared.instance.create_surface(make_target())
|
||
&& shared.adapter.is_surface_supported(&surface)
|
||
{
|
||
match Self::init_with_parts(shared, surface, size) {
|
||
Ok(surface) => return Ok(surface),
|
||
Err(err) => {
|
||
// The shared device may be lost. Drop it and start over.
|
||
i_slint_core::debug_log!(
|
||
"Failed to reuse the shared WGPU device: {err} . Re-initializing"
|
||
);
|
||
shared_context.0.wgpu_30_state.take();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let (instance, adapter, device, queue, surface) =
|
||
i_slint_core::graphics::wgpu_30::init_instance_adapter_device_queue_surface(
|
||
make_target(),
|
||
requested_graphics_api,
|
||
i_slint_core::graphics::wgpu_30::default_backends_to_avoid(),
|
||
)?;
|
||
let wgpu = Rc::new(SharedWgpuState { instance, adapter, device, queue });
|
||
let new_surface = Self::init_with_parts(wgpu.clone(), surface, size)?;
|
||
if !manual_configuration {
|
||
*shared_context.0.wgpu_30_state.borrow_mut() = Rc::downgrade(&wgpu);
|
||
}
|
||
Ok(new_surface)
|
||
}
|
||
|
||
fn name(&self) -> &'static str {
|
||
if self.surface.is_some() { "wgpu" } else { "wgpu-texture" }
|
||
}
|
||
|
||
fn resize_event(&self, size: PhysicalWindowSize) -> Result<(), PlatformError> {
|
||
let mut surface_config_opt = self.surface_config.borrow_mut();
|
||
let (Some(surface_config), Some(surface)) = (surface_config_opt.as_mut(), &self.surface)
|
||
else {
|
||
return Ok(());
|
||
};
|
||
self.discard_pending_frame();
|
||
|
||
// Skip reconfigure if size hasn't changed — DRM/KMS surfaces don't
|
||
// support being reconfigured.
|
||
if surface_config.width == size.width && surface_config.height == size.height {
|
||
return Ok(());
|
||
}
|
||
|
||
{
|
||
let gr_context = &mut self.gr_context.borrow_mut();
|
||
// This is brute force, but for the lack of access to the fences this seems to work: Avoid any pending work so that
|
||
// IDXGISwapChain::ResizeBuffers doesn't complain that the surface is still in use.
|
||
gr_context.flush_submit_and_sync_cpu();
|
||
}
|
||
|
||
surface_config.width = size.width;
|
||
surface_config.height = size.height;
|
||
|
||
surface.configure(&self.wgpu.device, surface_config);
|
||
Ok(())
|
||
}
|
||
|
||
fn render(
|
||
&self,
|
||
_window: &Window,
|
||
_size: PhysicalWindowSize,
|
||
callback: &dyn Fn(
|
||
&skia_safe::Canvas,
|
||
Option<&mut skia_safe::gpu::DirectContext>,
|
||
u8,
|
||
) -> Option<DirtyRegion>,
|
||
pre_present_callback: &RefCell<Option<Box<dyn FnMut()>>>,
|
||
) -> Result<DrawOutcome, PlatformError> {
|
||
let (Some(surface), Some(surface_config)) = (&self.surface, &*self.surface_config.borrow())
|
||
else {
|
||
return Err("WGPUSurface::render() called on offscreen surface".into());
|
||
};
|
||
|
||
let gr_context = &mut self.gr_context.borrow_mut();
|
||
|
||
// Jank-hunt: Vulkan stage timing (see vkprobe).
|
||
let __f = vkprobe::FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||
vkprobe::scn_reset();
|
||
|
||
// RENDER_AHEAD: present the frame prepared by the PREVIOUS render()
|
||
// call FIRST, so the present lands at (vsync wake + ~1.1 ms) instead
|
||
// of (vsync wake + full render). The frame prepared below is held
|
||
// (when ahead is active) until the next call presents it (one slot
|
||
// of constant latency).
|
||
//
|
||
// This block must run in BOTH modes: an unlocked render following a
|
||
// held frame must PRESENT the held frame (not just drop it) —
|
||
// wgpu's Presentation tracks the acquired image independently of the
|
||
// Rust Arc, so a bare drop leaves it "acquired" and the next
|
||
// get_current_texture fails with AlreadyAcquired.
|
||
let __ahead = vkprobe::RENDER_AHEAD.load(std::sync::atomic::Ordering::Relaxed)
|
||
&& vkprobe::RENDER_AHEAD_ACTIVE.load(std::sync::atomic::Ordering::Relaxed);
|
||
if let Some((pf, pf_idx)) = self.pending_frame.borrow_mut().take() {
|
||
if let Some(cb) = pre_present_callback.borrow_mut().as_mut() {
|
||
cb();
|
||
}
|
||
let t_pre = vkprobe::now_us();
|
||
// precpu = CPU time inside Queue::present (vkprobe::thread_cpu_ns
|
||
// exists already). pre_us≫precpu ⇒ present BLOCKED (binder/fence
|
||
// wait on the driver/SF — the "Android API call" contention);
|
||
// pre_us≈precpu ⇒ CPU-bound present.
|
||
let __pc0 = vkprobe::thread_cpu_ns();
|
||
self.wgpu.queue.present(pf);
|
||
let pre_us = vkprobe::now_us().saturating_sub(t_pre);
|
||
let precpu_us = vkprobe::thread_cpu_ns().saturating_sub(__pc0) / 1000;
|
||
vkprobe::report("PRE", &vkprobe::PRE, pre_us);
|
||
let mon = vkprobe::monotonic_ns();
|
||
if mon > 0 {
|
||
log::info!("PRES f={} mon={} pre_us={} precpu={}us", pf_idx, mon, pre_us, precpu_us);
|
||
}
|
||
}
|
||
let __cpu0 = vkprobe::thread_cpu_ns();
|
||
let __t_acq = vkprobe::now_us();
|
||
let frame = match surface.get_current_texture() {
|
||
wgpu::CurrentSurfaceTexture::Success(t) => t,
|
||
wgpu::CurrentSurfaceTexture::Occluded => return Ok(DrawOutcome::Occluded),
|
||
wgpu::CurrentSurfaceTexture::Timeout => return Ok(DrawOutcome::Timeout),
|
||
wgpu::CurrentSurfaceTexture::Validation => {
|
||
return Err("WGPU surface validation error in get_current_texture".into());
|
||
}
|
||
stale @ (wgpu::CurrentSurfaceTexture::Outdated
|
||
| wgpu::CurrentSurfaceTexture::Suboptimal(_)
|
||
| wgpu::CurrentSurfaceTexture::Lost) => {
|
||
// `Suboptimal` carries a live `SurfaceTexture`; matched with `_` it is not bound,
|
||
// so the value returned by `get_current_texture()` keeps it alive across the
|
||
// `surface.configure()` below — which wgpu forbids ("`SurfaceOutput` must be
|
||
// dropped before a new `Surface` is made"), panicking on the first frame on
|
||
// Wayland. Drop it first. (`Outdated`/`Lost` carry nothing → no-op.)
|
||
drop(stale);
|
||
surface.configure(&self.wgpu.device, surface_config);
|
||
match surface.get_current_texture() {
|
||
wgpu::CurrentSurfaceTexture::Success(t) => t,
|
||
_ => return Ok(DrawOutcome::Occluded),
|
||
}
|
||
}
|
||
};
|
||
vkprobe::report("ACQ", &vkprobe::ACQ, vkprobe::now_us().saturating_sub(__t_acq));
|
||
|
||
if !vkprobe::REORDER_NOCLR.load(std::sync::atomic::Ordering::Relaxed) {
|
||
// Skia renders through the raw backend queue, invisible to wgpu's usage
|
||
// tracking, so `Queue::present` would treat the frame as never written
|
||
// and clear it. Clear it through wgpu instead before Skia draws; the
|
||
// in-order queue keeps Skia's later-committed work on top.
|
||
let __t_clr = vkprobe::now_us();
|
||
let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||
let mut encoder =
|
||
self.wgpu.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||
label: Some("Slint frame init"),
|
||
});
|
||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("Slint frame init"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: &view,
|
||
depth_slice: None,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
timestamp_writes: None,
|
||
occlusion_query_set: None,
|
||
multiview_mask: None,
|
||
});
|
||
self.wgpu.queue.submit(Some(encoder.finish()));
|
||
vkprobe::report("CLR", &vkprobe::CLR, vkprobe::now_us().saturating_sub(__t_clr));
|
||
}
|
||
|
||
let __t_skd = vkprobe::now_us();
|
||
let skia_surface = self.backend.make_swapchain_surface(gr_context, &frame.texture);
|
||
|
||
let mut skia_surface = skia_surface
|
||
.ok_or_else(|| PlatformError::from("Failed to create Skia surface from WGPU"))?;
|
||
|
||
callback(skia_surface.canvas(), Some(gr_context), 0);
|
||
|
||
self.backend.release_swapchain_surface(gr_context, &mut skia_surface);
|
||
vkprobe::report("SKD", &vkprobe::SKD, vkprobe::now_us().saturating_sub(__t_skd));
|
||
vkprobe::scn_report(__f);
|
||
|
||
let __t_skf = vkprobe::now_us();
|
||
self.flush_and_submit(gr_context);
|
||
vkprobe::report("SKF", &vkprobe::SKF, vkprobe::now_us().saturating_sub(__t_skf));
|
||
|
||
// Skia drew via the raw queue behind wgpu's back, and `Queue::present` skips its own
|
||
// transition when the tracker already says PRESENT, so nothing would order the
|
||
// presentation after Skia's work: the clear submission above signals its semaphore
|
||
// before Skia even starts. Referencing the frame texture in a submission of our own
|
||
// makes present wait for it. (With REORDER_NOCLR this submit is the ONLY
|
||
// wgpu submission of the frame and happens AFTER Skia's, so the in-order
|
||
// vkQueue + present's semaphore wait still order presentation after the draw.)
|
||
let __t_ord = vkprobe::now_us();
|
||
let mut encoder =
|
||
self.wgpu.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||
label: Some("Skia present ordering encoder"),
|
||
});
|
||
encoder.transition_resources(
|
||
std::iter::empty(),
|
||
std::iter::once(wgpu::TextureTransition {
|
||
texture: &frame.texture,
|
||
selector: None,
|
||
state: wgpu::TextureUses::PRESENT,
|
||
}),
|
||
);
|
||
self.wgpu.queue.submit(Some(encoder.finish()));
|
||
vkprobe::report("ORD", &vkprobe::ORD, vkprobe::now_us().saturating_sub(__t_ord));
|
||
|
||
if let Some(pre_present_callback) = pre_present_callback.borrow_mut().as_mut() {
|
||
pre_present_callback();
|
||
}
|
||
|
||
let __t_pre = vkprobe::now_us();
|
||
let __pre_us = if __ahead {
|
||
// Render-ahead (locked/animating renders): do NOT present yet;
|
||
// hold the frame for the next render() call (one slot of
|
||
// constant latency). The PREVIOUS frame was already presented at
|
||
// the start of this call. (The very first frame of the app's
|
||
// lifetime is an unlocked render → presented immediately, so the
|
||
// screen never stays black.)
|
||
self.pending_frame.borrow_mut().replace((frame, __f));
|
||
0
|
||
} else {
|
||
// Unlocked render: present immediately. (Any frame left in the
|
||
// held slot was already presented at the top of this call.)
|
||
// PRESENT GATE: while animating, land this present at a fixed
|
||
// vsync-grid phase (next edge + 1.5 ms) so frame-to-frame
|
||
// cadence is exactly one period (see `present_gate`).
|
||
vkprobe::present_gate();
|
||
self.wgpu.queue.present(frame);
|
||
vkprobe::now_us().saturating_sub(__t_pre)
|
||
};
|
||
if __pre_us > 0 {
|
||
vkprobe::report("PRE", &vkprobe::PRE, __pre_us);
|
||
|
||
// Frame-alignment probe: monotonic ns at the moment present()
|
||
// returned, tagged with the frame counter. Correlated offline
|
||
// against VSYNC mon= stamps: phase = pres_mon - previous_vsync_mon.
|
||
let __mon = vkprobe::monotonic_ns();
|
||
if __mon > 0 {
|
||
log::info!(
|
||
"PRES f={} mon={} pre_us={} gate_us={}",
|
||
__f,
|
||
__mon,
|
||
__pre_us,
|
||
vkprobe::GATE_US.load(std::sync::atomic::Ordering::Relaxed)
|
||
);
|
||
}
|
||
}
|
||
|
||
// Jank-hunt: for slow frames, CPU-vs-wall split for the WHOLE render
|
||
// window. Correlated offline with the VKF line (time inside vk calls
|
||
// per frame) from the Vulkan shim:
|
||
// wall ≈ cpu → pure CPU work (Skia/wgpu on the UI thread)
|
||
// wall ≫ cpu, vk-time high → blocked inside the driver (fence etc.)
|
||
// wall ≫ cpu, vk-time low → descheduled/between calls
|
||
let __wall = vkprobe::now_us().saturating_sub(__t_acq);
|
||
let __cpu = vkprobe::thread_cpu_ns().saturating_sub(__cpu0) / 1000;
|
||
if __wall >= 8_000 {
|
||
log::info!("CPUW f={} wall_us={} cpu_us={}", __f, __wall, __cpu);
|
||
}
|
||
|
||
Ok(DrawOutcome::Success)
|
||
}
|
||
|
||
fn bits_per_pixel(&self) -> Result<u8, PlatformError> {
|
||
if let Some(surface_config) = &*self.surface_config.borrow() {
|
||
Ok(match surface_config.format {
|
||
wgpu_30::TextureFormat::Rgba8Unorm
|
||
| wgpu_30::TextureFormat::Rgba8UnormSrgb
|
||
| wgpu_30::TextureFormat::Bgra8Unorm
|
||
| wgpu_30::TextureFormat::Bgra8UnormSrgb => 32,
|
||
fmt => return Err(format!("Unsupported surface format {:#?}", fmt).into()),
|
||
})
|
||
} else {
|
||
// All supported render-target formats (Rgba8Unorm, Bgra8Unorm, and sRGB variants) are 32bpp.
|
||
Ok(32)
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
fn with_graphics_api(&self, callback: &mut dyn FnMut(GraphicsAPI<'_>)) {
|
||
let api = i_slint_core::graphics::create_graphics_api_wgpu_30(
|
||
self.wgpu.instance.clone(),
|
||
self.wgpu.device.clone(),
|
||
self.wgpu.queue.clone(),
|
||
);
|
||
callback(api)
|
||
}
|
||
|
||
#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
|
||
fn import_wgpu_texture(
|
||
&self,
|
||
canvas: &skia_safe::Canvas,
|
||
any_wgpu_texture: &i_slint_core::graphics::WGPUTexture,
|
||
) -> Option<skia_safe::Image> {
|
||
let texture: wgpu_30::Texture = match any_wgpu_texture {
|
||
#[cfg(feature = "unstable-wgpu-29")]
|
||
i_slint_core::graphics::WGPUTexture::WGPU29Texture(..) => return None,
|
||
#[cfg(feature = "unstable-wgpu-30")]
|
||
i_slint_core::graphics::WGPUTexture::WGPU30Texture(texture) => texture.clone(),
|
||
};
|
||
|
||
// Skia won't submit commands right away, so remember the texture and transition before
|
||
// submitting.
|
||
self.textures_to_transition_for_sampling.borrow_mut().push(texture.clone());
|
||
|
||
self.backend.import_texture(canvas, texture)
|
||
}
|
||
|
||
fn set_transparent(&self, transparent: bool) -> Result<(), PlatformError> {
|
||
// `Opaque` discards the scene's alpha; pick a translucent mode if offered.
|
||
// Metal (CAMetalLayer) only offers `PostMultiplied`, so it must be a fallback.
|
||
use wgpu::CompositeAlphaMode::{Opaque, PostMultiplied, PreMultiplied};
|
||
let wanted: &[wgpu::CompositeAlphaMode] =
|
||
if transparent { &[PreMultiplied, PostMultiplied] } else { &[Opaque] };
|
||
let Some(mode) = wanted.iter().copied().find(|m| self.alpha_modes.contains(m)) else {
|
||
return Ok(());
|
||
};
|
||
|
||
let mut surface_config_opt = self.surface_config.borrow_mut();
|
||
let (Some(surface_config), Some(surface)) = (surface_config_opt.as_mut(), &self.surface)
|
||
else {
|
||
return Ok(());
|
||
};
|
||
if surface_config.alpha_mode != mode {
|
||
surface_config.alpha_mode = mode;
|
||
self.discard_pending_frame();
|
||
surface.configure(&self.wgpu.device, surface_config);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn as_any(&self) -> &dyn core::any::Any {
|
||
self
|
||
}
|
||
}
|
||
|
||
struct WindowAndDisplayHandle(
|
||
Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
|
||
Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
|
||
);
|
||
|
||
impl raw_window_handle::HasWindowHandle for WindowAndDisplayHandle {
|
||
fn window_handle(
|
||
&self,
|
||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||
self.0.window_handle()
|
||
}
|
||
}
|
||
|
||
impl raw_window_handle::HasDisplayHandle for WindowAndDisplayHandle {
|
||
fn display_handle(
|
||
&self,
|
||
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||
self.1.display_handle()
|
||
}
|
||
}
|
||
|
||
pub(crate) enum Backend {
|
||
#[cfg(target_vendor = "apple")]
|
||
Metal,
|
||
#[cfg(target_family = "windows")]
|
||
Dx12,
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
Vulkan {
|
||
/// The family Skia has to hand the swapchain image back to, see
|
||
/// [`Backend::release_swapchain_surface`].
|
||
queue_family_index: u32,
|
||
},
|
||
}
|
||
|
||
impl Backend {
|
||
pub(crate) fn new(
|
||
adapter: &wgpu::Adapter,
|
||
_device: &wgpu::Device,
|
||
) -> Result<Self, PlatformError> {
|
||
match adapter.get_info().backend {
|
||
wgpu_30::Backend::Noop => {
|
||
Err(PlatformError::from("Cannot use WGPU Noop backend with Skia"))
|
||
}
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
wgpu_30::Backend::Vulkan => Ok(Self::Vulkan {
|
||
// SAFETY: `_device` is a Vulkan device, as the adapter's backend just said.
|
||
queue_family_index: unsafe { vulkan::queue_family_index(_device) }.ok_or_else(
|
||
|| {
|
||
PlatformError::from(
|
||
"Cannot query the queue family of the WGPU Vulkan device",
|
||
)
|
||
},
|
||
)?,
|
||
}),
|
||
#[cfg(target_vendor = "apple")]
|
||
wgpu_30::Backend::Metal => Ok(Self::Metal),
|
||
#[cfg(target_family = "windows")]
|
||
wgpu_30::Backend::Dx12 => Ok(Self::Dx12),
|
||
other => Err(PlatformError::from(format!(
|
||
"Unsupported WGPU backend for use with Skia: {}",
|
||
other
|
||
))),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn make_context(
|
||
&self,
|
||
_adapter: &wgpu::Adapter,
|
||
device: &wgpu::Device,
|
||
queue: &wgpu::Queue,
|
||
) -> Option<skia_safe::gpu::DirectContext> {
|
||
match self {
|
||
#[cfg(target_vendor = "apple")]
|
||
Self::Metal => metal::make_metal_context(device, queue),
|
||
#[cfg(target_family = "windows")]
|
||
Self::Dx12 => unsafe { dx12::make_dx12_context(&_adapter, &device, &queue) },
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
Self::Vulkan { .. } => unsafe { vulkan::make_vulkan_context(device, queue) },
|
||
}
|
||
}
|
||
|
||
pub(crate) fn make_surface(
|
||
&self,
|
||
gr_context: &mut skia_safe::gpu::DirectContext,
|
||
texture: &wgpu::Texture,
|
||
) -> Option<skia_safe::Surface> {
|
||
match self {
|
||
#[cfg(target_vendor = "apple")]
|
||
Self::Metal => unsafe { metal::make_metal_surface(gr_context, texture) },
|
||
#[cfg(target_family = "windows")]
|
||
Self::Dx12 => unsafe { dx12::make_dx12_surface(gr_context, texture) },
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
Self::Vulkan { .. } => unsafe {
|
||
vulkan::make_vulkan_surface(
|
||
gr_context,
|
||
texture,
|
||
skia_safe::gpu::vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
|
||
)
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Like [`Self::make_surface`], but for the swapchain image handed out by
|
||
/// [`wgpu::Surface::get_current_texture`].
|
||
///
|
||
/// wgpu returns surface textures to their `PRESENT` state at the end of every submission,
|
||
/// so that is the layout Skia finds the image in, not the color-attachment one a plain
|
||
/// render target is left in. Pair every call with [`Self::release_swapchain_surface`].
|
||
pub(crate) fn make_swapchain_surface(
|
||
&self,
|
||
gr_context: &mut skia_safe::gpu::DirectContext,
|
||
texture: &wgpu::Texture,
|
||
) -> Option<skia_safe::Surface> {
|
||
match self {
|
||
#[cfg(target_vendor = "apple")]
|
||
Self::Metal => unsafe { metal::make_metal_surface(gr_context, texture) },
|
||
#[cfg(target_family = "windows")]
|
||
Self::Dx12 => unsafe { dx12::make_dx12_surface(gr_context, texture) },
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
Self::Vulkan { .. } => unsafe {
|
||
vulkan::make_vulkan_surface(
|
||
gr_context,
|
||
texture,
|
||
skia_safe::gpu::vk::ImageLayout::PRESENT_SRC_KHR,
|
||
)
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Hands a surface made by [`Self::make_swapchain_surface`] back in the state
|
||
/// [`wgpu::Queue::present`] expects to find it in.
|
||
pub(crate) fn release_swapchain_surface(
|
||
&self,
|
||
_gr_context: &mut skia_safe::gpu::DirectContext,
|
||
_skia_surface: &mut skia_safe::Surface,
|
||
) {
|
||
match self {
|
||
// Metal and D3D12 have no image layout for Skia to hand back.
|
||
#[cfg(target_vendor = "apple")]
|
||
Self::Metal => {}
|
||
#[cfg(target_family = "windows")]
|
||
Self::Dx12 => {}
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
Self::Vulkan { queue_family_index } => vulkan::release_vulkan_swapchain_surface(
|
||
_gr_context,
|
||
_skia_surface,
|
||
*queue_family_index,
|
||
),
|
||
}
|
||
}
|
||
|
||
#[cfg_attr(not(feature = "unstable-wgpu-30"), allow(dead_code))]
|
||
pub(crate) fn import_texture(
|
||
&self,
|
||
canvas: &skia_safe::Canvas,
|
||
texture: wgpu::Texture,
|
||
) -> Option<skia_safe::Image> {
|
||
match self {
|
||
#[cfg(target_vendor = "apple")]
|
||
Self::Metal => unsafe { metal::import_metal_texture(canvas, texture) },
|
||
#[cfg(target_family = "windows")]
|
||
Self::Dx12 => unsafe { dx12::import_dx12_texture(canvas, texture) },
|
||
#[cfg(skia_wgpu_vulkan)]
|
||
Self::Vulkan { .. } => unsafe { vulkan::import_vulkan_texture(canvas, texture) },
|
||
}
|
||
}
|
||
}
|