Skip to content

Commit 61462da

Browse files
Desktop: Implement GPU accelerated offscreen rendering and improve rendering efficency (#3056)
* WIP accelerade offscreen canvas implementation * Implement vulkan dmabuf import * Add fps printing * Add feature gates * Forgot to add file * Experimental windows support * Cast ptr to isize * Remove testing chrome://flags url * Experimental macos support for texture import * Cleanup code and improve latency / frame pacing * Add path for importing textures on windows through dx12 * Update doc comment * Import textures through metal on macos * Review cleanup --------- Co-authored-by: Timon Schelling <[email protected]>
1 parent c1c8788 commit 61462da

18 files changed

+1226
-51
lines changed

Cargo.lock

Lines changed: 35 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/Cargo.toml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ edition = "2024"
99
rust-version = "1.87"
1010

1111
[features]
12-
default = ["gpu"]
12+
default = ["gpu", "accelerated_paint"]
1313
gpu = ["graphite-desktop-wrapper/gpu"]
1414

15+
# Hardware acceleration features
16+
accelerated_paint = ["accelerated_paint_dmabuf", "accelerated_paint_d3d11", "accelerated_paint_iosurface"]
17+
accelerated_paint_dmabuf = ["libc", "ash"]
18+
accelerated_paint_d3d11 = ["windows", "ash"]
19+
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]
20+
1521
[dependencies]
1622
# # Local dependencies
1723
graphite-desktop-wrapper = { path = "wrapper" }
@@ -32,3 +38,26 @@ vello = { workspace = true }
3238
derivative = { workspace = true }
3339
rfd = { workspace = true }
3440
open = { workspace = true }
41+
42+
# Hardware acceleration dependencies
43+
ash = { version = "0.38", optional = true }
44+
45+
# Windows-specific dependencies
46+
[target.'cfg(windows)'.dependencies]
47+
windows = { version = "0.58", features = [
48+
"Win32_Graphics_Direct3D11",
49+
"Win32_Graphics_Direct3D12",
50+
"Win32_Graphics_Dxgi",
51+
"Win32_Graphics_Dxgi_Common",
52+
"Win32_Foundation"
53+
], optional = true }
54+
55+
# macOS-specific dependencies
56+
[target.'cfg(target_os = "macos")'.dependencies]
57+
objc2-io-surface = { version = "0.3", optional = true }
58+
objc2-metal = { version = "0.3", optional = true }
59+
core-foundation = { version = "0.9", optional = true }
60+
61+
# Linux-specific dependencies
62+
[target.'cfg(target_os = "linux")'.dependencies]
63+
libc = { version = "0.2", optional = true }

desktop/src/app.rs

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
use crate::CustomEvent;
22
use crate::cef::WindowSize;
3-
use crate::consts::APP_NAME;
3+
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
44
use crate::render::GraphicsState;
55
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage};
66
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
7+
78
use rfd::AsyncFileDialog;
89
use std::sync::Arc;
910
use std::sync::mpsc::Sender;
11+
use std::sync::mpsc::SyncSender;
1012
use std::thread;
1113
use std::time::Duration;
1214
use std::time::Instant;
1315
use winit::application::ApplicationHandler;
1416
use winit::dpi::PhysicalSize;
15-
use winit::event::StartCause;
1617
use winit::event::WindowEvent;
1718
use winit::event_loop::ActiveEventLoop;
1819
use winit::event_loop::ControlFlow;
@@ -31,11 +32,23 @@ pub(crate) struct WinitApp {
3132
wgpu_context: WgpuContext,
3233
event_loop_proxy: EventLoopProxy<CustomEvent>,
3334
desktop_wrapper: DesktopWrapper,
35+
last_ui_update: Instant,
36+
avg_frame_time: f32,
37+
start_render_sender: SyncSender<()>,
3438
}
3539

