diff --git a/Makefile b/Makefile index 8c82a724a..fc07385e4 100644 --- a/Makefile +++ b/Makefile @@ -222,8 +222,8 @@ ifneq ($(shell pkg-config --exists gio-2.0 || echo 0),) GIO_CFLAGS = $(error The Gio library could not be found) GIO_LDFLAGS = $(error The Gio library could not be found) else -GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-2.0) -DG_LOG_USE_STRUCTURED -GIO_LDFLAGS := $(shell $(PKG_CONFIG) --libs gio-2.0) +GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-unix-2.0) -DG_LOG_USE_STRUCTURED +GIO_LDFLAGS := $(shell $(PKG_CONFIG) --libs gio-unix-2.0) ifeq ($(CONF),debug) GIO_CFLAGS += -DG_ENABLE_DEBUG else @@ -437,8 +437,6 @@ $(OBJ)/SDL/%.c.o: SDL/%.c $(OBJ)/XdgThumbnailer/%.c.o: XdgThumbnailer/%.c -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -c $< -o $@ -# Make sure not to attempt compiling this before generating the interface code. -$(OBJ)/XdgThumbnailer/main.c.o: $(OBJ)/XdgThumbnailer/interface.h # Make sure not to attempt compiling this before generating the resource code. $(OBJ)/XdgThumbnailer/emulate.c.o: $(OBJ)/XdgThumbnailer/resources.h # Silence warnings for this. It is code generated not by us, so we do not want `-Werror` to break @@ -447,10 +445,6 @@ $(OBJ)/XdgThumbnailer/%.c.o: $(OBJ)/XdgThumbnailer/%.c -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -w -c $< -o $@ -$(OBJ)/XdgThumbnailer/interface.c $(OBJ)/XdgThumbnailer/interface.h: XdgThumbnailer/interface.xml - -@$(MKDIR) -p $(dir $@) - gdbus-codegen --c-generate-autocleanup none --c-namespace Thumbnailer --interface-prefix org.freedesktop.thumbnails. --generate-c-code $(OBJ)/XdgThumbnailer/interface $< - $(OBJ)/XdgThumbnailer/resources.c $(OBJ)/XdgThumbnailer/resources.h: %: XdgThumbnailer/resources.gresource.xml $(BIN)/BootROMs/cgb_boot_fast.bin -@$(MKDIR) -p $(dir $@) CC=$(CC) glib-compile-resources --dependency-file $@.mk --generate-phony-targets --generate --target $@ $< diff --git a/XdgThumbnailer/emulate.c b/XdgThumbnailer/emulate.c index e1abebf27..4582cc5dc 100644 --- a/XdgThumbnailer/emulate.c +++ b/XdgThumbnailer/emulate.c @@ -1,8 +1,10 @@ #include "emulate.h" -#include #include +#include #include +#include +#include #include "Core/gb.h" @@ -15,15 +17,21 @@ /* --- */ -static char *async_input_callback(GB_gameboy_t *gb) { return NULL; } +static char *async_input_callback(GB_gameboy_t *gb) +{ + (void)gb; + return NULL; +} static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) { - // Swallow any logs. + (void)gb, (void)string, (void)attributes; // Swallow any logs. } static void vblank_callback(GB_gameboy_t *gb, GB_vblank_type_t type) { + (void)type; // Ignore the type, we use VBlank counting as a kind of pacing (and to avoid tearing). + unsigned nb_frames_left = GPOINTER_TO_UINT(GB_get_user_data(gb)); nb_frames_left--; GB_set_user_data(gb, GUINT_TO_POINTER(nb_frames_left)); @@ -51,45 +59,43 @@ static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) return rgba; } -unsigned emulate(enum FileKind kind, unsigned char const *rom, size_t rom_size, uint32_t screen[static 160 * 144]) +uint8_t emulate(char const *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]) { GB_gameboy_t gb; GB_init(&gb, GB_MODEL_CGB_E); + char const *last_dot = strrchr(path, '.'); + bool is_isx = last_dot && strcmp(last_dot + 1, "isx") == 0; + if (is_isx ? GB_load_isx(&gb, path) : GB_load_rom(&gb, path)) { + exit(EXIT_FAILURE); + } + GError *error = NULL; GBytes *boot_rom = g_resource_lookup_data(resources_get_resource(), "/thumbnailer/cgb_boot_fast.bin", G_RESOURCE_LOOKUP_FLAGS_NONE, &error); - g_assert_no_error(error); + g_assert_no_error(error); // This shouldn't be able to fail. size_t boot_rom_size; unsigned char const *boot_rom_data = g_bytes_get_data(boot_rom, &boot_rom_size); g_assert_cmpuint(boot_rom_size, ==, BOOT_ROM_SIZE); GB_load_boot_rom_from_buffer(&gb, boot_rom_data, boot_rom_size); g_bytes_unref(boot_rom); - if (kind == KIND_ISX) { - g_assert_not_reached(); // TODO: implement GB_load_isx_from_buffer - } - else { - GB_load_rom_from_buffer(&gb, rom, rom_size); - } - GB_set_user_data(&gb, GUINT_TO_POINTER(NB_FRAMES_TO_EMULATE)); GB_set_vblank_callback(&gb, vblank_callback); GB_set_pixels_output(&gb, screen); GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_async_input_callback(&gb, async_input_callback); - GB_set_log_callback(&gb, log_callback); + GB_set_log_callback(&gb, log_callback); // Anything bizarre the ROM does during emulation, we don't care about. GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_MODERN_BALANCED); GB_set_rendering_disabled(&gb, true); GB_set_turbo_mode(&gb, true, true); while (GPOINTER_TO_UINT(GB_get_user_data(&gb))) { - // TODO: handle cancellation GB_run(&gb); } - unsigned cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0; + int cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0; GB_free(&gb); return cgb_flag; } diff --git a/XdgThumbnailer/emulate.h b/XdgThumbnailer/emulate.h index 84bbe1ed1..f1a67010b 100644 --- a/XdgThumbnailer/emulate.h +++ b/XdgThumbnailer/emulate.h @@ -1,12 +1,8 @@ #pragma once -#include #include -enum FileKind { - KIND_GB, - KIND_GBC, - KIND_ISX, -}; +#define GB_SCREEN_WIDTH 160 +#define GB_SCREEN_HEIGHT 144 -unsigned emulate(enum FileKind kind, unsigned char const *rom, size_t rom_size, uint32_t screen[static 160 * 144]); +uint8_t emulate(char const *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]); diff --git a/XdgThumbnailer/interface.xml b/XdgThumbnailer/interface.xml deleted file mode 100644 index 33505b4cc..000000000 --- a/XdgThumbnailer/interface.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/XdgThumbnailer/main.c b/XdgThumbnailer/main.c index e7ffa96bc..02b4daa99 100644 --- a/XdgThumbnailer/main.c +++ b/XdgThumbnailer/main.c @@ -1,147 +1,128 @@ -#include "main.h" - -#include -#include -#include +#include +#include #include -#include #include +#include +#include #include #include -#include -#include "tasks.h" -#include "thumbnail.h" +#include "emulate.h" -// Auto-generated via `gdbus-codegen` from `interface.xml`. -#include "build/obj/XdgThumbnailer/interface.h" +static char const dmg_only_resource_path[] = "/thumbnailer/CartridgeTemplate.png"; +static char const dual_resource_path[] = "/thumbnailer/UniversalCartridgeTemplate.png"; +static char const cgb_only_resource_path[] = "/thumbnailer/ColorCartridgeTemplate.png"; -static char const name_on_bus[] = "com.github.liji32.sameboy.XdgThumbnailer"; -static char const object_path[] = "/com/github/liji32/sameboy/XdgThumbnailer"; - -ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface = NULL; -static unsigned max_nb_worker_threads; - -pid_t pid; - -static gboolean handle_queue(void *instance, GDBusMethodInvocation *invocation, char const *uri, char const *mime_type, - char const *flavor, gboolean urgent, void *user_data) +static GdkPixbuf *generate_thumbnail(char const *input_path) { - ThumbnailerSpecializedThumbnailer1 *skeleton = instance; - g_info("Received Queue(uri=\"%s\", mime_type=\"%s\", flavor=\"%s\", urgent=%s) request", uri, mime_type, flavor, - urgent ? "true" : "false"); - g_assert(skeleton == thumbnailer_interface); + uint32_t screen_raw[GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]; + uint8_t cgb_flag = emulate(input_path, screen_raw); - struct NewTaskInfo task_info = new_task(urgent); - start_thumbnailing(task_info.handle, task_info.cancellable, urgent, uri, mime_type, flavor); + // Generate the thumbnail from `screen_raw` and `cgb_flag`. - thumbnailer_specialized_thumbnailer1_complete_queue(skeleton, invocation, task_info.handle); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} + // `screen_raw` is properly formatted for this operation; see the comment in `rgb_encode` for a + // discussion of why and how. + GdkPixbuf *screen = gdk_pixbuf_new_from_data((uint8_t *)screen_raw, GDK_COLORSPACE_RGB, + true, // Yes, we have alpha! + 8, // bpp + GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT, // Size. + GB_SCREEN_WIDTH * sizeof(screen_raw[0]), // Row stride. + NULL, NULL); // Do not free the buffer. + // Scale the screen and position it in the appropriate place for compositing the cartridge templates. + GdkPixbuf *scaled_screen = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 1024, 1024); + gdk_pixbuf_scale(screen, // Source. + scaled_screen, // Destination. + 192, 298, // Match the displacement below. + GB_SCREEN_WIDTH * 4, GB_SCREEN_HEIGHT * 4, // How the scaled rectangle should be cropped. + 192, 298, // Displace the scaled screen so it lines up with the template. + 4, 4, // Scaling factors. + GDK_INTERP_NEAREST); + g_object_unref(screen); -static gboolean handle_dequeue(void *instance, GDBusMethodInvocation *invocation, unsigned handle, void *user_data) -{ - ThumbnailerSpecializedThumbnailer1 *skeleton = instance; - g_info("Received Dequeue(handle=%u) request", handle); - g_assert(skeleton == thumbnailer_interface); - - cancel_task(handle); - - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -static GMainLoop *main_loop; - -static void on_bus_acquired(GDBusConnection *connection, const char *name, void *user_data) -{ - g_assert_cmpstr(name, ==, name_on_bus); - (void)user_data; - g_info("Acquired bus"); - - // Create the interface, and hook up callbacks for when its methods are called. - thumbnailer_interface = thumbnailer_specialized_thumbnailer1_skeleton_new(); - g_signal_connect(thumbnailer_interface, "handle-queue", G_CALLBACK(handle_queue), NULL); - g_signal_connect(thumbnailer_interface, "handle-dequeue", G_CALLBACK(handle_dequeue), NULL); - - // Export the interface on the bus. GError *error = NULL; - GDBusInterfaceSkeleton *interface = G_DBUS_INTERFACE_SKELETON(thumbnailer_interface); - gboolean res = g_dbus_interface_skeleton_export(interface, connection, object_path, &error); - g_assert(res); + GdkPixbuf *template; + switch (cgb_flag) { + case 0xC0: + template = gdk_pixbuf_new_from_resource(cgb_only_resource_path, &error); + break; + case 0x80: + template = gdk_pixbuf_new_from_resource(dual_resource_path, &error); + break; + default: + template = gdk_pixbuf_new_from_resource(dmg_only_resource_path, &error); + break; + } g_assert_no_error(error); + g_assert_cmpint(gdk_pixbuf_get_width(template), ==, 1024); + g_assert_cmpint(gdk_pixbuf_get_height(template), ==, 1024); + gdk_pixbuf_composite(template, // Source. + scaled_screen, // Destination. + 0, 0, // Match the displacement below. + 1024, 1024, // Crop of the scaled rectangle. + 0, 0, // Displacement of the scaled rectangle. + 1, 1, // Scaling factors. + GDK_INTERP_NEAREST, // Doesn't really matter, but should be a little faster. + 255); // Blending factor of the source onto the destination. + g_object_unref(template); + + return scaled_screen; +} + +static GdkPixbuf *enforce_max_size(GdkPixbuf *thumbnail, unsigned long max_size) +{ + g_assert_cmpuint(gdk_pixbuf_get_width(thumbnail), ==, gdk_pixbuf_get_height(thumbnail)); + g_assert_cmpuint(gdk_pixbuf_get_width(thumbnail), ==, 1024); + // This is only a *max* size; don't bother scaling up. + // (This also prevents any overflow errors—notice that the scale function takes `int` size parameters!) + if (max_size > 1024) return thumbnail; + GdkPixbuf *scaled = gdk_pixbuf_scale_simple(thumbnail, max_size, max_size, GDK_INTERP_BILINEAR); + g_object_unref(thumbnail); + return scaled; +} + +static void write_thumbnail(GdkPixbuf *thumbnail, char const *output_path) +{ + GError *error = NULL; + // Intentionally be "not a good citizen": + // - Write directly to the provided path, instead of atomically replacing it with a fully-formed file; + // this is necessary for at least Tumbler (XFCE's thumbnailer daemon), which creates the file **and** keeps the + // returned FD—which keeps pointing to the deleted file... which is still empty! + // - Do not save any metadata to the PNG, since the thumbnailer daemon (again, at least XFCE's, the only one I have + // tested with) appears to read the PNG's pixels, and write a new one with the appropriate metadata. + // (Thank you! Saves me all that work.) + gdk_pixbuf_save(thumbnail, output_path, "png", &error, NULL); if (error) { - g_error("Error exporting interface \"%s\" at \"%s\": %s", g_dbus_interface_skeleton_get_info(interface)->name, - object_path, error->message); + g_error("Failed to save thumbnail: %s", error->message); // NOTREACHED } } -static void on_name_acquired(GDBusConnection *connection, const char *name, void *user_data) +int main(int argc, char *argv[]) { - g_assert_cmpstr(name, ==, name_on_bus); - (void)user_data; - - g_info("Acquired the name \"%s\" on the session bus", name); -} - -static void on_name_lost(GDBusConnection *connection, const char *name, void *user_data) -{ - g_assert_cmpstr(name, ==, name_on_bus); - (void)user_data; - - if (connection != NULL) { - g_info("Lost the name \"%s\" on the session bus", name); + if (argc != 3 && argc != 4) { + g_error("Usage: %s []", argv[0] ? argv[0] : "sameboy-thumbnailer"); + // NOTREACHED } - else { - g_error("Failed to connect to session bus"); + char const *input_path = argv[1]; + char *output_path = argv[2]; // Gets mutated in-place. + char const *max_size = argv[3]; // May be NULL. + + g_debug("%s -> %s [%s]", input_path, output_path, max_size ? max_size : "(none)"); + + GdkPixbuf *thumbnail = generate_thumbnail(input_path); + if (max_size) { + char *endptr; + errno = 0; + unsigned long size = strtoul(max_size, &endptr, 10); + if (errno != 0 || *max_size == '\0' || *endptr != '\0') { + g_error("Invalid size parameter \"%s\": %s", max_size, strerror(errno == 0 ? EINVAL : errno)); + // NOTREACHED + } + + thumbnail = enforce_max_size(thumbnail, size); } - g_main_loop_quit(main_loop); -} + write_thumbnail(thumbnail, output_path); + g_object_unref(thumbnail); -static gboolean handle_sigterm(void *user_data) -{ - g_info("SIGTERM received! Quitting..."); - - g_main_loop_quit(main_loop); - return G_SOURCE_CONTINUE; // Do not remove this source ourselves, let the post-main loop do so. -} - -/* --- */ - -int main(int argc, char const *argv[]) -{ - pid = getpid(); - - locate_and_create_thumbnail_dir(); - - max_nb_worker_threads = g_get_num_processors(); - // unsigned active_worker_threads = 0; - - // Create the task queue *before* starting to accept tasks from D-Bus. - init_tasks(); - // Likewise, create the main loop before then, so it can be aborted even before entering it. - main_loop = g_main_loop_new(NULL, false); - - // Refuse to replace the name or be replaced; there should only be one instance of the - // thumbnailer on the bus at all times. To replace this program, kill it. - unsigned owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, name_on_bus, G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, - on_bus_acquired, on_name_acquired, on_name_lost, NULL, NULL); - - unsigned sigterm_source_id = g_unix_signal_add(SIGTERM, handle_sigterm, NULL); - g_main_loop_run(main_loop); - // This must be done before destroying the main loop. - gboolean removed = g_source_remove(sigterm_source_id); - g_assert(removed); - - g_info("Waiting for outstanding tasks..."); - cleanup_tasks(); // Also waits for any remaining tasks. - // "Pedantic" cleanup for Valgrind et al. - g_main_loop_unref(main_loop); - g_bus_unown_name(owner_id); - if (thumbnailer_interface) { - g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(thumbnailer_interface)); - g_object_unref(thumbnailer_interface); - } - free_thumbnail_dir_path(); return 0; } diff --git a/XdgThumbnailer/main.h b/XdgThumbnailer/main.h deleted file mode 100644 index d9898e6f1..000000000 --- a/XdgThumbnailer/main.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -// As defined in the thumbnailer spec. -enum ErrorCode { - ERROR_UNKNOWN_SCHEME_OR_MIME, - ERROR_SPECIALIZED_THUMBNAILER_CONNECTION_FAILED, // Not applicable. - ERROR_INVALID_DATA, // Any file can be decoded as a GB ROM, apparently! - ERROR_THUMBNAIILING_THUMBNAIL, // We defer checking this to the generic thumbnailer. - ERROR_COULD_NOT_WRITE, - ERROR_UNSUPPORTED_FLAVOR, -}; - -struct _ThumbnailerSpecializedThumbnailer1; -extern struct _ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface; - -extern pid_t pid; diff --git a/XdgThumbnailer/sameboy.thumbnailer b/XdgThumbnailer/sameboy.thumbnailer new file mode 100644 index 000000000..63be4cfbb --- /dev/null +++ b/XdgThumbnailer/sameboy.thumbnailer @@ -0,0 +1,4 @@ +[Thumbnailer Entry] +TryExec=/home/issotm/SameBoy/build/bin/XdgThumbnailer/sameboy-thumbnailer +Exec=/home/issotm/SameBoy/build/bin/XdgThumbnailer/sameboy-thumbnailer %i %o %s +MimeType=application/x-gameboy-rom;application/x-gameboy-color-rom;application/x-gameboy-isx diff --git a/XdgThumbnailer/tasks.c b/XdgThumbnailer/tasks.c deleted file mode 100644 index e7a489432..000000000 --- a/XdgThumbnailer/tasks.c +++ /dev/null @@ -1,103 +0,0 @@ -#include "tasks.h" - -#include -#include -#include - -#define URGENT_FLAG (1u << (sizeof(unsigned) * CHAR_BIT - 1)) // The compiler should warn if this shift is out of range. - -struct Tasks { - // Note that the lock only applies to the whole array; individual elements may be mutated - // in-place just fine by the readers. - GRWLock lock; - GArray /* of GCancellable* */ *tasks; -}; -static struct Tasks urgent_tasks, tasks; - -static void init_task_list(struct Tasks *task_list) -{ - g_rw_lock_init(&task_list->lock); - task_list->tasks = g_array_new(false, false, sizeof(GCancellable *)); -} -void init_tasks(void) -{ - init_task_list(&urgent_tasks); - init_task_list(&tasks); -} - -static void cleanup_task_list(struct Tasks *task_list) -{ - // TODO: wait for the remaining tasks to end? - g_rw_lock_clear(&task_list->lock); - g_array_unref(task_list->tasks); -} -void cleanup_tasks(void) -{ - cleanup_task_list(&urgent_tasks); - cleanup_task_list(&tasks); -} - -struct NewTaskInfo new_task(gboolean is_urgent) -{ - struct Tasks *task_list = is_urgent ? &urgent_tasks : &tasks; - GCancellable **array = (void *)task_list->tasks->data; - - GCancellable *cancellable = g_cancellable_new(); - - // We may reallocate the array, so we need a writer lock. - g_rw_lock_writer_lock(&task_list->lock); - // First, look for a free slot in the array. - unsigned index = 0; - for (unsigned i = 0; i < task_list->tasks->len; ++i) { - if (array[i] == NULL) { - array[i] = cancellable; - index = i + 1; - goto got_slot; - } - } - // We need to allocate a new slot. - - // Each task list cannot contain 0x7FFFFFFF handles, as otherwise bit 7 cannot differentiate - // between regular and urgent tasks. - // Note that index 0 is invalid, since it's reserved for "no handle", so that's 1 less. - if (task_list->tasks->len == URGENT_FLAG - 2) { - g_object_unref(cancellable); - return (struct NewTaskInfo){.handle = 0}; - } - g_array_append_val(task_list->tasks, cancellable); - index = task_list->tasks->len; // We want the new index *plus one*. -got_slot: - g_rw_lock_writer_unlock(&task_list->lock); - - g_assert_cmpuint(index, !=, 0); - g_assert_cmpuint(index, <, URGENT_FLAG); - - return (struct NewTaskInfo){.handle = is_urgent ? (index | URGENT_FLAG) : index, .cancellable = cancellable}; -} - -void cancel_task(unsigned handle) -{ - struct Tasks *task_list = (handle & URGENT_FLAG) ? &urgent_tasks : &tasks; - - g_rw_lock_reader_lock(&task_list->lock); - GCancellable **slot = &((GCancellable **)task_list->tasks->data)[(handle & ~URGENT_FLAG) - 1]; - GCancellable *cancellable = *slot; - *slot = NULL; - g_rw_lock_reader_unlock(&task_list->lock); - - g_cancellable_cancel(cancellable); - g_object_unref(cancellable); -} - -void finished_task(unsigned handle) -{ - struct Tasks *task_list = (handle & URGENT_FLAG) ? &urgent_tasks : &tasks; - - g_rw_lock_reader_lock(&task_list->lock); - GCancellable **slot = &((GCancellable **)task_list->tasks->data)[(handle & ~URGENT_FLAG) - 1]; - GCancellable *cancellable = *slot; - *slot = NULL; - g_rw_lock_reader_unlock(&task_list->lock); - - g_object_unref(cancellable); -} diff --git a/XdgThumbnailer/tasks.h b/XdgThumbnailer/tasks.h deleted file mode 100644 index 2d462145b..000000000 --- a/XdgThumbnailer/tasks.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include - -void init_tasks(void); -void cleanup_tasks(void); - -struct NewTaskInfo { - unsigned handle; - GCancellable *cancellable; -}; -struct NewTaskInfo new_task(gboolean is_urgent); -void cancel_task(unsigned handle); -void finished_task(unsigned handle); diff --git a/XdgThumbnailer/thumbnail.c b/XdgThumbnailer/thumbnail.c deleted file mode 100644 index b8f588d73..000000000 --- a/XdgThumbnailer/thumbnail.c +++ /dev/null @@ -1,355 +0,0 @@ -#include "thumbnail.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "emulate.h" -#include "glibconfig.h" -#include "main.h" -#include "tasks.h" - -#define THUMBNAILING_ERROR_DOMAIN (g_quark_from_static_string("thumbnailing")) - -static char const dmg_only_resource_path[] = "/thumbnailer/CartridgeTemplate.png"; -static char const dual_resource_path[] = "/thumbnailer/UniversalCartridgeTemplate.png"; -static char const cgb_only_resource_path[] = "/thumbnailer/ColorCartridgeTemplate.png"; - -static char const gb_mime_type[] = "application/x-gameboy-rom"; -static char const gbc_mime_type[] = "application/x-gameboy-color-rom"; -static char const isx_mime_type[] = "application/x-gameboy-isx"; - -/* --- */ - -enum ThumbnailFlavor { - FLAVOR_NORMAL, // 128×128. - FLAVOR_LARGE, // 256×256. - FLAVOR_X_LARGE, // 512×512. - FLAVOR_XX_LARGE, // 1024×1024. -}; - -static char const flavor_dir_names[][9] = { - [FLAVOR_NORMAL] = "normal", - [FLAVOR_LARGE] = "large", - [FLAVOR_X_LARGE] = "x-large", - [FLAVOR_XX_LARGE] = "xx-large", -}; - -static GPathBuf thumbnail_dir_path; - -static __attribute__((returns_nonnull)) char const *get_home_path(void) -{ - char const *home = g_getenv("HOME"); // Statically allocated, so no need to free. - if (home != NULL) return home; - - // This is pushing it, but let's give it one last shot. - uid_t uid = geteuid(); - struct passwd *passwd_entry; - do { - errno = 0; // It may be left alone if no such entry can be found. - passwd_entry = getpwuid(uid); // Statically allocated, so no need to free. - } while (!passwd_entry && errno == EINTR); - if (passwd_entry == NULL) { - g_error("`XDG_CACHE_HOME` is unset, and failed to get the path to user %ju's home: %s", (uintmax_t)uid, - strerror(errno)); - // NOTREACHED - } - return passwd_entry->pw_dir; -} - -void locate_and_create_thumbnail_dir(void) -{ - // Compute the path to the thumbnail directory. - char const *cache_home = g_getenv("XDG_CACHE_HOME"); // Statically allocated, so no need to free. - if (cache_home != NULL) { - g_path_buf_init_from_path(&thumbnail_dir_path, cache_home); - } - else { - char const *home = get_home_path(); - g_path_buf_init_from_path(&thumbnail_dir_path, home); - g_path_buf_push(&thumbnail_dir_path, ".cache"); - } - g_path_buf_push(&thumbnail_dir_path, "thumbnails"); - - // Create the thumbnail directories if they don't already exist. - for (size_t i = 0; i < G_N_ELEMENTS(flavor_dir_names); ++i) { - g_path_buf_push(&thumbnail_dir_path, flavor_dir_names[i]); - char *path = g_path_buf_to_path(&thumbnail_dir_path); - - if (g_mkdir_with_parents(path, S_IRWXU) != 0) { // The permissions are mandated by the thumbnail spec. - g_error("Failed to create thumbnail cache directory at \"%s\": %s", path, strerror(errno)); - // NOTREACHED - } - - g_free(path); - // Restore the GPathBuf to its original state (pointing at the thumbnail cache's root dir). - g_path_buf_pop(&thumbnail_dir_path); - } - // ...thus, we end with the path buf pointing to the root dir, which is what the rest of this module expects. -} - -void free_thumbnail_dir_path(void) { g_path_buf_clear(&thumbnail_dir_path); } - -/* --- */ - -struct TaskData { - char *contents; - size_t length; - enum FileKind kind; - enum ThumbnailFlavor flavor; -}; - -static void destroy_task_data(void *data) -{ - struct TaskData *task_data = data; - - g_free(task_data->contents); - g_slice_free(struct TaskData, task_data); -} - -__attribute__((returns_nonnull)) char const *mime_type(enum FileKind kind) -{ - switch (kind) { - case KIND_GB: - return gb_mime_type; - case KIND_GBC: - return gbc_mime_type; - case KIND_ISX: - return isx_mime_type; - } - __builtin_unreachable(); -} - -/* --- */ - -#define GB_SCREEN_WIDTH 160 -#define GB_SCREEN_HEIGHT 144 - -static void generate_thumbnail(GTask *task, void *source_object, void *data, GCancellable *cancellable) -{ - struct TaskData *task_data = data; - char const *uri = g_task_get_name(task); - char *md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1); - - uint32_t screen_raw[GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]; - unsigned cgb_flag = - emulate(task_data->kind, (unsigned char const *)task_data->contents, task_data->length, screen_raw); - - // Generate the thumbnail from `screen_raw` and `cgb_flag`. - - // `screen_raw` is properly formatted for this operation; see the comment in `rgb_encode` for a - // discussion of why and how. - GdkPixbuf *screen = gdk_pixbuf_new_from_data((uint8_t *)screen_raw, GDK_COLORSPACE_RGB, - true, // Yes, we have alpha! - 8, // bpp - GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT, // Size. - GB_SCREEN_WIDTH * sizeof(screen_raw[0]), // Row stride. - NULL, NULL); // Do not free the buffer. - // Scale the screen and position it in the appropriate place for compositing the cartridge templates. - GdkPixbuf *scaled_screen = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 1024, 1024); - gdk_pixbuf_scale(screen, // Source. - scaled_screen, // Destination. - 192, 298, // Match the displacement below. - GB_SCREEN_WIDTH * 4, GB_SCREEN_HEIGHT * 4, // How the scaled rectangle should be cropped. - 192, 298, // Displace the scaled screen so it lines up with the template. - 4, 4, // Scaling factors. - GDK_INTERP_NEAREST); - g_object_unref(screen); - - GError *error = NULL; - GdkPixbuf *template; - switch (cgb_flag) { - case 0xC0: - template = gdk_pixbuf_new_from_resource(cgb_only_resource_path, &error); - break; - case 0x80: - template = gdk_pixbuf_new_from_resource(dual_resource_path, &error); - break; - default: - template = gdk_pixbuf_new_from_resource(dmg_only_resource_path, &error); - break; - } - g_assert_no_error(error); - g_assert_cmpint(gdk_pixbuf_get_width(template), ==, 1024); - g_assert_cmpint(gdk_pixbuf_get_height(template), ==, 1024); - gdk_pixbuf_composite(template, // Source. - scaled_screen, // Destination. - 0, 0, // Match the displacement below. - 1024, 1024, // Crop of the scaled rectangle. - 0, 0, // Displacement of the scaled rectangle. - 1, 1, // Scaling factors. - GDK_INTERP_NEAREST, // Doesn't really matter, but should be a little faster. - 255); // Blending factor of the source onto the destination. - g_object_unref(template); - - GPathBuf *output_path_buf = g_path_buf_copy(&thumbnail_dir_path); - g_path_buf_push(output_path_buf, flavor_dir_names[task_data->flavor]); - g_assert_cmpuint(strlen(md5), ==, 32); - // The buffer's size is checked by the compiler (`-Wformat-overflow=2`). - char temp_file_name[1 + 32 + 1 + 20 + 1 + 10 + 4 + 1]; // (".%s_%lu_%u.png", md5, pid, handle). - temp_file_name[0] = '.'; - memcpy(&temp_file_name[1], md5, 32); - sprintf(&temp_file_name[1 + 32], "_%lu_%u.png", - pid + 0lu, // Promote the `pid_t` to `long int` if possible; `-Wformat` will warn otherwise. - GPOINTER_TO_UINT(g_task_get_task_data(task))); - g_path_buf_push(output_path_buf, temp_file_name); - char *temp_file_path = g_path_buf_to_path(output_path_buf); - - g_debug("Saving pixel buf for \"%s\" to \"%s\"", g_task_get_name(task), temp_file_path); - char file_size[sizeof(G_STRINGIFY(SIZE_MAX))]; - sprintf(file_size, "%zu", task_data->length); - gdk_pixbuf_save(scaled_screen, temp_file_path, "png", &error, // "Base" parameters. - "tEXt::Thumb::URI", uri, // URI of the file being thumbnailed. - // "tEXt::Thumb::MTime", "", // TODO - "tEXt::Thumb::Size", file_size, // Size (in bytes) of the file being thumbnailed. - "tEXt::Thumb::Mimetype", mime_type(task_data->kind), // MIME type of the file being thumbnailed. - NULL); - g_object_unref(scaled_screen); - if (error) { - g_path_buf_free(output_path_buf); - g_free(temp_file_path); - - g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_COULD_NOT_WRITE, "Failed to write image: %s", error->message); - g_error_free(error); - g_object_unref(task); - return; - } - - // Trim off all of the "temporary" parts. - memcpy(&temp_file_name[1 + 32], ".png", sizeof(".png")); - g_path_buf_set_filename(output_path_buf, &temp_file_name[1]); - char *output_path = g_path_buf_free_to_path(output_path_buf); - g_debug("Moving thumbnail for \"%s\" from \"%s\" to \"%s\"", g_task_get_name(task), temp_file_path, output_path); - int rename_ret = rename(temp_file_path, output_path); - int rename_errno = errno; - g_free(temp_file_path); - g_free(output_path); - if (rename_ret != 0) { - g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_COULD_NOT_WRITE, "Failed to rename image from temp path: %s", strerror(rename_errno)); - g_object_unref(task); - return; - } - - g_task_return_boolean(task, true); - g_object_unref(task); -} - -// Callback when an async file operation completes. -static void on_file_ready(GObject *source_object, GAsyncResult *res, void *user_data) -{ - GFile *file = G_FILE(source_object); - GTask *task = user_data; - char const *uri = g_task_get_name(task); - g_debug("File \"%s\" is done being read", uri); - struct TaskData *task_data = g_task_get_task_data(task); - - GError *error = NULL; - g_file_load_contents_finish(file, res, &task_data->contents, &task_data->length, NULL, &error); - g_object_unref(file); - - if (error) { - g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_UNKNOWN_SCHEME_OR_MIME, - "Failed to load URI \"%s\": %s", uri, error->message); - g_error_free(error); - g_object_unref(task); - return; - } - - if (g_task_return_error_if_cancelled(task)) { - g_object_unref(task); - return; - } - - // TODO: cap the max number of active threads. - g_task_run_in_thread(task, generate_thumbnail); -} - -/* --- */ - -static void on_thumbnailing_end(GObject *source_object, GAsyncResult *res, void *user_data) -{ - // TODO: start a new thread if some task is pending. - - g_assert_null(source_object); // The object that was passed to `g_task_new`. - GTask *task = G_TASK(res); - g_info("Ending thumbnailing for \"%s\"", g_task_get_name(task)); - unsigned handle = GPOINTER_TO_UINT(user_data); - char const *uri = g_task_get_name(task); - - GError *error = NULL; - if (g_task_propagate_boolean(task, &error)) { - g_signal_emit_by_name(thumbnailer_interface, "ready", handle, uri); - } - else if (!g_cancellable_is_cancelled(g_task_get_cancellable(task))) { - // If the task was cancelled, do not emit an error response. - g_signal_emit_by_name(thumbnailer_interface, "error", handle, uri, error->code, error->message); - } - g_signal_emit_by_name(thumbnailer_interface, "finished", handle); - - finished_task(handle); -} - -void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, char const *uri, - char const *mime_type, char const *flavor_name) -{ - g_signal_emit_by_name(thumbnailer_interface, "started", handle); - - GTask *task = g_task_new(NULL, cancellable, on_thumbnailing_end, GUINT_TO_POINTER(handle)); - g_task_set_priority(task, is_urgent ? G_PRIORITY_HIGH : G_PRIORITY_DEFAULT); - g_task_set_name(task, uri); - - enum FileKind kind; - if (g_strcmp0(mime_type, gbc_mime_type) == 0) { - kind = KIND_GBC; - } - else if (g_strcmp0(mime_type, gb_mime_type) == 0) { - kind = KIND_GB; - } - else if (g_strcmp0(mime_type, isx_mime_type) == 0) { - kind = KIND_ISX; - } - else { - g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_UNKNOWN_SCHEME_OR_MIME, - "Unsupported MIME type \"%s\"", mime_type); - g_object_unref(task); - return; - } - - enum ThumbnailFlavor flavor; - if (g_strcmp0(flavor_name, "normal") == 0) { - flavor = FLAVOR_NORMAL; - } - else if (g_strcmp0(flavor_name, "large") == 0) { - flavor = FLAVOR_LARGE; - } - else if (g_strcmp0(flavor_name, "x-large") == 0) { - flavor = FLAVOR_X_LARGE; - } - else if (g_strcmp0(flavor_name, "xx-large") == 0) { - flavor = FLAVOR_XX_LARGE; - } - else { - g_task_return_new_error(task, THUMBNAILING_ERROR_DOMAIN, ERROR_UNSUPPORTED_FLAVOR, - "Unsupported thumbnail size/flavor \"%s\"", flavor_name); - g_object_unref(task); - return; - } - - struct TaskData *task_data = g_slice_new(struct TaskData); - task_data->contents = NULL; - task_data->kind = kind; - task_data->flavor = flavor; - g_task_set_task_data(task, task_data, destroy_task_data); - - GFile *file = g_file_new_for_uri(uri); - g_file_load_contents_async(file, cancellable, on_file_ready, task); -} diff --git a/XdgThumbnailer/thumbnail.h b/XdgThumbnailer/thumbnail.h deleted file mode 100644 index 1112a8331..000000000 --- a/XdgThumbnailer/thumbnail.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include -#include - -void locate_and_create_thumbnail_dir(void); -void free_thumbnail_dir_path(void); - -void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, char const *uri, - char const *mime_type, char const *flavor_name);