//! PyroWave host encoder (Windows) — **separate-plane zero-copy D3D11→Vulkan** via pyrowave's own
//! compat device (design/pyrowave-windows-host-zerocopy.md). The opt-in wired-LAN intra-only wavelet
//! codec, the Windows twin of `enc/linux/pyrowave.rs`.
//!
//! Shape (deliberately minimal — no `ash`, no hand-rolled external-memory import): pyrowave owns its
//! OWN Vulkan device, selected by the render GPU's vendor/device-id
//! (`pyrowave_create_device_by_compat`). The capturer's CSC produces TWO SEPARATE D3D11 plane
//! textures — a full-res `R8` **Y** + a half-res `R8G8` **CbCr** (BT.709 limited, matching the Linux
//! `rgb2yuv.comp` layout the wavelet clients decode) — each shared to that device as an NT handle
//! (`VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT`) via `pyrowave_image_create`. Separate
//! single/two-component textures import reliably on NVIDIA at any size, unlike a single planar NV12
//! texture (the vendored interop test: "only very specific resource sizes"). A shared
//! D3D11/D3D12 fence — signalled by the capturer *after* the convert — is imported as a Vulkan
//! timeline semaphore (`pyrowave_sync_object_create`) so the wavelet read is ordered after the
//! D3D11 convert. `pyrowave_encoder_encode_gpu_synchronous` performs the acquire (waiting the fence
//! value), the encode, and the release in ONE pyrowave-owned submission, referencing the external
//! image with `VK_QUEUE_FAMILY_EXTERNAL`. The dangerous cross-API import (incl. the NVIDIA
//! video-layout workaround) stays entirely inside validated pyrowave/Granite. Every AU is a
//! keyframe; the AU/wire-chunk framing is the shared [`crate::pyrowave_wire`] helper (byte-identical
//! to Linux).
//!
//! The capture side (a BGRA→YUV CSC into two shareable plane textures + a shared fence, gated on the
//! pyrowave session flag) lives in `pf-capture` (`windows/idd_push.rs`); the CbCr plane + fence ride
//! the frame on [`pf_frame::dxgi::D3d11Frame::pyro`], the Y plane on `D3d11Frame::texture`.
// UNSAFE-LINT EXEMPTION (rationale + exit criteria: `unsafe_op_in_unsafe_fn` in the workspace
// Cargo.toml). This body is `pyrowave-sys` C-API plus D3D11/Vulkan interop calls almost line for
// line; narrowing it would add one `unsafe {}` plus one SAFETY comment per call that could only
// restate the signature. Clearing this file means DELETING the markers that carry no caller
// contract, not wrapping the calls — until then the lint is off HERE and enforced everywhere else.
#![allow(unsafe_op_in_unsafe_fn)]

// Every `unsafe` block in this module carries a `// SAFETY:` proof (the crate root enforces it).

use crate::pyrowave_wire;
use crate::{EncodedFrame, Encoder, EncoderCaps};
use anyhow::{bail, Context, Result};
use pf_frame::{CapturedFrame, FramePayload};
use pyrowave_sys as pw;
use std::collections::VecDeque;
use windows::core::{Interface, PCWSTR};
use windows::Win32::Foundation::{CloseHandle, DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE};
use windows::Win32::Graphics::Direct3D11::ID3D11Texture2D;
use windows::Win32::Graphics::Dxgi::IDXGIResource1;
use windows::Win32::System::Threading::GetCurrentProcess;

/// Headroom over the per-frame rate budget for the packetized bitstream (block headers + meta).
const BS_SLACK: usize = 256 * 1024;
/// Bound the per-texture image-import cache. The IDD out-ring is a small fixed set (OUT_RING=3);
/// this only ever grows past it if the capturer recreates its out-ring within one encoder's life
/// (a desktop-switch device recreate), in which case the stale imports are evicted + destroyed.
const IMPORT_CACHE_CAP: usize = 8;

/// Plane-import cache key: the texture's COM address plus the extent it was imported at.
type PlaneKey = (isize, u32, u32);

// --- Vulkan enum values not surfaced by pyrowave-sys' bindgen (only enums *reachable* from the
// pyrowave C API are generated; these plain #define / flags-typedef values are stable spec
// constants). bindgen renders every reachable Vulkan enum as a `u32` type alias, so these u32
// literals assign straight into the generated struct fields. ---
// The usage the validated interop helper (`create_pyrowave_image_from_d3d11`) requests.
const VK_IMAGE_USAGE_TRANSFER_SRC_BIT: u32 = 0x0000_0001;
const VK_IMAGE_USAGE_TRANSFER_DST_BIT: u32 = 0x0000_0002;
const VK_IMAGE_USAGE_SAMPLED_BIT: u32 = 0x0000_0004;
/// `VK_QUEUE_FAMILY_EXTERNAL` (`~0u32 - 1`): the image is owned by an external (D3D11) queue family;
/// pyrowave's acquire/release transitions ownership in/out across the interop boundary.
const VK_QUEUE_FAMILY_EXTERNAL: u32 = 0xFFFF_FFFE;

fn pw_check(r: pw::pyrowave_result, what: &str) -> Result<()> {
    if r == pw::pyrowave_result_PYROWAVE_SUCCESS {
        Ok(())
    } else {
        bail!("pyrowave {what} failed: result {r}")
    }
}

fn budget_for(bitrate_bps: u64, fps: u32) -> usize {
    ((bitrate_bps / (8 * fps.max(1) as u64)) as usize).max(64 * 1024)
}

// GPU scheduling priority is deliberately NOT set here. `pf-frame`'s `dxgi::auto_priority_gate`
// owns that policy for the whole process and runs once from `create_device` — the call the Windows
// capture path always makes before any PyroWave texture exists.
//
// This module used to raise it itself, to HIGH, once per process. That was a SECOND owner of a
// process-wide setting and it raced the real one: pf-frame's default `auto` mode starts at HIGH and
// then UPGRADES to REALTIME once it has established that is safe (HAGS off, or HAGS on with VRAM
// headroom, with a monitor that drops back when VRAM tightens — REALTIME + NVIDIA + HAGS +
// near-full VRAM is a documented NVENC hang). Opening a PyroWave session after that upgrade stamped
// HIGH back over REALTIME and the monitor, silently losing the ceiling-raise on exactly the
// GPU-saturated workload PyroWave cares about most.
//
// The old `PUNKTFUNK_GPU_PRIORITY` knob went with it; `PUNKTFUNK_GPU_PRIORITY_CLASS`
// (`off|normal|high|realtime|auto`, default `auto`) is the one that survives and it is strictly
// more capable — the removed knob could not express the auto gate at all.

