优化C#窗口捕获代码以在Godot 4.4中用作纹理并解决帧率下降问题
优化C#窗口捕获代码以在Godot 4.4中用作纹理并解决帧率下降问题
看起来你已经搞定了窗口捕获的基础功能,但在Godot里跑起来帧率掉得有点离谱对吧?我来帮你拆解下问题,一步步优化这段代码,让它能跟上你的165Hz刷新率。
先揪出核心性能杀手
当前代码里有几个明显的问题,直接导致帧率雪崩:
- GDI资源泄漏:你创建了
hdcDest和hBitmap,但没调用DeleteDC和DeleteObject释放,每次调用都会残留系统资源,越跑越卡。 - 重复创建Bitmap:每捕获一次就生成新的
System.Drawing.Bitmap,频繁的对象创建销毁会触发GC频繁回收,拖慢整个程序。 - 无意义的重复操作:每次都调用
GetProcessesByName找进程、GetWindowRect拿尺寸,如果窗口没变化,这些重复计算完全是浪费。 - 主线程阻塞:如果在Godot的
_process里直接调用捕获函数,GDI操作会阻塞游戏主线程,直接拉低帧率。
分步优化代码
第一步:修复GDI资源泄漏
这是最紧急的,先把泄漏的资源补上,不然优化再多也白搭。在函数末尾加上这些释放代码:
// 原代码末尾 SelectObject(hdcDest, hOldBitmap); ReleaseDC(hWnd, hdcSrc); // 新增:释放DC和Bitmap对象 DeleteDC(hdcDest); DeleteObject(hBitmap); return bmp;
第二步:缓存复用,减少重复操作
把窗口句柄、窗口尺寸、Bitmap对象都缓存起来,只在必要时更新:
// 全局缓存变量,放在类里而不是函数内 private static IntPtr _cachedHWnd = IntPtr.Zero; private static System.Drawing.Bitmap _reusableBitmap; private static int _cachedWidth = 0; private static int _cachedHeight = 0; public static System.Drawing.Bitmap CaptureWindow(string processName) { // 缓存窗口句柄,仅在首次或窗口失效时重新查找 if (_cachedHWnd == IntPtr.Zero || !IsWindow(_cachedHWnd)) { Process[] processes = Process.GetProcessesByName(processName); if (processes.Length == 0) throw new InvalidOperationException($"找不到名为{processName}的进程"); _cachedHWnd = processes[0].MainWindowHandle; } RECT rect; GetWindowRect(_cachedHWnd, out rect); int width = rect.Right - rect.Left; int height = rect.Bottom - rect.Top; // 仅在窗口尺寸变化时重新创建Bitmap if (_reusableBitmap == null || _cachedWidth != width || _cachedHeight != height) { _reusableBitmap?.Dispose(); // 先释放旧的 _reusableBitmap = new System.Drawing.Bitmap(width, height); _cachedWidth = width; _cachedHeight = height; } // 剩下的GDI捕获逻辑 IntPtr hdcSrc = GetWindowDC(_cachedHWnd); IntPtr hdcDest = CreateCompatibleDC(hdcSrc); IntPtr hBitmap = _reusableBitmap.GetHbitmap(); // 复用已有的Bitmap的句柄 IntPtr hOldBitmap = SelectObject(hdcDest, hBitmap); // Win 10+ 使用PW_RENDERFULLCONTENT PrintWindow(_cachedHWnd, hdcDest, 2); // 直接复制像素到复用的Bitmap,避免新建对象 using (var g = System.Drawing.Graphics.FromImage(_reusableBitmap)) { IntPtr hdcBitmap = g.GetHdc(); BitBlt(hdcBitmap, 0, 0, width, height, hdcDest, 0, 0, 0x00CC0020); // SRCCOPY 直接复制像素 g.ReleaseHdc(hdcBitmap); } // 释放GDI资源(必须做!) SelectObject(hdcDest, hOldBitmap); DeleteObject(hBitmap); DeleteDC(hdcDest); ReleaseDC(_cachedHWnd, hdcSrc); return _reusableBitmap; }
第三步:把捕获移到后台线程,别阻塞Godot主线程
Godot的主线程负责渲染和输入,绝对不能在这里做GDI捕获这种耗时操作。把捕获逻辑放到C#异步线程里,然后用CallDeferred在主线程更新纹理:
// 你的Godot C#脚本里的代码 private Task _captureTask; private CancellationTokenSource _cts; private ImageTexture _windowTexture; private Image _windowImage; private readonly object _dataLock = new object(); // 线程安全锁,避免读写冲突 public override void _Ready() { // 初始化纹理和显示节点 _windowTexture = new ImageTexture(); var displayRect = GetNode<TextureRect>("WindowDisplay"); // 替换成你的TextureRect节点名 displayRect.Texture = _windowTexture; // 启动后台捕获线程 _cts = new CancellationTokenSource(); _captureTask = Task.Run(async () => { while (!_cts.Token.IsCancellationRequested) { try { var capturedBmp = CaptureWindow("你的进程名"); // 替换成目标进程名 // 把Bitmap转成Godot Image,直接复制像素数据 lock (_dataLock) { if (_windowImage == null || _windowImage.GetWidth() != capturedBmp.Width || _windowImage.GetHeight() != capturedBmp.Height) { _windowImage = Image.Create(capturedBmp.Width, capturedBmp.Height, false, Image.Format.Rgba8); } // 锁定Bitmap像素,直接复制到Godot Image,避免中间转换 var bmpData = capturedBmp.LockBits( new System.Drawing.Rectangle(0, 0, capturedBmp.Width, capturedBmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); _windowImage.SetData(bmpData.Scan0); capturedBmp.UnlockBits(bmpData); } // 让主线程更新纹理(必须用CallDeferred,不然会线程错误) CallDeferred(nameof(UpdateDisplayTexture)); } catch (Exception e) { GD.PrintErr($"捕获出错:{e.Message}"); } // 控制捕获频率,不用追165帧,60帧足够流畅了 await Task.Delay(16, _cts.Token); // 16ms≈60帧 } }, _cts.Token); } // 在主线程更新纹理 private void UpdateDisplayTexture() { lock (_dataLock) { if (_windowImage != null) { _windowTexture.SetImage(_windowImage); } } } // 退出时清理资源 public override void _ExitTree() { _cts?.Cancel(); _captureTask?.Wait(); _reusableBitmap?.Dispose(); _cts?.Dispose(); }
额外性能小技巧
- 降低捕获频率:除非你真的需要165帧的实时画面,60帧的捕获已经足够流畅,能省很多性能。
- 关闭窗口特效:Windows的毛玻璃、透明度等特效会增加
PrintWindow的捕获时间,临时关闭能提升速度。 - 只捕获需要的区域:如果不需要整个窗口,可以计算出目标区域,只捕获那部分,减少像素处理量。
- 尝试DirectX捕获:如果GDI还是不够快,可以试试用
IDXGIOutputDuplication(Direct3D桌面复制API),它比GDI快很多,适合高帧率捕获,但代码复杂度会高一些。
最后验证
优化完之后,先检查任务管理器的“详细信息”里,你的Godot进程的GDI对象数是不是稳定的(不会一直上涨),如果稳定了,说明资源泄漏修复了。然后看游戏帧率,应该能回到165Hz左右,即使开着捕获也不会掉太多。




