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]]
name = "scythe"
version = "0.2.2"
version = "0.2.3"
dependencies = [
"ashpd",
"cpal",

View file

@ -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",
] }

View file

@ -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"

View file

@ -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\*.*"

View file

@ -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<u8>,
}
#[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<ID3D11Texture2D> = 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<ID3D11Texture2D> = 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<IDXGIResource> = 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],
})
}
}
}
}

View file

@ -70,8 +70,8 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
"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,
};
}

View file

@ -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