From 2b9389ae016c8048a16ba033f27ca5c256985e4d Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 11 Jan 2024 20:18:34 +0100 Subject: Display windows with minimal delay, locked to 60 fps --- src/compositor.c | 150 +++++++++++++++++++++++++++++++++++++++++ src/compositor.h | 31 +++++++++ src/main.c | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/window_texture.c | 172 +++++++++++++++++++++++++++++++++++++++++++++++ src/window_texture.h | 32 +++++++++ 5 files changed, 570 insertions(+) create mode 100644 src/compositor.c create mode 100644 src/compositor.h create mode 100644 src/main.c create mode 100644 src/window_texture.c create mode 100644 src/window_texture.h (limited to 'src') diff --git a/src/compositor.c b/src/compositor.c new file mode 100644 index 0000000..4b622c0 --- /dev/null +++ b/src/compositor.c @@ -0,0 +1,150 @@ +#include "compositor.h" +#include +#include +#include +#include + +static double clock_get_monotonic_seconds(void) { + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 0; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001; +} + +static ngxc_window* ngxc_compositor_get_window_by_id(ngxc_compositor *self, Window window) { + for(int i = 0; i < self->num_windows; ++i) { + ngxc_window *window_obj = &self->windows[i]; + if(window_obj->texture.window == window) + return window_obj; + } + return NULL; +} + +void ngxc_compositor_init(ngxc_compositor *self, Display *dpy, Window composite_window) { + memset(self, 0, sizeof(*self)); + self->dpy = dpy; + self->composite_window = composite_window; + self->frame_timer = clock_get_monotonic_seconds(); +} + +void ngxc_compositor_deinit(ngxc_compositor *self) { + +} + +void ngxc_compositor_add_window(ngxc_compositor *self, Window window) { + if(self->num_windows == NGXC_COMPOSITOR_MAX_WINDOWS) { + fprintf(stderr, "error: reached max number of trackable windows (%d), ignoring window with id %ld\n", NGXC_COMPOSITOR_MAX_WINDOWS, window); + return; + } + + XWindowAttributes xattr; + if(!XGetWindowAttributes(self->dpy, window, &xattr)) { + fprintf(stderr, "error: failed to get window attributes, ignoring window with id %ld\n", window); + return; + } + + //Window c; + //XTranslateCoordinates(self->dpy, window, DefaultRootWindow(self->dpy), 0, 0, &xattr.x, &xattr.y, &c); + + ngxc_window *window_obj = &self->windows[self->num_windows]; + if(window_texture_init(&window_obj->texture, self->dpy, window) != 0) { + fprintf(stderr, "error: failed to create window texture, ignoring window with id %ld\n", window); + return; + } + + window_obj->x = xattr.x; + window_obj->y = xattr.y; + ++self->num_windows; + + fprintf(stderr, "ADDED WINDOW: %ld, x: %d, y: %d\n", window, xattr.x, xattr.y); +} + +void ngxc_compositor_remove_window(ngxc_compositor *self, Window window) { + for(int i = 0; i < self->num_windows; ++i) { + if(self->windows[i].texture.window != window) + continue; + + window_texture_deinit(&self->windows[i].texture); + for(int j = i + 1; j < self->num_windows; ++j) { + self->windows[j - 1] = self->windows[j]; + } + break; + } +} + +static void render_texture(const ngxc_window *window) { + float matrix[16] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f, + }; + + matrix[0] = 1.0f / (float)window->texture.width; + matrix[5] = 1.0f / (float)window->texture.height; + + glMatrixMode(GL_TEXTURE); + glLoadMatrixf(matrix); + glMatrixMode(GL_MODELVIEW); + glBindTexture(GL_TEXTURE_2D, window->texture.texture_id); + + glColor4ub(255, 255, 255, 255); + //glTranslatef(window->x, window->y, 0.0f); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); + glVertex2f(window->x, window->y); + + glTexCoord2f(window->texture.width, 0.0f); + glVertex2f(window->x + window->texture.width, window->y); + + glTexCoord2f(window->texture.width, window->texture.height); + glVertex2f(window->x + window->texture.width, window->y + window->texture.height); + + glTexCoord2f(0.0f, window->texture.height); + glVertex2f(window->x, window->y + window->texture.height); + glEnd(); + glLoadIdentity(); + + glBindTexture(GL_TEXTURE_2D, 0); +} + +/* TODO: Skip windows that are not visible on the screen */ +/* TODO: Disable compositing for fullscreen windows, if that option is enabled */ +void ngxc_compositor_render(ngxc_compositor *self) { + glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + for(int i = 0; i < self->num_windows; ++i) { + const ngxc_window *window_obj = &self->windows[i]; + render_texture(window_obj); + } + + const double now = clock_get_monotonic_seconds(); + const double frame_duration = now - self->frame_timer; + + glXSwapBuffers(self->dpy, self->composite_window); + glFlush(); + glFinish(); + + const double time_limit = 1.0f / 60.0f; // TODO: Get from randr + const double delayed_update = time_limit - frame_duration - 0.002; + //fprintf(stderr, "delayed update: %f\n", delayed_update); + if(delayed_update > 0.0) + usleep(delayed_update * 1000000.0); + + self->frame_timer = clock_get_monotonic_seconds(); +} + +void ngxc_compositor_on_configure(ngxc_compositor *self, const XConfigureEvent *configure_event) { + ngxc_window *window_obj = ngxc_compositor_get_window_by_id(self, configure_event->window); + if(!window_obj) + return; + + window_obj->x = configure_event->x; + window_obj->y = configure_event->y; + + if(configure_event->width != window_obj->texture.width || configure_event->height != window_obj->texture.height) { + window_texture_on_resize(&window_obj->texture); + } +} diff --git a/src/compositor.h b/src/compositor.h new file mode 100644 index 0000000..3d8d876 --- /dev/null +++ b/src/compositor.h @@ -0,0 +1,31 @@ +#ifndef NGXC_COMPOSITOR_H +#define NGXC_COMPOSITOR_H + +#include "window_texture.h" + +// TODO: Make dynamic +#define NGXC_COMPOSITOR_MAX_WINDOWS 100 + +typedef struct { + WindowTexture texture; + int x; + int y; +} ngxc_window; + +typedef struct { + Display *dpy; + Window composite_window; + double frame_timer; + ngxc_window windows[NGXC_COMPOSITOR_MAX_WINDOWS]; + int num_windows; +} ngxc_compositor; + +void ngxc_compositor_init(ngxc_compositor *self, Display *dpy, Window composite_window); +void ngxc_compositor_deinit(ngxc_compositor *self); + +void ngxc_compositor_add_window(ngxc_compositor *self, Window window); +void ngxc_compositor_remove_window(ngxc_compositor *self, Window window); +void ngxc_compositor_render(ngxc_compositor *self); +void ngxc_compositor_on_configure(ngxc_compositor *self, const XConfigureEvent *configure_event); + +#endif /* NGXC_COMPOSITOR_H */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d660e1d --- /dev/null +++ b/src/main.c @@ -0,0 +1,185 @@ +#include "compositor.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +static void fatal_if(bool condition, const char *fmt, ...) { + if(!condition) + return; + + va_list args; + va_start(args, fmt); + fprintf(stderr, "fatal: "); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); + exit(1); +} + +static int x_error_handler(Display *dpy, XErrorEvent *ee) { + (void)dpy; + (void)ee; + return 0; +} + +static void glx_context_choose(Display *dpy, GLXFBConfig **configs, XVisualInfo **visual_info) { + const int visual_attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_DOUBLEBUFFER, True, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, + GLX_ALPHA_SIZE, 0, + GLX_DEPTH_SIZE, 0, + None, + }; + + int c; + *configs = glXChooseFBConfig(dpy, 0, visual_attribs, &c); + fatal_if(!configs, "failed to choose fb config"); + + for (int i = 0; i < c; i++) { + GLXFBConfig *config = configs[i]; + XVisualInfo *visual = glXGetVisualFromFBConfig(dpy, *config); + if(!visual) + continue; + + /*if(attr.depth != visual->depth) { + XFree(visual); + continue; + }*/ + + *visual_info = visual; + *configs = config; + return; + } + + fatal_if(true, "failed to find a valid fb config"); +} + +static void allow_input_passthrough(Display *dpy, Window w) { + XserverRegion region = XFixesCreateRegion(dpy, NULL, 0); + + XFixesSetWindowShapeRegion(dpy, w, ShapeBounding, 0, 0, 0); + XFixesSetWindowShapeRegion(dpy, w, ShapeInput, 0, 0, region); + + XFixesDestroyRegion (dpy, region); +} + +static bool is_compositor_running(Display *dpy) { + Atom prop_atom = XInternAtom(dpy, "_NET_WM_CM_S0", False); + return XGetSelectionOwner(dpy, prop_atom) != None; +} + +static void set_compositor_owner(Display *dpy, Window window) { + Atom prop_atom = XInternAtom(dpy, "_NET_WM_CM_S0", False); + XSetSelectionOwner(dpy, prop_atom, window, CurrentTime); +} + +static bool x11_supports_composite_named_window_pixmap(Display *display) { + int extension_major; + int extension_minor; + if(!XCompositeQueryExtension(display, &extension_major, &extension_minor)) + return false; + + int major_version; + int minor_version; + return XCompositeQueryVersion(display, &major_version, &minor_version) && (major_version > 0 || minor_version >= 2); +} + +int main(int argc, char **argv) { + /* Make nvidia act reasonably */ + setenv("__GL_MaxFramesAllowed", "1", true); + setenv("__GL_YIELD", "", true); + + Display *dpy = XOpenDisplay(NULL); + fatal_if(!dpy, "failed to connect to the X server"); + XSetErrorHandler(x_error_handler); + + fatal_if(!x11_supports_composite_named_window_pixmap(dpy), "your X server server is too old (Xcomposite extension version is less than 0.2)"); + fatal_if(is_compositor_running(dpy), "another compositor is already running"); + + const Window root_window = DefaultRootWindow(dpy); + XSelectInput(dpy, root_window, SubstructureNotifyMask); + + GLXFBConfig *fb_config; + XVisualInfo *xvisual_info; + glx_context_choose(dpy, &fb_config, &xvisual_info); + GLXContext glx_context = glXCreateNewContext(dpy, *fb_config, GLX_RGBA_TYPE, NULL, True); + fatal_if(!glx_context, "failed to create glx context"); + + //Colormap colormap = XCreateColormap(dpy, root_window, xvisual_info->visual, AllocNone); + //fatal_if(!colormap, "failed to create colormap"); + + const Window overlay_window = XCompositeGetOverlayWindow(dpy, root_window); + fatal_if(!overlay_window, "failed to get overlay window"); + allow_input_passthrough(dpy, overlay_window); + + // TODO: Create one window for each monitor + const int composite_window_width = WidthOfScreen(DefaultScreenOfDisplay(dpy)); + const int composite_window_height = HeightOfScreen(DefaultScreenOfDisplay(dpy)); + const Window composite_window = XCreateWindow(dpy, overlay_window, 0, 0, composite_window_width, composite_window_height, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); + fatal_if(!composite_window, "failed to create X window"); + // TODO: Set window title to ngxc and other properties that are needed + set_compositor_owner(dpy, composite_window); + + glXMakeContextCurrent(dpy, composite_window, composite_window, glx_context); + + glXSwapIntervalEXT(dpy, composite_window, true); + glEnable(GL_TEXTURE_2D); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + allow_input_passthrough(dpy, composite_window); + XMapWindow(dpy, composite_window); + XFlush(dpy); + + // TODO: Also do this on monitor reconfigure + glViewport(0, 0, composite_window_width, composite_window_height); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0.0, composite_window_width, composite_window_height, 0.0, 0.0, 1.0); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + ngxc_compositor compositor; + ngxc_compositor_init(&compositor, dpy, composite_window); + + Window root_return, parent_return; + Window *children = NULL; + unsigned int num_children = 0; + XQueryTree(dpy, root_window, &root_return, &parent_return, &children, &num_children); + + for(unsigned int i = 0; i < num_children; ++i) { + ngxc_compositor_add_window(&compositor, children[i]); + } + + if(children) + XFree(children); + + XEvent xev; + for(;;) { + while(XPending(dpy)) { + XNextEvent(dpy, &xev); + switch(xev.type) { + case ConfigureNotify: { + /* TODO: Instead of doing this potentially multiple times at once, only do this for the last configure notify event for each window */ + ngxc_compositor_on_configure(&compositor, &xev.xconfigure); + break; + } + default: + break; + } + } + ngxc_compositor_render(&compositor); + } + + return 0; +} diff --git a/src/window_texture.c b/src/window_texture.c new file mode 100644 index 0000000..c3bde72 --- /dev/null +++ b/src/window_texture.c @@ -0,0 +1,172 @@ +#include "window_texture.h" +#include +#include + +int window_texture_init(WindowTexture *window_texture, Display *display, Window window) { + window_texture->display = display; + window_texture->window = window; + window_texture->pixmap = None; + window_texture->glx_pixmap = None; + window_texture->texture_id = 0; + window_texture->redirected = 0; + window_texture->width = 0; + window_texture->height = 0; + + XCompositeRedirectWindow(display, window, CompositeRedirectManual); + window_texture->redirected = 1; + return window_texture_on_resize(window_texture); +} + +static void window_texture_cleanup(WindowTexture *self, int delete_texture) { + if(delete_texture && self->texture_id) { + glDeleteTextures(1, &self->texture_id); + self->texture_id = 0; + } + + if(self->glx_pixmap) { + glXDestroyPixmap(self->display, self->glx_pixmap); + glXReleaseTexImageEXT(self->display, self->glx_pixmap, GLX_FRONT_EXT); + self->glx_pixmap = None; + } + + if(self->pixmap) { + XFreePixmap(self->display, self->pixmap); + self->pixmap = None; + } +} + +void window_texture_deinit(WindowTexture *self) { + if(self->redirected) { + XCompositeUnredirectWindow(self->display, self->window, CompositeRedirectManual); + self->redirected = 0; + } + window_texture_cleanup(self, 1); +} + +int window_texture_on_resize(WindowTexture *self) { + window_texture_cleanup(self, 0); + + int result = 0; + GLXFBConfig *configs = NULL; + Pixmap pixmap = None; + GLXPixmap glx_pixmap = None; + GLuint texture_id = 0; + int glx_pixmap_bound = 0; + + const int pixmap_config[] = { + GLX_BIND_TO_TEXTURE_RGB_EXT, True, + GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT | GLX_WINDOW_BIT, + GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT, + /*GLX_BIND_TO_MIPMAP_TEXTURE_EXT, True,*/ + GLX_BUFFER_SIZE, 24, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, + GLX_ALPHA_SIZE, 0, + None + }; + + const int pixmap_attribs[] = { + GLX_TEXTURE_TARGET_EXT, GLX_TEXTURE_2D_EXT, + GLX_TEXTURE_FORMAT_EXT, GLX_TEXTURE_FORMAT_RGB_EXT, + /*GLX_MIPMAP_TEXTURE_EXT, True,*/ + None + }; + + XWindowAttributes attr; + if (!XGetWindowAttributes(self->display, self->window, &attr)) { + fprintf(stderr, "Failed to get window attributes\n"); + return 1; + } + + GLXFBConfig config; + int c; + configs = glXChooseFBConfig(self->display, 0, pixmap_config, &c); + if(!configs) { + fprintf(stderr, "Failed to choose fb config\n"); + return 1; + } + + int found = 0; + for (int i = 0; i < c; i++) { + config = configs[i]; + XVisualInfo *visual = glXGetVisualFromFBConfig(self->display, config); + if (!visual) + continue; + + if (attr.depth != visual->depth) { + XFree(visual); + continue; + } + XFree(visual); + found = 1; + break; + } + + if(!found) { + fprintf(stderr, "No matching fb config found\n"); + result = 1; + goto cleanup; + } + + pixmap = XCompositeNameWindowPixmap(self->display, self->window); + if(!pixmap) { + result = 2; + goto cleanup; + } + + glx_pixmap = glXCreatePixmap(self->display, config, pixmap, pixmap_attribs); + if(!glx_pixmap) { + result = 3; + goto cleanup; + } + + if(self->texture_id == 0) { + glGenTextures(1, &texture_id); + if(texture_id == 0) { + result = 4; + goto cleanup; + } + glBindTexture(GL_TEXTURE_2D, texture_id); + } else { + glBindTexture(GL_TEXTURE_2D, self->texture_id); + } + + glXBindTexImageEXT(self->display, glx_pixmap, GLX_FRONT_EXT, NULL); + glx_pixmap_bound = 1; + + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + + /* + float fLargest = 0.0f; + glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fLargest); + */ + + glBindTexture(GL_TEXTURE_2D, 0); + + self->width = attr.width; + self->height = attr.height; + + XFree(configs); + self->pixmap = pixmap; + self->glx_pixmap = glx_pixmap; + if(texture_id != 0) + self->texture_id = texture_id; + return 0; + + cleanup: + if(texture_id != 0) glDeleteTextures(1, &texture_id); + if(glx_pixmap) glXDestroyPixmap(self->display, glx_pixmap); + if(glx_pixmap_bound) glXReleaseTexImageEXT(self->display, glx_pixmap, GLX_FRONT_EXT); + if(pixmap) XFreePixmap(self->display, pixmap); + if(configs) XFree(configs); + return result; +} + +GLuint window_texture_get_opengl_texture_id(WindowTexture *self) { + return self->texture_id; +} diff --git a/src/window_texture.h b/src/window_texture.h new file mode 100644 index 0000000..39826bc --- /dev/null +++ b/src/window_texture.h @@ -0,0 +1,32 @@ +#ifndef NGXC_WINDOW_TEXTURE_H +#define NGXC_WINDOW_TEXTURE_H + +#define GLX_GLXEXT_PROTOTYPES +#include +#include +#include + +typedef struct { + Display *display; + Window window; + Pixmap pixmap; + GLXPixmap glx_pixmap; + GLuint texture_id; + int redirected; + int width; + int height; +} WindowTexture; + +/* Returns 0 on success */ +int window_texture_init(WindowTexture *window_texture, Display *display, Window window); +void window_texture_deinit(WindowTexture *self); + +/* + This should ONLY be called when the target window is resized. + Returns 0 on success. +*/ +int window_texture_on_resize(WindowTexture *self); + +GLuint window_texture_get_opengl_texture_id(WindowTexture *self); + +#endif /* NGXC_WINDOW_TEXTURE_H */ -- cgit v1.2.3