Merge remote-tracking branch 'origin/main'
apple / swift (push) Successful in 1m14s
apple / screenshots (push) Successful in 5m59s
android / android (push) Successful in 4m56s
arch / build-publish (push) Successful in 6m10s
ci / web (push) Successful in 1m6s
ci / docs-site (push) Successful in 1m17s
ci / rust (push) Successful in 4m56s
deb / build-publish (push) Successful in 3m20s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
ci / bench (push) Successful in 5m9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m49s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m55s
docker / deploy-docs (push) Successful in 20s

This commit is contained in:
2026-07-06 20:02:48 +02:00
3 changed files with 80 additions and 40 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"name": "MIT OR Apache-2.0", "name": "MIT OR Apache-2.0",
"identifier": "MIT OR Apache-2.0" "identifier": "MIT OR Apache-2.0"
}, },
"version": "0.8.0" "version": "0.8.1"
}, },
"paths": { "paths": {
"/api/v1/clients": { "/api/v1/clients": {
+11 -3
View File
@@ -154,13 +154,21 @@ pub fn run() -> glib::ExitCode {
builder = builder.flags(gtk::gio::ApplicationFlags::NON_UNIQUE); builder = builder.flags(gtk::gio::ApplicationFlags::NON_UNIQUE);
} }
let app = builder.build(); let app = builder.build();
app.connect_activate(build_ui); // One SDL context for the whole process: `activate` fires again on every subsequent
// launch forwarded to this already-running singleton (another `--connect`, the desktop
// icon clicked twice, …). SDL only ever lets the FIRST thread that calls `sdl3::init()`
// hold the "main thread" — a second `GamepadService::start()` from a later `activate`
// would spawn a new thread that fails that check forever. Starting it once here and
// cloning it into each `build_ui` keeps the worker thread (and its pad state) shared
// across every window instead.
let gamepad = crate::gamepad::GamepadService::start();
app.connect_activate(move |gtk_app| build_ui(gtk_app, gamepad.clone()));
// GTK doesn't see our argv (`--connect` is handled in `build_ui`); an empty argv also // GTK doesn't see our argv (`--connect` is handled in `build_ui`); an empty argv also
// keeps GApplication from rejecting unknown options. // keeps GApplication from rejecting unknown options.
app.run_with_args(&[] as &[&str]) app.run_with_args(&[] as &[&str])
} }
fn build_ui(gtk_app: &adw::Application) { fn build_ui(gtk_app: &adw::Application, gamepad: crate::gamepad::GamepadService) {
let identity = match crate::trust::load_or_create_identity() { let identity = match crate::trust::load_or_create_identity() {
Ok(i) => i, Ok(i) => i,
Err(e) => { Err(e) => {
@@ -203,7 +211,7 @@ fn build_ui(gtk_app: &adw::Application) {
toasts, toasts,
settings: Rc::new(RefCell::new(Settings::load())), settings: Rc::new(RefCell::new(Settings::load())),
identity, identity,
gamepad: crate::gamepad::GamepadService::start(), gamepad,
busy: std::cell::Cell::new(false), busy: std::cell::Cell::new(false),
fullscreen, fullscreen,
// (`--browse` makes cli_connect_request None — browse mode returns to the // (`--browse` makes cli_connect_request None — browse mode returns to the
+68 -36
View File
@@ -54,7 +54,9 @@ use windows::Win32::Graphics::Direct3D11::{
ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, D3D11_BIND_RENDER_TARGET, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, D3D11_BIND_RENDER_TARGET,
D3D11_BIND_SHADER_RESOURCE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
}; };
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_FORMAT_P010, DXGI_SAMPLE_DESC}; use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT_NV12, DXGI_FORMAT_P010, DXGI_SAMPLE_DESC,
};
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
// Mirrored AMF C ABI (pinned to GPUOpen header release v1.4.36 — amf/public/include). // Mirrored AMF C ABI (pinned to GPUOpen header release v1.4.36 — amf/public/include).
@@ -283,8 +285,7 @@ mod sys {
pub get_dx9_device: Slot, pub get_dx9_device: Slot,
pub lock_dx9: Slot, pub lock_dx9: Slot,
pub unlock_dx9: Slot, pub unlock_dx9: Slot,
pub init_dx11: pub init_dx11: unsafe extern "system" fn(*mut AmfContext, *mut c_void, i32) -> AmfResult,
unsafe extern "system" fn(*mut AmfContext, *mut c_void, i32) -> AmfResult,
pub get_dx11_device: Slot, pub get_dx11_device: Slot,
pub lock_dx11: Slot, pub lock_dx11: Slot,
pub unlock_dx11: Slot, pub unlock_dx11: Slot,
@@ -371,8 +372,7 @@ mod sys {
pub terminate: unsafe extern "system" fn(*mut AmfComponent) -> AmfResult, pub terminate: unsafe extern "system" fn(*mut AmfComponent) -> AmfResult,
pub drain: unsafe extern "system" fn(*mut AmfComponent) -> AmfResult, pub drain: unsafe extern "system" fn(*mut AmfComponent) -> AmfResult,
pub flush: unsafe extern "system" fn(*mut AmfComponent) -> AmfResult, pub flush: unsafe extern "system" fn(*mut AmfComponent) -> AmfResult,
pub submit_input: pub submit_input: unsafe extern "system" fn(*mut AmfComponent, *mut AmfData) -> AmfResult,
unsafe extern "system" fn(*mut AmfComponent, *mut AmfData) -> AmfResult,
pub query_output: pub query_output:
unsafe extern "system" fn(*mut AmfComponent, *mut *mut AmfData) -> AmfResult, unsafe extern "system" fn(*mut AmfComponent, *mut *mut AmfData) -> AmfResult,
pub get_context: Slot, pub get_context: Slot,
@@ -391,11 +391,8 @@ mod sys {
// AMFInterface // AMFInterface
pub acquire: Slot, pub acquire: Slot,
pub release: unsafe extern "system" fn(*mut AmfData) -> i32, pub release: unsafe extern "system" fn(*mut AmfData) -> i32,
pub query_interface: unsafe extern "system" fn( pub query_interface:
*mut AmfData, unsafe extern "system" fn(*mut AmfData, *const AmfGuid, *mut *mut c_void) -> AmfResult,
*const AmfGuid,
*mut *mut c_void,
) -> AmfResult,
// AMFPropertyStorage // AMFPropertyStorage
pub set_property: pub set_property:
unsafe extern "system" fn(*mut AmfData, *const u16, AmfVariant) -> AmfResult, unsafe extern "system" fn(*mut AmfData, *const u16, AmfVariant) -> AmfResult,
@@ -1115,7 +1112,12 @@ impl AmfEncoder {
// Never B-frames: a full frame period of latency each (RDNA3+ defaults > 0). // Never B-frames: a full frame period of latency each (RDNA3+ defaults > 0).
set_prop(comp, w!("BPicturesPattern"), AmfVariant::from_i64(0), false)?; set_prop(comp, w!("BPicturesPattern"), AmfVariant::from_i64(0), false)?;
// Limited-range YUV input/output (matches the video processor's NV12). // Limited-range YUV input/output (matches the video processor's NV12).
set_prop(comp, w!("FullRangeColor"), AmfVariant::from_bool(false), false)?; set_prop(
comp,
w!("FullRangeColor"),
AmfVariant::from_bool(false),
false,
)?;
} }
Codec::H265 => { Codec::H265 => {
// In-band VPS/SPS/PPS on every IDR — the `EncodedFrame` wire contract (clean // In-band VPS/SPS/PPS on every IDR — the `EncodedFrame` wire contract (clean
@@ -1800,10 +1802,7 @@ impl Encoder for AmfEncoder {
} }
other => { other => {
self.force_kf = true; self.force_kf = true;
bail!( bail!("AMF SubmitInput failed: {} ({other})", result_name(other));
"AMF SubmitInput failed: {} ({other})",
result_name(other)
);
} }
} }
} }
@@ -1891,11 +1890,11 @@ impl Encoder for AmfEncoder {
inner.pending.clear(); inner.pending.clear();
inner.ready.clear(); // owed + buffered AUs are forfeited; the rebuilt stream restarts at IDR inner.ready.clear(); // owed + buffered AUs are forfeited; the rebuilt stream restarts at IDR
inner.hdr_pushed = None; // a re-Init'd component needs the HDR metadata again inner.hdr_pushed = None; // a re-Init'd component needs the HDR metadata again
// SAFETY: `inner.comp.0` is the live component, used only on this thread with no AMF // SAFETY: `inner.comp.0` is the live component, used only on this thread with no AMF
// call in flight (the session loop is synchronous). `Flush` discards queued frames, // call in flight (the session loop is synchronous). `Flush` discards queued frames,
// `Terminate` tears the hw session down (both legal on a wedged component — results are // `Terminate` tears the hw session down (both legal on a wedged component — results are
// deliberately ignored), `apply_static_props` + `init` then rebuild it; each call goes // deliberately ignored), `apply_static_props` + `init` then rebuild it; each call goes
// through the runtime's vtable. // through the runtime's vtable.
let rebuilt = unsafe { let rebuilt = unsafe {
let comp = inner.comp.0; let comp = inner.comp.0;
((*(*comp).vtbl).flush)(comp); ((*(*comp).vtbl).flush)(comp);
@@ -1915,7 +1914,9 @@ impl Encoder for AmfEncoder {
} }
}; };
if rebuilt { if rebuilt {
tracing::info!("AMF encoder rebuilt in place (Terminate + re-Init on the same context)"); tracing::info!(
"AMF encoder rebuilt in place (Terminate + re-Init on the same context)"
);
} else { } else {
self.ir_active = false; self.ir_active = false;
// Full teardown; the next submit reopens context + component on the current device. // Full teardown; the next submit reopens context + component on the current device.
@@ -1966,7 +1967,10 @@ mod tests {
#[test] #[test]
fn hdr_metadata_layout_matches_c() { fn hdr_metadata_layout_matches_c() {
assert_eq!(std::mem::size_of::<sys::AmfHdrMetadata>(), 28); assert_eq!(std::mem::size_of::<sys::AmfHdrMetadata>(), 28);
assert_eq!(std::mem::offset_of!(sys::AmfHdrMetadata, max_mastering_luminance), 16); assert_eq!(
std::mem::offset_of!(sys::AmfHdrMetadata, max_mastering_luminance),
16
);
assert_eq!( assert_eq!(
std::mem::offset_of!(sys::AmfHdrMetadata, max_content_light_level), std::mem::offset_of!(sys::AmfHdrMetadata, max_content_light_level),
24 24
@@ -2126,7 +2130,9 @@ mod tests {
#[test] #[test]
fn amf_latency_ab_bench() { fn amf_latency_ab_bench() {
if std::env::var("PUNKTFUNK_AMF_BENCH").as_deref() != Ok("1") { if std::env::var("PUNKTFUNK_AMF_BENCH").as_deref() != Ok("1") {
eprintln!("skipping: set PUNKTFUNK_AMF_BENCH=1 to run the native-vs-ffmpeg latency A/B"); eprintln!(
"skipping: set PUNKTFUNK_AMF_BENCH=1 to run the native-vs-ffmpeg latency A/B"
);
return; return;
} }
if let Err(e) = try_factory() { if let Err(e) = try_factory() {
@@ -2153,8 +2159,16 @@ mod tests {
ChromaFormat::Yuv420, ChromaFormat::Yuv420,
) )
.expect("native AMF open"); .expect("native AMF open");
let mut native_us = let mut native_us = drive_and_measure(
drive_and_measure(&mut native, &device, &tex, w, h, fps, PixelFormat::Nv12, frames); &mut native,
&device,
&tex,
w,
h,
fps,
PixelFormat::Nv12,
frames,
);
drop(native); drop(native);
let mut ffmpeg = crate::encode::ffmpeg_win::FfmpegWinEncoder::open( let mut ffmpeg = crate::encode::ffmpeg_win::FfmpegWinEncoder::open(
@@ -2169,8 +2183,16 @@ mod tests {
ChromaFormat::Yuv420, ChromaFormat::Yuv420,
) )
.expect("libavcodec AMF open"); .expect("libavcodec AMF open");
let mut ffmpeg_us = let mut ffmpeg_us = drive_and_measure(
drive_and_measure(&mut ffmpeg, &device, &tex, w, h, fps, PixelFormat::Nv12, frames); &mut ffmpeg,
&device,
&tex,
w,
h,
fps,
PixelFormat::Nv12,
frames,
);
drop(ffmpeg); drop(ffmpeg);
let iv = 1_000_000u128 / fps as u128; let iv = 1_000_000u128 / fps as u128;
@@ -2195,7 +2217,10 @@ mod tests {
f50 as f64 / iv as f64 f50 as f64 / iv as f64
); );
if n50 > 0 { if n50 > 0 {
eprintln!("native p50 is {:.1}x lower than ffmpeg", f50 as f64 / n50 as f64); eprintln!(
"native p50 is {:.1}x lower than ffmpeg",
f50 as f64 / n50 as f64
);
} }
// The core §5.2 claim: the native path retrieves faster than the libavcodec 2-frame hold, // The core §5.2 claim: the native path retrieves faster than the libavcodec 2-frame hold,
// and its per-frame encode_us collapses below one frame period (where the ~2-frame hold // and its per-frame encode_us collapses below one frame period (where the ~2-frame hold
@@ -2291,14 +2316,16 @@ mod tests {
); );
for run in [&first_run, &second_run] { for run in [&first_run, &second_run] {
let first = &run[0]; let first = &run[0];
assert!(first.keyframe, "{codec:?}: stream/reset start must be an IDR"); assert!(
first.keyframe,
"{codec:?}: stream/reset start must be an IDR"
);
if codec == Codec::Av1 { if codec == Codec::Av1 {
// AV1 is an OBU stream, not Annex-B — just require substance. // AV1 is an OBU stream, not Annex-B — just require substance.
assert!(!first.data.is_empty(), "Av1: empty key AU"); assert!(!first.data.is_empty(), "Av1: empty key AU");
} else { } else {
assert!( assert!(
first.data.starts_with(&[0, 0, 0, 1]) first.data.starts_with(&[0, 0, 0, 1]) || first.data.starts_with(&[0, 0, 1]),
|| first.data.starts_with(&[0, 0, 1]),
"{codec:?}: AU must be Annex-B (got {:02x?})", "{codec:?}: AU must be Annex-B (got {:02x?})",
&first.data[..first.data.len().min(8)] &first.data[..first.data.len().min(8)]
); );
@@ -2387,7 +2414,10 @@ mod tests {
} }
}; };
enc.set_hdr_meta(Some(sample_hdr_meta())); enc.set_hdr_meta(Some(sample_hdr_meta()));
assert!(enc.caps().supports_hdr_metadata, "HEVC 10-bit reports HDR SEI capability"); assert!(
enc.caps().supports_hdr_metadata,
"HEVC 10-bit reports HDR SEI capability"
);
let mut aus: Vec<EncodedFrame> = Vec::new(); let mut aus: Vec<EncodedFrame> = Vec::new();
for i in 0..6 { for i in 0..6 {
let frame = CapturedFrame { let frame = CapturedFrame {
@@ -2491,9 +2521,8 @@ mod tests {
let (name, block) = props.intra_refresh.expect("AVC/HEVC define intra-refresh"); let (name, block) = props.intra_refresh.expect("AVC/HEVC define intra-refresh");
let blocks = 640u32.div_ceil(block) * 480u32.div_ceil(block); let blocks = 640u32.div_ceil(block) * 480u32.div_ceil(block);
let per_slot = blocks.div_ceil(30).max(1); let per_slot = blocks.div_ceil(30).max(1);
let applied = let applied = set_prop(comp.0, name, AmfVariant::from_i64(per_slot as i64), false)
set_prop(comp.0, name, AmfVariant::from_i64(per_slot as i64), false) .expect("optional set_prop never errors");
.expect("optional set_prop never errors");
eprintln!( eprintln!(
"intra-refresh {codec:?}: {per_slot} units/slot accepted={applied} on this VCN" "intra-refresh {codec:?}: {per_slot} units/slot accepted={applied} on this VCN"
); );
@@ -2607,7 +2636,10 @@ mod tests {
let ctx = Ctx(ctx); let ctx = Ctx(ctx);
let r = ((*(*ctx.0).vtbl).init_dx11)(ctx.0, ptr::null_mut(), sys::AMF_DX11_1); let r = ((*(*ctx.0).vtbl).init_dx11)(ctx.0, ptr::null_mut(), sys::AMF_DX11_1);
if r != sys::AMF_OK { if r != sys::AMF_OK {
eprintln!("skipping: InitDX11(default device) failed ({})", result_name(r)); eprintln!(
"skipping: InitDX11(default device) failed ({})",
result_name(r)
);
return; return;
} }
let mut comp: *mut sys::AmfComponent = ptr::null_mut(); let mut comp: *mut sys::AmfComponent = ptr::null_mut();