diff --git a/.gitattributes b/.gitattributes index 2149ea1..49c17be 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 398ff24..332c592 100644 --- a/Core/gb.c +++ b/Core/gb.c @@ -550,7 +550,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/FreeDesktop/sameboy.xml b/FreeDesktop/sameboy.xml index 18123ed..2e15fde 100644 --- a/FreeDesktop/sameboy.xml +++ b/FreeDesktop/sameboy.xml @@ -1,23 +1,38 @@ - - Game Boy ROM - - - - - - - Game Boy Color ROM - - - - - - - Game Boy ISX binary - - - - + + Game Boy ROM + + + + + + + + + + + + + Game Boy Color ROM + + + + + + + + + + + + + Game Boy ISX binary + + + + + + + diff --git a/Makefile b/Makefile index 582fb97..fce2b1f 100644 --- a/Makefile +++ b/Makefile @@ -205,18 +205,39 @@ SDL_LDFLAGS += -lopenal endif SDL_AUDIO_DRIVERS += openal endif -else +else # ifneq ($(PKG_CONFIG),) SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2) SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) -lpthread # Allow OpenAL to be disabled even if the development libraries are available ifneq ($(ENABLE_OPENAL),0) -ifeq ($(shell $(PKG_CONFIG) --exists openal && echo 0),0) +ifneq ($(shell $(PKG_CONFIG) --exists openal && echo 0),) SDL_CFLAGS += $(shell $(PKG_CONFIG) --cflags openal) -DENABLE_OPENAL SDL_LDFLAGS += $(shell $(PKG_CONFIG) --libs openal) SDL_AUDIO_DRIVERS += openal endif endif + +ifneq ($(shell $(PKG_CONFIG) --exists gio-unix-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-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 +GIO_CFLAGS += -DG_DISABLE_ASSERT +endif +endif + +ifneq ($(shell $(PKG_CONFIG) --exists gdk-pixbuf-2.0 || echo 0),) +GDK_PIXBUF_CFLAGS = $(error The Gdk-Pixbuf library could not be found) +GDK_PIXBUF_LDFLAGS = $(error The Gdk-Pixbuf library could not be found) +else +GDK_PIXBUF_CFLAGS := $(shell $(PKG_CONFIG) --cflags gdk-pixbuf-2.0) +GDK_PIXBUF_LDFLAGS := $(shell $(PKG_CONFIG) --libs gdk-pixbuf-2.0) +endif endif ifeq (,$(PKG_CONFIG)) @@ -330,6 +351,7 @@ endif cocoa: $(BIN)/SameBoy.app quicklook: $(BIN)/SameBoy.qlgenerator +xdg-thumbnailer: $(BIN)/XdgThumbnailer/sameboy-thumbnailer 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 @@ -345,6 +367,9 @@ all: sdl tester libretro lib ifeq ($(PLATFORM),Darwin) all: cocoa ios-ipa ios-deb endif +ifneq ($(FREEDESKTOP),) +all: xdg-thumbnailer +endif # Get a list of our source files and their respective object file targets @@ -355,6 +380,7 @@ TESTER_SOURCES := $(shell ls Tester/*.c) IOS_SOURCES := $(filter-out iOS/installer.m, $(shell ls iOS/*.m)) $(shell ls AppleCommon/*.m) COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) $(shell ls JoyKit/*.m) $(shell ls AppleCommon/*.m) QUICKLOOK_SOURCES := $(shell ls QuickLook/*.m) $(shell ls QuickLook/*.c) +XDG_THUMBNAILER_SOURCES := $(shell ls XdgThumbnailer/*.c) ifeq ($(PLATFORM),windows32) CORE_SOURCES += $(shell ls Windows/*.c) @@ -367,6 +393,7 @@ IOS_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(IOS_SOURCES)) QUICKLOOK_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(QUICKLOOK_SOURCES)) SDL_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(SDL_SOURCES)) TESTER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(TESTER_SOURCES)) +XDG_THUMBNAILER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(XDG_THUMBNAILER_SOURCES)) $(OBJ)/XdgThumbnailer/resources.c.o lib: $(PUBLIC_HEADERS) @@ -410,9 +437,25 @@ $(OBJ)/SDL/%.c.o: SDL/%.c -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ +$(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 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 +# 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) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -w -c $< -o $@ + +$(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 $@ $< +-include $(OBJ)/XdgThumbnailer/resources.c.mk $(OBJ)/XdgThumbnailer/resources.h.mk + $(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 @@ -427,7 +470,7 @@ $(OBJ)/HexFiend/%.m.o: HexFiend/%.m $(OBJ)/%.m.o: %.m -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ - + # iOS Port $(BIN)/SameBoy-iOS.app: $(BIN)/SameBoy-iOS.app/SameBoy \ @@ -530,7 +573,13 @@ endif $(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin: $(BIN)/BootROMs/cgb_boot_fast.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + +# XDG thumbnailer + +$(BIN)/XdgThumbnailer/sameboy-thumbnailer: $(CORE_OBJECTS) $(XDG_THUMBNAILER_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(GIO_LDFLAGS) $(GDK_PIXBUF_LDFLAGS) + # SDL Port # Unix versions build only one binary @@ -584,41 +633,43 @@ $(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) -Wl,/subsystem:console -$(BIN)/SDL/%.bin: $(BOOTROMS_DIR)/%.bin - -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ - $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ $(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ $(BIN)/SameBoy-iOS.app/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ + +$(BIN)/SDL/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $< $@ $(BIN)/SDL/LICENSE: LICENSE -@$(MKDIR) -p $(dir $@) - grep -v "^ " $^ > $@ + grep -v "^ " $< > $@ $(BIN)/SDL/registers.sym: Misc/registers.sym -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ $(BIN)/SDL/background.bmp: SDL/background.bmp -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ -$(BIN)/SDL/Shaders: Shaders +$(BIN)/SDL/Shaders: $(wildcard Shaders/*.fsh) -@$(MKDIR) -p $@ - cp -rf Shaders/*.fsh $@ - + cp -f $^ $@ + touch $@ + $(BIN)/SDL/Palettes: Misc/Palettes -@$(MKDIR) -p $@ - cp -rf Misc/Palettes/*.sbp $@ + cp -f $ +#include +#include +#include +#include + +#include "Core/gb.h" + +// Auto-generated via `glib-compile-resources` from `resources.gresource.xml`. +#include "build/obj/XdgThumbnailer/resources.h" + +#define NB_FRAMES_TO_EMULATE (60 * 10) + +#define BOOT_ROM_SIZE (0x100 + 0x800) // The two "parts" of it, which are stored contiguously. + +/* --- */ + +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) +{ + (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 = GB_get_user_data(gb); + (*nb_frames_left)--; + + // *Do* render the very last frame. + 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) +{ + 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". + uint8_t *bytes = (uint8_t *)&rgba; + bytes[0] = r; + bytes[1] = g; + bytes[2] = b; + bytes[3] = 0xFF; + return rgba; +} + +uint8_t emulate(const char *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]) +{ + GB_gameboy_t gb; + GB_init(&gb, GB_MODEL_CGB_E); + + const char *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); // This shouldn't be able to fail. + size_t boot_rom_size; + const uint8_t *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); + + 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); // Anything bizarre the ROM does during emulation, we don't care about. + GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_MODERN_BALANCED); + + unsigned nb_frames_left = NB_FRAMES_TO_EMULATE; + GB_set_user_data(&gb, &nb_frames_left); + + GB_set_rendering_disabled(&gb, true); + GB_set_turbo_mode(&gb, true, true); + while (nb_frames_left) { + GB_run(&gb); + } + + int cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0; + GB_free(&gb); + return cgb_flag; +} diff --git a/XdgThumbnailer/emulate.h b/XdgThumbnailer/emulate.h new file mode 100644 index 0000000..3e75d4a --- /dev/null +++ b/XdgThumbnailer/emulate.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#define GB_SCREEN_WIDTH 160 +#define GB_SCREEN_HEIGHT 144 + +uint8_t emulate(const char *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]); diff --git a/XdgThumbnailer/main.c b/XdgThumbnailer/main.c new file mode 100644 index 0000000..2a263fb --- /dev/null +++ b/XdgThumbnailer/main.c @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "emulate.h" + +static const char dmg_only_resource_path[] = "/thumbnailer/CartridgeTemplate.png"; +static const char dual_resource_path[] = "/thumbnailer/UniversalCartridgeTemplate.png"; +static const char cgb_only_resource_path[] = "/thumbnailer/ColorCartridgeTemplate.png"; + +static GdkPixbuf *generate_thumbnail(const char *input_path) +{ + uint32_t screen_raw[GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]; + uint8_t cgb_flag = emulate(input_path, 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); + + return scaled_screen; +} + +static GdkPixbuf *enforce_max_size(GdkPixbuf *thumbnail, unsigned 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, const char *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("Failed to save thumbnail: %s", error->message); + // NOTREACHED + } +} + +int main(int argc, char *argv[]) +{ + if (argc != 3 && argc != 4) { + g_error("Usage: %s []", argv[0] ? argv[0] : "sameboy-thumbnailer"); + // NOTREACHED + } + const char *input_path = argv[1]; + char *output_path = argv[2]; // Gets mutated in-place. + const char *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; + /* This will implicitly truncate, but enforce_max_size will cap size to 1024 anyway. + (Not that 4 billion pixels wide icons make sense to begin with)*/ + unsigned 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); + } + write_thumbnail(thumbnail, output_path); + g_object_unref(thumbnail); + + return 0; +} diff --git a/XdgThumbnailer/resources.gresource.xml b/XdgThumbnailer/resources.gresource.xml new file mode 100644 index 0000000..f30ec17 --- /dev/null +++ b/XdgThumbnailer/resources.gresource.xml @@ -0,0 +1,9 @@ + + + + QuickLook/CartridgeTemplate.png + QuickLook/ColorCartridgeTemplate.png + QuickLook/UniversalCartridgeTemplate.png + build/bin/BootROMs/cgb_boot_fast.bin + + diff --git a/XdgThumbnailer/sameboy.thumbnailer b/XdgThumbnailer/sameboy.thumbnailer new file mode 100644 index 0000000..eee621a --- /dev/null +++ b/XdgThumbnailer/sameboy.thumbnailer @@ -0,0 +1,4 @@ +[Thumbnailer Entry] +TryExec=sameboy-thumbnailer +Exec=sameboy-thumbnailer %i %o %s +MimeType=application/x-gameboy-rom;application/x-gameboy-color-rom;application/x-gameboy-isx