pub struct PyroWaveEncoder {
    // pyrowave owns the whole Vulkan device (create_device_by_compat) — no ash on this side.
    pw_dev: pw::pyrowave_device,
    pw_enc: pw::pyrowave_encoder,
    // The imported shared fence (a Vulkan timeline semaphore aliasing the capturer's D3D11 fence).
    // Null until the capturer delivers the fence handle on the first frame (or after a rebuild).
    sync: pw::pyrowave_sync_object,
    // Imported plane textures, cached by the out-ring texture's raw pointer (stable per ring slot):
    // the full-res R8 Y plane and the half-res R8G8 CbCr plane, imported SEPARATELY (a single planar
    // NV12 import is unreliable on NVIDIA at arbitrary sizes).
    /// The capturer ring generation the cached plane imports below belong to. A recreate bumps it,
    /// and every cached import is destroyed — the COM addresses they are keyed on can be recycled
    /// by the allocator after a recreate, so identity cannot rest on the pointer alone.
    ring_gen: Option<u32>,
    y_images: Vec<(PlaneKey, pw::pyrowave_image)>,
    cbcr_images: Vec<(PlaneKey, pw::pyrowave_image)>,

    width: u32,
    height: u32,
    fps: u32,
    /// Session-fixed negotiated chroma: 4:4:4 = full-res CbCr plane + `Chroma444` pyrowave objects.
    chroma444: bool,
    /// Session-fixed negotiated depth ≥10: the capturer's HDR CSC writes P010-style studio codes
    /// into 16-bit UNORM planes (`R16_UNORM` Y + `R16G16_UNORM` CbCr) and the sequence header is
    /// stamped BT.2020/PQ.
    hdr16: bool,
    /// Per-frame bitstream budget (hard CBR): `bitrate / (8 * fps)`.
    frame_budget: usize,
    /// Datagram-aligned mode (plan §4.4): packetize at this boundary. `None` = one dense packet/AU.
    wire_chunk: Option<usize>,
    /// Measured windowing inflation → rate-budget deflation, so the bitrate pin holds on the
    /// WIRE, not just the raw bitstream (see [`pyrowave_wire::WireBudget`]).
    wire_budget: pyrowave_wire::WireBudget,
    bitstream: Vec<u8>,
    pending: VecDeque<EncodedFrame>,
    /// The AU currently being handed out in streamed chunks (PW6 — `Some` strictly between a
    /// `first` chunk and its `last`). See [`pyrowave_wire::AuChunker`]: this backend's encode is
    /// synchronous, so the AU is COMPLETE before the first chunk leaves — the split is for the
    /// send side, never an encode/send overlap.
    chunker: Option<pyrowave_wire::AuChunker>,
}

// SAFETY: used only from the single encode thread; the pyrowave handles are owned and only touched
// from that thread, and pyrowave only submits GPU work inside the API calls we make (mirrors the
// Linux `PyroWaveEncoder`'s `unsafe impl Send`). The D3D11 texture pointers travel as plain `isize`
// cache keys, never dereferenced here.
unsafe impl Send for PyroWaveEncoder {}

impl PyroWaveEncoder {
    pub fn open(
        width: u32,
        height: u32,
        fps: u32,
        bitrate_bps: u64,
        chroma: crate::ChromaFormat,
        bit_depth: u8,
    ) -> Result<Self> {
        let chroma444 = chroma.is_444();
        // A negotiated 10-bit session rides 16-bit UNORM planes carrying the P010-style
        // studio codes the capturer's HDR CSC writes (design/pyrowave-444-hdr.md §2.2) —
        // the wire is depth-agnostic, only the plane formats and the CSC change.
        let hdr16 = bit_depth >= 10;
        if !chroma444 && (width % 2 != 0 || height % 2 != 0) {
            bail!("pyrowave 4:2:0 needs even dimensions (got {width}x{height})");
        }
        // Checked against the chroma actually being opened, NOT hardcoded 4:4:4 — see the Linux
        // twin (`enc/linux/pyrowave.rs`) for the full rationale: the negotiator's 4:4:4 → 4:2:0
        // downgrade hands oversized modes to this open AS 4:2:0, so the old `chroma444`-gated
        // check was skipped exactly when it was needed.
        if !crate::pyrowave_mode_fits_rdo(width, height, chroma444) {
            bail!(
                "pyrowave {} at {width}x{height} exceeds the rate controller's 16-bit block \
                 index (see pyrowave-sys patches/0002 note) — lower the resolution",
                if chroma444 { "4:4:4" } else { "4:2:0" }
            );
        }
        let fps = fps.max(1);
        // Select pyrowave's device by the SELECTED render adapter's vendor/device-id — NOT by LUID:
        // in Session 0 (the host service context) the Vulkan ICD reports `deviceLUIDValid = false`,
        // so a by-LUID match would find nothing, while the vendor/device-id match + the external
        // import both work (design doc Stage 0; `pyrowave_c.cpp` guards LUID use behind validity).
        let (vid, pid) = pf_gpu::selected_gpu()
            .map(|s| (s.info.vendor_id, s.info.device_id))
            .unwrap_or((0, 0));
        // SAFETY: `create_device_by_compat` builds pyrowave's own instance/device from the
        // vendor/device-id (null uuids/luid = "don't constrain by those"); the out-param is a live
        // local. `confirm_interop_support` / `encoder_create` take that just-created non-null
        // device; on any failure we destroy what we created before returning. All pointers are
        // freshly created and owned by the returned struct (or freed on the error path).
        unsafe {
            let mut pw_dev: pw::pyrowave_device = std::ptr::null_mut();
            pw_check(
                pw::pyrowave_create_device_by_compat(
                    vid,
                    pid,
                    std::ptr::null(),
                    std::ptr::null(),
                    std::ptr::null(),
                    &mut pw_dev,
                ),
                "create_device_by_compat",
            )
            .with_context(|| {
                format!(
                    "open a PyroWave Vulkan device for GPU {vid:04x}:{pid:04x} (render adapter)"
                )
            })?;

            // The make-or-break gate (design doc Risk 1): confirm this device can do the
            // external-memory interop the zero-copy import needs. In a service context where the
            // import is unavailable this fails HERE (clean HEVC renegotiation) instead of at the
            // first frame's import.
            if !pw::pyrowave_device_confirm_interop_support(pw_dev) {
                pw::pyrowave_device_destroy(pw_dev);
                bail!(
                    "the PyroWave Vulkan device does not confirm external-memory interop support \
                     (D3D11→Vulkan zero-copy import unavailable on this GPU / in this session \
                     context) — the session should renegotiate to HEVC"
                );
            }

            let einfo = pw::pyrowave_encoder_create_info {
                device: pw_dev,
                width: width as i32,
                height: height as i32,
                chroma: if chroma444 {
                    pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
                } else {
                    pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
                },
            };
            let mut pw_enc: pw::pyrowave_encoder = std::ptr::null_mut();
            if let Err(e) = pw_check(
                pw::pyrowave_encoder_create(&einfo, &mut pw_enc),
                "encoder_create",
            ) {
                pw::pyrowave_device_destroy(pw_dev);
                return Err(e);
            }

            let frame_budget = budget_for(bitrate_bps.max(1_000_000), fps);
            tracing::info!(
                gpu = format!("{vid:04x}:{pid:04x}"),
                mode = %format!("{width}x{height}@{fps}"),
                budget_kib = frame_budget / 1024,
                chroma = if chroma444 { "4:4:4" } else { "4:2:0" },
                hdr = hdr16,
                "PyroWave encoder open (Windows separate-plane zero-copy, intra-only wavelet)"
            );

            Ok(Self {
                pw_dev,
                pw_enc,
                sync: std::ptr::null_mut(),
                ring_gen: None,
                y_images: Vec::new(),
                cbcr_images: Vec::new(),
                width,
                height,
                fps,
                chroma444,
                hdr16,
                frame_budget,
                wire_chunk: None,
                wire_budget: pyrowave_wire::WireBudget::new(),
                bitstream: Vec::new(),
                pending: VecDeque::new(),
                chunker: None,
            })
        }
    }

