mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 11:58:12 +08:00
Desktop: Isolate CEF-rendered UI into separate crate and process (#4321)
* Extract CEF rendered UI into a separate process and crate * Review * Review * Review * Review * Remove necessary workarounds * Block on frame copy ack * Crop and resample frames correctly * Skip blank frames * Fix deps * Fix fmt * Fix clippy warning * Review * Fix todo
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
use cef::args::Args;
|
||||
use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t, cef_thread_id_t};
|
||||
use cef::{
|
||||
App, Browser, BrowserSettings, CefString, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory,
|
||||
Settings, Task, ThreadId, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process, post_task,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME, WINDOWLESS_FRAME_RATE};
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::dirs::TempDir;
|
||||
use crate::frames::FrameStreamer;
|
||||
use crate::input::{self, InputEvent};
|
||||
use crate::internal::task::ClosureTask;
|
||||
use crate::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
|
||||
use crate::ipc::{MessageType, SendMessage};
|
||||
use crate::view::ViewInfoUpdate;
|
||||
|
||||
thread_local! {
|
||||
static CONTEXT: RefCell<Option<BrowserContext>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub(crate) struct CefContext {
|
||||
_not_send: PhantomData<*const ()>, // impl !Send for CefContext
|
||||
}
|
||||
|
||||
impl CefContext {
|
||||
pub(crate) fn create(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, accelerated_paint: bool) -> Result<Self, InitError> {
|
||||
let args = bootstrap(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::platform::mac::install_application();
|
||||
|
||||
let instance_dir = TempDir::new().map_err(|e| InitError::InstanceDirectoryCreationFailed(e.to_string()))?;
|
||||
initialize(&args, instance_dir.as_ref(), accelerated_paint)?;
|
||||
|
||||
let (created_tx, created_rx) = std::sync::mpsc::channel();
|
||||
let install_browser = move || {
|
||||
let result = create_browser(delegate, frames, view_info_sender, instance_dir, accelerated_paint).map(|context| CONTEXT.with(|b| *b.borrow_mut() = Some(context)));
|
||||
let _ = created_tx.send(result);
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
install_browser();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
run_on_ui_thread(install_browser);
|
||||
|
||||
created_rx.recv().unwrap_or(Err(InitError::BrowserCreationFailed))?;
|
||||
Ok(Self { _not_send: PhantomData })
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
|
||||
let result = control(CefContextHandle);
|
||||
let (dropped_sender, dropped_receiver) = std::sync::mpsc::channel();
|
||||
run_on_ui_thread(move || {
|
||||
drop(CONTEXT.take());
|
||||
let _ = dropped_sender.send(());
|
||||
});
|
||||
let _ = dropped_receiver.recv();
|
||||
cef::shutdown();
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
|
||||
let (result_sender, result_receiver) = std::sync::mpsc::channel();
|
||||
let control_thread = std::thread::Builder::new()
|
||||
.name("cef-host-control".to_string())
|
||||
.spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| control(CefContextHandle)));
|
||||
with_context(|context| {
|
||||
if let Some(host) = context.browser.host() {
|
||||
host.close_browser(1);
|
||||
}
|
||||
});
|
||||
run_on_ui_thread(cef::quit_message_loop);
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let _ = result_sender.send(result);
|
||||
}
|
||||
Err(panic) => std::panic::resume_unwind(panic),
|
||||
}
|
||||
})
|
||||
.expect("Failed to spawn the CEF control thread");
|
||||
cef::run_message_loop();
|
||||
drop(CONTEXT.take());
|
||||
cef::shutdown();
|
||||
if let Err(panic) = control_thread.join() {
|
||||
std::panic::resume_unwind(panic);
|
||||
}
|
||||
result_receiver.recv().expect("The CEF control thread ended without a result")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn execute_helper_process() -> std::process::ExitCode {
|
||||
let args = bootstrap(true);
|
||||
assert_eq!(args.as_cmd_line().unwrap().has_switch(Some(&"type".into())), 1, "Not a CEF helper process");
|
||||
let mut app = RenderProcessAppImpl::app();
|
||||
let code = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
|
||||
std::process::ExitCode::from(code as u8)
|
||||
}
|
||||
|
||||
fn bootstrap(helper: bool) -> Args {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
|
||||
assert!(loader.load());
|
||||
// LibraryLoader unloads the framework on drop
|
||||
std::mem::forget(loader);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = helper;
|
||||
|
||||
let _ = api_hash(CEF_API_VERSION_LAST, 0);
|
||||
Args::new()
|
||||
}
|
||||
|
||||
fn initialize(args: &Args, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> {
|
||||
let mut app = App::new(BrowserProcessAppImpl::new(accelerated_paint));
|
||||
if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)?), Some(&mut app), std::ptr::null_mut()) != 1 {
|
||||
return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn platform_settings(instance_dir: &Path) -> Result<Settings, InitError> {
|
||||
let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() {
|
||||
"debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE,
|
||||
"info" => cef_log_severity_t::LOGSEVERITY_INFO,
|
||||
"warn" => cef_log_severity_t::LOGSEVERITY_WARNING,
|
||||
"error" => cef_log_severity_t::LOGSEVERITY_ERROR,
|
||||
"none" => cef_log_severity_t::LOGSEVERITY_DISABLE,
|
||||
_ => cef_log_severity_t::LOGSEVERITY_FATAL,
|
||||
};
|
||||
|
||||
let Some(root_cache_path) = instance_dir.to_str().map(CefString::from) else {
|
||||
return Err(InitError::PathResolutionFailed(format!("non-UTF-8 instance directory path: {}", instance_dir.display())));
|
||||
};
|
||||
let base = Settings {
|
||||
windowless_rendering_enabled: 1,
|
||||
root_cache_path,
|
||||
cache_path: "".into(),
|
||||
disable_signal_handlers: 1,
|
||||
log_severity: LogSeverity::from(log_severity),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let exe = std::env::current_exe().map_err(|e| InitError::PathResolutionFailed(format!("cannot get current exe path: {e}")))?;
|
||||
let app_root = exe
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.and_then(|p| p.parent())
|
||||
.ok_or_else(|| InitError::PathResolutionFailed(format!("executable is not inside an app bundle: {}", exe.display())))?;
|
||||
let Some(main_bundle_path) = app_root.to_str().map(CefString::from) else {
|
||||
return Err(InitError::PathResolutionFailed(format!("invalid app bundle path: {}", app_root.display())));
|
||||
};
|
||||
Ok(Settings {
|
||||
main_bundle_path,
|
||||
multi_threaded_message_loop: 0,
|
||||
external_message_pump: 0,
|
||||
no_sandbox: 1, // GPU helper crashes when running with sandbox
|
||||
..base
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Ok(Settings {
|
||||
multi_threaded_message_loop: 1,
|
||||
#[cfg(target_os = "linux")]
|
||||
no_sandbox: 1,
|
||||
..base
|
||||
})
|
||||
}
|
||||
|
||||
fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, instance_dir: TempDir, accelerated_paint: bool) -> Result<BrowserContext, InitError> {
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
let _ = accelerated_paint;
|
||||
let mut client = Client::new(BrowserProcessClientImpl::new(&delegate, frames));
|
||||
|
||||
let window_info = WindowInfo {
|
||||
windowless_rendering_enabled: 1,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
shared_texture_enabled: accelerated_paint as i32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let settings = BrowserSettings {
|
||||
windowless_frame_rate: WINDOWLESS_FRAME_RATE,
|
||||
background_color: 0x0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let Some(mut incognito_request_context) = cef::request_context_create_context(
|
||||
Some(&RequestContextSettings {
|
||||
persist_session_cookies: 0,
|
||||
cache_path: "".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
Option::<&mut cef::RequestContextHandler>::None,
|
||||
) else {
|
||||
return Err(InitError::RequestContextCreationFailed);
|
||||
};
|
||||
|
||||
let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(delegate.clone()));
|
||||
incognito_request_context.clear_scheme_handler_factories();
|
||||
if incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)) != 1 {
|
||||
return Err(InitError::SchemeHandlerRegistrationFailed);
|
||||
}
|
||||
|
||||
let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/");
|
||||
browser_host_create_browser_sync(
|
||||
Some(&window_info),
|
||||
Some(&mut client),
|
||||
Some(&url.as_str().into()),
|
||||
Some(&settings),
|
||||
Option::<&mut DictionaryValue>::None,
|
||||
Some(&mut incognito_request_context),
|
||||
)
|
||||
.map(|browser| BrowserContext {
|
||||
delegate,
|
||||
browser,
|
||||
view_info_sender,
|
||||
_instance_dir: instance_dir,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("Failed to create browser");
|
||||
InitError::BrowserCreationFailed
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum InitError {
|
||||
#[error("Failed to create the instance directory: {0}")]
|
||||
InstanceDirectoryCreationFailed(String),
|
||||
#[error("Initialization failed with code: {0}")]
|
||||
InitializationFailureCode(u32),
|
||||
#[error("Browser creation failed")]
|
||||
BrowserCreationFailed,
|
||||
#[error("Request context creation failed")]
|
||||
RequestContextCreationFailed,
|
||||
#[error("Failed to resolve a required path: {0}")]
|
||||
PathResolutionFailed(String),
|
||||
#[error("Scheme handler registration failed")]
|
||||
SchemeHandlerRegistrationFailed,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CefContextHandle;
|
||||
|
||||
impl CefContextHandle {
|
||||
pub(crate) fn apply_input(&self, events: Vec<InputEvent>) {
|
||||
with_context(move |context| {
|
||||
for event in &events {
|
||||
input::apply(&context.browser, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) {
|
||||
with_context(move |context| context.update_view_info(update));
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_view_info(&self) {
|
||||
with_context(|context| context.refresh_view_info());
|
||||
}
|
||||
|
||||
pub(crate) fn send_web_message(&self, message: Vec<u8>) {
|
||||
with_context(move |context| context.send_web_message(message));
|
||||
}
|
||||
}
|
||||
|
||||
struct BrowserContext {
|
||||
delegate: BrowserDelegate,
|
||||
browser: Browser,
|
||||
view_info_sender: Sender<ViewInfoUpdate>,
|
||||
_instance_dir: TempDir,
|
||||
}
|
||||
|
||||
impl BrowserContext {
|
||||
fn update_view_info(&self, update: ViewInfoUpdate) {
|
||||
let _ = self.view_info_sender.send(update);
|
||||
}
|
||||
|
||||
fn refresh_view_info(&self) {
|
||||
let view_info = self.delegate.view_info();
|
||||
let Some(host) = self.browser.host() else {
|
||||
tracing::error!("Browser host is not available, cannot refresh view info");
|
||||
return;
|
||||
};
|
||||
host.set_zoom_level(view_info.zoom());
|
||||
host.was_resized();
|
||||
|
||||
// Fix for CEF not updating the view after resize
|
||||
// TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed
|
||||
host.invalidate(cef::PaintElementType::default());
|
||||
}
|
||||
|
||||
fn send_web_message(&self, message: Vec<u8>) {
|
||||
self.send_message(MessageType::SendToJS, &message);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BrowserContext {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("Shutting down CEF");
|
||||
if let Some(host) = self.browser.host() {
|
||||
host.close_browser(1);
|
||||
} else {
|
||||
tracing::error!("Browser host is not available, cannot close browser");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SendMessage for BrowserContext {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(frame) = self.browser.main_frame() else {
|
||||
tracing::error!("Main frame is not available, cannot send message");
|
||||
return;
|
||||
};
|
||||
frame.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_on_ui_thread<F>(closure: F)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let closure_task = ClosureTask::new(closure);
|
||||
let mut task = Task::new(closure_task);
|
||||
if post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)) != 1 {
|
||||
tracing::error!("Failed to post a task to the CEF UI thread");
|
||||
}
|
||||
}
|
||||
|
||||
fn with_context<F>(closure: F)
|
||||
where
|
||||
F: FnOnce(&mut BrowserContext) + Send + 'static,
|
||||
{
|
||||
run_on_ui_thread(move || {
|
||||
CONTEXT.with(|b| {
|
||||
if let Some(context) = b.borrow_mut().as_mut() {
|
||||
closure(context);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user