Implement the "plumbing" around thumbnail generation

Only the actual thumbnail generation is left!
This commit is contained in:
ISSOtm 2024-06-26 22:53:23 +02:00
parent e4ceb3d93b
commit b3cecf2413
9 changed files with 353 additions and 43 deletions

1
.gitattributes vendored
View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -1,4 +1,4 @@
#define G_LOG_DOMAIN "sameboy-thumbnailer"
#include "main.h"
#include <gio/gio.h>
#include <glib-object.h>
@ -8,45 +8,46 @@
#include <stdlib.h>
#include <string.h>
#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;
}

14
XdgThumbnailer/main.h Normal file
View File

@ -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;

102
XdgThumbnailer/tasks.c Normal file
View File

@ -0,0 +1,102 @@
#include "tasks.h"
#include <gio/gio.h>
#include <glib.h>
#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);
}

15
XdgThumbnailer/tasks.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include <glib.h>
#include <gio/gio.h>
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);

166
XdgThumbnailer/thumbnail.c Normal file
View File

@ -0,0 +1,166 @@
#include "thumbnail.h"
#include <gio/gio.h>
#include <glib.h>
#include <stdlib.h>
#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);
}

View File

@ -0,0 +1,10 @@
#pragma once
#include <glib.h>
#include <gio/gio.h>
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);