fix(state,avhw): bind initial globals manually + fix filter graph crash on niri

- registry_queue_init consumes registry events during its internal
  roundtrip without forwarding them to Dispatch<WlRegistry>. Added
  bind_initial_globals() to manually iterate GlobalList and bind all
  initial globals (wl_output, xdg_output_manager, dmabuf, screencopy,
  wlr_output_manager) at State::new time.
- Fix av_freep segfault in build_filter_graph: av_buffersrc_parameters_alloc
  returns a plain pointer, use av_free instead of av_freep (which expects
  pointer-to-pointer).
- Fix filter graph format negotiation: remove software format filter that
  broke scale_vaapi hardware pipeline. Chain is now src -> scale -> sink.
- Downgrade repeat_pps error to warning (not available in FFmpeg 6.x).
This commit is contained in:
dailz
2026-04-14 20:42:05 +08:00
parent b2e5f37cf6
commit 506e5ea30e
2 changed files with 147 additions and 15 deletions

View File

@@ -163,8 +163,12 @@ impl EncState {
fps: u32,
transform: Transform,
) -> Result<Self> {
tracing::info!(
"EncState::new: {width}x{height} enc={enc_width}x{enc_height} transform={transform:?}"
);
// 1. VAAPI device
let hw_device_ctx = AvHwDevCtx::new_vaapi(drm_device)?;
tracing::debug!("EncState::new: VAAPI device created");
// 2. Frame contexts (capture=XRGB/RGBZ, encode=NV12)
// frames_rgb uses original capture dimensions (matches raw framebuffer)
@@ -209,8 +213,8 @@ impl EncState {
// SAFETY: Set repeat_pps=1 on the encoder so PPS is inserted in every encoded frame.
// This ensures decoders can start decoding from any frame (important for WebRTC).
// Note: h264_metadata BSF does NOT have repeat_sps/repeat_pps options in any FFmpeg version
// (verified on both 6.1.3 and 8.0). The encoder's own repeat_pps option is the correct approach.
// Note: repeat_pps is only available in FFmpeg 7.0+ (not in 6.x). On older FFmpeg,
// IDR frames carry SPS by default; PPS repetition depends on the driver.
// For SPS repetition: IDR frames carry SPS by default, controlled by gop_size/idr_interval.
{
let key = CString::new("repeat_pps").unwrap();
@@ -219,17 +223,20 @@ impl EncState {
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0)
};
if ret < 0 {
bail!("av_opt_set repeat_pps on encoder failed: error {ret}");
tracing::warn!("av_opt_set repeat_pps failed (error {ret}), likely FFmpeg < 7.0; continuing without per-frame PPS");
}
}
// 4. Open encoder. Video::open() returns Encoder(Video); .0 extracts the Video.
tracing::debug!("EncState::new: opening encoder...");
let opened = enc
.open()
.map_err(|e| anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}"))?;
let enc_video = opened.0;
tracing::debug!("EncState::new: encoder opened");
// 5. Filter graph (inline)
tracing::debug!("EncState::new: building filter graph...");
let video_filter = build_filter_graph(
&hw_device_ctx,
&frames_rgb,
@@ -474,11 +481,11 @@ fn build_filter_graph(
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
let buffersink = ff::filter::find("buffersink")
.ok_or_else(|| anyhow::anyhow!("filter 'buffersink' not found"))?;
let format_filter =
ff::filter::find("format").ok_or_else(|| anyhow::anyhow!("filter 'format' not found"))?;
let scale_vaapi = ff::filter::find("scale_vaapi")
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
tracing::debug!("filter_graph: filters found");
// buffersrc — use AVBufferSrcParameters to set hw_frames_ctx properly
let args = format!(
"video_size={}x{}:pix_fmt={}:time_base=1/{fps}:pixel_aspect=1/1",
@@ -487,6 +494,7 @@ fn build_filter_graph(
Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32,
);
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
tracing::debug!("filter_graph: buffersrc added");
// SAFETY: av_buffersrc_parameters_alloc allocates params for the buffersrc.
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
@@ -504,15 +512,12 @@ fn build_filter_graph(
};
(*par).hw_frames_ctx = frames_rgb.ref_clone();
let ret = ffi::av_buffersrc_parameters_set(src_ctx.as_mut_ptr(), par);
ffi::av_freep(par as *mut _ as *mut _);
ffi::av_free(par as *mut _);
if ret < 0 {
bail!("av_buffersrc_parameters_set failed: error {ret}");
}
}
// format filter: negotiate pixel format to NV12
let mut fmt_ctx = graph.add(&format_filter, "fmt", "pix_fmts=nv12")?;
// scale_vaapi: hardware scaling and colourspace conversion (keeps original dimensions)
let mut scale_ctx = graph.add(&scale_vaapi, "scale", &format!("{width}:{height}"))?;
// SAFETY: scale_vaapi needs hw_device_ctx for VAAPI device access.
@@ -522,9 +527,10 @@ fn build_filter_graph(
// buffersink
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
tracing::debug!("filter_graph: all filters added, linking...");
// Build filter chain: src -> format -> scale -> [transpose] -> sink
src_ctx.link(0, &mut fmt_ctx, 0);
// Build filter chain: src -> scale -> [transpose] -> sink
src_ctx.link(0, &mut scale_ctx, 0);
match transform {
Transform::Normal90 | Transform::Normal270 => {
@@ -540,7 +546,6 @@ fn build_filter_graph(
unsafe {
(*trans_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
}
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut trans_ctx, 0);
trans_ctx.link(0, &mut sink_ctx, 0);
}
@@ -548,11 +553,9 @@ fn build_filter_graph(
tracing::warn!(
"Normal180 transform detected; rotation correction deferred to follow-up"
);
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut sink_ctx, 0);
}
_ => {
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut sink_ctx, 0);
}
}
@@ -560,6 +563,7 @@ fn build_filter_graph(
graph
.validate()
.map_err(|e| anyhow::anyhow!("Filter graph validation failed: {e}"))?;
tracing::debug!("filter_graph: graph validated");
Ok(graph)
}

View File

@@ -225,7 +225,7 @@ impl<S: CaptureSource> State<S> {
pub fn new(gm: GlobalList, args: Args, qhandle: QueueHandle<State<S>>) -> Self {
let fps = args.fps;
let drm_device = args.drm_device.as_ref().map(PathBuf::from);
Self {
let mut state = Self {
stage: EncConstructionStage::ProbingOutputs {
outputs: Vec::new(),
bound_outputs: Vec::new(),
@@ -249,6 +249,134 @@ impl<S: CaptureSource> State<S> {
qhandle,
drm_device,
drm_device_from_compositor: None,
};
// registry_queue_init consumes registry events internally during its
// initial roundtrip and does NOT forward them to our Dispatch impl.
// We must manually bind the initial globals here.
state.bind_initial_globals();
state
}
/// Iterate over the GlobalList from registry_queue_init and bind all
/// globals we care about. This is necessary because registry_queue_init
/// consumes registry events during its internal roundtrip without forwarding
/// them to our Dispatch<WlRegistry> handler.
fn bind_initial_globals(&mut self) {
use wayland_client::globals::Global;
let globals: Vec<Global> = self.gm.contents().clone_list();
let registry = self.gm.registry();
let qhandle = &self.qhandle;
// Sort globals so that managers are bound BEFORE wl_output.
// This ensures xdg_output_manager and zwlr_output_manager are available
// when we bind wl_output, so we can immediately get xdg_output / wlr head.
let globals = {
fn priority(interface: &str) -> u8 {
match interface {
"zwlr_screencopy_manager_v1" => 0,
"zwp_linux_dmabuf_v1" => 0,
"zxdg_output_manager_v1" => 1,
"zwlr_output_manager_v1" => 1,
"wl_output" => 2,
_ => 3,
}
}
let mut g = globals;
g.sort_by_key(|g| priority(&g.interface));
g
};
for Global {
name,
interface,
version,
} in globals
{
match interface.as_str() {
"zwlr_screencopy_manager_v1" => {
let v = version.min(3);
tracing::debug!("Init: binding zwlr_screencopy_manager_v1 v{v} (name={name})");
let mgr: ZwlrScreencopyManagerV1 = registry.bind(name, v, qhandle, ());
if let EncConstructionStage::ProbingOutputs {
screencopy_manager, ..
} = &mut self.stage
{
*screencopy_manager = Some(mgr);
}
}
"zwp_linux_dmabuf_v1" => {
let v = version.min(4);
tracing::debug!("Init: binding zwp_linux_dmabuf_v1 v{v} (name={name})");
let proxy: ZwpLinuxDmabufV1 = registry.bind(name, v, qhandle, ());
if let EncConstructionStage::ProbingOutputs {
dmabuf,
dmabuf_feedback,
..
} = &mut self.stage
{
*dmabuf = Some(proxy.clone());
if v >= 4 {
let feedback = proxy.get_default_feedback(qhandle, ());
*dmabuf_feedback = Some(feedback);
}
}
}
"zxdg_output_manager_v1" => {
let v = version.min(3);
tracing::debug!("Init: binding zxdg_output_manager_v1 v{v} (name={name})");
let xdg_mgr: ZxdgOutputManagerV1 = registry.bind(name, v, qhandle, ());
if let EncConstructionStage::ProbingOutputs {
bound_outputs,
xdg_output_manager,
output_names,
..
} = &mut self.stage
{
for (i, output) in bound_outputs.iter().enumerate() {
let oname = output_names.get(i).copied().unwrap_or(0);
let output_id = OutputId(oname);
xdg_mgr.get_xdg_output(output, qhandle, output_id);
}
*xdg_output_manager = Some(xdg_mgr);
}
}
"zwlr_output_manager_v1" => {
let v = version.min(4);
tracing::debug!("Init: binding zwlr_output_manager_v1 v{v} (name={name})");
let mgr: ZwlrOutputManagerV1 = registry.bind(name, v, qhandle, ());
if let EncConstructionStage::ProbingOutputs {
wlr_output_manager, ..
} = &mut self.stage
{
*wlr_output_manager = Some(mgr);
}
}
"wl_output" => {
let v = version.min(4);
tracing::debug!("Init: binding wl_output v{v} (name={name})");
let output: WlOutput = registry.bind(name, v, qhandle, OutputId(name));
if let EncConstructionStage::ProbingOutputs {
outputs,
bound_outputs,
output_names,
xdg_output_manager,
..
} = &mut self.stage
{
outputs.push(PartialOutputInfo::default());
bound_outputs.push(output.clone());
output_names.push(name);
if let Some(xdg_mgr) = xdg_output_manager {
let output_id = OutputId(name);
xdg_mgr.get_xdg_output(&output, qhandle, output_id);
}
}
}
_ => {}
}
}
}