Creating a window just to get its framebuffer?

177 views Asked by At

I've been writing this small ray-tracing engine in C/C++, and so far I've got basic functionality, where the finished image gets written to an image. However, I want to be able to create a window and get low-level access to its framebuffer. Is there a way I can do this in Windows without using any external library such as SDL3 or MiniFB?

1

There are 1 answers

0
Vertexwahn On BEST ANSWER

On Windows you can use WinAPI to create a Window.

Here is an example of a Window with a button:

//-----------------------------------------------------------------------------
// Defines

#define WIN32_LEAN_AND_MEAN // no MFC

//-----------------------------------------------------------------------------
// Header

#include <windows.h>
#include <TCHAR.h>

//-----------------------------------------------------------------------------
// Globals
HWND g_hwndButton = 0;

//-----------------------------------------------------------------------------
// Event Handler
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    switch(msg)
    {
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;


        case WM_COMMAND:
            {
                if (HIWORD(wparam) == BN_CLICKED &&
                    (HWND) lparam == g_hwndButton)
                {
                    DestroyWindow(hwnd);
                }
                return 0;
            }
            

        default: break;
    }

    return (DefWindowProc(hwnd, msg, wparam, lparam));
}

//-----------------------------------------------------------------------------
// Main
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdline, int nCmdShow)
{
    WNDCLASSEX winclass;
    winclass.cbSize = sizeof(WNDCLASSEX);
    winclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLKS;
    winclass.lpfnWndProc = WindowProc;
    winclass.cbClsExtra = 0; // extra class info space
    winclass.cbWndExtra = 0; // extra window info space
    winclass.hInstance = hInstance; // assign the application instance
    winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    winclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    winclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    winclass.lpszMenuName = NULL; // the name of the menu to attach
    winclass.lpszClassName = __T("WINCLASS1"); // the name of the class itself
    winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    RegisterClassEx(&winclass);
    HWND hwnd;
    hwnd = CreateWindowEx(  NULL,
                            __T("WINCLASS1"),
                            __T("Window Title"),
                            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                            0,
                            0,
                            200,
                            200,
                            NULL, // handle to parent
                            NULL, // handle to menu
                            hInstance, // instance of this application
                            NULL);

    if(hwnd==NULL)
        return -10;

    g_hwndButton = CreateWindow(__T("BUTTON"), __T("My Button"), WS_CHILD |
    WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 100, 20, hwnd, NULL, hInstance,
    NULL);

    //UpdateWindow();

    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0))
    {
        // translate any accelerator keys
        TranslateMessage(&msg);
        // send the message to the window proc
        DispatchMessage(&msg);
    }
    return 0;
}

Instead of a button you need a kind of canvas where you can draw. One way is to use WinAPI + GDI for this.

UINT                    *BitmapBytes;
BITMAPINFO              BitmapInfo;
BITMAPINFOHEADER        BitmapInfoHeader;
HDC                     BitmapHDC;
HBITMAP                 BitmapHandle;

BitmapInfoHeader.biSize = sizeof(BitmapInfo);
BitmapInfoHeader.biWidth = width;
BitmapInfoHeader.biHeight = height;
BitmapInfoHeader.biCompression = BI_RGB;
BitmapInfoHeader.biBitCount = 32;
BitmapInfoHeader.biPlanes = 1;
BitmapInfoHeader.biSizeImage = 0;
BitmapInfoHeader.biClrImportant = 0;
BitmapInfoHeader.biClrUsed = 0;
BitmapInfoHeader.biSizeImage = 0;
BitmapInfoHeader.biXPelsPerMeter = 0;
BitmapInfoHeader.biYPelsPerMeter = 0;

BitmapInfo.bmiHeader = BitmapInfoHeader;

BitmapHDC    = CreateCompatibleDC(hdc); 
    
BitmapHandle = CreateDIBSection(hdc, &BitmapInfo, 
                                0, (void**)&BitmapBytes, NULL, 0);

SelectObject(BitmapHDC, BitmapHandle);

Your bitmap class could look like this:

class BitmapDIBUncompressed32Bit
{
public:
    BitmapDIBUncompressed32Bit(int width, int height);
    BitmapDIBUncompressed32Bit(HDC hdc, int width, int height);
    int getWidth() const;
    int getHeight() const;

    virtual ~BitmapDIBUncompressed32Bit();

    void Release();
    
    HDC getHDC();

    UINT * getBytes() const;

