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