emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
21.67k stars 1.56k forks source link

Opening second immediate viewport leads to HUGE FPS loss. #4963

Open Barafu opened 1 month ago

Barafu commented 1 month ago

Describe the bug I am trying to show some animated graphs in multiple windows at once. For a single window, the FPS is limited by display FPS, that is 165Hz. But when I open a second viewport with similar animation, the impact depends on where the window is opened. If it is on the same window, the impact is low. If, however, the window is on another monitor, which is 60Hz, FPS drops to ~20 immediately.

To Reproduce I have created a test case. The code is posted below. Compile and run in release. Observe the fps. Try commenting out second_viewport and third_viewport

Expected behaviour I'd expect the FPS of two windows to drop by half + some overhead, not by 50 times.

Desktop

Demonstration Note the LOAD_FACTOR const that varies the load from drawing.

use std::time::{Duration, Instant};

use egui::{Color32, Painter, Shape, Ui, ViewportId};

const RENDER_MEASURE_SIZE: usize = 100;
const LOAD_FACTOR: usize = 10;

fn main() -> eframe::Result {
    let native_options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_position([0.0, 0.0])
            .with_inner_size([500.0, 500.0]),
        ..Default::default()
    };
    return eframe::run_native(
        "My egui App",
        native_options,
        Box::new(|_cc| Ok(Box::new(MyApp::default()))),
    );
}

pub struct MyApp {
    render_durations: FPSMeasureData,
}

impl Default for MyApp {
    fn default() -> Self {
        Self {
            render_durations: FPSMeasureData::new(),
        }
    }
}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        self.render_durations.record_timestamp();
        egui::CentralPanel::default().show(ctx, |ui| {

            ui.label(self.render_durations.to_string());
            paint_animation(ui);
            second_viewport(ui);
            third_viewport(ui);

            //request_updates(ui);
            ui.ctx().request_repaint();
        });
    }
}

pub fn second_viewport(ui: &mut egui::Ui) -> ViewportId {
    let title = "second viewport";
    let id: ViewportId = ViewportId::from_hash_of(title);

    ui.ctx().show_viewport_immediate(
        id.clone(),
        egui::ViewportBuilder::default()
            .with_title(title)
            .with_position([510.0, 0.0])
            .with_inner_size([500.0, 500.0]),
        move |ctx, _class| {
            egui::CentralPanel::default().show(ctx, |ui| {
                paint_animation(ui);
                //request_updates(ui);
            });
        },
    );

    id
}

pub fn third_viewport(ui: &mut egui::Ui) -> ViewportId {
    let title = "third viewport";
    let id: ViewportId = ViewportId::from_hash_of(title);

    ui.ctx().show_viewport_immediate(
        id.clone(),
        egui::ViewportBuilder::default()
            .with_title(title)
            .with_position([1020.0, 0.0])
            .with_inner_size([500.0, 500.0]),
        move |ctx, _class| {
            egui::CentralPanel::default().show(ctx, |ui| {
                paint_animation(ui);
                //request_updates(ui);
            });
        },
    );

    id
}

pub fn paint_animation(ui: &mut Ui) {
    let painter = Painter::new(
        ui.ctx().clone(),
        ui.layer_id(),
        ui.available_rect_before_wrap(),
    );

    let step = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .subsec_millis()
        / 5;
    for _ in 0..LOAD_FACTOR {
    painter.add(Shape::circle_filled(
        [100.0 + step as f32, 100.0 + step as f32].into(),
        100.0,
        Color32::RED,
    ));
    }
    // Make sure we allocate what we used (everything)
    ui.expand_to_include_rect(painter.clip_rect());
}

fn request_updates(ui: &mut Ui) {
    let mut ids: Vec<ViewportId> = Vec::new();
    ui.ctx().input(|i| {
        ids = i.raw.viewports.keys().cloned().collect();
    });
    for id in ids {
        ui.ctx().request_repaint_of(id);
    };
}

struct FPSMeasureData {
    avg: f32,
    worst: f32,
    render_timestamps: Vec<Instant>,
}

impl std::fmt::Display for FPSMeasureData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Avg: {:.2}, Worst: {:.2}", self.avg, self.worst)
    }
}

impl FPSMeasureData {
    fn new() -> Self {
        Self {
            avg: 0.0,
            worst: 0.0,
            render_timestamps: Vec::with_capacity(RENDER_MEASURE_SIZE),
        }
    }

    fn record_timestamp(&mut self) {
        self.render_timestamps.push(Instant::now());
        if self.render_timestamps.len() == RENDER_MEASURE_SIZE {
            let mut durations: Vec<Duration> = Vec::with_capacity(RENDER_MEASURE_SIZE);
            for t in self.render_timestamps.windows(2) {
                durations.push(t[1] - t[0]);
            }
            let sum: Duration = durations.iter().sum();
            let avg = sum.as_secs_f32() / durations.len() as f32;
            let worst = durations.iter().max().unwrap_or(&Duration::ZERO).as_secs_f32();
            self.avg = 1.0 / avg;
            self.worst = 1.0 / worst;
            self.render_timestamps.clear();
        }
    }
}
Barafu commented 1 month ago

P.S. Can't use deferred mode because of issue #4945.

Barafu commented 4 weeks ago

I looked carefully into the way I measure FPS. The part that I didn't measure because I thought it to be negligible - well, it wasn't. When I take it into account too I was able to localise problem more, so I updated the issue description.