mediaway 0.1.8

Convenience pipeline layer — composes encoder + container (+ device capture)
//! DX12 HAL escape hatch → Mediaway D3D12→D3D11 `GpuCopy` bridge.
//!
//! wgpu has no native video-encode API. This module reaches past wgpu's own
//! API via its **HAL interop escape hatches** (`wgpu::Device::as_hal`,
//! `wgpu::Device::create_texture_from_hal`) to recover the *native*
//! `ID3D12Device` / `ID3D12Resource` wgpu's DX12 backend already holds, then
//! hands that native device to the existing
//! [`mediaway_encoder::windows::D3d12SharedEncodeBridge`] (D3D12 shared heap →
//! native D3D11), so an app already rendering/compositing with wgpu can
//! encode through `mediaway-encoder-windows`'s WMF hardware encoder without a
//! GPU→CPU readback.
//!
//! **Path class: `GpuCopy`, not Zero-Copy.** wgpu has no D3D11 backend; its
//! only Windows-native backend is DX12, and Windows Media Foundation hardware
//! encoder MFTs reject `D3D11On12`-wrapped textures
//! (`MF_E_UNSUPPORTED_D3D_TYPE`, see `mediaway-encoder-windows` ADR-0006). So
//! the same one-copy-per-frame bridge ADR-0006 already ships for native D3D12
//! apps is the only real bridge available today — [`WgpuDx12Bridge::copy_frame`]
//! records one GPU→GPU `CopyResource`, then a CPU-side `device.poll(Wait)`
//! stall (the bridge's shared NT handle carries no cross-device fence yet)
//! before the caller may push the resulting handle into the encoder. See
//! [ADR-0001](../adr/0001-dx12-hal-gpucopy-bridge.md).

#![allow(unsafe_code)]

use mediaway_common::{GpuBufferHandle, GpuDeviceHandle, NativeHandle};
use mediaway_encoder::EncodeError;
// `wgpu-hal` 30.x pins the same `windows`/`windows-core` 0.62 line this
// workspace already uses (unlike 26.x, which pinned 0.58 — see ADR-0001's
// now-resolved "windows-rs version straddle" section), so no second,
// separately-versioned `windows` dependency is needed to name
// `wgpu_hal::dx12::Device::raw_device`/`texture_from_raw`'s types.
use windows::Win32::Graphics::Direct3D12::{ID3D12Device, ID3D12Resource};
use windows::core::Interface;

use crate::wgpu::error::WgpuInteropError;

/// Fixed pixel format the bridge's shared D3D12 texture is allocated in.
///
/// Matches [`mediaway_encoder::windows::D3d12SharedEncodeBridge`]'s own
/// `DXGI_FORMAT_B8G8R8A8_UNORM` allocation and `PixelFormat::Bgra8`'s
/// Zero-Copy DX11 input path (`mediaway-encoder-windows` ADR-0005). Callers
/// must render/hold `source` textures in this format.
pub const BRIDGE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;

/// wgpu DX12 HAL interop → Windows encode `GpuCopy` bridge.
///
/// Open once per encode session with a fixed `width`/`height`; call
/// [`Self::copy_frame`] once per frame. `device`/`queue` passed to
/// [`Self::copy_frame`] must be the same pair used to open this bridge via
/// [`Self::new`] — the wrapped destination texture (and the underlying
/// `ID3D12Device` the bridge opened on) are tied to that specific `wgpu`
/// device instance.
pub struct WgpuDx12Bridge {
    bridge: mediaway_encoder::windows::D3d12SharedEncodeBridge,
    /// The bridge's own shared D3D12 resource, re-wrapped as a `wgpu::Texture`
    /// once so [`Self::copy_frame`] can record an ordinary
    /// `copy_texture_to_texture` instead of hand-rolled HAL command recording.
    dest: wgpu::Texture,
    width: u32,
    height: u32,
}

impl WgpuDx12Bridge {
    /// Extract the native `ID3D12Device*` behind `device` (must be wgpu's DX12
    /// backend) and open a [`mediaway_encoder::windows::D3d12SharedEncodeBridge`]
    /// sized `width`x`height` on it.
    ///
    /// # Errors
    ///
    /// [`WgpuInteropError::HalUnavailable`] when `device` is not backed by
    /// wgpu's DX12 HAL (a Vulkan/Metal/GL/custom/`BrowserWebGpu` backend, or a
    /// build without the `dx12` wgpu feature). [`WgpuInteropError::InvalidInput`]
    /// for zero size or a null device pointer. [`WgpuInteropError::Bridge`]
    /// when the underlying D3D12/D3D11 bridge fails to open — see
    /// [`D3d12SharedEncodeBridge::open`](mediaway_encoder::windows::D3d12SharedEncodeBridge::open).
    pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Result<Self, WgpuInteropError> {
        if width == 0 || height == 0 {
            return Err(WgpuInteropError::InvalidInput);
        }

        let device_handle = extract_d3d12_device_handle(device)?;
        let bridge =
            mediaway_encoder::windows::D3d12SharedEncodeBridge::open(device_handle, width, height)?;
        let dest = wrap_bridge_resource(device, &bridge, width, height)?;

        Ok(Self {
            bridge,
            dest,
            width,
            height,
        })
    }

    /// Import a caller-owned D3D12 resource — already allocated with
    /// `D3D12_HEAP_FLAG_SHARED` (+ render-target-allow), [`BRIDGE_FORMAT`],
    /// `width`x`height`, and already living on the same `ID3D12Device` `device`
    /// resolves to — instead of letting [`Self::new`] allocate its own shared
    /// texture.
    ///
    /// **Zero-Copy by construction**: no internal texture is allocated, so
    /// there is nothing for [`Self::copy_frame`] to copy into. Callers using
    /// this constructor should use [`Self::render_target`] + [`Self::handle`]
    /// instead. See [ADR-0005](../../adr/wgpu/0005-render-target-and-external-shared-resource.md).
    ///
    /// # Errors
    ///
    /// Same as [`Self::new`] (`HalUnavailable`/`InvalidInput`/`Bridge`) — plus
    /// [`WgpuInteropError::InvalidInput`] if `resource` is not a live pointer,
    /// or [`WgpuInteropError::Bridge`] if it was not actually shared-heap
    /// allocated (`CreateSharedHandle` fails cleanly for a non-shared
    /// resource).
    pub fn from_external_shared_resource(
        device: &wgpu::Device,
        resource: NativeHandle,
        width: u32,
        height: u32,
    ) -> Result<Self, WgpuInteropError> {
        if width == 0 || height == 0 {
            return Err(WgpuInteropError::InvalidInput);
        }

        let device_handle = extract_d3d12_device_handle(device)?;
        let bridge = mediaway_encoder::windows::D3d12SharedEncodeBridge::open_with_resource(
            device_handle,
            resource,
        )?;
        let dest = wrap_bridge_resource(device, &bridge, width, height)?;

        Ok(Self {
            bridge,
            dest,
            width,
            height,
        })
    }

    /// [`GpuDeviceHandle::DirectX11`] for
    /// [`mediaway_encoder::VideoEncoderConfig::gpu_device`] — the native D3D11
    /// device the bridge opened on the same adapter.
    ///
    /// # Errors
    ///
    /// [`WgpuInteropError::Bridge`] if the live COM interface's pointer is
    /// somehow null (not expected in practice).
    #[must_use = "the returned handle is required to open a Zero-Copy-input encoder"]
    pub fn gpu_device_handle(&self) -> Result<GpuDeviceHandle, WgpuInteropError> {
        Ok(GpuDeviceHandle::DirectX11(
            self.bridge.d3d11_device_handle()?,
        ))
    }

    /// Copy `source` (a caller-owned `wgpu::Texture`, [`BRIDGE_FORMAT`], same
    /// `width`/`height` as [`Self::new`]) into the bridge's shared texture and
    /// wait for the GPU copy to complete.
    ///
    /// **Costly path — `GpuCopy` plus one CPU↔GPU sync stall per frame.** Not
    /// Zero-Copy: this records `copy_texture_to_texture` on `device`'s own
    /// queue, submits it, then blocks (`device.poll(PollType::Wait)`) until
    /// that submission finishes. The stall exists because the bridge's shared
    /// NT handle carries no cross-device fence — without it, WMF's own
    /// (different) D3D11 device could race the copy. Returns a
    /// [`GpuBufferHandle::DirectX11`] ready to push into a
    /// [`mediaway_common::VideoFrame`] for that same frame.
    ///
    /// # Errors
    ///
    /// [`WgpuInteropError::InvalidInput`] when `source`'s size or format does
    /// not match the bridge. [`WgpuInteropError::Bridge`] on a `poll` failure
    /// or a bridge handle failure.
    pub fn copy_frame(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        source: &wgpu::Texture,
    ) -> Result<GpuBufferHandle, WgpuInteropError> {
        if source.width() != self.width
            || source.height() != self.height
            || source.format() != BRIDGE_FORMAT
        {
            return Err(WgpuInteropError::InvalidInput);
        }

        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("mediaway-wgpu dx12 bridge copy"),
        });
        let extent = wgpu::Extent3d {
            width: self.width,
            height: self.height,
            depth_or_array_layers: 1,
        };
        encoder.copy_texture_to_texture(
            wgpu::TexelCopyTextureInfo {
                texture: source,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            wgpu::TexelCopyTextureInfo {
                texture: &self.dest,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            extent,
        );
        let submission_index = queue.submit(std::iter::once(encoder.finish()));

        device
            .poll(wgpu::PollType::Wait {
                submission_index: Some(submission_index),
                timeout: None,
            })
            .map_err(|_| WgpuInteropError::Bridge(EncodeError::Backend))?;

        Ok(self.bridge.as_dx11_handle()?)
    }

    /// The bridge's own shared texture, as an ordinary `wgpu::Texture` a caller
    /// can render into directly — the **Zero-Copy** alternative to
    /// [`Self::copy_frame`]: render into this instead of a separate texture,
    /// then call [`Self::handle`] (never both in the same frame).
    ///
    /// Returns the **same** underlying GPU allocation on every call — reused
    /// and overwritten each frame, exactly like [`Self::copy_frame`]'s
    /// destination — single-buffered, not a frame queue. Holding a view
    /// across the next frame's render observes that next frame's content.
    #[must_use]
    pub const fn render_target(&self) -> &wgpu::Texture {
        &self.dest
    }

    /// Wait for all of `device`'s outstanding GPU work, then return a
    /// [`GpuBufferHandle::DirectX11`] for the bridge's shared texture — the
    /// **Zero-Copy** counterpart of [`Self::copy_frame`] for callers that
    /// rendered directly into [`Self::render_target`].
    ///
    /// **Costly path — one CPU↔GPU sync stall, no payload copy.** Unlike
    /// `copy_frame`'s targeted `WaitForSubmissionIndex`-shaped wait, this
    /// waits for *all* of `device`'s outstanding work (the caller's render-pass
    /// submission index isn't known to the bridge) — a caller with other
    /// unrelated pending GPU work on the same `device` pays a longer stall.
    ///
    /// # Errors
    ///
    /// [`WgpuInteropError::Bridge`] on a `poll` failure or a bridge handle
    /// failure.
    pub fn handle(&self, device: &wgpu::Device) -> Result<GpuBufferHandle, WgpuInteropError> {
        device
            .poll(wgpu::PollType::Wait {
                submission_index: None,
                timeout: None,
            })
            .map_err(|_| WgpuInteropError::Bridge(EncodeError::Backend))?;

        Ok(self.bridge.as_dx11_handle()?)
    }
}

/// Extract the native `ID3D12Device*` behind `device` (must be wgpu's DX12
/// backend) as a [`NativeHandle`].
///
/// # Errors
///
/// [`WgpuInteropError::HalUnavailable`] when `device` is not backed by wgpu's
/// DX12 HAL. [`WgpuInteropError::InvalidInput`] for a null device pointer.
fn extract_d3d12_device_handle(device: &wgpu::Device) -> Result<NativeHandle, WgpuInteropError> {
    // SAFETY: `as_hal`'s contract only requires the returned resource not be
    // destroyed while the guard is the last live reference; we only read the
    // raw pointer through `Interface::as_raw` below and drop the guard
    // immediately after, never touching the device destructively.
    let native_device = unsafe { device.as_hal::<wgpu::hal::api::Dx12>() }
        .ok_or(WgpuInteropError::HalUnavailable)?;
    let raw_device: &ID3D12Device = native_device.raw_device();
    let device_handle = NativeHandle::new(Interface::as_raw(raw_device) as usize)
        .ok_or(WgpuInteropError::InvalidInput)?;
    drop(native_device);
    Ok(device_handle)
}

