EvanZhu0721 / 公开仓库
SBMS
SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.
文件
../ bininputuiconfig.rs81.2 KBcontrol.rs4.1 KBcontroller.rs43.6 KBdiagnostics.rs24.2 KBdisplay.rs29.8 KBgeometry.rs21.4 KBgpu_renderer.rs37.4 KBinput.rs79.7 KBlaunch_broker.rs10.3 KBlib.rs350 Blimits.rs714 Bmain.rs24.4 KBmapping.rs46.5 KBnetwork.rs9.2 KBrenderer.rs20.6 KBsession_gate.rs14.9 KBsunshine.rs12.2 KBui.rs68.5 KBvirtual_display.rs7.2 KBwin32_flyout.rs3.4 KBwin32_tray.rs22.3 KBwindow_migration.rs43.8 KBsrc/gpu_renderer.rs37.4 KB
1
//! GPU-only virtual-display renderer.2
//!3
//! The hot path stays on the render adapter:4
//! `Desktop Duplication -> D3D11 texture -> pixel shader -> flip-model swap chain`.5
//! In particular, this module never creates a staging texture, maps a GPU6
//! resource into CPU memory, or sends pixels through GDI.7
8
use std::fmt;9
use std::mem::size_of;10
use std::slice;11
use std::sync::atomic::{AtomicBool, Ordering};12
use std::time::{Duration, Instant};13
14
use crate::diagnostics::{self, Level};15
use windows::Win32::Foundation::{HMODULE, HWND, RECT};16
use windows::Win32::Graphics::Direct3D::Fxc::D3DCompile;17
use windows::Win32::Graphics::Direct3D::{18
D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1,19
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST, ID3DBlob, ID3DInclude,20
};21
use windows::Win32::Graphics::Direct3D11::{22
D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_BUFFER_DESC,23
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_FILTER_MIN_MAG_MIP_LINEAR, D3D11_SAMPLER_DESC,24
D3D11_SDK_VERSION, D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,25
D3D11_VIEWPORT, D3D11CreateDevice, ID3D11Buffer, ID3D11Device, ID3D11DeviceContext,26
ID3D11PixelShader, ID3D11RenderTargetView, ID3D11SamplerState, ID3D11ShaderResourceView,27
ID3D11Texture2D, ID3D11VertexShader,28
};29
use windows::Win32::Graphics::Dxgi::Common::{30
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC,31
};32
use windows::Win32::Graphics::Dxgi::{33
CreateDXGIFactory1, DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_DEVICE_REMOVED, DXGI_ERROR_DEVICE_RESET,34
DXGI_ERROR_NOT_FOUND, DXGI_ERROR_WAIT_TIMEOUT, DXGI_MWA_NO_ALT_ENTER, DXGI_OUTDUPL_FRAME_INFO,35
DXGI_OUTDUPL_MOVE_RECT, DXGI_PRESENT, DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1,36
DXGI_SWAP_EFFECT_FLIP_DISCARD, DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIAdapter1, IDXGIFactory1,37
IDXGIFactory2, IDXGIOutput, IDXGIOutput1, IDXGIOutputDuplication, IDXGIResource,38
IDXGISwapChain1,39
};40
use windows::core::{Error as WindowsError, Interface, PCSTR};41
42
const FRAME_WAIT_TIMEOUT_MS: u32 = 50;43
const COPY_MODE_ENV: &str = "SBMS_RENDER_COPY_MODE";44
const MAX_METADATA_BYTES: u32 = 1024 * 1024;45
const MAX_COPY_REGIONS: usize = 128;46
const MAX_PARTIAL_COPY_PERCENT: u64 = 50;47
const COPY_STATS_INTERVAL: Duration = Duration::from_secs(10);48
49
const SHADER: &str = r#"50
Texture2D source_texture : register(t0);51
SamplerState source_sampler : register(s0);52
53
cbuffer ScaleParameters : register(b0) {54
float2 source_size;55
float2 target_size;56
};57
58
struct VertexOutput {59
float4 position : SV_POSITION;60
float2 uv : TEXCOORD0;61
};62
63
VertexOutput vertex_main(uint id : SV_VertexID) {64
float2 positions[3] = {65
float2(-1.0, -1.0),66
float2(-1.0, 3.0),67
float2( 3.0, -1.0)68
};69
VertexOutput output;70
output.position = float4(positions[id], 0.0, 1.0);71
output.uv = float2(72
(positions[id].x + 1.0) * 0.5,73
(1.0 - positions[id].y) * 0.5);74
return output;75
}76
77
void area_axis(78
float first,79
float last,80
float source_extent,81
out float2 position,82
out float2 weight) {83
float base = floor(first);84
float3 coverage = float3(85
max(0.0, min(last, base + 1.0) - max(first, base)),86
max(0.0, min(last, base + 2.0) - max(first, base + 1.0)),87
max(0.0, min(last, base + 3.0) - max(first, base + 2.0)));88
coverage /= last - first;89
90
// Two adjacent weighted texels are exactly representable by one bilinear91
// lookup. The third texel becomes the second lookup for this axis.92
weight = float2(coverage.x + coverage.y, coverage.z);93
float blend = weight.x > 0.0 ? coverage.y / weight.x : 0.0;94
position = float2(base + 0.5 + blend, base + 2.5) / source_extent;95
}96
97
float4 exact_area(float2 target_pixel) {98
float2 scale = source_size / target_size;99
float2 first = target_pixel * scale;100
float2 last = (target_pixel + 1.0) * scale;101
float2 position_x;102
float2 position_y;103
float2 weight_x;104
float2 weight_y;105
area_axis(first.x, last.x, source_size.x, position_x, weight_x);106
area_axis(first.y, last.y, source_size.y, position_y, weight_y);107
108
return109
source_texture.SampleLevel(source_sampler, float2(position_x.x, position_y.x), 0.0)110
* weight_x.x * weight_y.x +111
source_texture.SampleLevel(source_sampler, float2(position_x.y, position_y.x), 0.0)112
* weight_x.y * weight_y.x +113
source_texture.SampleLevel(source_sampler, float2(position_x.x, position_y.y), 0.0)114
* weight_x.x * weight_y.y +115
source_texture.SampleLevel(source_sampler, float2(position_x.y, position_y.y), 0.0)116
* weight_x.y * weight_y.y;117
}118
119
float3 suppress_subpixel_fringe(float2 target_pixel, float3 area_color) {120
float2 center_uv = (target_pixel + 0.5) / target_size;121
float2 half_source_pixel = float2(0.5 / source_size.x, 0.0);122
float3 left = source_texture.SampleLevel(123
source_sampler, center_uv - half_source_pixel, 0.0).rgb;124
float3 right = source_texture.SampleLevel(125
source_sampler, center_uv + half_source_pixel, 0.0).rgb;126
float3 chroma_low_pass = (left + 2.0 * area_color + right) * 0.25;127
128
// ClearType encodes coverage in RGB subpixels. After a non-integer resize129
// that high-frequency chroma is no longer aligned with the physical panel.130
// Preserve the area-filtered luminance and only borrow low-passed chroma131
// where the wider neighborhood is close to neutral. Solid UI colors keep132
// their original chroma.133
const float3 luma_weights = float3(0.2126, 0.7152, 0.0722);134
float area_luma = dot(area_color, luma_weights);135
float low_pass_luma = dot(chroma_low_pass, luma_weights);136
float3 area_chroma = area_color - area_luma;137
float3 low_pass_chroma = chroma_low_pass - low_pass_luma;138
float neighborhood_saturation =139
max(chroma_low_pass.r, max(chroma_low_pass.g, chroma_low_pass.b)) -140
min(chroma_low_pass.r, min(chroma_low_pass.g, chroma_low_pass.b));141
float chroma_delta = length(area_chroma - low_pass_chroma);142
float neutral_gate = 1.0 - smoothstep(0.04, 0.18, neighborhood_saturation);143
float artifact_gate = saturate(chroma_delta * 8.0) * neutral_gate * 0.85;144
float3 filtered_chroma = lerp(area_chroma, low_pass_chroma, artifact_gate);145
return saturate(area_luma + filtered_chroma);146
}147
148
float4 pixel_main(VertexOutput input) : SV_TARGET {149
float2 scale = source_size / target_size;150
bool area_supported =151
scale.x > 1.0001 && scale.x <= 2.0 &&152
scale.y > 1.0001 && scale.y <= 2.0;153
if (area_supported) {154
float2 target_pixel = floor(input.position.xy);155
float4 area = exact_area(target_pixel);156
area.rgb = suppress_subpixel_fringe(target_pixel, area.rgb);157
return area;158
}159
return source_texture.Sample(source_sampler, input.uv);160
}161
"#;162
163
/// Everything the GPU loop needs from the existing window-owning renderer.164
#[derive(Clone, Copy, Debug)]165
pub struct GpuRendererConfig {166
pub target_window: HWND,167
pub target_width: u32,168
pub target_height: u32,169
pub source_rect: RECT,170
}171
172
#[derive(Debug)]173
pub enum GpuRendererError {174
/// Desktop Duplication must be rebuilt after a topology or mode change.175
AccessLost,176
/// The render adapter reset or was removed.177
DeviceLost,178
Failure(String),179
}180
181
impl fmt::Display for GpuRendererError {182
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {183
match self {184
Self::AccessLost => formatter.write_str("desktop duplication access was lost"),185
Self::DeviceLost => formatter.write_str("D3D11 render device was lost"),186
Self::Failure(message) => formatter.write_str(message),187
}188
}189
}190
191
impl std::error::Error for GpuRendererError {}192
193
/// Runs until `stop` is set or DXGI reports a topology/device change.194
///195
/// `on_present` is called only after a successful GPU present. It lets the196
/// existing renderer keep its first-frame and FPS reporting without exposing197
/// D3D objects outside this module.198
pub fn run_gpu_renderer(199
config: GpuRendererConfig,200
stop: &AtomicBool,201
mut on_present: impl FnMut() -> Result<(), String>,202
) -> Result<(), GpuRendererError> {203
validate_config(config)?;204
let copy_mode = CopyMode::from_environment();205
let copy_stats_enabled = CopyMode::diagnostics_requested();206
let (adapter, output) = find_source_output(config.source_rect)?;207
let (device, context) = create_device(&adapter)?;208
let duplication = unsafe { output.DuplicateOutput(&device) }.map_err(classify_windows_error)?;209
let pipeline = Pipeline::new(config, &adapter, device, context)?;210
pipeline.run(211
duplication,212
stop,213
copy_mode,214
copy_stats_enabled,215
&mut on_present,216
)217
}218
219
#[derive(Clone, Copy, Debug, Eq, PartialEq)]220
enum CopyMode {221
Full,222
Dirty,223
}224
225
impl CopyMode {226
fn from_environment() -> Self {227
let value = std::env::var_os(COPY_MODE_ENV);228
match Self::parse(value.as_deref()) {229
Ok(Self::Dirty) => {230
if value.is_some() {231
diagnostics::log(232
Level::Info,233
"renderer",234
"copy-mode",235
None,236
"dirty source copy diagnostics enabled",237
);238
}239
Self::Dirty240
}241
Ok(Self::Full) => Self::Full,242
Err(value) => {243
diagnostics::log(244
Level::Warn,245
"renderer",246
"copy-mode",247
None,248
format!("invalid {COPY_MODE_ENV}={value}; using full source copy"),249
);250
Self::Full251
}252
}253
}254
255
fn parse(value: Option<&std::ffi::OsStr>) -> Result<Self, String> {256
let Some(value) = value else {257
return Ok(Self::Dirty);258
};259
let Some(value) = value.to_str() else {260
return Err(format!("{value:?}"));261
};262
match value.trim().to_ascii_lowercase().as_str() {263
"" | "full" => Ok(Self::Full),264
"dirty" => Ok(Self::Dirty),265
_ => Err(format!("{value:?}")),266
}267
}268
269
fn diagnostics_requested() -> bool {270
let value = std::env::var_os(COPY_MODE_ENV);271
value.is_some() && Self::parse(value.as_deref()) == Ok(Self::Dirty)272
}273
}274
275
fn validate_config(config: GpuRendererConfig) -> Result<(), GpuRendererError> {276
if config.target_window.0.is_null() {277
return Err(GpuRendererError::Failure(278
"GPU renderer target HWND is null".into(),279
));280
}281
if config.target_width == 0 || config.target_height == 0 {282
return Err(GpuRendererError::Failure(283
"GPU renderer target dimensions must be non-zero".into(),284
));285
}286
if config.source_rect.right <= config.source_rect.left287
|| config.source_rect.bottom <= config.source_rect.top288
{289
return Err(GpuRendererError::Failure(290
"GPU renderer source rectangle is empty".into(),291
));292
}293
Ok(())294
}295
296
fn find_source_output(297
source_rect: RECT,298
) -> Result<(IDXGIAdapter1, IDXGIOutput1), GpuRendererError> {299
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.map_err(classify_windows_error)?;300
let mut adapter_index = 0;301
loop {302
let adapter = match unsafe { factory.EnumAdapters1(adapter_index) } {303
Ok(adapter) => adapter,304
Err(error) if error.code() == DXGI_ERROR_NOT_FOUND => break,305
Err(error) => return Err(classify_windows_error(error)),306
};307
let mut output_index = 0;308
loop {309
let output = match unsafe { adapter.EnumOutputs(output_index) } {310
Ok(output) => output,311
Err(error) if error.code() == DXGI_ERROR_NOT_FOUND => break,312
Err(error) => return Err(classify_windows_error(error)),313
};314
let description = unsafe { output.GetDesc() }.map_err(classify_windows_error)?;315
if same_rect(description.DesktopCoordinates, source_rect) {316
return output317
.cast::<IDXGIOutput1>()318
.map(|output| (adapter, output))319
.map_err(classify_windows_error);320
}321
output_index += 1;322
}323
adapter_index += 1;324
}325
Err(GpuRendererError::Failure(format!(326
"no DXGI output matches source rectangle ({}, {})-({}, {})",327
source_rect.left, source_rect.top, source_rect.right, source_rect.bottom328
)))329
}330
331
fn same_rect(left: RECT, right: RECT) -> bool {332
left.left == right.left333
&& left.top == right.top334
&& left.right == right.right335
&& left.bottom == right.bottom336
}337
338
fn create_device(339
adapter: &IDXGIAdapter1,340
) -> Result<(ID3D11Device, ID3D11DeviceContext), GpuRendererError> {341
let mut device = None;342
let mut context = None;343
let levels = [D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0];344
unsafe {345
D3D11CreateDevice(346
adapter,347
D3D_DRIVER_TYPE_UNKNOWN,348
HMODULE::default(),349
D3D11_CREATE_DEVICE_BGRA_SUPPORT,350
Some(&levels),351
D3D11_SDK_VERSION,352
Some(&mut device),353
None,354
Some(&mut context),355
)356
}357
.map_err(classify_windows_error)?;358
Ok((359
device.ok_or_else(|| GpuRendererError::Failure("D3D11 returned no device".into()))?,360
context.ok_or_else(|| {361
GpuRendererError::Failure("D3D11 returned no immediate context".into())362
})?,363
))364
}365
366
struct Pipeline {367
config: GpuRendererConfig,368
device: ID3D11Device,369
context: ID3D11DeviceContext,370
swap_chain: IDXGISwapChain1,371
render_target: ID3D11RenderTargetView,372
vertex_shader: ID3D11VertexShader,373
pixel_shader: ID3D11PixelShader,374
sampler: ID3D11SamplerState,375
scale_parameters: ID3D11Buffer,376
}377
378
impl Pipeline {379
fn new(380
config: GpuRendererConfig,381
adapter: &IDXGIAdapter1,382
device: ID3D11Device,383
context: ID3D11DeviceContext,384
) -> Result<Self, GpuRendererError> {385
let factory: IDXGIFactory2 =386
unsafe { adapter.GetParent() }.map_err(classify_windows_error)?;387
let swap_description = DXGI_SWAP_CHAIN_DESC1 {388
Width: config.target_width,389
Height: config.target_height,390
Format: DXGI_FORMAT_B8G8R8A8_UNORM,391
Stereo: false.into(),392
SampleDesc: DXGI_SAMPLE_DESC {393
Count: 1,394
Quality: 0,395
},396
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,397
BufferCount: 2,398
Scaling: DXGI_SCALING_STRETCH,399
SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,400
AlphaMode: DXGI_ALPHA_MODE_IGNORE,401
Flags: 0,402
};403
let swap_chain = unsafe {404
factory.CreateSwapChainForHwnd(405
&device,406
config.target_window,407
&swap_description,408
None,409
None::<&IDXGIOutput>,410
)411
}412
.map_err(classify_windows_error)?;413
unsafe { factory.MakeWindowAssociation(config.target_window, DXGI_MWA_NO_ALT_ENTER) }414
.map_err(classify_windows_error)?;415
416
let back_buffer: ID3D11Texture2D =417
unsafe { swap_chain.GetBuffer(0) }.map_err(classify_windows_error)?;418
let mut render_target = None;419
unsafe { device.CreateRenderTargetView(&back_buffer, None, Some(&mut render_target)) }420
.map_err(classify_windows_error)?;421
422
let vertex_code = compile_shader("vertex_main", "vs_5_0")?;423
let pixel_code = compile_shader("pixel_main", "ps_5_0")?;424
let mut vertex_shader = None;425
let mut pixel_shader = None;426
unsafe { device.CreateVertexShader(&vertex_code, None, Some(&mut vertex_shader)) }427
.map_err(classify_windows_error)?;428
unsafe { device.CreatePixelShader(&pixel_code, None, Some(&mut pixel_shader)) }429
.map_err(classify_windows_error)?;430
431
let sampler_description = D3D11_SAMPLER_DESC {432
Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR,433
AddressU: D3D11_TEXTURE_ADDRESS_CLAMP,434
AddressV: D3D11_TEXTURE_ADDRESS_CLAMP,435
AddressW: D3D11_TEXTURE_ADDRESS_CLAMP,436
MaxLOD: f32::MAX,437
..Default::default()438
};439
let mut sampler = None;440
unsafe { device.CreateSamplerState(&sampler_description, Some(&mut sampler)) }441
.map_err(classify_windows_error)?;442
443
let buffer_description = D3D11_BUFFER_DESC {444
ByteWidth: size_of::<ScaleParameters>() as u32,445
Usage: D3D11_USAGE_DEFAULT,446
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,447
..Default::default()448
};449
let mut scale_parameters = None;450
unsafe { device.CreateBuffer(&buffer_description, None, Some(&mut scale_parameters)) }451
.map_err(classify_windows_error)?;452
453
Ok(Self {454
config,455
device,456
context,457
swap_chain,458
render_target: render_target459
.ok_or_else(|| GpuRendererError::Failure("D3D11 returned no RTV".into()))?,460
vertex_shader: vertex_shader.ok_or_else(|| {461
GpuRendererError::Failure("D3D11 returned no vertex shader".into())462
})?,463
pixel_shader: pixel_shader.ok_or_else(|| {464
GpuRendererError::Failure("D3D11 returned no pixel shader".into())465
})?,466
sampler: sampler467
.ok_or_else(|| GpuRendererError::Failure("D3D11 returned no sampler".into()))?,468
scale_parameters: scale_parameters.ok_or_else(|| {469
GpuRendererError::Failure("D3D11 returned no constant buffer".into())470
})?,471
})472
}473
474
fn run(475
self,476
duplication: IDXGIOutputDuplication,477
stop: &AtomicBool,478
copy_mode: CopyMode,479
copy_stats_enabled: bool,480
on_present: &mut impl FnMut() -> Result<(), String>,481
) -> Result<(), GpuRendererError> {482
let mut source_texture = None;483
let mut source_view = None;484
let mut source_description = None;485
let mut copy_stats = CopyStats::new();486
let mut metadata_buffers = FrameMetadataBuffers::default();487
488
while !stop.load(Ordering::Acquire) {489
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();490
let mut desktop_resource: Option<IDXGIResource> = None;491
match unsafe {492
duplication.AcquireNextFrame(493
FRAME_WAIT_TIMEOUT_MS,494
&mut frame_info,495
&mut desktop_resource,496
)497
} {498
Ok(()) => {}499
Err(error) if error.code() == DXGI_ERROR_WAIT_TIMEOUT => continue,500
Err(error) => return Err(classify_windows_error(error)),501
}502
let frame = AcquiredFrame(&duplication);503
let desktop_resource = desktop_resource.ok_or_else(|| {504
GpuRendererError::Failure("Desktop Duplication returned no texture".into())505
})?;506
let acquired: ID3D11Texture2D =507
desktop_resource.cast().map_err(classify_windows_error)?;508
let mut description = D3D11_TEXTURE2D_DESC::default();509
unsafe { acquired.GetDesc(&mut description) };510
if description.Format != DXGI_FORMAT_B8G8R8A8_UNORM {511
return Err(GpuRendererError::Failure(format!(512
"unsupported Desktop Duplication texture format {}",513
description.Format.0514
)));515
}516
let texture_rebuilt =517
source_description != Some((description.Width, description.Height));518
if texture_rebuilt {519
let (texture, view) = self.create_source_texture(description)?;520
source_texture = Some(texture);521
source_view = Some(view);522
source_description = Some((description.Width, description.Height));523
self.update_scale_parameters(description.Width, description.Height);524
}525
let texture = source_texture.as_ref().ok_or_else(|| {526
GpuRendererError::Failure("source texture was not initialized".into())527
})?;528
self.copy_source_frame(529
&duplication,530
&frame_info,531
texture,532
&acquired,533
description.Width,534
description.Height,535
copy_mode,536
texture_rebuilt,537
&mut metadata_buffers,538
&mut copy_stats,539
);540
drop(frame);541
542
let view = source_view.as_ref().ok_or_else(|| {543
GpuRendererError::Failure("source texture view was not initialized".into())544
})?;545
self.draw(view);546
let present = unsafe { self.swap_chain.Present(1, DXGI_PRESENT(0)) };547
present.ok().map_err(|_| classify_hresult(present))?;548
on_present().map_err(GpuRendererError::Failure)?;549
unsafe {550
self.context.PSSetShaderResources(0, Some(&[None]));551
}552
copy_stats.maybe_log(553
copy_mode,554
copy_stats_enabled,555
description.Width,556
description.Height,557
);558
}559
Ok(())560
}561
562
#[allow(clippy::too_many_arguments)]563
fn copy_source_frame(564
&self,565
duplication: &IDXGIOutputDuplication,566
frame_info: &DXGI_OUTDUPL_FRAME_INFO,567
target: &ID3D11Texture2D,568
acquired: &ID3D11Texture2D,569
width: u32,570
height: u32,571
copy_mode: CopyMode,572
force_full_copy: bool,573
metadata_buffers: &mut FrameMetadataBuffers,574
stats: &mut CopyStats,575
) {576
let regions = if copy_mode == CopyMode::Dirty && !force_full_copy {577
dirty_copy_regions(duplication, frame_info, width, height, metadata_buffers)578
} else {579
None580
};581
582
let Some(regions) = regions else {583
unsafe { self.context.CopyResource(target, acquired) };584
stats.record_full(width, height);585
return;586
};587
588
for region in regions {589
let source_box = D3D11_BOX {590
left: region.left as u32,591
top: region.top as u32,592
front: 0,593
right: region.right as u32,594
bottom: region.bottom as u32,595
back: 1,596
};597
unsafe {598
self.context.CopySubresourceRegion(599
target,600
0,601
region.left as u32,602
region.top as u32,603
0,604
acquired,605
0,606
Some(&source_box),607
);608
}609
}610
stats.record_partial(regions);611
}612
613
fn create_source_texture(614
&self,615
mut description: D3D11_TEXTURE2D_DESC,616
) -> Result<(ID3D11Texture2D, ID3D11ShaderResourceView), GpuRendererError> {617
description.BindFlags = D3D11_BIND_SHADER_RESOURCE.0 as u32;618
description.CPUAccessFlags = 0;619
description.MiscFlags = 0;620
description.Usage = D3D11_USAGE_DEFAULT;621
let mut texture = None;622
unsafe {623
self.device624
.CreateTexture2D(&description, None, Some(&mut texture))625
}626
.map_err(classify_windows_error)?;627
let texture =628
texture.ok_or_else(|| GpuRendererError::Failure("D3D11 returned no texture".into()))?;629
let mut view = None;630
unsafe {631
self.device632
.CreateShaderResourceView(&texture, None, Some(&mut view))633
}634
.map_err(classify_windows_error)?;635
Ok((636
texture,637
view.ok_or_else(|| GpuRendererError::Failure("D3D11 returned no SRV".into()))?,638
))639
}640
641
fn update_scale_parameters(&self, source_width: u32, source_height: u32) {642
let parameters = ScaleParameters {643
source_size: [source_width as f32, source_height as f32],644
target_size: [645
self.config.target_width as f32,646
self.config.target_height as f32,647
],648
};649
unsafe {650
self.context.UpdateSubresource(651
&self.scale_parameters,652
0,653
None,654
(¶meters as *const ScaleParameters).cast(),655
0,656
0,657
);658
}659
}660
661
fn draw(&self, source_view: &ID3D11ShaderResourceView) {662
let viewport = D3D11_VIEWPORT {663
Width: self.config.target_width as f32,664
Height: self.config.target_height as f32,665
MinDepth: 0.0,666
MaxDepth: 1.0,667
..Default::default()668
};669
unsafe {670
self.context671
.OMSetRenderTargets(Some(&[Some(self.render_target.clone())]), None);672
self.context.RSSetViewports(Some(&[viewport]));673
self.context674
.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);675
self.context.VSSetShader(&self.vertex_shader, None);676
self.context.PSSetShader(&self.pixel_shader, None);677
self.context678
.PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));679
self.context680
.PSSetConstantBuffers(0, Some(&[Some(self.scale_parameters.clone())]));681
self.context682
.PSSetShaderResources(0, Some(&[Some(source_view.clone())]));683
self.context.Draw(3, 0);684
}685
}686
}687
688
fn dirty_copy_regions<'a>(689
duplication: &IDXGIOutputDuplication,690
frame_info: &DXGI_OUTDUPL_FRAME_INFO,691
width: u32,692
height: u32,693
buffers: &'a mut FrameMetadataBuffers,694
) -> Option<&'a [RECT]> {695
let metadata_bytes = frame_info.TotalMetadataBufferSize;696
if metadata_bytes == 0 {697
buffers.regions.clear();698
return Some(&buffers.regions);699
}700
if metadata_bytes > MAX_METADATA_BYTES {701
return None;702
}703
704
let FrameMetadataBuffers {705
moves,706
dirty,707
regions,708
} = buffers;709
let moves = read_move_rects(duplication, metadata_bytes, moves)?;710
let dirty = read_dirty_rects(duplication, metadata_bytes, dirty)?;711
if select_copy_regions(712
moves.iter().map(|movement| movement.DestinationRect),713
dirty.iter().copied(),714
width,715
height,716
regions,717
) {718
Some(regions.as_slice())719
} else {720
None721
}722
}723
724
#[derive(Default)]725
struct FrameMetadataBuffers {726
moves: Vec<DXGI_OUTDUPL_MOVE_RECT>,727
dirty: Vec<RECT>,728
regions: Vec<RECT>,729
}730
731
fn read_move_rects<'a>(732
duplication: &IDXGIOutputDuplication,733
metadata_bytes: u32,734
buffer: &'a mut Vec<DXGI_OUTDUPL_MOVE_RECT>,735
) -> Option<&'a [DXGI_OUTDUPL_MOVE_RECT]> {736
let element_bytes = size_of::<DXGI_OUTDUPL_MOVE_RECT>();737
let element_count = (metadata_bytes as usize).div_ceil(element_bytes).max(1);738
buffer.resize(element_count, DXGI_OUTDUPL_MOVE_RECT::default());739
let buffer_bytes = u32::try_from(buffer.len().checked_mul(element_bytes)?).ok()?;740
let mut required_bytes = 0;741
unsafe {742
duplication743
.GetFrameMoveRects(buffer_bytes, buffer.as_mut_ptr(), &mut required_bytes)744
.ok()?;745
}746
let required_bytes = required_bytes as usize;747
if required_bytes > buffer_bytes as usize || !required_bytes.is_multiple_of(element_bytes) {748
return None;749
}750
Some(&buffer[..required_bytes / element_bytes])751
}752
753
fn read_dirty_rects<'a>(754
duplication: &IDXGIOutputDuplication,755
metadata_bytes: u32,756
buffer: &'a mut Vec<RECT>,757
) -> Option<&'a [RECT]> {758
let element_bytes = size_of::<RECT>();759
let element_count = (metadata_bytes as usize).div_ceil(element_bytes).max(1);760
buffer.resize(element_count, RECT::default());761
let buffer_bytes = u32::try_from(buffer.len().checked_mul(element_bytes)?).ok()?;762
let mut required_bytes = 0;763
unsafe {764
duplication765
.GetFrameDirtyRects(buffer_bytes, buffer.as_mut_ptr(), &mut required_bytes)766
.ok()?;767
}768
let required_bytes = required_bytes as usize;769
if required_bytes > buffer_bytes as usize || !required_bytes.is_multiple_of(element_bytes) {770
return None;771
}772
Some(&buffer[..required_bytes / element_bytes])773
}774
775
fn select_copy_regions(776
moved: impl IntoIterator<Item = RECT>,777
dirty: impl IntoIterator<Item = RECT>,778
width: u32,779
height: u32,780
regions: &mut Vec<RECT>,781
) -> bool {782
regions.clear();783
let Some(source_pixels) = u64::from(width).checked_mul(u64::from(height)) else {784
return false;785
};786
if source_pixels == 0 {787
return false;788
}789
790
let mut copied_pixels = 0_u64;791
for region in moved.into_iter().chain(dirty) {792
if region.left < 0793
|| region.top < 0794
|| region.right <= region.left795
|| region.bottom <= region.top796
|| region.right as u32 > width797
|| region.bottom as u32 > height798
{799
return false;800
}801
if regions.contains(®ion) {802
continue;803
}804
if regions.len() == MAX_COPY_REGIONS {805
return false;806
}807
let region_pixels =808
(region.right - region.left) as u64 * (region.bottom - region.top) as u64;809
let Some(updated_pixels) = copied_pixels.checked_add(region_pixels) else {810
return false;811
};812
copied_pixels = updated_pixels;813
if copied_pixels.saturating_mul(100)814
>= source_pixels.saturating_mul(MAX_PARTIAL_COPY_PERCENT)815
{816
return false;817
}818
regions.push(region);819
}820
true821
}822
823
struct CopyStats {824
interval_started: Instant,825
frames: u64,826
full_frames: u64,827
partial_frames: u64,828
copied_regions: u64,829
copied_pixels: u64,830
}831
832
impl CopyStats {833
fn new() -> Self {834
Self {835
interval_started: Instant::now(),836
frames: 0,837
full_frames: 0,838
partial_frames: 0,839
copied_regions: 0,840
copied_pixels: 0,841
}842
}843
844
fn record_full(&mut self, width: u32, height: u32) {845
self.frames += 1;846
self.full_frames += 1;847
self.copied_pixels += u64::from(width) * u64::from(height);848
}849
850
fn record_partial(&mut self, regions: &[RECT]) {851
self.frames += 1;852
self.partial_frames += 1;853
self.copied_regions += regions.len() as u64;854
self.copied_pixels += regions855
.iter()856
.map(|region| (region.right - region.left) as u64 * (region.bottom - region.top) as u64)857
.sum::<u64>();858
}859
860
fn maybe_log(&mut self, copy_mode: CopyMode, enabled: bool, width: u32, height: u32) {861
if !enabled862
|| copy_mode != CopyMode::Dirty863
|| self.interval_started.elapsed() < COPY_STATS_INTERVAL864
{865
return;866
}867
let available_pixels = self868
.frames869
.saturating_mul(u64::from(width))870
.saturating_mul(u64::from(height));871
let copied_percent = if available_pixels == 0 {872
0.0873
} else {874
self.copied_pixels as f64 * 100.0 / available_pixels as f64875
};876
diagnostics::log(877
Level::Info,878
"renderer",879
"dirty-copy-stats",880
None,881
format!(882
"frames={} partial={} full_fallbacks={} rects={} copied={copied_percent:.2}%",883
self.frames, self.partial_frames, self.full_frames, self.copied_regions,884
),885
);886
*self = Self::new();887
}888
}889
890
struct AcquiredFrame<'a>(&'a IDXGIOutputDuplication);891
892
impl Drop for AcquiredFrame<'_> {893
fn drop(&mut self) {894
unsafe {895
let _ = self.0.ReleaseFrame();896
}897
}898
}899
900
#[repr(C)]901
struct ScaleParameters {902
source_size: [f32; 2],903
target_size: [f32; 2],904
}905
906
fn compile_shader(entry: &str, target: &str) -> Result<Vec<u8>, GpuRendererError> {907
let entry = null_terminated(entry);908
let target = null_terminated(target);909
let mut code: Option<ID3DBlob> = None;910
let mut diagnostics: Option<ID3DBlob> = None;911
let result = unsafe {912
D3DCompile(913
SHADER.as_ptr().cast(),914
SHADER.len(),915
PCSTR::null(),916
None,917
None::<&ID3DInclude>,918
PCSTR(entry.as_ptr()),919
PCSTR(target.as_ptr()),920
0,921
0,922
&mut code,923
Some(&mut diagnostics),924
)925
};926
if let Err(error) = result {927
let detail = diagnostics928
.as_ref()929
.map(blob_text)930
.filter(|message| !message.is_empty())931
.unwrap_or_else(|| error.to_string());932
return Err(GpuRendererError::Failure(format!(933
"D3D shader compilation failed: {detail}"934
)));935
}936
let code =937
code.ok_or_else(|| GpuRendererError::Failure("D3DCompile returned no bytecode".into()))?;938
let bytes =939
unsafe { slice::from_raw_parts(code.GetBufferPointer().cast(), code.GetBufferSize()) };940
Ok(bytes.to_vec())941
}942
943
fn blob_text(blob: &ID3DBlob) -> String {944
let bytes =945
unsafe { slice::from_raw_parts(blob.GetBufferPointer().cast(), blob.GetBufferSize()) };946
String::from_utf8_lossy(bytes)947
.trim_end_matches('\0')948
.trim()949
.to_owned()950
}951
952
fn null_terminated(value: &str) -> Vec<u8> {953
let mut bytes = Vec::with_capacity(value.len() + 1);954
bytes.extend_from_slice(value.as_bytes());955
bytes.push(0);956
bytes957
}958
959
fn classify_windows_error(error: WindowsError) -> GpuRendererError {960
classify_hresult(error.code())961
}962
963
fn classify_hresult(code: windows::core::HRESULT) -> GpuRendererError {964
if code == DXGI_ERROR_ACCESS_LOST {965
GpuRendererError::AccessLost966
} else if code == DXGI_ERROR_DEVICE_REMOVED || code == DXGI_ERROR_DEVICE_RESET {967
GpuRendererError::DeviceLost968
} else {969
GpuRendererError::Failure(format!("DXGI/D3D11 call failed: {code:?}"))970
}971
}972
973
#[cfg(test)]974
mod tests {975
use super::{976
CopyMode, DXGI_ERROR_ACCESS_LOST, GpuRendererError, MAX_COPY_REGIONS, ScaleParameters,977
classify_hresult, compile_shader, select_copy_regions,978
};979
use std::ffi::OsStr;980
use windows::Win32::Foundation::RECT;981
982
#[test]983
fn shaders_compile_with_the_system_d3d_compiler() {984
assert!(!compile_shader("vertex_main", "vs_5_0").unwrap().is_empty());985
assert!(!compile_shader("pixel_main", "ps_5_0").unwrap().is_empty());986
}987
988
#[test]989
fn dxgi_access_lost_is_a_recoverable_topology_loss() {990
assert!(matches!(991
classify_hresult(DXGI_ERROR_ACCESS_LOST),992
GpuRendererError::AccessLost993
));994
}995
996
#[test]997
fn scale_parameters_obey_constant_buffer_alignment() {998
assert_eq!(size_of::<ScaleParameters>(), 16);999
}1000
1001
#[test]1002
fn dirty_copy_keeps_move_destinations_and_dirty_rects() {1003
let moved = [RECT {1004
left: 1,1005
top: 2,1006
right: 11,1007
bottom: 12,1008
}];1009
let dirty = [RECT {1010
left: 20,1011
top: 30,1012
right: 40,1013
bottom: 50,1014
}];1015
let mut regions = Vec::new();1016
assert!(select_copy_regions(moved, dirty, 100, 100, &mut regions));1017
assert_eq!(regions, [moved[0], dirty[0]]);1018
}1019
1020
#[test]1021
fn dirty_copy_falls_back_for_invalid_or_large_metadata() {1022
let invalid = [RECT {1023
left: -1,1024
top: 0,1025
right: 10,1026
bottom: 10,1027
}];1028
let mut regions = Vec::new();1029
assert!(!select_copy_regions([], invalid, 100, 100, &mut regions));1030
1031
let half_frame = [RECT {1032
left: 0,1033
top: 0,1034
right: 50,1035
bottom: 100,1036
}];1037
assert!(!select_copy_regions([], half_frame, 100, 100, &mut regions));1038
1039
let too_many = (0..=MAX_COPY_REGIONS).map(|column| RECT {1040
left: column as i32,1041
top: 0,1042
right: column as i32 + 1,1043
bottom: 1,1044
});1045
assert!(!select_copy_regions([], too_many, 1000, 1000, &mut regions));1046
}1047
1048
#[test]1049
fn full_copy_remains_the_default_and_invalid_values_are_rejected() {1050
assert_eq!(CopyMode::parse(None), Ok(CopyMode::Dirty));1051
assert_eq!(1052
CopyMode::parse(Some(OsStr::new(" full "))),1053
Ok(CopyMode::Full)1054
);1055
assert_eq!(1056
CopyMode::parse(Some(OsStr::new("DIRTY"))),1057
Ok(CopyMode::Dirty)1058
);1059
assert!(CopyMode::parse(Some(OsStr::new("partial"))).is_err());1060
}1061
1062
#[test]1063
fn four_fetch_area_weights_preserve_all_16_phases() {1064
let scale = 29.0_f32 / 16.0;1065
for target_pixel in 0..16 {1066
let first = target_pixel as f32 * scale;1067
let last = (target_pixel + 1) as f32 * scale;1068
let base = first.floor();1069
let coverage = [1070
overlap(first, last, base, base + 1.0) / scale,1071
overlap(first, last, base + 1.0, base + 2.0) / scale,1072
overlap(first, last, base + 2.0, base + 3.0) / scale,1073
];1074
let paired_weight = coverage[0] + coverage[1];1075
let blend = coverage[1] / paired_weight;1076
let reconstructed = [1077
paired_weight * (1.0 - blend),1078
paired_weight * blend,1079
coverage[2],1080
];1081
for index in 0..3 {1082
assert!((coverage[index] - reconstructed[index]).abs() < 1.0e-6);1083
}1084
assert!((reconstructed.iter().sum::<f32>() - 1.0).abs() < 1.0e-6);1085
}1086
}1087
1088
fn overlap(first: f32, last: f32, pixel_first: f32, pixel_last: f32) -> f32 {1089
(last.min(pixel_last) - first.max(pixel_first)).max(0.0)1090
}1091
}1092