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; +}  | 
