fix(windows): resolve DWM transparency, eliminate white flash, fix capture staging, and terminate locked processes in installer
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions

This commit is contained in:
katR 2026-09-06 15:51:14 +05:30
parent fbfe6f2360
commit 5b4797ceff
7 changed files with 131 additions and 34 deletions

2
Cargo.lock generated
View file

@ -3700,7 +3700,7 @@ dependencies = [
[[package]] [[package]]
name = "scythe" name = "scythe"
version = "0.2.2" version = "0.2.3"
dependencies = [ dependencies = [
"ashpd", "ashpd",
"cpal", "cpal",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "scythe" name = "scythe"
version = "0.2.2" version = "0.2.3"
edition = "2024" edition = "2024"
[[bin]] [[bin]]
@ -45,9 +45,11 @@ windows = { version = "0.58.0", features = [
"Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common", "Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi", "Win32_Graphics_Gdi",
"Win32_Security", "Win32_Security",
"Win32_System_Com", "Win32_System_Com",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
"Win32_UI_Controls",
"Win32_System_Threading", "Win32_System_Threading",
] } ] }

View file

@ -26,6 +26,12 @@ RequestExecutionLevel user
!insertmacro MUI_LANGUAGE "English" !insertmacro MUI_LANGUAGE "English"
Section "Scythe" SecMain 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" SetOutPath "$INSTDIR"
File /r "..\..\dist\bundle\*.*" File /r "..\..\dist\bundle\*.*"
@ -43,6 +49,10 @@ Section "Scythe" SecMain
SectionEnd SectionEnd
Section "Uninstall" 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" Delete "$DESKTOP\scythe.lnk"
RMDir /r "$SMPROGRAMS\scythe" RMDir /r "$SMPROGRAMS\scythe"
RMDir /r "$INSTDIR" RMDir /r "$INSTDIR"

View file

@ -8,6 +8,12 @@ SilentInstall silent
RequestExecutionLevel user RequestExecutionLevel user
Section 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 ; Extract self-contained binaries and all runtime DLLs directly to user app data
SetOutPath "$LOCALAPPDATA\scythe" SetOutPath "$LOCALAPPDATA\scythe"
File /r "..\..\dist\bundle\*.*" File /r "..\..\dist\bundle\*.*"

View file

@ -15,9 +15,10 @@ pub struct WindowsCapture {
device: ID3D11Device, device: ID3D11Device,
context: ID3D11DeviceContext, context: ID3D11DeviceContext,
duplication: IDXGIOutputDuplication, duplication: IDXGIOutputDuplication,
gpu_texture: ID3D11Texture2D, staging_texture: ID3D11Texture2D,
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
last_frame: Vec<u8>,
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -85,33 +86,34 @@ impl WindowsCapture {
// Initialize Desktop Duplication // Initialize Desktop Duplication
let duplication = output1.DuplicateOutput(&device)?; let duplication = output1.DuplicateOutput(&device)?;
// Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture // Allocate a CPU-accessible staging texture for hardware frame reading
let gpu_desc = D3D11_TEXTURE2D_DESC { let staging_desc = D3D11_TEXTURE2D_DESC {
Width: width, Width: width,
Height: height, Height: height,
MipLevels: 1, MipLevels: 1,
ArraySize: 1, ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM, Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
Usage: D3D11_USAGE_DEFAULT, Usage: D3D11_USAGE_STAGING,
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, BindFlags: 0,
CPUAccessFlags: 0, CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
MiscFlags: 0, MiscFlags: 0,
}; };
let mut gpu_texture: Option<ID3D11Texture2D> = None; let mut staging_texture: Option<ID3D11Texture2D> = None;
device.CreateTexture2D(&gpu_desc, None, Some(&mut gpu_texture))?; device.CreateTexture2D(&staging_desc, None, Some(&mut staging_texture))?;
let gpu_texture = gpu_texture.ok_or("Failed to create GPU VRAM 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 { Ok(Self {
device, device,
context, context,
duplication, duplication,
gpu_texture, staging_texture,
width, width,
height, height,
last_frame: Vec::new(),
}) })
} }
} }
@ -143,32 +145,51 @@ impl FrameSource for WindowsCapture {
let mut desktop_resource: Option<IDXGIResource> = None; let mut desktop_resource: Option<IDXGIResource> = None;
for _ in 0..5 { 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(()) => { Ok(()) => {
if let Some(resource) = desktop_resource { if let Some(resource) = desktop_resource {
let texture: ID3D11Texture2D = resource.cast()?; let texture: ID3D11Texture2D = resource.cast()?;
// Copy directly on the GPU from desktop buffer to persistent VRAM texture // Copy GPU desktop texture to staging texture accessible by CPU
self.context.CopyResource(&self.gpu_texture, &texture); self.context.CopyResource(&self.staging_texture, &texture);
// Immediately release the desktop frame back to the DWM compositor // Immediately release the desktop frame back to the DWM compositor
let _ = self.duplication.ReleaseFrame(); let _ = self.duplication.ReleaseFrame();
return Ok(Frame::D3D11Texture { let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
handle: self.gpu_texture.as_raw() as usize, 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, width: self.width,
height: self.height, height: self.height,
stride: stride as u32,
data: self.last_frame.clone(),
}); });
} }
let _ = self.duplication.ReleaseFrame(); let _ = self.duplication.ReleaseFrame();
} }
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => { 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)); 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 => { Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
println!("DXGI access lost (display mode or fullscreen switch), reacquiring..."); println!("DXGI access lost (display mode or fullscreen switch), reacquiring...");
@ -182,11 +203,21 @@ impl FrameSource for WindowsCapture {
} }
} }
Ok(Frame::D3D11Texture { if !self.last_frame.is_empty() {
handle: self.gpu_texture.as_raw() as usize, Ok(Frame::Raw {
width: self.width, width: self.width,
height: self.height, 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],
})
}
} }
} }
} }