3640
impl WinitApp {
3741
pub(crate) fn new(cef_context: cef::Context<cef::Initialized>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
38-
let desktop_wrapper = DesktopWrapper::new();
42+
let rendering_loop_proxy = event_loop_proxy.clone();
43+
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
44+
std::thread::spawn(move || {
45+
loop {
46+
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
47+
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result));
48+
let _ = start_render_receiver.recv();
49+
}
50+
});
51+
3952
Self {
4053
cef_context,
4154
window: None,
@@ -44,7 +57,10 @@ impl WinitApp {
4457
window_size_sender,
4558
wgpu_context,
4659
event_loop_proxy,
47-
desktop_wrapper,
60+
desktop_wrapper: DesktopWrapper::new(),
61+
last_ui_update: Instant::now(),
62+
avg_frame_time: 0.,
63+
start_render_sender,
4864
}
4965
}
5066

@@ -152,23 +168,20 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
152168
// Set a timeout in case we miss any cef schedule requests
153169
let timeout = Instant::now() + Duration::from_millis(10);
154170
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
155-
self.cef_context.work();
156-
157-
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
158-
}
159-
160-
fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
161171
if let Some(schedule) = self.cef_schedule
162172
&& schedule < Instant::now()
163173
{
164174
self.cef_schedule = None;
165-
self.cef_context.work();
166-
}
167-
if let StartCause::ResumeTimeReached { .. } = cause {
168-
if let Some(window) = &self.window {
169-
window.request_redraw();
175+
// Poll cef message loop multiple times to avoid message loop starvation
176+
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
177+
self.cef_context.work();
170178
}
171179
}
180+
if let Some(window) = &self.window.as_ref() {
181+
window.request_redraw();
182+
}
183+
184+
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
172185
}
173186