    /// Import one capturer plane D3D11 texture (`R8_UNORM` Y or `R8G8_UNORM` CbCr) into pyrowave's
    /// Vulkan device. Creates a fresh shared NT handle from the texture (the capturer marked the ring
    /// `SHARED | SHARED_NTHANDLE`); `pyrowave_image_create` takes ownership of the handle and closes
    /// it on SUCCESSFUL import only (pyrowave-sys patch 0006 — same contract as the fence import in
    /// [`Self::import_fence`]), so this fn closes it on every failure return. Single/two-component
    /// textures import reliably on NVIDIA at any size — unlike a planar NV12 — so no
    /// MUTABLE_FORMAT / planar-layout workaround is involved.
    ///
    /// # Safety
    /// `texture` must be a live `ID3D11Texture2D` of format `vk_format`, sized `w`×`h`, created
    /// shareable, on the same physical GPU as `pw_dev`. The returned `pyrowave_image` is owned by the
    /// caller (destroyed in `Drop`/eviction). Takes `pw_dev` by value (not `&self`) so the cache
    /// closures don't double-borrow the encoder.
    unsafe fn import_plane(
        pw_dev: pw::pyrowave_device,
        texture: &ID3D11Texture2D,
        vk_format: pw::VkFormat,
        w: u32,
        h: u32,
    ) -> Result<pw::pyrowave_image> {
        // The shared NT handle (mirrors the interop test's `create_pyrowave_image_from_d3d11`).
        let res: IDXGIResource1 = texture
            .cast()
            .context("ID3D11Texture2D -> IDXGIResource1 (plane not created shareable?)")?;
        // GENERIC_ALL (0x1000_0000) — the access the interop test hands the shared handle.
        let handle: HANDLE = res
            .CreateSharedHandle(None, 0x1000_0000, PCWSTR::null())
            .context("IDXGIResource1::CreateSharedHandle(plane texture)")?;

        // Zero-init then set the fields we need (pNext/queue-family/initialLayout stay 0 = null /
        // UNDEFINED) — robust against however bindgen renders `Default` for the raw-pointer fields.
        let mut ici: pw::VkImageCreateInfo = std::mem::zeroed();
        ici.sType = pw::VkStructureType_VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
        ici.imageType = pw::VkImageType_VK_IMAGE_TYPE_2D;
        ici.format = vk_format;
        ici.extent = pw::VkExtent3D {
            width: w,
            height: h,
            depth: 1,
        };
        ici.mipLevels = 1;
        ici.arrayLayers = 1;
        ici.samples = pw::VkSampleCountFlagBits_VK_SAMPLE_COUNT_1_BIT;
        ici.tiling = pw::VkImageTiling_VK_IMAGE_TILING_OPTIMAL;
        ici.usage = VK_IMAGE_USAGE_SAMPLED_BIT
            | VK_IMAGE_USAGE_TRANSFER_SRC_BIT
            | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
        ici.sharingMode = pw::VkSharingMode_VK_SHARING_MODE_EXCLUSIVE;
        let info = pw::pyrowave_image_create_info {
            device: pw_dev,
            external_handle: handle.0 as usize as pw::pyrowave_os_handle,
            handle_type:
                pw::VkExternalMemoryHandleTypeFlagBits_VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT,
            image_create_info: &ici,
        };
        let mut image: pw::pyrowave_image = std::ptr::null_mut();
        if let Err(e) = pw_check(pw::pyrowave_image_create(&info, &mut image), "image_create") {
            // pyrowave consumes the handle ONLY on a successful import (pyrowave-sys patch 0006
            // pinned this at the API's success boundary) — so on EVERY failure return the handle
            // is still ours and this close is the single one. Before the patch, an
            // allocate-stage failure inside Granite had already closed it and this was a double
            // close of a possibly-recycled handle value.
            let _ = CloseHandle(handle);
            return Err(e);
        }
        Ok(image)
    }

    /// Import (cache) a plane texture by its stable per-slot pointer, evicting the oldest when the
    /// cache is over cap (the out-ring is small + fixed; growth only happens on a mid-life ring
    /// recreate). Returns the cached-or-fresh `pyrowave_image`.
    ///
    /// # Safety
    /// Same contract as [`import_plane`].
    /// Keyed on `(texture address, width, height)` rather than the bare address: the COM pointer
    /// carries no reference here, so a released texture's address can be recycled by a later
    /// allocation and return an import describing the WRONG surface. Folding the extent in means a
    /// recycled address at a different size can never alias. (A recycle at the SAME size is still
    /// possible in principle — the complete fix is to key on the capturer's ring generation, which
    /// needs that generation plumbed onto `PyroFrameShare`.)
    unsafe fn cached_plane(
        cache: &mut Vec<(PlaneKey, pw::pyrowave_image)>,
        make: impl FnOnce() -> Result<pw::pyrowave_image>,
        key: PlaneKey,
    ) -> Result<pw::pyrowave_image> {
        if let Some((_, img)) = cache.iter().find(|(k, _)| *k == key) {
            return Ok(*img);
        }
        let img = make()?;
        if cache.len() >= IMPORT_CACHE_CAP {
            let (_, old) = cache.remove(0);
            pw::pyrowave_image_destroy(old);
        }
        cache.push((key, img));
        Ok(img)
    }

