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.
This commit is contained in:
Simon Hausmann 2022-01-13 14:40:52 +01:00 committed by Simon Hausmann
parent 54d9ebdc19
commit 8959eac3d0
9 changed files with 295 additions and 25 deletions

View file

@ -28,6 +28,7 @@ as well as the [Rust migration guide for the `sixtyfps` crate](api/sixtyfps-rs/m
### Added
- `TextEdit::font-size` and `LineEdit::font-size` have been added to control the size of these widgets.
- Added `sixtyfps::Window::set_rendering_notifier` to get a callback before and after a new frame is being rendered.
### Fixed

View file

@ -128,6 +128,9 @@ fn gen_corelib(
.map(|x| x.to_string())
.collect();
let mut private_exported_types: std::collections::HashSet<String> =
config.export.include.iter().cloned().collect();
config.export.exclude = [
"SharedString",
"SharedVector",
@ -157,8 +160,10 @@ fn gen_corelib(
"sixtyfps_color_darker",
"sixtyfps_image_size",
"sixtyfps_image_path",
"TimerMode", // included in generated_public.h
"IntSize", // included in generated_public.h
"TimerMode", // included in generated_public.h
"IntSize", // included in generated_public.h
"RenderingState", // included in generated_public.h
"SetRenderingNotifierError", // included in generated_public.h
]
.iter()
.map(|x| x.to_string())
@ -201,6 +206,7 @@ fn gen_corelib(
.insert("StateInfo".to_owned(), " using Instant = uint64_t;".into());
properties_config.structure.derive_eq = true;
properties_config.structure.derive_neq = true;
private_exported_types.extend(properties_config.export.include.iter().cloned());
cbindgen::Builder::new()
.with_config(properties_config)
.with_src(crate_dir.join("properties.rs"))
@ -261,6 +267,7 @@ fn gen_corelib(
"sixtyfps_windowrc_set_focus_item",
"sixtyfps_windowrc_set_component",
"sixtyfps_windowrc_show_popup",
"sixtyfps_windowrc_set_rendering_notifier",
"sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
"sixtyfps_color_brighter",
@ -289,6 +296,9 @@ fn gen_corelib(
// Property<> fields uses the public `sixtyfps::Blah` type
special_config.namespaces =
Some(vec!["sixtyfps".into(), "cbindgen_private".into(), "types".into()]);
private_exported_types.extend(special_config.export.include.iter().cloned());
cbindgen::Builder::new()
.with_config(special_config)
.with_src(crate_dir.join("graphics.rs"))
@ -309,8 +319,15 @@ fn gen_corelib(
let mut public_config = config.clone();
public_config.namespaces = Some(vec!["sixtyfps".into()]);
public_config.export.item_types = vec![cbindgen::ItemType::Enums, cbindgen::ItemType::Structs];
public_config.export.include = vec!["TimerMode".into(), "IntSize".into()];
public_config.export.exclude.clear();
// Previously included types are now excluded (to avoid duplicates)
public_config.export.exclude = private_exported_types.into_iter().collect();
public_config.export.exclude.push("Point".into());
public_config.export.include = vec![
"TimerMode".into(),
"IntSize".into(),
"RenderingState".into(),
"SetRenderingNotifierError".into(),
];
public_config.export.body.insert(
"IntSize".to_owned(),
@ -324,6 +341,8 @@ fn gen_corelib(
.with_config(public_config)
.with_src(crate_dir.join("timers.rs"))
.with_src(crate_dir.join("graphics.rs"))
.with_src(crate_dir.join("window.rs"))
.with_src(crate_dir.join("api.rs"))
.with_after_include(format!(
r"
/// This macro expands to the to the numeric value of the major version of SixtyFPS you're

View file

@ -141,6 +141,23 @@ public:
cbindgen_private::sixtyfps_windowrc_show_popup(&inner, &popup, p, &parent_item);
}
template<typename F>
std::optional<SetRenderingNotifierError> set_rendering_notifier(F callback) const
{
auto actual_cb = [](RenderingState state, void *data) {
(*reinterpret_cast<F *>(data))(state);
};
SetRenderingNotifierError err;
if (cbindgen_private::sixtyfps_windowrc_set_rendering_notifier(
&inner, actual_cb,
[](void *user_data) { delete reinterpret_cast<F *>(user_data); },
new F(std::move(callback)), &err)) {
return {};
} else {
return err;
}
}
private:
cbindgen_private::WindowRcOpaque inner;
};
@ -314,6 +331,16 @@ public:
/// De-registers the window from the windowing system, therefore hiding it.
void hide() { inner.hide(); }
/// This function allows registering a callback that's invoked during the different phases of
/// rendering. This allows custom rendering on top or below of the scene.
/// On success, the function returns a std::optional without value. On error, the function
/// returns the error code as value in the std::optional.
template<typename F>
std::optional<SetRenderingNotifierError> set_rendering_notifier(F &&callback) const
{
return inner.set_rendering_notifier(std::forward<F>(callback));
}
/// \private
private_api::WindowRc &window_handle() { return inner; }
/// \private

View file

@ -16,7 +16,7 @@ enum OpenGLContextState {
#[cfg(not(target_arch = "wasm32"))]
Current(glutin::WindowedContext<glutin::PossiblyCurrent>),
#[cfg(target_arch = "wasm32")]
Current(Rc<winit::window::Window>),
Current { window: Rc<winit::window::Window>, canvas: web_sys::HtmlCanvasElement },
}
pub struct OpenGLContext(RefCell<Option<OpenGLContextState>>);
@ -29,7 +29,14 @@ impl OpenGLContext {
#[cfg(not(target_arch = "wasm32"))]
OpenGLContextState::Current(context) => context.window(),
#[cfg(target_arch = "wasm32")]
OpenGLContextState::Current(window) => window.as_ref(),
OpenGLContextState::Current { window, .. } => window.as_ref(),
})
}
#[cfg(target_arch = "wasm32")]
pub fn html_canvas_element(&self) -> std::cell::Ref<web_sys::HtmlCanvasElement> {
std::cell::Ref::map(self.0.borrow(), |state| match state.as_ref().unwrap() {
OpenGLContextState::Current { canvas, .. } => canvas,
})
}
@ -41,7 +48,7 @@ impl OpenGLContext {
let current_ctx = unsafe { not_current_ctx.make_current().unwrap() };
OpenGLContextState::Current(current_ctx)
}
state @ OpenGLContextState::Current(_) => state,
state @ OpenGLContextState::Current { .. } => state,
});
}
@ -60,12 +67,12 @@ impl OpenGLContext {
}
}
pub fn with_current_context<T>(&self, cb: impl FnOnce() -> T) -> T {
if matches!(self.0.borrow().as_ref().unwrap(), OpenGLContextState::Current(_)) {
cb()
pub fn with_current_context<T>(&self, cb: impl FnOnce(&Self) -> T) -> T {
if matches!(self.0.borrow().as_ref().unwrap(), OpenGLContextState::Current { .. }) {
cb(self)
} else {
self.make_current();
let result = cb();
let result = cb(self);
self.make_not_current();
result
}
@ -261,7 +268,15 @@ impl OpenGLContext {
let renderer =
femtovg::renderer::OpenGl::new_from_html_canvas(&window.canvas()).unwrap();
(Self(RefCell::new(Some(OpenGLContextState::Current(window)))), renderer)
(Self(RefCell::new(Some(OpenGLContextState::Current { window, canvas }))), renderer)
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_proc_address(&self, name: &str) -> *const std::ffi::c_void {
match &self.0.borrow().as_ref().unwrap() {
OpenGLContextState::NotCurrent(_) => std::ptr::null(),
OpenGLContextState::Current(current_ctx) => current_ctx.get_proc_address(name),
}
}
}

View file

@ -11,7 +11,9 @@ use std::rc::{Rc, Weak};
use super::{ItemGraphicsCache, TextureCache};
use crate::event_loop::WinitWindow;
use crate::glcontext::OpenGLContext;
use const_field_offset::FieldOffsets;
use corelib::api::{GraphicsAPI, RenderingNotifier, RenderingState, SetRenderingNotifierError};
use corelib::component::ComponentRc;
use corelib::graphics::*;
use corelib::input::KeyboardModifiers;
@ -38,6 +40,8 @@ pub struct GLWindow {
fps_counter: Option<Rc<FPSCounter>>,
rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>,
#[cfg(target_arch = "wasm32")]
canvas_id: String,
}
@ -61,16 +65,17 @@ impl GLWindow {
graphics_cache: Default::default(),
texture_cache: Default::default(),
fps_counter: FPSCounter::new(),
rendering_notifier: Default::default(),
#[cfg(target_arch = "wasm32")]
canvas_id,
})
}
fn with_current_context<T>(&self, cb: impl FnOnce() -> T) -> T {
fn with_current_context<T>(&self, cb: impl FnOnce(&OpenGLContext) -> T) -> Option<T> {
match &*self.map_state.borrow() {
GraphicsWindowBackendState::Unmapped => cb(),
GraphicsWindowBackendState::Unmapped => None,
GraphicsWindowBackendState::Mapped(window) => {
window.opengl_context.with_current_context(cb)
Some(window.opengl_context.with_current_context(cb))
}
}
}
@ -108,6 +113,38 @@ impl GLWindow {
pub fn default_font_properties(&self) -> FontRequest {
self.self_weak.upgrade().unwrap().default_font_properties()
}
fn release_graphics_resources(&self) {
// Release GL textures and other GPU bound resources.
self.with_current_context(|context| {
self.graphics_cache.borrow_mut().clear();
self.texture_cache.borrow_mut().clear();
self.invoke_rendering_notifier(RenderingState::RenderingTeardown, context);
});
}
/// Invoke any registered rendering notifiers about the state the backend renderer is currently in.
fn invoke_rendering_notifier(&self, state: RenderingState, opengl_context: &OpenGLContext) {
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
#[cfg(not(target_arch = "wasm32"))]
let api = GraphicsAPI::NativeOpenGL {
get_proc_address: &|name| opengl_context.get_proc_address(name),
};
#[cfg(target_arch = "wasm32")]
let canvas_element_id = opengl_context.html_canvas_element().id();
#[cfg(target_arch = "wasm32")]
let api = GraphicsAPI::WebGL {
canvas_element_id: canvas_element_id.as_str(),
context_type: "webgl",
};
callback.notify(state, &api)
}
}
fn has_rendering_notifier(&self) -> bool {
self.rendering_notifier.borrow().is_some()
}
}
impl WinitWindow for GLWindow {
@ -127,7 +164,7 @@ impl WinitWindow for GLWindow {
fn draw(self: Rc<Self>) {
let runtime_window = self.self_weak.upgrade().unwrap();
let scale_factor = runtime_window.scale_factor();
runtime_window.draw_contents(|components| {
runtime_window.clone().draw_contents(|components| {
let window = match self.borrow_mapped_window() {
Some(window) => window,
None => return, // caller bug, doesn't make sense to call draw() when not mapped
@ -151,6 +188,18 @@ impl WinitWindow for GLWindow {
size.height,
crate::to_femtovg_color(&window.clear_color),
);
// For the BeforeRendering rendering notifier callback it's important that this happens *after* clearing
// the back buffer, in order to allow the callback to provide its own rendering of the background.
// femtovg's clear_rect() will merely schedule a clear call, so flush right away to make it immediate.
if self.has_rendering_notifier() {
canvas.flush();
canvas.set_size(size.width, size.height, 1.0);
self.invoke_rendering_notifier(
RenderingState::BeforeRendering,
&window.opengl_context,
);
}
}
let mut renderer = crate::GLItemRenderer {
@ -184,6 +233,8 @@ impl WinitWindow for GLWindow {
drop(renderer);
self.invoke_rendering_notifier(RenderingState::AfterRendering, &window.opengl_context);
window.opengl_context.swap_buffers();
window.opengl_context.make_not_current();
});
@ -250,7 +301,7 @@ impl PlatformWindow for GLWindow {
})
.peekable();
if cache_entries_to_clear.peek().is_some() {
self.with_current_context(|| {
self.with_current_context(|_| {
cache_entries_to_clear.for_each(drop);
});
}
@ -258,6 +309,20 @@ impl PlatformWindow for GLWindow {
}
}
/// This function is called through the public API to register a callback that the backend needs to invoke during
/// different phases of rendering.
fn set_rendering_notifier(
&self,
callback: Box<dyn RenderingNotifier>,
) -> std::result::Result<(), SetRenderingNotifierError> {
let mut notifier = self.rendering_notifier.borrow_mut();
if notifier.replace(callback).is_some() {
Err(SetRenderingNotifierError::AlreadySet)
} else {
Ok(())
}
}
fn show_popup(&self, popup: &ComponentRc, position: Point) {
let runtime_window = self.self_weak.upgrade().unwrap();
let size = runtime_window.set_active_popup(PopupWindow {
@ -383,6 +448,8 @@ impl PlatformWindow for GLWindow {
)
.unwrap();
self.invoke_rendering_notifier(RenderingState::RenderingSetup, &opengl_context);
opengl_context.make_not_current();
let canvas = Rc::new(RefCell::new(canvas));
@ -443,10 +510,7 @@ impl PlatformWindow for GLWindow {
fn hide(self: Rc<Self>) {
// Release GL textures and other GPU bound resources.
self.with_current_context(|| {
self.graphics_cache.borrow_mut().clear();
self.texture_cache.borrow_mut().clear();
});
self.release_graphics_resources();
self.map_state.replace(GraphicsWindowBackendState::Unmapped);
/* FIXME:
@ -621,6 +685,12 @@ impl PlatformWindow for GLWindow {
}
}
impl Drop for GLWindow {
fn drop(&mut self) {
self.release_graphics_resources();
}
}
struct MappedWindow {
canvas: Option<CanvasRc>,
opengl_context: crate::OpenGLContext,
@ -632,7 +702,7 @@ impl Drop for MappedWindow {
fn drop(&mut self) {
if let Some(canvas) = self.canvas.take().map(|canvas| Rc::try_unwrap(canvas).ok()) {
// The canvas must be destructed with a GL context current, in order to clean up correctly
self.opengl_context.with_current_context(|| {
self.opengl_context.with_current_context(|_| {
drop(canvas);
});
} else {

View file

@ -123,6 +123,7 @@ mod the_backend {
_items: &mut dyn Iterator<Item = Pin<sixtyfps_corelib::items::ItemRef<'a>>>,
) {
}
fn show_popup(&self, _popup: &ComponentRc, _position: sixtyfps_corelib::graphics::Point) {
todo!()
}

View file

@ -224,8 +224,8 @@ impl WinitWindow for SimulatorWindow {
let size = self.opengl_context.window().inner_size();
self.opengl_context.with_current_context(|| {
self.opengl_context.ensure_resized();
self.opengl_context.with_current_context(|opengl_context| {
opengl_context.ensure_resized();
{
let mut canvas = self.canvas.borrow_mut();
@ -294,7 +294,7 @@ impl WinitWindow for SimulatorWindow {
canvas.flush();
canvas.delete_image(image_id);
self.opengl_context.swap_buffers();
opengl_context.swap_buffers();
});
}

View file

@ -10,6 +10,83 @@ use std::rc::Rc;
use crate::component::ComponentVTable;
use crate::window::WindowRc;
/// This enum describes a low-level access to specific graphcis APIs used
/// by the renderer.
#[derive(Clone)]
#[non_exhaustive]
pub enum GraphicsAPI<'a> {
/// The rendering is done using OpenGL.
NativeOpenGL {
/// Use this function pointer to obtain access to the OpenGL implementation - similar to `eglGetProcAddress`.
get_proc_address: &'a dyn Fn(&str) -> *const std::ffi::c_void,
},
/// The rendering is done on a HTML Canvas element using WebGL.
WebGL {
/// The DOM element id of the HTML Canvas element used for rendering.
canvas_element_id: &'a str,
/// The drawing context type used on the HTML Canvas element for rendering. This is the argument to the
/// `getContext` function on the HTML Canvas element.
context_type: &'a str,
},
}
impl<'a> std::fmt::Debug for GraphicsAPI<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphicsAPI::NativeOpenGL { .. } => write!(f, "GraphicsAPI::NativeOpenGL"),
GraphicsAPI::WebGL { context_type, .. } => {
write!(f, "GraphicsAPI::WebGL(context_type = {})", context_type)
}
}
}
}
/// This enum describes the different rendering states, that will be provided
/// to the parameter of the callback for `set_rendering_notifier` on the `Window`.
#[derive(Debug, Clone)]
#[repr(C)]
#[non_exhaustive]
pub enum RenderingState {
/// The window has been created and the graphics adapter/context initialized. When OpenGL
/// is used for rendering, the context will be current.
RenderingSetup,
/// The scene of items is about to be rendered. When OpenGL
/// is used for rendering, the context will be current.
BeforeRendering,
/// The scene of items was rendered, but the back buffer was not sent for display presentation
/// yet (for example GL swap buffers). When OpenGL is used for rendering, the context will be current.
AfterRendering,
/// The window will be destroyed and/or graphics resources need to be released due to other
/// constraints.
RenderingTeardown,
}
/// Internal trait that's used to map rendering state callbacks to either a Rust-API provided
/// impl FnMut or a struct that invokes a C callback and implements Drop to release the closure
/// on the C++ side.
pub trait RenderingNotifier {
/// Called to notify that rendering has reached a certain state.
fn notify(&mut self, state: RenderingState, graphics_api: &GraphicsAPI);
}
impl<F: FnMut(RenderingState, &GraphicsAPI)> RenderingNotifier for F {
fn notify(&mut self, state: RenderingState, graphics_api: &GraphicsAPI) {
self(state, graphics_api)
}
}
/// This enum describes the different error scenarios that may occur when the applicaton
/// registers a rendering notifier on a [`sixtyfps::Window`].
#[derive(Debug, Clone)]
#[repr(C)]
#[non_exhaustive]
pub enum SetRenderingNotifierError {
/// The rendering backend does not support rendering notifiers.
Unsupported,
/// There is already a rendering notifier set, multiple notifiers are not supported.
AlreadySet,
}
/// This type represents a window towards the windowing system, that's used to render the
/// scene of a component. It provides API to control windowing system specific aspects such
/// as the position on the screen.
@ -33,6 +110,15 @@ impl Window {
pub fn hide(&self) {
self.0.hide();
}
/// This function allows registering a callback that's invoked during the different phases of
/// rendering. This allows custom rendering on top or below of the scene.
pub fn set_rendering_notifier(
&self,
callback: impl FnMut(RenderingState, &GraphicsAPI) + 'static,
) -> std::result::Result<(), SetRenderingNotifierError> {
self.0.set_rendering_notifier(Box::new(callback))
}
}
impl crate::window::WindowHandleAccess for Window {

View file

@ -30,6 +30,15 @@ pub trait PlatformWindow {
/// implementation typically uses this to free the underlying graphics resources cached via [`crate::graphics::RenderingCache`].
fn free_graphics_resources<'a>(&self, items: &mut dyn Iterator<Item = Pin<ItemRef<'a>>>);
/// This function is called through the public API to register a callback that the backend needs to invoke during
/// different phases of rendering.
fn set_rendering_notifier(
&self,
_callback: Box<dyn crate::api::RenderingNotifier>,
) -> std::result::Result<(), crate::api::SetRenderingNotifierError> {
Err(crate::api::SetRenderingNotifierError::Unsupported)
}
/// Show a popup at the given position
fn show_popup(&self, popup: &ComponentRc, position: Point);
@ -571,6 +580,7 @@ pub mod ffi {
#![allow(unsafe_code)]
use super::*;
use crate::api::{RenderingNotifier, RenderingState, SetRenderingNotifierError};
use crate::slice::Slice;
#[allow(non_camel_case_types)]
@ -678,4 +688,45 @@ pub mod ffi {
let window = &*(handle as *const WindowRc);
window.close_popup();
}
/// C binding to the set_rendering_notifier() API of Window
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_windowrc_set_rendering_notifier(
handle: *const WindowRcOpaque,
callback: extern "C" fn(rendering_state: RenderingState, user_data: *mut c_void),
drop_user_data: extern "C" fn(user_data: *mut c_void),
user_data: *mut c_void,
error: *mut SetRenderingNotifierError,
) -> bool {
struct CNotifier {
callback: extern "C" fn(rendering_state: RenderingState, user_data: *mut c_void),
drop_user_data: extern "C" fn(*mut c_void),
user_data: *mut c_void,
}
impl Drop for CNotifier {
fn drop(&mut self) {
(self.drop_user_data)(self.user_data)
}
}
impl RenderingNotifier for CNotifier {
fn notify(&mut self, state: RenderingState, _graphics_api: &crate::api::GraphicsAPI) {
(self.callback)(state, self.user_data)
}
}
let window = &*(handle as *const WindowRc);
match window.set_rendering_notifier(Box::new(CNotifier {
callback,
drop_user_data,
user_data,
})) {
Ok(()) => true,
Err(err) => {
*error = err;
false
}
}
}
}