From 1b696135c64bd6540d3338346d9891f385d4d3e5 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Mon, 7 Sep 2026 22:59:21 +0530 Subject: [PATCH] fix(linux): fix DMA-BUF mmap offset, preserve screencast session, and verify replay clipping; bump v0.2.6 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/bin/scythe-daemon.rs | 37 ++++++++---- src/bin/scythe-ui.rs | 7 +++ src/capture/mod.rs | 3 + src/capture/wayland.rs | 8 ++- src/capture/wayland_stream.rs | 4 ++ src/encoder.rs | 108 +++++++++++++++++++--------------- src/overlay_egui.rs | 10 +++- 9 files changed, 114 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1bab06..693aa95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "scythe" -version = "0.2.5" +version = "0.2.6" dependencies = [ "ashpd", "cpal", diff --git a/Cargo.toml b/Cargo.toml index f9e4990..9584ad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scythe" -version = "0.2.5" +version = "0.2.6" edition = "2024" [[bin]] diff --git a/src/bin/scythe-daemon.rs b/src/bin/scythe-daemon.rs index 551af14..8421105 100644 --- a/src/bin/scythe-daemon.rs +++ b/src/bin/scythe-daemon.rs @@ -54,7 +54,7 @@ async fn main() -> Result<(), Box> { let (frame_tx, frame_rx) = bounded::(5); let (cmd_tx, cmd_rx) = bounded::(32); - let (mux_tx, mux_rx) = bounded::>(1); + let (mux_tx, mux_rx) = bounded::>(4); let (audio_tx, audio_rx) = bounded::>(500); let is_recording_state = Arc::new(AtomicBool::new(false)); @@ -182,7 +182,7 @@ async fn main() -> Result<(), Box> { std::process::exit(0); }, other => { - let _ = cmd_tx_ipc.try_send(other); + let _ = cmd_tx_ipc.send(other); } } } @@ -277,10 +277,9 @@ async fn main() -> Result<(), Box> { } }; - let first_frame = match source.next_frame() { + let first_frame = match source.next_frame_timeout(std::time::Duration::from_millis(600)) { Ok(f) => f, - Err(e) => { - eprintln!("Initial frame capture error: {}. Using default frame.", e); + Err(_) => { Frame::Raw { data: vec![0u8; 1920 * 1080 * 4], width: 1920, @@ -295,6 +294,7 @@ async fn main() -> Result<(), Box> { #[cfg(target_os = "windows")] Frame::D3D11Texture { width, height, .. } => (*width, *height), }; + println!("Capture stream active: resolution {}x{}", width, height); let _ = frame_tx.send(first_frame); @@ -329,6 +329,7 @@ async fn main() -> Result<(), Box> { let mut encoder = VideoEncoder::new_with_params(width, height, config.record_bitrate_kbps, config.fps, &config.video_codec) .expect("Failed to init encoder"); let codec_ctx_ptr = encoder.codec_ctx() as usize; + println!("Background recorder engine active and ready ({} fps, replay: {}).", config.fps, config.replay_enabled); let mut audio_encoder = if let Some((sr, ch)) = audio_info { scythe::encoder::AudioEncoder::new(sr as i32, ch as i32).ok() @@ -340,7 +341,7 @@ async fn main() -> Result<(), Box> { let video_time_base = unsafe { (*(codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext)).time_base }; let audio_time_base = audio_codec_ctx_ptr.map(|p| unsafe { (*(p as *mut ffmpeg_next::ffi::AVCodecContext)).time_base }); - let mut ring = HeapRb::::new((config.replay_duration_sec * 120).max(120) as usize); + let mut ring = HeapRb::::new((config.replay_duration_sec as usize) * (config.fps as usize + 60) + 120); let mut normal_muxer: Option = None; let mut normal_recording = false; let mut normal_waiting_keyframe = false; @@ -433,10 +434,16 @@ async fn main() -> Result<(), Box> { match unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } { Ok(mut muxer) => { for (_, p) in prepared { - let _ = muxer.write_packet(&p); + if let Err(e) = muxer.write_packet(&p) { + eprintln!("Error writing packet to replay muxer: {}", e); + break; + } + } + if let Err(e) = muxer.finalize() { + eprintln!("Error finalizing replay muxer: {}", e); + } else { + println!("Replay saved to {}!", full_path); } - let _ = muxer.finalize(); - println!("Replay saved to {}!", full_path); } Err(e) => { eprintln!("Failed to create muxer for {}: {}", full_path, e); @@ -470,7 +477,7 @@ async fn main() -> Result<(), Box> { replay_state_clone.store(config.replay_enabled, Ordering::SeqCst); audio_muted_clone.store(config.audio_mode == "muted", Ordering::SeqCst); println!("Daemon config reloaded! Audio mode: {}", config.audio_mode); - let new_capacity = (config.replay_duration_sec * 120).max(120) as usize; + let new_capacity = (config.replay_duration_sec as usize) * (config.fps as usize + 60) + 120; if ring.capacity().get() != new_capacity { ring = HeapRb::::new(new_capacity); println!("Replay buffer resized to {} packets.", new_capacity); @@ -499,7 +506,13 @@ async fn main() -> Result<(), Box> { if config.replay_enabled { let drain = ring.iter().cloned().collect::>(); println!("SaveReplay triggered: {} packets in ring buffer", drain.len()); - let _ = mux_tx.try_send(drain); + if drain.is_empty() { + eprintln!("Warning: Replay buffer is empty (no frames encoded yet)."); + } else { + let _ = mux_tx.send(drain); + } + } else { + println!("SaveReplay requested but replay is disabled in daemon config."); } }, Command::StartRecording => { @@ -663,7 +676,7 @@ async fn main() -> Result<(), Box> { } } } - } + } } } }); diff --git a/src/bin/scythe-ui.rs b/src/bin/scythe-ui.rs index 9e2b300..b20bca3 100644 --- a/src/bin/scythe-ui.rs +++ b/src/bin/scythe-ui.rs @@ -249,6 +249,13 @@ fn main() -> Result<(), Box> { if args.len() > 1 { match args[1].as_str() { "--save" => { + ensure_daemon_running(); + if let Ok(st) = query_status() { + if !st.is_replay_active { + show_shadowplay_toast("INSTANT REPLAY", "Replay is turned off", ToastIcon::Error); + return Ok(()); + } + } return send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); } "--notify-save" => { diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 6dfc390..95f9540 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -42,6 +42,9 @@ pub enum Frame { pub trait FrameSource: Send { fn next_frame(&mut self) -> Result>; + fn next_frame_timeout(&mut self, _timeout: std::time::Duration) -> Result> { + self.next_frame() + } } pub mod mock; diff --git a/src/capture/wayland.rs b/src/capture/wayland.rs index d55db1e..6c8d759 100644 --- a/src/capture/wayland.rs +++ b/src/capture/wayland.rs @@ -3,6 +3,8 @@ use ashpd::desktop::screencast::{CursorMode, Screencast, SourceType}; use ashpd::desktop::PersistMode; pub struct WaylandCapture { + _proxy: Screencast, + _session: ashpd::desktop::Session, stream: crate::capture::wayland_stream::PipeWireStream, } @@ -57,7 +59,7 @@ impl WaylandCapture { let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?; - Ok(Self { stream }) + Ok(Self { _proxy: proxy, _session: session, stream }) } } @@ -65,4 +67,8 @@ impl FrameSource for WaylandCapture { fn next_frame(&mut self) -> Result> { self.stream.next_frame() } + + fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result> { + self.stream.next_frame_timeout(timeout) + } } diff --git a/src/capture/wayland_stream.rs b/src/capture/wayland_stream.rs index 1784b66..39532ba 100644 --- a/src/capture/wayland_stream.rs +++ b/src/capture/wayland_stream.rs @@ -139,4 +139,8 @@ impl PipeWireStream { pub fn next_frame(&mut self) -> Result> { self.receiver.recv().map_err(|e| Box::new(e) as Box) } + + pub fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result> { + self.receiver.recv_timeout(timeout).map_err(|e| Box::new(e) as Box) + } } diff --git a/src/encoder.rs b/src/encoder.rs index 56374c7..8b3828d 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -13,6 +13,8 @@ pub struct VaapiEncoder { has_frame: bool, #[cfg(unix)] dma_mmap_cache: HashMap, + #[cfg(unix)] + direct_drm_unsupported: bool, } impl VaapiEncoder { @@ -149,6 +151,8 @@ impl VaapiEncoder { has_frame: false, #[cfg(unix)] dma_mmap_cache: HashMap::new(), + #[cfg(unix)] + direct_drm_unsupported: false, }) } } @@ -207,63 +211,68 @@ impl VaapiEncoder { self.send_staged_frame(&mut packets, pts)?; } Frame::DmaBuf { width: dma_width, height: dma_height, format, modifier, fd, stride, offset } => { - let mmap_size = (*stride as usize) * (*dma_height as usize); + let mmap_size = (*offset as usize) + (*stride as usize) * (*dma_height as usize); let mut mapped_direct = false; - let desc = av_malloc(std::mem::size_of::()) as *mut AVDRMFrameDescriptor; - if !desc.is_null() { - *desc = AVDRMFrameDescriptor { - nb_objects: 1, - objects: [AVDRMObjectDescriptor { - fd: *fd, - size: mmap_size, - format_modifier: *modifier, - }, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()], - nb_layers: 1, - layers: [AVDRMLayerDescriptor { - format: *format, - nb_planes: 1, - planes: [AVDRMPlaneDescriptor { - object_index: 0, - offset: *offset as isize, - pitch: *stride as isize, + #[cfg(unix)] + if !self.direct_drm_unsupported { + let desc = av_malloc(std::mem::size_of::()) as *mut AVDRMFrameDescriptor; + if !desc.is_null() { + *desc = AVDRMFrameDescriptor { + nb_objects: 1, + objects: [AVDRMObjectDescriptor { + fd: *fd, + size: mmap_size, + format_modifier: *modifier, }, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()], - }, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()], - }; + nb_layers: 1, + layers: [AVDRMLayerDescriptor { + format: *format, + nb_planes: 1, + planes: [AVDRMPlaneDescriptor { + object_index: 0, + offset: *offset as isize, + pitch: *stride as isize, + }, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()], + }, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()], + }; - let mut drm_frame = av_frame_alloc(); - (*drm_frame).format = AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; - (*drm_frame).width = *dma_width as i32; - (*drm_frame).height = *dma_height as i32; - (*drm_frame).data[0] = desc as *mut u8; - (*drm_frame).buf[0] = av_buffer_create( - desc as *mut u8, - std::mem::size_of::(), - None, - ptr::null_mut(), - 0, - ); + let mut drm_frame = av_frame_alloc(); + (*drm_frame).format = AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; + (*drm_frame).width = *dma_width as i32; + (*drm_frame).height = *dma_height as i32; + (*drm_frame).data[0] = desc as *mut u8; + (*drm_frame).buf[0] = av_buffer_create( + desc as *mut u8, + std::mem::size_of::(), + None, + ptr::null_mut(), + 0, + ); - let mut direct_hw = av_frame_alloc(); - (*direct_hw).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32; - (*direct_hw).hw_frames_ctx = av_buffer_ref(self.hw_frames_ctx); + let mut direct_hw = av_frame_alloc(); + (*direct_hw).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32; + (*direct_hw).hw_frames_ctx = av_buffer_ref(self.hw_frames_ctx); - if av_hwframe_map(direct_hw, drm_frame, 0) >= 0 { - (*direct_hw).pts = pts; + if av_hwframe_map(direct_hw, drm_frame, 0) >= 0 { + (*direct_hw).pts = pts; - if avcodec_send_frame(self.codec_ctx, direct_hw) >= 0 { - let mut pkt = av_packet_alloc(); - while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 { - let new_pkt = av_packet_alloc(); - av_packet_move_ref(new_pkt, pkt); - packets.push(crate::ring::Packet::new(new_pkt)); + if avcodec_send_frame(self.codec_ctx, direct_hw) >= 0 { + let mut pkt = av_packet_alloc(); + while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 { + let new_pkt = av_packet_alloc(); + av_packet_move_ref(new_pkt, pkt); + packets.push(crate::ring::Packet::new(new_pkt)); + } + av_packet_free(&mut pkt); + mapped_direct = true; } - av_packet_free(&mut pkt); - mapped_direct = true; + } else { + self.direct_drm_unsupported = true; } + av_frame_free(&mut direct_hw); + av_frame_free(&mut drm_frame); } - av_frame_free(&mut direct_hw); - av_frame_free(&mut drm_frame); } if !mapped_direct { @@ -278,7 +287,7 @@ impl VaapiEncoder { libc::PROT_READ, libc::MAP_SHARED, *fd, - *offset as libc::off_t, + 0, ); if p != libc::MAP_FAILED { if self.dma_mmap_cache.len() >= 16 { @@ -295,7 +304,8 @@ impl VaapiEncoder { }; if ptr != libc::MAP_FAILED { - let src_slices: [*const u8; 4] = [ptr as *const u8, ptr::null(), ptr::null(), ptr::null()]; + let frame_ptr = (ptr as *const u8).add(*offset as usize); + let src_slices: [*const u8; 4] = [frame_ptr, ptr::null(), ptr::null(), ptr::null()]; let src_strides: [i32; 4] = [*stride as i32, 0, 0, 0]; sws_scale( self.sws_ctx, diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index d894e91..5a5fc14 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -1027,9 +1027,13 @@ impl ScytheOverlayApp { self.status.is_replay_active = cfg.replay_enabled; self.replay_dropdown_open = false; } - if render_menu_item(ui, "Save Replay", accent, true) { - async_send_command(Command::SaveReplay); - self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay); + if render_menu_item(ui, "Save Replay", accent, is_replay_active) { + if is_replay_active { + async_send_command(Command::SaveReplay); + self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay); + } else { + self.show_hud_notification("INSTANT REPLAY", "Replay is turned off", crate::overlay::ToastIcon::Error); + } self.replay_dropdown_open = false; } });