    /// Import the capturer's shared fence as a Vulkan timeline semaphore. Called only when this
    /// encoder has no timeline yet (the first frame, or a fresh encoder after a mode-switch rebuild).
    /// pyrowave takes ownership of the handle and CLOSES it on import, so we hand it a private
    /// **duplicate** of the capturer's persistent handle — leaving the original valid for the next
    /// rebuild's re-import (the capturer passes the same handle on every frame).
    ///
    /// # Safety
    /// `handle` must be the capturer's live shared D3D11/D3D12 fence NT handle on `self.pw_dev`'s GPU.
    unsafe fn import_fence(&mut self, handle: isize) -> Result<()> {
        let mut dup = HANDLE::default();
        DuplicateHandle(
            GetCurrentProcess(),
            HANDLE(handle as *mut core::ffi::c_void),
            GetCurrentProcess(),
            &mut dup,
            0,
            false,
            DUPLICATE_SAME_ACCESS,
        )
        .context("DuplicateHandle(shared fence for pyrowave import)")?;
        let info = pw::pyrowave_sync_object_create_info {
            device: self.pw_dev,
            external_handle: dup.0 as usize as pw::pyrowave_os_handle,
            // D3D11 fence == D3D12 fence on Windows 10+; must be imported as TIMELINE.
            handle_type:
                pw::VkExternalSemaphoreHandleTypeFlagBits_VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT,
            semaphore_type: pw::VkSemaphoreType_VK_SEMAPHORE_TYPE_TIMELINE,
            import_flags: 0,
        };
        let mut sync: pw::pyrowave_sync_object = std::ptr::null_mut();
        if let Err(e) = pw_check(
            pw::pyrowave_sync_object_create(&info, &mut sync),
            "sync_object_create",
        ) {
            // pyrowave only closes the handle on a SUCCESSFUL import (Granite's semaphore import
            // has always had these semantics; patch 0006 made the image import match) — close
            // the dup on failure.
            let _ = CloseHandle(dup);
            return Err(e);
        }
        self.sync = sync;
        Ok(())
    }

    /// One frame, synchronously: import (cache) the two plane textures + fence → encode (pyrowave
    /// owns the submission: acquire waits the capturer's fence value, references both images as
    /// `QUEUE_FAMILY_EXTERNAL`, release hands them back) → packetize into an `EncodedFrame`.
    ///
    /// # Safety
    /// Runs on the single encode thread; all pyrowave calls take handles this struct owns.
    /// The per-frame budget handed to pyrowave rate control: `frame_budget`, deflated by the
    /// measured windowing inflation when the datagram-aligned wire is on — the bitrate pin is
    /// a promise about the wire, not the raw bitstream (see [`pyrowave_wire::WireBudget`]).
    fn rate_budget(&self) -> usize {
        match self.wire_chunk {
            Some(_) => self.wire_budget.deflate(self.frame_budget).max(64 * 1024),
            None => self.frame_budget,
        }
    }

