You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

优化C#窗口捕获代码以在Godot 4.4中用作纹理并解决帧率下降问题

优化C#窗口捕获代码以在Godot 4.4中用作纹理并解决帧率下降问题

看起来你已经搞定了窗口捕获的基础功能,但在Godot里跑起来帧率掉得有点离谱对吧?我来帮你拆解下问题,一步步优化这段代码,让它能跟上你的165Hz刷新率。

先揪出核心性能杀手

当前代码里有几个明显的问题,直接导致帧率雪崩:

  1. GDI资源泄漏:你创建了hdcDesthBitmap,但没调用DeleteDCDeleteObject释放,每次调用都会残留系统资源,越跑越卡。
  2. 重复创建Bitmap:每捕获一次就生成新的System.Drawing.Bitmap,频繁的对象创建销毁会触发GC频繁回收,拖慢整个程序。
  3. 无意义的重复操作:每次都调用GetProcessesByName找进程、GetWindowRect拿尺寸,如果窗口没变化,这些重复计算完全是浪费。
  4. 主线程阻塞:如果在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();
}

额外性能小技巧

  1. 降低捕获频率:除非你真的需要165帧的实时画面,60帧的捕获已经足够流畅,能省很多性能。
  2. 关闭窗口特效:Windows的毛玻璃、透明度等特效会增加PrintWindow的捕获时间,临时关闭能提升速度。
  3. 只捕获需要的区域:如果不需要整个窗口,可以计算出目标区域,只捕获那部分,减少像素处理量。
  4. 尝试DirectX捕获:如果GDI还是不够快,可以试试用IDXGIOutputDuplication(Direct3D桌面复制API),它比GDI快很多,适合高帧率捕获,但代码复杂度会高一些。

最后验证

优化完之后,先检查任务管理器的“详细信息”里,你的Godot进程的GDI对象数是不是稳定的(不会一直上涨),如果稳定了,说明资源泄漏修复了。然后看游戏帧率,应该能回到165Hz左右,即使开着捕获也不会掉太多。

火山引擎 最新活动