diff --git a/.gitattributes b/.gitattributes index 2149ea1dc..49c17bea4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,4 +7,5 @@ HexFiend/* linguist-vendored Core/*.h linguist-language=C SDL/*.h linguist-language=C Windows/*.h linguist-language=C +XdgThumbnailer/*.h linguist-language=C Cocoa/*.h linguist-language=Objective-C diff --git a/Core/gb.c b/Core/gb.c index 2369fbf9d..8d520c2f1 100644 --- a/Core/gb.c +++ b/Core/gb.c @@ -549,7 +549,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) bank = byte; if (byte >= 0x80) { READ(byte); - /* TODO: This is just a guess, the docs don't elaborator on how banks > 0xFF are saved, + /* TODO: This is just a guess, the docs don't elaborate on how banks > 0xFF are saved, other than the fact that banks >= 80 requires two bytes to store them, and I haven't encountered an ISX file for a ROM larger than 4MBs yet. */ bank += byte << 7; diff --git a/Makefile b/Makefile index 40f8a37d5..eeda729f1 100644 --- a/Makefile +++ b/Makefile @@ -338,7 +338,7 @@ endif cocoa: $(BIN)/SameBoy.app quicklook: $(BIN)/SameBoy.qlgenerator -xdg-thumbnailer: $(BIN)/XdgThumbnailer/sameboy-thumbnailer +xdg-thumbnailer: $(BIN)/XdgThumbnailer/sameboy-thumbnailer $(BIN)/SDL/cgb_boot_fast.bin sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_boot.bin $(BIN)/SDL/mgb_boot.bin $(BIN)/SDL/cgb0_boot.bin $(BIN)/SDL/cgb_boot.bin $(BIN)/SDL/agb_boot.bin $(BIN)/SDL/sgb_boot.bin $(BIN)/SDL/sgb2_boot.bin $(BIN)/SDL/LICENSE $(BIN)/SDL/registers.sym $(BIN)/SDL/background.bmp $(BIN)/SDL/Shaders $(BIN)/SDL/Palettes bootroms: $(BIN)/BootROMs/agb_boot.bin $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/cgb0_boot.bin $(BIN)/BootROMs/dmg_boot.bin $(BIN)/BootROMs/mgb_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin tester: $(TESTER_TARGET) $(BIN)/tester/dmg_boot.bin $(BIN)/tester/cgb_boot.bin $(BIN)/tester/agb_boot.bin $(BIN)/tester/sgb_boot.bin $(BIN)/tester/sgb2_boot.bin @@ -423,21 +423,21 @@ $(OBJ)/SDL/%.c.o: SDL/%.c $(OBJ)/XdgThumbnailer/%.c.o: XdgThumbnailer/%.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(GIO_CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(GIO_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 # Silence warnings for this. It is code generated not by us, so we do not want `-Werror` to break # compilation with some version of the generator and/or compiler. $(OBJ)/XdgThumbnailer/interface.c.o: $(OBJ)/XdgThumbnailer/interface.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(GIO_CFLAGS) -w -c $< -o $@ + $(CC) $(CFLAGS) $(GIO_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)/OpenDialog/%.c.o: OpenDialog/%.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ $(OBJ)/%.c.o: %.c @@ -688,7 +688,8 @@ ifneq ($(FREEDESKTOP),) ICON_NAMES := apps/sameboy mimetypes/x-gameboy-rom mimetypes/x-gameboy-color-rom ICON_SIZES := 16x16 32x32 64x64 128x128 256x256 512x512 ICONS := $(foreach name,$(ICON_NAMES), $(foreach size,$(ICON_SIZES),$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(size)/$(name).png)) -install: sdl $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml $(ICONS) FreeDesktop/sameboy.desktop +# TODO: install the thumbnailer as well +install: sdl xdg-thumbnailer $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml $(ICONS) FreeDesktop/sameboy.desktop -@$(MKDIR) -p $(dir $(DESTDIR)$(PREFIX)) mkdir -p $(DESTDIR)$(DATA_DIR)/ $(DESTDIR)$(PREFIX)/bin/ cp -rf $(BIN)/SDL/* $(DESTDIR)$(DATA_DIR)/ @@ -725,7 +726,7 @@ endif ios: @$(MAKE) _ios - + $(BIN)/SameBoy-iOS.ipa: ios iOS/sideload.entitlements $(MKDIR) -p $(OBJ)/Payload cp -rf $(BIN)/SameBoy-iOS.app $(OBJ)/Payload/SameBoy-iOS.app diff --git a/XdgThumbnailer/main.c b/XdgThumbnailer/main.c index da44ca606..54b4c5848 100644 --- a/XdgThumbnailer/main.c +++ b/XdgThumbnailer/main.c @@ -1,4 +1,4 @@ -#define G_LOG_DOMAIN "sameboy-thumbnailer" +#include "main.h" #include #include @@ -8,45 +8,46 @@ #include #include +#include "tasks.h" +#include "thumbnail.h" + // Auto-generated via `gdbus-codegen` from `interface.xml`. #include "build/obj/XdgThumbnailer/interface.h" static char const *const name_on_bus = "com.github.liji32.sameboy.XdgThumbnailer"; static char const *const object_path = "/com/github/liji32/sameboy/XdgThumbnailer"; -/* --- The main work being performed here --- */ +ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface = NULL; +static unsigned max_nb_worker_threads; -static GThreadPool *thread_pool; - -// The function called by the threads in `thread_pool`. -static void generate_thumbnail(void *data, void *user_data) +static gboolean handle_queue(void *instance, GDBusMethodInvocation *invocation, char const *uri, + char const *mime_type, char const *flavor, gboolean urgent, + void *user_data) { - // TODO -} + 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); -static gboolean handle_queue(ThumbnailerSpecializedThumbnailer1 *object, - GDBusMethodInvocation *invocation, char const *uri, char const *mime_type, - char const *flavor, gboolean urgent) -{ - g_info("Received Queue(uri=\"%s\", mime_type=\"%s\", flavor=\"%s\", urgent=%s) request", uri, mime_type, flavor, urgent ? "true" : "false"); - - // TODO + struct NewTaskInfo task_info = new_task(urgent); + start_thumbnailing(task_info.handle, task_info.cancellable, urgent, uri, mime_type); + thumbnailer_specialized_thumbnailer1_complete_queue(skeleton, invocation, task_info.handle); return G_DBUS_METHOD_INVOCATION_HANDLED; } -static gboolean handle_dequeue(ThumbnailerSpecializedThumbnailer1 *object, - GDBusMethodInvocation *invocation, unsigned handle) +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); - // TODO + cancel_task(handle); return G_DBUS_METHOD_INVOCATION_HANDLED; } -/* --- "Glue"; or, how the above is orchestrated / wired up --- */ - static GMainLoop *main_loop; static void on_bus_acquired(GDBusConnection *connection, const char *name, void *user_data) @@ -55,16 +56,13 @@ static void on_bus_acquired(GDBusConnection *connection, const char *name, void (void)user_data; g_info("Acquired bus"); - GError *error; - // Create the interface, and hook up callbacks for when its methods are called. - ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface = - thumbnailer_specialized_thumbnailer1_skeleton_new(); + 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. - error = NULL; + 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); @@ -108,14 +106,11 @@ static gboolean handle_sigterm(void *user_data) int main(int argc, char const *argv[]) { - GError *error; - - // Create the thread pool *before* starting to accept tasks from D-Bus. - // Make it non-exclusive so that the number of spawned threads grows dynamically, to consume - // fewer system resources when no thumbnails are being generated. - thread_pool = - g_thread_pool_new(generate_thumbnail, NULL, g_get_num_processors(), FALSE, &error); - g_assert_no_error(error); // Creating a non-exclusive thread pool cannot generate errors. + 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(); + load_boot_roms(); // Likewise, create the main loop before then, so it can be aborted even before entering it. main_loop = g_main_loop_new(NULL, FALSE); @@ -127,13 +122,19 @@ int main(int argc, char const *argv[]) unsigned sigterm_source_id = g_unix_signal_add(SIGTERM, handle_sigterm, NULL); g_main_loop_run(main_loop); - gboolean removed = - g_source_remove(sigterm_source_id); // This must be done before destroying the 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..."); - g_thread_pool_free(thread_pool, FALSE, TRUE); + cleanup_tasks(); // Also waits for any remaining tasks. + // "Pedantic" cleanup for Valgrind et al. + unload_boot_roms(); 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); return 0; } diff --git a/XdgThumbnailer/main.h b/XdgThumbnailer/main.h new file mode 100644 index 000000000..76438e10a --- /dev/null +++ b/XdgThumbnailer/main.h @@ -0,0 +1,14 @@ +#pragma once + +// As defined in the thumbnailer spec. +enum ErrorCode { + ERROR_UNKNOWN_SCHEME_OR_MIME, + ERROR_SPECIALIZED_THUMBNAILER_CONNECTION_FAILED, + ERROR_INVALID_DATA, + ERROR_THUMBNAIILING_THUMBNAIL, + ERROR_COULD_NOT_WRITE, + ERROR_UNSUPPORTED_FLAVOR, +}; + +struct _ThumbnailerSpecializedThumbnailer1; +extern struct _ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface; diff --git a/XdgThumbnailer/tasks.c b/XdgThumbnailer/tasks.c new file mode 100644 index 000000000..b99d3b6b0 --- /dev/null +++ b/XdgThumbnailer/tasks.c @@ -0,0 +1,102 @@ +#include "tasks.h" + +#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 new file mode 100644 index 000000000..ea050af8c --- /dev/null +++ b/XdgThumbnailer/tasks.h @@ -0,0 +1,15 @@ +#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 new file mode 100644 index 000000000..8b59753fa --- /dev/null +++ b/XdgThumbnailer/thumbnail.c @@ -0,0 +1,166 @@ +#include "thumbnail.h" + +#include +#include +#include + +#include "Core/gb.h" +#include "XdgThumbnailer/tasks.h" +#include "main.h" + +#define THUMBNAILING_ERROR_DOMAIN (g_quark_from_static_string("thumbnailing")) + +enum FileKind { + KIND_GB, + KIND_GBC, + KIND_ISX, +}; + +#define BOOT_ROM_SIZE (0x100 + 0x800) // The two "parts" of it, which are stored contiguously. +static char *boot_rom; + +void load_boot_roms(void) +{ + static char const *boot_rom_path = DATA_DIR "/cgb_boot_fast.bin"; + + size_t length; + GError *error = NULL; + g_file_get_contents(boot_rom_path, &boot_rom, &length, &error); + + if (error) { + g_error("Error loading boot ROM from \"%s\": %s", boot_rom_path, error->message); + // NOTREACHED + } + else if (length != BOOT_ROM_SIZE) { + g_error("Error loading boot ROM from \"%s\": expected to read %d bytes, got %zu", + boot_rom_path, BOOT_ROM_SIZE, length); + // NOTREACHED + } +} + +void unload_boot_roms(void) { g_free(boot_rom); } + +struct TaskData { + char *contents; + size_t length; + enum FileKind kind; +}; + +static void destroy_task_data(void *data) +{ + struct TaskData *task_data = data; + + g_free(task_data->contents); + g_slice_free(struct TaskData, task_data); +} + +static void generate_thumbnail(GTask *task, void *source_object, void *data, + GCancellable *cancellable) +{ + struct TaskData *task_data = data; + + GB_gameboy_t gb; + GB_init(&gb, GB_MODEL_CGB_E); + GB_load_boot_rom_from_buffer(&gb, (unsigned char const *)boot_rom, sizeof(boot_rom)); + + if (task_data->kind == KIND_ISX) { + g_assert_not_reached(); // TODO: implement GB_load_isx_from_buffer + } + else { + GB_load_rom_from_buffer(&gb, (unsigned char const *)task_data->contents, task_data->length); + } + // TODO + + GB_free(&gb); + + 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_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_debug("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) +{ + 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, "application/x-gameboy-color-rom") == 0) { + kind = KIND_GBC; + } + else if (g_strcmp0(mime_type, "application/x-gameboy-rom") == 0) { + kind = KIND_GB; + } + else if (g_strcmp0(mime_type, "application/x-gameboy-isx") == 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; + } + + struct TaskData *task_data = g_slice_new(struct TaskData); + task_data->contents = NULL; + task_data->kind = kind; + 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 new file mode 100644 index 000000000..35db9ff75 --- /dev/null +++ b/XdgThumbnailer/thumbnail.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +void load_boot_roms(void); +void unload_boot_roms(void); + +void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, + char const *uri, char const *mime_type);