Created
July 10, 2024 19:11
-
-
Save DevFelixFaber/6947def19c3cad9a278c88a880be8738 to your computer and use it in GitHub Desktop.
Wayland OpenGL Example Application. Includes Keyboard input (with key-repeat), mouse events, setting the mouse cursor image/icon, retrieving the refreshrate, and measuring render time.
This file contains 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
#define _GNU_SOURCE | |
#include <unistd.h> | |
#include <fcntl.h> | |
#include <sys/stat.h> | |
#include <assert.h> | |
#include <string.h> | |
#include <time.h> | |
#include <poll.h> | |
#include <sys/timerfd.h> | |
#include <sys/mman.h> | |
#include <xkbcommon/xkbcommon.h> | |
#include <linux/input-event-codes.h> // Mouse buttons | |
#include <wayland-client.h> | |
#include <wayland-cursor.h> | |
#include <wayland-egl.h> | |
#include "generated/xdg_shell.h" | |
#include "generated/xdg_shell.c" | |
// NOTE: I use this definition to get "more modern" OpenGL function prototypes. | |
// They most likely are available on every platform, but it's technically not guaranteed. | |
// Not used in this example, but this can become relevant once there are more OpenGL function calls. | |
#define GL_GLEXT_PROTOTYPES | |
#include <GL/gl.h> | |
#include <EGL/egl.h> | |
// Nicer primitives | |
#include <inttypes.h> | |
#include <stddef.h> | |
#include <stdint.h> | |
typedef int8_t s8; | |
typedef int16_t s16; | |
typedef int32_t s32; | |
typedef int64_t s64; | |
typedef uint8_t u8; | |
typedef uint16_t u16; | |
typedef uint32_t u32; | |
typedef uint64_t u64; | |
typedef s32 b32; | |
typedef float f32; | |
typedef double f64; | |
#define ARRAYCOUNT(Array) (sizeof(Array) / sizeof((Array)[0])) | |
#define MIN(a,b) ((a) < (b) ? (a) : (b)) | |
#define MAX(a,b) ((a) > (b) ? (a) : (b)) | |
// Structure definitions | |
typedef struct wayland_output wayland_output; | |
struct wayland_output | |
{ | |
b32 IsInitialized; | |
b32 DisplaysWindow; | |
struct wl_output *WlOutput; | |
double RefreshRateHz; | |
u32 NameID; | |
}; | |
typedef struct wayland_context wayland_context; | |
struct wayland_context | |
{ | |
// Wayland | |
struct wl_display *WlDisplay; | |
struct wl_registry *WlRegistry; | |
struct wl_compositor *WlCompositor; | |
struct wl_surface *WlSurface; | |
struct wl_egl_window *WlEglWindow; | |
struct wl_seat *WlSeat; | |
struct wl_shm *WlShm; | |
struct wl_keyboard *WlKeyboard; | |
struct wl_pointer *WlPointer; | |
wayland_output WaylandOutputs[16]; // Assuming that 16 Monitors are enough | |
struct wl_cursor_theme *WlCursorTheme; | |
struct wl_cursor *WlCursor; | |
struct wl_surface *WlCursorSurface; | |
struct wl_buffer *WlCursorBuffer; | |
struct wl_cursor_image *WlCursorImage; | |
// XDG (rendering to surface) | |
struct xdg_wm_base *XdgBase; | |
struct xdg_toplevel *XdgToplevel; | |
struct xdg_surface *XdgSurface; | |
// XKB (Keyboard input) | |
struct xkb_context *XkbContext; | |
struct xkb_keymap *XkbKeymap; | |
struct xkb_state *XkbState; | |
// Egl (OpenGL context) | |
EGLContext EglContext; | |
EGLDisplay EglDisplay; | |
EGLSurface EglSurface; | |
// Input for key repeat | |
int TimerFd; | |
struct itimerspec TimerProperties; | |
u32 KeyCode; | |
u32 KeyState; | |
// Some window properties | |
double WindowCurrentRefreshRate; // Max of all refreshrates the window is displayed on | |
s32 WindowCurrentWidth; | |
s32 WindowCurrentHeight; | |
s32 WindowWantsWidth; | |
s32 WindowWantsHeight; | |
b32 OpenGLNeedsResize; | |
b32 WindowFullscreen; | |
b32 WantsToExit; | |
// TODO: Start of your custom properties | |
}; | |
// Functions | |
// This function is called on every key event | |
static void | |
ProcessKeyUpdate(wayland_context *Context, u32 Key, u32 KeyState) | |
{ | |
char Utf8Character[5] = { 0 }; | |
u32 XkbScancode = Key+8; // Something something translation from evdev | |
xkb_keysym_t Symbol = xkb_state_key_get_one_sym(Context->XkbState, XkbScancode); | |
s32 BytesNeededToWrite = xkb_state_key_get_utf8(Context->XkbState, XkbScancode, Utf8Character, sizeof(Utf8Character)); | |
b32 IsPressed = (KeyState == WL_KEYBOARD_KEY_STATE_PRESSED); | |
char AsciiChar = Utf8Character[0]; | |
if (Symbol == XKB_KEY_Escape) | |
{ | |
Context->WantsToExit = 1; | |
} | |
} | |
// NOTE: Mouse events are generated in the WaylandCallbackPointer! | |
// Might need to be gathered or processed separately. | |
static void | |
WaylandCallbackUnused() // for C2X: use (...) | |
{ | |
// noop; | |
} | |
static void | |
WaylandCallbackKeyboardKeymap(void *Data, struct wl_keyboard *WlKeyboard, u32 Format, s32 Fd, u32 Size) | |
{ | |
wayland_context *Context = Data; | |
assert(Format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1); | |
char *MapShm = mmap(0, Size, PROT_READ, MAP_PRIVATE, Fd, 0); | |
assert(MapShm != MAP_FAILED); | |
Context->XkbKeymap = xkb_keymap_new_from_string(Context->XkbContext, MapShm, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); | |
munmap(MapShm, Size); | |
close(Fd); | |
Context->XkbState = xkb_state_new(Context->XkbKeymap); | |
} | |
static void | |
WaylandCallbackKeyboardKey(void *Data, struct wl_keyboard *WlKeyboard, u32 Serial, u32 Time, u32 Key, u32 State) | |
{ | |
// Store latest keypress for keyrepeat | |
wayland_context *ProgramState = Data; | |
if (Key == ProgramState->KeyCode && State == WL_KEYBOARD_KEY_STATE_RELEASED) | |
{ | |
// Released key that would be repeating | |
struct itimerspec DisabledTimer = { 0 }; | |
struct itimerspec Unused = { 0 }; | |
int ReturnValue = timerfd_settime(ProgramState->TimerFd, 0, &DisabledTimer, &Unused); | |
assert(ReturnValue == 0); | |
} | |
else if (State == WL_KEYBOARD_KEY_STATE_PRESSED) | |
{ | |
ProgramState->KeyCode = Key; | |
ProgramState->KeyState = State; | |
struct itimerspec Unused = { 0 }; | |
int ReturnValue = timerfd_settime(ProgramState->TimerFd, 0, &ProgramState->TimerProperties, &Unused); | |
assert(ReturnValue == 0); | |
} | |
ProcessKeyUpdate(Data, Key, State); | |
} | |
static void | |
WaylandCallbackKeyboardModifiers(void *Data, struct wl_keyboard *WlKeyboard, | |
u32 serial, u32 ModsDepressed, u32 ModsLatched, u32 ModsLocked, u32 Group) | |
{ | |
wayland_context *State = Data; | |
xkb_state_update_mask(State->XkbState, ModsDepressed, ModsLatched, ModsLocked, 0, 0, Group); | |
} | |
static void | |
WaylandCallbackKeyboardRepeatInfo(void *Data, struct wl_keyboard *WlKeyboard, s32 Rate, s32 Delay) | |
{ | |
wayland_context *State = Data; | |
u64 NsKeyDelay = (u64)Delay * (u64)1000llu * (u64)1000llu; | |
u64 NsKeyRepeat = ((u64)1000llu * (u64)1000llu * (u64)1000llu / (u64)Rate); | |
struct timespec InitialExpiration = { | |
.tv_nsec = (long int)NsKeyDelay, | |
}; | |
struct timespec PeriodicExpiration = { | |
.tv_nsec = (long int)NsKeyRepeat, | |
}; | |
struct itimerspec TimerProperties = { | |
.it_value = InitialExpiration, | |
.it_interval = PeriodicExpiration, | |
}; | |
State->TimerProperties = TimerProperties; | |
} | |
static struct wl_keyboard_listener GLOBALWlKeyboardListener = { | |
.keymap = WaylandCallbackKeyboardKeymap, | |
.enter = WaylandCallbackUnused, | |
.leave = WaylandCallbackUnused, | |
.key = WaylandCallbackKeyboardKey, | |
.modifiers = WaylandCallbackKeyboardModifiers, | |
.repeat_info = WaylandCallbackKeyboardRepeatInfo, | |
}; | |
static void | |
WaylandCallbackPointerEnter(void *Data, struct wl_pointer *WlPointer, | |
u32 Serial, struct wl_surface *WlSurface, | |
wl_fixed_t SurfaceX, wl_fixed_t SurfaceY) | |
{ | |
// Set default cursor | |
wayland_context *Context = Data; | |
wl_pointer_set_cursor(WlPointer, Serial, Context->WlCursorSurface, | |
(int)Context->WlCursorImage->hotspot_x, | |
(int)Context->WlCursorImage->hotspot_y); | |
} | |
static void | |
WaylandCallbackPointerMotion(void *Data, struct wl_pointer *WlPointer, | |
u32 Time, wl_fixed_t SurfaceX, wl_fixed_t SurfaceY) | |
{ | |
wayland_context *Context = Data; | |
int X = wl_fixed_to_int(SurfaceX); | |
int Y = wl_fixed_to_int(SurfaceY); | |
// Top left is (0, 0) | |
// TODO: Usage code | |
} | |
static void | |
WaylandCallbackPointerButton(void *Data, struct wl_pointer *WlPointer, | |
u32 Serial, u32 Time, u32 Button, u32 State) | |
{ | |
b32 Pressed = (State == WL_POINTER_BUTTON_STATE_PRESSED); | |
b32 Released = (State == WL_POINTER_BUTTON_STATE_RELEASED); | |
b32 IsLeftButton = (Button == BTN_LEFT); | |
b32 IsRightButton = (Button == BTN_RIGHT); | |
// Also available: MIDDLE, SIDE, FORWARD, BACK, ... | |
// TODO: Usage code | |
} | |
static struct wl_pointer_listener GLOBALWlPointerListener = { | |
.enter = WaylandCallbackPointerEnter, | |
.leave = WaylandCallbackUnused, | |
.motion = WaylandCallbackPointerMotion, | |
.button = WaylandCallbackPointerButton, | |
.axis = WaylandCallbackUnused, | |
.frame = WaylandCallbackUnused, | |
.axis_source = WaylandCallbackUnused, | |
.axis_stop = WaylandCallbackUnused, | |
.axis_discrete = WaylandCallbackUnused, | |
.axis_value120 = WaylandCallbackUnused, | |
.axis_relative_direction = WaylandCallbackUnused, | |
}; | |
static wayland_output * | |
WaylandFindMatchingOutput(wayland_context *Context, struct wl_output *ToFind) | |
{ | |
wayland_output *Output = 0; | |
b32 FoundMatchingWlOutput = 0; | |
for (u32 Index = 0; Index < ARRAYCOUNT(Context->WaylandOutputs); ++Index) | |
{ | |
Output = &(Context->WaylandOutputs[Index]); | |
if (Output->IsInitialized && Output->WlOutput == ToFind) | |
{ | |
FoundMatchingWlOutput = 1; | |
break; | |
} | |
} | |
assert(FoundMatchingWlOutput); | |
return Output; | |
} | |
static void | |
WaylandUpdateUsedRefreshRate(wayland_context *Context) | |
{ | |
f64 MaxUsedRefreshRateHz = 0; | |
for (u32 Index = 0; Index < ARRAYCOUNT(Context->WaylandOutputs); ++Index) | |
{ | |
wayland_output *Output = &(Context->WaylandOutputs[Index]); | |
if (Output->IsInitialized && Output->DisplaysWindow) | |
{ | |
MaxUsedRefreshRateHz = MAX(MaxUsedRefreshRateHz, Output->RefreshRateHz); | |
} | |
} | |
Context->WindowCurrentRefreshRate = MaxUsedRefreshRateHz; | |
} | |
static void | |
WaylandCallbackOutputMode(void *Data, struct wl_output *WlOutput, | |
u32 Flags, int Width, int Height, int Refresh) | |
{ | |
wayland_context *Context = Data; | |
wayland_output *Output = WaylandFindMatchingOutput(Context, WlOutput); | |
Output->RefreshRateHz = Refresh / 1000.0; // Refresh is in mHz | |
} | |
static struct wl_output_listener GLOBALWlOutputListener = { | |
.geometry = WaylandCallbackUnused, | |
.mode = WaylandCallbackOutputMode, | |
.done = WaylandCallbackUnused, | |
.scale = WaylandCallbackUnused, | |
.name = WaylandCallbackUnused, | |
.description = WaylandCallbackUnused, | |
}; | |
static void | |
WaylandCallbackSurfaceEnter(void *Data, struct wl_surface *WlSurface, struct wl_output *WlOutput) | |
{ | |
wayland_context *Context = Data; | |
wayland_output *Output = WaylandFindMatchingOutput(Context, WlOutput); | |
Output->DisplaysWindow = 1; | |
WaylandUpdateUsedRefreshRate(Context); | |
} | |
static void | |
WaylandCallbackSurfaceLeave(void *Data, struct wl_surface *WlSurface, struct wl_output *WlOutput) | |
{ | |
wayland_context *Context = Data; | |
wayland_output *Output = WaylandFindMatchingOutput(Context, WlOutput); | |
Output->DisplaysWindow = 0; | |
WaylandUpdateUsedRefreshRate(Context); | |
} | |
static struct wl_surface_listener GLOBALWlSurfaceListener = { | |
.enter = WaylandCallbackSurfaceEnter, | |
.leave = WaylandCallbackSurfaceLeave, | |
.preferred_buffer_scale = WaylandCallbackUnused, | |
.preferred_buffer_transform = WaylandCallbackUnused, | |
}; | |
static void | |
WaylandCallbackXdgToplevelConfigure(void *Data, struct xdg_toplevel *XdgToplevel, int32_t Width, int32_t Height, struct wl_array *States) | |
{ | |
wayland_context *Context = Data; | |
s32 WidthToUse = Width; | |
s32 HeightToUse = Height; | |
if (Width == 0) | |
{ | |
WidthToUse = Context->WindowWantsWidth; | |
} | |
if (Height == 0) | |
{ | |
HeightToUse = Context->WindowWantsHeight; | |
} | |
if (WidthToUse != Context->WindowCurrentWidth || | |
HeightToUse != Context->WindowCurrentHeight) | |
{ | |
Context->WindowCurrentWidth = WidthToUse; | |
Context->WindowCurrentHeight = HeightToUse; | |
Context->OpenGLNeedsResize = 1; | |
} | |
uint32_t *CurrentState; | |
wl_array_for_each(CurrentState, States) | |
{ | |
switch (*CurrentState) | |
{ | |
case XDG_TOPLEVEL_STATE_FULLSCREEN: | |
case XDG_TOPLEVEL_STATE_MAXIMIZED: | |
{ | |
Context->WindowFullscreen = 1; | |
} break; | |
} | |
} | |
} | |
static void | |
WaylandCallbackXdgToplevelClose(void *Data, struct xdg_toplevel *XdgToplevel) | |
{ | |
wayland_context *Context = Data; | |
Context->WantsToExit = 1; | |
} | |
static struct xdg_toplevel_listener GLOBALXdgToplevelListener = { | |
WaylandCallbackXdgToplevelConfigure, | |
WaylandCallbackXdgToplevelClose | |
}; | |
static void | |
WaylandCallbackXdgSurfaceConfigure(void *Data, struct xdg_surface *XdgSurface, uint32_t Serial) | |
{ | |
wayland_context *Context = Data; | |
if (Context->OpenGLNeedsResize) | |
{ | |
// Info on the last two parameters dx,dy | |
// https://lists.freedesktop.org/archives/wayland-devel/2014-May/014847.html | |
// For a floating, top-level window, dx/dy should cause the window to move. | |
// (Programmatically, not by the suer / server) | |
int DeltaX = 0; | |
int DeltaY = 0; | |
wl_egl_window_resize(Context->WlEglWindow, Context->WindowCurrentWidth, Context->WindowCurrentHeight, DeltaX, DeltaY); | |
// Consider performing | |
// Render(); eglSwapBuffers(Context->EglDisplay, Context->EglSurface); | |
// here | |
} | |
xdg_surface_ack_configure(XdgSurface, Serial); | |
} | |
static struct xdg_surface_listener GLOBALXdgSurfaceListener = { | |
WaylandCallbackXdgSurfaceConfigure | |
}; | |
static void | |
WaylandCallbackXdgBasePing(void *Data, struct xdg_wm_base *XdgBase, uint32_t Serial) | |
{ | |
xdg_wm_base_pong(XdgBase, Serial); | |
} | |
static struct xdg_wm_base_listener GLOBALXdgBaseListener = { | |
WaylandCallbackXdgBasePing | |
}; | |
static void | |
WaylandCallbackSeatCapabilities(void *Data, struct wl_seat *WlSeat, u32 Capabilities) | |
{ | |
// Assure that we can get input. | |
// This will fail for pointer if you e.g. temporarily have no monitors attached | |
//assert(Capabilities & WL_SEAT_CAPABILITY_KEYBOARD); | |
//assert(Capabilities & WL_SEAT_CAPABILITY_POINTER); | |
} | |
static struct wl_seat_listener GLOBALWlSeatListener = { | |
.capabilities = WaylandCallbackSeatCapabilities, | |
.name = WaylandCallbackUnused, | |
}; | |
static void | |
WaylandCallbackRegistryGlobalReceive(void *Data, struct wl_registry *WlRegistry, u32 Name, const char *Interface, u32 Version) | |
{ | |
wayland_context *Context = Data; | |
if ( ! strcmp(Interface, wl_compositor_interface.name)) | |
{ | |
Context->WlCompositor = wl_registry_bind(WlRegistry, Name, &wl_compositor_interface, Version); | |
} | |
else if ( ! strcmp(Interface, xdg_wm_base_interface.name)) | |
{ | |
Context->XdgBase = wl_registry_bind(WlRegistry, Name, &xdg_wm_base_interface, Version); | |
} | |
else if ( ! strcmp(Interface, wl_seat_interface.name)) | |
{ | |
Context->WlSeat = wl_registry_bind(WlRegistry, Name, &wl_seat_interface, Version); | |
wl_seat_add_listener(Context->WlSeat, &GLOBALWlSeatListener, Context); | |
} | |
else if ( ! strcmp(Interface, wl_shm_interface.name)) | |
{ | |
Context->WlShm = wl_registry_bind(WlRegistry, Name, &wl_shm_interface, Version); | |
} | |
else if ( ! strcmp(Interface, wl_output_interface.name)) | |
{ | |
b32 FoundEmptyOutputSlot = 0; | |
wayland_output *Output = 0; | |
for (u32 Index = 0; Index < ARRAYCOUNT(Context->WaylandOutputs); ++Index) | |
{ | |
Output = &(Context->WaylandOutputs[Index]); | |
if ( ! Output->IsInitialized) | |
{ | |
FoundEmptyOutputSlot = 1; | |
break; | |
} | |
} | |
assert(FoundEmptyOutputSlot); | |
Output->WlOutput = wl_registry_bind(WlRegistry, Name, &wl_output_interface, Version); | |
Output->IsInitialized = 1; | |
Output->NameID = Name; | |
wl_output_add_listener(Output->WlOutput, &GLOBALWlOutputListener, Context); | |
} | |
} | |
static void | |
WaylandCallbackRegistryGlobalRemove(void *Data, struct wl_registry *WlRegistry, u32 Name) | |
{ | |
wayland_context *Context = Data; | |
for (u32 Index = 0; Index < ARRAYCOUNT(Context->WaylandOutputs); ++Index) | |
{ | |
wayland_output *Output = &(Context->WaylandOutputs[Index]); | |
if (Output->NameID == Name) | |
{ | |
Output->IsInitialized = 0; | |
} | |
} | |
} | |
static struct wl_registry_listener GLOBALRegistryListener = { | |
.global = WaylandCallbackRegistryGlobalReceive, | |
.global_remove = WaylandCallbackRegistryGlobalRemove, | |
}; | |
static u64 | |
DeltaNs(struct timespec Start, struct timespec End) | |
{ | |
enum { NS_PER_SECOND = 1000000000ull }; | |
s64 NSec = End.tv_nsec - Start.tv_nsec; | |
s64 Sec = End.tv_sec - Start.tv_sec; | |
u64 Result = 0; | |
if (Sec > 0 && NSec < 0) | |
{ | |
Result = (u64)(NSec + NS_PER_SECOND); | |
} | |
else | |
{ | |
Result = (u64)NSec; | |
} | |
return Result; | |
} | |
int | |
main(int ArgumentCount, char **ArgumentStrings) | |
{ | |
// Wayland & Egl Window initialization | |
wayland_context Context = { 0 }; | |
{ | |
char *WindowTitle = "Wayland OpenGL Application"; | |
s32 DefaultWindowWidth = 1280; | |
s32 DefaultWindowHeight = 720; | |
// Important: Both "wants" and "Current" need to be set to the same value. | |
// Otherwise, the callbacks will call wl_egl_window_resize too early | |
// and cause a segmentation fault. | |
Context.WindowWantsWidth = DefaultWindowWidth; | |
Context.WindowWantsHeight = DefaultWindowHeight; | |
Context.WindowCurrentWidth = DefaultWindowWidth; | |
Context.WindowCurrentHeight = DefaultWindowHeight; | |
Context.WlDisplay = wl_display_connect(0); | |
Context.EglDisplay = eglGetDisplay(Context.WlDisplay); | |
eglInitialize(Context.EglDisplay, 0, 0); | |
EGLint ConfigAttributes[] = { | |
EGL_SURFACE_TYPE, EGL_WINDOW_BIT, | |
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, | |
EGL_RED_SIZE, 8, | |
EGL_GREEN_SIZE, 8, | |
EGL_BLUE_SIZE, 8, | |
EGL_ALPHA_SIZE, 8, | |
EGL_STENCIL_SIZE, 8, | |
EGL_NONE | |
}; | |
EGLConfig EglConfig; | |
EGLint ConfigNumberOfFrameBufferConfigurations; | |
EGLBoolean EglChooseConfigResult = eglChooseConfig(Context.EglDisplay, ConfigAttributes, &EglConfig, 1, &ConfigNumberOfFrameBufferConfigurations); | |
assert(EglChooseConfigResult == EGL_TRUE); | |
Context.WlRegistry = wl_display_get_registry(Context.WlDisplay); | |
wl_registry_add_listener(Context.WlRegistry, &GLOBALRegistryListener, &Context); | |
wl_display_dispatch(Context.WlDisplay); | |
wl_display_roundtrip(Context.WlDisplay); // I'm not sure this roundtrip is necessary | |
EGLint ContextAttributes[] = { | |
EGL_CONTEXT_CLIENT_VERSION, 3, | |
EGL_NONE | |
}; | |
eglBindAPI(EGL_OPENGL_API); | |
Context.EglContext = eglCreateContext(Context.EglDisplay, EglConfig, EGL_NO_CONTEXT, ContextAttributes); | |
Context.WlSurface = wl_compositor_create_surface(Context.WlCompositor); | |
wl_surface_add_listener(Context.WlSurface, &GLOBALWlSurfaceListener, &Context); | |
struct xdg_surface *XdgSurface = xdg_wm_base_get_xdg_surface(Context.XdgBase, Context.WlSurface); | |
xdg_surface_add_listener(XdgSurface, &GLOBALXdgSurfaceListener, &Context); | |
struct xdg_toplevel *XdgToplevel = xdg_surface_get_toplevel(XdgSurface); | |
xdg_toplevel_add_listener(XdgToplevel, &GLOBALXdgToplevelListener, &Context); | |
xdg_toplevel_set_title(XdgToplevel, WindowTitle); | |
wl_surface_commit(Context.WlSurface); | |
wl_display_dispatch(Context.WlDisplay); | |
wl_display_roundtrip(Context.WlDisplay); | |
Context.WlEglWindow = wl_egl_window_create(Context.WlSurface, DefaultWindowWidth, DefaultWindowHeight); | |
Context.EglSurface = eglCreateWindowSurface(Context.EglDisplay, EglConfig, Context.WlEglWindow, 0); | |
enum { VSYNC_DISABLE = 0, VSYNC_ENABLE = 1, }; | |
eglMakeCurrent(Context.EglDisplay, Context.EglSurface, Context.EglSurface, Context.EglContext); | |
eglSwapInterval(Context.EglDisplay, VSYNC_ENABLE); | |
eglSwapBuffers(Context.EglDisplay, Context.EglSurface); | |
wl_display_dispatch(Context.WlDisplay); | |
wl_display_roundtrip(Context.WlDisplay); | |
// Taken from https://bugaevc.gitbooks.io/writing-wayland-clients/content/beyond-the-black-square/cursors.html | |
char *CursorThemeName = 0; // same as "default" | |
char *CursorType = "left_ptr"; // Default cursor. Check wl_cursor[].name for possible entries | |
Context.WlCursorTheme = wl_cursor_theme_load(CursorThemeName, 24, Context.WlShm); // I have no idea where that magic 24 for the size is coming from. | |
Context.WlCursor = wl_cursor_theme_get_cursor(Context.WlCursorTheme, CursorType); | |
Context.WlCursorImage = Context.WlCursor->images[0]; | |
Context.WlCursorBuffer = wl_cursor_image_get_buffer(Context.WlCursorImage); | |
Context.WlCursorSurface = wl_compositor_create_surface(Context.WlCompositor); | |
wl_surface_attach(Context.WlCursorSurface, Context.WlCursorBuffer, 0, 0); | |
wl_surface_commit(Context.WlCursorSurface); | |
xdg_wm_base_add_listener(Context.XdgBase, &GLOBALXdgBaseListener, 0); | |
Context.XkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); | |
Context.WlKeyboard = wl_seat_get_keyboard(Context.WlSeat); | |
wl_keyboard_add_listener(Context.WlKeyboard, &GLOBALWlKeyboardListener, &Context); | |
Context.WlPointer = wl_seat_get_pointer(Context.WlSeat); | |
wl_pointer_add_listener(Context.WlPointer, &GLOBALWlPointerListener, &Context); | |
} // End of window initialization | |
// TODO: OpenGL usage code (shaders, textures, vertex buffer, uniforms, etc) | |
glClearColor(1.0, 0.0, 1.0, 1.0); | |
// Setup file descriptors for polling | |
Context.TimerFd = timerfd_create(CLOCK_MONOTONIC, 0); | |
assert(Context.TimerFd > 0); | |
struct pollfd FdToPoll = { .fd = Context.TimerFd, .events = POLLIN, }; | |
// NOTE: Main loop | |
while ( ! Context.WantsToExit) | |
{ | |
// Frame start | |
struct timespec FrameTimeStart; | |
clock_gettime(CLOCK_MONOTONIC_RAW, &FrameTimeStart); | |
// Event based loop | |
wl_display_dispatch_pending(Context.WlDisplay); | |
// Keyboard times | |
{ | |
FdToPoll.revents = 0; | |
poll(&FdToPoll, 1, 0); | |
b32 TimerFired = (FdToPoll.revents == POLLIN); | |
if (TimerFired) | |
{ | |
u64 TimersElapsed = 0; | |
read(Context.TimerFd, &TimersElapsed, sizeof(TimersElapsed)); | |
for (u64 Index = 0; Index < TimersElapsed; ++Index) | |
{ | |
ProcessKeyUpdate(&Context, Context.KeyCode, Context.KeyState); | |
} | |
} | |
} | |
// Render | |
f64 FrameTimeDelta = 1.0 / Context.WindowCurrentRefreshRate; // Assume constant frame delta | |
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |
// Frame End | |
struct timespec FrameTimeEnd; | |
clock_gettime(CLOCK_MONOTONIC_RAW, &FrameTimeEnd); | |
// Debug performance output, push stats into next frame | |
{ | |
u64 DeltaInNs = DeltaNs(FrameTimeStart, FrameTimeEnd); | |
f64 DeltaInMs = (f64)DeltaInNs / 1000000.0; | |
u32 FPS = (u32)(1000.0 / DeltaInMs); | |
// TODO: Usage code here | |
} | |
// If vsync: Block until frame is drawn | |
eglSwapBuffers(Context.EglDisplay, Context.EglSurface); | |
} | |
// NOTE: Automatic cleanup on process exit | |
return 0; | |
} |
This file contains 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
#!/bin/sh | |
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
cd $PROJECT_DIR | |
mkdir -p ./build | |
mkdir -p ./generated | |
if ! [ -f ./xdg_shell.c ]; then | |
wayland-scanner private-code < /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml > ./generated/xdg_shell.c | |
fi | |
if ! [ -f ./xdg_shell.h ]; then | |
wayland-scanner client-header < /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml > ./generated/xdg_shell.h | |
fi | |
WARNINGS="-Wextra -Wall -Werror -pedantic -std=c17 -Wformat=2 -Wformat-truncation -Wundef -Wdouble-promotion -Wshadow -Wpointer-arith -Wcast-align -Wfloat-conversion -Wsign-conversion" | |
DISABLEDWARNINGS="-Wno-unused-variable -Wno-unused-function -Wno-unused-parameter -Wno-missing-field-initializers" | |
LIBRARIES="-lwayland-egl -lwayland-client -lwayland-cursor -lEGL -lGL -lxkbcommon -lrt -lm" | |
DEBUG="-O0 -g" | |
RELEASE="-O3" | |
gcc 00_wayland_opengl_main.c -o ./build/template_opengl_wayland_debug $DEBUG $WARNINGS $DISABLEDWARNINGS $LIBRARIES & | |
gcc 00_wayland_opengl_main.c -o ./build/template_opengl_wayland $RELEASE $WARNINGS $DISABLEDWARNINGS $LIBRARIES & | |
wait |
This file contains 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
Resources: | |
- SDL | |
- uwindow (https://github.com/lennyerik/uwindow) (this helped tremendiously find an OpenGL init-bug of mine, which lengthened init time) | |
- https://wayland.app/ | |
- Focus editor (https://github.com/focus-editor/focus) | |
- https://lists.freedesktop.org/archives/wayland-devel/2014-May/014847.html (wl_egl_window_resize Info) | |
- https://bugaevc.gitbooks.io/writing-wayland-clients/content/beyond-the-black-square/cursors.html (Wayland Cursor Display) | |
- Weston Client example | |
- man pages | |
- https://wayland-book.com/ | |
- probably more which I can't recall right now | |
There's probably a lot of room for improvement / room for error checking in the example code, but this should be sufficient to get up and running. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment