Repository Wiki
ChanIok/SpinningMomo

超高清截图管线(8K–12K)

features::screenshot 模块实现了一条基于 Windows Graphics Capture(WGC)+ Direct3D 11 的异步截图管线:从窗口捕获、长曝光帧均值、客户区裁剪,到 WIC / Ultra HDR JPEG / JPEG XR 多路编码与结果回调,全程保持 GPU 全分辨率纹理,仅在最终编码阶段做一次 CPU 回读。

Purpose and Scope

本页覆盖 capture.screenshot 这一能力的完整端到端机制:

  • 公共入口 take_screenshot / cleanup_system 的契约与默认参数;
  • 捕获会话生命周期(ScreenshotState、active_sessions、会话 ID、空闲清理计时器);
  • 帧回调中的三条处理支线:长曝光均值累积、客户区裁剪、双路 HDR 保存;
  • save_capture_textures 中 Ultra HDR 与 JPEG XR 的重叠执行设计;
  • ScreenshotSaveResult 的语义、失败模式与并发模型。

有意留给兄弟页面的内容:Windows Graphics Capture 会话的底层封装与帧池细节、capture_region 的裁剪区域计算算法、photo_processing 的 GPU 均值累积着色器实现、hdr_encoder 的 Ultra HDR / JPEG XR 编码器内部实现、设置项(features/settings)与 UI 触发层,均不在本页展开,仅在交叉引用处给出指引。

Overview

截图管线的核心诉求是在不阻塞调用线程的前提下产出超高清(最高 8K–12K 级)窗口截图。为此整个流程被拆成三段:

  1. 请求排队与会话创建(调用线程):校验目标窗口未最小化、读取 WGC 真实捕获尺寸(消除窗口阴影导致的黑边)、分配唯一会话 ID、以回调方式注册帧处理器。
  2. GPU 帧处理(WGC 帧到达线程):把 Direct3D11CaptureFrame.Surface() 转成 ID3D11Texture2D,按需执行长曝光均值累积(shutter_frames > 0)与客户区裁剪,然后进入保存管线。全程纹理停留在 GPU 显存,不做逐像素 CPU 拷贝。
  3. 编码与收尾:非 HDR 走单路 WIC 编码;HDR+JXR 请求在同一个 D3D immediate context 上提交两路 GPU 准备,再让 JXR 的 WIC 编码与 Ultra HDR 的后续 GPU/CPU 工作重叠,最后以 completion_callback 一次性返回主图与 JXR 双份结果。

之所以能做到 8K–12K 量级,关键在于管线不缩放、不降采样:捕获尺寸直接取自 get_capture_item_size 返回的窗口真实大小,HDR 场景使用 R16G16B16A16Float 半精度浮点帧池保留高光动态范围,而 CPU 侧只在编码前的 staging 纹理 Map 时做一次回读。在已读取的 screenshot.cpp 段落中未观察到任何分辨率钳制或超采样缩放逻辑——超高清能力来自"全分辨率 GPU 端到端 + 单次回读"的结构性设计。

Architecture

Loading diagram...

架构分层说明:

层组件职责
特性层take_screenshot / do_screenshot_capture / finish_screenshot_session请求生命周期编排、会话注册与回收
特性层(编码编排)save_capture_textures按请求分派 WIC 单路或 HDR+JXR 双路保存
编码器层features::screenshot::hdr_encoderUltra HDR JPEG 与 JPEG XR 的读回/预处理/编码会话
图形工具层utils::graphics::capture / capture_region / photo_processingWGC 会话封装、客户区裁剪、GPU 均值累积
图像工具层utils::imageWIC 工厂创建与像素数据落盘
状态层ScreenshotState(features/screenshot/state.hpp)会话表、待处理队列、原子会话 ID、共享 WinRT 设备

frame_callback 是整个架构的枢纽:它不保存任何裸指针,而是通过 session_id 在 state.active_sessions 中查找 SessionInfo,从而把 WGC 的异步回调安全地绑定回截图请求。这一设计保证了多张截图并发时每条 WGC 帧都能路由到正确的会话。

主内容:核心控制流实现

入口 API 与请求契约

公开头文件只暴露两个函数,刻意把复杂度藏在实现内部:

cpp
1// 主要API:异步截图 2// output_dir_override: 指定时使用该目录,否则使用 output_dir_path 或 Videos/SpinningMomo 3auto take_screenshot( 4 core::AppState& state, HWND target_window, 5 std::move_only_function<void(ScreenshotSaveResult result)> completion_callback = nullptr, 6 utils::image::ImageFormat format = utils::image::ImageFormat::PNG, float jpeg_quality = 1.0f, 7 std::optional<std::filesystem::path> output_dir_override = std::nullopt, int shutter_frames = 0, 8 bool capture_client_area = true) -> std::expected<void, std::string>; 9 10// 系统管理函数 11auto cleanup_system(core::AppState& state) -> void;

