From e4e94fd1a656ccecd4e3b953fecd0fe1ca78e68f Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sun, 3 Nov 2024 10:10:43 -0800 Subject: [PATCH] feat(borders): use direct2d for anti-aliasing This commit overhauls the "Komorebi" borders implementation to use Direct2D, which enables anti-aliasing for rounded borders. A lot of the heavy lifting was done by @lukeyou05 in the tacky-borders project, which this commit largely adapts to komorebi. @lukeyou05 provided an incredible amount of guidance and feedback on the implementation of this feature on the komorebi Discord. This commit is a squashed interactive rebase of the following commits: 238271a71ebc7960e728ad7e9767f85d10f0d276 feat(borders): initial impl of direct2d border drawing 5525a382b988395a38fa0208204110a8780f0ef8 feat(borders): avoid multiple render target creation calls 431970d7b6b7fcbc177e6d3c0b2c2d9a1fa2c53d feat(borders): reduce redraws to improve perf 47cb19e54a5c328eff8bc256e1846d26de808ee7 feat(borders): remove black pixels around direct2d corners 3857d1a46c7cfbae12864de02a817b0cf7ffc689 feat(borders): clean up render targets on destroy --- Cargo.toml | 4 + komorebi/src/border_manager/border.rs | 280 ++++++++++++++++++-------- komorebi/src/border_manager/mod.rs | 31 ++- 3 files changed, 222 insertions(+), 93 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 224b6192f..cd397d19a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,11 +44,15 @@ which = "7" version = "0.58" features = [ "implement", + "Foundation_Numerics", "Win32_System_Com", "Win32_UI_Shell_Common", # for IObjectArray "Win32_Foundation", "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", + "Win32_Graphics_Direct2D", + "Win32_Graphics_Direct2D_Common", + "Win32_Graphics_Dxgi_Common", "Win32_System_LibraryLoader", "Win32_System_RemoteDesktop", "Win32_System_Threading", diff --git a/komorebi/src/border_manager/border.rs b/komorebi/src/border_manager/border.rs index 0aa250006..305c188aa 100644 --- a/komorebi/src/border_manager/border.rs +++ b/komorebi/src/border_manager/border.rs @@ -3,47 +3,78 @@ use crate::border_manager::WindowKind; use crate::border_manager::BORDER_OFFSET; use crate::border_manager::BORDER_WIDTH; use crate::border_manager::FOCUS_STATE; +use crate::border_manager::RENDER_TARGETS; use crate::border_manager::STYLE; use crate::border_manager::Z_ORDER; +use crate::core::BorderStyle; +use crate::core::Rect; use crate::windows_api; use crate::WindowsApi; use crate::WINDOWS_11; - -use crate::core::BorderStyle; -use crate::core::Rect; - +use std::ops::Deref; use std::sync::atomic::Ordering; use std::sync::mpsc; -use std::time::Duration; -use windows::core::PCWSTR; +use std::sync::LazyLock; +use windows::Foundation::Numerics::Matrix3x2; use windows::Win32::Foundation::BOOL; -use windows::Win32::Foundation::COLORREF; +use windows::Win32::Foundation::FALSE; use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::LPARAM; use windows::Win32::Foundation::LRESULT; +use windows::Win32::Foundation::TRUE; use windows::Win32::Foundation::WPARAM; +use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED; +use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F; +use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT; +use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F; +use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U; +use windows::Win32::Graphics::Direct2D::D2D1CreateFactory; +use windows::Win32::Graphics::Direct2D::ID2D1Factory; +use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE; +use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES; +use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED; +use windows::Win32::Graphics::Direct2D::D2D1_HWND_RENDER_TARGET_PROPERTIES; +use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY; +use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES; +use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT; +use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT; +use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow; +use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION; +use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE; +use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND; +use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN; use windows::Win32::Graphics::Gdi::BeginPaint; -use windows::Win32::Graphics::Gdi::CreatePen; -use windows::Win32::Graphics::Gdi::DeleteObject; +use windows::Win32::Graphics::Gdi::CreateRectRgn; use windows::Win32::Graphics::Gdi::EndPaint; use windows::Win32::Graphics::Gdi::InvalidateRect; -use windows::Win32::Graphics::Gdi::Rectangle; -use windows::Win32::Graphics::Gdi::RoundRect; -use windows::Win32::Graphics::Gdi::SelectObject; use windows::Win32::Graphics::Gdi::PAINTSTRUCT; -use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME; -use windows::Win32::Graphics::Gdi::PS_SOLID; use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; use windows::Win32::UI::WindowsAndMessaging::GetMessageW; +use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics; use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage; use windows::Win32::UI::WindowsAndMessaging::TranslateMessage; -use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW; -use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW; use windows::Win32::UI::WindowsAndMessaging::MSG; +use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN; use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY; use windows::Win32::UI::WindowsAndMessaging::WM_PAINT; +use windows::Win32::UI::WindowsAndMessaging::WM_SIZE; use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; +use windows_core::PCWSTR; + +#[allow(clippy::expect_used)] +static RENDER_FACTORY: LazyLock = unsafe { + LazyLock::new(|| { + D2D1CreateFactory::(D2D1_FACTORY_TYPE_MULTI_THREADED, None) + .expect("creating RENDER_FACTORY failed") + }) +}; + +static BRUSH_PROPERTIES: LazyLock = + LazyLock::new(|| D2D1_BRUSH_PROPERTIES { + opacity: 1.0, + transform: Matrix3x2::identity(), + }); pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL { let hwnds = unsafe { &mut *(lparam.0 as *mut Vec) }; @@ -83,7 +114,6 @@ impl Border { let window_class = WNDCLASSW { hInstance: h_module.into(), lpszClassName: class_name, - style: CS_HREDRAW | CS_VREDRAW, lpfnWndProc: Some(Self::callback), hbrBackground: WindowsApi::create_solid_brush(0), ..Default::default() @@ -110,32 +140,80 @@ impl Border { let _ = TranslateMessage(&msg); DispatchMessageW(&msg); } - - std::thread::sleep(Duration::from_millis(10)) } Ok(()) }); - Ok(Self { - hwnd: hwnd_receiver.recv()?, - }) + let hwnd = hwnd_receiver.recv()?; + let border = Self { hwnd }; + + // I have literally no idea, apparently this is to get rid of the black pixels + // around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys + unsafe { + let pos: i32 = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8; + let hrgn = CreateRectRgn(pos, 0, pos + 1, 1); + let mut bh: DWM_BLURBEHIND = Default::default(); + if !hrgn.is_invalid() { + bh = DWM_BLURBEHIND { + dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION, + fEnable: TRUE, + hRgnBlur: hrgn, + fTransitionOnMaximized: FALSE, + }; + } + + let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh); + } + + let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES { + hwnd: HWND(windows_api::as_ptr!(hwnd)), + pixelSize: Default::default(), + presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY, + }; + + let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES { + r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT, + pixelFormat: D2D1_PIXEL_FORMAT { + format: DXGI_FORMAT_UNKNOWN, + alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED, + }, + dpiX: 96.0, + dpiY: 96.0, + ..Default::default() + }; + + match unsafe { + RENDER_FACTORY + .CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties) + } { + Ok(render_target) => unsafe { + render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + let mut render_targets = RENDER_TARGETS.lock(); + render_targets.insert(hwnd, render_target); + Ok(border) + }, + Err(error) => Err(error.into()), + } } pub fn destroy(&self) -> color_eyre::Result<()> { + let mut render_targets = RENDER_TARGETS.lock(); + render_targets.remove(&self.hwnd); WindowsApi::close_window(self.hwnd) } - pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> { + pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> { // Make adjustments to the border let mut rect = *rect; - rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst)); - rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst)); + rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed)); + rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed)); // Update the position of the border if required + // This effectively handles WM_MOVE + // Also if I remove this no borders render at all lol if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) { WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?; - should_invalidate = true; } // Invalidate the rect to trigger the callback to update colours etc. @@ -158,72 +236,104 @@ impl Border { ) -> LRESULT { unsafe { match message { - WM_PAINT => { - let mut ps = PAINTSTRUCT::default(); - let hdc = BeginPaint(window, &mut ps); - - // With the rect that we set in Self::update - match WindowsApi::window_rect(window.0 as isize) { - Ok(rect) => { - // Grab the focus kind for this border - let window_kind = { - FOCUS_STATE - .lock() - .get(&(window.0 as isize)) - .copied() - .unwrap_or(WindowKind::Unfocused) + WM_SIZE | WM_PAINT => { + if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) { + let render_targets = RENDER_TARGETS.lock(); + if let Some(render_target) = render_targets.get(&(window.0 as isize)) { + let pixel_size = D2D_SIZE_U { + width: rect.right as u32, + height: rect.bottom as u32, }; - // Set up the brush to draw the border - let hpen = CreatePen( - PS_SOLID | PS_INSIDEFRAME, - BORDER_WIDTH.load(Ordering::SeqCst), - COLORREF(window_kind_colour(window_kind)), - ); - - let hbrush = WindowsApi::create_solid_brush(0); - - // Draw the border - SelectObject(hdc, hpen); - SelectObject(hdc, hbrush); - // TODO(raggi): this is approximately the correct curvature for - // the top left of a Windows 11 window (DWMWCP_DEFAULT), but - // often the bottom right has a different shape. Furthermore if - // the window was made with DWMWCP_ROUNDSMALL then this is the - // wrong size. In the future we should read the DWM properties - // of windows and attempt to match appropriately. - match STYLE.load() { - BorderStyle::System => { - if *WINDOWS_11 { - // TODO: error handling - let _ = - RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20); - } else { - // TODO: error handling - let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom); + let border_width = BORDER_WIDTH.load(Ordering::SeqCst); + let border_offset = BORDER_OFFSET.load(Ordering::SeqCst); + + let rect = D2D_RECT_F { + left: (border_width / 2 - border_offset) as f32, + top: (border_width / 2 - border_offset) as f32, + right: (rect.right - border_width / 2 + border_offset) as f32, + bottom: (rect.bottom - border_width / 2 + border_offset) as f32, + }; + + let _ = render_target.Resize(&pixel_size); + + // Get window kind and color + let window_kind = FOCUS_STATE + .lock() + .get(&(window.0 as isize)) + .copied() + .unwrap_or(WindowKind::Unfocused); + + let color = window_kind_colour(window_kind); + let color = D2D1_COLOR_F { + r: ((color & 0xFF) as f32) / 255.0, + g: (((color >> 8) & 0xFF) as f32) / 255.0, + b: (((color >> 16) & 0xFF) as f32) / 255.0, + a: 1.0, + }; + + if let Ok(brush) = render_target + .CreateSolidColorBrush(&color, Some(BRUSH_PROPERTIES.deref())) + { + render_target.BeginDraw(); + render_target.Clear(None); + + // Calculate border radius based on style + let style = match STYLE.load() { + BorderStyle::System => { + if *WINDOWS_11 { + BorderStyle::Rounded + } else { + BorderStyle::Square + } } + BorderStyle::Rounded => BorderStyle::Rounded, + BorderStyle::Square => BorderStyle::Square, + }; + + match style { + BorderStyle::Rounded => { + let radius = 8.0 + border_width as f32 / 2.0; + let rounded_rect = D2D1_ROUNDED_RECT { + rect, + radiusX: radius, + radiusY: radius, + }; + + render_target.DrawRoundedRectangle( + &rounded_rect, + &brush, + border_width as f32, + None, + ); + } + BorderStyle::Square => { + let rect = D2D_RECT_F { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + }; + + render_target.DrawRectangle( + &rect, + &brush, + border_width as f32, + None, + ); + } + _ => {} } - BorderStyle::Rounded => { - // TODO: error handling - let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20); - } - BorderStyle::Square => { - // TODO: error handling - let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom); - } + + let _ = render_target.EndDraw(None, None); + + // If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen + // https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234 + let _ = BeginPaint(window, &mut PAINTSTRUCT::default()); + let _ = EndPaint(window, &PAINTSTRUCT::default()); } - // TODO: error handling - let _ = DeleteObject(hpen); - // TODO: error handling - let _ = DeleteObject(hbrush); - } - Err(error) => { - tracing::error!("could not get border rect: {}", error.to_string()) } } - - // TODO: error handling - let _ = EndPaint(window, &ps); LRESULT(0) } WM_DESTROY => { diff --git a/komorebi/src/border_manager/mod.rs b/komorebi/src/border_manager/mod.rs index 48271b5f2..2af2dd9a6 100644 --- a/komorebi/src/border_manager/mod.rs +++ b/komorebi/src/border_manager/mod.rs @@ -1,7 +1,6 @@ #![deny(clippy::unwrap_used, clippy::expect_used)] mod border; - use crate::core::BorderImplementation; use crate::core::BorderStyle; use crate::core::WindowKind; @@ -30,6 +29,10 @@ use std::sync::atomic::AtomicU32; use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::OnceLock; +use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget; +use windows::Win32::System::Threading::GetCurrentThread; +use windows::Win32::System::Threading::SetThreadPriority; +use windows::Win32::System::Threading::THREAD_PRIORITY_TIME_CRITICAL; pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8); pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1); @@ -57,6 +60,8 @@ lazy_static! { static ref BORDERS_MONITORS: Mutex> = Mutex::new(HashMap::new()); static ref BORDER_STATE: Mutex> = Mutex::new(HashMap::new()); static ref FOCUS_STATE: Mutex> = Mutex::new(HashMap::new()); + static ref RENDER_TARGETS: Mutex> = + Mutex::new(HashMap::new()); } pub struct Notification(pub Option); @@ -95,6 +100,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> { borders.clear(); BORDERS_MONITORS.lock().clear(); FOCUS_STATE.lock().clear(); + RENDER_TARGETS.lock().clear(); let mut remaining_hwnds = vec![]; @@ -125,13 +131,22 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 { } pub fn listen_for_notifications(wm: Arc>) { - std::thread::spawn(move || loop { - match handle_notifications(wm.clone()) { - Ok(()) => { - tracing::warn!("restarting finished thread"); + std::thread::spawn(move || { + unsafe { + if let Err(error) = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) + { + tracing::error!("{error}"); } - Err(error) => { - tracing::warn!("restarting failed thread: {}", error); + } + + loop { + match handle_notifications(wm.clone()) { + Ok(()) => { + tracing::warn!("restarting finished thread"); + } + Err(error) => { + tracing::warn!("restarting failed thread: {}", error); + } } } }); @@ -444,7 +459,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result if rect != new_rect { rect = new_rect; - border.update(&rect, true)?; + border.update(&rect, false)?; } }