/// Re-wrap the bridge's own shared `ID3D12Resource` as a `wgpu::Texture`
/// (once, at open time) so [`WgpuDx12Bridge::copy_frame`] can use wgpu's
/// ordinary `copy_texture_to_texture` instead of recording raw HAL commands
/// by hand every frame.
fn wrap_bridge_resource(
    device: &wgpu::Device,
    bridge: &mediaway_encoder::windows::D3d12SharedEncodeBridge,
    width: u32,
    height: u32,
) -> Result<wgpu::Texture, WgpuInteropError> {
    let handle = bridge.d3d12_resource_handle()?;
    let raw = handle.get() as *mut core::ffi::c_void;
    // SAFETY: `handle` came from a live `ID3D12Resource` the bridge owns for
    // the whole session's lifetime; `from_raw_borrowed` + `clone()` AddRefs a
    // new, independent COM reference for the hal texture wrapper to own —
    // mirrors the existing `device_from_handle` pattern in
    // `mediaway-encoder-windows`'s `wmf::dx11` module.
    let borrowed =
        unsafe { ID3D12Resource::from_raw_borrowed(&raw) }.ok_or(WgpuInteropError::InvalidInput)?;
    let resource: ID3D12Resource = borrowed.clone(); // clone: COM AddRef, hal texture takes ownership

    let size = wgpu::Extent3d {
        width,
        height,
        depth_or_array_layers: 1,
    };
    // SAFETY: `resource` is the exact single-mip, single-sample,
    // `DXGI_FORMAT_B8G8R8A8_UNORM` (matching `BRIDGE_FORMAT`), `width`x`height`
    // D3D12 texture `D3d12SharedEncodeBridge::open` allocates — the size/format
    // arguments below describe that same shape, not an assumption.
    // `texture_from_raw` is an associated function on `dx12::Device` (not
    // `Texture` — it has no `&self` receiver either; it's a free constructor
    // namespaced under `Device`), confirmed against the vendored `wgpu-hal`
    // 30.0.0 source (`src/dx12/device.rs`), not guessed from an older/newer
    // `wgpu` version's docs.
    let hal_texture = unsafe {
        wgpu::hal::dx12::Device::texture_from_raw(
            resource,
            BRIDGE_FORMAT,
            wgpu::TextureDimension::D2,
            size,
            1,
            1,
        )
    };
    let texture_desc = wgpu::TextureDescriptor {
        label: Some("mediaway-wgpu dx12 bridge dest"),
        size,
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: BRIDGE_FORMAT,
        // RENDER_ATTACHMENT: the underlying D3D12 resource already allows render-target use
        // (D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET, mediaway-encoder ADR-0006) — this just lets
        // callers create a render-pass color attachment view via `Self::render_target` (ADR-0005).
        usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT,
        view_formats: &[],
    };
    // SAFETY: `hal_texture` was just built from `texture_desc`'s exact
    // size/format/mip/sample-count parameters above, and from a resource
    // whose owning `ID3D12Device` is the same one `device.as_hal::<Dx12>()`
    // returned in `WgpuDx12Bridge::new` (the bridge opened on that extracted
    // handle) — satisfying `create_texture_from_hal`'s "created from this
    // device's internal handle" contract. `initial_state: UNINITIALIZED` per
    // wgpu 30's own doc guidance for a not-yet-initialized resource — this
    // texture's contents are set by `copy_frame`'s `copy_texture_to_texture`
    // after wrapping, never read before that first copy.
    let wgpu_texture = unsafe {
        device.create_texture_from_hal::<wgpu::hal::api::Dx12>(
            hal_texture,
            &texture_desc,
            wgpu::TextureUses::UNINITIALIZED,
        )
    };
    Ok(wgpu_texture)
}