Skip to content

Instantly share code, notes, and snippets.

@fp64
Last active February 17, 2025 04:59
Show Gist options
  • Select an option

  • Save fp64/1a3166ca35118167af8839b40d4e7b94 to your computer and use it in GitHub Desktop.

Select an option

Save fp64/1a3166ca35118167af8839b40d4e7b94 to your computer and use it in GitHub Desktop.
Somewhat basic WinAPI & SDL2 graphical program.
// Public Domain under http://unlicense.org, see link for details.
// Self-reference: https://gist.github.com/fp64/1a3166ca35118167af8839b40d4e7b94
// Minimalist version: https://gist.github.com/fp64/2e25143e754424d4a2f592d9493451bc
// Somewhat minimalistic dual-backend C program for plain WinAPI and SDL2,
// that creates a (possibly fullscreen) window and provides keyboard input
// and in-memory colorbuffer to draw to (no GAPI, i.e. softrender).
// Compilation, WinAPI (assuming MinGW):
// gcc -std=c99 -O2 demo.c -o demo.exe -mwindows -static
// Compilation, SDL (assuming gcc and a shell supporting backticks):
// gcc -std=c99 -O2 `sdl2-config --cflags` demo.c -o demo `sdl2-config --libs`
// On both backends, colorbuffer is contiguous R8G8B8A8 (not BGRA)
// memory region, and has (0,0) at top-left corner.
// On both backends, nonzero keys[...] means the key is currently
// down. Which indices correspond to which keys differs between
// backends, but ' ' (Space), '0'..'9', 'A'..'Z' (uppercase),
// 8 (Backspace), 9 (Tab), 13 (Return), 27 (Esc), 112..123 (F1..F12),
// 37..40 (left/up/right/down arrow keys) all agree in both.
static int width=0,height=0; // Setup during init(). May be updated during frame().
static unsigned char *colorbuffer=0; // Setup during init(). May be updated during frame().
static unsigned char keys[256]={0}; // Updated during frame().
static int mouse_x=0,mouse_y=0,mouse_b=0; // Updated during frame().
// Platform-independent entry point, mimicing main(). NOTE: argv is assumed UTF-8.
extern int entrypoint(int argc,char **argv);
// Initialization. Create window and fill width/height/colorbuffer. Returns zero on failure.
static int init(int w,int h,int mode); // 0=borderless, 1=fixed, 2=resizable, 3=fullscreen.
// Draw window and then process messages and update width/height/colorbuffer/keys.
// Returns zero if needs to quit.
static int frame(void);
// Handy functions.
static double get_time_in_seconds(void);
static void set_window_title(const char *title);
static void sleep(double time_in_seconds);
//== Platform-dependent section ================================================
#if !defined(USE_SDL) && !defined(_WIN32)
#define USE_SDL 1 // Can be overridden via -DUSE_SDL=[...] option.
#endif
#if USE_SDL // SDL2 backend. Can be forced via -DUSE_SDL=1 option.
#include "SDL.h" // See https://nullprogram.com/blog/2023/01/08/
static SDL_Window *window=NULL;
static SDL_Surface *surface=NULL;
static int keystate[256]={0};
int init(int w,int h,int mode)
{
const Uint32 styles[]={SDL_WINDOW_BORDERLESS,0,SDL_WINDOW_RESIZABLE,SDL_WINDOW_FULLSCREEN_DESKTOP};
if(SDL_Init(SDL_INIT_VIDEO)<0)
{
SDL_Log("SDL_Init failed: %s\n",SDL_GetError());
return 0;
}
width=w; height=h;
if(!(window=SDL_CreateWindow("demo",
SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,width,height,
styles[mode])))
{
SDL_Log("SDL_CreateWindow failed: %s\n",SDL_GetError());
return 0;
}
surface=SDL_CreateRGBSurfaceWithFormat(0,width,height,32,SDL_PIXELFORMAT_RGBA32);
SDL_SetSurfaceBlendMode(surface,SDL_BLENDMODE_NONE);
colorbuffer=(unsigned char*)surface->pixels;
return 1;
}
int frame(void)
{
int i,quit=0,w,h;
SDL_Event e;
SDL_BlitSurface(surface,NULL,SDL_GetWindowSurface(window),NULL);
SDL_UpdateWindowSurface(window);
for(i=0;i<256;++i) keystate[i]=0;
while(SDL_PollEvent(&e)!=0)
{
int key=e.key.keysym.sym;
if(key>='a'&&key<='z') key+='A'-'a'; // Convert A..Z to VK_*.
if(key>=0x4000003A&&key<=0x40000045) key+=0x70-0x4000003A; // Convert F1..F12 to VK_*.
if(key>=0x4000004F&&key<=0x40000052) key=0x25+((0x1302>>((key-0x4000004F)*4))&15); // Convert arrow keys to VK_*.
if(key&~255) key=255;
if(e.type==SDL_QUIT) quit=1;
if(e.type==SDL_KEYDOWN) keys[key]=1;
if(e.type==SDL_KEYUP) keys[key]=0;
}
mouse_b=(int)(SDL_GetMouseState(&mouse_x,&mouse_y)/SDL_BUTTON(1))&7;
SDL_GetWindowSize(window,&w,&h);
if(w!=width||h!=height)
{
SDL_FreeSurface(surface);
if(!(surface=SDL_CreateRGBSurfaceWithFormat(0,width=w,height=h,32,SDL_PIXELFORMAT_RGBA32))) return 0;
SDL_SetSurfaceBlendMode(surface,SDL_BLENDMODE_NONE);
colorbuffer=(unsigned char*)surface->pixels;
}
return !quit;
}
static double get_time_in_seconds(void) {return 0.001*(double)SDL_GetTicks();} // Or SDL_GetTicks64().
static void set_window_title(const char *title) {SDL_SetWindowTitle(window,title);}
static void sleep(double time_in_seconds) {SDL_Delay((Uint32)(1000.0*time_in_seconds+0.5));}
int main(int argc,char **argv)
{
int ret=entrypoint(argc,argv);
if(surface) SDL_FreeSurface(surface);
if(window) SDL_DestroyWindow(window);
SDL_Quit();
return ret;
}
#else // WinAPI backend. Can be forced via -DUSE_SDL=0 option.
#include "windows.h"
static HWND hwnd=NULL;
static HINSTANCE hinst=NULL;
int init(int w,int h,int mode)
{
WNDCLASS wc={0}; // No need for extended window class/style.
RECT rect={0};
DWORD styles[]={WS_POPUP,WS_POPUP|WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX,WS_OVERLAPPEDWINDOW,WS_POPUP|WS_CLIPSIBLINGS|WS_CLIPCHILDREN};
DWORD style=styles[mode];
wc.lpfnWndProc=DefWindowProcA; // No need for custom WndProc.
wc.hInstance =hinst;
wc.hCursor =LoadCursor(NULL,IDC_ARROW); // hIcon is fine being NULL, but not hCursor.
wc.lpszClassName="Demo";
if(!RegisterClassA(&wc)) return 0;
if(mode==3) {w=GetSystemMetrics(SM_CXSCREEN); h=GetSystemMetrics(SM_CYSCREEN);}
rect.right=(width=w); rect.bottom=(height=h);
AdjustWindowRect(&rect,style,0); // width and height correspond to "client area", not the whole window.
if(!(hwnd=CreateWindowA(wc.lpszClassName,"demo",
style|WS_VISIBLE,
CW_USEDEFAULT*(mode!=3),CW_USEDEFAULT*(mode!=3),rect.right-rect.left,rect.bottom-rect.top,
NULL,NULL,hinst,NULL))) return 0;
if(!(colorbuffer=(unsigned char*)LocalAlloc(LPTR,(SIZE_T)(width*height*4*2)))) return 0;
return 1;
}
static int frame(void)
{
BITMAPINFOHEADER bmih={sizeof(BITMAPINFOHEADER),width,-height,1,32,BI_RGB,0,0,0,0,0};
HDC hdc=NULL;
MSG msg;
int x,y,i,quit=0;
RECT rect={0};
POINT cur={0};
for(y=0;y<height;++y) for(x=0;x<width;++x) for(i=0;i<4;++i) // Convert to BGRA.
colorbuffer[(width*height+y*width+x)*4+i]=colorbuffer[(y*width+x)*4+((2-i)&3)];
if(!(hdc=GetDC(hwnd))) return 0;
StretchDIBits(
hdc,
0,0,width,height,
0,0,width,height,
colorbuffer+(width*height*4),
(BITMAPINFO*)(&bmih),
DIB_RGB_COLORS,
SRCCOPY);
InvalidateRect(hwnd,NULL,FALSE); // Actually initiate redrawing.
ReleaseDC(hwnd,hdc);
while(PeekMessageA(&msg,NULL,0,0,PM_REMOVE))
{
quit|=(msg.message==WM_QUIT);
DispatchMessageA(&msg); // No need for TranslateMessageA(&msg);
}
for(i=0;i<256;++i) keys[i]=(unsigned char)(GetKeyState(i)>>15);
GetCursorPos(&cur);
ScreenToClient(hwnd,&cur);
mouse_x=cur.x;
mouse_y=cur.y;
mouse_b=((GetAsyncKeyState(VK_LBUTTON)>>15)&1)+((GetAsyncKeyState(VK_MBUTTON)>>15)&2)+((GetAsyncKeyState(VK_RBUTTON)>>15)&4);
GetClientRect(hwnd,&rect);
if(rect.right-rect.left!=width||rect.bottom-rect.top!=height)
{
width=rect.right-rect.left; height=rect.bottom-rect.top;
LocalFree(colorbuffer);
if(!(colorbuffer=(unsigned char*)LocalAlloc(LPTR,(SIZE_T)(width*height*4*2)))) return 0;
}
return !quit;
}
static double get_time_in_seconds(void)
{
static double f=-1.0;
LARGE_INTEGER c={0};
if(f<0.0) {QueryPerformanceFrequency(&c); f=(double)c.QuadPart;}
QueryPerformanceCounter(&c);
return (double)c.QuadPart/f;
}
static void set_window_title(const char *title) {SetWindowTextA(hwnd,title);}
static void sleep(double time_in_seconds) {Sleep((DWORD)(1000.0*time_in_seconds+0.5));}
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
// Based on https://stackoverflow.com/questions/74999026/is-there-the-commandlinetoargva-function-in-windows-c-c-vs-2022
int argc=0;
char **argv=0,*p;
LPWSTR *argvw=CommandLineToArgvW(GetCommandLineW(),&argc);
int i,len=0,ret=0;
(void)hPrevInstance; (void)lpCmdLine; (void)nCmdShow;
hinst=hInstance;
for(i=0;i<argc;++i)
len+=WideCharToMultiByte(CP_UTF8,0,argvw[i],-1,NULL,0,NULL,NULL)+1;
argv=(char**)LocalAlloc(LPTR,(SIZE_T)(argc+1)*sizeof(char*)+(SIZE_T)(len));
p=(char*)&(argv[argc+1]);
for(i=0;i<argc;++i)
{
argv[i]=p;
p+=WideCharToMultiByte(CP_UTF8,0,argvw[i],-1,p,len,NULL,NULL)+1;
}
argv[argc]=NULL;
LocalFree(argvw);
ret=entrypoint(argc,argv);
if(colorbuffer) LocalFree(colorbuffer);
if(hwnd) DestroyWindow(hwnd);
return ret;
}
#endif
#include <stdio.h>
#include <string.h>
int entrypoint(int argc,char **argv)
{
int w=640,h=480,mode=1,frames=0,o[2],i;
double prv,cur,spf=0.0;
for(i=1;i<argc;++i)
{
if(!strcmp(argv[i],"--borderless")) mode=0;
if(!strcmp(argv[i],"--default")) mode=1;
if(!strcmp(argv[i],"--resizable")) mode=2;
if(!strcmp(argv[i],"--fullscreen")) mode=3;
if(sscanf(argv[i],"--size=%dx%d",o+0,o+1)==2) {w=o[0]; h=o[1];}
}
if(!init(w,h,mode)) return 1;
prv=get_time_in_seconds();
while(frame())
{
int x,y,i;
char buf[256]={0};
++frames;
if(keys[27]||keys['Q']) break;
if((cur=get_time_in_seconds())-prv>=0.25) {spf=(cur-prv)/(double)frames; prv=cur; frames=0;}
sprintf(buf,"%dx%d %7.2f ms/frame",width,height,1000.0*spf);
set_window_title(buf);
sleep(0.001);
for(y=0;y<height;++y) for(x=0;x<width;++x) for(i=0;i<4;++i)
colorbuffer[(y*width+x)*4+i]=(unsigned char)((0xFF77AA22u>>(8*i))+((x^y)&16)+(unsigned)((x-mouse_x)*(x-mouse_x)+(y-mouse_y)*(y-mouse_y)<100?20+10*mouse_b:0));
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment