Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,885 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::{Arc, atomic::Ordering};
|
||||
|
||||
use windows::Graphics::Capture::{
|
||||
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, GraphicsCaptureSession,
|
||||
};
|
||||
use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
|
||||
use windows::Graphics::DirectX::DirectXPixelFormat;
|
||||
use windows::Graphics::SizeInt32;
|
||||
use windows::Win32::Foundation::{HWND, LPARAM, RECT, RPC_E_CHANGED_MODE};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, ID3D11Device,
|
||||
ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::IDXGIDevice;
|
||||
use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, HDC, HMONITOR};
|
||||
use windows::Win32::System::WinRT::Direct3D11::{
|
||||
CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess,
|
||||
};
|
||||
use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
|
||||
use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize};
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
|
||||
use windows::core::{BOOL, Interface};
|
||||
|
||||
use crate::dxgi_capture::{
|
||||
SharedTextureOutput, capture_timestamp_us, create_shared_output_texture,
|
||||
pacing_sleep_and_next_deadline, resolve_output_size,
|
||||
};
|
||||
use crate::nv12_gpu::Nv12GpuConverter;
|
||||
use crate::{
|
||||
CaptureInner, emit_lifecycle, emit_shared_texture_frame, note_media_frame_without_sink,
|
||||
resolve_frame_sink,
|
||||
};
|
||||
|
||||
const WGC_FRAME_POOL_BUFFERS: i32 = 2;
|
||||
const WGC_FRAME_DRAIN_LIMIT: u32 = 4;
|
||||
const WGC_MONITOR_ENUM_LIMIT: usize = 16;
|
||||
|
||||
fn ensure_winrt_initialized() {
|
||||
let result = unsafe { RoInitialize(RO_INIT_MULTITHREADED) };
|
||||
if let Err(error) = result {
|
||||
assert!(
|
||||
error.code() == RPC_E_CHANGED_MODE,
|
||||
"RoInitialize failed unexpectedly: {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wgc_capture_supported() -> bool {
|
||||
ensure_winrt_initialized();
|
||||
GraphicsCaptureSession::IsSupported().unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum WgcCaptureTarget {
|
||||
Window(HWND),
|
||||
Monitor(HMONITOR),
|
||||
}
|
||||
|
||||
impl WgcCaptureTarget {
|
||||
fn create_item(
|
||||
self,
|
||||
interop: &IGraphicsCaptureItemInterop,
|
||||
) -> Result<GraphicsCaptureItem, String> {
|
||||
match self {
|
||||
Self::Window(hwnd) => unsafe { interop.CreateForWindow(hwnd) }
|
||||
.map_err(|e| format!("IGraphicsCaptureItemInterop::CreateForWindow: {e}")),
|
||||
Self::Monitor(monitor) => unsafe { interop.CreateForMonitor(monitor) }
|
||||
.map_err(|e| format!("IGraphicsCaptureItemInterop::CreateForMonitor: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_alive(self) -> bool {
|
||||
match self {
|
||||
Self::Window(hwnd) => unsafe { IsWindow(Some(hwnd)) }.as_bool(),
|
||||
Self::Monitor(monitor) => !monitor.is_invalid(),
|
||||
}
|
||||
}
|
||||
|
||||
fn closed_message(self) -> &'static str {
|
||||
match self {
|
||||
Self::Window(_) => "window closed",
|
||||
Self::Monitor(_) => "monitor capture target unavailable",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_monitor_source_id(source_id: &str, source_kind: &str) -> Option<HMONITOR> {
|
||||
if source_kind != "screen" {
|
||||
return None;
|
||||
}
|
||||
let mut parts = source_id.split(':');
|
||||
if parts.next()? != "screen" {
|
||||
return None;
|
||||
}
|
||||
let index = parts.next()?.parse::<usize>().ok()?;
|
||||
let sub_id = parts.next()?;
|
||||
if sub_id != "0" && sub_id != "1" {
|
||||
return None;
|
||||
}
|
||||
if parts.next().is_some() || index >= WGC_MONITOR_ENUM_LIMIT {
|
||||
return None;
|
||||
}
|
||||
enumerate_monitors().get(index).copied()
|
||||
}
|
||||
|
||||
fn enumerate_monitors() -> Vec<HMONITOR> {
|
||||
struct MonitorEnumState {
|
||||
monitors: Vec<HMONITOR>,
|
||||
}
|
||||
|
||||
unsafe extern "system" fn enum_monitor(
|
||||
monitor: HMONITOR,
|
||||
_hdc: HDC,
|
||||
_rect: *mut RECT,
|
||||
param: LPARAM,
|
||||
) -> BOOL {
|
||||
let state = unsafe { &mut *(param.0 as *mut MonitorEnumState) };
|
||||
if state.monitors.len() >= WGC_MONITOR_ENUM_LIMIT {
|
||||
return BOOL(0);
|
||||
}
|
||||
if !monitor.is_invalid() {
|
||||
state.monitors.push(monitor);
|
||||
}
|
||||
BOOL(1)
|
||||
}
|
||||
|
||||
let mut state = MonitorEnumState {
|
||||
monitors: Vec::with_capacity(WGC_MONITOR_ENUM_LIMIT),
|
||||
};
|
||||
unsafe {
|
||||
let _ = EnumDisplayMonitors(
|
||||
None,
|
||||
None,
|
||||
Some(enum_monitor),
|
||||
LPARAM((&mut state as *mut MonitorEnumState) as isize),
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
state.monitors.len() <= WGC_MONITOR_ENUM_LIMIT,
|
||||
"monitor enumeration bounded"
|
||||
);
|
||||
state.monitors
|
||||
}
|
||||
|
||||
pub struct WgcCaptureSession {
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
d3d_device: IDirect3DDevice,
|
||||
item: GraphicsCaptureItem,
|
||||
target: WgcCaptureTarget,
|
||||
output_width: u32,
|
||||
output_height: u32,
|
||||
requested_width: Option<u32>,
|
||||
requested_height: Option<u32>,
|
||||
}
|
||||
|
||||
unsafe impl Send for WgcCaptureSession {}
|
||||
|
||||
impl WgcCaptureSession {
|
||||
pub fn new(
|
||||
hwnd: HWND,
|
||||
requested_width: Option<u32>,
|
||||
requested_height: Option<u32>,
|
||||
) -> Result<Self, String> {
|
||||
Self::new_for_target(
|
||||
WgcCaptureTarget::Window(hwnd),
|
||||
requested_width,
|
||||
requested_height,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_monitor(
|
||||
monitor: HMONITOR,
|
||||
requested_width: Option<u32>,
|
||||
requested_height: Option<u32>,
|
||||
) -> Result<Self, String> {
|
||||
Self::new_for_target(
|
||||
WgcCaptureTarget::Monitor(monitor),
|
||||
requested_width,
|
||||
requested_height,
|
||||
)
|
||||
}
|
||||
|
||||
fn new_for_target(
|
||||
target: WgcCaptureTarget,
|
||||
requested_width: Option<u32>,
|
||||
requested_height: Option<u32>,
|
||||
) -> Result<Self, String> {
|
||||
ensure_winrt_initialized();
|
||||
let (device, context) = crate::game_capture::create_shared_texture_device(None)?;
|
||||
let dxgi_device: IDXGIDevice = device
|
||||
.cast()
|
||||
.map_err(|e| format!("IDXGIDevice cast: {e}"))?;
|
||||
let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }
|
||||
.map_err(|e| format!("CreateDirect3D11DeviceFromDXGIDevice: {e}"))?;
|
||||
let d3d_device: IDirect3DDevice = inspectable
|
||||
.cast()
|
||||
.map_err(|e| format!("IDirect3DDevice cast: {e}"))?;
|
||||
let interop = windows::core::factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()
|
||||
.map_err(|e| format!("GraphicsCaptureItem interop factory: {e}"))?;
|
||||
let item = target.create_item(&interop)?;
|
||||
let size = item
|
||||
.Size()
|
||||
.map_err(|e| format!("GraphicsCaptureItem.Size: {e}"))?;
|
||||
let content_width = size.Width.max(1) as u32;
|
||||
let content_height = size.Height.max(1) as u32;
|
||||
let (output_width, output_height) = resolve_output_size(
|
||||
content_width,
|
||||
content_height,
|
||||
requested_width,
|
||||
requested_height,
|
||||
);
|
||||
assert!(output_width > 0, "WGC output width positive");
|
||||
assert!(output_height > 0, "WGC output height positive");
|
||||
Ok(Self {
|
||||
device,
|
||||
context,
|
||||
d3d_device,
|
||||
item,
|
||||
target,
|
||||
output_width,
|
||||
output_height,
|
||||
requested_width,
|
||||
requested_height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn capture_width(&self) -> u32 {
|
||||
assert!(self.output_width > 0, "WGC output width positive");
|
||||
assert!(self.output_height > 0, "WGC output height positive");
|
||||
self.output_width
|
||||
}
|
||||
|
||||
pub fn capture_height(&self) -> u32 {
|
||||
assert!(self.output_height > 0, "WGC output height positive");
|
||||
assert!(self.output_width > 0, "WGC output width positive");
|
||||
self.output_height
|
||||
}
|
||||
}
|
||||
|
||||
struct WgcState {
|
||||
frame_pool: Direct3D11CaptureFramePool,
|
||||
session: GraphicsCaptureSession,
|
||||
output_pipeline: Option<WgcOutputPipeline>,
|
||||
pixel_format: DirectXPixelFormat,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
out_w: u32,
|
||||
out_h: u32,
|
||||
}
|
||||
|
||||
impl WgcState {
|
||||
fn close(self) {
|
||||
let _ = self.session.Close();
|
||||
let _ = self.frame_pool.Close();
|
||||
}
|
||||
}
|
||||
|
||||
struct WgcNv12Pipeline {
|
||||
input_resource: ID3D11Resource,
|
||||
converter: Nv12GpuConverter,
|
||||
}
|
||||
|
||||
enum WgcOutputPipeline {
|
||||
Bgra(SharedTextureOutput),
|
||||
Nv12(WgcNv12Pipeline),
|
||||
}
|
||||
|
||||
enum WgcFrameResult {
|
||||
Ok,
|
||||
NoFrame,
|
||||
Resized { width: u32, height: u32 },
|
||||
Error(String),
|
||||
}
|
||||
|
||||
enum LoopStep {
|
||||
Paced,
|
||||
Restart,
|
||||
}
|
||||
|
||||
struct WgcLoopContext {
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
d3d_device: IDirect3DDevice,
|
||||
item: GraphicsCaptureItem,
|
||||
target: WgcCaptureTarget,
|
||||
requested_width: Option<u32>,
|
||||
requested_height: Option<u32>,
|
||||
}
|
||||
|
||||
fn wgc_loop_context(inner: &Arc<CaptureInner>) -> Option<WgcLoopContext> {
|
||||
let guard = inner.wgc_session.lock();
|
||||
let session = guard.as_ref()?;
|
||||
Some(WgcLoopContext {
|
||||
device: session.device.clone(),
|
||||
context: session.context.clone(),
|
||||
d3d_device: session.d3d_device.clone(),
|
||||
item: session.item.clone(),
|
||||
target: session.target,
|
||||
requested_width: session.requested_width,
|
||||
requested_height: session.requested_height,
|
||||
})
|
||||
}
|
||||
|
||||
fn teardown_wgc_state(wgc_state: &mut Option<WgcState>) {
|
||||
if let Some(state) = wgc_state.take() {
|
||||
state.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep_with_backoff(backoff: &mut std::time::Duration) {
|
||||
assert!(
|
||||
*backoff >= std::time::Duration::from_millis(100),
|
||||
"backoff at least base interval"
|
||||
);
|
||||
assert!(
|
||||
*backoff <= std::time::Duration::from_secs(2),
|
||||
"backoff bounded"
|
||||
);
|
||||
std::thread::sleep(*backoff);
|
||||
*backoff = (*backoff * 2).min(std::time::Duration::from_secs(2));
|
||||
}
|
||||
|
||||
pub fn capture_loop(inner: &Arc<CaptureInner>, frame_interval: std::time::Duration) {
|
||||
ensure_winrt_initialized();
|
||||
let Some(ctx) = wgc_loop_context(inner) else {
|
||||
emit_lifecycle(inner, "closed-clean", "no WGC session");
|
||||
return;
|
||||
};
|
||||
|
||||
let capture_id = inner.capture_id.lock().clone();
|
||||
let mut wgc_state: Option<WgcState> = None;
|
||||
let mut recreate_backoff = std::time::Duration::from_millis(100);
|
||||
let capture_start = std::time::Instant::now();
|
||||
let mut next_frame_deadline = capture_start + frame_interval;
|
||||
let mut frames_dropped_coalesced: u64 = 0;
|
||||
|
||||
while inner.running.load(Ordering::Acquire) {
|
||||
if !ctx.target.is_alive() {
|
||||
emit_lifecycle(inner, "closed", ctx.target.closed_message());
|
||||
break;
|
||||
}
|
||||
|
||||
if wgc_state.is_none() {
|
||||
match setup_wgc_state(&ctx) {
|
||||
Ok(state) => {
|
||||
wgc_state = Some(state);
|
||||
}
|
||||
Err(e) => {
|
||||
if !inner.running.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
emit_lifecycle(
|
||||
inner,
|
||||
"error",
|
||||
&format!("Failed to create WGC capture session: {e}"),
|
||||
);
|
||||
sleep_with_backoff(&mut recreate_backoff);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(state) = wgc_state.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let result = poll_and_emit_frame(
|
||||
inner,
|
||||
&ctx.context,
|
||||
state,
|
||||
capture_id.as_deref(),
|
||||
capture_start,
|
||||
&mut frames_dropped_coalesced,
|
||||
);
|
||||
match handle_frame_result(inner, &ctx, &mut wgc_state, result, &mut recreate_backoff) {
|
||||
LoopStep::Paced => {}
|
||||
LoopStep::Restart => continue,
|
||||
}
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
let (sleep_duration, deadline) =
|
||||
pacing_sleep_and_next_deadline(now, next_frame_deadline, frame_interval);
|
||||
next_frame_deadline = deadline;
|
||||
if sleep_duration > std::time::Duration::ZERO {
|
||||
std::thread::sleep(sleep_duration);
|
||||
}
|
||||
}
|
||||
|
||||
teardown_wgc_state(&mut wgc_state);
|
||||
inner.running.store(false, Ordering::Release);
|
||||
emit_lifecycle(inner, "closed-clean", "capture stopped");
|
||||
}
|
||||
|
||||
fn handle_frame_result(
|
||||
inner: &Arc<CaptureInner>,
|
||||
ctx: &WgcLoopContext,
|
||||
wgc_state: &mut Option<WgcState>,
|
||||
result: WgcFrameResult,
|
||||
recreate_backoff: &mut std::time::Duration,
|
||||
) -> LoopStep {
|
||||
match result {
|
||||
WgcFrameResult::Ok => {
|
||||
*recreate_backoff = std::time::Duration::from_millis(100);
|
||||
LoopStep::Paced
|
||||
}
|
||||
WgcFrameResult::NoFrame => LoopStep::Paced,
|
||||
WgcFrameResult::Resized { width, height } => {
|
||||
let Some(state) = wgc_state.as_mut() else {
|
||||
return LoopStep::Restart;
|
||||
};
|
||||
match resize_wgc_state(ctx, state, width, height) {
|
||||
Ok(()) => LoopStep::Paced,
|
||||
Err(e) => {
|
||||
emit_lifecycle(
|
||||
inner,
|
||||
"error",
|
||||
&format!("WGC frame pool resize failed: {e}"),
|
||||
);
|
||||
teardown_wgc_state(wgc_state);
|
||||
sleep_with_backoff(recreate_backoff);
|
||||
LoopStep::Restart
|
||||
}
|
||||
}
|
||||
}
|
||||
WgcFrameResult::Error(e) => {
|
||||
emit_lifecycle(inner, "error", &e);
|
||||
{
|
||||
let mut guard = inner.fallback.lock();
|
||||
if let Some(tracker) = guard.as_mut() {
|
||||
let _ = tracker.observe(crate::fallback::FailureSignature::DeviceLost);
|
||||
}
|
||||
}
|
||||
teardown_wgc_state(wgc_state);
|
||||
sleep_with_backoff(recreate_backoff);
|
||||
LoopStep::Restart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_wgc_state(ctx: &WgcLoopContext) -> Result<WgcState, String> {
|
||||
let size = ctx
|
||||
.item
|
||||
.Size()
|
||||
.map_err(|e| format!("GraphicsCaptureItem.Size: {e}"))?;
|
||||
let content_width = size.Width.max(1) as u32;
|
||||
let content_height = size.Height.max(1) as u32;
|
||||
let (out_w, out_h) = resolve_output_size(
|
||||
content_width,
|
||||
content_height,
|
||||
ctx.requested_width,
|
||||
ctx.requested_height,
|
||||
);
|
||||
create_wgc_state_for_format(
|
||||
ctx,
|
||||
DirectXPixelFormat::R16G16B16A16Float,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
)
|
||||
.or_else(|_| {
|
||||
create_wgc_state_for_format(
|
||||
ctx,
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_wgc_state_for_format(
|
||||
ctx: &WgcLoopContext,
|
||||
pixel_format: DirectXPixelFormat,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
out_w: u32,
|
||||
out_h: u32,
|
||||
) -> Result<WgcState, String> {
|
||||
let output_pipeline = create_wgc_output_pipeline(
|
||||
ctx,
|
||||
pixel_format,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
)?;
|
||||
let frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
|
||||
&ctx.d3d_device,
|
||||
pixel_format,
|
||||
WGC_FRAME_POOL_BUFFERS,
|
||||
SizeInt32 {
|
||||
Width: content_width as i32,
|
||||
Height: content_height as i32,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Direct3D11CaptureFramePool::CreateFreeThreaded: {e}"))?;
|
||||
let session = frame_pool
|
||||
.CreateCaptureSession(&ctx.item)
|
||||
.map_err(|e| format!("Direct3D11CaptureFramePool.CreateCaptureSession: {e}"))?;
|
||||
session
|
||||
.StartCapture()
|
||||
.map_err(|e| format!("GraphicsCaptureSession.StartCapture: {e}"))?;
|
||||
Ok(WgcState {
|
||||
frame_pool,
|
||||
session,
|
||||
output_pipeline,
|
||||
pixel_format,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
})
|
||||
}
|
||||
|
||||
fn resize_wgc_state(
|
||||
ctx: &WgcLoopContext,
|
||||
state: &mut WgcState,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), String> {
|
||||
assert!(width > 0, "WGC resize width positive");
|
||||
assert!(height > 0, "WGC resize height positive");
|
||||
let (out_w, out_h) =
|
||||
resolve_output_size(width, height, ctx.requested_width, ctx.requested_height);
|
||||
match resize_wgc_state_for_format(ctx, state, state.pixel_format, width, height, out_w, out_h) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(first_error) if state.pixel_format == DirectXPixelFormat::R16G16B16A16Float => {
|
||||
resize_wgc_state_for_format(
|
||||
ctx,
|
||||
state,
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
width,
|
||||
height,
|
||||
out_w,
|
||||
out_h,
|
||||
)
|
||||
.map_err(|fallback_error| {
|
||||
format!(
|
||||
"HDR resize failed ({first_error}); BGRA fallback failed ({fallback_error})"
|
||||
)
|
||||
})
|
||||
}
|
||||
Err(first_error) => Err(first_error),
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_wgc_state_for_format(
|
||||
ctx: &WgcLoopContext,
|
||||
state: &mut WgcState,
|
||||
pixel_format: DirectXPixelFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
out_w: u32,
|
||||
out_h: u32,
|
||||
) -> Result<(), String> {
|
||||
let output_pipeline =
|
||||
create_wgc_output_pipeline(ctx, pixel_format, width, height, out_w, out_h)?;
|
||||
state
|
||||
.frame_pool
|
||||
.Recreate(
|
||||
&ctx.d3d_device,
|
||||
pixel_format,
|
||||
WGC_FRAME_POOL_BUFFERS,
|
||||
SizeInt32 {
|
||||
Width: width as i32,
|
||||
Height: height as i32,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Direct3D11CaptureFramePool.Recreate: {e}"))?;
|
||||
state.content_width = width;
|
||||
state.content_height = height;
|
||||
state.out_w = out_w;
|
||||
state.out_h = out_h;
|
||||
state.pixel_format = pixel_format;
|
||||
state.output_pipeline = output_pipeline;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_wgc_output_pipeline(
|
||||
ctx: &WgcLoopContext,
|
||||
pixel_format: DirectXPixelFormat,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
out_w: u32,
|
||||
out_h: u32,
|
||||
) -> Result<Option<WgcOutputPipeline>, String> {
|
||||
if pixel_format == DirectXPixelFormat::R16G16B16A16Float {
|
||||
return create_wgc_nv12_pipeline(
|
||||
ctx,
|
||||
DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
crate::hdr::SourceFormat::Rgba16Float { hdr: true },
|
||||
)
|
||||
.map(WgcOutputPipeline::Nv12)
|
||||
.map(Some);
|
||||
}
|
||||
if out_w != content_width || out_h != content_height {
|
||||
return create_wgc_nv12_pipeline(
|
||||
ctx,
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
crate::hdr::SourceFormat::Bgra8,
|
||||
)
|
||||
.map(WgcOutputPipeline::Nv12)
|
||||
.map(Some);
|
||||
}
|
||||
Ok(
|
||||
create_shared_output_texture(&ctx.device, content_width, content_height)
|
||||
.ok()
|
||||
.map(WgcOutputPipeline::Bgra),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_wgc_nv12_pipeline(
|
||||
ctx: &WgcLoopContext,
|
||||
input_format: DXGI_FORMAT,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
out_w: u32,
|
||||
out_h: u32,
|
||||
source_format: crate::hdr::SourceFormat,
|
||||
) -> Result<WgcNv12Pipeline, String> {
|
||||
assert!(content_width > 0, "WGC NV12 input width positive");
|
||||
assert!(content_height > 0, "WGC NV12 input height positive");
|
||||
let input_desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: content_width,
|
||||
Height: content_height,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: input_format,
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D11_USAGE_DEFAULT,
|
||||
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
|
||||
CPUAccessFlags: 0,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let mut input_texture = None;
|
||||
unsafe {
|
||||
ctx.device
|
||||
.CreateTexture2D(&input_desc, None, Some(&mut input_texture))
|
||||
}
|
||||
.map_err(|e| format!("CreateTexture2D WGC NV12 input: {e}"))?;
|
||||
let input_texture =
|
||||
input_texture.ok_or_else(|| "CreateTexture2D WGC NV12 input returned null".to_string())?;
|
||||
let input_resource: ID3D11Resource = input_texture
|
||||
.cast()
|
||||
.map_err(|e| format!("ID3D11Resource WGC NV12 input cast: {e}"))?;
|
||||
let converter = Nv12GpuConverter::new(
|
||||
&ctx.device,
|
||||
&ctx.context,
|
||||
&input_texture,
|
||||
content_width,
|
||||
content_height,
|
||||
out_w,
|
||||
out_h,
|
||||
source_format,
|
||||
)
|
||||
.ok_or_else(|| "WGC NV12 converter unavailable".to_string())?;
|
||||
Ok(WgcNv12Pipeline {
|
||||
input_resource,
|
||||
converter,
|
||||
})
|
||||
}
|
||||
|
||||
fn poll_and_emit_frame(
|
||||
inner: &Arc<CaptureInner>,
|
||||
context: &ID3D11DeviceContext,
|
||||
state: &mut WgcState,
|
||||
capture_id: Option<&str>,
|
||||
capture_start: std::time::Instant,
|
||||
frames_dropped_coalesced: &mut u64,
|
||||
) -> WgcFrameResult {
|
||||
let mut newest: Option<Direct3D11CaptureFrame> = None;
|
||||
let mut drained: u32 = 0;
|
||||
while drained < WGC_FRAME_DRAIN_LIMIT {
|
||||
let Ok(frame) = state.frame_pool.TryGetNextFrame() else {
|
||||
break;
|
||||
};
|
||||
if let Some(previous) = newest.replace(frame) {
|
||||
*frames_dropped_coalesced += 1;
|
||||
let _ = previous.Close();
|
||||
}
|
||||
drained += 1;
|
||||
}
|
||||
assert!(drained <= WGC_FRAME_DRAIN_LIMIT, "frame drain bounded");
|
||||
let Some(frame) = newest else {
|
||||
return WgcFrameResult::NoFrame;
|
||||
};
|
||||
let result = emit_wgc_frame(inner, context, state, capture_id, &frame, capture_start);
|
||||
let _ = frame.Close();
|
||||
result
|
||||
}
|
||||
|
||||
fn wgc_frame_source_resource(frame: &Direct3D11CaptureFrame) -> Result<ID3D11Resource, String> {
|
||||
let surface = frame
|
||||
.Surface()
|
||||
.map_err(|e| format!("Direct3D11CaptureFrame.Surface: {e}"))?;
|
||||
let access: IDirect3DDxgiInterfaceAccess = surface
|
||||
.cast()
|
||||
.map_err(|e| format!("IDirect3DDxgiInterfaceAccess cast: {e}"))?;
|
||||
let source_texture: ID3D11Texture2D =
|
||||
unsafe { access.GetInterface() }.map_err(|e| format!("WGC surface GetInterface: {e}"))?;
|
||||
source_texture
|
||||
.cast()
|
||||
.map_err(|e| format!("ID3D11Resource WGC surface cast: {e}"))
|
||||
}
|
||||
|
||||
fn emit_wgc_frame(
|
||||
inner: &Arc<CaptureInner>,
|
||||
context: &ID3D11DeviceContext,
|
||||
state: &mut WgcState,
|
||||
capture_id: Option<&str>,
|
||||
frame: &Direct3D11CaptureFrame,
|
||||
capture_start: std::time::Instant,
|
||||
) -> WgcFrameResult {
|
||||
let content = match frame.ContentSize() {
|
||||
Ok(size) => size,
|
||||
Err(e) => {
|
||||
return WgcFrameResult::Error(format!("Direct3D11CaptureFrame.ContentSize: {e}"));
|
||||
}
|
||||
};
|
||||
let content_width = content.Width.max(1) as u32;
|
||||
let content_height = content.Height.max(1) as u32;
|
||||
if content_width != state.content_width || content_height != state.content_height {
|
||||
return WgcFrameResult::Resized {
|
||||
width: content_width,
|
||||
height: content_height,
|
||||
};
|
||||
}
|
||||
let Some(frame_sink) = resolve_frame_sink(inner, capture_id) else {
|
||||
note_media_frame_without_sink(
|
||||
inner,
|
||||
"WGC frame dropped because no native frame sink is registered",
|
||||
);
|
||||
return WgcFrameResult::Ok;
|
||||
};
|
||||
let source_resource = match wgc_frame_source_resource(frame) {
|
||||
Ok(resource) => resource,
|
||||
Err(e) => return WgcFrameResult::Error(e),
|
||||
};
|
||||
let timestamp_us = capture_timestamp_us(capture_start);
|
||||
let Some(output_pipeline) = state.output_pipeline.as_mut() else {
|
||||
return WgcFrameResult::Error("WGC shared texture output unavailable".into());
|
||||
};
|
||||
match output_pipeline {
|
||||
WgcOutputPipeline::Bgra(shared_output) => emit_wgc_bgra_frame(
|
||||
inner,
|
||||
context,
|
||||
&frame_sink,
|
||||
shared_output,
|
||||
&source_resource,
|
||||
content_width,
|
||||
content_height,
|
||||
timestamp_us,
|
||||
),
|
||||
WgcOutputPipeline::Nv12(pipeline) => emit_wgc_nv12_frame(
|
||||
inner,
|
||||
context,
|
||||
&frame_sink,
|
||||
pipeline,
|
||||
&source_resource,
|
||||
content_width,
|
||||
content_height,
|
||||
timestamp_us,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn emit_wgc_bgra_frame(
|
||||
inner: &Arc<CaptureInner>,
|
||||
context: &ID3D11DeviceContext,
|
||||
frame_sink: &crate::FrameSinkRef,
|
||||
shared_output: &mut SharedTextureOutput,
|
||||
source_resource: &ID3D11Resource,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
timestamp_us: i64,
|
||||
) -> WgcFrameResult {
|
||||
if shared_output.width != content_width || shared_output.height != content_height {
|
||||
return WgcFrameResult::Error(
|
||||
"WGC BGRA shared output size does not match content size".into(),
|
||||
);
|
||||
}
|
||||
let slot_index = shared_output.next_slot_index();
|
||||
let slot = &shared_output.slots[slot_index];
|
||||
let output_resource: ID3D11Resource = match slot.texture.cast() {
|
||||
Ok(resource) => resource,
|
||||
Err(e) => return WgcFrameResult::Error(format!("ID3D11Resource shared output cast: {e}")),
|
||||
};
|
||||
let src_box = D3D11_BOX {
|
||||
left: 0,
|
||||
top: 0,
|
||||
front: 0,
|
||||
right: content_width,
|
||||
bottom: content_height,
|
||||
back: 1,
|
||||
};
|
||||
unsafe {
|
||||
context.CopySubresourceRegion(
|
||||
&output_resource,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
source_resource,
|
||||
0,
|
||||
Some(&src_box),
|
||||
);
|
||||
context.Flush();
|
||||
}
|
||||
let _ = emit_shared_texture_frame(
|
||||
inner,
|
||||
frame_sink,
|
||||
slot.handle,
|
||||
shared_output.width,
|
||||
shared_output.height,
|
||||
shared_output.dxgi_format,
|
||||
timestamp_us,
|
||||
);
|
||||
WgcFrameResult::Ok
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn emit_wgc_nv12_frame(
|
||||
inner: &Arc<CaptureInner>,
|
||||
context: &ID3D11DeviceContext,
|
||||
frame_sink: &crate::FrameSinkRef,
|
||||
pipeline: &mut WgcNv12Pipeline,
|
||||
source_resource: &ID3D11Resource,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
timestamp_us: i64,
|
||||
) -> WgcFrameResult {
|
||||
let src_box = D3D11_BOX {
|
||||
left: 0,
|
||||
top: 0,
|
||||
front: 0,
|
||||
right: content_width,
|
||||
bottom: content_height,
|
||||
back: 1,
|
||||
};
|
||||
unsafe {
|
||||
context.CopySubresourceRegion(
|
||||
&pipeline.input_resource,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
source_resource,
|
||||
0,
|
||||
Some(&src_box),
|
||||
);
|
||||
}
|
||||
let frame = match pipeline.converter.convert_shared_texture() {
|
||||
Ok(frame) => frame,
|
||||
Err(error) => return WgcFrameResult::Error(error),
|
||||
};
|
||||
let _ = emit_shared_texture_frame(
|
||||
inner,
|
||||
frame_sink,
|
||||
frame.handle,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.dxgi_format,
|
||||
timestamp_us,
|
||||
);
|
||||
WgcFrameResult::Ok
|
||||
}
|
||||
Reference in New Issue
Block a user