diff --git a/Cargo.toml b/Cargo.toml index acc7bdcb..90c4cafc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ dpi = "0.1.2" tracing = { version = "0.1", optional = true } [target.'cfg(target_os="linux")'.dependencies] -x11rb = { version = "0.13.2", features = ["cursor", "resource_manager", "allow-unsafe-code", "dl-libxcb"], default-features = false } +x11rb = { version = "0.13.2", features = ["cursor", "dri3", "present", "resource_manager", "allow-unsafe-code", "dl-libxcb"], default-features = false } xkbcommon-dl = { version = "0.4.2", features = ["x11"] } x11-dl = { version = "2.21.0" } calloop = "0.14.4" @@ -92,7 +92,7 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [ ] } [workspace] -members = ["examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu"] +members = ["examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu", "examples/test-frame-pacing"] [lints.clippy] missing-safety-doc = "allow" diff --git a/examples/open_parented/src/main.rs b/examples/open_parented/src/main.rs index 4c9433fc..f242c742 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -31,7 +31,7 @@ impl ParentWindowHandler { } impl WindowHandler for ParentWindowHandler { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut buf = surface.buffer_mut()?; if self.damaged.get() { @@ -86,7 +86,7 @@ impl ChildWindowHandler { } impl WindowHandler for ChildWindowHandler { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut buf = surface.buffer_mut()?; if self.damaged.get() { diff --git a/examples/open_window/src/main.rs b/examples/open_window/src/main.rs index 83b299b1..8c00cfcb 100644 --- a/examples/open_window/src/main.rs +++ b/examples/open_window/src/main.rs @@ -8,8 +8,8 @@ use rtrb::{Consumer, RingBuffer}; use baseview::copy_to_clipboard; use baseview::dpi::{LogicalSize, PhysicalPosition}; use baseview::{ - Event, EventStatus, HandlerError, MouseEvent, Window, WindowContext, WindowHandler, - WindowSettings, WindowSize, + Event, EventStatus, HandlerError, MouseEvent, RedrawStrategy, Window, WindowContext, + WindowHandler, WindowSettings, WindowSize, }; #[derive(Debug, Clone)] @@ -24,7 +24,6 @@ struct OpenWindowExample { surface: RefCell>, mouse_pos: Cell>, is_cursor_inside: Cell, - damaged: Cell, } impl WindowHandler for OpenWindowExample { @@ -35,17 +34,12 @@ impl WindowHandler for OpenWindowExample { (NonZeroU32::new(new_size.physical.width), NonZeroU32::new(new_size.physical.height)) { self.surface.borrow_mut().resize(width, height)?; - self.damaged.set(true); } Ok(()) } - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut pixels = surface.buffer_mut()?; let size = self.window_context.size(); @@ -105,7 +99,6 @@ impl WindowHandler for OpenWindowExample { } pixels.present()?; - self.damaged.set(false); while let Ok(message) = self.rx.borrow_mut().pop() { println!("Message: {:?}", message); @@ -120,15 +113,15 @@ impl WindowHandler for OpenWindowExample { Event::Mouse(MouseEvent::ButtonPressed { .. }) => copy_to_clipboard("This is a test!"), Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { self.mouse_pos.set(position); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::CursorEntered) => { self.is_cursor_inside.set(true); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::CursorLeft) => { self.is_cursor_inside.set(false); - self.damaged.set(true); + self.window_context.request_redraw(); } _ => {} } @@ -140,7 +133,9 @@ impl WindowHandler for OpenWindowExample { } fn main() -> Result<(), baseview::Error> { - let window_open_options = WindowSettings::new().with_size(LogicalSize::new(512.0, 512.0)); + let window_open_options = WindowSettings::new() + .with_redraw_strategy(RedrawStrategy::OnDemand) + .with_size(LogicalSize::new(512.0, 512.0)); let (mut tx, rx) = RingBuffer::new(128); @@ -164,7 +159,6 @@ fn main() -> Result<(), baseview::Error> { rx: rx.into(), mouse_pos: PhysicalPosition::new(0., 0.).into(), is_cursor_inside: false.into(), - damaged: true.into(), }) })? .run_until_closed()?; diff --git a/examples/plugin_clack/src/window_handler.rs b/examples/plugin_clack/src/window_handler.rs index 16800ba9..f74b5c32 100644 --- a/examples/plugin_clack/src/window_handler.rs +++ b/examples/plugin_clack/src/window_handler.rs @@ -29,7 +29,7 @@ impl WindowHandler for OpenWindowExample { Ok(()) } - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { if !self.damaged.get() { return Ok(()); } diff --git a/examples/render_femtovg/src/main.rs b/examples/render_femtovg/src/main.rs index f2e3e0d5..db169e55 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -13,7 +13,6 @@ struct FemtovgExample { gl_context: GlContext, canvas: RefCell>, current_mouse_position: Cell>, - damaged: Cell, } impl FemtovgExample { @@ -34,18 +33,13 @@ impl FemtovgExample { gl_context, window_context, canvas: canvas.into(), - damaged: true.into(), current_mouse_position: Cell::new(PhysicalPosition::default()), }) } } impl WindowHandler for FemtovgExample { - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let context = &self.gl_context; unsafe { context.make_current()? }; @@ -81,7 +75,6 @@ impl WindowHandler for FemtovgExample { canvas.flush(); context.swap_buffers()?; unsafe { context.make_not_current()? }; - self.damaged.set(false); Ok(()) } @@ -89,7 +82,6 @@ impl WindowHandler for FemtovgExample { fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { let size = new_size.physical; self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); - self.damaged.set(true); Ok(()) } @@ -106,7 +98,6 @@ impl WindowHandler for FemtovgExample { if position.y > 400. && !self.window_context.has_focus() { let _ = self.window_context.focus(); } - self.damaged.set(true); } event => log_event(&event), }; diff --git a/examples/render_wgpu/src/main.rs b/examples/render_wgpu/src/main.rs index 7d254377..51b4bc45 100644 --- a/examples/render_wgpu/src/main.rs +++ b/examples/render_wgpu/src/main.rs @@ -131,7 +131,7 @@ impl WgpuExample { } impl WindowHandler for WgpuExample { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let surface_texture = match surface.get_current_texture() { diff --git a/examples/test-frame-pacing/Cargo.toml b/examples/test-frame-pacing/Cargo.toml new file mode 100644 index 00000000..315a98db --- /dev/null +++ b/examples/test-frame-pacing/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test-frame-pacing" +version = "0.1.0" +edition = "2024" + +[dependencies] +baseview = { path = "../..", features = ["opengl", "tracing"] } +femtovg = "0.26.0" +tracing-subscriber = { workspace = true } +keyboard-types = "0.8.3" diff --git a/examples/test-frame-pacing/LICENSE-Roboto b/examples/test-frame-pacing/LICENSE-Roboto new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/examples/test-frame-pacing/LICENSE-Roboto @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/test-frame-pacing/RobotoFlex-VariableFont.ttf b/examples/test-frame-pacing/RobotoFlex-VariableFont.ttf new file mode 100644 index 00000000..ccade3ef Binary files /dev/null and b/examples/test-frame-pacing/RobotoFlex-VariableFont.ttf differ diff --git a/examples/test-frame-pacing/src/main.rs b/examples/test-frame-pacing/src/main.rs new file mode 100644 index 00000000..18fa637c --- /dev/null +++ b/examples/test-frame-pacing/src/main.rs @@ -0,0 +1,173 @@ +use crate::perf::PerfGraph; +use baseview::dpi::LogicalSize; +use baseview::gl::{GlConfig, GlContext}; +use baseview::{ + Event, EventStatus, HandlerError, Window, WindowContext, WindowHandler, WindowSettings, + WindowSize, +}; +use femtovg::renderer::OpenGl; +use femtovg::{Align, Baseline, Canvas, Color, Paint, Renderer}; +use keyboard_types::{Key, KeyState, KeyboardEvent, NamedKey}; +use std::cell::{Cell, RefCell}; +use std::time::Instant; + +mod perf; + +const BAR_WIDTH: u32 = 10; +const BAR_COUNT: u32 = 5; +const BAR_SPEED_INCREMENTS: u32 = 3; + +struct FramePacingTest { + window_context: WindowContext, + gl_context: GlContext, + canvas: RefCell>, + perf_graph: PerfGraph, + previous_frame_time: Cell, + + bar_pos: Cell, + bar_speed: Cell, +} + +impl FramePacingTest { + fn new(window_context: WindowContext) -> Result { + let Some(gl_context) = window_context.gl_context() else { unreachable!() }; + unsafe { gl_context.make_current()? }; + + let renderer = + unsafe { OpenGl::new_from_function_cstr(|s| gl_context.get_proc_address(s)) }?; + + let mut canvas = Canvas::new(renderer)?; + let size = window_context.size(); + + canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32); + + canvas + .add_font_mem(include_bytes!("../RobotoFlex-VariableFont.ttf")) + .expect("Cannot add font"); + + unsafe { gl_context.make_not_current()? }; + Ok(Self { + gl_context, + window_context, + canvas: canvas.into(), + perf_graph: PerfGraph::new(), + previous_frame_time: Instant::now().into(), + bar_pos: 0.into(), + bar_speed: 6.into(), + }) + } +} + +impl WindowHandler for FramePacingTest { + fn draw(&self) -> Result<(), HandlerError> { + let now = Instant::now(); + let dt = (now - self.previous_frame_time.get()).as_secs_f32(); + self.previous_frame_time.set(now); + + self.perf_graph.update(dt); + + unsafe { self.gl_context.make_current()? }; + + let mut canvas = self.canvas.borrow_mut(); + + let screen_height = canvas.height(); + let screen_width = canvas.width(); + + // Clear + canvas.clear_rect(0, 0, screen_width, screen_height, Color::black()); + + // Draw bar + + let spacing = (screen_width - (BAR_WIDTH * BAR_COUNT)) / (BAR_COUNT) + BAR_WIDTH; + + for i in 0..BAR_COUNT { + draw_bar_may_split( + &mut canvas, + self.bar_pos.get() + i * spacing, + screen_width, + screen_height, + BAR_WIDTH, + ); + } + + // Move bar + let bar_speed = self.bar_speed.get(); + + self.bar_pos.set((self.bar_pos.get() + bar_speed) % screen_width); + + // Extras + + let text_paint = Paint::color(Color::rgba(240, 240, 240, 255)) + .with_font_size(14.0) + .with_text_align(Align::Left) + .with_text_baseline(Baseline::Bottom); + + let _ = canvas.fill_text( + 5.0, + screen_height as f32 - 20.0, + format!("Speed: {} pixels/sec", bar_speed), + &text_paint, + ); + + canvas.restore(); + + canvas.save(); + canvas.reset(); + self.perf_graph.render(&mut canvas, 5.0, 5.0); + canvas.restore(); + + // Tell renderer to execute all drawing commands + canvas.flush(); + self.gl_context.swap_buffers()?; + unsafe { self.gl_context.make_not_current()? }; + + Ok(()) + } + + fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { + let size = new_size.physical; + self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); + + Ok(()) + } + + fn on_event(&self, event: Event) -> EventStatus { + if let Event::Keyboard(KeyboardEvent { key, state: KeyState::Down, .. }) = event { + match key { + Key::Named(NamedKey::ArrowLeft) => { + self.bar_speed.set(self.bar_speed.get().saturating_sub(BAR_SPEED_INCREMENTS)) + } + Key::Named(NamedKey::ArrowRight) => { + self.bar_speed.set(self.bar_speed.get().saturating_add(BAR_SPEED_INCREMENTS)) + } + _ => {} + } + } + + EventStatus::Captured + } +} + +fn main() -> Result<(), baseview::Error> { + tracing_subscriber::fmt::init(); + + let window_open_options = WindowSettings::new() + .with_title("Femtovg on Baseview") + .with_size(LogicalSize::new(512, 512)) + .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); + + Window::create(window_open_options, FramePacingTest::new)?.run_until_closed()?; + Ok(()) +} + +fn draw_bar_may_split( + canvas: &mut Canvas, mut pos: u32, screen_width: u32, screen_height: u32, + bar_width: u32, +) { + pos %= screen_width; + canvas.clear_rect(pos, 0, BAR_WIDTH, screen_height, Color::white()); + + if pos + BAR_WIDTH > screen_width { + canvas.clear_rect(0, 0, BAR_WIDTH - (screen_width - pos), screen_height, Color::white()); + } +} diff --git a/examples/test-frame-pacing/src/perf.rs b/examples/test-frame-pacing/src/perf.rs new file mode 100644 index 00000000..43c43639 --- /dev/null +++ b/examples/test-frame-pacing/src/perf.rs @@ -0,0 +1,73 @@ +#![allow(unused)] + +use femtovg::{Align, Baseline, Canvas, Color, Paint, Path, Renderer}; +use std::cell::Cell; + +const HISTORY_COUNT: usize = 300; + +pub struct PerfGraph { + values: Vec>, + head: Cell, +} + +impl PerfGraph { + pub fn new() -> Self { + Self { values: vec![0.0.into(); HISTORY_COUNT], head: Default::default() } + } + + pub fn update(&self, frame_time: f32) { + self.head.set((self.head.get() + 1) % HISTORY_COUNT); + self.values[self.head.get()].set(frame_time); + } + + pub fn get_average(&self) -> f32 { + self.values.iter().map(|f| f.get()).sum::() / HISTORY_COUNT as f32 + } + + pub fn render(&self, canvas: &mut Canvas, x: f32, y: f32) { + let avg = self.get_average(); + + let w = 200.0; + let h = 35.0; + + let mut path = Path::new(); + path.rect(x, y, w, h); + canvas.fill_path(&path, &Paint::color(Color::rgba(0, 0, 0, 128))); + + let mut path = Path::new(); + path.move_to(x, y + h); + + for i in 0..HISTORY_COUNT { + let mut v = 1.0 / (0.00001 + self.values[(self.head.get() + i) % HISTORY_COUNT].get()); + if v > 80.0 { + v = 80.0; + } + let vx = x + (i as f32 / (HISTORY_COUNT - 1) as f32) * w; + let vy = y + h - ((v / 80.0) * h); + path.line_to(vx, vy); + } + + path.line_to(x + w, y + h); + canvas.fill_path(&path, &Paint::color(Color::rgba(255, 192, 0, 128))); + + let text_paint = Paint::color(Color::rgba(240, 240, 240, 255)).with_font_size(12.0); + let _ = canvas.fill_text(x + 5.0, y + 13.0, "Frame time", &text_paint); + + let text_paint = Paint::color(Color::rgba(240, 240, 240, 255)) + .with_font_size(14.0) + .with_text_align(Align::Right) + .with_text_baseline(Baseline::Top); + let _ = canvas.fill_text(x + w - 5.0, y, format!("{:.2} FPS", 1.0 / avg), &text_paint); + + let text_paint = Paint::color(Color::rgba(240, 240, 240, 200)) + .with_font_size(12.0) + .with_text_align(Align::Right) + .with_text_baseline(Baseline::Alphabetic); + let _ = canvas.fill_text( + x + w - 5.0, + y + h - 5.0, + format!("{:.2} ms", avg * 1000.0), + &text_paint, + ); + } +} diff --git a/src/context.rs b/src/context.rs index 5c1ca9ca..5a1ed3a8 100644 --- a/src/context.rs +++ b/src/context.rs @@ -34,6 +34,10 @@ impl WindowContext { self.inner.request_close(); } + pub fn request_redraw(&self) { + self.inner.request_redraw() + } + /// Returns `true` if this window currently has keyboard focus, `false` otherwise. pub fn has_focus(&self) -> bool { self.inner.has_focus() diff --git a/src/handler.rs b/src/handler.rs index 9b39cd91..4f893389 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -2,11 +2,12 @@ use super::*; use crate::platform::Result; pub trait WindowHandler: 'static { - /// Requests the handler to draw a new frame. + /// Requests the handler to draw a new frame immediately. /// /// If this returns an error, the window will be considered unable to render its contents, and /// will be subsequently closed. - fn on_frame(&self) -> core::result::Result<(), HandlerError>; + fn draw(&self) -> core::result::Result<(), HandlerError>; + /// Informs the handler that the window has been resized. /// /// # Errors diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 2813d7b4..506149f1 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -8,8 +8,8 @@ use crate::platform::x11::error::FatalError; use crate::platform::x11::window_thread::{ HostCallback, WindowThreadRequest, WindowThreadResponseMessage, }; -use crate::warn; use crate::wrappers::xkbcommon::XkbcommonState; +use crate::{warn, RedrawStrategy}; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowHandler, WindowSize}; use calloop::generic::Generic; use calloop::timer::{TimeoutAction, Timer}; @@ -21,6 +21,7 @@ use std::sync::mpsc::Receiver; use std::time::{Duration, Instant}; use x11rb::connection::Connection; use x11rb::errors::ConnectionError; +use x11rb::protocol::present::CompleteKind; use x11rb::protocol::Event as XEvent; pub struct MainThreadCaller { @@ -53,7 +54,9 @@ pub(crate) struct EventLoop { new_size: Option>, new_parent_size: Option>, - exposed: bool, + draw_now: bool, + last_requested_serial: Option, + last_received_present: Option<(u32, u64)>, loop_signal: LoopSignal, @@ -75,7 +78,7 @@ impl EventLoop { ) -> Result { let loop_handle = inner.handle(); - Self::setup_fallback_frame_timer(&loop_handle)?; + // Self::setup_fallback_frame_timer(&loop_handle)?; loop_handle .insert_source( @@ -97,7 +100,9 @@ impl EventLoop { handler, new_size: None, new_parent_size: None, - exposed: false, + draw_now: false, + last_requested_serial: None, + last_received_present: None, drag_n_drop: DragNDropState::NoCurrentSession, xkb_state: XkbcommonState::new(&window.connection), run_error: None, @@ -125,7 +130,7 @@ impl EventLoop { const FRAME_INTERVAL: Duration = Duration::from_millis(15); fn handle_frame(evloop: &mut EventLoop, previous_deadline: Instant) -> TimeoutAction { - evloop.exposed = true; + evloop.draw_now = true; // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in // the expected frame time, this will throttle down to prevent multiple frames from @@ -152,24 +157,59 @@ impl EventLoop { } fn handle_redraw(&mut self) { - if !self.exposed { + if !self.draw_now { return; } - self.exposed = false; + self.draw_now = false; if !self.window.visibility_state.own_window_is_viewable() { return; } - if let Err(e) = self.handler.on_frame() { + if let Err(e) = self.handler.draw() { self.trigger_fatal_error(e.into()); return; } + if self.window.redraw_strategy == RedrawStrategy::Continuous { + self.window.present_notify_requested.set(true); + } + // Any socket error will be handled in the next poll let _ = self.window.connection.conn.flush(); } + fn handle_present_notify(&mut self) -> Result<(), ConnectionError> { + if !self.window.present_notify_requested.get() { + return Ok(()); + } + + let (next_serial, target_msc) = + match (self.last_requested_serial, self.last_received_present) { + // First request, always send + (None, None) => (0, 0), + (Some(sent_serial), Some((received_serial, last_msc))) + if sent_serial == received_serial => + { + // TODO: why does 2 work but not 1 for next MSC?? + (sent_serial.wrapping_add(1), last_msc.wrapping_add(2)) + } + // We sent our first request but have not gotten a response yet. + // Or, we sent a request, but the last response we've gotten isn't that one. + // Do not send. + _ => { + self.window.present_notify_requested.set(false); + return Ok(()); + } + }; + + self.window.xcb_window.present_notify(target_msc, next_serial)?.check_warn(); // TODO: handle error + self.last_requested_serial = Some(next_serial); + self.window.present_notify_requested.set(false); + + Ok(()) + } + fn handle_coalesced_resize_events(&mut self) -> Result<(), FatalError> { if let Some(new_parent_size) = self.new_parent_size.take() { if new_parent_size != self.window.get_size() { @@ -210,7 +250,7 @@ impl EventLoop { } // Immediately schedule a redraw, do not wait for an "expose" event - self.exposed = true; + self.window.present_notify_requested.set(true); } Ok(()) @@ -313,6 +353,7 @@ impl EventLoop { loop { self.handle_coalesced_resize_events()?; self.handle_redraw(); + self.handle_present_notify()?; if !self.drain_xcb_events()? { break; @@ -422,7 +463,9 @@ impl EventLoop { } } - XEvent::Expose(e) if e.window == self.window.raw_id() => self.exposed = true, + XEvent::Expose(e) if e.window == self.window.raw_id() => { + self.window.present_notify_requested.set(true) + } //// // mouse @@ -516,7 +559,8 @@ impl EventLoop { let became_viewable = self.window.visibility_state.window_mapped(window_id); if became_viewable { - self.exposed = true; + self.window.xcb_window.present_select_input()?.unwrap().check_warn(); // TODO: unwrap: fallback to timer + self.window.present_notify_requested.set(true); } } } @@ -549,6 +593,39 @@ impl EventLoop { } } + XEvent::PresentCompleteNotify(e) => { + if e.kind != CompleteKind::NOTIFY_MSC { + return Ok(()); + } + + if e.window != self.window.raw_id() { + return Ok(()); + } + + let Some(last_requested_serial) = self.last_requested_serial else { + return Ok(()); + }; + + if last_requested_serial != e.serial { + return Ok(()); + } + + if let Some((last_received_serial, last_received_msc)) = self.last_received_present + { + if last_received_serial == e.serial { + return Ok(()); + } + + if e.msc <= last_received_msc { + self.last_received_present = Some((e.serial, e.msc)); + return Ok(()); + } + } + + self.last_received_present = Some((e.serial, e.msc)); + self.draw_now = true; + } + _ => {} } diff --git a/src/platform/x11/visibility_tree.rs b/src/platform/x11/visibility_tree.rs index da367abe..067f9742 100644 --- a/src/platform/x11/visibility_tree.rs +++ b/src/platform/x11/visibility_tree.rs @@ -113,7 +113,7 @@ impl AncestorVisibilityState { } if self.own_window_viewable.get() { - return false; + return true; } let all_mapped = self.ancestry.check_all_mapped(); diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index f885124d..eafa9d9c 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -6,7 +6,7 @@ use crate::platform::x11::xcb_connection::get_size_hints; use crate::platform::x11::xcb_window::XcbWindow; use crate::platform::*; use crate::utils::SizingStrategy; -use crate::{warn, MouseCursor, WindowHandler, WindowSettings, WindowSize}; +use crate::{warn, MouseCursor, RedrawStrategy, WindowHandler, WindowSettings, WindowSize}; use calloop::LoopSignal; use dpi::{PhysicalSize, Size}; use raw_window_handle::{DisplayHandle, XlibWindowHandle}; @@ -54,11 +54,13 @@ pub(crate) struct WindowInner { window_size: Cell>, pub(crate) sizing_strategy: SizingStrategy, + pub(crate) redraw_strategy: RedrawStrategy, mouse_cursor: Cell, pub(crate) visual_id: Visualid, pub(crate) is_focused: Cell, pub(crate) is_mapped: Cell, + pub(crate) present_notify_requested: Cell, pub(crate) loop_signal: LoopSignal, pub(crate) visibility_state: AncestorVisibilityState, @@ -139,11 +141,13 @@ impl WindowInner { suggested: options.fallback_scale_factor.into(), }, sizing_strategy, + redraw_strategy: options.redraw_strategy, mouse_cursor: MouseCursor::default().into(), loop_signal: ev_loop.get_signal(), is_focused: false.into(), is_mapped: false.into(), + present_notify_requested: false.into(), main_thread_shared: shared, visibility_state, @@ -194,6 +198,12 @@ impl WindowInner { self.loop_signal.wakeup(); } + pub fn request_redraw(&self) { + if self.redraw_strategy == RedrawStrategy::OnDemand { + self.present_notify_requested.set(true); + } + } + pub fn has_focus(&self) -> bool { self.is_focused.get() } diff --git a/src/platform/x11/xcb_connection.rs b/src/platform/x11/xcb_connection.rs index 229238e3..2b6a4314 100644 --- a/src/platform/x11/xcb_connection.rs +++ b/src/platform/x11/xcb_connection.rs @@ -6,9 +6,11 @@ use std::cell::RefCell; use std::collections::hash_map::{Entry, HashMap}; use std::num::NonZeroU32; use std::sync::Arc; +use x11rb::connection::RequestConnection; use x11rb::cookie::VoidCookie; use x11rb::cursor::Handle as CursorHandle; use x11rb::errors::ConnectionError; +use x11rb::protocol::present; use x11rb::protocol::xproto::{ self, ChangeWindowAttributesAux, ConnectionExt, Cursor, EventMask, Screen, }; @@ -54,6 +56,8 @@ pub struct X11Connection { pub(crate) resources: resource_manager::Database, pub(crate) cursor_handle: CursorHandle, pub(crate) cursor_cache: RefCell>, + + pub(crate) present_supported: bool, } impl X11Connection { @@ -66,12 +70,15 @@ impl X11Connection { let resources = resource_manager::new_from_default(xcb_conn)?; let cursor_handle = CursorHandle::new(xcb_conn, screen.into(), &resources)?.reply()?; + let present_supported = conn.extension_information(present::X11_EXTENSION_NAME)?.is_some(); + Ok(Self { conn: Arc::new(conn), atoms, resources, cursor_handle, cursor_cache: RefCell::new(HashMap::new()), + present_supported, }) } diff --git a/src/platform/x11/xcb_window.rs b/src/platform/x11/xcb_window.rs index 252c07dc..97bd8479 100644 --- a/src/platform/x11/xcb_window.rs +++ b/src/platform/x11/xcb_window.rs @@ -8,6 +8,8 @@ use x11rb::connection::Connection; use x11rb::cookie::VoidCookie; use x11rb::errors::{ConnectionError, ReplyOrIdError}; use x11rb::properties::WmSizeHints; +use x11rb::protocol::present; +use x11rb::protocol::present::ConnectionExt; use x11rb::protocol::xproto::{ AtomEnum, ConfigureWindowAux, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, WindowClass, @@ -18,6 +20,7 @@ use x11rb::xcb_ffi::XCBConnection; pub struct XcbWindow { connection: Rc, window_id: NonZeroU32, + present_notify_event_id: Option, } impl XcbWindow { @@ -59,7 +62,31 @@ impl XcbWindow { .border_pixel(0), )?; - Ok(Self { window_id, connection }) + let present_notify_event_id = if !connection.present_supported { + None + } else { + let Some(event_id) = NonZero::new(connection.conn.generate_id()?) else { + unreachable!(); + }; + + Some(event_id) + }; + + Ok(Self { window_id, connection, present_notify_event_id }) + } + + pub fn present_select_input( + &self, + ) -> Result>, ConnectionError> { + let Some(event_id) = self.present_notify_event_id else { + return Ok(None); + }; + + Ok(Some(self.connection.conn.present_select_input( + event_id.get(), + self.window_id.get(), + present::EventMask::COMPLETE_NOTIFY, + )?)) } pub fn map_window(&self) -> Result, ConnectionError> { @@ -126,6 +153,16 @@ impl XcbWindow { size_hints.set_normal_hints(&self.connection.conn as &XCBConnection, self.window_id.get()) } + pub fn present_supported(&self) -> bool { + self.present_notify_event_id.is_some() + } + + pub fn present_notify( + &self, target_msc: u64, serial: u32, + ) -> Result, ConnectionError> { + self.connection.conn.present_notify_msc(self.window_id.get(), serial, target_msc, 1, 0) + } + #[inline] pub fn id(&self) -> NonZeroU32 { self.window_id @@ -134,6 +171,17 @@ impl XcbWindow { impl Drop for XcbWindow { fn drop(&mut self) { + if let Some(event_id) = self.present_notify_event_id { + match self.connection.conn.present_select_input( + event_id.get(), + self.window_id.get(), + present::EventMask::NO_EVENT, + ) { + Err(e) => crate::warn!("Failed to send request to switch XPresent off: {}", e), + Ok(cookie) => cookie.check_warn(), + } + } + match self.connection.conn.destroy_window(self.window_id.get()) { Err(e) => crate::warn!("Failed to send request to destroy X window: {}", e), Ok(cookie) => cookie.check_warn(), diff --git a/src/settings.rs b/src/settings.rs index 671f1760..f681f3f4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -49,6 +49,8 @@ pub struct WindowSettings { /// On macOS, this function is always a no-op. pub fallback_scale_factor: Option, + pub redraw_strategy: RedrawStrategy, + /// If provided, then an OpenGL context will be created for this window. You'll be able to /// access this context through [crate::WindowContext::gl_context]. /// @@ -126,6 +128,12 @@ impl WindowSettings { self } + #[inline] + pub fn with_redraw_strategy(mut self, redraw_strategy: RedrawStrategy) -> Self { + self.redraw_strategy = redraw_strategy; + self + } + /// Sets [`gl_config`](Self::gl_config) to the given value. #[cfg(feature = "opengl")] #[inline] @@ -146,6 +154,7 @@ impl Default for WindowSettings { resizable: true, min_size: None, max_size: None, + redraw_strategy: RedrawStrategy::Continuous, #[cfg(feature = "opengl")] gl_config: None, } @@ -193,3 +202,10 @@ impl From for ParentWindowHandle { Self { inner } } } + +#[non_exhaustive] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum RedrawStrategy { + Continuous, + OnDemand, +}