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",
"identifier": "MIT OR Apache-2.0"
},
"version": "0.8.0"
"version": "0.8.1"
},
"paths": {
"/api/v1/clients": {
+11 -3
View File
@@ -154,13 +154,21 @@ pub fn run() -> glib::ExitCode {
builder = builder.flags(gtk::gio::ApplicationFlags::NON_UNIQUE);
}
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
// keeps GApplication from rejecting unknown options.
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() {
Ok(i) => i,
Err(e) => {
@@ -203,7 +211,7 @@ fn build_ui(gtk_app: &adw::Application) {
toasts,
settings: Rc::new(RefCell::new(Settings::load())),
identity,
gamepad: crate::gamepad::GamepadService::start(),
gamepad,
busy: std::cell::Cell::new(false),
fullscreen,
// (`--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,
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).
@@ -283,8 +285,7 @@ mod sys {
pub get_dx9_device: Slot,
pub lock_dx9: Slot,
pub unlock_dx9: Slot,
pub init_dx11:
unsafe extern "system" fn(*mut AmfContext, *mut c_void, i32) -> AmfResult,
pub init_dx11: unsafe extern "system" fn(*mut AmfContext, *mut c_void, i32) -> AmfResult,
pub get_dx11_device: Slot,
pub lock_dx11: Slot,
pub unlock_dx11: Slot,
@@ -371,8 +372,7 @@ mod sys {
pub terminate: 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 submit_input:
unsafe extern "system" fn(*mut AmfComponent, *mut AmfData) -> AmfResult,
pub submit_input: unsafe extern "system" fn(*mut AmfComponent, *mut AmfData) -> AmfResult,
pub query_output:
unsafe extern "system" fn(*mut AmfComponent, *mut *mut AmfData) -> AmfResult,
pub get_context: Slot,
@@ -391,11 +391,8 @@ mod sys {
// AMFInterface
pub acquire: Slot,
pub release: unsafe extern "system" fn(*mut AmfData) -> i32,
pub query_interface: unsafe extern "system" fn(
*mut AmfData,
*const AmfGuid,
*mut *mut c_void,
) -> AmfResult,
pub query_interface:
unsafe extern "system" fn(*mut AmfData, *const AmfGuid, *mut *mut c_void) -> AmfResult,
// AMFPropertyStorage
pub set_property:
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).
set_prop(comp, w!("BPicturesPattern"), AmfVariant::from_i64(0), false)?;
// 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 => {
// In-band VPS/SPS/PPS on every IDR — the `EncodedFrame` wire contract (clean
@@ -1800,10 +1802,7 @@ impl Encoder for AmfEncoder {
}
other => {
self.force_kf = true;
bail!(
"AMF SubmitInput failed: {} ({other})",
result_name(other)
);
bail!("AMF SubmitInput failed: {} ({other})", result_name(other));
}
}
}
@@ -1891,11 +1890,11 @@ impl Encoder for AmfEncoder {
inner.pending.clear();
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
// 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,
// `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
// through the runtime's vtable.
// 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,
// `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
// through the runtime's vtable.
let rebuilt = unsafe {
let comp = inner.comp.0;
((*(*comp).vtbl).flush)(comp);
@@ -1915,7 +1914,9 @@ impl Encoder for AmfEncoder {
}
};
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 {
self.ir_active = false;
// Full teardown; the next submit reopens context + component on the current device.
@@ -1966,7 +1967,10 @@ mod tests {
#[test]
fn hdr_metadata_layout_matches_c() {
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!(
std::mem::offset_of!(sys::AmfHdrMetadata, max_content_light_level),
24
@@ -2126,7 +2130,9 @@ mod tests {
#[test]
fn amf_latency_ab_bench() {
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;
}
if let Err(e) = try_factory() {
@@ -2153,8 +2159,16 @@ mod tests {
ChromaFormat::Yuv420,
)
.expect("native AMF open");
let mut native_us =
drive_and_measure(&mut native, &device, &tex, w, h, fps, PixelFormat::Nv12, frames);
let mut native_us = drive_and_measure(
&mut native,
&device,
&tex,
w,
h,
fps,
PixelFormat::Nv12,
frames,
);
drop(native);
let mut ffmpeg = crate::encode::ffmpeg_win::FfmpegWinEncoder::open(
@@ -2169,8 +2183,16 @@ mod tests {
ChromaFormat::Yuv420,
)
.expect("libavcodec AMF open");
let mut ffmpeg_us =
drive_and_measure(&mut ffmpeg, &device, &tex, w, h, fps, PixelFormat::Nv12, frames);
let mut ffmpeg_us = drive_and_measure(
&mut ffmpeg,
&device,
&tex,
w,
h,
fps,
PixelFormat::Nv12,
frames,
);
drop(ffmpeg);
let iv = 1_000_000u128 / fps as u128;
@@ -2195,7 +2217,10 @@ mod tests {
f50 as f64 / iv as f64
);
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,
// 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] {
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 {
// AV1 is an OBU stream, not Annex-B — just require substance.
assert!(!first.data.is_empty(), "Av1: empty key AU");
} else {
assert!(
first.data.starts_with(&[0, 0, 0, 1])
|| first.data.starts_with(&[0, 0, 1]),
first.data.starts_with(&[0, 0, 0, 1]) || first.data.starts_with(&[0, 0, 1]),
"{codec:?}: AU must be Annex-B (got {:02x?})",
&first.data[..first.data.len().min(8)]
);
@@ -2387,7 +2414,10 @@ mod tests {
}
};
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();
for i in 0..6 {
let frame = CapturedFrame {
@@ -2491,9 +2521,8 @@ mod tests {
let (name, block) = props.intra_refresh.expect("AVC/HEVC define intra-refresh");
let blocks = 640u32.div_ceil(block) * 480u32.div_ceil(block);
let per_slot = blocks.div_ceil(30).max(1);
let applied =
set_prop(comp.0, name, AmfVariant::from_i64(per_slot as i64), false)
.expect("optional set_prop never errors");
let applied = set_prop(comp.0, name, AmfVariant::from_i64(per_slot as i64), false)
.expect("optional set_prop never errors");
eprintln!(
"intra-refresh {codec:?}: {per_slot} units/slot accepted={applied} on this VCN"
);
@@ -2607,7 +2636,10 @@ mod tests {
let ctx = Ctx(ctx);
let r = ((*(*ctx.0).vtbl).init_dx11)(ctx.0, ptr::null_mut(), sys::AMF_DX11_1);
if r != sys::AMF_OK {
eprintln!("skipping: InitDX11(default device) failed ({})", result_name(r));
eprintln!(
"skipping: InitDX11(default device) failed ({})",
result_name(r)
);
return;
}
let mut comp: *mut sys::AmfComponent = ptr::null_mut();