docs(pf-capture): truth pass over comments, docs and log strings (sweep Phase 1)
Every item here is prose that actively misdirects a maintainer or an operator. Landing it before
the defect fixes means those diffs get reviewed against accurate comments.
1.1 (X5) — split two merged doc blocks that documented the item ABOVE them: `hdr_meta`'s whole
contract was glued onto `fn cursor()` in lib.rs (leaving `hdr_meta` undocumented), and
`hdr_p010_selftest_at`'s was glued onto `hdr_p010_convert_bars_on_luid` in dxgi.rs. rustdoc for
the trait's central HDR contract was simply wrong.
1.2 (L9 doc half) — the `Capturer` trait doc advertised a "bounded drop-oldest channel". The
Linux portal's `sync_channel(8)` + `try_send` is drop-NEWEST: under a consumer stall the arriving
frame is discarded and the encoder gets the stalest queued one. Say so. (Phase 2.6 changes the
behaviour; this commit only stops the doc from lying about today's code.)
1.3 — delete the DONT_FIXATE claim from the dmabuf offer comment. `build_dmabuf_format` emits a
plain MANDATORY `ChoiceEnum::Enum` and `param_changed` re-emits nothing, so there is no two-step
DMA-BUF handshake here; libspa 0.9's `ChoiceFlags` cannot even express DONT_FIXATE. Recorded as a
real follow-up instead of an implemented thing.
1.4 — `mainloop.run()` no longer "blocks until process exit": the quit channel attached above it
stops the loop from `PortalCapturer::drop`.
1.5 (L11) — the 6-arm negotiation log ladder told a fully zero-copy PyroWave session "VAAPI
encode with the CPU capture path — zero-copy was disabled", one line after logging that it had
advertised the PyroWave device's dmabuf modifiers: the passthrough arm was gated on
`pyrowave_modifiers.is_empty()`, so the pyrowave case fell through to the CPU warning. Ungate it;
the CPU-path arm is now reachable only when no dmabuf is advertised at all, which is what it
claims. Its parenthetical also gains the third real cause (the session asked for CPU frames).
Control-flow-neutral for every other combination.
1.6 (W13) — `kick_dwm_compose` claimed a "sub-millisecond" round trip while its
cursor-on-a-sibling-display branch sleeps 35 ms on the CALLER's thread. State the cost on the
function and at both call sites, including which one is on the live frame path.
1.7 (W16) — DDA-era residue, DDA having been removed:
- retarget the win32u hook's rationale + all four log strings: its job is no longer "keep DDA on
one adapter" but "keep the virtual display on the adapter SET_RENDER_ADAPTER pinned" (a DXGI
reparent surfaces as the driver's TEX_FAIL render-adapter mismatch). The hook stays installed.
- the per-monitor-v2 DPI awareness rationale likewise: not DuplicateOutput1's E_ACCESSDENIED any
more, but keeping cursor/window coordinates in the PHYSICAL pixels the host's CCD geometry
(`source_desktop_rect`, `desktop_bounds`) is already in.
- `HYBRID_HOOK_HITS` was write-only. Surface it as `hybrid_hook_hits()` on the IDD-push open
line, which is the first point where DXGI has actually been exercised — the patch-readback
check proves the bytes landed, only this proves DXGI reaches the export.
- delete `hdr_p010_selftest()`: an unreachable 64×64 wrapper (main.rs calls
`hdr_p010_selftest_at`); its description moves onto the function that survives.
- `VideoConverter`'s docs promised a P010/BT.2020 output and a live scRGB input path. It pins
`YCBCR_STUDIO_G22_LEFT_P709` unconditionally and its only caller always passes
`scrgb_input: false`.
1.8 (X7) — the native handshake's 4:4:4 gate comment stated the OPPOSITE of what
`pf_capture::capturer_supports_444` returns on Windows ("delivers subsampled NV12/P010 today, so
it returns false there" — it forwards `resolved_backend_ingests_rgb_444()` and the IDD ring passes
BGRA through for an SDR 4:4:4 session).
No `.rs` behaviour delta: comments, doc comments and log strings, plus 1.5's control-flow-neutral
ladder and 1.7's counter accessor. Linux tests 6/6; clippy --all-targets clean on both targets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,12 +40,20 @@ use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_FORMAT_R16_UNORM, DXGI_SAMPLE_DESC,
|
||||
};
|
||||
|
||||
/// How many times DXGI has actually called our hooked `NtGdiDdDDIGetCachedHybridQueryValue`. If this
|
||||
/// stays 0 while DDA churns with ACCESS_LOST, the hook is NOT on DXGI's GPU-preference path on this
|
||||
/// build (so reparenting can't be the cause — look at composition/independent-flip instead). >0 with
|
||||
/// continuing churn means the hook fires but reparenting isn't the trigger here.
|
||||
/// How many times DXGI has actually called our hooked `NtGdiDdDDIGetCachedHybridQueryValue`.
|
||||
/// Reported by [`hybrid_hook_hits`] on every IDD-push open, which is the first point at which DXGI
|
||||
/// has been exercised (factory → `EnumAdapterByLuid` → device). The patch-readback check in
|
||||
/// [`install_gpu_pref_hook`] proves the BYTES landed; only this counter proves DXGI actually
|
||||
/// reaches the export on this build — so `0` here means the hook is inert and a
|
||||
/// reparenting-flavoured symptom (see [`install_gpu_pref_hook`]) has some other cause.
|
||||
static HYBRID_HOOK_HITS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// See [`HYBRID_HOOK_HITS`] — surfaced in the IDD-push open log so a dead hook is visible in the
|
||||
/// field instead of being a silent write-only counter.
|
||||
pub(crate) fn hybrid_hook_hits() -> u64 {
|
||||
HYBRID_HOOK_HITS.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
// kernel32 — declared directly so we don't pull the whole Win32_System_Diagnostics_Debug feature for
|
||||
// one call. FlushInstructionCache serializes the i-cache after the inline patch: the patch is written
|
||||
// on the main thread but DXGI runs the hooked export from the encode/worker thread (possibly a
|
||||
@@ -75,11 +83,19 @@ unsafe extern "system" fn hybrid_query_hook(gpu_preference: *mut u32) -> i32 {
|
||||
/// The win32u GPU-preference hook (the same technique Apollo applies, reimplemented here from the
|
||||
/// documented DDI — no GPL source copied). On a HYBRID-GPU box DXGI resolves a GPU preference
|
||||
/// (registry + power settings + the hybrid-adapter DDI) and REPARENTS outputs onto the chosen render
|
||||
/// GPU — which constantly invalidates Desktop Duplication (DXGI_ERROR_ACCESS_LOST 0x887A0026, the
|
||||
/// freeze/churn observed on the RTX 4090 + AMD iGPU box; `SET_RENDER_ADAPTER` is ignored there). Faking
|
||||
/// a cached preference of UNSPECIFIED makes DXGI skip the resolution, so the output is NOT reparented
|
||||
/// and DDA stays stable on one adapter (this is what makes Apollo's DDA work on this hardware).
|
||||
/// Installed once, before the first DXGI factory/enumeration; lasts the process lifetime (like Apollo).
|
||||
/// GPU, ignoring `SET_RENDER_ADAPTER` (observed on the RTX 4090 + AMD iGPU box). Faking a cached
|
||||
/// preference of UNSPECIFIED makes DXGI skip that resolution, so an output is NOT reparented and
|
||||
/// stays on one adapter.
|
||||
///
|
||||
/// **Why it is still installed now that DXGI Desktop Duplication is gone:** the IDD-push ring and
|
||||
/// the driver's swap-chain must live on the SAME adapter, and the host pins that with
|
||||
/// `SET_RENDER_ADAPTER` at monitor ADD (see `idd_push::open_inner`). A DXGI reparent moves the
|
||||
/// virtual output off the pinned GPU behind the host's back — which surfaces as the driver's
|
||||
/// `DRV_STATUS_TEX_FAIL` ("could not open our textures — render-adapter mismatch") and costs a
|
||||
/// ring rebind. So the hook's job changed from "keep DDA on one adapter" to "keep the VIRTUAL
|
||||
/// DISPLAY on the adapter we pinned"; the mechanism is unchanged. Installed once from
|
||||
/// `main.rs`, before the virtual-display setup creates the first DXGI factory; lasts the process
|
||||
/// lifetime. [`hybrid_hook_hits`] reports whether DXGI ever actually calls it.
|
||||
pub fn install_gpu_pref_hook() {
|
||||
use std::sync::Once;
|
||||
static HOOK: Once = Once::new();
|
||||
@@ -103,25 +119,38 @@ pub fn install_gpu_pref_hook() {
|
||||
GetAwarenessFromDpiAwarenessContext, GetThreadDpiAwarenessContext,
|
||||
SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
|
||||
};
|
||||
// Per-monitor-v2 DPI awareness — REQUIRED for IDXGIOutput5::DuplicateOutput1 (without it the
|
||||
// call returns E_ACCESSDENIED forever, forcing the legacy DuplicateOutput path). Matches
|
||||
// Apollo's startup. SetProcessDpiAwarenessContext fails with E_ACCESS_DENIED if awareness was
|
||||
// already set (manifest / earlier call) — log the outcome AND the effective awareness so a
|
||||
// 100% DuplicateOutput1 E_ACCESSDENIED is diagnosable instead of silent.
|
||||
// Per-monitor-v2 DPI awareness. It was originally set here because
|
||||
// `IDXGIOutput5::DuplicateOutput1` returns E_ACCESSDENIED without it; DDA is gone, but the
|
||||
// awareness still matters — an UNAWARE/SYSTEM-aware process gets DPI-VIRTUALIZED window and
|
||||
// cursor coordinates, while every geometry the host computes comes from CCD in PHYSICAL
|
||||
// pixels (`source_desktop_rect`, `desktop_bounds`). Mixing the two mis-aims the compose
|
||||
// kick's `SetCursorPos` and the cursor-blend placement on any scaled display. Set here
|
||||
// because this is the earliest process-wide hook point. `SetProcessDpiAwarenessContext`
|
||||
// fails with E_ACCESS_DENIED if awareness was already set (manifest / earlier call) — log
|
||||
// the outcome AND the effective awareness so a mis-scaled pointer is diagnosable.
|
||||
match SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
|
||||
Ok(()) => tracing::info!("DPI awareness set: PER_MONITOR_AWARE_V2"),
|
||||
Err(e) => tracing::warn!(error = ?e,
|
||||
"SetProcessDpiAwarenessContext failed (already set?) — DuplicateOutput1 may E_ACCESSDENIED"),
|
||||
"SetProcessDpiAwarenessContext failed (already set?) — cursor/desktop coordinates \
|
||||
may be DPI-virtualized against the host's physical-pixel CCD geometry"),
|
||||
}
|
||||
// 0=UNAWARE 1=SYSTEM 2=PER_MONITOR(_V2). DuplicateOutput1 needs 2.
|
||||
// 0=UNAWARE 1=SYSTEM 2=PER_MONITOR(_V2). Physical-pixel coordinates need 2.
|
||||
let awareness = GetAwarenessFromDpiAwarenessContext(GetThreadDpiAwarenessContext()).0;
|
||||
tracing::info!(awareness, "effective DPI awareness (need 2=PER_MONITOR for DuplicateOutput1)");
|
||||
tracing::info!(
|
||||
awareness,
|
||||
"effective DPI awareness (need 2=PER_MONITOR for physical-pixel coordinates)"
|
||||
);
|
||||
let Ok(lib) = LoadLibraryA(s!("win32u.dll")) else {
|
||||
tracing::warn!("GPU-pref hook: win32u.dll not loadable — skipping (DDA may churn on hybrid GPUs)");
|
||||
tracing::warn!(
|
||||
"GPU-pref hook: win32u.dll not loadable — skipping (on a hybrid-GPU box DXGI may \
|
||||
reparent the virtual display off the pinned render adapter → TEX_FAIL rebinds)"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(target) = GetProcAddress(lib, s!("NtGdiDdDDIGetCachedHybridQueryValue")) else {
|
||||
tracing::warn!("GPU-pref hook: NtGdiDdDDIGetCachedHybridQueryValue not exported — skipping");
|
||||
tracing::warn!(
|
||||
"GPU-pref hook: NtGdiDdDDIGetCachedHybridQueryValue not exported — skipping"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let target = target as usize as *mut u8;
|
||||
@@ -135,7 +164,14 @@ pub fn install_gpu_pref_hook() {
|
||||
patch[10] = 0xFF;
|
||||
patch[11] = 0xE0; // jmp rax
|
||||
let mut old = PAGE_PROTECTION_FLAGS(0);
|
||||
if VirtualProtect(target as *const c_void, 12, PAGE_EXECUTE_READWRITE, &mut old).is_err() {
|
||||
if VirtualProtect(
|
||||
target as *const c_void,
|
||||
12,
|
||||
PAGE_EXECUTE_READWRITE,
|
||||
&mut old,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("GPU-pref hook: VirtualProtect failed — skipping");
|
||||
return;
|
||||
}
|
||||
@@ -153,12 +189,15 @@ pub fn install_gpu_pref_hook() {
|
||||
std::ptr::copy_nonoverlapping(target, readback.as_mut_ptr(), 12);
|
||||
if readback == patch {
|
||||
tracing::info!(
|
||||
"GPU-pref hook installed + verified (win32u hybrid-query -> UNSPECIFIED): reparenting disabled"
|
||||
"GPU-pref hook installed + verified (win32u hybrid-query -> UNSPECIFIED): DXGI \
|
||||
output reparenting disabled. Whether DXGI actually CALLS it shows up as \
|
||||
hybrid_hook_hits on the IDD-push open line."
|
||||
);
|
||||
} else {
|
||||
tracing::error!(
|
||||
want = %format!("{patch:02x?}"), got = %format!("{readback:02x?}"),
|
||||
"GPU-pref hook patch did NOT land — hook is DEAD (DXGI will still reparent → ACCESS_LOST churn)"
|
||||
"GPU-pref hook patch did NOT land — hook is DEAD (on a hybrid-GPU box DXGI can \
|
||||
still reparent the virtual display off the pinned render adapter)"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -775,22 +814,6 @@ fn p010_reference(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
|
||||
(yc, cbc, crc)
|
||||
}
|
||||
|
||||
/// Colour self-test for [`HdrP010Converter`] (the `hdr-p010-selftest` subcommand): create a hardware
|
||||
/// D3D11 device, upload a known scRGB FP16 pattern, run the P010 shader passes, read the Y (plane 0)
|
||||
/// and UV (plane 1) planes back from a staging copy, and compare against the [`p010_reference`] f64
|
||||
/// math. The ONLY validation we have without green-screening a live HDR stream. PASS if max abs error
|
||||
/// Y ≤ 4 codes, U/V ≤ 5 codes (rounding + chroma averaging). Prints a per-colour table + PASS/FAIL.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn hdr_p010_selftest() -> Result<()> {
|
||||
hdr_p010_selftest_at(64, 64, None)
|
||||
}
|
||||
|
||||
/// [`hdr_p010_selftest`] at an arbitrary even size and (optionally) on a specific GPU vendor
|
||||
/// (PCI vendor id, e.g. `0x8086` Intel / `0x10de` NVIDIA / `0x1002` AMD). The size matters on
|
||||
/// top of the 64×64 default because the field sessions run at capture resolutions whose height
|
||||
/// is NOT 16-aligned (1080 → the encoder's align16 pool seam) and a driver may treat the planar
|
||||
/// RTVs differently at real sizes; the vendor pin matters on dual-GPU boxes where the default
|
||||
/// adapter is not the one the session encodes on.
|
||||
/// Test support (used by pf-encode's live e2e): the 8 sRGB colour bars (white/yellow/cyan/green/
|
||||
/// magenta/red/blue/black, sRGB 1.0 = scRGB 1.0 = 80 nits) as a w×h FP16 scRGB texture on the
|
||||
/// adapter with `luid`, converted through the REAL [`HdrP010Converter`] into a P010 texture with
|
||||
@@ -920,6 +943,18 @@ pub fn hdr_p010_convert_bars_on_luid(
|
||||
}
|
||||
}
|
||||
|
||||
/// Colour self-test for [`HdrP010Converter`] (the `hdr-p010-selftest` subcommand): create a hardware
|
||||
/// D3D11 device, upload a known scRGB FP16 pattern, run the P010 shader passes, read the Y (plane 0)
|
||||
/// and UV (plane 1) planes back from a staging copy, and compare against the [`p010_reference`] f64
|
||||
/// math. The ONLY validation we have without green-screening a live HDR stream. PASS if max abs error
|
||||
/// Y ≤ 4 codes, U/V ≤ 5 codes (rounding + chroma averaging). Prints a per-colour table + PASS/FAIL.
|
||||
///
|
||||
/// `w`/`h` must be even and non-zero. Run it at the FIELD capture size, not a toy one: sessions run
|
||||
/// at resolutions whose height is not 16-aligned (1080 → the encoder's align16 pool seam) and a
|
||||
/// driver may treat the planar RTVs differently at real sizes. `vendor` pins the adapter by PCI
|
||||
/// vendor id (`0x8086` Intel / `0x10de` NVIDIA / `0x1002` AMD) — it matters on dual-GPU boxes where
|
||||
/// the default adapter is not the one the session encodes on. The chosen adapter is always printed,
|
||||
/// because a PASS only means anything for the GPU it actually ran on.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn hdr_p010_selftest_at(w: u32, h: u32, vendor: Option<u32>) -> Result<()> {
|
||||
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN};
|
||||
@@ -1286,8 +1321,15 @@ use windows::Win32::Graphics::Dxgi::Common::{
|
||||
/// D3D11 **Video Processor** colour/format converter — runs on the GPU's dedicated VIDEO engine, NOT
|
||||
/// the 3D engine, so the per-frame RGB→YUV conversion does not contend with a GPU-saturating game (the
|
||||
/// HDR pixel-shader path and NVENC's internal RGB→YUV both use the 3D/compute engine, which an AAA
|
||||
/// title pins at ~100%). Output is NV12 (SDR, BT.709 studio-range) or P010 (HDR, BT.2020 PQ
|
||||
/// studio-range) — NVENC's native YUV inputs, so it encodes them with no further conversion.
|
||||
/// title pins at ~100%). Output is **always NV12, BT.709 studio-range** — one of NVENC's native YUV
|
||||
/// inputs, so it encodes with no further conversion.
|
||||
///
|
||||
/// It does NOT produce P010/BT.2020 PQ: [`VideoConverter::new`] pins the output colour space to
|
||||
/// `YCBCR_STUDIO_G22_LEFT_P709` unconditionally, and NVIDIA's video processor cannot do RGB→P010 at
|
||||
/// all (it renders green) — the HDR path is [`HdrP010Converter`]'s shader instead. The `scrgb_input`
|
||||
/// arm of `new` is likewise the only part of the HDR story here (an FP16 desktop tone-mapped DOWN to
|
||||
/// 8-bit BT.709), and it currently has no caller: `idd_push::ensure_converter` builds this converter
|
||||
/// only on the SDR/BGRA path and always passes `false`.
|
||||
pub(crate) struct VideoConverter {
|
||||
vdev: ID3D11VideoDevice,
|
||||
vctx: ID3D11VideoContext1,
|
||||
@@ -1361,7 +1403,8 @@ impl VideoConverter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `input` (BGRA or scRGB FP16) → `output` (NV12 or P010) on the video engine. Views are
|
||||
/// Convert `input` (BGRA, or scRGB FP16 for a converter built with `scrgb_input`) → `output`
|
||||
/// (NV12, BT.709 studio-range — see the type doc: never P010) on the video engine. Views are
|
||||
/// created per call (cheap relative to the Blt) so the input texture can vary frame to frame.
|
||||
pub(crate) unsafe fn convert(
|
||||
&self,
|
||||
|
||||
@@ -234,9 +234,16 @@ impl Drop for KeyedMutexGuard<'_> {
|
||||
/// desktop region (always true single-display), two net-zero 1 px relative moves (the historical
|
||||
/// behavior, pointer ends exactly where it started); when it sits on a SIBLING display, jump the
|
||||
/// cursor to the target's center and straight back (`SetCursorPos` ×2 — each absolute move dirties
|
||||
/// the cursor layer of the display it lands on, so the target composes at least one frame; the
|
||||
/// round trip is sub-millisecond and throttled). Best-effort — injection can be unavailable on the
|
||||
/// secure desktop, where a fresh compose just happened anyway.
|
||||
/// the cursor layer of the display it lands on, so the target composes at least one frame).
|
||||
/// Best-effort — injection can be unavailable on the secure desktop, where a fresh compose just
|
||||
/// happened anyway.
|
||||
///
|
||||
/// **COST:** the sibling-display branch SLEEPS 35 ms on the calling thread between the two
|
||||
/// `SetCursorPos`es. The dwell is load-bearing (see the comment at that branch: a sub-tick
|
||||
/// jump-and-return never dirties anything), but the caller is the capture/encode thread, so a kick
|
||||
/// on that branch costs ~2 frames of latency at 60 Hz. Every call site is a first-frame or
|
||||
/// post-recreate recovery window where no frames are flowing anyway, and the global 50 ms throttle
|
||||
/// plus the callers' own 600–800 ms schedules bound how often it can happen.
|
||||
///
|
||||
/// **HID-first**: when the host has registered [`HID_COMPOSE_KICK`] (the resident pf-mouse virtual
|
||||
/// HID pointer), the kick goes through it INSTEAD of the `SendInput` paths below. A report from a
|
||||
@@ -1097,6 +1104,12 @@ impl IddPushCapturer {
|
||||
client_10bit,
|
||||
want_444,
|
||||
ring_fp16 = display_hdr,
|
||||
// Whether DXGI ever reached the win32u GPU-preference hook. By this point the
|
||||
// factory + `EnumAdapterByLuid` + `make_device` above have exercised DXGI, so a
|
||||
// 0 here means the hook is inert on this build — the first thing to check if a
|
||||
// hybrid-GPU box keeps reporting TEX_FAIL render-adapter mismatches
|
||||
// (`dxgi::install_gpu_pref_hook`).
|
||||
hybrid_hook_hits = crate::dxgi::hybrid_hook_hits(),
|
||||
"IDD push(host): created sealed ring + delivered the channel; waiting for the driver \
|
||||
to attach + publish"
|
||||
);
|
||||
@@ -1273,6 +1286,9 @@ impl IddPushCapturer {
|
||||
"IDD push: no first frame after attach delivery — falling back to a synthetic \
|
||||
compose kick (stash-capable drivers republish instantly; old driver?)"
|
||||
);
|
||||
// May BLOCK this thread ~35 ms (the cursor-on-a-sibling-display branch — see
|
||||
// `kick_dwm_compose`'s COST note). Fine here: we are inside the open-time
|
||||
// first-frame gate, so no frames are flowing yet.
|
||||
kick_dwm_compose(self.target_id);
|
||||
next_kick = Instant::now() + Duration::from_millis(800);
|
||||
}
|
||||
@@ -2022,7 +2038,11 @@ impl IddPushCapturer {
|
||||
// never sees a frame and the 3 s recover-or-drop above kills a healthy session. A
|
||||
// stash-capable driver republishes its retained frame at the re-attach, so this kick
|
||||
// is the legacy-driver fallback here too. Nudge DWM (rate-limited) once the natural
|
||||
// post-recreate compose (and the stash republish) has had its chance.
|
||||
// post-recreate compose (and the stash republish) has had its chance. This is the ONE
|
||||
// call site on the live frame path: the kick may BLOCK this (capture/encode) thread
|
||||
// ~35 ms on its cursor-on-a-sibling-display branch (see `kick_dwm_compose`'s COST
|
||||
// note) — acceptable only because we are already ≥600 ms into a recovery window with
|
||||
// no frames arriving, and the 800 ms schedule below bounds the repeat rate.
|
||||
if since.elapsed() > Duration::from_millis(600)
|
||||
&& self.last_kick.elapsed() > Duration::from_millis(800)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user