diff --git a/Cargo.lock b/Cargo.lock index ee131c8..9668093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "scythe" -version = "0.2.2" +version = "0.2.3" dependencies = [ "ashpd", "cpal", diff --git a/Cargo.toml b/Cargo.toml index 9354eb1..3c0c2e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scythe" -version = "0.2.2" +version = "0.2.3" edition = "2024" [[bin]] @@ -45,9 +45,11 @@ windows = { version = "0.58.0", features = [ "Win32_Graphics_Direct3D11", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", + "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", "Win32_Security", "Win32_System_Com", "Win32_UI_WindowsAndMessaging", + "Win32_UI_Controls", "Win32_System_Threading", ] } diff --git a/packaging/windows/scythe-installer.nsi b/packaging/windows/scythe-installer.nsi index 055904d..c439b2b 100644 --- a/packaging/windows/scythe-installer.nsi +++ b/packaging/windows/scythe-installer.nsi @@ -26,6 +26,12 @@ RequestExecutionLevel user !insertmacro MUI_LANGUAGE "English" Section "Scythe" SecMain + ; Terminate any running instances so locked executables and DLLs can be updated cleanly + nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1' + nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1' + Sleep 500 + + SetOverwrite on SetOutPath "$INSTDIR" File /r "..\..\dist\bundle\*.*" @@ -43,6 +49,10 @@ Section "Scythe" SecMain SectionEnd Section "Uninstall" + nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1' + nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1' + Sleep 500 + Delete "$DESKTOP\scythe.lnk" RMDir /r "$SMPROGRAMS\scythe" RMDir /r "$INSTDIR" diff --git a/packaging/windows/scythe-standalone.nsi b/packaging/windows/scythe-standalone.nsi index c86453b..df93487 100644 --- a/packaging/windows/scythe-standalone.nsi +++ b/packaging/windows/scythe-standalone.nsi @@ -8,6 +8,12 @@ SilentInstall silent RequestExecutionLevel user Section + ; Terminate any running instances so locked executables and DLLs can be updated cleanly + nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1' + nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1' + Sleep 500 + + SetOverwrite on ; Extract self-contained binaries and all runtime DLLs directly to user app data SetOutPath "$LOCALAPPDATA\scythe" File /r "..\..\dist\bundle\*.*" diff --git a/src/capture/windows.rs b/src/capture/windows.rs index 03f11f6..b5533db 100644 --- a/src/capture/windows.rs +++ b/src/capture/windows.rs @@ -15,9 +15,10 @@ pub struct WindowsCapture { device: ID3D11Device, context: ID3D11DeviceContext, duplication: IDXGIOutputDuplication, - gpu_texture: ID3D11Texture2D, + staging_texture: ID3D11Texture2D, pub width: u32, pub height: u32, + last_frame: Vec, } #[cfg(target_os = "windows")] @@ -85,33 +86,34 @@ impl WindowsCapture { // Initialize Desktop Duplication let duplication = output1.DuplicateOutput(&device)?; - // Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture - let gpu_desc = D3D11_TEXTURE2D_DESC { + // Allocate a CPU-accessible staging texture for hardware frame reading + let staging_desc = D3D11_TEXTURE2D_DESC { Width: width, Height: height, MipLevels: 1, ArraySize: 1, Format: DXGI_FORMAT_B8G8R8A8_UNORM, SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - CPUAccessFlags: 0, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, MiscFlags: 0, }; - let mut gpu_texture: Option = None; - device.CreateTexture2D(&gpu_desc, None, Some(&mut gpu_texture))?; - let gpu_texture = gpu_texture.ok_or("Failed to create GPU VRAM texture")?; + let mut staging_texture: Option = None; + device.CreateTexture2D(&staging_desc, None, Some(&mut staging_texture))?; + let staging_texture = staging_texture.ok_or("Failed to create D3D11 staging texture")?; - println!("Windows DXGI Hardware Capture active (Pure GPU Zero-Copy): {}x{}", width, height); + println!("Windows DXGI Hardware Desktop Duplication active: {}x{}", width, height); Ok(Self { device, context, duplication, - gpu_texture, + staging_texture, width, height, + last_frame: Vec::new(), }) } } @@ -143,32 +145,51 @@ impl FrameSource for WindowsCapture { let mut desktop_resource: Option = None; for _ in 0..5 { - match self.duplication.AcquireNextFrame(150, &mut frame_info, &mut desktop_resource) { + match self.duplication.AcquireNextFrame(100, &mut frame_info, &mut desktop_resource) { Ok(()) => { if let Some(resource) = desktop_resource { let texture: ID3D11Texture2D = resource.cast()?; - // Copy directly on the GPU from desktop buffer to persistent VRAM texture - self.context.CopyResource(&self.gpu_texture, &texture); + // Copy GPU desktop texture to staging texture accessible by CPU + self.context.CopyResource(&self.staging_texture, &texture); // Immediately release the desktop frame back to the DWM compositor let _ = self.duplication.ReleaseFrame(); - return Ok(Frame::D3D11Texture { - handle: self.gpu_texture.as_raw() as usize, + let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); + self.context.Map(&self.staging_texture, 0, D3D11_MAP_READ, 0, Some(&mut mapped))?; + + let stride = mapped.RowPitch as usize; + let total_bytes = stride * self.height as usize; + if self.last_frame.len() != total_bytes { + self.last_frame.resize(total_bytes, 0); + } + std::ptr::copy_nonoverlapping( + mapped.pData as *const u8, + self.last_frame.as_mut_ptr(), + total_bytes, + ); + self.context.Unmap(&self.staging_texture, 0); + + return Ok(Frame::Raw { width: self.width, height: self.height, + stride: stride as u32, + data: self.last_frame.clone(), }); } let _ = self.duplication.ReleaseFrame(); } Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => { + if !self.last_frame.is_empty() { + return Ok(Frame::Raw { + width: self.width, + height: self.height, + stride: self.width * 4, + data: self.last_frame.clone(), + }); + } std::thread::sleep(std::time::Duration::from_millis(8)); - return Ok(Frame::D3D11Texture { - handle: self.gpu_texture.as_raw() as usize, - width: self.width, - height: self.height, - }); } Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => { println!("DXGI access lost (display mode or fullscreen switch), reacquiring..."); @@ -182,11 +203,21 @@ impl FrameSource for WindowsCapture { } } - Ok(Frame::D3D11Texture { - handle: self.gpu_texture.as_raw() as usize, - width: self.width, - height: self.height, - }) + if !self.last_frame.is_empty() { + Ok(Frame::Raw { + width: self.width, + height: self.height, + stride: self.width * 4, + data: self.last_frame.clone(), + }) + } else { + Ok(Frame::Raw { + width: self.width, + height: self.height, + stride: self.width * 4, + data: vec![0u8; (self.width * self.height * 4) as usize], + }) + } } } } diff --git a/src/hotkey.rs b/src/hotkey.rs index 58b027f..b3763b6 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -70,8 +70,8 @@ pub fn parse_hotkey(s: &str) -> Option { "delete" | "del" => Some(Code::Delete), "home" => Some(Code::Home), "end" => Some(Code::End), - "pageup" | "pgup" => Some(Code::PageUp), - "pagedown" | "pgdn" => Some(Code::PageDown), + "pageup" | "pgup" | "page_up" => Some(Code::PageUp), + "pagedown" | "pgdn" | "page_down" => Some(Code::PageDown), _ => None, }; } diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 4f6b630..4fa8a8a 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -912,7 +912,7 @@ impl ScytheOverlayApp { self.output_dir = new_dir.clone(); self.config.output_directory = new_dir; let _ = self.config.save(); - let _ = ipc::send_command(Command::ReloadConfig); + async_send_command(Command::ReloadConfig); self.refresh_clips(); } } @@ -2130,18 +2130,21 @@ impl eframe::App for ScytheOverlayApp { if self.listening_keybind.is_some() || self.hud_notification.is_some() { ctx.request_repaint(); } else { - ctx.request_repaint_after(Duration::from_millis(33)); + ctx.request_repaint_after(Duration::from_millis(16)); } - // Auto-position and size to monitor on launch + // Auto-position, DWM transparency, and size to monitor on launch if !self.initial_pos_set { if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) { if monitor_size.x > 100.0 && monitor_size.y > 100.0 { ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0))); ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(monitor_size)); - self.initial_pos_set = true; } } + #[cfg(target_os = "windows")] + apply_windows_transparency("Scythe"); + ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true)); + self.initial_pos_set = true; } // Handle interactive keybind recording mode @@ -2155,6 +2158,10 @@ impl eframe::App for ScytheOverlayApp { break; } if let Some(key_name) = format_egui_key(*key) { + let k_lower = key_name.to_lowercase(); + if ["alt", "ctrl", "control", "shift", "super", "meta", "command"].contains(&k_lower.as_str()) { + continue; + } let mut parts = Vec::new(); if modifiers.ctrl { parts.push("Ctrl"); @@ -2259,6 +2266,27 @@ impl eframe::App for ScytheOverlayApp { } } +#[cfg(target_os = "windows")] +pub fn apply_windows_transparency(title: &str) { + unsafe { + use windows::Win32::UI::WindowsAndMessaging::FindWindowW; + use windows::Win32::Graphics::Dwm::DwmExtendFrameIntoClientArea; + use windows::Win32::UI::Controls::MARGINS; + use windows::core::HSTRING; + + let htitle = HSTRING::from(title); + if let Ok(hwnd) = FindWindowW(None, &htitle) { + let margins = MARGINS { + cxLeftWidth: -1, + cxRightWidth: -1, + cyTopHeight: -1, + cyBottomHeight: -1, + }; + let _ = DwmExtendFrameIntoClientArea(hwnd, &margins); + } + } +} + pub fn run_egui_overlay() { #[cfg(target_os = "windows")] let (screen_w, screen_h): (f32, f32) = unsafe { @@ -2280,6 +2308,7 @@ pub fn run_egui_overlay() { .with_resizable(false) .with_decorations(false) .with_transparent(true) + .with_visible(false) .with_always_on_top(), ..Default::default() }; @@ -2300,6 +2329,7 @@ pub struct ShadowPlayToastApp { accent: Color32, created_at: Instant, duration: Duration, + initial_setup: bool, } impl eframe::App for ShadowPlayToastApp { @@ -2308,6 +2338,13 @@ impl eframe::App for ShadowPlayToastApp { } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + if !self.initial_setup { + #[cfg(target_os = "windows")] + apply_windows_transparency("Scythe Notification"); + ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true)); + self.initial_setup = true; + } + let elapsed = self.created_at.elapsed().as_secs_f32(); let total_dur = self.duration.as_secs_f32(); if elapsed >= total_dur { @@ -2427,6 +2464,15 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc let toast_w: f32 = 340.0; let toast_h: f32 = 64.0; + #[cfg(target_os = "windows")] + unsafe { + use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, PostMessageW, WM_CLOSE}; + if let Ok(prev) = FindWindowW(None, windows::core::w!("Scythe Notification")) { + let _ = PostMessageW(prev, WM_CLOSE, windows::Win32::Foundation::WPARAM(0), windows::Win32::Foundation::LPARAM(0)); + std::thread::sleep(Duration::from_millis(30)); + } + } + #[cfg(target_os = "windows")] let screen_w: f32 = unsafe { use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN}; @@ -2447,6 +2493,7 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc .with_inner_size([toast_w, toast_h]) .with_decorations(false) .with_transparent(true) + .with_visible(false) .with_always_on_top() .with_resizable(false), ..Default::default() @@ -2459,6 +2506,7 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc accent, created_at: Instant::now(), duration: Duration::from_millis(2800), + initial_setup: false, }; // Watchdog thread to guarantee exit after duration