diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e37a3cae43..6e2f74efb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -359,6 +359,13 @@ jobs: dist/xemu-macos-universal-debug/xemu-macos-universal-debug.zip dist/xemu-ubuntu-release/xemu/xemu-v${{ env.XEMU_VERSION }}-x86_64.AppImage dist/xemu-ubuntu-debug/xemu/xemu-v${{ env.XEMU_VERSION }}-dbg-x86_64.AppImage + - name: Trigger website update + uses: benc-uk/workflow-dispatch@v1.2.2 + with: + workflow: build.yml + repo: xemu-project/xemu-website + token: ${{ secrets.XEMU_ROBOT_TOKEN }} + ref: master # Sync archive version of source (including submodule code) to the # ppa-snapshot branch to work around limitations of the Launchpad platform, diff --git a/.github/workflows/trigger-website-update.yml b/.github/workflows/trigger-website-update.yml deleted file mode 100644 index 2430f346f9..0000000000 --- a/.github/workflows/trigger-website-update.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Trigger website update - -on: - release: - types: [published] - -jobs: - trigger-website-update: - name: Trigger website update - runs-on: ubuntu-latest - steps: - - uses: benc-uk/workflow-dispatch@v1.2.2 - with: - workflow: build.yml - repo: xemu-project/xemu-website - token: ${{ secrets.XEMU_WEBSITE_TOKEN }} - ref: master diff --git a/block.c b/block.c index a18f052374..dbc28ccc30 100644 --- a/block.c +++ b/block.c @@ -2782,6 +2782,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 a77440d639..b69de11096 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 @@ -345,6 +346,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; @@ -400,9 +404,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 | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, overlapped, NULL); +#endif g_free(wfilename); if (s->hfile == INVALID_HANDLE_VALUE) { int err = GetLastError(); diff --git a/build.sh b/build.sh index 10d40de3bc..196ae9c52e 100755 --- a/build.sh +++ b/build.sh @@ -77,6 +77,9 @@ package_macos() { cp Info.plist dist/xemu.app/Contents/ + plutil -replace CFBundleShortVersionString -string $(cat ${project_source_dir}/XEMU_VERSION | cut -f1 -d-) dist/xemu.app/Contents/Info.plist + plutil -replace CFBundleVersion -string $(cat ${project_source_dir}/XEMU_VERSION | cut -f1 -d-) dist/xemu.app/Contents/Info.plist + codesign --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --sign - "${exe_path}" python3 ./scripts/gen-license.py --version-file=macos-libs/$target_arch/INSTALLED > dist/LICENSE.txt } @@ -170,6 +173,32 @@ else opts="--enable-lto" fi +most_recent_macosx_sdk_ver () { + local min_ver="${1}" + local macos_sdk_base=/Library/Developer/CommandLineTools/SDKs + local sdks=("${macos_sdk_base}"/MacOSX[0-9]*.[0-9]*.sdk) + for i in "${!sdks[@]}"; do + local newval="${sdks[i]##${macos_sdk_base}/MacOSX}" + sdks[$i]="${newval%%.sdk}" + done + + IFS=$'\n' sdks=($(sort -nr <<<"${sdks[*]}")) + unset IFS + + local newest_sdk_ver="${sdks[0]}" + + local sdk_path="${macos_sdk_base}/MacOSX${newest_sdk_ver}.sdk" + if ! test -d "${sdk_path}"; then + echo "" + return + fi + + if ! LC_ALL=C awk 'BEGIN {exit ('${newest_sdk_ver}' < '${min_ver}')}'; then + echo "" + return + fi + echo "${sdk_path}" +} case "$platform" in # Adjust compilation options based on platform Linux) @@ -180,47 +209,21 @@ case "$platform" in # Adjust compilation options based on platform ;; Darwin) echo "Compiling for MacOS for $target_arch..." - sdk_base=/Library/Developer/CommandLineTools/SDKs/ - sdk_macos_10_14="${sdk_base}/MacOSX10.14.sdk" - sdk_macos_10_15="${sdk_base}/MacOSX10.15.sdk" - sdk_macos_11_1="${sdk_base}/MacOSX11.1.sdk" - sdk_macos_11_3="${sdk_base}/MacOSX11.3.sdk" - sdk_macos_12_0="${sdk_base}/MacOSX12.0.sdk" - sdk_macos_12_1="${sdk_base}/MacOSX12.1.sdk" if [ "$target_arch" == "arm64" ]; then macos_min_ver=11.3 - if test -d "$sdk_macos_12_1"; then - sdk="$sdk_macos_12_1" - elif test -d "$sdk_macos_12_0"; then - sdk="$sdk_macos_12_0" - elif test -d "$sdk_macos_11_3"; then - sdk="$sdk_macos_11_3" - else - echo "SDK not found. Install Xcode Command Line Tools" - exit 1 - fi elif [ "$target_arch" == "x86_64" ]; then macos_min_ver=10.13 - if test -d "$sdk_macos_12_1"; then - sdk="$sdk_macos_12_1" - elif test -d "$sdk_macos_12_0"; then - sdk="$sdk_macos_12_0" - elif test -d "$sdk_macos_11_3"; then - sdk="$sdk_macos_11_3" - elif test -d "$sdk_macos_11_1"; then - sdk="$sdk_macos_11_1" - elif test -d "$sdk_macos_10_15"; then - sdk="$sdk_macos_10_15" - elif test -d "$sdk_macos_10_14"; then - sdk="$sdk_macos_10_14" - else - echo "SDK not found. Install Xcode Command Line Tools" - exit 1 - fi else echo "Unsupported arch $target_arch" exit 1 fi + + sdk="$(most_recent_macosx_sdk_ver ${macos_min_ver})" + if [[ -z "${sdk}" ]]; then + echo "SDK >= ${macos_min_ver} not found. Install Xcode Command Line Tools" + exit 1 + fi + python3 ./scripts/download-macos-libs.py ${target_arch} lib_prefix=${PWD}/macos-libs/${target_arch}/opt/local export CFLAGS="-arch ${target_arch} \ diff --git a/config_spec.yml b/config_spec.yml index 5511363ca3..8410264e61 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: @@ -108,6 +115,7 @@ display: default: 1 window: fullscreen_on_startup: bool + fullscreen_exclusive: bool startup_size: type: enum values: [last_used, 640x480, 1280x720, 1280x800, 1280x960, 1920x1080, 2560x1440, 2560x1600, 2560x1920, 3840x2160] @@ -130,8 +138,12 @@ display: default: true fit: type: enum - values: [center, scale, scale_16_9, scale_4_3, stretch] + values: [center, scale, stretch] default: scale + aspect_ratio: + type: enum + values: [native, auto, 4x3, 16x9] + default: auto scale: type: integer default: 1 diff --git a/hw/xbox/acpi_xbox.c b/hw/xbox/acpi_xbox.c index 39b1dc5e87..3ff0e16f54 100644 --- a/hw/xbox/acpi_xbox.c +++ b/hw/xbox/acpi_xbox.c @@ -30,6 +30,7 @@ #include "hw/xbox/xbox_pci.h" #include "hw/xbox/acpi_xbox.h" #include "migration/vmstate.h" +#include "ui/xemu-widescreen.h" // #define DEBUG #ifdef DEBUG @@ -44,6 +45,8 @@ #define XBOX_PM_GPIO_BASE 0xC0 #define XBOX_PM_GPIO_LEN 26 +#define XBOX_PM_GPIO_ASPECT_RATIO 0x16 + static int field_pin; static uint64_t xbox_pm_gpio_read(void *opaque, hwaddr addr, unsigned width) @@ -66,6 +69,12 @@ static void xbox_pm_gpio_write(void *opaque, hwaddr addr, uint64_t val, unsigned width) { XBOX_DPRINTF("pm gpio write [0x%llx] = 0x%llx\n", addr, val); + + if (addr == XBOX_PM_GPIO_ASPECT_RATIO) { + xemu_set_widescreen(val == 5); + } + + // FIXME: Add GPIO to VM state } static const MemoryRegionOps xbox_pm_gpio_ops = { diff --git a/hw/xbox/nv2a/nv2a.h b/hw/xbox/nv2a/nv2a.h index 9e3b0206f6..35b63749e4 100644 --- a/hw/xbox/nv2a/nv2a.h +++ b/hw/xbox/nv2a/nv2a.h @@ -27,5 +27,6 @@ int nv2a_get_framebuffer_surface(void); void nv2a_set_surface_scale_factor(unsigned int scale); unsigned int nv2a_get_surface_scale_factor(void); const uint8_t *nv2a_get_dac_palette(void); +int nv2a_get_screen_off(void); #endif diff --git a/hw/xbox/nv2a/nv2a_regs.h b/hw/xbox/nv2a/nv2a_regs.h index 12ffad87f9..108db8f716 100644 --- a/hw/xbox/nv2a/nv2a_regs.h +++ b/hw/xbox/nv2a/nv2a_regs.h @@ -335,6 +335,8 @@ # define NV_PGRAPH_CHEOPS_OFFSET_PROG_LD_PTR 0x000000FF # define NV_PGRAPH_CHEOPS_OFFSET_CONST_LD_PTR 0x0000FF00 #define NV_PGRAPH_DMA_STATE 0x00001034 +#define NV_PGRAPH_ANTIALIASING 0x00001800 +# define NV_PGRAPH_ANTIALIASING_ENABLE (1 << 0) #define NV_PGRAPH_BLEND 0x00001804 # define NV_PGRAPH_BLEND_EQN 0x00000007 # define NV_PGRAPH_BLEND_EN (1 << 3) @@ -1220,6 +1222,8 @@ # define NV097_SET_ZMIN_MAX_CONTROL_ZCLAMP_EN 0x000000F0 # define NV097_SET_ZMIN_MAX_CONTROL_ZCLAMP_EN_CULL 0 # define NV097_SET_ZMIN_MAX_CONTROL_ZCLAMP_EN_CLAMP 1 +# define NV097_SET_ANTI_ALIASING_CONTROL 0x00001D7C +# define NV097_SET_ANTI_ALIASING_CONTROL_ENABLE (1 << 0) # define NV097_SET_ZSTENCIL_CLEAR_VALUE 0x00001D8C # define NV097_SET_COLOR_CLEAR_VALUE 0x00001D90 # define NV097_CLEAR_SURFACE 0x00001D94 diff --git a/hw/xbox/nv2a/pfb.c b/hw/xbox/nv2a/pfb.c index ef2d7528af..147c468510 100644 --- a/hw/xbox/nv2a/pfb.c +++ b/hw/xbox/nv2a/pfb.c @@ -27,10 +27,6 @@ uint64_t pfb_read(void *opaque, hwaddr addr, unsigned int size) uint64_t r = 0; switch (addr) { - case NV_PFB_CFG0: - /* 3-4 memory partitions. The debug bios checks this. */ - r = 3; - break; case NV_PFB_CSTATUS: r = memory_region_size(d->vram); break; diff --git a/hw/xbox/nv2a/pgraph.c b/hw/xbox/nv2a/pgraph.c index 6132465377..f40fcaf689 100644 --- a/hw/xbox/nv2a/pgraph.c +++ b/hw/xbox/nv2a/pgraph.c @@ -2841,12 +2841,25 @@ DEF_METHOD(NV097, SET_BEGIN_END) bool depth_test = control_0 & NV_PGRAPH_CONTROL_0_ZENABLE; bool stencil_test = pg->regs[NV_PGRAPH_CONTROL_1] & NV_PGRAPH_CONTROL_1_STENCIL_TEST_ENABLE; + bool is_nop_draw = !(color_write || depth_test || stencil_test); if (parameter == NV097_SET_BEGIN_END_OP_END) { if (pg->primitive_mode == PRIM_TYPE_INVALID) { NV2A_DPRINTF("End without Begin!\n"); } nv2a_profile_inc_counter(NV2A_PROF_BEGIN_ENDS); + + if (is_nop_draw) { + // FIXME: Check PGRAPH register 0x880. + // HW uses bit 11 in 0x880 to enable or disable a color/zeta limit + // check that will raise an exception in the case that a draw should + // modify the color and/or zeta buffer but the target(s) are masked + // off. This check only seems to trigger during the fragment + // processing, it is legal to attempt a draw that is entirely + // clipped regardless of 0x880. See xemu#635 for context. + return; + } + pgraph_flush_draw(d); /* End of visibility testing */ @@ -2878,14 +2891,7 @@ DEF_METHOD(NV097, SET_BEGIN_END) pgraph_update_surface(d, true, true, depth_test || stencil_test); pgraph_reset_inline_buffers(pg); - if (!(color_write || depth_test || stencil_test)) { - // FIXME: Check PGRAPH register 0x880. - // HW uses bit 11 in 0x880 to enable or disable a color/zeta limit - // check that will raise an exception in the case that a draw should - // modify the color and/or zeta buffer but the target(s) are masked - // off. This check only seems to trigger during the fragment - // processing, it is legal to attempt a draw that is entirely - // clipped regardless of 0x880. See xemu#635 for context. + if (is_nop_draw) { return; } @@ -3042,15 +3048,17 @@ DEF_METHOD(NV097, SET_BEGIN_END) glEnable(GL_PROGRAM_POINT_SIZE); + bool anti_aliasing = GET_MASK(pg->regs[NV_PGRAPH_ANTIALIASING], NV_PGRAPH_ANTIALIASING_ENABLE); + /* Edge Antialiasing */ - if (pg->regs[NV_PGRAPH_SETUPRASTER] & - NV_PGRAPH_SETUPRASTER_LINESMOOTHENABLE) { + if (!anti_aliasing && pg->regs[NV_PGRAPH_SETUPRASTER] & + NV_PGRAPH_SETUPRASTER_LINESMOOTHENABLE) { glEnable(GL_LINE_SMOOTH); } else { glDisable(GL_LINE_SMOOTH); } - if (pg->regs[NV_PGRAPH_SETUPRASTER] & - NV_PGRAPH_SETUPRASTER_POLYSMOOTHENABLE) { + if (!anti_aliasing && pg->regs[NV_PGRAPH_SETUPRASTER] & + NV_PGRAPH_SETUPRASTER_POLYSMOOTHENABLE) { glEnable(GL_POLYGON_SMOOTH); } else { glDisable(GL_POLYGON_SMOOTH); @@ -3451,6 +3459,13 @@ DEF_METHOD(NV097, SET_ZMIN_MAX_CONTROL) } } +DEF_METHOD(NV097, SET_ANTI_ALIASING_CONTROL) +{ + SET_MASK(pg->regs[NV_PGRAPH_ANTIALIASING], NV_PGRAPH_ANTIALIASING_ENABLE, + GET_MASK(parameter, NV097_SET_ANTI_ALIASING_CONTROL_ENABLE)); + // FIXME: Handle the remaining bits (observed values 0xFFFF0000, 0xFFFF0001) +} + DEF_METHOD(NV097, SET_ZSTENCIL_CLEAR_VALUE) { pg->regs[NV_PGRAPH_ZSTENCILCLEARVALUE] = parameter; @@ -5335,6 +5350,11 @@ const uint8_t *nv2a_get_dac_palette(void) return g_nv2a->puserdac.palette; } +int nv2a_get_screen_off(void) +{ + return g_nv2a->vga.sr[VGA_SEQ_CLOCK_MODE] & VGA_SR01_SCREEN_OFF; +} + int nv2a_get_framebuffer_surface(void) { NV2AState *d = g_nv2a; diff --git a/hw/xbox/nv2a/pgraph_methods.h b/hw/xbox/nv2a/pgraph_methods.h index b3de47959d..380df07ea5 100644 --- a/hw/xbox/nv2a/pgraph_methods.h +++ b/hw/xbox/nv2a/pgraph_methods.h @@ -169,6 +169,7 @@ DEF_METHOD_RANGE(NV097, SET_VERTEX_DATA4S_M, 32) DEF_METHOD(NV097, SET_SEMAPHORE_OFFSET) DEF_METHOD(NV097, BACK_END_WRITE_SEMAPHORE_RELEASE) DEF_METHOD(NV097, SET_ZMIN_MAX_CONTROL) +DEF_METHOD(NV097, SET_ANTI_ALIASING_CONTROL) DEF_METHOD(NV097, SET_ZSTENCIL_CLEAR_VALUE) DEF_METHOD(NV097, SET_COLOR_CLEAR_VALUE) DEF_METHOD(NV097, CLEAR_SURFACE) diff --git a/hw/xbox/nv2a/vsh.c b/hw/xbox/nv2a/vsh.c index 87318419c9..5f338e7368 100644 --- a/hw/xbox/nv2a/vsh.c +++ b/hw/xbox/nv2a/vsh.c @@ -636,7 +636,16 @@ static const char* vsh_header = "#define MUL(dest, mask, src0, src1) dest.mask = _MUL(_in(src0), _in(src1)).mask\n" "vec4 _MUL(vec4 src0, vec4 src1)\n" "{\n" - " return src0 * src1;\n" + // Unfortunately mix() falls victim to the same handling of exceptional + // (inf/NaN) handling as a multiply, so per-component comparisons are used + // to guarantee HW behavior (anything * 0 must == 0). + " vec4 zero_components = sign(src0) * sign(src1);\n" + " vec4 ret = src0 * src1;\n" + " if (zero_components.x == 0.0) { ret.x = 0.0; }\n" + " if (zero_components.y == 0.0) { ret.y = 0.0; }\n" + " if (zero_components.z == 0.0) { ret.z = 0.0; }\n" + " if (zero_components.w == 0.0) { ret.w = 0.0; }\n" + " return ret;\n" "}\n" "\n" "#define ADD(dest, mask, src0, src1) dest.mask = _ADD(_in(src0), _in(src1)).mask\n" @@ -648,7 +657,7 @@ static const char* vsh_header = "#define MAD(dest, mask, src0, src1, src2) dest.mask = _MAD(_in(src0), _in(src1), _in(src2)).mask\n" "vec4 _MAD(vec4 src0, vec4 src1, vec4 src2)\n" "{\n" - " return src0 * src1 + src2;\n" + " return _MUL(src0, src1) + src2;\n" "}\n" "\n" "#define DP3(dest, mask, src0, src1) dest.mask = _DP3(_in(src0), _in(src1)).mask\n" diff --git a/hw/xbox/smbus_xbox_smc.c b/hw/xbox/smbus_xbox_smc.c index cc09ad3bda..c7cf02e3e5 100644 --- a/hw/xbox/smbus_xbox_smc.c +++ b/hw/xbox/smbus_xbox_smc.c @@ -78,6 +78,8 @@ #define SMC_REG_BOARDTEMP 0x0a #define SMC_REG_TRAYEJECT 0x0c #define SMC_REG_INTACK 0x0d +#define SMC_REG_ERROR_WRITE 0x0e +#define SMC_REG_ERROR_READ 0x0f #define SMC_REG_INTSTATUS 0x11 #define SMC_REG_INTSTATUS_POWER 0x01 #define SMC_REG_INTSTATUS_TRAYCLOSED 0x02 @@ -102,6 +104,7 @@ typedef struct SMBusSMCDevice { uint8_t avpack_reg; uint8_t intstatus_reg; uint8_t scratch_reg; + uint8_t error_reg; } SMBusSMCDevice; static void smc_quick_cmd(SMBusDevice *dev, uint8_t read) @@ -137,6 +140,10 @@ static int smc_write_data(SMBusDevice *dev, uint8_t *buf, uint8_t len) } break; + case SMC_REG_ERROR_WRITE: + smc->error_reg = buf[0]; + break; + case SMC_REG_SCRATCH: smc->scratch_reg = buf[0]; break; @@ -177,6 +184,9 @@ static uint8_t smc_receive_byte(SMBusDevice *dev) case SMC_REG_AVPACK: return smc->avpack_reg; + case SMC_REG_ERROR_READ: + return smc->error_reg; + case SMC_REG_INTSTATUS: { uint8_t r = smc->intstatus_reg; smc->intstatus_reg = 0; // FIXME: Confirm clear on read @@ -252,6 +262,7 @@ static void smbus_smc_realize(DeviceState *dev, Error **errp) smc->intstatus_reg = 0; smc->scratch_reg = 0; smc->cmd = 0; + smc->error_reg = 0; if (object_property_get_bool(qdev_get_machine(), "short-animation", NULL)) { smc->scratch_reg = SMC_REG_SCRATCH_SHORT_ANIMATION; diff --git a/include/block/block-common.h b/include/block/block-common.h index 297704c1e9..72528c0ba9 100644 --- a/include/block/block-common.h +++ b/include/block/block-common.h @@ -154,6 +154,10 @@ typedef enum { 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 a0cdb714f7..1146bcaf30 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 */ @@ -1537,6 +1539,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(); @@ -2698,6 +2703,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; @@ -3095,6 +3106,10 @@ bool delete_snapshot(const char *name, bool has_devices, return false; } +#ifdef XBOX + xemu_snapshots_mark_dirty(); +#endif + return true; } diff --git a/ubuntu-win64-cross/Dockerfile b/ubuntu-win64-cross/Dockerfile index b579248930..bd26ee8170 100644 --- a/ubuntu-win64-cross/Dockerfile +++ b/ubuntu-win64-cross/Dockerfile @@ -71,6 +71,15 @@ RUN V=1 MXE_VERBOSE=1 make -C /opt/mxe \ MXE_PLUGIN_DIRS=plugins/gcc10 \ sdl2 +COPY libslirp.mk /opt/mxe/src/libslirp.mk +COPY libslirp-1.patch /opt/mxe/src/libslirp-1.patch +RUN V=1 MXE_VERBOSE=1 make -C /opt/mxe \ + MXE_TARGETS=x86_64-w64-mingw32.static \ + MXE_PLUGIN_DIRS=plugins/gcc10 \ + libslirp + +RUN find /opt/mxe/usr -executable -type f -exec chmod a+x {} \; + ENV CROSSPREFIX=x86_64-w64-mingw32.static- ENV CROSSAR=${CROSSPREFIX}gcc-ar ENV PATH="/opt/mxe/.ccache/bin:/opt/mxe/usr/x86_64-pc-linux-gnu/bin:/opt/mxe/usr/bin:${PATH}" diff --git a/ubuntu-win64-cross/libslirp-1.patch b/ubuntu-win64-cross/libslirp-1.patch new file mode 100644 index 0000000000..eb4b6a328c --- /dev/null +++ b/ubuntu-win64-cross/libslirp-1.patch @@ -0,0 +1,34 @@ +From d1417aed0b335d03c67c998b17b72933764ccbe3 Mon Sep 17 00:00:00 2001 +From: Matt Borgerson +Date: Mon, 23 Jan 2023 03:06:46 -0700 +Subject: [PATCH] Rename pingtest's slirp_inet_aton + +--- + test/pingtest.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/test/pingtest.c b/test/pingtest.c +index 3bb0488..04b63ce 100644 +--- a/test/pingtest.c ++++ b/test/pingtest.c +@@ -24,7 +24,7 @@ + #ifdef _WIN32 + //#include + #include +-int slirp_inet_aton(const char *cp, struct in_addr *ia) ++int pingtest_inet_aton(const char *cp, struct in_addr *ia) + { + uint32_t addr = inet_addr(cp); + if (addr == 0xffffffff) { +@@ -33,7 +33,7 @@ int slirp_inet_aton(const char *cp, struct in_addr *ia) + ia->s_addr = addr; + return 1; + } +-#define inet_aton slirp_inet_aton ++#define inet_aton pingtest_inet_aton + #else + #include + #endif +-- +2.34.1 + diff --git a/ubuntu-win64-cross/libslirp.mk b/ubuntu-win64-cross/libslirp.mk new file mode 100644 index 0000000000..71449d14c6 --- /dev/null +++ b/ubuntu-win64-cross/libslirp.mk @@ -0,0 +1,19 @@ +# This file is part of MXE. See LICENSE.md for licensing information. + +PKG := libslirp +$(PKG)_WEBSITE := https://gitlab.freedesktop.org/slirp/libslirp +$(PKG)_IGNORE := +$(PKG)_VERSION := 4.7.0 +$(PKG)_SUBDIR := libslirp-v$($(PKG)_VERSION) +$(PKG)_FILE := libslirp-v$($(PKG)_VERSION).tar.gz +$(PKG)_CHECKSUM := 9398f0ec5a581d4e1cd6856b88ae83927e458d643788c3391a39e61b75db3d3b +$(PKG)_URL := https://gitlab.freedesktop.org/slirp/$(PKG)/-/archive/v$($(PKG)_VERSION)/$($(PKG)_FILE) +$(PKG)_DEPS := cc glib meson-wrapper + +define $(PKG)_BUILD + '$(MXE_MESON_WRAPPER)' $(MXE_MESON_OPTS) \ + $(PKG_MESON_OPTS) \ + --buildtype=plain \ + '$(BUILD_DIR)' '$(SOURCE_DIR)' + '$(MXE_NINJA)' -C '$(BUILD_DIR)' -j '$(JOBS)' install +endef diff --git a/ubuntu-win64-cross/sdl2.mk b/ubuntu-win64-cross/sdl2.mk index 793986def3..4ed53822b8 100644 --- a/ubuntu-win64-cross/sdl2.mk +++ b/ubuntu-win64-cross/sdl2.mk @@ -6,8 +6,9 @@ $(PKG)_DESCR := SDL2 $(PKG)_IGNORE := $(PKG)_VERSION := 2.26.2 $(PKG)_SUBDIR := SDL2-$($(PKG)_VERSION) -$(PKG)_URL := https://github.com/libsdl-org/SDL/releases/download/release-$($(PKG)_VERSION)/SDL2-$($(PKG)_VERSION).tar.gz +$(PKG)_FILE := SDL2-$($(PKG)_VERSION).tar.gz $(PKG)_CHECKSUM := 95d39bc3de037fbdfa722623737340648de4f180a601b0afad27645d150b99e0 +$(PKG)_URL := https://github.com/libsdl-org/SDL/releases/download/release-$($(PKG)_VERSION)/$($(PKG)_FILE) $(PKG)_GH_CONF := libsdl-org/SDL/releases/tag,release-,, $(PKG)_DEPS := cc libiconv libsamplerate diff --git a/ui/meson.build b/ui/meson.build index edd2fa71d1..d09be2dbc0 100644 --- a/ui/meson.build +++ b/ui/meson.build @@ -28,6 +28,9 @@ xemu_ss.add(files( 'xemu.c', 'xemu-data.c', + 'xemu-snapshots.c', + 'xemu-thumbnail.cc', + 'xemu-widescreen.c', )) subdir('xui') diff --git a/ui/thirdparty/meson.build b/ui/thirdparty/meson.build index a50dd1cb3c..213e26edb3 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-os-utils-windows.c b/ui/xemu-os-utils-windows.c index 8fc323746e..58c763e4af 100644 --- a/ui/xemu-os-utils-windows.c +++ b/ui/xemu-os-utils-windows.c @@ -19,13 +19,49 @@ #include "xemu-os-utils.h" #include +#include +#include +#include + +static const char *get_windows_build_info(void) +{ + WCHAR current_build[1024], product_name[1024]; + WCHAR build_size = 1024, product_size = 1024; + + if (RegGetValueW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + L"ProductName", RRF_RT_REG_SZ, (LPVOID)NULL, &product_name, + (LPDWORD)&product_size) != ERROR_SUCCESS) { + return "Windows"; + } + + if ((RegGetValueW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + L"DisplayVersion", RRF_RT_REG_SZ, (LPVOID)NULL, + ¤t_build, (LPDWORD)&build_size) == ERROR_SUCCESS) || + (RegGetValueW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + L"CSDVersion", RRF_RT_REG_SZ, (LPVOID)NULL, + ¤t_build, (LPDWORD)&build_size) == ERROR_SUCCESS)) { + return g_strdup_printf("%ls %ls", product_name, current_build); + } + + return g_strdup_printf("%ls", product_name); +} const char *xemu_get_os_info(void) { - return "Windows"; + static const char *buffer = NULL; + + if (buffer == NULL) { + buffer = get_windows_build_info(); + } + + return buffer; } + void xemu_open_web_browser(const char *url) { - ShellExecute(0, "open", url, 0, 0 , SW_SHOW); + ShellExecute(0, "open", url, 0, 0, SW_SHOW); } 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..5a695257c1 --- /dev/null +++ b/ui/xemu-snapshots.c @@ -0,0 +1,341 @@ +/* + * 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 "qapi/qapi-commands-block.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 void xemu_snapshots_load_data(BlockDriverState *bs_ro, + QEMUSnapshotInfo *info, + XemuSnapshotData *data, Error **err) +{ + data->disc_path = NULL; + data->xbe_title_name = NULL; + data->gl_thumbnail = 0; + + int res = bdrv_snapshot_load_tmp(bs_ro, info->id_str, info->name, err); + if (res < 0) { + return; + } + + uint32_t header[3]; + int64_t offset = 0; + res = bdrv_load_vmstate(bs_ro, (uint8_t *)&header, offset, sizeof(header)); + if (res != sizeof(header)) { + return; + } + offset += res; + + if (be32_to_cpu(header[0]) != XEMU_SNAPSHOT_DATA_MAGIC || + be32_to_cpu(header[1]) != XEMU_SNAPSHOT_DATA_VERSION) { + return; + } + + size_t size = be32_to_cpu(header[2]); + uint8_t *buf = g_malloc(size); + res = bdrv_load_vmstate(bs_ro, buf, offset, size); + if (res != size) { + g_free(buf); + return; + } + + assert(size >= 9); + + offset = 0; + + const size_t disc_path_size = be32_to_cpu(*(uint32_t *)&buf[offset]); + offset += 4; + + if (disc_path_size) { + data->disc_path = (char *)g_malloc(disc_path_size + 1); + assert(size >= (offset + disc_path_size)); + memcpy(data->disc_path, &buf[offset], disc_path_size); + data->disc_path[disc_path_size] = 0; + offset += disc_path_size; + } + + assert(size >= (offset + 4)); + const size_t xbe_title_name_size = buf[offset]; + offset += 1; + + if (xbe_title_name_size) { + data->xbe_title_name = (char *)g_malloc(xbe_title_name_size + 1); + assert(size >= (offset + xbe_title_name_size)); + memcpy(data->xbe_title_name, &buf[offset], xbe_title_name_size); + data->xbe_title_name[xbe_title_name_size] = 0; + offset += xbe_title_name_size; + } + + const size_t thumbnail_size = be32_to_cpu(*(uint32_t *)&buf[offset]); + offset += 4; + + if (thumbnail_size) { + GLuint thumbnail; + glGenTextures(1, &thumbnail); + assert(size >= (offset + thumbnail_size)); + if (xemu_snapshots_load_png_to_texture(thumbnail, &buf[offset], + thumbnail_size)) { + data->gl_thumbnail = thumbnail; + } else { + glDeleteTextures(1, &thumbnail); + } + offset += thumbnail_size; + } + + g_free(buf); +} + +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) { + g_free((*data)[i].xbe_title_name); + if ((*data)[i].gl_thumbnail) { + 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; +} + +char *xemu_get_currently_loaded_disc_path(void) +{ + char *file = NULL; + BlockInfoList *block_list, *info; + + block_list = qmp_query_block(NULL); + + for (info = block_list; info; info = info->next) { + if (strcmp("ide0-cd1", info->value->device)) { + continue; + } + + if (info->value->has_inserted) { + BlockDeviceInfo *inserted = info->value->inserted; + if (inserted->has_node_name) { + file = g_strdup(inserted->file); + } + } + } + + qapi_free_BlockInfoList(block_list); + return file; +} + +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) +{ + char *path = xemu_get_currently_loaded_disc_path(); + size_t path_size = path ? strlen(path) : 0; + + size_t xbe_title_name_size = 0; + char *xbe_title_name = NULL; + struct xbe *xbe_data = xemu_get_xbe_info(); + if (xbe_data && xbe_data->cert) { + glong items_written = 0; + xbe_title_name = g_utf16_to_utf8(xbe_data->cert->m_title_name, 40, NULL, &items_written, NULL); + if (xbe_title_name) { + xbe_title_name_size = items_written; + } + } + + size_t thumbnail_size = 0; + void *thumbnail_buf = xemu_snapshots_create_framebuffer_thumbnail_png(&thumbnail_size); + + qemu_put_be32(f, XEMU_SNAPSHOT_DATA_MAGIC); + qemu_put_be32(f, XEMU_SNAPSHOT_DATA_VERSION); + qemu_put_be32(f, 4 + path_size + 1 + xbe_title_name_size + 4 + thumbnail_size); + + qemu_put_be32(f, path_size); + if (path_size) { + qemu_put_buffer(f, (const uint8_t *)path, path_size); + g_free(path); + } + + qemu_put_byte(f, xbe_title_name_size); + if (xbe_title_name_size) { + qemu_put_buffer(f, (const uint8_t *)xbe_title_name, xbe_title_name_size); + g_free(xbe_title_name); + } + + qemu_put_be32(f, thumbnail_size); + if (thumbnail_size) { + qemu_put_buffer(f, (const uint8_t *)thumbnail_buf, thumbnail_size); + g_free(thumbnail_buf); + } + + xemu_snapshots_dirty = true; +} + +bool xemu_snapshots_offset_extra_data(QEMUFile *f) +{ + unsigned int v; + uint32_t version; + uint32_t size; + + v = qemu_get_be32(f); + if (v != XEMU_SNAPSHOT_DATA_MAGIC) { + qemu_file_skip(f, -4); + return true; + } + + version = qemu_get_be32(f); + (void)version; + + /* qemu_file_skip only works if you aren't skipping past internal buffer limit. + * Unfortunately, it's not usable here. + */ + size = qemu_get_be32(f); + void *buf = g_malloc(size); + qemu_get_buffer(f, buf, size); + g_free(buf); + + 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..a9809117a2 --- /dev/null +++ b/ui/xemu-snapshots.h @@ -0,0 +1,66 @@ +/* + * 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" +#include + +#define XEMU_SNAPSHOT_DATA_MAGIC 0x78656d75 // 'xemu' +#define XEMU_SNAPSHOT_DATA_VERSION 1 + +#define XEMU_SNAPSHOT_THUMBNAIL_WIDTH 160 +#define XEMU_SNAPSHOT_THUMBNAIL_HEIGHT 120 + +extern const char **g_snapshot_shortcut_index_key_map[]; + +typedef struct XemuSnapshotData { + char *disc_path; + char *xbe_title_name; + GLuint gl_thumbnail; +} XemuSnapshotData; + +// Implemented in xemu-snapshots.c +char *xemu_get_currently_loaded_disc_path(void); +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(GLuint tex, bool flip); +bool xemu_snapshots_load_png_to_texture(GLuint tex, void *buf, size_t size); +void *xemu_snapshots_create_framebuffer_thumbnail_png(size_t *size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/xemu-thumbnail.cc b/ui/xemu-thumbnail.cc new file mode 100644 index 0000000000..592408393c --- /dev/null +++ b/ui/xemu-thumbnail.cc @@ -0,0 +1,82 @@ +/* + * 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; +} + +bool xemu_snapshots_load_png_to_texture(GLuint tex, void *buf, size_t size) +{ + std::vector pixels; + unsigned int width, height, channels; + if (fpng::fpng_decode_memory(buf, size, pixels, width, height, channels, + 3) != fpng::FPNG_DECODE_SUCCESS) { + return false; + } + + 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()); + + return true; +} + +void *xemu_snapshots_create_framebuffer_thumbnail_png(size_t *size) +{ + /* + * 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; + } + + std::vector png; + if (!RenderFramebufferToPng(display_tex, display_flip, png, + 2 * XEMU_SNAPSHOT_THUMBNAIL_WIDTH, + 2 * XEMU_SNAPSHOT_THUMBNAIL_HEIGHT)) { + return NULL; + } + + void *buf = g_malloc(png.size()); + memcpy(buf, png.data(), png.size()); + *size = png.size(); + return buf; +} diff --git a/ui/xemu-widescreen.c b/ui/xemu-widescreen.c new file mode 100644 index 0000000000..bde81a3853 --- /dev/null +++ b/ui/xemu-widescreen.c @@ -0,0 +1,37 @@ +/* + * xemu wide screen handler + * + * Copyright (c) 2023 Matt Borgerson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "xemu-widescreen.h" + +static bool g_widescreen = false; + +void xemu_set_widescreen(bool widescreen) +{ + g_widescreen = widescreen; +} + +bool xemu_get_widescreen(void) +{ + return g_widescreen; +} diff --git a/ui/xemu-widescreen.h b/ui/xemu-widescreen.h new file mode 100644 index 0000000000..02da430ef9 --- /dev/null +++ b/ui/xemu-widescreen.h @@ -0,0 +1,40 @@ +/* + * xemu wide screen handler + * + * Copyright (c) 2023 Matt Borgerson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef XEMU_WIDESCREEN +#define XEMU_WIDESCREEN + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void xemu_set_widescreen(bool widescreen); +bool xemu_get_widescreen(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/xemu.c b/ui/xemu.c index 037ee17a9c..b06e19eb1c 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" @@ -54,6 +55,7 @@ #include "hw/xbox/smbus.h" // For eject, drive tray #include "hw/xbox/nv2a/nv2a.h" +#include "ui/xemu-notifications.h" #include @@ -382,7 +384,9 @@ static void set_full_screen(struct sdl2_console *scon, bool set) if (gui_fullscreen) { SDL_SetWindowFullscreen(scon->real_window, - SDL_WINDOW_FULLSCREEN_DESKTOP); + (g_config.display.window.fullscreen_exclusive ? + SDL_WINDOW_FULLSCREEN : + SDL_WINDOW_FULLSCREEN_DESKTOP)); gui_saved_grab = gui_grab; sdl_grab_start(scon); } else { @@ -1194,6 +1198,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(); @@ -1543,31 +1548,44 @@ int main(int argc, char **argv) while (1) { sdl2_gl_refresh(&sdl2_console[0].dcl); + assert(glGetError() == GL_NO_ERROR); } // rcu_unregister_thread(); } -void xemu_eject_disc(void) +void xemu_eject_disc(Error **errp) { + Error *error = NULL; + xbox_smc_eject_button(); + xemu_settings_set_string(&g_config.sys.files.dvd_path, ""); // Xbox software may request that the drive open, but do it now anyway - Error *err = NULL; - qmp_eject(true, "ide0-cd1", false, NULL, true, false, &err); + qmp_eject(true, "ide0-cd1", false, NULL, true, false, &error); + if (error) { + error_propagate(errp, error); + } xbox_smc_update_tray_state(); } -void xemu_load_disc(const char *path) +void xemu_load_disc(const char *path, Error **errp) { + Error *error = NULL; + // Ensure an eject sequence is always triggered so Xbox software reloads xbox_smc_eject_button(); + xemu_settings_set_string(&g_config.sys.files.dvd_path, ""); - Error *err = NULL; qmp_blockdev_change_medium(true, "ide0-cd1", false, NULL, path, false, "", false, false, false, 0, - &err); + &error); + if (error) { + error_propagate(errp, error); + } else { + xemu_settings_set_string(&g_config.sys.files.dvd_path, path); + } xbox_smc_update_tray_state(); } diff --git a/ui/xui/actions.cc b/ui/xui/actions.cc index e4d9e49005..dc2c830c50 100644 --- a/ui/xui/actions.cc +++ b/ui/xui/actions.cc @@ -17,17 +17,27 @@ // along with this program. If not, see . // #include "common.hh" +#include "actions.hh" #include "misc.hh" #include "xemu-hud.h" +#include "../xemu-snapshots.h" +#include "../xemu-notifications.h" +#include "snapshot-manager.hh" void ActionEjectDisc(void) { - xemu_settings_set_string(&g_config.sys.files.dvd_path, ""); - xemu_eject_disc(); + Error *err = NULL; + xemu_eject_disc(&err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } } void ActionLoadDisc(void) { + Error *err = NULL; + const char *iso_file_filters = ".iso Files\0*.iso\0All Files\0*.*\0"; const char *new_disc_path = PausedFileOpen(NOC_FILE_DIALOG_OPEN, iso_file_filters, @@ -36,8 +46,12 @@ void ActionLoadDisc(void) /* Cancelled */ return; } - xemu_settings_set_string(&g_config.sys.files.dvd_path, new_disc_path); - xemu_load_disc(new_disc_path); + + xemu_load_disc(new_disc_path, &err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } } void ActionTogglePause(void) @@ -62,4 +76,33 @@ 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 { + ActionLoadSnapshotChecked(snapshot_name); + } + + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } +} + +void ActionLoadSnapshotChecked(const char *name) +{ + g_snapshot_mgr.LoadSnapshotChecked(name); +} diff --git a/ui/xui/actions.hh b/ui/xui/actions.hh index 2ac680af78..08aedd3c9b 100644 --- a/ui/xui/actions.hh +++ b/ui/xui/actions.hh @@ -24,3 +24,5 @@ void ActionTogglePause(); void ActionReset(); void ActionShutdown(); void ActionScreenshot(); +void ActionActivateBoundSnapshot(int slot, bool save); +void ActionLoadSnapshotChecked(const char *name); diff --git a/ui/xui/common.hh b/ui/xui/common.hh index eef45ebabf..118d8b8542 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..4155ff2097 100644 --- a/ui/xui/gl-helpers.cc +++ b/ui/xui/gl-helpers.cc @@ -26,12 +26,15 @@ #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" +#include "ui/xemu-widescreen.h" Fbo *controller_fbo, *logo_fbo; GLuint g_controller_tex, - g_logo_tex; + g_logo_tex, + g_icon_tex; enum class ShaderType { Blit, @@ -155,10 +158,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 +445,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,47 +662,26 @@ void RenderLogo(uint32_t time) glUseProgram(0); } -void RenderFramebuffer(GLint tex, int width, int height, bool flip) +// Scale proportionally to fit in +void ScaleDimensions(int src_width, int src_height, int max_width, int max_height, int *out_width, int *out_height) +{ + float w_ratio = (float)max_width/(float)max_height; + float t_ratio = (float)src_width/(float)src_height; + + if (w_ratio >= t_ratio) { + *out_width = (float)max_width * t_ratio/w_ratio; + *out_height = max_height; + } else { + *out_width = max_width; + *out_height = (float)max_height * w_ratio/t_ratio; + } +} + +void RenderFramebuffer(GLint tex, int width, int height, bool flip, float scale[2]) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); - int tw, th; - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th); - - // Calculate scaling factors - float scale[2]; - if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) { - // Stretch to fit - scale[0] = 1.0; - scale[1] = 1.0; - } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_CENTER) { - // Centered - scale[0] = (float)tw/(float)width; - scale[1] = (float)th/(float)height; - } else { - float t_ratio; - if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) { - // Scale to fit window using a fixed 16:9 aspect ratio - t_ratio = 16.0f/9.0f; - } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) { - t_ratio = 4.0f/3.0f; - } else { - // Scale to fit, preserving framebuffer aspect ratio - t_ratio = (float)tw/(float)th; - } - - float w_ratio = (float)width/(float)height; - if (w_ratio >= t_ratio) { - scale[0] = t_ratio/w_ratio; - scale[1] = 1.0; - } else { - scale[0] = 1.0; - scale[1] = w_ratio/t_ratio; - } - } - DecalShader *s = g_framebuffer_shader; s->flip = flip; glViewport(0, 0, width, height); @@ -717,23 +701,76 @@ void RenderFramebuffer(GLint tex, int width, int height, bool flip) glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT); - glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL); + + if (!nv2a_get_screen_off()) { + glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL); + } } -void SaveScreenshot(GLuint tex, bool flip) +float GetDisplayAspectRatio(int width, int height) +{ + switch (g_config.display.ui.aspect_ratio) { + case CONFIG_DISPLAY_UI_ASPECT_RATIO_NATIVE: + return (float)width/(float)height; + case CONFIG_DISPLAY_UI_ASPECT_RATIO_16X9: + return 16.0f/9.0f; + case CONFIG_DISPLAY_UI_ASPECT_RATIO_4X3: + return 4.0f/3.0f; + case CONFIG_DISPLAY_UI_ASPECT_RATIO_AUTO: + default: + return xemu_get_widescreen() ? 16.0f/9.0f : 4.0f/3.0f; + } +} + +void RenderFramebuffer(GLint tex, int width, int height, bool flip) +{ + int tw, th; + float scale[2]; + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th); + + // Calculate scaling factors + if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) { + // Stretch to fit + scale[0] = 1.0; + scale[1] = 1.0; + } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_CENTER) { + // Centered + float t_ratio = GetDisplayAspectRatio(tw, th); + scale[0] = t_ratio*(float)th/(float)width; + scale[1] = (float)th/(float)height; + } else { + float t_ratio = GetDisplayAspectRatio(tw, th); + float w_ratio = (float)width/(float)height; + if (w_ratio >= t_ratio) { + scale[0] = t_ratio/w_ratio; + scale[1] = 1.0; + } else { + scale[0] = 1.0; + scale[1] = w_ratio/t_ratio; + } + } + + RenderFramebuffer(tex, width, height, flip, scale); +} + +bool RenderFramebufferToPng(GLuint tex, bool flip, std::vector &png, int max_width, int max_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); - 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); - } + width = height * GetDisplayAspectRatio(width, height); + + if (!max_width) max_width = width; + if (!max_height) max_height = height; + ScaleDimensions(width, height, max_width, max_height, &width, &height); std::vector pixels; pixels.resize(width * height * 3); @@ -742,7 +779,8 @@ void SaveScreenshot(GLuint tex, bool flip) fbo.Target(); bool blend = glIsEnabled(GL_BLEND); if (blend) glDisable(GL_BLEND); - RenderFramebuffer(tex, width, height, !flip); + float scale[2] = {1.0, 1.0}; + RenderFramebuffer(tex, width, height, !flip, scale); if (blend) glEnable(GL_BLEND); glPixelStorei(GL_PACK_ROW_LENGTH, width); glPixelStorei(GL_PACK_IMAGE_HEIGHT, height); @@ -750,10 +788,16 @@ 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 (RenderFramebufferToPng(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..7401540816 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); @@ -46,4 +48,7 @@ void RenderController(float frame_x, float frame_y, uint32_t primary_color, 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, float scale[2]); +bool RenderFramebufferToPng(GLuint tex, bool flip, std::vector &png, int max_width = 0, int max_height = 0); void SaveScreenshot(GLuint tex, bool flip); +void ScaleDimensions(int src_width, int src_height, int max_width, int max_height, int *out_width, int *out_height); diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index 19d12f9cc7..2940d10760 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -22,11 +22,14 @@ #include "main-menu.hh" #include "font-manager.hh" #include "input-manager.hh" +#include "snapshot-manager.hh" #include "viewport-manager.hh" #include "xemu-hud.h" #include "misc.hh" #include "gl-helpers.hh" #include "reporting.hh" +#include "qapi/error.h" +#include "actions.hh" #include "../xemu-input.h" #include "../xemu-notifications.h" @@ -337,10 +340,14 @@ void MainMenuDisplayView::Draw() ChevronCombo("Display mode", &g_config.display.ui.fit, "Center\0" "Scale\0" - "Scale (Widescreen 16:9)\0" - "Scale (4:3)\0" "Stretch\0", "Select how the framebuffer should fit or scale into the window"); + ChevronCombo("Aspect ratio", &g_config.display.ui.aspect_ratio, + "Native\0" + "Auto (Default)\0" + "4:3\0" + "16:9\0", + "Select the displayed aspect ratio"); } void MainMenuAudioView::Draw() @@ -639,115 +646,331 @@ void MainMenuNetworkView::DrawUdpOptions(bool appearing) ImGui::PopFont(); } -#if 0 -class MainMenuSnapshotsView : public virtual MainMenuTabView +MainMenuSnapshotsView::MainMenuSnapshotsView() : MainMenuTabView() { -protected: - GLuint screenshot; + xemu_snapshots_mark_dirty(); -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); + m_search_regex = NULL; + m_current_title_id = 0; +} + +MainMenuSnapshotsView::~MainMenuSnapshotsView() +{ + g_free(m_search_regex); +} + +bool MainMenuSnapshotsView::BigSnapshotButton(QEMUSnapshotInfo *snapshot, XemuSnapshotData *data, int current_snapshot_binding) +{ + ImGuiStyle &style = ImGui::GetStyle(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + + ImGui::PushFont(g_font_mgr.m_menu_font_small); + ImVec2 ts_sub = ImGui::CalcTextSize(snapshot->name); + 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_menu_font_medium); + + ImVec2 ts_title = ImGui::CalcTextSize(snapshot->name); + ImVec2 thumbnail_size = g_viewport_mgr.Scale(ImVec2(XEMU_SNAPSHOT_THUMBNAIL_WIDTH, XEMU_SNAPSHOT_THUMBNAIL_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 title_pos(name_pos.x, name_pos.y + ts_title.y + style.FramePadding.x); + ImVec2 date_pos(name_pos.x, title_pos.y + ts_title.y + style.FramePadding.x); + ImVec2 binding_pos(name_pos.x, date_pos.y + ts_title.y + style.FramePadding.x); + ImVec2 button_size(-FLT_MIN, fmax(thumbnail_size.y + style.FramePadding.y * 2, ts_title.y + ts_sub.y + style.FramePadding.y * 3)); + + bool load = ImGui::Button("###button", button_size); + + ImGui::PopFont(); + + const ImVec2 p0 = ImGui::GetItemRectMin(); + const ImVec2 p1 = ImGui::GetItemRectMax(); + draw_list->PushClipRect(p0, p1, true); + + // Snapshot thumbnail + GLuint thumbnail = data->gl_thumbnail ? data->gl_thumbnail : g_icon_tex; + int thumbnail_width, thumbnail_height; + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, thumbnail); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &thumbnail_width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &thumbnail_height); + + // Draw black background behind thumbnail + ImVec2 thumbnail_min(p0.x + thumbnail_pos.x, p0.y + thumbnail_pos.y); + ImVec2 thumbnail_max(thumbnail_min.x + thumbnail_size.x, thumbnail_min.y + thumbnail_size.y); + draw_list->AddRectFilled(thumbnail_min, thumbnail_max, IM_COL32_BLACK); + + // Draw centered thumbnail image + int scaled_width, scaled_height; + ScaleDimensions(thumbnail_width, thumbnail_height, thumbnail_size.x, thumbnail_size.y, &scaled_width, &scaled_height); + ImVec2 img_min = ImVec2(thumbnail_min.x + (thumbnail_size.x - scaled_width) / 2, + thumbnail_min.y + (thumbnail_size.y - scaled_height) / 2); + ImVec2 img_max = ImVec2(img_min.x + scaled_width, img_min.y + scaled_height); + draw_list->AddImage((ImTextureID)(uint64_t)thumbnail, img_min, img_max); + + // 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 XBE title name + ImGui::PushFont(g_font_mgr.m_menu_font_small); + const char *title_name = data->xbe_title_name ? data->xbe_title_name : "(Unknown XBE Title Name)"; + draw_list->AddText(ImVec2(p0.x + title_pos.x, p0.y + title_pos.y), IM_COL32(255, 255, 255, 200), title_name); + + // 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"); + draw_list->AddText(ImVec2(p0.x + date_pos.x, p0.y + date_pos.y), IM_COL32(255, 255, 255, 200), date_buf); + g_free(date_buf); + + // Snapshot keyboard binding + 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); + } + + ImGui::PopFont(); + draw_list->PopClipRect(); + ImGui::PopStyleVar(2); + + return load; +} + +void MainMenuSnapshotsView::ClearSearch() +{ + m_search_buf.clear(); + + if (m_search_regex) { + g_free(m_search_regex); + m_search_regex = NULL; + } +} + +int MainMenuSnapshotsView::OnSearchTextUpdate(ImGuiInputTextCallbackData *data) +{ + GError *gerr = NULL; + MainMenuSnapshotsView *win = (MainMenuSnapshotsView*)data->UserData; + + if (win->m_search_regex) { + g_free(win->m_search_regex); + win->m_search_regex = NULL; + } + + if (data->BufTextLen == 0) { + 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() +{ + g_snapshot_mgr.Refresh(); + + SectionTitle("Snapshots"); + Toggle("Filter by current title", &g_config.general.snapshots.filter_current_game, + "Only display snapshots created while running the currently running XBE"); + + if (g_config.general.snapshots.filter_current_game) { + struct xbe *xbe = xemu_get_xbe_info(); + if (xbe && xbe->cert) { + if (xbe->cert->m_titleid != m_current_title_id) { + char *title_name = g_utf16_to_utf8(xbe->cert->m_title_name, 40, NULL, NULL, NULL); + if (title_name) { + m_current_title_name = title_name; + g_free(title_name); + } else { + m_current_title_name.clear(); + } + + m_current_title_id = xbe->cert->m_titleid; + } + } else { + m_current_title_name.clear(); + m_current_title_id = 0; } } - void snapshotBigButton(const char *name, const char *title_name, GLuint screenshot) - { - ImGuiStyle &style = ImGui::GetStyle(); - ImVec2 pos = ImGui::GetCursorPos(); - ImDrawList *draw_list = ImGui::GetWindowDrawList(); + ImGui::SetNextItemWidth(ImGui::GetColumnWidth() * 0.8); + ImGui::PushFont(g_font_mgr.m_menu_font_small); + ImGui::InputTextWithHint("##search", "Search or name new snapshot...", + &m_search_buf, ImGuiInputTextFlags_CallbackEdit, + &OnSearchTextUpdate, this); - 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_menuFontSmall); - ImVec2 ts_sub = ImGui::CalcTextSize(name); - 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); - - 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::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; - - // 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)); - - draw_list->PushClipRect(p0, p1, true); - - // 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(); + bool snapshot_with_create_name_exists = false; + for (int i = 0; i < g_snapshot_mgr.m_snapshots_len; ++i) { + if (g_strcmp0(m_search_buf.c_str(), g_snapshot_mgr.m_snapshots[i].name) == 0) { + snapshot_with_create_name_exists = true; + break; } } -}; -#endif + + ImGui::SameLine(); + if (snapshot_with_create_name_exists) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8, 0, 0, 1)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1, 0, 0, 1)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1, 0, 0, 1)); + } + if (ImGui::Button(snapshot_with_create_name_exists ? "Replace" : "Create", ImVec2(-FLT_MIN, 0))) { + xemu_snapshots_save(m_search_buf.empty() ? NULL : m_search_buf.c_str(), NULL); + ClearSearch(); + } + if (snapshot_with_create_name_exists) { + ImGui::PopStyleColor(3); + } + + 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_search_buf.c_str()); + } + ImGui::PopFont(); + + bool at_least_one_snapshot_displayed = false; + + for (int i = g_snapshot_mgr.m_snapshots_len - 1; i >= 0; i--) { + if (g_config.general.snapshots.filter_current_game && g_snapshot_mgr.m_extra_data[i].xbe_title_name && + m_current_title_name.size() && strcmp(m_current_title_name.c_str(), g_snapshot_mgr.m_extra_data[i].xbe_title_name)) { + continue; + } + + if (m_search_regex) { + GMatchInfo *match; + bool keep_entry = false; + + g_regex_match(m_search_regex, g_snapshot_mgr.m_snapshots[i].name, (GRegexMatchFlags)0, &match); + keep_entry |= g_match_info_matches(match); + g_match_info_free(match); + + if (g_snapshot_mgr.m_extra_data[i].xbe_title_name) { + g_regex_match(m_search_regex, g_snapshot_mgr.m_extra_data[i].xbe_title_name, (GRegexMatchFlags)0, &match); + keep_entry |= g_match_info_matches(match); + g_free(match); + } + + if (!keep_entry) { + continue; + } + } + + QEMUSnapshotInfo *snapshot = &g_snapshot_mgr.m_snapshots[i]; + XemuSnapshotData *data = &g_snapshot_mgr.m_extra_data[i]; + + 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; + } + } + + ImGui::PushID(i); + + ImVec2 pos = ImGui::GetCursorScreenPos(); + bool load = BigSnapshotButton(snapshot, data, current_snapshot_binding); + + // FIXME: Provide context menu control annotation + if (ImGui::IsItemHovered() && ImGui::IsKeyPressed(ImGuiKey_GamepadFaceLeft)) { + ImGui::SetNextWindowPos(pos); + ImGui::OpenPopup("Snapshot Options"); + } + + DrawSnapshotContextMenu(snapshot, data, current_snapshot_binding); + + ImGui::PopID(); + + if (load) { + ActionLoadSnapshotChecked(snapshot->name); + } + + at_least_one_snapshot_displayed = true; + } + + if (!at_least_one_snapshot_displayed) { + ImGui::Dummy(g_viewport_mgr.Scale(ImVec2(0, 16))); + const char *msg; + if (g_snapshot_mgr.m_snapshots_len) { + if (!m_search_buf.empty()) { + msg = "Press Create to create new snapshot"; + } else { + msg = "No snapshots match filter criteria"; + } + } else { + msg = "No snapshots to display"; + } + ImVec2 dim = ImGui::CalcTextSize(msg); + ImVec2 cur = ImGui::GetCursorPos(); + ImGui::SetCursorPosX(cur.x + (ImGui::GetColumnWidth()-dim.x)/2); + ImGui::TextColored(ImVec4(0.94f, 0.94f, 0.94f, 0.70f), "%s", msg); + } +} + +void MainMenuSnapshotsView::DrawSnapshotContextMenu(QEMUSnapshotInfo *snapshot, XemuSnapshotData *data, int current_snapshot_binding) +{ + if (!ImGui::BeginPopupContextItem("Snapshot Options")) { + return; + } + + if (ImGui::MenuItem("Load")) { + ActionLoadSnapshotChecked(snapshot->name); + } + + if (ImGui::BeginMenu("Keybinding")) { + 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) { + if (ImGui::MenuItem("Unbind")) { + xemu_settings_set_string(g_snapshot_shortcut_index_key_map[current_snapshot_binding], ""); + current_snapshot_binding = -1; + } + } + ImGui::EndMenu(); + } + + ImGui::Separator(); + + Error *err = NULL; + + if (ImGui::MenuItem("Replace")) { + xemu_snapshots_save(snapshot->name, &err); + } + + if (ImGui::MenuItem("Delete")) { + xemu_snapshots_delete(snapshot->name, &err); + } + + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } + + ImGui::EndPopup(); +} MainMenuSystemView::MainMenuSystemView() : m_dirty(false) { @@ -929,7 +1152,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 +1163,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 +1172,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 +1200,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..7be564701c 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,10 +103,22 @@ public: class MainMenuSnapshotsView : public virtual MainMenuTabView { +protected: + GRegex *m_search_regex; + uint32_t m_current_title_id; + std::string m_current_title_name; + std::string m_search_buf; + + void ClearSearch(); + void DrawSnapshotContextMenu(QEMUSnapshotInfo *snapshot, XemuSnapshotData *data, int current_snapshot_binding); + bool BigSnapshotButton(QEMUSnapshotInfo *snapshot, XemuSnapshotData *data, int current_snapshot_binding); + static int OnSearchTextUpdate(ImGuiInputTextCallbackData *data); + public: - void SnapshotBigButton(const char *name, const char *title_name, - GLuint screenshot); + MainMenuSnapshotsView(); + ~MainMenuSnapshotsView(); void Draw() override; + }; class MainMenuSystemView : public virtual MainMenuTabView @@ -153,7 +166,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 +175,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 +187,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..4680dad29b 100644 --- a/ui/xui/main.cc +++ b/ui/xui/main.cc @@ -31,11 +31,13 @@ #include #include +#include "actions.hh" #include "common.hh" #include "xemu-hud.h" #include "misc.hh" #include "gl-helpers.hh" #include "input-manager.hh" +#include "snapshot-manager.hh" #include "viewport-manager.hh" #include "font-manager.hh" #include "scene.hh" @@ -53,6 +55,8 @@ #endif bool g_screenshot_pending; +const char *g_snapshot_pending_load_name; + float g_main_menu_height; static ImGuiStyle g_base_style; @@ -277,6 +281,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(); @@ -289,6 +301,7 @@ void xemu_hud_render(void) #endif g_scene_mgr.Draw(); if (!first_boot_window.is_open) notification_manager.Draw(); + g_snapshot_mgr.Draw(); // static bool show_demo = true; // if (show_demo) ImGui::ShowDemoWindow(&show_demo); diff --git a/ui/xui/menubar.cc b/ui/xui/menubar.cc index 0540631d10..9ce8a4a0c7 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(); @@ -147,11 +191,12 @@ void ShowMainMenu() } ImGui::Combo("Display Mode", &g_config.display.ui.fit, - "Center\0Scale\0Scale (Widescreen 16:9)\0Scale " - "(4:3)\0Stretch\0"); + "Center\0Scale\0Stretch\0"); ImGui::SameLine(); HelpMarker("Controls how the rendered content should be scaled " "into the window"); + ImGui::Combo("Aspect Ratio", &g_config.display.ui.aspect_ratio, + "Native\0Auto\0""4:3\0""16:9\0"); if (ImGui::MenuItem("Fullscreen", SHORTCUT_MENU_TEXT(Alt + F), xemu_is_fullscreen(), true)) { xemu_toggle_fullscreen(); diff --git a/ui/xui/meson.build b/ui/xui/meson.build index 088e68c764..2fe5b2c64a 100644 --- a/ui/xui/meson.build +++ b/ui/xui/meson.build @@ -16,6 +16,7 @@ xemu_ss.add(files( 'scene-components.cc', 'scene-manager.cc', 'scene.cc', + 'snapshot-manager.cc', 'viewport-manager.cc', 'welcome.cc', 'widgets.cc', diff --git a/ui/xui/popup-menu.cc b/ui/xui/popup-menu.cc index 6cad92e2a1..dcaf5e9b94 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) {} @@ -255,7 +258,7 @@ public: bool DrawItems(PopupMenuItemDelegate &nav) override { const char *values[] = { - "Center", "Scale", "Scale (Widescreen 16:9)", "Scale (4:3)", "Stretch" + "Center", "Scale", "Stretch" }; for (int i = 0; i < CONFIG_DISPLAY_UI_FIT__COUNT; i++) { @@ -269,11 +272,34 @@ public: } }; -extern Scene g_main_menu; +class AspectRatioPopupMenu : public virtual PopupMenu { +public: + bool DrawItems(PopupMenuItemDelegate &nav) override + { + const char *values[] = { + "Native", + "Auto (Default)", + "4:3", + "16:9" + }; + + for (int i = 0; i < CONFIG_DISPLAY_UI_ASPECT_RATIO__COUNT; i++) { + bool selected = g_config.display.ui.aspect_ratio == i; + if (m_focus && selected) ImGui::SetKeyboardFocusHere(); + if (PopupMenuCheck(values[i], "", selected)) + g_config.display.ui.aspect_ratio = i; + } + + return false; + } +}; + +extern MainMenuScene g_main_menu; class SettingsPopupMenu : public virtual PopupMenu { protected: DisplayModePopupMenu display_mode; + AspectRatioPopupMenu aspect_ratio; public: bool DrawItems(PopupMenuItemDelegate &nav) override @@ -292,6 +318,15 @@ public: nav.PushFocus(); nav.PushMenu(display_mode); } + if (PopupMenuSubmenuButton("Aspect Ratio", ICON_FA_EXPAND)) { + nav.PushFocus(); + nav.PushMenu(aspect_ratio); + } + 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 +373,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; diff --git a/ui/xui/snapshot-manager.cc b/ui/xui/snapshot-manager.cc new file mode 100644 index 0000000000..5362e3f00f --- /dev/null +++ b/ui/xui/snapshot-manager.cc @@ -0,0 +1,163 @@ +// +// 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 "common.hh" +#include "notifications.hh" +#include "snapshot-manager.hh" +#include "xemu-hud.h" + +SnapshotManager g_snapshot_mgr; + +SnapshotManager::SnapshotManager() +{ + m_snapshots = NULL; + m_extra_data = NULL; + m_load_failed = false; + m_open_pending = false; + m_snapshots_len = 0; +} + +SnapshotManager::~SnapshotManager() +{ + g_free(m_snapshots); + g_free(m_extra_data); + xemu_snapshots_mark_dirty(); +} + +void SnapshotManager::Refresh() +{ + Error *err = NULL; + + 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; + } +} + +void SnapshotManager::LoadSnapshotChecked(const char *name) +{ + Refresh(); + + XemuSnapshotData *data = NULL; + for (int i = 0; i < m_snapshots_len; i++) { + if (!strcmp(m_snapshots[i].name, name)) { + data = &m_extra_data[i]; + break; + } + } + + if (data == NULL) { + return; + } + + char *current_disc_path = xemu_get_currently_loaded_disc_path(); + if (data->disc_path && (!current_disc_path || strcmp(current_disc_path, data->disc_path))) { + if (current_disc_path) { + m_current_disc_path = current_disc_path; + } else { + m_current_disc_path.clear(); + } + m_target_disc_path = data->disc_path; + m_pending_load_name = name; + m_open_pending = true; + } else { + if (!data->disc_path) { + xemu_eject_disc(NULL); + } + LoadSnapshot(name); + } + + if (current_disc_path) { + g_free(current_disc_path); + } +} + +void SnapshotManager::LoadSnapshot(const char *name) +{ + Error *err = NULL; + + xemu_snapshots_load(name, &err); + + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } +} + +void SnapshotManager::Draw() +{ + DrawSnapshotDiscLoadDialog(); +} + +void SnapshotManager::DrawSnapshotDiscLoadDialog() +{ + if (m_open_pending) { + ImGui::OpenPopup("DVD Drive Image"); + m_open_pending = false; + } + + if (!ImGui::BeginPopupModal("DVD Drive Image", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + return; + } + + ImGui::Text("The DVD drive disc image mounted when the snapshot was created does not appear to be loaded:"); + ImGui::Spacing(); + ImGui::Indent(); + ImGui::Text("Current Image: %s", m_current_disc_path.length() ? m_current_disc_path.c_str() : "(None)"); + ImGui::Text("Expected Image: %s", m_target_disc_path.length() ? m_target_disc_path.c_str() : "(None)"); + ImGui::Unindent(); + ImGui::Spacing(); + ImGui::Text("Would you like to load it now?"); + + ImGui::Dummy(ImVec2(0,16)); + + if (ImGui::Button("Yes", ImVec2(120, 0))) { + xemu_eject_disc(NULL); + + Error *err = NULL; + xemu_load_disc(m_target_disc_path.c_str(), &err); + if (err) { + xemu_queue_error_message(error_get_pretty(err)); + error_free(err); + } else { + LoadSnapshot(m_pending_load_name.c_str()); + } + + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(120, 0))) { + LoadSnapshot(m_pending_load_name.c_str()); + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); +} diff --git a/ui/xui/snapshot-manager.hh b/ui/xui/snapshot-manager.hh new file mode 100644 index 0000000000..6966d85f85 --- /dev/null +++ b/ui/xui/snapshot-manager.hh @@ -0,0 +1,47 @@ +// +// 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 . +// + +#pragma once +#include +#include "../xemu-snapshots.h" + +class SnapshotManager +{ +public: + QEMUSnapshotInfo *m_snapshots; + XemuSnapshotData *m_extra_data; + bool m_load_failed; + bool m_open_pending; + int m_snapshots_len; + + std::string m_pending_load_name; + std::string m_current_disc_path; + std::string m_target_disc_path; + + SnapshotManager(); + ~SnapshotManager(); + void Refresh(); + void LoadSnapshot(const char *name); + void LoadSnapshotChecked(const char *name); + + void Draw(); + void DrawSnapshotDiscLoadDialog(); +}; + +extern SnapshotManager g_snapshot_mgr; diff --git a/ui/xui/widgets.cc b/ui/xui/widgets.cc index cea342f079..34a122ce6c 100644 --- a/ui/xui/widgets.cc +++ b/ui/xui/widgets.cc @@ -306,12 +306,14 @@ bool FilePicker(const char *str_id, const char **buf, const char *filters, ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); ImGuiStyle &style = ImGui::GetStyle(); ImVec2 p = ImGui::GetCursorScreenPos(); + ImVec2 cursor = ImGui::GetCursorPos(); const char *desc = strlen(*buf) ? *buf : "(None Selected)"; ImVec2 bb(ImGui::GetColumnWidth(), GetWidgetTitleDescriptionHeight(str_id, desc)); ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); ImGui::PushID(str_id); bool status = ImGui::Button("###file_button", bb); + ImGui::SetItemAllowOverlap(); if (status) { int flags = NOC_FILE_DIALOG_OPEN; if (dir) flags |= NOC_FILE_DIALOG_DIR; @@ -320,6 +322,7 @@ bool FilePicker(const char *str_id, const char **buf, const char *filters, if (new_path) { free((void*)*buf); *buf = strdup(new_path); + desc = *buf; changed = true; } } @@ -336,9 +339,32 @@ bool FilePicker(const char *str_id, const char **buf, const char *filters, ImGui::PushFont(g_font_mgr.m_menu_font); const char *icon = dir ? ICON_FA_FOLDER : ICON_FA_FILE; ImVec2 ts_icon = ImGui::CalcTextSize(icon); - draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x, - p0.y + (p1.y - p0.y - ts_icon.y) / 2), - ImGui::GetColorU32(ImGuiCol_Text), icon); + ImVec2 icon_pos = ImVec2(p1.x - style.FramePadding.x - ts_icon.x, + p0.y + (p1.y - p0.y - ts_icon.y) / 2); + draw_list->AddText(icon_pos, ImGui::GetColorU32(ImGuiCol_Text), icon); + + ImVec2 ts_clear_icon = ImGui::CalcTextSize(ICON_FA_XMARK); + ts_clear_icon.x += 2 * style.FramePadding.x; + ImVec2 clear_icon_pos = ImVec2(cursor.x + bb.x - ts_icon.x - ts_clear_icon.x, cursor.y); + + auto prev_pos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(clear_icon_pos); + + char *clear_button_id = g_strdup_printf("%s_clear", str_id); + ImGui::PushID(clear_button_id); + + bool clear = ImGui::Button(ICON_FA_XMARK, ImVec2(ts_clear_icon.x, bb.y)); + if (clear) { + free((void*)*buf); + *buf = strdup(""); + changed = true; + } + + ImGui::PopID(); + g_free(clear_button_id); + + ImGui::SetCursorPos(prev_pos); + ImGui::PopFont(); ImGui::PopStyleColor(); diff --git a/ui/xui/xemu-hud.h b/ui/xui/xemu-hud.h index ea8a931131..81e82aff18 100644 --- a/ui/xui/xemu-hud.h +++ b/ui/xui/xemu-hud.h @@ -34,8 +34,8 @@ extern "C" { int xemu_is_fullscreen(void); void xemu_monitor_init(void); void xemu_toggle_fullscreen(void); -void xemu_eject_disc(void); -void xemu_load_disc(const char *path); +void xemu_eject_disc(Error **errp); +void xemu_load_disc(const char *path, Error **errp); // Implemented in xemu_hud.cc void xemu_hud_init(SDL_Window *window, void *sdl_gl_context); diff --git a/xemu.appdata.xml b/xemu.appdata.xml index 0c35f5c290..30d2f4908e 100644 --- a/xemu.appdata.xml +++ b/xemu.appdata.xml @@ -1,32 +1,41 @@ app.xemu.xemu - + xemu Original Xbox Emulator - + CC0-1.0 GPL-2.0-only - + pointing keyboard gamepad - +

A free and open-source application that emulates the original Microsoft Xbox game console. Supports connecting up to 4 controllers for local play, networking for multiplayer, resolution scaling, and more.

+ + https://xemu.app/screenshots/0.png + https://xemu.app/screenshots/1.png + https://xemu.app/screenshots/2.png + https://xemu.app/screenshots/3.png + https://xemu.app/screenshots/4.png + https://xemu.app/screenshots/5.png + + Game Emulator - + https://xemu.app https://github.com/xemu-project/xemu/issues https://xemu.app/docs @@ -37,5 +46,4 @@ xemu -