Merge branch 'worktree-wave2-pw5-encode-overlap' into worktree-wave2-pyrowave
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m9s
ci / bun-nix (pull_request) Successful in 1m19s
ci / docs-site (pull_request) Successful in 1m57s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m13s
ci / web (pull_request) Successful in 3m25s
android / android (pull_request) Successful in 5m4s
ci / rust-arm64 (pull_request) Successful in 6m29s
ci / rust (pull_request) Successful in 11m37s
nix / flake (pull_request) Successful in 18m17s

# Conflicts:
#	crates/pf-encode/src/enc/linux/pyrowave.rs
This commit is contained in:
2026-08-09 01:14:59 +02:00
9 changed files with 1387 additions and 306 deletions
+120 -1
View File
@@ -73,6 +73,10 @@ struct UserData {
/// PW4 step 1: the producer-fence wait distribution, measured on this (the PipeWire loop)
/// thread. Per-session, like the fall-through tally.
fence_wait: FenceWaitStats,
/// PW5 step 1: the negotiated buffer-pool depth, counted from `add_buffer`/`remove_buffer`.
/// See [`PoolCensus`] — this is the number that decides whether a deeper encode pipeline is
/// safe, and until now nobody had it.
pool: PoolCensus,
/// Raw-passthrough frames that silently fell through to the CPU de-pad path, by reason — see
/// [`PassthroughFallbacks`]. Per-session: a fresh `UserData` is built per pipeline, so a
/// compositor that starts serving dmabufs again after a rebuild gets a fresh log budget.
@@ -503,6 +507,51 @@ impl FenceWaitStats {
}
}
/// PW5 stage 1: how many buffers the producer actually allocated for this stream.
///
/// **Nothing in this codebase had ever counted them.** The zero-copy path dups the dmabuf fd and
/// publishes the frame while the SPA buffer is handed straight back to the producer at `.process`
/// return — so the only thing keeping capture untorn is that the producer round-robins a pool
/// deeper than our import+encode window. That depth was an unmeasured assumption; this makes it a
/// logged number, on every producer, before anything is built on it.
///
/// `live` is maintained by the `add_buffer`/`remove_buffer` stream callbacks, which PipeWire fires
/// on the loop thread as the pool is allocated (and again, remove-then-add, on a renegotiation that
/// replaces it). There is no "pool complete" event, so the count is published from `.process`: by
/// the time the first buffer is dequeued the allocation has finished.
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct PoolCensus {
/// Buffers currently in the pool (adds minus removes).
live: u32,
/// Deepest `live` seen this session — the number a depth decision must key on, since a
/// renegotiation can transiently shrink the pool to zero.
high_water: u32,
/// The `live` value already logged, so a stable pool logs exactly one line per distinct depth
/// (a renegotiation that changes the depth is worth a second line; 240 frames a second of the
/// same number is not).
logged: Option<u32>,
}
impl PoolCensus {
fn add(&mut self) {
self.live += 1;
self.high_water = self.high_water.max(self.live);
}
fn remove(&mut self) {
self.live = self.live.saturating_sub(1);
}
/// Called from `.process`, once per buffer. Returns `Some(live)` the first time each distinct
/// depth is seen — the caller logs then and only then.
fn note_frame(&mut self) -> Option<u32> {
(self.logged != Some(self.live)).then(|| {
self.logged = Some(self.live);
self.live
})
}
}
/// Per-session tally of raw-passthrough frames that fell through to the CPU path, with a one-line
/// budget per distinct reason.
///
@@ -1399,6 +1448,7 @@ pub fn pipewire_thread(
linear_nv12_failed: false,
dbg_log_n: 0,
fence_wait: FenceWaitStats::default(),
pool: PoolCensus::default(),
passthrough_fallbacks: PassthroughFallbacks::default(),
cursor: CursorState::new(cursor_id0_hides),
expect_dims: if expect_exact_dims {
@@ -1497,6 +1547,11 @@ pub fn pipewire_thread(
}
}
})
// PW5 stage 1 — the pool census. PipeWire fires these on the loop thread as it allocates
// (and, on a renegotiation, frees then re-allocates) the stream's buffers. Counting only:
// the buffer pointer is not touched, so no lifetime question arises here.
.add_buffer(|_stream, ud, _buf| ud.pool.add())
.remove_buffer(|_stream, ud, _buf| ud.pool.remove())
.process(|stream, ud| {
// Latest-frame-only (OBS pattern): Mutter delivers buffers in bursts and recycles its
// pool; an older queued buffer carries a STALE frame. Drain all queued buffers, requeue
@@ -1525,6 +1580,25 @@ pub fn pipewire_thread(
newest = next;
drained += 1;
}
// PW5 stage 1: publish the depth the producer actually negotiated, once per distinct
// value. MEASURED, not requested: `build_dmabuf_buffers` asks for a range and the
// producer picks — this line is the only place the picked number is visible.
//
// Why it matters beyond curiosity: `stream.queue_raw_buffer(newest)` at the end of this
// callback hands the buffer back while the encode thread may still be importing and
// reading its dmabuf, so content stability rests entirely on the producer not cycling
// back to this buffer before we are done with it. That window is `pool_depth` buffer
// periods wide. A pool of 2 has essentially none.
if let Some(depth) = ud.pool.note_frame() {
tracing::info!(
pool_depth = depth,
high_water = ud.pool.high_water,
drained,
"pipewire buffer pool negotiated — this is the producer's ACTUAL count \
(add_buffer/remove_buffer), the window in which a buffer we handed back may \
be rewritten while the encoder still reads it"
);
}
// Sacrificial-mode gate (kwin.rs `create`): until the producer renegotiates to the
// expected dims, every buffer — frame AND cursor meta, whose positions are in the
// doomed mode's space — belongs to the birth mode; consuming one would build the
@@ -2130,7 +2204,7 @@ mod tests {
use super::{
consumer_kind, resolved_capture_arm, CaptureArm, ConsumerKind, FenceWaitStats,
PassthroughFallback, PassthroughFallbacks, FENCE_WAIT_BUCKETS_US,
PassthroughFallback, PassthroughFallbacks, PoolCensus, FENCE_WAIT_BUCKETS_US,
};
/// A PyroWave session is PyroWave even though it also flips `backend_is_vaapi` on (the
@@ -2344,4 +2418,49 @@ mod tests {
);
}
}
/// PW5 stage 1: a stable pool logs ONE line, not one per frame. `.process` runs at the capture
/// rate — an unconditional log here would be 240 lines a second of the same number.
#[test]
fn a_stable_pool_is_logged_once() {
let mut p = PoolCensus::default();
for _ in 0..8 {
p.add();
}
assert_eq!(p.note_frame(), Some(8));
for _ in 0..100 {
assert_eq!(p.note_frame(), None, "the same depth must not re-log");
}
}
/// A renegotiation frees the pool and re-allocates it. The LIVE count therefore dips (and the
/// new depth is worth a second line), but `high_water` — the number a pipeline-depth decision
/// keys on — must not follow the dip down.
#[test]
fn a_renegotiated_pool_relogs_but_the_high_water_holds() {
let mut p = PoolCensus::default();
for _ in 0..8 {
p.add();
}
assert_eq!(p.note_frame(), Some(8));
for _ in 0..8 {
p.remove();
}
for _ in 0..4 {
p.add();
}
assert_eq!(p.note_frame(), Some(4), "a changed depth is worth a line");
assert_eq!(p.high_water, 8, "the deepest pool seen this session");
}
/// `remove_buffer` without a matching `add_buffer` must not wrap the count to `u32::MAX` —
/// a depth gate reading that would happily pipeline against a pool of zero.
#[test]
fn unmatched_removes_saturate_at_zero() {
let mut p = PoolCensus::default();
p.remove();
p.remove();
assert_eq!(p.note_frame(), Some(0));
assert_eq!(p.high_water, 0);
}
}
+90 -6
View File
@@ -288,16 +288,57 @@ pub(super) fn build_shm_only_buffers() -> Result<Vec<u8>> {
})
}
/// Build a Buffers param requesting dmabuf-only buffers.
/// PW5 stage 2: the buffer-pool depth we ASK for on the zero-copy path, as a Choice range.
///
/// The zero-copy path hands the SPA buffer back to the producer at `.process` return, while the
/// encode thread still holds a dup of its dmabuf fd and has not yet imported, let alone read, the
/// contents. Nothing bounds that window — see the `queue_raw_buffer` comment in `pipewire.rs` — so
/// the only thing that keeps capture untorn is the producer round-robining a pool deeper than our
/// import+encode latency. Until PW5 stage 1 nobody had ever counted what that pool was; we never
/// even asked for a size (`build_dmabuf_buffers` set `dataType` and nothing else).
///
/// A **range**, deliberately, not a fixed count: SPA intersects the consumer's and producer's
/// Buffers params, so a fixed 8 against a producer that can only afford 4 empties the intersection
/// and the link silently stalls in "negotiating" — the exact failure mode the cursor-meta `size`
/// property already cost this codebase once (see `build_cursor_meta_param`). With a range the
/// producer clamps into it and negotiation still succeeds.
///
/// The numbers: `min` stays at 2 so nothing that works today stops working; `default` 8 is ~133 ms
/// of buffer at 60 Hz and ~33 ms at 240 Hz, comfortably past the ~3-4 ms capture→fence latency
/// measured in PW3/PW4 even with a second frame in flight; `max` 16 is a ceiling, not a request
/// (a 4K 4:4:4 buffer is ~25 MB, so 16 is ~400 MB of compositor allocation and worth capping).
/// **What the producer actually picks is logged by the stage-1 census — trust that line, not
/// these constants.**
const POOL_MIN: i32 = 2;
const POOL_DEFAULT: i32 = 8;
const POOL_MAX: i32 = 16;
/// Build a Buffers param requesting dmabuf-only buffers, with pool headroom (see [`POOL_DEFAULT`]).
pub(super) fn build_dmabuf_buffers() -> Result<Vec<u8>> {
serialize_pod(pw::spa::pod::Object {
type_: pw::spa::utils::SpaTypes::ObjectParamBuffers.as_raw(),
id: pw::spa::param::ParamType::Buffers.as_raw(),
properties: vec![pw::spa::pod::Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_dataType,
flags: pw::spa::pod::PropertyFlags::empty(),
value: pw::spa::pod::Value::Int(1i32 << pw::spa::sys::SPA_DATA_DmaBuf),
}],
properties: vec![
pw::spa::pod::Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_dataType,
flags: pw::spa::pod::PropertyFlags::empty(),
value: pw::spa::pod::Value::Int(1i32 << pw::spa::sys::SPA_DATA_DmaBuf),
},
pw::spa::pod::Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_buffers,
flags: pw::spa::pod::PropertyFlags::empty(),
value: pw::spa::pod::Value::Choice(pw::spa::pod::ChoiceValue::Int(
pw::spa::utils::Choice(
pw::spa::utils::ChoiceFlags::empty(),
pw::spa::utils::ChoiceEnum::Range {
default: POOL_DEFAULT,
min: POOL_MIN,
max: POOL_MAX,
},
),
)),
},
],
})
}
@@ -512,4 +553,47 @@ mod tests {
"libspa renumbered spa_video_transfer_function — update the hardcoded PQ id"
);
}
/// PW5 stage 2: the pool request must be a **Choice Range**, never a fixed Int.
///
/// This is the whole safety argument for asking at all: SPA intersects the two sides' Buffers
/// params, so a fixed count a producer cannot afford empties the intersection and the link
/// stalls in "negotiating" with no error anywhere — the same trap that cost this codebase the
/// entire Linux cursor channel once (see `build_cursor_meta_param`). Asserting the pod shape
/// is what keeps a later "simplify" from turning the range back into a number.
#[test]
fn the_dmabuf_pool_request_is_a_range_not_a_fixed_count() {
let pod = build_dmabuf_buffers().unwrap();
let key = spa::sys::SPA_PARAM_BUFFERS_buffers.to_ne_bytes();
let at = pod
.windows(4)
.position(|w| w == key)
.expect("the dmabuf Buffers pod must carry a buffers count");
let word = |off: usize| u32::from_ne_bytes(pod[off..off + 4].try_into().unwrap());
// Property = { key, flags, value_pod }; value_pod = { size, type, body }. A Choice body
// is { type: u32, flags: u32, child_size: u32, child_type: u32, values… }.
assert_eq!(
word(at + 12),
spa::sys::SPA_TYPE_Choice,
"the buffers count must be a Choice, not a bare Int — a fixed count can fail \
negotiation outright"
);
assert_eq!(
word(at + 16),
spa::sys::SPA_CHOICE_Range,
"the Choice must be a Range (default, min, max)"
);
assert_eq!(word(at + 24), 4, "Choice child pods are 4-byte Ints");
assert_eq!(word(at + 28), spa::sys::SPA_TYPE_Int, "…of type Int");
let vals: Vec<i32> = (0..3)
.map(|i| i32::from_ne_bytes(pod[at + 32 + i * 4..at + 36 + i * 4].try_into().unwrap()))
.collect();
assert_eq!(
vals,
vec![POOL_DEFAULT, POOL_MIN, POOL_MAX],
"Range values are serialized default-first"
);
// The minimum must not exceed what producers already serve, or the ask becomes a demand.
const { assert!(POOL_MIN <= 2) };
}
}
File diff suppressed because it is too large Load Diff
+18
View File
@@ -53,6 +53,24 @@ pub(crate) fn stamp_color_bits(bitstream: &mut [u8], seq_offset: usize, bt2020_p
}
}
/// Read the 3-bit wire sequence counter out of a pyrowave block header.
///
/// Every block header is `{ u16 ballot; u16 payload_words:12, sequence:3, extended:1; u32 ... }`
/// (`pyrowave_common.hpp`, `static_assert(sizeof == 8)`), so the counter is bits 12..14 of the
/// little-endian half-word at `packet_offset + 2` — the same word `stamp_color_bits` reaches into
/// from the other end.
///
/// This field is the entire frame-boundary signal on the wire: the decoder restarts a frame only
/// when the value CHANGES (`diff = (hdr.sequence - last_seq) & 0x7; restart = diff != 0`), so a
/// repeated value is read as more blocks of the same frame. That is why PW5's alternating encoder
/// handles need `pyrowave_encoder_set_next_sequence`, and why a test asserts this reader sees
/// +1 mod 8 across the pair.
pub(crate) fn wire_sequence(bitstream: &[u8], packet_offset: usize) -> Option<u8> {
let lo = *bitstream.get(packet_offset + 2)?;
let hi = *bitstream.get(packet_offset + 3)?;
Some(((u16::from_le_bytes([lo, hi]) >> 12) & 0x7) as u8)
}
/// The wavelet block space's total 32x32-block count for a mode — the exact counting walk of
/// upstream `WaveletBuffers::init_block_meta` (also ported to the Apple `WaveletLayout`, whose
/// golden tests pin it against real host AUs). Needed because the vendored RDO pass packs the
@@ -0,0 +1,123 @@
Encoder wire-sequence override — PUNKTFUNK LOCAL PATCH.
Not upstream. Exposes `Encoder::set_next_sequence(uint32_t)` (and a
`pyrowave_encoder_set_next_sequence` C entry) so the caller can stamp the 3-bit wire sequence
counter itself instead of relying on the encoder object's private one.
WHY IT EXISTS. PyroWave's `Encoder` structurally cannot hold two frames in flight: `Encoder::Impl`
owns ONE each of `wavelet_img_high_res`, `bucket_buffer`, `meta_buffer`, `block_stat_buffer`,
`payload_data` and `quant_buffer`, and `Impl::encode` OPENS by discarding them — an image barrier
with `VK_IMAGE_LAYOUT_UNDEFINED` as the old layout (a written promise nothing else is reading it)
plus three `fill_buffer` clears. Two `encode()` calls recorded into two command buffers and
submitted to the same queue have no execution dependency in Vulkan, so encode N+1's DWT would
overwrite the bands and zero the RDO buckets while encode N's block packing still reads them.
So overlapping frames means TWO encoder handles on one device, alternated — which is fine for
every resource above, because each handle gets its own. It is NOT fine for `sequence_count`, which
also lives on `Impl` and is stamped into every block header (pyrowave_encoder.cpp `packing_push`).
Two alternating handles each count 1,2,3... independently, so the wire sees 1,1,2,2,3,3...
That is silently fatal on the decode side. `pyrowave_decoder.cpp` computes
`diff = (hdr.sequence - last_seq) & 0x7` and treats `restart = diff != 0`, so a REPEATED value
reads as "more blocks of the same frame": `clear()` never runs, `decoded_frame_for_current_sequence`
stays true, and every second frame is swallowed. The symptom is "it works, just at half rate, with
occasional mixed-frame blocks" — the kind of failure that passes a smoke test. It would hit every
client, since pf-client-core and the Apple Metal hand-port parse the same field.
WHAT IT DOES. `set_next_sequence(seq)` stores `(seq - 1) & SequenceCountMask`, because
`Impl::encode` pre-increments before stamping — the setter's contract is about the next ENCODE, not
the next store. The Rust side keeps one monotonic counter across both handles and calls this before
each encode, so the wire sequence increments by exactly 1 mod 8 regardless of which handle produced
the frame.
INERT WHEN UNUSED. Nothing calls it unless the caller does, so the single-handle paths — including
the whole Windows backend — behave exactly as before. No `.def` change is needed: the C API is
built as a static archive (crates/pyrowave-sys/CMakeLists.txt).
Upstream status: not reported. It is a hook for a use case upstream explicitly designed against
("For low-latency use cases, overlapping frames in encode is meaningless due to latency and the
encoder is so fast anyway" — pyrowave.h). That reasoning holds at 1080p60 and stops holding at 4K
or under a GPU-bound game, which is what PW5 measured.
diff --git a/crates/pyrowave-sys/vendor/pyrowave/pyrowave.h b/crates/pyrowave-sys/vendor/pyrowave/pyrowave.h
index fc0d5834..aeb22ffc 100644
--- a/crates/pyrowave-sys/vendor/pyrowave/pyrowave.h
+++ b/crates/pyrowave-sys/vendor/pyrowave/pyrowave.h
@@ -476,6 +476,19 @@ PYROWAVE_PUBLIC_API pyrowave_result
pyrowave_encoder_packetize(pyrowave_encoder encoder, pyrowave_packet *packets, size_t packet_boundary,
size_t *out_packets, void *bitstream, size_t size);
+// PUNKTFUNK LOCAL EXTENSION (patches/0007-encoder-sequence-override.patch), not upstream.
+// The wire sequence counter is 3 bits (PyroWave::SequenceCountMask, pyrowave_common.hpp);
+// exported here so callers mask with the codec's own value instead of a copied literal.
+#define PYROWAVE_SEQUENCE_MASK 0x7u
+
+// Overrides the 3-bit wire sequence counter the NEXT encode will stamp into every block header.
+// The counter lives on the encoder object, so a caller that alternates TWO encoders to overlap
+// frames emits 1,1,2,2,3,3... and the decoder — which restarts a frame only when the value
+// CHANGES — reads the repeat as more blocks of the same frame and silently swallows every second
+// frame. Stamp a single monotonic counter across the handles with this. Value is masked to 3 bits.
+PYROWAVE_PUBLIC_API pyrowave_result
+pyrowave_encoder_set_next_sequence(pyrowave_encoder encoder, uint32_t sequence);
+
// Implementation ensures GPU is idle before destroying objects.
PYROWAVE_PUBLIC_API void
pyrowave_encoder_destroy(pyrowave_encoder encoder);
diff --git a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp
index 985cd0a9..fcd7d6f8 100644
--- a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp
+++ b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp
@@ -1196,6 +1196,17 @@ pyrowave_encoder_packetize(pyrowave_encoder encoder, pyrowave_packet *packets, s
return PYROWAVE_SUCCESS;
}
+// PUNKTFUNK LOCAL EXTENSION (patches/0007-encoder-sequence-override.patch), not upstream.
+pyrowave_result
+pyrowave_encoder_set_next_sequence(pyrowave_encoder encoder, uint32_t sequence)
+{
+ Util::set_thread_logging_interface(&null_logger);
+ if (!encoder)
+ return PYROWAVE_ERROR_GENERIC;
+ encoder->encoder.set_next_sequence(sequence);
+ return PYROWAVE_SUCCESS;
+}
+
void pyrowave_encoder_destroy(pyrowave_encoder encoder)
{
auto *device = encoder->device;
diff --git a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.cpp b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.cpp
index ad4e9746..f23717f3 100644
--- a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.cpp
+++ b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.cpp
@@ -1230,6 +1230,14 @@ bool Encoder::encode(CommandBuffer &cmd, const ViewBuffers &views, const Bitstre
return impl->encode(cmd, views, buffers);
}
+// PUNKTFUNK: see the declaration in pyrowave_encoder.hpp. Impl::encode PRE-increments
+// (sequence_count = (sequence_count + 1) & mask before stamping), so store one less than the value
+// the caller wants stamped — the setter's contract is about the next ENCODE, not the next store.
+void Encoder::set_next_sequence(uint32_t sequence)
+{
+ impl->sequence_count = (sequence - 1) & SequenceCountMask;
+}
+
const Vulkan::ImageView &Encoder::get_wavelet_band(int component, int level)
{
return *impl->component_layer_views[component][level];
diff --git a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.hpp b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.hpp
index a65447d5..8c0ef0d0 100644
--- a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.hpp
+++ b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_encoder.hpp
@@ -37,6 +37,12 @@ public:
bool init(Vulkan::Device *device, int width, int height, ChromaSubsampling chroma);
bool encode(Vulkan::CommandBuffer &cmd, const ViewBuffers &views, const BitstreamBuffers &buffers);
+ // PUNKTFUNK: override the 3-bit wire sequence counter the NEXT encode will stamp.
+ // The counter is per-Encoder, so alternating two encoder objects to overlap frames emits
+ // 1,1,2,2,3,3... and the decoder reads a repeated value as "more blocks of the same frame".
+ // See crates/pyrowave-sys/patches/0007-encoder-sequence-override.patch.
+ void set_next_sequence(uint32_t sequence);
+
// Debug hackery
const Vulkan::ImageView &get_wavelet_band(int component, int level);
bool encode_pre_transformed(Vulkan::CommandBuffer &cmd, const BitstreamBuffers &buffers, float quant_scale);
+13
View File
@@ -476,6 +476,19 @@ PYROWAVE_PUBLIC_API pyrowave_result
pyrowave_encoder_packetize(pyrowave_encoder encoder, pyrowave_packet *packets, size_t packet_boundary,
size_t *out_packets, void *bitstream, size_t size);
// PUNKTFUNK LOCAL EXTENSION (patches/0007-encoder-sequence-override.patch), not upstream.
// The wire sequence counter is 3 bits (PyroWave::SequenceCountMask, pyrowave_common.hpp);
// exported here so callers mask with the codec's own value instead of a copied literal.
#define PYROWAVE_SEQUENCE_MASK 0x7u
// Overrides the 3-bit wire sequence counter the NEXT encode will stamp into every block header.
// The counter lives on the encoder object, so a caller that alternates TWO encoders to overlap
// frames emits 1,1,2,2,3,3... and the decoder — which restarts a frame only when the value
// CHANGES — reads the repeat as more blocks of the same frame and silently swallows every second
// frame. Stamp a single monotonic counter across the handles with this. Value is masked to 3 bits.
PYROWAVE_PUBLIC_API pyrowave_result
pyrowave_encoder_set_next_sequence(pyrowave_encoder encoder, uint32_t sequence);
// Implementation ensures GPU is idle before destroying objects.
PYROWAVE_PUBLIC_API void
pyrowave_encoder_destroy(pyrowave_encoder encoder);
+11
View File
@@ -1196,6 +1196,17 @@ pyrowave_encoder_packetize(pyrowave_encoder encoder, pyrowave_packet *packets, s
return PYROWAVE_SUCCESS;
}
// PUNKTFUNK LOCAL EXTENSION (patches/0007-encoder-sequence-override.patch), not upstream.
pyrowave_result
pyrowave_encoder_set_next_sequence(pyrowave_encoder encoder, uint32_t sequence)
{
Util::set_thread_logging_interface(&null_logger);
if (!encoder)
return PYROWAVE_ERROR_GENERIC;
encoder->encoder.set_next_sequence(sequence);
return PYROWAVE_SUCCESS;
}
void pyrowave_encoder_destroy(pyrowave_encoder encoder)
{
auto *device = encoder->device;
@@ -1230,6 +1230,14 @@ bool Encoder::encode(CommandBuffer &cmd, const ViewBuffers &views, const Bitstre
return impl->encode(cmd, views, buffers);
}
// PUNKTFUNK: see the declaration in pyrowave_encoder.hpp. Impl::encode PRE-increments
// (sequence_count = (sequence_count + 1) & mask before stamping), so store one less than the value
// the caller wants stamped — the setter's contract is about the next ENCODE, not the next store.
void Encoder::set_next_sequence(uint32_t sequence)
{
impl->sequence_count = (sequence - 1) & SequenceCountMask;
}
const Vulkan::ImageView &Encoder::get_wavelet_band(int component, int level)
{
return *impl->component_layer_views[component][level];
@@ -37,6 +37,12 @@ public:
bool init(Vulkan::Device *device, int width, int height, ChromaSubsampling chroma);
bool encode(Vulkan::CommandBuffer &cmd, const ViewBuffers &views, const BitstreamBuffers &buffers);
// PUNKTFUNK: override the 3-bit wire sequence counter the NEXT encode will stamp.
// The counter is per-Encoder, so alternating two encoder objects to overlap frames emits
// 1,1,2,2,3,3... and the decoder reads a repeated value as "more blocks of the same frame".
// See crates/pyrowave-sys/patches/0007-encoder-sequence-override.patch.
void set_next_sequence(uint32_t sequence);
// Debug hackery
const Vulkan::ImageView &get_wavelet_band(int component, int level);
bool encode_pre_transformed(Vulkan::CommandBuffer &cmd, const BitstreamBuffers &buffers, float quant_scale);