Source: screenshot.hpp

设计意图有三处值得注意:

  • std::expected<void, std::string> 返回值:同步阶段(会话创建、尺寸探测)的错误通过返回值回传,异步阶段的错误则封装进 ScreenshotSaveResult 交给回调。同步失败绝不触发回调,避免调用方收到双重通知。
  • completion_callback 用 std::move_only_function:回调只允许移动、不允许复制,配合 safe_call_completion_callback 中"先 move 再调用"的写法,保证回调严格只被执行一次。
  • 默认目录回退链:output_dir_override → 设置中的 output_dir_path → Videos/SpinningMomo,调用方无需感知路径配置。

会话建立:do_screenshot_capture 前置校验

cpp
1auto do_screenshot_capture(features::screenshot::ScreenshotRequest& request, 2 features::screenshot::ScreenshotState& state) 3 -> std::expected<void, std::string> { 4 try { 5 // 最小化窗口不执行截图,避免创建无法完成的捕获会话 6 if (IsIconic(request.target_window)) { 7 return std::unexpected("Target window is minimized"); 8 } 9 10 // 获取 WGC 真实的捕获宽高以消除阴影引起的黑边 11 auto capture_size_result = 12 utils::graphics::capture::get_capture_item_size(request.target_window); 13 if (!capture_size_result) { 14 return std::unexpected("Failed to get capture item size: " + capture_size_result.error()); 15 } 16 17 int width = capture_size_result->first; 18 int height = capture_size_result->second; 19 if (width <= 0 || height <= 0) { 20 return std::unexpected("Invalid window size"); 21 } 22 23 // 生成唯一的会话ID 24 auto session_id = state.next_session_id.fetch_add(1);

Source: screenshot.cpp

两个前置校验都有明确动机:

  • IsIconic 拒截:最小化窗口无法产生 WGC 帧,如果照常创建会话,帧回调永远不触发,会话将永久泄漏在 active_sessions 中。提前拒绝是防泄漏手段,而非用户体验优化。
  • 以 get_capture_item_size 为准而非 GetClientRect:WGC 的捕获项尺寸包含 DWM 绘制的窗口投影(阴影),直接用客户区尺寸会得到带黑边的截图;反之,用它作为帧池尺寸可保证像素精确对齐。宽度/高度非正数校验兜住 WGC 返回 0 的异常场景。

帧回调:按 session_id 路由的三条支线

do_screenshot_capture 创建的帧回调是整个管线的核心,它按 session_id 从 state.active_sessions 查找会话,并依次穿过三条可选处理支线:

cpp
1auto frame_callback = [&state, 2 session_id](utils::graphics::capture::Direct3D11CaptureFrame frame) { 3 // 查找对应的会话信息 4 auto it = state.active_sessions.find(session_id); 5 if (it == state.active_sessions.end()) { 6 Logger().error("Session {} not found in frame callback", session_id); 7 return; 8 } 9 10 auto& session_info = it->second; 11 auto save_result = make_failed_save_result(session_info.request, "Captured frame is null"); 12 13 if (frame) { 14 auto surface = frame.Surface(); 15 if (surface) { 16 auto texture = 17 utils::graphics::capture::get_dxgi_interface_from_object<ID3D11Texture2D>(surface); 18 if (texture) { 19 ID3D11Texture2D* texture_to_save = texture.get(); 20 const int shutter_frames = std::max(0, session_info.request.shutter_frames);

Source: screenshot.cpp

回调一开始就把 save_result 初始化为失败结果(make_failed_save_result),只有每一步都成功才会被覆盖——这是"默认失败"的防御式编程模式,确保任何中途 return 都不会丢失回调通知。std::max(0, ...) 对 shutter_frames 做了夹取,负值等同于关闭长曝光。

支线一:长曝光(慢快门)均值累积。 首帧调用 initialize_average_accumulator 初始化 GPU 均值累积器,后续帧调用 accumulate_average_frame 加权混合;未达到目标帧数时直接 return 等待下一帧,累积完成后用 current_average 纹理替换原始帧:

cpp
1if (shutter_frames > 0) { 2 // 首帧初始化 GPU 均值累积器,后续帧做加权混合 3 if (!session_info.average_accumulator) { 4 auto accumulator_result = 5 utils::graphics::photo_processing::initialize_average_accumulator( 6 texture.get()); 7 if (!accumulator_result) { 8 Logger().error("Failed to initialize long exposure for session {}: {}", 9 session_id, accumulator_result.error()); 10 finish_screenshot_session( 11 state, it, session_id, 12 make_failed_save_result(session_info.request, accumulator_result.error())); 13 return; 14 } 15 session_info.average_accumulator = std::move(accumulator_result.value()); 16 } else { 17 auto accumulate_result = 18 utils::graphics::photo_processing::accumulate_average_frame( 19 *session_info.average_accumulator, texture.get()); 20 if (!accumulate_result) { 21 Logger().error("Failed to accumulate long exposure for session {}: {}", 22 session_id, accumulate_result.error()); 23 finish_screenshot_session( 24 state, it, session_id, 25 make_failed_save_result(session_info.request, accumulate_result.error())); 26 return; 27 } 28 } 29 30 // 未达到目标帧数时直接返回,等下一帧继续累积 31 if (session_info.average_accumulator->frame_count < 32 static_cast<std::uint32_t>(shutter_frames)) { 33 return; 34 } 35 36 // 累积完成,用混合后的均值纹理替换原始帧 37 texture_to_save = session_info.average_accumulator->current_average.get(); 38}

Source: screenshot.cpp

累积失败时同样通过 finish_screenshot_session 走失败收尾——长曝光路径上任何一个 GPU 错误都会终止会话并通知调用方,而不是无限等待。均值在 GPU 侧完成,8K–12K 分辨率下逐帧 CPU 混合是不可行的,这正是累积器必须驻留显存的原因。

支线二:客户区裁剪。 capture_client_area 为真时,先用 calculate_client_crop_region 把客户区矩形换算成纹理坐标区域,再调用 crop_texture_to_region 在 GPU 上复制出裁剪纹理;两条路径失败都只 Logger().warn 并降级为"保存未裁剪图",不终止截图:

cpp
1// 若开启了无边框捕获(仅捕获客户区),则在保存前裁剪纹理 2wil::com_ptr<ID3D11Texture2D> cropped_texture; 3if (session_info.request.capture_client_area) { 4 D3D11_TEXTURE2D_DESC desc; 5 texture_to_save->GetDesc(&desc); 6 7 auto crop_region_result = 8 utils::graphics::capture_region::calculate_client_crop_region( 9 session_info.request.target_window, desc.Width, desc.Height); 10 if (crop_region_result) { 11 wil::com_ptr<ID3D11Device> device; 12 texture_to_save->GetDevice(device.put()); 13 wil::com_ptr<ID3D11DeviceContext> context; 14 if (device) { 15 device->GetImmediateContext(context.put()); 16 } 17 if (device && context) { 18 auto crop_result = utils::graphics::capture_region::crop_texture_to_region( 19 device.get(), context.get(), texture_to_save, *crop_region_result, 20 cropped_texture); 21 if (crop_result) { 22 texture_to_save = crop_result.value(); 23 } else { 24 Logger().warn("Failed to crop texture for screenshot: {}", crop_result.error()); 25 } 26 } 27 } else { 28 Logger().warn("Failed to calculate client crop region for screenshot: {}", 29 crop_region_result.error()); 30 } 31}

Source: screenshot.cpp

裁剪失败仅告警是产品决策:一张带边框的截图仍可用,而一次失败的截图对用户是彻底丢失。裁剪所需的设备/上下文直接从当前纹理反查(GetDevice + GetImmediateContext),避免帧回调依赖外部捕获的 COM 指针,天然规避了设备销毁竞态。

支线三:双结果保存与日志分级。 save_capture_textures 返回后按 HDR / 长曝光 / 普通三档输出不同级别日志,JXR 失败单独记 error 而不影响主图成功判定:

cpp
1save_result = save_capture_textures(texture_to_save, session_info.request); 2if (save_result.success) { 3 if (session_info.request.use_hdr) { 4 Logger().info("HDR screenshot saved for session {}: {}", session_id, 5 utils::string::ToUtf8(session_info.request.file_path)); 6 } else if (shutter_frames > 0) { 7 Logger().debug("Long exposure screenshot saved successfully for session {}", 8 session_id); 9 } else { 10 Logger().debug("Screenshot saved successfully for session {}", session_id); 11 } 12} else { 13 if (session_info.request.use_hdr) { 14 Logger().error("HDR screenshot save failed for session {}: {}", session_id, 15 save_result.error); 16 } else { 17 Logger().error("Failed to save screenshot for session {}: {}", session_id, 18 save_result.error); 19 } 20} 21if (save_result.jxr_requested && !save_result.jxr_success) { 22 Logger().error("HDR JPEG XR save failed for session {}: {}", session_id, 23 save_result.jxr_error); 24}

Source: screenshot.cpp

回调末尾无条件调用 finish_screenshot_session(包括 frame / surface / texture 任一为空的失败分支,此时沿用预置的失败 save_result),保证会话一定会被回收。

帧池像素格式:HDR 的关键开关

cpp
1// 创建捕获会话 2utils::graphics::capture::CaptureSessionOptions capture_options; 3// 默认可捕获 8-bit BGRA;HDR 截图需要半精度浮点帧池才能保留高光动态范围。 4if (request.use_hdr) { 5 capture_options.pixel_format = 6 winrt::Windows::Graphics::DirectX::DirectXPixelFormat::R16G16B16A16Float; 7} 8 9auto session_result = utils::graphics::capture::create_capture_session( 10 request.target_window, state.winrt_device, width, height, frame_callback, 1, 11 capture_options); 12if (!session_result) { 13 return std::unexpected("Failed to create capture session: " + session_result.error()); 14}

Source: screenshot.cpp

这是 HDR 截图成败的分水岭:默认 BGRA8 只有 8 bit 精度,高光早已被截断;切换到 R16G16B16A16Float 后 WGC 帧池承载线性 HDR 值,后续 Ultra HDR / JXR 编码才有可用的动态范围。会话创建时 state.winrt_device 作为共享 WinRT 设备传入(避免每次截图都新建 Direct3DCreateDevice),帧池缓冲数量为 1,与"单帧即收尾"的截图语义一致。

注册表插入顺序同样经过设计:

cpp
1// 先在注册表中创建槽位;分配失败时 request 尚未移动,调用方仍能完成失败通知。 2auto [session_it, inserted] = state.active_sessions.try_emplace(session_id); 3if (!inserted) { 4 return std::unexpected("Screenshot session id already exists"); 5} 6auto& session_info = session_it->second; 7session_info.session = std::move(session_result.value());

Source: screenshot.cpp

先用 try_emplace 占住槽位再移入 request:try_emplace 失败时 request 仍是完整的,调用方可以基于原始 request 构造失败结果——注释里明确写出了这一动机。

Core Flow:端到端时序

Loading diagram...

HDR + JXR 双路重叠:save_capture_textures 的六个步骤

注释中的编号直接来自源码,这是整条管线最精巧的部分:

cpp
1// 1. 先创建 JXR staging texture 并提交 CopyResource,只在稍后 Map。 2std::optional<features::screenshot::hdr_encoder::JxrReadbackSession> jxr_readback; 3try { 4 auto jxr_readback_result = features::screenshot::hdr_encoder::begin_jxr_readback(texture); 5 if (jxr_readback_result) { 6 jxr_readback.emplace(std::move(jxr_readback_result.value())); 7 } else { 8 result.jxr_error = jxr_readback_result.error(); 9 } 10} catch (const std::exception& e) { 11 result.jxr_error = std::format("JPEG XR readback setup failed: {}", e.what()); 12} catch (...) { 13 result.jxr_error = "JPEG XR readback setup failed"; 14} 15 16// 2. 紧接着提交 Ultra HDR 直方图;此时两路 GPU 准备都已经进入同一个 immediate context。 17std::optional<features::screenshot::hdr_encoder::UltraHdrPreprocessSession> hdr_session; 18try { 19 auto hdr_session_result = features::screenshot::hdr_encoder::begin_ultrahdr_preprocess(texture); 20 if (hdr_session_result) { 21 hdr_session.emplace(std::move(hdr_session_result.value())); 22 } else { 23 result.error = hdr_session_result.error(); 24 } 25} catch (const std::exception& e) { 26 result.error = std::format("Ultra HDR preprocess setup failed: {}", e.what()); 27} catch (...) { 28 result.error = "Ultra HDR preprocess setup failed"; 29}

Source: screenshot.cpp

步骤 1、2 是性能关键:两次 GPU 提交(CopyResource 与直方图计算)背靠背压入同一个 immediate context,GPU 可以把它们当作一个批次连续执行,而不是"回读—等结果—再提交"的串行往返。在 8K 纹理规模下,一次无谓的 GPU 同步等待就可能引入数百毫秒的停顿。

步骤 3–4 把 CPU 密集部分彻底移出 D3D 线程:

cpp
1if (!jxr_readback) { 2 if (result.jxr_error.empty()) { 3 result.jxr_error = "JPEG XR readback is unavailable"; 4 } 5} else { 6 // 3. 在 D3D 线程完成 staging Map,复制成独立 CPU 缓冲后立即释放 D3D 资源。 7 try { 8 auto jxr_pixels_result = 9 features::screenshot::hdr_encoder::read_jxr_pixels(std::move(*jxr_readback)); 10 if (!jxr_pixels_result) { 11 result.jxr_error = jxr_pixels_result.error(); 12 } else { 13 // 4. WIC/文件写入只使用 CPU 像素,交给独立线程;该线程不触碰 D3D immediate context。 14 jxr_future.emplace(std::async( 15 std::launch::async, 16 [pixels = std::move(jxr_pixels_result.value()), file_path = request.jxr_file_path]() { 17 return features::screenshot::hdr_encoder::save_jxr_pixels(pixels, file_path); 18 })); 19 } 20 } catch (const std::exception& e) { 21 result.jxr_error = std::format("JPEG XR readback or encoding setup failed: {}", e.what()); 22 } catch (...) { 23 result.jxr_error = "JPEG XR readback or encoding setup failed"; 24 } 25}

Source: screenshot.cpp

步骤 3 强调"Map 后立即复制成独立 CPU 缓冲并释放 D3D 资源"——staging 纹理一旦持有,就会锁住显存并阻塞后续提交;步骤 4 的注释则点出线程纪律:异步线程只做 WIC 编码与文件 IO,绝不触碰 immediate context,这是避免多线程 D3D 竞态的根本约束。

步骤 5–6 完成主图编码与双结果聚合:

cpp
1// 5. 主线程读取直方图并继续 Ultra HDR GPU 预处理、读回和 JPEG 编码。 2if (!hdr_session) { 3 if (result.error.empty()) { 4 result.error = "Ultra HDR preprocess is unavailable"; 5 } 6} else { 7 try { 8 auto prepared_result = 9 features::screenshot::hdr_encoder::finish_ultrahdr_preprocess(*hdr_session); 10 if (!prepared_result) { 11 result.error = prepared_result.error(); 12 } else { 13 auto primary_result = 14 features::screenshot::hdr_encoder::save_prepared_images_as_ultrahdr_jpeg( 15 prepared_result.value(), request.file_path, hdr_options); 16 if (primary_result) { 17 result.success = true; 18 } else { 19 result.error = primary_result.error(); 20 } 21 } 22 } catch (const std::exception& e) { 23 result.error = std::format("Ultra HDR output failed: {}", e.what()); 24 } catch (...) { 25 result.error = "Ultra HDR output failed"; 26 } 27} 28 29// 6. 类似 Promise.allSettled:无论主图是否失败,都等待已启动的 JXR 任务并汇总结果。 30if (jxr_future) { 31 try { 32 auto jxr_result = jxr_future->get(); 33 if (jxr_result) { 34 result.jxr_success = true; 35 } else { 36 result.jxr_error = jxr_result.error(); 37 } 38 } catch (const std::exception& e) { 39 result.jxr_error = std::format("JPEG XR encoding task failed: {}", e.what()); 40 } catch (...) { 41 result.jxr_error = "JPEG XR encoding task failed"; 42 } 43}

Source: screenshot.cpp

步骤 6 的注释自比 Promise.allSettled:主图已定成败之后才 jxr_future->get(),即使主图失败也照样等待并汇总 JXR 结果——两份输出互不掩盖,与 ScreenshotSaveResult 的结构语义完全一致。

非 HDR / 未开 JXR 的旧单路流程则非常直接:

cpp
1// 非 HDR 或未开启 JXR 时保持单路旧流程;JXR 只对有效 HDR 请求生效。 2if (!jxr_requested) { 3 try { 4 auto primary_result = request.use_hdr 5 ? features::screenshot::hdr_encoder::save_texture_as_ultrahdr_jpeg( 6 texture, request.file_path, hdr_options) 7 : save_texture_with_wic(texture, request.file_path, request.format, 8 request.jpeg_quality); 9 if (primary_result) { 10 result.success = true; 11 } else { 12 result.error = primary_result.error(); 13 } 14 } catch (const std::exception& e) { 15 result.error = std::format("Screenshot output failed: {}", e.what()); 16 } catch (...) { 17 result.error = "Screenshot output failed"; 18 } 19 return result; 20}

Source: screenshot.cpp

会话收尾:finish_screenshot_session

cpp
1// 截图完成收尾:恢复光标 → 停止捕获 → 回调 → 移除会话 → 检查是否启动空闲清理 2auto finish_screenshot_session( 3 features::screenshot::ScreenshotState& state, 4 std::unordered_map<size_t, features::screenshot::SessionInfo>::iterator session_it, 5 size_t session_id, features::screenshot::ScreenshotSaveResult result) -> void { 6 auto& session_info = session_it->second; 7 8 if (session_info.session.need_hide_cursor) { 9 ShowCursor(TRUE); 10 } 11 12 utils::graphics::capture::stop_capture(session_info.session); 13 utils::graphics::capture::cleanup_capture_session(session_info.session); 14 safe_call_completion_callback(session_info.request, std::move(result)); 15 state.active_sessions.erase(session_it); 16 Logger().debug("Session {} completed and removed", session_id); 17 18 { 19 std::lock_guard<std::mutex> lock(state.request_mutex); 20 if (state.pending_requests.empty() && state.active_sessions.empty()) { 21 start_cleanup_timer(state); 22 } 23 } 24}

Source: screenshot.cpp

收尾顺序有讲究:先恢复光标(ShowCursor(TRUE) 是全局计数器,必须与隐藏时严格配对,否则系统光标会永久消失),再停止/清理 WGC 会话释放系统捕获资源,然后才触发用户回调,最后 erase 会话并检查是否进入空闲清理。若把回调放在 erase 之前(当前实现即是),回调内可以安全地观察到"会话仍存在但已停止捕获"的中间态;而 request_mutex 保护下的双空判断(pending_requests 与 active_sessions 同时为空)确保清理计时器只在真正空闲时启动,连续截图不会反复重建销毁 WGC 基础设施。

回调安全包装:safe_call_completion_callback

cpp
1// 安全调用完成回调的辅助函数 2auto safe_call_completion_callback(features::screenshot::ScreenshotRequest& request, 3 features::screenshot::ScreenshotSaveResult result) -> void { 4 if (!request.completion_callback) { 5 return; 6 } 7 8 try { 9 auto completion_callback = std::move(request.completion_callback); 10 completion_callback(std::move(result)); 11 } catch (...) { 12 Logger().error("Exception in completion callback"); 13 } 14}

Source: screenshot.cpp

先把回调 move 到局部变量再调用,等于把 request.completion_callback 置空——即便回调内部重入触发新的截图或销毁请求,也不可能造成同一回调被二次执行。catch (...) 保证用户回调抛出的任何异常都被拦截并记日志,不会击穿 WGC 帧回调线程。

数据模型 / 结果结构

cpp
1// 单次截图的主图与可选 JXR 输出结果。 2// 主图失败时仍会独立尝试保存 JXR,避免一份编码失败掩盖另一份可用输出。 3struct ScreenshotSaveResult { 4 bool success = false; 5 std::wstring path; 6 std::string error; 7 bool jxr_requested = false; 8 bool jxr_success = false; 9 std::wstring jxr_path; 10 std::string jxr_error; 11};

Source: types.hpp

字段语义一览:

字段类型语义
success / path / errorbool / std::wstring / std::string主图(Ultra HDR JPEG 或 WIC 编码图)的结果三元组
jxr_requestedbool是否请求了 JXR 副本(use_hdr && save_jxr),false 时其余 jxr 字段无意义
jxr_success / jxr_path / jxr_errorbool / std::wstring / std::stringJXR 副本的独立结果三元组

jxr_requested = request.use_hdr && request.save_jxr 这一定义在 save_capture_textures(screenshot.cpp L128)与 make_failed_save_result(L95–L106)中保持一致:捕获阶段整体失败时,JXR 也用同一错误字符串填充 jxr_error = "Screenshot capture failed",让调用方只需检查一套字段即可判断所有输出的状态。

会话侧的运行时结构 SessionInfo 与 ScreenshotState(定义于 features/screenshot/state.hpp,本次未读取源码,仅从 screenshot.cpp 的使用推导)至少包含:捕获会话对象 session(含 need_hide_cursor 标记)、请求副本 request、可选 average_accumulator(长曝光 GPU 累积器,通过 frame_count 与 current_average 访问)。ScreenshotState 则持有 active_sessions(unordered_map<size_t, SessionInfo>)、pending_requests、原子递增的 next_session_id、共享 winrt_device 与 request_mutex。推导内容以 src/features/screenshot/state.hpp 实际源码为准。

使用示例

基础用法:PNG 截图并接收回调

cpp
1auto callback = [](features::screenshot::ScreenshotSaveResult result) { 2 if (result.success) { 3 Logger().info("Saved to {}", utils::string::ToUtf8(result.path)); 4 } else { 5 Logger().error("Failed: {}", result.error); 6 } 7}; 8 9auto screenshot_result = features::screenshot::take_screenshot( 10 app_state, target_hwnd, std::move(callback), 11 utils::image::ImageFormat::PNG); 12if (!screenshot_result) { 13 Logger().error("Failed to enqueue screenshot: {}", screenshot_result.error()); 14}

调用签名取自 screenshot.hpp,回调消费 ScreenshotSaveResult 的方式与 screenshot.cpp 中 safe_call_completion_callback(L108-L121)传递的结构一致。

进阶用法:HDR 长曝光 + 客户区裁剪

cpp
1features::screenshot::ScreenshotRequest request; 2request.target_window = game_hwnd; 3request.use_hdr = true; // 触发 R16G16B16A16Float 帧池 + Ultra HDR 编码 4request.save_jxr = true; // 额外产出 .jxr 副本(双路重叠保存) 5request.shutter_frames = 8; // GPU 均值累积 8 帧(长曝光降噪) 6request.capture_client_area = true; // 裁掉标题栏/边框 7request.format = utils::image::ImageFormat::JPEG; 8request.jpeg_quality = 0.95f; 9request.hdr_target_peak_nits = 1000.0f; // 传入 UltraHdrEncodeOptions

Sources:

(ScreenshotRequest 的完整字段定义位于 features/screenshot/state.hpp,本页未读取该文件;上述字段均在 screenshot.cpp 已读段落中被实际访问,字段语义由此推导。)

WIC 保存路径:staging 回读与 RAII 解除映射

cpp
1// 创建暂存纹理 2D3D11_TEXTURE2D_DESC staging_desc = desc; 3staging_desc.Usage = D3D11_USAGE_STAGING; 4staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; 5staging_desc.BindFlags = 0; 6staging_desc.MiscFlags = 0; 7staging_desc.ArraySize = 1; 8staging_desc.MipLevels = 1; 9 10wil::com_ptr<ID3D11Texture2D> staging_texture; 11THROW_IF_FAILED(device->CreateTexture2D(&staging_desc, nullptr, staging_texture.put())); 12 13// 复制纹理数据 14context->CopyResource(staging_texture.get(), texture); 15 16// 映射纹理并写入像素数据 17D3D11_MAPPED_SUBRESOURCE mapped{}; 18THROW_IF_FAILED(context->Map(staging_texture.get(), 0, D3D11_MAP_READ, 0, &mapped)); 19 20// 使用 RAII 确保纹理总是被正确解除映射 21auto unmap_on_exit = wil::scope_exit([&] { context->Unmap(staging_texture.get(), 0); });

Source: screenshot.cpp

save_texture_with_wic 是 SDR 截图唯一的 CPU 触点:GPU 纹理 → staging CopyResource → Map → 交给 utils::image::save_pixel_data_to_file 以 WIC 编码落盘(L81-L83)。wil::scope_exit 保证任何后续异常路径都会 Unmap,否则映射中的 staging 纹理会拖死整个 immediate context。

API Reference

take_screenshot(...) -> std::expected<void, std::string>

异步截图主入口。同步阶段成功即返回 {},最终结果通过 completion_callback 送达。

参数:

参数类型默认值说明
statecore::AppState&—应用全局状态,从中取 features::screenshot::ScreenshotState 与输出目录设置
target_windowHWND—截图目标窗口;最小化时同步返回失败
completion_callbackstd::move_only_function<void(ScreenshotSaveResult)>nullptr完成回调,至多被调用一次,异常被捕获并记日志
formatutils::image::ImageFormatPNG非 HDR 主图的 WIC 编码格式
jpeg_qualityfloat1.0fJPEG 质量(仅 format 为 JPEG 时生效)
output_dir_overridestd::optional<std::filesystem::path>nullopt覆盖输出目录;缺省回退到设置或 Videos/SpinningMomo
shutter_framesint0长曝光帧数;> 0 启用 GPU 均值累积,<= 0 关闭
capture_client_areabooltrue是否裁剪为客户区(去边框/标题栏)

Returns: 同步失败原因(窗口最小化、尺寸获取失败、无效窗口尺寸、会话创建失败、会话 ID 冲突);成功时不携带结果,结果只走回调。

Throws: 实现内部以 try/catch 包裹,wil::ResultException 等异常被转为 std::unexpected 字符串;回调内异常被 safe_call_completion_callback 吞掉并记日志。

cleanup_system(core::AppState& state) -> void

系统管理函数:释放截图子系统资源(签名见 screenshot.hpp)。实现体位于 screenshot.cpp 未读取的后段,清理范围(start_cleanup_timer 的定时逻辑等)本页不展开。

内部关键函数(screenshot.cpp)

函数位置职责
save_texture_with_wic(texture, file_path, format, jpeg_quality)L31-L93GPU 纹理 → staging → WIC 编码保存;空纹理/工厂失败/编码失败均返回 std::unexpected
make_failed_save_result(request, error)L95-L106构造携带 jxr 联动错误的失败结果
safe_call_completion_callback(request, result)L108-L121move 后单次调用回调,异常隔离
save_capture_textures(texture, request)L125-L270双路保存编排:单路 WIC/UltraHDR 或 HDR+JXR 六步重叠流程
finish_screenshot_session(state, session_it, session_id, result)L272-L295会话收尾五连:光标 → 停捕获 → 清理 → 回调 → 移除 + 空闲检测
do_screenshot_capture(request, state)L298 起校验、建会话、注册帧回调
start_cleanup_timer(state)声明于 L28空闲清理计时器(实现未读取)

Failure Modes, Edge Cases & Concurrency

失败模式

场景检测点处理
目标窗口最小化IsIconic同步返回 "Target window is minimized",不创建会话
捕获项尺寸获取失败/为 0get_capture_item_result + 宽高校验同步返回错误字符串
WGC 帧为空 / Surface 为空 / 纹理获取失败帧回调三层判空沿用预置失败 save_result 走 finish_screenshot_session,回调仍被触发
长曝光累积器初始化/累积失败initialize_average_accumulator / accumulate_average_frame 返回值立即终止会话并回调失败结果
客户区裁剪区域计算或裁剪失败crop_region_result / crop_result仅 warn,降级保存未裁剪原图
WIC 编码失败save_pixel_data_to_file 返回值错误进入 result.error,回调失败
Ultra HDR 任一步失败六步流程各 try/catch + std::expected主图标记失败,但 JXR 结果仍独立聚合
JXR 读回/编码失败begin_jxr_readback / read_jxr_pixels / jxr_future->get()仅污染 jxr_error,不影响主图 success
用户回调抛异常safe_call_completion_callback 的 catch (...)记 "Exception in completion callback",会话照常回收
会话 ID 冲突try_emplace 返回 inserted == false同步返回 "Screenshot session id already exists"(理论上不可达,fetch_add 保证唯一)

边界条件

  • shutter_frames <= 0:std::max(0, ...) 夹取后等于 0,直接跳过长曝光支线,首帧即保存。
  • shutter_frames == 1:首帧初始化累积器后 frame_count 已达 1,立即用均值纹理(等同原图)保存。
  • jxr_requested == false(非 HDR 或未开 JXR):走单路旧流程,提前 return,JXR 相关字段保持默认。
  • staging Map 后的异常路径:wil::scope_exit 强制 Unmap,避免映射泄漏卡死 immediate context。

并发模型

  • 会话 ID 唯一性:state.next_session_id.fetch_add(1) 原子分配,帧回调凭 session_id 在 active_sessions 中路由,天然支持多截图并发。
  • D3D 线程纪律:所有 GPU 提交(CopyResource、直方图、裁剪、Ultra HDR 预处理)都发生在创建会话的 D3D 线程/帧回调线程;唯一例外是 std::launch::async 的 JXR 编码线程,且源码注释明确该线程"不触碰 D3D immediate context",只消费已拷出的 CPU 像素。
  • std::future 聚合:jxr_future->get() 在主图处理完成后阻塞等待,形成 allSettled 语义——任何一个输出失败都不会让另一个输出被跳过。
  • 状态锁:state.request_mutex 仅在空闲检测(pending_requests + active_sessions 双空 → start_cleanup_timer)处加锁,临界区极小,避免与帧回调争抢。
  • 光标计数器:ShowCursor(TRUE) 只在 session.need_hide_cursor 为真时执行,与隐藏侧严格配对,防止多次截图叠加导致计数失配。

Performance / Operational Notes 与 Extension Points

性能设计要点:

  1. 全分辨率 GPU 端到端 + 单次 CPU 回读:管线中唯一的 CPU 触点是 staging Map(SDR 路径)或 read_jxr_pixels(HDR 路径),且后者会把像素拷贝成独立 CPU 缓冲后立即释放 D3D 资源。8K–12K 纹理若走多轮 CPU 往返会显著劣化,当前结构将 CPU 参与压缩到编码前的最后一刻。
  2. 两路 GPU 提交背靠背(HDR+JXR 步骤 1–2):CopyResource 与 Ultra HDR 直方图在同一 immediate context 上连续提交,GPU 批量执行,消除一次串行同步点。
  3. CPU 编码与 GPU 处理重叠(步骤 3–5):JXR 的 WIC 编码在独立 std::async 线程运行时,主线程继续 Ultra HDR 的 GPU 预处理、读回与 JPEG 编码,两路工作在时间轴上交叠。
  4. 共享 WinRT 设备:state.winrt_device 被所有会话复用,避免每张截图付出设备创建成本。
  5. 空闲清理计时器:全部请求与会话清空后才启动 start_cleanup_timer,连续截图期间不重复销毁/重建 WGC 基础设施。

运维观察点: 日志已按结果分级——HDR 成功为 info、SDR/长曝光成功为 debug、任何失败(主图或 JXR)为 error,可通过日志级别直接区分正常流量与故障流量。

扩展点:

  • 新增输出格式/副本:ScreenshotSaveResult 已是"主图 + 独立副本"的双槽结构,若要再增加一种副图(例如 AVIF),可复制 JXR 的模式:在 save_capture_textures 中先提交 GPU 准备、再 std::async CPU 编码、最后并入聚合步骤。
  • 新增帧处理支线:帧回调中的三条支线(累积、裁剪、保存)是顺序插入的独立阶段,新滤镜(如锐化、水印)可按相同模式在 texture_to_save 上追加。
  • 更换捕获后端:do_screenshot_capture 依赖的 utils::graphics::capture 接口(get_capture_item_size / create_capture_session / stop_capture / cleanup_capture_session)是清晰的接缝;替换为 DXGI Desktop Duplication 等后端时只需重写该工具层。

测试线索:仓库存在 tests/scenarios/capture/screenshot.ts 与用户文档 docs/features/screenshot.md、docs/en/features/screenshot.md,可作为端到端行为与用户视角的补充参考(本页未读取其内容)。