#include "vram_viewer.h" #include #include "util.h" #define tileset_buffer_length 256 * 192 * 4 #define tilemap_buffer_length 256 * 256 * 4 struct _VramViewerWindow { GtkWindowClass parent_class; GtkStack *stack; GtkComboBoxText *tileset_palette_selector; GtkToggleButton *tileset_toggle_grid_button; GtkDrawingArea *tileset_canvas; GtkComboBoxText *tilemap_palette_selector; GtkComboBoxText *tilemap_tilemap_selector; GtkToggleButton *tilemap_toggle_grid_button; GtkToggleButton *tilemap_toggle_scrolling_button; GtkComboBoxText *tilemap_tileset_selector; GtkDrawingArea *tilemap_canvas; GtkTreeView *sprites; GtkTreeView *palettes; GtkCellRendererText *palette_cell_renderer_0; GtkCellRendererText *palette_cell_renderer_1; GtkCellRendererText *palette_cell_renderer_2; GtkCellRendererText *palette_cell_renderer_3; GtkLabel *status; bool is_cgb; uint16_t oam_count; uint8_t oam_height; uint8_t palette_data[16][0x40]; GB_oam_info_t oam_info[40]; Rect scroll_rect; uint8_t gb_lcdc; uint8_t *gb_vram; uint32_t *tileset_buffer; uint32_t *tilemap_buffer; }; G_DEFINE_TYPE(VramViewerWindow, vram_viewer_window, GTK_TYPE_WINDOW); static void visible_tab_changed(GObject *stack, GParamSpec *pspec, VramViewerWindow *window) { if (gtk_widget_in_destruction (GTK_WIDGET (stack))) return; gtk_label_set_text(window->status, ""); } static gboolean draw_tileset_canvas(GtkWidget *widget, cairo_t *cr, VramViewerWindow *window) { guint width, height; GtkStyleContext *context; context = gtk_widget_get_style_context(widget); width = gtk_widget_get_allocated_width(widget); height = gtk_widget_get_allocated_height(widget); gtk_render_background(context, cr, 0, 0, width, height); gtk_render_frame(context, cr, 0, 0, width, height); cairo_surface_t *surface = cairo_image_surface_create_for_data( (unsigned char *) window->tileset_buffer, CAIRO_FORMAT_RGB24, 256, 192, cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256) ); cairo_scale(cr, 2.0, 2.0); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); cairo_paint(cr); if (gtk_toggle_button_get_active(window->tileset_toggle_grid_button)) { cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25); cairo_set_line_width(cr, 1); const int divisions_x = 256 / 8; const int divisions_y = 192 / 8; for (int i = 0; i < divisions_x; i++) { const int j = 256 * i; cairo_move_to(cr, j / divisions_x, 0); cairo_line_to(cr, j / divisions_x, 192); } for (int i = 0; i < divisions_y; i++) { const int j = 192 * i; cairo_move_to(cr, 0, j / divisions_y); cairo_line_to(cr, 256, j / divisions_y); } cairo_stroke(cr); } return false; } static gboolean draw_tilemap_canvas(GtkWidget *widget, cairo_t *cr, VramViewerWindow *window) { guint width, height; GtkStyleContext *context; context = gtk_widget_get_style_context(widget); width = gtk_widget_get_allocated_width(widget); height = gtk_widget_get_allocated_height(widget); gtk_render_background(context, cr, 0, 0, width, height); gtk_render_frame(context, cr, 0, 0, width, height); cairo_surface_t *surface = cairo_image_surface_create_for_data( (unsigned char *) window->tilemap_buffer, CAIRO_FORMAT_RGB24, 256, 256, cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256) ); cairo_scale(cr, 2.0, 2.0); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); cairo_paint(cr); if (gtk_toggle_button_get_active(window->tilemap_toggle_grid_button)) { cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25); cairo_set_line_width(cr, 1); const int divisions = 256 / 8; for (int i = 0; i < divisions; i++) { const int j = 256 * i; cairo_move_to(cr, j / divisions, 0); cairo_line_to(cr, j / divisions, 256); cairo_move_to(cr, 0, j / divisions); cairo_line_to(cr, 256, j / divisions); } cairo_stroke(cr); } if (gtk_toggle_button_get_active(window->tilemap_toggle_scrolling_button)) { cairo_rectangle(cr, -2, -2, width + 2, height + 2); for (unsigned x = 0; x < 2; x++) { for (unsigned y = 0; y < 2; y++) { Rect rect = window->scroll_rect; rect.x -= 256 * x; rect.y += 256 * y; cairo_rectangle(cr, rect.x, rect.y, rect.width, rect.height); } } cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_set_line_width(cr, 2); cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.5); cairo_fill_preserve(cr); cairo_clip_preserve(cr); cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.6); cairo_stroke(cr); } return false; } static gboolean tileset_canvas_motion(GtkWidget *widget, GdkEventMotion *event, VramViewerWindow *window) { int x, y; if (event->is_hint) { gdk_window_get_pointer(event->window, &x, &y, NULL); } else { x = event->x; y = event->y; } // Compensate for our canvas scale x /= 2; y /= 2; uint8_t bank = x >= 128? 1 : 0; x &= 127; uint16_t tile = x / 8 + y / 8 * 16; gtk_label_set_text(window->status, g_strdup_printf("Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10)); return true; } static gboolean tilemap_canvas_motion(GtkWidget *widget, GdkEventMotion *event, VramViewerWindow *window) { if (!window->gb_vram) { gtk_label_set_text(window->status, ""); return false; } int x, y; if (event->is_hint) { gdk_window_get_pointer (event->window, &x, &y, NULL); } else { x = event->x; y = event->y; } // Compensate for our canvas scale x /= 2; y /= 2; GtkLabel *status = window->status; uint16_t map_offset = x / 8 + y / 8 * 32; uint16_t map_base = 0x1800; GB_map_type_t map_type = GB_MAP_AUTO; const gchar *map_type_id = vram_viewer_get_tilemap_type_id(window); if (g_strcmp0("auto", map_type_id) != 0) { map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00; } GB_tileset_type_t tileset_type = GB_TILESET_AUTO; const gchar *tileset_type_id = vram_viewer_get_tileset_type_id(window); if (g_strcmp0("auto", tileset_type_id) != 0) { tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000; } if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && window->gb_lcdc & 0x08)) { map_base = 0x1c00; } if (tileset_type == GB_TILESET_AUTO) { tileset_type = (window->gb_lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000; } uint8_t tile = window->gb_vram[map_base + map_offset]; uint16_t tile_address = 0; if (tileset_type == GB_TILESET_8000) { tile_address = 0x8000 + tile * 0x10; } else { tile_address = 0x9000 + (int8_t)tile * 0x10; } if (window->is_cgb) { uint8_t attributes = window->gb_vram[map_base + map_offset + 0x2000]; gtk_label_set_text(status, g_strdup_printf("Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)", tile, attributes & 0x8? 1 : 0, tile_address, 0x8000 + map_base + map_offset, (attributes & 0x80) ? 'P' : '-', (attributes & 0x40) ? 'V' : '-', (attributes & 0x20) ? 'H' : '-', attributes & 0x8? 1 : 0, attributes & 0x7 )); } else { gtk_label_set_text(status, g_strdup_printf("Tile number $%02x ($%04x) at map address $%04x", tile, tile_address, 0x8000 + map_base + map_offset )); } return true; } static void vram_viewer_window_init(VramViewerWindow *window) { gtk_widget_init_template(GTK_WIDGET(window)); window->tileset_buffer = g_new0(uint32_t, tileset_buffer_length); window->tilemap_buffer = g_new0(uint32_t, tilemap_buffer_length); window->is_cgb = false; window->oam_count = 0; window->oam_height = 0; gtk_widget_add_events(GTK_WIDGET(window->tileset_canvas), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); gtk_widget_add_events(GTK_WIDGET(window->tilemap_canvas), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); } static void vram_viewer_window_class_init(VramViewerWindowClass *class) { gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(class), RESOURCE_PREFIX "ui/vram_viewer.ui"); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, stack); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tileset_palette_selector); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tileset_toggle_grid_button); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tileset_canvas); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tilemap_palette_selector); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tilemap_tilemap_selector); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tilemap_toggle_grid_button); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tilemap_toggle_scrolling_button); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tilemap_tileset_selector); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, tilemap_canvas); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, sprites); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, palettes); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, palette_cell_renderer_0); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, palette_cell_renderer_1); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, palette_cell_renderer_2); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, palette_cell_renderer_3); gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), VramViewerWindow, status); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), visible_tab_changed); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), draw_tileset_canvas); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), draw_tilemap_canvas); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), tileset_canvas_motion); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), tilemap_canvas_motion); } VramViewerWindow *vram_viewer_new(void) { return g_object_new (VRAM_VIEWER_WINDOW_TYPE, NULL); } const gchar *vram_viewer_active_tab_name(VramViewerWindow *window) { return gtk_stack_get_visible_child_name(window->stack); } const gchar *vram_viewer_get_tileset_palette_id(VramViewerWindow *window) { return gtk_combo_box_get_active_id(GTK_COMBO_BOX(window->tileset_palette_selector)); } const gchar *vram_viewer_get_tilemap_palette_id(VramViewerWindow *window) { return gtk_combo_box_get_active_id(GTK_COMBO_BOX(window->tilemap_palette_selector)); } const gchar *vram_viewer_get_tilemap_type_id(VramViewerWindow *window) { return gtk_combo_box_get_active_id(GTK_COMBO_BOX(window->tilemap_tilemap_selector)); } const gchar *vram_viewer_get_tileset_type_id(VramViewerWindow *window) { return gtk_combo_box_get_active_id(GTK_COMBO_BOX(window->tilemap_tileset_selector)); } static void update_sprite_list(VramViewerWindow *window) { GtkTreeIter iter; GtkListStore *store = gtk_list_store_new(7, GDK_TYPE_PIXBUF, // Preview image G_TYPE_STRING, // X position G_TYPE_STRING, // Y position G_TYPE_STRING, // Tile G_TYPE_STRING, // Tile Address G_TYPE_STRING, // OAM Address G_TYPE_STRING // Attributes ); gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); GB_oam_info_t *oam_info = window->oam_info; for (unsigned row = 0; row < window->oam_count; ++row) { GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes( g_bytes_new(oam_info[row].image, 128 * sizeof(uint32_t)), GDK_COLORSPACE_RGB, true, 8, 8, window->oam_height, 8 * sizeof(uint32_t) ); GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, window->oam_height * 2); gdk_pixbuf_scale(pixbuf, dest, 0, 0, 8 * 2, window->oam_height * 2, 0, 0, 2.0, 2.0, GDK_INTERP_NEAREST ); gtk_list_store_insert_with_values(store, &iter, -1, 0, dest, 1, g_strdup_printf("%i", oam_info[row].x - 8), 2, g_strdup_printf("%i", oam_info[row].y - 16), 3, g_strdup_printf("$%02x", oam_info[row].tile), 4, g_strdup_printf("$%04x", 0x8000 + oam_info[row].tile * 0x10), 5, g_strdup_printf("$%04x", oam_info[row].oam_addr), 6, window->is_cgb ? g_strdup_printf("%c%c%c%d%d", oam_info[row].flags & 0x80? 'P' : '-', oam_info[row].flags & 0x40? 'Y' : '-', oam_info[row].flags & 0x20? 'X' : '-', oam_info[row].flags & 0x08? 1 : 0, oam_info[row].flags & 0x07) : g_strdup_printf("%c%c%c%d", oam_info[row].flags & 0x80? 'P' : '-', oam_info[row].flags & 0x40? 'Y' : '-', oam_info[row].flags & 0x20? 'X' : '-', oam_info[row].flags & 0x10? 1 : 0), -1 ); g_object_unref(pixbuf); g_object_unref(dest); } gtk_tree_view_set_model(window->sprites, GTK_TREE_MODEL(store)); g_object_unref(store); } static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data_ptr) { const gchar *title = gtk_tree_view_column_get_title(col); const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10); const uint8_t column_index = 2 + (2 * color_index); GValue color_val = G_VALUE_INIT; gtk_tree_model_get_value(model, iter, column_index, &color_val); gint color = g_value_get_int(&color_val); gchar *color_string = g_strdup_printf("#%06x", color); gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF); GValue color_str = G_VALUE_INIT; g_value_init(&color_str, G_TYPE_STRING); g_value_set_string(&color_str, color_string); g_object_set_property(G_OBJECT(renderer), "background", &color_str); GValue fg_color_str = G_VALUE_INIT; g_value_init(&fg_color_str, G_TYPE_STRING); g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF"); g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str); g_value_unset(&color_val); g_value_unset(&color_str); g_value_unset(&fg_color_str); g_free(color_string); } static void update_palettes(VramViewerWindow *window) { GtkTreeIter iter; GtkListStore *store = gtk_list_store_new(9, G_TYPE_STRING, // Name G_TYPE_STRING, // Color 0 string G_TYPE_INT, // Color 0 integer G_TYPE_STRING, // Color 1 string G_TYPE_INT, // Color 1 integer G_TYPE_STRING, // Color 2 string G_TYPE_INT, // Color 2 integer G_TYPE_STRING, // Color 3 string G_TYPE_INT // Color 3 integer ); gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); for (unsigned row = 0; row < 16; ++row) { uint8_t offset = (row & 7) * 4; uint16_t color_0 = (window->palette_data[row][((0 + offset) << 1) + 1] << 8) | window->palette_data[row][((0 + offset) << 1)]; uint16_t color_1 = (window->palette_data[row][((1 + offset) << 1) + 1] << 8) | window->palette_data[row][((1 + offset) << 1)]; uint16_t color_2 = (window->palette_data[row][((2 + offset) << 1) + 1] << 8) | window->palette_data[row][((2 + offset) << 1)]; uint16_t color_3 = (window->palette_data[row][((3 + offset) << 1) + 1] << 8) | window->palette_data[row][((3 + offset) << 1)]; gtk_list_store_insert_with_values(store, &iter, -1, 0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7), 1, g_strdup_printf("$%04x", color_0 & 0x7FFF), 2, convert_color(color_0), 3, g_strdup_printf("$%04x", color_1 & 0x7FFF), 4, convert_color(color_1), 5, g_strdup_printf("$%04x", color_2 & 0x7FFF), 6, convert_color(color_2), 7, g_strdup_printf("$%04x", color_3 & 0x7FFF), 8, convert_color(color_3), -1 ); } GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(window->palettes, 1); GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(window->palettes, 2); GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(window->palettes, 3); GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(window->palettes, 4); GtkCellRendererText *cell_renderer_0 = window->palette_cell_renderer_0; GtkCellRendererText *cell_renderer_1 = window->palette_cell_renderer_1; GtkCellRendererText *cell_renderer_2 = window->palette_cell_renderer_2; GtkCellRendererText *cell_renderer_3 = window->palette_cell_renderer_3; gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL); gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL); gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL); gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL); gtk_tree_view_set_model(window->palettes, GTK_TREE_MODEL(store)); g_object_unref(store); } void vram_viewer_update(VramViewerWindow *window, GB_gameboy_t *gb) { if (!gtk_widget_is_visible(GTK_WIDGET(window))) return; window->is_cgb = GB_is_cgb(gb); window->gb_lcdc = ((uint8_t *)GB_get_direct_access(gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC]; window->gb_vram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL); const gchar *active_tab_name = vram_viewer_active_tab_name(window); if (g_strcmp0(active_tab_name, VRAM_VIEWER_TAB_TILESET) == 0) { const gchar *palette_id = vram_viewer_get_tileset_palette_id(window); GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); GB_draw_tileset(gb, window->tileset_buffer, palette_type, palette_index ); } else if (g_strcmp0(active_tab_name, VRAM_VIEWER_TAB_TILEMAP) == 0) { const gchar *palette_id = vram_viewer_get_tilemap_palette_id(window); uint8_t palette_index = 0; GB_palette_type_t palette_type = GB_PALETTE_AUTO; if (g_strcmp0("auto", palette_id) != 0) { palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); } GB_map_type_t map_type = GB_MAP_AUTO; const gchar *map_type_id = vram_viewer_get_tilemap_type_id(window); if (g_strcmp0("auto", map_type_id) != 0) { map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00; } GB_tileset_type_t tileset_type = GB_TILESET_AUTO; const gchar *tileset_type_id = vram_viewer_get_tileset_type_id(window); if (g_strcmp0("auto", tileset_type_id) != 0) { tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000; } GB_draw_tilemap(gb, window->tilemap_buffer, palette_type, palette_index, map_type, tileset_type ); window->scroll_rect = (Rect){ GB_read_memory(gb, 0xFF00 | GB_IO_SCX), GB_read_memory(gb, 0xFF00 | GB_IO_SCY), 160, 144 }; } else if (g_strcmp0(active_tab_name, VRAM_VIEWER_TAB_SPRITES) == 0) { window->oam_count = GB_get_oam_info(gb, (GB_oam_info_t *)window->oam_info, &window->oam_height); g_idle_add((GSourceFunc) update_sprite_list, window); } else if (g_strcmp0(active_tab_name, VRAM_VIEWER_TAB_PALETTES) == 0) { size_t size; for (unsigned row = 0; row < 16; ++row) { uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL); memcpy(window->palette_data[row], palette_data, size); } g_idle_add((GSourceFunc) update_palettes, window); } }