Add a setting to show time to first window draw and frames per second in status bar (#16422)
I want to showcase Zed's performance via videos, and this seemed like a good way to demonstrate it. https://github.com/user-attachments/assets/f4a5fabc-efe7-4b48-9ba5-719882fdc856 Release Notes: - On macOS, you can now set assign `performance.show_in_status_bar: true` in your settings to show the time to the first window draw on startup and then current FPS of the containing window's renderer. --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Kirill Bulatov <kirill@zed.dev> Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com> Co-authored-by: Danny Hua <danny.hua@hey.com>
This commit is contained in:
parent
6f93b42ecb
commit
11753914d7
20 changed files with 524 additions and 61 deletions
|
@ -6,7 +6,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{atomic::Ordering::SeqCst, Arc},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
@ -142,6 +142,12 @@ impl App {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets a start time for tracking time to first window draw.
|
||||
pub fn measure_time_to_first_window_draw(self, start: Instant) -> Self {
|
||||
self.0.borrow_mut().time_to_first_window_draw = Some(TimeToFirstWindowDraw::Pending(start));
|
||||
self
|
||||
}
|
||||
|
||||
/// Start the application. The provided callback will be called once the
|
||||
/// app is fully launched.
|
||||
pub fn run<F>(self, on_finish_launching: F)
|
||||
|
@ -247,6 +253,7 @@ pub struct AppContext {
|
|||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||
pub(crate) propagate_event: bool,
|
||||
pub(crate) prompt_builder: Option<PromptBuilder>,
|
||||
pub(crate) time_to_first_window_draw: Option<TimeToFirstWindowDraw>,
|
||||
}
|
||||
|
||||
impl AppContext {
|
||||
|
@ -300,6 +307,7 @@ impl AppContext {
|
|||
layout_id_buffer: Default::default(),
|
||||
propagate_event: true,
|
||||
prompt_builder: Some(PromptBuilder::Default),
|
||||
time_to_first_window_draw: None,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -1302,6 +1310,14 @@ impl AppContext {
|
|||
|
||||
(task, is_first)
|
||||
}
|
||||
|
||||
/// Returns the time to first window draw, if available.
|
||||
pub fn time_to_first_window_draw(&self) -> Option<Duration> {
|
||||
match self.time_to_first_window_draw {
|
||||
Some(TimeToFirstWindowDraw::Done(duration)) => Some(duration),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
@ -1465,6 +1481,15 @@ impl<G: Global> DerefMut for GlobalLease<G> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents the initialization duration of the application.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TimeToFirstWindowDraw {
|
||||
/// The application is still initializing, and contains the start time.
|
||||
Pending(Instant),
|
||||
/// The application has finished initializing, and contains the total duration.
|
||||
Done(Duration),
|
||||
}
|
||||
|
||||
/// Contains state associated with an active drag operation, started by dragging an element
|
||||
/// within the window or by dragging into the app from the underlying platform.
|
||||
pub struct AnyDrag {
|
||||
|
|
|
@ -16,6 +16,7 @@ mod blade;
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test;
|
||||
|
||||
mod fps;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
|
@ -51,6 +52,7 @@ use strum::EnumIter;
|
|||
use uuid::Uuid;
|
||||
|
||||
pub use app_menu::*;
|
||||
pub use fps::*;
|
||||
pub use keystroke::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
@ -354,7 +356,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>);
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn draw(&self, scene: &Scene);
|
||||
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>);
|
||||
fn completed_frame(&self) {}
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
|
||||
|
@ -379,6 +381,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
}
|
||||
fn set_client_inset(&self, _inset: Pixels) {}
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs>;
|
||||
fn fps(&self) -> Option<f32>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot;
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVMetalTextureCache;
|
||||
#[cfg(target_os = "macos")]
|
||||
|
@ -537,7 +538,12 @@ impl BladeRenderer {
|
|||
self.gpu.destroy_command_encoder(&mut self.command_encoder);
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
// Required to compile on macOS, but not currently supported.
|
||||
_on_complete: Option<oneshot::Sender<()>>,
|
||||
) {
|
||||
self.command_encoder.start();
|
||||
self.atlas.before_frame(&mut self.command_encoder);
|
||||
self.rasterize_paths(scene.paths());
|
||||
|
@ -766,4 +772,9 @@ impl BladeRenderer {
|
|||
self.wait_for_gpu();
|
||||
self.last_sync_point = Some(sync_point);
|
||||
}
|
||||
|
||||
/// Required to compile on macOS, but not currently supported.
|
||||
pub fn fps(&self) -> f32 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
|
94
crates/gpui/src/platform/fps.rs
Normal file
94
crates/gpui/src/platform/fps.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
const NANOS_PER_SEC: u64 = 1_000_000_000;
|
||||
const WINDOW_SIZE: usize = 128;
|
||||
|
||||
/// Represents a rolling FPS (Frames Per Second) counter.
|
||||
///
|
||||
/// This struct provides a lock-free mechanism to measure and calculate FPS
|
||||
/// continuously, updating with every frame. It uses atomic operations to
|
||||
/// ensure thread-safety without the need for locks.
|
||||
pub struct FpsCounter {
|
||||
frame_times: [AtomicU64; WINDOW_SIZE],
|
||||
head: AtomicUsize,
|
||||
tail: AtomicUsize,
|
||||
}
|
||||
|
||||
impl FpsCounter {
|
||||
/// Creates a new `Fps` counter.
|
||||
///
|
||||
/// Returns an `Arc<Fps>` for safe sharing across threads.
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
frame_times: std::array::from_fn(|_| AtomicU64::new(0)),
|
||||
head: AtomicUsize::new(0),
|
||||
tail: AtomicUsize::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Increments the FPS counter with a new frame timestamp.
|
||||
///
|
||||
/// This method updates the internal state to maintain a rolling window
|
||||
/// of frame data for the last second. It uses atomic operations to
|
||||
/// ensure thread-safety.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `timestamp_ns` - The timestamp of the new frame in nanoseconds.
|
||||
pub fn increment(&self, timestamp_ns: u64) {
|
||||
let mut head = self.head.load(Ordering::Relaxed);
|
||||
let mut tail = self.tail.load(Ordering::Relaxed);
|
||||
|
||||
// Add new timestamp
|
||||
self.frame_times[head].store(timestamp_ns, Ordering::Relaxed);
|
||||
// Increment head and wrap around to 0 if it reaches WINDOW_SIZE
|
||||
head = (head + 1) % WINDOW_SIZE;
|
||||
self.head.store(head, Ordering::Relaxed);
|
||||
|
||||
// Remove old timestamps (older than 1 second)
|
||||
while tail != head {
|
||||
let oldest = self.frame_times[tail].load(Ordering::Relaxed);
|
||||
if timestamp_ns.wrapping_sub(oldest) <= NANOS_PER_SEC {
|
||||
break;
|
||||
}
|
||||
// Increment tail and wrap around to 0 if it reaches WINDOW_SIZE
|
||||
tail = (tail + 1) % WINDOW_SIZE;
|
||||
self.tail.store(tail, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates and returns the current FPS.
|
||||
///
|
||||
/// This method computes the FPS based on the frames recorded in the last second.
|
||||
/// It uses atomic loads to ensure thread-safety.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The calculated FPS as a `f32`, or 0.0 if no frames have been recorded.
|
||||
pub fn fps(&self) -> f32 {
|
||||
let head = self.head.load(Ordering::Relaxed);
|
||||
let tail = self.tail.load(Ordering::Relaxed);
|
||||
|
||||
if head == tail {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let newest =
|
||||
self.frame_times[head.wrapping_sub(1) & (WINDOW_SIZE - 1)].load(Ordering::Relaxed);
|
||||
let oldest = self.frame_times[tail].load(Ordering::Relaxed);
|
||||
|
||||
let time_diff = newest.wrapping_sub(oldest) as f32;
|
||||
if time_diff == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let frame_count = if head > tail {
|
||||
head - tail
|
||||
} else {
|
||||
WINDOW_SIZE - tail + head
|
||||
};
|
||||
|
||||
(frame_count as f32 - 1.0) * NANOS_PER_SEC as f32 / time_diff
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use std::sync::Arc;
|
|||
|
||||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use raw_window_handle as rwh;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
@ -827,7 +827,7 @@ impl PlatformWindow for WaylandWindow {
|
|||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
) -> Option<Receiver<usize>> {
|
||||
) -> Option<oneshot::Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -934,9 +934,9 @@ impl PlatformWindow for WaylandWindow {
|
|||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.renderer.draw(scene);
|
||||
state.renderer.draw(scene, on_complete);
|
||||
}
|
||||
|
||||
fn completed_frame(&self) {
|
||||
|
@ -1009,6 +1009,10 @@ impl PlatformWindow for WaylandWindow {
|
|||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
self.borrow().renderer.gpu_specs().into()
|
||||
}
|
||||
|
||||
fn fps(&self) -> Option<f32> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_window(mut state: RefMut<WaylandWindowState>) {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use anyhow::Context;
|
||||
|
||||
use crate::{
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GPUSpecs,
|
||||
|
@ -9,7 +7,9 @@ use crate::{
|
|||
X11ClientStatePtr,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use blade_graphics as gpu;
|
||||
use futures::channel::oneshot;
|
||||
use raw_window_handle as rwh;
|
||||
use util::{maybe, ResultExt};
|
||||
use x11rb::{
|
||||
|
@ -1210,9 +1210,10 @@ impl PlatformWindow for X11Window {
|
|||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
// TODO: on_complete not yet supported for X11 windows
|
||||
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
inner.renderer.draw(scene);
|
||||
inner.renderer.draw(scene, on_complete);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
|
@ -1398,4 +1399,8 @@ impl PlatformWindow for X11Window {
|
|||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
self.0.state.borrow().renderer.gpu_specs().into()
|
||||
}
|
||||
|
||||
fn fps(&self) -> Option<f32> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::metal_atlas::MetalAtlas;
|
||||
use crate::{
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
|
||||
Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
FpsCounter, Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
|
@ -14,6 +14,7 @@ use cocoa::{
|
|||
use collections::HashMap;
|
||||
use core_foundation::base::TCFType;
|
||||
use foreign_types::ForeignType;
|
||||
use futures::channel::oneshot;
|
||||
use media::core_video::CVMetalTextureCache;
|
||||
use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
use objc::{self, msg_send, sel, sel_impl};
|
||||
|
@ -105,6 +106,7 @@ pub(crate) struct MetalRenderer {
|
|||
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
|
||||
sprite_atlas: Arc<MetalAtlas>,
|
||||
core_video_texture_cache: CVMetalTextureCache,
|
||||
fps_counter: Arc<FpsCounter>,
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
|
@ -250,6 +252,7 @@ impl MetalRenderer {
|
|||
instance_buffer_pool,
|
||||
sprite_atlas,
|
||||
core_video_texture_cache,
|
||||
fps_counter: FpsCounter::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,7 +295,8 @@ impl MetalRenderer {
|
|||
// nothing to do
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
pub fn draw(&mut self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
|
||||
let on_complete = Arc::new(Mutex::new(on_complete));
|
||||
let layer = self.layer.clone();
|
||||
let viewport_size = layer.drawable_size();
|
||||
let viewport_size: Size<DevicePixels> = size(
|
||||
|
@ -319,13 +323,24 @@ impl MetalRenderer {
|
|||
Ok(command_buffer) => {
|
||||
let instance_buffer_pool = self.instance_buffer_pool.clone();
|
||||
let instance_buffer = Cell::new(Some(instance_buffer));
|
||||
let block = ConcreteBlock::new(move |_| {
|
||||
if let Some(instance_buffer) = instance_buffer.take() {
|
||||
instance_buffer_pool.lock().release(instance_buffer);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
command_buffer.add_completed_handler(&block);
|
||||
let device = self.device.clone();
|
||||
let fps_counter = self.fps_counter.clone();
|
||||
let completed_handler =
|
||||
ConcreteBlock::new(move |_: &metal::CommandBufferRef| {
|
||||
let mut cpu_timestamp = 0;
|
||||
let mut gpu_timestamp = 0;
|
||||
device.sample_timestamps(&mut cpu_timestamp, &mut gpu_timestamp);
|
||||
|
||||
fps_counter.increment(gpu_timestamp);
|
||||
if let Some(on_complete) = on_complete.lock().take() {
|
||||
on_complete.send(()).ok();
|
||||
}
|
||||
if let Some(instance_buffer) = instance_buffer.take() {
|
||||
instance_buffer_pool.lock().release(instance_buffer);
|
||||
}
|
||||
});
|
||||
let completed_handler = completed_handler.copy();
|
||||
command_buffer.add_completed_handler(&completed_handler);
|
||||
|
||||
if self.presents_with_transaction {
|
||||
command_buffer.commit();
|
||||
|
@ -1117,6 +1132,10 @@ impl MetalRenderer {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn fps(&self) -> f32 {
|
||||
self.fps_counter.fps()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pipeline_state(
|
||||
|
|
|
@ -784,14 +784,14 @@ impl PlatformWindow for MacWindow {
|
|||
self.0.as_ref().lock().bounds()
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
self.0.as_ref().lock().window_bounds()
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> bool {
|
||||
self.0.as_ref().lock().is_maximized()
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
self.0.as_ref().lock().window_bounds()
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.0.as_ref().lock().content_size()
|
||||
}
|
||||
|
@ -975,8 +975,6 @@ impl PlatformWindow for MacWindow {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut this = self.0.as_ref().lock();
|
||||
this.renderer
|
||||
|
@ -1007,30 +1005,6 @@ impl PlatformWindow for MacWindow {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
unsafe {
|
||||
let window = self.0.lock().native_window;
|
||||
msg_send![window, setDocumentEdited: edited as BOOL]
|
||||
}
|
||||
|
||||
// Changing the document edited state resets the traffic light position,
|
||||
// so we have to move it again.
|
||||
self.0.lock().move_traffic_light();
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
let this = self.0.lock();
|
||||
let window = this.native_window;
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let _: () = msg_send![app, orderFrontCharacterPalette: window];
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
let window = self.0.lock().native_window;
|
||||
unsafe {
|
||||
|
@ -1107,18 +1081,48 @@ impl PlatformWindow for MacWindow {
|
|||
self.0.lock().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &crate::Scene) {
|
||||
fn draw(&self, scene: &crate::Scene, on_complete: Option<oneshot::Sender<()>>) {
|
||||
let mut this = self.0.lock();
|
||||
this.renderer.draw(scene);
|
||||
this.renderer.draw(scene, on_complete);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
self.0.lock().renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
unsafe {
|
||||
let window = self.0.lock().native_window;
|
||||
msg_send![window, setDocumentEdited: edited as BOOL]
|
||||
}
|
||||
|
||||
// Changing the document edited state resets the traffic light position,
|
||||
// so we have to move it again.
|
||||
self.0.lock().move_traffic_light();
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
let this = self.0.lock();
|
||||
let window = this.native_window;
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let _: () = msg_send![app, orderFrontCharacterPalette: window];
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn gpu_specs(&self) -> Option<crate::GPUSpecs> {
|
||||
None
|
||||
}
|
||||
|
||||
fn fps(&self) -> Option<f32> {
|
||||
Some(self.0.lock().renderer.fps())
|
||||
}
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for MacWindow {
|
||||
|
|
|
@ -251,7 +251,12 @@ impl PlatformWindow for TestWindow {
|
|||
|
||||
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn draw(&self, _scene: &crate::Scene) {}
|
||||
fn draw(
|
||||
&self,
|
||||
_scene: &crate::Scene,
|
||||
_on_complete: Option<futures::channel::oneshot::Sender<()>>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
|
||||
self.0.lock().sprite_atlas.clone()
|
||||
|
@ -277,6 +282,10 @@ impl PlatformWindow for TestWindow {
|
|||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
None
|
||||
}
|
||||
|
||||
fn fps(&self) -> Option<f32> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestAtlasState {
|
||||
|
|
|
@ -660,8 +660,8 @@ impl PlatformWindow for WindowsWindow {
|
|||
self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
self.0.state.borrow_mut().renderer.draw(scene)
|
||||
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
|
||||
self.0.state.borrow_mut().renderer.draw(scene, on_complete)
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
|
@ -675,6 +675,10 @@ impl PlatformWindow for WindowsWindow {
|
|||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
Some(self.0.state.borrow().renderer.gpu_specs())
|
||||
}
|
||||
|
||||
fn fps(&self) -> Option<f32> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[implement(IDropTarget)]
|
||||
|
|
|
@ -11,9 +11,9 @@ use crate::{
|
|||
PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
|
||||
Replay, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
|
||||
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
|
||||
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
|
||||
WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS,
|
||||
TimeToFirstWindowDraw, TransformationMatrix, Underline, UnderlineStyle, View, VisualContext,
|
||||
WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls,
|
||||
WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
|
@ -544,6 +544,8 @@ pub struct Window {
|
|||
hovered: Rc<Cell<bool>>,
|
||||
pub(crate) dirty: Rc<Cell<bool>>,
|
||||
pub(crate) needs_present: Rc<Cell<bool>>,
|
||||
/// We assign this to be notified when the platform graphics backend fires the next completion callback for drawing the window.
|
||||
present_completed: RefCell<Option<oneshot::Sender<()>>>,
|
||||
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
|
||||
pub(crate) refreshing: bool,
|
||||
pub(crate) draw_phase: DrawPhase,
|
||||
|
@ -820,6 +822,7 @@ impl Window {
|
|||
hovered,
|
||||
dirty,
|
||||
needs_present,
|
||||
present_completed: RefCell::default(),
|
||||
last_input_timestamp,
|
||||
refreshing: false,
|
||||
draw_phase: DrawPhase::None,
|
||||
|
@ -1489,13 +1492,29 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.refreshing = false;
|
||||
self.window.draw_phase = DrawPhase::None;
|
||||
self.window.needs_present.set(true);
|
||||
|
||||
if let Some(TimeToFirstWindowDraw::Pending(start)) = self.app.time_to_first_window_draw {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
*self.window.present_completed.borrow_mut() = Some(tx);
|
||||
self.spawn(|mut cx| async move {
|
||||
rx.await.ok();
|
||||
cx.update(|cx| {
|
||||
let duration = start.elapsed();
|
||||
cx.time_to_first_window_draw = Some(TimeToFirstWindowDraw::Done(duration));
|
||||
log::info!("time to first window draw: {:?}", duration);
|
||||
cx.push_effect(Effect::Refresh);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn present(&self) {
|
||||
let on_complete = self.window.present_completed.take();
|
||||
self.window
|
||||
.platform_window
|
||||
.draw(&self.window.rendered_frame.scene);
|
||||
.draw(&self.window.rendered_frame.scene, on_complete);
|
||||
self.window.needs_present.set(false);
|
||||
profiling::finish_frame!();
|
||||
}
|
||||
|
@ -3718,6 +3737,12 @@ impl<'a> WindowContext<'a> {
|
|||
pub fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
self.window.platform_window.gpu_specs()
|
||||
}
|
||||
|
||||
/// Get the current FPS (frames per second) of the window.
|
||||
/// This is only supported on macOS currently.
|
||||
pub fn fps(&self) -> Option<f32> {
|
||||
self.window.platform_window.fps()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue