fix(windows): resolve capture freeze, duplicate hotkey saves, overlay transparency, and settings UI alignment
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
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:
parent
aa1c6643b8
commit
b2d18297da
7 changed files with 289 additions and 157 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "scythe"
|
||||
version = "0.2.14"
|
||||
version = "0.2.15"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"cpal",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "scythe"
|
||||
version = "0.2.14"
|
||||
version = "0.2.15"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
|
|
@ -461,6 +461,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let mut normal_muxer: Option<Muxer> = None;
|
||||
let mut normal_recording = false;
|
||||
let mut normal_waiting_keyframe = false;
|
||||
let mut normal_keyframe_timeout: usize = 0;
|
||||
let mut last_replay_save = std::time::Instant::now() - std::time::Duration::from_secs(10);
|
||||
let mut rec_base_video_pts: i64 = 0;
|
||||
let mut rec_base_audio_pts: i64 = -1;
|
||||
|
||||
|
|
@ -648,12 +650,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
},
|
||||
Command::SaveReplay => {
|
||||
if config.replay_enabled {
|
||||
let drain = ring.iter().cloned().collect::<Vec<_>>();
|
||||
println!("SaveReplay triggered: {} packets in ring buffer", drain.len());
|
||||
if drain.is_empty() {
|
||||
eprintln!("Warning: Replay buffer is empty (no frames encoded yet).");
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(last_replay_save) > std::time::Duration::from_millis(800) {
|
||||
last_replay_save = now;
|
||||
let drain = ring.iter().cloned().collect::<Vec<_>>();
|
||||
println!("SaveReplay triggered: {} packets in ring buffer", drain.len());
|
||||
if drain.is_empty() {
|
||||
eprintln!("Warning: Replay buffer is empty (no frames encoded yet).");
|
||||
} else {
|
||||
let _ = mux_tx.send(drain);
|
||||
}
|
||||
} else {
|
||||
let _ = mux_tx.send(drain);
|
||||
println!("Ignoring duplicate SaveReplay command (debounced)");
|
||||
}
|
||||
} else {
|
||||
println!("SaveReplay requested but replay is disabled in daemon config.");
|
||||
|
|
@ -663,11 +671,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
if !normal_recording {
|
||||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
normal_keyframe_timeout = 0;
|
||||
encoder.force_keyframe();
|
||||
rec_state_clone.store(true, Ordering::SeqCst);
|
||||
rec_base_audio_pts = -1;
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
rec_start_clone.store(now, Ordering::SeqCst);
|
||||
println!("StartRecording requested, waiting for keyframe...");
|
||||
println!("StartRecording requested, forcing keyframe...");
|
||||
}
|
||||
},
|
||||
Command::StopRecording => {
|
||||
|
|
@ -677,6 +687,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
normal_recording = false;
|
||||
normal_waiting_keyframe = false;
|
||||
normal_keyframe_timeout = 0;
|
||||
rec_base_video_pts = 0;
|
||||
rec_base_audio_pts = -1;
|
||||
rec_state_clone.store(false, Ordering::SeqCst);
|
||||
|
|
@ -692,6 +703,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
normal_recording = false;
|
||||
normal_waiting_keyframe = false;
|
||||
normal_keyframe_timeout = 0;
|
||||
rec_base_video_pts = 0;
|
||||
rec_base_audio_pts = -1;
|
||||
rec_state_clone.store(false, Ordering::SeqCst);
|
||||
|
|
@ -701,11 +713,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
} else {
|
||||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
normal_keyframe_timeout = 0;
|
||||
encoder.force_keyframe();
|
||||
rec_state_clone.store(true, Ordering::SeqCst);
|
||||
rec_base_audio_pts = -1;
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
rec_start_clone.store(now, Ordering::SeqCst);
|
||||
println!("ToggleRecording: StartRecording requested, waiting for keyframe...");
|
||||
println!("ToggleRecording: StartRecording requested, forcing keyframe...");
|
||||
}
|
||||
},
|
||||
Command::ToggleAudio => {
|
||||
|
|
@ -942,8 +956,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
for mut pkt in packets {
|
||||
pkt.set_stream_index(0);
|
||||
if normal_recording {
|
||||
if normal_waiting_keyframe && pkt.is_keyframe() {
|
||||
if normal_waiting_keyframe && (pkt.is_keyframe() || normal_keyframe_timeout >= 20) {
|
||||
normal_waiting_keyframe = false;
|
||||
normal_keyframe_timeout = 0;
|
||||
rec_base_video_pts = pkt.pts();
|
||||
rec_base_audio_pts = -1;
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
|
|
@ -952,6 +967,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
|
||||
normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", full_path);
|
||||
} else if normal_waiting_keyframe {
|
||||
normal_keyframe_timeout += 1;
|
||||
}
|
||||
|
||||
if !normal_waiting_keyframe
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use scythe::ipc::{Command, send_command, query_status};
|
||||
use scythe::overlay::{show_shadowplay_toast, ToastIcon};
|
||||
use std::env;
|
||||
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent};
|
||||
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent, HotKeyState};
|
||||
|
||||
fn ensure_wayland_env() {
|
||||
scythe::overlay::ensure_wayland_env();
|
||||
|
|
@ -572,6 +572,9 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
|
||||
while let Ok(event) = receiver.try_recv() {
|
||||
if event.state != HotKeyState::Pressed {
|
||||
continue;
|
||||
}
|
||||
if Some(event.id) == id_menu {
|
||||
std::thread::spawn(|| {
|
||||
ensure_daemon_running();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use std::sync::Arc;
|
|||
pub struct WindowsCapture {
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
duplication: IDXGIOutputDuplication,
|
||||
duplication: Option<IDXGIOutputDuplication>,
|
||||
staging_textures: [ID3D11Texture2D; 2],
|
||||
staging_idx: usize,
|
||||
has_staged_frame: bool,
|
||||
|
|
@ -94,7 +94,7 @@ impl WindowsCapture {
|
|||
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
||||
|
||||
// Initialize Desktop Duplication
|
||||
let duplication = output1.DuplicateOutput(&device)?;
|
||||
let duplication = output1.DuplicateOutput(&device).ok();
|
||||
|
||||
// Allocate double-buffered CPU-accessible staging textures for asynchronous zero-stall GPU readback
|
||||
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||
|
|
@ -139,53 +139,81 @@ impl WindowsCapture {
|
|||
|
||||
fn reinit_duplication(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
unsafe {
|
||||
// Drop old duplication interface first to release COM reference
|
||||
self.duplication = None;
|
||||
self.has_staged_frame = false;
|
||||
self.staging_idx = 0;
|
||||
|
||||
// Wait a moment for DWM compositor to release the previous duplication handle
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
|
||||
// Check if device was removed/lost
|
||||
if self.device.GetDeviceRemovedReason().is_err() {
|
||||
println!("D3D11 device removed or reset. Recreating device...");
|
||||
if let Ok(new_cap) = WindowsCapture::new() {
|
||||
self.device = new_cap.device;
|
||||
self.context = new_cap.context;
|
||||
self.duplication = new_cap.duplication;
|
||||
self.staging_textures = new_cap.staging_textures;
|
||||
self.width = new_cap.width;
|
||||
self.height = new_cap.height;
|
||||
self.last_stride = new_cap.last_stride;
|
||||
self.buffer_pool.clear();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let dxgi_device: IDXGIDevice = self.device.cast()?;
|
||||
let adapter = dxgi_device.GetAdapter()?;
|
||||
let mut o_idx = 0;
|
||||
while let Ok(output) = adapter.EnumOutputs(o_idx) {
|
||||
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
|
||||
if let Ok(dup) = output1.DuplicateOutput(&self.device) {
|
||||
self.duplication = dup;
|
||||
self.has_staged_frame = false;
|
||||
self.staging_idx = 0;
|
||||
if let Ok(desc) = output1.GetDesc() {
|
||||
let new_w = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
|
||||
let new_h = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
||||
if new_w != self.width || new_h != self.height {
|
||||
self.width = new_w;
|
||||
self.height = new_h;
|
||||
self.last_stride = new_w * 4;
|
||||
self.buffer_pool.clear();
|
||||
self.last_frame = None;
|
||||
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: new_w,
|
||||
Height: new_h,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
|
||||
Usage: D3D11_USAGE_STAGING,
|
||||
BindFlags: 0,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let mut tex0: Option<ID3D11Texture2D> = None;
|
||||
let mut tex1: Option<ID3D11Texture2D> = None;
|
||||
let ok0 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex0)).is_ok();
|
||||
let ok1 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex1)).is_ok();
|
||||
if ok0 && ok1 {
|
||||
if let (Some(t0), Some(t1)) = (tex0, tex1) {
|
||||
self.staging_textures = [t0, t1];
|
||||
match output1.DuplicateOutput(&self.device) {
|
||||
Ok(dup) => {
|
||||
self.duplication = Some(dup);
|
||||
if let Ok(desc) = output1.GetDesc() {
|
||||
let new_w = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
|
||||
let new_h = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
||||
if new_w != self.width || new_h != self.height {
|
||||
self.width = new_w;
|
||||
self.height = new_h;
|
||||
self.last_stride = new_w * 4;
|
||||
self.buffer_pool.clear();
|
||||
self.last_frame = None;
|
||||
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: new_w,
|
||||
Height: new_h,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
|
||||
Usage: D3D11_USAGE_STAGING,
|
||||
BindFlags: 0,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let mut tex0: Option<ID3D11Texture2D> = None;
|
||||
let mut tex1: Option<ID3D11Texture2D> = None;
|
||||
let ok0 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex0)).is_ok();
|
||||
let ok1 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex1)).is_ok();
|
||||
if ok0 && ok1 {
|
||||
if let (Some(t0), Some(t1)) = (tex0, tex1) {
|
||||
self.staging_textures = [t0, t1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Successfully reinitialized Windows desktop duplication: {}x{}", self.width, self.height);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("DuplicateOutput attempt on output {} failed: {:?}", o_idx, e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
o_idx += 1;
|
||||
}
|
||||
Err("Failed to reinitialize desktop duplication".into())
|
||||
Err("Failed to reinitialize desktop duplication on any output".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,11 +222,36 @@ impl WindowsCapture {
|
|||
impl FrameSource for WindowsCapture {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
unsafe {
|
||||
if self.duplication.is_none() {
|
||||
let _ = self.reinit_duplication();
|
||||
}
|
||||
|
||||
let mut dup = match self.duplication.clone() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
if let Some(ref last) = self.last_frame {
|
||||
return Ok(Frame::Raw {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
stride: self.last_stride,
|
||||
data: Arc::clone(last),
|
||||
});
|
||||
} else {
|
||||
return Ok(Frame::Raw {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
stride: self.last_stride,
|
||||
data: Arc::new(vec![0u8; (self.last_stride * self.height) as usize]),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
|
||||
let mut desktop_resource: Option<IDXGIResource> = None;
|
||||
|
||||
for _ in 0..10 {
|
||||
match self.duplication.AcquireNextFrame(25, &mut frame_info, &mut desktop_resource) {
|
||||
for _ in 0..4 {
|
||||
match dup.AcquireNextFrame(8, &mut frame_info, &mut desktop_resource) {
|
||||
Ok(()) => {
|
||||
if let Some(resource) = desktop_resource {
|
||||
let texture: ID3D11Texture2D = resource.cast()?;
|
||||
|
|
@ -210,7 +263,7 @@ impl FrameSource for WindowsCapture {
|
|||
self.context.CopyResource(&self.staging_textures[write_idx], &texture);
|
||||
|
||||
// Immediately release the desktop frame back to the DWM compositor
|
||||
let _ = self.duplication.ReleaseFrame();
|
||||
let _ = dup.ReleaseFrame();
|
||||
|
||||
// Use ping-pong texture:
|
||||
// On first frame, read write_idx directly.
|
||||
|
|
@ -269,33 +322,32 @@ impl FrameSource for WindowsCapture {
|
|||
data: to_send,
|
||||
});
|
||||
}
|
||||
let _ = self.duplication.ReleaseFrame();
|
||||
let _ = dup.ReleaseFrame();
|
||||
}
|
||||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
||||
// Desktop static / no new frame presented. Sleep briefly and wait.
|
||||
// Do NOT allocate duplicate frames!
|
||||
std::thread::sleep(std::time::Duration::from_millis(3));
|
||||
continue;
|
||||
// Desktop static / no new frame presented.
|
||||
break;
|
||||
}
|
||||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST
|
||||
|| e.code() == DXGI_ERROR_ACCESS_DENIED
|
||||
|| e.code() == DXGI_ERROR_INVALID_CALL => {
|
||||
let _ = self.duplication.ReleaseFrame();
|
||||
let _ = dup.ReleaseFrame();
|
||||
drop(dup);
|
||||
self.duplication = None;
|
||||
let _ = self.reinit_duplication();
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = self.duplication.ReleaseFrame();
|
||||
let _ = dup.ReleaseFrame();
|
||||
drop(dup);
|
||||
self.duplication = None;
|
||||
let _ = self.reinit_duplication();
|
||||
std::thread::sleep(std::time::Duration::from_millis(16));
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If desktop was completely idle across multiple iterations,
|
||||
// return zero-copy reference to last frame so stream stays active without heap thrashing
|
||||
// If desktop was completely idle or recovering, return last frame
|
||||
if let Some(ref last) = self.last_frame {
|
||||
Ok(Frame::Raw {
|
||||
width: self.width,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub struct VaapiEncoder {
|
|||
staged_nv12: *mut AVFrame,
|
||||
hw_frame: *mut AVFrame,
|
||||
has_frame: bool,
|
||||
pub force_idr: bool,
|
||||
#[cfg(unix)]
|
||||
dma_mmap_cache: HashMap<i32, (*mut libc::c_void, usize)>,
|
||||
#[cfg(unix)]
|
||||
|
|
@ -18,6 +19,10 @@ pub struct VaapiEncoder {
|
|||
}
|
||||
|
||||
impl VaapiEncoder {
|
||||
pub fn force_keyframe(&mut self) {
|
||||
self.force_idr = true;
|
||||
}
|
||||
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, String> {
|
||||
Self::new_with_params(width, height, 18_000, 60, "h264")
|
||||
}
|
||||
|
|
@ -180,6 +185,7 @@ impl VaapiEncoder {
|
|||
staged_nv12,
|
||||
hw_frame,
|
||||
has_frame: false,
|
||||
force_idr: false,
|
||||
#[cfg(unix)]
|
||||
dma_mmap_cache: HashMap::new(),
|
||||
#[cfg(unix)]
|
||||
|
|
@ -203,6 +209,12 @@ impl VaapiEncoder {
|
|||
}
|
||||
|
||||
(*self.hw_frame).pts = pts;
|
||||
if self.force_idr {
|
||||
(*self.hw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
|
||||
self.force_idr = false;
|
||||
} else {
|
||||
(*self.hw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
|
||||
}
|
||||
|
||||
if avcodec_send_frame(self.codec_ctx, self.hw_frame) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
|
|
@ -416,10 +428,15 @@ pub struct WindowsHwEncoder {
|
|||
sw_frame: *mut AVFrame,
|
||||
pub has_frame: bool,
|
||||
native_bgr: bool,
|
||||
pub force_idr: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl WindowsHwEncoder {
|
||||
pub fn force_keyframe(&mut self) {
|
||||
self.force_idr = true;
|
||||
}
|
||||
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, String> {
|
||||
Self::new_with_params(width, height, 20_000, 60, "h264")
|
||||
}
|
||||
|
|
@ -580,6 +597,7 @@ impl WindowsHwEncoder {
|
|||
sw_frame,
|
||||
has_frame: false,
|
||||
native_bgr: is_native_bgr,
|
||||
force_idr: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -632,6 +650,12 @@ impl WindowsHwEncoder {
|
|||
}
|
||||
|
||||
(*self.sw_frame).pts = pts;
|
||||
if self.force_idr {
|
||||
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
|
||||
self.force_idr = false;
|
||||
} else {
|
||||
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
|
||||
}
|
||||
|
||||
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
|
|
@ -647,6 +671,12 @@ impl WindowsHwEncoder {
|
|||
#[cfg(target_os = "windows")]
|
||||
Frame::D3D11Texture { handle: _, .. } => {
|
||||
(*self.sw_frame).pts = pts;
|
||||
if self.force_idr {
|
||||
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
|
||||
self.force_idr = false;
|
||||
} else {
|
||||
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
|
||||
}
|
||||
|
||||
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
|
|
@ -676,6 +706,12 @@ impl WindowsHwEncoder {
|
|||
return Ok(packets);
|
||||
}
|
||||
(*self.sw_frame).pts = pts;
|
||||
if self.force_idr {
|
||||
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
|
||||
self.force_idr = false;
|
||||
} else {
|
||||
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
|
||||
}
|
||||
|
||||
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
|
|
|
|||
|
|
@ -554,6 +554,10 @@ fn render_keycap_button(
|
|||
|
||||
// Modern squared button matching GPU Screen Recorder Button styling
|
||||
fn squared_button(ui: &mut egui::Ui, text: &str, active: bool, accent: Color32) -> bool {
|
||||
squared_button_sized(ui, text, active, accent, None)
|
||||
}
|
||||
|
||||
fn squared_button_sized(ui: &mut egui::Ui, text: &str, active: bool, accent: Color32, min_size: Option<Vec2>) -> bool {
|
||||
let fill = if active {
|
||||
accent
|
||||
} else {
|
||||
|
|
@ -562,7 +566,7 @@ fn squared_button(ui: &mut egui::Ui, text: &str, active: bool, accent: Color32)
|
|||
let stroke = if active {
|
||||
Stroke::new(1.0_f32, accent)
|
||||
} else {
|
||||
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 20))
|
||||
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 22))
|
||||
};
|
||||
let text_color = if active {
|
||||
if accent.r() as u16 + accent.g() as u16 + accent.b() as u16 > 400 {
|
||||
|
|
@ -573,10 +577,12 @@ fn squared_button(ui: &mut egui::Ui, text: &str, active: bool, accent: Color32)
|
|||
} else {
|
||||
Color32::from_rgb(220, 220, 225)
|
||||
};
|
||||
let sz = min_size.unwrap_or(Vec2::new(36.0, 26.0));
|
||||
let btn = egui::Button::new(egui::RichText::new(text).size(11.5).strong().color(text_color))
|
||||
.fill(fill)
|
||||
.stroke(stroke)
|
||||
.corner_radius(CornerRadius::ZERO);
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.min_size(sz);
|
||||
ui.add(btn).clicked()
|
||||
}
|
||||
|
||||
|
|
@ -961,6 +967,7 @@ pub struct ScytheOverlayApp {
|
|||
autostart_replay: bool,
|
||||
autostart_overlay: bool,
|
||||
pub settings_view_advanced: bool,
|
||||
last_content_h: f32,
|
||||
hud_notification: Option<HudNotification>,
|
||||
}
|
||||
|
||||
|
|
@ -1084,6 +1091,7 @@ impl ScytheOverlayApp {
|
|||
autostart_replay,
|
||||
autostart_overlay,
|
||||
settings_view_advanced: false,
|
||||
last_content_h: 520.0,
|
||||
hud_notification: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1221,6 +1229,7 @@ impl ScytheOverlayApp {
|
|||
self.replay_dropdown_open = false;
|
||||
self.record_dropdown_open = false;
|
||||
self.stream_dropdown_open = false;
|
||||
self.last_content_h = 520.0;
|
||||
}
|
||||
|
||||
fn render_top_bar(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
|
|
@ -1613,7 +1622,8 @@ impl ScytheOverlayApp {
|
|||
// Exact GsrPage geometry formula from scratch/gsr-ui/src/gui/GsrPage.cpp:
|
||||
// content_page_size = (window_size * vec2(0.3333f, 0.70f)).floor();
|
||||
let content_w = (screen_w * 0.3333).clamp(580.0, 680.0);
|
||||
let content_h = (screen_h * 0.70).clamp(520.0, 780.0);
|
||||
let max_content_h = (screen_h * 0.72).clamp(460.0, 780.0);
|
||||
let content_h = self.last_content_h.clamp(360.0, max_content_h);
|
||||
let spacing = (screen_w / 50.0).clamp(20.0, 38.0);
|
||||
let side_w = (screen_w / 10.0).clamp(160.0, 192.0);
|
||||
|
||||
|
|
@ -1825,14 +1835,17 @@ impl ScytheOverlayApp {
|
|||
ShadowPlayView::ReplaySettings | ShadowPlayView::RecordSettings | ShadowPlayView::StreamSettings
|
||||
);
|
||||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(content_rect), |ui| {
|
||||
ui.add_space(14.0);
|
||||
let inner_rect = content_rect.shrink2(egui::vec2(16.0, 12.0));
|
||||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(inner_rect), |ui| {
|
||||
ui.set_width(inner_rect.width());
|
||||
ui.add_space(4.0);
|
||||
|
||||
// [ Simple view ] [ Advanced view ] Horizontal Radio Buttons
|
||||
if has_view_toggle {
|
||||
ui.horizontal(|ui| {
|
||||
let total_radio_w = 260.0;
|
||||
let pad = ((content_w - total_radio_w) * 0.5).max(0.0);
|
||||
let pad = ((inner_rect.width() - total_radio_w) * 0.5).max(0.0);
|
||||
ui.add_space(pad);
|
||||
|
||||
let item_w = 126.0;
|
||||
|
|
@ -1916,15 +1929,15 @@ impl ScytheOverlayApp {
|
|||
self.settings_view_advanced = true;
|
||||
}
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
ui.add_space(12.0);
|
||||
}
|
||||
|
||||
let scroll_h = content_h - (if has_view_toggle { 70.0 } else { 30.0 });
|
||||
egui::ScrollArea::vertical()
|
||||
let scroll_h = inner_rect.height() - (if has_view_toggle { 48.0 } else { 8.0 });
|
||||
let scroll_resp = egui::ScrollArea::vertical()
|
||||
.max_height(scroll_h)
|
||||
.auto_shrink([false, false])
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(content_w - 36.0);
|
||||
ui.set_width(inner_rect.width());
|
||||
ui.add_space(2.0);
|
||||
|
||||
match self.current_view {
|
||||
|
|
@ -1934,11 +1947,12 @@ impl ScytheOverlayApp {
|
|||
ui.label(egui::RichText::new("Directory to save replays:").size(12.0).color(Color32::WHITE));
|
||||
ui.add_space(3.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(ui.available_width() - 140.0));
|
||||
if squared_button(ui, "Change", false, accent) {
|
||||
let edit_w = (ui.available_width() - 152.0).max(120.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(edit_w));
|
||||
if squared_button_sized(ui, "Change", false, accent, Some(egui::vec2(70.0, 26.0))) {
|
||||
pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
|
||||
}
|
||||
if squared_button(ui, "Open", false, accent) {
|
||||
if squared_button_sized(ui, "Open", false, accent, Some(egui::vec2(60.0, 26.0))) {
|
||||
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
|
||||
}
|
||||
});
|
||||
|
|
@ -2155,11 +2169,12 @@ impl ScytheOverlayApp {
|
|||
ui.label(egui::RichText::new("Directory to save recordings:").size(12.0).color(Color32::WHITE));
|
||||
ui.add_space(3.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(ui.available_width() - 140.0));
|
||||
if squared_button(ui, "Change", false, accent) {
|
||||
let edit_w = (ui.available_width() - 152.0).max(120.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(edit_w));
|
||||
if squared_button_sized(ui, "Change", false, accent, Some(egui::vec2(70.0, 26.0))) {
|
||||
pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
|
||||
}
|
||||
if squared_button(ui, "Open", false, accent) {
|
||||
if squared_button_sized(ui, "Open", false, accent, Some(egui::vec2(60.0, 26.0))) {
|
||||
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
|
||||
}
|
||||
});
|
||||
|
|
@ -2461,9 +2476,9 @@ impl ScytheOverlayApp {
|
|||
);
|
||||
}
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
if squared_button(ui, "Reset Hotkeys to Defaults", false, accent) {
|
||||
if squared_button_sized(ui, "Reset Hotkeys to Defaults", false, accent, Some(Vec2::new(180.0, 28.0))) {
|
||||
let old_menu = self.config.menu_hotkey.clone();
|
||||
let old_save = self.config.save_hotkey.clone();
|
||||
let old_rec = self.config.record_hotkey.clone();
|
||||
|
|
@ -2490,58 +2505,77 @@ impl ScytheOverlayApp {
|
|||
render_section_card(ui, "Appearance", accent, |ui| {
|
||||
ui.label(egui::RichText::new("Interface Accent Color:").size(12.0).color(Color32::WHITE));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let palettes = [
|
||||
("amd", "AMD Radeon Red", Color32::from_rgb(221, 0, 49)),
|
||||
("nvidia", "NVIDIA GeForce Green", Color32::from_rgb(118, 185, 0)),
|
||||
("intel", "Intel Arc Blue", Color32::from_rgb(8, 109, 183)),
|
||||
("blue", "Charming Blue", Color32::from_rgb(56, 189, 248)),
|
||||
("green", "Emerald Green", Color32::from_rgb(34, 197, 94)),
|
||||
("yellow", "Solar Yellow", Color32::from_rgb(250, 204, 21)),
|
||||
("purple", "Royal Purple", Color32::from_rgb(168, 85, 247)),
|
||||
("pink", "Neon Pink", Color32::from_rgb(244, 63, 94)),
|
||||
];
|
||||
|
||||
for (id, name, col) in palettes {
|
||||
let is_sel = self.config.accent_color.to_lowercase() == id;
|
||||
let bg = if is_sel {
|
||||
col
|
||||
} else {
|
||||
Color32::from_rgba_unmultiplied(0, 0, 0, 120)
|
||||
};
|
||||
let stroke = if is_sel {
|
||||
Stroke::new(1.5_f32, Color32::WHITE)
|
||||
} else {
|
||||
Stroke::new(1.0_f32, col)
|
||||
};
|
||||
let text_col = if is_sel {
|
||||
if col.r() as u16 + col.g() as u16 + col.b() as u16 > 400 {
|
||||
Color32::BLACK
|
||||
let palettes: [(&str, &str, Color32); 8] = [
|
||||
("amd", "AMD Radeon Red", Color32::from_rgb(221, 0, 49)),
|
||||
("nvidia", "NVIDIA GeForce Green", Color32::from_rgb(118, 185, 0)),
|
||||
("intel", "Intel Arc Blue", Color32::from_rgb(8, 109, 183)),
|
||||
("blue", "Charming Blue", Color32::from_rgb(56, 189, 248)),
|
||||
("green", "Emerald Green", Color32::from_rgb(34, 197, 94)),
|
||||
("yellow", "Solar Yellow", Color32::from_rgb(250, 204, 21)),
|
||||
("purple", "Royal Purple", Color32::from_rgb(168, 85, 247)),
|
||||
("pink", "Neon Pink", Color32::from_rgb(244, 63, 94)),
|
||||
];
|
||||
|
||||
let total_w = ui.available_width();
|
||||
let gap = 6.0_f32;
|
||||
let cols = 4;
|
||||
let btn_w = ((total_w - (cols - 1) as f32 * gap) / cols as f32).floor();
|
||||
let btn_h = 32.0_f32;
|
||||
|
||||
for chunk in palettes.chunks(cols) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = gap;
|
||||
for &(id, name, col) in chunk {
|
||||
let is_sel = self.config.accent_color.to_lowercase() == id;
|
||||
let (rect, resp) = ui.allocate_exact_size(egui::vec2(btn_w, btn_h), egui::Sense::click());
|
||||
let is_hovered = resp.hovered();
|
||||
|
||||
let bg = if is_sel {
|
||||
col
|
||||
} else if is_hovered {
|
||||
Color32::from_rgba_unmultiplied(255, 255, 255, 18)
|
||||
} else {
|
||||
Color32::WHITE
|
||||
Color32::from_rgba_unmultiplied(0, 0, 0, 140)
|
||||
};
|
||||
|
||||
let stroke = if is_sel {
|
||||
Stroke::new(1.5_f32, Color32::WHITE)
|
||||
} else if is_hovered {
|
||||
Stroke::new(1.5_f32, col)
|
||||
} else {
|
||||
Stroke::new(1.0_f32, col)
|
||||
};
|
||||
|
||||
let text_col = if is_sel {
|
||||
if col.r() as u16 + col.g() as u16 + col.b() as u16 > 400 {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::WHITE
|
||||
}
|
||||
} else {
|
||||
Color32::from_rgb(225, 225, 230)
|
||||
};
|
||||
|
||||
ui.painter().rect_filled(rect, CornerRadius::ZERO, bg);
|
||||
ui.painter().rect_stroke(rect, CornerRadius::ZERO, stroke, egui::StrokeKind::Inside);
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
name,
|
||||
FontId::proportional(11.0),
|
||||
text_col,
|
||||
);
|
||||
|
||||
if resp.clicked() {
|
||||
self.config.accent_color = id.to_string();
|
||||
let _ = self.config.save();
|
||||
self.show_hud_notification("THEME ACCENT", &format!("Selected: {}", name), crate::overlay::ToastIcon::Info);
|
||||
}
|
||||
} else {
|
||||
Color32::from_rgb(225, 225, 230)
|
||||
};
|
||||
|
||||
let btn = egui::Button::new(
|
||||
egui::RichText::new(name)
|
||||
.size(11.5)
|
||||
.strong()
|
||||
.color(text_col),
|
||||
)
|
||||
.fill(bg)
|
||||
.stroke(stroke)
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.min_size(Vec2::new(132.0, 30.0));
|
||||
|
||||
if ui.add(btn).clicked() {
|
||||
self.config.accent_color = id.to_string();
|
||||
let _ = self.config.save();
|
||||
self.show_hud_notification("THEME ACCENT", &format!("Selected: {}", name), crate::overlay::ToastIcon::Info);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(gap);
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
|
@ -2560,7 +2594,7 @@ impl ScytheOverlayApp {
|
|||
let cur_status = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default();
|
||||
match cur_status {
|
||||
crate::updater::UpdateStatus::Idle => {
|
||||
if squared_button(ui, "Check for Updates", false, accent) {
|
||||
if squared_button_sized(ui, "Check for Updates", false, accent, Some(egui::vec2(130.0, 26.0))) {
|
||||
crate::updater::spawn_update_check(self.update_status.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -2579,7 +2613,7 @@ impl ScytheOverlayApp {
|
|||
.strong(),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
if squared_button(ui, "Check Again", false, accent) {
|
||||
if squared_button_sized(ui, "Check Again", false, accent, Some(egui::vec2(90.0, 26.0))) {
|
||||
crate::updater::spawn_update_check(self.update_status.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -2591,7 +2625,7 @@ impl ScytheOverlayApp {
|
|||
.strong(),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
if squared_button(ui, "VIEW RELEASE / DOWNLOAD", true, accent) {
|
||||
if squared_button_sized(ui, "VIEW RELEASE / DOWNLOAD", true, accent, Some(egui::vec2(180.0, 26.0))) {
|
||||
crate::updater::open_browser_url(&info.html_url);
|
||||
}
|
||||
}
|
||||
|
|
@ -2602,7 +2636,7 @@ impl ScytheOverlayApp {
|
|||
.color(Color32::from_rgb(239, 68, 68)),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
if squared_button(ui, "Retry", false, accent) {
|
||||
if squared_button_sized(ui, "Retry", false, accent, Some(egui::vec2(70.0, 26.0))) {
|
||||
crate::updater::spawn_update_check(self.update_status.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -2633,11 +2667,12 @@ impl ScytheOverlayApp {
|
|||
ui.label(egui::RichText::new("Directory to save screenshots:").size(12.0).color(Color32::WHITE));
|
||||
ui.add_space(3.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(ui.available_width() - 140.0));
|
||||
if squared_button(ui, "Change", false, accent) {
|
||||
let edit_w = (ui.available_width() - 152.0).max(120.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(edit_w));
|
||||
if squared_button_sized(ui, "Change", false, accent, Some(egui::vec2(70.0, 26.0))) {
|
||||
pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
|
||||
}
|
||||
if squared_button(ui, "Open", false, accent) {
|
||||
if squared_button_sized(ui, "Open", false, accent, Some(egui::vec2(60.0, 26.0))) {
|
||||
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
|
||||
}
|
||||
});
|
||||
|
|
@ -2673,6 +2708,9 @@ impl ScytheOverlayApp {
|
|||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
let needed_h = scroll_resp.content_size.y + (if has_view_toggle { 52.0 } else { 12.0 }) + 36.0;
|
||||
self.last_content_h = (self.last_content_h * 0.7 + needed_h * 0.3).clamp(360.0, max_content_h);
|
||||
});
|
||||
}
|
||||
fn render_gallery_view(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
|
|
@ -3335,13 +3373,8 @@ impl eframe::App for ScytheOverlayApp {
|
|||
}
|
||||
|
||||
self.frame_count += 1;
|
||||
// Auto-position, DWM transparency, and size to monitor on launch
|
||||
if !self.initial_pos_set && self.frame_count >= 2 {
|
||||
if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size)
|
||||
&& 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));
|
||||
}
|
||||
// Apply transparency and make visible smoothly on first frame
|
||||
if !self.initial_pos_set {
|
||||
#[cfg(target_os = "windows")]
|
||||
apply_windows_transparency("Scythe");
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
|
||||
|
|
@ -3552,20 +3585,11 @@ pub fn apply_windows_transparency(title: &str) {
|
|||
if let Some(proc) = GetProcAddress(user32, windows::core::s!("SetWindowCompositionAttribute")) {
|
||||
let set_wca: SetWindowCompositionAttributeFn = std::mem::transmute(proc);
|
||||
|
||||
let mut policy = if is_overlay {
|
||||
AccentPolicy {
|
||||
accent_state: 3, // ACCENT_ENABLE_BLURBEHIND
|
||||
accent_flags: 2,
|
||||
gradient_color: 0x99101014, // Translucent dark obsidian tint
|
||||
animation_id: 0,
|
||||
}
|
||||
} else {
|
||||
AccentPolicy {
|
||||
accent_state: 2, // ACCENT_ENABLE_TRANSPARENTGRADIENT
|
||||
accent_flags: 2,
|
||||
gradient_color: 0x00000000, // Fully transparent
|
||||
animation_id: 0,
|
||||
}
|
||||
let mut policy = AccentPolicy {
|
||||
accent_state: 2, // ACCENT_ENABLE_TRANSPARENTGRADIENT (true per-pixel alpha on Win 10 & 11)
|
||||
accent_flags: 2,
|
||||
gradient_color: 0x00000000, // Fully transparent backbuffer
|
||||
animation_id: 0,
|
||||
};
|
||||
|
||||
let mut data = WindowCompositionAttributeData {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue