Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 491a344f23 |
@@ -27,9 +27,13 @@ ui = ["dep:pf-console-ui", "dep:serde_json"]
|
|||||||
# Same Linux+Windows gating as the rest of the client stack; elsewhere this is a stub
|
# Same Linux+Windows gating as the rest of the client stack; elsewhere this is a stub
|
||||||
# binary.
|
# binary.
|
||||||
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||||
pf-presenter = { path = "../../crates/pf-presenter" }
|
# `default-features = false` on both: THIS crate's `pyrowave` feature (above) is the single
|
||||||
|
# switch that turns the wavelet codec on, and it enables it explicitly on each. Inheriting their
|
||||||
|
# defaults instead would make `--no-default-features` a lie — the Windows ARM64 leg builds that
|
||||||
|
# way precisely to skip the vendored PyroWave C++, which has no ARM64 SIMD path.
|
||||||
|
pf-presenter = { path = "../../crates/pf-presenter", default-features = false }
|
||||||
pf-console-ui = { path = "../../crates/pf-console-ui", optional = true }
|
pf-console-ui = { path = "../../crates/pf-console-ui", optional = true }
|
||||||
pf-client-core = { path = "../../crates/pf-client-core" }
|
pf-client-core = { path = "../../crates/pf-client-core", default-features = false }
|
||||||
punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
|
punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
|
||||||
# The fake-library dev hook (`PUNKTFUNK_FAKE_LIBRARY`, browse mode) parses GameEntry JSON.
|
# The fake-library dev hook (`PUNKTFUNK_FAKE_LIBRARY`, browse mode) parses GameEntry JSON.
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
|
|||||||
@@ -29,7 +29,17 @@ punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
|
|||||||
# The shared client service layer: the trust/settings stores (ONE `Settings` struct for the
|
# The shared client service layer: the trust/settings stores (ONE `Settings` struct for the
|
||||||
# shell and the spawned session binary — src/trust.rs re-exports it) and the game-library
|
# shell and the spawned session binary — src/trust.rs re-exports it) and the game-library
|
||||||
# data model (fetch + art pipeline) behind the library page.
|
# data model (fetch + art pipeline) behind the library page.
|
||||||
pf-client-core = { path = "../../crates/pf-client-core" }
|
#
|
||||||
|
# `default-features = false` drops pf-client-core's default `pyrowave`, which would otherwise
|
||||||
|
# build the vendored PyroWave C++ INTO THE SHELL — dead weight here (the shell never decodes;
|
||||||
|
# it only offers "pyrowave" as a codec preference string the session binary acts on) and fatal
|
||||||
|
# on ARM64, where Granite's math falls back to x86 SSE intrinsics and stops at
|
||||||
|
# `simd.hpp: #error "Implement me."`. This does NOT drop PyroWave from the Windows client:
|
||||||
|
# decode lives in the spawned punktfunk-session binary, whose own default enables the feature,
|
||||||
|
# and cargo's feature unification turns it back on for the shared pf-client-core whenever that
|
||||||
|
# binary is in the same build (x64). On the ARM64 leg both are built --no-default-features, so
|
||||||
|
# nothing enables it and the C++ is never compiled.
|
||||||
|
pf-client-core = { path = "../../crates/pf-client-core", default-features = false }
|
||||||
|
|
||||||
# WinUI 3 UI via windows-reactor (a declarative React-like framework backed by WinUI). Its
|
# WinUI 3 UI via windows-reactor (a declarative React-like framework backed by WinUI). Its
|
||||||
# `build.rs` downloads the Windows App SDK NuGets and stages the bootstrap DLL + resources.pri
|
# `build.rs` downloads the Windows App SDK NuGets and stages the bootstrap DLL + resources.pri
|
||||||
|
|||||||
@@ -59,9 +59,14 @@
|
|||||||
</Application>
|
</Application>
|
||||||
<!--
|
<!--
|
||||||
Second entry point: the couch/console UI, for an HTPC or a TV-attached box where the
|
Second entry point: the couch/console UI, for an HTPC or a TV-attached box where the
|
||||||
desktop shell is the wrong first screen. Same full-trust executable, launched with
|
desktop shell is the wrong first screen. Its own executable (punktfunk-console.exe)
|
||||||
`--console`, which hands straight off to the session binary's controller-driven
|
because an MSIX Application entry cannot pass arguments to a full-trust exe; it hands
|
||||||
browse mode (host list, pairing, settings, library) fullscreen.
|
straight off to the session binary's controller-driven browse mode (host list,
|
||||||
|
pairing, settings, library) fullscreen.
|
||||||
|
|
||||||
|
NOTE: never write a double hyphen in this file. XML forbids it inside a comment, and
|
||||||
|
makepri rejects the whole manifest ("Appx manifest not found or is invalid") — which
|
||||||
|
is exactly how the console flag spelled out here broke the v0.15.0 MSIX build.
|
||||||
-->
|
-->
|
||||||
<Application Id="PunktfunkConsole" Executable="punktfunk-console.exe"
|
<Application Id="PunktfunkConsole" Executable="punktfunk-console.exe"
|
||||||
EntryPoint="Windows.FullTrustApplication">
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
|
|||||||
@@ -1851,20 +1851,7 @@ impl VulkanVideoEncoder {
|
|||||||
let f = &self.frames[slot];
|
let f = &self.frames[slot];
|
||||||
let mut fb = [[0u32; 2]; 1];
|
let mut fb = [[0u32; 2]; 1];
|
||||||
dev.get_query_pool_results(f.query_pool, 0, &mut fb, vk::QueryResultFlags::WAIT)?;
|
dev.get_query_pool_results(f.query_pool, 0, &mut fb, vk::QueryResultFlags::WAIT)?;
|
||||||
// The (offset, bytes-written) pair is driver-reported: validate it against the bitstream
|
let (off, len) = (fb[0][0] as usize, fb[0][1] as usize);
|
||||||
// allocation BEFORE mapping, or the `from_raw_parts` below reads outside the buffer and
|
|
||||||
// ships whatever it finds straight onto the wire. Checked in u64 so the add cannot wrap,
|
|
||||||
// and before `map_memory` so there is no unmap to unwind on the error path.
|
|
||||||
let (off64, len64) = (fb[0][0] as u64, fb[0][1] as u64);
|
|
||||||
if off64.saturating_add(len64) > self.bs_size {
|
|
||||||
anyhow::bail!(
|
|
||||||
"vulkan-encode: driver reported bitstream feedback offset={off64} \
|
|
||||||
bytes_written={len64}, outside the {} byte bitstream buffer — the encode likely \
|
|
||||||
overflowed its destination range",
|
|
||||||
self.bs_size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let (off, len) = (off64 as usize, len64 as usize);
|
|
||||||
let p =
|
let p =
|
||||||
dev.map_memory(f.bs_mem, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *const u8;
|
dev.map_memory(f.bs_mem, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *const u8;
|
||||||
let prefix: &[u8] = if f.keyframe {
|
let prefix: &[u8] = if f.keyframe {
|
||||||
@@ -1936,28 +1923,7 @@ impl Encoder for VulkanVideoEncoder {
|
|||||||
if first_frame < 0 || first_frame > last_frame {
|
if first_frame < 0 || first_frame > last_frame {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Taint sweep BEFORE picking the anchor (the fecbec2d fix AMF and QSV got; this backend was
|
|
||||||
// carved out one commit later and never received it). "Resident and older than THIS loss" is
|
|
||||||
// not the same as "the client decoded it": after an earlier loss [a,b] was recovered at wire
|
|
||||||
// r, everything in [a, r-1] is undecodable at the client — the lost frames plus every frame
|
|
||||||
// that predicted through the gap. Those wires stay valid anchor candidates here until the
|
|
||||||
// 8-slot ring rolls them out, so a LATER loss can anchor on one and ship corruption tagged
|
|
||||||
// `recovery_anchor` — which is the client's definitive re-anchor signal (reanchor.rs), so it
|
|
||||||
// lifts the post-loss freeze onto a picture built from a reference it never had.
|
|
||||||
//
|
|
||||||
// Blank `slot_wire` ONLY. `slot_poc` must keep naming every physically-resident DPB picture
|
|
||||||
// for `build_h265_rps_s0`, or a conforming decoder evicts them and the anchor references a
|
|
||||||
// picture the client already dropped. `slot_wire` is the RFI/loss domain; `slot_poc` is the
|
|
||||||
// reference-delta domain. `prev_slot` and the normal P-frame path are indices, not wires, so
|
|
||||||
// ordinary prediction is unaffected.
|
|
||||||
for w in self.slot_wire.iter_mut() {
|
|
||||||
if *w >= first_frame {
|
|
||||||
*w = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Can we anchor a clean P-frame to a resident slot strictly older than the loss?
|
// Can we anchor a clean P-frame to a resident slot strictly older than the loss?
|
||||||
// (A sweep that empties every candidate yields `None` here and declines the RFI, matching
|
|
||||||
// `qsv_live_ltr_rfi_taint_sweep_declines`.)
|
|
||||||
match pick_recovery_slot(&self.slot_wire, first_frame) {
|
match pick_recovery_slot(&self.slot_wire, first_frame) {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
self.pending_loss = Some(first_frame);
|
self.pending_loss = Some(first_frame);
|
||||||
@@ -2731,62 +2697,6 @@ mod tests {
|
|||||||
assert_eq!(pick_recovery_slot(&[-1; 8], 5), None);
|
assert_eq!(pick_recovery_slot(&[-1; 8], 5), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The taint sweep (fecbec2d's fix, ported here): a slot encoded inside an EARLIER, still
|
|
||||||
/// unrepaired loss window must not become the "known-good" anchor of a LATER loss. Without the
|
|
||||||
/// sweep, `pick_recovery_slot` accepts it — it is resident and its wire is below the second
|
|
||||||
/// loss start — and the frame ships tagged `recovery_anchor`, lifting the client's freeze onto
|
|
||||||
/// a reference it never decoded.
|
|
||||||
#[test]
|
|
||||||
fn taint_sweep_excludes_slots_from_an_earlier_loss() {
|
|
||||||
// Apply the sweep exactly as `invalidate_ref_frames` does.
|
|
||||||
fn sweep(wires: &mut [i64], loss_first: i64) {
|
|
||||||
for w in wires.iter_mut() {
|
|
||||||
if *w >= loss_first {
|
|
||||||
*w = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slots hold wires 0..7. Loss 1 starts at wire 4, so wires 4..7 are undecodable at the
|
|
||||||
// client. A second loss report arrives at wire 6 while they are all still resident.
|
|
||||||
let tainted = [4i64, 5, 6, 7];
|
|
||||||
|
|
||||||
// WITHOUT the sweep this is the bug: the newest wire below 6 is wire 5 — squarely inside
|
|
||||||
// loss 1's unrepaired window — and it would be served as the "known-good" anchor.
|
|
||||||
let unswept = [0i64, 1, 2, 3, 4, 5, 6, 7];
|
|
||||||
let picked = pick_recovery_slot(&unswept, 6).expect("unswept picks something");
|
|
||||||
assert!(
|
|
||||||
tainted.contains(&unswept[picked]),
|
|
||||||
"precondition: without the sweep the anchor comes from the earlier loss window"
|
|
||||||
);
|
|
||||||
|
|
||||||
// WITH the sweep, loss 1 blanks 4..7, so loss 2 can only reach genuinely clean wires.
|
|
||||||
let mut wires = unswept;
|
|
||||||
sweep(&mut wires, 4);
|
|
||||||
assert_eq!(wires, [0, 1, 2, 3, -1, -1, -1, -1]);
|
|
||||||
let picked = pick_recovery_slot(&wires, 6).expect("clean wires remain");
|
|
||||||
assert_eq!(picked, 3, "newest clean survivor is wire 3");
|
|
||||||
assert!(!tainted.contains(&wires[picked]));
|
|
||||||
|
|
||||||
// Encoding resumes after recovery; wires 8..11 refill the swept slots and are clean. A
|
|
||||||
// later loss at wire 10 legitimately anchors on wire 9 — the sweep must not over-reject.
|
|
||||||
wires[4] = 8;
|
|
||||||
wires[5] = 9;
|
|
||||||
wires[6] = 10;
|
|
||||||
wires[7] = 11;
|
|
||||||
sweep(&mut wires, 10);
|
|
||||||
assert_eq!(
|
|
||||||
pick_recovery_slot(&wires, 10),
|
|
||||||
Some(5),
|
|
||||||
"wire 9 is post-recovery, clean"
|
|
||||||
);
|
|
||||||
|
|
||||||
// A loss covering every live wire leaves nothing clean → decline, caller serves an IDR.
|
|
||||||
let mut all = [5i64, 6, 7, 8, 9, 10, 11, 12];
|
|
||||||
sweep(&mut all, 5);
|
|
||||||
assert_eq!(pick_recovery_slot(&all, 5), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The full-retention RPS: every resident picture is listed (so the decoder keeps it), the
|
/// The full-retention RPS: every resident picture is listed (so the decoder keeps it), the
|
||||||
/// setup slot's dying occupant is not, and `used_by_curr_pic` marks exactly the real reference.
|
/// setup slot's dying occupant is not, and `used_by_curr_pic` marks exactly the real reference.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -714,16 +714,7 @@ pub struct QsvEncoder {
|
|||||||
/// `EncoderCaps::supports_rfi` and all per-frame marking/forcing below.
|
/// `EncoderCaps::supports_rfi` and all per-frame marking/forcing below.
|
||||||
ltr_active: bool,
|
ltr_active: bool,
|
||||||
/// The wire frame index stored in each LTR slot (`None` = never marked).
|
/// The wire frame index stored in each LTR slot (`None` = never marked).
|
||||||
///
|
|
||||||
/// This mirrors the HARDWARE DPB, so an entry must not be cleared merely because we distrust
|
|
||||||
/// it: nulling issues no VPL call, and the encoder keeps the frame marked long-term until that
|
|
||||||
/// `LongTermIdx` is re-marked or an IDR flushes it. Distrust is recorded in `ltr_tainted`
|
|
||||||
/// instead, so the rejection list can still NAME the entry the hardware is holding.
|
|
||||||
ltr_slots: [Option<i64>; NUM_LTR_SLOTS],
|
ltr_slots: [Option<i64>; NUM_LTR_SLOTS],
|
||||||
/// Per-slot taint from `invalidate_ref_frames`' sweep: the mark is still live in the hardware
|
|
||||||
/// DPB but was encoded inside the client's corrupt window, so it may not anchor a recovery —
|
|
||||||
/// it must be REJECTED instead. Cleared wherever the slot is re-marked or the DPB is flushed.
|
|
||||||
ltr_tainted: [bool; NUM_LTR_SLOTS],
|
|
||||||
next_ltr_slot: usize,
|
next_ltr_slot: usize,
|
||||||
ltr_mark_interval: i64,
|
ltr_mark_interval: i64,
|
||||||
/// Set by `invalidate_ref_frames`: the slot the next submitted frame force-references.
|
/// Set by `invalidate_ref_frames`: the slot the next submitted frame force-references.
|
||||||
@@ -798,7 +789,6 @@ impl QsvEncoder {
|
|||||||
ir_active: false,
|
ir_active: false,
|
||||||
ltr_active: false,
|
ltr_active: false,
|
||||||
ltr_slots: [None; NUM_LTR_SLOTS],
|
ltr_slots: [None; NUM_LTR_SLOTS],
|
||||||
ltr_tainted: [false; NUM_LTR_SLOTS],
|
|
||||||
next_ltr_slot: 0,
|
next_ltr_slot: 0,
|
||||||
ltr_mark_interval: ltr_mark_interval(fps),
|
ltr_mark_interval: ltr_mark_interval(fps),
|
||||||
pending_force: None,
|
pending_force: None,
|
||||||
@@ -923,7 +913,6 @@ impl QsvEncoder {
|
|||||||
self.ltr_active = ltr_active;
|
self.ltr_active = ltr_active;
|
||||||
self.ir_active = ir_active;
|
self.ir_active = ir_active;
|
||||||
self.ltr_slots = [None; NUM_LTR_SLOTS];
|
self.ltr_slots = [None; NUM_LTR_SLOTS];
|
||||||
self.ltr_tainted = [false; NUM_LTR_SLOTS];
|
|
||||||
self.next_ltr_slot = 0;
|
self.next_ltr_slot = 0;
|
||||||
self.pending_force = None;
|
self.pending_force = None;
|
||||||
self.hdr_applied = self.hdr_meta;
|
self.hdr_applied = self.hdr_meta;
|
||||||
@@ -1049,7 +1038,6 @@ impl Encoder for QsvEncoder {
|
|||||||
// An IDR voids the decoder's reference buffers — drop stale slots and any
|
// An IDR voids the decoder's reference buffers — drop stale slots and any
|
||||||
// queued force; the mark cadence below re-anchors on the IDR itself.
|
// queued force; the mark cadence below re-anchors on the IDR itself.
|
||||||
self.ltr_slots = [None; NUM_LTR_SLOTS];
|
self.ltr_slots = [None; NUM_LTR_SLOTS];
|
||||||
self.ltr_tainted = [false; NUM_LTR_SLOTS]; // the IDR flushed the DPB with them
|
|
||||||
self.next_ltr_slot = 0;
|
self.next_ltr_slot = 0;
|
||||||
self.pending_force = None;
|
self.pending_force = None;
|
||||||
} else if self.ltr_test_force_at == Some(cur_idx) {
|
} else if self.ltr_test_force_at == Some(cur_idx) {
|
||||||
@@ -1065,9 +1053,7 @@ impl Encoder for QsvEncoder {
|
|||||||
// emptied the slot since the force was queued. An empty slot means there is
|
// emptied the slot since the force was queued. An empty slot means there is
|
||||||
// nothing clean to re-reference — the frame must ship as a plain P WITHOUT the
|
// nothing clean to re-reference — the frame must ship as a plain P WITHOUT the
|
||||||
// `recovery_anchor` tag (the client lifts its post-loss freeze on that tag).
|
// `recovery_anchor` tag (the client lifts its post-loss freeze on that tag).
|
||||||
// The slot is no longer emptied by the sweep, so test the taint flag too — a
|
if let Some(idx) = self.ltr_slots[slot] {
|
||||||
// tainted slot is exactly the "nothing clean to re-reference" case.
|
|
||||||
if let Some(idx) = self.ltr_slots[slot].filter(|_| !self.ltr_tainted[slot]) {
|
|
||||||
force_ltr = Some((slot, idx));
|
force_ltr = Some((slot, idx));
|
||||||
recovery_anchor = true;
|
recovery_anchor = true;
|
||||||
}
|
}
|
||||||
@@ -1075,9 +1061,6 @@ impl Encoder for QsvEncoder {
|
|||||||
if force_ltr.is_none() && (forced || cur_idx % self.ltr_mark_interval == 0) {
|
if force_ltr.is_none() && (forced || cur_idx % self.ltr_mark_interval == 0) {
|
||||||
let slot = self.next_ltr_slot;
|
let slot = self.next_ltr_slot;
|
||||||
self.ltr_slots[slot] = Some(cur_idx);
|
self.ltr_slots[slot] = Some(cur_idx);
|
||||||
// Re-marking replaces the hardware's LongTermIdx: the tainted frame is gone from
|
|
||||||
// the DPB and this slot is clean again.
|
|
||||||
self.ltr_tainted[slot] = false;
|
|
||||||
self.next_ltr_slot = (self.next_ltr_slot + 1) % NUM_LTR_SLOTS;
|
self.next_ltr_slot = (self.next_ltr_slot + 1) % NUM_LTR_SLOTS;
|
||||||
mark_slot = Some(slot);
|
mark_slot = Some(slot);
|
||||||
}
|
}
|
||||||
@@ -1340,23 +1323,13 @@ impl Encoder for QsvEncoder {
|
|||||||
// loss ships corruption as the recovery anchor, and every subsequent mark re-samples
|
// loss ships corruption as the recovery anchor, and every subsequent mark re-samples
|
||||||
// the soup — the sustained-loss field failure where the picture never healed. Dropped
|
// the soup — the sustained-loss field failure where the picture never healed. Dropped
|
||||||
// slots stay dropped; the cadence re-marks a clean frame within ~1/4 s.
|
// slots stay dropped; the cadence re-marks a clean frame within ~1/4 s.
|
||||||
//
|
for marked in self.ltr_slots.iter_mut() {
|
||||||
// Mark tainted rather than clearing: `ltr_slots` mirrors the HARDWARE DPB, and nulling an
|
|
||||||
// entry issues no VPL call — the frame stays marked long-term in the encoder. Clearing it
|
|
||||||
// made the rejection list below (which iterates the post-sweep mirror and only names `Some`
|
|
||||||
// slots) silently SKIP the one entry the sweep exists to distrust, so the recovery frame
|
|
||||||
// could still predict from it. With two slots the "exactly one swept" case is the modal
|
|
||||||
// one, and it was the broken one.
|
|
||||||
for (slot, marked) in self.ltr_slots.iter().enumerate() {
|
|
||||||
if marked.is_some_and(|idx| idx >= first) {
|
if marked.is_some_and(|idx| idx >= first) {
|
||||||
self.ltr_tainted[slot] = true;
|
*marked = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut best: Option<(usize, i64)> = None;
|
let mut best: Option<(usize, i64)> = None;
|
||||||
for (slot, marked) in self.ltr_slots.iter().enumerate() {
|
for (slot, marked) in self.ltr_slots.iter().enumerate() {
|
||||||
if self.ltr_tainted[slot] {
|
|
||||||
continue; // still in the DPB, but encoded inside the corrupt window
|
|
||||||
}
|
|
||||||
if let Some(idx) = *marked {
|
if let Some(idx) = *marked {
|
||||||
if idx < first && best.is_none_or(|(_, b)| idx > b) {
|
if idx < first && best.is_none_or(|(_, b)| idx > b) {
|
||||||
best = Some((slot, idx));
|
best = Some((slot, idx));
|
||||||
@@ -1481,7 +1454,6 @@ impl Encoder for QsvEncoder {
|
|||||||
self.ltr_active = ltr;
|
self.ltr_active = ltr;
|
||||||
self.ir_active = ir;
|
self.ir_active = ir;
|
||||||
self.ltr_slots = [None; NUM_LTR_SLOTS];
|
self.ltr_slots = [None; NUM_LTR_SLOTS];
|
||||||
self.ltr_tainted = [false; NUM_LTR_SLOTS];
|
|
||||||
self.next_ltr_slot = 0;
|
self.next_ltr_slot = 0;
|
||||||
self.pending_force = None;
|
self.pending_force = None;
|
||||||
if let Some(inner) = self.inner.as_mut() {
|
if let Some(inner) = self.inner.as_mut() {
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ repository.workspace = true
|
|||||||
# Same Linux+Windows gating as the rest of the client stack (dmabuf import is the one
|
# Same Linux+Windows gating as the rest of the client stack (dmabuf import is the one
|
||||||
# Linux-only module — see lib.rs).
|
# Linux-only module — see lib.rs).
|
||||||
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||||
pf-client-core = { path = "../pf-client-core" }
|
# `default-features = false`: the PyroWave decode backend is turned on through THIS crate's own
|
||||||
|
# `pyrowave` feature (which re-exports it below), never by inheriting the dependency's default.
|
||||||
|
# Otherwise a consumer that deliberately builds us without `pyrowave` still drags the vendored
|
||||||
|
# C++ in — fatal on Windows ARM64, where Granite has no SIMD path.
|
||||||
|
pf-client-core = { path = "../pf-client-core", default-features = false }
|
||||||
# AVVkFrame access (Vulkan Video frames: live sync state under the frames lock).
|
# AVVkFrame access (Vulkan Video frames: live sync state under the frames lock).
|
||||||
pf-ffvk = { path = "../pf-ffvk" }
|
pf-ffvk = { path = "../pf-ffvk" }
|
||||||
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||||
|
|||||||
Reference in New Issue
Block a user