mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-07 20:48:12 +08:00
Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)
* Add rank polymorphism node audit classifying all 271 nodes
* Implement StaticType for Item<T>
* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input
* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate
* Document the Item kernel implementation and staging plan
* Route Item<Vector> through TaggedValue::TypeDefault
* Add executor integration tests covering the Item and List wire variants
* Collapse element-wise Item/List wire pairs to the List form for conversion insertion
* Migrate sixteen vector modifier nodes to Item element-wise kernels
* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels
* Fix bevel_with_transform test to actually exercise the transform attribute
* Implement From<T> for Item<T>
* Register PromoteNode rank adapters wrapping bare values into Item wires
* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor
* Define a real promote node backing the PromoteNode registry identifiers
* Zip ranked Item connectors by frame slot in the mapped element-wise variant
* Register ItemToListNode singleton raise adapters
* Resolve Item wires against List connectors by inserting promotion adapters at construction
* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end
* Implement Clampable for Item and List wires with per-variant clamp bounds
* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire
* Implement ApplyTransform for Item
* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0
* Detect element-wise nodes by lazy primary connectors declaring Output = Item
* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone
* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2
* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters
* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes
* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro
* Amend the audit with the DashPattern value type resolution
* Migrate the string family to Item element-wise kernels
* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters
* Shadow owned node parameters in bodies instead of mut in signatures
* Migrate the math family and string measure nodes to Item element-wise kernels
* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows
* Flat-map expander kernels returning List under the mapped variant's frame
* Migrate the expander nodes to Item kernels flat-mapping under the frame
* Remove the unused peel_list helper
* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in
Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).
Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.
* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk
* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites
* Flip the vector shape generators to emit rank-0 Item<Vector>
The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.
Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.
* Parse ranked Item<T> parameter defaults against the bare element type
A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.
* Parse an element-wise primary's scalar default against the bare element type
An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.
* Add the DashPattern value type for stroke dash sequences
Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.
* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector
Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.
Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.
Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.
* Register rank adapters for the ranked Stroke enum parameters
The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.
* Display Item wires in the Data panel without a List's ID column
Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.
* Register MonitorNode for Item wire types so the Data panel introspects them directly
Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.
* Color and double Item/List wires and cleave layer-stack connectors in the node graph
* Route wire color and rank through hidden nodes and refresh them on type changes
* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node
* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires
* Back DashPattern with a List<f64> so the Data panel can introspect its lengths
* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>
* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions
* Rank the remaining bare and unnecessarily-List connectors across the node catalog
* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values
* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill
* Restore generator frame-from-params ranking to the roadmap as a planned stage
* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract
* Unload only the wires whose displayed style changed when types update
* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized
* Restore the whole-List Transform variant so rank-1 content wires resolve again
* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes
* Give every ranked connector a field adapter and add numeric cast variants for legacy wires
* Key a ranked param's type default off its Item wire form when no literal default exists
* Inherit the layer's content value when splicing a node into an empty chain
* Migrate stale List-form TypeDefault inputs to the definition's current default
* Generate the mapped wire variant only when the element-wise node has a frame source
* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps
* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot
* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers
* Convert the measure family to element-wise Item kernels per the audit classification
* Prefer the bare element value over the Item type default so ranked params keep their widgets
* Rename GradientStopsUI to GradientUI
* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix
* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2
* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes
* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions
* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius
* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness
* Rank the value, color, and text generator params to Item
* Rank the raster, web-request, and context-reader generator params to Item
* Fix the repeat and brush test wirings left behind by the param-ranking sweeps
* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes
* Delete the Attach Attribute node, folding its role into Write Attribute
* Add the Filter and Sort list companion nodes
* Guard the removed-definition migration swap target with a test
* Add the Box Corners value type in place of the rectangle corner radius list
* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node
* Rank the Combine Channels node's channel connectors to Item
* Make Map Points an element-wise node
* Delete the deprecated Upload Texture node
* Update the implementation roadmap to reflect the landed stages
* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path
* Prefer the rank-0 default when disconnecting a rank-capable input
* Make Path Modify an element-wise node
* Wrap node paths in a NodeIdPath newtype so they flow as a single Item
* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves
* Stop the Brush node from setting layer attributes its paint operation doesn't produce
* Present-gate Flatten Path's adopted layer path like its fill and stroke
* Gate carried layer attributes on static column presence, not runtime values
* Give the remaining graphic Item<T> types a default so unconnected primaries resolve
* Dispatch a ranked param's Properties widget from its rank-0 element type
* Make Extract Transform an element-wise node, restoring the Origins to Polyline body
* Rename Flatten Path to Combine Paths
* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath
* Drop the dead List<u8> and List<NodeId> wire rows
* Rank Flatten Graphic's Fully Flatten toggle to Item
* Update the implementation roadmap with the endgame scope
* Make Combine Paths a reducer that collapses the whole frame into one path
* Stop type-converter nodes from carrying the source's unrelated attributes
* Format the Origins to Polyline regression test
* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value
* Make Switch a framed element-wise select, bundling whole collections
* Widen and align element-type coverage across the list and graphic nodes
* Register the compiler's cache chain pair for every ranked enum and newtype wire
* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires
* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion
* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg
* Delete the vestigial Clone debug node
* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters
* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR
* Deduplicate the promotion adapter registrations into the field adapter macro
* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge
* Vertical wire styling
* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>
* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row
* Rank Path Modify's modification parameter to Item<Box<VectorModification>>
* Rename the field adapter node family to input adapter
* Drop the dead bare scalar rows from Context Modification's implementations list
* Move the dynamic executor's test module into its own file
* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode
* Materialize stored TaggedValues as ranked Item wires at the source
* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization
* Plant the input adapter for List-only inputs, composing position conversion from standard rows
* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers
* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect
* Enforce ranked node inputs in the macro, rejecting bare wire declarations
* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel
* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration
* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced
* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs
* Rewrite the element-wise accessor wire type at the primary input, not raw index 0
* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row
* Gate the remaining Raster<GPU> registry rows behind the gpu feature
* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter
* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index
* Store paint picks as plain color/gradient values, removing the FillChoice value type
* Code review restructuring
* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node
* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR
* Parse Fill and Stroke color defaults through the paint wire's Graphic element
* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing
* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration
* Migrate the legacy 4-input Fill directly to the split has-transform shape
* Upgrade the demo artwork
* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues
* Remove the rank polymorphism working documents
* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation
* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types
* Derive PartialEq for Item now that attributes participate in equality
* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types
* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "graphite-desktop-ui"
|
||||
description = "Renders the Graphite editor frontend UI into wgpu textures"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
|
||||
accelerated_paint = ["dep:ash", "dep:bytemuck", "dep:objc2-io-surface", "dep:objc2-metal", "dep:mach2"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-desktop-embedded-resources = { path = "../embedded-resources", optional = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
wgpu-sync = { workspace = true }
|
||||
winit = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
cef = { workspace = true }
|
||||
bytemuck = { workspace = true, optional = true }
|
||||
ipc-channel = "0.22"
|
||||
|
||||
# Linux-specific dependencies
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
ash = { version = "0.38", optional = true }
|
||||
|
||||
# Windows-specific dependencies
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D12",
|
||||
"Win32_Security",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
# Mac-specific dependencies
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
libc = "0.2"
|
||||
mach2 = { version = "0.4", optional = true }
|
||||
objc2 = { version = "0.6.1", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
objc2-io-surface = { version = "0.3.2", optional = true }
|
||||
objc2-metal = { version = "0.3", features = ["objc2-io-surface"], optional = true }
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const RESOURCE_SCHEME: &str = "resources";
|
||||
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
|
||||
|
||||
pub(crate) const BROWSER_HOST_CONFIG_FLAG: &str = "--graphite-browser-host=";
|
||||
|
||||
pub(crate) const WINDOWLESS_FRAME_RATE: i32 = 60;
|
||||
pub(crate) const FRAMES_IN_FLIGHT_LIMIT: u64 = 3;
|
||||
pub(crate) const FRAME_SEGMENT_POOL_SIZE: u64 = FRAMES_IN_FLIGHT_LIMIT + 1; // allow one extra staged frame
|
||||
pub(crate) const FRAME_SEGMENT_GRANULARITY: usize = 2 * 1024 * 1024; // 2 MiB
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) const FRAME_ACK_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) const HOST_HELLO_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub(crate) const HOST_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const IPC_BOOTSTRAP_PREFIX: &str = "art.graphite.Graphite.ipc.";
|
||||
|
||||
pub(crate) const SCROLL_LINE_HEIGHT: usize = 40;
|
||||
pub(crate) const SCROLL_LINE_WIDTH: usize = 40;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 3.;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 3.;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 1.;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 1.;
|
||||
|
||||
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.;
|
||||
|
||||
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::remote::messages::EventMessage;
|
||||
use super::view::{ViewInfo, ViewInfoReceiver, ViewInfoUpdate};
|
||||
use crate::Cursor;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BrowserDelegate(Arc<Inner>);
|
||||
|
||||
struct Inner {
|
||||
sender: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
view_info: Mutex<ViewInfoReceiver>,
|
||||
}
|
||||
|
||||
impl BrowserDelegate {
|
||||
pub(crate) fn new(sender: Arc<Mutex<IpcSender<EventMessage>>>, view_info_receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self(Arc::new(Inner {
|
||||
sender,
|
||||
view_info: Mutex::new(ViewInfoReceiver::new(view_info_receiver)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn send(&self, message: EventMessage) {
|
||||
let Ok(sender) = self.0.sender.lock() else {
|
||||
tracing::error!("Failed to lock host message sender");
|
||||
return;
|
||||
};
|
||||
if let Err(e) = sender.send(message) {
|
||||
tracing::debug!("Failed to send message to main process: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn view_info(&self) -> ViewInfo {
|
||||
let Ok(mut guard) = self.0.view_info.lock() else {
|
||||
tracing::error!("Failed to lock the view info mirror");
|
||||
return ViewInfo::new();
|
||||
};
|
||||
guard.current()
|
||||
}
|
||||
|
||||
pub(crate) fn load_resource(&self, path: PathBuf) -> Option<crate::resources::Resource> {
|
||||
crate::resources::load(path)
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_change(&self, cursor: Cursor) {
|
||||
self.send(EventMessage::CursorChange(cursor));
|
||||
}
|
||||
|
||||
pub(crate) fn initialized_web_communication(&self) {
|
||||
self.send(EventMessage::WebCommunicationInitialized);
|
||||
}
|
||||
|
||||
pub(crate) fn receive_web_message(&self, message: &[u8]) {
|
||||
self.send(EventMessage::WebMessage(message.to_vec()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const APP_DIRECTORY_NAME: &str = "graphite";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const APP_DIRECTORY_NAME: &str = "Graphite";
|
||||
|
||||
pub(crate) fn app_tmp_dir() -> PathBuf {
|
||||
let path = std::env::temp_dir().join(APP_DIRECTORY_NAME);
|
||||
if let Err(e) = fs::create_dir_all(&path) {
|
||||
tracing::error!("Failed to create temp directory at {path:?}: {e}");
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Temporary directory that is automatically deleted when dropped.
|
||||
pub struct TempDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
Self::new_with_parent(app_tmp_dir())
|
||||
}
|
||||
|
||||
pub fn new_with_parent(parent: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let random_suffix = format!("{:032x}", rand::random::<u128>());
|
||||
let name = format!("{}_{}", std::process::id(), random_suffix);
|
||||
let path = parent.as_ref().join(name);
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let result = fs::remove_dir_all(&self.path);
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to remove temporary directory at {:?}: {}", self.path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for TempDir {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use crate::UiEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EventQueue {
|
||||
sender: std::sync::mpsc::Sender<UiEvent>,
|
||||
terminated: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl EventQueue {
|
||||
pub(crate) fn new() -> (Self, Receiver<UiEvent>) {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
(
|
||||
Self {
|
||||
sender,
|
||||
terminated: Arc::new(AtomicBool::new(false)),
|
||||
},
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, event: UiEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
|
||||
pub(crate) fn terminate(&self, event: UiEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
self.terminated.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_terminated(&self) {
|
||||
self.terminated.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn is_terminated(&self) -> bool {
|
||||
self.terminated.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) mod import;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) mod plane;
|
||||
pub(crate) mod receive;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
mod resample;
|
||||
pub(crate) mod sequence;
|
||||
pub(crate) mod sink;
|
||||
mod streamer;
|
||||
mod surface;
|
||||
|
||||
pub(crate) use streamer::FrameStreamer;
|
||||
pub(crate) use surface::FrameSurface;
|
||||
@@ -0,0 +1,117 @@
|
||||
use cef::sys::cef_color_type_t;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod d3d11;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod dmabuf;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod iosurface;
|
||||
|
||||
pub(crate) type TextureImportResult = Result<wgpu::Texture, TextureImportError>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum TextureImportError {
|
||||
#[error("Invalid texture handle: {0}")]
|
||||
InvalidHandle(String),
|
||||
#[error("Unsupported texture format: {format:?}")]
|
||||
UnsupportedFormat { format: cef_color_type_t },
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[error("Hardware acceleration not available: {reason}")]
|
||||
HardwareUnavailable { reason: String },
|
||||
#[error("Vulkan operation failed: {operation}")]
|
||||
#[cfg(target_os = "linux")]
|
||||
VulkanError { operation: String },
|
||||
#[error("Platform-specific error: {message}")]
|
||||
PlatformError { message: String },
|
||||
}
|
||||
|
||||
impl From<wgpu::hal::DeviceError> for TextureImportError {
|
||||
fn from(e: wgpu::hal::DeviceError) -> Self {
|
||||
TextureImportError::PlatformError {
|
||||
message: format!("wgpu-hal DeviceError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct ContentRect {
|
||||
pub(crate) x: u32,
|
||||
pub(crate) y: u32,
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
pub(crate) source_width: u32,
|
||||
pub(crate) source_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ContentMapping {
|
||||
Identity,
|
||||
Scaled(ContentRect),
|
||||
}
|
||||
|
||||
impl ContentRect {
|
||||
pub(crate) fn mapping(self, width: u32, height: u32) -> ContentMapping {
|
||||
let valid = self.width > 0
|
||||
&& self.height > 0
|
||||
&& self.source_width > 0
|
||||
&& self.source_height > 0
|
||||
&& self.x.checked_add(self.width).is_some_and(|right| right <= width)
|
||||
&& self.y.checked_add(self.height).is_some_and(|bottom| bottom <= height);
|
||||
let full = self.x == 0 && self.y == 0 && (self.width, self.height) == (width, height) && (self.source_width, self.source_height) == (width, height);
|
||||
if valid && !full { ContentMapping::Scaled(self) } else { ContentMapping::Identity }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&cef::AcceleratedPaintInfo> for ContentRect {
|
||||
type Error = TextureImportError;
|
||||
|
||||
fn try_from(info: &cef::AcceleratedPaintInfo) -> Result<Self, Self::Error> {
|
||||
let invalid = || TextureImportError::InvalidHandle("Failed to create content rect".into());
|
||||
let content = &info.extra.content_rect;
|
||||
let width = u32::try_from(content.width).ok().filter(|&width| width > 0).ok_or_else(invalid)?;
|
||||
let height = u32::try_from(content.height).ok().filter(|&height| height > 0).ok_or_else(invalid)?;
|
||||
let source = &info.extra.source_size;
|
||||
let (source_width, source_height) = if info.extra.has_source_size != 0 && source.width > 0 && source.height > 0 {
|
||||
(source.width as u32, source.height as u32)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Ok(Self {
|
||||
x: content.x.max(0) as u32,
|
||||
y: content.y.max(0) as u32,
|
||||
width,
|
||||
height,
|
||||
source_width,
|
||||
source_height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TextureImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult;
|
||||
}
|
||||
|
||||
fn wgpu_format(format: cef_color_type_t) -> Result<wgpu::TextureFormat, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8Unorm),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8Unorm),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_descriptor(width: u32, height: u32, format: cef_color_type_t, label: &'static str) -> Result<wgpu::TextureDescriptor<'static>, TextureImportError> {
|
||||
Ok(wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
|
||||
use cef::sys::cef_color_type_t;
|
||||
use std::os::raw::c_void;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct D3D11Importer {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for D3D11Importer {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
if self.handle.is_null() {
|
||||
return Err(TextureImportError::InvalidHandle("Null D3D11 shared texture handle".to_string()));
|
||||
}
|
||||
|
||||
let is_d3d12_backend = unsafe { device.as_hal::<api::Dx12>().is_some() };
|
||||
|
||||
if is_d3d12_backend {
|
||||
let texture = self.import_via_d3d12(device)?;
|
||||
return Ok(texture);
|
||||
}
|
||||
|
||||
let texture = self.import_via_vulkan(device)?;
|
||||
tracing::trace!("Successfully imported D3D11 shared texture via Vulkan");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl D3D11Importer {
|
||||
pub fn from_parts(handle: u64, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self {
|
||||
handle: handle as *mut c_void,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::hal::api;
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<api::Dx12>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using D3D12 backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let d3d12_resource = self.import_d3d11_handle_to_d3d12(&hal_device)?;
|
||||
|
||||
let hal_texture = <api::Dx12 as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
d3d12_resource,
|
||||
wgpu_format(self.format)?,
|
||||
wgpu::TextureDimension::D2,
|
||||
wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
1, // mip_level_count
|
||||
1, // sample_count
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<api::Dx12>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11→D3D12 Shared Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let hal_texture = <Vulkan as wgpu::hal::Api>::Device::texture_from_d3d11_shared_handle(
|
||||
&hal_device,
|
||||
windows::Win32::Foundation::HANDLE(self.handle),
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF D3D11 Shared Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to import D3D11 shared handle into Vulkan: {:?}", e),
|
||||
})?;
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<Vulkan>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11 Shared Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_d3d11_handle_to_d3d12(&self, hal_device: &<wgpu::hal::api::Dx12 as wgpu::hal::Api>::Device) -> Result<windows::Win32::Graphics::Direct3D12::ID3D12Resource, TextureImportError> {
|
||||
use windows::Win32::Graphics::Direct3D12::*;
|
||||
|
||||
let d3d12_device = hal_device.raw_device();
|
||||
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut shared_resource: Option<ID3D12Resource> = None;
|
||||
d3d12_device
|
||||
.OpenSharedHandle(windows::Win32::Foundation::HANDLE(self.handle), &mut shared_resource)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to open D3D11 shared handle on D3D12: {:?}", e),
|
||||
})?;
|
||||
|
||||
shared_resource.ok_or_else(|| TextureImportError::InvalidHandle("Failed to get D3D12 resource from shared handle".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
|
||||
use ash::vk;
|
||||
use cef::sys::cef_color_type_t;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct DmaBufImporter {
|
||||
fds: Vec<std::os::fd::OwnedFd>,
|
||||
format: cef_color_type_t,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
strides: Vec<u32>,
|
||||
offsets: Vec<u32>,
|
||||
}
|
||||
|
||||
impl TextureImporter for DmaBufImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
if self.fds.len() != 1 {
|
||||
return Err(TextureImportError::InvalidHandle(format!("Expected exactly one DMA-BUF plane fd, got {}", self.fds.len())));
|
||||
}
|
||||
|
||||
if self.strides.len() != self.fds.len() || self.offsets.len() != self.fds.len() {
|
||||
return Err(TextureImportError::InvalidHandle(format!(
|
||||
"DMA-BUF plane count mismatch: {} fds, {} strides, {} offsets",
|
||||
self.fds.len(),
|
||||
self.strides.len(),
|
||||
self.offsets.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let texture = self.import_via_vulkan(device)?;
|
||||
tracing::trace!("Successfully imported DMA-BUF texture via Vulkan");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaBufImporter {
|
||||
pub fn from_parts(fds: Vec<std::os::fd::OwnedFd>, strides: Vec<u32>, offsets: Vec<u32>, modifier: u64, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self {
|
||||
fds,
|
||||
format,
|
||||
modifier,
|
||||
width,
|
||||
height,
|
||||
strides,
|
||||
offsets,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<api::Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let (vk_image, device_memory) = self.create_vulkan_image_from_dmabuf(&hal_device)?;
|
||||
|
||||
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
&hal_device,
|
||||
vk_image,
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF DMA-BUF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None,
|
||||
wgpu::hal::vulkan::TextureMemory::Dedicated(device_memory),
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<Vulkan>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF DMA-BUF Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn create_vulkan_image_from_dmabuf(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<(vk::Image, vk::DeviceMemory), TextureImportError> {
|
||||
let device = hal_device.raw_device();
|
||||
let instance = hal_device.shared_instance().raw_instance();
|
||||
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string()));
|
||||
}
|
||||
|
||||
let image_create_info = vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(vulkan_format(self.format)?)
|
||||
.extent(vk::Extent3D {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
|
||||
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||
|
||||
let plane_layouts = self
|
||||
.offsets
|
||||
.iter()
|
||||
.zip(&self.strides)
|
||||
.map(|(&offset, &stride)| vk::SubresourceLayout {
|
||||
offset: offset as u64,
|
||||
size: 0, // Will be calculated by driver
|
||||
row_pitch: stride as u64,
|
||||
array_pitch: 0,
|
||||
depth_pitch: 0,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
||||
.drm_format_modifier(self.modifier)
|
||||
.plane_layouts(&plane_layouts);
|
||||
|
||||
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
|
||||
|
||||
let image_create_info = image_create_info.push_next(&mut drm_format_modifier).push_next(&mut external_memory_info);
|
||||
|
||||
let image = unsafe {
|
||||
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to create Vulkan image: {e:?}"),
|
||||
})?
|
||||
};
|
||||
|
||||
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||
|
||||
// Duplicate the file descriptor
|
||||
let dup_fd = unsafe { libc::dup(std::os::fd::AsRawFd::as_raw_fd(&self.fds[0])) };
|
||||
if dup_fd == -1 {
|
||||
// SAFETY: the image was created above and never bound or returned.
|
||||
unsafe { device.destroy_image(image, None) };
|
||||
return Err(TextureImportError::PlatformError {
|
||||
message: "Failed to duplicate DMA-BUF file descriptor".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let external_memory_fd = ash::khr::external_memory_fd::Device::new(instance, device);
|
||||
let mut fd_properties = vk::MemoryFdPropertiesKHR::default();
|
||||
if let Err(e) = unsafe { external_memory_fd.get_memory_fd_properties(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup_fd, &mut fd_properties) } {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to query DMA-BUF fd memory properties: {e:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
|
||||
|
||||
let memory_properties = unsafe { instance.get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||
|
||||
let compatible_type_bits = memory_requirements.memory_type_bits & fd_properties.memory_type_bits;
|
||||
let Some(memory_type_index) = find_memory_type_index(compatible_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: "Failed to find suitable memory type for DMA-BUF".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(memory_requirements.size)
|
||||
.memory_type_index(memory_type_index)
|
||||
.push_next(&mut import_memory_fd);
|
||||
|
||||
let device_memory = match unsafe { device.allocate_memory(&allocate_info, None) } {
|
||||
Ok(memory) => memory,
|
||||
Err(e) => {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = unsafe { device.bind_image_memory(image, device_memory, 0) } {
|
||||
// SAFETY: import failed, need to clean up the image and free the memory
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
device.free_memory(device_memory, None);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to bind memory to image: {e:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((image, device_memory))
|
||||
}
|
||||
}
|
||||
|
||||
fn vulkan_format(format: cef_color_type_t) -> Result<vk::Format, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(vk::Format::B8G8R8A8_UNORM),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(vk::Format::R8G8B8A8_UNORM),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_memory_type_index(type_filter: u32, properties: vk::MemoryPropertyFlags, mem_properties: &vk::PhysicalDeviceMemoryProperties) -> Option<u32> {
|
||||
(0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties))
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor};
|
||||
use cef::sys::cef_color_type_t;
|
||||
use objc2::rc::Retained;
|
||||
use objc2_io_surface::IOSurfaceRef;
|
||||
use objc2_metal::{MTLDevice, MTLPixelFormat, MTLStorageMode, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage};
|
||||
use wgpu::TextureDescriptor;
|
||||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
pub struct IOSurfaceImporter {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for IOSurfaceImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
let texture = self.import_via_metal(device)?;
|
||||
tracing::trace!("Successfully imported IOSurface texture via Metal");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl IOSurfaceImporter {
|
||||
pub fn from_parts(handle: *mut c_void, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self { handle, format, width, height }
|
||||
}
|
||||
|
||||
fn get_metal_desc(&self, texture_desc: &TextureDescriptor) -> Result<Retained<MTLTextureDescriptor>, TextureImportError> {
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid IOSurface texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
let metal_desc = MTLTextureDescriptor::new();
|
||||
unsafe {
|
||||
metal_desc.setWidth(texture_desc.size.width as _);
|
||||
metal_desc.setHeight(texture_desc.size.height as _);
|
||||
metal_desc.setArrayLength(texture_desc.array_layer_count() as _);
|
||||
metal_desc.setMipmapLevelCount(texture_desc.mip_level_count as _);
|
||||
metal_desc.setSampleCount(texture_desc.sample_count as _);
|
||||
metal_desc.setTextureType(MTLTextureType::Type2D);
|
||||
metal_desc.setPixelFormat(match texture_desc.format {
|
||||
wgpu::TextureFormat::Rgba8Unorm => MTLPixelFormat::RGBA8Unorm,
|
||||
wgpu::TextureFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm,
|
||||
_ => unimplemented!(),
|
||||
});
|
||||
metal_desc.setUsage(MTLTextureUsage::ShaderRead);
|
||||
metal_desc.setStorageMode(MTLStorageMode::Managed);
|
||||
}
|
||||
|
||||
Ok(metal_desc)
|
||||
}
|
||||
|
||||
fn import_via_metal(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
let io_surface = std::ptr::NonNull::new(self.handle.cast::<IOSurfaceRef>()).ok_or(TextureImportError::InvalidHandle("Invalid IOSurface handle".to_string()))?;
|
||||
|
||||
let texture_desc = texture_descriptor(self.width, self.height, self.format, "Cef Texture")?;
|
||||
let hal_tex = {
|
||||
let metal_desc = self.get_metal_desc(&texture_desc)?;
|
||||
|
||||
let texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<wgpu::wgc::api::Metal>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::InvalidHandle("Failed to get Metal device from wgpu".to_string()));
|
||||
};
|
||||
|
||||
let texture = hal_device
|
||||
.raw_device()
|
||||
.newTextureWithDescriptor_iosurface_plane(metal_desc.as_ref(), io_surface.as_ref(), 0)
|
||||
.ok_or(TextureImportError::InvalidHandle("Invalid IOSurface handle".to_string()))?;
|
||||
|
||||
let hal_tex = <wgpu::wgc::api::Metal as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
texture,
|
||||
texture_desc.format,
|
||||
MTLTextureType::Type2D,
|
||||
texture_desc.array_layer_count(),
|
||||
texture_desc.mip_level_count,
|
||||
wgpu::hal::CopyExtent {
|
||||
width: texture_desc.size.width,
|
||||
height: texture_desc.size.height,
|
||||
depth: texture_desc.array_layer_count(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_tex)
|
||||
}?;
|
||||
texture
|
||||
};
|
||||
|
||||
Ok(unsafe { device.create_texture_from_hal::<wgpu::wgc::api::Metal>(hal_tex, &texture_desc) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) use linux::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use win::*;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use mac::*;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) enum RecvResult {
|
||||
Frame(WireFrame),
|
||||
WouldBlock,
|
||||
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Decode the wire representation of `cef_color_type_t` (its `u32` discriminant),
|
||||
/// logging unknown discriminants.
|
||||
fn wire_color_type(format: u32) -> Option<cef::sys::cef_color_type_t> {
|
||||
match format {
|
||||
0 => Some(cef::sys::cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888),
|
||||
1 => Some(cef::sys::cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888),
|
||||
_ => {
|
||||
tracing::error!("Unknown color type {format} in accelerated frame");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::RecvResult;
|
||||
use crate::frames::surface::FrameSurface;
|
||||
use crate::remote::HostConfig;
|
||||
use crate::remote::messages::EventMessage;
|
||||
pub(crate) const FRAME_SOCKET_CHILD_FD: RawFd = 3;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct FrameDescriptor {
|
||||
seq: u64,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
plane_count: u32,
|
||||
strides: [u32; 4],
|
||||
offsets: [u32; 4],
|
||||
content_x: u32,
|
||||
content_y: u32,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
}
|
||||
|
||||
const DESCRIPTOR_BYTES: usize = std::mem::size_of::<FrameDescriptor>();
|
||||
const MAX_PLANES: usize = 4;
|
||||
|
||||
pub(crate) fn socketpair() -> std::io::Result<(OwnedFd, OwnedFd)> {
|
||||
let mut fds = [0 as RawFd; 2];
|
||||
// SAFETY: socketpair call; on success fds are owned by us.
|
||||
let result = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC, 0, fds.as_mut_ptr()) };
|
||||
if result != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: socketpair succeeded, so both fds are valid and not owned elsewhere.
|
||||
Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) })
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneSender {
|
||||
socket: OwnedFd,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, _events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
let fd = config.frame_socket_fd?;
|
||||
// SAFETY: the spawner dup2'd this fd for us; nothing else owns it.
|
||||
let socket = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
|
||||
// Restore CLOEXEC so subprocesses don't inherit the socket.
|
||||
// SAFETY: plain fcntl on an fd we own.
|
||||
if unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 {
|
||||
tracing::warn!("Failed to set CLOEXEC on the frame socket: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
Some(Self { socket })
|
||||
}
|
||||
|
||||
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
|
||||
let coded_size = &info.extra.coded_size;
|
||||
if coded_size.width <= 0 || coded_size.height <= 0 {
|
||||
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
|
||||
return None;
|
||||
}
|
||||
|
||||
let plane_count = (info.plane_count.max(0) as usize).min(info.planes.len());
|
||||
let mut fds = Vec::with_capacity(plane_count);
|
||||
let mut strides = [0u32; 4];
|
||||
let mut offsets = [0u32; 4];
|
||||
for (i, plane) in info.planes[..plane_count].iter().enumerate() {
|
||||
// SAFETY: CEF keeps the plane fds valid for the `on_accelerated_paint` callback.
|
||||
let fd = unsafe { BorrowedFd::borrow_raw(plane.fd) };
|
||||
match fd.try_clone_to_owned() {
|
||||
Ok(owned) => fds.push(owned),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to duplicate DMA-BUF plane fd: {e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
strides[i] = plane.stride;
|
||||
offsets[i] = plane.offset as u32;
|
||||
}
|
||||
|
||||
let content = crate::frames::import::ContentRect::try_from(info).unwrap_or_default();
|
||||
Some(StagedFrame {
|
||||
descriptor: FrameDescriptor {
|
||||
seq: 0,
|
||||
modifier: info.modifier,
|
||||
width: coded_size.width as u32,
|
||||
height: coded_size.height as u32,
|
||||
format: *info.format.as_ref() as u32,
|
||||
plane_count: plane_count as u32,
|
||||
strides,
|
||||
offsets,
|
||||
content_x: content.x,
|
||||
content_y: content.y,
|
||||
content_width: content.width,
|
||||
content_height: content.height,
|
||||
source_width: content.source_width,
|
||||
source_height: content.source_height,
|
||||
},
|
||||
fds,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> {
|
||||
let mut descriptor = frame.descriptor;
|
||||
descriptor.seq = seq;
|
||||
|
||||
debug_assert!(frame.fds.len() <= MAX_PLANES);
|
||||
let fd_bytes = frame.fds.len() * std::mem::size_of::<RawFd>();
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: &descriptor as *const FrameDescriptor as *mut libc::c_void,
|
||||
iov_len: DESCRIPTOR_BYTES,
|
||||
};
|
||||
let mut cmsg_buffer = [0u8; unsafe { libc::CMSG_SPACE((MAX_PLANES * std::mem::size_of::<RawFd>()) as u32) } as usize];
|
||||
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
|
||||
msg.msg_iov = &mut iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buffer.as_mut_ptr().cast();
|
||||
msg.msg_controllen = unsafe { libc::CMSG_SPACE(fd_bytes as u32) } as _;
|
||||
unsafe {
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
(*cmsg).cmsg_level = libc::SOL_SOCKET;
|
||||
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
|
||||
(*cmsg).cmsg_len = libc::CMSG_LEN(fd_bytes as u32) as _;
|
||||
let data = libc::CMSG_DATA(cmsg) as *mut RawFd;
|
||||
for (i, fd) in frame.fds.iter().enumerate() {
|
||||
data.add(i).write_unaligned(fd.as_raw_fd());
|
||||
}
|
||||
}
|
||||
loop {
|
||||
// SAFETY: msg and everything it points to are valid for the duration of the call.
|
||||
let sent = unsafe { libc::sendmsg(self.socket.as_raw_fd(), &msg, libc::MSG_NOSIGNAL) };
|
||||
if sent >= 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.kind() != std::io::ErrorKind::Interrupted {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StagedFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
fds: Vec<OwnedFd>,
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneReceiver {
|
||||
socket: OwnedFd,
|
||||
}
|
||||
|
||||
impl PlaneReceiver {
|
||||
pub(crate) fn new(socket: OwnedFd) -> Self {
|
||||
Self { socket }
|
||||
}
|
||||
|
||||
pub(crate) fn recv_blocking(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(false)
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(true)
|
||||
}
|
||||
|
||||
fn recv(&self, nonblocking: bool) -> std::io::Result<RecvResult> {
|
||||
let mut descriptor: FrameDescriptor = bytemuck::Zeroable::zeroed();
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: (&mut descriptor as *mut FrameDescriptor).cast(),
|
||||
iov_len: DESCRIPTOR_BYTES,
|
||||
};
|
||||
// SAFETY: pure size computation.
|
||||
let mut cmsg_buffer = [0u8; unsafe { libc::CMSG_SPACE((MAX_PLANES * std::mem::size_of::<RawFd>()) as u32) } as usize];
|
||||
|
||||
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
|
||||
msg.msg_iov = &mut iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buffer.as_mut_ptr().cast();
|
||||
msg.msg_controllen = cmsg_buffer.len() as _;
|
||||
|
||||
let flags = libc::MSG_CMSG_CLOEXEC | if nonblocking { libc::MSG_DONTWAIT } else { 0 };
|
||||
let received = loop {
|
||||
// SAFETY: msg and everything it points to are valid for the duration of the call.
|
||||
let received = unsafe { libc::recvmsg(self.socket.as_raw_fd(), &mut msg, flags) };
|
||||
if received >= 0 {
|
||||
break received;
|
||||
}
|
||||
let error = std::io::Error::last_os_error();
|
||||
match error.kind() {
|
||||
std::io::ErrorKind::Interrupted => continue,
|
||||
std::io::ErrorKind::WouldBlock => return Ok(RecvResult::WouldBlock),
|
||||
_ => return Err(error),
|
||||
}
|
||||
};
|
||||
|
||||
let mut fds = Vec::new();
|
||||
// SAFETY: traversing the cmsgs recvmsg just filled; SCM_RIGHTS payload is fds now owned by us.
|
||||
unsafe {
|
||||
let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
while !cmsg.is_null() {
|
||||
if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS {
|
||||
let data = libc::CMSG_DATA(cmsg) as *const RawFd;
|
||||
let count = ((*cmsg).cmsg_len as usize - libc::CMSG_LEN(0) as usize) / std::mem::size_of::<RawFd>();
|
||||
for i in 0..count {
|
||||
fds.push(OwnedFd::from_raw_fd(data.add(i).read_unaligned()));
|
||||
}
|
||||
}
|
||||
cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
|
||||
}
|
||||
}
|
||||
|
||||
if received == 0 {
|
||||
return Ok(RecvResult::Closed);
|
||||
}
|
||||
if received as usize != DESCRIPTOR_BYTES || (msg.msg_flags & libc::MSG_CTRUNC) != 0 {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"malformed frame message: {received} bytes, flags {:#x} ({} fds)",
|
||||
msg.msg_flags,
|
||||
fds.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(RecvResult::Frame(WireFrame { descriptor, fds }))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WireFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
fds: Vec<OwnedFd>,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.descriptor.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
let descriptor = self.descriptor;
|
||||
let format = super::wire_color_type(descriptor.format)?;
|
||||
let plane_count = (descriptor.plane_count as usize).min(self.fds.len());
|
||||
let content = crate::frames::import::ContentRect {
|
||||
x: descriptor.content_x,
|
||||
y: descriptor.content_y,
|
||||
width: descriptor.content_width,
|
||||
height: descriptor.content_height,
|
||||
source_width: descriptor.source_width,
|
||||
source_height: descriptor.source_height,
|
||||
};
|
||||
let importer = crate::frames::import::dmabuf::DmaBufImporter::from_parts(
|
||||
self.fds,
|
||||
descriptor.strides[..plane_count].to_vec(),
|
||||
descriptor.offsets[..plane_count].to_vec(),
|
||||
descriptor.modifier,
|
||||
descriptor.width,
|
||||
descriptor.height,
|
||||
format,
|
||||
);
|
||||
surface.import_texture(importer, content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::ffi::CString;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use mach2::kern_return::KERN_SUCCESS;
|
||||
use mach2::message::{
|
||||
MACH_MSG_PORT_DESCRIPTOR, MACH_MSG_SUCCESS, MACH_MSG_TIMEOUT_NONE, MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND, MACH_MSGH_BITS_COMPLEX, MACH_RCV_MSG, MACH_RCV_TIMED_OUT, MACH_RCV_TIMEOUT,
|
||||
MACH_SEND_MSG, mach_msg, mach_msg_body_t, mach_msg_header_t,
|
||||
};
|
||||
use mach2::port::{MACH_PORT_NULL, mach_port_t};
|
||||
use mach2::traps::mach_task_self;
|
||||
use objc2_io_surface::IOSurfaceRef;
|
||||
|
||||
use super::RecvResult;
|
||||
use crate::frames::surface::FrameSurface;
|
||||
use crate::remote::HostConfig;
|
||||
use crate::remote::messages::EventMessage;
|
||||
|
||||
// From libSystem, stable since 10.0, not coverd by mach2
|
||||
unsafe extern "C" {
|
||||
static bootstrap_port: mach_port_t;
|
||||
fn bootstrap_check_in(bp: mach_port_t, service_name: *const std::ffi::c_char, sp: *mut mach_port_t) -> mach2::kern_return::kern_return_t;
|
||||
fn bootstrap_look_up(bp: mach_port_t, service_name: *const std::ffi::c_char, sp: *mut mach_port_t) -> mach2::kern_return::kern_return_t;
|
||||
}
|
||||
|
||||
// `mach_msg_port_descriptor_t` kernel ABI
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct PortDescriptor {
|
||||
name: mach_port_t,
|
||||
pad1: u32,
|
||||
pad2: u16,
|
||||
disposition: u8,
|
||||
descriptor_type: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct FrameDescriptor {
|
||||
seq: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content_x: u32,
|
||||
content_y: u32,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct FrameMessage {
|
||||
header: mach_msg_header_t,
|
||||
body: mach_msg_body_t,
|
||||
surface: PortDescriptor,
|
||||
descriptor: FrameDescriptor,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct FrameMessageBuffer {
|
||||
message: FrameMessage,
|
||||
trailer: [u8; 64],
|
||||
}
|
||||
|
||||
struct SendRight(mach_port_t);
|
||||
|
||||
// SAFETY: mach port names are task-wide; rights may be used from any thread.
|
||||
unsafe impl Send for SendRight {}
|
||||
unsafe impl Sync for SendRight {}
|
||||
|
||||
impl Drop for SendRight {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own one reference on this send right.
|
||||
unsafe { mach2::mach_port::mach_port_deallocate(mach_task_self(), self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_service(name: &str) -> std::io::Result<mach_port_t> {
|
||||
let c_name = CString::new(name).map_err(std::io::Error::other)?;
|
||||
let mut port: mach_port_t = MACH_PORT_NULL;
|
||||
// SAFETY: plain bootstrap call; on success we own the service's receive right.
|
||||
let result = unsafe { bootstrap_check_in(bootstrap_port, c_name.as_ptr(), &mut port) };
|
||||
if result != KERN_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("bootstrap_check_in failed: {result:#x}")));
|
||||
}
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
fn look_up_service(name: &str) -> std::io::Result<SendRight> {
|
||||
let c_name = CString::new(name).map_err(std::io::Error::other)?;
|
||||
let mut port: mach_port_t = MACH_PORT_NULL;
|
||||
// SAFETY: plain bootstrap call; on success we own a send right.
|
||||
let result = unsafe { bootstrap_look_up(bootstrap_port, c_name.as_ptr(), &mut port) };
|
||||
if result != KERN_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("bootstrap_look_up failed: {result:#x}")));
|
||||
}
|
||||
Ok(SendRight(port))
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneSender {
|
||||
service: SendRight,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, _events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
let name = config.frame_service.as_deref()?;
|
||||
match look_up_service(name) {
|
||||
Ok(service) => Some(Self { service }),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to look up the accelerated frame service, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
|
||||
let coded_size = &info.extra.coded_size;
|
||||
if coded_size.width <= 0 || coded_size.height <= 0 {
|
||||
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(surface) = std::ptr::NonNull::new(info.shared_texture_io_surface.cast::<IOSurfaceRef>()) else {
|
||||
tracing::error!("Accelerated paint delivered a null IOSurface");
|
||||
return None;
|
||||
};
|
||||
|
||||
// SAFETY: CEF keeps the surface valid for the `on_accelerated_paint` callback.
|
||||
let port = unsafe { surface.as_ref() }.create_mach_port();
|
||||
if port == MACH_PORT_NULL {
|
||||
tracing::error!("Failed to wrap the IOSurface in a mach port");
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = crate::frames::import::ContentRect::try_from(info).unwrap_or_default();
|
||||
Some(StagedFrame {
|
||||
descriptor: FrameDescriptor {
|
||||
seq: 0,
|
||||
width: coded_size.width as u32,
|
||||
height: coded_size.height as u32,
|
||||
format: *info.format.as_ref() as u32,
|
||||
content_x: content.x,
|
||||
content_y: content.y,
|
||||
content_width: content.width,
|
||||
content_height: content.height,
|
||||
source_width: content.source_width,
|
||||
source_height: content.source_height,
|
||||
_pad: 0,
|
||||
},
|
||||
surface: SendRight(port),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> {
|
||||
let mut descriptor = frame.descriptor;
|
||||
descriptor.seq = seq;
|
||||
let mut message = FrameMessage {
|
||||
header: mach_msg_header_t {
|
||||
msgh_bits: MACH_MSG_TYPE_COPY_SEND | MACH_MSGH_BITS_COMPLEX,
|
||||
msgh_size: std::mem::size_of::<FrameMessage>() as u32,
|
||||
msgh_remote_port: self.service.0,
|
||||
msgh_local_port: MACH_PORT_NULL,
|
||||
msgh_voucher_port: MACH_PORT_NULL,
|
||||
msgh_id: 0,
|
||||
},
|
||||
body: mach_msg_body_t { msgh_descriptor_count: 1 },
|
||||
surface: PortDescriptor {
|
||||
name: frame.surface.0,
|
||||
pad1: 0,
|
||||
pad2: 0,
|
||||
disposition: MACH_MSG_TYPE_MOVE_SEND as u8,
|
||||
descriptor_type: MACH_MSG_PORT_DESCRIPTOR as u8,
|
||||
},
|
||||
descriptor,
|
||||
};
|
||||
|
||||
// SAFETY: message is a well-formed complex message of the declared size.
|
||||
let result = unsafe {
|
||||
mach_msg(
|
||||
&mut message.header,
|
||||
MACH_SEND_MSG,
|
||||
std::mem::size_of::<FrameMessage>() as u32,
|
||||
0,
|
||||
MACH_PORT_NULL,
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_PORT_NULL,
|
||||
)
|
||||
};
|
||||
if result != MACH_MSG_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("mach_msg send failed: {result:#x}")));
|
||||
}
|
||||
|
||||
// Kernel took ownership of the surface and moves it to the receiver. We must not drop.
|
||||
std::mem::forget(frame.surface);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StagedFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
surface: SendRight,
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneReceiver {
|
||||
port: mach_port_t,
|
||||
}
|
||||
|
||||
impl PlaneReceiver {
|
||||
pub(crate) fn new(port: mach_port_t) -> Self {
|
||||
Self { port }
|
||||
}
|
||||
|
||||
pub(crate) fn recv_blocking(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(false)
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(true)
|
||||
}
|
||||
|
||||
fn recv(&self, nonblocking: bool) -> std::io::Result<RecvResult> {
|
||||
// SAFETY: zeroed is a valid representation for these plain-data structs.
|
||||
let mut buffer: FrameMessageBuffer = unsafe { std::mem::zeroed() };
|
||||
let (options, timeout) = if nonblocking {
|
||||
(MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0)
|
||||
} else {
|
||||
(MACH_RCV_MSG, MACH_MSG_TIMEOUT_NONE)
|
||||
};
|
||||
|
||||
// SAFETY: the buffer is large enough for the message plus the basic trailer.
|
||||
let result = unsafe {
|
||||
mach_msg(
|
||||
&mut buffer.message.header,
|
||||
options,
|
||||
0,
|
||||
std::mem::size_of::<FrameMessageBuffer>() as u32,
|
||||
self.port,
|
||||
timeout,
|
||||
MACH_PORT_NULL,
|
||||
)
|
||||
};
|
||||
if result == MACH_RCV_TIMED_OUT {
|
||||
return Ok(RecvResult::WouldBlock);
|
||||
}
|
||||
if result != MACH_MSG_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("mach_msg receive failed: {result:#x}")));
|
||||
}
|
||||
|
||||
let received_complex = buffer.message.header.msgh_bits & MACH_MSGH_BITS_COMPLEX != 0;
|
||||
let descriptor_count = if received_complex { buffer.message.body.msgh_descriptor_count } else { 0 };
|
||||
let surface = (descriptor_count == 1 && buffer.message.surface.descriptor_type == MACH_MSG_PORT_DESCRIPTOR as u8).then(|| SendRight(buffer.message.surface.name));
|
||||
|
||||
if buffer.message.header.msgh_size as usize != std::mem::size_of::<FrameMessage>() {
|
||||
return Err(std::io::Error::other(format!("malformed frame message: {} bytes", buffer.message.header.msgh_size)));
|
||||
}
|
||||
let Some(surface) = surface else {
|
||||
return Err(std::io::Error::other("frame message carried no surface port"));
|
||||
};
|
||||
|
||||
Ok(RecvResult::Frame(WireFrame {
|
||||
descriptor: buffer.message.descriptor,
|
||||
surface,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WireFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
surface: SendRight,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.descriptor.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
let WireFrame { descriptor, surface: port } = self;
|
||||
let format = super::wire_color_type(descriptor.format)?;
|
||||
|
||||
// Lookup takes its own reference on the surface, port can be dropped.
|
||||
let io_surface = IOSurfaceRef::lookup_from_mach_port(port.0);
|
||||
drop(port);
|
||||
|
||||
let Some(io_surface) = io_surface else {
|
||||
tracing::error!("Failed to look up the IOSurface for frame {}", descriptor.seq);
|
||||
return None;
|
||||
};
|
||||
let io_surface_ref: &IOSurfaceRef = &io_surface;
|
||||
|
||||
let content = crate::frames::import::ContentRect {
|
||||
x: descriptor.content_x,
|
||||
y: descriptor.content_y,
|
||||
width: descriptor.content_width,
|
||||
height: descriptor.content_height,
|
||||
source_width: descriptor.source_width,
|
||||
source_height: descriptor.source_height,
|
||||
};
|
||||
let importer = crate::frames::import::iosurface::IOSurfaceImporter::from_parts(io_surface_ref as *const _ as *mut std::os::raw::c_void, descriptor.width, descriptor.height, format);
|
||||
surface.import_texture(importer, content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use windows::Win32::Foundation::{CloseHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE};
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE};
|
||||
|
||||
use crate::frames::import::ContentRect;
|
||||
use crate::frames::surface::FrameSurface;
|
||||
use crate::remote::HostConfig;
|
||||
use crate::remote::messages::EventMessage;
|
||||
|
||||
struct MainProcess(HANDLE);
|
||||
|
||||
// SAFETY: process handles may be used and closed from any thread.
|
||||
unsafe impl Send for MainProcess {}
|
||||
unsafe impl Sync for MainProcess {}
|
||||
|
||||
impl MainProcess {
|
||||
fn open(pid: u32) -> windows::core::Result<Self> {
|
||||
// SAFETY: plain OpenProcess call; on success the handle is ours to close.
|
||||
unsafe { OpenProcess(PROCESS_DUP_HANDLE, false, pid).map(Self) }
|
||||
}
|
||||
|
||||
fn duplicate_into(&self, handle: HANDLE) -> windows::core::Result<u64> {
|
||||
let mut target = HANDLE::default();
|
||||
// SAFETY: both process handles are valid; `target` receives the duplicate.
|
||||
unsafe { DuplicateHandle(GetCurrentProcess(), handle, self.0, &mut target, 0, false, DUPLICATE_SAME_ACCESS)? };
|
||||
Ok(target.0 as u64)
|
||||
}
|
||||
|
||||
fn close_in_main(&self, handle: u64) {
|
||||
let mut reclaimed = HANDLE::default();
|
||||
// SAFETY: `handle` came from `duplicate_into` and is valid.
|
||||
unsafe {
|
||||
if let Err(e) = DuplicateHandle(self.0, HANDLE(handle as _), GetCurrentProcess(), &mut reclaimed, 0, false, DUPLICATE_CLOSE_SOURCE) {
|
||||
tracing::warn!("Failed to reclaim a frame handle from the main process: {e}");
|
||||
}
|
||||
if !reclaimed.is_invalid() {
|
||||
let _ = CloseHandle(reclaimed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MainProcess {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own the process handle.
|
||||
unsafe {
|
||||
let _ = CloseHandle(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneSender {
|
||||
main: Arc<MainProcess>,
|
||||
events: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
match MainProcess::open(config.main_pid) {
|
||||
Ok(main) => Some(Self { main: Arc::new(main), events }),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to open the main process for handle duplication, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
|
||||
let coded_size = &info.extra.coded_size;
|
||||
if coded_size.width <= 0 || coded_size.height <= 0 {
|
||||
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
|
||||
return None;
|
||||
}
|
||||
|
||||
let handle = match self.main.duplicate_into(HANDLE(info.shared_texture_handle)) {
|
||||
Ok(handle) => handle,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to duplicate the shared texture handle into the main process: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(StagedFrame {
|
||||
handle: HandleInMain { handle, main: self.main.clone() },
|
||||
width: coded_size.width as u32,
|
||||
height: coded_size.height as u32,
|
||||
format: *info.format.as_ref() as u32,
|
||||
content: ContentRect::try_from(info).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> {
|
||||
let message = EventMessage::AcceleratedFrame {
|
||||
seq,
|
||||
handle: frame.handle.handle,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
format: frame.format,
|
||||
content: frame.content,
|
||||
};
|
||||
let sender = self.events.lock().map_err(|_| std::io::Error::other("the host message sender lock is poisoned"))?;
|
||||
match sender.send(message) {
|
||||
Ok(()) => {
|
||||
// Dropping the handle would reclaim it. We must not drop.
|
||||
std::mem::forget(frame.handle);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(std::io::Error::other(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StagedFrame {
|
||||
handle: HandleInMain,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content: Option<ContentRect>,
|
||||
}
|
||||
|
||||
struct HandleInMain {
|
||||
handle: u64,
|
||||
main: Arc<MainProcess>,
|
||||
}
|
||||
|
||||
impl Drop for HandleInMain {
|
||||
fn drop(&mut self) {
|
||||
self.main.close_in_main(self.handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WireFrame {
|
||||
seq: u64,
|
||||
handle: ReceivedHandle,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content: Option<ContentRect>,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn new(seq: u64, handle: u64, width: u32, height: u32, format: u32, content: Option<ContentRect>) -> Self {
|
||||
Self {
|
||||
seq,
|
||||
handle: ReceivedHandle(handle),
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
let format = super::wire_color_type(self.format)?;
|
||||
let content = self.content.unwrap_or_default();
|
||||
surface.import_texture(crate::frames::import::d3d11::D3D11Importer::from_parts(self.handle.0, self.width, self.height, format), content)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReceivedHandle(u64);
|
||||
|
||||
impl Drop for ReceivedHandle {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: the host duplicated this handle into our process for us to own.
|
||||
if let Err(e) = unsafe { CloseHandle(HANDLE(self.0 as _)) } {
|
||||
tracing::warn!("Failed to close a remote frame handle: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::FrameSurface;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::plane;
|
||||
use super::sink::FrameSink;
|
||||
use crate::UiEvent;
|
||||
use crate::events::EventQueue;
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
|
||||
pub(crate) enum PendingFrame {
|
||||
Software {
|
||||
seq: u64,
|
||||
segment: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
},
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
Accelerated(plane::WireFrame),
|
||||
}
|
||||
|
||||
impl PendingFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
match self {
|
||||
PendingFrame::Software { seq, .. } => *seq,
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
PendingFrame::Accelerated(frame) => frame.seq(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SegmentTable(Vec<Option<IpcSharedMemory>>);
|
||||
|
||||
impl SegmentTable {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn advertise(&mut self, index: u32, shm: IpcSharedMemory) {
|
||||
let index = index as usize;
|
||||
if self.0.len() <= index {
|
||||
self.0.resize_with(index + 1, || None);
|
||||
}
|
||||
self.0[index] = Some(shm);
|
||||
}
|
||||
|
||||
fn frame(&self, seq: u64, segment: u32, width: u32, height: u32) -> Option<&[u8]> {
|
||||
let frame_bytes = width as usize * height as usize * 4;
|
||||
match self.0.get(segment as usize).and_then(Option::as_ref) {
|
||||
Some(shm) if shm.len() >= frame_bytes => Some(&shm[..frame_bytes]),
|
||||
Some(shm) => {
|
||||
tracing::error!("Frame {seq} needs {frame_bytes} bytes but segment {segment} holds {}", shm.len());
|
||||
None
|
||||
}
|
||||
None => {
|
||||
tracing::error!("Frame {seq} references unadvertised segment {segment}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameConsumer {
|
||||
surface: FrameSurface,
|
||||
events: EventQueue,
|
||||
sender: IpcSender<HostControlMessage>,
|
||||
sink: Arc<FrameSink>,
|
||||
}
|
||||
|
||||
impl FrameConsumer {
|
||||
pub(crate) fn new(surface: FrameSurface, events: EventQueue, sender: IpcSender<HostControlMessage>) -> Self {
|
||||
Self {
|
||||
surface,
|
||||
events,
|
||||
sender,
|
||||
sink: Arc::new(FrameSink::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver(&self, seq: u64, install: impl FnOnce(&FrameSurface) -> Option<wgpu::Texture>) {
|
||||
self.sink.deliver(&self.sender, seq, || match install(&self.surface) {
|
||||
Some(texture) => {
|
||||
self.events.send(UiEvent::Frame(texture));
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn deliver_pending(&self, frame: PendingFrame, segments: &SegmentTable) {
|
||||
match frame {
|
||||
PendingFrame::Software { seq, segment, width, height } => {
|
||||
self.deliver(seq, |surface| {
|
||||
segments.frame(seq, segment, width, height).and_then(|pixels| surface.upload_buffer(pixels, width, height))
|
||||
});
|
||||
}
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
PendingFrame::Accelerated(frame) => self.deliver_accelerated(frame),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn deliver_accelerated(&self, frame: plane::WireFrame) {
|
||||
let seq = frame.seq();
|
||||
self.deliver(seq, |surface| frame.import(surface));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
pub(crate) fn plane_receiver_loop(receiver: plane::PlaneReceiver, consumer: FrameConsumer) {
|
||||
loop {
|
||||
let mut frame = loop {
|
||||
match receiver.recv_blocking() {
|
||||
Ok(plane::RecvResult::Frame(frame)) => break frame,
|
||||
Ok(plane::RecvResult::WouldBlock) => continue,
|
||||
Ok(plane::RecvResult::Closed) => return,
|
||||
Err(e) => {
|
||||
tracing::error!("Accelerated frame plane failed: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Drain any newer frames that have arrived since the blocking receive
|
||||
while let Ok(plane::RecvResult::Frame(newer)) = receiver.try_recv() {
|
||||
frame = newer;
|
||||
}
|
||||
consumer.deliver_accelerated(frame);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Resampler {
|
||||
device: wgpu::Device,
|
||||
pipeline: Arc<OnceLock<Pipeline>>,
|
||||
}
|
||||
|
||||
struct Pipeline {
|
||||
format: wgpu::TextureFormat,
|
||||
sampler: wgpu::Sampler,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl Resampler {
|
||||
pub(super) fn new(device: wgpu::Device) -> Self {
|
||||
Self {
|
||||
device,
|
||||
pipeline: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn encode(&self, encoder: &mut wgpu::CommandEncoder, source: &wgpu::Texture, content_origin: wgpu::Origin3d, content_size: wgpu::Extent3d, target: &wgpu::Texture) {
|
||||
let pipeline = self.pipeline.get_or_init(|| Pipeline::new(&self.device, target.format()));
|
||||
debug_assert_eq!(pipeline.format, target.format());
|
||||
|
||||
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("CEF Resample Bind Group"),
|
||||
layout: &pipeline.layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&source.create_view(&Default::default())),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("CEF Resample Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &target.create_view(&Default::default()),
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_pipeline(&pipeline.pipeline);
|
||||
pass.set_immediates(
|
||||
0,
|
||||
bytemuck::bytes_of(&Immediates {
|
||||
content_origin: [content_origin.x as f32, content_origin.y as f32],
|
||||
content_size: [content_size.width as f32, content_size.height as f32],
|
||||
}),
|
||||
);
|
||||
pass.set_bind_group(0, &bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("CEF Resample Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("CEF Resample Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("CEF Resample Pipeline Layout"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
immediate_size: std::mem::size_of::<Immediates>() as u32,
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::include_wgsl!("resample.wgsl"));
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("CEF Resample Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
Self { format, sampler, layout, pipeline }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Immediates {
|
||||
content_origin: [f32; 2],
|
||||
content_size: [f32; 2],
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// =============
|
||||
// VERTEX SHADER
|
||||
// =============
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let pos = array(
|
||||
vec2f(-1.0, -1.0),
|
||||
vec2f(3.0, -1.0),
|
||||
vec2f(-1.0, 3.0),
|
||||
);
|
||||
let xy = pos[vertex_index];
|
||||
out.clip_position = vec4f(xy, 0.0, 1.0);
|
||||
let coords = xy / 2. + 0.5;
|
||||
out.tex_coords = vec2f(coords.x, 1. - coords.y);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===============
|
||||
// FRAGMENT SHADER
|
||||
// ===============
|
||||
|
||||
struct Immediates {
|
||||
content_origin: vec2<f32>,
|
||||
content_size: vec2<f32>,
|
||||
};
|
||||
|
||||
var<immediate> immediates: Immediates;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_frame: texture_2d<f32>;
|
||||
@group(0) @binding(1)
|
||||
var s_frame: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let sample_pos = in.tex_coords * immediates.content_size;
|
||||
let nearest = floor(sample_pos - 0.5) + 0.5;
|
||||
let t = sample_pos - nearest;
|
||||
|
||||
// Catmull-Rom spline interpolation based sampeling
|
||||
// See https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1
|
||||
|
||||
let weight_before = t * (-0.5 + t * (1.0 - 0.5 * t));
|
||||
let weight_nearest = 1.0 + t * t * (-2.5 + 1.5 * t);
|
||||
let weight_next = t * (0.5 + t * (2.0 - 1.5 * t));
|
||||
let weight_after = t * t * (-0.5 + 0.5 * t);
|
||||
|
||||
let weight_middle = weight_nearest + weight_next;
|
||||
let middle = nearest + weight_next / weight_middle;
|
||||
|
||||
let frame_size = vec2<f32>(textureDimensions(t_frame));
|
||||
let content_min = vec2<f32>(0.5);
|
||||
let content_max = immediates.content_size - 0.5;
|
||||
let uv_before = (immediates.content_origin + clamp(nearest - 1.0, content_min, content_max)) / frame_size;
|
||||
let uv_middle = (immediates.content_origin + clamp(middle, content_min, content_max)) / frame_size;
|
||||
let uv_after = (immediates.content_origin + clamp(nearest + 2.0, content_min, content_max)) / frame_size;
|
||||
|
||||
var color = vec4<f32>(0.0);
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_before.y), 0.0) * weight_before.x * weight_before.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_before.y), 0.0) * weight_middle.x * weight_before.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_before.y), 0.0) * weight_after.x * weight_before.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_middle.y), 0.0) * weight_before.x * weight_middle.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_middle.y), 0.0) * weight_middle.x * weight_middle.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_middle.y), 0.0) * weight_after.x * weight_middle.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_after.y), 0.0) * weight_before.x * weight_after.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_after.y), 0.0) * weight_middle.x * weight_after.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_after.y), 0.0) * weight_after.x * weight_after.y;
|
||||
|
||||
return clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Condvar, Mutex, PoisonError};
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::consts::FRAMES_IN_FLIGHT_LIMIT;
|
||||
|
||||
pub(crate) struct SequenceState {
|
||||
last_sent: AtomicU64,
|
||||
last_acked: AtomicU64,
|
||||
ack_lock: Mutex<()>,
|
||||
ack_signal: Condvar,
|
||||
}
|
||||
|
||||
impl SequenceState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
last_sent: AtomicU64::new(0),
|
||||
last_acked: AtomicU64::new(0),
|
||||
ack_lock: Mutex::new(()),
|
||||
ack_signal: Condvar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn claim(self: &Arc<Self>) -> Option<FrameSequenceClaim> {
|
||||
let last_sent = self.last_sent.load(Ordering::Relaxed);
|
||||
let last_acked = self.last_acked.load(Ordering::Relaxed);
|
||||
if last_sent.saturating_sub(last_acked) >= FRAMES_IN_FLIGHT_LIMIT {
|
||||
return None;
|
||||
}
|
||||
let seq = last_sent + 1;
|
||||
self.last_sent.store(seq, Ordering::Relaxed);
|
||||
Some(FrameSequenceClaim {
|
||||
seq,
|
||||
sequence: self.clone(),
|
||||
commited: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ack(&self, seq: u64) {
|
||||
self.last_acked.fetch_max(seq, Ordering::Relaxed);
|
||||
drop(self.ack_lock.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
self.ack_signal.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FrameSequenceClaim {
|
||||
seq: u64,
|
||||
commited: bool,
|
||||
sequence: Arc<SequenceState>,
|
||||
}
|
||||
|
||||
impl FrameSequenceClaim {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.seq
|
||||
}
|
||||
|
||||
pub(crate) fn commit(mut self) {
|
||||
self.commited = true;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn wait_for_ack(&self) -> bool {
|
||||
let deadline = Instant::now() + crate::consts::FRAME_ACK_TIMEOUT;
|
||||
let mut guard = self.sequence.ack_lock.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
loop {
|
||||
if self.sequence.last_acked.load(Ordering::Relaxed) >= self.seq {
|
||||
return true;
|
||||
}
|
||||
let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
|
||||
return false;
|
||||
};
|
||||
guard = self.sequence.ack_signal.wait_timeout(guard, remaining).unwrap_or_else(PoisonError::into_inner).0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrameSequenceClaim {
|
||||
fn drop(&mut self) {
|
||||
// Roll back the claim if it was never committed to free the sequence number
|
||||
if !self.commited {
|
||||
let _ = self.sequence.last_sent.compare_exchange(self.seq, self.seq - 1, Ordering::Relaxed, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
|
||||
pub(super) struct FrameSink {
|
||||
state: Mutex<FrameSinkState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FrameSinkState {
|
||||
newest_installed: u64,
|
||||
last_acked: u64,
|
||||
}
|
||||
|
||||
impl FrameSink {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
state: Mutex::new(FrameSinkState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn deliver(&self, sender: &IpcSender<HostControlMessage>, seq: u64, install: impl FnOnce() -> bool) {
|
||||
let Ok(mut state) = self.state.lock() else {
|
||||
tracing::error!("Failed to lock the frame sink");
|
||||
return;
|
||||
};
|
||||
|
||||
if seq > 1 && seq - 1 > state.last_acked {
|
||||
if let Err(e) = sender.send(HostControlMessage::FrameAck { seq: seq - 1 }) {
|
||||
tracing::debug!("Failed to ack superseded frames to CEF host: {e}");
|
||||
}
|
||||
state.last_acked = seq - 1;
|
||||
}
|
||||
if seq > state.newest_installed && install() {
|
||||
state.newest_installed = seq;
|
||||
}
|
||||
if seq > state.last_acked {
|
||||
if let Err(e) = sender.send(HostControlMessage::FrameAck { seq }) {
|
||||
tracing::debug!("Failed to ack frame to CEF host: {e}");
|
||||
}
|
||||
state.last_acked = seq;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::plane;
|
||||
use super::sequence::{FrameSequenceClaim, SequenceState};
|
||||
use crate::consts::{FRAME_SEGMENT_GRANULARITY, FRAME_SEGMENT_POOL_SIZE};
|
||||
use crate::remote::messages::EventMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameStreamer(Arc<StreamerInner>);
|
||||
|
||||
struct StreamerInner {
|
||||
events: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
sequence: Arc<SequenceState>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane: Option<plane::PlaneSender>,
|
||||
staged: Mutex<Staged>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Staged {
|
||||
segments: Vec<IpcSharedMemory>,
|
||||
pending_adverts: Vec<(u32, IpcSharedMemory)>,
|
||||
buffer: Option<StagedBuffer>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
accelerated: Option<(FrameSequenceClaim, plane::StagedFrame)>,
|
||||
}
|
||||
|
||||
struct StagedBuffer {
|
||||
claim: FrameSequenceClaim,
|
||||
segment: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl FrameStreamer {
|
||||
pub(crate) fn new(events: Arc<Mutex<IpcSender<EventMessage>>>, sequence: Arc<SequenceState>, #[cfg(feature = "accelerated_paint")] plane: Option<plane::PlaneSender>) -> Self {
|
||||
Self(Arc::new(StreamerInner {
|
||||
events,
|
||||
sequence,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane,
|
||||
staged: Mutex::new(Staged::default()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn stage_buffer(&self, buffer: &[u8], width: u32, height: u32) {
|
||||
debug_assert_eq!(buffer.len(), width as usize * height as usize * 4);
|
||||
if buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(claim) = self.0.sequence.claim() else {
|
||||
return;
|
||||
};
|
||||
let segment = (claim.seq() % FRAME_SEGMENT_POOL_SIZE) as u32;
|
||||
|
||||
let Ok(mut staged) = self.0.staged.lock() else {
|
||||
tracing::error!("Failed to lock the frame staging state");
|
||||
return;
|
||||
};
|
||||
let staged = &mut *staged;
|
||||
if staged.segments.len() < FRAME_SEGMENT_POOL_SIZE as usize {
|
||||
staged.segments.resize_with(FRAME_SEGMENT_POOL_SIZE as usize, || IpcSharedMemory::from_bytes(&[]));
|
||||
}
|
||||
|
||||
let backing = &mut staged.segments[segment as usize];
|
||||
if backing.len() < buffer.len() {
|
||||
let capacity = buffer.len().next_multiple_of(FRAME_SEGMENT_GRANULARITY);
|
||||
*backing = IpcSharedMemory::from_byte(0, capacity);
|
||||
staged.pending_adverts.push((segment, backing.clone()));
|
||||
}
|
||||
|
||||
unsafe { backing.deref_mut()[..buffer.len()].copy_from_slice(buffer) };
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if !staged.pending_adverts.iter().any(|(index, _)| *index == segment) {
|
||||
staged.pending_adverts.push((segment, backing.clone()));
|
||||
}
|
||||
|
||||
staged.buffer = Some(StagedBuffer { claim, segment, width, height });
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn stage_texture(&self, info: &cef::AcceleratedPaintInfo) {
|
||||
let Some(plane) = &self.0.plane else {
|
||||
tracing::error!("Accelerated paint delivered without a frame plane");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(claim) = self.0.sequence.claim() else {
|
||||
return;
|
||||
};
|
||||
let Some(frame) = plane.stage(info) else {
|
||||
return;
|
||||
};
|
||||
let Ok(mut staged) = self.0.staged.lock() else {
|
||||
tracing::error!("Failed to lock the frame staging state");
|
||||
return;
|
||||
};
|
||||
staged.accelerated = Some((claim, frame));
|
||||
}
|
||||
|
||||
pub(crate) fn publish(&self) {
|
||||
let Ok(mut staged) = self.0.staged.lock() else {
|
||||
tracing::error!("Failed to lock the frame staging state");
|
||||
return;
|
||||
};
|
||||
let adverts = std::mem::take(&mut staged.pending_adverts);
|
||||
let software = staged.buffer.take();
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
let accelerated = staged.accelerated.take();
|
||||
drop(staged);
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
if let Some((claim, frame)) = accelerated
|
||||
&& let Some(plane) = &self.0.plane
|
||||
{
|
||||
match plane.send(claim.seq(), frame) {
|
||||
Ok(()) => {
|
||||
if !claim.wait_for_ack() {
|
||||
tracing::warn!("Accelerated frame {} was not acked", claim.seq());
|
||||
}
|
||||
claim.commit();
|
||||
}
|
||||
Err(e) => tracing::debug!("Failed to send accelerated frame to main process: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
if adverts.is_empty() && software.is_none() {
|
||||
return;
|
||||
}
|
||||
let Ok(sender) = self.0.events.lock() else {
|
||||
tracing::error!("Failed to lock host message sender");
|
||||
return;
|
||||
};
|
||||
for (index, shm) in adverts {
|
||||
if let Err(e) = sender.send(EventMessage::AdvertiseFrameSegment { index, shm }) {
|
||||
tracing::debug!("Failed to send frame segment to main process: {e}");
|
||||
}
|
||||
}
|
||||
if let Some(StagedBuffer { claim, segment, width, height }) = software {
|
||||
match sender.send(EventMessage::SoftwareFrame {
|
||||
seq: claim.seq(),
|
||||
segment,
|
||||
width,
|
||||
height,
|
||||
}) {
|
||||
Ok(()) => claim.commit(),
|
||||
Err(e) => tracing::debug!("Failed to send frame to main process: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::import::ContentMapping;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::resample::Resampler;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameSurface {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu_sync::Queue,
|
||||
slot: Arc<Mutex<Option<wgpu::Texture>>>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
resampler: Resampler,
|
||||
}
|
||||
|
||||
impl FrameSurface {
|
||||
pub(crate) fn new(device: wgpu::Device, queue: wgpu_sync::Queue) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
resampler: Resampler::new(device.clone()),
|
||||
device,
|
||||
queue,
|
||||
slot: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn upload_buffer(&self, buffer: &[u8], width: u32, height: u32) -> Option<wgpu::Texture> {
|
||||
debug_assert_eq!(buffer.len(), width as usize * height as usize * 4);
|
||||
|
||||
let Ok(mut slot) = self.slot.lock() else {
|
||||
tracing::error!("Failed to lock the frame surface");
|
||||
return None;
|
||||
};
|
||||
|
||||
if buffer.chunks_exact(4).take(width as usize).all(|pixel| pixel[3] == 0) {
|
||||
tracing::debug!("Skipping fully transparent frame");
|
||||
return None;
|
||||
}
|
||||
|
||||
if slot.as_ref().is_none_or(|texture| texture.width() != width || texture.height() != height) {
|
||||
*slot = Some(self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("CEF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
}));
|
||||
}
|
||||
let texture = slot.as_ref()?;
|
||||
|
||||
self.queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
buffer,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width),
|
||||
rows_per_image: None,
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
Some(texture.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn import_texture(&self, importer: impl crate::frames::import::TextureImporter, content_rect: crate::frames::import::ContentRect) -> Option<wgpu::Texture> {
|
||||
let imported = match importer.import_to_wgpu(&self.device) {
|
||||
Ok(texture) => texture,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to import remote accelerated frame: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("CEF Frame Copy Encoder"),
|
||||
});
|
||||
let output = match content_rect.mapping(imported.width(), imported.height()) {
|
||||
ContentMapping::Identity => {
|
||||
let output = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("CEF Imported Frame Copy"),
|
||||
size: imported.size(),
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: imported.format(),
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &imported,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &output,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
imported.size(),
|
||||
);
|
||||
output
|
||||
}
|
||||
ContentMapping::Scaled(content_rect) => {
|
||||
let output = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("CEF Imported Scaled Frame Copy"),
|
||||
size: wgpu::Extent3d {
|
||||
width: content_rect.source_width,
|
||||
height: content_rect.source_height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: imported.format(),
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let size = wgpu::Extent3d {
|
||||
width: content_rect.width,
|
||||
height: content_rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
self.resampler.encode(
|
||||
&mut encoder,
|
||||
&imported,
|
||||
wgpu::Origin3d {
|
||||
x: content_rect.x,
|
||||
y: content_rect.y,
|
||||
z: 0,
|
||||
},
|
||||
size,
|
||||
&output,
|
||||
);
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let blank_check = blank_check::encode_readback(&self.device, &mut encoder, &output);
|
||||
|
||||
let submission = self.queue.submit([encoder.finish()]);
|
||||
|
||||
let blank_check = blank_check.map();
|
||||
|
||||
let _ = self.device.poll(wgpu::PollType::Wait {
|
||||
submission_index: Some(submission),
|
||||
timeout: None,
|
||||
});
|
||||
|
||||
if blank_check.check_is_blank() {
|
||||
tracing::debug!("Skipping fully transparent accelerated frame");
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(mut slot) = self.slot.lock() else {
|
||||
tracing::error!("Failed to lock the frame surface");
|
||||
return None;
|
||||
};
|
||||
*slot = Some(output.clone());
|
||||
Some(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
mod blank_check {
|
||||
use std::sync::mpsc;
|
||||
|
||||
const STRIP_BYTES: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
const STRIP_TEXELS: u32 = STRIP_BYTES / 4;
|
||||
|
||||
pub(super) fn encode_readback(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> PendingBlankCheck {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("CEF Blank Check"),
|
||||
size: STRIP_BYTES as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let width = texture.width().min(STRIP_TEXELS);
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d {
|
||||
x: (texture.width() - width) / 2,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(STRIP_BYTES),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
PendingBlankCheck { buffer, width }
|
||||
}
|
||||
|
||||
pub(super) struct PendingBlankCheck {
|
||||
buffer: wgpu::Buffer,
|
||||
width: u32,
|
||||
}
|
||||
|
||||
impl PendingBlankCheck {
|
||||
pub(super) fn map(self) -> MappedBlankCheck {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
self.buffer.slice(..u64::from(self.width) * 4).map_async(wgpu::MapMode::Read, move |result| {
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
MappedBlankCheck {
|
||||
buffer: self.buffer,
|
||||
width: self.width,
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct MappedBlankCheck {
|
||||
buffer: wgpu::Buffer,
|
||||
width: u32,
|
||||
receiver: mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
|
||||
}
|
||||
|
||||
impl MappedBlankCheck {
|
||||
pub(super) fn check_is_blank(self) -> bool {
|
||||
match self.receiver.try_recv() {
|
||||
Ok(Ok(())) => {
|
||||
let slice = self.buffer.slice(..u64::from(self.width) * 4);
|
||||
slice.get_mapped_range().chunks_exact(4).all(|texel| texel[3] == 0)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t};
|
||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
|
||||
mod keymap;
|
||||
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
||||
|
||||
mod state;
|
||||
pub(crate) use state::{CefModifiers, InputState};
|
||||
|
||||
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||
|
||||
/// A window input translated into the plain data CEF consumes — no winit types, so it can
|
||||
/// be applied to a browser living in another process.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum InputEvent {
|
||||
MouseMove { data: MouseData, leave: bool },
|
||||
MouseClick { data: MouseData, button: MouseButtonKind, up: bool, click_count: i32 },
|
||||
MouseWheel { data: MouseData, delta_x: i32, delta_y: i32 },
|
||||
Key(KeyData),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct MouseData {
|
||||
pub(crate) x: i32,
|
||||
pub(crate) y: i32,
|
||||
pub(crate) modifiers: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum MouseButtonKind {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum KeyEventKind {
|
||||
RawKeyDown,
|
||||
KeyUp,
|
||||
Char,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct KeyData {
|
||||
pub(crate) kind: KeyEventKind,
|
||||
pub(crate) modifiers: u32,
|
||||
pub(crate) windows_key_code: i32,
|
||||
pub(crate) native_key_code: i32,
|
||||
pub(crate) character: u16,
|
||||
pub(crate) unmodified_character: u16,
|
||||
}
|
||||
|
||||
/// Turns a winit event into zero or more [`InputEvent`]s, updating the tracked input state
|
||||
/// (cursor position, click counting, modifiers) along the way.
|
||||
pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Vec<InputEvent> {
|
||||
match event {
|
||||
WindowEvent::PointerMoved { position, .. } => {
|
||||
if !input_state.cursor_move(position) {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![InputEvent::MouseMove {
|
||||
data: input_state.mouse_data(),
|
||||
leave: false,
|
||||
}]
|
||||
}
|
||||
WindowEvent::PointerEntered { position, .. } => {
|
||||
let _ = input_state.cursor_move(position);
|
||||
vec![InputEvent::MouseMove {
|
||||
data: input_state.mouse_data(),
|
||||
leave: false,
|
||||
}]
|
||||
}
|
||||
WindowEvent::PointerLeft { position, .. } => {
|
||||
if let Some(position) = position {
|
||||
let _ = input_state.cursor_move(position);
|
||||
}
|
||||
vec![InputEvent::MouseMove {
|
||||
data: input_state.mouse_data(),
|
||||
leave: true,
|
||||
}]
|
||||
}
|
||||
WindowEvent::PointerButton { state, button, position, .. } => {
|
||||
let mouse_button = match button {
|
||||
ButtonSource::Mouse(mouse_button) => mouse_button,
|
||||
_ => {
|
||||
return Vec::new(); // TODO: Handle touch input
|
||||
}
|
||||
};
|
||||
|
||||
let _ = input_state.cursor_move(position);
|
||||
let click_count = input_state.mouse_input(mouse_button, state).into();
|
||||
let up = matches!(state, ElementState::Released);
|
||||
let button = match mouse_button {
|
||||
MouseButton::Left => MouseButtonKind::Left,
|
||||
MouseButton::Right => MouseButtonKind::Right,
|
||||
MouseButton::Middle => MouseButtonKind::Middle,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
vec![InputEvent::MouseClick {
|
||||
data: input_state.mouse_data(),
|
||||
button,
|
||||
up,
|
||||
click_count,
|
||||
}]
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
||||
let (mut delta_x, mut delta_y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32),
|
||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||
};
|
||||
delta_x *= SCROLL_SPEED_X;
|
||||
delta_y *= SCROLL_SPEED_Y;
|
||||
|
||||
vec![InputEvent::MouseWheel {
|
||||
data: input_state.mouse_data(),
|
||||
delta_x: delta_x as i32,
|
||||
delta_y: delta_y as i32,
|
||||
}]
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
input_state.modifiers_changed(&modifiers.state());
|
||||
Vec::new()
|
||||
}
|
||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||
input_state.modifiers_apply_key_event(&event.logical_key, &event.state);
|
||||
|
||||
let mut kind = match (event.state, &event.logical_key) {
|
||||
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => KeyEventKind::Char,
|
||||
(ElementState::Pressed, _) => KeyEventKind::RawKeyDown,
|
||||
(ElementState::Released, _) => KeyEventKind::KeyUp,
|
||||
};
|
||||
|
||||
let modifiers = input_state.cef_modifiers(&event.location, event.repeat).into();
|
||||
|
||||
let windows_key_code = match &event.logical_key {
|
||||
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
|
||||
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let native_key_code = event.physical_key.to_native_keycode();
|
||||
|
||||
let char_representation = event.logical_key.to_char_representation();
|
||||
#[allow(unused_mut)]
|
||||
let mut character = char_representation as u16;
|
||||
|
||||
if event.state == ElementState::Pressed && character != 0 {
|
||||
kind = KeyEventKind::Char;
|
||||
}
|
||||
|
||||
let unmodified_character = event.key_without_modifiers.to_char_representation() as u16;
|
||||
|
||||
#[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650
|
||||
if character == 0 && unmodified_character == 0 && event.text_with_all_modifiers.is_some() {
|
||||
character = 1;
|
||||
}
|
||||
|
||||
let key = KeyData {
|
||||
kind,
|
||||
modifiers,
|
||||
windows_key_code,
|
||||
native_key_code,
|
||||
character,
|
||||
unmodified_character,
|
||||
};
|
||||
|
||||
if kind == KeyEventKind::Char {
|
||||
// CEF expects a raw key-down before the character event it produces.
|
||||
vec![
|
||||
InputEvent::Key(KeyData {
|
||||
kind: KeyEventKind::RawKeyDown,
|
||||
..key
|
||||
}),
|
||||
InputEvent::Key(KeyData {
|
||||
windows_key_code: char_representation as i32,
|
||||
..key
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
vec![InputEvent::Key(key)]
|
||||
}
|
||||
}
|
||||
WindowEvent::PinchGesture { delta, .. } => {
|
||||
if !delta.is_normal() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let data = MouseData {
|
||||
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
|
||||
..input_state.mouse_data()
|
||||
};
|
||||
|
||||
vec![InputEvent::MouseWheel {
|
||||
data,
|
||||
delta_x: 0,
|
||||
delta_y: (delta * PINCH_ZOOM_SPEED).round() as i32,
|
||||
}]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a translated [`InputEvent`] to the browser. Must run on the thread owning the browser.
|
||||
pub(crate) fn apply(browser: &Browser, event: &InputEvent) {
|
||||
let Some(host) = browser.host() else { return };
|
||||
match event {
|
||||
InputEvent::MouseMove { data, leave } => {
|
||||
host.send_mouse_move_event(Some(&data.into()), *leave as i32);
|
||||
}
|
||||
InputEvent::MouseClick { data, button, up, click_count } => {
|
||||
let cef_button = cef::MouseButtonType::from(match button {
|
||||
MouseButtonKind::Left => cef_mouse_button_type_t::MBT_LEFT,
|
||||
MouseButtonKind::Right => cef_mouse_button_type_t::MBT_RIGHT,
|
||||
MouseButtonKind::Middle => cef_mouse_button_type_t::MBT_MIDDLE,
|
||||
});
|
||||
host.send_mouse_click_event(Some(&data.into()), cef_button, *up as i32, *click_count);
|
||||
}
|
||||
InputEvent::MouseWheel { data, delta_x, delta_y } => {
|
||||
host.send_mouse_wheel_event(Some(&data.into()), *delta_x, *delta_y);
|
||||
}
|
||||
InputEvent::Key(key) => {
|
||||
let key_event = KeyEvent {
|
||||
type_: match key.kind {
|
||||
KeyEventKind::RawKeyDown => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
|
||||
KeyEventKind::KeyUp => cef_key_event_type_t::KEYEVENT_KEYUP,
|
||||
KeyEventKind::Char => cef_key_event_type_t::KEYEVENT_CHAR,
|
||||
}
|
||||
.into(),
|
||||
modifiers: key.modifiers,
|
||||
windows_key_code: key.windows_key_code,
|
||||
native_key_code: key.native_key_code,
|
||||
character: key.character,
|
||||
unmodified_character: key.unmodified_character,
|
||||
..Default::default()
|
||||
};
|
||||
host.send_key_event(Some(&key_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MouseData> for MouseEvent {
|
||||
fn from(data: &MouseData) -> Self {
|
||||
MouseEvent {
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
modifiers: data.modifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
use winit::keyboard::{Key, NamedKey, PhysicalKey};
|
||||
|
||||
pub(crate) trait ToCharRepresentation {
|
||||
fn to_char_representation(&self) -> char;
|
||||
}
|
||||
|
||||
impl ToCharRepresentation for Key {
|
||||
fn to_char_representation(&self) -> char {
|
||||
match self {
|
||||
Key::Named(named) => match named {
|
||||
NamedKey::Tab => '\t',
|
||||
NamedKey::Enter => '\r',
|
||||
NamedKey::Backspace => '\x08',
|
||||
NamedKey::Escape => '\x1b',
|
||||
_ => '\0',
|
||||
},
|
||||
Key::Character(char) => char.chars().next().unwrap_or_default(),
|
||||
_ => '\0',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToNativeKeycode {
|
||||
fn to_native_keycode(&self) -> i32;
|
||||
}
|
||||
|
||||
impl ToNativeKeycode for PhysicalKey {
|
||||
fn to_native_keycode(&self) -> i32 {
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or_default()
|
||||
}
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
self.to_scancode().map(|c| c as i32).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToVKBits {
|
||||
fn to_vk_bits(&self) -> i32;
|
||||
}
|
||||
|
||||
macro_rules! map_enum {
|
||||
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
|
||||
match $target {
|
||||
$(
|
||||
$enum::$variant => $code,
|
||||
)+
|
||||
_ => 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
impl ToVKBits for winit::keyboard::NamedKey {
|
||||
fn to_vk_bits(&self) -> i32 {
|
||||
map_enum!(
|
||||
self,
|
||||
NamedKey,
|
||||
(0x12, Alt),
|
||||
(0xA5, AltGraph),
|
||||
(0x14, CapsLock),
|
||||
(0x11, Control),
|
||||
(0x90, NumLock),
|
||||
(0x91, ScrollLock),
|
||||
(0x10, Shift),
|
||||
(0x5B, Meta),
|
||||
(0x0D, Enter),
|
||||
(0x09, Tab),
|
||||
(0x25, ArrowLeft),
|
||||
(0x26, ArrowUp),
|
||||
(0x27, ArrowRight),
|
||||
(0x28, ArrowDown),
|
||||
(0x23, End),
|
||||
(0x24, Home),
|
||||
(0x22, PageDown),
|
||||
(0x21, PageUp),
|
||||
(0x08, Backspace),
|
||||
(0x0C, Clear),
|
||||
(0xF7, CrSel),
|
||||
(0x2E, Delete),
|
||||
(0xF9, EraseEof),
|
||||
(0xF8, ExSel),
|
||||
(0x2D, Insert),
|
||||
(0x1E, Accept),
|
||||
(0xF6, Attn),
|
||||
(0x03, Cancel),
|
||||
(0x5D, ContextMenu),
|
||||
(0x1B, Escape),
|
||||
(0x2B, Execute),
|
||||
(0x2F, Help),
|
||||
(0x13, Pause),
|
||||
(0xFA, Play),
|
||||
(0x5D, Props),
|
||||
(0x29, Select),
|
||||
(0xFB, ZoomIn),
|
||||
(0xFB, ZoomOut),
|
||||
(0x2C, PrintScreen),
|
||||
(0x5F, Standby),
|
||||
(0x1C, Convert),
|
||||
(0x18, FinalMode),
|
||||
(0x1F, ModeChange),
|
||||
(0x1D, NonConvert),
|
||||
(0xE5, Process),
|
||||
(0x15, HangulMode),
|
||||
(0x19, HanjaMode),
|
||||
(0x17, JunjaMode),
|
||||
(0x15, KanaMode),
|
||||
(0x19, KanjiMode),
|
||||
(0xB0, MediaFastForward),
|
||||
(0xB3, MediaPause),
|
||||
(0xB3, MediaPlay),
|
||||
(0xB3, MediaPlayPause),
|
||||
(0xB1, MediaRewind),
|
||||
(0xB2, MediaStop),
|
||||
(0xB0, MediaTrackNext),
|
||||
(0xB1, MediaTrackPrevious),
|
||||
(0x2A, Print),
|
||||
(0xAE, AudioVolumeDown),
|
||||
(0xAF, AudioVolumeUp),
|
||||
(0xAD, AudioVolumeMute),
|
||||
(0xB6, LaunchApplication1),
|
||||
(0xB7, LaunchApplication2),
|
||||
(0xB4, LaunchMail),
|
||||
(0xB5, LaunchMediaPlayer),
|
||||
(0xB5, LaunchMusicPlayer),
|
||||
(0xA6, BrowserBack),
|
||||
(0xAB, BrowserFavorites),
|
||||
(0xA7, BrowserForward),
|
||||
(0xAC, BrowserHome),
|
||||
(0xA8, BrowserRefresh),
|
||||
(0xAA, BrowserSearch),
|
||||
(0xA9, BrowserStop),
|
||||
(0xFB, ZoomToggle),
|
||||
(0x70, F1),
|
||||
(0x71, F2),
|
||||
(0x72, F3),
|
||||
(0x73, F4),
|
||||
(0x74, F5),
|
||||
(0x75, F6),
|
||||
(0x76, F7),
|
||||
(0x77, F8),
|
||||
(0x78, F9),
|
||||
(0x79, F10),
|
||||
(0x7A, F11),
|
||||
(0x7B, F12),
|
||||
(0x7C, F13),
|
||||
(0x7D, F14),
|
||||
(0x7E, F15),
|
||||
(0x7F, F16),
|
||||
(0x80, F17),
|
||||
(0x81, F18),
|
||||
(0x82, F19),
|
||||
(0x83, F20),
|
||||
(0x84, F21),
|
||||
(0x85, F22),
|
||||
(0x86, F23),
|
||||
(0x87, F24),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! map {
|
||||
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
|
||||
match $target {
|
||||
$(
|
||||
$variant => $code,
|
||||
)+
|
||||
_ => 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
impl ToVKBits for char {
|
||||
fn to_vk_bits(&self) -> i32 {
|
||||
map!(
|
||||
self,
|
||||
(0x41, 'a'),
|
||||
(0x42, 'b'),
|
||||
(0x43, 'c'),
|
||||
(0x44, 'd'),
|
||||
(0x45, 'e'),
|
||||
(0x46, 'f'),
|
||||
(0x47, 'g'),
|
||||
(0x48, 'h'),
|
||||
(0x49, 'i'),
|
||||
(0x4a, 'j'),
|
||||
(0x4b, 'k'),
|
||||
(0x4c, 'l'),
|
||||
(0x4d, 'm'),
|
||||
(0x4e, 'n'),
|
||||
(0x4f, 'o'),
|
||||
(0x50, 'p'),
|
||||
(0x51, 'q'),
|
||||
(0x52, 'r'),
|
||||
(0x53, 's'),
|
||||
(0x54, 't'),
|
||||
(0x55, 'u'),
|
||||
(0x56, 'v'),
|
||||
(0x57, 'w'),
|
||||
(0x58, 'x'),
|
||||
(0x59, 'y'),
|
||||
(0x5a, 'z'),
|
||||
(0x41, 'A'),
|
||||
(0x42, 'B'),
|
||||
(0x43, 'C'),
|
||||
(0x44, 'D'),
|
||||
(0x45, 'E'),
|
||||
(0x46, 'F'),
|
||||
(0x47, 'G'),
|
||||
(0x48, 'H'),
|
||||
(0x49, 'I'),
|
||||
(0x4a, 'J'),
|
||||
(0x4b, 'K'),
|
||||
(0x4c, 'L'),
|
||||
(0x4d, 'M'),
|
||||
(0x4e, 'N'),
|
||||
(0x4f, 'O'),
|
||||
(0x50, 'P'),
|
||||
(0x51, 'Q'),
|
||||
(0x52, 'R'),
|
||||
(0x53, 'S'),
|
||||
(0x54, 'T'),
|
||||
(0x55, 'U'),
|
||||
(0x56, 'V'),
|
||||
(0x57, 'W'),
|
||||
(0x58, 'X'),
|
||||
(0x59, 'Y'),
|
||||
(0x5a, 'Z'),
|
||||
(0x31, '1'),
|
||||
(0x32, '2'),
|
||||
(0x33, '3'),
|
||||
(0x34, '4'),
|
||||
(0x35, '5'),
|
||||
(0x36, '6'),
|
||||
(0x37, '7'),
|
||||
(0x38, '8'),
|
||||
(0x39, '9'),
|
||||
(0x30, '0'),
|
||||
(0x31, '!'),
|
||||
(0x32, '@'),
|
||||
(0x33, '#'),
|
||||
(0x34, '$'),
|
||||
(0x35, '%'),
|
||||
(0x36, '^'),
|
||||
(0x37, '&'),
|
||||
(0x38, '*'),
|
||||
(0x39, '('),
|
||||
(0x30, ')'),
|
||||
(0xC0, '`'),
|
||||
(0xC0, '~'),
|
||||
(0xBD, '-'),
|
||||
(0xBD, '_'),
|
||||
(0xBB, '='),
|
||||
(0xBB, '+'),
|
||||
(0xDB, '['),
|
||||
(0xDB, '{'),
|
||||
(0xDD, ']'),
|
||||
(0xDD, '}'),
|
||||
(0xDC, '\\'),
|
||||
(0xDC, '|'),
|
||||
(0xBA, ';'),
|
||||
(0xBA, ':'),
|
||||
(0xBC, ','),
|
||||
(0xBC, '<'),
|
||||
(0xBE, '.'),
|
||||
(0xBE, '>'),
|
||||
(0xDE, '\''),
|
||||
(0xDE, '"'),
|
||||
(0xBF, '/'),
|
||||
(0xBF, '?'),
|
||||
(0x20, ' '),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
use cef::sys::cef_event_flags_t;
|
||||
use std::time::Instant;
|
||||
use winit::dpi::PhysicalPosition;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
||||
|
||||
use super::MouseData;
|
||||
use crate::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct InputState {
|
||||
modifiers: ModifiersState,
|
||||
mouse_position: MousePosition,
|
||||
mouse_state: MouseState,
|
||||
mouse_click_tracker: ClickTracker,
|
||||
}
|
||||
impl InputState {
|
||||
pub(crate) fn modifiers_changed(&mut self, modifiers: &ModifiersState) {
|
||||
self.modifiers = *modifiers;
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers_apply_key_event(&mut self, key: &Key, state: &ElementState) {
|
||||
let bits = match key {
|
||||
Key::Named(NamedKey::Shift) => ModifiersState::SHIFT,
|
||||
Key::Named(NamedKey::Control) => ModifiersState::CONTROL,
|
||||
Key::Named(NamedKey::Alt) => ModifiersState::ALT,
|
||||
Key::Named(NamedKey::Meta) => ModifiersState::META,
|
||||
_ => return,
|
||||
};
|
||||
let is_pressed = matches!(state, ElementState::Pressed);
|
||||
self.modifiers.set(bits, is_pressed);
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
|
||||
let new = position.into();
|
||||
if self.mouse_position == new {
|
||||
return false;
|
||||
}
|
||||
self.mouse_position = new;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
|
||||
self.mouse_state.update(button, state);
|
||||
self.mouse_click_tracker.input(button, state, self.mouse_position)
|
||||
}
|
||||
|
||||
pub(crate) fn cef_modifiers(&self, location: &KeyLocation, is_repeat: bool) -> CefModifiers {
|
||||
CefModifiers::new(self, location, is_repeat)
|
||||
}
|
||||
|
||||
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
|
||||
self.cef_modifiers(&KeyLocation::Standard, false)
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_data(&self) -> MouseData {
|
||||
MouseData {
|
||||
x: self.mouse_position.x,
|
||||
y: self.mouse_position.y,
|
||||
modifiers: self.cef_mouse_modifiers().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) struct MousePosition {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
impl From<&PhysicalPosition<f64>> for MousePosition {
|
||||
fn from(position: &PhysicalPosition<f64>) -> Self {
|
||||
Self {
|
||||
x: position.x as i32,
|
||||
y: position.y as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct MouseState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
middle: bool,
|
||||
}
|
||||
impl MouseState {
|
||||
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
|
||||
match state {
|
||||
ElementState::Pressed => match button {
|
||||
MouseButton::Left => self.left = true,
|
||||
MouseButton::Right => self.right = true,
|
||||
MouseButton::Middle => self.middle = true,
|
||||
_ => {}
|
||||
},
|
||||
ElementState::Released => match button {
|
||||
MouseButton::Left => self.left = false,
|
||||
MouseButton::Right => self.right = false,
|
||||
MouseButton::Middle => self.middle = false,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClickTracker {
|
||||
left: Option<ClickRecord>,
|
||||
middle: Option<ClickRecord>,
|
||||
right: Option<ClickRecord>,
|
||||
}
|
||||
impl ClickTracker {
|
||||
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
|
||||
let record = match button {
|
||||
MouseButton::Left => &mut self.left,
|
||||
MouseButton::Right => &mut self.right,
|
||||
MouseButton::Middle => &mut self.middle,
|
||||
_ => return ClickCount::Single,
|
||||
};
|
||||
|
||||
let Some(record) = record else {
|
||||
*record = Some(ClickRecord {
|
||||
down_position: position,
|
||||
up_position: position,
|
||||
..Default::default()
|
||||
});
|
||||
return ClickCount::Single;
|
||||
};
|
||||
|
||||
let now = Instant::now();
|
||||
let within_time = now.saturating_duration_since(record.time) <= MULTICLICK_TIMEOUT;
|
||||
|
||||
let (prev_count, prev_position) = match state {
|
||||
ElementState::Pressed => (record.down_count, record.down_position),
|
||||
ElementState::Released => (record.up_count, record.up_position),
|
||||
};
|
||||
|
||||
let dx = position.x.abs_diff(prev_position.x) as usize;
|
||||
let dy = position.y.abs_diff(prev_position.y) as usize;
|
||||
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
||||
|
||||
let count = match (prev_count, within_time, within_dist) {
|
||||
(ClickCount::Single, true, true) => ClickCount::Double,
|
||||
(ClickCount::Double, true, true) => ClickCount::Triple,
|
||||
(ClickCount::Triple, true, true) => ClickCount::Double,
|
||||
_ => ClickCount::Single,
|
||||
};
|
||||
|
||||
record.time = now;
|
||||
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
record.down_position = position;
|
||||
record.down_count = count;
|
||||
}
|
||||
ElementState::Released => {
|
||||
record.up_position = position;
|
||||
record.up_count = count;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub(crate) enum ClickCount {
|
||||
#[default]
|
||||
Single,
|
||||
Double,
|
||||
Triple,
|
||||
}
|
||||
impl From<ClickCount> for i32 {
|
||||
fn from(count: ClickCount) -> i32 {
|
||||
match count {
|
||||
ClickCount::Single => 1,
|
||||
ClickCount::Double => 2,
|
||||
ClickCount::Triple => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ClickRecord {
|
||||
time: Instant,
|
||||
down_position: MousePosition,
|
||||
up_position: MousePosition,
|
||||
down_count: ClickCount,
|
||||
up_count: ClickCount,
|
||||
}
|
||||
|
||||
impl Default for ClickRecord {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: Instant::now(),
|
||||
down_position: Default::default(),
|
||||
up_position: Default::default(),
|
||||
down_count: Default::default(),
|
||||
up_count: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CefModifiers(cef_event_flags_t);
|
||||
impl CefModifiers {
|
||||
fn new(input_state: &InputState, location: &KeyLocation, is_repeat: bool) -> Self {
|
||||
let mut inner = cef_event_flags_t::EVENTFLAG_NONE;
|
||||
|
||||
if input_state.modifiers.shift_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN;
|
||||
}
|
||||
if input_state.modifiers.control_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN;
|
||||
}
|
||||
if input_state.modifiers.alt_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN;
|
||||
}
|
||||
if input_state.modifiers.meta_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN;
|
||||
}
|
||||
|
||||
if input_state.mouse_state.left {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON;
|
||||
}
|
||||
if input_state.mouse_state.right {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON;
|
||||
}
|
||||
if input_state.mouse_state.middle {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON;
|
||||
}
|
||||
|
||||
if is_repeat {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT;
|
||||
}
|
||||
|
||||
inner |= match location {
|
||||
KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT,
|
||||
KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT,
|
||||
KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD,
|
||||
KeyLocation::Standard => cef_event_flags_t::EVENTFLAG_NONE,
|
||||
};
|
||||
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t(
|
||||
cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0,
|
||||
));
|
||||
}
|
||||
|
||||
impl From<CefModifiers> for u32 {
|
||||
fn from(val: CefModifiers) -> Self {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return val.0.0;
|
||||
#[cfg(target_os = "windows")]
|
||||
return val.0.0 as u32;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
mod browser_process_app;
|
||||
mod browser_process_client;
|
||||
mod browser_process_handler;
|
||||
|
||||
mod render_process_app;
|
||||
mod render_process_handler;
|
||||
mod render_process_v8_handler;
|
||||
|
||||
mod context_menu_handler;
|
||||
mod display_handler;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod keyboard_handler;
|
||||
mod life_span_handler;
|
||||
mod load_handler;
|
||||
mod request_handler;
|
||||
mod resource_handler;
|
||||
mod resource_request_handler;
|
||||
mod scheme_handler_factory;
|
||||
|
||||
pub(super) mod render_handler;
|
||||
|
||||
pub(super) mod task;
|
||||
|
||||
pub(super) use browser_process_app::BrowserProcessAppImpl;
|
||||
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
||||
pub(super) use render_process_app::RenderProcessAppImpl;
|
||||
pub(super) use scheme_handler_factory::SchemeHandlerFactoryImpl;
|
||||
@@ -0,0 +1,134 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
|
||||
|
||||
use super::browser_process_handler::BrowserProcessHandlerImpl;
|
||||
use super::scheme_handler_factory::register_schemes;
|
||||
|
||||
pub(crate) struct BrowserProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
accelerated_paint: bool,
|
||||
}
|
||||
impl BrowserProcessAppImpl {
|
||||
pub(crate) fn new(accelerated_paint: bool) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
accelerated_paint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplApp for BrowserProcessAppImpl {
|
||||
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
||||
if let Some(cmd) = command_line {
|
||||
cmd.append_switch_with_value(Some(&"renderer-process-limit".into()), Some(&"1".into()));
|
||||
cmd.append_switch_with_value(Some(&"password-store".into()), Some(&"basic".into()));
|
||||
cmd.append_switch_with_value(Some(&"disk-cache-size".into()), Some(&"0".into()));
|
||||
cmd.append_switch(Some(&"no-sandbox".into()));
|
||||
cmd.append_switch(Some(&"no-first-run".into()));
|
||||
cmd.append_switch(Some(&"noerrdialogs".into()));
|
||||
cmd.append_switch(Some(&"no-default-browser-check".into()));
|
||||
cmd.append_switch(Some(&"mute-audio".into()));
|
||||
cmd.append_switch(Some(&"use-fake-device-for-media-stream".into()));
|
||||
cmd.append_switch(Some(&"incognito".into()));
|
||||
cmd.append_switch(Some(&"disable-sync".into()));
|
||||
cmd.append_switch(Some(&"disable-file-system".into()));
|
||||
cmd.append_switch(Some(&"disable-component-update".into()));
|
||||
cmd.append_switch(Some(&"disable-geolocation".into()));
|
||||
cmd.append_switch(Some(&"disable-notifications".into()));
|
||||
cmd.append_switch(Some(&"disable-background-networking".into()));
|
||||
cmd.append_switch(Some(&"disable-default-apps".into()));
|
||||
cmd.append_switch(Some(&"disable-breakpad".into()));
|
||||
cmd.append_switch_with_value(Some(&"disable-blink-features".into()), Some(&"WebBluetooth,WebUSB,Serial".into()));
|
||||
|
||||
let extra_disabled_features = ["OptimizationHints", "OnDeviceModelService", "TranslateUI"];
|
||||
let disabled_features_switch = Some(&"disable-features".into());
|
||||
let mut disabled_features: Vec<String> = CefString::from(&cmd.switch_value(disabled_features_switch))
|
||||
.to_string()
|
||||
.split(',')
|
||||
.filter(|feature| !feature.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
disabled_features.extend(extra_disabled_features.into_iter().map(ToOwned::to_owned));
|
||||
cmd.append_switch_with_value(disabled_features_switch, Some(&disabled_features.join(",").as_str().into()));
|
||||
|
||||
if self.accelerated_paint {
|
||||
cmd.append_switch(Some(&"enable-gpu".into()));
|
||||
cmd.append_switch(Some(&"enable-gpu-compositing".into()));
|
||||
cmd.append_switch(Some(&"enable-begin-frame-scheduling".into()));
|
||||
cmd.append_switch(Some(&"off-screen-rendering-enabled".into()));
|
||||
cmd.append_switch(Some(&"enable-accelerated-2d-canvas".into()));
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
cmd.append_switch_with_value(Some(&"use-angle".into()), Some(&"gl-egl".into()));
|
||||
|
||||
let use_wayland = std::env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|var| !var.is_empty())
|
||||
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
|
||||
.filter(|var| !var.is_empty())
|
||||
.is_some();
|
||||
if use_wayland {
|
||||
cmd.append_switch_with_value(Some(&"ozone-platform".into()), Some(&"wayland".into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd.append_switch(Some(&"disable-gpu".into()));
|
||||
cmd.append_switch(Some(&"disable-gpu-compositing".into()));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Hide user prompt asking for keychain access
|
||||
cmd.append_switch(Some(&"use-mock-keychain".into()));
|
||||
}
|
||||
|
||||
// Enable browser debugging via environment variable
|
||||
if let Some(env) = std::env::var("GRAPHITE_BROWSER_DEBUG_PORT").ok()
|
||||
&& let Some(port) = env.parse::<u16>().ok()
|
||||
{
|
||||
cmd.append_switch_with_value(Some(&"remote-debugging-port".into()), Some(&port.to_string().as_str().into()));
|
||||
cmd.append_switch_with_value(Some(&"remote-allow-origins".into()), Some(&"*".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_app_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
accelerated_paint: self.accelerated_paint,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapApp for BrowserProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
|
||||
use cef::{ContextMenuHandler, DisplayHandler, ImplClient, LifeSpanHandler, LoadHandler, RenderHandler, RequestHandler, WrapClient};
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::frames::FrameStreamer;
|
||||
use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::context_menu_handler::ContextMenuHandlerImpl;
|
||||
use super::display_handler::DisplayHandlerImpl;
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::keyboard_handler::KeyboardHandlerImpl;
|
||||
use super::life_span_handler::LifeSpanHandlerImpl;
|
||||
use super::load_handler::LoadHandlerImpl;
|
||||
use super::render_handler::RenderHandlerImpl;
|
||||
use super::request_handler::RequestHandlerImpl;
|
||||
|
||||
pub(crate) struct BrowserProcessClientImpl {
|
||||
object: *mut RcImpl<_cef_client_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
load_handler: LoadHandler,
|
||||
render_handler: RenderHandler,
|
||||
display_handler: DisplayHandler,
|
||||
request_handler: RequestHandler,
|
||||
}
|
||||
impl BrowserProcessClientImpl {
|
||||
pub(crate) fn new(delegate: &BrowserDelegate, frames: FrameStreamer) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate: delegate.clone(),
|
||||
load_handler: LoadHandler::new(LoadHandlerImpl::new(delegate.clone())),
|
||||
render_handler: RenderHandler::new(RenderHandlerImpl::new(delegate.clone(), frames)),
|
||||
display_handler: DisplayHandler::new(DisplayHandlerImpl::new(delegate.clone())),
|
||||
request_handler: RequestHandler::new(RequestHandlerImpl::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplClient for BrowserProcessClientImpl {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> std::ffi::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::Initialized,
|
||||
data: _,
|
||||
}) => self.delegate.initialized_web_communication(),
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToNative,
|
||||
data,
|
||||
}) => self.delegate.receive_web_message(data),
|
||||
|
||||
_ => {
|
||||
tracing::error!("Unexpected message type received in browser process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn load_handler(&self) -> Option<cef::LoadHandler> {
|
||||
Some(self.load_handler.clone())
|
||||
}
|
||||
|
||||
fn render_handler(&self) -> Option<RenderHandler> {
|
||||
Some(self.render_handler.clone())
|
||||
}
|
||||
|
||||
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
|
||||
Some(LifeSpanHandler::new(LifeSpanHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn display_handler(&self) -> Option<cef::DisplayHandler> {
|
||||
Some(self.display_handler.clone())
|
||||
}
|
||||
|
||||
fn request_handler(&self) -> Option<cef::RequestHandler> {
|
||||
Some(self.request_handler.clone())
|
||||
}
|
||||
|
||||
fn context_menu_handler(&self) -> Option<cef::ContextMenuHandler> {
|
||||
Some(ContextMenuHandler::new(ContextMenuHandlerImpl::new()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn keyboard_handler(&self) -> Option<cef::KeyboardHandler> {
|
||||
Some(cef::KeyboardHandler::new(KeyboardHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_client_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessClientImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
load_handler: self.load_handler.clone(),
|
||||
render_handler: self.render_handler.clone(),
|
||||
display_handler: self.display_handler.clone(),
|
||||
request_handler: self.request_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessClientImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapClient for BrowserProcessClientImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t};
|
||||
use cef::{CefString, ImplBrowserProcessHandler, WrapBrowserProcessHandler};
|
||||
|
||||
pub(crate) struct BrowserProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
||||
}
|
||||
impl BrowserProcessHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl {
|
||||
fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> std::ffi::c_int {
|
||||
1 // Return 1 to prevent default behavior of opening a empty browser window
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_browser_process_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapBrowserProcessHandler for BrowserProcessHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
|
||||
|
||||
pub(crate) struct ContextMenuHandlerImpl {
|
||||
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
|
||||
}
|
||||
impl ContextMenuHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn run_context_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_params: Option<&mut cef::ContextMenuParams>,
|
||||
_model: Option<&mut cef::MenuModel>,
|
||||
_callback: Option<&mut cef::RunContextMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent context menu
|
||||
1
|
||||
}
|
||||
|
||||
fn run_quick_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_location: Option<&cef::Point>,
|
||||
_size: Option<&cef::Size>,
|
||||
_edit_state_flags: cef::QuickMenuEditStateFlags,
|
||||
_callback: Option<&mut cef::RunQuickMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent quick menu
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ContextMenuHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for ContextMenuHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_display_handler_t, cef_base_ref_counted_t, cef_cursor_type_t::*, cef_log_severity_t::*};
|
||||
use cef::{CefString, ImplDisplayHandler, Point, Size, WrapDisplayHandler};
|
||||
use winit::cursor::CursorIcon;
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
|
||||
pub(crate) struct DisplayHandlerImpl {
|
||||
object: *mut RcImpl<_cef_display_handler_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
|
||||
impl DisplayHandlerImpl {
|
||||
pub fn new(delegate: BrowserDelegate) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
type CefCursorHandle = cef::CursorHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
type CefCursorHandle = *mut u8;
|
||||
|
||||
impl ImplDisplayHandler for DisplayHandlerImpl {
|
||||
fn on_cursor_change(&self, _browser: Option<&mut cef::Browser>, _cursor: CefCursorHandle, cursor_type: cef::CursorType, custom_cursor_info: Option<&cef::CursorInfo>) -> std::ffi::c_int {
|
||||
if let Some(custom_cursor_info) = custom_cursor_info {
|
||||
let Size { width, height } = custom_cursor_info.size;
|
||||
let Point { x: hotspot_x, y: hotspot_y } = custom_cursor_info.hotspot;
|
||||
let buffer_size = (width * height * 4) as usize;
|
||||
let buffer_ptr = custom_cursor_info.buffer as *const u8;
|
||||
|
||||
if !buffer_ptr.is_null() && buffer_ptr.align_offset(std::mem::align_of::<u8>()) == 0 {
|
||||
let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) }.to_vec();
|
||||
self.delegate.cursor_change(crate::Cursor::Custom {
|
||||
rgba: buffer,
|
||||
width: width as u16,
|
||||
height: height as u16,
|
||||
hotspot_x: hotspot_x as u16,
|
||||
hotspot_y: hotspot_y as u16,
|
||||
});
|
||||
return 1; // We handled the cursor change.
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = match cursor_type.into() {
|
||||
CT_POINTER => CursorIcon::Default,
|
||||
CT_CROSS => CursorIcon::Crosshair,
|
||||
CT_HAND => CursorIcon::Pointer,
|
||||
CT_IBEAM => CursorIcon::Text,
|
||||
CT_WAIT => CursorIcon::Wait,
|
||||
CT_HELP => CursorIcon::Help,
|
||||
CT_EASTRESIZE => CursorIcon::EResize,
|
||||
CT_NORTHRESIZE => CursorIcon::NResize,
|
||||
CT_NORTHEASTRESIZE => CursorIcon::NeResize,
|
||||
CT_NORTHWESTRESIZE => CursorIcon::NwResize,
|
||||
CT_SOUTHRESIZE => CursorIcon::SResize,
|
||||
CT_SOUTHEASTRESIZE => CursorIcon::SeResize,
|
||||
CT_SOUTHWESTRESIZE => CursorIcon::SwResize,
|
||||
CT_WESTRESIZE => CursorIcon::WResize,
|
||||
CT_NORTHSOUTHRESIZE => CursorIcon::NsResize,
|
||||
CT_EASTWESTRESIZE => CursorIcon::EwResize,
|
||||
CT_NORTHEASTSOUTHWESTRESIZE => CursorIcon::NeswResize,
|
||||
CT_NORTHWESTSOUTHEASTRESIZE => CursorIcon::NwseResize,
|
||||
CT_COLUMNRESIZE => CursorIcon::ColResize,
|
||||
CT_ROWRESIZE => CursorIcon::RowResize,
|
||||
CT_MIDDLEPANNING => CursorIcon::AllScroll,
|
||||
CT_EASTPANNING => CursorIcon::AllScroll,
|
||||
CT_NORTHPANNING => CursorIcon::AllScroll,
|
||||
CT_NORTHEASTPANNING => CursorIcon::AllScroll,
|
||||
CT_NORTHWESTPANNING => CursorIcon::AllScroll,
|
||||
CT_SOUTHPANNING => CursorIcon::AllScroll,
|
||||
CT_SOUTHEASTPANNING => CursorIcon::AllScroll,
|
||||
CT_SOUTHWESTPANNING => CursorIcon::AllScroll,
|
||||
CT_WESTPANNING => CursorIcon::AllScroll,
|
||||
CT_MOVE => CursorIcon::Move,
|
||||
CT_VERTICALTEXT => CursorIcon::VerticalText,
|
||||
CT_CELL => CursorIcon::Cell,
|
||||
CT_CONTEXTMENU => CursorIcon::ContextMenu,
|
||||
CT_ALIAS => CursorIcon::Alias,
|
||||
CT_PROGRESS => CursorIcon::Progress,
|
||||
CT_NODROP => CursorIcon::NoDrop,
|
||||
CT_COPY => CursorIcon::Copy,
|
||||
CT_NOTALLOWED => CursorIcon::NotAllowed,
|
||||
CT_ZOOMIN => CursorIcon::ZoomIn,
|
||||
CT_ZOOMOUT => CursorIcon::ZoomOut,
|
||||
CT_GRAB => CursorIcon::Grab,
|
||||
CT_GRABBING => CursorIcon::Grabbing,
|
||||
CT_MIDDLE_PANNING_VERTICAL => CursorIcon::AllScroll,
|
||||
CT_MIDDLE_PANNING_HORIZONTAL => CursorIcon::AllScroll,
|
||||
CT_DND_NONE => CursorIcon::Default,
|
||||
CT_DND_MOVE => CursorIcon::Move,
|
||||
CT_DND_COPY => CursorIcon::Copy,
|
||||
CT_DND_LINK => CursorIcon::Alias,
|
||||
CT_NUM_VALUES => CursorIcon::Default,
|
||||
CT_NONE => {
|
||||
self.delegate.cursor_change(crate::Cursor::None);
|
||||
return 1; // We handled the cursor change.
|
||||
}
|
||||
_ => CursorIcon::Default,
|
||||
};
|
||||
|
||||
self.delegate.cursor_change(cursor.into());
|
||||
|
||||
1 // We handled the cursor change.
|
||||
}
|
||||
|
||||
fn on_console_message(&self, _browser: Option<&mut cef::Browser>, level: cef::LogSeverity, message: Option<&CefString>, source: Option<&CefString>, line: std::ffi::c_int) -> std::ffi::c_int {
|
||||
let message = message.map(|m| m.to_string()).unwrap_or_default();
|
||||
let source = source.map(|s| s.to_string()).unwrap_or_default();
|
||||
let line = line as i64;
|
||||
let browser_source = format!("{source}:{line}");
|
||||
static BROWSER: &str = "browser";
|
||||
match level.as_ref() {
|
||||
LOGSEVERITY_FATAL | LOGSEVERITY_ERROR => tracing::error!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_WARNING => tracing::warn!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_INFO => tracing::info!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_DEFAULT | LOGSEVERITY_VERBOSE => tracing::debug!(target: BROWSER, "{browser_source} {message}"),
|
||||
_ => tracing::trace!(target: BROWSER, "{browser_source} {message}"),
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_display_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for DisplayHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for DisplayHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapDisplayHandler for DisplayHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_keyboard_handler_t, cef_base_ref_counted_t, cef_event_flags_t, cef_key_event_type_t};
|
||||
use cef::{Browser, Frame, ImplBrowser, ImplBrowserHost, ImplFrame, ImplKeyboardHandler, KeyEvent, WrapKeyboardHandler};
|
||||
|
||||
const SHIFT: u32 = cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0;
|
||||
const CONTROL: u32 = cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0;
|
||||
const ALT: u32 = cef_event_flags_t::EVENTFLAG_ALT_DOWN.0;
|
||||
const COMMAND: u32 = cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0;
|
||||
const MODIFIER_MASK: u32 = SHIFT | CONTROL | ALT | COMMAND;
|
||||
const COMMAND_SHIFT: u32 = COMMAND | SHIFT;
|
||||
const SHIFT_ALT: u32 = SHIFT | ALT;
|
||||
|
||||
const VK_END: i32 = 0x23;
|
||||
const VK_HOME: i32 = 0x24;
|
||||
const VK_UP: i32 = 0x26;
|
||||
const VK_DOWN: i32 = 0x28;
|
||||
const VK_A: i32 = 0x41;
|
||||
const VK_C: i32 = 0x43;
|
||||
const VK_V: i32 = 0x56;
|
||||
const VK_X: i32 = 0x58;
|
||||
const VK_Z: i32 = 0x5A;
|
||||
|
||||
const KVK_HOME: i32 = 0x73;
|
||||
const KVK_END: i32 = 0x77;
|
||||
const NS_HOME_FUNCTION_KEY: u16 = 0xF729;
|
||||
const NS_END_FUNCTION_KEY: u16 = 0xF72B;
|
||||
|
||||
pub(crate) struct KeyboardHandlerImpl {
|
||||
object: *mut RcImpl<_cef_keyboard_handler_t, Self>,
|
||||
}
|
||||
impl KeyboardHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplKeyboardHandler for KeyboardHandlerImpl {
|
||||
fn on_key_event(&self, browser: Option<&mut Browser>, event: Option<&KeyEvent>, _os_event: *mut u8) -> std::ffi::c_int {
|
||||
let (Some(browser), Some(event)) = (browser, event) else { return 0 };
|
||||
if event.type_ != cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let shortcut = (event.modifiers & MODIFIER_MASK, event.windows_key_code);
|
||||
|
||||
let edit_operation: Option<fn(&Frame)> = match shortcut {
|
||||
(COMMAND, VK_A) => Some(Frame::select_all),
|
||||
(COMMAND, VK_C) => Some(Frame::copy),
|
||||
(COMMAND, VK_V) => Some(Frame::paste),
|
||||
(COMMAND_SHIFT, VK_V) => Some(Frame::paste_and_match_style),
|
||||
(COMMAND, VK_X) => Some(Frame::cut),
|
||||
(COMMAND, VK_Z) => Some(Frame::undo),
|
||||
(COMMAND_SHIFT, VK_Z) => Some(Frame::redo),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(edit_operation) = edit_operation {
|
||||
let Some(frame) = browser.focused_frame() else { return 0 };
|
||||
edit_operation(&frame);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let remap = match shortcut {
|
||||
(SHIFT_ALT, VK_UP) => Some((VK_HOME, KVK_HOME, NS_HOME_FUNCTION_KEY)),
|
||||
(SHIFT_ALT, VK_DOWN) => Some((VK_END, KVK_END, NS_END_FUNCTION_KEY)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((windows_key_code, native_key_code, character)) = remap {
|
||||
let Some(host) = browser.host() else { return 0 };
|
||||
host.send_key_event(Some(&KeyEvent {
|
||||
type_: cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into(),
|
||||
modifiers: SHIFT,
|
||||
windows_key_code,
|
||||
native_key_code,
|
||||
character,
|
||||
unmodified_character: character,
|
||||
..Default::default()
|
||||
}));
|
||||
return 1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_keyboard_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for KeyboardHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for KeyboardHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapKeyboardHandler for KeyboardHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_keyboard_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
|
||||
|
||||
pub(crate) struct LifeSpanHandlerImpl {
|
||||
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
|
||||
}
|
||||
impl LifeSpanHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplLifeSpanHandler for LifeSpanHandlerImpl {
|
||||
fn on_before_popup(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_popup_id: std::ffi::c_int,
|
||||
target_url: Option<&cef::CefString>,
|
||||
_target_frame_name: Option<&cef::CefString>,
|
||||
_target_disposition: cef::WindowOpenDisposition,
|
||||
_user_gesture: std::ffi::c_int,
|
||||
_popup_features: Option<&cef::PopupFeatures>,
|
||||
_window_info: Option<&mut cef::WindowInfo>,
|
||||
_client: Option<&mut Option<cef::Client>>,
|
||||
_settings: Option<&mut cef::BrowserSettings>,
|
||||
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
|
||||
_no_javascript_access: Option<&mut std::ffi::c_int>,
|
||||
) -> std::ffi::c_int {
|
||||
let target = target_url.map(|url| url.to_string()).unwrap_or("unknown".to_string());
|
||||
tracing::error!("Browser tried to open a popup at URL: {}", target);
|
||||
|
||||
// Deny any popup by returning 1
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_life_span_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LifeSpanHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for LifeSpanHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapLifeSpanHandler for LifeSpanHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t};
|
||||
use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler};
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
|
||||
pub(crate) struct LoadHandlerImpl {
|
||||
object: *mut RcImpl<cef_load_handler_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
impl LoadHandlerImpl {
|
||||
pub(crate) fn new(delegate: BrowserDelegate) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplLoadHandler for LoadHandlerImpl {
|
||||
fn on_loading_state_change(&self, browser: Option<&mut cef::Browser>, is_loading: std::ffi::c_int, _can_go_back: std::ffi::c_int, _can_go_forward: std::ffi::c_int) {
|
||||
let view_info = self.delegate.view_info();
|
||||
|
||||
if let Some(browser) = browser
|
||||
&& is_loading == 0
|
||||
{
|
||||
browser.host().unwrap().set_zoom_level(view_info.zoom());
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_load_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LoadHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for LoadHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapLoadHandler for LoadHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Browser, ImplRenderHandler, PaintElementType, Rect, WrapRenderHandler};
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::frames::FrameStreamer;
|
||||
|
||||
pub(crate) struct RenderHandlerImpl {
|
||||
object: *mut RcImpl<_cef_render_handler_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
frames: FrameStreamer,
|
||||
}
|
||||
impl RenderHandlerImpl {
|
||||
pub(crate) fn new(delegate: BrowserDelegate, frames: FrameStreamer) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
frames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRenderHandler for RenderHandlerImpl {
|
||||
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
|
||||
if let Some(rect) = rect {
|
||||
let view_info = self.delegate.view_info();
|
||||
*rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: view_info.width() as i32,
|
||||
height: view_info.height() as i32,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn on_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, buffer: *const u8, width: std::ffi::c_int, height: std::ffi::c_int) {
|
||||
if type_ != PaintElementType::default() {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer_size = (width * height * 4) as usize;
|
||||
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
|
||||
|
||||
self.frames.stage_buffer(buffer_slice, width as u32, height as u32);
|
||||
self.frames.publish();
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, info: Option<&cef::AcceleratedPaintInfo>) {
|
||||
if type_ != PaintElementType::default() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(info) = info else {
|
||||
tracing::error!("Accelerated paint callback received no info about the painted frame");
|
||||
return;
|
||||
};
|
||||
self.frames.stage_texture(info);
|
||||
self.frames.publish();
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_render_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
frames: self.frames.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for RenderHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRenderHandler for RenderHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp};
|
||||
|
||||
use super::render_process_handler::RenderProcessHandlerImpl;
|
||||
use super::scheme_handler_factory::register_schemes;
|
||||
|
||||
pub(crate) struct RenderProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
render_process_handler: RenderProcessHandler,
|
||||
}
|
||||
impl RenderProcessAppImpl {
|
||||
pub(crate) fn app() -> App {
|
||||
App::new(Self {
|
||||
object: std::ptr::null_mut(),
|
||||
render_process_handler: RenderProcessHandler::new(RenderProcessHandlerImpl::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplApp for RenderProcessAppImpl {
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
|
||||
Some(self.render_process_handler.clone())
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_app_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
render_process_handler: self.render_process_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapApp for RenderProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use cef::rc::{ConvertReturnValue, Rc, RcImpl};
|
||||
use cef::sys::{_cef_render_process_handler_t, cef_base_ref_counted_t, cef_render_process_handler_t, cef_v8_propertyattribute_t, cef_v8_value_create_array_buffer_with_copy};
|
||||
use cef::{ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_value_create_function};
|
||||
|
||||
use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::render_process_v8_handler::RenderProcessV8HandlerImpl;
|
||||
|
||||
pub(crate) struct RenderProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_render_process_handler_t, Self>,
|
||||
}
|
||||
impl RenderProcessHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> std::ffi::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToJS,
|
||||
data,
|
||||
}) => {
|
||||
let Some(frame) = frame else {
|
||||
tracing::error!("Frame is not available");
|
||||
return 0;
|
||||
};
|
||||
let Some(context) = frame.v8_context() else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return 0;
|
||||
};
|
||||
if context.enter() == 0 {
|
||||
tracing::error!("Failed to enter V8 context");
|
||||
return 0;
|
||||
}
|
||||
let mut value: V8Value = unsafe { cef_v8_value_create_array_buffer_with_copy(data.as_ptr() as *mut std::ffi::c_void, data.len()) }.wrap_result();
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return 0;
|
||||
};
|
||||
|
||||
let function_name = "receiveNativeMessage";
|
||||
let property_name = "receiveNativeMessageData";
|
||||
|
||||
let function_call = format!("window.{function_name}(window.{property_name})");
|
||||
|
||||
global.set_value_bykey(Some(&property_name.into()), Some(&mut value), cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY.wrap_result());
|
||||
|
||||
if global.value_bykey(Some(&function_name.into())).is_some() {
|
||||
frame.execute_java_script(Some(&function_call.as_str().into()), None, 0);
|
||||
}
|
||||
|
||||
if context.exit() == 0 {
|
||||
tracing::error!("Failed to exit V8 context");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("Unexpected message type received in render process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
|
||||
let register_js_function = |context: &mut cef::V8Context, name: &'static str| {
|
||||
let mut v8_handler = V8Handler::new(RenderProcessV8HandlerImpl::new());
|
||||
let Some(mut function) = v8_value_create_function(Some(&name.into()), Some(&mut v8_handler)) else {
|
||||
tracing::error!("Failed to create V8 function {name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return;
|
||||
};
|
||||
global.set_value_bykey(Some(&name.into()), Some(&mut function), V8Propertyattribute::default());
|
||||
};
|
||||
|
||||
let Some(context) = context else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return;
|
||||
};
|
||||
|
||||
let initialized_function_name = "initializeNativeCommunication";
|
||||
let send_function_name = "sendNativeMessage";
|
||||
|
||||
register_js_function(context, initialized_function_name);
|
||||
register_js_function(context, send_function_name);
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context_get_current_context};
|
||||
|
||||
use crate::ipc::{MessageType, SendMessage};
|
||||
|
||||
pub struct RenderProcessV8HandlerImpl {
|
||||
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
|
||||
}
|
||||
impl RenderProcessV8HandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplV8Handler for RenderProcessV8HandlerImpl {
|
||||
fn execute(
|
||||
&self,
|
||||
name: Option<&cef::CefString>,
|
||||
_object: Option<&mut V8Value>,
|
||||
arguments: Option<&[Option<V8Value>]>,
|
||||
_retval: Option<&mut Option<V8Value>>,
|
||||
_exception: Option<&mut cef::CefString>,
|
||||
) -> std::ffi::c_int {
|
||||
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
|
||||
"initializeNativeCommunication" => {
|
||||
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
|
||||
}
|
||||
"sendNativeMessage" => {
|
||||
let Some(args) = arguments else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
};
|
||||
let Some(arg1) = args.first() else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
};
|
||||
let Some(arg1) = arg1.as_ref() else {
|
||||
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
|
||||
return 0;
|
||||
};
|
||||
if arg1.is_array_buffer() == 0 {
|
||||
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let size = arg1.array_buffer_byte_length();
|
||||
let ptr = arg1.array_buffer_data();
|
||||
let data = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, size) };
|
||||
|
||||
v8_context_get_current_context().send_message(MessageType::SendToNative, data);
|
||||
|
||||
return 1;
|
||||
}
|
||||
name => {
|
||||
tracing::error!("Unknown V8 function called: {}", name);
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut cef::sys::_cef_v8_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessV8HandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessV8HandlerImpl {
|
||||
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapV8Handler for RenderProcessV8HandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_request_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{AuthCallback, Browser, CefString, Frame, ImplRequest, ImplRequestHandler, Request, ResourceRequestHandler, WrapRequestHandler};
|
||||
use std::ffi::c_int;
|
||||
|
||||
use super::resource_request_handler::ResourceRequestHandlerImpl;
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
|
||||
pub(crate) struct RequestHandlerImpl {
|
||||
object: *mut RcImpl<_cef_request_handler_t, Self>,
|
||||
}
|
||||
|
||||
impl RequestHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRequestHandler for RequestHandlerImpl {
|
||||
fn on_before_browse(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, request: Option<&mut Request>, _user_gesture: c_int, _is_redirect: c_int) -> c_int {
|
||||
let Some(request) = request else { return 1 };
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
if url.starts_with(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) {
|
||||
0
|
||||
} else {
|
||||
tracing::warn!("Blocked navigation to: {}", url);
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_request_handler(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_frame: Option<&mut Frame>,
|
||||
_request: Option<&mut Request>,
|
||||
_is_navigation: c_int,
|
||||
_is_download: c_int,
|
||||
_request_initiator: Option<&CefString>,
|
||||
_disable_default_handling: Option<&mut c_int>,
|
||||
) -> Option<ResourceRequestHandler> {
|
||||
Some(ResourceRequestHandler::new(ResourceRequestHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn auth_credentials(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_origin_url: Option<&CefString>,
|
||||
_is_proxy: c_int,
|
||||
_host: Option<&CefString>,
|
||||
_port: c_int,
|
||||
_realm: Option<&CefString>,
|
||||
_scheme: Option<&CefString>,
|
||||
_callback: Option<&mut AuthCallback>,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_request_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RequestHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RequestHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRequestHandler for RequestHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_request_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Callback, CefString, ImplResourceHandler, ImplResponse, Request, ResourceReadCallback, Response, WrapResourceHandler};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::c_int;
|
||||
use std::io::Read;
|
||||
|
||||
use crate::resources::{Resource, ResourceReader};
|
||||
|
||||
pub(crate) struct ResourceHandlerImpl {
|
||||
object: *mut RcImpl<_cef_resource_handler_t, Self>,
|
||||
reader: Option<RefCell<ResourceReader>>,
|
||||
mimetype: Option<String>,
|
||||
}
|
||||
|
||||
impl ResourceHandlerImpl {
|
||||
pub fn new(resource: Option<Resource>) -> Self {
|
||||
if let Some(resource) = resource {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
reader: Some(resource.reader.into()),
|
||||
mimetype: resource.mimetype,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
reader: None,
|
||||
mimetype: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplResourceHandler for ResourceHandlerImpl {
|
||||
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
|
||||
if let Some(handle_request) = handle_request {
|
||||
*handle_request = 1;
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
|
||||
if let Some(response_length) = response_length {
|
||||
*response_length = -1; // Indicating that the length is unknown
|
||||
}
|
||||
if let Some(response) = response {
|
||||
if self.reader.is_some() {
|
||||
if let Some(mimetype) = &self.mimetype {
|
||||
response.set_mime_type(Some(&mimetype.as_str().into()));
|
||||
} else {
|
||||
response.set_mime_type(None);
|
||||
}
|
||||
response.set_status(200);
|
||||
} else {
|
||||
response.set_status(404);
|
||||
response.set_mime_type(Some(&"text/plain".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
|
||||
let Some(bytes_read) = bytes_read else { unreachable!() };
|
||||
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
|
||||
if let Some(reader) = &self.reader {
|
||||
if let Ok(read) = reader.borrow_mut().read(out) {
|
||||
*bytes_read = read as i32;
|
||||
if read > 0 {
|
||||
return 1; // Indicating that data was read
|
||||
}
|
||||
} else {
|
||||
*bytes_read = -2; // Indicating ERR_FAILED
|
||||
}
|
||||
}
|
||||
0 // Indicating no data was read
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_resource_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ResourceHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
reader: self.reader.clone(),
|
||||
mimetype: self.mimetype.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for ResourceHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapResourceHandler for ResourceHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_resource_request_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Browser, Callback, CefString, Frame, ImplRequest, ImplResourceRequestHandler, Request, ReturnValue, WrapResourceRequestHandler};
|
||||
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
|
||||
// TODO: Deny all external requests once we stop relying on google fonts for font preview
|
||||
fn is_allowed_url(url: &str) -> bool {
|
||||
url.starts_with(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) || url.starts_with("https://fonts.googleapis.com/css2") || url.starts_with("https://fonts.gstatic.com/")
|
||||
}
|
||||
|
||||
pub(crate) struct ResourceRequestHandlerImpl {
|
||||
object: *mut RcImpl<_cef_resource_request_handler_t, Self>,
|
||||
}
|
||||
|
||||
impl ResourceRequestHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplResourceRequestHandler for ResourceRequestHandlerImpl {
|
||||
fn on_before_resource_load(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, request: Option<&mut Request>, _callback: Option<&mut Callback>) -> ReturnValue {
|
||||
let Some(request) = request else { return ReturnValue::CANCEL };
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
if is_allowed_url(&url) {
|
||||
ReturnValue::CONTINUE
|
||||
} else {
|
||||
tracing::error!("Blocked resource load: {}", url);
|
||||
ReturnValue::CANCEL
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_resource_request_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ResourceRequestHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for ResourceRequestHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapResourceRequestHandler for ResourceRequestHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_request_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
|
||||
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
|
||||
|
||||
use super::resource_handler::ResourceHandlerImpl;
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
use crate::delegate::BrowserDelegate;
|
||||
|
||||
pub(crate) struct SchemeHandlerFactoryImpl {
|
||||
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
impl SchemeHandlerFactoryImpl {
|
||||
pub(crate) fn new(delegate: BrowserDelegate) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
|
||||
if let Some(registrar) = registrar {
|
||||
let mut scheme_options = 0;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
|
||||
registrar.add_custom_scheme(Some(&RESOURCE_SCHEME.into()), scheme_options);
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl {
|
||||
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
|
||||
if let Some(request) = request {
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
let path = url
|
||||
.strip_prefix(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"))
|
||||
.expect("CEF should only call this for our custom scheme and domain that we registered this factory for");
|
||||
let resource = self.delegate.load_resource(path.to_string().into());
|
||||
return Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)));
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SchemeHandlerFactoryImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for SchemeHandlerFactoryImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_task_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplTask, WrapTask};
|
||||
use std::cell::RefCell;
|
||||
|
||||
// Closure-based task wrapper following CEF patterns
|
||||
pub struct ClosureTask<F> {
|
||||
pub(crate) object: *mut RcImpl<_cef_task_t, Self>,
|
||||
pub(crate) closure: RefCell<Option<F>>,
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> ClosureTask<F> {
|
||||
pub fn new(closure: F) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
closure: RefCell::new(Some(closure)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> ImplTask for ClosureTask<F> {
|
||||
fn execute(&self) {
|
||||
if let Some(closure) = self.closure.borrow_mut().take() {
|
||||
closure();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_task_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> Clone for ClosureTask<F> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
if !self.object.is_null() {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
closure: RefCell::new(None), // Closure can only be executed once
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> Rc for ClosureTask<F> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> WrapTask for ClosureTask<F> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_task_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// TODO: Consider inlining this file into internal
|
||||
|
||||
use cef::{Frame, ImplBinaryValue, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||
|
||||
pub(crate) enum MessageType {
|
||||
Initialized,
|
||||
SendToJS,
|
||||
SendToNative,
|
||||
}
|
||||
impl From<MessageType> for MessageInfo {
|
||||
fn from(val: MessageType) -> Self {
|
||||
match val {
|
||||
MessageType::Initialized => MessageInfo {
|
||||
name: "initialized".to_string(),
|
||||
target: cef_process_id_t::PID_BROWSER.into(),
|
||||
},
|
||||
MessageType::SendToJS => MessageInfo {
|
||||
name: "send_to_js".to_string(),
|
||||
target: cef_process_id_t::PID_RENDERER.into(),
|
||||
},
|
||||
MessageType::SendToNative => MessageInfo {
|
||||
name: "send_to_native".to_string(),
|
||||
target: cef_process_id_t::PID_BROWSER.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryFrom<String> for MessageType {
|
||||
type Error = ();
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"initialized" => Ok(MessageType::Initialized),
|
||||
"send_to_js" => Ok(MessageType::SendToJS),
|
||||
"send_to_native" => Ok(MessageType::SendToNative),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MessageInfo {
|
||||
name: String,
|
||||
target: ProcessId,
|
||||
}
|
||||
|
||||
pub(crate) trait SendMessage {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]);
|
||||
}
|
||||
impl SendMessage for Option<V8Context> {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(context) = self else {
|
||||
tracing::error!("Current V8 context is not available, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
context.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for V8Context {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(frame) = self.frame() else {
|
||||
tracing::error!("Current V8 context does not have a frame, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
frame.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for Frame {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let MessageInfo { name, target } = message_type.into();
|
||||
|
||||
let Some(mut process_message) = cef::process_message_create(Some(&name.as_str().into())) else {
|
||||
tracing::error!("Failed to create process message: {}", name);
|
||||
return;
|
||||
};
|
||||
let Some(arg_list) = process_message.argument_list() else { return };
|
||||
let mut value = ::cef::binary_value_create(Some(message));
|
||||
arg_list.set_binary(0, value.as_mut());
|
||||
|
||||
self.send_process_message(target, Some(&mut process_message));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UnpackedMessage<'a> {
|
||||
pub(crate) message_type: MessageType,
|
||||
pub(crate) data: &'a [u8],
|
||||
}
|
||||
|
||||
trait Sealed {}
|
||||
impl Sealed for cef::ProcessMessage {}
|
||||
#[allow(private_bounds)]
|
||||
pub(crate) trait UnpackMessage: Sealed {
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the message is valid.
|
||||
/// Message should come from cef.
|
||||
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>>;
|
||||
}
|
||||
impl UnpackMessage for cef::ProcessMessage {
|
||||
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>> {
|
||||
let pointer: *mut cef::sys::_cef_string_utf16_t = self.name().into();
|
||||
let message = unsafe { super::utility::pointer_to_string(pointer) };
|
||||
let Ok(message_type) = message.try_into() else {
|
||||
tracing::error!("Failed to get message type from process message");
|
||||
return None;
|
||||
};
|
||||
let arglist = self.argument_list()?;
|
||||
let binary = arglist.binary(0)?;
|
||||
let size = binary.size();
|
||||
let ptr = binary.raw_data();
|
||||
let buffer = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) };
|
||||
Some(UnpackedMessage { message_type, data: buffer })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
use std::process::ExitCode;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{Receiver, RecvTimeoutError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
use crate::remote::spawn::HostHandle;
|
||||
|
||||
mod consts;
|
||||
mod context;
|
||||
mod delegate;
|
||||
mod dirs;
|
||||
mod events;
|
||||
mod frames;
|
||||
mod input;
|
||||
mod internal;
|
||||
mod ipc;
|
||||
mod platform;
|
||||
mod remote;
|
||||
mod resources;
|
||||
mod utility;
|
||||
mod view;
|
||||
|
||||
pub struct UiContext<S: Stage = Started> {
|
||||
inner: S::ContextData,
|
||||
}
|
||||
|
||||
impl UiContext<Setup> {
|
||||
pub fn setup() -> UiSetupResult {
|
||||
#[cfg(target_os = "macos")]
|
||||
ipc_channel::set_bootstrap_prefix(consts::IPC_BOOTSTRAP_PREFIX);
|
||||
|
||||
let raw_args: Vec<String> = std::env::args().collect();
|
||||
if raw_args.iter().any(|arg| arg.starts_with(consts::BROWSER_HOST_CONFIG_FLAG)) {
|
||||
remote::host::run();
|
||||
return UiSetupResult::Helper(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if raw_args.iter().any(|arg| arg.starts_with("--type=")) {
|
||||
return UiSetupResult::Helper(run_helper());
|
||||
}
|
||||
UiSetupResult::Ready(UiContext { inner: () })
|
||||
}
|
||||
|
||||
pub fn start(self, config: UiConfig) -> Result<UiContext<Started>, UiError> {
|
||||
let acceleration = platform::accelerated_paint(matches!(config.acceleration, Acceleration::Disabled));
|
||||
let handle = remote::spawn::spawn_host(acceleration)?;
|
||||
Ok(UiContext { inner: Arc::new(handle) })
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub enum UiSetupResult {
|
||||
Ready(UiContext<Setup>),
|
||||
Failed,
|
||||
Helper(ExitCode),
|
||||
}
|
||||
|
||||
impl UiContext<Started> {
|
||||
pub fn instance(&self, device: &wgpu::Device, queue: &wgpu_sync::Queue) -> Result<UiInstance, UiError> {
|
||||
let surface = frames::FrameSurface::new(device.clone(), queue.clone());
|
||||
|
||||
let (queue, events) = events::EventQueue::new();
|
||||
let shutdown_complete = remote::spawn::start_instance(&self.inner, surface, queue.clone())?;
|
||||
|
||||
Ok(UiInstance {
|
||||
inner: Arc::new(UiInstanceInner {
|
||||
host: self.inner.clone(),
|
||||
input: Mutex::new(input::InputState::default()),
|
||||
events: Mutex::new(events),
|
||||
queue,
|
||||
shutdown_complete: Mutex::new(shutdown_complete),
|
||||
shutdown_started: AtomicBool::new(false),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for UiContext<Started> {
|
||||
fn clone(&self) -> Self {
|
||||
UiContext { inner: self.inner.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Setup {}
|
||||
pub enum Started {}
|
||||
|
||||
#[expect(private_bounds)]
|
||||
pub trait Stage: Sealed {}
|
||||
impl Stage for Setup {}
|
||||
impl Stage for Started {}
|
||||
trait Sealed {
|
||||
type ContextData;
|
||||
}
|
||||
impl Sealed for Setup {
|
||||
type ContextData = ();
|
||||
}
|
||||
impl Sealed for Started {
|
||||
type ContextData = Arc<HostHandle>;
|
||||
}
|
||||
|
||||
pub fn temp_dir_root() -> std::path::PathBuf {
|
||||
dirs::app_tmp_dir()
|
||||
}
|
||||
|
||||
pub fn run_helper() -> ExitCode {
|
||||
context::execute_helper_process()
|
||||
}
|
||||
|
||||
pub struct UiConfig {
|
||||
pub acceleration: Acceleration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Acceleration {
|
||||
Auto,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum UiError {
|
||||
#[error("failed to bootstrap the UI backend: {0}")]
|
||||
Bootstrap(String),
|
||||
#[error("failed to spawn the UI backend host process: {0}")]
|
||||
Spawn(std::io::Error),
|
||||
#[error("the UI backend host process exited during startup: {0}")]
|
||||
HostExited(String),
|
||||
#[error("timed out waiting for the UI backend host process to connect")]
|
||||
HandshakeTimeout,
|
||||
#[error("UI backend handshake failed: {0}")]
|
||||
Handshake(String),
|
||||
#[error("the UI runtime already drives an instance")]
|
||||
InstanceLimit,
|
||||
}
|
||||
|
||||
pub struct UiInstance {
|
||||
inner: Arc<UiInstanceInner>,
|
||||
}
|
||||
|
||||
pub(crate) struct UiInstanceInner {
|
||||
host: Arc<HostHandle>,
|
||||
input: Mutex<input::InputState>,
|
||||
events: Mutex<Receiver<UiEvent>>,
|
||||
queue: events::EventQueue,
|
||||
shutdown_complete: Mutex<Receiver<()>>,
|
||||
shutdown_started: AtomicBool,
|
||||
}
|
||||
|
||||
impl UiInstance {
|
||||
pub fn send(&self, command: UiCommand) {
|
||||
let shared = &self.inner;
|
||||
match command {
|
||||
UiCommand::Input(event) => {
|
||||
let events = {
|
||||
let Ok(mut input) = shared.input.lock() else {
|
||||
tracing::error!("Failed to lock the input state");
|
||||
return;
|
||||
};
|
||||
input::translate(&mut input, &event)
|
||||
};
|
||||
if !events.is_empty() {
|
||||
shared.host.send(HostControlMessage::Input(events));
|
||||
}
|
||||
}
|
||||
UiCommand::Resized { width, height } => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Size { width, height })),
|
||||
UiCommand::ScaleChanged(scale) => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Scale(scale))),
|
||||
UiCommand::Refresh => shared.host.send(HostControlMessage::RefreshViewInfo),
|
||||
UiCommand::Message(message) => shared.host.send(HostControlMessage::SendWebMessage(message)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv(&self) -> Option<UiEvent> {
|
||||
let shared = &self.inner;
|
||||
let Ok(receiver) = shared.events.lock() else {
|
||||
return None;
|
||||
};
|
||||
loop {
|
||||
match receiver.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(event) => return Some(event),
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
if shared.queue.is_terminated() {
|
||||
return receiver.try_recv().ok();
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for UiInstance {
|
||||
fn clone(&self) -> Self {
|
||||
UiInstance { inner: self.inner.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl UiInstanceInner {
|
||||
fn shutdown(&self) {
|
||||
if self.shutdown_started.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let Ok(receiver) = self.shutdown_complete.lock() {
|
||||
self.host.shutdown(&receiver);
|
||||
}
|
||||
self.queue.mark_terminated();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UiInstanceInner {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UiCommand {
|
||||
Input(winit::event::WindowEvent),
|
||||
Resized { width: u32, height: u32 },
|
||||
ScaleChanged(f64),
|
||||
Refresh,
|
||||
Message(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UiEvent {
|
||||
Ready,
|
||||
Frame(wgpu::Texture),
|
||||
Cursor(Cursor),
|
||||
Message(Vec<u8>),
|
||||
Failure(String),
|
||||
Crashed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Cursor {
|
||||
Icon(winit::cursor::CursorIcon),
|
||||
Custom { rgba: Vec<u8>, width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<winit::cursor::CursorIcon> for Cursor {
|
||||
fn from(icon: winit::cursor::CursorIcon) -> Self {
|
||||
Cursor::Icon(icon)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod mac;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod win;
|
||||
|
||||
pub(crate) fn accelerated_paint(disable_gpu_acceleration: bool) -> bool {
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
{
|
||||
!disable_gpu_acceleration && should_enable_hardware_acceleration()
|
||||
}
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
{
|
||||
let _ = disable_gpu_acceleration;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn should_enable_hardware_acceleration() -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Check if running on Wayland or X11
|
||||
let has_wayland = std::env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|var| !var.is_empty())
|
||||
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
|
||||
.filter(|var| !var.is_empty())
|
||||
.is_some();
|
||||
|
||||
let has_x11 = std::env::var("DISPLAY").ok().filter(|var| !var.is_empty()).is_some();
|
||||
|
||||
if !has_wayland && !has_x11 {
|
||||
tracing::warn!("No display server detected, disabling hardware acceleration");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for NVIDIA proprietary driver (known to have issues)
|
||||
if let Ok(driver_info) = std::fs::read_to_string("/proc/driver/nvidia/version")
|
||||
&& driver_info.contains("NVIDIA")
|
||||
{
|
||||
tracing::warn!("NVIDIA proprietary driver detected, hardware acceleration may be unstable");
|
||||
// Still return true but with warning
|
||||
}
|
||||
|
||||
// Check for basic GPU capabilities
|
||||
if has_wayland {
|
||||
tracing::info!("Wayland detected, enabling hardware acceleration");
|
||||
true
|
||||
} else if has_x11 {
|
||||
tracing::info!("X11 detected, enabling hardware acceleration");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows generally has good D3D11 support
|
||||
tracing::info!("Windows detected, enabling hardware acceleration");
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// macOS has good Metal/IOSurface support
|
||||
tracing::info!("macOS detected, enabling hardware acceleration");
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
tracing::warn!("Unsupported platform for hardware acceleration");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use crate::frames::plane;
|
||||
|
||||
pub(crate) fn setup_command(command: &mut Command, #[cfg(feature = "accelerated_paint")] host_frame_fd: Option<std::os::fd::RawFd>) {
|
||||
let main_pid = std::process::id() as libc::pid_t;
|
||||
// SAFETY: the closure runs in the forked child before exec and only makes async-signal-safe calls
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
// Tie the host lifetime to the main process
|
||||
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
if libc::getppid() != main_pid {
|
||||
return Err(std::io::Error::from_raw_os_error(libc::ESRCH));
|
||||
}
|
||||
|
||||
// Move the host end of the frame socket to its advertised fd
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
if let Some(fd) = host_frame_fd {
|
||||
let target = plane::FRAME_SOCKET_CHILD_FD;
|
||||
if fd == target {
|
||||
if libc::fcntl(target, libc::F_SETFD, 0) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
} else if libc::dup2(fd, target) == -1 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::Bool;
|
||||
use objc2::{ClassType, define_class, msg_send};
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEvent, NSResponder};
|
||||
use objc2_foundation::NSObject;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol};
|
||||
|
||||
static HANDLING_SEND_EVENT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
||||
#[name = "GraphiteCefHostApplication"]
|
||||
struct CefHostApplication;
|
||||
|
||||
unsafe impl CrAppProtocol for CefHostApplication {
|
||||
#[unsafe(method(isHandlingSendEvent))]
|
||||
fn is_handling_send_event(&self) -> Bool {
|
||||
Bool::new(HANDLING_SEND_EVENT.load(Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl CrAppControlProtocol for CefHostApplication {
|
||||
#[unsafe(method(setHandlingSendEvent:))]
|
||||
fn set_handling_send_event(&self, handling: Bool) {
|
||||
HANDLING_SEND_EVENT.store(handling.as_bool(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl CefAppProtocol for CefHostApplication {}
|
||||
|
||||
impl CefHostApplication {
|
||||
#[unsafe(method(sendEvent:))]
|
||||
fn send_event(&self, event: &NSEvent) {
|
||||
let was_handling = HANDLING_SEND_EVENT.swap(true, Ordering::Relaxed);
|
||||
let _: () = unsafe { msg_send![super(self), sendEvent: event] };
|
||||
HANDLING_SEND_EVENT.store(was_handling, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
pub(crate) fn install_application() {
|
||||
let app: Retained<NSApplication> = unsafe { msg_send![CefHostApplication::class(), sharedApplication] };
|
||||
app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited);
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_parent_watchdog(main_pid: u32) {
|
||||
let result = std::thread::Builder::new().name("parent-watchdog".to_string()).spawn(move || {
|
||||
loop {
|
||||
// SAFETY: getppid is always safe to call.
|
||||
if unsafe { libc::getppid() } as u32 != main_pid {
|
||||
tracing::warn!("Parent process is gone, exiting...");
|
||||
std::process::exit(0);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
});
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to spawn parent watchdog: {e}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::System::JobObjects::{
|
||||
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, SetInformationJobObject,
|
||||
};
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
pub(crate) struct KillOnCloseJob(HANDLE);
|
||||
|
||||
// SAFETY: job object handles may be used and closed from any thread.
|
||||
unsafe impl Send for KillOnCloseJob {}
|
||||
unsafe impl Sync for KillOnCloseJob {}
|
||||
|
||||
impl KillOnCloseJob {
|
||||
pub(crate) fn assign(child: &std::process::Child) -> windows::core::Result<Self> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
unsafe {
|
||||
let job = CreateJobObjectW(None, PCWSTR::null())?;
|
||||
let job = Self(job);
|
||||
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
|
||||
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(
|
||||
job.0,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&info as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
)?;
|
||||
AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle()))?;
|
||||
Ok(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for KillOnCloseJob {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = CloseHandle(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::consts::BROWSER_HOST_CONFIG_FLAG;
|
||||
|
||||
pub(crate) mod host;
|
||||
pub(crate) mod messages;
|
||||
pub(crate) mod spawn;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct HostConfig {
|
||||
pub(crate) server: String,
|
||||
pub(crate) main_pid: u32,
|
||||
pub(crate) acceleration: bool,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) frame_socket_fd: Option<std::os::fd::RawFd>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) frame_service: Option<String>,
|
||||
}
|
||||
|
||||
impl HostConfig {
|
||||
pub(crate) fn to_arg(&self) -> String {
|
||||
let json = serde_json::to_string(self).expect("HostConfig always serializes");
|
||||
format!("{BROWSER_HOST_CONFIG_FLAG}{json}")
|
||||
}
|
||||
|
||||
pub(crate) fn from_args(args: &[String]) -> Option<Self> {
|
||||
let json = args.iter().find_map(|arg| arg.strip_prefix(BROWSER_HOST_CONFIG_FLAG))?;
|
||||
match serde_json::from_str(json) {
|
||||
Ok(config) => Some(config),
|
||||
Err(e) => {
|
||||
tracing::error!("Malformed host config on the command line: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::HostConfig;
|
||||
use super::messages::{EventMessage, HostControlMessage};
|
||||
use crate::context::{CefContext, CefContextHandle};
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::frames::FrameStreamer;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use crate::frames::plane::PlaneSender;
|
||||
use crate::frames::sequence::SequenceState;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::platform::mac;
|
||||
|
||||
pub(crate) fn run() {
|
||||
// Ignore SIGINT, the controlling process is responsible for shutting down the host
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
unsafe {
|
||||
libc::signal(libc::SIGINT, libc::SIG_IGN);
|
||||
}
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let config = HostConfig::from_args(&args).expect("CEF host started without a valid host config argument");
|
||||
let acceleration_requested = config.acceleration;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::spawn_parent_watchdog(config.main_pid);
|
||||
|
||||
let bootstrap = IpcSender::<EventMessage>::connect(config.server.clone()).expect("Failed to connect to the main process bootstrap server");
|
||||
let event_sender = Arc::new(Mutex::new(bootstrap));
|
||||
let (control_sender, control_receiver) = ipc_channel::ipc::channel::<HostControlMessage>().expect("Failed to create control channel");
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
let plane = if acceleration_requested { PlaneSender::from_config(&config, event_sender.clone()) } else { None };
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
let acceleration = plane.is_some();
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
let acceleration = {
|
||||
if acceleration_requested {
|
||||
tracing::error!("UI acceleration requested but the accelerated_paint feature is disabled; using software frames");
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
event_sender
|
||||
.lock()
|
||||
.expect("The host message sender cannot be poisoned before threads exist")
|
||||
.send(EventMessage::Hello {
|
||||
pid: std::process::id(),
|
||||
control_sender,
|
||||
acceleration,
|
||||
})
|
||||
.expect("Failed to send Hello to the main process");
|
||||
|
||||
let sequence = Arc::new(SequenceState::new());
|
||||
let frames = FrameStreamer::new(
|
||||
event_sender.clone(),
|
||||
sequence.clone(),
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane,
|
||||
);
|
||||
let (view_info_sender, view_info_receiver) = std::sync::mpsc::channel();
|
||||
let delegate = BrowserDelegate::new(event_sender.clone(), view_info_receiver);
|
||||
|
||||
let context = match CefContext::create(delegate, frames, view_info_sender, acceleration) {
|
||||
Ok(context) => {
|
||||
if let Ok(sender) = event_sender.lock() {
|
||||
let _ = sender.send(EventMessage::BrowserCreated);
|
||||
}
|
||||
context
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("CEF initialization failed in host process: {e}");
|
||||
if let Ok(sender) = event_sender.lock() {
|
||||
let _ = sender.send(EventMessage::InitFailed(e));
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let outcome = context.run(move |handle| control_loop(&control_receiver, &handle, sequence.as_ref()));
|
||||
|
||||
match outcome {
|
||||
ControlOutcome::Shutdown => {
|
||||
tracing::debug!("Shut down CEF host");
|
||||
if let Ok(sender) = event_sender.lock() {
|
||||
let _ = sender.send(EventMessage::ShutdownComplete);
|
||||
}
|
||||
}
|
||||
ControlOutcome::Disconnected => std::process::exit(0),
|
||||
}
|
||||
}
|
||||
|
||||
enum ControlOutcome {
|
||||
Shutdown,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
fn control_loop(receiver: &IpcReceiver<HostControlMessage>, context: &CefContextHandle, sequence: &SequenceState) -> ControlOutcome {
|
||||
loop {
|
||||
match receiver.recv() {
|
||||
Ok(HostControlMessage::Input(events)) => context.apply_input(events),
|
||||
Ok(HostControlMessage::UpdateViewInfo(update)) => context.update_view_info(update),
|
||||
Ok(HostControlMessage::RefreshViewInfo) => context.refresh_view_info(),
|
||||
Ok(HostControlMessage::SendWebMessage(message)) => context.send_web_message(message),
|
||||
Ok(HostControlMessage::FrameAck { seq }) => sequence.ack(seq),
|
||||
Ok(HostControlMessage::Shutdown) => return ControlOutcome::Shutdown,
|
||||
Err(ipc_channel::IpcError::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("Control channel closed ({e:?}), shutting down CEF host");
|
||||
return ControlOutcome::Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Cursor;
|
||||
use crate::context::InitError;
|
||||
use crate::input::InputEvent;
|
||||
use crate::view::ViewInfoUpdate;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) enum HostControlMessage {
|
||||
Input(Vec<InputEvent>),
|
||||
UpdateViewInfo(ViewInfoUpdate),
|
||||
RefreshViewInfo,
|
||||
SendWebMessage(Vec<u8>),
|
||||
FrameAck { seq: u64 },
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) enum EventMessage {
|
||||
Hello {
|
||||
pid: u32,
|
||||
control_sender: IpcSender<HostControlMessage>,
|
||||
acceleration: bool,
|
||||
},
|
||||
BrowserCreated,
|
||||
InitFailed(InitError),
|
||||
WebCommunicationInitialized,
|
||||
WebMessage(Vec<u8>),
|
||||
CursorChange(Cursor),
|
||||
AdvertiseFrameSegment {
|
||||
index: u32,
|
||||
shm: IpcSharedMemory,
|
||||
},
|
||||
SoftwareFrame {
|
||||
seq: u64,
|
||||
segment: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
},
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
AcceleratedFrame {
|
||||
seq: u64,
|
||||
handle: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content: Option<crate::frames::import::ContentRect>,
|
||||
},
|
||||
ShutdownComplete,
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{self, RecvTimeoutError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::HostConfig;
|
||||
use super::messages::{EventMessage, HostControlMessage};
|
||||
use crate::consts::{HOST_HELLO_TIMEOUT, HOST_SHUTDOWN_TIMEOUT};
|
||||
use crate::events::EventQueue;
|
||||
use crate::frames::FrameSurface;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use crate::frames::plane;
|
||||
use crate::frames::receive::{FrameConsumer, PendingFrame, SegmentTable};
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::platform;
|
||||
use crate::{UiError, UiEvent};
|
||||
|
||||
pub(crate) struct HostHandle {
|
||||
sender: IpcSender<HostControlMessage>,
|
||||
receivers: Mutex<Option<InstanceReceivers>>,
|
||||
child: Arc<Mutex<Child>>,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
died_reported: Arc<AtomicBool>,
|
||||
#[cfg_attr(any(not(feature = "accelerated_paint"), target_os = "windows"), expect(dead_code))]
|
||||
host_acceleration: bool,
|
||||
#[cfg(target_os = "windows")]
|
||||
_job: Option<platform::win::KillOnCloseJob>,
|
||||
}
|
||||
|
||||
struct InstanceReceivers {
|
||||
events: IpcReceiver<EventMessage>,
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
frame_plane: Option<plane::PlaneReceiver>,
|
||||
}
|
||||
|
||||
impl HostHandle {
|
||||
pub(crate) fn send(&self, message: HostControlMessage) {
|
||||
if let Err(e) = self.sender.send(message) {
|
||||
tracing::debug!("Failed to send message to CEF host: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self, shutdown_complete_receiver: &mpsc::Receiver<()>) {
|
||||
self.shutting_down.store(true, Ordering::SeqCst);
|
||||
let deadline = Instant::now() + HOST_SHUTDOWN_TIMEOUT;
|
||||
|
||||
if self.sender.send(HostControlMessage::Shutdown).is_ok() {
|
||||
match shutdown_complete_receiver.recv_timeout(HOST_SHUTDOWN_TIMEOUT) {
|
||||
Ok(()) => tracing::debug!("CEF host completed shutdown"),
|
||||
Err(RecvTimeoutError::Timeout) => tracing::warn!("Timed out waiting for the CEF host to shut down"),
|
||||
Err(RecvTimeoutError::Disconnected) => tracing::debug!("CEF host connection closed during shutdown"),
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.child.lock() {
|
||||
Ok(mut child) => match child.try_wait() {
|
||||
Ok(None) => {}
|
||||
Ok(Some(_)) | Err(_) => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
|
||||
tracing::warn!("CEF host did not exit in time, killing it");
|
||||
if let Ok(mut child) = self.child.lock() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HostHandle {
|
||||
fn drop(&mut self) {
|
||||
if !self.shutting_down.load(Ordering::SeqCst) {
|
||||
let _ = self.sender.send(HostControlMessage::Shutdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_host(acceleration: bool) -> Result<HostHandle, UiError> {
|
||||
let (server, server_name) = IpcOneShotServer::<EventMessage>::new().map_err(|e| UiError::Bootstrap(format!("failed to create the bootstrap server: {e}")))?;
|
||||
|
||||
let executable = std::env::current_exe().map_err(|e| UiError::Bootstrap(format!("failed to get the current executable path: {e}")))?;
|
||||
let mut command = Command::new(executable);
|
||||
|
||||
#[cfg_attr(not(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint")), expect(unused_mut))]
|
||||
let mut config = HostConfig {
|
||||
server: server_name,
|
||||
main_pid: std::process::id(),
|
||||
acceleration,
|
||||
#[cfg(target_os = "linux")]
|
||||
frame_socket_fd: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
frame_service: None,
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "accelerated_paint"))]
|
||||
let frame_socket = if acceleration {
|
||||
match plane::socketpair() {
|
||||
Ok((main_end, host_end)) => {
|
||||
config.frame_socket_fd = Some(plane::FRAME_SOCKET_CHILD_FD);
|
||||
Some((main_end, host_end))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create the accelerated frame socket, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "accelerated_paint"))]
|
||||
let frame_service = if acceleration {
|
||||
let name = format!("art.graphite.Graphite.cef-frames.{}.{:x}", std::process::id(), rand::random::<u64>());
|
||||
match plane::create_service(&name) {
|
||||
Ok(port) => {
|
||||
config.frame_service = Some(name);
|
||||
Some(port)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create the accelerated frame service, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
command.arg(config.to_arg());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
platform::linux::setup_command(
|
||||
&mut command,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
frame_socket.as_ref().map(|(_, host_end)| {
|
||||
use std::os::fd::AsRawFd;
|
||||
host_end.as_raw_fd()
|
||||
}),
|
||||
);
|
||||
|
||||
let mut child = command.spawn().map_err(UiError::Spawn)?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let job = match platform::win::KillOnCloseJob::assign(&child) {
|
||||
Ok(job) => Some(job),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to assign the CEF host to a job object (orphan prevention degraded): {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "accelerated_paint"))]
|
||||
let frame_plane = frame_socket.map(|(main_end, host_end)| {
|
||||
drop(host_end);
|
||||
plane::PlaneReceiver::new(main_end)
|
||||
});
|
||||
|
||||
let (hello_tx, hello_rx) = mpsc::channel();
|
||||
let accept_thread = std::thread::Builder::new().name("cef-host-accept".to_string()).spawn(move || {
|
||||
let _ = hello_tx.send(server.accept());
|
||||
});
|
||||
if let Err(e) = accept_thread {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Bootstrap(format!("failed to spawn the host accept thread: {e}")));
|
||||
}
|
||||
|
||||
let deadline = Instant::now() + HOST_HELLO_TIMEOUT;
|
||||
let (event_receiver, hello) = loop {
|
||||
match hello_rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(Ok(accepted)) => break accepted,
|
||||
Ok(Err(e)) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Handshake(format!("failed to accept the host connection: {e}")));
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
return Err(UiError::HostExited(status.to_string()));
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::HandshakeTimeout);
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Handshake("the accept thread disappeared".to_string()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let EventMessage::Hello {
|
||||
pid,
|
||||
control_sender,
|
||||
acceleration: host_acceleration,
|
||||
} = hello
|
||||
else {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Handshake("the first message from the host was not Hello".to_string()));
|
||||
};
|
||||
tracing::info!("CEF host process connected (pid {pid})");
|
||||
if acceleration && !host_acceleration {
|
||||
tracing::warn!("UI acceleration was requested but the CEF host could not set up its frame plane; falling back to software frames");
|
||||
}
|
||||
|
||||
Ok(HostHandle {
|
||||
sender: control_sender,
|
||||
receivers: Mutex::new(Some(InstanceReceivers {
|
||||
events: event_receiver,
|
||||
#[cfg(all(target_os = "linux", feature = "accelerated_paint"))]
|
||||
frame_plane,
|
||||
#[cfg(all(target_os = "macos", feature = "accelerated_paint"))]
|
||||
frame_plane: frame_service.map(plane::PlaneReceiver::new),
|
||||
})),
|
||||
child: Arc::new(Mutex::new(child)),
|
||||
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||
died_reported: Arc::new(AtomicBool::new(false)),
|
||||
host_acceleration,
|
||||
#[cfg(target_os = "windows")]
|
||||
_job: job,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn start_instance(handle: &HostHandle, surface: FrameSurface, events: EventQueue) -> Result<mpsc::Receiver<()>, UiError> {
|
||||
let receive_side = match handle.receivers.lock() {
|
||||
Ok(mut receive_side) => receive_side.take(),
|
||||
Err(_) => None,
|
||||
};
|
||||
let Some(receive_side) = receive_side else {
|
||||
return Err(UiError::InstanceLimit);
|
||||
};
|
||||
|
||||
let (shutdown_complete_sender, shutdown_complete_receiver) = mpsc::channel();
|
||||
let consumer = FrameConsumer::new(surface, events.clone(), handle.sender.clone());
|
||||
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
if let Some(receiver) = receive_side.frame_plane
|
||||
&& handle.host_acceleration
|
||||
{
|
||||
let consumer = consumer.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("cef-frames".to_string())
|
||||
.spawn(move || crate::frames::receive::plane_receiver_loop(receiver, consumer))
|
||||
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the frame receiver thread: {e}")))?;
|
||||
}
|
||||
|
||||
{
|
||||
let receiver = receive_side.events;
|
||||
let shutting_down = handle.shutting_down.clone();
|
||||
let died_reported = handle.died_reported.clone();
|
||||
let events = events.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("cef-host".to_string())
|
||||
.spawn(move || event_receiver_loop(receiver, consumer, events, shutting_down, died_reported, shutdown_complete_sender))
|
||||
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the host event receiver thread: {e}")))?;
|
||||
}
|
||||
|
||||
{
|
||||
let child = handle.child.clone();
|
||||
let shutting_down = handle.shutting_down.clone();
|
||||
let died_reported = handle.died_reported.clone();
|
||||
let events = events.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("cef-host-supervisor".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
let status = match child.lock() {
|
||||
Ok(mut child) => child.try_wait(),
|
||||
Err(_) => return,
|
||||
};
|
||||
match status {
|
||||
Ok(None) => std::thread::sleep(Duration::from_millis(100)),
|
||||
Ok(Some(status)) => {
|
||||
if shutting_down.load(Ordering::SeqCst) {
|
||||
tracing::debug!("CEF host exited during shutdown: {status}");
|
||||
} else {
|
||||
report_host_died(&died_reported, &events, &format!("CEF host process exited unexpectedly: {status}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the host supervisor thread: {e}")))?;
|
||||
}
|
||||
|
||||
Ok(shutdown_complete_receiver)
|
||||
}
|
||||
|
||||
fn report_host_died(died_reported: &AtomicBool, events: &EventQueue, message: &str) {
|
||||
if died_reported.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
tracing::error!("{message}");
|
||||
events.terminate(UiEvent::Crashed);
|
||||
}
|
||||
|
||||
fn event_receiver_loop(
|
||||
receiver: IpcReceiver<EventMessage>,
|
||||
consumer: FrameConsumer,
|
||||
events: EventQueue,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
died_reported: Arc<AtomicBool>,
|
||||
shutdown_complete_sender: mpsc::Sender<()>,
|
||||
) {
|
||||
let mut newest_frame: Option<PendingFrame> = None;
|
||||
let mut segments = SegmentTable::new();
|
||||
|
||||
loop {
|
||||
let message = match receiver.recv() {
|
||||
Ok(message) => message,
|
||||
Err(ipc_channel::IpcError::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => {
|
||||
if !shutting_down.load(Ordering::SeqCst) {
|
||||
report_host_died(&died_reported, &events, &format!("Lost connection to the CEF host process: {e:?}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
handle_message(message, &events, &shutting_down, &died_reported, &shutdown_complete_sender, &mut newest_frame, &mut segments);
|
||||
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(message) => handle_message(message, &events, &shutting_down, &died_reported, &shutdown_complete_sender, &mut newest_frame, &mut segments),
|
||||
Err(ipc_channel::TryRecvError::Empty) => break,
|
||||
Err(ipc_channel::TryRecvError::IpcError(ipc_channel::IpcError::Io(ref io))) if io.kind() == std::io::ErrorKind::Interrupted => break,
|
||||
Err(ipc_channel::TryRecvError::IpcError(e)) => {
|
||||
if !shutting_down.load(Ordering::SeqCst) {
|
||||
report_host_died(&died_reported, &events, &format!("Lost connection to the CEF host process: {e:?}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(frame) = newest_frame.take() {
|
||||
consumer.deliver_pending(frame, &segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(
|
||||
message: EventMessage,
|
||||
events: &EventQueue,
|
||||
shutting_down: &AtomicBool,
|
||||
died_reported: &AtomicBool,
|
||||
shutdown_complete_sender: &mpsc::Sender<()>,
|
||||
newest_frame: &mut Option<PendingFrame>,
|
||||
segments: &mut SegmentTable,
|
||||
) {
|
||||
match message {
|
||||
EventMessage::Hello { .. } => tracing::error!("Unexpected second Hello from the CEF host"),
|
||||
EventMessage::BrowserCreated => tracing::info!("CEF host created the browser"),
|
||||
EventMessage::InitFailed(e) => {
|
||||
tracing::error!("CEF initialization failed in the host process: {e}");
|
||||
died_reported.store(true, Ordering::SeqCst);
|
||||
events.terminate(UiEvent::Failure(e.to_string()));
|
||||
}
|
||||
EventMessage::WebCommunicationInitialized => events.send(UiEvent::Ready),
|
||||
EventMessage::WebMessage(message) => events.send(UiEvent::Message(message)),
|
||||
EventMessage::CursorChange(cursor) => events.send(UiEvent::Cursor(cursor)),
|
||||
EventMessage::AdvertiseFrameSegment { index, shm } => segments.advertise(index, shm),
|
||||
EventMessage::SoftwareFrame { seq, segment, width, height } => {
|
||||
let frame = PendingFrame::Software { seq, segment, width, height };
|
||||
if newest_frame.as_ref().is_none_or(|newest| newest.seq() < frame.seq()) {
|
||||
*newest_frame = Some(frame);
|
||||
}
|
||||
}
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
EventMessage::AcceleratedFrame {
|
||||
seq,
|
||||
handle,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
content,
|
||||
} => {
|
||||
let frame = PendingFrame::Accelerated(plane::WireFrame::new(seq, handle, width, height, format, content));
|
||||
if newest_frame.as_ref().is_none_or(|newest| newest.seq() < frame.seq()) {
|
||||
*newest_frame = Some(frame);
|
||||
}
|
||||
}
|
||||
EventMessage::ShutdownComplete => {
|
||||
shutting_down.store(true, Ordering::SeqCst);
|
||||
let _ = shutdown_complete_sender.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use std::fs::File;
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Resource {
|
||||
pub(crate) reader: ResourceReader,
|
||||
pub(crate) mimetype: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ResourceReader {
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
Embedded(io::Cursor<&'static [u8]>),
|
||||
File(Arc<File>),
|
||||
}
|
||||
impl Read for ResourceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
ResourceReader::Embedded(cursor) => cursor.read(buf),
|
||||
ResourceReader::File(file) => file.as_ref().read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum WebResources {
|
||||
Embedded,
|
||||
External(PathBuf),
|
||||
}
|
||||
|
||||
pub(crate) fn load(path: PathBuf) -> Option<Resource> {
|
||||
if path.components().any(|component| matches!(component, Component::ParentDir)) {
|
||||
tracing::error!("Rejected resource path with a parent directory component: {path:?}");
|
||||
return None;
|
||||
}
|
||||
|
||||
let resources = if cfg!(feature = "embedded_resources") {
|
||||
WebResources::Embedded
|
||||
} else {
|
||||
match std::env::var("GRAPHITE_RESOURCES") {
|
||||
Ok(dir) => WebResources::External(dir.into()),
|
||||
Err(_) => {
|
||||
tracing::error!("GRAPHITE_RESOURCES must point to the frontend assets when embedded resources are disabled");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path };
|
||||
|
||||
let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") {
|
||||
"html" => Some("text/html".to_string()),
|
||||
"css" => Some("text/css".to_string()),
|
||||
"txt" => Some("text/plain".to_string()),
|
||||
"wasm" => Some("application/wasm".to_string()),
|
||||
"js" => Some("application/javascript".to_string()),
|
||||
"png" => Some("image/png".to_string()),
|
||||
"jpg" | "jpeg" => Some("image/jpeg".to_string()),
|
||||
"svg" => Some("image/svg+xml".to_string()),
|
||||
"xml" => Some("application/xml".to_string()),
|
||||
"json" => Some("application/json".to_string()),
|
||||
"ico" => Some("image/x-icon".to_string()),
|
||||
"woff" => Some("font/woff".to_string()),
|
||||
"woff2" => Some("font/woff2".to_string()),
|
||||
"ttf" => Some("font/ttf".to_string()),
|
||||
"otf" => Some("font/otf".to_string()),
|
||||
"webmanifest" => Some("application/manifest+json".to_string()),
|
||||
"graphite" => Some("application/graphite+json".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match resources {
|
||||
WebResources::Embedded => {
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
{
|
||||
if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES
|
||||
&& let Some(file) = resources.get_file(&path)
|
||||
{
|
||||
return Some(Resource {
|
||||
reader: ResourceReader::Embedded(io::Cursor::new(file.contents())),
|
||||
mimetype,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
#[cfg(not(feature = "embedded_resources"))]
|
||||
{
|
||||
tracing::error!("Embedded resources requested but the embedded_resources feature is disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
WebResources::External(dir) => {
|
||||
let file_path = dir.join(path.strip_prefix("/").unwrap_or(&path));
|
||||
if file_path.exists()
|
||||
&& file_path.is_file()
|
||||
&& let Ok(file) = std::fs::File::open(file_path)
|
||||
{
|
||||
return Some(Resource {
|
||||
reader: ResourceReader::File(file.into()),
|
||||
mimetype,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub unsafe fn pointer_to_string(pointer: *mut cef::sys::_cef_string_utf16_t) -> String {
|
||||
let str = unsafe { (*pointer).str_ };
|
||||
let len = unsafe { (*pointer).length };
|
||||
let slice = unsafe { std::slice::from_raw_parts(str, len) };
|
||||
String::from_utf16(slice).unwrap()
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ViewInfo {
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl ViewInfo {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { width: 1, height: 1, scale: 1. }
|
||||
}
|
||||
|
||||
pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) {
|
||||
match update {
|
||||
ViewInfoUpdate::Size { width, height } if width > 0 && height > 0 => {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
ViewInfoUpdate::Scale(scale) if scale > 0. => {
|
||||
self.scale = scale;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn zoom(&self) -> f64 {
|
||||
self.scale.ln() / 1.2_f64.ln()
|
||||
}
|
||||
|
||||
pub(crate) fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub(crate) fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ViewInfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum ViewInfoUpdate {
|
||||
Size { width: u32, height: u32 },
|
||||
Scale(f64),
|
||||
}
|
||||
|
||||
pub(super) struct ViewInfoReceiver {
|
||||
view_info: ViewInfo,
|
||||
receiver: Receiver<ViewInfoUpdate>,
|
||||
}
|
||||
|
||||
impl ViewInfoReceiver {
|
||||
pub(super) fn new(receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self { view_info: ViewInfo::new(), receiver }
|
||||
}
|
||||
|
||||
pub(super) fn current(&mut self) -> ViewInfo {
|
||||
for update in self.receiver.try_iter() {
|
||||
self.view_info.apply_update(update);
|
||||
}
|
||||
self.view_info
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user