174187
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
@@ -220,6 +233,11 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
220233
if let Some(graphics_state) = self.graphics_state.as_mut() {
221234
graphics_state.resize(texture.width(), texture.height());
222235
graphics_state.bind_ui_texture(texture);
236+
let elapsed = self.last_ui_update.elapsed().as_secs_f32();
237+
self.last_ui_update = Instant::now();
238+
if elapsed < 0.5 {
239+
self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.;
240+
}
223241
}
224242
if let Some(window) = &self.window {
225243
window.request_redraw();
@@ -251,16 +269,18 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
251269
WindowEvent::RedrawRequested => {
252270
let Some(ref mut graphics_state) = self.graphics_state else { return };
253271
// Only rerender once we have a new ui texture to display
254-
255-
match graphics_state.render() {
256-
Ok(_) => {}
257-
Err(wgpu::SurfaceError::Lost) => {
258-
tracing::warn!("lost surface");
259-
}
260-
Err(wgpu::SurfaceError::OutOfMemory) => {
261-
event_loop.exit();
272+
if let Some(window) = &self.window {
273+
match graphics_state.render(window.as_ref()) {
274+
Ok(_) => {}
275+
Err(wgpu::SurfaceError::Lost) => {
276+
tracing::warn!("lost surface");
277+
}
278+
Err(wgpu::SurfaceError::OutOfMemory) => {
279+
event_loop.exit();
280+
}
281+
Err(e) => tracing::error!("{:?}", e),
262282
}
263-
Err(e) => tracing::error!("{:?}", e),
283+
let _ = self.start_render_sender.try_send(());
264284
}
265285
}
266286
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881

desktop/src/cef.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
//! CEF (Chromium Embedded Framework) integration for Graphite Desktop
2+
//!
3+
//! This module provides CEF browser integration with hardware-accelerated texture sharing.
4+
//!
5+
//! # Hardware Acceleration
6+
//!
7+
//! The texture import system supports platform-specific hardware acceleration:
8+
//!
9+
//! - **Linux**: DMA-BUF via Vulkan external memory (`accelerated_paint_dmabuf` feature)
10+
//! - **Windows**: D3D11 shared textures via either Vulkan or D3D12 interop (`accelerated_paint_d3d11` feature)
11+
//! - **macOS**: IOSurface via Metal/Vulkan interop (`accelerated_paint_iosurface` feature)
12+
//!
13+
//!
14+
//! The system gracefully falls back to CPU textures when hardware acceleration is unavailable.
15+
116
use crate::CustomEvent;
217
use crate::render::FrameBufferRef;
318
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
@@ -10,15 +25,23 @@ mod dirs;
1025
mod input;
1126
mod internal;
1227
mod ipc;
28+
mod platform;
1329
mod scheme_handler;
1430
mod utility;
1531

32+
#[cfg(feature = "accelerated_paint")]
33+
mod texture_import;
34+
#[cfg(feature = "accelerated_paint")]
35+
use texture_import::SharedTextureHandle;
36+
1637
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
1738
use winit::event_loop::EventLoopProxy;
1839

1940
pub(crate) trait CefEventHandler: Clone {
2041
fn window_size(&self) -> WindowSize;
2142
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
43+
#[cfg(feature = "accelerated_paint")]
44+
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
2245
/// Scheudule the main event loop to run the cef event loop after the timeout
2346
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
2447
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
@@ -128,4 +151,16 @@ impl CefEventHandler for CefHandler {
128151
};
129152
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
130153
}
154+
155+
#[cfg(feature = "accelerated_paint")]
156+
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
157+
match shared_texture.import_texture(&self.wgpu_context.device) {
158+
Ok(texture) => {
159+
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
160+
}
161+
Err(e) => {
162+
tracing::error!("Failed to import shared texture: {}", e);
163+
}
164+
}
165+
}
131166
}

desktop/src/cef/context.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,17 @@ impl Context<Setup> {
8686
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
8787

8888
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
89+
// let url = CefString::from("chrome://gpu");
8990

9091
let window_info = WindowInfo {
9192
windowless_rendering_enabled: 1,
93+
#[cfg(feature = "accelerated_paint")]
94+
shared_texture_enabled: if crate::cef::platform::should_enable_hardware_acceleration() { 1 } else { 0 },
9295
..Default::default()
9396
};
9497

9598
let settings = BrowserSettings {
96-
windowless_frame_rate: 60,
99+
windowless_frame_rate: crate::consts::CEF_WINDOWLESS_FRAME_RATE,
97100
background_color: 0x0,
98101
..Default::default()
99102
};

desktop/src/cef/internal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod browser_process_app;
22
mod browser_process_client;
33
mod browser_process_handler;
44
mod browser_process_life_span_handler;
5-
mod render_handler;
5+
pub mod render_handler;
66
mod render_process_app;
77
mod render_process_handler;
88
mod render_process_v8_handler;

desktop/src/cef/internal/browser_process_app.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,28 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
3434

3535
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
3636
if let Some(cmd) = command_line {
37-
// Disable GPU acceleration, because it is not supported for Offscreen Rendering and can cause crashes.
38-
cmd.append_switch(Some(&CefString::from("disable-gpu")));
39-
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
37+
#[cfg(not(feature = "accelerated_paint"))]
38+
{
39+
// Disable GPU acceleration when accelerated_paint feature is not enabled
40+
cmd.append_switch(Some(&CefString::from("disable-gpu")));
41+
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
42+
}
43+
44+
#[cfg(feature = "accelerated_paint")]
45+
{
46+
// Enable GPU acceleration switches for better performance
47+
cmd.append_switch(Some(&CefString::from("enable-gpu-rasterization")));
48+
cmd.append_switch(Some(&CefString::from("enable-accelerated-2d-canvas")));
49+
}
50+
51+
#[cfg(all(feature = "accelerated_paint", target_os = "linux"))]
52+
{
53+
// Use Vulkan for accelerated painting
54+
cmd.append_switch_with_value(Some(&CefString::from("use-angle")), Some(&CefString::from("vulkan")));
55+
}
4056

4157
// Tell CEF to use Wayland if available
42-
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
58+
#[cfg(target_os = "linux")]
4359
{
4460
let use_wayland = env::var("WAYLAND_DISPLAY")
4561
.ok()

0 commit comments

Comments
 (0)