diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 7c87074..d8ee89e 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -1,5 +1,5 @@ { - "globs": ["**/*.md", "!node_modules", "!target", "!app/node_modules", "!THIRD-PARTY-BUNDLED.md", "!THIRD-PARTY-LICENSES.md"], + "globs": ["**/*.md", "!node_modules", "!target", "!app/node_modules", "!vendor/softbuffer", "!THIRD-PARTY-BUNDLED.md", "!THIRD-PARTY-LICENSES.md"], "config": { "default": true, "MD013": false, diff --git a/Cargo.lock b/Cargo.lock index eef4dbc..01a6c23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6125,8 +6125,6 @@ dependencies = [ [[package]] name = "softbuffer" version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "as-raw-xcb-connection", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index b5944fa..1710fce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "3" members = ["crates/*", "app/src-tauri"] +exclude = ["vendor"] [workspace.package] version = "2.0.0" @@ -65,3 +66,7 @@ unsafe_code = "deny" [workspace.lints.clippy] all = { level = "deny", priority = -1 } + +[patch.crates-io] +# macOS drops the alpha of every pixel without it; vendor/README.md says why and when it goes. +softbuffer = { path = "vendor/softbuffer" } diff --git a/THIRD-PARTY-BUNDLED.md b/THIRD-PARTY-BUNDLED.md index 2a6bcae..9770a94 100644 --- a/THIRD-PARTY-BUNDLED.md +++ b/THIRD-PARTY-BUNDLED.md @@ -3,7 +3,8 @@ LinkUnbound is GPL-3.0-only. The binary carries the work below, each under its own -licence. Nothing of it was copied into LinkUnbound's own source. The licence text +licence. Nothing of it was copied into LinkUnbound's own source, save the crates under +`vendor/`, kept as published with the one change `vendor/README.md` describes. The licence text of every crate is in [THIRD-PARTY-LICENSES.md](https://github.com/rgdevment/LinkUnbound/blob/main/THIRD-PARTY-LICENSES.md), also under About → Licence texts. diff --git a/crates/linkunbound-mac/Cargo.toml b/crates/linkunbound-mac/Cargo.toml index 36b535f..1af8686 100644 --- a/crates/linkunbound-mac/Cargo.toml +++ b/crates/linkunbound-mac/Cargo.toml @@ -79,11 +79,13 @@ objc2-app-kit = { version = "0.3", default-features = false, features = [ "block2", "objc2-uniform-type-identifiers", "objc2-quartz-core", + "NSAccessibility", "NSAppearance", "NSApplication", "NSBitmapImageRep", "NSColor", "NSEvent", + "NSGlassEffectView", "NSGraphics", "NSGraphicsContext", "NSImage", @@ -92,7 +94,9 @@ objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSResponder", "NSRunningApplication", "NSScreen", + "NSUserInterfaceItemIdentification", "NSView", + "NSVisualEffectView", "NSWindow", "NSWorkspace", ] } diff --git a/crates/linkunbound-mac/src/glass.rs b/crates/linkunbound-mac/src/glass.rs new file mode 100644 index 0000000..9bdecd2 --- /dev/null +++ b/crates/linkunbound-mac/src/glass.rs @@ -0,0 +1,195 @@ +use std::cell::Cell; +use std::ptr::NonNull; +use std::rc::Rc; + +use block2::RcBlock; +use objc2::Message; +use objc2::rc::Retained; +use objc2::runtime::AnyClass; +use objc2_app_kit::{ + NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, + NSBackingStoreType, NSColor, NSGlassEffectView, NSUserInterfaceItemIdentification, NSView, + NSVisualEffectBlendingMode, NSVisualEffectMaterial, NSVisualEffectState, NSVisualEffectView, + NSWindow, NSWindowDidMoveNotification, NSWindowDidResizeNotification, NSWindowOrderingMode, + NSWindowStyleMask, NSWorkspace, +}; +use objc2_foundation::{MainThreadMarker, NSNotification, NSNotificationCenter, NSPoint, NSString}; + +use crate::native::window_of; + +const GLASS: &str = "linkunbound-glass"; + +/// Asked by name: the class only exists from macOS 26, and naming it in code, a downcast +/// included, panics on the systems before it. +fn glass_exists() -> bool { + AnyClass::get(c"NSGlassEffectView").is_some() +} + +#[must_use] +pub fn liquid_glass() -> bool { + let skipped = + cfg!(debug_assertions) && std::env::var_os("LINKUNBOUND_NO_LIQUID_GLASS").is_some(); + !skipped && glass_exists() +} + +#[must_use] +pub fn reduces_transparency() -> bool { + NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceTransparency() +} + +fn material(mtm: MainThreadMarker) -> Retained { + if liquid_glass() { + return Retained::into_super(NSGlassEffectView::new(mtm)); + } + let vibrancy = NSVisualEffectView::new(mtm); + // Popover draws close to solid from macOS 26; menus stay see-through on every version. + vibrancy.setMaterial(NSVisualEffectMaterial::Menu); + vibrancy.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow); + vibrancy.setState(NSVisualEffectState::Active); + Retained::into_super(vibrancy) +} + +fn round(material: &NSView, corner: f64) { + if glass_exists() + && let Some(glass) = material.downcast_ref::() + { + glass.setCornerRadius(corner); + return; + } + material.setWantsLayer(true); + if let Some(layer) = material.layer() { + layer.setCornerRadius(corner); + layer.setMasksToBounds(true); + } +} + +#[allow(unsafe_code)] +fn tone(material: &NSView, dark: bool) { + // SAFETY: constant names AppKit exports. + let appearance = NSAppearance::appearanceNamed(unsafe { + if dark { + NSAppearanceNameDarkAqua + } else { + NSAppearanceNameAqua + } + }); + material.setAppearance(appearance.as_deref()); +} + +fn is_glass(window: &NSWindow) -> bool { + let name = NSString::from_str(GLASS); + window + .contentView() + .and_then(|view| view.identifier()) + .is_some_and(|id| *id == *name) +} + +/// The window holding the material behind the picker, if it wears one. +#[must_use] +pub(crate) fn glass_behind(picker: &NSWindow) -> Option> { + picker.childWindows()?.iter().find(|child| is_glass(child)) +} + +#[allow(unsafe_code)] +fn glass_window(mtm: MainThreadMarker, picker: &NSWindow) -> Retained { + // SAFETY: a fresh allocation, initialised once with constant arguments. + let window = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + mtm.alloc(), + picker.frame(), + NSWindowStyleMask::Borderless, + NSBackingStoreType::Buffered, + false, + ) + }; + // SAFETY: the window is owned here and never closed by AppKit on its own. + unsafe { window.setReleasedWhenClosed(false) }; + window.setOpaque(false); + window.setBackgroundColor(Some(&NSColor::clearColor())); + window.setHasShadow(false); + window.setIgnoresMouseEvents(true); + window.setHidesOnDeactivate(false); + window.setCollectionBehavior(picker.collectionBehavior()); + let material = material(mtm); + material.setIdentifier(Some(&NSString::from_str(GLASS))); + window.setContentView(Some(&material)); + // SAFETY: both windows live on the main thread; AppKit keeps the child with its parent. + unsafe { picker.addChildWindow_ordered(&window, NSWindowOrderingMode::Below) }; + keep_with(picker, &window); + window +} + +/// AppKit grows and shrinks a window from its bottom edge: the picker keeps its top instead, as it +/// does on Windows and as the bear leaning on it expects, and the glass takes its frame again. +#[allow(unsafe_code)] +fn keep_with(picker: &NSWindow, glass: &NSWindow) { + let top = Rc::new(Cell::new(None::)); + let moved = { + let (picker, glass, top) = (picker.retain(), glass.retain(), Rc::clone(&top)); + RcBlock::new(move |_: NonNull| { + let frame = picker.frame(); + top.set(Some(frame.origin.y + frame.size.height)); + glass.setFrame_display(frame, true); + }) + }; + let resized = { + let (picker, glass) = (picker.retain(), glass.retain()); + RcBlock::new(move |_: NonNull| { + let frame = picker.frame(); + if let Some(top) = top.get() + && (frame.origin.y + frame.size.height - top).abs() > 0.5 + { + picker.setFrameTopLeftPoint(NSPoint::new(frame.origin.x, top)); + } + glass.setFrame_display(picker.frame(), true); + }) + }; + let centre = NSNotificationCenter::defaultCenter(); + // SAFETY: AppKit's own names, posted on the main thread where the blocks run; the centre keeps + // both observers for as long as the picker, which lives as long as the app. + unsafe { + centre.addObserverForName_object_queue_usingBlock( + Some(NSWindowDidMoveNotification), + Some(picker), + None, + &moved, + ); + centre.addObserverForName_object_queue_usingBlock( + Some(NSWindowDidResizeNotification), + Some(picker), + None, + &resized, + ); + } +} + +/// The painted sheet is back: the glass behind it would show in its shadow margin. +pub fn take_off_glass(view: isize) { + let Some(picker) = window_of(view) else { + return; + }; + if let Some(glass) = glass_behind(&picker) { + picker.removeChildWindow(&glass); + glass.orderOut(None); + } +} + +/// The material lives in a window of its own just behind the picker: AppKit only blends it with +/// what is behind when it is part of a window's content, and the picker's content is winit's. +pub fn wear_glass(view: isize, dark: bool, corner: f64) -> bool { + let Some(mtm) = MainThreadMarker::new() else { + return false; + }; + let Some(picker) = window_of(view) else { + return false; + }; + let glass = glass_behind(&picker).unwrap_or_else(|| glass_window(mtm, &picker)); + glass.setLevel(picker.level()); + glass.setFrame_display(picker.frame(), true); + let Some(material) = glass.contentView() else { + return false; + }; + round(&material, corner); + tone(&material, dark); + true +} diff --git a/crates/linkunbound-mac/src/lib.rs b/crates/linkunbound-mac/src/lib.rs index eda32d6..35b3b60 100644 --- a/crates/linkunbound-mac/src/lib.rs +++ b/crates/linkunbound-mac/src/lib.rs @@ -3,6 +3,8 @@ mod detect; #[cfg(target_os = "macos")] mod events; #[cfg(target_os = "macos")] +mod glass; +#[cfg(target_os = "macos")] mod icons; #[cfg(target_os = "macos")] mod legacy; @@ -29,6 +31,8 @@ pub use detect::{app_named, bundle_id_named, installed_browsers}; #[cfg(target_os = "macos")] pub use events::{Event, Listening, listen}; #[cfg(target_os = "macos")] +pub use glass::{liquid_glass, reduces_transparency, take_off_glass, wear_glass}; +#[cfg(target_os = "macos")] pub use icons::icon_for; #[cfg(target_os = "macos")] pub use legacy::{ @@ -38,8 +42,8 @@ pub use legacy::{ pub use native::{ Watching, copy_text, cursor, digit_behind, dress_window, front_application, front_is_ours, is_in_front, keep_off_the_taskbar, let_whoever_opens_next_come_forward, menu_bar_is_light, - retire, shift_is_down, source_app, take_the_keyboard, translated, watch_activations, - windows_are_light, work_area_at, + retire, shift_is_down, source_app, take_the_keyboard, translated, tuck_beside, + watch_activations, windows_are_light, work_area_at, }; #[cfg(target_os = "macos")] pub use registration::{ @@ -53,8 +57,9 @@ mod audit { use std::fs; use std::path::Path; - const AUDITED: [&str; 5] = [ + const AUDITED: [&str; 6] = [ "events.rs", + "glass.rs", "icons.rs", "native.rs", "registration.rs", diff --git a/crates/linkunbound-mac/src/native.rs b/crates/linkunbound-mac/src/native.rs index 4d921d6..ba37003 100644 --- a/crates/linkunbound-mac/src/native.rs +++ b/crates/linkunbound-mac/src/native.rs @@ -10,7 +10,7 @@ use objc2_app_kit::{ NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, NSApplication, NSApplicationActivationPolicy, NSColor, NSEvent, NSEventModifierFlags, NSEventType, NSFloatingWindowLevel, NSPasteboard, NSPasteboardTypeString, NSRunningApplication, - NSScreen, NSView, NSWindow, NSWindowCollectionBehavior, NSWorkspace, + NSScreen, NSView, NSWindow, NSWindowCollectionBehavior, NSWindowOrderingMode, NSWorkspace, NSWorkspaceDidActivateApplicationNotification, }; use objc2_foundation::{ @@ -83,7 +83,7 @@ pub fn work_area_at(x: i32, y: i32) -> Option<(i32, i32, i32, i32)> { } #[allow(unsafe_code)] -fn window_of(view: isize) -> Option> { +pub(crate) fn window_of(view: isize) -> Option> { let view = view as *const NSView; if view.is_null() { return None; @@ -114,6 +114,37 @@ pub fn keep_off_the_taskbar(view: isize, corner: f64) { window.invalidateShadow(); } +/// The bear's windows ride with the picker: on its level, out of the way of clicks, the head +/// under it so the glass blurs it and the paws over its edge. +pub fn tuck_beside(view: isize, picker: isize, under: bool) { + let (Some(window), Some(picker)) = (window_of(view), window_of(picker)) else { + return; + }; + // Over the picker means a level above it: taking the keyboard brings the picker to the front + // of its own level again. + window.setLevel(picker.level() + isize::from(!under)); + window.setCollectionBehavior( + NSWindowCollectionBehavior::CanJoinAllSpaces + | NSWindowCollectionBehavior::FullScreenAuxiliary, + ); + window.setHidesOnDeactivate(false); + window.setOpaque(false); + window.setBackgroundColor(Some(&NSColor::clearColor())); + window.setHasShadow(false); + window.setIgnoresMouseEvents(true); + let place = if under { + NSWindowOrderingMode::Below + } else { + NSWindowOrderingMode::Above + }; + // Under the glass, not just the picker, so the material blurs the hidden half of the head. + let beside = under + .then(|| crate::glass::glass_behind(&picker)) + .flatten() + .unwrap_or(picker); + window.orderWindow_relativeTo(place, beside.windowNumber()); +} + pub fn take_the_keyboard(view: isize) { let Some(mtm) = MainThreadMarker::new() else { return; diff --git a/crates/linkunbound-mac/tests/main_thread.rs b/crates/linkunbound-mac/tests/main_thread.rs index 0eabb3a..6605578 100644 --- a/crates/linkunbound-mac/tests/main_thread.rs +++ b/crates/linkunbound-mac/tests/main_thread.rs @@ -30,14 +30,15 @@ fn main() { use linkunbound_mac::{ Event, cursor, dress_window, is_in_front, keep_off_the_taskbar, - let_whoever_opens_next_come_forward, listen, work_area_at, + let_whoever_opens_next_come_forward, liquid_glass, listen, reduces_transparency, + take_off_glass, tuck_beside, wear_glass, work_area_at, }; use objc2::MainThreadOnly; use objc2::rc::Retained; use objc2_app_kit::{ NSAppearanceCustomization, NSApplication, NSApplicationActivationPolicy, - NSBackingStoreType, NSFloatingWindowLevel, NSWindow, NSWindowCollectionBehavior, - NSWindowStyleMask, + NSBackingStoreType, NSFloatingWindowLevel, NSGlassEffectView, NSVisualEffectView, NSWindow, + NSWindowCollectionBehavior, NSWindowStyleMask, }; use objc2_core_services::keyDirectObject; use objc2_foundation::{ @@ -85,6 +86,97 @@ fn main() { assert_eq!(layer.cornerRadius(), 10.0); assert!(layer.masksToBounds()); + let _ = reduces_transparency(); + assert!(wear_glass(handle, true, 14.0)); + let children = window.childWindows().expect("the glass behind the picker"); + assert_eq!(children.len(), 1); + let glass = children.iter().next().expect("one child"); + assert_eq!( + glass.frame(), + window.frame(), + "the glass covers the picker exactly" + ); + assert_eq!(glass.level(), window.level()); + assert!( + glass.ignoresMouseEvents(), + "clicks go through to the picker" + ); + assert!(!glass.hasShadow(), "the picker casts the only shadow"); + let material = glass.contentView().expect("the material"); + // Only named where it exists: naming the class on a macOS before 26 panics. + let as_glass = |material: &objc2_app_kit::NSView| { + liquid_glass() + .then(|| material.downcast_ref::()) + .flatten() + .map(NSGlassEffectView::cornerRadius) + }; + let corner = |material: &objc2_app_kit::NSView| { + as_glass(material) + .unwrap_or_else(|| material.layer().expect("a rounded layer").cornerRadius()) + }; + assert_eq!(as_glass(&material).is_some(), liquid_glass()); + assert_eq!( + material.downcast_ref::().is_some(), + !liquid_glass() + ); + assert_eq!(corner(&material), 14.0); + let tone = + |material: &objc2_app_kit::NSView| material.appearance().map(|a| a.name().to_string()); + assert_eq!(tone(&material).as_deref(), Some("NSAppearanceNameDarkAqua")); + assert!(wear_glass(handle, false, 10.0)); + assert_eq!( + window.childWindows().map(|c| c.len()), + Some(1), + "worn again, the same glass is brought up to date" + ); + assert_eq!(corner(&material), 10.0); + assert_eq!(tone(&material).as_deref(), Some("NSAppearanceNameAqua")); + + window.setFrameTopLeftPoint(NSPoint::new(50.0, 500.0)); + let grown = NSRect::new(NSPoint::new(50.0, 490.0), NSSize::new(10.0, 50.0)); + window.setFrame_display(grown, false); + let frame = window.frame(); + assert_eq!( + frame.origin.y + frame.size.height, + 500.0, + "the picker grows downwards, keeping its top" + ); + assert_eq!(glass.frame(), frame, "and the glass grows with it"); + + take_off_glass(handle); + assert_eq!( + window.childWindows().map(|c| c.len()).unwrap_or_default(), + 0, + "the painted sheet has no glass behind it" + ); + + // SAFETY: made and used on the main thread, never shown, dropped at the end. + let bear = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + NSWindow::alloc(mtm), + NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(10.0, 10.0)), + NSWindowStyleMask::Borderless, + NSBackingStoreType::Buffered, + false, + ) + }; + let bear_view = bear.contentView().expect("a content view"); + let bear_handle = Retained::as_ptr(&bear_view) as isize; + tuck_beside(bear_handle, handle, true); + assert_eq!( + bear.level(), + window.level(), + "the head shares the picker's level" + ); + assert!(bear.ignoresMouseEvents()); + assert!(!bear.hasShadow()); + tuck_beside(bear_handle, handle, false); + assert_eq!( + bear.level(), + window.level() + 1, + "the paws stay over a picker that takes the keyboard" + ); + let window_handle = Retained::as_ptr(&window) as isize; let named = |window: &NSWindow| window.appearance().map(|a| a.name().to_string()); dress_window(window_handle, Some(true)); diff --git a/crates/linkunbound-shell/examples/preview.rs b/crates/linkunbound-shell/examples/preview.rs index 0861716..7466411 100644 --- a/crates/linkunbound-shell/examples/preview.rs +++ b/crates/linkunbound-shell/examples/preview.rs @@ -1,7 +1,7 @@ #![windows_subsystem = "windows"] //! `cargo run -p linkunbound-shell --example preview -- [--look=list|tiles|compact|bear] [--glass] -//! [--count=N] [--light] [--en] [--six | --twelve] +//! [--count=N] [--tint=0..1] [--light] [--en] [--six | --twelve] //! [--scale=N] [--update | --store | --stage=starting|getting|installing|failed] [--notice]` use linkunbound_core::Language; @@ -115,6 +115,13 @@ fn main() -> Result<(), slint::PlatformError> { window.set_look(look); let glass = flag("--glass"); window.global::().set_clear(glass); + if let Some(tint) = args + .iter() + .find_map(|a| a.strip_prefix("--tint=")) + .and_then(|v| v.parse::().ok()) + { + window.global::().set_tint(tint); + } window.set_halo(if look == Look::List || glass { 0.0 } else { @@ -214,5 +221,27 @@ fn main() -> Result<(), slint::PlatformError> { }); } + #[cfg(target_os = "macos")] + if glass { + let handle = window.as_weak(); + let dark = !flag("--light"); + let corner = if look == Look::List { + linkunbound_shell::CLASSIC_CORNER + } else { + linkunbound_shell::CORNER + }; + slint::Timer::single_shot(std::time::Duration::from_millis(50), move || { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + let Some(w) = handle.upgrade() else { return }; + if let Ok(found) = w.window().window_handle().window_handle() + && let RawWindowHandle::AppKit(appkit) = found.as_raw() + { + let view = appkit.ns_view.as_ptr() as isize; + linkunbound_mac::keep_off_the_taskbar(view, corner); + linkunbound_mac::wear_glass(view, dark, corner); + } + }); + } + window.run() } diff --git a/crates/linkunbound-shell/src/look.rs b/crates/linkunbound-shell/src/look.rs index 712a1a3..0f407a1 100644 --- a/crates/linkunbound-shell/src/look.rs +++ b/crates/linkunbound-shell/src/look.rs @@ -11,10 +11,17 @@ pub fn style(picker: &Picker, chosen: PickerStyle) { PickerStyle::Compact => Look::Compact, PickerStyle::Bear => Look::Bear, }; - let clear = acrylic(); + let clear = native_glass(); picker.set_look(look); - picker.global::().set_clear(clear); - picker.global::().set_see_through(see_through()); + let palette = picker.global::(); + palette.set_clear(clear); + palette.set_tint(tint()); + palette.set_glass_corner(if cfg!(target_os = "macos") { + corner_of(picker) as f32 + } else { + DWM_CORNER + }); + palette.set_see_through(see_through()); picker.set_halo(if look == Look::List || clear { 0.0 } else { @@ -22,6 +29,24 @@ pub fn style(picker: &Picker, chosen: PickerStyle) { }); } +/// How much of the picker's tone is laid over the material: Acrylic comes milky, Liquid Glass +/// and menu vibrancy let far more of the desktop through. +#[cfg(target_os = "macos")] +fn tint() -> f32 { + if linkunbound_mac::liquid_glass() { + 0.55 + } else { + 0.5 + } +} + +#[cfg(not(target_os = "macos"))] +fn tint() -> f32 { + 0.77 +} + +const DWM_CORNER: f32 = 8.0; + pub fn corner_of(picker: &Picker) -> f64 { if picker.get_look() == Look::List { CLASSIC_CORNER @@ -34,7 +59,11 @@ pub fn corner_of(picker: &Picker) -> f64 { /// painted sheet and its shadow come back. Returns whether the window changed size. pub fn wear_backdrop(picker: &Picker, handle: isize) -> bool { let palette = picker.global::(); - if !palette.get_clear() || wear_acrylic(handle, !palette.get_light()) { + if !palette.get_clear() { + take_off_glass(handle); + return false; + } + if wear_glass(handle, !palette.get_light(), corner_of(picker)) { return false; } palette.set_clear(false); @@ -91,10 +120,18 @@ fn place_near( if let Some(ui) = crate::ui() { ui.mascot.anchor(at_x, at_y); } + // AppKit keeps a window's bottom edge when it grows, so the size Slint would set later would + // lift the picker off the place asked for, and off the bear. #[cfg(target_os = "macos")] - picker - .window() - .set_position(slint::LogicalPosition::new(at_x as f32, at_y as f32)); + { + picker.window().set_size(slint::LogicalSize::new( + picker.get_wanted_width(), + picker.get_wanted_height(), + )); + picker + .window() + .set_position(slint::LogicalPosition::new(at_x as f32, at_y as f32)); + } #[cfg(not(target_os = "macos"))] picker .window() @@ -124,36 +161,62 @@ fn see_through() -> bool { linkunbound_win::transparency_is_on() } -#[cfg(not(windows))] +#[cfg(target_os = "macos")] +fn see_through() -> bool { + !linkunbound_mac::reduces_transparency() +} + +#[cfg(not(any(windows, target_os = "macos")))] fn see_through() -> bool { true } #[cfg(windows)] -fn acrylic() -> bool { +fn native_glass() -> bool { // A debug build can be told to paint, to see on Windows 11 what Windows 10 shows. let painted = cfg!(debug_assertions) && std::env::var_os("LINKUNBOUND_NO_ACRYLIC").is_some(); linkunbound_win::acrylic_is_available() && !painted } #[cfg(windows)] -fn wear_acrylic(window: isize, dark: bool) -> bool { +fn wear_glass(window: isize, dark: bool, _corner: f64) -> bool { linkunbound_win::wear_acrylic(window, dark) } -#[cfg(not(windows))] -fn acrylic() -> bool { +/// Vibrancy is there on every macOS the app runs on, Liquid Glass from 26; only the person asking +/// for less transparency sends the picker back to its painted sheet. +#[cfg(target_os = "macos")] +fn native_glass() -> bool { + !linkunbound_mac::reduces_transparency() +} + +#[cfg(target_os = "macos")] +fn wear_glass(view: isize, dark: bool, corner: f64) -> bool { + linkunbound_mac::wear_glass(view, dark, corner) +} + +#[cfg(target_os = "macos")] +fn take_off_glass(view: isize) { + linkunbound_mac::take_off_glass(view); +} + +/// Acrylic is a window attribute DWM drops when the system turns transparency off. +#[cfg(not(target_os = "macos"))] +fn take_off_glass(_window: isize) {} + +#[cfg(not(any(windows, target_os = "macos")))] +fn native_glass() -> bool { false } -#[cfg(not(windows))] -fn wear_acrylic(_window: isize, _dark: bool) -> bool { +#[cfg(not(any(windows, target_os = "macos")))] +fn wear_glass(_window: isize, _dark: bool, _corner: f64) -> bool { false } #[cfg(test)] mod tests { - use super::{acrylic, corner_of, place_near, style, wear_backdrop}; + use super::{corner_of, native_glass, place_near, style, wear_backdrop}; use linkunbound_core::PickerStyle; use linkunbound_shell::{CLASSIC_CORNER, CORNER, HALO, Palette, Picker}; use slint::ComponentHandle; @@ -168,7 +231,7 @@ mod tests { #[test] fn the_shadow_is_drawn_only_where_nothing_else_draws_one() { let picker = picker(); - let clear = acrylic(); + let clear = native_glass(); style(&picker, PickerStyle::Sheet); assert_eq!(picker.global::().get_clear(), clear); diff --git a/crates/linkunbound-shell/src/mascot.rs b/crates/linkunbound-shell/src/mascot.rs index 7c8cdab..7385bbb 100644 --- a/crates/linkunbound-shell/src/mascot.rs +++ b/crates/linkunbound-shell/src/mascot.rs @@ -73,28 +73,17 @@ impl Mascot { // Measured from what the look asks for, not from the window: right after a change of // look or place the window still reports the size and spot it had before. let window = picker.window(); - let scale = window.scale_factor(); - let (x, y) = self - .anchor - .get() - .unwrap_or((window.position().x, window.position().y)); - let at = slint::PhysicalPosition::new(x, y); - let px = |logical: f32| (logical * scale).round() as i32; - let centre = at.x + px(picker.get_wanted_width() / 2.0); - // A painted sheet keeps a margin for its shadow; the bear leans on the sheet, not on it. - let top = at.y + px(picker.get_halo()); - self.head - .window() - .set_position(slint::PhysicalPosition::new( - centre - px(pose.head / 2.0), - top - px(pose.rise), - )); - self.paws - .window() - .set_position(slint::PhysicalPosition::new( - centre - px(pose.gap / 2.0 + pose.paw), - top - px(pose.paw * 0.3), - )); + let scale = placing_scale(window); + let (x, y) = self.anchor.get().unwrap_or_else(|| where_it_is(window)); + let (head, paws) = spots( + pose, + (x, y), + picker.get_wanted_width(), + picker.get_halo(), + scale, + ); + put(self.head.window(), head.0, head.1); + put(self.paws.window(), paws.0, paws.1); self.mood(0.0, picker.get_private_on()); let _ = self.head.show(); let _ = self.paws.show(); @@ -112,6 +101,53 @@ impl Mascot { } } +/// Where the head and the paws go for a picker put at `at`, in the units it was put in. +fn spots( + pose: &Pose, + (x, y): (i32, i32), + width: f32, + halo: f32, + scale: f32, +) -> ((i32, i32), (i32, i32)) { + let px = |logical: f32| (logical * scale).round() as i32; + let centre = x + px(width / 2.0); + // A painted sheet keeps a margin for its shadow; the bear leans on the sheet, not on it. + let top = y + px(halo); + ( + (centre - px(pose.head / 2.0), top - px(pose.rise)), + ( + centre - px(pose.gap / 2.0 + pose.paw), + top - px(pose.paw * 0.3), + ), + ) +} + +/// The picker is placed in points on macOS and in pixels elsewhere; the bear goes in the same units. +fn placing_scale(window: &slint::Window) -> f32 { + if cfg!(target_os = "macos") { + 1.0 + } else { + window.scale_factor() + } +} + +fn where_it_is(window: &slint::Window) -> (i32, i32) { + let at = window.position(); + if cfg!(target_os = "macos") { + let at = at.to_logical(window.scale_factor()); + (at.x.round() as i32, at.y.round() as i32) + } else { + (at.x, at.y) + } +} + +fn put(window: &slint::Window, x: i32, y: i32) { + #[cfg(target_os = "macos")] + window.set_position(slint::LogicalPosition::new(x as f32, y as f32)); + #[cfg(not(target_os = "macos"))] + window.set_position(slint::PhysicalPosition::new(x, y)); +} + /// The picker says where the bear should look and whether it wears the mask, and an empty slot /// sends the person to settings to fill it. pub fn listen(picker: &Picker) { @@ -146,12 +182,26 @@ fn stack(head: &BearHead, paws: &BearPaws, picker: &Picker) { linkunbound_win::tuck_under(head, picker); } -#[cfg(not(windows))] +#[cfg(target_os = "macos")] +fn stack(head: &BearHead, paws: &BearPaws, picker: &Picker) { + use crate::native_handle; + let (Some(head), Some(paws), Some(picker)) = ( + native_handle(head.window()), + native_handle(paws.window()), + native_handle(picker.window()), + ) else { + return; + }; + linkunbound_mac::tuck_beside(head, picker, true); + linkunbound_mac::tuck_beside(paws, picker, false); +} + +#[cfg(not(any(windows, target_os = "macos")))] fn stack(_head: &BearHead, _paws: &BearPaws, _picker: &Picker) {} #[cfg(test)] mod tests { - use super::{BEAR, COMPACT, Mascot, rise}; + use super::{BEAR, COMPACT, Mascot, rise, spots}; use linkunbound_shell::{Look, Palette, Picker}; use slint::ComponentHandle; @@ -196,4 +246,20 @@ mod tests { assert!(!mascot.head.window().is_visible()); assert!(!mascot.paws.window().is_visible()); } + + /// The head stands over the middle of the sheet and rises its pose's height over it, the paws + /// on the edge, whatever the scale the picker was placed at. + #[test] + fn the_bear_leans_on_the_middle_of_the_sheet() { + let (head, paws) = spots(&BEAR, (100, 300), 400.0, 0.0, 1.0); + assert_eq!(head, (300 - 46, 300 - 62)); + assert_eq!(paws, (300 - 65, 300 - 9)); + + let (head, _) = spots(&BEAR, (200, 600), 400.0, 18.0, 2.0); + assert_eq!( + head, + (600 - 92, 636 - 124), + "pixels on a dense screen, under a shadow margin" + ); + } } diff --git a/crates/linkunbound-shell/ui/parts.slint b/crates/linkunbound-shell/ui/parts.slint index 93130f9..3a129a7 100644 --- a/crates/linkunbound-shell/ui/parts.slint +++ b/crates/linkunbound-shell/ui/parts.slint @@ -19,10 +19,14 @@ export global Palette { in property light: false; // Over Acrylic the system paints the material, the corners and the edge of the picker. in property clear: false; - // Laid over Acrylic: the blur is the system's, the tone and the lit edge stay the picker's. + // Laid over the material: the blur is the system's, the tone and the lit edge stay the picker's. + // Each material takes a different share of it: Acrylic is milky, Liquid Glass far clearer. + in property tint: 0.77; + // DWM rounds Acrylic at its own 8px; on macOS the glass view is cut to the sheet's corner. + in property glass-corner: 8px; out property veil: light - ? @linear-gradient(180deg, #FFFFFFC4 0%, #F3F5F9CC 100%) - : @linear-gradient(180deg, #282B36C4 0%, #1A1C23CC 100%); + ? @linear-gradient(180deg, #FFFFFF.with-alpha(tint) 0%, #F3F5F9.with-alpha(tint + 0.03) 100%) + : @linear-gradient(180deg, #282B36.with-alpha(tint) 0%, #1A1C23.with-alpha(tint + 0.03) 100%); out property rim: light ? #FFFFFFE6 : #FFFFFF2E; // Without a material: the same tone drawn by the picker, a touch see-through unless the system // asked for no transparency, with the accent's glint coming in at the top corner. diff --git a/crates/linkunbound-shell/ui/shell.slint b/crates/linkunbound-shell/ui/shell.slint index 2a4bcbd..d86926f 100644 --- a/crates/linkunbound-shell/ui/shell.slint +++ b/crates/linkunbound-shell/ui/shell.slint @@ -244,7 +244,7 @@ export component Picker inherits Window { y: root.halo; width: root.sheet-width; height: root.sheet-height; - border-radius: Palette.clear ? 8px : (root.look == Look.list ? 10px : 14px); + border-radius: Palette.clear ? Palette.glass-corner : (root.look == Look.list ? 10px : 14px); background: Palette.clear ? Palette.veil : Palette.pane; border-width: Palette.clear && !root.private-on ? 0px : 1px; border-color: root.private-on ? Palette.secret : Palette.glow; diff --git a/scripts/rules.sh b/scripts/rules.sh index 8d12ccb..3fbe961 100755 --- a/scripts/rules.sh +++ b/scripts/rules.sh @@ -144,6 +144,7 @@ unsafe_stays_where_it_was_audited() { local audited found audited="app/src-tauri/src/shop.rs crates/linkunbound-mac/src/events.rs +crates/linkunbound-mac/src/glass.rs crates/linkunbound-mac/src/icons.rs crates/linkunbound-mac/src/native.rs crates/linkunbound-mac/src/registration.rs diff --git a/scripts/third-party.mjs b/scripts/third-party.mjs index 370661a..255a136 100644 --- a/scripts/third-party.mjs +++ b/scripts/third-party.mjs @@ -214,7 +214,8 @@ const crates = () => { if (!one) continue; const [, name, version, from, licence] = one; const key = `${name}@${version}`; - if ((from && existsSync(from)) || seen.has(key)) continue; + const ours = from && existsSync(from) && !resolve(from).startsWith(join(root, "vendor")); + if (ours || seen.has(key)) continue; seen.set(key, { name, version, licence: licence || "see the crate" }); } } @@ -343,7 +344,8 @@ writeFileSync( LinkUnbound is GPL-3.0-only. The binary carries the work below, each under its own -licence. Nothing of it was copied into LinkUnbound's own source. The licence text +licence. Nothing of it was copied into LinkUnbound's own source, save the crates under +\`vendor/\`, kept as published with the one change \`vendor/README.md\` describes. The licence text of every crate is in [THIRD-PARTY-LICENSES.md](https://github.com/rgdevment/LinkUnbound/blob/main/THIRD-PARTY-LICENSES.md), also under About → Licence texts. diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 0000000..883a569 --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,31 @@ +# Vendored crates + +Third-party crates kept here with a change LinkUnbound needs and upstream does not have yet. +They are not workspace members (`exclude = ["vendor"]`), so fmt, clippy, tests, the line +ceiling and SonarCloud leave them alone. They reach the build only through `[patch.crates-io]` +in the root `Cargo.toml`. + +## softbuffer 0.4.8 + +**Why:** on macOS, softbuffer hands every frame to Core Graphics as +`CGImageAlphaInfo::NoneSkipFirst`, which reads the alpha byte as padding: every pixel lands +opaque. Slint's software renderer does write premultiplied alpha into that byte, so with it +dropped nothing behind the picker's view can show through, and the Liquid Glass or vibrancy +view under it stays hidden. + +**What changed:** one line in `src/backends/cg.rs`, marked `LinkUnbound:`, from +`NoneSkipFirst` to `PremultipliedFirst`. Everything else is the crate as published on +crates.io (`.cargo_vcs_info.json` names the upstream commit), minus the files only its own +CI used. Windows and the other backends are untouched. + +**Licence:** MIT OR Apache-2.0, both texts beside it. It was already listed in the +third-party notices as a dependency of Slint. + +**When it goes:** once a released softbuffer keeps the alpha on macOS and Slint depends on +it. Then delete `vendor/softbuffer` and the `[patch.crates-io]` entry, and check that +`cargo run -p linkunbound-shell --example preview -- --glass` on a Mac still shows the +desktop blurred behind the picker. + +**Updating Slint meanwhile:** if Slint moves to a softbuffer other than 0.4.8, the patch +stops applying and Cargo warns that it is unused. Copy the new version here, carry the +same line over, and update this note. diff --git a/vendor/softbuffer/.cargo_vcs_info.json b/vendor/softbuffer/.cargo_vcs_info.json new file mode 100644 index 0000000..32d6440 --- /dev/null +++ b/vendor/softbuffer/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "d871852faa1137d4615b99b9ad31c3ba80f345b5" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/softbuffer/CHANGELOG.md b/vendor/softbuffer/CHANGELOG.md new file mode 100644 index 0000000..b1e1b15 --- /dev/null +++ b/vendor/softbuffer/CHANGELOG.md @@ -0,0 +1,128 @@ +# Unreleased + +# 0.4.7 + +- Fix documentation building on `docs.rs`. + +# 0.4.7 + +- Added support for Android using the `ndk` crate. +- Added support for `wasm64-*` targets. +- Improved examples. +- Added `Buffer::width()` and `Buffer::height()` getters. +- `Context` now implements `Clone`. +- `Context`, `Surface` and `Buffer` now implement `Debug`. +- Bump MSRV to Rust 1.71. +- Replace `log` with `tracing`. +- Remove `cfg_aliases` dependency. +- Update to `objc2` 0.6, `objc2-*` 0.3, `drm` 0.14, `rustix` 1.0 and `windows-sys` 0.61. + +# 0.4.6 + +- Added support for iOS, tvOS, watchOS and visionOS (UIKit). +- Redo the way surfaces work on macOS to work directly with layers, which will + allow initializing directly from a `CALayer` in the future. +- Update to `windows-sys` 0.59.0 and `core-graphics` v0.24.0. +- Add an MSRV policy. + +# 0.4.5 + +- Make the `wayland-sys` dependency optional. (#223) +- Allow for transparent visuals on X11. This technically doesn't work, but + otherwise `winit` users might get crashes. (#226) + +# 0.4.4 + +- Make `Context` `Send`+`Sync` and `Surface` `Send`. (#217) + +# 0.4.3 + +- Use objc2 as the backend for the CoreGraphics implementation. (#210) +- Update drm-rs to version v0.12.0. (#209) +- Bump MSRV to 1.70.0 to be in line with Winit. + +# 0.4.2 + +- Add the ability to get the underlying window handle. (#193) +- Rework the backend to use a trait-based interface. (#196) +- On Orbital, fix window resize. (#200) +- Fix `bytes()` for KMS/DRM implementation. (#203) + +# 0.4.1 + +- On MacOS, Fix double-free of `NSWindow`. (#180) +- Update `drm` to 0.11 (#178) + * Fixes build on architectures where drm-rs did not have generated bindings. +- Update x11rb to v0.13 (#183) +- On Web, add support for more `RawWindowHandle` variants. (#188) +- On Wayland, fix buffer age. (#191) + +# 0.4.0 + +- **Breaking:** Port to use `raw-window-handle` v0.6.(#132) +- Enable creating X11 displays without an existing connection. (#171) + +# 0.3.3 + +- Fix a bug in the new shared memory model in X11. (#170) + +# 0.3.2 + +* Document that `present_with_damage` is supported on web platforms. (#152) +* Replace our usage of `nix` with `rustix`. This enables this crate to run without `libc`. (#164) +* Use POSIX shared memory instead of Sys-V shared memory for the X11 backend. (#165) +* Bump version for the following dependencies: + * `memmap2` (#156) + * `redox_syscall` (#161) + * `drm` (#163) + +# 0.3.1 + +* On X11, fix the length of the returned buffer when using the wire-transferred buffer. +* On Web, fix incorrect starting coordinates when handling buffer damage. +* On Redox, use `MAP_SHARED`; fixing behavior with latest Orbital. +* Error instead of segfault on macOS if size isn't set. +* Add `OffscreenCanvas` support in web backend. +* Add DRM/KMS backend, for running on tty without X/Wayland. +* Make `fetch` error on Windows, where it wasn't working correctly. +* Implement `Error` trait for `SoftBufferError`. +* Dependency updates. + +# 0.3.0 + +- On MacOS, the contents scale is updated when set_buffer() is called, to adapt when the window is on a new screen (#68). +- **Breaking:** Split the `GraphicsContext` type into `Context` and `Surface` (#64). +- On Web, cache the document in the `Context` type (#66). +- **Breaking:** Introduce a new "owned buffer" for no-copy presentation (#65). +- Enable support for multi-threaded WASM (#77). +- Fix buffer resizing on X11 (#69). +- Add a set of functions for handling buffer damage (#99). +- Add a `fetch()` function for getting the window contents (#104). +- Bump MSRV to 1.64 (#81). + +# 0.2.1 + +- Bump `windows-sys` to 0.48 + +# 0.2.0 + +- Add support for Redox/Orbital. +- Add support for BSD distributions. +- Ported Windows backend from `winapi` to `windows-sys`. +- **Breaking:** Take a reference to a window instead of owning the window. +- Add a `from_raw` function for directly using raw handles. +- Improvements for Wayland support. +- Support for HiDPI on macOS. +- **Breaking:** Add feature flags for `x11` and `wayland` backends. +- Use static dispatch instead of dynamic dispatch for the backends. +- Add `libxcb` support to the X11 backend. +- Use X11 MIT-SHM extension, if available. + +# 0.1.1 + +- Added WASM support (Thanks to [Liamolucko](https://github.com/Liamolucko)!) +- CALayer is now used for Mac OS backend, which is more flexible about what happens in the windowing library (Thanks to [lunixbochs](https://github.com/lunixbochs)!) + +# 0.1.0 + +Initial published version with support for Linux (X11 and Wayland), Mac OS (but buggy), and Windows. diff --git a/vendor/softbuffer/Cargo.toml b/vendor/softbuffer/Cargo.toml new file mode 100644 index 0000000..9f02005 --- /dev/null +++ b/vendor/softbuffer/Cargo.toml @@ -0,0 +1,282 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.71.0" +name = "softbuffer" +version = "0.4.8" +build = false +exclude = ["examples"] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Cross-platform software buffer" +documentation = "https://docs.rs/softbuffer" +readme = "README.md" +keywords = [ + "framebuffer", + "windowing", +] +categories = [ + "game-development", + "graphics", + "gui", + "multimedia", + "rendering", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-windowing/softbuffer" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "wasm32-unknown-unknown", +] + +[features] +default = [ + "kms", + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] +kms = [ + "bytemuck", + "drm", + "rustix", +] +wayland = [ + "wayland-backend", + "wayland-client", + "wayland-sys", + "memmap2", + "rustix", + "fastrand", +] +wayland-dlopen = ["wayland-sys/dlopen"] +x11 = [ + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "rustix", + "tiny-xlib", + "x11rb", +] +x11-dlopen = [ + "tiny-xlib/dlopen", + "x11rb/dl-libxcb", +] + +[lib] +name = "softbuffer" +path = "src/lib.rs" + +[[bench]] +name = "buffer_mut" +path = "benches/buffer_mut.rs" +harness = false + +[dependencies.raw_window_handle] +version = "0.6" +features = ["std"] +package = "raw-window-handle" + +[dependencies.tracing] +version = "0.1.41" +default-features = false + +[dev-dependencies.colorous] +version = "1.0.12" + +[dev-dependencies.criterion] +version = "0.8.1" +features = ["cargo_bench_support"] +default-features = false + +[dev-dependencies.image] +version = "0.25.0" +features = ["jpeg"] +default-features = false + +[dev-dependencies.web-time] +version = "1.0.0" + +[dev-dependencies.winit] +version = "0.30.0" + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.as-raw-xcb-connection] +version = "1.0.0" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.bytemuck] +version = "1.12.3" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.drm] +version = "0.14.1" +optional = true +default-features = false + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.fastrand] +version = "2.0.0" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.memmap2] +version = "0.9.0" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.rustix] +version = "1.0.1" +features = [ + "fs", + "mm", + "shm", + "std", +] +optional = true +default-features = false + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.tiny-xlib] +version = "0.2.1" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.wayland-backend] +version = "0.3.0" +features = ["client_system"] +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.wayland-client] +version = "0.31.0" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.wayland-sys] +version = "0.31.0" +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies.x11rb] +version = "0.13.0" +features = [ + "allow-unsafe-code", + "shm", +] +optional = true + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dev-dependencies.rustix] +version = "1.0.1" +features = ["event"] + +[target.'cfg(not(target_family = "wasm"))'.dev-dependencies.rayon] +version = "1.5.1" + +[target.'cfg(target_family = "wasm")'.dependencies.js-sys] +version = "0.3.63" + +[target.'cfg(target_family = "wasm")'.dependencies.wasm-bindgen] +version = "0.2.86" + +[target.'cfg(target_family = "wasm")'.dependencies.web-sys] +version = "0.3.55" +features = [ + "CanvasRenderingContext2d", + "Document", + "Element", + "HtmlCanvasElement", + "ImageData", + "OffscreenCanvas", + "OffscreenCanvasRenderingContext2d", + "Window", +] + +[target.'cfg(target_family = "wasm")'.dev-dependencies.wasm-bindgen-test] +version = "0.3" + +[target.'cfg(target_os = "android")'.dependencies.bytemuck] +version = "1.12.3" + +[target.'cfg(target_os = "android")'.dependencies.ndk] +version = "0.9.0" + +[target.'cfg(target_os = "android")'.dev-dependencies.android-activity] +version = "0.6" + +[target.'cfg(target_os = "android")'.dev-dependencies.winit] +version = "0.30.0" +features = ["android-native-activity"] + +[target.'cfg(target_os = "redox")'.dependencies.redox_syscall] +version = "0.5" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.61.2" +features = [ + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", +] + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2] +version = "0.6.0" + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-foundation] +version = "0.3.2" +features = [ + "std", + "CFCGTypes", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-graphics] +version = "0.3.2" +features = [ + "std", + "objc2", + "CGColorSpace", + "CGDataProvider", + "CGImage", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation] +version = "0.3.2" +features = [ + "std", + "objc2-core-foundation", + "NSDictionary", + "NSGeometry", + "NSKeyValueObserving", + "NSString", + "NSThread", + "NSValue", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-quartz-core] +version = "0.3.2" +features = [ + "std", + "objc2-core-foundation", + "CALayer", + "CATransaction", +] +default-features = false diff --git a/vendor/softbuffer/Cargo.toml.orig b/vendor/softbuffer/Cargo.toml.orig new file mode 100644 index 0000000..68238c0 --- /dev/null +++ b/vendor/softbuffer/Cargo.toml.orig @@ -0,0 +1,183 @@ +[package] +name = "softbuffer" +version = "0.4.8" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Cross-platform software buffer" +documentation = "https://docs.rs/softbuffer" +readme = "README.md" +repository = "https://github.com/rust-windowing/softbuffer" +keywords = ["framebuffer", "windowing"] +categories = ["game-development", "graphics", "gui", "multimedia", "rendering"] +exclude = ["examples"] +rust-version = "1.71.0" + +[[bench]] +name = "buffer_mut" +harness = false + +[features] +default = ["kms", "x11", "x11-dlopen", "wayland", "wayland-dlopen"] +kms = ["bytemuck", "drm", "rustix"] +wayland = [ + "wayland-backend", + "wayland-client", + "wayland-sys", + "memmap2", + "rustix", + "fastrand", +] +wayland-dlopen = ["wayland-sys/dlopen"] +x11 = [ + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "rustix", + "tiny-xlib", + "x11rb", +] +x11-dlopen = ["tiny-xlib/dlopen", "x11rb/dl-libxcb"] + +[dependencies] +raw_window_handle = { package = "raw-window-handle", version = "0.6", features = [ + "std", +] } +tracing = { version = "0.1.41", default-features = false } + +[target.'cfg(target_os = "android")'.dependencies] +bytemuck = "1.12.3" +ndk = "0.9.0" + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dependencies] +as-raw-xcb-connection = { version = "1.0.0", optional = true } +bytemuck = { version = "1.12.3", optional = true } +drm = { version = "0.14.1", default-features = false, optional = true } +fastrand = { version = "2.0.0", optional = true } +memmap2 = { version = "0.9.0", optional = true } +rustix = { version = "1.0.1", features = [ + "fs", + "mm", + "shm", + "std", +], default-features = false, optional = true } +tiny-xlib = { version = "0.2.1", optional = true } +wayland-backend = { version = "0.3.0", features = [ + "client_system", +], optional = true } +wayland-client = { version = "0.31.0", optional = true } +wayland-sys = { version = "0.31.0", optional = true } +x11rb = { version = "0.13.0", features = [ + "allow-unsafe-code", + "shm", +], optional = true } + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.61.2" +features = [ + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", +] + +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2-core-graphics = { version = "0.3.2", default-features = false, features = [ + "std", + "objc2", + "CGColorSpace", + "CGDataProvider", + "CGImage", +] } +objc2 = "0.6.0" +objc2-core-foundation = { version = "0.3.2", default-features = false, features = [ + "std", + "CFCGTypes", +] } +objc2-foundation = { version = "0.3.2", default-features = false, features = [ + "std", + "objc2-core-foundation", + "NSDictionary", + "NSGeometry", + "NSKeyValueObserving", + "NSString", + "NSThread", + "NSValue", +] } +objc2-quartz-core = { version = "0.3.2", default-features = false, features = [ + "std", + "objc2-core-foundation", + "CALayer", + "CATransaction", +] } + +[target.'cfg(target_family = "wasm")'.dependencies] +js-sys = "0.3.63" +wasm-bindgen = "0.2.86" + +[target.'cfg(target_family = "wasm")'.dependencies.web-sys] +version = "0.3.55" +features = [ + "CanvasRenderingContext2d", + "Document", + "Element", + "HtmlCanvasElement", + "ImageData", + "OffscreenCanvas", + "OffscreenCanvasRenderingContext2d", + "Window", +] + +[target.'cfg(target_os = "redox")'.dependencies] +redox_syscall = "0.5" + +[dev-dependencies] +colorous = "1.0.12" +criterion = { version = "0.8.1", default-features = false, features = [ + "cargo_bench_support", +] } +web-time = "1.0.0" +winit = "0.30.0" + +[target.'cfg(target_os = "android")'.dev-dependencies] +winit = { version = "0.30.0", features = ["android-native-activity"] } +android-activity = "0.6" + +[dev-dependencies.image] +version = "0.25.0" +# Disable rayon on web +default-features = false +features = ["jpeg"] + +[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] +# Turn rayon back on everywhere else; creating the separate entry resets the features to default. +rayon = "1.5.1" + +[target.'cfg(target_family = "wasm")'.dev-dependencies] +wasm-bindgen-test = "0.3" + +[target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dev-dependencies] +rustix = { version = "1.0.1", features = ["event"] } + +[workspace] +members = ["run-wasm"] + +[[example]] +# Run with `cargo apk r --example winit_android` +name = "winit_android" +crate-type = ["cdylib"] + +[[example]] +# Run with `cargo apk r --example winit_multithread_android` +name = "winit_multithread_android" +crate-type = ["cdylib"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "wasm32-unknown-unknown", +] diff --git a/vendor/softbuffer/LICENSE-APACHE b/vendor/softbuffer/LICENSE-APACHE new file mode 100644 index 0000000..5b1aded --- /dev/null +++ b/vendor/softbuffer/LICENSE-APACHE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Kirill Chibisov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/softbuffer/LICENSE-MIT b/vendor/softbuffer/LICENSE-MIT new file mode 100644 index 0000000..96e3624 --- /dev/null +++ b/vendor/softbuffer/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright 2022 Kirill Chibisov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/softbuffer/README.md b/vendor/softbuffer/README.md new file mode 100644 index 0000000..85faaae --- /dev/null +++ b/vendor/softbuffer/README.md @@ -0,0 +1,163 @@ +# Softbuffer + +Enables software rendering via drawing an image straight to a window. + +Softbuffer integrates with the [`raw-window-handle`](https://crates.io/crates/raw-window-handle) crate +to allow writing pixels to a window in a cross-platform way while using the very high quality dedicated window management +libraries that are available in the Rust ecosystem. + +## Alternatives + +[minifb](https://crates.io/crates/minifb) also allows putting a 2D buffer/image on a window in a platform-independent way. +Minifb's approach to doing window management itself, however, is problematic code duplication. We already have very high quality +libraries for this in the Rust ecosystem (such as [winit](https://crates.io/crates/winit)), and minifb's implementation +of window management is not ideal. For example, it occasionally segfaults and is missing key features such as setting +a window icon on some platforms. While adding these features to minifb would be possible, it makes more sense to use +the standard window handling systems instead. + +What about [pixels](https://crates.io/crates/pixels)? Pixels accomplishes a very similar goal to Softbuffer, +however there are two key differences. Pixels provides some capacity for GPU-accelerated post-processing of what is +displayed, while Softbuffer does not. Due to not having this post-processing, Softbuffer does not rely on the GPU or +hardware accelerated graphics stack in any way, and is thus more portable to installations that do not have access to +hardware acceleration (e.g. VMs, older computers, computers with misconfigured drivers). Softbuffer should be used over +pixels when its GPU-accelerated post-processing effects are not needed. + +## License & Credits + +This library is dual-licensed under MIT or Apache-2.0, just like minifb and rust. Significant portions of code were taken +from the minifb library to do platform-specific work. + +## Platform support: + +Some, but not all, platforms supported in [raw-window-handle](https://crates.io/crates/raw-window-handle) are supported +by Softbuffer. Pull requests are welcome to add new platforms! **Nonetheless, all major desktop platforms that winit uses +on desktop are supported.** + +For now, the priority for new platforms is: + +1. to have at least one platform on each OS working (e.g. one of Win32 or WinRT, or one of Xlib, Xcb, and Wayland) and +2. for that one platform on each OS to be the one that winit uses. + +(PRs will be accepted for any platform, even if it does not follow the above priority.) + +| Platform || +|-----------|--| +|Android NDK|✅| +| AppKit |✅| +| Orbital |✅| +| UIKit |✅| +| Wayland |✅| +| Web |✅| +| Win32 |✅| +| WinRT |❌| +| XCB |✅| +| Xlib |✅| + +✅: Present\ +❔: Immature\ +❌: Absent + +## WebAssembly + +To run an example with the web backend: `cargo run-wasm --example winit` + +## Android + +To run the Android-specific example on an Android phone: `cargo apk r --example winit_android` or `cargo apk r --example winit_multithread_android`. + +## Example + +```rust,no_run +use std::num::NonZeroU32; +use std::rc::Rc; +use winit::event::{Event, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::Window; + +#[path = "../examples/utils/winit_app.rs"] +mod winit_app; + +fn main() { + let event_loop = EventLoop::new().unwrap(); + let context = softbuffer::Context::new(event_loop.owned_display_handle()).unwrap(); + + let mut app = winit_app::WinitAppBuilder::with_init( + |elwt| { + let window = elwt.create_window(Window::default_attributes()); + Rc::new(window.unwrap()) + }, + |_elwt, window| softbuffer::Surface::new(&context, window.clone()).unwrap(), + ) + .with_event_handler(|window, surface, window_id, event, elwt| { + elwt.set_control_flow(ControlFlow::Wait); + + if window_id != window.id() { + return; + } + + match event { + WindowEvent::RedrawRequested => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; + let size = window.inner_size(); + surface + .resize( + NonZeroU32::new(size.width).unwrap(), + NonZeroU32::new(size.height).unwrap(), + ) + .unwrap(); + + let mut buffer = surface.buffer_mut().unwrap(); + for index in 0..(buffer.width().get() * buffer.height().get()) { + let y = index / buffer.width().get(); + let x = index % buffer.width().get(); + let red = x % 255; + let green = y % 255; + let blue = (x * y) % 255; + + buffer[index as usize] = blue | (green << 8) | (red << 16); + } + + buffer.present().unwrap(); + } + WindowEvent::CloseRequested => { + elwt.exit(); + } + _ => {} + } + }); + + event_loop.run_app(&mut app).unwrap(); +} +``` + +## MSRV Policy + +This crate's Minimum Supported Rust Version (MSRV) is **1.71**. Changes to +the MSRV will be accompanied by a minor version bump. + +As a **tentative** policy, the upper bound of the MSRV is given by the following +formula: + +```text +min(sid, stable - 3) +``` + +Where `sid` is the current version of `rustc` provided by [Debian Sid], and +`stable` is the latest stable version of Rust. This bound may be broken in case of a major ecosystem shift or a security vulnerability. + +[Debian Sid]: https://packages.debian.org/sid/rustc + +Orbital is not covered by this MSRV policy, as it requires a Rust nightly +toolchain to compile. + +All crates in the [`rust-windowing`] organizations have the +same MSRV policy. + +[`rust-windowing`]: https://github.com/rust-windowing + +## Changelog + +See the [changelog](CHANGELOG.md) for a list of this package's versions and the changes made in each version. diff --git a/vendor/softbuffer/benches/buffer_mut.rs b/vendor/softbuffer/benches/buffer_mut.rs new file mode 100644 index 0000000..e383102 --- /dev/null +++ b/vendor/softbuffer/benches/buffer_mut.rs @@ -0,0 +1,66 @@ +#![allow(deprecated)] // TODO + +use criterion::{criterion_group, criterion_main, Criterion}; + +fn buffer_mut(c: &mut Criterion) { + #[cfg(target_family = "wasm")] + { + // Do nothing. + let _ = c; + } + + #[cfg(not(target_family = "wasm"))] + { + use criterion::black_box; + use softbuffer::{Context, Surface}; + use std::num::NonZeroU32; + use winit::event_loop::ControlFlow; + use winit::platform::run_on_demand::EventLoopExtRunOnDemand; + + let mut evl = winit::event_loop::EventLoop::new().unwrap(); + let context = Context::new(evl.owned_display_handle()).unwrap(); + let window = evl + .create_window(winit::window::Window::default_attributes().with_visible(false)) + .unwrap(); + + evl.run_on_demand(move |ev, elwt| { + elwt.set_control_flow(ControlFlow::Poll); + + if let winit::event::Event::AboutToWait = ev { + elwt.exit(); + + let mut surface = Surface::new(&context, &window).unwrap(); + + let size = window.inner_size(); + surface + .resize( + NonZeroU32::new(size.width).unwrap(), + NonZeroU32::new(size.height).unwrap(), + ) + .unwrap(); + + c.bench_function("buffer_mut()", |b| { + b.iter(|| { + for _ in 0..500 { + black_box(surface.buffer_mut().unwrap()); + } + }); + }); + + c.bench_function("pixels_mut()", |b| { + let mut buffer = surface.buffer_mut().unwrap(); + b.iter(|| { + for _ in 0..500 { + let x: &mut [u32] = &mut buffer; + black_box(x); + } + }); + }); + } + }) + .unwrap(); + } +} + +criterion_group!(benches, buffer_mut); +criterion_main!(benches); diff --git a/vendor/softbuffer/src/backend_dispatch.rs b/vendor/softbuffer/src/backend_dispatch.rs new file mode 100644 index 0000000..3bfe94b --- /dev/null +++ b/vendor/softbuffer/src/backend_dispatch.rs @@ -0,0 +1,277 @@ +//! Implements `buffer_interface::*` traits for enums dispatching to backends + +use crate::{backend_interface::*, backends, InitError, Rect, SoftBufferError}; + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::fmt; +use std::num::NonZeroU32; + +/// A macro for creating the enum used to statically dispatch to the platform-specific implementation. +macro_rules! make_dispatch { + ( + <$dgen: ident, $wgen: ident> => + $( + $(#[$attr:meta])* + $name: ident + ($context_inner: ty, $surface_inner: ty, $buffer_inner: ty), + )* + ) => { + #[derive(Clone)] + pub(crate) enum ContextDispatch<$dgen> { + $( + $(#[$attr])* + $name($context_inner), + )* + } + + impl ContextDispatch { + pub fn variant_name(&self) -> &'static str { + match self { + $( + $(#[$attr])* + Self::$name(_) => stringify!($name), + )* + } + } + } + + impl ContextInterface for ContextDispatch { + fn new(mut display: D) -> Result> + where + D: Sized, + { + $( + $(#[$attr])* + match <$context_inner as ContextInterface>::new(display) { + Ok(x) => { + return Ok(Self::$name(x)); + } + Err(InitError::Unsupported(d)) => display = d, + Err(InitError::Failure(f)) => return Err(InitError::Failure(f)), + } + )* + + Err(InitError::Unsupported(display)) + } + } + + impl fmt::Debug for ContextDispatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.fmt(f), + )* + } + } + } + + #[allow(clippy::large_enum_variant)] // it's boxed anyways + pub(crate) enum SurfaceDispatch<$dgen, $wgen> { + $( + $(#[$attr])* + $name($surface_inner), + )* + } + + impl SurfaceInterface for SurfaceDispatch { + type Context = ContextDispatch; + type Buffer<'a> = BufferDispatch<'a, D, W> where Self: 'a; + + fn new(window: W, display: &Self::Context) -> Result> + where + W: Sized, + Self: Sized { + match display { + $( + $(#[$attr])* + ContextDispatch::$name(inner) => Ok(Self::$name(<$surface_inner>::new(window, inner)?)), + )* + } + } + + fn window(&self) -> &W { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.window(), + )* + } + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.resize(width, height), + )* + } + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + match self { + $( + $(#[$attr])* + Self::$name(inner) => Ok(BufferDispatch::$name(inner.buffer_mut()?)), + )* + } + } + + fn fetch(&mut self) -> Result, SoftBufferError> { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.fetch(), + )* + } + } + } + + impl fmt::Debug for SurfaceDispatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.fmt(f), + )* + } + } + } + + pub(crate) enum BufferDispatch<'a, $dgen, $wgen> { + $( + $(#[$attr])* + $name($buffer_inner), + )* + } + + impl<'a, D: HasDisplayHandle, W: HasWindowHandle> BufferInterface for BufferDispatch<'a, D, W> { + #[inline] + fn width(&self) -> NonZeroU32 { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.width(), + )* + } + } + + #[inline] + fn height(&self) -> NonZeroU32 { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.height(), + )* + } + } + + #[inline] + fn pixels(&self) -> &[u32] { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.pixels(), + )* + } + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.pixels_mut(), + )* + } + } + + fn age(&self) -> u8 { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.age(), + )* + } + } + + fn present(self) -> Result<(), SoftBufferError> { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.present(), + )* + } + } + + fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.present_with_damage(damage), + )* + } + } + } + + impl fmt::Debug for BufferDispatch<'_, D, W> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.fmt(f), + )* + } + } + } + }; +} + +// XXX empty enum with generic bound is invalid? + +make_dispatch! { + => + #[cfg(target_os = "android")] + Android(D, backends::android::AndroidImpl, backends::android::BufferImpl<'a, D, W>), + #[cfg(all( + feature = "x11", + not(any( + target_os = "android", + target_vendor = "apple", + target_os = "redox", + target_family = "wasm", + target_os = "windows" + )) + ))] + X11(std::sync::Arc>, backends::x11::X11Impl, backends::x11::BufferImpl<'a, D, W>), + #[cfg(all( + feature = "wayland", + not(any( + target_os = "android", + target_vendor = "apple", + target_os = "redox", + target_family = "wasm", + target_os = "windows" + )) + ))] + Wayland(std::sync::Arc>, backends::wayland::WaylandImpl, backends::wayland::BufferImpl<'a, D, W>), + #[cfg(all( + feature = "kms", + not(any( + target_os = "android", + target_vendor = "apple", + target_os = "redox", + target_family = "wasm", + target_os = "windows" + )) + ))] + Kms(std::sync::Arc>, backends::kms::KmsImpl, backends::kms::BufferImpl<'a, D, W>), + #[cfg(target_os = "windows")] + Win32(D, backends::win32::Win32Impl, backends::win32::BufferImpl<'a, D, W>), + #[cfg(target_vendor = "apple")] + CoreGraphics(D, backends::cg::CGImpl, backends::cg::BufferImpl<'a, D, W>), + #[cfg(target_family = "wasm")] + Web(backends::web::WebDisplayImpl, backends::web::WebImpl, backends::web::BufferImpl<'a, D, W>), + #[cfg(target_os = "redox")] + Orbital(D, backends::orbital::OrbitalImpl, backends::orbital::BufferImpl<'a, D, W>), +} diff --git a/vendor/softbuffer/src/backend_interface.rs b/vendor/softbuffer/src/backend_interface.rs new file mode 100644 index 0000000..b6a5666 --- /dev/null +++ b/vendor/softbuffer/src/backend_interface.rs @@ -0,0 +1,45 @@ +//! Interface implemented by backends + +use crate::{InitError, Rect, SoftBufferError}; + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::num::NonZeroU32; + +pub(crate) trait ContextInterface { + fn new(display: D) -> Result> + where + D: Sized, + Self: Sized; +} + +pub(crate) trait SurfaceInterface { + type Context: ContextInterface; + type Buffer<'a>: BufferInterface + where + Self: 'a; + + fn new(window: W, context: &Self::Context) -> Result> + where + W: Sized, + Self: Sized; + /// Get the inner window handle. + fn window(&self) -> &W; + /// Resize the internal buffer to the given width and height. + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError>; + /// Get a mutable reference to the buffer. + fn buffer_mut(&mut self) -> Result, SoftBufferError>; + /// Fetch the buffer from the window. + fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } +} + +pub(crate) trait BufferInterface { + fn width(&self) -> NonZeroU32; + fn height(&self) -> NonZeroU32; + fn pixels(&self) -> &[u32]; + fn pixels_mut(&mut self) -> &mut [u32]; + fn age(&self) -> u8; + fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError>; + fn present(self) -> Result<(), SoftBufferError>; +} diff --git a/vendor/softbuffer/src/backends/android.rs b/vendor/softbuffer/src/backends/android.rs new file mode 100644 index 0000000..12d489b --- /dev/null +++ b/vendor/softbuffer/src/backends/android.rs @@ -0,0 +1,182 @@ +//! Implementation of software buffering for Android. + +use std::marker::PhantomData; +use std::num::{NonZeroI32, NonZeroU32}; + +use ndk::{ + hardware_buffer_format::HardwareBufferFormat, + native_window::{NativeWindow, NativeWindowBufferLockGuard}, +}; +#[cfg(doc)] +use raw_window_handle::AndroidNdkWindowHandle; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; + +use crate::error::InitError; +use crate::{util, BufferInterface, Rect, SoftBufferError, SurfaceInterface}; + +/// The handle to a window for software buffering. +#[derive(Debug)] +pub struct AndroidImpl { + native_window: NativeWindow, + window: W, + _display: PhantomData, +} + +impl SurfaceInterface for AndroidImpl { + type Context = D; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + /// Create a new [`AndroidImpl`] from an [`AndroidNdkWindowHandle`]. + fn new(window: W, _display: &Self::Context) -> Result> { + let raw = window.window_handle()?.as_raw(); + let RawWindowHandle::AndroidNdk(a) = raw else { + return Err(InitError::Unsupported(window)); + }; + + // Acquire a new owned reference to the window, that will be freed on drop. + // SAFETY: We have confirmed that the window handle is valid. + let native_window = unsafe { NativeWindow::clone_from_ptr(a.a_native_window.cast()) }; + + Ok(Self { + native_window, + _display: PhantomData, + window, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window + } + + /// Also changes the pixel format to [`HardwareBufferFormat::R8G8B8A8_UNORM`]. + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + let (width, height) = (|| { + let width = NonZeroI32::try_from(width).ok()?; + let height = NonZeroI32::try_from(height).ok()?; + Some((width, height)) + })() + .ok_or(SoftBufferError::SizeOutOfRange { width, height })?; + + self.native_window + .set_buffers_geometry( + width.into(), + height.into(), + // Default is typically R5G6B5 16bpp, switch to 32bpp + Some(HardwareBufferFormat::R8G8B8X8_UNORM), + ) + .map_err(|err| { + SoftBufferError::PlatformError( + Some("Failed to set buffer geometry on ANativeWindow".to_owned()), + Some(Box::new(err)), + ) + }) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + let native_window_buffer = self.native_window.lock(None).map_err(|err| { + SoftBufferError::PlatformError( + Some("Failed to lock ANativeWindow".to_owned()), + Some(Box::new(err)), + ) + })?; + + if !matches!( + native_window_buffer.format(), + // These are the only formats we support + HardwareBufferFormat::R8G8B8A8_UNORM | HardwareBufferFormat::R8G8B8X8_UNORM + ) { + return Err(SoftBufferError::PlatformError( + Some(format!( + "Unexpected buffer format {:?}, please call \ + .resize() first to change it to RGBx8888", + native_window_buffer.format() + )), + None, + )); + } + + let buffer = vec![0; native_window_buffer.width() * native_window_buffer.height()]; + + Ok(BufferImpl { + native_window_buffer, + buffer: util::PixelBuffer(buffer), + marker: PhantomData, + }) + } + + /// Fetch the buffer from the window. + fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D: ?Sized, W> { + native_window_buffer: NativeWindowBufferLockGuard<'a>, + buffer: util::PixelBuffer, + marker: PhantomData<(&'a D, &'a W)>, +} + +// TODO: Move to NativeWindowBufferLockGuard? +unsafe impl<'a, D, W> Send for BufferImpl<'a, D, W> {} + +impl<'a, D: HasDisplayHandle, W: HasWindowHandle> BufferInterface for BufferImpl<'a, D, W> { + fn width(&self) -> NonZeroU32 { + NonZeroU32::new(self.native_window_buffer.width() as u32).unwrap() + } + + fn height(&self) -> NonZeroU32 { + NonZeroU32::new(self.native_window_buffer.height() as u32).unwrap() + } + + #[inline] + fn pixels(&self) -> &[u32] { + &self.buffer + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + &mut self.buffer + } + + #[inline] + fn age(&self) -> u8 { + 0 + } + + // TODO: This function is pretty slow this way + fn present(mut self) -> Result<(), SoftBufferError> { + let input_lines = self.buffer.chunks(self.native_window_buffer.width()); + for (output, input) in self + .native_window_buffer + .lines() + // Unreachable as we checked before that this is a valid, mappable format + .unwrap() + .zip(input_lines) + { + // .lines() removed the stride + assert_eq!(output.len(), input.len() * 4); + + for (i, pixel) in input.iter().enumerate() { + // Swizzle colors from BGR(A) to RGB(A) + let [b, g, r, a] = pixel.to_le_bytes(); + output[i * 4].write(r); + output[i * 4 + 1].write(g); + output[i * 4 + 2].write(b); + output[i * 4 + 3].write(a); + } + } + Ok(()) + } + + fn present_with_damage(self, _damage: &[Rect]) -> Result<(), SoftBufferError> { + // TODO: Android requires the damage rect _at lock time_ + // Since we're faking the backing buffer _anyway_, we could even fake the surface lock + // and lock it here (if it doesn't influence timings). + self.present() + } +} diff --git a/vendor/softbuffer/src/backends/cg.rs b/vendor/softbuffer/src/backends/cg.rs new file mode 100644 index 0000000..4c676b1 --- /dev/null +++ b/vendor/softbuffer/src/backends/cg.rs @@ -0,0 +1,389 @@ +//! Softbuffer implementation using CoreGraphics. +use crate::backend_interface::*; +use crate::error::InitError; +use crate::{util, Rect, SoftBufferError}; +use objc2::rc::Retained; +use objc2::runtime::{AnyObject, Bool}; +use objc2::{define_class, msg_send, AllocAnyThread, DefinedClass, MainThreadMarker, Message}; +use objc2_core_foundation::{CFRetained, CGPoint}; +use objc2_core_graphics::{ + CGBitmapInfo, CGColorRenderingIntent, CGColorSpace, CGDataProvider, CGImage, CGImageAlphaInfo, + CGImageByteOrderInfo, CGImageComponentInfo, CGImagePixelFormatInfo, +}; +use objc2_foundation::{ + ns_string, NSDictionary, NSKeyValueChangeKey, NSKeyValueChangeNewKey, + NSKeyValueObservingOptions, NSNumber, NSObject, NSObjectNSKeyValueObserverRegistration, + NSString, NSValue, +}; +use objc2_quartz_core::{kCAGravityTopLeft, CALayer, CATransaction}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; + +use std::ffi::c_void; +use std::marker::PhantomData; +use std::mem::size_of; +use std::num::NonZeroU32; +use std::ops::Deref; +use std::ptr::{self, slice_from_raw_parts_mut, NonNull}; + +define_class!( + #[unsafe(super(NSObject))] + #[name = "SoftbufferObserver"] + #[ivars = SendCALayer] + #[derive(Debug)] + struct Observer; + + /// NSKeyValueObserving + impl Observer { + #[unsafe(method(observeValueForKeyPath:ofObject:change:context:))] + fn observe_value( + &self, + key_path: Option<&NSString>, + _object: Option<&AnyObject>, + change: Option<&NSDictionary>, + _context: *mut c_void, + ) { + self.update(key_path, change); + } + } +); + +impl Observer { + fn new(layer: &CALayer) -> Retained { + let this = Self::alloc().set_ivars(SendCALayer(layer.retain())); + unsafe { msg_send![super(this), init] } + } + + fn update( + &self, + key_path: Option<&NSString>, + change: Option<&NSDictionary>, + ) { + let layer = self.ivars(); + + let change = + change.expect("requested a change dictionary in `addObserver`, but none was provided"); + let new = change + .objectForKey(unsafe { NSKeyValueChangeNewKey }) + .expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`"); + + // NOTE: Setting these values usually causes a quarter second animation to occur, which is + // undesirable. + // + // However, since we're setting them inside an observer, there already is a transaction + // ongoing, and as such we don't need to wrap this in a `CATransaction` ourselves. + + if key_path == Some(ns_string!("contentsScale")) { + let new = new.downcast::().unwrap(); + let scale_factor = new.as_cgfloat(); + + // Set the scale factor of the layer to match the root layer when it changes (e.g. if + // moved to a different monitor, or monitor settings changed). + layer.setContentsScale(scale_factor); + } else if key_path == Some(ns_string!("bounds")) { + let new = new.downcast::().unwrap(); + let bounds = new.get_rect().expect("new bounds value was not CGRect"); + + // Set `bounds` and `position` so that the new layer is inside the superlayer. + // + // This differs from just setting the `bounds`, as it also takes into account any + // translation that the superlayer may have that we'd want to preserve. + layer.setFrame(bounds); + } else { + panic!("unknown observed keypath {key_path:?}"); + } + } +} + +#[derive(Debug)] +pub struct CGImpl { + /// Our layer. + layer: SendCALayer, + /// The layer that our layer was created from. + /// + /// Can also be retrieved from `layer.superlayer()`. + root_layer: SendCALayer, + observer: Retained, + color_space: CFRetained, + /// The width of the underlying buffer. + width: usize, + /// The height of the underlying buffer. + height: usize, + window_handle: W, + _display: PhantomData, +} + +impl Drop for CGImpl { + fn drop(&mut self) { + // SAFETY: Registered in `new`, must be removed before the observer is deallocated. + unsafe { + self.root_layer + .removeObserver_forKeyPath(&self.observer, ns_string!("contentsScale")); + self.root_layer + .removeObserver_forKeyPath(&self.observer, ns_string!("bounds")); + } + } +} + +impl SurfaceInterface for CGImpl { + type Context = D; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + fn new(window_src: W, _display: &D) -> Result> { + // `NSView`/`UIView` can only be accessed from the main thread. + let _mtm = MainThreadMarker::new().ok_or(SoftBufferError::PlatformError( + Some("can only access Core Graphics handles from the main thread".to_string()), + None, + ))?; + + let root_layer = match window_src.window_handle()?.as_raw() { + RawWindowHandle::AppKit(handle) => { + // SAFETY: The pointer came from `WindowHandle`, which ensures that the + // `AppKitWindowHandle` contains a valid pointer to an `NSView`. + // + // We use `NSObject` here to avoid importing `objc2-app-kit`. + let view: &NSObject = unsafe { handle.ns_view.cast().as_ref() }; + + // Force the view to become layer backed + let _: () = unsafe { msg_send![view, setWantsLayer: Bool::YES] }; + + // SAFETY: `-[NSView layer]` returns an optional `CALayer` + let layer: Option> = unsafe { msg_send![view, layer] }; + layer.expect("failed making the view layer-backed") + } + RawWindowHandle::UiKit(handle) => { + // SAFETY: The pointer came from `WindowHandle`, which ensures that the + // `UiKitWindowHandle` contains a valid pointer to an `UIView`. + // + // We use `NSObject` here to avoid importing `objc2-ui-kit`. + let view: &NSObject = unsafe { handle.ui_view.cast().as_ref() }; + + // SAFETY: `-[UIView layer]` returns `CALayer` + let layer: Retained = unsafe { msg_send![view, layer] }; + layer + } + _ => return Err(InitError::Unsupported(window_src)), + }; + + // Add a sublayer, to avoid interfering with the root layer, since setting the contents of + // e.g. a view-controlled layer is brittle. + let layer = CALayer::new(); + root_layer.addSublayer(&layer); + + // Set the anchor point and geometry. Softbuffer's uses a coordinate system with the origin + // in the top-left corner. + // + // NOTE: This doesn't really matter unless we start modifying the `position` of our layer + // ourselves, but it's nice to have in place. + layer.setAnchorPoint(CGPoint::new(0.0, 0.0)); + layer.setGeometryFlipped(true); + + // Do not use auto-resizing mask. + // + // This is done to work around a bug in macOS 14 and above, where views using auto layout + // may end up setting fractional values as the bounds, and that in turn doesn't propagate + // properly through the auto-resizing mask and with contents gravity. + // + // Instead, we keep the bounds of the layer in sync with the root layer using an observer, + // see below. + // + // layer.setAutoresizingMask(kCALayerHeightSizable | kCALayerWidthSizable); + + let observer = Observer::new(&layer); + // Observe changes to the root layer's bounds and scale factor, and apply them to our layer. + // + // The previous implementation updated the scale factor inside `resize`, but this works + // poorly with transactions, and is generally inefficient. Instead, we update the scale + // factor only when needed because the super layer's scale factor changed. + // + // Note that inherent in this is an explicit design decision: We control the `bounds` and + // `contentsScale` of the layer directly, and instead let the `resize` call that the user + // controls only be the size of the underlying buffer. + // + // SAFETY: Observer deregistered in `Drop` before the observer object is deallocated. + unsafe { + root_layer.addObserver_forKeyPath_options_context( + &observer, + ns_string!("contentsScale"), + NSKeyValueObservingOptions::New | NSKeyValueObservingOptions::Initial, + ptr::null_mut(), + ); + root_layer.addObserver_forKeyPath_options_context( + &observer, + ns_string!("bounds"), + NSKeyValueObservingOptions::New | NSKeyValueObservingOptions::Initial, + ptr::null_mut(), + ); + } + + // Set the content so that it is placed in the top-left corner if it does not have the same + // size as the surface itself. + // + // TODO(madsmtm): Consider changing this to `kCAGravityResize` to stretch the content if + // resized to something that doesn't fit, see #177. + layer.setContentsGravity(unsafe { kCAGravityTopLeft }); + + // Initialize color space here, to reduce work later on. + let color_space = CGColorSpace::new_device_rgb().unwrap(); + + // Grab initial width and height from the layer (whose properties have just been initialized + // by the observer using `NSKeyValueObservingOptionInitial`). + let size = layer.bounds().size; + let scale_factor = layer.contentsScale(); + let width = (size.width * scale_factor) as usize; + let height = (size.height * scale_factor) as usize; + + Ok(Self { + layer: SendCALayer(layer), + root_layer: SendCALayer(root_layer), + observer, + color_space, + width, + height, + _display: PhantomData, + window_handle: window_src, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window_handle + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + self.width = width.get() as usize; + self.height = height.get() as usize; + Ok(()) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + Ok(BufferImpl { + buffer: util::PixelBuffer(vec![0; self.width * self.height]), + imp: self, + }) + } +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D, W> { + imp: &'a mut CGImpl, + buffer: util::PixelBuffer, +} + +impl BufferInterface for BufferImpl<'_, D, W> { + fn width(&self) -> NonZeroU32 { + NonZeroU32::new(self.imp.width as u32).unwrap() + } + + fn height(&self) -> NonZeroU32 { + NonZeroU32::new(self.imp.height as u32).unwrap() + } + + #[inline] + fn pixels(&self) -> &[u32] { + &self.buffer + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + &mut self.buffer + } + + fn age(&self) -> u8 { + 0 + } + + fn present(self) -> Result<(), SoftBufferError> { + unsafe extern "C-unwind" fn release( + _info: *mut c_void, + data: NonNull, + size: usize, + ) { + let data = data.cast::(); + let slice = slice_from_raw_parts_mut(data.as_ptr(), size / size_of::()); + // SAFETY: This is the same slice that we passed to `Box::into_raw` below. + drop(unsafe { Box::from_raw(slice) }) + } + + let data_provider = { + let len = self.buffer.len() * size_of::(); + let buffer: *mut [u32] = Box::into_raw(self.buffer.0.into_boxed_slice()); + // Convert slice pointer to thin pointer. + let data_ptr = buffer.cast::(); + + // SAFETY: The data pointer and length are valid. + // The info pointer can safely be NULL, we don't use it in the `release` callback. + unsafe { + CGDataProvider::with_data(ptr::null_mut(), data_ptr, len, Some(release)).unwrap() + } + }; + + // `CGBitmapInfo` consists of a combination of `CGImageAlphaInfo`, `CGImageComponentInfo` + // `CGImageByteOrderInfo` and `CGImagePixelFormatInfo` (see e.g. `CGBitmapInfoMake`). + // + // TODO: Use `CGBitmapInfo::new` once the next version of objc2-core-graphics is released. + // LinkUnbound: keep the alpha Slint writes, premultiplied, so the glass under the view shows. + let bitmap_info = CGBitmapInfo( + CGImageAlphaInfo::PremultipliedFirst.0 + | CGImageComponentInfo::Integer.0 + | CGImageByteOrderInfo::Order32Little.0 + | CGImagePixelFormatInfo::Packed.0, + ); + + let image = unsafe { + CGImage::new( + self.imp.width, + self.imp.height, + 8, + 32, + self.imp.width * 4, + Some(&self.imp.color_space), + bitmap_info, + Some(&data_provider), + ptr::null(), + false, + CGColorRenderingIntent::RenderingIntentDefault, + ) + } + .unwrap(); + + // The CALayer has a default action associated with a change in the layer contents, causing + // a quarter second fade transition to happen every time a new buffer is applied. This can + // be avoided by wrapping the operation in a transaction and disabling all actions. + CATransaction::begin(); + CATransaction::setDisableActions(true); + + // SAFETY: The contents is `CGImage`, which is a valid class for `contents`. + unsafe { self.imp.layer.setContents(Some(image.as_ref())) }; + + CATransaction::commit(); + Ok(()) + } + + fn present_with_damage(self, _damage: &[Rect]) -> Result<(), SoftBufferError> { + self.present() + } +} + +#[derive(Debug)] +struct SendCALayer(Retained); + +// SAFETY: CALayer is dubiously thread safe, like most things in Core Animation. +// But since we make sure to do our changes within a CATransaction, it is +// _probably_ fine for us to use CALayer from different threads. +// +// See also: +// https://developer.apple.com/documentation/quartzcore/catransaction/1448267-lock?language=objc +// https://stackoverflow.com/questions/76250226/how-to-render-content-of-calayer-on-a-background-thread +unsafe impl Send for SendCALayer {} +// SAFETY: Same as above. +unsafe impl Sync for SendCALayer {} + +impl Deref for SendCALayer { + type Target = CALayer; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/vendor/softbuffer/src/backends/kms.rs b/vendor/softbuffer/src/backends/kms.rs new file mode 100644 index 0000000..d18d633 --- /dev/null +++ b/vendor/softbuffer/src/backends/kms.rs @@ -0,0 +1,427 @@ +//! Backend for DRM/KMS for raw rendering directly to the screen. +//! +//! This strategy uses dumb buffers for rendering. + +use drm::buffer::{Buffer, DrmFourcc}; +use drm::control::dumbbuffer::{DumbBuffer, DumbMapping}; +use drm::control::{ + connector, crtc, framebuffer, plane, ClipRect, Device as CtrlDevice, PageFlipFlags, +}; +use drm::Device; + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; + +use std::collections::HashSet; +use std::fmt; +use std::marker::PhantomData; +use std::num::NonZeroU32; +use std::os::unix::io::{AsFd, BorrowedFd}; +use std::sync::Arc; + +use crate::backend_interface::*; +use crate::error::{InitError, SoftBufferError, SwResultExt}; + +#[derive(Debug)] +pub(crate) struct KmsDisplayImpl { + /// The underlying raw device file descriptor. + fd: BorrowedFd<'static>, + + /// Holds a reference to the display. + _display: D, +} + +impl AsFd for KmsDisplayImpl { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd + } +} + +impl Device for KmsDisplayImpl {} +impl CtrlDevice for KmsDisplayImpl {} + +impl ContextInterface for Arc> { + fn new(display: D) -> Result> + where + D: Sized, + { + let RawDisplayHandle::Drm(drm) = display.display_handle()?.as_raw() else { + return Err(InitError::Unsupported(display)); + }; + if drm.fd == -1 { + return Err(SoftBufferError::IncompleteDisplayHandle.into()); + } + + // SAFETY: Invariants guaranteed by the user. + let fd = unsafe { BorrowedFd::borrow_raw(drm.fd) }; + + Ok(Arc::new(KmsDisplayImpl { + fd, + _display: display, + })) + } +} + +/// All the necessary types for the Drm/Kms backend. +#[derive(Debug)] +pub(crate) struct KmsImpl { + /// The display implementation. + display: Arc>, + + /// The connectors to use. + connectors: Vec, + + /// The CRTC to render to. + crtc: crtc::Info, + + /// The dumb buffer we're using as a buffer. + buffer: Option, + + /// Window handle that we are keeping around. + window_handle: W, +} + +#[derive(Debug)] +struct Buffers { + /// The involved set of buffers. + buffers: [SharedBuffer; 2], + + /// Whether to use the first buffer or the second buffer as the front buffer. + first_is_front: bool, +} + +/// The buffer implementation. +pub(crate) struct BufferImpl<'a, D: ?Sized, W: ?Sized> { + /// The mapping of the dump buffer. + mapping: DumbMapping<'a>, + + /// The framebuffer object of the current front buffer. + front_fb: framebuffer::Handle, + + /// The CRTC handle. + crtc_handle: crtc::Handle, + + /// This is used to change the front buffer. + first_is_front: &'a mut bool, + + /// The current size. + size: (NonZeroU32, NonZeroU32), + + /// The display implementation. + display: &'a KmsDisplayImpl, + + /// Age of the front buffer. + front_age: &'a mut u8, + + /// Age of the back buffer. + back_age: &'a mut u8, + + /// Window reference. + _window: PhantomData<&'a mut W>, +} + +impl fmt::Debug for BufferImpl<'_, D, W> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // FIXME: Derive instead once `DumbMapping` impls `Debug`. + f.debug_struct("BufferImpl").finish_non_exhaustive() + } +} + +/// The combined frame buffer and dumb buffer. +#[derive(Debug)] +struct SharedBuffer { + /// The frame buffer. + fb: framebuffer::Handle, + + /// The dumb buffer. + db: DumbBuffer, + + /// The age of this buffer. + age: u8, +} + +impl SurfaceInterface for KmsImpl { + type Context = Arc>; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + /// Create a new KMS backend. + fn new(window: W, display: &Arc>) -> Result> { + // Make sure that the window handle is valid. + let RawWindowHandle::Drm(drm) = window.window_handle()?.as_raw() else { + return Err(InitError::Unsupported(window)); + }; + let plane_handle = + NonZeroU32::new(drm.plane).ok_or(SoftBufferError::IncompleteWindowHandle)?; + let plane_handle = plane::Handle::from(plane_handle); + + let plane_info = display + .get_plane(plane_handle) + .swbuf_err("failed to get plane info")?; + let handles = display + .resource_handles() + .swbuf_err("failed to get resource handles")?; + + // Use either the attached CRTC or the primary CRTC. + let crtc = { + let handle = match plane_info.crtc() { + Some(crtc) => crtc, + None => { + tracing::warn!("no CRTC attached to plane, falling back to primary CRTC"); + handles + .filter_crtcs(plane_info.possible_crtcs()) + .first() + .copied() + .swbuf_err("failed to find a primary CRTC")? + } + }; + + // Get info about the CRTC. + display + .get_crtc(handle) + .swbuf_err("failed to get CRTC info")? + }; + + // Figure out all of the encoders that are attached to this CRTC. + let encoders = handles + .encoders + .iter() + .flat_map(|handle| display.get_encoder(*handle)) + .filter(|encoder| encoder.crtc() == Some(crtc.handle())) + .map(|encoder| encoder.handle()) + .collect::>(); + + // Get a list of every connector that the CRTC is connected to via encoders. + let connectors = handles + .connectors + .iter() + .flat_map(|handle| display.get_connector(*handle, false)) + .filter(|connector| { + connector + .current_encoder() + .is_some_and(|encoder| encoders.contains(&encoder)) + }) + .map(|info| info.handle()) + .collect::>(); + + Ok(Self { + crtc, + connectors, + display: display.clone(), + buffer: None, + window_handle: window, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window_handle + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + // Don't resize if we don't have to. + if let Some(buffer) = &self.buffer { + let (buffer_width, buffer_height) = buffer.size(); + if buffer_width == width && buffer_height == height { + return Ok(()); + } + } + + // Create a new buffer set. + let front_buffer = SharedBuffer::new(&self.display, width, height)?; + let back_buffer = SharedBuffer::new(&self.display, width, height)?; + + self.buffer = Some(Buffers { + first_is_front: true, + buffers: [front_buffer, back_buffer], + }); + + Ok(()) + } + + /* + fn fetch(&mut self) -> Result, SoftBufferError> { + // TODO: Implement this! + } + */ + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + // Map the dumb buffer. + let set = self + .buffer + .as_mut() + .expect("Must set size of surface before calling `buffer_mut()`"); + + let size = set.size(); + + let [first_buffer, second_buffer] = &mut set.buffers; + let (front_buffer, back_buffer) = if set.first_is_front { + (first_buffer, second_buffer) + } else { + (second_buffer, first_buffer) + }; + + let front_fb = front_buffer.fb; + let front_age = &mut front_buffer.age; + let back_age = &mut back_buffer.age; + + let mapping = self + .display + .map_dumb_buffer(&mut front_buffer.db) + .swbuf_err("failed to map dumb buffer")?; + + Ok(BufferImpl { + mapping, + size, + first_is_front: &mut set.first_is_front, + front_fb, + crtc_handle: self.crtc.handle(), + display: &self.display, + front_age, + back_age, + _window: PhantomData, + }) + } +} + +impl Drop for KmsImpl { + fn drop(&mut self) { + // Map the CRTC to the information that was there before. + self.display + .set_crtc( + self.crtc.handle(), + self.crtc.framebuffer(), + self.crtc.position(), + &self.connectors, + self.crtc.mode(), + ) + .ok(); + } +} + +impl BufferInterface for BufferImpl<'_, D, W> { + fn width(&self) -> NonZeroU32 { + self.size.0 + } + + fn height(&self) -> NonZeroU32 { + self.size.1 + } + + #[inline] + fn pixels(&self) -> &[u32] { + bytemuck::cast_slice(self.mapping.as_ref()) + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + bytemuck::cast_slice_mut(self.mapping.as_mut()) + } + + #[inline] + fn age(&self) -> u8 { + *self.front_age + } + + #[inline] + fn present_with_damage(self, damage: &[crate::Rect]) -> Result<(), SoftBufferError> { + let rectangles = damage + .iter() + .map(|&rect| { + let err = || SoftBufferError::DamageOutOfRange { rect }; + Ok::<_, SoftBufferError>(ClipRect::new( + rect.x.try_into().map_err(|_| err())?, + rect.y.try_into().map_err(|_| err())?, + rect.x + .checked_add(rect.width.get()) + .and_then(|x| x.try_into().ok()) + .ok_or_else(err)?, + rect.y + .checked_add(rect.height.get()) + .and_then(|y| y.try_into().ok()) + .ok_or_else(err)?, + )) + }) + .collect::, _>>()?; + + // Dirty the framebuffer with out damage rectangles. + // + // Some drivers don't support this, so we just ignore the `ENOSYS` error. + // TODO: It would be nice to not have to heap-allocate the above rectangles if we know that + // this is going to fail. Low hanging fruit PR: add a flag that's set to false if this + // returns `ENOSYS` and check that before allocating the above and running this. + match self.display.dirty_framebuffer(self.front_fb, &rectangles) { + Ok(()) => {} + Err(e) if e.raw_os_error() == Some(rustix::io::Errno::NOSYS.raw_os_error()) => {} + Err(e) => { + return Err(SoftBufferError::PlatformError( + Some("failed to dirty framebuffer".into()), + Some(e.into()), + )); + } + } + + // Swap the buffers. + // TODO: Use atomic commits here! + self.display + .page_flip(self.crtc_handle, self.front_fb, PageFlipFlags::EVENT, None) + .swbuf_err("failed to page flip")?; + + // Flip the front and back buffers. + *self.first_is_front = !*self.first_is_front; + + // Set the ages. + *self.front_age = 1; + if *self.back_age != 0 { + *self.back_age += 1; + } + + Ok(()) + } + + #[inline] + fn present(self) -> Result<(), SoftBufferError> { + let (width, height) = self.size; + self.present_with_damage(&[crate::Rect { + x: 0, + y: 0, + width, + height, + }]) + } +} + +impl SharedBuffer { + /// Create a new buffer set. + pub(crate) fn new( + display: &KmsDisplayImpl, + width: NonZeroU32, + height: NonZeroU32, + ) -> Result { + let db = display + .create_dumb_buffer((width.get(), height.get()), DrmFourcc::Xrgb8888, 32) + .swbuf_err("failed to create dumb buffer")?; + let fb = display + .add_framebuffer(&db, 24, 32) + .swbuf_err("failed to add framebuffer")?; + + Ok(SharedBuffer { fb, db, age: 0 }) + } + + /// Get the size of this buffer. + pub(crate) fn size(&self) -> (NonZeroU32, NonZeroU32) { + let (width, height) = self.db.size(); + + NonZeroU32::new(width) + .and_then(|width| NonZeroU32::new(height).map(|height| (width, height))) + .expect("buffer size is zero") + } +} + +impl Buffers { + /// Get the size of this buffer. + pub(crate) fn size(&self) -> (NonZeroU32, NonZeroU32) { + self.buffers[0].size() + } +} diff --git a/vendor/softbuffer/src/backends/mod.rs b/vendor/softbuffer/src/backends/mod.rs new file mode 100644 index 0000000..067ca90 --- /dev/null +++ b/vendor/softbuffer/src/backends/mod.rs @@ -0,0 +1,52 @@ +use crate::{ContextInterface, InitError}; +use raw_window_handle::HasDisplayHandle; + +#[cfg(target_os = "android")] +pub(crate) mod android; +#[cfg(target_vendor = "apple")] +pub(crate) mod cg; +#[cfg(all( + feature = "kms", + not(any( + target_os = "android", + target_vendor = "apple", + target_os = "redox", + target_family = "wasm", + target_os = "windows" + )) +))] +pub(crate) mod kms; +#[cfg(target_os = "redox")] +pub(crate) mod orbital; +#[cfg(all( + feature = "wayland", + not(any( + target_os = "android", + target_vendor = "apple", + target_os = "redox", + target_family = "wasm", + target_os = "windows" + )) +))] +pub(crate) mod wayland; +#[cfg(target_family = "wasm")] +pub(crate) mod web; +#[cfg(target_os = "windows")] +pub(crate) mod win32; +#[cfg(all( + feature = "x11", + not(any( + target_os = "android", + target_vendor = "apple", + target_os = "redox", + target_family = "wasm", + target_os = "windows" + )) +))] +pub(crate) mod x11; + +impl ContextInterface for D { + fn new(display: D) -> Result> { + Ok(display) + } +} diff --git a/vendor/softbuffer/src/backends/orbital.rs b/vendor/softbuffer/src/backends/orbital.rs new file mode 100644 index 0000000..301baba --- /dev/null +++ b/vendor/softbuffer/src/backends/orbital.rs @@ -0,0 +1,253 @@ +use crate::error::InitError; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, OrbitalWindowHandle, RawWindowHandle}; +use std::{cmp, marker::PhantomData, num::NonZeroU32, slice, str}; + +use crate::backend_interface::*; +use crate::{util, Rect, SoftBufferError}; + +#[derive(Debug)] +struct OrbitalMap { + address: usize, + size: usize, + size_unaligned: usize, +} + +impl OrbitalMap { + unsafe fn new(fd: usize, size_unaligned: usize) -> syscall::Result { + // Page align size + let pages = (size_unaligned + syscall::PAGE_SIZE - 1) / syscall::PAGE_SIZE; + let size = pages * syscall::PAGE_SIZE; + + // Map window buffer + let address = unsafe { + syscall::fmap( + fd, + &syscall::Map { + offset: 0, + size, + flags: syscall::PROT_READ | syscall::PROT_WRITE | syscall::MAP_SHARED, + address: 0, + }, + )? + }; + + Ok(Self { + address, + size, + size_unaligned, + }) + } + + unsafe fn data(&self) -> &[u32] { + unsafe { slice::from_raw_parts(self.address as *const u32, self.size_unaligned / 4) } + } + + unsafe fn data_mut(&mut self) -> &mut [u32] { + unsafe { slice::from_raw_parts_mut(self.address as *mut u32, self.size_unaligned / 4) } + } +} + +impl Drop for OrbitalMap { + fn drop(&mut self) { + unsafe { + // Unmap window buffer on drop + syscall::funmap(self.address, self.size).expect("failed to unmap orbital window"); + } + } +} + +#[derive(Debug)] +pub struct OrbitalImpl { + handle: ThreadSafeWindowHandle, + width: u32, + height: u32, + presented: bool, + window_handle: W, + _display: PhantomData, +} + +#[derive(Debug)] +struct ThreadSafeWindowHandle(OrbitalWindowHandle); +unsafe impl Send for ThreadSafeWindowHandle {} +unsafe impl Sync for ThreadSafeWindowHandle {} + +impl OrbitalImpl { + fn window_fd(&self) -> usize { + self.handle.0.window.as_ptr() as usize + } + + // Read the current width and size + fn window_size(&self) -> (usize, usize) { + let mut window_width = 0; + let mut window_height = 0; + + let mut buf: [u8; 4096] = [0; 4096]; + let count = syscall::fpath(self.window_fd(), &mut buf).unwrap(); + let path = str::from_utf8(&buf[..count]).unwrap(); + // orbital:/x/y/w/h/t + let mut parts = path.split('/').skip(3); + if let Some(w) = parts.next() { + window_width = w.parse::().unwrap_or(0); + } + if let Some(h) = parts.next() { + window_height = h.parse::().unwrap_or(0); + } + + (window_width, window_height) + } + + fn set_buffer(&self, buffer: &[u32], width_u32: u32, height_u32: u32) { + // Read the current width and size + let (window_width, window_height) = self.window_size(); + + { + // Map window buffer + let mut window_map = + unsafe { OrbitalMap::new(self.window_fd(), window_width * window_height * 4) } + .expect("failed to map orbital window"); + + // Window buffer is u32 color data in 0xAABBGGRR format + let window_data = unsafe { window_map.data_mut() }; + + // Copy each line, cropping to fit + let width = width_u32 as usize; + let height = height_u32 as usize; + let min_width = cmp::min(width, window_width); + let min_height = cmp::min(height, window_height); + for y in 0..min_height { + let offset_buffer = y * width; + let offset_data = y * window_width; + window_data[offset_data..offset_data + min_width] + .copy_from_slice(&buffer[offset_buffer..offset_buffer + min_width]); + } + + // Window buffer map is dropped here + } + + // Tell orbital to show the latest window data + syscall::fsync(self.window_fd()).expect("failed to sync orbital window"); + } +} + +impl SurfaceInterface for OrbitalImpl { + type Context = D; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + fn new(window: W, _display: &D) -> Result> { + let raw = window.window_handle()?.as_raw(); + let RawWindowHandle::Orbital(handle) = raw else { + return Err(InitError::Unsupported(window)); + }; + + Ok(Self { + handle: ThreadSafeWindowHandle(handle), + width: 0, + height: 0, + presented: false, + window_handle: window, + _display: PhantomData, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window_handle + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + let width = width.get(); + let height = height.get(); + if width != self.width || height != self.height { + self.presented = false; + self.width = width; + self.height = height; + } + Ok(()) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + let (window_width, window_height) = self.window_size(); + let pixels = if self.width as usize == window_width && self.height as usize == window_height + { + Pixels::Mapping( + unsafe { OrbitalMap::new(self.window_fd(), window_width * window_height * 4) } + .expect("failed to map orbital window"), + ) + } else { + Pixels::Buffer(util::PixelBuffer(vec![ + 0; + self.width as usize + * self.height as usize + ])) + }; + Ok(BufferImpl { imp: self, pixels }) + } +} + +#[derive(Debug)] +enum Pixels { + Mapping(OrbitalMap), + Buffer(util::PixelBuffer), +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D, W> { + imp: &'a mut OrbitalImpl, + pixels: Pixels, +} + +impl BufferInterface for BufferImpl<'_, D, W> { + fn width(&self) -> NonZeroU32 { + NonZeroU32::new(self.imp.width as u32).unwrap() + } + + fn height(&self) -> NonZeroU32 { + NonZeroU32::new(self.imp.height as u32).unwrap() + } + + #[inline] + fn pixels(&self) -> &[u32] { + match &self.pixels { + Pixels::Mapping(mapping) => unsafe { mapping.data() }, + Pixels::Buffer(buffer) => buffer, + } + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + match &mut self.pixels { + Pixels::Mapping(mapping) => unsafe { mapping.data_mut() }, + Pixels::Buffer(buffer) => buffer, + } + } + + fn age(&self) -> u8 { + match self.pixels { + Pixels::Mapping(_) if self.imp.presented => 1, + _ => 0, + } + } + + fn present(self) -> Result<(), SoftBufferError> { + match self.pixels { + Pixels::Mapping(mapping) => { + drop(mapping); + syscall::fsync(self.imp.window_fd()).expect("failed to sync orbital window"); + self.imp.presented = true; + } + Pixels::Buffer(buffer) => { + self.imp + .set_buffer(&buffer, self.imp.width, self.imp.height); + } + } + + Ok(()) + } + + fn present_with_damage(self, _damage: &[Rect]) -> Result<(), SoftBufferError> { + self.present() + } +} diff --git a/vendor/softbuffer/src/backends/wayland/buffer.rs b/vendor/softbuffer/src/backends/wayland/buffer.rs new file mode 100644 index 0000000..49e641f --- /dev/null +++ b/vendor/softbuffer/src/backends/wayland/buffer.rs @@ -0,0 +1,201 @@ +use memmap2::MmapMut; +use std::{ + ffi::CStr, + fs::File, + os::unix::prelude::{AsFd, AsRawFd}, + slice, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; +use wayland_client::{ + protocol::{wl_buffer, wl_shm, wl_shm_pool, wl_surface}, + Connection, Dispatch, QueueHandle, +}; + +use super::State; + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn create_memfile() -> File { + use rustix::fs::{MemfdFlags, SealFlags}; + + let name = unsafe { CStr::from_bytes_with_nul_unchecked("softbuffer\0".as_bytes()) }; + let fd = rustix::fs::memfd_create(name, MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING) + .expect("Failed to create memfd to store buffer."); + rustix::fs::fcntl_add_seals(&fd, SealFlags::SHRINK | SealFlags::SEAL) + .expect("Failed to seal memfd."); + File::from(fd) +} + +#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] +fn create_memfile() -> File { + use rustix::{fs::Mode, io::Errno, shm::OFlags}; + use std::iter; + + // Use a cached RNG to avoid hammering the thread local. + let mut rng = fastrand::Rng::new(); + + for _ in 0..=4 { + let mut name = String::from("softbuffer-"); + name.extend(iter::repeat_with(|| rng.alphanumeric()).take(7)); + name.push('\0'); + + let name = unsafe { CStr::from_bytes_with_nul_unchecked(name.as_bytes()) }; + // `CLOEXEC` is implied with `shm_open` + let fd = rustix::shm::open( + name, + OFlags::RDWR | OFlags::CREATE | OFlags::EXCL, + Mode::RWXU, + ); + if !matches!(fd, Err(Errno::EXIST)) { + let fd = fd.expect("Failed to create POSIX shm to store buffer."); + let _ = rustix::shm::unlink(name); + return File::from(fd); + } + } + + panic!("Failed to generate non-existent shm name") +} + +// Round size to use for pool for given dimensions, rounding up to power of 2 +fn get_pool_size(width: i32, height: i32) -> i32 { + ((width * height * 4) as u32).next_power_of_two() as i32 +} + +unsafe fn map_file(file: &File) -> MmapMut { + unsafe { MmapMut::map_mut(file.as_raw_fd()).expect("Failed to map shared memory") } +} + +#[derive(Debug)] +pub(super) struct WaylandBuffer { + qh: QueueHandle, + tempfile: File, + map: MmapMut, + pool: wl_shm_pool::WlShmPool, + pool_size: i32, + buffer: wl_buffer::WlBuffer, + pub width: i32, + pub height: i32, + released: Arc, + pub age: u8, +} + +impl WaylandBuffer { + pub fn new(shm: &wl_shm::WlShm, width: i32, height: i32, qh: &QueueHandle) -> Self { + // Calculate size to use for shm pool + let pool_size = get_pool_size(width, height); + + // Create an `mmap` shared memory + let tempfile = create_memfile(); + let _ = tempfile.set_len(pool_size as u64); + let map = unsafe { map_file(&tempfile) }; + + // Create wayland shm pool and buffer + let pool = shm.create_pool(tempfile.as_fd(), pool_size, qh, ()); + let released = Arc::new(AtomicBool::new(true)); + let buffer = pool.create_buffer( + 0, + width, + height, + width * 4, + wl_shm::Format::Xrgb8888, + qh, + released.clone(), + ); + + Self { + qh: qh.clone(), + map, + tempfile, + pool, + pool_size, + buffer, + width, + height, + released, + age: 0, + } + } + + pub fn resize(&mut self, width: i32, height: i32) { + // If size is the same, there's nothing to do + if self.width != width || self.height != height { + // Destroy old buffer + self.buffer.destroy(); + + // Grow pool, if needed + let size = ((width * height * 4) as u32).next_power_of_two() as i32; + if size > self.pool_size { + let _ = self.tempfile.set_len(size as u64); + self.pool.resize(size); + self.pool_size = size; + self.map = unsafe { map_file(&self.tempfile) }; + } + + // Create buffer with correct size + self.buffer = self.pool.create_buffer( + 0, + width, + height, + width * 4, + wl_shm::Format::Xrgb8888, + &self.qh, + self.released.clone(), + ); + self.width = width; + self.height = height; + } + } + + pub fn attach(&self, surface: &wl_surface::WlSurface) { + self.released.store(false, Ordering::SeqCst); + surface.attach(Some(&self.buffer), 0, 0); + } + + pub fn released(&self) -> bool { + self.released.load(Ordering::SeqCst) + } + + fn len(&self) -> usize { + self.width as usize * self.height as usize + } + + pub unsafe fn mapped_mut(&mut self) -> &mut [u32] { + unsafe { slice::from_raw_parts_mut(self.map.as_mut_ptr() as *mut u32, self.len()) } + } +} + +impl Drop for WaylandBuffer { + fn drop(&mut self) { + self.buffer.destroy(); + self.pool.destroy(); + } +} + +impl Dispatch for State { + fn event( + _: &mut State, + _: &wl_shm_pool::WlShmPool, + _: wl_shm_pool::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch> for State { + fn event( + _: &mut State, + _: &wl_buffer::WlBuffer, + event: wl_buffer::Event, + released: &Arc, + _: &Connection, + _: &QueueHandle, + ) { + if let wl_buffer::Event::Release = event { + released.store(true, Ordering::SeqCst); + } + } +} diff --git a/vendor/softbuffer/src/backends/wayland/mod.rs b/vendor/softbuffer/src/backends/wayland/mod.rs new file mode 100644 index 0000000..2d37494 --- /dev/null +++ b/vendor/softbuffer/src/backends/wayland/mod.rs @@ -0,0 +1,339 @@ +use crate::{ + backend_interface::*, + error::{InitError, SwResultExt}, + util, Rect, SoftBufferError, +}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; +use std::{ + num::{NonZeroI32, NonZeroU32}, + sync::{Arc, Mutex}, +}; +use wayland_client::{ + backend::{Backend, ObjectId}, + globals::{registry_queue_init, GlobalListContents}, + protocol::{wl_registry, wl_shm, wl_surface}, + Connection, Dispatch, EventQueue, Proxy, QueueHandle, +}; + +mod buffer; +use buffer::WaylandBuffer; + +struct State; + +#[derive(Debug)] +pub struct WaylandDisplayImpl { + conn: Option, + event_queue: Mutex>, + qh: QueueHandle, + shm: wl_shm::WlShm, + + /// The object that owns the display handle. + /// + /// This has to be dropped *after* the `conn` field, because the `conn` field implicitly borrows + /// this. + _display: D, +} + +impl WaylandDisplayImpl { + fn conn(&self) -> &Connection { + self.conn.as_ref().unwrap() + } +} + +impl ContextInterface for Arc> { + fn new(display: D) -> Result> + where + D: Sized, + { + let raw = display.display_handle()?.as_raw(); + let RawDisplayHandle::Wayland(w) = raw else { + return Err(InitError::Unsupported(display)); + }; + + let backend = unsafe { Backend::from_foreign_display(w.display.as_ptr().cast()) }; + let conn = Connection::from_backend(backend); + let (globals, event_queue) = + registry_queue_init(&conn).swbuf_err("Failed to make round trip to server")?; + let qh = event_queue.handle(); + let shm: wl_shm::WlShm = globals + .bind(&qh, 1..=1, ()) + .swbuf_err("Failed to instantiate Wayland Shm")?; + Ok(Arc::new(WaylandDisplayImpl { + conn: Some(conn), + event_queue: Mutex::new(event_queue), + qh, + shm, + _display: display, + })) + } +} + +impl Drop for WaylandDisplayImpl { + fn drop(&mut self) { + // Make sure the connection is dropped first. + self.conn = None; + } +} + +#[derive(Debug)] +pub struct WaylandImpl { + display: Arc>, + surface: Option, + buffers: Option<(WaylandBuffer, WaylandBuffer)>, + size: Option<(NonZeroI32, NonZeroI32)>, + + /// The pointer to the window object. + /// + /// This has to be dropped *after* the `surface` field, because the `surface` field implicitly + /// borrows this. + window_handle: W, +} + +impl WaylandImpl { + fn surface(&self) -> &wl_surface::WlSurface { + self.surface.as_ref().unwrap() + } + + fn present_with_damage(&mut self, damage: &[Rect]) -> Result<(), SoftBufferError> { + let _ = self + .display + .event_queue + .lock() + .unwrap_or_else(|x| x.into_inner()) + .dispatch_pending(&mut State); + + if let Some((front, back)) = &mut self.buffers { + // Swap front and back buffer + std::mem::swap(front, back); + + front.age = 1; + if back.age != 0 { + back.age += 1; + } + + front.attach(self.surface.as_ref().unwrap()); + + // Like Mesa's EGL/WSI implementation, we damage the whole buffer with `i32::MAX` if + // the compositor doesn't support `damage_buffer`. + // https://bugs.freedesktop.org/show_bug.cgi?id=78190 + if self.surface().version() < 4 { + self.surface().damage(0, 0, i32::MAX, i32::MAX); + } else { + for rect in damage { + // Introduced in version 4, it is an error to use this request in version 3 or lower. + let (x, y, width, height) = (|| { + Some(( + i32::try_from(rect.x).ok()?, + i32::try_from(rect.y).ok()?, + i32::try_from(rect.width.get()).ok()?, + i32::try_from(rect.height.get()).ok()?, + )) + })() + .ok_or(SoftBufferError::DamageOutOfRange { rect: *rect })?; + self.surface().damage_buffer(x, y, width, height); + } + } + + self.surface().commit(); + } + + let _ = self + .display + .event_queue + .lock() + .unwrap_or_else(|x| x.into_inner()) + .flush(); + + Ok(()) + } +} + +impl SurfaceInterface + for WaylandImpl +{ + type Context = Arc>; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + fn new(window: W, display: &Arc>) -> Result> { + // Get the raw Wayland window. + let raw = window.window_handle()?.as_raw(); + let RawWindowHandle::Wayland(w) = raw else { + return Err(InitError::Unsupported(window)); + }; + + let surface_id = unsafe { + ObjectId::from_ptr( + wl_surface::WlSurface::interface(), + w.surface.as_ptr().cast(), + ) + } + .swbuf_err("Failed to create proxy for surface ID.")?; + let surface = wl_surface::WlSurface::from_id(display.conn(), surface_id) + .swbuf_err("Failed to create proxy for surface ID.")?; + Ok(Self { + display: display.clone(), + surface: Some(surface), + buffers: Default::default(), + size: None, + window_handle: window, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window_handle + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + self.size = Some( + (|| { + let width = NonZeroI32::try_from(width).ok()?; + let height = NonZeroI32::try_from(height).ok()?; + Some((width, height)) + })() + .ok_or(SoftBufferError::SizeOutOfRange { width, height })?, + ); + Ok(()) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + let (width, height) = self + .size + .expect("Must set size of surface before calling `buffer_mut()`"); + + if let Some((_front, back)) = &mut self.buffers { + // Block if back buffer not released yet + if !back.released() { + let mut event_queue = self + .display + .event_queue + .lock() + .unwrap_or_else(|x| x.into_inner()); + while !back.released() { + event_queue.blocking_dispatch(&mut State).map_err(|err| { + SoftBufferError::PlatformError( + Some("Wayland dispatch failure".to_string()), + Some(Box::new(err)), + ) + })?; + } + } + + // Resize, if buffer isn't large enough + back.resize(width.get(), height.get()); + } else { + // Allocate front and back buffer + self.buffers = Some(( + WaylandBuffer::new( + &self.display.shm, + width.get(), + height.get(), + &self.display.qh, + ), + WaylandBuffer::new( + &self.display.shm, + width.get(), + height.get(), + &self.display.qh, + ), + )); + }; + + let width = self.buffers.as_mut().unwrap().1.width; + let height = self.buffers.as_mut().unwrap().1.height; + let age = self.buffers.as_mut().unwrap().1.age; + Ok(BufferImpl { + stack: util::BorrowStack::new(self, |buffer| { + Ok(unsafe { buffer.buffers.as_mut().unwrap().1.mapped_mut() }) + })?, + width, + height, + age, + }) + } +} + +impl Drop for WaylandImpl { + fn drop(&mut self) { + // Make sure the surface is dropped first. + self.surface = None; + } +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D: ?Sized, W> { + stack: util::BorrowStack<'a, WaylandImpl, [u32]>, + width: i32, + height: i32, + age: u8, +} + +impl BufferInterface for BufferImpl<'_, D, W> { + fn width(&self) -> NonZeroU32 { + NonZeroU32::new(self.width as u32).unwrap() + } + + fn height(&self) -> NonZeroU32 { + NonZeroU32::new(self.height as usize as u32).unwrap() + } + + #[inline] + fn pixels(&self) -> &[u32] { + self.stack.member() + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + self.stack.member_mut() + } + + fn age(&self) -> u8 { + self.age + } + + fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + self.stack.into_container().present_with_damage(damage) + } + + fn present(self) -> Result<(), SoftBufferError> { + let imp = self.stack.into_container(); + let (width, height) = imp + .size + .expect("Must set size of surface before calling `present()`"); + imp.present_with_damage(&[Rect { + x: 0, + y: 0, + // We know width/height will be non-negative + width: width.try_into().unwrap(), + height: height.try_into().unwrap(), + }]) + } +} + +impl Dispatch for State { + fn event( + _: &mut State, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + // Ignore globals added after initialization + } +} + +impl Dispatch for State { + fn event( + _: &mut State, + _: &wl_shm::WlShm, + _: wl_shm::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} diff --git a/vendor/softbuffer/src/backends/web.rs b/vendor/softbuffer/src/backends/web.rs new file mode 100644 index 0000000..358e934 --- /dev/null +++ b/vendor/softbuffer/src/backends/web.rs @@ -0,0 +1,446 @@ +//! Implementation of software buffering for web targets. + +#![allow(clippy::uninlined_format_args)] + +use js_sys::Object; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::ImageData; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; +use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; + +use crate::backend_interface::*; +use crate::error::{InitError, SwResultExt}; +use crate::{util, NoDisplayHandle, NoWindowHandle, Rect, SoftBufferError}; +use std::marker::PhantomData; +use std::num::NonZeroU32; + +/// Display implementation for the web platform. +/// +/// This just caches the document to prevent having to query it every time. +#[derive(Clone, Debug)] +pub struct WebDisplayImpl { + document: web_sys::Document, + _display: D, +} + +impl ContextInterface for WebDisplayImpl { + fn new(display: D) -> Result> { + let raw = display.display_handle()?.as_raw(); + let RawDisplayHandle::Web(..) = raw else { + return Err(InitError::Unsupported(display)); + }; + + let document = web_sys::window() + .swbuf_err("`Window` is not present in this runtime")? + .document() + .swbuf_err("`Document` is not present in this runtime")?; + + Ok(Self { + document, + _display: display, + }) + } +} + +#[derive(Debug)] +pub struct WebImpl { + /// The handle and context to the canvas that we're drawing to. + canvas: Canvas, + + /// The buffer that we're drawing to. + buffer: util::PixelBuffer, + + /// Buffer has been presented. + buffer_presented: bool, + + /// The current canvas width/height. + size: Option<(NonZeroU32, NonZeroU32)>, + + /// The underlying window handle. + window_handle: W, + + /// The underlying display handle. + _display: PhantomData, +} + +/// Holding canvas and context for [`HtmlCanvasElement`] or [`OffscreenCanvas`], +/// since they have different types. +#[derive(Debug)] +enum Canvas { + Canvas { + canvas: HtmlCanvasElement, + ctx: CanvasRenderingContext2d, + }, + OffscreenCanvas { + canvas: OffscreenCanvas, + ctx: OffscreenCanvasRenderingContext2d, + }, +} + +impl WebImpl { + fn from_canvas(canvas: HtmlCanvasElement, window: W) -> Result { + let ctx = Self::resolve_ctx(canvas.get_context("2d").ok(), "CanvasRenderingContext2d")?; + + Ok(Self { + canvas: Canvas::Canvas { canvas, ctx }, + buffer: util::PixelBuffer(Vec::new()), + buffer_presented: false, + size: None, + window_handle: window, + _display: PhantomData, + }) + } + + fn from_offscreen_canvas(canvas: OffscreenCanvas, window: W) -> Result { + let ctx = Self::resolve_ctx( + canvas.get_context("2d").ok(), + "OffscreenCanvasRenderingContext2d", + )?; + + Ok(Self { + canvas: Canvas::OffscreenCanvas { canvas, ctx }, + buffer: util::PixelBuffer(Vec::new()), + buffer_presented: false, + size: None, + window_handle: window, + _display: PhantomData, + }) + } + + fn resolve_ctx( + result: Option>, + name: &str, + ) -> Result { + let ctx = result + .swbuf_err("Canvas already controlled using `OffscreenCanvas`")? + .swbuf_err(format!( + "A canvas context other than `{name}` was already created" + ))? + .dyn_into() + .unwrap_or_else(|_| panic!("`getContext(\"2d\") didn't return a `{name}`")); + + Ok(ctx) + } + + fn present_with_damage(&mut self, damage: &[Rect]) -> Result<(), SoftBufferError> { + let (buffer_width, _buffer_height) = self + .size + .expect("Must set size of surface before calling `present_with_damage()`"); + + let union_damage = if let Some(rect) = util::union_damage(damage) { + rect + } else { + return Ok(()); + }; + + // Create a bitmap from the buffer. + let bitmap: Vec<_> = self + .buffer + .chunks_exact(buffer_width.get() as usize) + .skip(union_damage.y as usize) + .take(union_damage.height.get() as usize) + .flat_map(|row| { + row.iter() + .skip(union_damage.x as usize) + .take(union_damage.width.get() as usize) + }) + .copied() + .flat_map(|pixel| [(pixel >> 16) as u8, (pixel >> 8) as u8, pixel as u8, 255]) + .collect(); + + debug_assert_eq!( + bitmap.len() as u32, + union_damage.width.get() * union_damage.height.get() * 4 + ); + + #[cfg(target_feature = "atomics")] + #[allow(non_local_definitions)] + let result = { + // When using atomics, the underlying memory becomes `SharedArrayBuffer`, + // which can't be shared with `ImageData`. + use js_sys::{Uint8Array, Uint8ClampedArray}; + use wasm_bindgen::prelude::wasm_bindgen; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_name = ImageData)] + type ImageDataExt; + + #[wasm_bindgen(catch, constructor, js_class = ImageData)] + fn new(array: Uint8ClampedArray, sw: u32) -> Result; + } + + let array = Uint8Array::new_with_length(bitmap.len() as u32); + array.copy_from(&bitmap); + let array = Uint8ClampedArray::new(&array); + ImageDataExt::new(array, union_damage.width.get()) + .map(JsValue::from) + .map(ImageData::unchecked_from_js) + }; + #[cfg(not(target_feature = "atomics"))] + let result = ImageData::new_with_u8_clamped_array( + wasm_bindgen::Clamped(&bitmap), + union_damage.width.get(), + ); + // This should only throw an error if the buffer we pass's size is incorrect. + let image_data = result.unwrap(); + + for rect in damage { + // This can only throw an error if `data` is detached, which is impossible. + self.canvas + .put_image_data( + &image_data, + union_damage.x.into(), + union_damage.y.into(), + (rect.x - union_damage.x).into(), + (rect.y - union_damage.y).into(), + rect.width.get().into(), + rect.height.get().into(), + ) + .unwrap(); + } + + self.buffer_presented = true; + + Ok(()) + } +} + +impl SurfaceInterface for WebImpl { + type Context = WebDisplayImpl; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + fn new(window: W, display: &WebDisplayImpl) -> Result> { + let raw = window.window_handle()?.as_raw(); + let canvas: HtmlCanvasElement = match raw { + RawWindowHandle::Web(handle) => { + display + .document + .query_selector(&format!("canvas[data-raw-handle=\"{}\"]", handle.id)) + // `querySelector` only throws an error if the selector is invalid. + .unwrap() + .swbuf_err("No canvas found with the given id")? + // We already made sure this was a canvas in `querySelector`. + .unchecked_into() + } + RawWindowHandle::WebCanvas(handle) => { + let value: &JsValue = unsafe { handle.obj.cast().as_ref() }; + value.clone().unchecked_into() + } + RawWindowHandle::WebOffscreenCanvas(handle) => { + let value: &JsValue = unsafe { handle.obj.cast().as_ref() }; + let canvas: OffscreenCanvas = value.clone().unchecked_into(); + + return Self::from_offscreen_canvas(canvas, window).map_err(InitError::Failure); + } + _ => return Err(InitError::Unsupported(window)), + }; + + Self::from_canvas(canvas, window).map_err(InitError::Failure) + } + + /// Get the inner window handle. + #[inline] + fn window(&self) -> &W { + &self.window_handle + } + + /// De-duplicates the error handling between `HtmlCanvasElement` and `OffscreenCanvas`. + /// Resize the canvas to the given dimensions. + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + if self.size != Some((width, height)) { + self.buffer_presented = false; + self.buffer.resize(total_len(width.get(), height.get()), 0); + self.canvas.set_width(width.get()); + self.canvas.set_height(height.get()); + self.size = Some((width, height)); + } + + Ok(()) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + Ok(BufferImpl { imp: self }) + } + + fn fetch(&mut self) -> Result, SoftBufferError> { + let (width, height) = self + .size + .expect("Must set size of surface before calling `fetch()`"); + + let image_data = self + .canvas + .get_image_data(0., 0., width.get().into(), height.get().into()) + .ok() + // TODO: Can also error if width or height are 0. + .swbuf_err("`Canvas` contains pixels from a different origin")?; + + Ok(image_data + .data() + .0 + .chunks_exact(4) + .map(|chunk| u32::from_be_bytes([0, chunk[0], chunk[1], chunk[2]])) + .collect()) + } +} + +/// Extension methods for the Wasm target on [`Surface`](crate::Surface). +pub trait SurfaceExtWeb: Sized { + /// Creates a new instance of this struct, using the provided [`HtmlCanvasElement`]. + /// + /// # Errors + /// - If the canvas was already controlled by an `OffscreenCanvas`. + /// - If a another context then "2d" was already created for this canvas. + fn from_canvas(canvas: HtmlCanvasElement) -> Result; + + /// Creates a new instance of this struct, using the provided [`OffscreenCanvas`]. + /// + /// # Errors + /// If a another context then "2d" was already created for this canvas. + fn from_offscreen_canvas(offscreen_canvas: OffscreenCanvas) -> Result; +} + +impl SurfaceExtWeb for crate::Surface { + fn from_canvas(canvas: HtmlCanvasElement) -> Result { + let imple = crate::SurfaceDispatch::Web(WebImpl::from_canvas(canvas, NoWindowHandle(()))?); + + Ok(Self { + surface_impl: Box::new(imple), + _marker: PhantomData, + }) + } + + fn from_offscreen_canvas(offscreen_canvas: OffscreenCanvas) -> Result { + let imple = crate::SurfaceDispatch::Web(WebImpl::from_offscreen_canvas( + offscreen_canvas, + NoWindowHandle(()), + )?); + + Ok(Self { + surface_impl: Box::new(imple), + _marker: PhantomData, + }) + } +} + +impl Canvas { + fn set_width(&self, width: u32) { + match self { + Self::Canvas { canvas, .. } => canvas.set_width(width), + Self::OffscreenCanvas { canvas, .. } => canvas.set_width(width), + } + } + + fn set_height(&self, height: u32) { + match self { + Self::Canvas { canvas, .. } => canvas.set_height(height), + Self::OffscreenCanvas { canvas, .. } => canvas.set_height(height), + } + } + + fn get_image_data(&self, sx: f64, sy: f64, sw: f64, sh: f64) -> Result { + match self { + Canvas::Canvas { ctx, .. } => ctx.get_image_data(sx, sy, sw, sh), + Canvas::OffscreenCanvas { ctx, .. } => ctx.get_image_data(sx, sy, sw, sh), + } + } + + // NOTE: suppress the lint because we mirror `CanvasRenderingContext2D.putImageData()`, and + // this is just an internal API used by this module only, so it's not too relevant. + #[allow(clippy::too_many_arguments)] + fn put_image_data( + &self, + imagedata: &ImageData, + dx: f64, + dy: f64, + dirty_x: f64, + dirty_y: f64, + width: f64, + height: f64, + ) -> Result<(), JsValue> { + match self { + Self::Canvas { ctx, .. } => ctx + .put_image_data_with_dirty_x_and_dirty_y_and_dirty_width_and_dirty_height( + imagedata, dx, dy, dirty_x, dirty_y, width, height, + ), + Self::OffscreenCanvas { ctx, .. } => ctx + .put_image_data_with_dirty_x_and_dirty_y_and_dirty_width_and_dirty_height( + imagedata, dx, dy, dirty_x, dirty_y, width, height, + ), + } + } +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D, W> { + imp: &'a mut WebImpl, +} + +impl BufferInterface for BufferImpl<'_, D, W> { + fn width(&self) -> NonZeroU32 { + self.imp + .size + .expect("must set size of surface before calling `width()` on the buffer") + .0 + } + + fn height(&self) -> NonZeroU32 { + self.imp + .size + .expect("must set size of surface before calling `height()` on the buffer") + .1 + } + + fn pixels(&self) -> &[u32] { + &self.imp.buffer + } + + fn pixels_mut(&mut self) -> &mut [u32] { + &mut self.imp.buffer + } + + fn age(&self) -> u8 { + if self.imp.buffer_presented { + 1 + } else { + 0 + } + } + + /// Push the buffer to the canvas. + fn present(self) -> Result<(), SoftBufferError> { + let (width, height) = self + .imp + .size + .expect("Must set size of surface before calling `present()`"); + self.imp.present_with_damage(&[Rect { + x: 0, + y: 0, + width, + height, + }]) + } + + fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + self.imp.present_with_damage(damage) + } +} + +#[inline(always)] +fn total_len(width: u32, height: u32) -> usize { + // Convert width and height to `usize`, then multiply. + width + .try_into() + .ok() + .and_then(|w: usize| height.try_into().ok().and_then(|h| w.checked_mul(h))) + .unwrap_or_else(|| { + panic!( + "Overflow when calculating total length of buffer: {}x{}", + width, height + ); + }) +} diff --git a/vendor/softbuffer/src/backends/win32.rs b/vendor/softbuffer/src/backends/win32.rs new file mode 100644 index 0000000..75de23e --- /dev/null +++ b/vendor/softbuffer/src/backends/win32.rs @@ -0,0 +1,482 @@ +//! Implementation of software buffering for Windows. +//! +//! This module converts the input buffer into a bitmap and then stretches it to the window. + +use crate::backend_interface::*; +use crate::{Rect, SoftBufferError}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; + +use std::io; +use std::marker::PhantomData; +use std::mem; +use std::num::{NonZeroI32, NonZeroU32}; +use std::ptr::{self, NonNull}; +use std::slice; +use std::sync::{mpsc, Mutex, OnceLock}; +use std::thread; + +use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::Graphics::Gdi; + +const ZERO_QUAD: Gdi::RGBQUAD = Gdi::RGBQUAD { + rgbBlue: 0, + rgbGreen: 0, + rgbRed: 0, + rgbReserved: 0, +}; + +#[derive(Debug)] +struct Buffer { + dc: Gdi::HDC, + bitmap: Gdi::HBITMAP, + pixels: NonNull, + width: NonZeroI32, + height: NonZeroI32, + presented: bool, +} + +unsafe impl Send for Buffer {} + +impl Drop for Buffer { + fn drop(&mut self) { + unsafe { + Gdi::DeleteObject(self.bitmap); + } + + Allocator::get().deallocate(self.dc); + } +} + +impl Buffer { + fn new(window_dc: Gdi::HDC, width: NonZeroI32, height: NonZeroI32) -> Self { + let dc = Allocator::get().allocate(window_dc); + assert!(!dc.is_null()); + + // Create a new bitmap info struct. + let bitmap_info = BitmapInfo { + bmi_header: Gdi::BITMAPINFOHEADER { + biSize: mem::size_of::() as u32, + biWidth: width.get(), + biHeight: -height.get(), + biPlanes: 1, + biBitCount: 32, + biCompression: Gdi::BI_BITFIELDS, + biSizeImage: 0, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmi_colors: [ + Gdi::RGBQUAD { + rgbRed: 0xff, + ..ZERO_QUAD + }, + Gdi::RGBQUAD { + rgbGreen: 0xff, + ..ZERO_QUAD + }, + Gdi::RGBQUAD { + rgbBlue: 0xff, + ..ZERO_QUAD + }, + ], + }; + + // XXX alignment? + // XXX better to use CreateFileMapping, and pass hSection? + // XXX test return value? + let mut pixels: *mut u32 = ptr::null_mut(); + let bitmap = unsafe { + Gdi::CreateDIBSection( + dc, + &bitmap_info as *const BitmapInfo as *const _, + Gdi::DIB_RGB_COLORS, + &mut pixels as *mut *mut u32 as _, + ptr::null_mut(), + 0, + ) + }; + assert!(!bitmap.is_null()); + let pixels = NonNull::new(pixels).unwrap(); + + unsafe { + Gdi::SelectObject(dc, bitmap); + } + + Self { + dc, + bitmap, + width, + height, + pixels, + presented: false, + } + } + + #[inline] + fn pixels(&self) -> &[u32] { + unsafe { + slice::from_raw_parts( + self.pixels.as_ptr(), + i32::from(self.width) as usize * i32::from(self.height) as usize, + ) + } + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + unsafe { + slice::from_raw_parts_mut( + self.pixels.as_ptr(), + i32::from(self.width) as usize * i32::from(self.height) as usize, + ) + } + } +} + +/// The handle to a window for software buffering. +#[derive(Debug)] +pub struct Win32Impl { + /// The window handle. + window: OnlyUsedFromOrigin, + + /// The device context for the window. + dc: OnlyUsedFromOrigin, + + /// The buffer used to hold the image. + buffer: Option, + + /// The handle for the window. + /// + /// This should be kept alive in order to keep `window` valid. + handle: W, + + /// The display handle. + /// + /// We don't use this, but other code might. + _display: PhantomData, +} + +impl Drop for Win32Impl { + fn drop(&mut self) { + // Release our resources. + Allocator::get().release(self.window.0, self.dc.0); + } +} + +/// The Win32-compatible bitmap information. +#[repr(C)] +struct BitmapInfo { + bmi_header: Gdi::BITMAPINFOHEADER, + bmi_colors: [Gdi::RGBQUAD; 3], +} + +impl Win32Impl { + fn present_with_damage(&mut self, damage: &[Rect]) -> Result<(), SoftBufferError> { + let buffer = self.buffer.as_mut().unwrap(); + unsafe { + for rect in damage.iter().copied() { + let (x, y, width, height) = (|| { + Some(( + i32::try_from(rect.x).ok()?, + i32::try_from(rect.y).ok()?, + i32::try_from(rect.width.get()).ok()?, + i32::try_from(rect.height.get()).ok()?, + )) + })() + .ok_or(SoftBufferError::DamageOutOfRange { rect })?; + Gdi::BitBlt( + self.dc.0, + x, + y, + width, + height, + buffer.dc, + x, + y, + Gdi::SRCCOPY, + ); + } + + // Validate the window. + Gdi::ValidateRect(self.window.0, ptr::null_mut()); + } + buffer.presented = true; + + Ok(()) + } +} + +impl SurfaceInterface for Win32Impl { + type Context = D; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + /// Create a new `Win32Impl` from a `Win32WindowHandle`. + fn new(window: W, _display: &D) -> Result> { + let raw = window.window_handle()?.as_raw(); + let RawWindowHandle::Win32(handle) = raw else { + return Err(crate::InitError::Unsupported(window)); + }; + + // Get the handle to the device context. + // SAFETY: We have confirmed that the window handle is valid. + let hwnd = handle.hwnd.get() as HWND; + let dc = Allocator::get().get_dc(hwnd); + + // GetDC returns null if there is a platform error. + if dc.is_null() { + return Err(SoftBufferError::PlatformError( + Some("Device Context is null".into()), + Some(Box::new(io::Error::last_os_error())), + ) + .into()); + } + + Ok(Self { + dc: dc.into(), + window: hwnd.into(), + buffer: None, + handle: window, + _display: PhantomData, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.handle + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + let (width, height) = (|| { + let width = NonZeroI32::try_from(width).ok()?; + let height = NonZeroI32::try_from(height).ok()?; + Some((width, height)) + })() + .ok_or(SoftBufferError::SizeOutOfRange { width, height })?; + + if let Some(buffer) = self.buffer.as_ref() { + if buffer.width == width && buffer.height == height { + return Ok(()); + } + } + + self.buffer = Some(Buffer::new(self.dc.0, width, height)); + + Ok(()) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + if self.buffer.is_none() { + panic!("Must set size of surface before calling `buffer_mut()`"); + } + + Ok(BufferImpl(self)) + } + + /// Fetch the buffer from the window. + fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D, W>(&'a mut Win32Impl); + +impl BufferInterface for BufferImpl<'_, D, W> { + fn width(&self) -> NonZeroU32 { + self.0.buffer.as_ref().unwrap().width.try_into().unwrap() + } + + fn height(&self) -> NonZeroU32 { + self.0.buffer.as_ref().unwrap().height.try_into().unwrap() + } + + #[inline] + fn pixels(&self) -> &[u32] { + self.0.buffer.as_ref().unwrap().pixels() + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + self.0.buffer.as_mut().unwrap().pixels_mut() + } + + fn age(&self) -> u8 { + match self.0.buffer.as_ref() { + Some(buffer) if buffer.presented => 1, + _ => 0, + } + } + + fn present(self) -> Result<(), SoftBufferError> { + let imp = self.0; + let buffer = imp.buffer.as_ref().unwrap(); + imp.present_with_damage(&[Rect { + x: 0, + y: 0, + // We know width/height will be non-negative + width: buffer.width.try_into().unwrap(), + height: buffer.height.try_into().unwrap(), + }]) + } + + fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + let imp = self.0; + imp.present_with_damage(damage) + } +} + +/// Allocator for device contexts. +/// +/// Device contexts can only be allocated or freed on the thread that originated them. +/// So we spawn a thread specifically for allocating and freeing device contexts. +/// This is the interface to that thread. +struct Allocator { + /// The channel for sending commands. + sender: Mutex>, +} + +impl Allocator { + /// Get the global instance of the allocator. + fn get() -> &'static Allocator { + static ALLOCATOR: OnceLock = OnceLock::new(); + ALLOCATOR.get_or_init(|| { + let (sender, receiver) = mpsc::channel::(); + + // Create a thread responsible for DC handling. + thread::Builder::new() + .name(concat!("softbuffer_", env!("CARGO_PKG_VERSION"), "_dc_allocator").into()) + .spawn(move || { + while let Ok(command) = receiver.recv() { + command.handle(); + } + }) + .expect("failed to spawn the DC allocator thread"); + + Allocator { + sender: Mutex::new(sender), + } + }) + } + + /// Send a command to the allocator thread. + fn send_command(&self, cmd: Command) { + self.sender.lock().unwrap().send(cmd).unwrap(); + } + + /// Get the device context for a window. + fn get_dc(&self, window: HWND) -> Gdi::HDC { + let (callback, waiter) = mpsc::sync_channel(1); + + // Send command to the allocator. + self.send_command(Command::GetDc { window, callback }); + + // Wait for the response back. + waiter.recv().unwrap() + } + + /// Allocate a new device context. + fn allocate(&self, dc: Gdi::HDC) -> Gdi::HDC { + let (callback, waiter) = mpsc::sync_channel(1); + + // Send command to the allocator. + self.send_command(Command::Allocate { dc, callback }); + + // Wait for the response back. + waiter.recv().unwrap() + } + + /// Deallocate a device context. + fn deallocate(&self, dc: Gdi::HDC) { + self.send_command(Command::Deallocate(dc)); + } + + /// Release a device context. + fn release(&self, owner: HWND, dc: Gdi::HDC) { + self.send_command(Command::Release { dc, owner }); + } +} + +/// Commands to be sent to the allocator. +enum Command { + /// Call `GetDc` to get the device context for the provided window. + GetDc { + /// The window to provide a device context for. + window: HWND, + + /// Send back the device context. + callback: mpsc::SyncSender, + }, + + /// Allocate a new device context using `GetCompatibleDc`. + Allocate { + /// The DC to be compatible with. + dc: Gdi::HDC, + + /// Send back the device context. + callback: mpsc::SyncSender, + }, + + /// Deallocate a device context. + Deallocate(Gdi::HDC), + + /// Release a window-associated device context. + Release { + /// The device context to release. + dc: Gdi::HDC, + + /// The window that owns this device context. + owner: HWND, + }, +} + +unsafe impl Send for Command {} + +impl Command { + /// Handle this command. + /// + /// This should be called on the allocator thread. + fn handle(self) { + match self { + Self::GetDc { window, callback } => { + // Get the DC and send it back. + let dc = unsafe { Gdi::GetDC(window) }; + callback.send(dc).ok(); + } + + Self::Allocate { dc, callback } => { + // Allocate a DC and send it back. + let dc = unsafe { Gdi::CreateCompatibleDC(dc) }; + callback.send(dc).ok(); + } + + Self::Deallocate(dc) => { + // Deallocate this DC. + unsafe { + Gdi::DeleteDC(dc); + } + } + + Self::Release { dc, owner } => { + // Release this DC. + unsafe { + Gdi::ReleaseDC(owner, dc); + } + } + } + } +} + +#[derive(Debug)] +struct OnlyUsedFromOrigin(T); +unsafe impl Send for OnlyUsedFromOrigin {} + +impl From for OnlyUsedFromOrigin { + fn from(t: T) -> Self { + Self(t) + } +} diff --git a/vendor/softbuffer/src/backends/x11.rs b/vendor/softbuffer/src/backends/x11.rs new file mode 100644 index 0000000..711b0b7 --- /dev/null +++ b/vendor/softbuffer/src/backends/x11.rs @@ -0,0 +1,1023 @@ +//! Implementation of software buffering for X11. +//! +//! This module converts the input buffer into an XImage and then sends it over the wire to be +//! drawn by the X server. The SHM extension is used if available. + +#![allow(clippy::uninlined_format_args)] + +use crate::backend_interface::*; +use crate::error::{InitError, SwResultExt}; +use crate::{util, Rect, SoftBufferError}; +use raw_window_handle::{ + HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, + XcbWindowHandle, +}; +use rustix::{ + fd::{AsFd, BorrowedFd, OwnedFd}, + mm, shm as posix_shm, +}; + +use std::{ + collections::HashSet, + fmt, + fs::File, + io, mem, + num::{NonZeroU16, NonZeroU32}, + ptr::{null_mut, NonNull}, + slice, + sync::Arc, +}; + +use as_raw_xcb_connection::AsRawXcbConnection; +use x11rb::connection::{Connection, SequenceNumber}; +use x11rb::cookie::Cookie; +use x11rb::errors::{ConnectionError, ReplyError, ReplyOrIdError}; +use x11rb::protocol::shm::{self, ConnectionExt as _}; +use x11rb::protocol::xproto::{self, ConnectionExt as _, ImageOrder, VisualClass, Visualid}; +use x11rb::xcb_ffi::XCBConnection; + +#[derive(Debug)] +pub struct X11DisplayImpl { + /// The handle to the XCB connection. + connection: Option, + + /// SHM extension is available. + is_shm_available: bool, + + /// All visuals using softbuffer's pixel representation + supported_visuals: HashSet, + + /// The generic display where the `connection` field comes from. + /// + /// Without `&mut`, the underlying connection cannot be closed without other unsafe behavior. + /// With `&mut`, the connection can be dropped without us knowing about it. Therefore, we + /// cannot provide `&mut` access to this field. + _display: D, +} + +impl ContextInterface for Arc> { + /// Create a new `X11DisplayImpl`. + fn new(display: D) -> Result> + where + D: Sized, + { + // Get the underlying libxcb handle. + let raw = display.display_handle()?.as_raw(); + let xcb_handle = match raw { + RawDisplayHandle::Xcb(xcb_handle) => xcb_handle, + RawDisplayHandle::Xlib(xlib) => { + // Convert to an XCB handle. + let connection = xlib.display.map(|display| { + // Get the underlying XCB connection. + // SAFETY: The user has asserted that the display handle is valid. + unsafe { + let display = tiny_xlib::Display::from_ptr(display.as_ptr()); + NonNull::new_unchecked(display.as_raw_xcb_connection()).cast() + } + }); + + // Construct the equivalent XCB display and window handles. + XcbDisplayHandle::new(connection, xlib.screen) + } + _ => return Err(InitError::Unsupported(display)), + }; + + // Validate the display handle to ensure we can use it. + let connection = match xcb_handle.connection { + Some(connection) => { + // Wrap the display handle in an x11rb connection. + // SAFETY: We don't own the connection, so don't drop it. We also assert that the connection is valid. + let result = + unsafe { XCBConnection::from_raw_xcb_connection(connection.as_ptr(), false) }; + + result.swbuf_err("Failed to wrap XCB connection")? + } + None => { + // The user didn't provide an XCB connection, so create our own. + tracing::info!("no XCB connection provided by the user, so spawning our own"); + XCBConnection::connect(None) + .swbuf_err("Failed to spawn XCB connection")? + .0 + } + }; + + let is_shm_available = is_shm_available(&connection); + if !is_shm_available { + tracing::warn!("SHM extension is not available. Performance may be poor."); + } + + let supported_visuals = supported_visuals(&connection); + + Ok(Arc::new(X11DisplayImpl { + connection: Some(connection), + is_shm_available, + supported_visuals, + _display: display, + })) + } +} + +impl X11DisplayImpl { + fn connection(&self) -> &XCBConnection { + self.connection + .as_ref() + .expect("X11DisplayImpl::connection() called after X11DisplayImpl::drop()") + } +} + +/// The handle to an X11 drawing context. +#[derive(Debug)] +pub struct X11Impl { + /// X display this window belongs to. + display: Arc>, + + /// The window to draw to. + window: xproto::Window, + + /// The graphics context to use when drawing. + gc: xproto::Gcontext, + + /// The depth (bits per pixel) of the drawing context. + depth: u8, + + /// The visual ID of the drawing context. + visual_id: u32, + + /// The buffer we draw to. + buffer: Buffer, + + /// Buffer has been presented. + buffer_presented: bool, + + /// The current buffer width/height. + size: Option<(NonZeroU16, NonZeroU16)>, + + /// Keep the window alive. + window_handle: W, +} + +/// The buffer that is being drawn to. +#[derive(Debug)] +enum Buffer { + /// A buffer implemented using shared memory to prevent unnecessary copying. + Shm(ShmBuffer), + + /// A normal buffer that we send over the wire. + Wire(util::PixelBuffer), +} + +#[derive(Debug)] +struct ShmBuffer { + /// The shared memory segment, paired with its ID. + seg: Option<(ShmSegment, shm::Seg)>, + + /// A cookie indicating that the shared memory segment is ready to be used. + /// + /// We can't soundly read from or write to the SHM segment until the X server is done processing the + /// `shm::PutImage` request. However, the X server handles requests in order, which means that, if + /// we send a very small request after the `shm::PutImage` request, then the X server will have to + /// process that request before it can process the `shm::PutImage` request. Therefore, we can use + /// the reply to that small request to determine when the `shm::PutImage` request is done. + /// + /// In this case, we use `GetInputFocus` since it is a very small request. + /// + /// We store the sequence number instead of the `Cookie` since we cannot hold a self-referential + /// reference to the `connection` field. + done_processing: Option, +} + +impl SurfaceInterface for X11Impl { + type Context = Arc>; + type Buffer<'a> + = BufferImpl<'a, D, W> + where + Self: 'a; + + /// Create a new `X11Impl` from a `HasWindowHandle`. + fn new(window_src: W, display: &Arc>) -> Result> { + // Get the underlying raw window handle. + let raw = window_src.window_handle()?.as_raw(); + let window_handle = match raw { + RawWindowHandle::Xcb(xcb) => xcb, + RawWindowHandle::Xlib(xlib) => { + let window = NonZeroU32::new(xlib.window as u32) + .ok_or(SoftBufferError::IncompleteWindowHandle)?; + let mut xcb_window_handle = XcbWindowHandle::new(window); + xcb_window_handle.visual_id = NonZeroU32::new(xlib.visual_id as u32); + xcb_window_handle + } + _ => { + return Err(InitError::Unsupported(window_src)); + } + }; + + tracing::trace!("new: window_handle={:X}", window_handle.window); + let window = window_handle.window.get(); + + // Run in parallel: start getting the window depth and (if necessary) visual. + let display2 = display.clone(); + let tokens = { + let geometry_token = display2 + .connection() + .get_geometry(window) + .swbuf_err("Failed to send geometry request")?; + let window_attrs_token = if window_handle.visual_id.is_none() { + Some( + display2 + .connection() + .get_window_attributes(window) + .swbuf_err("Failed to send window attributes request")?, + ) + } else { + None + }; + + (geometry_token, window_attrs_token) + }; + + // Create a new graphics context to draw to. + let gc = display + .connection() + .generate_id() + .swbuf_err("Failed to generate GC ID")?; + display + .connection() + .create_gc( + gc, + window, + &xproto::CreateGCAux::new().graphics_exposures(0), + ) + .swbuf_err("Failed to send GC creation request")? + .check() + .swbuf_err("Failed to create GC")?; + + // Finish getting the depth of the window. + let (geometry_reply, visual_id) = { + let (geometry_token, window_attrs_token) = tokens; + let geometry_reply = geometry_token + .reply() + .swbuf_err("Failed to get geometry reply")?; + let visual_id = match window_attrs_token { + None => window_handle.visual_id.unwrap().get(), + Some(window_attrs) => { + window_attrs + .reply() + .swbuf_err("Failed to get window attributes reply")? + .visual + } + }; + + (geometry_reply, visual_id) + }; + + if !display.supported_visuals.contains(&visual_id) { + return Err(SoftBufferError::PlatformError( + Some(format!( + "Visual 0x{visual_id:x} does not use softbuffer's pixel format and is unsupported" + )), + None, + ) + .into()); + } + + // See if SHM is available. + let buffer = if display.is_shm_available { + // SHM is available. + Buffer::Shm(ShmBuffer { + seg: None, + done_processing: None, + }) + } else { + // SHM is not available. + Buffer::Wire(util::PixelBuffer(Vec::new())) + }; + + Ok(Self { + display: display.clone(), + window, + gc, + depth: geometry_reply.depth, + visual_id, + buffer, + buffer_presented: false, + size: None, + window_handle: window_src, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window_handle + } + + fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + tracing::trace!( + "resize: window={:X}, size={}x{}", + self.window, + width, + height + ); + + // Width and height should fit in u16. + let width: NonZeroU16 = width + .try_into() + .or(Err(SoftBufferError::SizeOutOfRange { width, height }))?; + let height: NonZeroU16 = height.try_into().or(Err(SoftBufferError::SizeOutOfRange { + width: width.into(), + height, + }))?; + + if self.size != Some((width, height)) { + self.buffer_presented = false; + self.buffer + .resize(self.display.connection(), width.get(), height.get()) + .swbuf_err("Failed to resize X11 buffer")?; + + // We successfully resized the buffer. + self.size = Some((width, height)); + } + + Ok(()) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + tracing::trace!("buffer_mut: window={:X}", self.window); + + // Finish waiting on the previous `shm::PutImage` request, if any. + self.buffer.finish_wait(self.display.connection())?; + + // We can now safely call `buffer_mut` on the buffer. + Ok(BufferImpl(self)) + } + + fn fetch(&mut self) -> Result, SoftBufferError> { + tracing::trace!("fetch: window={:X}", self.window); + + let (width, height) = self + .size + .expect("Must set size of surface before calling `fetch()`"); + + // TODO: Is it worth it to do SHM here? Probably not. + let reply = self + .display + .connection() + .get_image( + xproto::ImageFormat::Z_PIXMAP, + self.window, + 0, + 0, + width.get(), + height.get(), + u32::MAX, + ) + .swbuf_err("Failed to send image fetching request")? + .reply() + .swbuf_err("Failed to fetch image from window")?; + + if reply.depth == self.depth && reply.visual == self.visual_id { + let mut out = vec![0u32; reply.data.len() / 4]; + bytemuck::cast_slice_mut::(&mut out).copy_from_slice(&reply.data); + Ok(out) + } else { + Err(SoftBufferError::PlatformError( + Some("Mismatch between reply and window data".into()), + None, + )) + } + } +} + +#[derive(Debug)] +pub struct BufferImpl<'a, D: ?Sized, W: ?Sized>(&'a mut X11Impl); + +impl BufferInterface + for BufferImpl<'_, D, W> +{ + fn width(&self) -> NonZeroU32 { + self.0.size.unwrap().0.into() + } + + fn height(&self) -> NonZeroU32 { + self.0.size.unwrap().1.into() + } + + #[inline] + fn pixels(&self) -> &[u32] { + // SAFETY: We called `finish_wait` on the buffer, so it is safe to call `buffer()`. + unsafe { self.0.buffer.buffer() } + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + // SAFETY: We called `finish_wait` on the buffer, so it is safe to call `buffer_mut`. + unsafe { self.0.buffer.buffer_mut() } + } + + fn age(&self) -> u8 { + if self.0.buffer_presented { + 1 + } else { + 0 + } + } + + /// Push the buffer to the window. + fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + let imp = self.0; + + let (surface_width, surface_height) = imp + .size + .expect("Must set size of surface before calling `present_with_damage()`"); + + tracing::trace!("present: window={:X}", imp.window); + + match imp.buffer { + Buffer::Wire(ref wire) => { + // This is a suboptimal strategy, raise a stink in the debug logs. + tracing::debug!("Falling back to non-SHM method for window drawing."); + + imp.display + .connection() + .put_image( + xproto::ImageFormat::Z_PIXMAP, + imp.window, + imp.gc, + surface_width.get(), + surface_height.get(), + 0, + 0, + 0, + imp.depth, + bytemuck::cast_slice(wire), + ) + .map(|c| c.ignore_error()) + .push_err() + .swbuf_err("Failed to draw image to window")?; + } + + Buffer::Shm(ref mut shm) => { + // If the X server is still processing the last image, wait for it to finish. + // SAFETY: We know that we called finish_wait() before this. + // Put the image into the window. + if let Some((_, segment_id)) = shm.seg { + damage + .iter() + .try_for_each(|rect| { + let (src_x, src_y, dst_x, dst_y, width, height) = (|| { + Some(( + u16::try_from(rect.x).ok()?, + u16::try_from(rect.y).ok()?, + i16::try_from(rect.x).ok()?, + i16::try_from(rect.y).ok()?, + u16::try_from(rect.width.get()).ok()?, + u16::try_from(rect.height.get()).ok()?, + )) + })( + ) + .ok_or(SoftBufferError::DamageOutOfRange { rect: *rect })?; + imp.display + .connection() + .shm_put_image( + imp.window, + imp.gc, + surface_width.get(), + surface_height.get(), + src_x, + src_y, + width, + height, + dst_x, + dst_y, + imp.depth, + xproto::ImageFormat::Z_PIXMAP.into(), + false, + segment_id, + 0, + ) + .push_err() + .map(|c| c.ignore_error()) + .swbuf_err("Failed to draw image to window") + }) + .and_then(|()| { + // Send a short request to act as a notification for when the X server is done processing the image. + shm.begin_wait(imp.display.connection()) + .swbuf_err("Failed to draw image to window") + })?; + } + } + } + + imp.buffer_presented = true; + + Ok(()) + } + + fn present(self) -> Result<(), SoftBufferError> { + let (width, height) = self + .0 + .size + .expect("Must set size of surface before calling `present()`"); + self.present_with_damage(&[Rect { + x: 0, + y: 0, + width: width.into(), + height: height.into(), + }]) + } +} + +impl Buffer { + /// Resize the buffer to the given size. + fn resize( + &mut self, + conn: &impl Connection, + width: u16, + height: u16, + ) -> Result<(), PushBufferError> { + match self { + Buffer::Shm(ref mut shm) => shm.alloc_segment(conn, total_len(width, height)), + Buffer::Wire(wire) => { + wire.resize(total_len(width, height) / 4, 0); + Ok(()) + } + } + } + + /// Finish waiting for an ongoing `shm::PutImage` request, if there is one. + fn finish_wait(&mut self, conn: &impl Connection) -> Result<(), SoftBufferError> { + if let Buffer::Shm(ref mut shm) = self { + shm.finish_wait(conn) + .swbuf_err("Failed to wait for X11 buffer")?; + } + + Ok(()) + } + + /// Get a reference to the buffer. + /// + /// # Safety + /// + /// `finish_wait()` must be called in between `shm::PutImage` requests and this function. + #[inline] + unsafe fn buffer(&self) -> &[u32] { + match self { + Buffer::Shm(ref shm) => unsafe { shm.as_ref() }, + Buffer::Wire(wire) => wire, + } + } + + /// Get a mutable reference to the buffer. + /// + /// # Safety + /// + /// `finish_wait()` must be called in between `shm::PutImage` requests and this function. + #[inline] + unsafe fn buffer_mut(&mut self) -> &mut [u32] { + match self { + Buffer::Shm(ref mut shm) => unsafe { shm.as_mut() }, + Buffer::Wire(wire) => wire, + } + } +} + +impl ShmBuffer { + /// Allocate a new `ShmSegment` of the given size. + fn alloc_segment( + &mut self, + conn: &impl Connection, + buffer_size: usize, + ) -> Result<(), PushBufferError> { + // Round the size up to the next power of two to prevent frequent reallocations. + let size = buffer_size.next_power_of_two(); + + // Get the size of the segment currently in use. + let needs_realloc = match self.seg { + Some((ref seg, _)) => seg.size() < size, + None => true, + }; + + // Reallocate if necessary. + if needs_realloc { + let new_seg = ShmSegment::new(size, buffer_size)?; + self.associate(conn, new_seg)?; + } else if let Some((ref mut seg, _)) = self.seg { + seg.set_buffer_size(buffer_size); + } + + Ok(()) + } + + /// Get the SHM buffer as a reference. + /// + /// # Safety + /// + /// `finish_wait()` must be called before this function is. + #[inline] + unsafe fn as_ref(&self) -> &[u32] { + match self.seg.as_ref() { + Some((seg, _)) => { + let buffer_size = seg.buffer_size(); + + // SAFETY: No other code should be able to access the segment. + bytemuck::cast_slice(unsafe { &seg.as_ref()[..buffer_size] }) + } + None => { + // Nothing has been allocated yet. + &[] + } + } + } + + /// Get the SHM buffer as a mutable reference. + /// + /// # Safety + /// + /// `finish_wait()` must be called before this function is. + #[inline] + unsafe fn as_mut(&mut self) -> &mut [u32] { + match self.seg.as_mut() { + Some((seg, _)) => { + let buffer_size = seg.buffer_size(); + + // SAFETY: No other code should be able to access the segment. + bytemuck::cast_slice_mut(unsafe { &mut seg.as_mut()[..buffer_size] }) + } + None => { + // Nothing has been allocated yet. + &mut [] + } + } + } + + /// Associate an SHM segment with the server. + fn associate( + &mut self, + conn: &impl Connection, + seg: ShmSegment, + ) -> Result<(), PushBufferError> { + // Register the guard. + let new_id = conn.generate_id()?; + conn.shm_attach_fd(new_id, seg.as_fd().try_clone_to_owned().unwrap(), true)? + .ignore_error(); + + // Take out the old one and detach it. + if let Some((old_seg, old_id)) = self.seg.replace((seg, new_id)) { + // Wait for the old segment to finish processing. + self.finish_wait(conn)?; + + conn.shm_detach(old_id)?.ignore_error(); + + // Drop the old segment. + drop(old_seg); + } + + Ok(()) + } + + /// Begin waiting for the SHM processing to finish. + fn begin_wait(&mut self, c: &impl Connection) -> Result<(), PushBufferError> { + let cookie = c.get_input_focus()?.sequence_number(); + let old_cookie = self.done_processing.replace(cookie); + debug_assert!(old_cookie.is_none()); + Ok(()) + } + + /// Wait for the SHM processing to finish. + fn finish_wait(&mut self, c: &impl Connection) -> Result<(), PushBufferError> { + if let Some(done_processing) = self.done_processing.take() { + // Cast to a cookie and wait on it. + let cookie = Cookie::<_, xproto::GetInputFocusReply>::new(c, done_processing); + cookie.reply()?; + } + + Ok(()) + } +} + +#[derive(Debug)] +struct ShmSegment { + id: File, + ptr: NonNull, + size: usize, + buffer_size: usize, +} + +// SAFETY: We respect Rust's mutability rules for the inner allocation. +unsafe impl Send for ShmSegment {} + +impl ShmSegment { + /// Create a new `ShmSegment` with the given size. + fn new(size: usize, buffer_size: usize) -> io::Result { + assert!(size >= buffer_size); + + // Create a shared memory segment. + let id = File::from(create_shm_id()?); + + // Set its length. + id.set_len(size as u64)?; + + // Map the shared memory to our file descriptor space. + let ptr = NonNull::new(unsafe { + mm::mmap( + null_mut(), + size, + mm::ProtFlags::READ | mm::ProtFlags::WRITE, + mm::MapFlags::SHARED, + &id, + 0, + )? + }) + .ok_or(io::Error::new( + io::ErrorKind::Other, + "unexpected null when mapping SHM segment", + ))? + .cast(); + + Ok(Self { + id, + ptr, + size, + buffer_size, + }) + } + + /// Get this shared memory segment as a reference. + /// + /// # Safety + /// + /// One must ensure that no other processes are writing to this memory. + unsafe fn as_ref(&self) -> &[i8] { + unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.size) } + } + + /// Get this shared memory segment as a mutable reference. + /// + /// # Safety + /// + /// One must ensure that no other processes are reading from or writing to this memory. + unsafe fn as_mut(&mut self) -> &mut [i8] { + unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.size) } + } + + /// Set the size of the buffer for this shared memory segment. + fn set_buffer_size(&mut self, buffer_size: usize) { + assert!(self.size >= buffer_size); + self.buffer_size = buffer_size + } + + /// Get the size of the buffer for this shared memory segment. + fn buffer_size(&self) -> usize { + self.buffer_size + } + + /// Get the size of this shared memory segment. + fn size(&self) -> usize { + self.size + } +} + +impl AsFd for ShmSegment { + fn as_fd(&self) -> BorrowedFd<'_> { + self.id.as_fd() + } +} + +impl Drop for ShmSegment { + fn drop(&mut self) { + unsafe { + // Unmap the shared memory segment. + mm::munmap(self.ptr.as_ptr().cast(), self.size).ok(); + } + } +} + +impl Drop for X11DisplayImpl { + fn drop(&mut self) { + // Make sure that the x11rb connection is dropped before its source is. + self.connection = None; + } +} + +impl Drop for X11Impl { + fn drop(&mut self) { + // If we used SHM, make sure it's detached from the server. + if let Buffer::Shm(mut shm) = mem::replace( + &mut self.buffer, + Buffer::Wire(util::PixelBuffer(Vec::new())), + ) { + // If we were in the middle of processing a buffer, wait for it to finish. + shm.finish_wait(self.display.connection()).ok(); + + if let Some((segment, seg_id)) = shm.seg.take() { + if let Ok(token) = self.display.connection().shm_detach(seg_id) { + token.ignore_error(); + } + + // Drop the segment. + drop(segment); + } + } + + // Close the graphics context that we created. + if let Ok(token) = self.display.connection().free_gc(self.gc) { + token.ignore_error(); + } + } +} + +/// Create a shared memory identifier. +fn create_shm_id() -> io::Result { + use posix_shm::{Mode, OFlags}; + + let mut rng = fastrand::Rng::new(); + let mut name = String::with_capacity(23); + + // Only try four times; the chances of a collision on this space is astronomically low, so if + // we miss four times in a row we're probably under attack. + for i in 0..4 { + name.clear(); + name.push_str("softbuffer-x11-"); + name.extend(std::iter::repeat_with(|| rng.alphanumeric()).take(7)); + + // Try to create the shared memory segment. + match posix_shm::open( + &name, + OFlags::RDWR | OFlags::CREATE | OFlags::EXCL, + Mode::RWXU, + ) { + Ok(id) => { + posix_shm::unlink(&name).ok(); + return Ok(id); + } + + Err(rustix::io::Errno::EXIST) => { + tracing::warn!("x11: SHM ID collision at {} on try number {}", name, i); + } + + Err(e) => return Err(e.into()), + }; + } + + Err(io::Error::new( + io::ErrorKind::Other, + "failed to generate a non-existent SHM name", + )) +} + +/// Test to see if SHM is available. +fn is_shm_available(c: &impl Connection) -> bool { + // Create a small SHM segment. + let seg = match ShmSegment::new(0x1000, 0x1000) { + Ok(seg) => seg, + Err(_) => return false, + }; + + // Attach and detach it. + let seg_id = match c.generate_id() { + Ok(id) => id, + Err(_) => return false, + }; + + let (attach, detach) = { + let attach = c.shm_attach_fd(seg_id, seg.as_fd().try_clone_to_owned().unwrap(), false); + let detach = c.shm_detach(seg_id); + + match (attach, detach) { + (Ok(attach), Ok(detach)) => (attach, detach), + _ => return false, + } + }; + + // Check the replies. + matches!((attach.check(), detach.check()), (Ok(()), Ok(()))) +} + +/// Collect all visuals that use softbuffer's pixel format +fn supported_visuals(c: &impl Connection) -> HashSet { + // Check that depth 24 uses 32 bits per pixels + // HACK(notgull): Also support depth 32 for transparent visuals. + // Otherwise winit users get weird errors. + if !c + .setup() + .pixmap_formats + .iter() + .any(|f| (f.depth == 24 || f.depth == 32) && f.bits_per_pixel == 32) + { + tracing::warn!("X11 server does not have a depth 24/32 format with 32 bits per pixel"); + return HashSet::new(); + } + + // How does the server represent red, green, blue components of a pixel? + #[cfg(target_endian = "little")] + let own_byte_order = ImageOrder::LSB_FIRST; + #[cfg(target_endian = "big")] + let own_byte_order = ImageOrder::MSB_FIRST; + let expected_masks = if c.setup().image_byte_order == own_byte_order { + (0xff0000, 0xff00, 0xff) + } else { + // This is the byte-swapped version of our wished-for format + (0xff00, 0xff0000, 0xff000000) + }; + + c.setup() + .roots + .iter() + .flat_map(|screen| { + screen + .allowed_depths + .iter() + .filter(|depth| depth.depth == 24 || depth.depth == 32) + .flat_map(|depth| { + depth + .visuals + .iter() + .filter(|visual| { + // Ignore grayscale or indexes / color palette visuals + visual.class == VisualClass::TRUE_COLOR + || visual.class == VisualClass::DIRECT_COLOR + }) + .filter(|visual| { + // Colors must be laid out as softbuffer expects + expected_masks == (visual.red_mask, visual.green_mask, visual.blue_mask) + }) + .map(|visual| visual.visual_id) + }) + }) + .collect() +} + +/// An error that can occur when pushing a buffer to the window. +#[derive(Debug)] +enum PushBufferError { + /// We encountered an X11 error. + X11(ReplyError), + + /// We exhausted the XID space. + XidExhausted, + + /// A system error occurred while creating the shared memory segment. + System(io::Error), +} + +impl fmt::Display for PushBufferError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::X11(e) => write!(f, "X11 error: {}", e), + Self::XidExhausted => write!(f, "XID space exhausted"), + Self::System(e) => write!(f, "System error: {}", e), + } + } +} + +impl std::error::Error for PushBufferError {} + +impl From for PushBufferError { + fn from(e: ConnectionError) -> Self { + Self::X11(ReplyError::ConnectionError(e)) + } +} + +impl From for PushBufferError { + fn from(e: ReplyError) -> Self { + Self::X11(e) + } +} + +impl From for PushBufferError { + fn from(e: ReplyOrIdError) -> Self { + match e { + ReplyOrIdError::ConnectionError(e) => Self::X11(ReplyError::ConnectionError(e)), + ReplyOrIdError::X11Error(e) => Self::X11(ReplyError::X11Error(e)), + ReplyOrIdError::IdsExhausted => Self::XidExhausted, + } + } +} + +impl From for PushBufferError { + fn from(e: io::Error) -> Self { + Self::System(e) + } +} + +/// Convenient wrapper to cast errors into PushBufferError. +trait PushResultExt { + fn push_err(self) -> Result; +} + +impl> PushResultExt for Result { + fn push_err(self) -> Result { + self.map_err(Into::into) + } +} + +/// Get the length that a slice needs to be to hold a buffer of the given dimensions. +#[inline(always)] +fn total_len(width: u16, height: u16) -> usize { + let width: usize = width.into(); + let height: usize = height.into(); + + width + .checked_mul(height) + .and_then(|len| len.checked_mul(4)) + .unwrap_or_else(|| panic!("Dimensions are too large: ({} x {})", width, height)) +} diff --git a/vendor/softbuffer/src/error.rs b/vendor/softbuffer/src/error.rs new file mode 100644 index 0000000..b79be6b --- /dev/null +++ b/vendor/softbuffer/src/error.rs @@ -0,0 +1,222 @@ +use raw_window_handle::{HandleError, RawDisplayHandle, RawWindowHandle}; +use std::error::Error; +use std::fmt; +use std::num::NonZeroU32; + +#[derive(Debug)] +#[non_exhaustive] +/// A sum type of all of the errors that can occur during the operation of this crate. +pub enum SoftBufferError { + /// A [`raw-window-handle`] error occurred. + /// + /// [`raw-window-handle`]: raw_window_handle + RawWindowHandle(HandleError), + + /// The [`RawDisplayHandle`] passed into [`Context::new`] is not supported by this crate. + /// + /// [`RawDisplayHandle`]: raw_window_handle::RawDisplayHandle + /// [`Context::new`]: crate::Context::new + UnsupportedDisplayPlatform { + /// The platform name of the display that was passed into [`Context::new`]. + /// + /// This is a human-readable string that describes the platform of the display that was + /// passed into [`Context::new`]. The value is not guaranteed to be stable and this + /// exists for debugging purposes only. + /// + /// [`Context::new`]: crate::Context::new + human_readable_display_platform_name: &'static str, + + /// The [`RawDisplayHandle`] that was passed into [`Context::new`]. + /// + /// [`RawDisplayHandle`]: raw_window_handle::RawDisplayHandle + /// [`Context::new`]: crate::Context::new + display_handle: RawDisplayHandle, + }, + + /// The [`RawWindowHandle`] passed into [`Surface::new`] is not supported by this crate. + /// + /// [`RawWindowHandle`]: raw_window_handle::RawWindowHandle + /// [`Surface::new`]: crate::Surface::new + UnsupportedWindowPlatform { + /// The platform name of the window that was passed into [`Surface::new`]. + /// + /// This is a human-readable string that describes the platform of the window that was + /// passed into [`Surface::new`]. The value is not guaranteed to be stable and this + /// exists for debugging purposes only. + /// + /// [`Surface::new`]: crate::Surface::new + human_readable_window_platform_name: &'static str, + + /// The platform name of the display used by the [`Context`]. + /// + /// It is possible for a window to be created on a different type of display than the + /// display that was passed into [`Context::new`]. This is a human-readable string that + /// describes the platform of the display that was passed into [`Context::new`]. The value + /// is not guaranteed to be stable and this exists for debugging purposes only. + /// + /// [`Context`]: crate::Context + /// [`Context::new`]: crate::Context::new + human_readable_display_platform_name: &'static str, + + /// The [`RawWindowHandle`] that was passed into [`Surface::new`]. + /// + /// [`RawWindowHandle`]: raw_window_handle::RawWindowHandle + /// [`Surface::new`]: crate::Surface::new + window_handle: RawWindowHandle, + }, + + /// The [`RawWindowHandle`] passed into [`Surface::new`] is missing necessary fields. + /// + /// [`RawWindowHandle`]: raw_window_handle::RawWindowHandle + /// [`Surface::new`]: crate::Surface::new + IncompleteWindowHandle, + + /// The [`RawDisplayHandle`] passed into [`Context::new`] is missing necessary fields. + /// + /// [`RawDisplayHandle`]: raw_window_handle::RawDisplayHandle + /// [`Context::new`]: crate::Context::new + IncompleteDisplayHandle, + + /// The provided size is outside of the range supported by the backend. + SizeOutOfRange { + /// The width that was out of range. + width: NonZeroU32, + + /// The height that was out of range. + height: NonZeroU32, + }, + + /// The provided damage rect is outside of the range supported by the backend. + DamageOutOfRange { + /// The damage rect that was out of range. + rect: crate::Rect, + }, + + /// A platform-specific backend error occurred. + /// + /// The first field provides a human-readable description of the error. The second field + /// provides the actual error that occurred. Note that the second field is, under the hood, + /// a private wrapper around the actual error, preventing the user from downcasting to the + /// actual error type. + PlatformError(Option, Option>), + + /// This function is unimplemented on this platform. + Unimplemented, +} + +impl fmt::Display for SoftBufferError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::RawWindowHandle(err) => fmt::Display::fmt(err, f), + Self::UnsupportedDisplayPlatform { + human_readable_display_platform_name, + display_handle, + } => write!( + f, + "The provided display returned an unsupported platform: {}.\nDisplay handle: {:?}", + human_readable_display_platform_name, display_handle + ), + Self::UnsupportedWindowPlatform { + human_readable_window_platform_name, + human_readable_display_platform_name, + window_handle, + } => write!( + f, + "The provided window returned an unsupported platform: {}, {}.\nWindow handle: {:?}", + human_readable_window_platform_name, human_readable_display_platform_name, window_handle + ), + Self::IncompleteWindowHandle => write!(f, "The provided window handle is null."), + Self::IncompleteDisplayHandle => write!(f, "The provided display handle is null."), + Self::SizeOutOfRange { width, height } => write!( + f, + "Surface size {width}x{height} out of range for backend.", + ), + Self::PlatformError(msg, None) => write!(f, "Platform error: {msg:?}"), + Self::PlatformError(msg, Some(err)) => write!(f, "Platform error: {msg:?}: {err}"), + Self::DamageOutOfRange { rect } => write!( + f, + "Damage rect {}x{} at ({}, {}) out of range for backend.", + rect.width, rect.height, rect.x, rect.y + ), + Self::Unimplemented => write!(f, "This function is unimplemented on this platform."), + } + } +} + +impl std::error::Error for SoftBufferError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::RawWindowHandle(err) => Some(err), + Self::PlatformError(_, err) => err.as_deref(), + _ => None, + } + } +} + +impl From for SoftBufferError { + fn from(err: HandleError) -> Self { + Self::RawWindowHandle(err) + } +} + +/// Simple unit error type used to bubble up rejected platforms. +pub(crate) enum InitError { + /// Failed to initialize. + Failure(SoftBufferError), + + /// Cannot initialize this handle on this platform. + Unsupported(D), +} + +impl From for InitError { + fn from(err: SoftBufferError) -> Self { + Self::Failure(err) + } +} + +impl From for InitError { + fn from(err: HandleError) -> Self { + Self::Failure(err.into()) + } +} + +/// Convenient wrapper to cast errors into SoftBufferError. +#[allow(dead_code)] +pub(crate) trait SwResultExt { + fn swbuf_err(self, msg: impl Into) -> Result; +} + +impl SwResultExt for Result { + fn swbuf_err(self, msg: impl Into) -> Result { + self.map_err(|e| { + SoftBufferError::PlatformError(Some(msg.into()), Some(Box::new(LibraryError(e)))) + }) + } +} + +impl SwResultExt for Option { + fn swbuf_err(self, msg: impl Into) -> Result { + self.ok_or_else(|| SoftBufferError::PlatformError(Some(msg.into()), None)) + } +} + +/// A wrapper around a library error. +/// +/// This prevents `x11-dl` and `x11rb` from becoming public dependencies, since users cannot downcast +/// to this type. +#[allow(dead_code)] +struct LibraryError(E); + +impl fmt::Debug for LibraryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for LibraryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl std::error::Error for LibraryError {} diff --git a/vendor/softbuffer/src/lib.rs b/vendor/softbuffer/src/lib.rs new file mode 100644 index 0000000..7923c3e --- /dev/null +++ b/vendor/softbuffer/src/lib.rs @@ -0,0 +1,377 @@ +#![doc = include_str!("../README.md")] +#![allow(clippy::needless_doctest_main)] +#![deny(unsafe_op_in_unsafe_fn)] +#![warn(missing_docs)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +extern crate core; + +mod backend_dispatch; +use backend_dispatch::*; +mod backend_interface; +use backend_interface::*; +mod backends; +mod error; +mod util; + +use std::cell::Cell; +use std::marker::PhantomData; +use std::num::NonZeroU32; +use std::ops; +use std::sync::Arc; + +use error::InitError; +pub use error::SoftBufferError; + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; + +#[cfg(target_family = "wasm")] +pub use backends::web::SurfaceExtWeb; + +/// An instance of this struct contains the platform-specific data that must be managed in order to +/// write to a window on that platform. +#[derive(Clone, Debug)] +pub struct Context { + /// The inner static dispatch object. + context_impl: ContextDispatch, + + /// This is Send+Sync IFF D is Send+Sync. + _marker: PhantomData>, +} + +impl Context { + /// Creates a new instance of this struct, using the provided display. + pub fn new(display: D) -> Result { + match ContextDispatch::new(display) { + Ok(context_impl) => Ok(Self { + context_impl, + _marker: PhantomData, + }), + Err(InitError::Unsupported(display)) => { + let raw = display.display_handle()?.as_raw(); + Err(SoftBufferError::UnsupportedDisplayPlatform { + human_readable_display_platform_name: display_handle_type_name(&raw), + display_handle: raw, + }) + } + Err(InitError::Failure(f)) => Err(f), + } + } +} + +/// A rectangular region of the buffer coordinate space. +#[derive(Clone, Copy, Debug)] +pub struct Rect { + /// x coordinate of top left corner + pub x: u32, + /// y coordinate of top left corner + pub y: u32, + /// width + pub width: NonZeroU32, + /// height + pub height: NonZeroU32, +} + +/// A surface for drawing to a window with software buffers. +#[derive(Debug)] +pub struct Surface { + /// This is boxed so that `Surface` is the same size on every platform. + surface_impl: Box>, + _marker: PhantomData>, +} + +impl Surface { + /// Creates a new surface for the context for the provided window. + pub fn new(context: &Context, window: W) -> Result { + match SurfaceDispatch::new(window, &context.context_impl) { + Ok(surface_dispatch) => Ok(Self { + surface_impl: Box::new(surface_dispatch), + _marker: PhantomData, + }), + Err(InitError::Unsupported(window)) => { + let raw = window.window_handle()?.as_raw(); + Err(SoftBufferError::UnsupportedWindowPlatform { + human_readable_window_platform_name: window_handle_type_name(&raw), + human_readable_display_platform_name: context.context_impl.variant_name(), + window_handle: raw, + }) + } + Err(InitError::Failure(f)) => Err(f), + } + } + + /// Get a reference to the underlying window handle. + pub fn window(&self) -> &W { + self.surface_impl.window() + } + + /// Set the size of the buffer that will be returned by [`Surface::buffer_mut`]. + /// + /// If the size of the buffer does not match the size of the window, the buffer is drawn + /// in the upper-left corner of the window. It is recommended in most production use cases + /// to have the buffer fill the entire window. Use your windowing library to find the size + /// of the window. + pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + self.surface_impl.resize(width, height) + } + + /// Copies the window contents into a buffer. + /// + /// ## Platform Dependent Behavior + /// + /// - On X11, the window must be visible. + /// - On AppKit, UIKit, Redox and Wayland, this function is unimplemented. + /// - On Web, this will fail if the content was supplied by + /// a different origin depending on the sites CORS rules. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + self.surface_impl.fetch() + } + + /// Return a [`Buffer`] that the next frame should be rendered into. The size must + /// be set with [`Surface::resize`] first. The initial contents of the buffer may be zeroed, or + /// may contain a previous frame. Call [`Buffer::age`] to determine this. + /// + /// ## Platform Dependent Behavior + /// + /// - On DRM/KMS, there is no reliable and sound way to wait for the page flip to happen from within + /// `softbuffer`. Therefore it is the responsibility of the user to wait for the page flip before + /// sending another frame. + pub fn buffer_mut(&mut self) -> Result, SoftBufferError> { + Ok(Buffer { + buffer_impl: self.surface_impl.buffer_mut()?, + _marker: PhantomData, + }) + } +} + +impl AsRef for Surface { + #[inline] + fn as_ref(&self) -> &W { + self.window() + } +} + +impl HasWindowHandle for Surface { + #[inline] + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + self.window().window_handle() + } +} + +/// A buffer that can be written to by the CPU and presented to the window. +/// +/// This derefs to a `[u32]`, which depending on the backend may be a mapping into shared memory +/// accessible to the display server, so presentation doesn't require any (client-side) copying. +/// +/// This trusts the display server not to mutate the buffer, which could otherwise be unsound. +/// +/// # Data representation +/// +/// The format of the buffer is as follows. There is one `u32` in the buffer for each pixel in +/// the area to draw. The first entry is the upper-left most pixel. The second is one to the right +/// etc. (Row-major top to bottom left to right one `u32` per pixel). Within each `u32` the highest +/// order 8 bits are to be set to 0. The next highest order 8 bits are the red channel, then the +/// green channel, and then the blue channel in the lowest-order 8 bits. See the examples for +/// one way to build this format using bitwise operations. +/// +/// -------- +/// +/// Pixel format (`u32`): +/// +/// 00000000RRRRRRRRGGGGGGGGBBBBBBBB +/// +/// 0: Bit is 0 +/// R: Red channel +/// G: Green channel +/// B: Blue channel +/// +/// # Platform dependent behavior +/// No-copy presentation is currently supported on: +/// - Wayland +/// - X, when XShm is available +/// - Win32 +/// - Orbital, when buffer size matches window size +/// +/// Currently [`Buffer::present`] must block copying image data on: +/// - Web +/// - AppKit +/// - UIKit +/// +/// Buffer copies an channel swizzling happen on: +/// - Android +#[derive(Debug)] +pub struct Buffer<'a, D, W> { + buffer_impl: BufferDispatch<'a, D, W>, + _marker: PhantomData<(Arc, Cell<()>)>, +} + +impl Buffer<'_, D, W> { + /// The amount of pixels wide the buffer is. + pub fn width(&self) -> NonZeroU32 { + let width = self.buffer_impl.width(); + debug_assert_eq!( + width.get() as usize * self.buffer_impl.height().get() as usize, + self.len(), + "buffer must be sized correctly" + ); + width + } + + /// The amount of pixels tall the buffer is. + pub fn height(&self) -> NonZeroU32 { + let height = self.buffer_impl.height(); + debug_assert_eq!( + height.get() as usize * self.buffer_impl.width().get() as usize, + self.len(), + "buffer must be sized correctly" + ); + height + } + + /// `age` is the number of frames ago this buffer was last presented. So if the value is + /// `1`, it is the same as the last frame, and if it is `2`, it is the same as the frame + /// before that (for backends using double buffering). If the value is `0`, it is a new + /// buffer that has unspecified contents. + /// + /// This can be used to update only a portion of the buffer. + pub fn age(&self) -> u8 { + self.buffer_impl.age() + } + + /// Presents buffer to the window. + /// + /// # Platform dependent behavior + /// + /// ## Wayland + /// + /// On Wayland, calling this function may send requests to the underlying `wl_surface`. The + /// graphics context may issue `wl_surface.attach`, `wl_surface.damage`, `wl_surface.damage_buffer` + /// and `wl_surface.commit` requests when presenting the buffer. + /// + /// If the caller wishes to synchronize other surface/window changes, such requests must be sent to the + /// Wayland compositor before calling this function. + pub fn present(self) -> Result<(), SoftBufferError> { + self.buffer_impl.present() + } + + /// Presents buffer to the window, with damage regions. + /// + /// # Platform dependent behavior + /// + /// Supported on: + /// - Wayland + /// - X, when XShm is available + /// - Win32 + /// - Web + /// + /// Otherwise this is equivalent to [`Self::present`]. + pub fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + self.buffer_impl.present_with_damage(damage) + } +} + +impl ops::Deref for Buffer<'_, D, W> { + type Target = [u32]; + + #[inline] + fn deref(&self) -> &[u32] { + self.buffer_impl.pixels() + } +} + +impl ops::DerefMut for Buffer<'_, D, W> { + #[inline] + fn deref_mut(&mut self) -> &mut [u32] { + self.buffer_impl.pixels_mut() + } +} + +/// There is no display handle. +#[derive(Debug)] +#[allow(dead_code)] +pub struct NoDisplayHandle(core::convert::Infallible); + +impl HasDisplayHandle for NoDisplayHandle { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + match self.0 {} + } +} + +/// There is no window handle. +#[derive(Debug)] +pub struct NoWindowHandle(()); + +impl HasWindowHandle for NoWindowHandle { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Err(raw_window_handle::HandleError::NotSupported) + } +} + +fn window_handle_type_name(handle: &RawWindowHandle) -> &'static str { + match handle { + RawWindowHandle::Xlib(_) => "Xlib", + RawWindowHandle::Win32(_) => "Win32", + RawWindowHandle::WinRt(_) => "WinRt", + RawWindowHandle::Web(_) => "Web", + RawWindowHandle::Wayland(_) => "Wayland", + RawWindowHandle::AndroidNdk(_) => "AndroidNdk", + RawWindowHandle::AppKit(_) => "AppKit", + RawWindowHandle::Orbital(_) => "Orbital", + RawWindowHandle::UiKit(_) => "UiKit", + RawWindowHandle::Xcb(_) => "XCB", + RawWindowHandle::Drm(_) => "DRM", + RawWindowHandle::Gbm(_) => "GBM", + RawWindowHandle::Haiku(_) => "Haiku", + _ => "Unknown Name", //don't completely fail to compile if there is a new raw window handle type that's added at some point + } +} + +fn display_handle_type_name(handle: &RawDisplayHandle) -> &'static str { + match handle { + RawDisplayHandle::Xlib(_) => "Xlib", + RawDisplayHandle::Web(_) => "Web", + RawDisplayHandle::Wayland(_) => "Wayland", + RawDisplayHandle::AppKit(_) => "AppKit", + RawDisplayHandle::Orbital(_) => "Orbital", + RawDisplayHandle::UiKit(_) => "UiKit", + RawDisplayHandle::Xcb(_) => "XCB", + RawDisplayHandle::Drm(_) => "DRM", + RawDisplayHandle::Gbm(_) => "GBM", + RawDisplayHandle::Haiku(_) => "Haiku", + RawDisplayHandle::Windows(_) => "Windows", + RawDisplayHandle::Android(_) => "Android", + _ => "Unknown Name", //don't completely fail to compile if there is a new raw window handle type that's added at some point + } +} + +#[cfg(not(target_family = "wasm"))] +fn __assert_send() { + fn is_send() {} + fn is_sync() {} + + is_send::>(); + is_sync::>(); + is_send::>(); + is_send::>(); + + /// ```compile_fail + /// use softbuffer::Surface; + /// + /// fn __is_sync() {} + /// __is_sync::>(); + /// ``` + fn __surface_not_sync() {} + /// ```compile_fail + /// use softbuffer::Buffer; + /// + /// fn __is_sync() {} + /// __is_sync::>(); + /// ``` + fn __buffer_not_sync() {} +} diff --git a/vendor/softbuffer/src/util.rs b/vendor/softbuffer/src/util.rs new file mode 100644 index 0000000..572de5e --- /dev/null +++ b/vendor/softbuffer/src/util.rs @@ -0,0 +1,137 @@ +// Not needed on all platforms +#![allow(dead_code)] + +use std::cmp; +use std::fmt; +use std::num::NonZeroU32; +use std::ops; + +use crate::Rect; +use crate::SoftBufferError; + +/// Takes a mutable reference to a container and a function deriving a +/// reference into it, and stores both, making it possible to get back the +/// reference to the container once the other reference is no longer needed. +/// +/// This should be consistent with stacked borrow rules, and miri seems to +/// accept it at least in simple cases. +pub struct BorrowStack<'a, T: 'a + ?Sized, U: 'a + ?Sized> { + container: *mut T, + member: *mut U, + _phantom: std::marker::PhantomData<&'a mut T>, +} + +unsafe impl<'a, T: 'a + Send + ?Sized, U: 'a + Send + ?Sized> Send for BorrowStack<'a, T, U> {} + +impl<'a, T: 'a + ?Sized, U: 'a + ?Sized> BorrowStack<'a, T, U> { + pub fn new(container: &'a mut T, f: F) -> Result + where + F: for<'b> FnOnce(&'b mut T) -> Result<&'b mut U, SoftBufferError>, + { + let container = container as *mut T; + let member = f(unsafe { &mut *container })? as *mut U; + Ok(Self { + container, + member, + _phantom: std::marker::PhantomData, + }) + } + + pub fn member(&self) -> &U { + unsafe { &*self.member } + } + + pub fn member_mut(&mut self) -> &mut U { + unsafe { &mut *self.member } + } + + pub fn into_container(self) -> &'a mut T { + // SAFETY: Since we consume self and no longer reference member, this + // mutable reference is unique. + unsafe { &mut *self.container } + } +} + +impl<'a, T: 'a + ?Sized, U: 'a + ?Sized + fmt::Debug> fmt::Debug for BorrowStack<'a, T, U> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BorrowStack") + .field("member", &self.member()) + .finish_non_exhaustive() + } +} + +/// Calculates the smallest `Rect` necessary to represent all damaged `Rect`s. +pub(crate) fn union_damage(damage: &[Rect]) -> Option { + struct Region { + left: u32, + top: u32, + bottom: u32, + right: u32, + } + + let region = damage + .iter() + .map(|rect| Region { + left: rect.x, + top: rect.y, + right: rect.x + rect.width.get(), + bottom: rect.y + rect.height.get(), + }) + .reduce(|mut prev, next| { + prev.left = cmp::min(prev.left, next.left); + prev.top = cmp::min(prev.top, next.top); + prev.right = cmp::max(prev.right, next.right); + prev.bottom = cmp::max(prev.bottom, next.bottom); + prev + })?; + + Some(Rect { + x: region.left, + y: region.top, + width: NonZeroU32::new(region.right - region.left) + .expect("`right` must always be bigger then `left`"), + height: NonZeroU32::new(region.bottom - region.top) + .expect("`bottom` must always be bigger then `top`"), + }) +} + +/// A wrapper around a `Vec` of pixels that doesn't print the whole buffer on `Debug`. +#[derive(PartialEq, Eq, Hash, Clone)] +pub(crate) struct PixelBuffer(pub Vec); + +impl fmt::Debug for PixelBuffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PixelBuffer").finish_non_exhaustive() + } +} + +impl ops::Deref for PixelBuffer { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for PixelBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_borrowstack_slice_int() { + fn f(mut stack: BorrowStack<[u32], u32>) { + assert_eq!(*stack.member(), 3); + *stack.member_mut() = 42; + assert_eq!(stack.into_container(), &[1, 2, 42, 4, 5]); + } + + let mut v = vec![1, 2, 3, 4, 5]; + f(BorrowStack::new(v.as_mut(), |v: &mut [u32]| Ok(&mut v[2])).unwrap()); + assert_eq!(&v, &[1, 2, 42, 4, 5]); + } +}