超高清截图管线(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 级)窗口截图。为此整个流程被拆成三段:
- 请求排队与会话创建(调用线程):校验目标窗口未最小化、读取 WGC 真实捕获尺寸(消除窗口阴影导致的黑边)、分配唯一会话 ID、以回调方式注册帧处理器。
- GPU 帧处理(WGC 帧到达线程):把
Direct3D11CaptureFrame.Surface()转成ID3D11Texture2D,按需执行长曝光均值累积(shutter_frames > 0)与客户区裁剪,然后进入保存管线。全程纹理停留在 GPU 显存,不做逐像素 CPU 拷贝。 - 编码与收尾:非 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
架构分层说明:
| 层 | 组件 | 职责 |
|---|---|---|
| 特性层 | take_screenshot / do_screenshot_capture / finish_screenshot_session | 请求生命周期编排、会话注册与回收 |
| 特性层(编码编排) | save_capture_textures | 按请求分派 WIC 单路或 HDR+JXR 双路保存 |
| 编码器层 | features::screenshot::hdr_encoder | Ultra HDR JPEG 与 JPEG XR 的读回/预处理/编码会话 |
| 图形工具层 | utils::graphics::capture / capture_region / photo_processing | WGC 会话封装、客户区裁剪、GPU 均值累积 |
| 图像工具层 | utils::image | WIC 工厂创建与像素数据落盘 |
| 状态层 | ScreenshotState(features/screenshot/state.hpp) | 会话表、待处理队列、原子会话 ID、共享 WinRT 设备 |
frame_callback 是整个架构的枢纽:它不保存任何裸指针,而是通过 session_id 在 state.active_sessions 中查找 SessionInfo,从而把 WGC 的异步回调安全地绑定回截图请求。这一设计保证了多张截图并发时每条 WGC 帧都能路由到正确的会话。
主内容:核心控制流实现
入口 API 与请求契约
公开头文件只暴露两个函数,刻意把复杂度藏在实现内部:
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 前置校验
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 查找会话,并依次穿过三条可选处理支线:
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 纹理替换原始帧:
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 并降级为"保存未裁剪图",不终止截图:
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 而不影响主图成功判定:
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 的关键开关
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,与"单帧即收尾"的截图语义一致。
注册表插入顺序同样经过设计:
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:端到端时序
HDR + JXR 双路重叠:save_capture_textures 的六个步骤
注释中的编号直接来自源码,这是整条管线最精巧的部分:
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 线程:
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 完成主图编码与双结果聚合:
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 的旧单路流程则非常直接:
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
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
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 帧回调线程。
数据模型 / 结果结构
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 / error | bool / std::wstring / std::string | 主图(Ultra HDR JPEG 或 WIC 编码图)的结果三元组 |
jxr_requested | bool | 是否请求了 JXR 副本(use_hdr && save_jxr),false 时其余 jxr 字段无意义 |
jxr_success / jxr_path / jxr_error | bool / std::wstring / std::string | JXR 副本的独立结果三元组 |
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 截图并接收回调
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 长曝光 + 客户区裁剪
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; // 传入 UltraHdrEncodeOptionsSources:
- screenshot.cpp(
UltraHdrEncodeOptions.target_display_peak_nits构造)- screenshot.cpp(
use_hdr→ 像素格式切换)
(ScreenshotRequest 的完整字段定义位于 features/screenshot/state.hpp,本页未读取该文件;上述字段均在 screenshot.cpp 已读段落中被实际访问,字段语义由此推导。)
WIC 保存路径:staging 回读与 RAII 解除映射
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 送达。
参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
state | core::AppState& | — | 应用全局状态,从中取 features::screenshot::ScreenshotState 与输出目录设置 |
target_window | HWND | — | 截图目标窗口;最小化时同步返回失败 |
completion_callback | std::move_only_function<void(ScreenshotSaveResult)> | nullptr | 完成回调,至多被调用一次,异常被捕获并记日志 |
format | utils::image::ImageFormat | PNG | 非 HDR 主图的 WIC 编码格式 |
jpeg_quality | float | 1.0f | JPEG 质量(仅 format 为 JPEG 时生效) |
output_dir_override | std::optional<std::filesystem::path> | nullopt | 覆盖输出目录;缺省回退到设置或 Videos/SpinningMomo |
shutter_frames | int | 0 | 长曝光帧数;> 0 启用 GPU 均值累积,<= 0 关闭 |
capture_client_area | bool | true | 是否裁剪为客户区(去边框/标题栏) |
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-L93 | GPU 纹理 → staging → WIC 编码保存;空纹理/工厂失败/编码失败均返回 std::unexpected |
make_failed_save_result(request, error) | L95-L106 | 构造携带 jxr 联动错误的失败结果 |
safe_call_completion_callback(request, result) | L108-L121 | move 后单次调用回调,异常隔离 |
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",不创建会话 |
| 捕获项尺寸获取失败/为 0 | get_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
性能设计要点:
- 全分辨率 GPU 端到端 + 单次 CPU 回读:管线中唯一的 CPU 触点是 staging
Map(SDR 路径)或read_jxr_pixels(HDR 路径),且后者会把像素拷贝成独立 CPU 缓冲后立即释放 D3D 资源。8K–12K 纹理若走多轮 CPU 往返会显著劣化,当前结构将 CPU 参与压缩到编码前的最后一刻。 - 两路 GPU 提交背靠背(HDR+JXR 步骤 1–2):
CopyResource与 Ultra HDR 直方图在同一 immediate context 上连续提交,GPU 批量执行,消除一次串行同步点。 - CPU 编码与 GPU 处理重叠(步骤 3–5):JXR 的 WIC 编码在独立
std::async线程运行时,主线程继续 Ultra HDR 的 GPU 预处理、读回与 JPEG 编码,两路工作在时间轴上交叠。 - 共享 WinRT 设备:
state.winrt_device被所有会话复用,避免每张截图付出设备创建成本。 - 空闲清理计时器:全部请求与会话清空后才启动
start_cleanup_timer,连续截图期间不重复销毁/重建 WGC 基础设施。
运维观察点: 日志已按结果分级——HDR 成功为 info、SDR/长曝光成功为 debug、任何失败(主图或 JXR)为 error,可通过日志级别直接区分正常流量与故障流量。
扩展点:
- 新增输出格式/副本:
ScreenshotSaveResult已是"主图 + 独立副本"的双槽结构,若要再增加一种副图(例如 AVIF),可复制 JXR 的模式:在save_capture_textures中先提交 GPU 准备、再std::asyncCPU 编码、最后并入聚合步骤。 - 新增帧处理支线:帧回调中的三条支线(累积、裁剪、保存)是顺序插入的独立阶段,新滤镜(如锐化、水印)可按相同模式在
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,可作为端到端行为与用户视角的补充参考(本页未读取其内容)。
Related Links
- screenshot.hpp — 公共 API 声明
- screenshot.cpp — 管线实现
- types.hpp —
ScreenshotSaveResult定义 - docs/features/screenshot.md / docs/en/features/screenshot.md — 用户文档
- tests/scenarios/capture/screenshot.ts — 捕获场景测试
- WGC 会话封装、
capture_region裁剪算法、photo_processingGPU 均值累积、hdr_encoder编码器内部实现、设置与快捷键触发层:属于兄弟目录页面的范畴,本页仅引用其接口。