diff options
-rw-r--r-- | src/gtk_fb.c | 210 | ||||
-rw-r--r-- | src/main.c | 192 |
2 files changed, 402 insertions, 0 deletions
diff --git a/src/gtk_fb.c b/src/gtk_fb.c new file mode 100644 index 0000000..137e560 --- /dev/null +++ b/src/gtk_fb.c @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2021 - Vito Caputo - <vcaputo@pengaru.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 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 <http://www.gnu.org/licenses/>. + */ +#include <assert.h> +#include <errno.h> +#include <gtk/gtk.h> +#include <inttypes.h> +#include <stdlib.h> + +#include "fb.h" +#include "settings.h" + +/* glimmer's GTK+-3.0 backend fb for rototiller */ + +typedef struct gtk_fb_t { + GtkWidget *window; + GtkWidget *image; + guint tick_callback; + unsigned width, height; + unsigned fullscreen:1; +} gtk_fb_t; + +typedef struct gtk_fb_page_t gtk_fb_page_t; + +struct gtk_fb_page_t { + cairo_surface_t *surface; +}; + + +/* this doesn't really do anything significant on gtk */ +static int gtk_fb_init(const settings_t *settings, void **res_context) +{ + const char *fullscreen; + const char *size; + gtk_fb_t *c; + int r; + + assert(settings); + assert(res_context); + + fullscreen = settings_get_value(settings, "fullscreen"); + if (!fullscreen) + return -EINVAL; + + size = settings_get_value(settings, "size"); + if (!size && !strcasecmp(fullscreen, "off")) + return -EINVAL; + + c = calloc(1, sizeof(gtk_fb_t)); + if (!c) + return -ENOMEM; + + if (!strcasecmp(fullscreen, "on")) + c->fullscreen = 1; + + if (size) /* TODO: errors */ + sscanf(size, "%u%*[xX]%u", &c->width, &c->height); + + *res_context = c; + + return 0; +} + + +static void gtk_fb_shutdown(fb_t *fb, void *context) +{ + gtk_fb_t *c = context; + + free(c); +} + + +/* This performs the page flip on the "draw" signal, triggerd + * on every "tick" by queue_draw_cb() below. + * Note that "tick" in this context is a gtk concept, and unrelated to + * rototiller ticks. See gtk frame clocks for more info. + * This is a little awkward as we're calling the public fb API from + * the underlying implementation, maybe fix it up later. + */ +static gboolean draw_cb(GtkWidget *widget, cairo_t *cr, gpointer user_data) +{ + fb_t *fb = user_data; + + fb_flip(fb); + + return FALSE; +} + +/* this just queues drawing the image on the "tick" */ +static gboolean queue_draw_cb(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) +{ + gtk_fb_t *c = user_data; + + gtk_widget_queue_draw(c->image); + + return G_SOURCE_CONTINUE; +} + + +static int gtk_fb_acquire(fb_t *fb, void *context, void *page) +{ + gtk_fb_t *c = context; + gtk_fb_page_t *p = page; + + c->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + c->image = gtk_image_new_from_surface(p->surface); + g_signal_connect(c->image, "draw", G_CALLBACK(draw_cb), fb); + c->tick_callback = gtk_widget_add_tick_callback(c->image, queue_draw_cb, c, NULL); + gtk_container_add(GTK_CONTAINER(c->window), c->image); + gtk_widget_show_all(c->window); + + return 0; +} + + +static void gtk_fb_release(fb_t *fb, void *context) +{ + gtk_fb_t *c = context; + + gtk_widget_remove_tick_callback(c->image, c->tick_callback); + gtk_widget_destroy(c->window); + gtk_widget_destroy(c->image); +} + + +static void * gtk_fb_page_alloc(fb_t *fb, void *context, fb_page_t *res_page) +{ + gtk_fb_t *c = context; + gtk_fb_page_t *p; + + p = calloc(1, sizeof(gtk_fb_page_t)); + if (!p) + return NULL; + + p->surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, c->width, c->height); + + res_page->fragment.buf = (uint32_t *)cairo_image_surface_get_data(p->surface); + res_page->fragment.width = c->width; + res_page->fragment.frame_width = c->width; + res_page->fragment.height = c->height; + res_page->fragment.frame_height = c->height; + res_page->fragment.stride = cairo_image_surface_get_stride(p->surface) - (c->width * 4); + res_page->fragment.pitch = cairo_image_surface_get_stride(p->surface); + + cairo_surface_flush(p->surface); + cairo_surface_mark_dirty(p->surface); + + return p; +} + + +static int gtk_fb_page_free(fb_t *fb, void *context, void *page) +{ + gtk_fb_t *c = context; + gtk_fb_page_t *p = page; + + cairo_surface_destroy(p->surface); + free(p); + + return 0; +} + + +/* XXX: due to gtk's event-driven nature, this isn't a vsync-synchronous page flip, + * so and fb_flip() must be scheduled independently to not just spin. + * The "draw" signal on the image is used to drive fb_flip() on frameclock "ticks", + * a method suggested by Christian Hergert, thanks! + */ +static int gtk_fb_page_flip(fb_t *fb, void *context, void *page) +{ + gtk_fb_t *c = context; + gtk_fb_page_t *p = page; + + cairo_surface_mark_dirty(p->surface); + gtk_image_set_from_surface(GTK_IMAGE(c->image), p->surface); + + return 0; +} + + +fb_ops_t gtk_fb_ops = { + /* TODO: .setup may not be necessary in the gtk frontend, unless maybe + * it learns to use multiple fb backends, and would like to do the whole dynamic + * settings iterative dance established by rototiller. + .setup = gtk_fb_setup, + */ + + /* everything else seems to not be too far out of wack for the new frontend as-is, + * I only had to plumb down the fb_t *fb, which classic rototiller didn't need to do. + */ + .init = gtk_fb_init, + .shutdown = gtk_fb_shutdown, + .acquire = gtk_fb_acquire, + .release = gtk_fb_release, + .page_alloc = gtk_fb_page_alloc, + .page_free = gtk_fb_page_free, + .page_flip = gtk_fb_page_flip +}; diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..5866bb5 --- /dev/null +++ b/src/main.c @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2021 - Vito Caputo - <vcaputo@pengaru.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 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 <http://www.gnu.org/licenses/>. + */ + +#include <gtk/gtk.h> +#include <pthread.h> +#include <sys/time.h> + +#include "rototiller.h" + +/* glimmer is a GTK+-3.0 frontend for rototiller */ + +extern fb_ops_t gtk_fb_ops; + +#define DEFAULT_WIDTH 320 +#define DEFAULT_HEIGHT 480 + +#define BOX_SPACING 4 +#define NUM_FB_PAGES 3 + +static struct glimmer_t { + GtkWidget *modules_list; + + fb_t *fb; + settings_t *fb_settings; + + settings_t *module_settings; + const rototiller_module_t *module; + void *module_context; + pthread_t thread; + struct timeval start_tv; + unsigned ticks_offset; /* XXX: this isn't leveraged currently */ +} glimmer; + + +static unsigned get_ticks(const struct timeval *start, const struct timeval *now, unsigned offset) +{ + return (unsigned)((now->tv_sec - start->tv_sec) * 1000 + (now->tv_usec - start->tv_usec) / 1000) + offset; +} + + +/* TODO: this should probably move into librototiller */ +static void * glimmer_thread(void *foo) +{ + struct timeval now; + + for (;;) { + fb_page_t *page; + unsigned ticks; + + page = fb_page_get(glimmer.fb); + gettimeofday(&now, NULL); + ticks = get_ticks(&glimmer.start_tv, &now, glimmer.ticks_offset); + rototiller_module_render(glimmer.module, glimmer.module_context, ticks, &page->fragment); + fb_page_put(glimmer.fb, page); + } +} + + +static void glimmer_go(GtkButton *button, gpointer user_data) +{ + int r; + + if (glimmer.fb) { + pthread_cancel(glimmer.thread); + pthread_join(glimmer.thread, NULL); + + /* FIXME: all these _free() functions in librototiller should return NULL */ + fb_free(glimmer.fb); + glimmer.fb = NULL; + settings_free(glimmer.fb_settings); + glimmer.fb_settings = NULL; + settings_free(glimmer.module_settings); + glimmer.module_settings = NULL; + } + + /* TODO: translate the GTK+ settings panel values into + * glimmer.{fb,module}_settings + */ + + /* For now, construct a simple 640x480 non-fullscreen fb, and + * simply don't do any module setup (those *should* have static builtin + * defaults that at least work on some level. + */ + glimmer.fb_settings = settings_new("fullscreen=off,size=640x480"); + glimmer.module_settings = settings_new("TODO"); + + r = fb_new(>k_fb_ops, glimmer.fb_settings, NUM_FB_PAGES, &glimmer.fb); + if (r < 0) { + puts("fb no go!"); + return; + } + + gettimeofday(&glimmer.start_tv, NULL); + glimmer.module = rototiller_lookup_module(gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(glimmer.modules_list))); + r = rototiller_module_create_context( + glimmer.module, + get_ticks( + &glimmer.start_tv, + &glimmer.start_tv, + glimmer.ticks_offset), + &glimmer.module_context); + if (r < 0) { + puts("context no go!"); + return; + } + + pthread_t thread; + + pthread_create(&glimmer.thread, NULL, glimmer_thread, NULL); +} + + +static void activate(GtkApplication *app, gpointer user_data) +{ + GtkWidget *window, *vbox, *settings, *button; + const rototiller_module_t **modules; + size_t n_modules; + + rototiller_get_modules(&modules, &n_modules); + + window = gtk_application_window_new(app); + gtk_window_set_title(GTK_WINDOW(window), "glimmer"); + gtk_window_set_default_size(GTK_WINDOW(window), DEFAULT_WIDTH, DEFAULT_HEIGHT); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, BOX_SPACING); + gtk_container_add(GTK_CONTAINER(window), vbox); + + glimmer.modules_list = gtk_combo_box_text_new(); + for (size_t i = 0; i < n_modules; i++) { + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(glimmer.modules_list), NULL, modules[i]->name); + + /* like rototiller, default to rtv */ + if (!strcmp(modules[i]->name, "rtv")) + gtk_combo_box_set_active(GTK_COMBO_BOX(glimmer.modules_list), i); + } + gtk_container_add(GTK_CONTAINER(vbox), glimmer.modules_list); + + gtk_box_set_child_packing( + GTK_BOX(vbox), + glimmer.modules_list, + FALSE, + FALSE, + BOX_SPACING * 4, /* FIXME: having the combo box too near the window edge puts the pointer into the scroll-up arrow on click :/ */ + GTK_PACK_START); + + /* TODO: below the combobox, present framebuffer and the selected module's settings */ + settings = gtk_label_new("TODO: fb/module settings here"); + gtk_container_add(GTK_CONTAINER(vbox), settings); + gtk_box_set_child_packing( + GTK_BOX(vbox), + settings, + TRUE, + TRUE, + BOX_SPACING, + GTK_PACK_START); + + /* button to rototill as configured */ + button = gtk_button_new_with_label("Go!"); + gtk_container_add(GTK_CONTAINER(vbox), button); + g_signal_connect(button, "clicked", G_CALLBACK(glimmer_go), NULL); + + gtk_widget_show_all(window); +} + + +int main(int argc, char **argv) +{ + GtkApplication *app; + int status; + + rototiller_init(); + app = gtk_application_new("com.pengaru.glimmer", G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + status = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + rototiller_shutdown(); + + return status; +} |