Last active
January 6, 2025 22:23
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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