GDI, all you need..

Main point in this chapter are device contexts. You can't access hardware directly in windows (at least not in the scope of this tutorial). All you have is this device context thing.

Obvious bad side of device context use is the overhead of converting data into required form. Good side is that when we do things like this, you don't have to care whether the user is viewing your program via EGA, Matrox Millennium, VR-gear, hologram projector, rapid laser printer or even a plotter.

Device contexts are one of these resources that are always being used up. I don't know the exact story, but in windows 3.x program, you could crash windows by allocating some 16 device contexts or so. Quite probably with much less. So, device contexts should be allocated, used, and freed as soon as they are not needed anymore.

But since I'm also a victim of this democoder attitude, we'll allocate our framebuffer DC at start and free it at closing time.. This works fine since we aren't using any other DC:s in this program, but in slightly larger windows application you might be allocating and deallocating DC:s all the time, and might run out of them pretty fast.

So, here's the code.

#include <windows.h>
#include <math.h>

char progname[]="Cute plasma";
char SINTAB[256];

// forward declaration:
LRESULT CALLBACK winproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow)
{
  WNDCLASSEX winclass;
  HWND hWnd;
  MSG msg;
  int i;
  for (i=0;i<256;i++)
    SINTAB[i]=sin(((i+1)*3.14159265359)/128)*127+128;

  winclass.cbSize=sizeof(WNDCLASSEX);
  winclass.style=CS_DBLCLKS;
  winclass.lpfnWndProc=&winproc;
  winclass.cbClsExtra=0;
  winclass.cbWndExtra=0;
  winclass.hInstance=hInst;
  winclass.hIcon=LoadIcon(NULL,IDI_WINLOGO);
  winclass.hCursor=LoadCursor(NULL,IDC_NO);
  winclass.hbrBackground=NULL;
  winclass.lpszMenuName=NULL;
  winclass.lpszClassName=progname;
  winclass.hIconSm=NULL;

  if (!RegisterClassEx(&winclass))
    return 0;
  hWnd=CreateWindow(
    progname,
    progname,
    WS_SYSMENU|WS_CAPTION|WS_BORDER|WS_OVERLAPPED|WS_VISIBLE|WS_MINIMIZEBOX,
    CW_USEDEFAULT,
    0,
    320+2,
    200+16+2,
    NULL,
    NULL,
    hInst,
    NULL);
  ShowWindow(hWnd,nCmdShow);
  UpdateWindow(hWnd);
  while (GetMessage(&msg,NULL,0,0)) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return (msg.wParam);
}

HDC pDC;
HBITMAP old;
HBITMAP ourbitmap;
int * framebuf;

void render_effect(int tick,int * framebuf)
{
  int i,j,k;
  tick/=4;
  for (k=0,i=0;i<200;i++)
    for (j=0;j<320;j++,k++)
      *(framebuf+k)=RGB(SINTAB[(i+tick)&0xff],
        SINTAB[(j-tick)&0xff],
        SINTAB[(SINTAB[tick&0xff]+(k>>6))&0xff]);
}

void render(HDC hDC)
{
  render_effect(GetTickCount(),framebuf);
  BitBlt(hDC,0,0,320,200,pDC,0,0,SRCCOPY);
}

void deinit_framebuf(void)
{
  SelectObject(pDC,old);
  DeleteDC(pDC);
  DeleteObject(ourbitmap);
}

void init_framebuf(void)
{
  HDC hDC;
  BITMAPINFO bitmapinfo;
  hDC=CreateCompatibleDC(NULL);
  bitmapinfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
  bitmapinfo.bmiHeader.biWidth=320;
  bitmapinfo.bmiHeader.biHeight=-200; /* top-down */
  bitmapinfo.bmiHeader.biPlanes=1;
  bitmapinfo.bmiHeader.biBitCount=32;
  bitmapinfo.bmiHeader.biCompression=BI_RGB;
  bitmapinfo.bmiHeader.biSizeImage=0;
  bitmapinfo.bmiHeader.biClrUsed=256;
  bitmapinfo.bmiHeader.biClrImportant=256;
  ourbitmap=CreateDIBSection(hDC,&bitmapinfo,DIB_RGB_COLORS,&framebuf,0,0);
  pDC=CreateCompatibleDC(NULL);
  old=SelectObject(pDC,ourbitmap);
  DeleteDC(hDC);
}

