0
votes

I've looked at multiple responses to similar questions from both this site and others, and while I feel like I've gotten closer, I just can't quite get it right. Still, this is probably a super-noobish question.


So I used to only call the WndProc case "WM_Paint" (via InvalidateRect) one every few minutes, so I didn't really notice the leak. Now I've added something that calls it about 5 times a second. In that second my memory usage jumps about 3800k. Yeah, that got noticed... Here's the code:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        // Other cases omitted since we skip them due to "case WM_Paint:".
        case WM_PAINT:
            wndProc_Paint(hwnd);
        break;
        // Other cases omitted since we skip them due to "break;".
    }
    return 0;
}

void wndProc_Paint(HWND hwnd)
{
    g_hbmBoard = ConvertIplImageToHBITMAP(targetBoardImg); //OpenCV command
    BITMAP bm;
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps); // <- Breakpoint here while monitoring mem usage shows this is what is adding ~772k per call which never gets released.
    HDC hdcMem = CreateCompatibleDC(hdc);

    HBITMAP hbmOld = (HBITMAP)SelectObject(hdcMem, g_hbmBoard);
    GetObject(g_hbmBoard, sizeof(bm), &bm);
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    hbmOld = (HBITMAP)SelectObject(hdcMem, g_hbmGreenLight);
    GetObject(g_hbmGreenLight, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 59, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    hbmOld = (HBITMAP)SelectObject(hdcMem, g_hbmWorkingLight);
    GetObject(g_hbmWorkingLight, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 94, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    hbmOld = (HBITMAP)SelectObject(hdcMem, g_hbmWorkingIndicator);
    GetObject(g_hbmWorkingIndicator, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 129, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    //DeleteObject(hbmOld);
    SelectObject(hdcMem, hbmOld);
    DeleteDC(hdcMem);
    //SelectObject(hdc, hbmOld);
    //ReleaseDC(hwnd, hdc);
    //DeleteDC(hdc);
    //DeleteObject(hdc);

    EndPaint(hwnd, &ps);
}

With the exception of the g_hbmWorkingIndicator segment and the commented out parts, this is what my WM_Paint looked like before it got called 5times/sec (even before I put it into it's own function - which was a separate issue).

The line "HDC hdc = BeginPaint(hwnd, &ps);" is where the memory gets added, and it never gets released. This happens every time we go through the function. So now for the stuff I tried in fixing the issue.

Reading up on similar issues, I assumed that I needed to Release or Delete the DC of hdc. I wasn't (still aren't - yes I've read the MSDN page) sure how SelectObject works with these, so I've tried it with and without ReleaseDC & DeleteDC (&both) for both hdc and hdcMem. I also tried DeleteObject for hdc & hdcMem. None of those had any affect.

DeleteObject(hbmOld) had an effect. It solved the issue. Except it's the wrong solution, because although I don't have the memory climbing out of control, I do have some of the visuals getting replaced with the wrong visuals. g_hbmGreenLight usually gets g_hbmBoard's graphic while g_hbmWorkingLight turns green (g_hbmGreenLight's graphic). Moving "DeleteObject(hbmOld);" to after EndPaint(~) or trying to use a "SelectObject" just changes which objects get replaced by which wrong graphic - also; returns the memory leak.


EDIT: For completeness's sake, I have included the code for ConvertIplImageToHBITMAP(IplImage* image) here. It is entirely possible that this is the culprit.

HBITMAP ConvertIplImageToHBITMAP(IplImage* pImage)
{
    IplImage* image = (IplImage*)pImage;
    bool imgConverted = false;

    if(pImage->nChannels != 3)
    {
        IplImage* imageCh3 = cvCreateImage(cvGetSize(pImage), 8, 3);
        if(pImage->nChannels==1){cvCvtColor(pImage, imageCh3, CV_GRAY2RGB);}
        image = imageCh3;
        imgConverted = true;
    }

    int bpp = image->nChannels * 8;
    assert(image->width >= 0 && image->height >= 0 && (bpp == 8 || bpp == 24 || bpp == 32));
    CvMat dst;
    void* dst_ptr = 0;
    HBITMAP hbmp = NULL;
    unsigned char buffer[sizeof(BITMAPINFO) + 255*sizeof(RGBQUAD)];
    BITMAPINFO* bmi = (BITMAPINFO*)buffer;
    BITMAPINFOHEADER* bmih = &(bmi->bmiHeader);

    ZeroMemory(bmih, sizeof(BITMAPINFOHEADER));
    bmih->biSize = sizeof(BITMAPINFOHEADER);
    bmih->biWidth = image->width;
    bmih->biHeight = image->origin ? abs(image->height) : -abs(image->height);
    bmih->biPlanes = 1;
    bmih->biBitCount = bpp;
    bmih->biCompression = BI_RGB;

    if (bpp == 8)
    {
        RGBQUAD* palette = bmi->bmiColors;
        int i;
        for (i = 0; i < 256; i++)
        {
            palette[i].rgbRed = palette[i].rgbGreen = palette[i].rgbBlue = (BYTE)i;
            palette[i].rgbReserved = 0;
        }
    }

    hbmp = CreateDIBSection(NULL, bmi, DIB_RGB_COLORS, &dst_ptr, 0, 0);
    cvInitMatHeader(&dst, image->height, image->width, CV_8UC3, dst_ptr, (image->width * image->nChannels + 3) & -4);
    cvConvertImage(image, &dst, image->origin ? CV_CVTIMG_FLIP : 0);

    if(imgConverted)
    {cvReleaseImage(&image);}

    return hbmp;
}

So, all that said: Help me StackExchange, you're my only hope! ;_;

1
If anything, I would look at the repeated invoke of g_hbmBoard = ... with each WM_PAINT message. The rest of your setup for your paint struct, in-memory dc, and other GDI stuff at-least-initially looks reasonable (though I admit I didn't really look closely beyond cursory examination). You select the old hbm correctly, and "delete" what you "created", which is dc-correct.WhozCraig
Why haven't you cut this down to the bare minimum and made a complete program that we can compile and run?David Heffernan
Remy's answer is stellar and deserves final-selection. Unrelated, if you feel up to the challenge you may consider utilizing the invalid region coords from the PAINTSTRUCT you initialize to refine the areas you''re blt-ing, or even avoid them entirely if the update-region in question is not within the blt rectangle for any specific given operation. That, of course, assumes you are properly invalidating only the region(s) that need updating in the first place by code not-seen here. Otherwise there is no real win. Best of luck.WhozCraig
@WhozCraig I am invalidating only the regions that need updating in code-not-seen-here, so I'll certainly look into that. That said; despite Remy's superb answer the memory leak remains out of control. I want to understand and fix the error or asking the question in the first place doesn't actually serve a purpose.Alexander

1 Answers

9
votes

You are losing the original HBITMAP that is returned by SelectObject(hdcMem, g_hbmBoard), so you are not restoring it correctly before calling DeleteDC(), and thus it gets leaked. You are overwriting the hbmOld variable every time you call SelectObject(), but you are not restoring the current hbmOld value back into dcMem before overwriting hbmOld again. When using SelectObject(), you MUST restore the original object when you are done making changes to the HDC.

Try this instead:

void wndProc_Paint(HWND hwnd)
{
    g_hbmBoard = ConvertIplImageToHBITMAP(targetBoardImg); //OpenCV command

    BITMAP bm;
    PAINTSTRUCT ps;

    HDC hdc = BeginPaint(hwnd, &ps);
    HDC hdcMem = CreateCompatibleDC(hdc);

    HBITMAP hbmOld = (HBITMAP) SelectObject(hdcMem, g_hbmBoard); // save the original HBITMAP
    GetObject(g_hbmBoard, sizeof(bm), &bm);
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, g_hbmGreenLight); // returns g_hbmBoard, no need to save it to hbmpOld
    GetObject(g_hbmGreenLight, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 59, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, g_hbmWorkingLight); // returns g_hbmGreenLight, no need to save it to hbmpOld
    GetObject(g_hbmWorkingLight, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 94, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, g_hbmWorkingIndicator); // returns g_hbmWorkingLight, no need to save it to hbmpOld
    GetObject(g_hbmWorkingIndicator, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 129, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, hbmOld); // restore the original HBITMAP

    DeleteDC(hdcMem);
    EndPaint(hwnd, &ps);

    // who owns g_hbmBoard? ConvertIplImageToHBITMAP() creates
    // a new HBITMAP, so you need to free it with DeleteObject()
    // before calling ConvertIplImageToHBITMAP() again.  It would
    // be better to create g_hbmBoard one time outside of WM_PAINT,
    // recreate g_hbmBoard only when the source image actually changes,
    // and then re-use g_hbmBoard as-is inside of WM_PAINT.
    // ConvertIplImageToHBITMAP() really does not belong in WM_PAINT...
    //
    //DeleteObject(g_hbmBoard);
}

Alternatively, use SaveDC()/RestoreDC() instead:

void wndProc_Paint(HWND hwnd)
{
    g_hbmBoard = ConvertIplImageToHBITMAP(targetBoardImg); //OpenCV command

    BITMAP bm;
    PAINTSTRUCT ps;

    HDC hdc = BeginPaint(hwnd, &ps);
    HDC hdcMem = CreateCompatibleDC(hdc);

    int iOldState = SaveDC(hdcMem); // save everything the HDC currently has selected

    SelectObject(hdcMem, g_hbmBoard);
    GetObject(g_hbmBoard, sizeof(bm), &bm);
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, g_hbmGreenLight);
    GetObject(g_hbmGreenLight, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 59, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, g_hbmWorkingLight);
    GetObject(g_hbmWorkingLight, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 94, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    SelectObject(hdcMem, g_hbmWorkingIndicator);
    GetObject(g_hbmWorkingIndicator, sizeof(bm), &bm);
    BitBlt(hdc, screenResW - 129, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

    RestoreDC(hdcMem, iOldState); // restore everything the HDC originally had selected

    DeleteDC(hdcMem);
    EndPaint(hwnd, &ps);

    // who owns g_hbmBoard? ConvertIplImageToHBITMAP() creates
    // a new HBITMAP, so you need to free it with DeleteObject()
    // before calling ConvertIplImageToHBITMAP() again.  It would
    // be better to create g_hbmBoard one time outside of WM_PAINT,
    // recreate g_hbmBoard only when the source image actually changes,
    // and then re-use g_hbmBoard as-is inside of WM_PAINT...
    // ConvertIplImageToHBITMAP() really does not belong in WM_PAINT...
    //
    //DeleteObject(g_hbmBoard);
}