c++winapigraphicsframebufferraytracing

Creating a window just to get its framebuffer?


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?


Solution

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