#include "xwin.hpp"
#include <X11/XKBlib.h>
#if USE_PPM
#include <linux/limits.h>
#endif
XFrame::XFrame(res_t res, rgb_t* buf) : res(res), buf(buf), dirty{{0, 0}, {res.w - 1, res.h - 1}} {}
#if USE_PPM
/** @brief Dump frame as PPM image for debugging purposes; blocking. */
static void dump_ppm(const XFrame::rgb_t* buf, const res_t& res, unsigned ctr) {
char fn[PATH_MAX];
snprintf(fn, PATH_MAX, "frame-%u.ppm", ctr);
int fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
LOG_ERRNO("open(%s)", fn);
return;
}
if (dprintf(fd, "P6\n%d %d\n%d\n", res.w, res.h, 255) <= 0 ||
write(fd, buf, (size_t)(res.w * res.h * 3)) != (ssize_t)(res.w * res.h * 3)) {
LOG("cannot write to %s", fn);
}
close(fd);
}
#endif
XImg::XImg(res_t res, res_t off, x_ctx_t& x)
: buf(create_buf(res)),
off(off),
x(x),
gctx(DefaultGC(x.display, DefaultScreen(x.display))),
xim(create_image(res, x, buf)),
frame(res, buf.data) {
XSync(x.display, 0);
}
XImg::buf_t XImg::create_buf(res_t res) {
#if USE_X_SHM
buf_t buf{};
buf.shm.shmid = shmget(IPC_PRIVATE, sizeof(XFrame::rgb_t) * (size_t)(res.w * res.h), IPC_CREAT | 0777);
buf.shm.shmaddr = (char*)shmat(buf.shm.shmid, 0, 0);
buf.shm.readOnly = 1;
buf.data = (XFrame::rgb_t*)buf.shm.shmaddr;
return buf;
#else
return {(XFrame::rgb_t*)calloc((size_t)res.w * (size_t)res.h, sizeof(XFrame::rgb_t))};
#endif
}
XImage* XImg::create_image(res_t res, x_ctx_t& x, buf_t& buf) {
std::lock_guard lock(x.mtx);
#if !USE_X_SHM
return XCreateImage(x.display, x.visual, 24, ZPixmap, 0, (char*)buf.data, (unsigned)res.w, (unsigned)res.h,
sizeof(XFrame::rgb_t) * 8, res.w * (int)sizeof(XFrame::rgb_t));
#else
// https://www.x.org/releases/X11R7.5/doc/Xext/mit-shm.html
assert(XShmQueryExtension(x.display));
XImage* xim =
XShmCreateImage(x.display, x.visual, 24, ZPixmap, (char*)buf.data, &buf.shm, (unsigned)res.w, (unsigned)res.h);
assert(xim);
assert(xim->bytes_per_line == res.w * (int)sizeof(XFrame::rgb_t));
if (XShmAttach(x.display, &buf.shm) == 0) {
assert(false);
}
return xim;
#endif
}
XFrame& XImg::get() {
return frame;
}
void XImg::draw() {
const std::lock_guard<std::mutex> lock(x.mtx);
#if !USE_X_SHM
XPutImage(x.display, x.window, gctx, xim, 0, 0, off.w, off.h, (unsigned)xim->width, (unsigned)xim->height);
XFlush(x.display);
#else
XShmPutImage(x.display, x.window, gctx, xim, 0, 0, off.w, off.h, (unsigned)xim->width, (unsigned)xim->height, 0);
XFlush(x.display); // XSync(display, 0);
#endif
}
XImg::~XImg() {
const std::lock_guard<std::mutex> lock(x.mtx);
#if !USE_X_SHM
XDestroyImage(xim); // also frees buf
#else
XShmDetach(x.display, &buf.shm);
XDestroyImage(xim);
shmdt(buf.shm.shmaddr);
shmctl(buf.shm.shmid, IPC_RMID, 0);
#endif
}
FdReadSelector::FdReadSelector(int fd, time_t timeout_ms) : fd(fd), timeout(timeout_ms) {
assert(timeout_ms > 0);
}
bool FdReadSelector::wait() {
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv = {timeout / 1000, (timeout % 1000) * 1000}; // micro
const int num_fd = select(fd + 1, &fds, NULL, NULL, &tv);
if (num_fd > 0) {
return true;
} else if (num_fd == 0) {
return false;
} else {
LOG_ERRNO("X11 select()");
return false;
}
}
void XKeyEvents::set(key_t key, msec_t ts, bool pressed) {
const std::lock_guard<std::mutex> lock(mtx);
key_state_t& state = keys[key];
if (state.start > 0 && !pressed) { // newly released
state.value += std::max((msec_t)1, ts - state.start);
state.start = 0;
} else if (state.start == 0 && pressed) { // newly pressed
state.start = ts;
dirty = true; // became dirty if not already before
}
}
void XKeyEvents::notify() {
const std::lock_guard<std::mutex> lock(mtx);
if (dirty) {
dirty = false;
cond.notify_all();
}
}
bool XKeyEvents::pop_movement(float sensitivity, vertex_t& movement) {
const std::lock_guard<std::mutex> lock(mtx);
const msec_t now = Timer::now();
dirty = false;
bool noop = true;
for (auto& state : keys) {
if (state.start > 0) {
state.value += std::max((msec_t)1, now - state.start);
state.start = now;
noop = false;
} else if (state.value > 0) {
noop = false;
}
}
if (noop) {
return false;
}
const vertex_t offset{
((fcoord_t)std::exchange(keys[key_t::KEY_LEFT].value, 0) * -sensitivity) +
((fcoord_t)std::exchange(keys[key_t::KEY_RIGHT].value, 0) * sensitivity),
((fcoord_t)std::exchange(keys[key_t::KEY_DOWN].value, 0) * -sensitivity) +
((fcoord_t)std::exchange(keys[key_t::KEY_UP].value, 0) * sensitivity),
0.0F,
};
const fcoord_t max_len = std::max(std::fabs(offset.x), std::fabs(offset.y));
movement = max_len > MICRO_COORD ? offset.normed() * max_len : offset; // prevent diagonals being faster
return movement.sqlen() > MICRO_COORD * MICRO_COORD;
}
bool XKeyEvents::pop_key(key_t key) {
if (keys[key].start > 0 || keys[key].value > 0) {
keys[key].start = 0;
keys[key].value = 0;
return true;
}
return false;
}
void XKeyEvents::pop_action(action_t& action) {
const std::lock_guard<std::mutex> lock(mtx);
action.action = pop_key(KEY_ACT);
action.restart = pop_key(KEY_RESTART);
action.regen = pop_key(KEY_REGEN);
action.quit = pop_key(KEY_QUIT);
}
bool XKeyEvents::is_dirty() const {
return std::any_of(keys.begin(), keys.end(),
[](const auto& state) { return (state.start > 0 || state.value > 0); });
}
void XKeyEvents::wait() {
std::unique_lock<std::mutex> lock(mtx);
#if USE_X
while (!is_dirty()) {
cond.wait(lock);
}
#endif
}
XInput::XInput(x_ctx_t& x) : x(x), xfd(x.fd, 1000), shutdown(false), thread([this]() { worker(); }) {}
XInput::~XInput() {
shutdown = true;
thread.join();
}
XKeyEvents& XInput::get_keys() {
return events;
}
void XInput::worker() {
XEvent ev;
char key_vector_buf[32];
char* key_vector = nullptr;
KeySym symbols[256];
{
std::lock_guard<std::mutex> lock(x.mtx);
for (int i = 0; i < 256; ++i) {
symbols[i] = XkbKeycodeToKeysym(x.display, (KeyCode)i, 0, 0);
}
}
while (!shutdown) {
{
std::unique_lock<std::mutex> lock(x.mtx);
while (XPending(x.display) == 0) {
lock.unlock();
(void)xfd.wait();
if (shutdown) {
return;
}
lock.lock();
}
XNextEvent(x.display, &ev);
if (ev.type == KeymapNotify) {
key_vector = ev.xkeymap.key_vector; // no need for memcpy
} else if (ev.type == KeyPress || ev.type == KeyRelease) {
XQueryKeymap(x.display, key_vector_buf);
key_vector = key_vector_buf;
} else {
continue;
}
}
const msec_t now = Timer::now();
for (int i = 0; i < 32; ++i) {
for (int b = 0; b < 8; ++b) {
const bool pressed = ((key_vector[i] & (1 << b)) != 0);
const KeyCode keycode = (KeyCode)((i * 8) + b);
const KeySym keysym = symbols[keycode];
switch (keysym) {
#if USE_WASD
case XK_w:
events.set(XKeyEvents::key_t::KEY_UP, now, pressed);
break;
case XK_a:
events.set(XKeyEvents::key_t::KEY_LEFT, now, pressed);
break;
case XK_s:
events.set(XKeyEvents::key_t::KEY_DOWN, now, pressed);
break;
case XK_d:
events.set(XKeyEvents::key_t::KEY_RIGHT, now, pressed);
break;
case XK_e:
events.set(XKeyEvents::key_t::KEY_ACT, now, pressed);
break;
#else
case XK_Up:
events.set(XKeyEvents::key_t::KEY_UP, now, pressed);
break;
case XK_Left:
events.set(XKeyEvents::key_t::KEY_LEFT, now, pressed);
break;
case XK_Down:
events.set(XKeyEvents::key_t::KEY_DOWN, now, pressed);
break;
case XK_Right:
events.set(XKeyEvents::key_t::KEY_RIGHT, now, pressed);
break;
case XK_space:
events.set(XKeyEvents::key_t::KEY_ACT, now, pressed);
break;
#endif
case XK_F5:
events.set(XKeyEvents::key_t::KEY_RESTART, now, pressed);
break;
case XK_F3:
events.set(XKeyEvents::key_t::KEY_REGEN, now, pressed);
break;
case XK_Escape:
events.set(XKeyEvents::key_t::KEY_QUIT, now, pressed);
break;
case NoSymbol:
break;
}
}
}
events.notify();
}
}
XWin::XWin(res_t res, res_t off) {
#if USE_X
x.display = XOpenDisplay(NULL);
if (x.display == NULL) {
assert(false);
return;
}
x.fd = ConnectionNumber(x.display);
x.visual = DefaultVisual(x.display, 0);
if (x.visual->c_class != TrueColor) {
XCloseDisplay(x.display);
x.display = NULL;
assert(false);
return;
}
assert(res.w > 0 && res.h > 0);
x.window = XCreateSimpleWindow(x.display, RootWindow(x.display, 0), 0, 0, (unsigned)(res.w + (off.w * 2)),
(unsigned)(res.h + (off.h * 2)), 1, 0, 0);
if (x.window == NULL) {
XCloseDisplay(x.display);
x.display = NULL;
assert(false);
return;
}
if (XStoreName(x.display, x.window, "maze-tracer") == 0 ||
XSelectInput(x.display, x.window, KeymapStateMask | KeyPressMask | KeyReleaseMask | ExposureMask) == 0 ||
XMapWindow(x.display, x.window) == 0) {
XDestroyWindow(x.display, x.window);
XCloseDisplay(x.display);
x.window = 0;
x.display = NULL;
assert(false);
return;
}
XSync(x.display, 0);
ximg = std::make_unique<XImg>(res, off, x);
xinput = std::make_unique<XInput>(x);
#else
buf = std::make_unique<XFrame::rgb_t[]>((size_t)(res.w * res.h));
ximg = std::make_unique<XFrame>(res, buf.get());
xinput = std::make_unique<XKeyEvents>();
#endif
}
void XWin::draw() {
#if USE_X
ximg->draw();
#elif USE_PPM
static unsigned frame_counter = 0;
dump_ppm(buf.get(), ximg->resolution(), ++frame_counter);
#endif
}
XFrame& XWin::get_frame() {
#if USE_X
return ximg->get();
#else
return *ximg;
#endif
}
XKeyEvents& XWin::get_keys() {
#if USE_X
return xinput->get_keys();
#else
return *xinput;
#endif
};
XWin::~XWin() {
#if USE_X
ximg.reset();
xinput.reset();
XDestroyWindow(x.display, x.window);
XCloseDisplay(x.display);
#endif
}