fastest way to copy array to wxBitmap

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
PB
Part Of The Furniture
Part Of The Furniture
Posts: 2387
Joined: Sun Jan 03, 2010 5:45 pm

Re: fastest way to copy array to wxBitmap

Post by PB » Fri May 22, 2020 2:57 pm

doublemax, as usual, you are correct. Assuming all bitmaps have the same size (e.g., are coming from a video capture), not reusing the wxBitmap was my very stupid mistake.

When I create the wxBitmap only once (in the TimeConversion()), instead of creating it during each and every RGB to wxBitmap conversion, the absolute as well as MSW vs portable times drastically change (see here for the results running the ::SetDIBBits() using code with same parameters but without reusing the wxBitmap):

ConvertRGBTowxBitmapMSW1() uses ::SetDIBits()
ConvertRGBTowxBitmapMSW2() uses mempcy() for each row
reused-bitmap.png
reused-bitmap.png (9.01 KiB) Viewed 69 times

Code: Select all

#include <wx/wx.h>
#include <wx/rawbmp.h>
#include <wx/stopwatch.h>

struct RGBData
{
    size_t width{0}, height{0};
    size_t bytesPerPixel{3};
    wxByte* bits{nullptr};
};

bool ConvertRGBTowxBitmapPortable(const RGBData& data, wxBitmap& bitmap)
{
    wxCHECK(data.bytesPerPixel * 8 == 24, false);

    const wxByte* currentRGB = data.bits;
    wxNativePixelData pixelData(bitmap);

    if ( !pixelData )
        return false;

    for ( int row = 0; row < pixelData.GetHeight(); ++row )
    {
        wxNativePixelData::Iterator iterator(pixelData);

        iterator.MoveTo(pixelData, 0, row);
        for ( int col = 0; col < pixelData.GetWidth(); ++col, ++iterator )
        {
            iterator.Red()   = *currentRGB++;
            iterator.Green() = *currentRGB++;
            iterator.Blue()  = *currentRGB++;
        }
    }

    return bitmap.IsOk();
}

#ifndef __WXMSW__
    #define ConvertRGBTowxBitmapMSW ConvertRGBTowxBitmapPortable
#else

// data must be stored as BGR, not RGB!
bool ConvertRGBTowxBitmapMSW1(const RGBData& data, wxBitmap& bitmap)
{
    if ( (data.width * data.bytesPerPixel) % 4 != 0 )
        return ConvertRGBTowxBitmapPortable(data, bitmap);

    wxCHECK(bitmap.IsOk() && bitmap.IsDIB(), false);
    wxCHECK(data.bytesPerPixel * 8 == 24 && bitmap.GetDepth() == 24, false);

    HDC hScreenDC = ::GetDC(nullptr);
    int oldICCMMode = 0;
    BITMAPINFO bitmapInfo{0};
    bool success;

    bitmapInfo.bmiHeader.biSize          = sizeof(BITMAPINFO) - sizeof(RGBQUAD);
    bitmapInfo.bmiHeader.biWidth         = bitmap.GetWidth();
    bitmapInfo.bmiHeader.biHeight        = 0 - bitmap.GetHeight();
    bitmapInfo.bmiHeader.biPlanes        = 1;
    bitmapInfo.bmiHeader.biBitCount      = 24;
    bitmapInfo.bmiHeader.biCompression   = BI_RGB;

    oldICCMMode = ::SetICMMode(hScreenDC, ICM_OFF);
    success = ::SetDIBits(hScreenDC, bitmap.GetHBITMAP(), 0, bitmap.GetHeight(),
                    data.bits, &bitmapInfo, DIB_RGB_COLORS) != 0;

    if ( oldICCMMode != 0 )
        ::SetICMMode(hScreenDC, oldICCMMode);
    ::ReleaseDC(nullptr, hScreenDC);

    if ( !success )
    {
        return false;
    }

    return true;
}

