Kivi
Kivi

Reputation: 97

Performance efficient way of setting pixels in GDI

I've created a basic program that renders sprites in a windows console using the SetPixel() method, and it works fine, but there is massive overhead. I've made some optimizations to this, which helped but it's still too slow.

Currently my program uses two buffers of COLORREF draws the newer to the screen, swaps them and starts over. However it only redraws a pixel if said pixel has changed. This improved performance massively but it's still slow. Buffer swapping isn't done with pointers as of yet, but the real overhead is SetPixel() so, i'm looking for an alternative way of creating pixel level graphics with GDI, which is faster than SetPixel() (ignore anim_frame and the first dimension of the img_data vector, they are just there for the future if i decide to add animated objects)

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;
}

And this is the update_buffer() method:

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);
        }
    }
}
}

Upvotes: 8

Views: 4563

Answers (2)

Andrew Lim
Andrew Lim

Reputation: 358

If you have full control over the pixels, use SetDIBitsToDevice or StretchDIBits. You don't even need to create a memory device context.

Here is a 16x16 bitmap manually defined and displayed with StretchDIBits

StretchDIBits

#include <windows.h>
#include <cstdint>
const int CLIENT_WIDTH = 320;
const int CLIENT_HEIGHT = 240;
const int IMAGE_WIDTH = 16;
const int IMAGE_HEIGHT = 16;
const uint32_t bg = 0xffd800;
const uint32_t imageData [16*16] = {
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg,  0,  0,  0, bg, bg, bg, bg,  0,  0,  0, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg,  0,  0, bg, bg, bg, bg,  0,  0, bg, bg, bg, bg,
bg, bg, bg, bg,  0,  0, bg, bg, bg, bg,  0,  0, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg,  0,  0,  0,  0,  0,  0,  0,  0, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg, bg,
};

void drawImage(HDC hdc)
{
  BITMAPINFOHEADER bmih = {0};
  bmih.biSize     = sizeof(BITMAPINFOHEADER);
  bmih.biWidth    = IMAGE_WIDTH;
  bmih.biHeight   = -IMAGE_HEIGHT;
  bmih.biPlanes   = 1;
  bmih.biBitCount = 32;
  bmih.biCompression  = BI_RGB ;
  bmih.biSizeImage    = 0;
  bmih.biXPelsPerMeter    =   10;
  bmih.biYPelsPerMeter    =   10;

  BITMAPINFO dbmi = {0};
  dbmi.bmiHeader = bmih;

  // Draw pixels without stretching
//  SetDIBitsToDevice(hdc, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT,
//                    0, 0, 0, IMAGE_HEIGHT, imageData, &dbmi, 0 );

  StretchDIBits(hdc, 0, 0, CLIENT_WIDTH, CLIENT_HEIGHT,
                0, 0, IMAGE_WIDTH, IMAGE_HEIGHT,
                imageData, &dbmi, 0, SRCCOPY);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) {
  switch(Message) {
    case WM_PAINT: {
      PAINTSTRUCT ps ;
      HDC hdc = BeginPaint(hwnd, &ps);
      drawImage(hdc);
      EndPaint(hwnd, &ps) ;
      break;
    }
    case WM_DESTROY: { PostQuitMessage(0); break; }
    default: return DefWindowProc(hwnd, Message, wParam, lParam);
  }
  return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow) {
  WNDCLASSEX wc = {0};
  HWND hwnd;
  MSG msg;
  wc.cbSize    = sizeof(WNDCLASSEX);
  wc.lpfnWndProc = WndProc;
  wc.hInstance = hInstance;
  wc.lpszClassName = "GDIPixelsClass";
  wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
  RegisterClassEx(&wc);
  hwnd = CreateWindowEx(WS_EX_CLIENTEDGE,wc.lpszClassName,"StretchDIBits",
                        WS_VISIBLE|WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT, 480, 320,
                        NULL,NULL,hInstance,NULL);
  DWORD dwStyle = (DWORD)GetWindowLongPtr( hwnd, GWL_STYLE ) ;
  DWORD dwExStyle = (DWORD)GetWindowLongPtr( hwnd, GWL_EXSTYLE ) ;
  HMENU menu = GetMenu( hwnd ) ;
  RECT rc = { 0, 0, CLIENT_WIDTH, CLIENT_HEIGHT } ;
  AdjustWindowRectEx( &rc, dwStyle, menu ? TRUE : FALSE, dwExStyle );
  SetWindowPos( hwnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top,
                SWP_NOZORDER | SWP_NOMOVE ) ;
  while(GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

Upvotes: 3

Barmak Shemirani
Barmak Shemirani

Reputation: 31599

Ideally you want to use BitBlt and draw on the screen once for each frame.

Otherwise you do multiple paint calls for each frame, and drawing is slow with flicker. For example:

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

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

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

The main problem is not SetPixel, but the fact that we are making thousands of drawing requests to the graphics card, for each frame.

We can solve this by using a buffer in the form of "memory device context":

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

Now you can do all of your drawings on memdc. These drawings will be fast because they are not sent to the graphics card. Once you are finished drawing on memdc, you BitBlt the memdc on the actual hdc for target window device context:

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

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

In practice you rarely need SetPixel. Usually you load a bitmap in to your background and sprite(s), and then draw everything on memdc, and BitBlt to hdc.

In Windows Vista and above you can use BeginBufferedPaint routine which might be a little more convenient. Example:

#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;
}

Note, this will be good enough for simple drawings. But GDI functions can't handle matrices etc. they have limited transparency support, so you might want to use a different technology like Direct2D which has better integration with the GPU

Upvotes: 9

Related Questions