feat(punktfunk/1): negotiable encoder bitrate + bandwidth speed-test probe
ci / rust (push) Has been cancelled

Two related additions to the native protocol, host-side (the client side of
each is exposed over the C ABI so the platform clients can wire it up).

Bitrate negotiation
- Hello/Welcome carry `bitrate_kbps` (appended trailing-byte field, back-compat:
  old peers decode 0 = host default). The client requests a rate; the host
  clamps it to [500 kbps, 500 Mbps] (or its 20 Mbps default when 0) and echoes
  the resolved value in Welcome. Replaces the hardcoded 20 Mbps NVENC bitrate in
  m3.rs — threaded through virtual_stream → build_pipeline → open_video, applied
  on the initial mode and every reconfigure rebuild.
- C ABI: punktfunk_connect_ex3(..., bitrate_kbps, ...) (ex2 delegates with 0);
  punktfunk_connection_bitrate() reads the resolved value.

Speed test (bandwidth probe)
- New typed control messages ProbeRequest{target_kbps,duration_ms} (0x20) /
  ProbeResult{bytes_sent,packets_sent,duration_ms} (0x21), plus a FLAG_PROBE
  packet flag. The client asks the host to burst zero-filled, FLAG_PROBE-tagged
  access units over the data plane at a target goodput for a duration (clamped
  ≤ 1 Gbps / ≤ 5 s), pacing by a bytes-allowed budget; video pauses for the
  burst. The host reports what it actually sent; the client measures received
  bytes + window → goodput and loss. Probe filler is never fed to the decoder
  (diverted in the connector pump and the reference client's poll loop).
- The host control task now multiplexes Reconfigure + ProbeRequest (inbound)
  and ProbeResult (outbound) over select!; a probe channel reaches the
  data-plane thread (both virtual and synthetic sources).
- Connector: NativeClient::request_probe()/probe_result() with an internal
  accumulator; C ABI punktfunk_connection_speed_test() +
  punktfunk_connection_probe_result() → PunktfunkProbeResult.
- punktfunk-client-rs gains `--bitrate KBPS` and `--speed-test KBPS:MS` (its own
  loop measures + logs goodput/loss) for loopback verification.

Validated on loopback (synthetic source): a 20 Mbps / 2 s probe measured
20050 kbps at 0% loss, bitrate negotiated (0→20000 and 50000→50000), and the
interleaved probe AUs were correctly excluded from frame verification
(mismatched=0). Wire codecs + trailing-byte back-compat have unit tests. C
header regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:44:47 +00:00
parent dcb2850c7c
commit 74819b1be8
7 changed files with 906 additions and 89 deletions
+93
View File
@@ -137,6 +137,11 @@
#define FLAG_SOF 4
// Bandwidth-probe filler, not decodable video: a [`crate::quic::ProbeRequest`] speed test makes
// the host burst access units carrying this flag so the client measures throughput/loss without
// feeding them to the decoder. Punktfunk/1 only (GameStream never sets it).
#define FLAG_PROBE 8
// Largest UDP datagram the core will send or accept. `Config::validate` bounds
// `shard_payload` so `HEADER_LEN + shard_payload + CRYPTO_OVERHEAD ≤ MAX_DATAGRAM_BYTES`.
#define MAX_DATAGRAM_BYTES 2048
@@ -151,6 +156,16 @@
#define MSG_RECONFIGURED 2
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Type byte of [`ProbeRequest`].
#define MSG_PROBE_REQUEST 32
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Type byte of [`ProbeResult`].
#define MSG_PROBE_RESULT 33
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Type byte of [`PairRequest`].
#define MSG_PAIR_REQUEST 16
@@ -408,6 +423,26 @@ typedef struct {
} PunktfunkRichInput;
#endif
// A speed-test measurement, filled by [`punktfunk_connection_probe_result`]. `done` is 0 until
// the host's end-of-burst report lands, then 1 (the numbers are final). `throughput_kbps` is the
// measured goodput to drive a bitrate choice from; `loss_pct` is the delivery loss at that rate.
typedef struct {
// 1 once the host's end-of-burst report arrived (measurement final); else 0 (partial).
uint8_t done;
// Probe payload bytes / packets the client received.
uint64_t recv_bytes;
uint32_t recv_packets;
// Probe payload bytes / packets the host reported sending.
uint64_t host_bytes;
uint32_t host_packets;
// Client-measured receive window (first→last probe AU), milliseconds.
uint32_t elapsed_ms;
// Measured goodput = `recv_bytes * 8 / elapsed_ms` (kilobits/second).
uint32_t throughput_kbps;
// Delivery loss `(host_bytes - recv_bytes) / host_bytes` as a percentage (0 if unknown).
float loss_pct;
} PunktfunkProbeResult;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
@@ -562,6 +597,30 @@ PunktfunkConnection *punktfunk_connect_ex2(const char *host,
uint32_t timeout_ms);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Like [`punktfunk_connect_ex2`], but additionally requests the video encoder `bitrate_kbps`
// (kilobits per second). `0` lets the host pick its default; any other value is clamped to the
// host's supported range. After a speed test ([`punktfunk_connection_speed_test`]) a client can
// reconnect (or pick at connect time) with the measured rate. The value the host actually
// configured is readable via [`punktfunk_connection_bitrate`].
//
// # Safety
// Same as [`punktfunk_connect`].
PunktfunkConnection *punktfunk_connect_ex3(const char *host,
uint16_t port,
uint32_t width,
uint32_t height,
uint32_t refresh_hz,
uint32_t compositor,
uint32_t gamepad,
uint32_t bitrate_kbps,
const uint8_t *pin_sha256,
uint8_t *observed_sha256_out,
const char *client_cert_pem,
const char *client_key_pem,
uint32_t timeout_ms);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Generate a persistent client identity: a self-signed certificate + private key, both
// PEM, NUL-terminated, written into the caller's buffers. Generate ONCE, store both
@@ -715,6 +774,16 @@ PunktfunkStatus punktfunk_connection_mode(const PunktfunkConnection *c,
PunktfunkStatus punktfunk_connection_gamepad(const PunktfunkConnection *c, uint32_t *gamepad);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// The video encoder bitrate (kilobits per second) the host actually configured for this session
// — the [`punktfunk_connect_ex3`] request clamped to the host's range, or its default when `0`
// was requested. `0` = an older host that didn't report it. Safe any time after connect.
//
// # Safety
// `c` is a valid connection handle; `bitrate_kbps` is writable (NULL is skipped).
PunktfunkStatus punktfunk_connection_bitrate(const PunktfunkConnection *c, uint32_t *bitrate_kbps);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Ask the host to switch the live session to `width`x`height`@`refresh_hz` without
// reconnecting (window resized, refresh changed). Non-blocking enqueue: on acceptance the
@@ -731,6 +800,30 @@ PunktfunkStatus punktfunk_connection_request_mode(const PunktfunkConnection *c,
uint32_t refresh_hz);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Start a bandwidth speed test: ask the host to burst filler over the data plane at
// `target_kbps` of goodput for `duration_ms` (each clamped host-side to ≤ 1 Gbps / ≤ 5 s),
// *briefly pausing video*. Non-blocking — poll [`punktfunk_connection_probe_result`] until its
// `done` field is 1. Starting a probe resets any prior measurement.
//
// # Safety
// `c` is a valid connection handle.
PunktfunkStatus punktfunk_connection_speed_test(const PunktfunkConnection *c,
uint32_t target_kbps,
uint32_t duration_ms);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Read the current speed-test measurement into `*out` (partial until `out->done == 1`). Safe to
// poll repeatedly after [`punktfunk_connection_speed_test`]; before any probe it reports zeros.
//
// # Safety
// `c` is a valid connection handle; `out` is writable for one `PunktfunkProbeResult` (NULL is an
// error).
PunktfunkStatus punktfunk_connection_probe_result(const PunktfunkConnection *c,
PunktfunkProbeResult *out);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Close the connection and free the handle (joins the internal threads). NULL is a no-op.
//