diff --git a/block.c b/block.c index e97ce0b1c8..b3a12fb318 100644 --- a/block.c +++ b/block.c @@ -2599,6 +2599,12 @@ static void bdrv_default_perms_for_storage(BlockDriverState *bs, BdrvChild *c, shared |= BLK_PERM_WRITE | BLK_PERM_RESIZE; } +#ifdef XBOX + if (bs->open_flags & BDRV_O_RO_WRITE_SHARE) { + shared |= BLK_PERM_WRITE | BLK_PERM_RESIZE; + } +#endif + *nperm = perm; *nshared = shared; } diff --git a/block/file-win32.c b/block/file-win32.c index 053496afeb..8a8ab436e7 100644 --- a/block/file-win32.c +++ b/block/file-win32.c @@ -36,6 +36,7 @@ #include "qapi/qmp/qstring.h" #include #include +#include #define FTYPE_FILE 0 #define FTYPE_CD 1 @@ -341,6 +342,9 @@ static int raw_open(BlockDriverState *bs, QDict *options, int flags, bool use_aio; OnOffAuto locking; int ret; +#ifdef XBOX + int sharing_flags; +#endif s->type = FTYPE_FILE; @@ -396,9 +400,21 @@ static int raw_open(BlockDriverState *bs, QDict *options, int flags, if (!filename) { goto fail; } + +#ifdef XBOX + sharing_flags = FILE_SHARE_READ; + if (flags & BDRV_O_RO_WRITE_SHARE) { + assert(access_flags == GENERIC_READ); + sharing_flags = FILE_SHARE_READ | FILE_SHARE_WRITE; + } + s->hfile = CreateFileW(wfilename, access_flags, + sharing_flags, NULL, + OPEN_EXISTING, overlapped, NULL); +#else s->hfile = CreateFileW(wfilename, access_flags, FILE_SHARE_READ, NULL, OPEN_EXISTING, overlapped, NULL); +#endif g_free(wfilename); if (s->hfile == INVALID_HANDLE_VALUE) { int err = GetLastError(); diff --git a/config_spec.yml b/config_spec.yml index cb300cce54..c43f5732f8 100644 --- a/config_spec.yml +++ b/config_spec.yml @@ -11,6 +11,13 @@ general: # throttle_io: bool last_viewed_menu_index: integer user_token: string + snapshots: + shortcuts: + f5: string + f6: string + f7: string + f8: string + filter_current_game: bool input: bindings: diff --git a/include/block/block.h b/include/block/block.h index 3477290f9a..eccd27e884 100644 --- a/include/block/block.h +++ b/include/block/block.h @@ -123,6 +123,10 @@ typedef struct HDGeometry { #define BDRV_O_AUTO_RDONLY 0x20000 /* degrade to read-only if opening read-write fails */ #define BDRV_O_IO_URING 0x40000 /* use io_uring instead of the thread pool */ +#ifdef XBOX +#define BDRV_O_RO_WRITE_SHARE 0x80000 /* allow the file to open RO alongside an existing RW handle */ +#endif + #define BDRV_O_CACHE_MASK (BDRV_O_NOCACHE | BDRV_O_NO_FLUSH) diff --git a/migration/savevm.c b/migration/savevm.c index 7b7b64bd13..a2d1350239 100644 --- a/migration/savevm.c +++ b/migration/savevm.c @@ -67,6 +67,8 @@ #include "qemu/yank.h" #include "yank_functions.h" +#include "ui/xemu-snapshots.h" + const unsigned int postcopy_ram_discard_version; /* Subcommands for QEMU_VM_COMMAND */ @@ -1570,6 +1572,9 @@ static int qemu_savevm_state(QEMUFile *f, Error **errp) ms->to_dst_file = f; qemu_mutex_unlock_iothread(); +#ifdef XBOX + xemu_snapshots_save_extra_data(f); +#endif qemu_savevm_state_header(f); qemu_savevm_state_setup(f); qemu_mutex_lock_iothread(); @@ -2691,6 +2696,12 @@ int qemu_loadvm_state(QEMUFile *f) return -EINVAL; } +#ifdef XBOX + if (!xemu_snapshots_offset_extra_data(f)) { + return -EINVAL; + } +#endif + ret = qemu_loadvm_state_header(f); if (ret) { return ret; @@ -3086,6 +3097,10 @@ bool delete_snapshot(const char *name, bool has_devices, return false; } +#ifdef XBOX + xemu_snapshots_mark_dirty(); +#endif + return true; } diff --git a/ui/meson.build b/ui/meson.build index 02064602e7..8306d0857b 100644 --- a/ui/meson.build +++ b/ui/meson.build @@ -26,6 +26,8 @@ xemu_ss.add(files( 'xemu.c', 'xemu-data.c', + 'xemu-snapshots.c', + 'xemu-thumbnail.cc', )) subdir('xui') diff --git a/ui/thirdparty/meson.build b/ui/thirdparty/meson.build index 803efe465c..528b9f40be 100644 --- a/ui/thirdparty/meson.build +++ b/ui/thirdparty/meson.build @@ -5,6 +5,7 @@ imgui_files = files( 'imgui/imgui_widgets.cpp', 'imgui/backends/imgui_impl_sdl.cpp', 'imgui/backends/imgui_impl_opengl3.cpp', + 'imgui/misc/cpp/imgui_stdlib.cpp', #'imgui/imgui_demo.cpp', ) diff --git a/ui/xemu-settings.h b/ui/xemu-settings.h index 32cb6987dd..491e5ce8de 100644 --- a/ui/xemu-settings.h +++ b/ui/xemu-settings.h @@ -26,6 +26,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -58,8 +59,9 @@ void xemu_settings_save(void); static inline void xemu_settings_set_string(const char **str, const char *new_str) { - free((char*)*str); - *str = strdup(new_str); + assert(new_str); + free((char*)*str); + *str = strdup(new_str); } void add_net_nat_forward_ports(int host, int guest, CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol); diff --git a/ui/xemu-snapshots.c b/ui/xemu-snapshots.c new file mode 100644 index 0000000000..becb0dd859 --- /dev/null +++ b/ui/xemu-snapshots.c @@ -0,0 +1,319 @@ +/* + * xemu User Interface + * + * Copyright (C) 2020-2022 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "xemu-snapshots.h" +#include "xemu-settings.h" +#include "xemu-xbe.h" + +#include +#include + +#include "block/aio.h" +#include "block/block_int.h" +#include "block/qapi.h" +#include "block/qdict.h" +#include "migration/qemu-file.h" +#include "migration/snapshot.h" +#include "qapi/error.h" +#include "sysemu/runstate.h" +#include "qemu-common.h" + +#include "ui/console.h" +#include "ui/input.h" + +static QEMUSnapshotInfo *xemu_snapshots_metadata = NULL; +static XemuSnapshotData *xemu_snapshots_extra_data = NULL; +static int xemu_snapshots_len = 0; +static bool xemu_snapshots_dirty = true; + +const char **g_snapshot_shortcut_index_key_map[] = { + &g_config.general.snapshots.shortcuts.f5, + &g_config.general.snapshots.shortcuts.f6, + &g_config.general.snapshots.shortcuts.f7, + &g_config.general.snapshots.shortcuts.f8, +}; + +static bool xemu_snapshots_load_thumbnail(BlockDriverState *bs_ro, + XemuSnapshotData *data, + int64_t *offset) +{ + int res = bdrv_load_vmstate(bs_ro, (uint8_t *)&data->thumbnail, *offset, + sizeof(TextureBuffer) - + sizeof(data->thumbnail.buffer)); + if (res != sizeof(TextureBuffer) - sizeof(data->thumbnail.buffer)) + return false; + *offset += res; + + data->thumbnail.buffer = g_malloc(data->thumbnail.size); + + res = bdrv_load_vmstate(bs_ro, (uint8_t *)data->thumbnail.buffer, *offset, + data->thumbnail.size); + if (res != data->thumbnail.size) { + return false; + } + *offset += res; + + return true; +} + +static void xemu_snapshots_load_data(BlockDriverState *bs_ro, + QEMUSnapshotInfo *info, + XemuSnapshotData *data, Error **err) +{ + int res; + XemuSnapshotHeader header; + int64_t offset = 0; + + data->xbe_title_present = false; + data->thumbnail_present = false; + res = bdrv_snapshot_load_tmp(bs_ro, info->id_str, info->name, err); + if (res < 0) + return; + + res = bdrv_load_vmstate(bs_ro, (uint8_t *)&header, offset, sizeof(header)); + if (res != sizeof(header)) + goto error; + offset += res; + + if (header.magic != XEMU_SNAPSHOT_DATA_MAGIC) + goto error; + + res = bdrv_load_vmstate(bs_ro, (uint8_t *)&data->xbe_title_len, offset, + sizeof(data->xbe_title_len)); + if (res != sizeof(data->xbe_title_len)) + goto error; + offset += res; + + data->xbe_title = (char *)g_malloc(data->xbe_title_len); + + res = bdrv_load_vmstate(bs_ro, (uint8_t *)data->xbe_title, offset, + data->xbe_title_len); + if (res != data->xbe_title_len) + goto error; + offset += res; + + data->xbe_title_present = (offset <= sizeof(header) + header.size); + + if (offset == sizeof(header) + header.size) + return; + + if (!xemu_snapshots_load_thumbnail(bs_ro, data, &offset)) { + goto error; + } + + data->thumbnail_present = (offset <= sizeof(header) + header.size); + + if (data->thumbnail_present) { + glGenTextures(1, &data->gl_thumbnail); + xemu_snapshots_render_thumbnail(data->gl_thumbnail, &data->thumbnail); + } + return; + +error: + g_free(data->xbe_title); + data->xbe_title_present = false; + + g_free(data->thumbnail.buffer); + data->thumbnail_present = false; +} + +static void xemu_snapshots_all_load_data(QEMUSnapshotInfo **info, + XemuSnapshotData **data, + int snapshots_len, Error **err) +{ + BlockDriverState *bs_ro; + QDict *opts = qdict_new(); + + assert(info && data); + + if (*data) { + for (int i = 0; i < xemu_snapshots_len; ++i) { + if ((*data)[i].xbe_title_present) { + g_free((*data)[i].xbe_title); + } + + if ((*data)[i].thumbnail_present) { + g_free((*data)[i].thumbnail.buffer); + glDeleteTextures(1, &((*data)[i].gl_thumbnail)); + } + } + g_free(*data); + } + + *data = + (XemuSnapshotData *)g_malloc(sizeof(XemuSnapshotData) * snapshots_len); + memset(*data, 0, sizeof(XemuSnapshotData) * snapshots_len); + + qdict_put_bool(opts, BDRV_OPT_READ_ONLY, true); + bs_ro = bdrv_open(g_config.sys.files.hdd_path, NULL, opts, + BDRV_O_RO_WRITE_SHARE | BDRV_O_AUTO_RDONLY, err); + if (!bs_ro) { + return; + } + + for (int i = 0; i < snapshots_len; ++i) { + xemu_snapshots_load_data(bs_ro, (*info) + i, (*data) + i, err); + if (*err) { + break; + } + } + + bdrv_flush(bs_ro); + bdrv_drain(bs_ro); + bdrv_unref(bs_ro); + assert(bs_ro->refcnt == 0); + if (!(*err)) + xemu_snapshots_dirty = false; +} + +int xemu_snapshots_list(QEMUSnapshotInfo **info, XemuSnapshotData **extra_data, + Error **err) +{ + BlockDriverState *bs; + AioContext *aio_context; + int snapshots_len; + assert(err); + + if (!xemu_snapshots_dirty && xemu_snapshots_extra_data && + xemu_snapshots_metadata) { + goto done; + } + + if (xemu_snapshots_metadata) + g_free(xemu_snapshots_metadata); + + bs = bdrv_all_find_vmstate_bs(NULL, false, NULL, err); + if (!bs) { + return -1; + } + + aio_context = bdrv_get_aio_context(bs); + + aio_context_acquire(aio_context); + snapshots_len = bdrv_snapshot_list(bs, &xemu_snapshots_metadata); + aio_context_release(aio_context); + xemu_snapshots_all_load_data(&xemu_snapshots_metadata, + &xemu_snapshots_extra_data, snapshots_len, + err); + if (*err) { + return -1; + } + + xemu_snapshots_len = snapshots_len; + +done: + if (info) { + *info = xemu_snapshots_metadata; + } + + if (extra_data) { + *extra_data = xemu_snapshots_extra_data; + } + + return xemu_snapshots_len; +} + +void xemu_snapshots_load(const char *vm_name, Error **err) +{ + bool vm_running = runstate_is_running(); + vm_stop(RUN_STATE_RESTORE_VM); + if (load_snapshot(vm_name, NULL, false, NULL, err) && vm_running) { + vm_start(); + } +} + +void xemu_snapshots_save(const char *vm_name, Error **err) +{ + save_snapshot(vm_name, true, NULL, false, NULL, err); +} + +void xemu_snapshots_delete(const char *vm_name, Error **err) +{ + delete_snapshot(vm_name, false, NULL, err); +} + +void xemu_snapshots_save_extra_data(QEMUFile *f) +{ + struct xbe *xbe_data = xemu_get_xbe_info(); + + int64_t xbe_title_len = 0; + char *xbe_title = g_utf16_to_utf8(xbe_data->cert->m_title_name, 40, NULL, + &xbe_title_len, NULL); + xbe_title_len++; + + XemuSnapshotHeader header = { XEMU_SNAPSHOT_DATA_MAGIC, 0 }; + + header.size += sizeof(xbe_title_len); + header.size += xbe_title_len; + + TextureBuffer *thumbnail = xemu_snapshots_extract_thumbnail(); + if (thumbnail && thumbnail->buffer) { + header.size += sizeof(TextureBuffer) - sizeof(thumbnail->buffer); + header.size += thumbnail->size; + } + + qemu_put_buffer(f, (const uint8_t *)&header, sizeof(header)); + qemu_put_buffer(f, (const uint8_t *)&xbe_title_len, sizeof(xbe_title_len)); + qemu_put_buffer(f, (const uint8_t *)xbe_title, xbe_title_len); + + if (thumbnail && thumbnail->buffer) { + qemu_put_buffer(f, (const uint8_t *)thumbnail, + sizeof(TextureBuffer) - sizeof(thumbnail->buffer)); + qemu_put_buffer(f, (const uint8_t *)thumbnail->buffer, thumbnail->size); + } + + g_free(xbe_title); + + if (thumbnail && thumbnail->buffer) { + g_free(thumbnail->buffer); + } + + g_free(thumbnail); + + xemu_snapshots_dirty = true; +} + +bool xemu_snapshots_offset_extra_data(QEMUFile *f) +{ + size_t ret; + XemuSnapshotHeader header; + ret = qemu_get_buffer(f, (uint8_t *)&header, sizeof(header)); + if (ret != sizeof(header)) { + return false; + } + + if (header.magic == XEMU_SNAPSHOT_DATA_MAGIC) { + /* + * qemu_file_skip only works if you aren't skipping past its buffer. + * Unfortunately, it's not usable here. + */ + void *buf = g_malloc(header.size); + qemu_get_buffer(f, buf, header.size); + g_free(buf); + } else { + qemu_file_skip(f, -((int)sizeof(header))); + } + + return true; +} + +void xemu_snapshots_mark_dirty(void) +{ + xemu_snapshots_dirty = true; +} diff --git a/ui/xemu-snapshots.h b/ui/xemu-snapshots.h new file mode 100644 index 0000000000..2f6dc4182f --- /dev/null +++ b/ui/xemu-snapshots.h @@ -0,0 +1,79 @@ +/* + * xemu User Interface + * + * Copyright (C) 2020-2022 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef XEMU_SNAPSHOTS_H +#define XEMU_SNAPSHOTS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "qemu/osdep.h" +#include "block/snapshot.h" + +#define XEMU_SNAPSHOT_DATA_MAGIC 0x78656d75 +#define XEMU_SNAPSHOT_HEIGHT 120 +#define XEMU_SNAPSHOT_WIDTH 160 + +extern const char **g_snapshot_shortcut_index_key_map[]; + +#pragma pack(1) +typedef struct TextureBuffer { + int channels; + unsigned long size; + void *buffer; +} TextureBuffer; +#pragma pack() + +typedef struct XemuSnapshotHeader { + uint32_t magic; + uint32_t size; +} XemuSnapshotHeader; + +typedef struct XemuSnapshotData { + int64_t xbe_title_len; + char *xbe_title; + bool xbe_title_present; + TextureBuffer thumbnail; + bool thumbnail_present; + unsigned int gl_thumbnail; +} XemuSnapshotData; + +// Implemented in xemu-snapshots.c +int xemu_snapshots_list(QEMUSnapshotInfo **info, XemuSnapshotData **extra_data, + Error **err); +void xemu_snapshots_load(const char *vm_name, Error **err); +void xemu_snapshots_save(const char *vm_name, Error **err); +void xemu_snapshots_delete(const char *vm_name, Error **err); + +void xemu_snapshots_save_extra_data(QEMUFile *f); +bool xemu_snapshots_offset_extra_data(QEMUFile *f); +void xemu_snapshots_mark_dirty(void); + +// Implemented in xemu-thumbnail.cc +void xemu_snapshots_set_framebuffer_texture(unsigned int tex, bool flip); +void xemu_snapshots_render_thumbnail(unsigned int tex, + TextureBuffer *thumbnail); +TextureBuffer *xemu_snapshots_extract_thumbnail(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/xemu-thumbnail.cc b/ui/xemu-thumbnail.cc new file mode 100644 index 0000000000..968d1c8c2b --- /dev/null +++ b/ui/xemu-thumbnail.cc @@ -0,0 +1,92 @@ +/* + * xemu User Interface + * + * Copyright (C) 2020-2022 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "xemu-snapshots.h" +#include "xui/gl-helpers.hh" + +static GLuint display_tex = 0; +static bool display_flip = false; + +void xemu_snapshots_set_framebuffer_texture(GLuint tex, bool flip) +{ + display_tex = tex; + display_flip = flip; +} + +void xemu_snapshots_render_thumbnail(GLuint tex, TextureBuffer *thumbnail) +{ + std::vector pixels; + unsigned int width, height, channels; + if (fpng::fpng_decode_memory( + thumbnail->buffer, thumbnail->size, pixels, width, height, channels, + thumbnail->channels) != fpng::FPNG_DECODE_SUCCESS) { + return; + } + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, + GL_UNSIGNED_BYTE, pixels.data()); +} + +TextureBuffer *xemu_snapshots_extract_thumbnail() +{ + /* + * Avoids crashing if a snapshot is made on a thread with no GL context + * Normally, this is not an issue, but it is better to fail safe than assert + * here. + * FIXME: Allow for dispatching a thumbnail request to the UI thread to + * remove this altogether. + */ + if (!SDL_GL_GetCurrentContext() || display_tex == 0) { + return NULL; + } + + // Render at 2x the base size to account for potential UI scaling + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, display_tex); + int thumbnail_width = XEMU_SNAPSHOT_WIDTH * 2; + int tex_width; + int tex_height; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tex_width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &tex_height); + int thumbnail_height = (int)(((float)tex_height / (float)tex_width) * (float)thumbnail_width); + + std::vector png; + if (!ExtractFramebufferPixels(display_tex, display_flip, png, thumbnail_width, thumbnail_height)) { + return NULL; + } + + TextureBuffer *thumbnail = (TextureBuffer *)g_malloc(sizeof(TextureBuffer)); + thumbnail->buffer = g_malloc(png.size() * sizeof(uint8_t)); + + thumbnail->channels = 3; + thumbnail->size = png.size() * sizeof(uint8_t); + memcpy(thumbnail->buffer, png.data(), thumbnail->size); + return thumbnail; +} diff --git a/ui/xemu.c b/ui/xemu.c index f0af36cea9..4cb9891b20 100644 --- a/ui/xemu.c +++ b/ui/xemu.c @@ -47,6 +47,7 @@ #include "xemu-input.h" #include "xemu-settings.h" // #include "xemu-shaders.h" +#include "xemu-snapshots.h" #include "xemu-version.h" #include "xemu-os-utils.h" @@ -1199,6 +1200,7 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl) glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT); + xemu_snapshots_set_framebuffer_texture(tex, flip_required); xemu_hud_set_framebuffer_texture(tex, flip_required); xemu_hud_render(); @@ -1548,6 +1550,7 @@ int main(int argc, char **argv) while (1) { sdl2_gl_refresh(&sdl2_console[0].dcl); + assert(glGetError() == GL_NO_ERROR); } // rcu_unregister_thread(); diff --git a/ui/xui/actions.cc b/ui/xui/actions.cc index e4d9e49005..f38be7dcb5 100644 --- a/ui/xui/actions.cc +++ b/ui/xui/actions.cc @@ -19,6 +19,8 @@ #include "common.hh" #include "misc.hh" #include "xemu-hud.h" +#include "../xemu-snapshots.h" +#include "../xemu-notifications.h" void ActionEjectDisc(void) { @@ -62,4 +64,28 @@ void ActionShutdown(void) void ActionScreenshot(void) { g_screenshot_pending = true; -} \ No newline at end of file +} + +void ActionActivateBoundSnapshot(int slot, bool save) +{ + assert(slot < 4 && slot >= 0); + const char *snapshot_name = *(g_snapshot_shortcut_index_key_map[slot]); + if (!snapshot_name || !(snapshot_name[0])) { + char *msg = g_strdup_printf("F%d is not bound to a snapshot", slot + 5); + xemu_queue_notification(msg); + g_free(msg); + return; + } + + Error *err = NULL; + if (save) { + xemu_snapshots_save(snapshot_name, &err); + } else { + xemu_snapshots_load(snapshot_name, &err); + } + + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } +} diff --git a/ui/xui/actions.hh b/ui/xui/actions.hh index 2ac680af78..60235bf373 100644 --- a/ui/xui/actions.hh +++ b/ui/xui/actions.hh @@ -24,3 +24,4 @@ void ActionTogglePause(); void ActionReset(); void ActionShutdown(); void ActionScreenshot(); +void ActionActivateBoundSnapshot(int slot, bool save); diff --git a/ui/xui/common.hh b/ui/xui/common.hh index c937ed5deb..13240c84e4 100644 --- a/ui/xui/common.hh +++ b/ui/xui/common.hh @@ -28,6 +28,7 @@ #include #include #include +#include #include extern "C" { diff --git a/ui/xui/gl-helpers.cc b/ui/xui/gl-helpers.cc index 94d8d21ac9..a7ce17261f 100644 --- a/ui/xui/gl-helpers.cc +++ b/ui/xui/gl-helpers.cc @@ -26,12 +26,14 @@ #include "data/controller_mask.png.h" #include "data/logo_sdf.png.h" #include "ui/shader/xemu-logo-frag.h" +#include "data/xemu_64x64.png.h" #include "notifications.hh" Fbo *controller_fbo, *logo_fbo; GLuint g_controller_tex, - g_logo_tex; + g_logo_tex, + g_icon_tex; enum class ShaderType { Blit, @@ -155,10 +157,10 @@ static GLuint InitTexture(unsigned char *data, int width, int height, return tex; } -static GLuint LoadTextureFromMemory(const unsigned char *buf, unsigned int size) +static GLuint LoadTextureFromMemory(const unsigned char *buf, unsigned int size, bool flip=true) { // Flip vertically so textures are loaded according to GL convention. - stbi_set_flip_vertically_on_load(1); + stbi_set_flip_vertically_on_load(flip); int width, height, channels = 0; unsigned char *data = stbi_load_from_memory(buf, size, &width, &height, &channels, 4); @@ -442,6 +444,8 @@ void InitCustomRendering(void) g_logo_shader = NewDecalShader(ShaderType::Logo); logo_fbo = new Fbo(512, 512); + g_icon_tex = LoadTextureFromMemory(xemu_64x64_data, xemu_64x64_size, false); + g_framebuffer_shader = NewDecalShader(ShaderType::BlitGamma); } @@ -657,7 +661,7 @@ void RenderLogo(uint32_t time) glUseProgram(0); } -void RenderFramebuffer(GLint tex, int width, int height, bool flip) +void RenderFramebuffer(GLint tex, int width, int height, bool flip, bool apply_scaling_factor) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); @@ -668,7 +672,10 @@ void RenderFramebuffer(GLint tex, int width, int height, bool flip) // Calculate scaling factors float scale[2]; - if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) { + if (!apply_scaling_factor) { + scale[0] = 1.0; + scale[1] = 1.0; + } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) { // Stretch to fit scale[0] = 1.0; scale[1] = 1.0; @@ -720,19 +727,27 @@ void RenderFramebuffer(GLint tex, int width, int height, bool flip) glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL); } -void SaveScreenshot(GLuint tex, bool flip) +bool ExtractFramebufferPixels(GLuint tex, bool flip, std::vector &png, int width, int height) { - int width, height; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); - glBindTexture(GL_TEXTURE_2D, 0); + assert((width == 0 && height == 0) || (width > 0 && height > 0)); - if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) { - width = height * (16.0f / 9.0f); - } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) { - width = height * (4.0f / 3.0f); + bool params_from_tex = false; + if (width <= 0 && height <= 0) { + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); + params_from_tex = true; + } + glBindTexture(GL_TEXTURE_2D, 0); + assert(width > 0 && height > 0); + + if (params_from_tex) { + if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) { + width = height * (16.0f / 9.0f); + } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) { + width = height * (4.0f / 3.0f); + } } std::vector pixels; @@ -742,7 +757,7 @@ void SaveScreenshot(GLuint tex, bool flip) fbo.Target(); bool blend = glIsEnabled(GL_BLEND); if (blend) glDisable(GL_BLEND); - RenderFramebuffer(tex, width, height, !flip); + RenderFramebuffer(tex, width, height, !flip, params_from_tex); if (blend) glEnable(GL_BLEND); glPixelStorei(GL_PACK_ROW_LENGTH, width); glPixelStorei(GL_PACK_IMAGE_HEIGHT, height); @@ -750,10 +765,15 @@ void SaveScreenshot(GLuint tex, bool flip) glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); fbo.Restore(); - char fname[128]; + return fpng::fpng_encode_image_to_memory(pixels.data(), width, height, 3, png); +} + +void SaveScreenshot(GLuint tex, bool flip) +{ Error *err = NULL; + char fname[128]; std::vector png; - if (fpng::fpng_encode_image_to_memory(pixels.data(), width, height, 3, png)) { + if (ExtractFramebufferPixels(tex, flip, png)) { time_t t = time(NULL); struct tm *tmp = localtime(&t); if (tmp) { diff --git a/ui/xui/gl-helpers.hh b/ui/xui/gl-helpers.hh index 9891c5fdc8..f881f9fd5b 100644 --- a/ui/xui/gl-helpers.hh +++ b/ui/xui/gl-helpers.hh @@ -17,6 +17,7 @@ // along with this program. If not, see . // #pragma once +#include #include "common.hh" #include "../xemu-input.h" @@ -38,6 +39,7 @@ public: }; extern Fbo *controller_fbo, *logo_fbo; +extern GLuint g_icon_tex; void InitCustomRendering(void); void RenderLogo(uint32_t time); @@ -45,5 +47,6 @@ void RenderController(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, ControllerState *state); void RenderControllerPort(float frame_x, float frame_y, int i, uint32_t port_color); -void RenderFramebuffer(GLint tex, int width, int height, bool flip); +void RenderFramebuffer(GLint tex, int width, int height, bool flip, bool apply_scaling_factor = true); +bool ExtractFramebufferPixels(GLuint tex, bool flip, std::vector &png, int width = 0, int height = 0); void SaveScreenshot(GLuint tex, bool flip); diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index 19d12f9cc7..b011a4cb43 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -27,6 +27,7 @@ #include "misc.hh" #include "gl-helpers.hh" #include "reporting.hh" +#include "qapi/error.h" #include "../xemu-input.h" #include "../xemu-notifications.h" @@ -34,6 +35,7 @@ #include "../xemu-monitor.h" #include "../xemu-version.h" #include "../xemu-net.h" +#include "../xemu-snapshots.h" #include "../xemu-os-utils.h" #include "../xemu-xbe.h" @@ -639,115 +641,356 @@ void MainMenuNetworkView::DrawUdpOptions(bool appearing) ImGui::PopFont(); } -#if 0 -class MainMenuSnapshotsView : public virtual MainMenuTabView +void MainMenuSnapshotsView::Load() { -protected: - GLuint screenshot; + Error *err = NULL; -public: - void initScreenshot() - { - if (screenshot == 0) { - glGenTextures(1, &screenshot); - int w, h, n; - stbi_set_flip_vertically_on_load(0); - unsigned char *data = stbi_load("./data/screenshot.png", &w, &h, &n, 4); - assert(n == 4); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, screenshot); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - stbi_image_free(data); + if (!m_load_failed) { + m_snapshots_len = xemu_snapshots_list(&m_snapshots, &m_extra_data, &err); + } + + if (err) { + m_load_failed = true; + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + m_snapshots_len = 0; + } + + struct xbe *xbe = xemu_get_xbe_info(); + if (xbe && xbe->cert->m_titleid != m_current_title_id) { + g_free(m_current_title_name); + m_current_title_name = g_utf16_to_utf8(xbe->cert->m_title_name, 40, NULL, NULL, NULL); + m_current_title_id = xbe->cert->m_titleid; + } +} + +MainMenuSnapshotsView::MainMenuSnapshotsView(): MainMenuTabView() +{ + xemu_snapshots_mark_dirty(); + m_load_failed = false; + + m_search_regex = NULL; + m_current_title_name = NULL; + m_current_title_id = 0; + +} + +MainMenuSnapshotsView::~MainMenuSnapshotsView() +{ + g_free(m_snapshots); + g_free(m_extra_data); + xemu_snapshots_mark_dirty(); + + g_free(m_current_title_name); + g_free(m_search_regex); +} + +void MainMenuSnapshotsView::SnapshotBigButton(QEMUSnapshotInfo *snapshot, const char *title_name, GLuint screenshot) +{ + Error *err = NULL; + int current_snapshot_binding = -1; + for (int i = 0; i < 4; ++i) { + if (g_strcmp0(*(g_snapshot_shortcut_index_key_map[i]), snapshot->name) == 0) { + assert(current_snapshot_binding == -1); + current_snapshot_binding = i; } } - void snapshotBigButton(const char *name, const char *title_name, GLuint screenshot) - { - ImGuiStyle &style = ImGui::GetStyle(); - ImVec2 pos = ImGui::GetCursorPos(); - ImDrawList *draw_list = ImGui::GetWindowDrawList(); + ImGuiStyle &style = ImGui::GetStyle(); + ImVec2 pos = ImGui::GetCursorPos(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); - ImGui::PushFont(g_font_mgr.m_menuFont); - const char *icon = ICON_FA_CIRCLE_XMARK; - ImVec2 ts_icon = ImGui::CalcTextSize(icon); - ts_icon.x += 2*style.FramePadding.x; - ImGui::PopFont(); + ImGui::PushFont(g_font_mgr.m_menu_font); + const char *close_icon = ICON_FA_CIRCLE_XMARK; + ImVec2 ts_close_icon = ImGui::CalcTextSize(close_icon); + ts_close_icon.x += 2*style.FramePadding.x; + ImGui::PopFont(); - ImGui::PushFont(g_font_mgr.m_menuFontSmall); - ImVec2 ts_sub = ImGui::CalcTextSize(name); - ImGui::PopFont(); + ImGui::PushFont(g_font_mgr.m_menu_font); + const char *save_icon = ICON_FA_FLOPPY_DISK; + ImVec2 ts_save_icon = ImGui::CalcTextSize(save_icon); + ts_save_icon.x += 2*style.FramePadding.x; + ImGui::PopFont(); - ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, g_viewport_mgr.scale(ImVec2(5, 5))); - ImGui::PushFont(g_font_mgr.m_menuFontMedium); + ImGui::PushFont(g_font_mgr.m_menu_font); + const char *binding_icon = ICON_FA_KEYBOARD; + ImVec2 ts_binding_icon = ImGui::CalcTextSize(binding_icon); + ts_binding_icon.x += 2*style.FramePadding.x; + ImGui::PopFont(); - ImVec2 ts_title = ImGui::CalcTextSize(name); - ImVec2 thumbnail_size = g_viewport_mgr.scale(ImVec2(160, 120)); - ImVec2 thumbnail_pos(style.FramePadding.x, style.FramePadding.y); - ImVec2 text_pos(thumbnail_pos.x + thumbnail_size.x + style.FramePadding.x * 2, thumbnail_pos.y); - ImVec2 subtext_pos(text_pos.x, text_pos.y + ts_title.y + style.FramePadding.x); + ImGui::PushFont(g_font_mgr.m_menu_font_small); + ImVec2 ts_sub = ImGui::CalcTextSize(snapshot->name); + ImGui::PopFont(); - ImGui::Button("###button", ImVec2(ImGui::GetContentRegionAvail().x, fmax(thumbnail_size.y + style.FramePadding.y * 2, - ts_title.y + ts_sub.y + style.FramePadding.y * 3))); - ImGui::PopFont(); - const ImVec2 sz = ImGui::GetItemRectSize(); - const ImVec2 p0 = ImGui::GetItemRectMin(); - const ImVec2 p1 = ImGui::GetItemRectMax(); - ts_icon.y = sz.y; + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, g_viewport_mgr.Scale(ImVec2(5, 5))); + ImGui::PushFont(g_font_mgr.m_menu_font_medium); - // Snapshot thumbnail - ImGui::SetItemAllowOverlap(); - ImGui::SameLine(); - ImGui::SetCursorPosX(pos.x + thumbnail_pos.x); - ImGui::SetCursorPosY(pos.y + thumbnail_pos.y); - ImGui::Image((ImTextureID)screenshot, thumbnail_size, ImVec2(0,0), ImVec2(1,1)); + ImVec2 ts_title = ImGui::CalcTextSize(snapshot->name); + ImVec2 thumbnail_size = g_viewport_mgr.Scale(ImVec2(XEMU_SNAPSHOT_WIDTH, XEMU_SNAPSHOT_HEIGHT)); + ImVec2 thumbnail_pos(style.FramePadding.x, style.FramePadding.y); + ImVec2 name_pos(thumbnail_pos.x + thumbnail_size.x + style.FramePadding.x * 2, thumbnail_pos.y); + ImVec2 date_pos(name_pos.x, name_pos.y + ts_title.y + style.FramePadding.x); + ImVec2 title_pos(name_pos.x, date_pos.y + ts_title.y + style.FramePadding.x); + ImVec2 binding_pos(name_pos.x, title_pos.y + ts_title.y + style.FramePadding.x); - draw_list->PushClipRect(p0, p1, true); + bool load = ImGui::Button("###button", ImVec2(ImGui::GetContentRegionAvail().x, fmax(thumbnail_size.y + style.FramePadding.y * 2, + ts_title.y + ts_sub.y + style.FramePadding.y * 3))); + ImGui::PopFont(); + const ImVec2 sz = ImGui::GetItemRectSize(); + const ImVec2 p0 = ImGui::GetItemRectMin(); + const ImVec2 p1 = ImGui::GetItemRectMax(); + ts_close_icon.y = sz.y / 1; + ts_save_icon.y = sz.y / 1; + ts_binding_icon.y = sz.y / 1; - // Snapshot title - ImGui::PushFont(g_font_mgr.m_menuFontMedium); - draw_list->AddText(ImVec2(p0.x + text_pos.x, p0.y + text_pos.y), IM_COL32(255, 255, 255, 255), name); - ImGui::PopFont(); - - // Snapshot subtitle - ImGui::PushFont(g_font_mgr.m_menuFontSmall); - draw_list->AddText(ImVec2(p0.x + subtext_pos.x, p0.y + subtext_pos.y), IM_COL32(255, 255, 255, 200), title_name); - ImGui::PopFont(); - - draw_list->PopClipRect(); - - // Delete button - ImGui::SameLine(); - ImGui::SetCursorPosY(pos.y); - ImGui::SetCursorPosX(pos.x + sz.x - ts_icon.x); - ImGui::PushFont(g_font_mgr.m_menuFont); - ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); - ImGui::Button(icon, ts_icon); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(1); - ImGui::PopFont(); - ImGui::PopStyleVar(2); - } - - void Draw() - { - initScreenshot(); - for (int i = 0; i < 15; i++) { - char buf[64]; - snprintf(buf, sizeof(buf), "%s", "Apr 9 2022 19:44"); - ImGui::PushID(i); - snapshotBigButton(buf, "Halo: Combat Evolved", screenshot); - ImGui::PopID(); + if (load) { + xemu_snapshots_load(snapshot->name, &err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); } } -}; -#endif + + // Snapshot thumbnail + ImGui::SetItemAllowOverlap(); + ImGui::SameLine(); + ImGui::SetCursorPosX(pos.x + thumbnail_pos.x); + ImGui::SetCursorPosY(pos.y + thumbnail_pos.y); + if (screenshot > 0) { + ImGui::Image((ImTextureID)(uint64_t)screenshot, thumbnail_size); + } else { + ImGui::Image((ImTextureID)(uint64_t)g_icon_tex, thumbnail_size); + } + + draw_list->PushClipRect(p0, p1, true); + + // Snapshot title + ImGui::PushFont(g_font_mgr.m_menu_font_medium); + draw_list->AddText(ImVec2(p0.x + name_pos.x, p0.y + name_pos.y), IM_COL32(255, 255, 255, 255), snapshot->name); + ImGui::PopFont(); + + // Snapshot date + g_autoptr(GDateTime) date = g_date_time_new_from_unix_local(snapshot->date_sec); + char *date_buf = g_date_time_format(date, "%Y-%m-%d %H:%M:%S"); + ImGui::PushFont(g_font_mgr.m_menu_font_small); + draw_list->AddText(ImVec2(p0.x + date_pos.x, p0.y + date_pos.y), IM_COL32(255, 255, 255, 200), date_buf); + ImGui::PopFont(); + g_free(date_buf); + + // Snapshot title + ImGui::PushFont(g_font_mgr.m_menu_font_small); + draw_list->AddText(ImVec2(p0.x + title_pos.x, p0.y + title_pos.y), IM_COL32(255, 255, 255, 200), title_name); + ImGui::PopFont(); + + // Snapshot binding + ImGui::PushFont(g_font_mgr.m_menu_font_small); + if (current_snapshot_binding != -1) { + char *binding_text = g_strdup_printf("Bound to F%d", current_snapshot_binding + 5); + draw_list->AddText(ImVec2(p0.x + binding_pos.x, p0.y + binding_pos.y), IM_COL32(255, 255, 255, 200), binding_text); + g_free(binding_text); + } else { + ImGui::Text("Not Bound"); + draw_list->AddText(ImVec2(p0.x + binding_pos.x, p0.y + binding_pos.y), IM_COL32(255, 255, 255, 200), "Not Bound"); + } + + ImGui::PopFont(); + + draw_list->PopClipRect(); + + // Delete button + ImGui::SameLine(); + ImGui::SetCursorPosY(pos.y); + ImGui::SetCursorPosX(pos.x + sz.x - ts_close_icon.x); + ImGui::PushFont(g_font_mgr.m_menu_font); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); + ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); + if (ImGui::Button(close_icon, ts_close_icon)) { + xemu_snapshots_delete(snapshot->name, &err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } + } + ImGui::PopStyleColor(); + ImGui::PopStyleVar(1); + ImGui::PopFont(); + + // Save button + ImGui::SameLine(); + ImGui::SetCursorPosY(pos.y); + ImGui::SetCursorPosX(pos.x + sz.x - ts_save_icon.x - ts_close_icon.x); + ImGui::PushFont(g_font_mgr.m_menu_font); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); + ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); + if (ImGui::Button(save_icon, ts_save_icon)) { + xemu_snapshots_save(snapshot->name, &err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } + } + ImGui::PopStyleColor(); + ImGui::PopStyleVar(1); + ImGui::PopFont(); + + // Bind button + ImGui::SameLine(); + ImGui::SetCursorPosY(pos.y); + ImGui::SetCursorPosX(pos.x + sz.x - ts_binding_icon.x - ts_save_icon.x - ts_close_icon.x); + ImGui::PushFont(g_font_mgr.m_menu_font); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); + ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); + if (ImGui::Button(binding_icon, ts_binding_icon)) { + ImGui::OpenPopup("Bind Snapshot Key"); + } + + if (ImGui::BeginPopupContextItem("Bind Snapshot Key")) { + for (int i = 0; i < 4; ++i) { + char *item_name = g_strdup_printf("Bind to F%d", i + 5); + + if (ImGui::MenuItem(item_name)) { + if (current_snapshot_binding >= 0) { + xemu_settings_set_string(g_snapshot_shortcut_index_key_map[current_snapshot_binding], ""); + } + xemu_settings_set_string(g_snapshot_shortcut_index_key_map[i], snapshot->name); + current_snapshot_binding = i; + + ImGui::CloseCurrentPopup(); + } + + g_free(item_name); + } + + if (current_snapshot_binding >= 0) { + ImGui::Separator(); + if (ImGui::MenuItem("Unbind")) { + xemu_settings_set_string(g_snapshot_shortcut_index_key_map[current_snapshot_binding], ""); + current_snapshot_binding = -1; + } + } + + ImGui::EndPopup(); + } + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(1); + ImGui::PopFont(); + + + ImGui::PopStyleVar(2); +} + +static int MainMenuSnapshotsViewUpdateSearchBox(ImGuiInputTextCallbackData *data) +{ + GError *gerr = NULL; + MainMenuSnapshotsView *win = (MainMenuSnapshotsView*)data->UserData; + + if (win->m_search_regex) g_free(win->m_search_regex); + if (data->BufTextLen == 0) { + win->m_search_regex = NULL; + return 0; + } + + char *buf = g_strdup_printf("(.*)%s(.*)", data->Buf); + + win->m_search_regex = g_regex_new(buf, (GRegexCompileFlags)0, (GRegexMatchFlags)0, &gerr); + g_free(buf); + if (gerr) { + win->m_search_regex = NULL; + return 1; + } + + return 0; +} + +void MainMenuSnapshotsView::Draw() +{ + Load(); + SectionTitle("Snapshots"); + ImGui::Checkbox("Filter by current title", &g_config.general.snapshots.filter_current_game); + ImGui::InputTextWithHint("##search", "Filter by name...", &m_search_buf, ImGuiInputTextFlags_CallbackEdit, + &MainMenuSnapshotsViewUpdateSearchBox, this); + + ImGui::InputTextWithHint("##create", "Create new snapshot", &m_create_buf); + + bool snapshot_with_create_name_exists = false; + for (int i = 0; i < m_snapshots_len; ++i) { + if (g_strcmp0(m_create_buf.c_str(), m_snapshots[i].name) == 0) { + snapshot_with_create_name_exists = true; + break; + } + } + + ImGui::SameLine(); + if (ImGui::Button(snapshot_with_create_name_exists ? "Save" : "Create") && !m_create_buf.empty()) { + xemu_snapshots_save(m_create_buf.c_str(), NULL); + m_create_buf.clear(); + } + + if (snapshot_with_create_name_exists && ImGui::IsItemHovered()) { + ImGui::SetTooltip("A snapshot with the name \"%s\" already exists. This button will overwrite the existing snapshot.", m_create_buf.c_str()); + } + + bool search_buf_equal = false; + for (int i = m_snapshots_len - 1; i >= 0; i--) { + if (g_config.general.snapshots.filter_current_game && m_extra_data[i].xbe_title_present && + (strcmp(m_current_title_name, m_extra_data[i].xbe_title) != 0)) { + continue; + } + + if (m_search_regex) { + GMatchInfo *match; + bool keep_entry = false; + + g_regex_match(m_search_regex, m_snapshots[i].name, (GRegexMatchFlags)0, &match); + keep_entry |= g_match_info_matches(match); + g_match_info_free(match); + + if (m_extra_data[i].xbe_title_present) { + g_regex_match(m_search_regex, m_extra_data[i].xbe_title, (GRegexMatchFlags)0, &match); + keep_entry |= g_match_info_matches(match); + g_free(match); + } + + if (!keep_entry) { + continue; + } + } + + search_buf_equal |= g_strcmp0(m_search_buf.c_str(), m_snapshots[i].name) == 0; + + ImGui::PushID(i); + GLuint thumbnail = 0; + if (m_extra_data[i].thumbnail_present) { + thumbnail = m_extra_data[i].gl_thumbnail; + } + SnapshotBigButton( + m_snapshots + i, + m_extra_data[i].xbe_title_present ? m_extra_data[i].xbe_title : "Unknown", + thumbnail + ); + ImGui::PopID(); + } + + /* Snapshot names are unique, don't give option to create new one if it exists already */ + if (!search_buf_equal && !m_search_buf.empty()) { + char *new_snapshot = g_strdup_printf("Create Snapshot '%s'", m_search_buf.c_str()); + ImGui::PushFont(g_font_mgr.m_menu_font_small); + ImVec2 new_snapshot_size = ImGui::CalcTextSize(new_snapshot); + ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - new_snapshot_size.x)/2); + if (ImGui::Button(new_snapshot)) { + Error *err = NULL; + xemu_snapshots_save(m_search_buf.c_str(), &err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } + } + ImGui::PopFont(); + g_free(new_snapshot); + } +} MainMenuSystemView::MainMenuSystemView() : m_dirty(false) { @@ -929,7 +1172,7 @@ MainMenuScene::MainMenuScene() m_display_button("Display", ICON_FA_TV), m_audio_button("Audio", ICON_FA_VOLUME_HIGH), m_network_button("Network", ICON_FA_NETWORK_WIRED), - // m_snapshots_button("Snapshots", ICON_FA_CLOCK_ROTATE_LEFT), + m_snapshots_button("Snapshots", ICON_FA_CLOCK_ROTATE_LEFT), m_system_button("System", ICON_FA_MICROCHIP), m_about_button("About", ICON_FA_CIRCLE_INFO) { @@ -940,7 +1183,7 @@ MainMenuScene::MainMenuScene() m_tabs.push_back(&m_display_button); m_tabs.push_back(&m_audio_button); m_tabs.push_back(&m_network_button); - // m_tabs.push_back(&m_snapshots_button); + m_tabs.push_back(&m_snapshots_button); m_tabs.push_back(&m_system_button); m_tabs.push_back(&m_about_button); @@ -949,7 +1192,7 @@ MainMenuScene::MainMenuScene() m_views.push_back(&m_display_view); m_views.push_back(&m_audio_view); m_views.push_back(&m_network_view); - // m_views.push_back(&m_snapshots_view); + m_views.push_back(&m_snapshots_view); m_views.push_back(&m_system_view); m_views.push_back(&m_about_view); @@ -977,15 +1220,18 @@ void MainMenuScene::ShowNetwork() { SetNextViewIndexWithFocus(4); } -// void MainMenuScene::showSnapshots() { SetNextViewIndexWithFocus(5); } -void MainMenuScene::ShowSystem() +void MainMenuScene::ShowSnapshots() { SetNextViewIndexWithFocus(5); } -void MainMenuScene::ShowAbout() +void MainMenuScene::ShowSystem() { SetNextViewIndexWithFocus(6); } +void MainMenuScene::ShowAbout() +{ + SetNextViewIndexWithFocus(7); +} void MainMenuScene::SetNextViewIndexWithFocus(int i) { diff --git a/ui/xui/main-menu.hh b/ui/xui/main-menu.hh index ced9db147a..2b469c1112 100644 --- a/ui/xui/main-menu.hh +++ b/ui/xui/main-menu.hh @@ -24,6 +24,7 @@ #include "widgets.hh" #include "scene.hh" #include "scene-components.hh" +#include "../xemu-snapshots.h" extern "C" { #include "net/pcap.h" @@ -102,8 +103,24 @@ public: class MainMenuSnapshotsView : public virtual MainMenuTabView { +protected: + QEMUSnapshotInfo *m_snapshots; + XemuSnapshotData *m_extra_data; + int m_snapshots_len; + uint32_t m_current_title_id; + char *m_current_title_name; + std::string m_search_buf; + std::string m_create_buf; + bool m_load_failed; + +private: + void Load(); + public: - void SnapshotBigButton(const char *name, const char *title_name, + GRegex *m_search_regex; + MainMenuSnapshotsView(); + ~MainMenuSnapshotsView(); + void SnapshotBigButton(QEMUSnapshotInfo *snapshot, const char *title_name, GLuint screenshot); void Draw() override; }; @@ -153,7 +170,7 @@ protected: m_display_button, m_audio_button, m_network_button, - // m_snapshots_button, + m_snapshots_button, m_system_button, m_about_button; std::vector m_views; @@ -162,7 +179,7 @@ protected: MainMenuDisplayView m_display_view; MainMenuAudioView m_audio_view; MainMenuNetworkView m_network_view; - // MainMenuSnapshotsView m_snapshots_view; + MainMenuSnapshotsView m_snapshots_view; MainMenuSystemView m_system_view; MainMenuAboutView m_about_view; @@ -174,7 +191,7 @@ public: void ShowDisplay(); void ShowAudio(); void ShowNetwork(); - // void ShowSnapshots(); + void ShowSnapshots(); void ShowSystem(); void ShowAbout(); void SetNextViewIndexWithFocus(int i); diff --git a/ui/xui/main.cc b/ui/xui/main.cc index e46255d8ef..e027c409a9 100644 --- a/ui/xui/main.cc +++ b/ui/xui/main.cc @@ -31,6 +31,7 @@ #include #include +#include "actions.hh" #include "common.hh" #include "xemu-hud.h" #include "misc.hh" @@ -277,6 +278,14 @@ void xemu_hud_render(void) !ImGui::IsAnyItemFocused() && !ImGui::IsAnyItemHovered())) { g_scene_mgr.PushScene(g_popup_menu); } + + bool mod_key_down = ImGui::IsKeyDown(ImGuiKey_ModShift); + for (int f_key = 0; f_key < 4; ++f_key) { + if (ImGui::IsKeyPressed(f_key + ImGuiKey_F5)) { + ActionActivateBoundSnapshot(f_key, mod_key_down); + break; + } + } } first_boot_window.Draw(); diff --git a/ui/xui/menubar.cc b/ui/xui/menubar.cc index 0540631d10..71c2eab69a 100644 --- a/ui/xui/menubar.cc +++ b/ui/xui/menubar.cc @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . // +#include "ui/xemu-notifications.h" #include "common.hh" #include "main-menu.hh" #include "menubar.hh" @@ -88,6 +89,48 @@ void ShowMainMenu() if (ImGui::MenuItem(running ? "Pause" : "Resume", SHORTCUT_MENU_TEXT(P))) ActionTogglePause(); if (ImGui::MenuItem("Screenshot", "F12")) ActionScreenshot(); + if (ImGui::BeginMenu("Snapshot")) { + if (ImGui::MenuItem("Create Snapshot")) { + xemu_snapshots_save(NULL, NULL); + xemu_queue_notification("Created new snapshot"); + } + + for (int i = 0; i < 4; ++i) { + char *hotkey = g_strdup_printf("Shift+F%d", i + 5); + + char *load_name; + char *save_name; + + assert(g_snapshot_shortcut_index_key_map[i]); + bool bound = *(g_snapshot_shortcut_index_key_map[i]) && + (**(g_snapshot_shortcut_index_key_map[i]) != 0); + + if (bound) { + load_name = g_strdup_printf("Load '%s'", *(g_snapshot_shortcut_index_key_map[i])); + save_name = g_strdup_printf("Save '%s'", *(g_snapshot_shortcut_index_key_map[i])); + } else { + load_name = g_strdup_printf("Load F%d (Unbound)", i + 5); + save_name = g_strdup_printf("Save F%d (Unbound)", i + 5); + } + + ImGui::Separator(); + + if (ImGui::MenuItem(load_name, hotkey + sizeof("Shift+") - 1, false, bound)) { + ActionActivateBoundSnapshot(i, false); + } + + if (ImGui::MenuItem(save_name, hotkey, false, bound)) { + ActionActivateBoundSnapshot(i, false); + } + + g_free(hotkey); + g_free(load_name); + g_free(save_name); + } + + ImGui::EndMenu(); + } + ImGui::Separator(); if (ImGui::MenuItem("Eject Disc", SHORTCUT_MENU_TEXT(E))) ActionEjectDisc(); @@ -101,6 +144,7 @@ void ShowMainMenu() if (ImGui::MenuItem(" Display")) g_main_menu.ShowDisplay(); if (ImGui::MenuItem(" Audio")) g_main_menu.ShowAudio(); if (ImGui::MenuItem(" Network")) g_main_menu.ShowNetwork(); + if (ImGui::MenuItem(" Snapshots")) g_main_menu.ShowSnapshots(); if (ImGui::MenuItem(" System")) g_main_menu.ShowSystem(); ImGui::Separator(); diff --git a/ui/xui/popup-menu.cc b/ui/xui/popup-menu.cc index 6cad92e2a1..cf6fc3f130 100644 --- a/ui/xui/popup-menu.cc +++ b/ui/xui/popup-menu.cc @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . // +#include "ui/xemu-notifications.h" #include #include #include "misc.hh" @@ -27,6 +28,8 @@ #include "input-manager.hh" #include "xemu-hud.h" #include "IconsFontAwesome6.h" +#include "../xemu-snapshots.h" +#include "main-menu.hh" PopupMenuItemDelegate::~PopupMenuItemDelegate() {} void PopupMenuItemDelegate::PushMenu(PopupMenu &menu) {} @@ -269,7 +272,7 @@ public: } }; -extern Scene g_main_menu; +extern MainMenuScene g_main_menu; class SettingsPopupMenu : public virtual PopupMenu { protected: @@ -292,6 +295,11 @@ public: nav.PushFocus(); nav.PushMenu(display_mode); } + if (PopupMenuButton("Snapshots...", ICON_FA_CLOCK_ROTATE_LEFT)) { + nav.ClearMenuStack(); + g_scene_mgr.PushScene(g_main_menu); + g_main_menu.ShowSnapshots(); + } if (PopupMenuButton("All settings...", ICON_FA_SLIDERS)) { nav.ClearMenuStack(); g_scene_mgr.PushScene(g_main_menu); @@ -338,6 +346,11 @@ public: ActionScreenshot(); pop = true; } + if (PopupMenuButton("Save Snapshot", ICON_FA_DOWNLOAD)) { + xemu_snapshots_save(NULL, NULL); + xemu_queue_notification("Created new snapshot"); + pop = true; + } if (PopupMenuButton("Eject Disc", ICON_FA_EJECT)) { ActionEjectDisc(); pop = true;