/* * Copyright (C) 2023 - Vito Caputo - * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include "til_stream.h" #include "til_tap.h" /* A stream in libtil is basically a hash table for tracking dynamic * information for modules to create/modify/access. The objects stored in the * table are "stream pipes" and endpoints called taps, making this something * like a miniature in-memory implementation of named pipes, conceptually * anyways (there are no actual file descriptors). */ #if 0 /* example usage: */ typedef struct foo_t { struct { til_tap_t position; } taps; struct { v2f_t position; } vars; v2f_t *position; } foo_t; foo_t * foo_create_context(void) { foo_t *foo = malloc(sizeof(foo_t)); /* This creates an isolated (pipe-)tap binding our local position variable and pointer * to a name for later "tapping" onto a stream. */ foo->taps.position = til_tap_init_v2f(&foo->position, 1, &foo->vars.position, "position"); } foo_render(foo_t *foo, til_fb_fragment_t *fragment) { if (!til_stream_tap_context(fragment->stream, foo, &foo->taps.position)) { /* got nothing, we're driving position */ foo->position->x = cosf(ticks); foo->position->y = sinf(ticks); } /* else { got something, just use foo->position as-is */ } #endif #define TIL_STREAM_BUCKETS_COUNT 256 typedef struct til_stream_pipe_t til_stream_pipe_t; struct til_stream_pipe_t { til_stream_pipe_t *next; const void *owner; /* for untap_owner() differentiation */ const void *owner_foo; /* supplemental pointer for owner's use */ char *parent_path; const til_tap_t *driving_tap; /* tap producing values for the pipe */ uint32_t hash; /* hash of (driving_tap->name_hash ^ .parent_hash) */ }; typedef struct til_stream_t { pthread_mutex_t mutex; const til_stream_hooks_t *hooks; void *hooks_context; til_stream_pipe_t *buckets[TIL_STREAM_BUCKETS_COUNT]; } til_stream_t; til_stream_t * til_stream_new(void) { til_stream_t *stream; stream = calloc(1, sizeof(til_stream_t)); if (!stream) return NULL; pthread_mutex_init(&stream->mutex, NULL); return stream; } til_stream_t * til_stream_free(til_stream_t *stream) { if (!stream) return NULL; for (int i = 0; i < TIL_STREAM_BUCKETS_COUNT; i++) { for (til_stream_pipe_t *p = stream->buckets[i], *p_next; p != NULL; p = p_next) { p_next = p->next; free(p->parent_path); free(p); } } pthread_mutex_destroy(&stream->mutex); free(stream); return NULL; } /* hooks are presently implemented as a singleton per-stream, I could imagine a stack * form where multiple triggers filter through until one of the callbacks * claim to have processed the trigger. But let's just keep it stupid simple for now. * * this function is idempotent in the sense that a caller can keep resetting its hooks * when it's succeeded as becoming the owner of the hooks (by being first to set them on * a stream). Only when different set of hooks are attempted to be set on a stream already * having hooks will it error out, to change hooks the caller must first unset them with the * matching hooks pointer. There's no way to read the hooks out by a caller. This is a weak * defensive mechanism to prevent multiple modules silently fighting over the hooks. As long * as they're checking the return code they can say meaningful things about failing to set the * hooks... and it's assumed it'd be a flawed composition attempting to use multiple hook-setting * modules on the same stream (or program bug). */ int til_stream_set_hooks(til_stream_t *stream, const til_stream_hooks_t *hooks, void *context) { assert(stream); assert(hooks); if (stream->hooks && stream->hooks != hooks) return -EEXIST; stream->hooks = hooks; stream->hooks_context = context; return 0; } int til_stream_unset_hooks(til_stream_t *stream, const til_stream_hooks_t *hooks) { assert(stream); assert(hooks); if (stream->hooks && stream->hooks != hooks) return -EINVAL; stream->hooks = stream->hooks_context = NULL; return 0; } /* Taps the key-named type-typed pipe on the supplied stream. * If this is the first use of the pipe on this stream, new pipe will be created. * If the pipe exists on this stream, and the type/n_elems match, existing pipe will be used as-is. * If the pipe exists on this stream, but the type/n_elems mismatch, it's a program error and asserted * * -errno is returned on error, 0 when tap is driving, 1 when tap is passenger. * * If stream is NULL it's treated as if the key doesn't exist without a pipe creation. */ int til_stream_tap(til_stream_t *stream, const void *owner, const void *owner_foo, const char *parent_path, uint32_t parent_hash, const til_tap_t *tap) { uint32_t hash, bucket; til_stream_pipe_t *pipe; const void **p_owner = &owner, **p_owner_foo = &owner_foo; const til_tap_t **p_tap = &tap; assert(tap); if (!stream) { *(tap->ptr) = tap->elems; return 0; } pthread_mutex_lock(&stream->mutex); hash = (tap->name_hash ^ parent_hash); bucket = hash % TIL_STREAM_BUCKETS_COUNT; for (pipe = stream->buckets[bucket]; pipe != NULL; pipe = pipe->next) { if (pipe->hash == hash) { if (pipe->driving_tap == tap) { /* this is the pipe and we're driving */ *(tap->ptr) = pipe->driving_tap->elems; pthread_mutex_unlock(&stream->mutex); return 0; } if (pipe->driving_tap->elems == *(tap->ptr) || (!strcmp(pipe->driving_tap->name, tap->name) && !strcmp(pipe->parent_path, parent_path))) { if (pipe->driving_tap->type != tap->type || pipe->driving_tap->n_elems != tap->n_elems) { assert(0); /* I don't really want to handle such errors at runtime */ return -1; } /* this looks to be the pipe, but we're not driving, should we be? */ if (pipe->driving_tap->inactive) pipe->driving_tap = tap; *(tap->ptr) = pipe->driving_tap->elems; pthread_mutex_unlock(&stream->mutex); return (tap != pipe->driving_tap); } } } if (stream->hooks && stream->hooks->pipe_ctor) { int r; r = stream->hooks->pipe_ctor(stream->hooks_context, stream, owner, owner_foo, parent_path, parent_hash, tap, p_owner, p_owner_foo, p_tap); if (r < 0) return r; } /* matching pipe not found, create new one with tap as driver */ pipe = calloc(1, sizeof(til_stream_pipe_t)); if (!pipe) { pthread_mutex_unlock(&stream->mutex); return -ENOMEM; } pipe->owner = *p_owner; pipe->owner_foo = *p_owner_foo; pipe->driving_tap = *p_tap; pipe->parent_path = strdup(parent_path); if (!pipe->parent_path) { free(pipe); pthread_mutex_unlock(&stream->mutex); return -ENOMEM; } pipe->hash = hash; pipe->next = stream->buckets[bucket]; stream->buckets[bucket] = pipe; pthread_mutex_unlock(&stream->mutex); return 0; } /* remove all pipes belonging to owner in stream, including pipes whose driving_tap is owned by owner */ void til_stream_untap_owner(til_stream_t *stream, const void *owner) { for (int i = 0; i < TIL_STREAM_BUCKETS_COUNT; i++) { for (til_stream_pipe_t *p = stream->buckets[i], *p_next, *p_prev; p != NULL; p = p_next) { p_next = p->next; if (p->owner == owner || p->driving_tap->owner == owner) { if (p == stream->buckets[i]) stream->buckets[i] = p_next; else p_prev->next = p_next; if (stream->hooks && stream->hooks->pipe_dtor) stream->hooks->pipe_dtor(stream->hooks_context, stream, p->owner, p->owner_foo, p->parent_path, p->driving_tap); free(p); } else p_prev = p; } } } /* We need the higher-order types defined in order to print their contents. * libtil should probably just formally define these smoewhere for modules to * make use of. Until now it's been very deliberate to try leave modules to be * relatively self-contained, even if they often reinvent the wheel as a * result... it's relatively harmless for small functionalities while keeping * the listings easier to grok as a whole esp. for a newcomer who isn't * necessarily comfortable jumping around a sprawling tree of files. */ typedef struct v2f_t { float x, y; } v2f_t; typedef struct v3f_t { float x, y, z; } v3f_t; typedef struct v4f_t { float x, y, z, w; } v4f_t; static int til_stream_fprint_pipe_cb(void *arg, til_stream_pipe_t *pipe, const void *owner, const void *owner_foo, const til_tap_t *driving_tap) { FILE *out = arg; fprintf(out, "%s/%s: ", pipe->parent_path, pipe->driving_tap->name); for (size_t j = 0; j < pipe->driving_tap->n_elems; j++) { const char *sep = j ? ", " : ""; switch (pipe->driving_tap->type) { case TIL_TAP_TYPE_I8: fprintf(out, "%"PRIi8"%s", *(*((int8_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_I16: fprintf(out, "%"PRIi16"%s", *(*((int16_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_I32: fprintf(out, "%"PRIi32"%s", *(*((int32_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_I64: fprintf(out, "%"PRIi64"%s", *(*((int64_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_U8: fprintf(out, "%"PRIu8"%s", *(*((int8_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_U16: fprintf(out, "%"PRIu16"%s", *(*((int16_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_U32: fprintf(out, "%"PRIu32"%s", *(*((int32_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_U64: fprintf(out, "%"PRIu64"%s", *(*((int64_t **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_FLOAT: fprintf(out, "%f%s", *(*((float **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_DOUBLE: fprintf(out, "%f%s", *(*((double **)pipe->driving_tap->ptr)), sep); break; case TIL_TAP_TYPE_V2F: fprintf(out, "{%f,%f}%s", (*((v2f_t **)pipe->driving_tap->ptr))->x, (*((v2f_t **)pipe->driving_tap->ptr))->y, sep); break; case TIL_TAP_TYPE_V3F: fprintf(out, "{%f,%f,%f}%s", (*((v3f_t **)pipe->driving_tap->ptr))->x, (*((v3f_t **)pipe->driving_tap->ptr))->y, (*((v3f_t **)pipe->driving_tap->ptr))->z, sep); break; case TIL_TAP_TYPE_V4F: fprintf(out, "{%f,%f,%f,%f}%s", (*((v4f_t **)pipe->driving_tap->ptr))->x, (*((v4f_t **)pipe->driving_tap->ptr))->y, (*((v4f_t **)pipe->driving_tap->ptr))->z, (*((v4f_t **)pipe->driving_tap->ptr))->w, sep); break; case TIL_TAP_TYPE_M4F: fprintf(out, "M4F TODO%s", sep); break; case TIL_TAP_TYPE_VOIDP: fprintf(out, "%p%s", *((void **)pipe->driving_tap->ptr), sep); break; default: assert(0); } fprintf(out, "\n"); } return 1; } /* XXX: note that while yes, this does acquire stream->mutex to serialize access to the table/pipes, * this mutex does not serialize access to the tapped variables. So if this print is performed * during the threaded rendering phase of things, it will technically be racy. The only strictly * correct place to perform this print race-free is in the rendering loop between submissions to * the rendering threads. Arguably if careful about only printing while serial, there's no need * to acquire the mutex - but it's also an uncontended lock if that's the case so just take the * mutex anyways since we're accessing the underlying structure it protects. */ void til_stream_fprint(til_stream_t *stream, FILE *out) { fprintf(out, "Pipes on stream %p:\n", stream); (void) til_stream_for_each_pipe(stream, til_stream_fprint_pipe_cb, out); fprintf(out, "\n"); } /* returns -errno on error (from pipe_cb), 0 otherwise */ int til_stream_for_each_pipe(til_stream_t *stream, til_stream_iter_func_t pipe_cb, void *cb_context) { assert(stream); assert(pipe_cb); pthread_mutex_lock(&stream->mutex); for (int i = 0; i < TIL_STREAM_BUCKETS_COUNT; i++) { for (til_stream_pipe_t *p = stream->buckets[i]; p != NULL; p = p->next) { int r; r = pipe_cb(cb_context, p, p->owner, p->owner_foo, p->driving_tap); if (r < 0) { pthread_mutex_unlock(&stream->mutex); return r; } } } pthread_mutex_unlock(&stream->mutex); return 0; } void til_stream_pipe_set_owner(til_stream_pipe_t *pipe, const void *owner, const void *owner_foo) { assert(pipe); pipe->owner = owner; pipe->owner_foo = owner_foo; } /* NULLing out the driving_tap isn't supported, since the tap name is part of the pipe's identity, * just set tap.inactive to indicate another tap should take over driving. */ void til_stream_pipe_set_driving_tap(til_stream_pipe_t *pipe, const til_tap_t *driving_tap) { assert(pipe); assert(driving_tap); pipe->driving_tap = driving_tap; }