Implement image compositing and rendering

Not saving to the correct place yet, but almost there!
This commit is contained in:
ISSOtm
2024-06-28 16:14:58 +02:00
parent d873abfadf
commit 0b6a73f380
8 changed files with 136 additions and 81 deletions

View File

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

View File

@@ -1,7 +1,7 @@
#include "emulate.h"
#include <glib.h>
#include <gio/gio.h>
#include <glib.h>
#include <stdint.h>
#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);

View File

@@ -5,6 +5,7 @@
#include <glib-unix.h>
#include <glib.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
@@ -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);

View File

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

View File

@@ -2,6 +2,7 @@
#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.
@@ -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)

View File

@@ -1,7 +1,7 @@
#pragma once
#include <glib.h>
#include <gio/gio.h>
#include <glib.h>
void init_tasks(void);
void cleanup_tasks(void);

View File

@@ -1,30 +1,25 @@
#include "thumbnail.h"
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gio/gio.h>
#include <glib.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#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);

View File

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