// data must be stored as BGR, not RGB!
bool ConvertRGBTowxBitmapMSW2(const RGBData& data, wxBitmap& bitmap)
{
    if ( (data.width * data.bytesPerPixel) % 4 != 0 )
        return ConvertRGBTowxBitmapPortable(data, bitmap);

    wxCHECK(bitmap.IsOk() && bitmap.IsDIB(), false);
    wxCHECK(data.bytesPerPixel * 8 == 24 && bitmap.GetDepth() == 24, false);

    BITMAP bmp{0};

    if ( !::GetObject(bitmap.GetHBITMAP(), sizeof(bmp), &bmp) )
        return false;

    wxCHECK(bmp.bmWidth == bitmap.GetWidth() && bmp.bmHeight == bitmap.GetHeight()
            && bmp.bmBitsPixel == 24 && bmp.bmPlanes == 1, false );

    // we cannot just copy the whole block because the DIB
    // wouble be bottom-to-top
    const size_t rowSize = data.width * 3;
    wxByte* dest = static_cast<wxByte*>(bmp.bmBits) + ((data.height - 1) * rowSize);
    wxByte* src = data.bits;

    for ( size_t i = 0; i < data.height; ++i )
    {
        memcpy(dest, src, rowSize);
        dest -= rowSize;
        src  += rowSize;
    }

    return true;
}

#endif // #ifndef __WXMSW__

typedef bool (*RGB2wxBitmapFn)(const RGBData&, wxBitmap&);

bool TimeConversion(RGB2wxBitmapFn conversionFunction,
                    const RGBData& data, size_t runCount,
                    long& totalTimeInMs)
{
    wxStopWatch stopWatch;
    wxBitmap bitmap(data.width, data.height, 24);

    for ( size_t i = 0; i < runCount; ++i )
    {
        if ( !conversionFunction(data, bitmap) )
            return false;
    }

    totalTimeInMs = stopWatch.Time();
    return true;
}

class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        const size_t runCount      = 100;
        const size_t imageWidth    = 4000;
        const size_t imageHeight   = 4000;
        const size_t bytesPerPixel = 3; // no alpha
        const size_t RGBDataSize = imageWidth * imageHeight * bytesPerPixel;

        RGBData rgbData;
        bool portableSuccess, MSW1Success, MSW2Success;
        long totalPortableTime = 0, totalMSW1Time = 0, totalMSW2Time = 0;

        rgbData.width = imageWidth; rgbData.height = imageHeight;
        rgbData.bytesPerPixel = bytesPerPixel;
        rgbData.bits = new wxByte[RGBDataSize];
        memset(rgbData.bits, 255, RGBDataSize);

        portableSuccess = TimeConversion(ConvertRGBTowxBitmapPortable, rgbData, runCount, totalPortableTime);
        MSW1Success = TimeConversion(ConvertRGBTowxBitmapMSW1, rgbData, runCount, totalMSW1Time);
        MSW2Success = TimeConversion(ConvertRGBTowxBitmapMSW2, rgbData, runCount, totalMSW2Time);

        if ( portableSuccess && MSW1Success )
        {
            long avgPortableTime = totalPortableTime / runCount;
            long avgMSW1Time = totalMSW1Time / runCount;
            long avgMSW2Time = totalMSW2Time / runCount;

            wxLogMessage("Run count: %zu (image size: %zu x %zu)\n"
                         "ConvertRGBTowxBitmapPortable(): %ld ms total, ~%ld ms per run\n"
                         "ConvertRGBTowxBitmapMSW1(): %ld ms total, ~%ld ms per run\n"
                         "ConvertRGBTowxBitmapMSW2(): %ld ms total, ~%ld ms per run\n"
                         "Total MSW1 and MSW2 / Portable: %.0f%% and %.0f%%",
                         runCount, imageWidth, imageHeight,
                         totalPortableTime, avgPortableTime,
                         totalMSW1Time, avgMSW1Time, totalMSW2Time, avgMSW2Time,
                         ((double)totalMSW1Time / totalPortableTime) * 100,
                         ((double)totalMSW2Time / totalPortableTime) * 100);
        }
        else
            wxLogError("Conversion failed!");

        delete[] rgbData.bits;

        return false;
    }
}; wxIMPLEMENT_APP(MyApp);
However, I am not sure if the absolute times are not distorted by the benchmark always using the same RGB data, which can be kept in the CPU cache, unlike when working with different source frames in a real-world scenario...

Post Reply