    unsafe fn encode_frame(&mut self, frame: &CapturedFrame) -> Result<()> {
        // A failed `reset()` leaves the encoder destroyed and null — fail cleanly rather than
        // handing null to pyrowave (see the Linux twin).
        anyhow::ensure!(
            !self.pw_enc.is_null(),
            "pyrowave: encode after a failed reset (encoder was destroyed and not rebuilt)"
        );
        // The plane textures are imported at the encoder's CONFIGURED extent, not the frame's, so a
        // capture that changed size would be read under a stale `VkImageCreateInfo`. This is
        // reachable without any client Reconfigure: the IDD capturer autonomously recreates its ring
        // on a confirmed display-descriptor change (e.g. a fullscreen game mode-setting the virtual
        // display). Refuse instead — the session must reopen the encoder at the new mode. Mirrors
        // the guard the QSV and AMF backends already carry.
        anyhow::ensure!(
            frame.width == self.width && frame.height == self.height,
            "pyrowave: captured frame {}x{} != encoder {}x{} (the capturer recreated its ring at a \
             new mode — the encoder must be reopened)",
            frame.width,
            frame.height,
            self.width,
            self.height
        );
        let FramePayload::D3d11(d3d) = &frame.payload else {
            bail!("pyrowave (Windows) needs a D3D11 frame (the capturer must be in pyrowave mode)")
        };
        let share = d3d.pyro.as_ref().context(
            "pyrowave (Windows): the frame carries no PyroWave payload — the capturer was not opened \
             in pyrowave mode (session_plan::output_format must set OutputFormat::pyrowave)",
        )?;

        // Ring recreate ⇒ every cached plane import belongs to textures that no longer exist. Their
        // COM addresses can be handed back out by the allocator, so a pointer-keyed hit could return
        // an image bound to freed memory. Flush on the generation change rather than relying on the
        // address (or the FIFO cap) to notice.
        if self.ring_gen != Some(share.ring_gen) {
            if self.ring_gen.is_some() {
                tracing::info!(
                    from = ?self.ring_gen,
                    to = share.ring_gen,
                    cached = self.y_images.len() + self.cbcr_images.len(),
                    "pyrowave: capturer recreated its ring — flushing stale plane imports"
                );
            }
            for (_, img) in self.y_images.drain(..).chain(self.cbcr_images.drain(..)) {
                pw::pyrowave_image_destroy(img);
            }
            self.ring_gen = Some(share.ring_gen);
        }

        // Import the fence whenever this encoder has no timeline yet — the first frame, OR a fresh
        // encoder after a client mode-switch rebuild (the capturer passes the persistent handle on
        // every frame precisely so a rebuilt encoder can re-import it).
        if self.sync.is_null() {
            let h = share
                .fence_handle
                .context("pyrowave (Windows): frame carried no shared fence handle")?;
            self.import_fence(h)?;
        }

        // Import (cache) the two SEPARATE plane textures by their stable per-slot pointers: the
        // full-res R8 Y on `d3d.texture`, the half-res R8G8 CbCr on `share.cbcr`. `pw_dev` is a Copy
        // handle so the cache closures don't borrow `self` alongside `&mut self.*_images`.
        let (w, h) = (self.width, self.height);
        // Plane geometry/formats follow the negotiated session: chroma half- or full-res,
        // 8-bit (SDR BT.709) or 16-bit UNORM (HDR: P010-style studio codes from the CSC).
        let (cw, ch) = if self.chroma444 {
            (w, h)
        } else {
            (w / 2, h / 2)
        };
        let (yf, cf) = if self.hdr16 {
            (
                pw::VkFormat_VK_FORMAT_R16_UNORM,
                pw::VkFormat_VK_FORMAT_R16G16_UNORM,
            )
        } else {
            (
                pw::VkFormat_VK_FORMAT_R8_UNORM,
                pw::VkFormat_VK_FORMAT_R8G8_UNORM,
            )
        };
        let pw_dev = self.pw_dev;
        let y_img = {
            let key = (d3d.texture.as_raw() as isize, w, h);
            let tex = &d3d.texture;
            Self::cached_plane(
                &mut self.y_images,
                || Self::import_plane(pw_dev, tex, yf, w, h),
                key,
            )?
        };
        let cbcr_img = {
            let key = (share.cbcr.as_raw() as isize, cw, ch);
            let tex = &share.cbcr;
            Self::cached_plane(
                &mut self.cbcr_images,
                || Self::import_plane(pw_dev, tex, cf, cw, ch),
                key,
            )?
        };

        // Plane views built BY HAND exactly like the Linux encoder (`enc/linux/pyrowave.rs`): Y from
        // the R8 image (full-res, IDENTITY), Cb/Cr from the R8G8 image (half-res) with R/G swizzle to
        // synthesize the two chroma planes from the interleaved CbCr — the documented NV12-style
        // hand-off. All GENERAL layout (pyrowave's GPU-buffer contract accepts it without transitions).
        let y_vk = pw::pyrowave_image_get_handle(y_img);
        let cbcr_vk = pw::pyrowave_image_get_handle(cbcr_img);
        let plane = |image, pw_w, pw_h, fmt, swizzle| pw::pyrowave_image_view {
            image,
            width: pw_w,
            height: pw_h,
            image_format: fmt,
            view_format: fmt,
            mip_level: 0,
            layer: 0,
            aspect: pw::VkImageAspectFlagBits_VK_IMAGE_ASPECT_COLOR_BIT,
            swizzle,
            layout: pw::VkImageLayout_VK_IMAGE_LAYOUT_GENERAL,
        };
        let buffers = pw::pyrowave_gpu_buffers {
            planes: [
                plane(
                    y_vk,
                    w,
                    h,
                    yf,
                    pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_IDENTITY,
                ),
                plane(
                    cbcr_vk,
                    cw,
                    ch,
                    cf,
                    pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_R,
                ),
                plane(
                    cbcr_vk,
                    cw,
                    ch,
                    cf,
                    pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_G,
                ),
            ],
        };

        // Acquire the two external images (owned by the D3D11 queue family), waiting the capturer's
        // fence value so the wavelet read is ordered after the D3D11 CSC; release hands them back.
        // pyrowave owns the submission (no explicit command buffer).
        let refs = [
            pw::pyrowave_gpu_external_reference {
                image: y_img,
                queue_family_index: VK_QUEUE_FAMILY_EXTERNAL,
            },
            pw::pyrowave_gpu_external_reference {
                image: cbcr_img,
                queue_family_index: VK_QUEUE_FAMILY_EXTERNAL,
            },
        ];
        let acquire = pw::pyrowave_gpu_sync_operation {
            images: refs.as_ptr(),
            num_images: refs.len(),
            sync: pw::pyrowave_sync_point {
                semaphore: pw::pyrowave_sync_object_get_semaphore(self.sync),
                value: share.fence_value,
            },
        };
        let release = pw::pyrowave_gpu_sync_operation {
            images: refs.as_ptr(),
            num_images: refs.len(),
            // No release signal needed (null semaphore): encode is synchronous and the out-ring depth
            // guarantees the slot is not reused before the next synchronous encode completes (the same
            // contract the NVENC path relies on).
            sync: std::mem::zeroed(),
        };
        let rc = pw::pyrowave_rate_control {
            maximum_bitstream_size: self.rate_budget(),
        };
        pw_check(
            pw::pyrowave_encoder_encode_gpu_synchronous(
                self.pw_enc,
                &acquire,
                &release,
                &buffers,
                &rc,
            ),
            "encode_gpu_synchronous",
        )?;

        // ---- packetize (shared framing helper — byte-identical to the Linux encoder) ----
        let cap = self.frame_budget + BS_SLACK;
        self.bitstream.resize(cap, 0);
        let boundary = pyrowave_wire::packet_boundary(self.wire_chunk, cap);
        let mut n: usize = 0;
        pw_check(
            pw::pyrowave_encoder_compute_num_packets(self.pw_enc, boundary, &mut n),
            "compute_num_packets",
        )?;
        if n == 0 || (self.wire_chunk.is_none() && n != 1) {
            bail!("pyrowave: unexpected packet count {n} at boundary {boundary}");
        }
        let mut packets = vec![pw::pyrowave_packet { offset: 0, size: 0 }; n];
        let mut out_n: usize = 0;
        pw_check(
            pw::pyrowave_encoder_packetize(
                self.pw_enc,
                packets.as_mut_ptr(),
                boundary,
                &mut out_n,
                self.bitstream.as_mut_ptr() as *mut std::ffi::c_void,
                cap,
            ),
            "packetize",
        )?;
        packets.truncate(out_n.max(1));
        // Correct pyrowave's zeroed sequence-header VUI: it signals ycbcr_range=FULL, but our CSC
        // emits studio range — patch the bits HONEST so VUI-honoring clients don't wash out
        // blacks; an HDR session additionally stamps BT.2020 primaries + PQ + BT.2020 matrix
        // (matching the negotiated ColorInfo).
        if let Some(p) = packets.first() {
            pyrowave_wire::stamp_color_bits(&mut self.bitstream, p.offset, self.hdr16);
        }
        let pkts: Vec<(usize, usize)> = packets.iter().map(|p| (p.offset, p.size)).collect();
        let au = pyrowave_wire::build_au(&pkts, &self.bitstream, self.wire_chunk);
        if self.wire_chunk.is_some() {
            let raw: usize = pkts.iter().map(|&(_, s)| s).sum();
            self.wire_budget.observe(raw, au.len());
        }
        self.pending.push_back(EncodedFrame {
            data: au,
            pts_ns: frame.pts_ns,
            // Every frame is independently decodable — the codec's whole recovery story.
            keyframe: true,
            recovery_anchor: false,
            chunk_aligned: self.wire_chunk.is_some(),
        });
        Ok(())
    }
}

impl Encoder for PyroWaveEncoder {
    fn submit(&mut self, frame: &CapturedFrame) -> Result<()> {
        // SAFETY: single-threaded encoder; `encode_frame` records/submits on handles this struct
        // owns and pyrowave waits its own fence before packetize returns.
        unsafe { self.encode_frame(frame) }
    }

    fn caps(&self) -> EncoderCaps {
        // No RFI (every frame is intra). Report the real opened chroma so the session glue's
        // post-open cross-check stays quiet on a genuine 4:4:4 session (this codec gained 4:4:4
        // after the caps() default was written — a hardcoded `default()` here mis-reports a 4:4:4
        // open as 4:2:0 and fires a spurious "chroma disagrees with the negotiated Welcome" warn).
        EncoderCaps {
            // The Windows capturer composites the pointer itself; this backend never reads it.
            blends_cursor: false,
            chroma_444: self.chroma444,
            ..EncoderCaps::default()
        }
    }

    fn poll(&mut self) -> Result<Option<EncodedFrame>> {
        // Trait contract: each AU is drained through ONE method. Erroring beats double-emitting
        // the bytes the chunk cursor already handed out (which would reach the wire twice, under
        // the same frame index, and fail the receiver's retro-validation).
        if self.chunker.is_some() {
            bail!("pyrowave: poll() on an AU already being drained through poll_chunk");
        }
        Ok(self.pending.pop_front())
    }

    // --- streamed AU (PW6) — see `pyrowave_wire::AuChunker` for what this does and does NOT buy.
    // Byte-identical to the Linux twin BY CONSTRUCTION: all of the cutting lives in the shared
    // helper, which compiles and unit-tests on every platform. This file cannot be compiled from
    // a Linux/macOS dev box, so anything written here directly would ship unverified.
    fn supports_chunked_poll(&self) -> bool {
        pyrowave_wire::stream_chunk_step(self.wire_chunk).is_some()
    }

    fn poll_chunk(&mut self) -> Result<Option<crate::AuChunk>> {
        // Finish the AU already in flight before opening the next one — the host's `handle_chunk`
        // keys begin/finish off `first`/`last` and cannot interleave two AUs.
        if let Some(c) = self.chunker.as_mut() {
            if let Some(chunk) = c.next() {
                return Ok(Some(chunk));
            }
            self.chunker = None;
        }
        let Some(f) = self.pending.pop_front() else {
            return Ok(None);
        };
        // No blocking wait here (the trait allows one): `submit` already ran the whole encode
        // synchronously, so an AU in `pending` is complete by construction.
        match pyrowave_wire::stream_chunk_step(self.wire_chunk) {
            Some(step) => Ok(self
                .chunker
                .insert(pyrowave_wire::AuChunker::new(f, step))
                .next()),
            // Unarmed / dense: the trait's own default shape, so a host that polls chunks anyway
            // still gets whole AUs.
            None => Ok(Some(crate::AuChunk::whole(f))),
        }
    }

    fn reset(&mut self) -> bool {
        // A rebuild forfeits every in-flight frame — including an AU only half-handed-out through
        // `poll_chunk`. Dropping the cursor here (ahead of every `pending.clear()` arm below) is
        // what keeps the next `poll_chunk` from splicing the tail of a dead AU onto a fresh one;
        // the host sees a `first` without the previous `last`, logs "streamed AU abandoned
        // mid-flight" and lets the client age that frame out.
        self.chunker = None;
        // Cheap in-place rebuild: recreate only the pyrowave encoder object (no rate-control /
        // reference state to preserve). The device, imported textures and fence survive.
        // SAFETY: encode is synchronous (no work in flight); the device outlives the swapped encoder.
        unsafe {
            pw::pyrowave_encoder_destroy(self.pw_enc);
            // Publish the null IMMEDIATELY — see the Linux twin. The create below is fallible and
            // `pyrowave_encoder_destroy` is a plain `delete` with no null check, so leaving the
            // freed pointer in the field makes `Drop` a double free.
            self.pw_enc = std::ptr::null_mut();
            let einfo = pw::pyrowave_encoder_create_info {
                device: self.pw_dev,
                width: self.width as i32,
                height: self.height as i32,
                // Rebuild at the session's real chroma — a hardcoded 420 here would leave a 4:4:4
                // session's full-res chroma plane + CSC feeding a 4:2:0 pyrowave encoder.
                chroma: if self.chroma444 {
                    pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
                } else {
                    pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
                },
            };
            let mut enc: pw::pyrowave_encoder = std::ptr::null_mut();
            let r = pw::pyrowave_encoder_create(&einfo, &mut enc);
            if r != pw::pyrowave_result_PYROWAVE_SUCCESS {
                tracing::error!(result = ?r, "pyrowave: encoder rebuild failed");
                // `pw_enc` stays null — `Drop` and `encode_frame` both guard on it.
                self.pending.clear();
                return false;
            }
            self.pw_enc = enc;
        }
        self.pending.clear();
        true
    }

    fn reconfigure_bitrate(&mut self, bps: u64) -> bool {
        // Rate control is a plain per-frame byte budget — an in-place retarget is free (no IDR,
        // nothing in flight). Phase 3 pins the session rate and bypasses ABR; this faithfully
        // applies whatever the caller asks until then.
        self.frame_budget = budget_for(bps.max(1_000_000), self.fps);
        tracing::debug!(
            mbps = bps / 1_000_000,
            budget_kib = self.frame_budget / 1024,
            "pyrowave: per-frame rate budget retargeted in place"
        );
        true
    }

    fn set_wire_chunking(&mut self, shard_payload: usize) {
        // Sanity floor: a boundary below one block header + payload word is meaningless.
        if shard_payload >= 64 {
            self.wire_chunk = Some(shard_payload);
            tracing::info!(
                shard_payload,
                "pyrowave: datagram-aligned packetization on (partial-frame loss mode)"
            );
        }
    }

    fn flush(&mut self) -> Result<()> {
        // Synchronous per-frame encode: nothing buffered beyond `pending`.
        Ok(())
    }
}