View file

@ -70,8 +70,8 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
"delete" | "del" => Some(Code::Delete), "delete" | "del" => Some(Code::Delete),
"home" => Some(Code::Home), "home" => Some(Code::Home),
"end" => Some(Code::End), "end" => Some(Code::End),
"pageup" | "pgup" => Some(Code::PageUp), "pageup" | "pgup" | "page_up" => Some(Code::PageUp),
"pagedown" | "pgdn" => Some(Code::PageDown), "pagedown" | "pgdn" | "page_down" => Some(Code::PageDown),
_ => None, _ => None,
}; };
} }

View file

@ -912,7 +912,7 @@ impl ScytheOverlayApp {
self.output_dir = new_dir.clone(); self.output_dir = new_dir.clone();
self.config.output_directory = new_dir; self.config.output_directory = new_dir;
let _ = self.config.save(); let _ = self.config.save();
let _ = ipc::send_command(Command::ReloadConfig); async_send_command(Command::ReloadConfig);
self.refresh_clips(); self.refresh_clips();
} }
} }
@ -2130,18 +2130,21 @@ impl eframe::App for ScytheOverlayApp {
if self.listening_keybind.is_some() || self.hud_notification.is_some() { if self.listening_keybind.is_some() || self.hud_notification.is_some() {
ctx.request_repaint(); ctx.request_repaint();
} else { } 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 !self.initial_pos_set {
if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) { if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) {
if monitor_size.x > 100.0 && monitor_size.y > 100.0 { 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::OuterPosition(egui::pos2(0.0, 0.0)));
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(monitor_size)); 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 // Handle interactive keybind recording mode
@ -2155,6 +2158,10 @@ impl eframe::App for ScytheOverlayApp {
break; break;
} }
if let Some(key_name) = format_egui_key(*key) { 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(); let mut parts = Vec::new();
if modifiers.ctrl { if modifiers.ctrl {
parts.push("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() { pub fn run_egui_overlay() {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let (screen_w, screen_h): (f32, f32) = unsafe { let (screen_w, screen_h): (f32, f32) = unsafe {
@ -2280,6 +2308,7 @@ pub fn run_egui_overlay() {
.with_resizable(false) .with_resizable(false)
.with_decorations(false) .with_decorations(false)
.with_transparent(true) .with_transparent(true)
.with_visible(false)
.with_always_on_top(), .with_always_on_top(),
..Default::default() ..Default::default()
}; };
@ -2300,6 +2329,7 @@ pub struct ShadowPlayToastApp {
accent: Color32, accent: Color32,
created_at: Instant, created_at: Instant,
duration: Duration, duration: Duration,
initial_setup: bool,
} }
impl eframe::App for ShadowPlayToastApp { 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) { 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 elapsed = self.created_at.elapsed().as_secs_f32();
let total_dur = self.duration.as_secs_f32(); let total_dur = self.duration.as_secs_f32();
if elapsed >= total_dur { 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_w: f32 = 340.0;
let toast_h: f32 = 64.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")] #[cfg(target_os = "windows")]
let screen_w: f32 = unsafe { let screen_w: f32 = unsafe {
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN}; 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_inner_size([toast_w, toast_h])
.with_decorations(false) .with_decorations(false)
.with_transparent(true) .with_transparent(true)
.with_visible(false)
.with_always_on_top() .with_always_on_top()
.with_resizable(false), .with_resizable(false),
..Default::default() ..Default::default()
@ -2459,6 +2506,7 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
accent, accent,
created_at: Instant::now(), created_at: Instant::now(),
duration: Duration::from_millis(2800), duration: Duration::from_millis(2800),
initial_setup: false,
}; };
// Watchdog thread to guarantee exit after duration // Watchdog thread to guarantee exit after duration