资讯动态

CEF离屏渲染优化:OnAcceleratedPaint与D3D11共享纹理实战指南

发布时间:2026/9/23 19:12:36 来源:尧图企业网站定制
简介使用CEF的高性能离屏渲染演示工程是面向桌面应用开发者的完整示例基于浏览器内核与图形接口的共享纹理技术展示借助硬件加速回调完成网页离屏绘制可解决常规软件渲染在复杂界面下的帧率与资源占用问题适合需要将网页用户界面嵌入原生程序并追求流畅交互的中高级开发人员。资源包内含三十二个文件压缩后仅一百三十七KB主体为源代码与头文件并配套构建脚本、工程生成批处理、补丁文件及说明文档各模块划分清晰方便直接查阅与编译。已有七十七人参与学习下载内容聚焦最新的图形加速离屏渲染路径覆盖从环境配置到工程生成的完整关键环节。通过学习该示例可深入理解渲染合成与图层管理的实现思路掌握把网页内容高效输出到原生窗口的具体方法并能在实际项目中复用相关补丁与代码结构有效缩短自研渲染方案的开发周期。1. CEF 离屏渲染与 OnAcceleratedPaint这套演示解决的不只是「不卡」CEFChromium Embedded Framework做离屏渲染OSR时画质和功能都不缺真正卡住项目的是画面怎么从 Chromium 里高效拿出来。老接口 OnPaint 给的是 CPU 位图每帧要先把 RGBA 数组拷出渲染进程再上传 GPU帧率稍高一点 CPU 就吃不消而 OnAcceleratedPaint 直接交出 GPU 共享纹理绕开了这条最贵的路。这个演示包把完整的 D3D11 共享纹理 OSR 混合器工程摊开给你看CMake 构建、composition/web_layer/image_layer 多层合成、透明 HUD 叠加一套全齐。适合做游戏启动器、桌面工具箱、监控面板也适合想搞懂 CEF 多进程构建的人抄作业。2. 先读架构再编译composition、web_layer 与 D3D11 共享纹理的分工2.1 老回调 OnPaint 慢在哪CPU 位图到 GPU 的冤枉路先说最核心的痛点。CEF 在窗口模式on-screen下有自己的合成器你不需要碰像素但切到 OSR 之后CEF 把每帧画面通过CefRenderHandler::OnPaint交给你。老接口拿到的是一块纯 CPU 内存里面是 RGBA 字节宽高就是页面尺寸。你要做的事是把这块内存拷贝到自己的 D3D11 纹理再UpdateSubresource上传到显存最后在渲染循环里把它贴到后缓冲上。这条链路有两个明显浪费。第一Chromium 的 GPU 进程本来已经在显存里合成好了页面OnPaint 却强制把结果读回 CPU等于把已经画好的画从墙上摘下来、扫描成照片、再打印一份挂回去。第二UpdateSubresource是一次 CPU→GPU 的 PCIe 传输1080p 一帧就是 8MB 左右几十帧下来 CPU 占用非常可观。实测里这种路径轻松吃掉 2~5ms/帧还没算上后续的混合叠加。维度OnPaintOnAcceleratedPaint数据形态CPU 侧 RGBA 位图指针GPU 共享纹理句柄数据流GPU 合成 → 读回 CPU → 再上传 GPUGPU 显存内直接跨进程共享带宽开销每帧两次 PCIe 往返几乎没有 CPU 拷贝透明 HUD 叠加要手动处理 alpha 通道纹理自带 alpha可直接混合典型耗时1080p 下约 2~5ms/帧通常 0.1~0.5ms/帧这套演示核心就是第二行用OnAcceleratedPaint()配合 D3D11 共享纹理把「读回 CPU」这一步彻底省掉。2.2 OnAcceleratedPaint 给的不是像素是纹理句柄新回调的声明在老回调旁边一眼能看出差别// 老的 CPU 位图回调 void OnPaint(CefRefPtrCefBrowser browser, PaintElementType type, const RectList dirtyRects, const void* buffer, int width, int height) override { // buffer 是 RGBA 字节数组宽高分别由 width/height 给出。 // 这里不能直接跨线程保存 buffer下一次回调它就被 CEF 回收了。 } // 新的 GPU 共享纹理回调 void OnAcceleratedPaint(CefRefPtrCefBrowser browser, PaintElementType type, const RectList dirtyRects, const CefAcceleratedPaintInfo info) override { // 这不是位图而是一个 D3D11 共享纹理的句柄。 // 具体字段名以你当前 CEF 头文件为准关键信息包括 // info.release_handle —— 共享句柄传给 OpenSharedResource 用 // info.sync_token —— 帧完成同步信息保证读到的是完整一帧 // info.shared_rect —— 有效内容区域不一定等于整个纹理尺寸 }这里要注意OnAcceleratedPaint通常跑在 CEF 的内部 IO 线程上不是你的渲染线程。常见做法是只把release_handle和shared_rect记下来通过队列丢给 D3D11 渲染线程去OpenSharedResource不要在回调里直接调用 D3D11 设备。原因很简单回调频率跟 Chromium 合成帧率一致直接在里面做重活会把 CEF 的合成线程拖住反而掉帧。拿到句柄之后你的 D3D11 设备要用OpenSharedResource打开这份纹理ComPtrID3D11Texture2D layer_tex; HRESULT hr d3d11_device-OpenSharedResource( handle, __uuidof(ID3D11Texture2D), (void**)layer_tex); if (FAILED(hr)) { // 句柄可能已过期需要从 CEF 侧重新拿最新一帧的 handle。 return; }参数上handle是info.release_handle但不同 CEF 版本的字段来源可能不同有的版本走shared_handle有的版本需要你自己维护base::SharedMemory映射。这套演示之所以叫「高性能」就是因为它把这步封装好了你不需要自己调 Mojo 或共享内存协议。2.3 演示里的三层分层composition / web_layer / image_layer打开src目录核心文件其实就三个角色composition.cpp管合成顺序web_layer.cpp管 CEF 页面纹理image_layer.cpp管静态图片d3d11.cpp统一管设备和纹理生命周期。先说为什么分层。OSR 场景里你往往不只是显示一个网页启动器可能需要底部一张背景图、中间是网页内容、最上面浮一层实时状态 HUD。如果每个元素都自己往后缓冲上画顺序一出错画面就乱。这套演示的做法是每个 layer 先渲染到自己的共享纹理最后一帧在 composition 阶段统一混合。// 伪代码composition 每帧的合成顺序 void CompositionFrame() { d3d11_context-ClearRenderTargetView(rtv, clear_color); // 先清屏 for (Layer* layer : layers) { // 按 z-order 从底到顶 ID3D11Texture2D* tex layer-GetSharedTexture(); layer-BlendTo(back_buffer, tex); // 各层做 alpha 混合 } swapchain-Present(1, 0); }每一层都持有独立纹理这点很关键。如果你的 web_layer 直接把纹理画到后缓冲下一帧 image_layer 再往上画那网页内容就被覆盖了独立纹理 统一合成才能做到网页、图片、HUD 三个元素互不干扰地叠加。web_layer.cpp里通常维护一个CefRefPtrCefBrowser在OnAcceleratedPaint里更新共享纹理句柄image_layer.cpp处理overlay.svg这类静态资源把它解码成 D3D11 纹理后一次上传、反复使用。这也是推荐做法活动网页走 CEF 纹理静态装饰走 image layer别让 CEF 去加载不必要的东西省一整个渲染进程的开销。3. 从零构建CEF_ROOT、gen_vs2017.bat 与 x86/x64 的差别3.1 先把 CEF_ROOT 指对二进制分发版本决定后面所有事想编译这套演示你需要先准备三样东西CMake、Visual Studio 2017 或 2019装 C/C 开发工具、一份 CEF 二进制分发版。示例里给的是 Chromium 72 x64 的 release 分发下载解压后目录名大概长这样cef_binary_3.3440.xxxx_windows64里面能看到libcef.dll、cmake文件夹和include头文件目录。打开命令提示符先设置环境变量set CEF_ROOTD:\libs\cef_binary_3.3440.1804_windows64注意CEF_ROOT必须指向「包含 libcef.dll 和 cmake 文件夹」的那一层不能多一层也不能少一层。FindCEF.cmake会直接用这个路径去拼libcef.lib、头文件路径和cef_sandbox.lib的完整路径路径指错了CMake 会在 configure 阶段直接报错错误信息通常是找不到libcef_dll_wrapper或cef_variables.cmake。另外这套演示的示例二进制分发只有 Release 版本没有 Debug 版 libcef.dll。所以后面生成工程时建议直接选 Release 配置编译硬编 Debug 会卡在链接阶段因为导入库根本不存在。3.2 gen_vs2017.bat 实际做了什么设置完CEF_ROOT后运行gen_vs2017.bat。脚本内容不长拆开看就是标准的 CMake 两步echo off if %CEF_ROOT% ( echo [ERROR] CEF_ROOT is not set. exit /b 1 ) cmake -S . -B build_vs2017 -G Visual Studio 15 2017 -A x64 ^ -DCEF_ROOT%CEF_ROOT% cmake --build build_vs2017 --config Release --target osr_demo第一段检查环境变量有没有漏设第二段用-A x64显式指定 64 位架构避免旧写法里依赖 generator 默认值第三段直接编译 Release。如果你用 VS2019就把 generator 换成Visual Studio 16 2019脚本里已经准备了gen_vs2019.bat。工程里带的三个 cmake 文件各司其职。cef_variables.cmake负责把 CEF 的编译选项、平台宏、OSR 相关特性读进来FindCEF.cmake负责定位libcef.lib、沙箱库和头文件目录cef_macros.cmake提供cef_compile_app_util、cef_add_resources这类封装宏帮你把libcef_dll_wrapper、字符集设置和 Windows 入口点一次性链接对。新手最容易漏的其实是CEF_USE_SANDBOX这个变量演示脚本里一般默认关掉沙箱不然运行时需要额外配置cef_sandbox.lib。3.3 x86 构建三个地方要一起改如果你要产出 32 位程序光改 CMake generator 不够必须三处同步。第一处是gen_vs2017.bat里的-A x64改成-A Win32第二处是CEF_ROOT必须指向 x86 版本的 CEF 二进制分发比如cef_binary_3.3440.xxxx_windows32第三处是确认你的 Visual Studio 安装了 x86 版本的 C/C 编译工具通常 VS2017 默认会带但精简安装可能没装。set CEF_ROOTD:\libs\cef_binary_3.3440.1804_windows32 cmake -S . -B build_vs2017_x86 -G Visual Studio 15 2017 -A Win32 ^ -DCEF_ROOT%CEF_ROOT% cmake --build build_vs2017_x86 --config Release --target osr_demox86 构建设置好之后接下来看main.cpp。CEF 是多进程架构主程序既能当浏览器进程也能当渲染子进程和 GPU 子进程靠的就是CefExecuteProcess分流int APIENTRY wWinMain(HINSTANCE inst, HINSTANCE, LPWSTR, int) { CefMainArgs main_args(inst); CefRefPtrCefApp app(new DemoApp); int exit_code CefExecuteProcess(main_args, app, nullptr); if (exit_code 0) { // exit_code 0 说明当前进程被 CEF 用作子进程跑完就退出。 return exit_code; } CefSettings settings; CefString(settings.browser_subprocess_path).FromASCII(osr_demo.exe); settings.multi_threaded_message_loop true; settings.background_color 0; // 透明背景配合 OSR 使用 if (!CefInitialize(main_args, settings, app, nullptr)) { return -1; } CefRunMessageLoop(); CefShutdown(); return 0; }这段逻辑值得细看。CefExecuteProcess返回负数时才说明当前进程是浏览器进程可以继续CefInitialize如果返回非负值说明 CEF 把这个进程当成了子进程跑完直接退出。browser_subprocess_path建议显式设置尤其当你的 exe 改了名如果不设置CEF 默认用自己的子进程逻辑按主程序名去找名字对不上就会白屏或直接崩。4. 避坑记录OnAcceleratedPaint 不生效、纹理黑屏与 GPU 子进程崩溃4.1 现象一OnAcceleratedPaint 一次都没进只有白屏编译成功、窗口也能打开但页面始终白屏打断点在老回调 OnPaint 里也没命中。原因通常是三选一CEF 版本太老不支持OnAcceleratedPaint创建窗口时没有开启共享纹理开关或者 GPU 合成被禁用。示例二进制分发是 Chromium 72本身就带这个回调所以排第一的是窗口信息。CefWindowInfo window_info; window_info.SetAsWindowless(kNullWindowHandle, true); window_info.shared_texture_enabled true; // 老版本字段名可能不同解决方法是把shared_texture_enabled显式置为 true。CEF 之所以给这个开关是因为共享纹理路径依赖 GPU 进程有些纯软件渲染的部署环境必须退回OnPaint。如果你的程序没有实现OnPaint而shared_texture_enabled恰好是 false就会白屏。如果开关已经打开还是没回调去chrome://gpu对应的日志里看 GPU 进程是否用了硬件加速。远程桌面、虚拟机里 D3D11 设备可能创建失败CEF 会退到 SwiftShader而 SwiftShader 路径通常不走共享纹理。4.2 现象二纹理能打开但画面一直是黑的OpenSharedResource成功、不报错CopyResource 也执行了但屏幕上就是黑块。这多半是同步问题。CEF 在 GPU 进程里写完共享纹理后需要通知你「这一帧已经可用」而这个通知就是sync_token。如果你拿到句柄后立刻OpenSharedResource再CopyResource很可能读到的是上一帧残留甚至是一块未初始化的显存。常见做法是用 IDXGIKeyedMutex 做同步CEF 内部如果用了这个机制ComPtrIDXGIKeyedMutex keyed_mutex; hr layer_tex-QueryInterface(__uuidof(IDXGIKeyedMutex), (void**)keyed_mutex); if (SUCCEEDED(hr)) { // 参数一keyCEF 侧写入时用同一个 key。 // 参数二等待毫秒数这里给 100ms超时就放弃本帧。 keyed_mutex-AcquireSync(0, 100); context-CopyResource(back_buffer, layer_tex.Get()); keyed_mutex-ReleaseSync(1); }如果你的 CEF 版本不走 KeyedMutex而是用 D3D11 Fence那就改成在 GPU 队列上做 Wait。判断标准很简单打开d3d11.cpp看它创建纹理时用的是D3D11_RESOURCE_MISC_SHARED还是D3D11_RESOURCE_MISC_SHARED_NTHANDLE后者往往配 Fence。另一个隐蔽坑是句柄过期。release_handle不是创建一次就能永久使用的CEF 每次合成新帧可能释放旧共享纹理。一旦 OpenSharedResource 返回DXGI_ERROR_INVALID_CALL不要重试同一个句柄而是取 CEF 侧最新回调里的 handle 再试。4.3 现象三gpu 子进程反复崩溃重启日志里一堆 D3D11 device removed表现是程序启动后窗口闪一下然后 GPU 进程不断被拉起又崩溃任务管理器里能看到多个同名 exe 进程反复出现。CEF 用自己的子进程承担渲染和 GPU 合成。如果主程序入口没有CefExecuteProcess分流CEF 就找不到合法的子进程入口导致 GPU 进程初始化失败。解决方法是回到第 3 章的main.cpp模板确保CefExecuteProcess在CefInitialize之前执行。第二类原因是 GPU 设备不匹配。OSR 模式下 CE4 通常用 D3D11 创建设备如果你的宿主程序也用 D3D11两台设备能力差异过大时Chromium 在D3D11CreateDevice阶段就会返回E_INVALIDARG。比如 CEF 要求 feature level 11_0而你的环境只支持 10_0。开发阶段可以临时在CefSettings里设置settings.no_sandbox true;沙箱会限制 GPU 进程的部分 D3D 调用开发期关掉能省很多排查时间。发布时再重新打开沙箱并做完整测试。另外注意browser_subprocess_path指向的 exe 必须有正确的资源段manifest被改过图标或压缩过的 exe 有时会导致子进程启动失败。4.4 现象四x86 构建报 LNK1112 模块计算机类型冲突错误大概长这样LNK1112: module machine type x64 conflicts with target machine type x86。原因很直接CMake generator 是 Win32但CEF_ROOT指到的 CEF 二进制是 x64或者反过来。CEF 的导入库libcef.lib是分架构的x64 的 lib 不可能链接进 x86 的 exe。解决方法是按 3.3 的做法把-A Win32和 x86 的 CEF_ROOT 配套使用。还有个小坑是 Debug 配置。示例分发版只有 Release如果你用 Visual Studio 直接在 Debug 下 F5会报找不到libcef.dll或导入库不匹配。不要试图混用Debug 工程链接 Release 导入库在 CEF 这种带libcef_dll_wrapper的项目里会产生大量符号冲突最省事的做法是始终用 Release 生成调试日志用CEF.Log输出。4.5 现象五透明 HUD 一直垫底或被网页盖住演示包里overlay.svg、hud.html都准备好了但合成时 HUD 要么在最底下看不见要么盖住网页全部内容。这是图层顺序和混合状态的问题。composition.cpp的 for 循环按 z-order 从底到顶绘制如果你把 HUD 的 layer 加到了最前面网页内容就会被盖上。正确顺序是先画背景/网页层最后画 HUD 层。但如果顺序对、HUD 还是被网页盖住问题往往出在混合状态。D3D11 默认的 blend state 是不透明的直接把带 alpha 的纹理画上去会整个覆盖。需要手动开启 alpha blendD3D11_BLEND_DESC bd{}; bd.RenderTarget[0].BlendEnable TRUE; bd.RenderTarget[0].SrcBlend D3D11_BLEND_SRC_ALPHA; bd.RenderTarget[0].DestBlend D3D11_BLEND_INV_SRC_ALPHA; bd.RenderTarget[0].BlendOp D3D11_BLEND_OP_ADD; bd.RenderTarget[0].SrcBlendAlpha D3D11_BLEND_ONE; bd.RenderTarget[0].DestBlendAlpha D3D11_BLEND_INV_SRC_ALPHA; ComPtrID3D11BlendState blend_state; d3d11_device-CreateBlendState(bd, blend_state); d3d11_context-OMSetBlendState(blend_state.Get(), nullptr, 0xffffffff);这套配置表示源像素 alpha 参与混合目标像素按 1-alpha 保留正是 HUD 叠加最常见的模式。逐层确认 blend state 生效后透明叠加就不会再出问题。5. 自己编 CEF 才需要 patchesshared_textures 补丁怎么对应怎么打5.1 三个补丁分别干什么演示包里patches目录放了三个 patch很多第一次拿到资源的人会困惑示例不是能跑吗为什么还要补丁答案是示例二进制分发版已经内置了OnAcceleratedPaint你不打补丁就能跑。但 CEF 官方版本并非所有分支都默认开启共享纹理路径尤其当你需要基于特定 Chromium 版本自己编译 CEF、定制显示模块时就得把共享纹理能力移植过去。补丁文件适用分支典型用途cef_issue_2559.patch较老 CEF 分支对应 CEF issue 2559把 OSR 共享纹理开关补回指定分支shared_textures_3440.patchChromium 3440 系列约 Chrome 72在 3440 分支启用 D3D11 共享纹理路径shared_textures_3497.patchChromium 3497 系列把共享纹理改动同步到更新的 3497 分支命名里的 3440、3497 是 Chromium 版本号不是 CEF 自己的版本号。示例二进制分发是 Chromium 72 x64对应 3440 这条线所以shared_textures_3440.patch和它最匹配。cef_issue_2559.patch是功能开关类补丁通常补在更早的位置。5.2 给 CEF 源码树打补丁的完整流程先明确一点补丁是打在 CEF/Chromium 源码树上的不是打在libcef.dll上的。你需要先按 CEF 官方流程用automate-git.py拉一份源码切到对应分支然后再打补丁。# 假设你已经用 automate-git.py 拉好了 CEF 源码目录叫 cef cd cef # 先切到要改的分支这里以 3440 为例 git checkout 3440 # 先检查能不能干净打上不要直接 apply git apply --check patches/cef_issue_2559.patch git apply patches/cef_issue_2559.patch # 再打 shared_textures 补丁 git apply --check patches/shared_textures_3440.patch git apply patches/shared_textures_3440.patch # 打完后重新生成工程并编译 libcef python cef_create_projects.py ninja -C out/Release_GN_x64 cefgit apply --check这一步很重要它只做校验不写入能提前发现补丁与当前代码差异过大。如果 check 通过再执行真正的git apply。顺序上建议先打小补丁再打大补丁cef_issue_2559.patch通常是功能前提先让它生效后续 shared texture 的改动才有挂载点。编译时注意ninja一定要用 GN 生成的 Release 配置且目标架构和你最终要链接的宿主程序一致。打完补丁后的libcef.dll和libcef.lib会覆盖到 CEF 二进制分发对应位置之后回到演示工程目录重新走一遍set CEF_ROOT和gen_vs2017.bat让 CMake 拾取新编译的产物。5.3 补丁冲突、回退和什么时候别打git apply --check失败是常态尤其你切的分支和补丁基线差了好几个小版本。现象是报一串error: patch does not apply有时还带fuzz提示。解决方法是改用--reject强制应用git apply --reject patches/shared_textures_3497.patch该命令会尽量打上能打的部分并把失败部分写到.rej文件里。然后逐个打开.rej文件对照上下文手动合入。这种操作要求你对 Chromium 的 Overlay 合成流程有一定熟悉度如果只是做应用层开发不建议深入。还有一个更容易踩的坑别把补丁打到官方二进制的头文件目录上。有些人图省事把patches复制到cef_binary_.../下直接git apply结果提示「not a git repository」。因为二进制分发里没有.git补丁根本无处落地正确做法永远是「针对源码树」操作。最后说说什么情况不需要打补丁。如果你的 CEF 版本已经较新例如 4xxx 以后OnAcceleratedPaint是默认能力打旧补丁反而会把代码改坏。新版本会直接提供CefAcceleratedPaintInfo和shared_texture_enabled开关你只需要在工程里把开关打开。也就是说示例二进制跑得动就别动 patches真的需要自编 CEF 时再看 README 确认补丁顺序。6. 进阶技巧把透明 HUD 接进你自己的 D3D11 管线6.1 用 file:// 加载透明 HTML 界面演示包里hud.html和overlay.svg就是为 HUD 准备的。先把 HUD 页面加载到独立 browser 里CefWindowInfo hud_window_info; hud_window_info.SetAsWindowless(kNullWindowHandle, true); hud_window_info.shared_texture_enabled true; CefBrowserSettings hud_settings; CefRefPtrCefBrowser hud_browser CefBrowserHost::CreateBrowserSync( hud_window_info, hud_handler, // 复用同一个 RenderHandler file:///D:/demo/hud.html, // 透明背景的 HTML 页面 hud_settings, nullptr, nullptr);hud.html内部要把html和body的背景全部设成透明才能让上层叠加看不出网页底色。实时数据可以直接用 JavaScript 更新 DOM不必每帧重建页面CPU 占用会低很多。6.2 合成顺序与性能验证接入自有管线时把 HUD 层放到合成循环的最后一步并保持每层独立纹理。验证是否真的走通了共享纹理路径用QueryPerformanceCounter量一下从回调进入到CopyResource返回的时间LARGE_INTEGER freq, t0, t1; QueryPerformanceFrequency(freq); QueryPerformanceCounter(t0); // OnAcceleratedPaint 回调里做 OpenSharedResource CopyResource CopySharedTextureToBackBuffer(); QueryPerformanceCounter(t1); double copy_ms (t1.QuadPart - t0.QuadPart) * 1000.0 / freq.QuadPart; // 如果 copy_ms 长期大于 1ms说明可能退回 OnPaint 路径了。量的时候注意context-Flush()否则 D3D11 命令只是排队CPU 计时会偏短看起来很快但实际 GPU 还没干完。我习惯在复制结束后调用一次 Flush再取结束时间。我吃过一次亏升级 CEF 版本后发现shared_texture_enabled默认值变了整个 OSR 退回软渲染帧率从 60 掉到 15最后逐行查回调才发现是新版本把开关默认关了。从那以后我每次换 CEF 二进制第一件事就是跑一遍OnAcceleratedPaint回调计数确认走的还是新纹理路径再动业务代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价