在 GDI 中设置像素的性能高效方式
Performance efficient way of setting pixels in GDI
我创建了一个基本程序,它使用 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);
}
}
}
}
理想情况下,您希望使用 BitBlt
并为每一帧在屏幕上绘制一次。
否则,您为每一帧执行多次绘制调用,并且绘制缓慢且闪烁。例如:
case WM_PAINT:
{
PAINTSTRUCT ps;
auto hdc = BeginPaint(hwnd, &ps);
for (...)
SetPixelV(hdc, ...) //<- slow with possible flicker
EndPaint(hwnd, &ps);
return 0;
}
主要问题不是 SetPixel
,而是我们为每一帧向显卡发出数千个绘图请求这一事实。
我们可以通过使用 "memory device context":
形式的缓冲区来解决这个问题
HDC hdesktop = GetDC(0);
memdc = CreateCompatibleDC(hdesktop);
hbitmap = CreateCompatibleBitmap(hdesktop, w, h);
SelectObject(memdc, hbitmap);
现在您可以在 memdc
上完成所有绘图。这些图纸会很快,因为它们不会发送到显卡。在 memdc
上完成绘图后,您 BitBlt
实际 hdc
上的 memdc
用于目标 window 设备上下文:
//draw on memdc instead of drawing on hdc:
...
//draw memdc on to hdc:
BitBlt(hdc, 0, 0, w, h, memdc, 0, 0, SRCCOPY);
实际上你很少需要 SetPixel
。通常你将位图加载到你的背景和精灵中,然后在 memdc
和 BitBlt
上绘制所有内容到 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 函数无法处理矩阵等。它们对透明度的支持有限,因此您可能想要使用不同的技术,例如 Direct2D,它与 GPU 的集成更好
我创建了一个基本程序,它使用 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);
}
}
}
}
理想情况下,您希望使用 BitBlt
并为每一帧在屏幕上绘制一次。
否则,您为每一帧执行多次绘制调用,并且绘制缓慢且闪烁。例如:
case WM_PAINT:
{
PAINTSTRUCT ps;
auto hdc = BeginPaint(hwnd, &ps);
for (...)
SetPixelV(hdc, ...) //<- slow with possible flicker
EndPaint(hwnd, &ps);
return 0;
}
主要问题不是 SetPixel
,而是我们为每一帧向显卡发出数千个绘图请求这一事实。
我们可以通过使用 "memory device context":
形式的缓冲区来解决这个问题HDC hdesktop = GetDC(0);
memdc = CreateCompatibleDC(hdesktop);
hbitmap = CreateCompatibleBitmap(hdesktop, w, h);
SelectObject(memdc, hbitmap);
现在您可以在 memdc
上完成所有绘图。这些图纸会很快,因为它们不会发送到显卡。在 memdc
上完成绘图后,您 BitBlt
实际 hdc
上的 memdc
用于目标 window 设备上下文:
//draw on memdc instead of drawing on hdc:
...
//draw memdc on to hdc:
BitBlt(hdc, 0, 0, w, h, memdc, 0, 0, SRCCOPY);
实际上你很少需要 SetPixel
。通常你将位图加载到你的背景和精灵中,然后在 memdc
和 BitBlt
上绘制所有内容到 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 函数无法处理矩阵等。它们对透明度的支持有限,因此您可能想要使用不同的技术,例如 Direct2D,它与 GPU 的集成更好