【问题标题】:Performance efficient way of setting pixels in GDI在 GDI 中设置像素的高效方法
【发布时间】:2019-05-26 06:58:36
【问题描述】:

我创建了一个使用SetPixel() 方法在Windows 控制台中渲染精灵的基本程序,它运行良好,但是开销很大。 我对此进行了一些优化,这有所帮助,但仍然太慢了。

目前我的程序使用COLORREF 的两个缓冲区将较新的缓冲区绘制到屏幕上,交换它们并重新开始。但是,如果所述像素已更改,它只会重绘一个像素。这极大地提高了性能,但仍然很慢。 缓冲区交换尚未使用指针完成,但真正的开销是 SetPixel() 所以,我正在寻找一种使用 GDI 创建像素级图形的替代方法,它比 SetPixel() 更快 (忽略 anim_frame 和 img_data 向量的第一维,如果我决定添加动画对象,它们就在那里)

void graphics_context::update_screen()
{
update_buffer();

for (int x = 0; x < this->width; x++)
{
    for (int y = 0; y < this->height; y++)
    {
        if (this->buffer.at(x).at(y) != this->buffer_past.at(x).at(y))
        {
            for (int i = 0; i < this->scale_factor; i++)
            {
                for (int j = 0; j < this->scale_factor; j++)
                {
                    int posX = i + (this->scale_factor  * x) + this->width_offset;
                    int posY = j + (this->scale_factor  * y) + this->height_offset;

                    SetPixel(this->target_dc, posX, posY, this->buffer.at(x).at(y));
                }
            }
        }
    }
}

buffer_past = buffer;
}

这就是update_buffer() 方法:

void graphics_context::update_buffer()
{
for (int x = 0; x < this->width; x++)
{
    for (int y = 0; y < this->height; y++)
    {
        buffer.at(x).at(y) = RGB(0, 0, 0);
    }
}

//this->layers.at(1)->sprite; <- pointer to member gfx_obj pointer

for (int i = 0; i < this->layers.size(); i++)
{
    gfx_object tmp_gfx = *this->layers.at(i)->sprite;

    for (int x = 0; x < tmp_gfx.img_data.at(0).size(); x++)
    {
        for (int y = 0; y < tmp_gfx.img_data.at(tmp_gfx.anim_frame).at(0).size(); y++)
        {
            if(tmp_gfx.img_data.at(tmp_gfx.anim_frame).at(x).at(y) != RGB(0,255,0))
            buffer.at(x + this->layers.at(i)->locX).at(y + this->layers.at(i)->locY) = tmp_gfx.img_data.at(tmp_gfx.anim_frame).at(x).at(y);
        }
    }
}
}

【问题讨论】:

  • SetPixel 是出了名的慢,部分原因是它必须在每次调用时发现要写入的图像的属性。使用LockBits 并直接对位图字节进行操作要快得多,如this question,但这也更复杂,因为位图格式可能不同。
  • 如果target_dc 是一个窗口设备上下文,那么你正在做很多GDI 调用,它会很慢。如果target_dc 是内存设备上下文,那么SetPixel 相当快。使用SetPixelV,它不会返回现有颜色并且速度会更快一些。无论哪种方式都是错误的。最后你想使用BitBlt 来绘制图像。您也可以使用BitBlt 来绘制精灵。
  • .at 也不快,但这可能是次要的
  • @BarmakShemirani 所以我必须改用位图,才能获得相当有效的绘图?而target_dc 是一个窗口设备上下文
  • @MSalters 是 [] 运算符更快吗?还是我应该用一些指针魔法直接访问内存?

标签: c++ gdi


【解决方案1】:

理想情况下,您希望使用BitBlt 并为每一帧在屏幕上绘制一次。

否则,您会为每一帧执行多次绘制调用,并且绘制速度很慢且闪烁。例如:

case WM_PAINT: 
{
    PAINTSTRUCT ps;
    auto hdc = BeginPaint(hwnd, &ps);

    for (...)
        SetPixelV(hdc, ...) //<- slow with possible flicker

    EndPaint(hwnd, &ps);
    return 0;
}

主要问题不是SetPixel,而是我们为每一帧向显卡发出数千个绘图请求这一事实。

我们可以通过使用“内存设备上下文”形式的缓冲区来解决这个问题:

HDC hdesktop = GetDC(0);
memdc = CreateCompatibleDC(hdesktop);
hbitmap = CreateCompatibleBitmap(hdesktop, w, h);
SelectObject(memdc, hbitmap);