    operator UINT * () { return BitmapBytes; };

private:
    UINT                    *BitmapBytes;
    BITMAPINFO              BitmapInfo;
    BITMAPINFOHEADER        BitmapInfoHeader;
    HDC                     BitmapHDC;
    HBITMAP                 BitmapHandle;
};

Your canvas can then make use of this bitmap. A canvas class could look like this:

class Canvas : public Component
{
public:
    Canvas();
    
    void setPixel(int x, int y, int color);


    void paint();

    void clear();

    virtual ~Canvas();

private:    

    void create(HWND hWnd, HINSTANCE hInstance);

    BitmapDIBUncompressed32Bit  Bitmap;
    static int NumOfRegisteredWindows;
};

The implementation of the paint method:

void Canvas::paint()
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(m_hWnd, &ps);
    HDC hdcWindow = GetWindowDC(m_hWnd);

    BitBlt(hdcWindow, 0, 0, m_Size.getWidth(), m_Size.getHeight(), Bitmap.getHDC(), 0, 0, SRCCOPY);

    ReleaseDC(m_hWnd, hdcWindow);
    EndPaint(m_hWnd, &ps);
}

How the canvas is created:

void Canvas::create(HWND hWnd, HINSTANCE hInstance)
{
    WNDCLASS wndcls;
    wndcls.style            = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
    wndcls.lpfnWndProc      = (WNDPROC)WindowsManager::windowEventsProcessor;
    wndcls.cbClsExtra       = wndcls.cbWndExtra = 0;
    wndcls.hInstance        = hInstance;
    wndcls.hIcon            = NULL;
    wndcls.hCursor          = LoadCursor(NULL, IDC_ARROW);
    wndcls.hbrBackground    = (HBRUSH) (COLOR_3DFACE + 1);
    wndcls.lpszMenuName     = NULL;
    
    NumOfRegisteredWindows++;
    string UniqueClassName =  "UniqueClassNameID_Canvas_" + String::IntToString(NumOfRegisteredWindows);
    wndcls.lpszClassName    = UniqueClassName.c_str();
    
    if(RegisterClass(&wndcls) == 0)
        cout<<"shit happens"<<endl;

    m_hWnd = CreateWindowEx(NULL,UniqueClassName.c_str(), "YourCanvas", WS_CHILD |
    WS_VISIBLE, 20, 20, 100, 20, hWnd, NULL, hInstance,
    NULL);
}

setPixel:

void Canvas::setPixel(int x, int y)
{
    Bitmap[x+y*Bitmap.getWidth()] = 0xFFFF0000;
}

Have also a look at PixelToaster.

If you want to got the WinAPI + GDI approach you can also find more information in the book "Programming Windows: The Definitive Guide To The Win32 Api"

As an alternative, you can create a OpenGL/Direct3D/Vulkan renderer device an a texture object, and write to the texture memory an upload it.

You can also use tev. It provides a network protocol to write to its framebuffer. This way you get tone mapping for free.

Also Qt can be an option for you - you can use a QImage and draw it on a widget - e.g.:

class RenderWidget : public QWidget
{
    Q_OBJECT

public:
    RenderWidget(const CommandLineArguments& cla, QWidget* parent = nullptr)  : QWidget(parent), render_scene_thread_(cla), render_preview_thread_(&render_scene_thread_) {
        draw_next_frame();

        setFixedSize(100,100);
    }

    virtual ~RenderWidget() {
        if(render_preview_thread_.isRunning()) {
            render_preview_thread_.quit();
        }
        if(render_scene_thread_.isRunning()) {
            render_scene_thread_.quit();
        }
    }

    void paintEvent(QPaintEvent * e) override {
        if(render_preview_thread_.qimage_) {
            QPainter p(this);
            p.drawImage(rect(), *render_preview_thread_.qimage_);
        }
        else {
            LOG(INFO) << "Nothing to draw";
        }
    }
        
public Q_SLOTS:
    void draw_next_frame() {
        if(render_scene_thread_.render_state_ == RenderState::Rendering ||
           render_scene_thread_.render_state_ == RenderState::Done) {
            static Vector2i old_size = Vector2i(-1,-1);
            Vector2i size = render_scene_thread_.scene->sensor()->film()->size();

            if(old_size != size) {
                this->setFixedSize(size.x(), size.y());
                old_size = size;
            }
        }

        repaint();

        QTimer::singleShot(1000, this, SLOT(draw_next_frame()));
    }

public:
    RenderSceneThread render_scene_thread_;
    RenderPreviewThread render_preview_thread_;
};