test: add the fake gamescope producer and the end-to-end scripts
fake_gamescope mirrors gamescope 3.16.28's src/pipewire.cpp closely enough to exercise the requested_size negotiation, MemFd buffers and CORRUPTED frames. e2e-fake.sh runs it against the client and the mock in the current PipeWire session; nested-gamescope.sh does the same with a real nested gamescope and compares the load with the old gstreamer pipeline.
This commit is contained in:
@@ -0,0 +1,740 @@
|
||||
//! Imitates gamescope's PipeWire producer for local end-to-end tests (spec 9.3).
|
||||
//!
|
||||
//! Mirrors `src/pipewire.cpp` of gamescope 3.16.28: node properties, the four EnumFormat
|
||||
//! objects with the custom `requested_size`/`focus_appid` keys, `calculate_capture_size`,
|
||||
//! MemFd buffers, CORRUPTED marking of buffers whose size does not match the render size,
|
||||
//! and renegotiation of the format when the capture size changes.
|
||||
//!
|
||||
//! cargo run --release --example fake_gamescope -- --name gamescope --width 3840 --height 2160
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::{Cursor, IsTerminal};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use gamescope_hyperhdr::capture::{
|
||||
requested_size_of, SPA_FORMAT_VIDEO_GAMESCOPE_FOCUS_APPID, SPA_FORMAT_VIDEO_REQUESTED_SIZE,
|
||||
SPA_META_REQUESTED_SIZE_SCALE,
|
||||
};
|
||||
use pipewire as pw;
|
||||
use pw::properties::properties;
|
||||
use pw::spa;
|
||||
use pw::stream::{Stream, StreamFlags, StreamRc, StreamState};
|
||||
use spa::param::format::{FormatProperties, MediaSubtype, MediaType};
|
||||
use spa::param::video::{VideoFormat, VideoInfoRaw};
|
||||
use spa::param::ParamType;
|
||||
use spa::pod::deserialize::PodDeserializer;
|
||||
use spa::pod::serialize::PodSerializer;
|
||||
use spa::pod::{ChoiceValue, Object, Pod, Property, PropertyFlags, Value};
|
||||
use spa::sys as spa_sys;
|
||||
use spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Id, Rectangle, SpaTypes};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
const DRM_FORMAT_MOD_LINEAR: i64 = 0;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Fake gamescope PipeWire producer")]
|
||||
struct Opt {
|
||||
/// node.name of the stream
|
||||
#[arg(long, default_value = "gamescope")]
|
||||
name: String,
|
||||
/// Output (display) width
|
||||
#[arg(long, default_value_t = 3840)]
|
||||
width: u32,
|
||||
/// Output (display) height
|
||||
#[arg(long, default_value_t = 2160)]
|
||||
height: u32,
|
||||
/// Frames per second while streaming
|
||||
#[arg(long, default_value_t = 60)]
|
||||
fps: u32,
|
||||
/// Stop producing frames after the first few correctly sized ones (static picture)
|
||||
#[arg(long = "static")]
|
||||
static_picture: bool,
|
||||
/// Ignore requested_size, like a gamescope without the extension
|
||||
#[arg(long)]
|
||||
ignore_requested: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Dim {
|
||||
w: u32,
|
||||
h: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Negotiated {
|
||||
format: VideoFormat,
|
||||
size: Dim,
|
||||
stride: u32,
|
||||
dmabuf: bool,
|
||||
}
|
||||
|
||||
impl Negotiated {
|
||||
/// `stride * height`, plus the chroma plane for NV12.
|
||||
fn byte_size(&self, height: u32) -> u32 {
|
||||
let mut size = self.stride * height;
|
||||
if self.format == VideoFormat::NV12 {
|
||||
size += self.stride * height.div_ceil(2);
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
struct Fake {
|
||||
output: Dim,
|
||||
requested: Dim,
|
||||
capture: Dim,
|
||||
format: Option<Negotiated>,
|
||||
streaming: bool,
|
||||
seq: u64,
|
||||
frame_no: u64,
|
||||
good: u64,
|
||||
corrupted: u64,
|
||||
out_of_buffers: u64,
|
||||
static_picture: bool,
|
||||
ignore_requested: bool,
|
||||
}
|
||||
|
||||
impl Fake {
|
||||
/// gamescope's `calculate_capture_size`, including its float math.
|
||||
fn calculate_capture_size(&mut self) {
|
||||
let (ow, oh) = (self.output.w, self.output.h);
|
||||
let (rw, rh) = (self.requested.w, self.requested.h);
|
||||
let (mut cw, mut ch) = (ow, oh);
|
||||
if rw > 0 && rh > 0 && (ow > rw || oh > rh) {
|
||||
let ratio_w = rw as f32 / ow as f32;
|
||||
let ratio_h = rh as f32 / oh as f32;
|
||||
if ratio_w <= ratio_h {
|
||||
cw = rw;
|
||||
ch = (ratio_w * oh as f32).ceil() as u32;
|
||||
} else {
|
||||
cw = (ratio_h * ow as f32).ceil() as u32;
|
||||
ch = rh;
|
||||
}
|
||||
}
|
||||
self.capture = Dim {
|
||||
w: cw.next_multiple_of(2),
|
||||
h: ch.next_multiple_of(2),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A MemFd buffer; `texture` is the size frames are rendered at (gamescope's CVulkanTexture).
|
||||
struct ShmBuffer {
|
||||
fd: i32,
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
video: Negotiated,
|
||||
texture: Dim,
|
||||
}
|
||||
|
||||
fn serialize(object: Object) -> Vec<u8> {
|
||||
PodSerializer::serialize(Cursor::new(Vec::new()), &Value::Object(object))
|
||||
.expect("serializing into a Vec cannot fail")
|
||||
.0
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
fn id_enum(default: u32, alternatives: &[u32]) -> Value {
|
||||
Value::Choice(ChoiceValue::Id(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Enum {
|
||||
default: Id(default),
|
||||
alternatives: alternatives.iter().map(|v| Id(*v)).collect(),
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
/// gamescope's `build_format_params`: BGRx and NV12, each with and without a modifier.
|
||||
fn build_format_params(capture: Dim) -> Vec<Vec<u8>> {
|
||||
let mut params = Vec::new();
|
||||
for format in [VideoFormat::BGRx, VideoFormat::NV12] {
|
||||
for with_modifier in [true, false] {
|
||||
let mut properties = vec![
|
||||
Property::new(
|
||||
FormatProperties::MediaType.as_raw(),
|
||||
Value::Id(Id(MediaType::Video.as_raw())),
|
||||
),
|
||||
Property::new(
|
||||
FormatProperties::MediaSubtype.as_raw(),
|
||||
Value::Id(Id(MediaSubtype::Raw.as_raw())),
|
||||
),
|
||||
Property::new(
|
||||
FormatProperties::VideoFormat.as_raw(),
|
||||
Value::Id(Id(format.as_raw())),
|
||||
),
|
||||
Property::new(
|
||||
FormatProperties::VideoSize.as_raw(),
|
||||
Value::Rectangle(Rectangle {
|
||||
width: capture.w,
|
||||
height: capture.h,
|
||||
}),
|
||||
),
|
||||
Property::new(
|
||||
FormatProperties::VideoFramerate.as_raw(),
|
||||
Value::Fraction(Fraction { num: 0, denom: 1 }),
|
||||
),
|
||||
Property::new(
|
||||
SPA_FORMAT_VIDEO_REQUESTED_SIZE,
|
||||
Value::Choice(ChoiceValue::Rectangle(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: Rectangle {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
min: Rectangle {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
max: Rectangle {
|
||||
width: u32::MAX,
|
||||
height: u32::MAX,
|
||||
},
|
||||
},
|
||||
))),
|
||||
),
|
||||
Property::new(
|
||||
SPA_FORMAT_VIDEO_GAMESCOPE_FOCUS_APPID,
|
||||
Value::Choice(ChoiceValue::Long(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 0,
|
||||
min: i64::MIN,
|
||||
max: i64::MAX,
|
||||
},
|
||||
))),
|
||||
),
|
||||
];
|
||||
if format == VideoFormat::NV12 {
|
||||
properties.push(Property::new(
|
||||
FormatProperties::VideoColorMatrix.as_raw(),
|
||||
id_enum(
|
||||
spa_sys::SPA_VIDEO_COLOR_MATRIX_BT601,
|
||||
&[
|
||||
spa_sys::SPA_VIDEO_COLOR_MATRIX_BT601,
|
||||
spa_sys::SPA_VIDEO_COLOR_MATRIX_BT709,
|
||||
],
|
||||
),
|
||||
));
|
||||
properties.push(Property::new(
|
||||
FormatProperties::VideoColorRange.as_raw(),
|
||||
id_enum(
|
||||
spa_sys::SPA_VIDEO_COLOR_RANGE_16_235,
|
||||
&[
|
||||
spa_sys::SPA_VIDEO_COLOR_RANGE_16_235,
|
||||
spa_sys::SPA_VIDEO_COLOR_RANGE_0_255,
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
if with_modifier {
|
||||
properties.push(Property {
|
||||
key: FormatProperties::VideoModifier.as_raw(),
|
||||
flags: PropertyFlags::MANDATORY,
|
||||
value: Value::Choice(ChoiceValue::Long(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Enum {
|
||||
default: DRM_FORMAT_MOD_LINEAR,
|
||||
alternatives: vec![DRM_FORMAT_MOD_LINEAR],
|
||||
},
|
||||
))),
|
||||
});
|
||||
}
|
||||
params.push(serialize(Object {
|
||||
type_: SpaTypes::ObjectParamFormat.as_raw(),
|
||||
id: ParamType::EnumFormat.as_raw(),
|
||||
properties,
|
||||
}));
|
||||
}
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
fn meta_param(meta_type: u32, size: usize) -> Vec<u8> {
|
||||
serialize(Object {
|
||||
type_: SpaTypes::ObjectParamMeta.as_raw(),
|
||||
id: ParamType::Meta.as_raw(),
|
||||
properties: vec![
|
||||
Property::new(spa_sys::SPA_PARAM_META_type, Value::Id(Id(meta_type))),
|
||||
Property::new(spa_sys::SPA_PARAM_META_size, Value::Int(size as i32)),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Buffers and Meta params set by `stream_handle_param_changed`.
|
||||
fn buffer_params(n: &Negotiated) -> Vec<Vec<u8>> {
|
||||
let data_type = if n.dmabuf {
|
||||
1 << spa_sys::SPA_DATA_DmaBuf
|
||||
} else {
|
||||
1 << spa_sys::SPA_DATA_MemFd
|
||||
};
|
||||
let buffers = serialize(Object {
|
||||
type_: SpaTypes::ObjectParamBuffers.as_raw(),
|
||||
id: ParamType::Buffers.as_raw(),
|
||||
properties: vec![
|
||||
Property::new(
|
||||
spa_sys::SPA_PARAM_BUFFERS_buffers,
|
||||
Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 4,
|
||||
min: 1,
|
||||
max: 8,
|
||||
},
|
||||
))),
|
||||
),
|
||||
Property::new(spa_sys::SPA_PARAM_BUFFERS_blocks, Value::Int(1)),
|
||||
Property::new(
|
||||
spa_sys::SPA_PARAM_BUFFERS_size,
|
||||
Value::Int(n.byte_size(n.size.h) as i32),
|
||||
),
|
||||
Property::new(
|
||||
spa_sys::SPA_PARAM_BUFFERS_stride,
|
||||
Value::Int(n.stride as i32),
|
||||
),
|
||||
Property::new(
|
||||
spa_sys::SPA_PARAM_BUFFERS_dataType,
|
||||
Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Flags {
|
||||
default: data_type,
|
||||
flags: vec![],
|
||||
},
|
||||
))),
|
||||
),
|
||||
],
|
||||
});
|
||||
vec![
|
||||
buffers,
|
||||
meta_param(
|
||||
spa_sys::SPA_META_Header,
|
||||
size_of::<spa_sys::spa_meta_header>(),
|
||||
),
|
||||
meta_param(SPA_META_REQUESTED_SIZE_SCALE, size_of::<f32>()),
|
||||
]
|
||||
}
|
||||
|
||||
fn update_params(stream: &Stream, pods: &[Vec<u8>]) {
|
||||
let mut params: Vec<&Pod> = pods
|
||||
.iter()
|
||||
.map(|p| Pod::from_bytes(p).expect("valid pod"))
|
||||
.collect();
|
||||
if let Err(e) = stream.update_params(&mut params) {
|
||||
error!("pw_stream_update_params failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn has_modifier(format: &Pod) -> bool {
|
||||
matches!(
|
||||
PodDeserializer::deserialize_any_from(format.as_bytes()),
|
||||
Ok((_, Value::Object(o))) if o.properties.iter().any(|p| p.key == FormatProperties::VideoModifier.as_raw())
|
||||
)
|
||||
}
|
||||
|
||||
fn on_param_changed(stream: &Stream, fake: &RefCell<Fake>, id: u32, param: Option<&Pod>) {
|
||||
if id != ParamType::Format.as_raw() {
|
||||
return;
|
||||
}
|
||||
let Some(param) = param else {
|
||||
return;
|
||||
};
|
||||
let mut info = VideoInfoRaw::new();
|
||||
if info.parse(param).is_err() {
|
||||
error!("spa_format_video_raw_parse failed");
|
||||
return;
|
||||
}
|
||||
let requested = requested_size_of(param).map_or(Dim { w: 0, h: 0 }, |r| Dim {
|
||||
w: r.width,
|
||||
h: r.height,
|
||||
});
|
||||
let negotiated = {
|
||||
let mut f = fake.borrow_mut();
|
||||
f.requested = if f.ignore_requested {
|
||||
Dim { w: 0, h: 0 }
|
||||
} else {
|
||||
requested
|
||||
};
|
||||
f.calculate_capture_size();
|
||||
let size = Dim {
|
||||
w: info.size().width,
|
||||
h: info.size().height,
|
||||
};
|
||||
let bpp = if info.format() == VideoFormat::NV12 {
|
||||
1
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let negotiated = Negotiated {
|
||||
format: info.format(),
|
||||
size,
|
||||
stride: (size.w * bpp).next_multiple_of(4),
|
||||
dmabuf: has_modifier(param),
|
||||
};
|
||||
f.format = Some(negotiated);
|
||||
info!(
|
||||
"format changed (size {}x{}, requested {}x{}, capture {}x{}, format {:?}, stride {}, dmabuf {})",
|
||||
size.w,
|
||||
size.h,
|
||||
requested.w,
|
||||
requested.h,
|
||||
f.capture.w,
|
||||
f.capture.h,
|
||||
negotiated.format,
|
||||
negotiated.stride,
|
||||
negotiated.dmabuf
|
||||
);
|
||||
negotiated
|
||||
};
|
||||
update_params(stream, &buffer_params(&negotiated));
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `pw_buffer` must be the buffer passed to the `add_buffer` event.
|
||||
unsafe fn on_add_buffer(fake: &Fake, pw_buffer: *mut pw::sys::pw_buffer) {
|
||||
let spa_buffer = (*pw_buffer).buffer;
|
||||
if spa_buffer.is_null() || (*spa_buffer).n_datas < 1 {
|
||||
return;
|
||||
}
|
||||
let data = &mut *(*spa_buffer).datas;
|
||||
let Some(n) = fake.format else {
|
||||
error!("add_buffer without a format");
|
||||
return;
|
||||
};
|
||||
if data.type_ & (1 << spa_sys::SPA_DATA_MemFd) == 0 {
|
||||
error!(
|
||||
"unsupported data types {:#x}: the fake only implements MemFd",
|
||||
data.type_
|
||||
);
|
||||
data.type_ = spa_sys::SPA_DATA_Invalid;
|
||||
return;
|
||||
}
|
||||
let len = n.byte_size(n.size.h) as usize;
|
||||
let fd = libc::memfd_create(c"gamescope-fake-pw".as_ptr(), libc::MFD_CLOEXEC);
|
||||
if fd < 0 {
|
||||
error!("memfd_create failed");
|
||||
return;
|
||||
}
|
||||
if libc::ftruncate(fd, len as libc::off_t) != 0 {
|
||||
error!("ftruncate failed");
|
||||
libc::close(fd);
|
||||
return;
|
||||
}
|
||||
let ptr = libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
len,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
if ptr == libc::MAP_FAILED {
|
||||
error!("mmap failed");
|
||||
libc::close(fd);
|
||||
return;
|
||||
}
|
||||
data.type_ = spa_sys::SPA_DATA_MemFd;
|
||||
data.flags = spa_sys::SPA_DATA_FLAG_READABLE;
|
||||
data.fd = i64::from(fd);
|
||||
data.mapoffset = 0;
|
||||
data.maxsize = len as u32;
|
||||
data.data = ptr;
|
||||
debug!(
|
||||
len,
|
||||
"added buffer, texture {}x{}", fake.capture.w, fake.capture.h
|
||||
);
|
||||
|
||||
let buffer = Box::new(ShmBuffer {
|
||||
fd,
|
||||
ptr: ptr.cast(),
|
||||
len,
|
||||
video: n,
|
||||
texture: fake.capture,
|
||||
});
|
||||
(*pw_buffer).user_data = Box::into_raw(buffer).cast();
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `pw_buffer` must be the buffer passed to the `remove_buffer` event.
|
||||
unsafe fn on_remove_buffer(pw_buffer: *mut pw::sys::pw_buffer) {
|
||||
let ptr = (*pw_buffer).user_data.cast::<ShmBuffer>();
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
(*pw_buffer).user_data = std::ptr::null_mut();
|
||||
let buffer = Box::from_raw(ptr);
|
||||
libc::munmap(buffer.ptr.cast(), buffer.len);
|
||||
libc::close(buffer.fd);
|
||||
}
|
||||
|
||||
/// RGB colour bars that move by one bar every 8 frames.
|
||||
fn render(buffer: &mut ShmBuffer, frame_no: u64) {
|
||||
const BARS: [[u8; 3]; 8] = [
|
||||
[235, 235, 235],
|
||||
[235, 235, 16],
|
||||
[16, 235, 235],
|
||||
[16, 235, 16],
|
||||
[235, 16, 235],
|
||||
[235, 16, 16],
|
||||
[16, 16, 235],
|
||||
[16, 16, 16],
|
||||
];
|
||||
// SAFETY: `ptr` maps `len` bytes for the lifetime of the buffer.
|
||||
let mem = unsafe { std::slice::from_raw_parts_mut(buffer.ptr, buffer.len) };
|
||||
let (w, h) = (buffer.texture.w as usize, buffer.texture.h as usize);
|
||||
let stride = buffer.video.stride as usize;
|
||||
if buffer.video.format == VideoFormat::NV12 {
|
||||
mem.fill(128);
|
||||
return;
|
||||
}
|
||||
let shift = (frame_no / 8) as usize;
|
||||
for x in 0..w {
|
||||
let [r, g, b] = BARS[(x * BARS.len() / w + shift) % BARS.len()];
|
||||
mem[x * 4..x * 4 + 4].copy_from_slice(&[b, g, r, 0xFF]);
|
||||
}
|
||||
for y in 1..h {
|
||||
mem.copy_within(0..w * 4, y * stride);
|
||||
}
|
||||
}
|
||||
|
||||
/// gamescope's `paint_pipewire` + `dispatch_nudge` + `copy_buffer`, once per frame.
|
||||
fn tick(stream: &Stream, fake: &RefCell<Fake>) {
|
||||
let renegotiate = {
|
||||
let f = fake.borrow();
|
||||
if !f.streaming {
|
||||
return;
|
||||
}
|
||||
let current = f.format.map_or(Dim { w: 0, h: 0 }, |n| n.size);
|
||||
(f.capture != current).then(|| {
|
||||
debug!(
|
||||
"renegotiating stream params (size: {}x{})",
|
||||
f.capture.w, f.capture.h
|
||||
);
|
||||
build_format_params(f.capture)
|
||||
})
|
||||
};
|
||||
if let Some(pods) = renegotiate {
|
||||
update_params(stream, &pods);
|
||||
}
|
||||
|
||||
let mut f = fake.borrow_mut();
|
||||
if f.static_picture && f.good >= 5 {
|
||||
return;
|
||||
}
|
||||
// SAFETY: the buffer is queued back below and only accessed in between.
|
||||
unsafe {
|
||||
let raw = stream.dequeue_raw_buffer();
|
||||
if raw.is_null() {
|
||||
f.out_of_buffers += 1;
|
||||
if f.out_of_buffers.is_power_of_two() {
|
||||
warn!("out of buffers ({} times)", f.out_of_buffers);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let shm = (*raw).user_data.cast::<ShmBuffer>();
|
||||
if shm.is_null() {
|
||||
stream.queue_raw_buffer(raw);
|
||||
return;
|
||||
}
|
||||
let shm = &mut *shm;
|
||||
let spa_buffer = (*raw).buffer;
|
||||
let needs_reneg = shm.video.size != shm.texture;
|
||||
|
||||
let header = spa_sys::spa_buffer_find_meta_data(
|
||||
spa_buffer,
|
||||
spa_sys::SPA_META_Header,
|
||||
size_of::<spa_sys::spa_meta_header>(),
|
||||
)
|
||||
.cast::<spa_sys::spa_meta_header>();
|
||||
if !header.is_null() {
|
||||
(*header).pts = -1;
|
||||
(*header).flags = if needs_reneg {
|
||||
spa_sys::SPA_META_HEADER_FLAG_CORRUPTED
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(*header).seq = f.seq;
|
||||
(*header).dts_offset = 0;
|
||||
}
|
||||
f.seq += 1;
|
||||
let scale = spa_sys::spa_buffer_find_meta_data(
|
||||
spa_buffer,
|
||||
SPA_META_REQUESTED_SIZE_SCALE,
|
||||
size_of::<f32>(),
|
||||
)
|
||||
.cast::<f32>();
|
||||
if !scale.is_null() {
|
||||
*scale = shm.texture.w as f32 / f.output.w as f32;
|
||||
}
|
||||
|
||||
let data = &mut *(*spa_buffer).datas;
|
||||
let chunk = &mut *data.chunk;
|
||||
chunk.flags = if needs_reneg {
|
||||
spa_sys::SPA_CHUNK_FLAG_CORRUPTED as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
// Like gamescope, the chunk size follows the current format, the stride the buffer.
|
||||
let current = f.format.unwrap_or(shm.video);
|
||||
chunk.offset = 0;
|
||||
chunk.size = Negotiated {
|
||||
stride: shm.video.stride,
|
||||
..current
|
||||
}
|
||||
.byte_size(current.size.h);
|
||||
chunk.stride = shm.video.stride as i32;
|
||||
|
||||
if needs_reneg {
|
||||
f.corrupted += 1;
|
||||
} else {
|
||||
render(shm, f.frame_no);
|
||||
f.good += 1;
|
||||
}
|
||||
f.frame_no += 1;
|
||||
drop(f);
|
||||
stream.queue_raw_buffer(raw);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let opt = Opt::parse();
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(false)
|
||||
.with_writer(std::io::stderr)
|
||||
.with_ansi(std::io::stderr().is_terminal())
|
||||
.init();
|
||||
|
||||
pw::init();
|
||||
let mainloop = pw::main_loop::MainLoopRc::new(None)?;
|
||||
let context = pw::context::ContextRc::new(&mainloop, None)?;
|
||||
let core = context.connect_rc(None)?;
|
||||
|
||||
let _signals = [pw::loop_::Signal::INT, pw::loop_::Signal::TERM].map(|signal| {
|
||||
let mainloop_weak = mainloop.downgrade();
|
||||
mainloop.loop_().add_signal_local(signal, move || {
|
||||
if let Some(mainloop) = mainloop_weak.upgrade() {
|
||||
mainloop.quit();
|
||||
}
|
||||
})
|
||||
});
|
||||
let _core_listener = {
|
||||
let mainloop_weak = mainloop.downgrade();
|
||||
core.add_listener_local()
|
||||
.error(move |id, _, res, message| {
|
||||
error!(id, res, "PipeWire error: {message}");
|
||||
if id == pw::sys::PW_ID_CORE {
|
||||
if let Some(mainloop) = mainloop_weak.upgrade() {
|
||||
mainloop.quit();
|
||||
}
|
||||
}
|
||||
})
|
||||
.register()
|
||||
};
|
||||
|
||||
let fake = Rc::new(RefCell::new(Fake {
|
||||
output: Dim {
|
||||
w: opt.width,
|
||||
h: opt.height,
|
||||
},
|
||||
requested: Dim { w: 0, h: 0 },
|
||||
capture: Dim { w: 0, h: 0 },
|
||||
format: None,
|
||||
streaming: false,
|
||||
seq: 0,
|
||||
frame_no: 0,
|
||||
good: 0,
|
||||
corrupted: 0,
|
||||
out_of_buffers: 0,
|
||||
static_picture: opt.static_picture,
|
||||
ignore_requested: opt.ignore_requested,
|
||||
}));
|
||||
fake.borrow_mut().calculate_capture_size();
|
||||
|
||||
// gamescope passes only media.class. With PipeWire 1.6 the stream name becomes media.name
|
||||
// and node.name defaults to the program name, which is "gamescope" for the real binary,
|
||||
// so the fake sets node.name explicitly.
|
||||
let stream = StreamRc::new(
|
||||
core.clone(),
|
||||
&opt.name,
|
||||
properties! {
|
||||
*pw::keys::MEDIA_CLASS => "Video/Source",
|
||||
*pw::keys::NODE_NAME => opt.name.as_str(),
|
||||
},
|
||||
)?;
|
||||
let _listener = {
|
||||
let mainloop_weak = mainloop.downgrade();
|
||||
stream
|
||||
.add_local_listener_with_user_data(fake.clone())
|
||||
.state_changed(move |stream, fake, old, new| {
|
||||
info!("stream state changed: {old:?} -> {new:?}");
|
||||
let mut f = fake.borrow_mut();
|
||||
match new {
|
||||
StreamState::Paused => {
|
||||
info!("stream available on node ID: {}", stream.node_id());
|
||||
f.streaming = false;
|
||||
f.seq = 0;
|
||||
}
|
||||
StreamState::Streaming => f.streaming = true,
|
||||
StreamState::Error(_) | StreamState::Unconnected => {
|
||||
if let Some(mainloop) = mainloop_weak.upgrade() {
|
||||
mainloop.quit();
|
||||
}
|
||||
}
|
||||
StreamState::Connecting => {}
|
||||
}
|
||||
})
|
||||
.param_changed(|stream, fake, id, param| on_param_changed(stream, fake, id, param))
|
||||
// SAFETY: the pointers come straight from the stream events.
|
||||
.add_buffer(|_, fake, buffer| unsafe { on_add_buffer(&fake.borrow(), buffer) })
|
||||
.remove_buffer(|_, _, buffer| unsafe { on_remove_buffer(buffer) })
|
||||
.register()?
|
||||
};
|
||||
|
||||
let pods = build_format_params(fake.borrow().capture);
|
||||
let mut params: Vec<&Pod> = pods
|
||||
.iter()
|
||||
.map(|p| Pod::from_bytes(p).expect("valid pod"))
|
||||
.collect();
|
||||
stream.connect(
|
||||
Direction::Output,
|
||||
None,
|
||||
StreamFlags::DRIVER | StreamFlags::ALLOC_BUFFERS,
|
||||
&mut params,
|
||||
)?;
|
||||
|
||||
let frame_timer = {
|
||||
let (stream, fake) = (stream.clone(), fake.clone());
|
||||
mainloop.loop_().add_timer(move |_| tick(&stream, &fake))
|
||||
};
|
||||
let interval = Duration::from_secs(1) / opt.fps.max(1);
|
||||
let _ = frame_timer.update_timer(Some(interval), Some(interval));
|
||||
|
||||
let report_timer = {
|
||||
let fake = fake.clone();
|
||||
mainloop.loop_().add_timer(move |_| {
|
||||
let f = fake.borrow();
|
||||
info!(
|
||||
"frames: {} ok, {} corrupted, out of buffers {}, streaming {}, capture {}x{}",
|
||||
f.good, f.corrupted, f.out_of_buffers, f.streaming, f.capture.w, f.capture.h
|
||||
);
|
||||
})
|
||||
};
|
||||
let _ = report_timer.update_timer(Some(Duration::from_secs(5)), Some(Duration::from_secs(5)));
|
||||
|
||||
info!(
|
||||
"fake gamescope '{}' output {}x{} at {} fps{}{}",
|
||||
opt.name,
|
||||
opt.width,
|
||||
opt.height,
|
||||
opt.fps,
|
||||
if opt.static_picture { ", static" } else { "" },
|
||||
if opt.ignore_requested {
|
||||
", ignoring requested_size"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
mainloop.run();
|
||||
Ok(())
|
||||
}
|
||||
Executable
+214
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env bash
|
||||
# End-to-end check in the current PipeWire session (spec 9.3):
|
||||
# examples/fake_gamescope + gamescope-hyperhdr + examples/mock_hyperhdr.
|
||||
#
|
||||
# The fake node gets a unique name, so a real gamescope node is never touched.
|
||||
# Requires: pw-link, pw-dump, jq; gst-launch-1.0 with pipewiresink for scenario 4.
|
||||
set -uo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$root"
|
||||
cargo build --release --bin gamescope-hyperhdr --example fake_gamescope --example mock_hyperhdr || exit 1
|
||||
|
||||
bin="$root/target/release"
|
||||
logs="$(mktemp -d /tmp/gshdr-e2e.XXXXXX)"
|
||||
name="gamescope-e2e-$$"
|
||||
client_node="gamescope-hyperhdr"
|
||||
port=$((20000 + RANDOM % 20000))
|
||||
failures=0
|
||||
mock_pid="" fake_pid="" client_pid="" gst_pid=""
|
||||
|
||||
pass() { printf ' \033[32mPASS\033[0m %s\n' "$*"; }
|
||||
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; failures=$((failures + 1)); }
|
||||
skip() { printf ' \033[33mSKIP\033[0m %s\n' "$*"; }
|
||||
check() {
|
||||
local desc=$1
|
||||
shift
|
||||
if "$@"; then pass "$desc"; else fail "$desc"; fi
|
||||
}
|
||||
|
||||
# wait_pid PID TIMEOUT_S: returns the exit status, or kills the process and returns 124.
|
||||
wait_pid() {
|
||||
local pid=$1 end=$((SECONDS + $2))
|
||||
while [[ -e /proc/$pid && "$(awk '{ print $3 }' "/proc/$pid/stat" 2>/dev/null)" != Z ]]; do
|
||||
if ((SECONDS >= end)); then
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
return 124
|
||||
fi
|
||||
sleep 0.05
|
||||
done
|
||||
wait "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for pid in $client_pid $fake_pid $gst_pid $mock_pid; do
|
||||
kill "$pid" 2>/dev/null
|
||||
wait_pid "$pid" 3
|
||||
done
|
||||
echo "logs: $logs"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# wait_count FILE REGEX MIN_COUNT TIMEOUT_S [FROM_LINE]
|
||||
wait_count() {
|
||||
local file=$1 re=$2 min=$3 from=${5:-1} end=$((SECONDS + $4))
|
||||
while :; do
|
||||
local n
|
||||
n=$(tail -n "+$from" "$file" 2>/dev/null | grep -Ec -- "$re")
|
||||
((n >= min)) && return 0
|
||||
((SECONDS >= end)) && return 1
|
||||
sleep 0.2
|
||||
done
|
||||
}
|
||||
wait_log() { wait_count "$1" "$2" 1 "${3:-10}" "${4:-1}"; }
|
||||
next_line() { echo $(($(wc -l <"$1") + 1)); }
|
||||
absent_since() { ! tail -n "+$3" "$1" | grep -Eq -- "$2"; }
|
||||
|
||||
start_mock() {
|
||||
"$bin/examples/mock_hyperhdr" --listen "127.0.0.1:$port" --timeout-secs 5 >>"$logs/mock.log" 2>&1 &
|
||||
mock_pid=$!
|
||||
}
|
||||
start_fake() {
|
||||
echo "--- start $*" >>"$logs/fake.log"
|
||||
"$bin/examples/fake_gamescope" --name "$name" "$@" >>"$logs/fake.log" 2>&1 &
|
||||
fake_pid=$!
|
||||
}
|
||||
stop_fake() {
|
||||
kill "$fake_pid" 2>/dev/null
|
||||
wait_pid "$fake_pid" 3
|
||||
fake_pid=""
|
||||
}
|
||||
start_client() {
|
||||
local log=$1
|
||||
shift
|
||||
"$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" --target "$name" "$@" >"$log" 2>&1 &
|
||||
client_pid=$!
|
||||
}
|
||||
stop_client() {
|
||||
kill -TERM "$client_pid" 2>/dev/null
|
||||
wait_pid "$client_pid" 3
|
||||
client_pid=""
|
||||
}
|
||||
|
||||
# "<output port> -> <input port>" for every link.
|
||||
links() {
|
||||
pw-link -l 2>/dev/null | awk '/^[^ \t]/ { port = $0 } /\|->/ { sub(/^[ \t]*\|->[ \t]*/, ""); print port " -> " $0 }'
|
||||
}
|
||||
linked_to_fake() { links | grep -q "^$name:.* -> $client_node:"; }
|
||||
foreign_links() { links | grep " -> $client_node:" | grep -v "^$name:"; }
|
||||
node_exists() {
|
||||
pw-dump 2>/dev/null | jq -e --arg n "$1" \
|
||||
'any(.[]; .type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n)' >/dev/null
|
||||
}
|
||||
# Negotiated Format of the client's ports, e.g. "BGRx 320x180".
|
||||
client_port_format() {
|
||||
pw-dump 2>/dev/null | jq -r --arg n "$client_node" '
|
||||
[.[] | select(.type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n) | .id] as $ids
|
||||
| .[] | select(.type == "PipeWire:Interface:Port" and (.info.props["node.id"] as $id | $ids | index($id)))
|
||||
| .info.params.Format[]? | "\(.format) \(.size.width)x\(.size.height)"'
|
||||
}
|
||||
|
||||
echo "== setup (fake node '$name', mock on port $port, logs in $logs)"
|
||||
start_mock
|
||||
wait_log "$logs/mock.log" "listening on" 5 || { fail "mock did not start"; exit 1; }
|
||||
start_client "$logs/client.log"
|
||||
wait_log "$logs/client.log" "waiting for a '$name'" 5 || { fail "client did not start"; exit 1; }
|
||||
start_fake --width 3840 --height 2160 --fps 60
|
||||
|
||||
echo "== 1. requested_size renegotiation"
|
||||
check "1st Format event is the output size 3840x2160" wait_log "$logs/client.log" 'format: BGRx 3840x2160.*event=1'
|
||||
check "2nd Format event is 320x180" wait_log "$logs/client.log" 'format: BGRx 320x180.*event=2'
|
||||
check "frames arrive as 320x180" wait_log "$logs/client.log" 'receiving frames: 320x180 .*sending 320x180'
|
||||
check "registered with priority 150" wait_log "$logs/client.log" 'registered with priority 150'
|
||||
check "mock receives 320x180 RGB" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true'
|
||||
|
||||
echo "== 2. graph"
|
||||
sleep 1
|
||||
links >"$logs/links.txt"
|
||||
client_port_format >"$logs/format.txt"
|
||||
check "client is linked to the fake node" linked_to_fake
|
||||
check "nothing else is linked to the client" test -z "$(foreign_links)"
|
||||
check "pw-dump: negotiated $(tr '\n' ' ' <"$logs/format.txt")" grep -q "BGRx 320x180" "$logs/format.txt"
|
||||
|
||||
echo "== 3. source disappears and returns"
|
||||
mock_mark=$(next_line "$logs/mock.log")
|
||||
client_mark=$(next_line "$logs/client.log")
|
||||
stop_fake
|
||||
check "client sends Clear when the node disappears" wait_log "$logs/client.log" 'source gone: sent Clear' 5 "$client_mark"
|
||||
check "mock gets Clear{150}" wait_log "$logs/mock.log" 'Clear \{ priority: 150 \}' 5 "$mock_mark"
|
||||
check "mock sees the client close the connection" wait_log "$logs/mock.log" 'Disconnected\(ClosedByClient\)' 5 "$mock_mark"
|
||||
sleep 1
|
||||
mock_mark=$(next_line "$logs/mock.log")
|
||||
start_fake --width 3840 --height 2160 --fps 60
|
||||
check "client re-registers without a restart" wait_log "$logs/mock.log" 'Register \{ origin: "gamescope", priority: 150 \}' 10 "$mock_mark"
|
||||
check "frames flow again" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' 10 "$mock_mark"
|
||||
|
||||
echo "== 4. foreign Video/Source is ignored"
|
||||
client_mark=$(next_line "$logs/client.log")
|
||||
stop_fake
|
||||
wait_log "$logs/client.log" 'destroying stream' 5 "$client_mark"
|
||||
client_mark=$(next_line "$logs/client.log")
|
||||
if command -v gst-launch-1.0 >/dev/null; then
|
||||
foreign="e2e-foreign-$$"
|
||||
gst-launch-1.0 -q videotestsrc is-live=true \
|
||||
! video/x-raw,format=BGRx,width=320,height=180,framerate=30/1 \
|
||||
! pipewiresink mode=provide "stream-properties=props,media.class=Video/Source,node.name=$foreign" \
|
||||
>"$logs/gst.log" 2>&1 &
|
||||
gst_pid=$!
|
||||
sleep 3
|
||||
if node_exists "$foreign"; then
|
||||
check "client does not create a stream" absent_since "$logs/client.log" 'stream created' "$client_mark"
|
||||
check "client node does not exist" test "$(node_exists "$client_node" && echo yes)" != yes
|
||||
check "nothing is linked to the client" test -z "$(links | grep " -> $client_node:")"
|
||||
else
|
||||
skip "pipewiresink did not create node '$foreign'"
|
||||
fi
|
||||
kill "$gst_pid" 2>/dev/null
|
||||
wait_pid "$gst_pid" 3
|
||||
gst_pid=""
|
||||
else
|
||||
skip "gst-launch-1.0 not installed"
|
||||
fi
|
||||
|
||||
echo "== 5. SIGTERM"
|
||||
mock_mark=$(next_line "$logs/mock.log")
|
||||
start_fake --width 3840 --height 2160 --fps 60
|
||||
wait_log "$logs/mock.log" 'image 320x180' 10 "$mock_mark"
|
||||
started=$(date +%s%N)
|
||||
kill -TERM "$client_pid"
|
||||
wait_pid "$client_pid" 3
|
||||
status=$?
|
||||
elapsed_ms=$((($(date +%s%N) - started) / 1000000))
|
||||
client_pid=""
|
||||
check "exit code 0 (got $status)" test "$status" -eq 0
|
||||
check "exits within 1 s (${elapsed_ms} ms)" test "$elapsed_ms" -lt 1000
|
||||
check "sends Clear on shutdown" grep -q 'shutting down: sent Clear' "$logs/client.log"
|
||||
check "mock gets Clear{150}" wait_log "$logs/mock.log" 'Clear \{ priority: 150 \}' 2 "$mock_mark"
|
||||
|
||||
echo "== 6. --supersample 2"
|
||||
mock_mark=$(next_line "$logs/mock.log")
|
||||
start_client "$logs/client-supersample.log" --supersample 2
|
||||
check "requests 640x360" wait_log "$logs/client-supersample.log" 'format: BGRx 640x360.*requested_size 640x360'
|
||||
check "downscales 640x360 to 320x180" wait_log "$logs/client-supersample.log" 'receiving frames: 640x360 .*sending 320x180'
|
||||
check "mock receives 320x180" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' 10 "$mock_mark"
|
||||
stop_client
|
||||
|
||||
echo "== 7. static picture: keepalive holds the connection"
|
||||
stop_fake
|
||||
start_fake --width 3840 --height 2160 --fps 60 --static
|
||||
mock_mark=$(next_line "$logs/mock.log")
|
||||
start_client "$logs/client-static.log"
|
||||
wait_log "$logs/mock.log" 'image 320x180' 10 "$mock_mark"
|
||||
sleep 8
|
||||
check "no idle timeout on the server (5 s)" absent_since "$logs/mock.log" 'Disconnected' "$mock_mark"
|
||||
check "keepalive images keep arriving" wait_count "$logs/mock.log" 'images in' 3 1 "$mock_mark"
|
||||
stop_client
|
||||
|
||||
echo
|
||||
if ((failures == 0)); then
|
||||
echo "all scenarios passed"
|
||||
else
|
||||
echo "$failures check(s) failed"
|
||||
fi
|
||||
exit $((failures > 0))
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# Spec 9.4.1 / 9.4.3 with a real gamescope: a nested headless gamescope running vkcube,
|
||||
# gamescope-hyperhdr and the mock HyperHDR, plus a load comparison with the old gst pipeline
|
||||
# (pipewiresrc → videorate → videoscale → videoconvert, into fakesink instead of v4l2loopback).
|
||||
#
|
||||
# Environment: W, H (output size, default 3840x2160), REFRESH (60), SECS (measurement, 10),
|
||||
# CLIENT_ARGS (extra client flags, e.g. "--supersample 2").
|
||||
set -uo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$root"
|
||||
cargo build --release --bin gamescope-hyperhdr --example mock_hyperhdr || exit 1
|
||||
|
||||
W=${W:-3840} H=${H:-2160} REFRESH=${REFRESH:-60} SECS=${SECS:-10}
|
||||
read -r -a client_args <<<"${CLIENT_ARGS:-}"
|
||||
bin="$root/target/release"
|
||||
logs="$(mktemp -d /tmp/gshdr-nested.XXXXXX)"
|
||||
port=$((20000 + RANDOM % 20000))
|
||||
mock_pid="" gs_pid="" client_pid="" gst_pid=""
|
||||
|
||||
node_exists() {
|
||||
pw-dump 2>/dev/null | jq -e --arg n "$1" \
|
||||
'any(.[]; .type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n)' >/dev/null
|
||||
}
|
||||
if node_exists gamescope; then
|
||||
echo "a 'gamescope' node already exists (gamescope-tv running?); stop it first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_pid() {
|
||||
local pid=$1 end=$((SECONDS + $2))
|
||||
while [[ -e /proc/$pid && "$(awk '{ print $3 }' "/proc/$pid/stat" 2>/dev/null)" != Z ]]; do
|
||||
if ((SECONDS >= end)); then
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
return 124
|
||||
fi
|
||||
sleep 0.02
|
||||
done
|
||||
wait "$pid" 2>/dev/null
|
||||
}
|
||||
cleanup() {
|
||||
for pid in $client_pid $gst_pid $gs_pid $mock_pid; do
|
||||
kill "$pid" 2>/dev/null
|
||||
wait_pid "$pid" 5
|
||||
done
|
||||
echo "logs: $logs"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_log() {
|
||||
local end=$((SECONDS + $3))
|
||||
until grep -Eq -- "$2" "$1" 2>/dev/null; do
|
||||
((SECONDS >= end)) && return 1
|
||||
sleep 0.1
|
||||
done
|
||||
}
|
||||
links() {
|
||||
pw-link -l 2>/dev/null | awk '/^[^ \t]/ { port = $0 } /\|->/ { sub(/^[ \t]*\|->[ \t]*/, ""); print port " -> " $0 }'
|
||||
}
|
||||
port_format() {
|
||||
pw-dump 2>/dev/null | jq -r --arg n "$1" '
|
||||
[.[] | select(.type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n) | .id] as $ids
|
||||
| .[] | select(.type == "PipeWire:Interface:Port" and (.info.props["node.id"] as $id | $ids | index($id)))
|
||||
| .info.params.Format[]? | "\(.format) \(.size.width)x\(.size.height)"'
|
||||
}
|
||||
cpu_ticks() { awk '{ print $14 + $15 }' "/proc/$1/stat"; }
|
||||
rss_kib() { awk '/^VmRSS:/ { print $2 }' "/proc/$1/status"; }
|
||||
# measure NAME=PID...: CPU (% of one core) and RSS over SECS seconds
|
||||
measure() {
|
||||
local hz item t0 t1
|
||||
hz=$(getconf CLK_TCK)
|
||||
local -A before
|
||||
for item in "$@"; do before[$item]=$(cpu_ticks "${item#*=}"); done
|
||||
t0=$(date +%s%N)
|
||||
sleep "$SECS"
|
||||
t1=$(date +%s%N)
|
||||
for item in "$@"; do
|
||||
local pid=${item#*=}
|
||||
awk -v ticks="$(($(cpu_ticks "$pid") - before[$item]))" -v hz="$hz" -v ns="$((t1 - t0))" \
|
||||
-v name="${item%%=*}" -v rss="$(rss_kib "$pid")" \
|
||||
'BEGIN { printf " %-20s CPU %6.2f %% RSS %6.1f MB\n", name, ticks / hz / (ns / 1e9) * 100, rss / 1024 }'
|
||||
done
|
||||
}
|
||||
|
||||
"$bin/examples/mock_hyperhdr" --listen "127.0.0.1:$port" >"$logs/mock.log" 2>&1 &
|
||||
mock_pid=$!
|
||||
|
||||
echo "== nested gamescope ${W}x${H}@${REFRESH} (headless, vkcube)"
|
||||
gamescope --backend headless -W "$W" -H "$H" -r "$REFRESH" -- vkcube >"$logs/gamescope.log" 2>&1 &
|
||||
gs_pid=$!
|
||||
for _ in $(seq 50); do node_exists gamescope && break; sleep 0.2; done
|
||||
node_exists gamescope || { echo "gamescope node did not appear, see $logs/gamescope.log"; exit 1; }
|
||||
sleep 2
|
||||
|
||||
echo "== gamescope-hyperhdr ${client_args[*]}"
|
||||
"$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" "${client_args[@]}" >"$logs/client.log" 2>&1 &
|
||||
client_pid=$!
|
||||
wait_log "$logs/client.log" 'receiving frames' 15 || echo " no frames within 15 s"
|
||||
wait_log "$logs/client.log" 'registered with priority' 5
|
||||
grep -E 'format:|receiving frames|registered|warn|WARN' "$logs/client.log" | sed 's/^[^ ]* */ /'
|
||||
echo " links: $(links | grep ' -> gamescope-hyperhdr:' | tr '\n' ' ')"
|
||||
echo " format: $(port_format gamescope-hyperhdr | tr '\n' ' ')"
|
||||
echo " load over $SECS s:"
|
||||
measure gamescope="$gs_pid" gamescope-hyperhdr="$client_pid"
|
||||
echo " mock: $(grep 'images in' "$logs/mock.log" | tail -1 | sed 's/^.*INFO //')"
|
||||
|
||||
started=$(date +%s%N)
|
||||
kill -TERM "$client_pid"
|
||||
wait_pid "$client_pid" 3
|
||||
status=$?
|
||||
client_pid=""
|
||||
echo " SIGTERM: exit $status after $((($(date +%s%N) - started) / 1000000)) ms, $(grep -c 'Clear { priority' "$logs/mock.log") Clear(s) at the mock"
|
||||
|
||||
if command -v gst-launch-1.0 >/dev/null; then
|
||||
echo "== old pipeline: pipewiresrc → videorate 30 → videoscale 320x180 → videoconvert YUY2 → fakesink"
|
||||
sleep 2
|
||||
gst-launch-1.0 -q pipewiresrc target-object=gamescope do-timestamp=true keepalive-time=200 \
|
||||
! videorate ! video/x-raw,framerate=30/1 \
|
||||
! videoscale ! video/x-raw,width=320,height=180 \
|
||||
! videoconvert ! video/x-raw,format=YUY2 \
|
||||
! fakesink sync=false >"$logs/gst.log" 2>&1 &
|
||||
gst_pid=$!
|
||||
sleep 4
|
||||
echo " format: $(links | grep -oE '[^ ]+ -> gst-launch[^ ]*' | head -1) $(port_format gst-launch-1.0 | tr '\n' ' ')"
|
||||
echo " load over $SECS s:"
|
||||
measure gamescope="$gs_pid" gst-launch-1.0="$gst_pid"
|
||||
kill "$gst_pid"
|
||||
wait_pid "$gst_pid" 3
|
||||
gst_pid=""
|
||||
fi
|
||||
|
||||
echo "== gamescope stops → Clear"
|
||||
"$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" "${client_args[@]}" >"$logs/client2.log" 2>&1 &
|
||||
client_pid=$!
|
||||
wait_log "$logs/client2.log" 'registered with priority' 15
|
||||
clears_before=$(grep -c 'Clear { priority' "$logs/mock.log")
|
||||
started=$(date +%s%N)
|
||||
kill -TERM "$gs_pid"
|
||||
until (($(grep -c 'Clear { priority' "$logs/mock.log") > clears_before)); do
|
||||
(($(date +%s%N) - started > 10000000000)) && break
|
||||
sleep 0.01
|
||||
done
|
||||
echo " Clear at the mock $((($(date +%s%N) - started) / 1000000)) ms after SIGTERM to gamescope"
|
||||
wait_pid "$gs_pid" 10
|
||||
gs_pid=""
|
||||
sleep 1
|
||||
grep -E 'source node removed|destroying stream|source gone|ERROR' "$logs/client2.log" | sed 's/^[^ ]* */ /'
|
||||
Reference in New Issue
Block a user