fix(linux): fix DMA-BUF mmap offset, preserve screencast session, and verify replay clipping; bump v0.2.6
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-07 22:59:21 +05:30
parent 6be83b21e6
commit 1b696135c6
9 changed files with 114 additions and 67 deletions

2
Cargo.lock generated
View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "scythe"
version = "0.2.5"
version = "0.2.6"
edition = "2024"
[[bin]]

View file

@ -54,7 +54,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (frame_tx, frame_rx) = bounded::<Frame>(5);
let (cmd_tx, cmd_rx) = bounded::<Command>(32);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(4);
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(500);
let is_recording_state = Arc::new(AtomicBool::new(false));
@ -182,7 +182,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
}
};
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<dyn std::error::Error + Send + Sync>> {
#[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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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::<Packet>::new((config.replay_duration_sec * 120).max(120) as usize);
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec as usize) * (config.fps as usize + 60) + 120);
let mut normal_muxer: Option<Muxer> = None;
let mut normal_recording = false;
let mut normal_waiting_keyframe = false;
@ -433,10 +434,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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::<Packet>::new(new_capacity);
println!("Replay buffer resized to {} packets.", new_capacity);
@ -499,7 +506,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if config.replay_enabled {
let drain = ring.iter().cloned().collect::<Vec<_>>();
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<dyn std::error::Error + Send + Sync>> {
}
}
}
}
}
}
}
});

View file

@ -249,6 +249,13 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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" => {

View file

@ -42,6 +42,9 @@ pub enum Frame {
pub trait FrameSource: Send {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>>;
fn next_frame_timeout(&mut self, _timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.next_frame()
}
}
pub mod mock;

View file

@ -3,6 +3,8 @@ use ashpd::desktop::screencast::{CursorMode, Screencast, SourceType};
use ashpd::desktop::PersistMode;
pub struct WaylandCapture {
_proxy: Screencast,
_session: ashpd::desktop::Session<Screencast>,
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<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.stream.next_frame()
}
fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.stream.next_frame_timeout(timeout)
}
}

View file

@ -139,4 +139,8 @@ impl PipeWireStream {
pub fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.receiver.recv().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
pub fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.receiver.recv_timeout(timeout).map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
}

View file

@ -13,6 +13,8 @@ pub struct VaapiEncoder {
has_frame: bool,
#[cfg(unix)]
dma_mmap_cache: HashMap<i32, (*mut libc::c_void, usize)>,
#[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::<AVDRMFrameDescriptor>()) 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::<AVDRMFrameDescriptor>()) 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::<AVDRMFrameDescriptor>(),
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::<AVDRMFrameDescriptor>(),
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,

View file

@ -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;
}
});