/*****************************************************************************\ Snes9x - Portable Super Nintendo Entertainment System (TM) emulator. This file is licensed under the Snes9x License. For further information, consult the LICENSE file in the root directory. \*****************************************************************************/ #include "gtk_compat.h" #include "threadpool.h" #include "gtk_s9x.h" #include "gtk_display.h" #include "gtk_display_driver.h" #include "gtk_display_driver_gtk.h" #include "snes9x.h" #include "memmap.h" #include "cpuexec.h" #include "ppu.h" #include "gfx.h" #include "netplay.h" #if defined(USE_XV) && defined(GDK_WINDOWING_X11) #include "gtk_display_driver_xv.h" #endif #include "gtk_display_driver_opengl.h" #ifdef USE_SLANG #include "gtk_display_driver_vulkan.h" #endif void filter_scanlines(uint8 *, int, uint8 *, int, int, int); void filter_2x(uint8 *, int, uint8 *, int, int, int); void filter_3x(uint8 *, int, uint8 *, int, int, int); void filter_4x(uint8 *, int, uint8 *, int, int, int); static struct { int index; void (*filter_func)(uint8 *, int, uint8 *, int, int, int); int xscale; int yscale; } filter_data[NUM_FILTERS] = { { FILTER_NONE, nullptr, 1, 1 }, { FILTER_SUPEREAGLE, SuperEagle, 2, 2 }, { FILTER_2XSAI, _2xSaI, 2, 2 }, { FILTER_SUPER2XSAI, Super2xSaI, 2, 2 }, { FILTER_EPX, EPX_16_unsafe, 2, 2 }, { FILTER_EPX_SMOOTH, EPX_16_smooth_unsafe, 2, 2 }, { FILTER_NTSC, nullptr, 1, 1 }, { FILTER_SCANLINES, filter_scanlines, 1, 2 }, { FILTER_SIMPLE2X, filter_2x, 2, 2 }, { FILTER_SIMPLE3X, filter_3x, 3, 3 }, { FILTER_SIMPLE4X, filter_4x, 4, 4 }, { FILTER_HQ2X, HQ2X_16, 2, 2 }, { FILTER_HQ3X, HQ3X_16, 3, 3 }, { FILTER_HQ4X, HQ4X_16, 4, 4 }, { FILTER_2XBRZ, filter_2xBRZ, 2, 2 }, { FILTER_3XBRZ, filter_3xBRZ, 3, 3 }, { FILTER_4XBRZ, filter_4xBRZ, 4, 4 }, }; static S9xDisplayDriver *driver; static snes_ntsc_t snes_ntsc; static int burst_phase = 0; static uint8 *y_table, *u_table, *v_table; static std::vector scaled_image; static std::unique_ptr pool; /* Scanline constants for the NTSC filter */ static const unsigned int scanline_offsets[] = { 0, /* 0% */ 3, /* 12.5% */ 2, /* 25% */ 1, /* 50% */ 0, /* 100% */ }; static const unsigned short scanline_masks[] = { 0x0000, /* 0% */ 0x18E3, /* 12.5% */ 0x39E7, /* 25% */ 0x7BEF, /* 50% */ 0xffff, /* 100% */ }; /* Scanline constants for standard scanline filter */ static const uint8 scanline_shifts[] = { 3, 2, 1, 0 }; double S9xGetAspect() { double native_aspect = 256.0 / (gui_config->overscan ? 239.0 : 224.0); double aspect; switch (gui_config->aspect_ratio) { case 0: /* Square pixels */ case 1: aspect = native_aspect; break; case 2: /* 4:3 */ case 3: aspect = native_aspect * 7 / 6; break; case 4: /* NTSC 64:49 */ case 5: default: aspect = native_aspect * 8 / 7; break; } return aspect; } S9xRect S9xApplyAspect(int src_width, int src_height, int dst_width, int dst_height) { double screen_aspect = (double)dst_width / (double)dst_height; double snes_aspect = S9xGetAspect(); bool integer = gui_config->aspect_ratio & 1; double granularity = 1.0 / (double)MAX(dst_width, dst_height); int x, y, w, h; if (!gui_config->scale_to_fit) { if (gui_config->maintain_aspect_ratio) { w = src_height * snes_aspect + 0.5; h = src_height; x = (dst_width - w) / 2; y = (dst_height - src_height) / 2; } else { w = src_width; h = src_height; x = (dst_width - w) / 2; y = (dst_height - h) / 2; } } else if (gui_config->maintain_aspect_ratio && integer) { for (h = src_height * 2; h <= dst_height && (int)(h * (snes_aspect) + 0.5) <= dst_width; h += src_height) { } h -= src_height; w = h * snes_aspect + 0.5; x = (dst_width - w) / 2; y = (dst_height - h) / 2; if (w > dst_width || h > dst_height) { w = dst_width; h = dst_height; x = 0; y = 0; } } else if (gui_config->maintain_aspect_ratio && !(screen_aspect <= snes_aspect * (1.0 + granularity) && screen_aspect >= snes_aspect * (1.0 - granularity))) { if (screen_aspect > snes_aspect) { w = dst_height * snes_aspect + 0.5; h = dst_height; x = (dst_width - w) / 2; y = 0; } else { w = dst_width; h = dst_width / snes_aspect + 0.5; x = 0; y = (dst_height - h) / 2; } } else { x = 0; y = 0; w = dst_width; h = dst_height; } return { x, y, w, h }; } void S9xRegisterYUVTables(uint8 *y, uint8 *u, uint8 *v) { y_table = y; u_table = u; v_table = v; } /* YUY2 in LSB first format */ static void internal_convert_16_yuv(void *src_buffer, void *dst_buffer, int src_pitch, int dst_pitch, int width, int height, int bpp) { int x, y; uint16 *src; uint8 *dst; uint16 p0, p1; for (y = 0; y < height; y++) { src = (uint16 *)(((uint8 *)src_buffer) + src_pitch * y); dst = (uint8 *)(((uint8 *)dst_buffer) + dst_pitch * y); for (x = 0; x < width / 2; x++) { p0 = *src++; p1 = *src++; *dst++ = y_table[p0]; *dst++ = (u_table[p0] + u_table[p1]) >> 1; *dst++ = y_table[p1]; *dst++ = (v_table[p0] + v_table[p1]) >> 1; } } } static void internal_convert_mask(void *src_buffer, void *dst_buffer, int src_pitch, int dst_pitch, int width, int height, unsigned int inv_rshift, unsigned int inv_gshift, unsigned int inv_bshift, unsigned int bpp) { switch (bpp) { case 15: case 16: for (int y = 0; y < height; y++) { uint16 *data = (uint16 *)((uint8 *)dst_buffer + y * dst_pitch); uint16 *snes = (uint16 *)(((uint8 *)src_buffer) + y * src_pitch); for (int x = 0; x < width; x++) { uint32 pixel = *snes++; *data++ = ((pixel & 0xf800) << 0) >> inv_rshift | ((pixel & 0x07e0) << 5) >> inv_gshift | ((pixel & 0x001f) << 11) >> inv_bshift; } } break; case 32: for (int y = 0; y < height; y++) { uint32 *data = (uint32 *)((uint8 *)dst_buffer + y * dst_pitch); uint16 *snes = (uint16 *)(((uint8 *)src_buffer) + y * src_pitch); for (int x = 0; x < width; x++) { uint32 pixel = *snes++; *data++ = ((uint32)(pixel & 0xf800) << 16) >> inv_rshift | ((uint32)(pixel & 0x07e0) << 21) >> inv_gshift | ((uint32)(pixel & 0x001f) << 27) >> inv_bshift; } } break; } } static void internal_convert(void *src_buffer, void *dst_buffer, int src_pitch, int dst_pitch, int width, int height, int bpp) { if (bpp == 32) { /* Format in fourcc is xxxxxxxx rrrrrrrr gggggggg bbbbbbbb */ for (int y = 0; y < height; y++) { uint8 *data = (uint8 *)dst_buffer + y * dst_pitch; uint16 *snes = (uint16 *)(((uint8 *)src_buffer) + y * src_pitch); for (int x = 0; x < width; x++) { uint16 pixel = *snes++; *data++ = ((pixel & 0x001f) << 3) | ((pixel >> 2) & 0x07); /* Blue */ *data++ = ((pixel & 0x07e0) >> 3) | ((pixel >> 9) & 0x03); /* Green */ *data++ = ((pixel & 0xf800) >> 8) | ((pixel >> 13) & 0x07); /* Red */ *data++ = 0xff; /* Null */ } } } } static void S9xForceHires(void *buffer, int pitch, int &width, int &height) { if (width <= 256) { for (int y = (height)-1; y >= 0; y--) { uint16 *line = (uint16 *)((uint8 *)buffer + y * pitch); for (int x = (width * 2) - 1; x >= 0; x--) { *(line + x) = *(line + (x >> 1)); } } width *= 2; } } static inline uint16 average_565(uint16 colora, uint16 colorb) { return (((colora) & (colorb)) + ((((colora) ^ (colorb)) & 0xF7DE) >> 1)); } static void S9xMergeHires(void *buffer, int pitch, int &width, int &height) { if (width < 512) return; for (int y = 0; y < height; y++) { uint16 *input = (uint16 *)((uint8 *)buffer + y * pitch); uint16 *output = input; for (int x = 0; x < (width >> 1); x++) { *output++ = average_565(input[0], input[1]); input += 2; } } width >>= 1; } void filter_2x(uint8 *src, int src_pitch, uint8 *dst, int dst_pitch, int width, int height) { for (int y = 0; y < height; y++) { uint16 *in = (uint16 *)((uint8 *)src + y * src_pitch); uint16 *out = (uint16 *)((uint8 *)dst + (y * 2) * dst_pitch); for (int x = 0; x < width; x++) { uint16 pixel = *in++; *out++ = pixel; *out++ = pixel; } memcpy((uint8 *)dst + (y * 2 + 1) * dst_pitch, (uint8 *)dst + (y * 2) * dst_pitch, width * 2 * 2); } } void filter_3x(uint8 *src, int src_pitch, uint8 *dst, int dst_pitch, int width, int height) { for (int y = 0; y < height; y++) { uint16 *in = (uint16 *)((uint8 *)src + y * src_pitch); uint16 *out = (uint16 *)((uint8 *)dst + (y * 3) * dst_pitch); for (int x = 0; x < width; x++) { uint16 pixel = *in++; *out++ = pixel; *out++ = pixel; *out++ = pixel; } for (int line = 1; line <= 2; line++) { memcpy((uint8 *)dst + ((y * 3) + line) * dst_pitch, (uint8 *)dst + ((y * 3)) * dst_pitch, width * 2 * 3); } } } void filter_4x(uint8 *src, int src_pitch, uint8 *dst, int dst_pitch, int width, int height) { for (int y = 0; y < height; y++) { uint16 *in = (uint16 *)((uint8 *)src + y * src_pitch); uint16 *out = (uint16 *)((uint8 *)dst + (y * 4) * dst_pitch); for (int x = 0; x < width; x++) { uint16 pixel = *in++; *out++ = pixel; *out++ = pixel; *out++ = pixel; *out++ = pixel; } for (int line = 1; line <= 3; line++) { memcpy((uint8 *)dst + ((y * 4) + line) * dst_pitch, (uint8 *)dst + (y * 4) * dst_pitch, width * 2 * 4); } } } void filter_scanlines(uint8 *src_buffer, int src_pitch, uint8 *dst_buffer, int dst_pitch, int width, int height) { uint8 shift = scanline_shifts[gui_config->scanline_filter_intensity]; uint16 mask = scanline_masks[gui_config->scanline_filter_intensity + 1]; uint16 *src = (uint16 *)src_buffer; uint16 *dst_a = (uint16 *)dst_buffer; uint16 *dst_b = ((uint16 *)dst_buffer) + (dst_pitch >> 1); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { dst_a[x] = src[x]; dst_b[x] = (src[x] - (src[x] >> shift & mask)); } src += src_pitch >> 1; dst_a += dst_pitch; dst_b += dst_pitch; } } void apply_filter_scale(int &width, int &height) { if (gui_config->scale_method == FILTER_NTSC) { width = SNES_NTSC_OUT_WIDTH(width); height *= 2; return; } const auto &filter = filter_data[gui_config->scale_method]; width *= filter.xscale; height *= filter.yscale; return; } static void internal_filter(uint8 *src_buffer, int src_pitch, uint8 *dst_buffer, int dst_pitch, int width, int height) { if (gui_config->scale_method == FILTER_NONE) { return; } if (gui_config->scale_method == FILTER_NTSC) { if (width > 256) snes_ntsc_blit_hires_scanlines(&snes_ntsc, (SNES_NTSC_IN_T *)src_buffer, src_pitch >> 1, burst_phase, width, height, (void *)dst_buffer, dst_pitch); else snes_ntsc_blit_scanlines(&snes_ntsc, (SNES_NTSC_IN_T *)src_buffer, src_pitch >> 1, burst_phase, width, height, (void *)dst_buffer, dst_pitch); } else { filter_data[gui_config->scale_method].filter_func(src_buffer, src_pitch, dst_buffer, dst_pitch, width, height); } } static void create_thread_pool() { if (!pool) { pool = std::make_unique(); pool->start(gui_config->num_threads); } } static void internal_threaded_convert(void *src_buffer, void *dst_buffer, int src_pitch, int dst_pitch, int width, int height, int bpp) { create_thread_pool(); auto func = (bpp == -1) ? internal_convert_16_yuv : internal_convert; int coverage = 0; for (int i = 0; i < gui_config->num_threads; i++) { int job_height = (height / gui_config->num_threads) & 3; job_height = height - coverage; pool->queue([=] { func((uint8 *)src_buffer + (src_pitch * coverage), (uint8 *)dst_buffer + (dst_pitch * coverage), src_pitch, dst_pitch, width, job_height, bpp); }); } pool->wait_idle(); } static void internal_threaded_convert_mask(void *src_buffer, void *dst_buffer, int src_pitch, int dst_pitch, int width, int height, int inv_rmask, int inv_gmask, int inv_bmask, int bpp) { create_thread_pool(); int lines_handled = 0; int job_height = height / gui_config->num_threads; for (int i = 0; i < gui_config->num_threads; i++) { if (i == gui_config->num_threads - 1) job_height = height - lines_handled; pool->queue([=] { internal_convert_mask((uint8 *)src_buffer + (src_pitch * lines_handled), (uint8 *)dst_buffer + (dst_pitch * lines_handled), src_pitch, dst_pitch, width, job_height, inv_rmask, inv_gmask, inv_bmask, bpp); }); } pool->wait_idle(); } static void internal_threaded_filter(uint8 *src_buffer, int src_pitch, uint8 *dst_buffer, int dst_pitch, int width, int height) { /* If the threadpool doesn't exist, create it */ create_thread_pool(); int yscale = filter_data[gui_config->scale_method].yscale; int coverage = 0; for (int i = 0; i < gui_config->num_threads; i++) { int job_height = height / gui_config->num_threads & ~3; if (i == gui_config->num_threads - 1) job_height = height - coverage; pool->queue([=] { internal_filter(src_buffer + (src_pitch * coverage), src_pitch, dst_buffer + (dst_pitch * coverage * yscale), dst_pitch, width, job_height); }); coverage += job_height; } pool->wait_idle(); } void S9xFilter(uint8 *src_buffer, int src_pitch, uint8 *dst_buffer, int dst_pitch, int &width, int &height) { burst_phase = (burst_phase + 1) % 3; if (gui_config->multithreading) internal_threaded_filter(src_buffer, src_pitch, dst_buffer, dst_pitch, width, height); else internal_filter(src_buffer, src_pitch, dst_buffer, dst_pitch, width, height); apply_filter_scale(width, height); } void S9xConvertYUV(void *src_buffer, void *dst_buffer, int src_pitch, int dst_pitch, int width, int height) { if (gui_config->multithreading) internal_threaded_convert(src_buffer, dst_buffer, src_pitch, dst_pitch, width, height, -1); else internal_convert_16_yuv(src_buffer, dst_buffer, src_pitch, dst_pitch, width, height, -1); } void S9xConvert(void *src, void *dst, int src_pitch, int dst_pitch, int width, int height, int bpp) { if (gui_config->multithreading) internal_threaded_convert(src, dst, src_pitch, dst_pitch, width, height, bpp); else internal_convert(src, dst, src_pitch, dst_pitch, width, height, bpp); } void S9xConvertMask(void *src, void *dst, int src_pitch, int dst_pitch, int width, int height, int rshift, int gshift, int bshift, int bpp) { if (gui_config->multithreading) internal_threaded_convert_mask(src, dst, src_pitch, dst_pitch, width, height, rshift, gshift, bshift, bpp); else internal_convert_mask(src, dst, src_pitch, dst_pitch, width, height, rshift, gshift, bshift, bpp); } void S9xDisplayRefresh() { driver->refresh(); } static void ntsc_filter_init() { snes_ntsc_scanline_offset = scanline_offsets[gui_config->ntsc_scanline_intensity]; snes_ntsc_scanline_mask = scanline_masks[gui_config->ntsc_scanline_intensity]; snes_ntsc_init(&snes_ntsc, &gui_config->ntsc_setup); } void S9xDisplayReconfigure() { ntsc_filter_init(); if (pool) { pool->stop(); pool = nullptr; } } void S9xQueryDrivers() { GdkDisplay *gdk_display = top_level->window->get_display()->gobj(); gui_config->allow_xv = false; #if defined(USE_XV) && defined(GDK_WINDOWING_X11) if (GDK_IS_X11_DISPLAY(gdk_display)) gui_config->allow_xv = S9xXVDisplayDriver::query_availability(); #endif gui_config->allow_opengl = S9xOpenGLDisplayDriver::query_availability(); gui_config->allow_xrandr = false; #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY(gdk_display)) { Display *dpy = gdk_x11_display_get_xdisplay(gdk_display); Window xid = gdk_x11_window_get_xid(top_level->window->get_window()->gobj()); gui_config->allow_xrandr = true; if (!gui_config->xrr_screen_resources) gui_config->xrr_screen_resources = XRRGetScreenResourcesCurrent(dpy, xid); if (!gui_config->xrr_crtc_info) gui_config->xrr_crtc_info = XRRGetCrtcInfo(dpy, gui_config->xrr_screen_resources, gui_config->xrr_screen_resources->crtcs[0]); } #endif auto &dd = gui_config->display_drivers; dd.clear(); dd.push_back("none"); if (gui_config->allow_opengl) dd.push_back("opengl"); if (gui_config->allow_xv) dd.push_back("xv"); #ifdef USE_SLANG dd.push_back("vulkan"); #endif } bool8 S9xDeinitUpdate(int width, int height) { int yoffset = 0; if (width <= 0 || height <= 0) return false; if (top_level->last_height > height) { memset(GFX.Screen + GFX.RealPPL * height, 0, GFX.Pitch * (top_level->last_height - height)); } top_level->last_height = height; top_level->last_width = width; if (gui_config->overscan) { if (height == SNES_HEIGHT) { yoffset = -8; height = SNES_HEIGHT_EXTENDED; } if (height == SNES_HEIGHT * 2) { yoffset = -16; height = SNES_HEIGHT_EXTENDED * 2; } } else { if (height == SNES_HEIGHT_EXTENDED) { yoffset = 7; height = SNES_HEIGHT; } if (height == SNES_HEIGHT_EXTENDED * 2) { yoffset = 14; height = SNES_HEIGHT * 2; } } uint16_t *screen_view = GFX.Screen + (yoffset * (int)GFX.RealPPL); if (!Settings.Paused && !NetPlay.Paused) { if (gui_config->hires_effect == HIRES_SCALE) { S9xForceHires(screen_view, GFX.Pitch, width, height); top_level->last_width = width; } else if (gui_config->hires_effect == HIRES_MERGE) { S9xMergeHires(screen_view, GFX.Pitch, width, height); top_level->last_width = width; } } if (gui_config->scale_method > 0) { int scaled_width = width; int scaled_height = height; apply_filter_scale(scaled_width, scaled_height); if ((int)scaled_image.size() < (scaled_width * scaled_height * 2)) { scaled_image.resize(scaled_width * scaled_height * 2); } S9xFilter((uint8_t *)screen_view, GFX.Pitch, &scaled_image[0], scaled_width * 2, width, height); driver->update((uint16_t *)&scaled_image[0], width, height, scaled_width); return true; } driver->update(screen_view, width, height, GFX.RealPPL); return true; } static void S9xInitDriver() { // Only OpenGL is supported on Wayland #ifdef GDK_WINDOWING_WAYLAND if (GDK_IS_WAYLAND_DISPLAY(gdk_display_get_default())) { if (gui_config->display_driver != "vulkan") gui_config->display_driver = "opengl"; } #endif if ("opengl" == gui_config->display_driver) { driver = new S9xOpenGLDisplayDriver(top_level, gui_config); } #ifdef USE_SLANG else if ("vulkan" == gui_config->display_driver) { driver = new S9xVulkanDisplayDriver(top_level, gui_config); } #endif #if defined(USE_XV) && defined(GDK_WINDOWING_X11) else if ("xv" == gui_config->display_driver) { driver = new S9xXVDisplayDriver(top_level, gui_config); } #endif else { driver = new S9xGTKDisplayDriver(top_level, gui_config); } if (driver->init()) { delete driver; driver = new S9xGTKDisplayDriver(top_level, gui_config); driver->init(); gui_config->display_driver = "none"; Gtk::MessageDialog dialog("Couldn't load display driver. Using default."); dialog.run(); } pool = NULL; } S9xDisplayDriver *S9xDisplayGetDriver() { return driver; } void S9xDeinitDisplay() { if (driver) driver->deinit(); delete driver; if (pool) { pool->stop(); pool = nullptr; } } void S9xReinitDisplay() { uint16_t *buffer = NULL; int width, height; buffer = new uint16_t[512 * 512]; memmove(buffer, GFX.Screen, 512 * 478 * 2); width = top_level->last_width; height = top_level->last_height; S9xDeinitDisplay(); S9xInitDriver(); S9xDisplayReconfigure(); top_level->last_width = width; top_level->last_height = height; memmove(GFX.Screen, buffer, 512 * 478 * 2); delete[] buffer; } bool8 S9xContinueUpdate(int width, int height) { S9xDeinitUpdate(width, height); return true; } bool8 S9xInitUpdate() { return true; } void S9xTextMode() { } void S9xGraphicsMode() { } #include "var8x10font.h" static const int font_width = 8; static const int font_height = 10; static inline int CharWidth(uint8 c) { return font_width - var8x10font_kern[c - 32][0] - var8x10font_kern[c - 32][1]; } static int StringWidth(const char *str) { int length = strlen(str); int pixcount = 0; if (length > 0) pixcount++; for (int i = 0; i < length; i++) { pixcount += (CharWidth(str[i]) - 1); } return pixcount; } static void GTKDisplayChar(int x, int y, uint8 c, bool monospace = false, int overlap = 0) { int cindex = c - 32; int crow = cindex >> 4; int ccol = cindex & 15; int cwidth = font_width - (monospace ? 0 : (var8x10font_kern[cindex][0] + var8x10font_kern[cindex][1])); int line = crow * font_height; int offset = ccol * font_width + (monospace ? 0 : var8x10font_kern[cindex][0]); int scale = IPPU.RenderedScreenWidth / SNES_WIDTH; uint16 *s = GFX.Screen + y * GFX.RealPPL + x * scale; for (int h = 0; h < font_height; h++, line++, s += GFX.RealPPL - cwidth * scale) { for (int w = 0; w < cwidth; w++, s++) { if (var8x10font[line][offset + w] == '#') *s = Settings.DisplayColor; else if (var8x10font[line][offset + w] == '.') *s = 0x0000; // else if (!monospace && w >= overlap) // *s = (*s & 0xf7de) >> 1; // *s = (*s & 0xe79c) >> 2; if (scale > 1) { s[1] = s[0]; s++; } } } } static void S9xGTKDisplayString(const char *string, int linesFromBottom, int pixelsFromLeft, bool allowWrap, int type) { bool monospace = true; if (type == S9X_NO_INFO) { if (linesFromBottom <= 0) linesFromBottom = 1; if (linesFromBottom >= 5) linesFromBottom -= 3; if (pixelsFromLeft > 128) pixelsFromLeft = SNES_WIDTH - StringWidth(string); monospace = false; } int dst_x = pixelsFromLeft; int dst_y = IPPU.RenderedScreenHeight - (font_height)*linesFromBottom; int len = strlen(string); if (IPPU.RenderedScreenHeight % 224 && !gui_config->overscan) dst_y -= 8; else if (gui_config->overscan) dst_y += 8; int overlap = 0; for (int i = 0; i < len; i++) { int cindex = string[i] - 32; int char_width = font_width - (monospace ? 1 : (var8x10font_kern[cindex][0] + var8x10font_kern[cindex][1])); if (dst_x + char_width > SNES_WIDTH || (uint8)string[i] < 32) { if (!allowWrap) break; linesFromBottom--; dst_y = IPPU.RenderedScreenHeight - font_height * linesFromBottom; dst_x = pixelsFromLeft; if (dst_y >= IPPU.RenderedScreenHeight) break; } if ((uint8)string[i] < 32) continue; GTKDisplayChar(dst_x, dst_y, string[i], monospace, overlap); dst_x += char_width - 1; overlap = 1; } } void S9xInitDisplay(int argc, char **argv) { S9xBlit2xSaIFilterInit(); #ifdef USE_HQ2X S9xBlitHQ2xFilterInit(); #endif /* USE_HQ2SX */ S9xQueryDrivers(); S9xInitDriver(); S9xGraphicsInit(); S9xDisplayReconfigure(); S9xCustomDisplayString = S9xGTKDisplayString; } bool S9xDisplayDriverIsReady() { if (!driver) return false; return driver->is_ready(); }