Skip to content

Instantly share code, notes, and snippets.

@ganbatte8
Last active June 10, 2023 16:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ganbatte8/23b65249cdd6c560726ea3f8014320d7 to your computer and use it in GitHub Desktop.
Save ganbatte8/23b65249cdd6c560726ea3f8014320d7 to your computer and use it in GitHub Desktop.
Simple game loop in Linux/XCB, writing pixels with CPU, enforcing a framerate, reading joystick, keyboard and mouse input.
// Linking: -lxcb -lxcb-image
//#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#include <malloc.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/joystick.h>
//#include <time.h>
#include <sys/stat.h>
#define internal static
typedef uint8_t u8;
typedef uint32_t u32;
typedef int64_t s64;
typedef int32_t s32;
typedef float f32;
typedef int b32;
#define Assert(Expression) if (!(Expression)) {*(int *)0 = 0;}
typedef struct timespec timespec;
typedef struct js_event js_event;
typedef struct
{
u32 Width;
u32 Height;
s32 Pitch;
u32 BytesPerPixel;
void *Memory;
xcb_image_t *Image;
xcb_pixmap_t Pixmap;
xcb_gcontext_t GraphicsContext;
} linux_offscreen_buffer;
internal timespec
LinuxGetWallClock()
{
timespec Result = {};
clock_gettime(CLOCK_MONOTONIC, &Result);
return Result;
}
internal void
WriteGradientTest(linux_offscreen_buffer *Buffer, u32 OffsetX, u32 OffsetY)
{
u32 *Pixel = (u32 *)Buffer->Memory;
for (int y = 0; y < Buffer->Height; ++y)
{
for (int x = 0; x < Buffer->Width; ++x)
{
u32 ColorX = (x + OffsetX) & 255;
u32 ColorY = (y + OffsetY) & 255;
*Pixel++ = (ColorX << 16) | (ColorY << 16);
}
}
}
int main(void)
{
xcb_connection_t *Connection = xcb_connect(0, 0);
const xcb_setup_t *XCBSetup = xcb_get_setup(Connection);
xcb_screen_iterator_t Iter = xcb_setup_roots_iterator(XCBSetup);
xcb_screen_t *Screen = Iter.data;
// NOTE(vincent): Stuff we have to do to initialize a backbuffer and window in xcb:
// - allocate the buffer, maybe specify width, height and pitch for our own convenience;
// - specify a list of X events to subscribe to
// - create a window, a graphics context, a pixmap and an "image"; link all that crap together.
// (I don't understand how the GC, pixmap and image work individually,
// but I haven't been able to get rid of any of them and keep this program working).
linux_offscreen_buffer Buffer = {};
u32 Mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
u32 Values[2] =
{
Screen->black_pixel,
XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE |
XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE
};
s32 Width = 480;//960;//1920;
s32 Height = 270;//540;//1080;
Buffer.Width = Width;
Buffer.Height = Height;
Buffer.Pitch = Width*4;
Buffer.Memory = malloc(Width*Height*4);
Buffer.GraphicsContext = xcb_generate_id(Connection);
Buffer.Pixmap = xcb_generate_id(Connection);
xcb_window_t Window = xcb_generate_id(Connection);
xcb_create_window(Connection,
Screen->root_depth,
Window,
Screen->root,
0, 0, Width, Height, 0,
XCB_WINDOW_CLASS_INPUT_OUTPUT, Screen->root_visual,
Mask, Values);
xcb_create_pixmap(Connection, Screen->root_depth, Buffer.Pixmap, Window, Width, Height);
xcb_create_gc(Connection, Buffer.GraphicsContext, Buffer.Pixmap, 0, 0);
// NOTE(vincent): Not sure what the difference is with xcb_image_create
Buffer.Image = xcb_image_create_native(Connection, Width, Height,
XCB_IMAGE_FORMAT_Z_PIXMAP, Screen->root_depth,
(u8 *)Buffer.Memory,
Width*Height*4,
(u8 *)Buffer.Memory);
u32 OffsetX = 0;
u32 OffsetY = 0;
int JoystickFD = -1;
struct stat JoystickStatus = {};
time_t LastJoystickCTime = JoystickStatus.st_ctime;
f32 JoystickRefreshClock = 0.0f;
f32 GameUpdateHz = 60.0f;
f32 dtForFrame = 1.0f / GameUpdateHz;
u32 TargetNSPerFrame = (1000 * 1000 * 1000) / GameUpdateHz;
timespec LastWallClock = LinuxGetWallClock();
xcb_map_window(Connection, Window);
xcb_flush(Connection);
for (;;)
{
// NOTE(vincent): X event handling
xcb_generic_event_t *Event;
while ((Event = xcb_poll_for_event(Connection)))
{
// NOTE(vincent): I've seen many examples that mask this bit out, but idk why
switch (Event->response_type & ~0x80)
{
case XCB_EXPOSE:
{
xcb_expose_event_t *x = (xcb_expose_event_t *)Event;
printf("Expose\n");
} break;
case XCB_BUTTON_PRESS:
{
xcb_button_press_event_t *BP = (xcb_button_press_event_t *)Event;
printf("Button press %d\n", BP->detail);
} break;
case XCB_BUTTON_RELEASE:
{
xcb_button_release_event_t *BR = (xcb_button_release_event_t *)Event;
printf("Button release %d\n", BR->detail);
} break;
case XCB_KEY_PRESS:
{
xcb_key_press_event_t *KP = (xcb_key_press_event_t *)Event;
printf("Key press. Keycode %d\n", KP->detail);
} break;
case XCB_KEY_RELEASE:
{
xcb_key_release_event_t *KR = (xcb_key_release_event_t *)Event;
printf("Key release. Keycode %d\n", KR->detail);
} break;
case XCB_MOTION_NOTIFY:
{
// In full screen 1920*1080 with border size 0 on i3, I get the following intervals
// of values inside of the client area:
// [0, 1079] for y (top to bottom)
// [0, 1919] for x (left to right)
// Values can get out of these bounds when the mouse cursor is outside
// of the client area.
xcb_motion_notify_event_t *Motion = (xcb_motion_notify_event_t *)Event;
printf("Mouse move x: %d y: %d\n", Motion->event_x, Motion->event_y);
} break;
default:
{
printf("Unhandled event: %d\n", Event->response_type);
// NOTE(vincent): Event 14 is firing a lot (once every frame it looks like).
// I don't know why, or what it is.
}
}
free(Event);
}
// NOTE(vincent): Refresh gamepad.
// - Problem statement: we want to let the controller disconnect and reconnect
// - It seems more sane to verify every second (as we do) than every frame, but I'm not sure
// - Closing and reopening the JoystickFD every time causes huge stalls, especially when
// the controller is plugged in. Looking at the CTime with fstat() mitigates a lot of the cost.
if (JoystickRefreshClock >= 1.0f)
{
JoystickRefreshClock -= 1.0f;
if (JoystickFD >= 0)
{
fstat(JoystickFD, &JoystickStatus);
if (JoystickStatus.st_ctime != LastJoystickCTime)
{
printf("New joystick CTime: %d (closing FD)\n", JoystickStatus.st_ctime);
close(JoystickFD);
LastJoystickCTime = JoystickStatus.st_ctime;
JoystickFD = -1;
}
}
else
{
printf("Trying to reopen joystick FD...\n");
JoystickFD = open("/dev/input/js0", O_RDONLY | O_NONBLOCK);
if (JoystickFD >= 0)
{
fstat(JoystickFD, &JoystickStatus);
LastJoystickCTime = JoystickStatus.st_ctime;
printf("Reopened joystick\n");
}
}
}
JoystickRefreshClock += dtForFrame;
// NOTE(vincent): Gamepad event handling
if (JoystickFD >= 0)
{
js_event JoystickEvent;
while (read(JoystickFD, &JoystickEvent, sizeof(js_event)) == sizeof(js_event))
{
JoystickEvent.type &= ~JS_EVENT_INIT;
switch (JoystickEvent.type)
{
case JS_EVENT_BUTTON:
{
printf("JS_EVENT_BUTTON: %d, %d\n", JoystickEvent.number, JoystickEvent.value);
} break;
case JS_EVENT_AXIS:
{
printf("JS_EVENT_AXIS: %d, %d\n", JoystickEvent.number, JoystickEvent.value);
} break;
}
}
}
// NOTE(vincent): Update and render
++OffsetX;
++OffsetY;
WriteGradientTest(&Buffer, OffsetX, OffsetY);
xcb_image_put(Connection, Buffer.Pixmap, Buffer.GraphicsContext, Buffer.Image, 0, 0, 0);
xcb_copy_area(Connection,
Buffer.Pixmap, // source drawable
Window, // dest drawable
Buffer.GraphicsContext,
0, 0, 0, 0, Width, Height);
// NOTE(vincent): nanosleep() and update LastWallClock
timespec TargetWallClock;
TargetWallClock.tv_sec = LastWallClock.tv_sec;
TargetWallClock.tv_nsec = LastWallClock.tv_nsec + TargetNSPerFrame;
if (TargetWallClock.tv_nsec > 1000 * 1000 * 1000)
{
TargetWallClock.tv_nsec -= 1000 * 1000 * 1000;
Assert(TargetWallClock.tv_nsec < 1000 * 1000 * 1000);
TargetWallClock.tv_sec++;
}
timespec ClockAfterWork = LinuxGetWallClock();
b32 ShouldSleep = (ClockAfterWork.tv_sec < TargetWallClock.tv_sec)
|| (ClockAfterWork.tv_sec == TargetWallClock.tv_sec &&
ClockAfterWork.tv_nsec < TargetWallClock.tv_nsec);
if (ShouldSleep)
{
timespec SleepAmount;
SleepAmount.tv_sec = 0;
SleepAmount.tv_nsec = TargetWallClock.tv_nsec - ClockAfterWork.tv_nsec;
if (SleepAmount.tv_nsec < 0)
SleepAmount.tv_nsec += 1000*1000*1000;
printf("Sleep amount tv_sec: %d tv_nsec: %d\n", SleepAmount.tv_sec, SleepAmount.tv_nsec);
nanosleep(&SleepAmount, 0);
}
LastWallClock = LinuxGetWallClock();
}
//xcb_free_pixmap(Connection, Buffer.Pixmap);
//xcb_disconnect(Connection);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment