Scrap all of that, and use the undocumented spec that everyone actually uses

This commit is contained in:
ISSOtm
2024-07-04 20:02:51 +02:00
parent 323f3e89b7
commit 4cc34f97ec
11 changed files with 134 additions and 686 deletions

View File

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

View File

@@ -1,8 +1,10 @@
#include "emulate.h"
#include <gio/gio.h>
#include <glib.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View File

@@ -1,12 +1,8 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
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]);

View File

@@ -1,32 +0,0 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/com/github/liji32/sameboy/XdgThumbnailer">
<interface name="org.freedesktop.thumbnails.SpecializedThumbnailer1">
<method name="Queue">
<arg type="s" name="uri" direction="in" />
<arg type="s" name="mime_type" direction="in" />
<arg type="s" name="flavor" direction="in" />
<arg type="b" name="urgent" direction="in" />
<arg type="u" name="handle" direction="out" />
</method>
<method name="Dequeue">
<arg type="u" name="handle" direction="in" />
</method>
<signal name="Ready">
<arg type="u" name="handle" />
<arg type="s" name="uri" />
</signal>
<signal name="Started">
<arg type="u" name="handle" />
</signal>
<signal name="Finished">
<arg type="u" name="handle" />
</signal>
<signal name="Error">
<arg type="u" name="handle" />
<arg type="s" name="failed_uri" />
<arg type="i" name="error_code" />
<arg type="s" name="message" />
</signal>
</interface>
</node>

View File

@@ -1,147 +1,128 @@
#include "main.h"
#include <gio/gio.h>
#include <glib-object.h>
#include <glib-unix.h>
#include <errno.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <glib.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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 <input path> <output path> [<size>]", 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;
}

View File

@@ -1,18 +0,0 @@
#pragma once
#include <unistd.h>
// 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;

View File

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

View File

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

View File

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

View File

@@ -1,355 +0,0 @@
#include "thumbnail.h"
#include <fcntl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gio/gio.h>
#include <glib-unix.h>
#include <glib.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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);
}

View File

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