diff --git a/Makefile b/Makefile index 9f754276a..113aba8e0 100644 --- a/Makefile +++ b/Makefile @@ -228,9 +228,6 @@ endif GDK_PIXBUF_CFLAGS := $(shell $(PKG_CONFIG) --cflags gdk-pixbuf-2.0) GDK_PIXBUF_LDFLAGS := $(shell $(PKG_CONFIG) --libs gdk-pixbuf-2.0) - -LIBPNG_CFLAGS := $(shell $(PKG_CONFIG) --cflags libpng) -LIBPNG_LDFLAGS := $(shell $(PKG_CONFIG) --libs libpng) endif ifeq (,$(PKG_CONFIG)) @@ -429,7 +426,7 @@ $(OBJ)/SDL/%.c.o: SDL/%.c $(OBJ)/XdgThumbnailer/%.c.o: XdgThumbnailer/%.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) $(LIBPNG_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -c $< -o $@ + $(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. @@ -438,7 +435,7 @@ $(OBJ)/XdgThumbnailer/emulate.c.o: $(OBJ)/XdgThumbnailer/resources.h # compilation with some version of the generator and/or compiler. $(OBJ)/XdgThumbnailer/%.c.o: $(OBJ)/XdgThumbnailer/%.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) $(LIBPNG_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -w -c $< -o $@ + $(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 $@) @@ -574,7 +571,7 @@ $(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin: $(BIN)/BootROMs $(BIN)/XdgThumbnailer/sameboy-thumbnailer: $(CORE_OBJECTS) $(XDG_THUMBNAILER_OBJECTS) -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(GIO_LDFLAGS) $(GDK_PIXBUF_LDFLAGS) $(LIBPNG_LDFLAGS) + $(CC) $^ -o $@ $(LDFLAGS) $(GIO_LDFLAGS) $(GDK_PIXBUF_LDFLAGS) # SDL Port @@ -657,14 +654,14 @@ $(BIN)/SDL/background.bmp: SDL/background.bmp -@$(MKDIR) -p $(dir $@) cp -f $< $@ -$(BIN)/SDL/Shaders: $(wildcard Shaders/*.fsh) +$(BIN)/SDL/Shaders: Shaders -@$(MKDIR) -p $@ - cp -rf $^ $@ + cp -rf $< $@ touch $@ -$(BIN)/SDL/Palettes: $(wildcard Misc/Palettes/*.sbp) +$(BIN)/SDL/Palettes: Misc/Palettes -@$(MKDIR) -p $@ - cp -rf $^ $@ + cp -rf $< $@ touch $@ # Boot ROMs diff --git a/XdgThumbnailer/emulate.c b/XdgThumbnailer/emulate.c index bd403e177..86a77fec1 100644 --- a/XdgThumbnailer/emulate.c +++ b/XdgThumbnailer/emulate.c @@ -1,7 +1,7 @@ #include "emulate.h" -#include #include +#include #include #include "Core/gb.h" @@ -27,8 +27,8 @@ void load_boot_rom(void) // 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); + g_error("Error loading boot ROM from \"%s\": expected to read %d bytes, got %zu", boot_rom_path, BOOT_ROM_SIZE, + length); // NOTREACHED } } @@ -37,10 +37,7 @@ void unload_boot_rom(void) { g_free(boot_rom); } /* --- */ -static char *async_input_callback(GB_gameboy_t *gb) -{ - return NULL; -} +static char *async_input_callback(GB_gameboy_t *gb) { return NULL; } static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) { @@ -54,14 +51,26 @@ static void vblank_callback(GB_gameboy_t *gb, GB_vblank_type_t type) GB_set_user_data(gb, GUINT_TO_POINTER(nb_frames_left)); // *Do* render the very last frame. - if (nb_frames_left == 0) { + if (nb_frames_left == 1) { GB_set_rendering_disabled(gb, false); } } static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { - return r | g << 8 | b << 16 | 0xFF << 24; + uint32_t rgba; + // The GdkPixbuf that will be created from the screen buffer later, expects components in the + // order [red, green, blue, alpha], from a uint8_t[] buffer. + // But SameBoy requires a uint32_t[] buffer, and don't know the endianness of `uint32_t`. + // So we treat each uint32_t as a 4-byte buffer, and write the bytes accordingly. + // This is guaranteed to not be UB, because casting a `T*` to any flavour of `char*` accesses + // and modifies the `T`'s "object representation". + unsigned char *bytes = (uint8_t *)&rgba; + bytes[0] = r; + bytes[1] = g; + bytes[2] = b; + bytes[3] = 0xFF; + return rgba; } unsigned emulate(enum FileKind kind, unsigned char const *rom, size_t rom_size, uint32_t screen[static 160 * 144]) @@ -70,7 +79,8 @@ unsigned emulate(enum FileKind kind, unsigned char const *rom, size_t rom_size, GB_init(&gb, GB_MODEL_CGB_E); GError *error = NULL; - GBytes *boot_rom = g_resource_lookup_data(resources_get_resource(), "/thumbnailer/cgb_boot_fast.bin", G_RESOURCE_LOOKUP_FLAGS_NONE, &error); + 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); size_t boot_rom_size; unsigned char const *boot_rom_data = g_bytes_get_data(boot_rom, &boot_rom_size); diff --git a/XdgThumbnailer/main.c b/XdgThumbnailer/main.c index ff384025c..8e3297ec0 100644 --- a/XdgThumbnailer/main.c +++ b/XdgThumbnailer/main.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -21,13 +22,12 @@ static char const *const object_path = "/com/github/liji32/sameboy/XdgThumbnaile ThumbnailerSpecializedThumbnailer1 *thumbnailer_interface = NULL; static unsigned max_nb_worker_threads; -static gboolean handle_queue(void *instance, GDBusMethodInvocation *invocation, char const *uri, - char const *mime_type, char const *flavor, gboolean urgent, - 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) { 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_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); struct NewTaskInfo task_info = new_task(urgent); @@ -37,8 +37,7 @@ static gboolean handle_queue(void *instance, GDBusMethodInvocation *invocation, return G_DBUS_METHOD_INVOCATION_HANDLED; } -static gboolean handle_dequeue(void *instance, GDBusMethodInvocation *invocation, unsigned handle, - void *user_data) +static gboolean handle_dequeue(void *instance, GDBusMethodInvocation *invocation, unsigned handle, void *user_data) { ThumbnailerSpecializedThumbnailer1 *skeleton = instance; g_info("Received Dequeue(handle=%u) request", handle); @@ -69,8 +68,8 @@ static void on_bus_acquired(GDBusConnection *connection, const char *name, void g_assert(res); g_assert_no_error(error); 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("Error exporting interface \"%s\" at \"%s\": %s", g_dbus_interface_skeleton_get_info(interface)->name, + object_path, error->message); // NOTREACHED } } @@ -112,13 +111,12 @@ int main(int argc, char const *argv[]) // 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); + 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 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); diff --git a/XdgThumbnailer/main.h b/XdgThumbnailer/main.h index 76a32c0c2..846a013ae 100644 --- a/XdgThumbnailer/main.h +++ b/XdgThumbnailer/main.h @@ -2,12 +2,12 @@ // 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, + 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; diff --git a/XdgThumbnailer/tasks.c b/XdgThumbnailer/tasks.c index b99d3b6b0..e7a489432 100644 --- a/XdgThumbnailer/tasks.c +++ b/XdgThumbnailer/tasks.c @@ -2,6 +2,7 @@ #include #include +#include #define URGENT_FLAG (1u << (sizeof(unsigned) * CHAR_BIT - 1)) // The compiler should warn if this shift is out of range. @@ -16,7 +17,7 @@ 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 *)); + task_list->tasks = g_array_new(false, false, sizeof(GCancellable *)); } void init_tasks(void) { @@ -24,7 +25,8 @@ void init_tasks(void) init_task_list(&tasks); } -static void cleanup_task_list(struct Tasks *task_list) { +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); @@ -70,8 +72,7 @@ got_slot: g_assert_cmpuint(index, !=, 0); g_assert_cmpuint(index, <, URGENT_FLAG); - return (struct NewTaskInfo){.handle = is_urgent ? (index | URGENT_FLAG) : index, - .cancellable = cancellable}; + return (struct NewTaskInfo){.handle = is_urgent ? (index | URGENT_FLAG) : index, .cancellable = cancellable}; } void cancel_task(unsigned handle) diff --git a/XdgThumbnailer/tasks.h b/XdgThumbnailer/tasks.h index ea050af8c..2d462145b 100644 --- a/XdgThumbnailer/tasks.h +++ b/XdgThumbnailer/tasks.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include void init_tasks(void); void cleanup_tasks(void); diff --git a/XdgThumbnailer/thumbnail.c b/XdgThumbnailer/thumbnail.c index e1ce61fa8..85b63e36b 100644 --- a/XdgThumbnailer/thumbnail.c +++ b/XdgThumbnailer/thumbnail.c @@ -1,30 +1,25 @@ #include "thumbnail.h" +#include #include #include +#include #include +#include #include #include "emulate.h" #include "main.h" #include "tasks.h" +#define DMG_ONLY_RESOURCE_PATH "/thumbnailer/CartridgeTemplate.png" +#define DUAL_RESOURCE_PATH "/thumbnailer/UniversalCartridgeTemplate.png" +#define CGB_ONLY_RESOURCE_PATH "/thumbnailer/ColorCartridgeTemplate.png" + #define THUMBNAILING_ERROR_DOMAIN (g_quark_from_static_string("thumbnailing")) /* --- */ -enum CartridgeType { - CART_DMG_ONLY, - CART_DUAL, - CART_CGB_ONLY, -}; - -void load_cartridge_images(void) { - // TODO -} - -/* --- */ - struct TaskData { char *contents; size_t length; @@ -39,33 +34,88 @@ static void destroy_task_data(void *data) g_slice_free(struct TaskData, task_data); } +char const *mime_type(enum FileKind kind) +{ + switch (kind) { + case KIND_GB: + return "application/x-gameboy-rom"; + case KIND_GBC: + return "application/x-gameboy-color-rom"; + case KIND_ISX: + return "application/x-gameboy-isx"; + } +} + /* --- */ -static void generate_thumbnail(GTask *task, void *source_object, void *data, - GCancellable *cancellable) +#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; - uint32_t screen[160 * 144]; - unsigned cgb_flag = emulate(task_data->kind, (unsigned char const *)task_data->contents, - task_data->length, screen); + 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` and `cgb_flag`. - enum CartridgeType type; + // 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), // 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, scaled_screen, 192, 298, // Match the displacement below. + GB_SCREEN_WIDTH * 4, GB_SCREEN_HEIGHT * 4, // Cropping the scaled rectangle... + 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: - type = CART_CGB_ONLY; - break; - case 0x80: - type = CART_DUAL; - break; - default: - type = CART_DMG_ONLY; - break; + 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; } - (void)type; + 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. + g_object_unref(template); - g_task_return_boolean(task, TRUE); + char file_size[sizeof(G_STRINGIFY(SIZE_MAX))]; + sprintf(file_size, "%zu", task_data->length); + // TODO: proper file name + gdk_pixbuf_save(scaled_screen, "/tmp/output.png", "png", &error, // "Base" parameters. + "tEXt::Thumb::URI", g_task_get_name(task), // 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); + + g_task_return_boolean(task, true); g_object_unref(task); } @@ -116,16 +166,15 @@ static void on_thumbnailing_end(GObject *source_object, GAsyncResult *res, void } 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, "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) +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); diff --git a/XdgThumbnailer/thumbnail.h b/XdgThumbnailer/thumbnail.h index 8880adb71..5759d38d0 100644 --- a/XdgThumbnailer/thumbnail.h +++ b/XdgThumbnailer/thumbnail.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include -void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, - char const *uri, char const *mime_type); +void start_thumbnailing(unsigned handle, GCancellable *cancellable, gboolean is_urgent, char const *uri, + char const *mime_type);