Opening second immediate viewport leads to HUGE FPS loss.
- Dominant language
- Rust
- Stars
- 30.6k
- Forks
- 2.1k
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 72
Description
**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**
- OS: Win 11
**Demonstration**
Note the LOAD_FACTOR const that varies the load from drawing.
```rust
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 = 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,
}
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 = 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();
}
}
}
```
Contributor guide
Research direction
Start by compiling and running the provided Rust reproduction in release mode on Windows 11, varying LOAD_FACTOR and placing the immediate viewports on the same and different monitors. Inspect the viewport repaint and frame scheduling paths involved in show_viewport_immediate and compare the behavior with and without the extra viewports. Done means multi-monitor animation no longer collapses from 165 Hz to roughly 20 FPS without proportional load.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100