impl Drop for PyroWaveEncoder {
    fn drop(&mut self) {
        // SAFETY: owned handles, destroyed exactly once; pyrowave objects (encoder, images, sync) go
        // before the device they borrow (per pyrowave.h).
        unsafe {
            // Null when a failed `reset()` already destroyed it — `pyrowave_encoder_destroy`
            // is not null-safe (same guard `sync` below has had all along).
            if !self.pw_enc.is_null() {
                pw::pyrowave_encoder_destroy(self.pw_enc);
            }
            for (_, img) in self.y_images.drain(..).chain(self.cbcr_images.drain(..)) {
                pw::pyrowave_image_destroy(img);
            }
            if !self.sync.is_null() {
                pw::pyrowave_sync_object_destroy(self.sync);
            }
            pw::pyrowave_device_destroy(self.pw_dev);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pf_frame::dxgi::{D3d11Frame, PyroFrameShare};
    use pf_frame::PixelFormat;
    use windows::Win32::Foundation::HMODULE;
    use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_1};
    use windows::Win32::Graphics::Direct3D11::{
        D3D11CreateDevice, ID3D11Device, ID3D11Device5, ID3D11DeviceContext, ID3D11DeviceContext4,
        ID3D11Fence, ID3D11Texture2D, D3D11_BIND_RENDER_TARGET, D3D11_CPU_ACCESS_WRITE,
        D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_FENCE_FLAG_SHARED, D3D11_MAPPED_SUBRESOURCE,
        D3D11_MAP_WRITE, D3D11_RESOURCE_MISC_SHARED, D3D11_RESOURCE_MISC_SHARED_NTHANDLE,
        D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING,
    };
    use windows::Win32::Graphics::Dxgi::Common::{
        DXGI_FORMAT, DXGI_FORMAT_R16G16_UNORM, DXGI_FORMAT_R16_UNORM, DXGI_FORMAT_R8G8_UNORM,
        DXGI_FORMAT_R8_UNORM, DXGI_SAMPLE_DESC,
    };

    /// Decode a dense PyroWave AU with upstream's own decoder → YUV420P plane means (the golden
    /// oracle, mirroring the Linux `decode_plane_means`).
    ///
    /// # Safety
    /// `au` must be a complete dense PyroWave AU for a `w`×`h` 4:2:0 frame.
    unsafe fn decode_plane_means(w: u32, h: u32, au: &[u8], chroma444: bool) -> (f64, f64, f64) {
        let mut dev: pw::pyrowave_device = std::ptr::null_mut();
        assert_eq!(
            pw::pyrowave_create_default_device(&mut dev),
            pw::pyrowave_result_PYROWAVE_SUCCESS
        );
        let dinfo = pw::pyrowave_decoder_create_info {
            device: dev,
            width: w as i32,
            height: h as i32,
            chroma: if chroma444 {
                pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
            } else {
                pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
            },
            fragment_path: false,
        };
        let mut dec: pw::pyrowave_decoder = std::ptr::null_mut();
        assert_eq!(
            pw::pyrowave_decoder_create(&dinfo, &mut dec),
            pw::pyrowave_result_PYROWAVE_SUCCESS
        );
        assert_eq!(
            pw::pyrowave_decoder_push_packet(dec, au.as_ptr() as *const _, au.len()),
            pw::pyrowave_result_PYROWAVE_SUCCESS
        );
        assert!(pw::pyrowave_decoder_decode_is_ready(dec, false));
        let (cw2, ch2) = if chroma444 { (w, h) } else { (w / 2, h / 2) };
        let mut y = vec![0u8; (w * h) as usize];
        let mut cb = vec![0u8; (cw2 * ch2) as usize];
        let mut cr = vec![0u8; (cw2 * ch2) as usize];
        let mut buf: pw::pyrowave_cpu_buffer = std::mem::zeroed();
        buf.format = if chroma444 {
            pw::pyrowave_cpu_buffer_format_PYROWAVE_CPU_BUFFER_FORMAT_YUV444P
        } else {
            pw::pyrowave_cpu_buffer_format_PYROWAVE_CPU_BUFFER_FORMAT_YUV420P
        };
        buf.width = w as i32;
        buf.height = h as i32;
        buf.data = [
            y.as_mut_ptr() as *mut _,
            cb.as_mut_ptr() as *mut _,
            cr.as_mut_ptr() as *mut _,
        ];
        buf.row_stride_in_bytes = [w as usize, cw2 as usize, cw2 as usize];
        buf.plane_size_in_bytes = [y.len(), cb.len(), cr.len()];
        assert_eq!(
            pw::pyrowave_decoder_decode_cpu_buffer_synchronous(dec, &buf),
            pw::pyrowave_result_PYROWAVE_SUCCESS
        );
        pw::pyrowave_decoder_destroy(dec);
        pw::pyrowave_device_destroy(dev);
        let mean = |v: &[u8]| v.iter().map(|&x| x as f64).sum::<f64>() / v.len() as f64;
        (mean(&y), mean(&cb), mean(&cr))
    }

    /// Create a shareable `format` plane texture (`bpp` bytes/texel), fill each texel with `bytes`
    /// via a CPU staging copy, and return it. Mirrors the capturer's SHARED|SHARED_NTHANDLE +
    /// RENDER_TARGET out-ring textures.
    ///
    /// # Safety
    /// `bytes.len() == bpp`; runs on a live D3D11 device/context.
    unsafe fn make_plane(
        device: &ID3D11Device,
        context: &ID3D11DeviceContext,
        w: u32,
        h: u32,
        format: DXGI_FORMAT,
        bpp: usize,
        bytes: &[u8],
    ) -> ID3D11Texture2D {
        let mut desc = D3D11_TEXTURE2D_DESC {
            Width: w,
            Height: h,
            MipLevels: 1,
            ArraySize: 1,
            Format: format,
            SampleDesc: DXGI_SAMPLE_DESC {
                Count: 1,
                Quality: 0,
            },
            Usage: D3D11_USAGE_DEFAULT,
            BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32,
            CPUAccessFlags: 0,
            MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0 | D3D11_RESOURCE_MISC_SHARED.0)
                as u32,
        };
        let mut tex: Option<ID3D11Texture2D> = None;
        device
            .CreateTexture2D(&desc, None, Some(&mut tex))
            .expect("CreateTexture2D(plane default)");
        let tex = tex.unwrap();
        desc.BindFlags = 0;
        desc.MiscFlags = 0;
        desc.Usage = D3D11_USAGE_STAGING;
        desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE.0 as u32;
        let mut staging: Option<ID3D11Texture2D> = None;
        device
            .CreateTexture2D(&desc, None, Some(&mut staging))
            .expect("CreateTexture2D(plane staging)");
        let staging = staging.unwrap();
        let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
        context
            .Map(&staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))
            .expect("Map(plane staging)");
        let pitch = mapped.RowPitch as usize;
        let base = mapped.pData as *mut u8;
        for row in 0..(h as usize) {
            let r = base.add(row * pitch);
            for x in 0..(w as usize) {
                for (b, &v) in bytes.iter().enumerate() {
                    *r.add(x * bpp + b) = v;
                }
            }
        }
        context.Unmap(&staging, 0);
        context.CopyResource(&tex, &staging);
        tex
    }

    /// End-to-end zero-copy smoke: distinct solid Y/Cb/Cr filled into SEPARATE shareable plane
    /// textures (full-res R8 Y + half-res R8G8 CbCr) → shared to pyrowave's own Vulkan device (the
    /// SESSION-0-relevant `create_device_by_compat` + `D3D11_TEXTURE_BIT` import + shared-fence path)
    /// → encode → upstream-decode. Returns the decoded plane means. A flat gray can't detect a plane
    /// swap / spatial error, so this fills Y≠Cb≠Cr.
    ///
    /// # Safety
    /// Runs on a real D3D11 + Vulkan-1.3 GPU; all COM/FFI handles are locally owned.
    unsafe fn run_case(w: u32, h: u32, hdr: bool, chroma444: bool) -> (f64, f64, f64) {
        // A fresh D3D11 device on the default hardware adapter.
        let mut device: Option<ID3D11Device> = None;
        let mut context: Option<ID3D11DeviceContext> = None;
        D3D11CreateDevice(
            None,
            D3D_DRIVER_TYPE_HARDWARE,
            HMODULE::default(),
            D3D11_CREATE_DEVICE_BGRA_SUPPORT,
            Some(&[D3D_FEATURE_LEVEL_11_1]),
            D3D11_SDK_VERSION,
            Some(&mut device),
            None,
            Some(&mut context),
        )
        .expect("D3D11CreateDevice");
        let device = device.unwrap();
        let context = context.unwrap();

        // Distinct plane fills at the session's plane formats/geometry. 16-bit fills use
        // v16 = v8 * 257 (0xVV,0xVV LE), whose UNORM value equals v8/255 EXACTLY — so the
        // 8-bit decode means expect the same 100/180/60 in every mode.
        let (cw, ch) = if chroma444 { (w, h) } else { (w / 2, h / 2) };
        let (y_tex, cbcr_tex) = if hdr {
            (
                make_plane(
                    &device,
                    &context,
                    w,
                    h,
                    DXGI_FORMAT_R16_UNORM,
                    2,
                    &[0x64, 0x64],
                ),
                make_plane(
                    &device,
                    &context,
                    cw,
                    ch,
                    DXGI_FORMAT_R16G16_UNORM,
                    4,
                    &[0xB4, 0xB4, 0x3C, 0x3C],
                ),
            )
        } else {
            (
                make_plane(&device, &context, w, h, DXGI_FORMAT_R8_UNORM, 1, &[100]),
                make_plane(
                    &device,
                    &context,
                    cw,
                    ch,
                    DXGI_FORMAT_R8G8_UNORM,
                    2,
                    &[180, 60],
                ),
            )
        };

        // Shared fence signalled after the fills (mirrors the capturer's convert→signal ordering).
        let dev5: ID3D11Device5 = device.cast().expect("ID3D11Device5");
        let mut fence: Option<ID3D11Fence> = None;
        dev5.CreateFence(0, D3D11_FENCE_FLAG_SHARED, &mut fence)
            .expect("CreateFence");
        let fence = fence.unwrap();
        let fence_handle = fence
            .CreateSharedHandle(None, 0x1000_0000, windows::core::PCWSTR::null())
            .expect("fence CreateSharedHandle");
        let ctx4: ID3D11DeviceContext4 = context.cast().expect("ID3D11DeviceContext4");
        ctx4.Signal(&fence, 1).expect("Signal");
        context.Flush();

        // Encode the shared textures through the real backend.
        let mut enc = PyroWaveEncoder::open(
            w,
            h,
            60,
            100_000_000,
            if chroma444 {
                crate::ChromaFormat::Yuv444
            } else {
                crate::ChromaFormat::Yuv420
            },
            if hdr { 10 } else { 8 },
        )
        .expect("PyroWaveEncoder::open");
        let frame = CapturedFrame {
            width: w,
            height: h,
            pts_ns: 0,
            format: PixelFormat::Nv12,
            payload: FramePayload::D3d11(D3d11Frame {
                texture: y_tex,
                device: device.clone(),
                pyro: Some(PyroFrameShare {
                    cbcr: cbcr_tex,
                    fence_handle: Some(fence_handle.0 as isize),
                    fence_value: 1,
                    // One synthetic ring for the whole case: a constant generation exercises the
                    // steady-state cache-hit path (a changing one would flush every frame).
                    ring_gen: 1,
                }),
            }),
            cursor: None,
        };
        enc.submit(&frame).expect("submit");
        let au = enc.poll().expect("poll").expect("one AU per frame");
        assert!(au.keyframe, "every pyrowave AU is a keyframe");
        assert!(!au.data.is_empty(), "AU is non-empty");
        // The dense AU starts with the 8-byte BitstreamSequenceHeader; the range VUI must read
        // LIMITED (bit 30 = byte 7 bit 6 = 0x40) — `mark_limited_range` corrects pyrowave's zeroed
        // default so VUI-honoring clients (Apple) don't wash out blacks.
        assert_eq!(
            au.data[7] & 0x40,
            0x40,
            "sequence header must signal ycbcr_range=LIMITED"
        );
        if hdr {
            assert_eq!(
                au.data[7] & 0x78,
                0x78,
                "HDR sequence header must signal BT.2020 primaries + PQ + BT.2020 matrix"
            );
        }
        decode_plane_means(w, h, &au.data, chroma444)
    }

    /// The Windows NV12 zero-copy path end-to-end on a real GPU. `#[ignore]`d (needs D3D11 + a
    /// Vulkan-1.3 device); build anywhere, run on the GPU host:
    ///   cargo test -p pf-encode --features pyrowave --no-run
    ///   <bin> --ignored --nocapture pyrowave_win_smoke
    /// Runs both a known-good square size and real streaming sizes to characterize the documented
    /// NVIDIA NV12 D3D11→Vulkan import size sensitivity (design doc Risk 4 / the interop-test note).
    #[test]
    #[ignore = "needs a real D3D11 + Vulkan-1.3 GPU (run on the Windows host, not the build box)"]
    fn pyrowave_win_smoke() {
        // The SDR 4:2:0 base case across real streaming sizes (the NVIDIA import
        // size-sensitivity check), then every other (hdr, chroma) mode at two sizes —
        // the R16/R16G16 and full-res-chroma imports are new surface for the same quirk.
        let mut cases = vec![
            (1024u32, 1024u32, false, false),
            (1280, 720, false, false),
            (1920, 1080, false, false),
            (2560, 1440, false, false),
        ];
        for &(hdr, c444) in &[(false, true), (true, false), (true, true)] {
            cases.push((1280, 720, hdr, c444));
            cases.push((1920, 1080, hdr, c444));
        }
        for (w, h, hdr, c444) in cases {
            // SAFETY: single-threaded test; `run_case` owns every COM/FFI handle it touches.
            let (ym, cbm, crm) = unsafe { run_case(w, h, hdr, c444) };
            eprintln!(
                "{w}x{h} hdr={hdr} 444={c444}: decoded means Y={ym:.1} Cb={cbm:.1} Cr={crm:.1} \
                 (expect 100/180/60)"
            );
            assert!(
                (ym - 100.0).abs() < 6.0 && (cbm - 180.0).abs() < 6.0 && (crm - 60.0).abs() < 6.0,
                "{w}x{h} hdr={hdr} 444={c444}: round-trip means (Y {ym:.1}, Cb {cbm:.1}, \
                 Cr {crm:.1}) drifted from the filled 100/180/60 — plane mapping/format wrong"
            );
        }
    }
}
