Evan's Blog
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.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / src
a37d6c4b· 固定源码快照
src/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 GPU
6//! resource into CPU memory, or sends pixels through GDI.
7
8use std::fmt;
9use std::mem::size_of;
10use std::slice;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::time::{Duration, Instant};
13
14use crate::diagnostics::{self, Level};
15use windows::Win32::Foundation::{HMODULE, HWND, RECT};
16use windows::Win32::Graphics::Direct3D::Fxc::D3DCompile;
17use 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};
21use 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};
29use windows::Win32::Graphics::Dxgi::Common::{
30 DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC,
31};
32use 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};
40use windows::core::{Error as WindowsError, Interface, PCSTR};
41
42const FRAME_WAIT_TIMEOUT_MS: u32 = 50;
43const COPY_MODE_ENV: &str = "SBMS_RENDER_COPY_MODE";
44const MAX_METADATA_BYTES: u32 = 1024 * 1024;
45const MAX_COPY_REGIONS: usize = 128;
46const MAX_PARTIAL_COPY_PERCENT: u64 = 50;
47const COPY_STATS_INTERVAL: Duration = Duration::from_secs(10);
48
49const SHADER: &str = r#"
50Texture2D source_texture : register(t0);
51SamplerState source_sampler : register(s0);
52
53cbuffer ScaleParameters : register(b0) {
54 float2 source_size;
55 float2 target_size;
56};
57
58struct VertexOutput {
59 float4 position : SV_POSITION;
60 float2 uv : TEXCOORD0;
61};
62
63VertexOutput 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
77void 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 bilinear
91 // 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
97float4 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 return
109 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
119float3 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 resize
129 // that high-frequency chroma is no longer aligned with the physical panel.
130 // Preserve the area-filtered luminance and only borrow low-passed chroma
131 // where the wider neighborhood is close to neutral. Solid UI colors keep
132 // 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
148float4 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)]
165pub 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)]
173pub 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
181impl 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
191impl 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 the
196/// existing renderer keep its first-frame and FPS reporting without exposing
197/// D3D objects outside this module.
198pub 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)]
220enum CopyMode {
221 Full,
222 Dirty,
223}
224
225impl 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::Dirty
240 }
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::Full
251 }
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
275fn 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.left
287 || config.source_rect.bottom <= config.source_rect.top
288 {
289 return Err(GpuRendererError::Failure(
290 "GPU renderer source rectangle is empty".into(),
291 ));
292 }
293 Ok(())
294}
295
296fn 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 output
317 .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.bottom
328 )))
329}
330
331fn same_rect(left: RECT, right: RECT) -> bool {
332 left.left == right.left
333 && left.top == right.top
334 && left.right == right.right
335 && left.bottom == right.bottom
336}
337
338fn 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
366struct 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
378impl 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_target
459 .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: sampler
467 .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.0
514 )));
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 None
580 };
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.device
624 .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.device
632 .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 (&parameters 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.context
671 .OMSetRenderTargets(Some(&[Some(self.render_target.clone())]), None);
672 self.context.RSSetViewports(Some(&[viewport]));
673 self.context
674 .IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
675 self.context.VSSetShader(&self.vertex_shader, None);
676 self.context.PSSetShader(&self.pixel_shader, None);
677 self.context
678 .PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));
679 self.context
680 .PSSetConstantBuffers(0, Some(&[Some(self.scale_parameters.clone())]));
681 self.context
682 .PSSetShaderResources(0, Some(&[Some(source_view.clone())]));
683 self.context.Draw(3, 0);
684 }
685 }
686}
687
688fn 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 None
721 }
722}
723
724#[derive(Default)]
725struct FrameMetadataBuffers {
726 moves: Vec<DXGI_OUTDUPL_MOVE_RECT>,
727 dirty: Vec<RECT>,
728 regions: Vec<RECT>,
729}
730
731fn 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 duplication
743 .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
753fn 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 duplication
765 .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
775fn 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 < 0
793 || region.top < 0
794 || region.right <= region.left
795 || region.bottom <= region.top
796 || region.right as u32 > width
797 || region.bottom as u32 > height
798 {
799 return false;
800 }
801 if regions.contains(&region) {
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 true
821}
822
823struct 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
832impl 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 += regions
855 .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 !enabled
862 || copy_mode != CopyMode::Dirty
863 || self.interval_started.elapsed() < COPY_STATS_INTERVAL
864 {
865 return;
866 }
867 let available_pixels = self
868 .frames
869 .saturating_mul(u64::from(width))
870 .saturating_mul(u64::from(height));
871 let copied_percent = if available_pixels == 0 {
872 0.0
873 } else {
874 self.copied_pixels as f64 * 100.0 / available_pixels as f64
875 };
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
890struct AcquiredFrame<'a>(&'a IDXGIOutputDuplication);
891
892impl Drop for AcquiredFrame<'_> {
893 fn drop(&mut self) {
894 unsafe {
895 let _ = self.0.ReleaseFrame();
896 }
897 }
898}
899
900#[repr(C)]
901struct ScaleParameters {
902 source_size: [f32; 2],
903 target_size: [f32; 2],
904}
905
906fn 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 = diagnostics
928 .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
943fn 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
952fn 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 bytes
957}
958
959fn classify_windows_error(error: WindowsError) -> GpuRendererError {
960 classify_hresult(error.code())
961}
962
963fn classify_hresult(code: windows::core::HRESULT) -> GpuRendererError {
964 if code == DXGI_ERROR_ACCESS_LOST {
965 GpuRendererError::AccessLost
966 } else if code == DXGI_ERROR_DEVICE_REMOVED || code == DXGI_ERROR_DEVICE_RESET {
967 GpuRendererError::DeviceLost
968 } else {
969 GpuRendererError::Failure(format!("DXGI/D3D11 call failed: {code:?}"))
970 }
971}
972
973#[cfg(test)]
974mod 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::AccessLost
993 ));
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