现在您可以在memdc 上完成所有绘图。这些绘图会很快,因为它们不会发送到图形卡。在 memdc 上完成绘图后,您将 BitBlt memdc 上的实际 hdc 用于目标窗口设备上下文:

//draw on memdc instead of drawing on hdc:
...

//draw memdc on to hdc:
BitBlt(hdc, 0, 0, w, h, memdc, 0, 0, SRCCOPY);

实际上你很少需要SetPixel。通常,您将位图加载到背景和精灵中,然后在memdcBitBlt 上绘制所有内容到hdc

在 Windows Vista 及更高版本中,您可以使用BeginBufferedPaint 例程,这可能更方便一些。示例:

#ifndef UNICODE
#define UNICODE
#endif
#include <Windows.h>

class memory_dc
{
    HDC hdc;
    HBITMAP hbitmap;
    HBITMAP holdbitmap;
public:
    int w, h;

    memory_dc()
    {
        hdc = NULL;
        hbitmap = NULL;
    }

    ~memory_dc()
    {
        cleanup();
    }

    void cleanup()
    {
        if(hdc)
        {
            SelectObject(hdc, holdbitmap);
            DeleteObject(hbitmap);
            DeleteDC(hdc);
        }
    }

    void resize(int width, int height)
    {
        cleanup();
        w = width;
        h = height;
        HDC hdesktop = GetDC(0);
        hdc = CreateCompatibleDC(hdesktop);
        hbitmap = CreateCompatibleBitmap(hdesktop, w, h);
        holdbitmap = (HBITMAP)SelectObject(hdc, hbitmap);
        ReleaseDC(0, hdc);
    }

    //handy operator to return HDC
    operator HDC() { return hdc; }
};

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    static memory_dc buffer;
    static memory_dc sprite;
    static memory_dc background;

    switch(msg)
    {
    case WM_CREATE:
    {
        RECT rc;
        GetClientRect(hwnd, &rc);

        buffer.resize(rc.right, rc.bottom);
        background.resize(rc.right, rc.bottom);
        sprite.resize(20, 20);

        //draw the background
        rc = RECT{ 0, 0, sprite.w, sprite.h };
        FillRect(sprite, &rc, (HBRUSH)GetStockObject(GRAY_BRUSH));

        //draw the sprite
        rc = RECT{ 0, 0, background.w, background.h };
        FillRect(background, &rc, (HBRUSH)GetStockObject(WHITE_BRUSH));

        return 0;
    }

    case WM_PAINT: 
    {
        PAINTSTRUCT ps;
        auto hdc = BeginPaint(hwnd, &ps);

        //draw the background on to buffer
        BitBlt(buffer, 0, 0, background.w, background.w, background, 0, 0, SRCCOPY);

        //draw the sprite on top, at some location
        //or use TransparentBlt...
        POINT pt;
        GetCursorPos(&pt);
        ScreenToClient(hwnd, &pt);
        BitBlt(buffer, pt.x, pt.y, sprite.w, sprite.h, sprite, 0, 0, SRCCOPY);

        //draw the buffer on to HDC
        BitBlt(hdc, 0, 0, buffer.w, buffer.w, buffer, 0, 0, SRCCOPY);

        EndPaint(hwnd, &ps);
        return 0;
    }

    case WM_MOUSEMOVE:
        InvalidateRect(hwnd, NULL, FALSE);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, msg, wparam, lparam);
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
    wcex.lpfnWndProc = WndProc;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszClassName = L"classname";
    RegisterClassEx(&wcex);

    CreateWindow(wcex.lpszClassName, L"Test", WS_VISIBLE | WS_OVERLAPPEDWINDOW, 
        0, 0, 600, 400, 0, 0, hInstance, 0);

    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

注意,这对于简单的绘图来说已经足够了。但 GDI 函数无法处理矩阵等。它们对透明度的支持有限,因此您可能希望使用与 GPU 更好集成的 Direct2D 等不同技术

【讨论】:

  • 这就像一个魅力,我从中得到了一些合理的结果,现在我知道我的代码在其他地方仍然有很多开销,仍然需要修复'
  • 太棒了。我更新了memory_dc 类,因此它可以自动清理 GDI 对象
猜你喜欢
  • 1970-01-01
  • 2011-04-22
  • 2018-12-13
  • 1970-01-01
  • 2023-03-28
  • 2018-01-08
  • 1970-01-01
  • 1970-01-01
  • 2013-12-08
相关资源
最近更新 更多