mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
366 lines
13 KiB
Rust
366 lines
13 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use windows::Win32::Graphics::Direct3D11::{
|
|
D3D11_BIND_RENDER_TARGET, D3D11_RESOURCE_MISC_SHARED, D3D11_TEXTURE2D_DESC,
|
|
D3D11_USAGE_DEFAULT, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, D3D11_VIDEO_PROCESSOR_CONTENT_DESC,
|
|
D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0,
|
|
D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0,
|
|
D3D11_VIDEO_PROCESSOR_STREAM, D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
|
D3D11_VPIV_DIMENSION_TEXTURE2D, D3D11_VPOV_DIMENSION_TEXTURE2D, ID3D11Device,
|
|
ID3D11DeviceContext, ID3D11Texture2D, ID3D11VideoContext, ID3D11VideoContext1,
|
|
ID3D11VideoDevice, ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator,
|
|
ID3D11VideoProcessorInputView, ID3D11VideoProcessorOutputView,
|
|
};
|
|
use windows::Win32::Graphics::Dxgi::Common::{
|
|
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709, DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709,
|
|
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, DXGI_COLOR_SPACE_TYPE,
|
|
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, DXGI_FORMAT_NV12, DXGI_RATIONAL, DXGI_SAMPLE_DESC,
|
|
};
|
|
use windows::Win32::Graphics::Dxgi::IDXGIResource;
|
|
use windows::core::Interface;
|
|
|
|
use crate::hdr;
|
|
|
|
fn vlog(msg: &str) {
|
|
if crate::game_capture_abi::env_flag_enabled(crate::game_capture_abi::ENV_VERBOSE) {
|
|
use std::io::Write;
|
|
let _ = writeln!(std::io::stderr(), "[fluxer-nv12] {msg}");
|
|
}
|
|
}
|
|
|
|
pub const NV12_OUTPUT_SLOT_COUNT: usize = 3;
|
|
|
|
struct Nv12OutputSlot {
|
|
_texture: ID3D11Texture2D,
|
|
view: ID3D11VideoProcessorOutputView,
|
|
handle: u64,
|
|
}
|
|
|
|
pub struct Nv12GpuConverter {
|
|
_video_device: ID3D11VideoDevice,
|
|
video_context: ID3D11VideoContext,
|
|
processor: ID3D11VideoProcessor,
|
|
_enumerator: ID3D11VideoProcessorEnumerator,
|
|
input_view: ID3D11VideoProcessorInputView,
|
|
output_slots: [Nv12OutputSlot; NV12_OUTPUT_SLOT_COUNT],
|
|
slot_cursor: usize,
|
|
context: ID3D11DeviceContext,
|
|
out_width: u32,
|
|
out_height: u32,
|
|
}
|
|
|
|
pub struct Nv12SharedTextureFrame {
|
|
pub handle: u64,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub dxgi_format: u32,
|
|
}
|
|
|
|
impl Nv12GpuConverter {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new(
|
|
device: &ID3D11Device,
|
|
context: &ID3D11DeviceContext,
|
|
input: &ID3D11Texture2D,
|
|
in_width: u32,
|
|
in_height: u32,
|
|
out_width: u32,
|
|
out_height: u32,
|
|
source_format: hdr::SourceFormat,
|
|
) -> Option<Self> {
|
|
let out_width = (out_width & !1).max(2);
|
|
let out_height = (out_height & !1).max(2);
|
|
let video_device = device
|
|
.cast::<ID3D11VideoDevice>()
|
|
.inspect_err(|e| vlog(&format!("cast ID3D11VideoDevice failed: {e:?}")))
|
|
.ok()?;
|
|
let video_context = context
|
|
.cast::<ID3D11VideoContext>()
|
|
.inspect_err(|e| vlog(&format!("cast ID3D11VideoContext failed: {e:?}")))
|
|
.ok()?;
|
|
|
|
let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
|
|
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
|
InputFrameRate: DXGI_RATIONAL {
|
|
Numerator: 60,
|
|
Denominator: 1,
|
|
},
|
|
InputWidth: in_width,
|
|
InputHeight: in_height,
|
|
OutputFrameRate: DXGI_RATIONAL {
|
|
Numerator: 60,
|
|
Denominator: 1,
|
|
},
|
|
OutputWidth: out_width,
|
|
OutputHeight: out_height,
|
|
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
|
};
|
|
let enumerator = unsafe { video_device.CreateVideoProcessorEnumerator(&content_desc) }
|
|
.inspect_err(|e| vlog(&format!("CreateVideoProcessorEnumerator: {e:?}")))
|
|
.ok()?;
|
|
let processor = unsafe { video_device.CreateVideoProcessor(&enumerator, 0) }
|
|
.inspect_err(|e| vlog(&format!("CreateVideoProcessor: {e:?}")))
|
|
.ok()?;
|
|
|
|
if let Ok(vctx1) = video_context.cast::<ID3D11VideoContext1>() {
|
|
let input_cs = input_colour_space(source_format);
|
|
unsafe {
|
|
vctx1.VideoProcessorSetStreamColorSpace1(&processor, 0, input_cs);
|
|
vctx1.VideoProcessorSetOutputColorSpace1(
|
|
&processor,
|
|
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709,
|
|
);
|
|
}
|
|
vlog(&format!(
|
|
"video processor colour space set: input={} -> output=YCbCr studio Rec.709",
|
|
input_cs.0
|
|
));
|
|
} else {
|
|
vlog("ID3D11VideoContext1 unavailable; using default SDR Rec.709 colour space");
|
|
}
|
|
|
|
let output_desc = D3D11_TEXTURE2D_DESC {
|
|
Width: out_width,
|
|
Height: out_height,
|
|
MipLevels: 1,
|
|
ArraySize: 1,
|
|
Format: DXGI_FORMAT_NV12,
|
|
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.0 as u32,
|
|
};
|
|
let mut output_slots = Vec::with_capacity(NV12_OUTPUT_SLOT_COUNT);
|
|
for _ in 0..NV12_OUTPUT_SLOT_COUNT {
|
|
output_slots.push(create_output_slot(
|
|
device,
|
|
&video_device,
|
|
&enumerator,
|
|
&output_desc,
|
|
)?);
|
|
}
|
|
assert_eq!(
|
|
output_slots.len(),
|
|
NV12_OUTPUT_SLOT_COUNT,
|
|
"all NV12 output slots created"
|
|
);
|
|
let Ok(output_slots) = <[Nv12OutputSlot; NV12_OUTPUT_SLOT_COUNT]>::try_from(output_slots)
|
|
else {
|
|
vlog("NV12 output slot count mismatch");
|
|
return None;
|
|
};
|
|
|
|
let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
|
|
FourCC: 0,
|
|
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
|
|
Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
|
|
Texture2D: windows::Win32::Graphics::Direct3D11::D3D11_TEX2D_VPIV {
|
|
MipSlice: 0,
|
|
ArraySlice: 0,
|
|
},
|
|
},
|
|
};
|
|
let mut input_view = None;
|
|
unsafe {
|
|
video_device.CreateVideoProcessorInputView(
|
|
input,
|
|
&enumerator,
|
|
&input_view_desc,
|
|
Some(&mut input_view),
|
|
)
|
|
}
|
|
.inspect_err(|e| vlog(&format!("CreateVideoProcessorInputView: {e:?}")))
|
|
.ok()?;
|
|
let input_view = input_view?;
|
|
vlog(&format!(
|
|
"NV12 converter built OK ({in_width}x{in_height} -> {out_width}x{out_height})"
|
|
));
|
|
|
|
Some(Self {
|
|
_video_device: video_device,
|
|
video_context,
|
|
processor,
|
|
_enumerator: enumerator,
|
|
input_view,
|
|
output_slots,
|
|
slot_cursor: 0,
|
|
context: context.clone(),
|
|
out_width,
|
|
out_height,
|
|
})
|
|
}
|
|
|
|
pub fn dxgi_format(&self) -> u32 {
|
|
DXGI_FORMAT_NV12.0 as u32
|
|
}
|
|
|
|
pub fn convert_shared_texture(&mut self) -> Result<Nv12SharedTextureFrame, String> {
|
|
assert!(
|
|
self.slot_cursor < NV12_OUTPUT_SLOT_COUNT,
|
|
"slot cursor in range"
|
|
);
|
|
assert!(self.out_width >= 2, "output width at least 2");
|
|
let slot_index = self.slot_cursor;
|
|
self.slot_cursor = (slot_index + 1) % NV12_OUTPUT_SLOT_COUNT;
|
|
self.run_video_processor(slot_index)?;
|
|
unsafe {
|
|
self.context.Flush();
|
|
}
|
|
Ok(Nv12SharedTextureFrame {
|
|
handle: self.output_slots[slot_index].handle,
|
|
width: self.out_width,
|
|
height: self.out_height,
|
|
dxgi_format: self.dxgi_format(),
|
|
})
|
|
}
|
|
|
|
fn run_video_processor(&self, slot_index: usize) -> Result<(), String> {
|
|
assert!(slot_index < NV12_OUTPUT_SLOT_COUNT, "slot index in range");
|
|
let mut stream = D3D11_VIDEO_PROCESSOR_STREAM {
|
|
Enable: windows::core::BOOL(1),
|
|
OutputIndex: 0,
|
|
InputFrameOrField: 0,
|
|
PastFrames: 0,
|
|
FutureFrames: 0,
|
|
ppPastSurfaces: std::ptr::null_mut(),
|
|
pInputSurface: std::mem::ManuallyDrop::new(Some(self.input_view.clone())),
|
|
ppFutureSurfaces: std::ptr::null_mut(),
|
|
ppPastSurfacesRight: std::ptr::null_mut(),
|
|
pInputSurfaceRight: std::mem::ManuallyDrop::new(None),
|
|
ppFutureSurfacesRight: std::ptr::null_mut(),
|
|
};
|
|
let blt = unsafe {
|
|
self.video_context.VideoProcessorBlt(
|
|
&self.processor,
|
|
&self.output_slots[slot_index].view,
|
|
0,
|
|
std::slice::from_ref(&stream),
|
|
)
|
|
};
|
|
unsafe {
|
|
std::mem::ManuallyDrop::drop(&mut stream.pInputSurface);
|
|
}
|
|
blt.inspect_err(|e| vlog(&format!("VideoProcessorBlt RGB->NV12: {e:?}")))
|
|
.map_err(|e| format!("VideoProcessorBlt RGB->NV12: {e}"))
|
|
}
|
|
}
|
|
|
|
fn create_output_slot(
|
|
device: &ID3D11Device,
|
|
video_device: &ID3D11VideoDevice,
|
|
enumerator: &ID3D11VideoProcessorEnumerator,
|
|
output_desc: &D3D11_TEXTURE2D_DESC,
|
|
) -> Option<Nv12OutputSlot> {
|
|
assert!(output_desc.Width >= 2, "output width at least 2");
|
|
assert!(output_desc.Height >= 2, "output height at least 2");
|
|
let mut output_texture = None;
|
|
unsafe { device.CreateTexture2D(output_desc, None, Some(&mut output_texture)) }
|
|
.inspect_err(|e| vlog(&format!("CreateTexture2D NV12 output: {e:?}")))
|
|
.ok()?;
|
|
let output_texture = output_texture?;
|
|
let resource: IDXGIResource = output_texture
|
|
.cast()
|
|
.inspect_err(|e| {
|
|
vlog(&format!(
|
|
"QueryInterface IDXGIResource for NV12 output: {e:?}"
|
|
))
|
|
})
|
|
.ok()?;
|
|
let shared_handle = unsafe { resource.GetSharedHandle() }
|
|
.inspect_err(|e| vlog(&format!("GetSharedHandle NV12 output: {e:?}")))
|
|
.ok()?;
|
|
if shared_handle.is_invalid() {
|
|
vlog("GetSharedHandle NV12 output returned an invalid handle");
|
|
return None;
|
|
}
|
|
let shared_handle = shared_handle.0 as usize as u64;
|
|
|
|
let output_view_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
|
|
ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
|
|
Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
|
|
Texture2D: windows::Win32::Graphics::Direct3D11::D3D11_TEX2D_VPOV { MipSlice: 0 },
|
|
},
|
|
};
|
|
let mut output_view = None;
|
|
unsafe {
|
|
video_device.CreateVideoProcessorOutputView(
|
|
&output_texture,
|
|
enumerator,
|
|
&output_view_desc,
|
|
Some(&mut output_view),
|
|
)
|
|
}
|
|
.inspect_err(|e| vlog(&format!("CreateVideoProcessorOutputView: {e:?}")))
|
|
.ok()?;
|
|
let output_view = output_view?;
|
|
|
|
Some(Nv12OutputSlot {
|
|
_texture: output_texture,
|
|
view: output_view,
|
|
handle: shared_handle,
|
|
})
|
|
}
|
|
|
|
fn input_colour_space(source_format: hdr::SourceFormat) -> DXGI_COLOR_SPACE_TYPE {
|
|
match source_format {
|
|
hdr::SourceFormat::R10G10B10A2 { hdr: true } => DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020,
|
|
hdr::SourceFormat::Rgba16Float { hdr: true } => DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709,
|
|
_ => DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709,
|
|
}
|
|
}
|
|
|
|
unsafe impl Send for Nv12GpuConverter {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn cs_value(source_format: hdr::SourceFormat) -> i32 {
|
|
input_colour_space(source_format).0
|
|
}
|
|
|
|
#[test]
|
|
fn eight_bit_sources_use_sdr_rec709_colour_space() {
|
|
assert_eq!(
|
|
cs_value(hdr::SourceFormat::Bgra8),
|
|
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
|
|
);
|
|
assert_eq!(
|
|
cs_value(hdr::SourceFormat::Rgba8),
|
|
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unflagged_high_precision_sources_stay_sdr_rec709() {
|
|
assert_eq!(
|
|
cs_value(hdr::SourceFormat::R10G10B10A2 { hdr: false }),
|
|
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
|
|
);
|
|
assert_eq!(
|
|
cs_value(hdr::SourceFormat::Rgba16Float { hdr: false }),
|
|
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ten_bit_hdr_uses_pq_rec2020_input_space() {
|
|
assert_eq!(
|
|
cs_value(hdr::SourceFormat::R10G10B10A2 { hdr: true }),
|
|
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fp16_hdr_uses_linear_extended_rec709_input_space() {
|
|
assert_eq!(
|
|
cs_value(hdr::SourceFormat::Rgba16Float { hdr: true }),
|
|
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709.0
|
|
);
|
|
}
|
|
}
|