/* Get an X11 window on the screen and displaying stuff in the * simplest way possible. Unfortunately, this is X11, so it isn’t * very simple; remember xiafpos. * * This program takes a different approach from xiafpos (q.v.) in that * it uses the X Shared Memory extension in order to provide, as far * as possible, just a dumb framebuffer interface. * * See xshμ.README.md for more details. * * Doesn’t handle window resizes or copy-and-paste or drag-and-drop * yet. * * Since I don’t have the X11 manual handy, much of the code closely * follows the z3d.l library from picoLisp, which bears the following * copyright notice: * * PicoLisp Copyright (c) Software Lab. Alexander Burger * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * (End of copyright notice.) * * This program is copyright (c) Kragen Javier Sitaker, who is me. I * license this program under the same terms as PicoLisp. */ #include #include #include #include #include #include #include #include #include #include #include "xshmu.h" static void default_error_handler(const char *msg) { fprintf(stderr, "xshμ: %s\n", msg); abort(); } xshmu_error_handler_t xshmu_error_handler = default_error_handler; /* The type xshmu_event, declared but not defined in xshmu.h. */ struct xshmu_event { enum { xshmu_raw = 0, xshmu_key, xshmu_mouse, xshmu_die } type; union { xshmu_key_event key; xshmu_mouse_event mouse; xshmu_raw_event raw; } body; }; /* The destination type of the opaque pointer type xshmu, declared but * not defined in xshmu.h */ struct _xshmu { Display *display; int screen; Colormap colormap; int depth, pixsize, width, height; GC gc; Window window; XImage *image; XShmSegmentInfo shm_segment_info; Atom WM_PROTOCOLS, WM_DELETE_WINDOW; XPixmapFormatValues *formats; // XXX not even using this! int fbsize; // saved for debugging /* The most recently received event. */ XEvent xevent; xshmu_event event; uint8_t has_event; // True if there’s an unprocessed event in event uint8_t valid; // True if client called xshmu_framebuffer after xshmu_flush char keybuf[16]; }; xshmu xshmu_open(const char *name, int width, int height, const char *options) { xshmu w = malloc(sizeof(*w)); char *err = "xshmu_open malloc failed"; if (!w) goto fail; memset(w, 0, sizeof(*w)); err = "XOpenDisplay failed"; if (!(w->display = XOpenDisplay(NULL))) goto fail; w->screen = DefaultScreen(w->display); w->colormap = DefaultColormap(w->display, w->screen); int n_pixel_formats = 0; w->formats = XListPixmapFormats(w->display, &n_pixel_formats); err = "no pixel formats"; if (n_pixel_formats == 0) goto fail; int best_index = 0; for (int i = 1; i < n_pixel_formats; i++) { if (w->formats[i].depth > w->formats[best_index].depth) best_index = i; } /* XXX the depth must match the depth of the drawable, i.e. the window */ /* XCreateWindow allows you to specify the window depth, but also requires you to specify the class (InputOutput), the Visual, the valuemask (CWcrap; xfe uses CWBackPixel | CWBorderPixel | CWBitGravity | CWEventMask | CWColormap), and the XSetWindowAttributes (which includes .background_pixel, .border_pixel, .bit_gravity, .event_mask, and .colormap (XDefaultColormap(display, screen)), but can otherwise be zero). */ w->depth = 24;//formats[best_index].depth; w->pixsize = 4;//(formats[best_index].bits_per_pixel + 7) / 8; w->width = width; w->height = height; w->window = XCreateSimpleWindow(w->display, RootWindow(w->display, w->screen), 0, 0, width, height, 0, /* border_width */ BlackPixel(w->display, w->screen), WhitePixel(w->display, w->screen)); XStoreName(w->display, w->window, name); w->WM_DELETE_WINDOW = XInternAtom(w->display, "WM_DELETE_WINDOW", True); w->WM_PROTOCOLS = XInternAtom(w->display, "WM_PROTOCOLS", True); err = "XSetWMProtocols"; if (!XSetWMProtocols(w->display, w->window, &w->WM_DELETE_WINDOW, 1)) goto fail; XMapWindow(w->display, w->window); /* Apparently StructureNotifyMask is what you use to get MapNotify * events? */ XSelectInput(w->display, w->window, KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask ); w->gc = XCreateGC(w->display, RootWindow(w->display, w->screen), 0, NULL); /* XXX maybe only allocate a buffer for the window size, if the window is smaller than requested? This seems to require waiting for the MapNotify. */ err = "no XShm"; if (!XShmQueryExtension(w->display)) goto fail; /* Now wait for the window to open so that we can draw on it safely */ w->xevent.type = 0; while (w->xevent.type != MapNotify) { xshmu_wait(w); xshmu_get_event(w); } /* We seem to always have final geometry at this point; testing at * earlier points usually works but not always — there seems to be a * race condition. Maybe we should do the test in xshmu_framebuffer * and allocate the shared memory segment there instead of here? */ return w; fail: xshmu_close(w); xshmu_error_handler(err); return NULL; } /* Factored out because it gets called on close and on resize */ static void destroy_framebuffer(xshmu w) { if (w->shm_segment_info.shmaddr && w->shm_segment_info.shmaddr != (char*)-1) { /* XXX use intptr_t */ shmdt(w->shm_segment_info.shmaddr); w->shm_segment_info.shmaddr = 0; } if (w->image) XDestroyImage(w->image); w->image = 0; } void xshmu_close(xshmu w) { if (!w) return; destroy_framebuffer(w); free(w->formats); if (w->gc) XFreeGC(w->display, w->gc); if (w->display) { XCloseDisplay(w->display); w->display = 0; } free(w); } xshmu_pic xshmu_subpic(xshmu_pic start, int x, int y, int w, int h) { if (x < 0) x = 0; if (y < 0) y = 0; if (w > start.w - x) w = start.w - x; if (h > start.h - y) h = start.h - y; if (w < 0) w = 0; if (h < 0) h = 0; xshmu_pic result = { start.p + y*start.stride + x, w, start.stride, h }; return result; } void xshmu_copy(xshmu_pic dest, xshmu_pic src) { int w = src.w, h = src.h; if (w > dest.w) w = dest.w; if (h > dest.h) h = dest.h; if ((size_t)((uintptr_t)src.p - (uintptr_t)dest.p) <= src.stride * (src.h + 1)) { // Possible vertical overlap requires copying upwards for (int y = h-1; y >= 0; y--) { memmove(&dest.p[y * dest.stride], &src.p[y * src.stride], w * 4); } } else { for (int y = 0; y < h; y++) { memmove(&dest.p[y * dest.stride], &src.p[y * src.stride], w * 4); } } } xshmu_pic xshmu_canvas(int w, int h) { xshmu_pic result = { malloc(w*h*4), w, w, h }; if (!result.p) xshmu_error_handler("canvas malloc"); return result; } static int setup_framebuffer(xshmu w, int width, int height) { // If we have an existing framebuffer (presumably before a window // resize event) we need to clean it up first. if (w->image) destroy_framebuffer(w); // XXX are we guaranteed that the Window was created with the default visual? w->image = XShmCreateImage(w->display, DefaultVisual(w->display, w->screen), w->depth, ZPixmap, NULL, &w->shm_segment_info, width, height); char *err = "XShmCreateImage"; if (!w->image) goto fail; w->fbsize = width * height * w->pixsize; w->shm_segment_info.shmid = shmget(IPC_PRIVATE, w->fbsize, IPC_CREAT | 0600); err = "shmget"; if (w->shm_segment_info.shmid < 0) goto fail; w->shm_segment_info.shmaddr = shmat(w->shm_segment_info.shmid, 0, 0); err = "shmat"; if (w->shm_segment_info.shmaddr == (char*)-1) goto fail; /* XXX use intptr_t */ w->image->data = w->shm_segment_info.shmaddr; err = "XShmAttach"; if (!XShmAttach(w->display, &w->shm_segment_info)) goto fail; w->width = width; w->height = height; return 1; fail: xshmu_error_handler(err); return 0; } xshmu_pic xshmu_framebuffer(xshmu w) { xshmu_pic failure = {0}; XWindowAttributes attrs; /* So far I haven’t seen this fail, but in theory it can; presumably * if the window had been destroyed, for example */ if (!XGetWindowAttributes(w->display, w->window, &attrs)) { xshmu_error_handler("XGetWindowAttributes"); return failure; } /* We might have no shared-memory framebuffer or one with a * different size; in those cases we need to make a new one. */ if (!w->image || w->width != attrs.width || w->height != attrs.height) { if (!setup_framebuffer(w, attrs.width, attrs.height)) return failure; } // This check saves you from the common SDL bug where you draw a // bunch of stuff and nothing shows up on the screen because you // never called flip(): if (w->valid) { xshmu_error_handler("xshmu_flush wasn’t called"); return failure; } w->valid = 1; xshmu_pic result = { (uint32_t*)w->shm_segment_info.shmaddr, w->width, w->width, w->height }; return result; } static void mouse_event(xshmu_event *ev, int x, int y, int state) { ev->type = xshmu_mouse; /* button 1 is 0x100, 2 is 0x200, 3 is 0x400, 4 (wheel up) 0x800, 5 (wheel down) 0x1000 */ xshmu_mouse_event mev = { .x = x, .y = y, .buttons = (state >> 8) & 0x1f }; ev->body.mouse = mev; } /** Read an event from X (in blocking mode) and translate it to an `xshmu_event` * * This is invoked both from `xshmu_wait` (since that’s the way X * gives us to wait for an event) and from `xshmu_get_event` (when * there isn’t an event waiting for us from `xshmu_wait`.) */ static void process_pending_event(xshmu w) { xshmu_event *ev = &w->event; memset(ev, 0, sizeof(w->event)); /* sets event type to xshmu_raw */ XNextEvent(w->display, &w->xevent); int type = w->xevent.type; if (type == MotionNotify) { XMotionEvent *mev = &w->xevent.xmotion; mouse_event(ev, mev->x, mev->y, mev->state); } else if (type == ButtonPress || type == ButtonRelease) { XButtonEvent *bev = &w->xevent.xbutton; mouse_event(ev, bev->x, bev->y, bev->state); ev->body.mouse.buttons ^= 1 << (bev->button - 1); } else if (type == KeyPress || type == KeyRelease) { xshmu_key_event kev = { .s = &w->keybuf[0], .down = (type == KeyPress) }; KeySym keysym; kev.len = XLookupString(&w->xevent.xkey, kev.s, sizeof(w->keybuf)-1, &keysym, NULL); kev.s[kev.len] = 'E'; kev.keysym = keysym; ev->type = xshmu_key; ev->body.key = kev; } else if (type == ClientMessage) { XClientMessageEvent *cm = &w->xevent.xclient; if (cm->message_type == w->WM_PROTOCOLS && cm->format == 32 /* 32-bit words */ && cm->data.l[0] == w->WM_DELETE_WINDOW) { ev->type = xshmu_die; } } else { ev->type = xshmu_raw; ev->body.raw = &w->xevent; } } void xshmu_wait(xshmu w) { /* Note that calling this twice in a row without calling `xshmu_get_event` in between will drop events. This is intentional. */ process_pending_event(w); w->has_event = 1; } xshmu_event * xshmu_get_event(xshmu w) { if (w->has_event) { w->has_event = 0; return &w->event; } if (!XPending(w->display)) { return NULL; } // Though this is a blocking call, XPending returned true, so it’s // guaranteed not to block now. I hope. process_pending_event(w); return &w->event; } xshmu_mouse_event * xshmu_as_mouse_event(xshmu_event *ev) { return (ev->type == xshmu_mouse) ? &ev->body.mouse : NULL; } xshmu_key_event * xshmu_as_key_event(xshmu_event *ev) { return (ev->type == xshmu_key) ? &ev->body.key : NULL; } xshmu_raw_event * xshmu_as_raw_event(xshmu_event *ev) { return (ev->type == xshmu_raw) ? &ev->body.raw : NULL; } xshmu_die_event xshmu_as_die_event(xshmu_event *ev) { return ev->type == xshmu_die; } void xshmu_flush(xshmu w) { if (!w->valid) { xshmu_error_handler("xshmu_framebuffer wasn’t called"); return; } XShmPutImage(w->display, w->window, w->gc, w->image, 0, 0, 0, 0, w->width, w->height, False); XSync(w->display, False); // Cabbage the framebuffer to force clients to redraw. // This is costly: it costs 300+ μs at 640×480. So I may remove it. memset(w->shm_segment_info.shmaddr, 192, 4 * w->width * w->height); // Force the clients to call `xshmu_framebuffer` again before // redrawing. w->valid = 0; } int xshmu_fd(xshmu w) { return ConnectionNumber(w->display); } #ifndef __GNUC__ #define __attribute__(x) #endif int __attribute__((weak)) main(int argc, char **argv) { xshmu w = xshmu_open("xshm test", 640, 480, ""); int xorg = 0, yorg = 0; struct timeval start; gettimeofday(&start, NULL); int frames = 0; for (;;frames++) { struct timeval now; gettimeofday(&now, NULL); long us = (now.tv_sec - start.tv_sec) * 1000L*1000 + (now.tv_usec - start.tv_usec); for (xshmu_event *ev; (ev = xshmu_get_event(w));) { if (xshmu_as_die_event(ev)) { xshmu_close(w); return 0; } xshmu_mouse_event *mev = xshmu_as_mouse_event(ev); if (mev) { xorg = mev->x; yorg = mev->y; } } xshmu_pic fb = xshmu_framebuffer(w); for (int y = 0; y < fb.h; y++) { int dy = y - yorg; for (int x = 0; x < fb.w; x++) { int dx = x - xorg; *xshmu_pix(fb, x, y) = dx*dx + dy*dy - us / 4096; } } xshmu_flush(w); if (us && !(frames & 0xff)) { /* This is showing me 480 fps on my laptop, or 92 fps at 1920×1080. */ printf("%d frames in %ld μs = %.1f fps\n", frames, us, ((double)frames/us)*1e6); } } }