glava-ridged/glava/glava.c

578 lines
22 KiB
C
Raw Normal View History

2023-09-07 16:16:06 -06:00
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <pthread.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <getopt.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "fifo.h"
#include "pulse_input.h"
#include "render.h"
#include "xwin.h"
#ifdef GLAD_DEBUG
#define GLAVA_RELEASE_TYPE_PREFIX "debug, "
#else
#define GLAVA_RELEASE_TYPE_PREFIX "stable, "
#endif
#ifdef GLAVA_STANDALONE
#define GLAVA_RELEASE_TYPE_BUILD "standalone"
#elif GLAVA_UNIX
#define GLAVA_RELEASE_TYPE_BUILD "unix/fhs"
#elif GLAVA_OSX
#define GLAVA_RELEASE_TYPE_BUILD "osx"
#else
#define GLAVA_RELEASE_TYPE_BUILD "?"
#endif
#define GLAVA_RELEASE_TYPE GLAVA_RELEASE_TYPE_PREFIX GLAVA_RELEASE_TYPE_BUILD
#define FORMAT(...) \
({ \
char* buf = malloc(PATH_MAX); \
snprintf(buf, PATH_MAX, __VA_ARGS__); \
buf; \
})
#define ENV(e, ...) \
({ \
const char* _e = getenv(e); \
if (!_e) \
_e = FORMAT(__VA_ARGS__); \
_e; \
})
#ifdef GLAVA_STANDALONE
#define SHADER_INSTALL_PATH "../shaders/glava"
#define SHADER_USER_PATH "userconf"
/* FHS compliant systems */
#elif defined(__unix__) || defined(GLAVA_UNIX)
#ifndef SHADER_INSTALL_PATH
#define SHADER_INSTALL_PATH "/etc/xdg/glava"
#endif
#define SHADER_USER_PATH FORMAT("%s/glava", ENV("XDG_CONFIG_HOME", "%s/.config", ENV("HOME", "/home")))
/* OSX */
#elif (defined(__APPLE__) && defined(__MACH__)) || defined(GLAVA_OSX)
#ifndef SHADER_INSTALL_PATH
#define SHADER_INSTALL_PATH "/Library/glava"
#endif
#define SHADER_USER_PATH FORMAT("%s/Library/Preferences/glava", ENV("HOME", "/"))
#else
#error "Unsupported target system"
#endif
#ifndef ACCESSPERMS
#define ACCESSPERMS (S_IRWXU|S_IRWXG|S_IRWXO) /* 0777 */
#endif
static volatile bool reload = false;
__attribute__((noreturn, visibility("default"))) void glava_return_builtin(void) { exit(EXIT_SUCCESS); }
__attribute__((noreturn, visibility("default"))) void glava_abort_builtin (void) { exit(EXIT_FAILURE); }
__attribute__((noreturn, visibility("default"))) void (*glava_return) (void) = glava_return_builtin;
__attribute__((noreturn, visibility("default"))) void (*glava_abort) (void) = glava_abort_builtin;
/* Copy installed shaders/configuration from the installed location
(usually /etc/xdg). Modules (folders) will be linked instead of
copied. */
static void copy_cfg(const char* path, const char* dest, bool verbose) {
size_t
sl = strlen(path),
tl = strlen(dest),
pgsz = (size_t) getpagesize(); /* optimal buffer size */
DIR* dir = opendir(path);
if (!dir) {
fprintf(stderr, "'%s' does not exist\n", path);
glava_abort();
}
umask(~(S_IRWXU | S_IRGRP | S_IROTH | S_IXGRP | S_IXOTH));
if (mkdir(dest, ACCESSPERMS) && errno != EEXIST) {
fprintf(stderr, "could not create directory '%s': %s\n", dest, strerror(errno));
glava_abort();
}
struct dirent* d;
while ((d = readdir(dir)) != NULL) {
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
int type = 0;
size_t
dl = strlen(d->d_name),
pl = sl + dl + 2,
fl = tl + dl + 2;
char p[pl], f[fl];
snprintf(p, pl, "%s/%s", path, d->d_name);
snprintf(f, fl, "%s/%s", dest, d->d_name);
if (d->d_type != DT_UNKNOWN) /* don't bother with stat if we already have the type */
type = d->d_type == DT_REG ? 1 : (d->d_type == DT_DIR ? 2 : 0);
else {
struct stat st;
if (lstat(p, &st)) {
fprintf(stderr, "failed to stat '%s': %s\n", p, strerror(errno));
} else
type = S_ISREG(st.st_mode) ? 1 : (S_ISDIR(st.st_mode) ? 2 : 0);
}
switch (type) {
case 1: {
int source = -1, dest = -1;
uint8_t buf[pgsz];
ssize_t r, t, w, a;
if (!strncmp(p, "env_", 4))
break;
if ((source = open(p, O_RDONLY)) < 0) {
fprintf(stderr, "failed to open (source) '%s': %s\n", p, strerror(errno));
goto cleanup;
}
if ((dest = open(f, O_TRUNC | O_WRONLY | O_CREAT, ACCESSPERMS)) < 0) {
fprintf(stderr, "failed to open (destination) '%s': %s\n", f, strerror(errno));
goto cleanup;
}
for (t = 0; (r = read(source, buf, pgsz)) > 0; t += r) {
for (a = 0; a < r && (w = write(dest, buf + a, r - a)) > 0; a += w);
if (w < 0) {
fprintf(stderr, "error while writing '%s': %s\n", f, strerror(errno));
goto cleanup;
}
}
if (r < 0) {
fprintf(stderr, "error while reading '%s': %s\n", p, strerror(errno));
goto cleanup;
}
if (verbose)
printf("copy '%s' -> '%s'\n", p, f);
cleanup:
if (source > 0) close(source);
if (dest > 0) close(dest);
}
break;
case 2:
if (symlink(p, f) && errno != EEXIST)
fprintf(stderr, "failed to symlink '%s' -> '%s': %s\n", p, f, strerror(errno));
else if (verbose)
printf("symlink '%s' -> '%s'\n", p, f);
break;
}
}
closedir(dir);
}
#define GLAVA_VERSION_STRING "GLava (glava) " GLAVA_VERSION " (" GLAVA_RELEASE_TYPE ")"
static const char* help_str =
"Usage: %s [OPTIONS]...\n"
"Opens a window with an OpenGL context to draw an audio visualizer.\n"
"\n"
"Available arguments:\n"
"-h, --help show this help and exit\n"
"-v, --verbose enables printing of detailed information about execution\n"
"-d, --desktop enables running glava as a desktop window by detecting the\n"
" desktop environment and setting the appropriate properties\n"
" automatically. Can override properties in \"rc.glsl\".\n"
"-r, --request=REQUEST evaluates the specified request after loading \"rc.glsl\".\n"
"-m, --force-mod=NAME forces the specified module to load instead, ignoring any\n"
" `#request mod` instances in the entry point.\n"
"-e, --entry=FILE specifies the name of the file to look for when loading shaders,\n"
" by default this is \"rc.glsl\".\n"
"-C, --copy-config creates copies and symbolic links in the user configuration\n"
" directory for glava, copying any files in the root directory\n"
" of the installed shader directory, and linking any modules.\n"
"-b, --backend specifies a window creation backend to use. By default, the most\n"
" appropriate backend will be used for the underlying windowing\n"
" system.\n"
"-a, --audio=BACKEND specifies an audio input backend to use.\n"
"-p, --pipe[=BIND[:TYPE]] binds value(s) to be read from stdin. The input my be read using\n"
" `@name` or `@name:default` syntax within shader sources.\n"
" A stream of inputs (each overriding the previous) must be\n"
" assigned with the `name = value` syntax and separated by\n"
" newline (\'\\n\') characters.\n"
"-V, --version print application version and exit\n"
"\n"
"The REQUEST argument is evaluated identically to the \'#request\' preprocessor directive\n"
"in GLSL files.\n"
"\n"
"The FILE argument may be any file path. All specified file paths are relative to the\n"
"active configuration root (usually ~/.config/glava if present).\n"
"\n"
"The BACKEND argument may be any of the following strings (for this particular build):\n"
"%s"
"\n"
"The BIND argument must a valid GLSL identifier."
"\n"
"The TYPE argument must be a valid GLSL type. If `--pipe` is used without a \n"
"type argument, the default type is `vec4` (type used for RGBA colors).\n"
"\n"
GLAVA_VERSION_STRING "\n";
static const char* opt_str = "dhvVe:Cm:b:r:a:i::p::";
static struct option p_opts[] = {
{"help", no_argument, 0, 'h'},
{"verbose", no_argument, 0, 'v'},
{"desktop", no_argument, 0, 'd'},
{"audio", required_argument, 0, 'a'},
{"request", required_argument, 0, 'r'},
{"entry", required_argument, 0, 'e'},
{"force-mod", required_argument, 0, 'm'},
{"copy-config", no_argument, 0, 'C'},
{"backend", required_argument, 0, 'b'},
{"pipe", optional_argument, 0, 'p'},
{"stdin", optional_argument, 0, 'i'},
{"version", no_argument, 0, 'V'},
#ifdef GLAVA_DEBUG
{"run-tests", no_argument, 0, 'T'},
#endif
{0, 0, 0, 0 }
};
#define append_buf(buf, sz_store, ...) \
({ \
buf = realloc(buf, ++(*sz_store) * sizeof(*buf)); \
buf[*sz_store - 1] = __VA_ARGS__; \
})
/* Wait for glava_renderer target texture to be initialized and valid */
__attribute__((visibility("default")))
void glava_wait(glava_handle* ref) {
while(__atomic_load_n(ref, __ATOMIC_SEQ_CST) == NULL) {
/* Edge case: handle has not been assigned */
struct timespec tv = {
.tv_sec = 0, .tv_nsec = 10 * 1000000
};
nanosleep(&tv, NULL);
}
pthread_mutex_lock(&(*ref)->lock);
while ((*ref)->flag == false)
pthread_cond_wait(&(*ref)->cond, &(*ref)->lock);
pthread_mutex_unlock(&(*ref)->lock);
}
__attribute__((visibility("default")))
unsigned int glava_tex(glava_handle r) {
return r->off_tex;
}
/* Atomic size request */
__attribute__((visibility("default")))
void glava_sizereq(glava_handle r, int x, int y, int w, int h) {
r->sizereq = (typeof(r->sizereq)) { .x = x, .y = y, .w = w, .h = h };
__atomic_store_n(&r->sizereq_flag, GLAVA_REQ_RESIZE, __ATOMIC_SEQ_CST);
}
/* Atomic terminate request */
__attribute__((visibility("default")))
void glava_terminate(glava_handle* ref) {
glava_handle store = __atomic_exchange_n(ref, NULL, __ATOMIC_SEQ_CST);
if (store)
__atomic_store_n(&store->alive, false, __ATOMIC_SEQ_CST);
}
/* Atomic reload request */
__attribute__((visibility("default")))
void glava_reload(glava_handle* ref) {
glava_handle store = __atomic_exchange_n(ref, NULL, __ATOMIC_SEQ_CST);
if (store) {
__atomic_store_n(&reload, true, __ATOMIC_SEQ_CST);
__atomic_store_n(&store->alive, false, __ATOMIC_SEQ_CST);
}
}
/* Main entry */
__attribute__((visibility("default")))
void glava_entry(int argc, char** argv, glava_handle* ret) {
/* Evaluate these macros only once, since they allocate */
const char
* install_path = SHADER_INSTALL_PATH,
* user_path = SHADER_USER_PATH,
* entry = "rc.glsl",
* force = NULL,
* backend = NULL,
* audio_impl_name = "pulseaudio";
const char* system_shader_paths[] = { user_path, install_path, NULL };
int stdin_type = STDIN_TYPE_NONE;
char** requests = malloc(1);
size_t requests_sz = 0;
struct rd_bind* binds = malloc(1);
size_t binds_sz = 0;
bool verbose = false, copy_mode = false, desktop = false, test = false;
int c, idx;
while ((c = getopt_long(argc, argv, opt_str, p_opts, &idx)) != -1) {
switch (c) {
case 'v': verbose = true; break;
case 'C': copy_mode = true; break;
case 'd': desktop = true; break;
case 'r': append_buf(requests, &requests_sz, optarg); break;
case 'e': entry = optarg; break;
case 'm': force = optarg; break;
case 'b': backend = optarg; break;
case 'a': audio_impl_name = optarg; break;
case '?': glava_abort(); break;
case 'V':
puts(GLAVA_VERSION_STRING);
glava_return();
break;
default:
case 'h': {
char buf[2048];
size_t bsz = 0;
for (size_t t = 0; t < audio_impls_idx; ++t)
bsz += snprintf(buf + bsz, sizeof(buf) - bsz, "\t\"%s\"%s\n", audio_impls[t]->name,
!strcmp(audio_impls[t]->name, audio_impl_name) ? " (default)" : "");
printf(help_str, argc > 0 ? argv[0] : "glava", buf);
glava_return();
break;
}
case 'p': {
if (stdin_type != STDIN_TYPE_NONE) goto conflict_error;
char* parsed_name = NULL;
const char* parsed_type = NULL;
if (optarg) {
size_t in_sz = strlen(optarg);
int sep = -1;
for (size_t t = 0; t < in_sz; ++t) {
switch (optarg[t]) {
case ' ': optarg[t] = '\0'; goto after;
case ':': sep = (int) t; break;
}
}
after:
if (sep >= 0) {
parsed_type = optarg + sep + 1;
optarg[sep] = '\0';
}
parsed_name = optarg;
} else parsed_name = PIPE_DEFAULT;
if (*parsed_name == '\0') {
fprintf(stderr, "Error: invalid pipe binding name: \"%s\"\n"
"Zero length names are not permitted.\n", parsed_name);
glava_abort();
}
for (char* c = parsed_name; *c != '\0'; ++c) {
switch (*c) {
case '0' ... '9':
if (c == parsed_name) {
fprintf(stderr, "Error: invalid pipe binding name: \"%s\" ('%c')\n"
"Valid names may not start with a number.\n", parsed_name, *c);
glava_abort();
}
case 'a' ... 'z':
case 'A' ... 'Z':
case '_': continue;
default:
fprintf(stderr, "Error: invalid pipe binding name: \"%s\" ('%c')\n"
"Valid names may only contain [a..z], [A..Z], [0..9] "
"and '_' characters.\n", parsed_name, *c);
glava_abort();
}
}
for (size_t t = 0; t < binds_sz; ++t) {
if (!strcmp(binds[t].name, parsed_name)) {
fprintf(stderr, "Error: attempted to re-bind pipe argument: \"%s\"\n", parsed_name);
glava_abort();
}
}
int type = -1;
if (parsed_type == NULL || strlen(parsed_type) == 0) {
type = STDIN_TYPE_VEC4;
parsed_type = bind_types[STDIN_TYPE_VEC4].n;
} else {
for (size_t t = 0 ; bind_types[t].n != NULL; ++t) {
if (!strcmp(bind_types[t].n, parsed_type)) {
type = bind_types[t].i;
parsed_type = bind_types[t].n;
break;
}
}
}
if (type == -1) {
fprintf(stderr, "Error: Unsupported `--pipe` GLSL type: \"%s\"\n", parsed_type);
glava_abort();
}
struct rd_bind bd = {
.name = parsed_name,
.type = type,
.stype = parsed_type
};
append_buf(binds, &binds_sz, bd);
break;
}
case 'i': {
if (binds_sz > 0) goto conflict_error;
fprintf(stderr, "Warning: `--stdin` is deprecated and will be "
"removed in a future release, use `--pipe` instead. \n");
stdin_type = -1;
if (optarg == NULL) {
stdin_type = STDIN_TYPE_VEC4;
} else {
for (size_t t = 0 ; bind_types[t].n != NULL; ++t) {
if (!strcmp(bind_types[t].n, optarg)) {
stdin_type = bind_types[t].i;
break;
}
}
}
if (stdin_type == -1) {
fprintf(stderr, "Error: Unsupported `--stdin` GLSL type: \"%s\"\n", optarg);
glava_abort();
}
break;
}
conflict_error:
fprintf(stderr, "Error: cannot use `--pipe` and `--stdin` together\n");
glava_abort();
#ifdef GLAVA_DEBUG
case 'T': {
entry = "test_rc.glsl";
test = true;
}
#endif
}
}
if (copy_mode) {
copy_cfg(install_path, user_path, verbose);
glava_return();
}
/* Handle `--force` argument as a request override */
if (force) {
const size_t bsz = 5 + strlen(force);
char* force_req_buf = malloc(bsz);
snprintf(force_req_buf, bsz, "mod %s", force);
append_buf(requests, &requests_sz, force_req_buf);
}
/* Null terminate array arguments */
append_buf(requests, &requests_sz, NULL);
append_buf(binds, &binds_sz, (struct rd_bind) { .name = NULL });
float* b0, * b1, * lb, * rb;
size_t t;
struct audio_data audio;
struct audio_impl* impl = NULL;
pthread_t thread;
int return_status;
for (t = 0; t < audio_impls_idx; ++t) {
if (!strcmp(audio_impls[t]->name, audio_impl_name)) {
impl = audio_impls[t];
break;
}
}
if (!impl) {
fprintf(stderr, "The specified audio backend (\"%s\") is not available.\n", audio_impl_name);
glava_abort();
}
instantiate: {}
glava_renderer* rd = rd_new(system_shader_paths, entry, (const char**) requests,
backend, binds, stdin_type, desktop, verbose, test);
if (ret)
__atomic_store_n(ret, rd, __ATOMIC_SEQ_CST);
b0 = malloc(rd->bufsize_request * sizeof(float));
b1 = malloc(rd->bufsize_request * sizeof(float));
lb = malloc(rd->bufsize_request * sizeof(float));
rb = malloc(rd->bufsize_request * sizeof(float));
for (t = 0; t < rd->bufsize_request; ++t) {
b0[t] = 0.0F;
b1[t] = 0.0F;
}
audio = (struct audio_data) {
.source = ({
char* src = NULL;
if (rd->audio_source_request && strcmp(rd->audio_source_request, "auto") != 0) {
src = strdup(rd->audio_source_request);
}
src;
}),
.rate = (unsigned int) rd->rate_request,
.format = -1,
.terminate = 0,
.channels = rd->mirror_input ? 1 : 2,
.audio_out_r = b0,
.audio_out_l = b1,
.mutex = PTHREAD_MUTEX_INITIALIZER,
.audio_buf_sz = rd->bufsize_request,
.sample_sz = rd->samplesize_request,
.modified = false
};
impl->init(&audio);
if (verbose) printf("Using audio source: %s\n", audio.source);
pthread_create(&thread, NULL, impl->entry, (void*) &audio);
while (__atomic_load_n(&rd->alive, __ATOMIC_SEQ_CST)) {
rd_time(rd); /* update timer for this frame */
bool modified; /* if the audio buffer has been updated by the streaming thread */
/* lock the audio mutex and read our data */
pthread_mutex_lock(&audio.mutex);
modified = audio.modified;
if (modified) {
/* create our own copies of the audio buffers, so the streaming
thread can continue to append to it */
memcpy(lb, (void*) audio.audio_out_l, rd->bufsize_request * sizeof(float));
memcpy(rb, (void*) audio.audio_out_r, rd->bufsize_request * sizeof(float));
audio.modified = false; /* set this flag to false until the next time we read */
}
pthread_mutex_unlock(&audio.mutex);
bool ret = rd_update(rd, lb, rb, rd->bufsize_request, modified);
if (!ret) {
/* Sleep for 50ms and then attempt to render again */
struct timespec tv = {
.tv_sec = 0, .tv_nsec = 50 * 1000000
};
nanosleep(&tv, NULL);
}
#ifdef GLAVA_DEBUG
if (ret && rd_get_test_mode(rd))
break;
#endif
}
#ifdef GLAVA_DEBUG
if (rd_get_test_mode(rd)) {
if (rd_test_evaluate(rd)) {
fprintf(stderr, "Test results did not match expected output\n");
fflush(stderr);
glava_abort();
}
}
#endif
audio.terminate = 1;
if ((return_status = pthread_join(thread, NULL))) {
fprintf(stderr, "Failed to join with audio thread: %s\n", strerror(return_status));
}
free(audio.source);
free(b0);
free(b1);
free(lb);
free(rb);
rd_destroy(rd);
if (__atomic_exchange_n(&reload, false, __ATOMIC_SEQ_CST))
goto instantiate;
}