Skip to content

Commit 8959eac

Browse files
committed
Add rendering callbacks to sixtyfps::Window
This API allows specifying a callback that will be invoked when setting up graphics (great for compiling shaders), before rendering a frame (but after the clearning of the surface background), after rendering a frame (before swapbuffers) and when releasing graphics resources.
1 parent 54d9ebd commit 8959eac

File tree

9 files changed

+295
-25
lines changed

9 files changed

+295
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ as well as the [Rust migration guide for the `sixtyfps` crate](api/sixtyfps-rs/m
2828
### Added
2929

3030
- `TextEdit::font-size` and `LineEdit::font-size` have been added to control the size of these widgets.
31+
- Added `sixtyfps::Window::set_rendering_notifier` to get a callback before and after a new frame is being rendered.
3132

3233
### Fixed
3334

api/cpp/cbindgen.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ fn gen_corelib(
128128
.map(|x| x.to_string())
129129
.collect();
130130

131+
let mut private_exported_types: std::collections::HashSet<String> =
132+
config.export.include.iter().cloned().collect();
133+
131134
config.export.exclude = [
132135
"SharedString",
133136
"SharedVector",
@@ -157,8 +160,10 @@ fn gen_corelib(
157160
"sixtyfps_color_darker",
158161
"sixtyfps_image_size",
159162
"sixtyfps_image_path",
160-
"TimerMode", // included in generated_public.h
161-
"IntSize", // included in generated_public.h
163+
"TimerMode", // included in generated_public.h
164+
"IntSize", // included in generated_public.h
165+
"RenderingState", // included in generated_public.h
166+
"SetRenderingNotifierError", // included in generated_public.h
162167
]
163168
.iter()
164169
.map(|x| x.to_string())
@@ -201,6 +206,7 @@ fn gen_corelib(
201206
.insert("StateInfo".to_owned(), " using Instant = uint64_t;".into());
202207
properties_config.structure.derive_eq = true;
203208
properties_config.structure.derive_neq = true;
209+
private_exported_types.extend(properties_config.export.include.iter().cloned());
204210
cbindgen::Builder::new()
205211
.with_config(properties_config)
206212
.with_src(crate_dir.join("properties.rs"))
@@ -261,6 +267,7 @@ fn gen_corelib(
261267
"sixtyfps_windowrc_set_focus_item",
262268
"sixtyfps_windowrc_set_component",
263269
"sixtyfps_windowrc_show_popup",
270+
"sixtyfps_windowrc_set_rendering_notifier",
264271
"sixtyfps_new_path_elements",
265272
"sixtyfps_new_path_events",
266273
"sixtyfps_color_brighter",
@@ -289,6 +296,9 @@ fn gen_corelib(
289296
// Property<> fields uses the public `sixtyfps::Blah` type
290297
special_config.namespaces =
291298
Some(vec!["sixtyfps".into(), "cbindgen_private".into(), "types".into()]);
299+
300+
private_exported_types.extend(special_config.export.include.iter().cloned());
301+
292302
cbindgen::Builder::new()
293303
.with_config(special_config)
294304
.with_src(crate_dir.join("graphics.rs"))
@@ -309,8 +319,15 @@ fn gen_corelib(
309319
let mut public_config = config.clone();
310320
public_config.namespaces = Some(vec!["sixtyfps".into()]);
311321
public_config.export.item_types = vec![cbindgen::ItemType::Enums, cbindgen::ItemType::Structs];
312-
public_config.export.include = vec!["TimerMode".into(), "IntSize".into()];
313-
public_config.export.exclude.clear();
322+
// Previously included types are now excluded (to avoid duplicates)
323+
public_config.export.exclude = private_exported_types.into_iter().collect();
324+
public_config.export.exclude.push("Point".into());
325+
public_config.export.include = vec![
326+
"TimerMode".into(),
327+
"IntSize".into(),
328+
"RenderingState".into(),
329+
"SetRenderingNotifierError".into(),
330+
];
314331

315332
public_config.export.body.insert(
316333
"IntSize".to_owned(),
@@ -324,6 +341,8 @@ fn gen_corelib(
324341
.with_config(public_config)
325342
.with_src(crate_dir.join("timers.rs"))
326343
.with_src(crate_dir.join("graphics.rs"))
344+
.with_src(crate_dir.join("window.rs"))
345+
.with_src(crate_dir.join("api.rs"))
327346
.with_after_include(format!(
328347
r"
329348
/// This macro expands to the to the numeric value of the major version of SixtyFPS you're

api/cpp/include/sixtyfps.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,23 @@ class WindowRc
141141
cbindgen_private::sixtyfps_windowrc_show_popup(&inner, &popup, p, &parent_item);
142142
}
143143

144+
template<typename F>
145+
std::optional<SetRenderingNotifierError> set_rendering_notifier(F callback) const
146+
{
147+
auto actual_cb = [](RenderingState state, void *data) {
148+
(*reinterpret_cast<F *>(data))(state);
149+
};
150+
SetRenderingNotifierError err;
151+
if (cbindgen_private::sixtyfps_windowrc_set_rendering_notifier(
152+
&inner, actual_cb,
153+
[](void *user_data) { delete reinterpret_cast<F *>(user_data); },
154+
new F(std::move(callback)), &err)) {
155+
return {};
156+
} else {
157+
return err;
158+
}
159+
}
160+
144161
private:
145162
cbindgen_private::WindowRcOpaque inner;
146163
};
@@ -314,6 +331,16 @@ class Window
314331
/// De-registers the window from the windowing system, therefore hiding it.
315332
void hide() { inner.hide(); }
316333

334+
/// This function allows registering a callback that's invoked during the different phases of
335+
/// rendering. This allows custom rendering on top or below of the scene.
336+
/// On success, the function returns a std::optional without value. On error, the function
337+
/// returns the error code as value in the std::optional.
338+
template<typename F>
339+
std::optional<SetRenderingNotifierError> set_rendering_notifier(F &&callback) const
340+
{
341+
return inner.set_rendering_notifier(std::forward<F>(callback));
342+
}
343+
317344
/// \private
318345
private_api::WindowRc &window_handle() { return inner; }
319346
/// \private

internal/backends/gl/glcontext.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ enum OpenGLContextState {
1616
#[cfg(not(target_arch = "wasm32"))]
1717
Current(glutin::WindowedContext<glutin::PossiblyCurrent>),
1818
#[cfg(target_arch = "wasm32")]
19-
Current(Rc<winit::window::Window>),
19+
Current { window: Rc<winit::window::Window>, canvas: web_sys::HtmlCanvasElement },
2020
}
2121

2222
pub struct OpenGLContext(RefCell<Option<OpenGLContextState>>);
@@ -29,7 +29,14 @@ impl OpenGLContext {
2929
#[cfg(not(target_arch = "wasm32"))]
3030
OpenGLContextState::Current(context) => context.window(),
3131
#[cfg(target_arch = "wasm32")]
32-
OpenGLContextState::Current(window) => window.as_ref(),
32+
OpenGLContextState::Current { window, .. } => window.as_ref(),
33+
})
34+
}
35+
36+
#[cfg(target_arch = "wasm32")]
37+
pub fn html_canvas_element(&self) -> std::cell::Ref<web_sys::HtmlCanvasElement> {
38+
std::cell::Ref::map(self.0.borrow(), |state| match state.as_ref().unwrap() {
39+
OpenGLContextState::Current { canvas, .. } => canvas,
3340
})
3441
}
3542

@@ -41,7 +48,7 @@ impl OpenGLContext {
4148
let current_ctx = unsafe { not_current_ctx.make_current().unwrap() };
4249
OpenGLContextState::Current(current_ctx)
4350
}
44-
state @ OpenGLContextState::Current(_) => state,
51+
state @ OpenGLContextState::Current { .. } => state,
4552
});
4653
}
4754

@@ -60,12 +67,12 @@ impl OpenGLContext {
6067
}
6168
}
6269

63-
pub fn with_current_context<T>(&self, cb: impl FnOnce() -> T) -> T {
64-
if matches!(self.0.borrow().as_ref().unwrap(), OpenGLContextState::Current(_)) {
65-
cb()
70+
pub fn with_current_context<T>(&self, cb: impl FnOnce(&Self) -> T) -> T {
71+
if matches!(self.0.borrow().as_ref().unwrap(), OpenGLContextState::Current { .. }) {
72+
cb(self)
6673
} else {
6774
self.make_current();
68-
let result = cb();
75+
let result = cb(self);
6976
self.make_not_current();
7077
result
7178
}
@@ -261,7 +268,15 @@ impl OpenGLContext {
261268

262269
let renderer =
263270
femtovg::renderer::OpenGl::new_from_html_canvas(&window.canvas()).unwrap();
264-
(Self(RefCell::new(Some(OpenGLContextState::Current(window)))), renderer)
271+
(Self(RefCell::new(Some(OpenGLContextState::Current { window, canvas }))), renderer)
272+
}
273+
}
274+
275+
#[cfg(not(target_arch = "wasm32"))]
276+
pub fn get_proc_address(&self, name: &str) -> *const std::ffi::c_void {
277+
match &self.0.borrow().as_ref().unwrap() {
278+
OpenGLContextState::NotCurrent(_) => std::ptr::null(),
279+
OpenGLContextState::Current(current_ctx) => current_ctx.get_proc_address(name),
265280
}
266281
}
267282
}

internal/backends/gl/glwindow.rs

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use std::rc::{Rc, Weak};
1111

1212
use super::{ItemGraphicsCache, TextureCache};
1313
use crate::event_loop::WinitWindow;
14+
use crate::glcontext::OpenGLContext;
1415
use const_field_offset::FieldOffsets;
16+
use corelib::api::{GraphicsAPI, RenderingNotifier, RenderingState, SetRenderingNotifierError};
1517
use corelib::component::ComponentRc;
1618
use corelib::graphics::*;
1719
use corelib::input::KeyboardModifiers;
@@ -38,6 +40,8 @@ pub struct GLWindow {
3840

3941
fps_counter: Option<Rc<FPSCounter>>,
4042

43+
rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>,
44+
4145
#[cfg(target_arch = "wasm32")]
4246
canvas_id: String,
4347
}
@@ -61,16 +65,17 @@ impl GLWindow {
6165
graphics_cache: Default::default(),
6266
texture_cache: Default::default(),
6367
fps_counter: FPSCounter::new(),
68+
rendering_notifier: Default::default(),
6469
#[cfg(target_arch = "wasm32")]
6570
canvas_id,
6671
})
6772
}
6873

69-
fn with_current_context<T>(&self, cb: impl FnOnce() -> T) -> T {
74+
fn with_current_context<T>(&self, cb: impl FnOnce(&OpenGLContext) -> T) -> Option<T> {
7075
match &*self.map_state.borrow() {
71-
GraphicsWindowBackendState::Unmapped => cb(),
76+
GraphicsWindowBackendState::Unmapped => None,
7277
GraphicsWindowBackendState::Mapped(window) => {
73-
window.opengl_context.with_current_context(cb)
78+
Some(window.opengl_context.with_current_context(cb))
7479
}
7580
}
7681
}
@@ -108,6 +113,38 @@ impl GLWindow {
108113
pub fn default_font_properties(&self) -> FontRequest {
109114
self.self_weak.upgrade().unwrap().default_font_properties()
110115
}
116+
117+
fn release_graphics_resources(&self) {
118+
// Release GL textures and other GPU bound resources.
119+
self.with_current_context(|context| {
120+
self.graphics_cache.borrow_mut().clear();
121+
self.texture_cache.borrow_mut().clear();
122+
123+
self.invoke_rendering_notifier(RenderingState::RenderingTeardown, context);
124+
});
125+
}
126+
127+
/// Invoke any registered rendering notifiers about the state the backend renderer is currently in.
128+
fn invoke_rendering_notifier(&self, state: RenderingState, opengl_context: &OpenGLContext) {
129+
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
130+
#[cfg(not(target_arch = "wasm32"))]
131+
let api = GraphicsAPI::NativeOpenGL {
132+
get_proc_address: &|name| opengl_context.get_proc_address(name),
133+
};
134+
#[cfg(target_arch = "wasm32")]
135+
let canvas_element_id = opengl_context.html_canvas_element().id();
136+
#[cfg(target_arch = "wasm32")]
137+
let api = GraphicsAPI::WebGL {
138+
canvas_element_id: canvas_element_id.as_str(),
139+
context_type: "webgl",
140+
};
141+
callback.notify(state, &api)
142+
}
143+
}
144+
145+
fn has_rendering_notifier(&self) -> bool {
146+
self.rendering_notifier.borrow().is_some()
147+
}
111148
}
112149

113150
impl WinitWindow for GLWindow {
@@ -127,7 +164,7 @@ impl WinitWindow for GLWindow {
127164
fn draw(self: Rc<Self>) {
128165
let runtime_window = self.self_weak.upgrade().unwrap();
129166
let scale_factor = runtime_window.scale_factor();
130-
runtime_window.draw_contents(|components| {
167+
runtime_window.clone().draw_contents(|components| {
131168
let window = match self.borrow_mapped_window() {
132169
Some(window) => window,
133170
None => return, // caller bug, doesn't make sense to call draw() when not mapped
@@ -151,6 +188,18 @@ impl WinitWindow for GLWindow {
151188
size.height,
152189
crate::to_femtovg_color(&window.clear_color),
153190
);
191+
// For the BeforeRendering rendering notifier callback it's important that this happens *after* clearing
192+
// the back buffer, in order to allow the callback to provide its own rendering of the background.
193+
// femtovg's clear_rect() will merely schedule a clear call, so flush right away to make it immediate.
194+
if self.has_rendering_notifier() {
195+
canvas.flush();
196+
canvas.set_size(size.width, size.height, 1.0);
197+
198+
self.invoke_rendering_notifier(
199+
RenderingState::BeforeRendering,
200+
&window.opengl_context,
201+
);
202+
}
154203
}
155204

156205
let mut renderer = crate::GLItemRenderer {
@@ -184,6 +233,8 @@ impl WinitWindow for GLWindow {
184233

185234
drop(renderer);
186235

236+
self.invoke_rendering_notifier(RenderingState::AfterRendering, &window.opengl_context);
237+
187238
window.opengl_context.swap_buffers();
188239
window.opengl_context.make_not_current();
189240
});
@@ -250,14 +301,28 @@ impl PlatformWindow for GLWindow {
250301
})
251302
.peekable();
252303
if cache_entries_to_clear.peek().is_some() {
253-
self.with_current_context(|| {
304+
self.with_current_context(|_| {
254305
cache_entries_to_clear.for_each(drop);
255306
});
256307
}
257308
}
258309
}
259310
}
260311

312+
/// This function is called through the public API to register a callback that the backend needs to invoke during
313+
/// different phases of rendering.
314+
fn set_rendering_notifier(
315+
&self,
316+
callback: Box<dyn RenderingNotifier>,
317+
) -> std::result::Result<(), SetRenderingNotifierError> {
318+
let mut notifier = self.rendering_notifier.borrow_mut();
319+
if notifier.replace(callback).is_some() {
320+
Err(SetRenderingNotifierError::AlreadySet)
321+
} else {
322+
Ok(())
323+
}
324+
}
325+
261326
fn show_popup(&self, popup: &ComponentRc, position: Point) {
262327
let runtime_window = self.self_weak.upgrade().unwrap();
263328
let size = runtime_window.set_active_popup(PopupWindow {
@@ -383,6 +448,8 @@ impl PlatformWindow for GLWindow {
383448
)
384449
.unwrap();
385450

451+
self.invoke_rendering_notifier(RenderingState::RenderingSetup, &opengl_context);
452+
386453
opengl_context.make_not_current();
387454

388455
let canvas = Rc::new(RefCell::new(canvas));
@@ -443,10 +510,7 @@ impl PlatformWindow for GLWindow {
443510

444511
fn hide(self: Rc<Self>) {
445512
// Release GL textures and other GPU bound resources.
446-
self.with_current_context(|| {
447-
self.graphics_cache.borrow_mut().clear();
448-
self.texture_cache.borrow_mut().clear();
449-
});
513+
self.release_graphics_resources();
450514

451515
self.map_state.replace(GraphicsWindowBackendState::Unmapped);
452516
/* FIXME:
@@ -621,6 +685,12 @@ impl PlatformWindow for GLWindow {
621685
}
622686
}
623687

688+
impl Drop for GLWindow {
689+
fn drop(&mut self) {
690+
self.release_graphics_resources();
691+
}
692+
}
693+
624694
struct MappedWindow {
625695
canvas: Option<CanvasRc>,
626696
opengl_context: crate::OpenGLContext,
@@ -632,7 +702,7 @@ impl Drop for MappedWindow {
632702
fn drop(&mut self) {
633703
if let Some(canvas) = self.canvas.take().map(|canvas| Rc::try_unwrap(canvas).ok()) {
634704
// The canvas must be destructed with a GL context current, in order to clean up correctly
635-
self.opengl_context.with_current_context(|| {
705+
self.opengl_context.with_current_context(|_| {
636706
drop(canvas);
637707
});
638708
} else {

internal/backends/mcu/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ mod the_backend {
123123
_items: &mut dyn Iterator<Item = Pin<sixtyfps_corelib::items::ItemRef<'a>>>,
124124
) {
125125
}
126+
126127
fn show_popup(&self, _popup: &ComponentRc, _position: sixtyfps_corelib::graphics::Point) {
127128
todo!()
128129
}

0 commit comments

Comments
 (0)