@@ -14,6 +14,7 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
1414use super :: dxgi_state:: { point_in_output, window_center, DxgiState } ;
1515use super :: frame_diff:: { crop_frame, Rect } ;
1616use super :: gdi_capture:: gdi_capture;
17+ use super :: wgc_capture:: WgcState ;
1718
1819/// 捕获帧(BGRA8 像素)。
1920#[ derive( Debug , Clone ) ]
@@ -39,9 +40,25 @@ const DXGI_RECREATE_INTERVAL: std::time::Duration = std::time::Duration::from_se
3940/// 不再等 30s 周期重建(否则最长 30s 空窗/裁剪错位)。
4041const DXGI_REBIND_FAST_INTERVAL : std:: time:: Duration = std:: time:: Duration :: from_secs ( 2 ) ;
4142
42- /// 屏幕捕获采样器(DXGI 主路径 + GDI 降级 + 周期重建自愈)。
43+ /// 捕获后端(ADR-022 三级降级链:WGC 窗口级 → DXGI 屏幕级 → GDI 兜底)。
44+ ///
45+ /// @ai-context: WGC=窗口级(目标窗口不被遮挡,主路径);DXGI=屏幕级+窗口裁剪
46+ /// (降级 1,当前既有主路径);GDI=全场景兜底。WGC 失败后不回切
47+ /// (YAGNI——沿用 DXGI 已有 30s 周期重建)。
48+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
49+ pub ( crate ) enum CaptureBackend {
50+ Wgc ,
51+ Dxgi ,
52+ Gdi ,
53+ }
54+
55+ /// 屏幕捕获采样器(WGC 主路径 + DXGI 降级 1 + GDI 兜底 + 周期重建自愈)。
4356pub struct ScreenCaptureSampler {
57+ /// ADR-022:WGC 窗口级捕获(主路径;None=未启用/已降级)
58+ wgc : Option < WgcState > ,
4459 dxgi : Option < DxgiState > ,
60+ /// 当前生效后端(诊断/路由——capture() 按其分派)
61+ backend : CaptureBackend ,
4562 window_rect : Rect ,
4663 started : std:: time:: Instant ,
4764 /// 目标窗口句柄(重建 DXGI 与刷新窗口矩形用;None=全屏)
@@ -55,7 +72,7 @@ pub struct ScreenCaptureSampler {
5572}
5673
5774impl ScreenCaptureSampler {
58- /// 创建采样器:优先 DXGI(按窗口所在显示器 duplication),失败降级 GDI。
75+ /// 创建采样器:WGC → DXGI → GDI 逐级尝试(ADR-022 三级自愈链) 。
5976 pub fn new ( hwnd : Option < HWND > ) -> crate :: error:: Result < Self > {
6077 let window_rect = match hwnd {
6178 Some ( h) => {
@@ -66,20 +83,35 @@ impl ScreenCaptureSampler {
6683 }
6784 None => Rect { left : 0 , top : 0 , right : 0 , bottom : 0 } ,
6885 } ;
69- let dxgi = match DxgiState :: create ( hwnd) {
70- Ok ( state) => Some ( state) ,
71- Err ( e) => {
72- // 创建失败降级 GDI——必须可观测(用户反馈 4/5 会话无 OCR 排查:
73- // DXGI 并发上限/远程桌面/驱动异常时此处曾静默,无法定位)
74- eprintln ! ( "[ScreenCapture] DXGI 初始化失败,降级 GDI: {}" , e) ;
75- None
76- }
86+ // 主路径:WGC(窗口级,抗遮挡)——仅窗口捕获启用;失败逐级降级。
87+ // 全屏捕获(无目标窗口)无窗口级语义,直接 DXGI。
88+ let ( wgc, dxgi) = match hwnd {
89+ Some ( h) => match WgcState :: create ( h) {
90+ Ok ( state) => ( Some ( state) , None ) ,
91+ Err ( e) => {
92+ // WGC 不可用(Win10 1903 以下/安全桌面/无 DWM/窗口最小化)→ DXGI
93+ eprintln ! ( "[ScreenCapture] WGC 初始化失败,降级 DXGI: {}" , e) ;
94+ ( None , DxgiState :: create ( hwnd) . ok ( ) )
95+ }
96+ } ,
97+ None => ( None , DxgiState :: create ( hwnd) . ok ( ) ) ,
98+ } ;
99+ let backend = if wgc. is_some ( ) {
100+ CaptureBackend :: Wgc
101+ } else if dxgi. is_some ( ) {
102+ CaptureBackend :: Dxgi
103+ } else {
104+ eprintln ! ( "[ScreenCapture] DXGI 初始化失败,降级 GDI" ) ;
105+ CaptureBackend :: Gdi
77106 } ;
78107 Ok ( Self {
108+ wgc,
79109 dxgi,
110+ backend,
80111 window_rect,
81112 started : std:: time:: Instant :: now ( ) ,
82113 hwnd,
114+ // GDI 期间周期尝试重建(backend_name 同步)
83115 last_recreate : std:: time:: Instant :: now ( ) ,
84116 out_of_bounds : false ,
85117 pending_event : None ,
@@ -130,6 +162,7 @@ impl ScreenCaptureSampler {
130162 Ok ( state) => {
131163 eprintln ! ( "[ScreenCapture] DXGI 主路径已恢复" ) ;
132164 self . out_of_bounds = false ;
165+ self . backend = CaptureBackend :: Dxgi ;
133166 self . dxgi = Some ( state) ;
134167 }
135168 Err ( e) => eprintln ! ( "[ScreenCapture] DXGI 重建失败(节流后重试): {}" , e) ,
@@ -149,55 +182,89 @@ impl ScreenCaptureSampler {
149182 !point_in_output ( cx, cy, state. desktop_coords )
150183 }
151184
152- /// 捕获一帧(可选裁剪区域,相对桌面帧坐标 )。
185+ /// 捕获一帧(可选裁剪区域,相对捕获帧坐标 )。
153186 ///
154- /// @ai-context: 返回 Ok(None) 表示桌面无变化(超时);DXGI 连续失效自动切 GDI。
155- /// @ai-context: 先按目标窗口矩形裁剪(ADR-002 承诺,审查 M3 修复),再叠加区域裁剪;
156- /// 字幕区裁剪(bottom_quarter)基于窗口尺寸由上层重新计算。
187+ /// @ai-context: 返回 Ok(None) 表示无新帧(WGC 无新帧 / DXGI 桌面超时);
188+ /// 后端失效自动降级下一级(WGC→DXGI→GDI,ADR-022 三级自愈链)。
189+ /// @ai-context: WGC 帧=窗口内容(无需窗口矩形裁剪,仅叠加上层 crop);DXGI
190+ /// 先按目标窗口矩形裁剪(ADR-002 承诺,审查 M3 修复),再叠加
191+ /// 区域裁剪;字幕区裁剪(bottom_quarter)由上层基于窗口尺寸计算。
157192 pub fn capture ( & mut self , crop : Option < & Rect > ) -> crate :: error:: Result < Option < CapturedFrame > > {
158193 // ADR-007:每次捕获前刷新窗口矩形(移动/缩放/关闭自适应)
159194 self . refresh_window_rect ( ) ;
160195 let elapsed_ms = self . started . elapsed ( ) . as_millis ( ) as u64 ;
161- // TD-033:先判越界再取可变借用(借用规则:不可变检查与可变修改分离)
162- let window_out_of_output = self . dxgi . as_ref ( ) . is_some_and ( |s| self . window_center_out_of_output ( s) ) ;
163- if let Some ( state) = self . dxgi . as_mut ( ) {
164- // TD-033:窗口中心越出 duplication 输出区域(跨显示器移动)→ 弃用当前
165- // DXGI 立即重建;否则持续复制旧显示器桌面,窗口裁剪为空画面(最长 30s 空窗)
166- if window_out_of_output {
167- eprintln ! ( "[ScreenCapture] 窗口中心越出输出区域,弃用 DXGI 快速重建(TD-033)" ) ;
168- self . dxgi = None ;
169- self . out_of_bounds = true ;
170- self . try_create_dxgi ( ) ;
171- self . last_recreate = std:: time:: Instant :: now ( ) ;
172- } else {
173- match state. capture_frame ( elapsed_ms) {
174- Ok ( Some ( mut frame) ) => {
175- // 窗口裁剪(全屏时 window_rect 为空矩形,裁剪无效果)
176- let window_crop = ( self . window_rect . width ( ) > 0 ) . then_some ( self . window_rect ) ;
177- if let Some ( w) = window_crop {
178- crop_frame ( & mut frame. bgraw , & mut frame. width , & mut frame. height , Some ( & w) ) ;
196+ // WGC 主路径(窗口级——帧即窗口内容,不做窗口矩形裁剪)
197+ if self . backend == CaptureBackend :: Wgc {
198+ match self . wgc . as_mut ( ) . expect ( "backend=Wgc 必有 wgc" ) . capture ( elapsed_ms) {
199+ Ok ( Some ( mut frame) ) => {
200+ crop_frame ( & mut frame. bgraw , & mut frame. width , & mut frame. height , crop) ;
201+ return Ok ( Some ( frame) ) ;
202+ }
203+ Ok ( None ) => return Ok ( None ) ,
204+ Err ( e) => {
205+ // WGC 失效(窗口关闭/最小化/会话丢失)→ 降级 DXGI(一次性,
206+ // 不回切 WGC——YAGNI,沿用 DXGI 周期重建)
207+ eprintln ! ( "[ScreenCapture] WGC 失效,降级 DXGI: {}" , e) ;
208+ self . wgc = None ;
209+ self . dxgi = DxgiState :: create ( self . hwnd ) . ok ( ) ;
210+ self . backend = if self . dxgi . is_some ( ) {
211+ CaptureBackend :: Dxgi
212+ } else {
213+ eprintln ! ( "[ScreenCapture] DXGI 重建失败,降级 GDI" ) ;
214+ CaptureBackend :: Gdi
215+ } ;
216+ }
217+ }
218+ }
219+ // DXGI 降级 1(屏幕级 + 窗口裁剪——既有逻辑)
220+ if self . backend == CaptureBackend :: Dxgi {
221+ // TD-033:先判越界再取可变借用(借用规则:不可变检查与可变修改分离)
222+ let window_out_of_output = self . dxgi . as_ref ( ) . is_some_and ( |s| self . window_center_out_of_output ( s) ) ;
223+ if let Some ( state) = self . dxgi . as_mut ( ) {
224+ // TD-033:窗口中心越出 duplication 输出区域(跨显示器移动)→ 弃用当前
225+ // DXGI 立即重建;否则持续复制旧显示器桌面,窗口裁剪为空画面(最长 30s 空窗)
226+ if window_out_of_output {
227+ eprintln ! ( "[ScreenCapture] 窗口中心越出输出区域,弃用 DXGI 快速重建(TD-033)" ) ;
228+ self . dxgi = None ;
229+ self . out_of_bounds = true ;
230+ self . try_create_dxgi ( ) ;
231+ self . last_recreate = std:: time:: Instant :: now ( ) ;
232+ } else {
233+ match state. capture_frame ( elapsed_ms) {
234+ Ok ( Some ( mut frame) ) => {
235+ // 窗口裁剪(全屏时 window_rect 为空矩形,裁剪无效果)
236+ let window_crop = ( self . window_rect . width ( ) > 0 ) . then_some ( self . window_rect ) ;
237+ if let Some ( w) = window_crop {
238+ crop_frame ( & mut frame. bgraw , & mut frame. width , & mut frame. height , Some ( & w) ) ;
239+ }
240+ crop_frame ( & mut frame. bgraw , & mut frame. width , & mut frame. height , crop) ;
241+ return Ok ( Some ( frame) ) ;
242+ }
243+ Ok ( None ) => return Ok ( None ) ,
244+ Err ( e) => {
245+ // DXGI 失效(远程桌面/锁屏/设备丢失)→ 降级 GDI(ADR-002)
246+ eprintln ! ( "[ScreenCapture] DXGI 失效,降级 GDI: {}" , e) ;
247+ self . dxgi = None ;
248+ self . backend = CaptureBackend :: Gdi ;
249+ self . last_recreate = std:: time:: Instant :: now ( ) ;
179250 }
180- crop_frame ( & mut frame. bgraw , & mut frame. width , & mut frame. height , crop) ;
181- return Ok ( Some ( frame) ) ;
182- }
183- Ok ( None ) => return Ok ( None ) ,
184- Err ( e) => {
185- // DXGI 失效(远程桌面/锁屏/设备丢失)→ 降级 GDI(ADR-002)
186- eprintln ! ( "[ScreenCapture] DXGI 失效,降级 GDI: {}" , e) ;
187- self . dxgi = None ;
188- self . last_recreate = std:: time:: Instant :: now ( ) ;
189251 }
190252 }
191253 }
192254 }
193- // GDI 期间周期尝试重建 DXGI 主路径(ADR-007 自愈;TD-033 越界后为 2s 快速节流)
255+ // GDI 兜底(全场景可用);GDI 期间周期尝试重建 DXGI 主路径
256+ // (ADR-007 自愈;TD-033 越界后为 2s 快速节流)
194257 self . maybe_recreate_dxgi ( ) ;
195258 self . capture_gdi ( crop, elapsed_ms)
196259 }
197260
198261 /// 当前后端名(诊断/日志——live_session 启动时记录,增强捕获后端可观测性)。
199262 pub fn backend_name ( & self ) -> & ' static str {
200- if self . dxgi . is_some ( ) { "dxgi" } else { "gdi" }
263+ match self . backend {
264+ CaptureBackend :: Wgc => "wgc" ,
265+ CaptureBackend :: Dxgi => "dxgi" ,
266+ CaptureBackend :: Gdi => "gdi" ,
267+ }
201268 }
202269
203270 fn capture_gdi ( & mut self , crop : Option < & Rect > , elapsed_ms : u64 ) -> crate :: error:: Result < Option < CapturedFrame > > {
0 commit comments