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

Answer

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