LRESULT CALLBACK winproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hDC;
  PAINTSTRUCT PtStr;
  switch (uMsg) {
  case WM_DESTROY:
    deinit_framebuf();
    PostQuitMessage(0);
    KillTimer (hWnd, 1);
    break;
  case WM_CREATE:
    SetTimer (hWnd, 1, 1, NULL);
    init_framebuf();
    break;
  case WM_TIMER:
    InvalidateRgn(hWnd,0,0);
    UpdateWindow (hWnd);
    break;
  case WM_PAINT:
    hDC=BeginPaint(hWnd,&PtStr);
    render(hDC);
    EndPaint(hWnd,&PtStr);
    break;
  default:
    return DefWindowProc (hWnd, uMsg, wParam, lParam);
    break;
  }
  return 0;
}

If you go and compile it, it'll give you a window with a very simple RGB plasma. Slow, too. But it does what it's meant to do: now you have a RGB framebuffer.

void init_framebuf(void)
{
  HDC hDC;
  BITMAPINFO bitmapinfo;
  hDC=CreateCompatibleDC(NULL);
  bitmapinfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
  bitmapinfo.bmiHeader.biWidth=320;
  bitmapinfo.bmiHeader.biHeight=-200; /* top-down */
  bitmapinfo.bmiHeader.biPlanes=1;
  bitmapinfo.bmiHeader.biBitCount=32;
  bitmapinfo.bmiHeader.biCompression=BI_RGB;
  bitmapinfo.bmiHeader.biSizeImage=0;
  bitmapinfo.bmiHeader.biClrUsed=256;
  bitmapinfo.bmiHeader.biClrImportant=256;
  ourbitmap=CreateDIBSection(hDC,&bitmapinfo,DIB_RGB_COLORS,&framebuf,0,0);
  pDC=CreateCompatibleDC(NULL);
  old=SelectObject(pDC,ourbitmap);
  DeleteDC(hDC);
}

Let's start from the logical top. First we request a compatible DC of the default device, ie. screen. Next we create a Device Independent Bitmap section, which will become our framebuffer, which will be initialized to be used with hDC like output device. Our DIB will be 320x200, 32bit ARGB, top down (default would be bottoms-up). Next we create a new compatible DC and place our DIB into this DC, saving its (possible) old DIB so that we don't waste resources.

void render(HDC hDC)
{
  render_effect(GetTickCount(),framebuf);
  BitBlt(hDC,0,0,320,200,pDC,0,0,SRCCOPY);
}

When rendering, we give our effect the target framebuffer and timer tick (one msec resolution). Then we'll just blit our framebuffer into the screen device context. Please note that the BitBlt will only move data into dirty areas, so nothing is wasted by using larger blitting sizes. SRCCOPY is just copy-over.. windows can do lots of other kinds of blits as well.

I'm not going to explain how the plasma rendering function works; it should be quite obvious.

case WM_DESTROY:
  deinit_framebuf();
  PostQuitMessage(0);
  KillTimer (hWnd, 1);
  break;
case WM_CREATE:
  SetTimer (hWnd, 1, 1, NULL);
  init_framebuf();
  break;
case WM_TIMER:
  InvalidateRgn(hWnd,0,0);
  UpdateWindow (hWnd);
  break;
case WM_PAINT:
  hDC=BeginPaint(hWnd,&PtStr);
  render(hDC);
  EndPaint(hWnd,&PtStr);
  break;

Most interesting changes have happened at the window procedure. At creating time we allocate a timer, which will send us WM_TIMER messages at about one millisecond intervals, give or take 10 milliseconds =). This is not a "multimedia timer", so it isn't all that accurate.

Next we call our framebuffer initializer. In closing time we deinitialize the framebuffer first, kill off our allocated timer and remember to post that quit message!.

So the timer throws us messages all the time. When we get one, we'll invalidate the whole client area of our window, and request the window update. And when the WM_PAINT arrives, we first start painting, call our own rendering function, then end the painting, and wait for the next WM_TIMER message.

void deinit_framebuf(void)
{
  SelectObject(pDC,old);
  DeleteDC(pDC);
  DeleteObject(ourbitmap);
}

And when deinitializing, we first put the old DIB back to our framebuffer hDC, kill the framebuffer hDC off, and finish by killing our DIB off.

That's practically it!

Next up: resources, menus and dialog boxes.

Journey onwards