diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 03bf4ca06..09391757a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: true + submodules: recursive - uses: actions/setup-java@v4 with: diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 0509197c5..cd63b20c0 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -16,10 +16,10 @@ jobs: version: '14.0' pkginstall: sudo pkg install -y alsa-lib ccache cmake evdev-proto git libao libevdev libudev-devd libzip miniupnpc ninja pkgconf pulseaudio sdl2 - operating_system: netbsd - version: '9.3' + version: '10.0' pkginstall: sudo pkgin update && sudo pkgin -y install alsa-lib ccache cmake gcc12 git libao libzip miniupnpc ninja-build pkgconf pulseaudio SDL2 && export PATH=/usr/pkg/gcc12/bin:$PATH - operating_system: openbsd - version: '7.4' + version: '7.5' pkginstall: sudo pkg_add ccache cmake git libao libzip miniupnpc ninja pkgconf pulseaudio sdl2 exclude: - architecture: arm64 @@ -36,7 +36,7 @@ jobs: key: ccache-${{ matrix.operating_system }}-${{ matrix.architecture }}-${{ github.sha }} restore-keys: ccache-${{ matrix.operating_system }}-${{ matrix.architecture }}- - - uses: cross-platform-actions/action@v0.23.0 + - uses: cross-platform-actions/action@v0.24.0 with: operating_system: ${{ matrix.operating_system }} architecture: ${{ matrix.architecture }} diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 80a04d48e..78120d27b 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -18,11 +18,11 @@ jobs: matrix: config: - {name: i686-pc-windows-msvc, os: windows-latest, shell: cmd, arch: x86, cmakeArgs: -G Ninja, buildType: Release} - - {name: apple-darwin, os: macos-latest, shell: sh, cmakeArgs: -G Xcode -DAPPLE_BREAKPAD=ON, destDir: osx, buildType: RelWithDebInfo} + - {name: apple-darwin, os: macos-latest, shell: sh, cmakeArgs: -G Xcode -DAPPLE_BREAKPAD=ON -DUSE_DISCORD=ON, destDir: osx, buildType: RelWithDebInfo} - {name: apple-ios, os: macos-latest, shell: sh, cmakeArgs: -DCMAKE_SYSTEM_NAME=iOS -G Xcode, destDir: ios, buildType: Release} - - {name: x86_64-pc-linux-gnu, os: ubuntu-20.04, shell: sh, cmakeArgs: -G Ninja, destDir: linux, buildType: RelWithDebInfo} - - {name: x86_64-pc-windows-msvc, os: windows-latest, shell: cmd, arch: x64, cmakeArgs: -G Ninja, buildType: Release} - - {name: x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -G Ninja, destDir: win, buildType: RelWithDebInfo} + - {name: x86_64-pc-linux-gnu, os: ubuntu-20.04, shell: sh, cmakeArgs: -G Ninja -DUSE_DISCORD=ON, destDir: linux, buildType: RelWithDebInfo} + - {name: x86_64-pc-windows-msvc, os: windows-latest, shell: cmd, arch: x64, cmakeArgs: -G Ninja -DUSE_DISCORD=ON, buildType: Release} + - {name: x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -G Ninja -DUSE_DISCORD=ON, destDir: win, buildType: RelWithDebInfo} - {name: libretro-x86_64-pc-linux-gnu, os: ubuntu-latest, shell: sh, cmakeArgs: -DLIBRETRO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -G Ninja, buildType: Release} - {name: libretro-x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -DLIBRETRO=ON -G Ninja, buildType: Release} @@ -70,7 +70,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: true + submodules: recursive - name: Compile a universal OpenMP (macOS) run: HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew reinstall --build-from-source --formula ./shell/apple/libomp.rb diff --git a/.gitmodules b/.gitmodules index c4747d15b..876c80ebf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,12 @@ [submodule "core/deps/Spout"] path = core/deps/Spout url = https://github.com/vkedwardli/Spout2.git +[submodule "core/deps/discord-rpc"] + path = core/deps/discord-rpc + url = https://github.com/flyinghead/discord-rpc +[submodule "core/deps/libadrenotools"] + path = core/deps/libadrenotools + url = https://github.com/bylaws/libadrenotools +[submodule "core/deps/rcheevos"] + path = core/deps/rcheevos + url = https://github.com/RetroAchievements/rcheevos.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c620e8cb..928667710 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ option(APPLE_BREAKPAD "macOS: Build breakpad client and dump symbols" OFF) option(ENABLE_GDB_SERVER "Build with GDB debugging support" OFF) option(ENABLE_DC_PROFILER "Build with support for target machine (SH4) profiler" OFF) option(ENABLE_FC_PROFILER "Build with support for host app (Flycast) profiler" OFF) +option(USE_DISCORD "Use Discord Presence API" OFF) if(IOS AND NOT LIBRETRO) set(USE_VULKAN OFF CACHE BOOL "Force vulkan off" FORCE) @@ -534,6 +535,7 @@ if(UNIX AND NOT APPLE AND NOT ANDROID) if(USE_GLES2) target_compile_definitions(${PROJECT_NAME} PRIVATE GLES GLES2) if(USE_VIDEOCORE) + target_compile_definitions(${PROJECT_NAME} PRIVATE TARGET_VIDEOCORE USE_OMX) target_link_libraries(${PROJECT_NAME} PRIVATE "-lbrcmGLESv2") target_link_directories(${PROJECT_NAME} PRIVATE "/opt/vc/lib") endif() @@ -704,12 +706,13 @@ target_sources(${PROJECT_NAME} PRIVATE core/deps/lzma/7zArcIn.c core/deps/lzma/7 add_subdirectory(core/deps/libelf) target_link_libraries(${PROJECT_NAME} PRIVATE elf) if(NOT LIBRETRO) - target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_DEMO_WINDOWS) + target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_DEMO_WINDOWS IMGUI_DEFINE_MATH_OPERATORS) target_include_directories(${PROJECT_NAME} PRIVATE core/deps/imgui core/deps/imgui/backends) target_sources(${PROJECT_NAME} PRIVATE core/deps/imgui/imgui.cpp core/deps/imgui/imgui_demo.cpp core/deps/imgui/imgui_draw.cpp + core/deps/imgui/imgui_stdlib.cpp core/deps/imgui/imgui_tables.cpp core/deps/imgui/imgui_widgets.cpp) @@ -725,6 +728,15 @@ endif() target_sources(${PROJECT_NAME} PRIVATE core/deps/xbrz/xbrz.cpp) target_sources(${PROJECT_NAME} PRIVATE core/deps/md5/md5.cpp) +if(USE_DISCORD AND NOT LIBRETRO) + option(BUILD_EXAMPLES "Build example apps" OFF) + add_subdirectory(core/deps/discord-rpc) + target_include_directories(${PROJECT_NAME} PRIVATE core/deps/discord-rpc/include) + target_link_libraries(${PROJECT_NAME} PRIVATE discord-rpc) + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_DISCORD) + target_sources(${PROJECT_NAME} PRIVATE core/ui/discord.cpp) +endif() + cmrc_add_resource_library(flycast-resources ALIAS flycast::res NAMESPACE flycast) target_link_libraries(${PROJECT_NAME} PRIVATE flycast::res) @@ -1001,7 +1013,10 @@ cmrc_add_resources(flycast-resources fonts/printer_kanji24x24.bin.zip) if(NOT LIBRETRO) - cmrc_add_resources(flycast-resources fonts/Roboto-Medium.ttf.zip) + cmrc_add_resources(flycast-resources + fonts/Roboto-Medium.ttf.zip + fonts/Roboto-Regular.ttf.zip + fonts/fa-solid-900.ttf.zip) if(ANDROID) cmrc_add_resources(flycast-resources WHENCE resources @@ -1048,19 +1063,18 @@ else() core/linux/unwind_info.cpp) if(NINTENDO_SWITCH) target_sources(${PROJECT_NAME} PRIVATE - core/linux/libnx_vmem.cpp + shell/switch/libnx_vmem.cpp shell/switch/stubs.c shell/switch/context_switch.S shell/switch/nswitch.h shell/switch/switch_gamepad.h + shell/switch/switch_main.cpp shell/switch/ucontext.h) endif() endif() if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE - core/linux-dist/dispmanx.cpp - core/linux-dist/dispmanx.h core/linux-dist/evdev.cpp core/linux-dist/evdev.h core/linux-dist/icon.h @@ -1119,6 +1133,8 @@ if(NOT LIBRETRO) core/audio/audiobackend_pulseaudio.cpp core/audio/audiobackend_sdl2.cpp core/audio/audiostream.cpp + core/oslib/http_client.cpp + core/oslib/http_client.h core/oslib/oslib.cpp) endif() @@ -1163,6 +1179,44 @@ target_sources(${PROJECT_NAME} PRIVATE core/reios/reios_elf.h) cmrc_add_resources(flycast-resources fonts/biosfont.bin.zip) +target_sources(${PROJECT_NAME} PRIVATE + core/achievements/achievements.cpp + core/achievements/achievements.h) +if(NOT LIBRETRO) + target_sources(${PROJECT_NAME} PRIVATE + core/deps/rcheevos/src/rc_client_raintegration.c + core/deps/rcheevos/src/rc_client.c + core/deps/rcheevos/src/rc_compat.c + core/deps/rcheevos/src/rc_util.c + core/deps/rcheevos/src/rc_version.c + core/deps/rcheevos/src/rapi/rc_api_common.c + core/deps/rcheevos/src/rapi/rc_api_editor.c + core/deps/rcheevos/src/rapi/rc_api_info.c + core/deps/rcheevos/src/rapi/rc_api_runtime.c + core/deps/rcheevos/src/rapi/rc_api_user.c + core/deps/rcheevos/src/rcheevos/alloc.c + core/deps/rcheevos/src/rcheevos/condition.c + core/deps/rcheevos/src/rcheevos/condset.c + core/deps/rcheevos/src/rcheevos/consoleinfo.c + core/deps/rcheevos/src/rcheevos/format.c + core/deps/rcheevos/src/rcheevos/lboard.c + core/deps/rcheevos/src/rcheevos/memref.c + core/deps/rcheevos/src/rcheevos/operand.c + core/deps/rcheevos/src/rcheevos/rc_validate.c + core/deps/rcheevos/src/rcheevos/richpresence.c + core/deps/rcheevos/src/rcheevos/runtime_progress.c + core/deps/rcheevos/src/rcheevos/runtime.c + core/deps/rcheevos/src/rcheevos/trigger.c + core/deps/rcheevos/src/rcheevos/value.c + core/deps/rcheevos/src/rhash/aes.c + core/deps/rcheevos/src/rhash/cdreader.c + core/deps/rcheevos/src/rhash/hash.c + core/deps/rcheevos/src/rhash/md5.c + core/deps/rcheevos/src/rurl/url.c) + target_include_directories(${PROJECT_NAME} PRIVATE core/deps/rcheevos/include) + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_RACHIEVEMENTS RC_DISABLE_LUA) +endif() + target_sources(${PROJECT_NAME} PRIVATE core/wsi/context.h core/wsi/libretro.cpp @@ -1238,26 +1292,26 @@ target_sources(${PROJECT_NAME} PRIVATE core/rend/norend/norend.cpp) if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE - core/rend/game_scanner.h - core/rend/imgui_driver.h - core/rend/gui.cpp - core/rend/gui.h - core/rend/gui_android.cpp - core/rend/gui_android.h - core/rend/gui_chat.h - core/rend/gui_cheats.cpp - core/rend/gui_util.cpp - core/rend/gui_util.h - core/rend/mainui.cpp - core/rend/mainui.h - core/rend/boxart/boxart.cpp - core/rend/boxart/boxart.h - core/rend/boxart/gamesdb.cpp - core/rend/boxart/gamesdb.h - core/rend/boxart/http_client.cpp - core/rend/boxart/http_client.h - core/rend/boxart/scraper.cpp - core/rend/boxart/scraper.h) + core/ui/game_scanner.cpp + core/ui/game_scanner.h + core/ui/imgui_driver.h + core/ui/gui.cpp + core/ui/gui.h + core/ui/gui_achievements.cpp + core/ui/gui_android.cpp + core/ui/gui_android.h + core/ui/gui_chat.h + core/ui/gui_cheats.cpp + core/ui/gui_util.cpp + core/ui/gui_util.h + core/ui/mainui.cpp + core/ui/mainui.h + core/ui/boxart/boxart.cpp + core/ui/boxart/boxart.h + core/ui/boxart/gamesdb.cpp + core/ui/boxart/gamesdb.h + core/ui/boxart/scraper.cpp + core/ui/boxart/scraper.h) endif() if(USE_VULKAN) @@ -1281,6 +1335,11 @@ if(USE_VULKAN) target_compile_options(VulkanMemoryAllocator INTERFACE $<$,$>:-Wno-nullability-completeness>) target_link_libraries(${PROJECT_NAME} PRIVATE GPUOpen::VulkanMemoryAllocator) + if(ANDROID AND NOT LIBRETRO AND "arm64" IN_LIST ARCHITECTURE) + add_subdirectory(core/deps/libadrenotools) + target_link_libraries(${PROJECT_NAME} PRIVATE adrenotools) + endif() + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_VULKAN HAVE_VULKAN) target_sources(${PROJECT_NAME} PRIVATE core/rend/vulkan/oit/oit_buffer.h @@ -1293,6 +1352,7 @@ if(USE_VULKAN) core/rend/vulkan/oit/oit_renderpass.h core/rend/vulkan/oit/oit_shaders.cpp core/rend/vulkan/oit/oit_shaders.h + core/rend/vulkan/adreno.cpp core/rend/vulkan/buffer.cpp core/rend/vulkan/buffer.h core/rend/vulkan/commandpool.cpp @@ -1496,7 +1556,7 @@ endif() if((USE_OPENGL OR USE_GLES2 OR USE_GLES) AND NOT LIBRETRO) add_library(glad STATIC core/deps/glad/src/gl.c) if(NOT APPLE AND NOT WIN32 AND NOT SDL2_FOUND) - # When SDL2 is not found, we can use EGL with ANativeWindow (Android), DispmanX (Raspberry Pi) or X11 + # When SDL2 is not found, we can use EGL with ANativeWindow (Android) or X11 target_sources(glad PRIVATE core/deps/glad/src/egl.c) endif() target_include_directories(glad PUBLIC core/deps/glad/include) @@ -1560,6 +1620,7 @@ if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE shell/apple/common/http_client.mm + shell/apple/common/util.mm shell/apple/emulator-ios/emulator/AppDelegate.h shell/apple/emulator-ios/emulator/AppDelegate.mm shell/apple/emulator-ios/emulator/ios_main.mm @@ -1655,6 +1716,7 @@ if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE shell/apple/common/http_client.mm + shell/apple/common/util.mm shell/apple/emulator-osx/emulator-osx/SDLApplicationDelegate.h shell/apple/emulator-osx/emulator-osx/SDLApplicationDelegate.mm shell/apple/emulator-osx/emulator-osx/osx-main.mm) @@ -1710,18 +1772,18 @@ if(NOT LIBRETRO) ${CMAKE_CURRENT_BINARY_DIR}/Flycast.app/Contents/Frameworks/libvulkan.dylib) endif() endif() - elseif(UNIX OR NINTENDO_SWITCH) + elseif(UNIX) if(NOT BUILD_TESTING) target_sources(${PROJECT_NAME} PRIVATE core/linux-dist/main.cpp) endif() elseif(WIN32) + target_sources(${PROJECT_NAME} PRIVATE + core/windows/clock.c + core/windows/rawinput.cpp + core/windows/rawinput.h) if(NOT BUILD_TESTING) - target_sources(${PROJECT_NAME} PRIVATE - core/windows/clock.c - core/windows/rawinput.cpp - core/windows/rawinput.h - core/windows/winmain.cpp) + target_sources(${PROJECT_NAME} PRIVATE core/windows/winmain.cpp) endif() if(WINDOWS_STORE) file(READ shell/uwp/Package.appxmanifest MANIFEST) diff --git a/core/achievements/achievements.cpp b/core/achievements/achievements.cpp new file mode 100644 index 000000000..264bdf81a --- /dev/null +++ b/core/achievements/achievements.cpp @@ -0,0 +1,1090 @@ +/* + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +// Derived from duckstation: https://github.com/stenzek/duckstation/blob/master/src/core/achievements.cpp +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) +#include "achievements.h" +#include "serialize.h" +#ifdef USE_RACHIEVEMENTS +#include "oslib/directory.h" +#include "oslib/http_client.h" +#include "hw/sh4/sh4_mem.h" +#include "ui/gui_achievements.h" +#include "imgread/common.h" +#include "cfg/option.h" +#include "oslib/oslib.h" +#include "emulator.h" +#include "stdclass.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace achievements +{ + +class Achievements +{ +public: + Achievements(); + ~Achievements(); + bool init(); + void term(); + std::future login(const char *username, const char *password); + void logout(); + bool isLoggedOn() const { return loggedOn; } + bool isActive() const { return active; } + Game getCurrentGame(); + std::vector getAchievementList(); + void serialize(Serializer& ser); + void deserialize(Deserializer& deser); + + static Achievements& Instance(); + +private: + bool createClient(); + std::string getGameHash(); + void loadGame(); + void gameLoaded(int result, const char *errorMessage); + void unloadGame(); + void pauseGame(); + void resumeGame(); + void loadCache(); + std::string getOrDownloadImage(const char *url); + std::pair getCachedImage(const char *url); + void diskChange(); + + static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata); + static void clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, void *userdata); + void authenticationSuccess(const rc_client_user_t *user); + static void clientMessageCallback(const char *message, const rc_client_t *client); + static u32 clientReadMemory(u32 address, u8 *buffer, u32 num_bytes, rc_client_t *client); + static void clientServerCall(const rc_api_request_t *request, rc_client_server_callback_t callback, + void *callback_data, rc_client_t *client); + static void clientEventHandler(const rc_client_event_t *event, rc_client_t *client); + void handleResetEvent(const rc_client_event_t *event); + void handleUnlockEvent(const rc_client_event_t *event); + void handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event); + void handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event); + void handleGameCompleted(const rc_client_event_t *event); + void handleShowAchievementProgress(const rc_client_event_t *event); + void handleHideAchievementProgress(const rc_client_event_t *event); + void handleUpdateAchievementProgress(const rc_client_event_t *event); + void handleLeaderboardStarted(const rc_client_event_t *event); + void handleLeaderboardFailed(const rc_client_event_t *event); + void handleLeaderboardSubmitted(const rc_client_event_t *event); + void handleShowLeaderboardTracker(const rc_client_event_t *event); + void handleHideLeaderboardTracker(const rc_client_event_t *event); + void handleUpdateLeaderboardTracker(const rc_client_event_t *event); + static void emuEventCallback(Event event, void *arg); + + rc_client_t *rc_client = nullptr; + bool loggedOn = false; + std::atomic_bool loadingGame {}; + bool active = false; + bool paused = false; + std::string lastError; + std::future asyncServerCall; + cResetEvent resetEvent; + std::string cachePath; + std::unordered_map cacheMap; + std::mutex cacheMutex; + std::future asyncImageDownload; +}; + +bool init() { + return Achievements::Instance().init(); +} + +void term() { + Achievements::Instance().term(); +} + +std::future login(const char *username, const char *password) { + return Achievements::Instance().login(username, password); +} + +void logout() { + Achievements::Instance().logout(); +} + +bool isLoggedOn() { + return Achievements::Instance().isLoggedOn(); +} + +bool isActive() { + return Achievements::Instance().isActive(); +} + +Game getCurrentGame() { + return Achievements::Instance().getCurrentGame(); +} + +std::vector getAchievementList() { + return Achievements::Instance().getAchievementList(); +} + +void serialize(Serializer& ser) { + Achievements::Instance().serialize(ser); +} +void deserialize(Deserializer& deser) { + Achievements::Instance().deserialize(deser); +} + +Achievements& Achievements::Instance() { + static Achievements instance; + return instance; +} +// create the instance at start up +OnLoad _([]() { Achievements::Instance(); }); + +Achievements::Achievements() +{ + EventManager::listen(Event::Start, emuEventCallback, this); + EventManager::listen(Event::Terminate, emuEventCallback, this); + EventManager::listen(Event::Pause, emuEventCallback, this); + EventManager::listen(Event::Resume, emuEventCallback, this); + EventManager::listen(Event::DiskChange, emuEventCallback, this); +} + +Achievements::~Achievements() +{ + EventManager::unlisten(Event::Start, emuEventCallback, this); + EventManager::unlisten(Event::Terminate, emuEventCallback, this); + EventManager::unlisten(Event::Pause, emuEventCallback, this); + EventManager::unlisten(Event::Resume, emuEventCallback, this); + EventManager::unlisten(Event::DiskChange, emuEventCallback, this); + term(); +} + +bool Achievements::init() +{ + if (rc_client != nullptr) + return true; + + if (!createClient()) + return false; + + rc_client_set_event_handler(rc_client, clientEventHandler); + rc_client_set_hardcore_enabled(rc_client, 0); + // TODO Expose these settings? + //rc_client_set_encore_mode_enabled(rc_client, 0); + //rc_client_set_unofficial_enabled(rc_client, 0); + //rc_client_set_spectator_mode_enabled(rc_client, 0); + loadCache(); + + if (!config::AchievementsUserName.get().empty() && !config::AchievementsToken.get().empty()) + { + INFO_LOG(COMMON, "RA: Attempting login with user '%s'...", config::AchievementsUserName.get().c_str()); + rc_client_begin_login_with_token(rc_client, config::AchievementsUserName.get().c_str(), + config::AchievementsToken.get().c_str(), clientLoginWithTokenCallback, nullptr); + } + + return true; +} + +bool Achievements::createClient() +{ + http::init(); + rc_client = rc_client_create(clientReadMemory, clientServerCall); + if (rc_client == nullptr) + { + WARN_LOG(COMMON, "Can't create RetroAchievements client"); + return false; + } + +#if !defined(NDEBUG) || defined(DEBUGFAST) + rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, clientMessageCallback); +#else + rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_WARN, clientMessageCallback); +#endif + + rc_client_set_userdata(rc_client, this); + + return true; +} + +void Achievements::loadCache() +{ + cachePath = get_writable_data_path("achievements/"); + flycast::mkdir(cachePath.c_str(), 0755); + DIR *dir = flycast::opendir(cachePath.c_str()); + if (dir != nullptr) + { + while (true) + { + dirent *direntry = flycast::readdir(dir); + if (direntry == nullptr) + break; + std::string name = direntry->d_name; + if (name == "." || name == "..") + continue; + std::string s = get_file_basename(name); + u64 v = strtoull(s.c_str(), nullptr, 16); + std::lock_guard _(cacheMutex); + cacheMap[v] = name; + } + flycast::closedir(dir); + } +} + +static u64 hashUrl(const char *url) { + return XXH64(url, strlen(url), 13); +} + +std::pair Achievements::getCachedImage(const char *url) +{ + u64 hash = hashUrl(url); + std::lock_guard _(cacheMutex); + auto it = cacheMap.find(hash); + if (it != cacheMap.end()) { + return std::make_pair(cachePath + it->second, true); + } + else + { + std::stringstream stream; + stream << std::hex << hash << ".png"; + return std::make_pair(cachePath + stream.str(), false); + } +} + +std::string Achievements::getOrDownloadImage(const char *url) +{ + u64 hash = hashUrl(url); + { + std::lock_guard _(cacheMutex); + auto it = cacheMap.find(hash); + if (it != cacheMap.end()) + return cachePath + it->second; + } + std::vector content; + std::string content_type; + int rc = http::get(url, content, content_type); + if (!http::success(rc)) + return {}; + std::stringstream stream; + stream << std::hex << hash << ".png"; + std::string localPath = cachePath + stream.str(); + FILE *f = nowide::fopen(localPath.c_str(), "wb"); + if (f == nullptr) { + WARN_LOG(COMMON, "Can't save image to %s", localPath.c_str()); + return {}; + } + fwrite(content.data(), 1, content.size(), f); + fclose(f); + { + std::lock_guard _(cacheMutex); + cacheMap[hash] = stream.str(); + } + DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, localPath.c_str()); + return localPath; +} + +void Achievements::term() +{ + if (rc_client == nullptr) + return; + unloadGame(); + if (asyncImageDownload.valid()) + asyncImageDownload.get(); + rc_client_destroy(rc_client); + rc_client = nullptr; +} + +void Achievements::authenticationSuccess(const rc_client_user_t *user) +{ + NOTICE_LOG(COMMON, "RA Login successful: token %s", config::AchievementsToken.get().c_str()); + char url[512]; + int rc = rc_client_user_get_image_url(user, url, sizeof(url)); + if (rc == RC_OK) + { + std::string image = getOrDownloadImage(url); + std::string text = "User " + config::AchievementsUserName.get() + " authenticated"; + notifier.notify(Notification::Login, image, text); + } + loggedOn = true; + if (!settings.content.fileName.empty()) // TODO better test? + loadGame(); +} + +void Achievements::clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, + void *userdata) +{ + Achievements *achievements = (Achievements *)rc_client_get_userdata(client); + if (result != RC_OK) + { + WARN_LOG(COMMON, "RA Login failed: %s", error_message); + notifier.notify(Notification::Login, "", "RetroAchievements authentication failed", error_message); + return; + } + achievements->authenticationSuccess(rc_client_get_user_info(client)); +} + +std::future Achievements::login(const char* username, const char* password) +{ + init(); + std::promise *promise = new std::promise(); + rc_client_begin_login_with_password(rc_client, username, password, clientLoginWithPasswordCallback, promise); + return promise->get_future(); +} + +void Achievements::clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, + void *userdata) +{ + Achievements *achievements = (Achievements *)rc_client_get_userdata(client); + std::promise *promise = (std::promise *)userdata; + if (result != RC_OK) + { + std::string errorMsg = rc_error_str(result); + if (error_message != nullptr) + errorMsg += ": " + std::string(error_message); + promise->set_exception(std::make_exception_ptr(FlycastException(errorMsg))); + delete promise; + WARN_LOG(COMMON, "RA Login failed: %s", errorMsg.c_str()); + return; + } + + const rc_client_user_t* user = rc_client_get_user_info(client); + if (!user || !user->token) + { + WARN_LOG(COMMON, "RA: rc_client_get_user_info() returned NULL"); + promise->set_exception(std::make_exception_ptr(FlycastException("No user token returned"))); + delete promise; + return; + } + + // Store token in config + config::AchievementsToken = user->token; + SaveSettings(); + achievements->authenticationSuccess(user); + promise->set_value(); + delete promise; +} + +void Achievements::logout() +{ + unloadGame(); + rc_client_logout(rc_client); + // Reset token in config + config::AchievementsToken = ""; + SaveSettings(); + loggedOn = false; +} + +void Achievements::clientMessageCallback(const char* message, const rc_client_t* client) +{ +#if !defined(NDEBUG) || defined(DEBUGFAST) + DEBUG_LOG(COMMON, "RA: %s", message); +#else + WARN_LOG(COMMON, "RA error: %s", message); +#endif +} + +u32 Achievements::clientReadMemory(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client) +{ + if (address + num_bytes > RAM_SIZE) + return 0; + address += 0x0C000000; + switch (num_bytes) + { + case 1: + *buffer = ReadMem8_nommu(address); + break; + case 2: + *(u16 *)buffer = ReadMem16_nommu(address); + break; + case 4: + *(u32 *)buffer = ReadMem32_nommu(address); + break; + default: + return 0; + } + return num_bytes; +} + +void Achievements::clientServerCall(const rc_api_request_t *request, rc_client_server_callback_t callback, + void *callback_data, rc_client_t *client) +{ + Achievements *achievements = (Achievements *)rc_client_get_userdata(client); + std::string url {request->url}; + std::string payload; + if (request->post_data != nullptr) + payload = request->post_data; + std::string contentType; + if (request->content_type != nullptr) + contentType = request->content_type; + const auto& callServer = [url, contentType, payload, callback, callback_data]() + { + ThreadName _("Flycast-RA"); + int rc; + std::vector reply; + if (!payload.empty()) + rc = http::post(url, payload.c_str(), contentType.empty() ? nullptr : contentType.c_str(), reply); + else + rc = http::get(url, reply); + rc_api_server_response_t rr; + rr.http_status_code = rc; // TODO RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR if connection fails? + rr.body_length = reply.size(); + rr.body = (const char *)reply.data(); + callback(&rr, callback_data); + }; + if (achievements->asyncServerCall.valid()) + { + if (achievements->asyncServerCall.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) + { + INFO_LOG(COMMON, "Async server call already in progress"); + // process synchronously + callServer(); + return; + } + achievements->asyncServerCall.get(); + } + achievements->asyncServerCall = std::async(std::launch::async, callServer); +} + +static void handleServerError(const rc_client_server_error_t* error) +{ + WARN_LOG(COMMON, "RA server error: %s - %s", error->api, error->error_message); + notifier.notify(Notification::Error, "", error->api, error->error_message); +} + +static void notifyError(const std::string& msg) +{ + WARN_LOG(COMMON, "RA error: %s", msg.c_str()); + notifier.notify(Notification::Error, "", msg); +} + +void Achievements::clientEventHandler(const rc_client_event_t* event, rc_client_t* client) +{ + Achievements *achievements = (Achievements *)rc_client_get_userdata(client); + switch (event->type) + { + case RC_CLIENT_EVENT_RESET: + achievements->handleResetEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + achievements->handleUnlockEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + achievements->handleAchievementChallengeIndicatorShowEvent(event); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + achievements->handleAchievementChallengeIndicatorHideEvent(event); + break; + + case RC_CLIENT_EVENT_GAME_COMPLETED: + achievements->handleGameCompleted(event); + break; + + case RC_CLIENT_EVENT_SERVER_ERROR: + handleServerError(event->server_error); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + achievements->handleShowAchievementProgress(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + achievements->handleHideAchievementProgress(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + achievements->handleUpdateAchievementProgress(event); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + achievements->handleLeaderboardStarted(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + achievements->handleLeaderboardFailed(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + achievements->handleLeaderboardSubmitted(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + achievements->handleShowLeaderboardTracker(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + achievements->handleHideLeaderboardTracker(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + achievements->handleUpdateLeaderboardTracker(event); + break; +// TODO case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: + + case RC_CLIENT_EVENT_DISCONNECTED: + notifyError("RetroAchievements disconnected"); + break; + + case RC_CLIENT_EVENT_RECONNECTED: + notifyError("RetroAchievements reconnected"); + break; + + default: + WARN_LOG(COMMON, "RA: Unhandled event: %u", event->type); + break; + } +} + +void Achievements::handleResetEvent(const rc_client_event_t *event) +{ + // This never seems to be called, probably because hardcore mode is enabled before starting the game. + INFO_LOG(COMMON, "RA: Resetting runtime due to reset event"); + rc_client_reset(rc_client); +} + +void Achievements::handleUnlockEvent(const rc_client_event_t *event) +{ + const rc_client_achievement_t* cheevo = event->achievement; + assert(cheevo != nullptr); + + INFO_LOG(COMMON, "RA: Achievement %s (%u) for game %s unlocked", cheevo->title, cheevo->id, settings.content.title.c_str()); + + char url[512]; + int rc = rc_client_achievement_get_image_url(cheevo, cheevo->state, url, sizeof(url)); + if (rc == RC_OK) + { + std::string image = getOrDownloadImage(url); + std::string text = "Achievement " + std::string(cheevo->title) + " unlocked!"; + notifier.notify(Notification::Login, image, text, cheevo->description); + } +} + +void Achievements::handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event) +{ + INFO_LOG(COMMON, "RA: Challenge: %s", event->achievement->title); + char url[128]; + int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)); + if (rc == RC_OK) + { + std::string image = getOrDownloadImage(url); + notifier.showChallenge(image); + } +} + +void Achievements::handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event) +{ + INFO_LOG(COMMON, "RA: Challenge hidden: %s", event->achievement->title); + char url[128]; + int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)); + if (rc == RC_OK) + { + std::string image = getOrDownloadImage(url); + notifier.hideChallenge(image); + } +} + +void Achievements::handleLeaderboardStarted(const rc_client_event_t *event) +{ + const rc_client_leaderboard_t *leaderboard = event->leaderboard; + INFO_LOG(COMMON, "RA: Leaderboard started: %s", leaderboard->title); + std::string text = "Leaderboard " + std::string(leaderboard->title) + " started"; + notifier.notify(Notification::Unlocked, "", text, leaderboard->description); +} +void Achievements::handleLeaderboardFailed(const rc_client_event_t *event) +{ + const rc_client_leaderboard_t *leaderboard = event->leaderboard; + INFO_LOG(COMMON, "RA: Leaderboard failed: %s", leaderboard->title); + std::string text = "Leaderboard " + std::string(leaderboard->title) + " failed"; + notifier.notify(Notification::Unlocked, "", text, leaderboard->description); +} +void Achievements::handleLeaderboardSubmitted(const rc_client_event_t *event) +{ + const rc_client_leaderboard_t *leaderboard = event->leaderboard; + INFO_LOG(COMMON, "RA: Leaderboard submitted: %s", leaderboard->title); + std::string text = "Leaderboard " + std::string(leaderboard->title) + " submitted"; + notifier.notify(Notification::Unlocked, "", text, leaderboard->description); +} +void Achievements::handleShowLeaderboardTracker(const rc_client_event_t *event) +{ + const rc_client_leaderboard_tracker_t *leaderboard = event->leaderboard_tracker; + DEBUG_LOG(COMMON, "RA: Show leaderboard[%d]: %s", leaderboard->id, leaderboard->display); + notifier.showLeaderboard(leaderboard->id, leaderboard->display); +} +void Achievements::handleHideLeaderboardTracker(const rc_client_event_t *event) +{ + const rc_client_leaderboard_tracker_t *leaderboard = event->leaderboard_tracker; + DEBUG_LOG(COMMON, "RA: Hide leaderboard[%d]: %s", leaderboard->id, leaderboard->display); + notifier.hideLeaderboard(leaderboard->id); +} +void Achievements::handleUpdateLeaderboardTracker(const rc_client_event_t *event) +{ + const rc_client_leaderboard_tracker_t *leaderboard = event->leaderboard_tracker; + DEBUG_LOG(COMMON, "RA: Update leaderboard[%d]: %s", leaderboard->id, leaderboard->display); + notifier.showLeaderboard(leaderboard->id, leaderboard->display); +} + +void Achievements::handleGameCompleted(const rc_client_event_t *event) +{ + const rc_client_game_t* game = rc_client_get_game_info(rc_client); + std::string image; + char url[128]; + if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK) + image = getOrDownloadImage(url); + std::string text1 = (rc_client_get_hardcore_enabled(rc_client) ? "Mastered " : "Completed ") + std::string(game->title); + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(rc_client, &summary); + std::stringstream ss; + ss << summary.num_unlocked_achievements << " achievements, " << summary.points_unlocked << " points"; + std::string text3 = rc_client_get_user_info(rc_client)->display_name; + notifier.notify(Notification::Mastery, image, text1, ss.str(), text3); +} + +void Achievements::handleShowAchievementProgress(const rc_client_event_t *event) +{ + handleUpdateAchievementProgress(event); +} +void Achievements::handleHideAchievementProgress(const rc_client_event_t *event) +{ + notifier.notify(Notification::Progress, "", ""); +} +void Achievements::handleUpdateAchievementProgress(const rc_client_event_t *event) +{ + char url[128]; + std::string image; + if (rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK) + image = getOrDownloadImage(url); + notifier.notify(Notification::Progress, image, event->achievement->measured_progress); +} + +static Disc *hashDisk; +static bool add150; + +static void *cdreader_open_track(const char* path, u32 track) +{ + DEBUG_LOG(COMMON, "RA: cdreader_open_track %s track %d", path, track); + if (track == RC_HASH_CDTRACK_FIRST_DATA) + { + for (const Track& track : hashDisk->tracks) + if (track.isDataTrack()) + return const_cast(&track); + return nullptr; + } + if (track <= hashDisk->tracks.size()) + return const_cast(&hashDisk->tracks[track - 1]); + else + return nullptr; +} + +static size_t cdreader_read_sector(void* track_handle, u32 sector, void* buffer, size_t requested_bytes) +{ + if (requested_bytes == 2048) + // add 150 sectors to FAD corresponding to files + // FIXME get rid of this + add150 = true; + //DEBUG_LOG(COMMON, "RA: cdreader_read_sector track %p sec %d+%d num %zd", track_handle, sector, add150 ? 150 : 0, requested_bytes); + if (add150) + sector += 150; + u8 locbuf[2048]; + hashDisk->ReadSectors(sector, 1, locbuf, 2048); + requested_bytes = std::min(requested_bytes, 2048); + memcpy(buffer, locbuf, requested_bytes); + + return requested_bytes; +} + +static void cdreader_close_track(void* track_handle) +{ +} + +static u32 cdreader_first_track_sector(void* track_handle) +{ + Track& track = *static_cast(track_handle); + DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %p -> %d", track_handle, track.StartFAD); + return track.StartFAD; +} + +std::string Achievements::getGameHash() +{ + if (settings.platform.isConsole()) + { + const u32 diskType = libGDR_GetDiscType(); + if (diskType == NoDisk || diskType == Open) + return {}; + // Reopen the disk locally to avoid threading issues (CHD) + try { + hashDisk = OpenDisc(settings.content.path); + } catch (const FlycastException& e) { + return {}; + } + add150 = false; + rc_hash_cdreader hooks = { + cdreader_open_track, + cdreader_read_sector, + cdreader_close_track, + cdreader_first_track_sector + }; + rc_hash_init_custom_cdreader(&hooks); + rc_hash_init_error_message_callback([](const char *msg) { + WARN_LOG(COMMON, "cdreader: %s", msg); + }); +#if !defined(NDEBUG) || defined(DEBUGFAST) + rc_hash_init_verbose_message_callback([](const char *msg) { + DEBUG_LOG(COMMON, "cdreader: %s", msg); + }); +#endif + } + char hash[33] {}; + rc_hash_generate_from_file(hash, settings.platform.isConsole() ? RC_CONSOLE_DREAMCAST : RC_CONSOLE_ARCADE, + settings.content.fileName.c_str()); // fileName is only used for arcade + delete hashDisk; + hashDisk = nullptr; + + return hash; +} + +void Achievements::pauseGame() +{ + paused = true; + if (active) + { + resetEvent.Reset(); + if (asyncServerCall.valid()) + asyncServerCall.get(); + asyncServerCall = std::async(std::launch::async, [this]() { + while (paused) + { + resetEvent.Wait(1000); + if (paused) + rc_client_idle(rc_client); + } + }); + } +} + +void Achievements::resumeGame() +{ + paused = false; + resetEvent.Set(); + if (asyncServerCall.valid()) + asyncServerCall.get(); + if (config::EnableAchievements) + { + loadGame(); + if (settings.raHardcoreMode && !config::AchievementsHardcoreMode) + { + settings.raHardcoreMode = false; + rc_client_set_hardcore_enabled(rc_client, 0); + } + } + else + term(); +} + +void Achievements::emuEventCallback(Event event, void *arg) +{ + Achievements *instance = ((Achievements *)arg); + switch (event) + { + case Event::Start: + case Event::Resume: + instance->resumeGame(); + break; + case Event::Terminate: + instance->unloadGame(); + break; + case Event::VBlank: + rc_client_do_frame(instance->rc_client); + break; + case Event::Pause: + instance->pauseGame(); + break; + case Event::DiskChange: + instance->diskChange(); + break; + default: + break; + } +} + +void Achievements::loadGame() +{ + if (loadingGame.exchange(true)) + // already loading + return; + if (active) + { + // already loaded + loadingGame = false; + return; + } + if (!init() || !isLoggedOn()) { + if (!isLoggedOn()) + INFO_LOG(COMMON, "Not logged on. Not loading game yet"); + loadingGame = false; + return; + } + std::string gameHash = getGameHash(); + if (!gameHash.empty()) + { + // settings.raHardcoreMode is set before enabling cheats and loading the initial savestate + rc_client_set_hardcore_enabled(rc_client, settings.raHardcoreMode); + rc_client_begin_load_game(rc_client, gameHash.c_str(), [](int result, const char *error_message, rc_client_t *client, void *userdata) { + ((Achievements *)userdata)->gameLoaded(result, error_message); + }, this); + } + else { + INFO_LOG(COMMON, "RA: empty hash. Aborting load"); + loadingGame = false; + settings.raHardcoreMode = false; + } +} + +void Achievements::gameLoaded(int result, const char *errorMessage) +{ + if (result != RC_OK) + { + if (result == RC_NO_GAME_LOADED) + // Unknown game. + INFO_LOG(COMMON, "RA: Unknown game, disabling achievements."); + else if (result == RC_LOGIN_REQUIRED) { + // We would've asked to re-authenticate, so leave HC on for now. + // Once we've done so, we'll reload the game. + } + else + WARN_LOG(COMMON, "RA Loading game failed: %s", errorMessage); + settings.raHardcoreMode = false; + loadingGame = false; + return; + } + const rc_client_game_t* info = rc_client_get_game_info(rc_client); + if (info == nullptr) + { + WARN_LOG(COMMON, "RA: rc_client_get_game_info() returned NULL"); + settings.raHardcoreMode = false; + loadingGame = false; + return; + } + active = true; + loadingGame = false; + EventManager::listen(Event::VBlank, emuEventCallback, this); + NOTICE_LOG(COMMON, "RA: game %d loaded: %s, achievements %d leaderboards %d rich presence %d", info->id, info->title, + rc_client_has_achievements(rc_client), rc_client_has_leaderboards(rc_client), rc_client_has_rich_presence(rc_client)); + if (!rc_client_is_processing_required(rc_client)) + settings.raHardcoreMode = false; + else + settings.raHardcoreMode = (bool)rc_client_get_hardcore_enabled(rc_client); + std::string image; + char url[512]; + if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK) + image = getOrDownloadImage(url); + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(rc_client, &summary); + std::string text; + if (summary.num_core_achievements > 0) + text = "You have " + std::to_string(summary.num_unlocked_achievements) + + " of " + std::to_string(summary.num_core_achievements) + " achievements unlocked."; + else + text = "This game has no achievements."; + std::string text2 = settings.raHardcoreMode ? "Hardcore Mode" : ""; + notifier.notify(Notification::Login, image, info->title, text, text2); +} + +void Achievements::unloadGame() +{ + if (!active) + return; + active = false; + paused = false; + resetEvent.Set(); + if (asyncServerCall.valid()) + asyncServerCall.get(); + EventManager::unlisten(Event::VBlank, emuEventCallback, this); + rc_client_unload_game(rc_client); + settings.raHardcoreMode = false; +} + +void Achievements::diskChange() +{ + if (!active) + return; + std::string hash = getGameHash(); + if (hash == "") { + unloadGame(); + return; + } + rc_client_begin_change_media_from_hash(rc_client, hash.c_str(), [](int result, const char *errorMessage, rc_client_t *client, void *userdata) { + if (result == RC_HARDCORE_DISABLED) { + settings.raHardcoreMode = false; + notifier.notify(Notification::Login, "", "Hardcore mode disabled", "Unrecognized media inserted"); + } + else if (result != RC_OK) + { + settings.raHardcoreMode = false; + if (errorMessage == nullptr) + errorMessage = rc_error_str(result); + notifier.notify(Notification::Login, "", "Media change failed", errorMessage); + } + }, this); +} + +Game Achievements::getCurrentGame() +{ + if (!active) + return Game{}; + const rc_client_game_t *info = rc_client_get_game_info(rc_client); + if (info == nullptr) + return Game{}; + char url[128]; + std::string image; + if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK) + { + bool cached; + std::tie(image, cached) = getCachedImage(url); + if (!cached) + { + if (asyncImageDownload.valid()) + { + if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) + INFO_LOG(COMMON, "Async image download already in progress"); + else + asyncImageDownload.get(); + } + if (!asyncImageDownload.valid()) + { + std::string surl = url; + asyncImageDownload = std::async(std::launch::async, [this, surl]() { + getOrDownloadImage(surl.c_str()); + }); + } + } + } + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(rc_client, &summary); + + return Game{ image, info->title, summary.num_unlocked_achievements, summary.num_core_achievements, summary.points_unlocked, summary.points_core }; +} + +std::vector Achievements::getAchievementList() +{ + std::vector achievements; + rc_client_achievement_list_t *list = rc_client_create_achievement_list(rc_client, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + std::vector uncachedImages; + for (u32 i = 0; i < list->num_buckets; i++) + { + const char *label = list->buckets[i].label; + for (u32 j = 0; j < list->buckets[i].num_achievements; j++) + { + const rc_client_achievement_t *achievement = list->buckets[i].achievements[j]; + char url[128]; + std::string image; + if (rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK) + { + bool cached; + std::tie(image, cached) = getCachedImage(url); + if (!cached) + uncachedImages.push_back(url); + } + std::string status; + if (achievement->measured_percent) + status = achievement->measured_progress; + achievements.emplace_back(image, achievement->title, achievement->description, label, status); + } + } + rc_client_destroy_achievement_list(list); + if (!uncachedImages.empty()) + { + if (asyncImageDownload.valid()) + { + if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) + INFO_LOG(COMMON, "Async image download already in progress"); + else + asyncImageDownload.get(); + } + if (!asyncImageDownload.valid()) + { + asyncImageDownload = std::async(std::launch::async, [this, uncachedImages]() { + for (const std::string& url : uncachedImages) + getOrDownloadImage(url.c_str()); + }); + } + } + + return achievements; +} + +void Achievements::serialize(Serializer& ser) +{ + u32 size = (u32)rc_client_progress_size(rc_client); + if (size > 0) + { + u8 *buffer = new u8[size]; + if (rc_client_serialize_progress(rc_client, buffer) != RC_OK) + size = 0; + ser << size; + ser.serialize(buffer, size); + delete[] buffer; + } + else { + ser << size; + } +} +void Achievements::deserialize(Deserializer& deser) +{ + if (deser.version() < Deserializer::V50) { + rc_client_deserialize_progress(rc_client, nullptr); + } + else { + u32 size; + deser >> size; + if (size > 0) + { + u8 *buffer = new u8[size]; + deser.deserialize(buffer, size); + rc_client_deserialize_progress(rc_client, buffer); + delete[] buffer; + } + else { + rc_client_deserialize_progress(rc_client, nullptr); + } + } +} + +} // namespace achievements + +#else // !USE_RACHIEVEMENTS + +namespace achievements +{ + +// Maintain savestate compatiblity with RA-enabled builds +void serialize(Serializer& ser) +{ + u32 size = 0; + ser << size; +} + +void deserialize(Deserializer& deser) +{ + if (deser.version() >= Deserializer::V50) + { + u32 size; + deser >> size; + deser.skip(size); + } +} + +} +#endif diff --git a/core/achievements/achievements.h b/core/achievements/achievements.h new file mode 100644 index 000000000..f716c87b3 --- /dev/null +++ b/core/achievements/achievements.h @@ -0,0 +1,68 @@ +/* + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#pragma once +#include "types.h" +#include +#include + +namespace achievements +{ +#ifdef USE_RACHIEVEMENTS + +struct Game +{ + std::string image; + std::string title; + u32 unlockedAchievements; + u32 totalAchievements; + u32 points; + u32 totalPoints; +}; + +struct Achievement +{ + Achievement() = default; + Achievement(const std::string& image, const std::string& title, const std::string& description, const std::string& category, const std::string& status) + : image(image), title(title), description(description), category(category), status(status) {} + std::string image; + std::string title; + std::string description; + std::string category; + std::string status; +}; + +bool init(); +void term(); +std::future login(const char *username, const char *password); +void logout(); +bool isLoggedOn(); +bool isActive(); +Game getCurrentGame(); +std::vector getAchievementList(); + +#else + +static inline bool isActive() { + return false; +} + +#endif + +void serialize(Serializer& ser); +void deserialize(Deserializer& deser); + +} diff --git a/core/archive/ZipArchive.cpp b/core/archive/ZipArchive.cpp index cd279191c..ef10710f2 100644 --- a/core/archive/ZipArchive.cpp +++ b/core/archive/ZipArchive.cpp @@ -47,7 +47,7 @@ ArchiveFile* ZipArchive::OpenFile(const char* name) return nullptr; zip_stat_t stat; zip_stat(zip, name, 0, &stat); - return new ZipArchiveFile(zip_file, stat.size); + return new ZipArchiveFile(zip_file, stat.size, stat.name); } static zip_file *zip_fopen_by_crc(zip_t *za, u32 crc, int flags, zip_uint64_t& index) @@ -77,7 +77,7 @@ ArchiveFile* ZipArchive::OpenFileByCrc(u32 crc) zip_stat_t stat; zip_stat_index(zip, index, 0, &stat); - return new ZipArchiveFile(zip_file, stat.size); + return new ZipArchiveFile(zip_file, stat.size, stat.name); } u32 ZipArchiveFile::Read(void* buffer, u32 length) @@ -104,5 +104,15 @@ ArchiveFile *ZipArchive::OpenFirstFile() return nullptr; zip_stat_t stat; zip_stat_index(zip, 0, 0, &stat); - return new ZipArchiveFile(zipFile, stat.size); + return new ZipArchiveFile(zipFile, stat.size, stat.name); +} + +ArchiveFile *ZipArchive::OpenFileByIndex(size_t index) +{ + zip_file_t *zipFile = zip_fopen_index(zip, index, 0); + if (zipFile == nullptr) + return nullptr; + zip_stat_t stat; + zip_stat_index(zip, index, 0, &stat); + return new ZipArchiveFile(zipFile, stat.size, stat.name); } diff --git a/core/archive/ZipArchive.h b/core/archive/ZipArchive.h index 1369a5c48..8b475ab78 100644 --- a/core/archive/ZipArchive.h +++ b/core/archive/ZipArchive.h @@ -31,11 +31,11 @@ public: ArchiveFile* OpenFile(const char* name) override; ArchiveFile* OpenFileByCrc(u32 crc) override; - bool Open(const void *data, size_t size); - ArchiveFile *OpenFirstFile(); - -protected: bool Open(FILE *file) override; + bool Open(const void *data, size_t size); + + ArchiveFile *OpenFirstFile(); + ArchiveFile *OpenFileByIndex(size_t index); private: zip_t *zip = nullptr; @@ -44,8 +44,8 @@ private: class ZipArchiveFile : public ArchiveFile { public: - ZipArchiveFile(zip_file_t *zip_file, size_t length) - : zip_file(zip_file), _length(length) {} + ZipArchiveFile(zip_file_t *zip_file, size_t length, const char *name) + : zip_file(zip_file), _length(length), name(name) {} ~ZipArchiveFile() override { zip_fclose(zip_file); } @@ -53,8 +53,12 @@ public: size_t length() override { return _length; } + const char *getName() override { + return name; + } private: zip_file_t *zip_file; size_t _length; + const char *name; }; diff --git a/core/archive/archive.h b/core/archive/archive.h index 720613f07..49e925e0c 100644 --- a/core/archive/archive.h +++ b/core/archive/archive.h @@ -28,6 +28,7 @@ public: virtual ~ArchiveFile() = default; virtual u32 Read(void *buffer, u32 length) = 0; virtual size_t length() = 0; + virtual const char *getName() { return nullptr; } }; class Archive diff --git a/core/archive/rzip.cpp b/core/archive/rzip.cpp index 43109e21f..9a965e8f5 100644 --- a/core/archive/rzip.cpp +++ b/core/archive/rzip.cpp @@ -23,14 +23,11 @@ const u8 RZipHeader[8] = { '#', 'R', 'Z', 'I', 'P', 'v', 1, '#' }; -bool RZipFile::Open(const std::string& path, bool write) +bool RZipFile::Open(FILE *file, bool write) { - verify(file == nullptr); - this->write = write; - - file = nowide::fopen(path.c_str(), write ? "wb" : "rb"); - if (file == nullptr) - return false; + verify(this->file == nullptr); + verify(file != nullptr); + startOffset = std::ftell(file); if (!write) { u8 header[sizeof(RZipHeader)]; @@ -39,7 +36,7 @@ bool RZipFile::Open(const std::string& path, bool write) || std::fread(&maxChunkSize, sizeof(maxChunkSize), 1, file) != 1 || std::fread(&size, sizeof(size), 1, file) != 1) { - Close(); + std::fseek(file, startOffset, SEEK_SET); return false; } // savestates created on 32-bit platforms used to have a 32-bit size @@ -59,11 +56,24 @@ bool RZipFile::Open(const std::string& path, bool write) || std::fwrite(&maxChunkSize, sizeof(maxChunkSize), 1, file) != 1 || std::fwrite(&size, sizeof(size), 1, file) != 1) { - Close(); + std::fseek(file, startOffset, SEEK_SET); return false; } } + this->write = write; + this->file = file; + return true; +} +bool RZipFile::Open(const std::string& path, bool write) +{ + FILE *f = nowide::fopen(path.c_str(), write ? "wb" : "rb"); + if (f == nullptr) + return false; + if (!Open(f, write)) { + Close(); + return false; + } return true; } @@ -73,7 +83,7 @@ void RZipFile::Close() { if (write) { - std::fseek(file, sizeof(RZipHeader) + sizeof(maxChunkSize), SEEK_SET); + std::fseek(file, startOffset + sizeof(RZipHeader) + sizeof(maxChunkSize), SEEK_SET); std::fwrite(&size, sizeof(size), 1, file); } std::fclose(file); diff --git a/core/archive/rzip.h b/core/archive/rzip.h index 6d2dc287f..0d0edf9a4 100644 --- a/core/archive/rzip.h +++ b/core/archive/rzip.h @@ -28,6 +28,7 @@ public: ~RZipFile() { Close(); } bool Open(const std::string& path, bool write); + bool Open(FILE *file, bool write); void Close(); size_t Size() const { return size; } size_t Read(void *data, size_t length); @@ -42,4 +43,5 @@ private: u32 chunkSize = 0; u32 chunkIndex = 0; bool write = false; + long startOffset = 0; }; diff --git a/core/audio/audiobackend_directsound.cpp b/core/audio/audiobackend_directsound.cpp index ae81884b7..726f785e0 100644 --- a/core/audio/audiobackend_directsound.cpp +++ b/core/audio/audiobackend_directsound.cpp @@ -9,6 +9,7 @@ #include #include "stdclass.h" #include "windows/comptr.h" +#include "oslib/oslib.h" HWND getNativeHwnd(); #define verifyc(x) verify(!FAILED(x)) @@ -46,6 +47,7 @@ class DirectSoundBackend : public AudioBackend void audioThreadMain() { + ThreadName _("FlyDirectSound"); audioThreadRunning = true; while (true) { diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index e7d74779d..b3bd2e5ad 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -108,6 +108,7 @@ Option PerPixelLayers("rend.PerPixelLayers", 32); Option NativeDepthInterpolation("rend.NativeDepthInterpolation", false); Option EmulateFramebuffer("rend.EmulateFramebuffer", false); Option FixUpscaleBleedingEdge("rend.FixUpscaleBleedingEdge", true); +Option CustomGpuDriver("rend.CustomGpuDriver", false); #ifdef VIDEO_ROUTING Option VideoRouting("rend.VideoRouting", false); Option VideoRoutingScale("rend.VideoRoutingScale", false); @@ -130,6 +131,7 @@ Option OpenGlChecks("OpenGlChecks", false, "validate"); Option, false> ContentPath("Dreamcast.ContentPath"); Option HideLegacyNaomiRoms("Dreamcast.HideLegacyNaomiRoms", true); Option UploadCrashLogs("UploadCrashLogs", true); +Option DiscordPresence("DiscordPresence", true); // Profiler Option ProfilerEnabled("Profiler.Enabled"); @@ -157,10 +159,6 @@ Option NetworkOutput("NetworkOutput", false, "network"); Option MultiboardSlaves("MultiboardSlaves", 1, "network"); Option BattleCableEnable("BattleCable", false, "network"); -#ifdef SUPPORT_DISPMANX -Option DispmanxMaintainAspect("maintain_aspect", true, "dispmanx"); -#endif - #ifdef USE_OMX Option OmxAudioLatency("audio_latency", 100, "omx"); Option OmxAudioHdmi("audio_hdmi", true, "omx"); @@ -199,4 +197,11 @@ Option UseRawInput("RawInput", false, "input"); Option LuaFileName("LuaFileName", "flycast.lua"); #endif +// RetroAchievements + +Option EnableAchievements("Enabled", false, "achievements"); +Option AchievementsHardcoreMode("HardcoreMode", false, "achievements"); +OptionString AchievementsUserName("UserName", "", "achievements"); +OptionString AchievementsToken("Token", "", "achievements"); + } // namespace config diff --git a/core/cfg/option.h b/core/cfg/option.h index 4dc8d76c1..c5ecd239b 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -474,6 +474,7 @@ extern Option DupeFrames; extern Option NativeDepthInterpolation; extern Option EmulateFramebuffer; extern Option FixUpscaleBleedingEdge; +extern Option CustomGpuDriver; #ifdef VIDEO_ROUTING extern Option VideoRouting; extern Option VideoRoutingScale; @@ -496,6 +497,7 @@ extern Option OpenGlChecks; extern Option, false> ContentPath; extern Option HideLegacyNaomiRoms; extern Option UploadCrashLogs; +extern Option DiscordPresence; // Profiling extern Option ProfilerEnabled; @@ -523,10 +525,6 @@ extern Option NetworkOutput; extern Option MultiboardSlaves; extern Option BattleCableEnable; -#ifdef SUPPORT_DISPMANX -extern Option DispmanxMaintainAspect; -#endif - #ifdef USE_OMX extern Option OmxAudioLatency; extern Option OmxAudioHdmi; @@ -549,4 +547,11 @@ constexpr bool UseRawInput = false; extern Option LuaFileName; #endif +// RetroAchievements + +extern Option EnableAchievements; +extern Option AchievementsHardcoreMode; +extern OptionString AchievementsUserName; +extern OptionString AchievementsToken; + } // namespace config diff --git a/core/cheats.cpp b/core/cheats.cpp index d42c1b898..e6f2afa35 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -432,9 +432,12 @@ void CheatManager::reset(const std::string& gameId) setActive(false); this->gameId = gameId; #ifndef LIBRETRO - std::string cheatFile = cfgLoadStr("cheats", gameId, ""); - if (!cheatFile.empty()) - loadCheatFile(cheatFile); + if (!settings.raHardcoreMode) + { + std::string cheatFile = cfgLoadStr("cheats", gameId, ""); + if (!cheatFile.empty()) + loadCheatFile(cheatFile); + } #endif size_t cheatCount = cheats.size(); if (gameId == "Fixed BOOT strapper") // Extreme Hunting 2 @@ -492,7 +495,7 @@ void CheatManager::reset(const std::string& gameId) if (cheats.size() > cheatCount) setActive(true); } - if (config::WidescreenGameHacks) + if (config::WidescreenGameHacks && !settings.raHardcoreMode) { if (settings.platform.isConsole()) { diff --git a/core/debug/gdb_server.cpp b/core/debug/gdb_server.cpp index eb8cb2f7c..4141a7d20 100644 --- a/core/debug/gdb_server.cpp +++ b/core/debug/gdb_server.cpp @@ -23,6 +23,7 @@ #include "debug_agent.h" #include "network/net_platform.h" #include "cfg/option.h" +#include "oslib/oslib.h" #include #include #include @@ -140,6 +141,7 @@ private: void serverThread() { + ThreadName _("GdbServer"); while (!stopRequested) { fd_set fds; diff --git a/core/deps/SDL b/core/deps/SDL index f461d91cd..fb1497566 160000 --- a/core/deps/SDL +++ b/core/deps/SDL @@ -1 +1 @@ -Subproject commit f461d91cd265d7b9a44b4d472b1df0c0ad2855a0 +Subproject commit fb1497566c5a05e2babdcf45ef0ab5c7cca2c4ae diff --git a/core/deps/discord-rpc b/core/deps/discord-rpc new file mode 160000 index 000000000..c1197e1a1 --- /dev/null +++ b/core/deps/discord-rpc @@ -0,0 +1 @@ +Subproject commit c1197e1a1e2ff09c077e84541bd88cf90581648c diff --git a/core/deps/imgui/backends/imgui_impl_dx11.cpp b/core/deps/imgui/backends/imgui_impl_dx11.cpp index b878e79f8..6ae2658bc 100644 --- a/core/deps/imgui/backends/imgui_impl_dx11.cpp +++ b/core/deps/imgui/backends/imgui_impl_dx11.cpp @@ -59,7 +59,8 @@ struct ImGui_ImplDX11_Data ID3D11PixelShader* pPixelShader; ID3D11SamplerState* pFontSampler; ID3D11SamplerState* pTextureSampler; - ID3D11ShaderResourceView* pFontTextureView; + ID3D11SamplerState* pPointSampler; + ImTextureDX11 FontTexture; ID3D11RasterizerState* pRasterizerState; ID3D11BlendState* pBlendState; ID3D11DepthStencilState* pDepthStencilState; @@ -279,11 +280,17 @@ void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data) ctx->RSSetScissorRects(1, &r); // Bind texture, Draw - ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID(); - if (pcmd->TextureId != (ImTextureID)bd->pFontTextureView) - ctx->PSSetSamplers(0, 1, &bd->pTextureSampler); + ImTextureDX11 *tex = (ImTextureDX11 *)pcmd->GetTexID(); + if (tex != &bd->FontTexture) + { + if (tex->pointSampling) + ctx->PSSetSamplers(0, 1, &bd->pPointSampler); + else + ctx->PSSetSamplers(0, 1, &bd->pTextureSampler); + } else ctx->PSSetSamplers(0, 1, &bd->pFontSampler); + ID3D11ShaderResourceView* texture_srv = tex->shaderResourceView; ctx->PSSetShaderResources(0, 1, &texture_srv); ctx->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset); } @@ -350,12 +357,12 @@ static void ImGui_ImplDX11_CreateFontsTexture() srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MipLevels = desc.MipLevels; srvDesc.Texture2D.MostDetailedMip = 0; - bd->pd3dDevice->CreateShaderResourceView(pTexture, &srvDesc, &bd->pFontTextureView); + bd->pd3dDevice->CreateShaderResourceView(pTexture, &srvDesc, &bd->FontTexture.shaderResourceView); pTexture->Release(); } // Store our identifier - io.Fonts->SetTexID((ImTextureID)bd->pFontTextureView); + io.Fonts->SetTexID((ImTextureID)&bd->FontTexture); // Create texture samplers // (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling) @@ -375,6 +382,9 @@ static void ImGui_ImplDX11_CreateFontsTexture() desc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER; desc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER; bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTextureSampler); + + desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + bd->pd3dDevice->CreateSamplerState(&desc, &bd->pPointSampler); } } @@ -540,7 +550,13 @@ void ImGui_ImplDX11_InvalidateDeviceObjects() if (bd->pFontSampler) { bd->pFontSampler->Release(); bd->pFontSampler = nullptr; } if (bd->pTextureSampler) { bd->pTextureSampler->Release(); bd->pTextureSampler = nullptr; } - if (bd->pFontTextureView) { bd->pFontTextureView->Release(); bd->pFontTextureView = nullptr; ImGui::GetIO().Fonts->SetTexID(0); } // We copied data->pFontTextureView to io.Fonts->TexID so let's clear that as well. + if (bd->pPointSampler) { bd->pPointSampler->Release(); bd->pPointSampler = nullptr; } + if (bd->FontTexture.shaderResourceView) { + bd->FontTexture.shaderResourceView->Release(); + bd->FontTexture.shaderResourceView = nullptr; + // We copied data->FontTexture.shaderResourceView to io.Fonts->TexID so let's clear that as well. + ImGui::GetIO().Fonts->SetTexID(0); + } if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; } if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; } if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; } @@ -603,7 +619,7 @@ void ImGui_ImplDX11_Shutdown() void ImGui_ImplDX11_NewFrame() { ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); - IM_ASSERT(bd != nullptr && "Did you call ImGui_ImplDX11_Init()?"); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?"); if (!bd->pFontSampler) ImGui_ImplDX11_CreateDeviceObjects(); diff --git a/core/deps/imgui/backends/imgui_impl_dx11.h b/core/deps/imgui/backends/imgui_impl_dx11.h index 20887f370..be5657bd6 100644 --- a/core/deps/imgui/backends/imgui_impl_dx11.h +++ b/core/deps/imgui/backends/imgui_impl_dx11.h @@ -19,6 +19,7 @@ struct ID3D11Device; struct ID3D11DeviceContext; +struct ID3D11ShaderResourceView; IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context); IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown(); @@ -29,4 +30,11 @@ IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data); IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects(); IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects(); +// ImTextureID should be a pointer to this struct +struct ImTextureDX11 +{ + ID3D11ShaderResourceView *shaderResourceView; + bool pointSampling; +}; + #endif // #ifndef IMGUI_DISABLE diff --git a/core/deps/imgui/backends/imgui_impl_dx9.cpp b/core/deps/imgui/backends/imgui_impl_dx9.cpp index fcda7b4f7..0eb4a61e3 100644 --- a/core/deps/imgui/backends/imgui_impl_dx9.cpp +++ b/core/deps/imgui/backends/imgui_impl_dx9.cpp @@ -48,7 +48,7 @@ struct ImGui_ImplDX9_Data LPDIRECT3DDEVICE9 pd3dDevice; LPDIRECT3DVERTEXBUFFER9 pVB; LPDIRECT3DINDEXBUFFER9 pIB; - LPDIRECT3DTEXTURE9 FontTexture; + ImTextureDX9 FontTexture; int VertexBufferSize; int IndexBufferSize; @@ -244,17 +244,30 @@ void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data) // Apply Scissor/clipping rectangle, Bind texture, Draw const RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y }; - const LPDIRECT3DTEXTURE9 texture = (LPDIRECT3DTEXTURE9)pcmd->GetTexID(); + const ImTextureDX9 *tex = (const ImTextureDX9 *)pcmd->GetTexID(); + const LPDIRECT3DTEXTURE9 texture = tex->d3dTexture; bd->pd3dDevice->SetTexture(0, texture); - if (texture != bd->FontTexture) + if (tex != &bd->FontTexture) { bd->pd3dDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_BORDER); bd->pd3dDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_BORDER); + if (tex->pointSampling) + { + bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_POINT); + bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT); + } + else + { + bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); + bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); + } } else { bd->pd3dDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); bd->pd3dDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP); + bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); + bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); } bd->pd3dDevice->SetScissorRect(&r); bd->pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, pcmd->VtxOffset + global_vtx_offset, 0, (UINT)cmd_list->VtxBuffer.Size, pcmd->IdxOffset + global_idx_offset, pcmd->ElemCount / 3); @@ -338,18 +351,18 @@ static bool ImGui_ImplDX9_CreateFontsTexture() #endif // Upload texture to graphics system - bd->FontTexture = nullptr; - if (bd->pd3dDevice->CreateTexture(width, height, 1, D3DUSAGE_DYNAMIC, rgba_support ? D3DFMT_A8B8G8R8 : D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &bd->FontTexture, nullptr) < 0) + bd->FontTexture.d3dTexture = nullptr; + if (bd->pd3dDevice->CreateTexture(width, height, 1, D3DUSAGE_DYNAMIC, rgba_support ? D3DFMT_A8B8G8R8 : D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &bd->FontTexture.d3dTexture, nullptr) < 0) return false; D3DLOCKED_RECT tex_locked_rect; - if (bd->FontTexture->LockRect(0, &tex_locked_rect, nullptr, 0) != D3D_OK) + if (bd->FontTexture.d3dTexture->LockRect(0, &tex_locked_rect, nullptr, 0) != D3D_OK) return false; for (int y = 0; y < height; y++) memcpy((unsigned char*)tex_locked_rect.pBits + (size_t)tex_locked_rect.Pitch * y, pixels + (size_t)width * bytes_per_pixel * y, (size_t)width * bytes_per_pixel); - bd->FontTexture->UnlockRect(0); + bd->FontTexture.d3dTexture->UnlockRect(0); // Store our identifier - io.Fonts->SetTexID((ImTextureID)bd->FontTexture); + io.Fonts->SetTexID((ImTextureID)&bd->FontTexture); #ifndef IMGUI_USE_BGRA_PACKED_COLOR if (!rgba_support && io.Fonts->TexPixelsUseColors) @@ -376,15 +389,21 @@ void ImGui_ImplDX9_InvalidateDeviceObjects() return; if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; } if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; } - if (bd->FontTexture) { bd->FontTexture->Release(); bd->FontTexture = nullptr; ImGui::GetIO().Fonts->SetTexID(0); } // We copied bd->pFontTextureView to io.Fonts->TexID so let's clear that as well. + if (bd->FontTexture.d3dTexture) + { + bd->FontTexture.d3dTexture->Release(); + bd->FontTexture.d3dTexture = nullptr; + // We copied bd->FontTexture to io.Fonts->TexID so let's clear that as well. + ImGui::GetIO().Fonts->SetTexID(0); + } } void ImGui_ImplDX9_NewFrame() { ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData(); - IM_ASSERT(bd != nullptr && "Did you call ImGui_ImplDX9_Init()?"); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX9_Init()?"); - if (!bd->FontTexture) + if (!bd->FontTexture.d3dTexture) ImGui_ImplDX9_CreateDeviceObjects(); } diff --git a/core/deps/imgui/backends/imgui_impl_dx9.h b/core/deps/imgui/backends/imgui_impl_dx9.h index 3965583bd..e8205a6e3 100644 --- a/core/deps/imgui/backends/imgui_impl_dx9.h +++ b/core/deps/imgui/backends/imgui_impl_dx9.h @@ -18,6 +18,7 @@ #ifndef IMGUI_DISABLE struct IDirect3DDevice9; +struct IDirect3DTexture9; IMGUI_IMPL_API bool ImGui_ImplDX9_Init(IDirect3DDevice9* device); IMGUI_IMPL_API void ImGui_ImplDX9_Shutdown(); @@ -28,4 +29,11 @@ IMGUI_IMPL_API void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data); IMGUI_IMPL_API bool ImGui_ImplDX9_CreateDeviceObjects(); IMGUI_IMPL_API void ImGui_ImplDX9_InvalidateDeviceObjects(); +// ImTextureID should be a pointer to this struct +struct ImTextureDX9 +{ + IDirect3DTexture9 *d3dTexture; + bool pointSampling; +}; + #endif // #ifndef IMGUI_DISABLE diff --git a/core/deps/imgui/backends/imgui_impl_opengl3.cpp b/core/deps/imgui/backends/imgui_impl_opengl3.cpp index 6f485a0a6..6eba3a348 100644 --- a/core/deps/imgui/backends/imgui_impl_opengl3.cpp +++ b/core/deps/imgui/backends/imgui_impl_opengl3.cpp @@ -66,9 +66,7 @@ #include "TargetConditionals.h" #endif -#include "wsi/gl_context.h" #include "rend/gles/glcache.h" -#include "hw/pvr/Renderer_if.h" // OpenGL Data static char g_GlslVersionString[32] = ""; @@ -81,7 +79,6 @@ static unsigned int g_VboHandle = 0, g_ElementsHandle = 0; // Functions static bool ImGui_ImplOpenGL3_CreateDeviceObjects(); static void ImGui_ImplOpenGL3_DestroyDeviceObjects(); -static void ImGui_ImplOpenGL3_DrawBackground(); bool ImGui_ImplOpenGL3_Init() { @@ -114,7 +111,6 @@ void ImGui_ImplOpenGL3_NewFrame() { if (!g_FontTexture) ImGui_ImplOpenGL3_CreateDeviceObjects(); - ImGui_ImplOpenGL3_DrawBackground(); } // OpenGL3 Render function. @@ -489,15 +485,3 @@ static void ImGui_ImplOpenGL3_DestroyDeviceObjects() ImGui_ImplOpenGL3_DestroyFontsTexture(); } - -static void ImGui_ImplOpenGL3_DrawBackground() -{ -#ifndef TARGET_IPHONE - glBindFramebuffer(GL_FRAMEBUFFER, 0); -#endif - glcache.Disable(GL_SCISSOR_TEST); - glcache.ClearColor(0, 0, 0, 0); - glClear(GL_COLOR_BUFFER_BIT); - if (renderer != nullptr) - renderer->RenderLastFrame(); -} diff --git a/core/deps/imgui/backends/imgui_impl_vulkan.cpp b/core/deps/imgui/backends/imgui_impl_vulkan.cpp index a8962ecd2..7d1c230c4 100644 --- a/core/deps/imgui/backends/imgui_impl_vulkan.cpp +++ b/core/deps/imgui/backends/imgui_impl_vulkan.cpp @@ -33,6 +33,7 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2024-04-19: Vulkan: Added convenience support for Volk via IMGUI_IMPL_VULKAN_USE_VOLK define (you can also use IMGUI_IMPL_VULKAN_NO_PROTOTYPES + wrap Volk via ImGui_ImplVulkan_LoadFunctions().) // 2024-02-14: *BREAKING CHANGE*: Moved RenderPass parameter from ImGui_ImplVulkan_Init() function to ImGui_ImplVulkan_InitInfo structure. Not required when using dynamic rendering. // 2024-02-12: *BREAKING CHANGE*: Dynamic rendering now require filling PipelineRenderingCreateInfo structure. // 2024-01-19: Vulkan: Fixed vkAcquireNextImageKHR() validation errors in VulkanSDK 1.3.275 by allocating one extra semaphore than in-flight frames. (#7236) @@ -108,12 +109,13 @@ void ImGui_ImplVulkanH_CreateWindowCommandBuffers(VkPhysicalDevice physical_devi // Vulkan prototypes for use with custom loaders // (see description of IMGUI_IMPL_VULKAN_NO_PROTOTYPES in imgui_impl_vulkan.h -#ifdef VK_NO_PROTOTYPES +#if defined(VK_NO_PROTOTYPES) && !defined(VOLK_H_) +#define IMGUI_IMPL_VULKAN_USE_LOADER static bool g_FunctionsLoaded = false; #else static bool g_FunctionsLoaded = true; #endif -#ifdef VK_NO_PROTOTYPES +#ifdef IMGUI_IMPL_VULKAN_USE_LOADER #define IMGUI_VULKAN_FUNC_MAP(IMGUI_VULKAN_FUNC_MAP_MACRO) \ IMGUI_VULKAN_FUNC_MAP_MACRO(vkAllocateCommandBuffers) \ IMGUI_VULKAN_FUNC_MAP_MACRO(vkAllocateDescriptorSets) \ @@ -184,7 +186,7 @@ static bool g_FunctionsLoaded = true; #define IMGUI_VULKAN_FUNC_DEF(func) static PFN_##func func; IMGUI_VULKAN_FUNC_MAP(IMGUI_VULKAN_FUNC_DEF) #undef IMGUI_VULKAN_FUNC_DEF -#endif // VK_NO_PROTOTYPES +#endif // IMGUI_IMPL_VULKAN_USE_LOADER #ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING static PFN_vkCmdBeginRenderingKHR ImGuiImplVulkanFuncs_vkCmdBeginRenderingKHR; @@ -1048,8 +1050,8 @@ bool ImGui_ImplVulkan_LoadFunctions(PFN_vkVoidFunction(*loader_func)(const ch // Load function pointers // You can use the default Vulkan loader using: // ImGui_ImplVulkan_LoadFunctions([](const char* function_name, void*) { return vkGetInstanceProcAddr(your_vk_isntance, function_name); }); - // But this would be equivalent to not setting VK_NO_PROTOTYPES. -#ifdef VK_NO_PROTOTYPES + // But this would be roughly equivalent to not setting VK_NO_PROTOTYPES. +#ifdef IMGUI_IMPL_VULKAN_USE_LOADER #define IMGUI_VULKAN_FUNC_LOAD(func) \ func = reinterpret_cast(loader_func(#func, user_data)); \ if (func == nullptr) \ @@ -1078,7 +1080,7 @@ bool ImGui_ImplVulkan_Init(ImGui_ImplVulkan_InitInfo* info) if (info->UseDynamicRendering) { #ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING -#ifndef VK_NO_PROTOTYPES +#ifndef IMGUI_IMPL_VULKAN_USE_LOADER ImGuiImplVulkanFuncs_vkCmdBeginRenderingKHR = reinterpret_cast(vkGetInstanceProcAddr(info->Instance, "vkCmdBeginRenderingKHR")); ImGuiImplVulkanFuncs_vkCmdEndRenderingKHR = reinterpret_cast(vkGetInstanceProcAddr(info->Instance, "vkCmdEndRenderingKHR")); #endif @@ -1131,7 +1133,7 @@ void ImGui_ImplVulkan_Shutdown() void ImGui_ImplVulkan_NewFrame() { ImGui_ImplVulkan_Data* bd = ImGui_ImplVulkan_GetBackendData(); - IM_ASSERT(bd != nullptr && "Did you call ImGui_ImplVulkan_Init()?"); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplVulkan_Init()?"); if (!bd->FontDescriptorSet) ImGui_ImplVulkan_CreateFontsTexture(); diff --git a/core/deps/imgui/backends/imgui_impl_vulkan.h b/core/deps/imgui/backends/imgui_impl_vulkan.h index f77fc235b..c174a6ca1 100644 --- a/core/deps/imgui/backends/imgui_impl_vulkan.h +++ b/core/deps/imgui/backends/imgui_impl_vulkan.h @@ -42,13 +42,20 @@ // If you have no idea what this is, leave it alone! //#define IMGUI_IMPL_VULKAN_NO_PROTOTYPES -// Vulkan includes +// Convenience support for Volk +// (you can also technically use IMGUI_IMPL_VULKAN_NO_PROTOTYPES + wrap Volk via ImGui_ImplVulkan_LoadFunctions().) +//#define IMGUI_IMPL_VULKAN_USE_VOLK + #if defined(IMGUI_IMPL_VULKAN_NO_PROTOTYPES) && !defined(VK_NO_PROTOTYPES) #define VK_NO_PROTOTYPES #endif #if defined(VK_USE_PLATFORM_WIN32_KHR) && !defined(NOMINMAX) #define NOMINMAX -#include +#endif + +// Vulkan includes +#ifdef IMGUI_IMPL_VULKAN_USE_VOLK +#include #else #include #endif diff --git a/core/deps/imgui/imgui.cpp b/core/deps/imgui/imgui.cpp index a309d6836..00637d27a 100644 --- a/core/deps/imgui/imgui.cpp +++ b/core/deps/imgui/imgui.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 // (main code and documentation) // Help: @@ -7,15 +7,19 @@ // - Read top of imgui.cpp for more details, links and comments. // Resources: -// - FAQ https://dearimgui.com/faq -// - Getting Started https://dearimgui.com/getting-started -// - Homepage https://github.com/ocornut/imgui -// - Releases & changelog https://github.com/ocornut/imgui/releases -// - Gallery https://github.com/ocornut/imgui/issues/6897 (please post your screenshots/video there!) -// - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) -// - Glossary https://github.com/ocornut/imgui/wiki/Glossary -// - Issues & support https://github.com/ocornut/imgui/issues -// - Tests & Automation https://github.com/ocornut/imgui_test_engine +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues/7503 (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) // For first-time users having issues compiling/linking/running/loading fonts: // please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. @@ -26,7 +30,7 @@ // See LICENSE.txt for copyright and licensing details (standard MIT License). // This library is free but needs your support to sustain development and maintenance. // Businesses: you can support continued development via B2B invoiced technical support, maintenance and sponsoring contracts. -// PLEASE reach out at omar AT dearimgui DOT com. See https://github.com/ocornut/imgui/wiki/Sponsors +// PLEASE reach out at omar AT dearimgui DOT com. See https://github.com/ocornut/imgui/wiki/Funding // Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. // It is recommended that you don't modify imgui.cpp! It will become difficult for you to update the library. @@ -73,6 +77,7 @@ CODE // [SECTION] RENDER HELPERS // [SECTION] INITIALIZATION, SHUTDOWN // [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +// [SECTION] ID STACK // [SECTION] INPUTS // [SECTION] ERROR CHECKING // [SECTION] ITEM SUBMISSION @@ -425,6 +430,15 @@ CODE When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. You can read releases logs https://github.com/ocornut/imgui/releases for more details. + - 2024/04/18 (1.90.6) - TreeNode: Fixed a layout inconsistency when using an empty/hidden label followed by a SameLine() call. (#7505, #282) + - old: TreeNode("##Hidden"); SameLine(); Text("Hello"); // <-- This was actually incorrect! BUT appeared to look ok with the default style where ItemSpacing.x == FramePadding.x * 2 (it didn't look aligned otherwise). + - new: TreeNode("##Hidden"); SameLine(0, 0); Text("Hello"); // <-- This is correct for all styles values. + with the fix, IF you were successfully using TreeNode("")+SameLine(); you will now have extra spacing between your TreeNode and the following item. + You'll need to change the SameLine() call to SameLine(0,0) to remove this extraneous spacing. This seemed like the more sensible fix that's not making things less consistent. + (Note: when using this idiom you are likely to also use ImGuiTreeNodeFlags_SpanAvailWidth). + - 2024/03/18 (1.90.5) - merged the radius_x/radius_y parameters in ImDrawList::AddEllipse(), AddEllipseFilled() and PathEllipticalArcTo() into a single ImVec2 parameter. Exceptionally, because those functions were added in 1.90, we are not adding inline redirection functions. The transition is easy and should affect few users. (#2743, #7417) + - 2024/03/08 (1.90.5) - inputs: more formally obsoleted GetKeyIndex() when IMGUI_DISABLE_OBSOLETE_FUNCTIONS is set. It has been unnecessary and a no-op since 1.87 (it returns the same value as passed when used with a 1.87+ backend using io.AddKeyEvent() function). (#4921) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) - 2024/01/15 (1.90.2) - commented out obsolete ImGuiIO::ImeWindowHandle marked obsolete in 1.87, favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. - 2023/12/19 (1.90.1) - commented out obsolete ImGuiKey_KeyPadEnter redirection to ImGuiKey_KeypadEnter. - 2023/11/06 (1.90.1) - removed CalcListClipping() marked obsolete in 1.86. Prefer using ImGuiListClipper which can return non-contiguous ranges. @@ -935,7 +949,7 @@ CODE A: - Businesses: please reach out to "omar AT dearimgui DOT com" if you work in a place using Dear ImGui! We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. This is among the most useful thing you can do for Dear ImGui. With increased funding, we sustain and grow work on this project. - Also see https://github.com/ocornut/imgui/wiki/Sponsors + >>> See https://github.com/ocornut/imgui/wiki/Funding - Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, and see how you want to help and can help! - Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. @@ -1026,6 +1040,7 @@ CODE #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 #pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) // We disable -Wpragmas because GCC doesn't provide a has_warning equivalent and some forks/patches may not follow the warning/version association. #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind @@ -1124,6 +1139,7 @@ static void RenderWindowDecorations(ImGuiWindow* window, const ImRec static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); static void RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col); static void RenderDimmedBackgrounds(); +static void SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect); // Viewports const ImGuiID IMGUI_VIEWPORT_DEFAULT_ID = 0x11111111; // Using an arbitrary constant instead of e.g. ImHashStr("ViewportDefault", 0); so it's easier to spot in the debugger. The exact value doesn't matter. @@ -1179,58 +1195,59 @@ static void* GImAllocatorUserData = NULL; ImGuiStyle::ImGuiStyle() { - Alpha = 1.0f; // Global alpha applies to everything in Dear ImGui. - DisabledAlpha = 0.60f; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. - WindowPadding = ImVec2(8,8); // Padding within a window - WindowRounding = 0.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. - WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. - WindowMinSize = ImVec2(32,32); // Minimum window size - WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text - WindowMenuButtonPosition= ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. - ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows - ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. - PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows - PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. - FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) - FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). - FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. - ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines - ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) - CellPadding = ImVec2(4,2); // Padding within a table cell. CellPadding.y may be altered between different rows. - TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! - IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). - ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). - ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar - ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar - GrabMinSize = 12.0f; // Minimum width/height of a grab box for slider/scrollbar - GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. - LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. - TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. - TabBorderSize = 0.0f; // Thickness of border around tabs. - TabMinWidthForCloseButton = 0.0f; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. - TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. - TableAngledHeadersAngle = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees). - ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. - ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. - SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. - SeparatorTextBorderSize = 3.0f; // Thickkness of border in SeparatorText() - SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). - SeparatorTextPadding = ImVec2(20.0f,3.f);// Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. - DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. - DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. - MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. - AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. - AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). - AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). - CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. - CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + Alpha = 1.0f; // Global alpha applies to everything in Dear ImGui. + DisabledAlpha = 0.60f; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + WindowPadding = ImVec2(8,8); // Padding within a window + WindowRounding = 0.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. + WindowMinSize = ImVec2(32,32); // Minimum window size + WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text + WindowMenuButtonPosition = ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. + ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows + ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. + PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows + PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. + FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) + FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). + FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. + ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines + ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + CellPadding = ImVec2(4,2); // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. + TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar + ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar + GrabMinSize = 12.0f; // Minimum width/height of a grab box for slider/scrollbar + GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + TabBorderSize = 0.0f; // Thickness of border around tabs. + TabMinWidthForCloseButton = 0.0f; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + TableAngledHeadersAngle = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees). + TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell + ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + SeparatorTextBorderSize = 3.0f; // Thickkness of border in SeparatorText() + SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + SeparatorTextPadding = ImVec2(20.0f,3.f);// Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. + AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). + CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. // Behaviors - HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. - HoverDelayShort = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. - HoverDelayNormal = 0.40f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " - HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. - HoverFlagsForTooltipNav = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + HoverDelayShort = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + HoverDelayNormal = 0.40f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + HoverFlagsForTooltipNav = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. // Default theme ImGui::StyleColorsDark(this); @@ -2318,6 +2335,20 @@ const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const cha return in_text_start; } +int ImTextCountLines(const char* in_text, const char* in_text_end) +{ + if (in_text_end == NULL) + in_text_end = in_text + strlen(in_text); // FIXME-OPT: Not optimal approach, discourage use for now. + int count = 0; + while (in_text < in_text_end) + { + const char* line_end = (const char*)memchr(in_text, '\n', in_text_end - in_text); + in_text = line_end ? line_end + 1 : in_text_end; + count++; + } + return count; +} + IM_MSVC_RUNTIME_CHECKS_RESTORE //----------------------------------------------------------------------------- @@ -3100,35 +3131,38 @@ void ImGui::PopStyleColor(int count) static const ImGuiDataVarInfo GStyleVarInfo[] = { - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) }, // ImGuiStyleVar_TabBarBorderSize - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign - { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)},// ImGuiStyleVar_SeparatorTextBorderSize - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBorderSize) }, // ImGuiStyleVar_TabBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) }, // ImGuiStyleVar_TabBarBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding }; const ImGuiDataVarInfo* ImGui::GetStyleVarInfo(ImGuiStyleVar idx) @@ -3773,45 +3807,6 @@ ImGuiWindow::~ImGuiWindow() ColumnsStorage.clear_destruct(); } -ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) -{ - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); - ImGuiContext& g = *Ctx; - if (g.DebugHookIdInfo == id) - ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); - return id; -} - -ImGuiID ImGuiWindow::GetID(const void* ptr) -{ - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); - ImGuiContext& g = *Ctx; - if (g.DebugHookIdInfo == id) - ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); - return id; -} - -ImGuiID ImGuiWindow::GetID(int n) -{ - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashData(&n, sizeof(n), seed); - ImGuiContext& g = *Ctx; - if (g.DebugHookIdInfo == id) - ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); - return id; -} - -// This is only used in rare/specific situations to manufacture an ID out of nowhere. -ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) -{ - ImGuiID seed = IDStack.back(); - ImRect r_rel = ImGui::WindowRectAbsToRel(this, r_abs); - ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); - return id; -} - static void SetCurrentWindow(ImGuiWindow* window) { ImGuiContext& g = *GImGui; @@ -4554,6 +4549,27 @@ void ImGui::UpdateHoveredWindowAndCaptureFlags() io.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; } +// Calling SetupDrawListSharedData() is followed by SetCurrentFont() which sets up the remaining data. +static void SetupDrawListSharedData() +{ + ImGuiContext& g = *GImGui; + ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (ImGuiViewportP* viewport : g.Viewports) + virtual_space.Add(viewport->GetMainRect()); + g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); + g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; + g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); + g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; + if (g.Style.AntiAliasedLines) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; + if (g.Style.AntiAliasedLinesUseTex && !(g.IO.Fonts->Flags & ImFontAtlasFlags_NoBakedLines)) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLinesUseTex; + if (g.Style.AntiAliasedFill) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; + if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; +} + void ImGui::NewFrame() { IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); @@ -4596,23 +4612,9 @@ void ImGui::NewFrame() // Setup current font and draw list shared data g.IO.Fonts->Locked = true; + SetupDrawListSharedData(); SetCurrentFont(GetDefaultFont()); IM_ASSERT(g.Font->IsLoaded()); - ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); - for (ImGuiViewportP* viewport : g.Viewports) - virtual_space.Add(viewport->GetMainRect()); - g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); - g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; - g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); - g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; - if (g.Style.AntiAliasedLines) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; - if (g.Style.AntiAliasedLinesUseTex && !(g.Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedLines)) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLinesUseTex; - if (g.Style.AntiAliasedFill) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; - if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; // Mark rendering data as invalid to prevent user who may have a handle on it to use it. for (ImGuiViewportP* viewport : g.Viewports) @@ -5893,7 +5895,7 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& si int ret_auto_fit_mask = 0x00; const float grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); - const float grip_hover_inner_size = IM_TRUNC(grip_draw_size * 0.75f); + const float grip_hover_inner_size = (resize_grip_count > 0) ? IM_TRUNC(grip_draw_size * 0.75f) : 0.0f; const float grip_hover_outer_size = g.IO.ConfigWindowsResizeFromEdges ? WINDOWS_HOVER_PADDING : 0.0f; ImRect clamp_rect = visibility_rect; @@ -6021,10 +6023,13 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& si border_target = ImClamp(border_target, clamp_min, clamp_max); if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent { - if ((flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (flags & ImGuiWindowFlags_NoScrollbar)) - border_target.x = ImClamp(border_target.x, window->ParentWindow->InnerClipRect.Min.x, window->ParentWindow->InnerClipRect.Max.x); - if (flags & ImGuiWindowFlags_NoScrollbar) - border_target.y = ImClamp(border_target.y, window->ParentWindow->InnerClipRect.Min.y, window->ParentWindow->InnerClipRect.Max.y); + ImGuiWindowFlags parent_flags = window->ParentWindow->Flags; + ImRect border_limit_rect = window->ParentWindow->InnerRect; + border_limit_rect.Expand(ImVec2(-ImMax(window->WindowPadding.x, window->WindowBorderSize), -ImMax(window->WindowPadding.y, window->WindowBorderSize))); + if ((parent_flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (parent_flags & ImGuiWindowFlags_NoScrollbar)) + border_target.x = ImClamp(border_target.x, border_limit_rect.Min.x, border_limit_rect.Max.x); + if (parent_flags & ImGuiWindowFlags_NoScrollbar) + border_target.y = ImClamp(border_target.y, border_limit_rect.Min.y, border_limit_rect.Max.y); } if (!ignore_resize) CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); @@ -6330,6 +6335,30 @@ void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags } } +// [EXPERIMENTAL] Called by Begin(). NextWindowData is valid at this point. +// This is designed as a toy/test-bed for +void ImGui::UpdateWindowSkipRefresh(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + window->SkipRefresh = false; + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasRefreshPolicy) == 0) + return; + if (g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_TryToAvoidRefresh) + { + // FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. + if (window->Appearing) // If currently appearing + return; + if (window->Hidden) // If was hidden (previous frame) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnHover) && g.HoveredWindow && window->RootWindow == g.HoveredWindow->RootWindow) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnFocus) && g.NavWindow && window->RootWindow == g.NavWindow->RootWindow) + return; + window->DrawList = NULL; + window->SkipRefresh = true; + } +} + // When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags_NoFocusOnAppearing) // should be positioned behind that modal window, unless the window was created inside the modal begin-stack. // In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent. @@ -6464,7 +6493,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) PushFocusScope((flags & ImGuiWindowFlags_NavFlattened) ? g.CurrentFocusScopeId : window->ID); window->NavRootFocusScopeId = g.CurrentFocusScopeId; - // Add to popup stack + // Add to popup stacks: update OpenPopupStack[] data, push to BeginPopupStack[] if (flags & ImGuiWindowFlags_Popup) { ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; @@ -6528,11 +6557,14 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) if (window->Appearing) SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + // [EXPERIMENTAL] Skip Refresh mode + UpdateWindowSkipRefresh(window); + // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() g.CurrentWindow = NULL; // When reusing window again multiple times a frame, just append content (don't need to setup again) - if (first_begin_of_the_frame) + if (first_begin_of_the_frame && !window->SkipRefresh) { // Initialize const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) @@ -6611,8 +6643,14 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->DC.MenuBarOffset.x = ImMax(ImMax(window->WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x); window->DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y; + // Depending on condition we use previous or current window size to compare against contents size to decide if a scrollbar should be visible. + // Those flags will be altered further down in the function depending on more conditions. bool use_current_size_for_scrollbar_x = window_just_created; bool use_current_size_for_scrollbar_y = window_just_created; + if (window_size_x_set_by_api && window->ContentSizeExplicit.x != 0.0f) + use_current_size_for_scrollbar_x = true; + if (window_size_y_set_by_api && window->ContentSizeExplicit.y != 0.0f) // #7252 + use_current_size_for_scrollbar_y = true; // Collapse window by double-clicking on title bar // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing @@ -6620,8 +6658,9 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), so verify that we don't have items over the title bar. ImRect title_bar_rect = window->TitleBarRect(); - if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) && g.IO.MouseClickedCount[0] == 2) - window->WantCollapseToggle = true; + if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max)) + if (g.IO.MouseClickedCount[0] == 2 && GetKeyOwner(ImGuiKey_MouseLeft) == ImGuiKeyOwner_None) + window->WantCollapseToggle = true; if (window->WantCollapseToggle) { window->Collapsed = !window->Collapsed; @@ -6827,17 +6866,19 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->DecoOuterSizeY2; // Inner clipping rectangle. - // Will extend a little bit outside the normal work region. - // This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. - // Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + // - Extend a outside of normal work region up to borders. + // - This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. + // - It also makes clipped items be more noticeable. + // - And is consistent on both axis (prior to 2024/05/03 ClipRect used WindowPadding.x * 0.5f on left and right edge), see #3312 + // - Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. // Note that if our window is collapsed we will end up with an inverted (~null) clipping rectangle which is the correct behavior. // Affected by window/frame border size. Used by: // - Begin() initial clip rect float top_border_size = (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); - window->InnerClipRect.Min.x = ImTrunc(0.5f + window->InnerRect.Min.x + ImMax(ImTrunc(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); - window->InnerClipRect.Min.y = ImTrunc(0.5f + window->InnerRect.Min.y + top_border_size); - window->InnerClipRect.Max.x = ImTrunc(0.5f + window->InnerRect.Max.x - ImMax(ImTrunc(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); - window->InnerClipRect.Max.y = ImTrunc(0.5f + window->InnerRect.Max.y - window->WindowBorderSize); + window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + window->WindowBorderSize); + window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size); + window->InnerClipRect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - window->WindowBorderSize); + window->InnerClipRect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y - window->WindowBorderSize); window->InnerClipRect.ClipWithFull(host_rect); // Default item width. Make it proportional to window size if window manually resizes @@ -6998,7 +7039,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin(). // This is useful to allow creating context menus on title bar only, etc. - SetLastItemData(window->MoveId, g.CurrentItemFlags, IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0, title_bar_rect); + SetLastItemDataForWindow(window, title_bar_rect); // [DEBUG] #ifndef IMGUI_DISABLE_DEBUG_TOOLS @@ -7014,11 +7055,17 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) } else { + // Skip refresh always mark active + if (window->SkipRefresh) + window->Active = true; + // Append SetCurrentWindow(window); + SetLastItemDataForWindow(window, window->TitleBarRect()); } - PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); + if (!window->SkipRefresh) + PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) window->WriteAccessed = false; @@ -7026,7 +7073,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) g.NextWindowData.ClearFlags(); // Update visibility - if (first_begin_of_the_frame) + if (first_begin_of_the_frame && !window->SkipRefresh) { if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_ChildMenu)) { @@ -7072,6 +7119,11 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) skip_items = true; window->SkipItems = skip_items; } + else if (first_begin_of_the_frame) + { + // Skip refresh mode + window->SkipItems = true; + } // [DEBUG] io.ConfigDebugBeginReturnValue override return value to test Begin/End and BeginChild/EndChild behaviors. // (The implicit fallback window is NOT automatically ended allowing it to always be able to receive commands without crashing) @@ -7088,6 +7140,12 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) return !window->SkipItems; } +static void ImGui::SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + SetLastItemData(window->MoveId, g.CurrentItemFlags, IsMouseHoveringRect(rect.Min, rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0, rect); +} + void ImGui::End() { ImGuiContext& g = *GImGui; @@ -7108,9 +7166,16 @@ void ImGui::End() // Close anything that is open if (window->DC.CurrentColumns) EndColumns(); - PopClipRect(); // Inner window clip rectangle + if (!window->SkipRefresh) + PopClipRect(); // Inner window clip rectangle PopFocusScope(); + if (window->SkipRefresh) + { + IM_ASSERT(window->DrawList == NULL); + window->DrawList = &window->DrawListInst; + } + // Stop logging if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging LogFinish(); @@ -7795,6 +7860,14 @@ void ImGui::SetNextWindowBgAlpha(float alpha) g.NextWindowData.BgAlphaVal = alpha; } +// This is experimental and meant to be a toy for exploring a future/wider range of features. +void ImGui::SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasRefreshPolicy; + g.NextWindowData.RefreshFlagsVal = flags; +} + ImDrawList* ImGui::GetWindowDrawList() { ImGuiWindow* window = GetCurrentWindow(); @@ -7965,6 +8038,69 @@ ImGuiStorage* ImGui::GetStateStorage() return window->DC.StateStorage; } +bool ImGui::IsRectVisible(const ImVec2& size) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); +} + +bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); +} + +//----------------------------------------------------------------------------- +// [SECTION] ID STACK +//----------------------------------------------------------------------------- + +// This is one of the very rare legacy case where we use ImGuiWindow methods, +// it should ideally be flattened at some point but it's been used a lots by widgets. +ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; +} + +ImGuiID ImGuiWindow::GetID(const void* ptr) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); +#endif + return id; +} + +ImGuiID ImGuiWindow::GetID(int n) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; +} + +// This is only used in rare/specific situations to manufacture an ID out of nowhere. +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) +{ + ImGuiID seed = IDStack.back(); + ImRect r_rel = ImGui::WindowRectAbsToRel(this, r_abs); + ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); + return id; +} + void ImGui::PushID(const char* str_id) { ImGuiContext& g = *GImGui; @@ -8002,8 +8138,10 @@ void ImGui::PushOverrideID(ImGuiID id) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; +#ifndef IMGUI_DISABLE_DEBUG_TOOLS if (g.DebugHookIdInfo == id) DebugHookIdInfo(id, ImGuiDataType_ID, NULL, NULL); +#endif window->IDStack.push_back(id); } @@ -8013,18 +8151,22 @@ void ImGui::PushOverrideID(ImGuiID id) ImGuiID ImGui::GetIDWithSeed(const char* str, const char* str_end, ImGuiID seed) { ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS ImGuiContext& g = *GImGui; if (g.DebugHookIdInfo == id) DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif return id; } ImGuiID ImGui::GetIDWithSeed(int n, ImGuiID seed) { ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS ImGuiContext& g = *GImGui; if (g.DebugHookIdInfo == id) DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif return id; } @@ -8053,19 +8195,6 @@ ImGuiID ImGui::GetID(const void* ptr_id) return window->GetID(ptr_id); } -bool ImGui::IsRectVisible(const ImVec2& size) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); -} - -bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); -} - - //----------------------------------------------------------------------------- // [SECTION] INPUTS //----------------------------------------------------------------------------- @@ -9196,7 +9325,7 @@ void ImGui::SetNextFrameWantCaptureMouse(bool want_capture_mouse) #ifndef IMGUI_DISABLE_DEBUG_TOOLS static const char* GetInputSourceName(ImGuiInputSource source) { - const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad", "Clipboard" }; + const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad" }; IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT && source >= 0 && source < ImGuiInputSource_COUNT); return input_source_names[source]; } @@ -9512,13 +9641,15 @@ bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags //----------------------------------------------------------------------------- // Helper function to verify ABI compatibility between caller code and compiled version of Dear ImGui. +// This is called by IMGUI_CHECKVERSION(). // Verify that the type sizes are matching between the calling file's compilation unit and imgui.cpp's compilation unit -// If this triggers you have an issue: -// - Most commonly: mismatched headers and compiled code version. -// - Or: mismatched configuration #define, compilation settings, packing pragma etc. -// The configuration settings mentioned in imconfig.h must be set for all compilation units involved with Dear ImGui, -// which is way it is required you put them in your imconfig file (and not just before including imgui.h). -// Otherwise it is possible that different compilation units would see different structure layout +// If this triggers you have mismatched headers and compiled code versions. +// - It could be because of a build issue (using new headers with old compiled code) +// - It could be because of mismatched configuration #define, compilation settings, packing pragma etc. +// THE CONFIGURATION SETTINGS MENTIONED IN imconfig.h MUST BE SET FOR ALL COMPILATION UNITS INVOLVED WITH DEAR IMGUI. +// Which is why it is required you put them in your imconfig file (and NOT only before including imgui.h). +// Otherwise it is possible that different compilation units would see different structure layout. +// If you don't want to modify imconfig.h you can use the IMGUI_USER_CONFIG define to change filename. bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_vert, size_t sz_idx) { bool error = false; @@ -10775,7 +10906,7 @@ void ImGui::OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) ImGuiPopupData popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. popup_ref.PopupId = id; popup_ref.Window = NULL; - popup_ref.BackupNavWindow = g.NavWindow; // When popup closes focus may be restored to NavWindow (depend on window type). + popup_ref.RestoreNavWindow = g.NavWindow; // When popup closes focus may be restored to NavWindow (depend on window type). popup_ref.OpenFrameCount = g.FrameCount; popup_ref.OpenParentId = parent_window->IDStack.back(); popup_ref.OpenPopupPos = NavCalcPreferredRefPos(); @@ -10824,6 +10955,7 @@ void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to return; // Don't close our own child popup windows. + //IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\") restore_under=%d\n", ref_window ? ref_window->Name : "", restore_focus_to_window_under_popup); int popup_count_to_keep = 0; if (ref_window) { @@ -10880,18 +11012,19 @@ void ImGui::ClosePopupsExceptModals() void ImGui::ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup) { ImGuiContext& g = *GImGui; - IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupToLevel(%d), restore_focus_to_window_under_popup=%d\n", remaining, restore_focus_to_window_under_popup); + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupToLevel(%d), restore_under=%d\n", remaining, restore_focus_to_window_under_popup); IM_ASSERT(remaining >= 0 && remaining < g.OpenPopupStack.Size); // Trim open popup stack - ImGuiWindow* popup_window = g.OpenPopupStack[remaining].Window; - ImGuiWindow* popup_backup_nav_window = g.OpenPopupStack[remaining].BackupNavWindow; + ImGuiPopupData prev_popup = g.OpenPopupStack[remaining]; g.OpenPopupStack.resize(remaining); - if (restore_focus_to_window_under_popup) + // Restore focus (unless popup window was not yet submitted, and didn't have a chance to take focus anyhow. See #7325 for an edge case) + if (restore_focus_to_window_under_popup && prev_popup.Window) { - ImGuiWindow* focus_window = (popup_window && popup_window->Flags & ImGuiWindowFlags_ChildMenu) ? popup_window->ParentWindow : popup_backup_nav_window; - if (focus_window && !focus_window->WasActive && popup_window) + ImGuiWindow* popup_window = prev_popup.Window; + ImGuiWindow* focus_window = (popup_window->Flags & ImGuiWindowFlags_ChildMenu) ? popup_window->ParentWindow : prev_popup.RestoreNavWindow; + if (focus_window && !focus_window->WasActive) FocusTopMostWindowUnderOne(popup_window, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); // Fallback else FocusWindow(focus_window, (g.NavLayer == ImGuiNavLayer_Main) ? ImGuiFocusRequestFlags_RestoreFocusedChild : ImGuiFocusRequestFlags_None); @@ -12283,8 +12416,10 @@ void ImGui::NavMoveRequestApplyResult() g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; } - // FIXME: Could become optional e.g. ImGuiNavMoveFlags_NoClearActiveId if we later want to apply navigation requests without altering active input. - if (g.ActiveId != result->ID) + // Clear active id unless requested not to + // FIXME: ImGuiNavMoveFlags_NoClearActiveId is currently unused as we don't have a clear strategy to preserve active id after interaction, + // so this is mostly provided as a gateway for further experiments (see #1418, #2890) + if (g.ActiveId != result->ID && (g.NavMoveFlags & ImGuiNavMoveFlags_NoClearActiveId) == 0) ClearActiveID(); // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) @@ -12918,6 +13053,7 @@ bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) source_drag_active = true; } + IM_ASSERT(g.DragDropWithinTarget == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() if (source_drag_active) { if (!g.DragDropActive) @@ -13033,7 +13169,7 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) if (window->SkipItems) return false; - IM_ASSERT(g.DragDropWithinTarget == false); + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() g.DragDropTargetRect = bb; g.DragDropTargetClipRect = window->ClipRect; // May want to be overriden by user depending on use case? g.DragDropTargetId = id; @@ -13068,7 +13204,7 @@ bool ImGui::BeginDragDropTarget() if (g.DragDropPayload.SourceId == id) return false; - IM_ASSERT(g.DragDropWithinTarget == false); + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() g.DragDropTargetRect = display_rect; g.DragDropTargetClipRect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; g.DragDropTargetId = id; @@ -14455,9 +14591,9 @@ void ImGui::ShowMetricsWindow(bool* p_open) { // As it's difficult to interact with tree nodes while popups are open, we display everything inline. ImGuiWindow* window = popup_data.Window; - BulletText("PopupID: %08x, Window: '%s' (%s%s), BackupNavWindow '%s', ParentWindow '%s'", + BulletText("PopupID: %08x, Window: '%s' (%s%s), RestoreNavWindow '%s', ParentWindow '%s'", popup_data.PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? "Child;" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? "Menu;" : "", - popup_data.BackupNavWindow ? popup_data.BackupNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); + popup_data.RestoreNavWindow ? popup_data.RestoreNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); } TreePop(); } diff --git a/core/deps/imgui/imgui.h b/core/deps/imgui/imgui.h index ecadd5077..40efb4aed 100644 --- a/core/deps/imgui/imgui.h +++ b/core/deps/imgui/imgui.h @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 WIP // (headers) // Help: @@ -7,15 +7,19 @@ // - Read top of imgui.cpp for more details, links and comments. // Resources: -// - FAQ https://dearimgui.com/faq -// - Getting Started https://dearimgui.com/getting-started -// - Homepage https://github.com/ocornut/imgui -// - Releases & changelog https://github.com/ocornut/imgui/releases -// - Gallery https://github.com/ocornut/imgui/issues/6897 (please post your screenshots/video there!) -// - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) -// - Glossary https://github.com/ocornut/imgui/wiki/Glossary -// - Issues & support https://github.com/ocornut/imgui/issues -// - Tests & Automation https://github.com/ocornut/imgui_test_engine +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues/7503 (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) // For first-time users having issues compiling/linking/running/loading fonts: // please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. @@ -23,8 +27,8 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') -#define IMGUI_VERSION "1.90.4" -#define IMGUI_VERSION_NUM 19040 +#define IMGUI_VERSION "1.90.6" +#define IMGUI_VERSION_NUM 19060 #define IMGUI_HAS_TABLE /* @@ -126,6 +130,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" #pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind @@ -171,8 +176,9 @@ struct ImGuiViewport; // A Platform Window (always only one in 'ma // Enumerations // - We don't use strongly typed enums much because they add constraints (can't extend in private code, can't store typed in bit fields, extra casting on iteration) // - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! -// In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. -// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. enum ImGuiKey : int; // -> enum ImGuiKey // Enum: A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value) enum ImGuiMouseSource : int; // -> enum ImGuiMouseSource // Enum; A mouse input source identifier (Mouse, TouchScreen, Pen) typedef int ImGuiCol; // -> enum ImGuiCol_ // Enum: A color identifier for styling @@ -187,8 +193,9 @@ typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A // Flags (declared as int to allow using as flags without overhead, and to not pollute the top of this file) // - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! -// In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. -// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build @@ -1096,11 +1103,12 @@ enum ImGuiTreeNodeFlags_ ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow. IMPORTANT: node can still be marked open/close if you don't set the _Leaf flag! ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). - ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line. In the future we may refactor the hit system to be front-to-back, allowing natural overlaps and then this can become the default. - ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (bypass the indented area). - ImGuiTreeNodeFlags_SpanAllColumns = 1 << 13, // Frame will span all columns of its container table (text will still fit in current column) - ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 14, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) - //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 15, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line without using AllowOverlap mode. + ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (cover the indent area). + ImGuiTreeNodeFlags_SpanTextWidth = 1 << 13, // Narrow hit box + narrow hovering highlight, will only cover the label text. + ImGuiTreeNodeFlags_SpanAllColumns = 1 << 14, // Frame will span all columns of its container table (text will still fit in current column) + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 15, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 16, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS @@ -1544,41 +1552,45 @@ enum ImGuiCol_ // - The enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. // During initialization or between frames, feel free to just poke into ImGuiStyle directly. // - Tip: Use your programming IDE navigation facilities on the names in the _second column_ below to find the actual members and their description. -// In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. -// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. // - When changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. enum ImGuiStyleVar_ { - // Enum name --------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) - ImGuiStyleVar_Alpha, // float Alpha - ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha - ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding - ImGuiStyleVar_WindowRounding, // float WindowRounding - ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize - ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize - ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign - ImGuiStyleVar_ChildRounding, // float ChildRounding - ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize - ImGuiStyleVar_PopupRounding, // float PopupRounding - ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize - ImGuiStyleVar_FramePadding, // ImVec2 FramePadding - ImGuiStyleVar_FrameRounding, // float FrameRounding - ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize - ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing - ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing - ImGuiStyleVar_IndentSpacing, // float IndentSpacing - ImGuiStyleVar_CellPadding, // ImVec2 CellPadding - ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize - ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding - ImGuiStyleVar_GrabMinSize, // float GrabMinSize - ImGuiStyleVar_GrabRounding, // float GrabRounding - ImGuiStyleVar_TabRounding, // float TabRounding - ImGuiStyleVar_TabBarBorderSize, // float TabBarBorderSize - ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign - ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign - ImGuiStyleVar_SeparatorTextBorderSize,// float SeparatorTextBorderSize - ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign - ImGuiStyleVar_SeparatorTextPadding,// ImVec2 SeparatorTextPadding + // Enum name -------------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_TabBorderSize, // float TabBorderSize + ImGuiStyleVar_TabBarBorderSize, // float TabBarBorderSize + ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle + ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize + ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign + ImGuiStyleVar_SeparatorTextPadding, // ImVec2 SeparatorTextPadding ImGuiStyleVar_COUNT }; @@ -1993,7 +2005,7 @@ struct ImGuiStyle float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). - ImVec2 CellPadding; // Padding within a table cell. CellPadding.y may be altered between different rows. + ImVec2 CellPadding; // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). @@ -2007,6 +2019,7 @@ struct ImGuiStyle float TabMinWidthForCloseButton; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. float TableAngledHeadersAngle; // Angle of angled headers (supported values range from -50.0f degrees to +50.0f degrees). + ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. @@ -2040,6 +2053,9 @@ struct ImGuiStyle //----------------------------------------------------------------------------- // Communicate most settings and inputs/outputs to Dear ImGui using this structure. // Access via ImGui::GetIO(). Read 'Programmer guide' section in .cpp file for general usage. +// It is generally expected that: +// - initialization: backends and user code writes to ImGuiIO. +// - main loop: backends writes to ImGuiIO, user code and imgui code reads from ImGuiIO. //----------------------------------------------------------------------------- // [Internal] Storage used by IsKeyDown(), IsKeyPressed() etc functions. @@ -2186,16 +2202,6 @@ struct ImGuiIO int MetricsActiveWindows; // Number of active windows ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. - // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. - // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). - // Old (<1.87): ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Space]) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) -#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO - int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. - bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. - float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. - //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. -#endif - //------------------------------------------------------------------ // [Internal] Dear ImGui will maintain those fields. Forward compatibility not guaranteed! //------------------------------------------------------------------ @@ -2241,6 +2247,16 @@ struct ImGuiIO ImWchar16 InputQueueSurrogate; // For AddInputCharacterUTF16() ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform backend). Fill using AddInputCharacter() helper. + // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. + // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). + // Old (<1.87): ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Space]) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. + bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. + float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. + //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. +#endif + IMGUI_API ImGuiIO(); }; @@ -2265,6 +2281,8 @@ struct ImGuiInputTextCallbackData void* UserData; // What user passed to InputText() // Read-only // Arguments for the different callback events + // - During Resize callback, Buf will be same as your input buffer. + // - However, during Completion/History/Always callback, Buf always points to our own internal data (it is not the same as your buffer)! Changes to it will be reflected into your own buffer shortly after the callback. // - To modify the text buffer in a callback, prefer using the InsertChars() / DeleteChars() function. InsertChars() will take care of calling the resize callback if necessary. // - If you know your edits are not going to resize the underlying buffer allocation, you may modify the contents of 'Buf[]' directly. You need to update 'BufTextLen' accordingly (0 <= BufTextLen < BufSize) and set 'BufDirty'' to true so InputText can update its internal state. ImWchar EventChar; // Character input // Read-write // [CharFilter] Replace character with another one, or set to zero to drop. return 1 is equivalent to setting EventChar=0; @@ -2708,15 +2726,15 @@ struct ImDrawList // [Internal, used while building lists] unsigned int _VtxCurrentIdx; // [Internal] generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) - const char* _OwnerName; // Pointer to owner window's name for debugging ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) - ImVector _ClipRectStack; // [Internal] - ImVector _TextureIdStack; // [Internal] ImVector _Path; // [Internal] current path building ImDrawCmdHeader _CmdHeader; // [Internal] template of active commands. Fields should match those of CmdBuffer.back(). ImDrawListSplitter _Splitter; // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!) + ImVector _ClipRectStack; // [Internal] + ImVector _TextureIdStack; // [Internal] float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content + const char* _OwnerName; // Pointer to owner window's name for debugging // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) ImDrawList(ImDrawListSharedData* shared_data) { memset(this, 0, sizeof(*this)); _Data = shared_data; } @@ -2749,15 +2767,20 @@ struct ImDrawList IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); - IMGUI_API void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f); - IMGUI_API void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0); + IMGUI_API void AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0); IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); - IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); - IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); IMGUI_API void AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0); // Cubic Bezier (4 control points) IMGUI_API void AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments = 0); // Quadratic Bezier (3 control points) + // General polygon + // - Only simple polygons are supported by filling functions (no self-intersections, no holes). + // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience fo user but not used by main library. + IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); + IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); + // Image primitives // - Read FAQ to understand what ImTextureID is. // - "p_min" and "p_max" represent the upper-left and lower-right corners of the rectangle. @@ -2773,10 +2796,11 @@ struct ImDrawList inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle - IMGUI_API void PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse + IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); @@ -2809,6 +2833,9 @@ struct ImDrawList inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index // Obsolete names + //inline void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0) { PathEllipticalArcTo(center, ImVec2(radius_x, radius_y), rot, a_min, a_max, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) //inline void AddBezierCurve(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0) { AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); } // OBSOLETED in 1.80 (Jan 2021) //inline void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); } // OBSOLETED in 1.80 (Jan 2021) @@ -3163,15 +3190,6 @@ struct ImGuiPlatformImeData // Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. //----------------------------------------------------------------------------- -namespace ImGui -{ -#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO - IMGUI_API ImGuiKey GetKeyIndex(ImGuiKey key); // map ImGuiKey_* values into legacy native key index. == io.KeyMap[key] -#else - static inline ImGuiKey GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END && "ImGuiKey and native_index was merged together and native_index is disabled by IMGUI_DISABLE_OBSOLETE_KEYIO. Please switch to ImGuiKey."); return key; } -#endif -} - #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS namespace ImGui { @@ -3193,6 +3211,9 @@ namespace ImGui // OBSOLETED in 1.88 (from May 2022) static inline void CaptureKeyboardFromApp(bool want_capture_keyboard = true) { SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } // Renamed as name was misleading + removed default value. static inline void CaptureMouseFromApp(bool want_capture_mouse = true) { SetNextFrameWantCaptureMouse(want_capture_mouse); } // Renamed as name was misleading + removed default value. + // OBSOLETED in 1.87 (from February 2022) + IMGUI_API ImGuiKey GetKeyIndex(ImGuiKey key); // Map ImGuiKey_* values into legacy native key index. == io.KeyMap[key]. When using a 1.87+ backend using io.AddKeyEvent(), calling GetKeyIndex() with ANY ImGuiKey_XXXX values will return the same value! + //static inline ImGuiKey GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END); return key; } // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) //-- OBSOLETED in 1.86 (from November 2021) diff --git a/core/deps/imgui/imgui_demo.cpp b/core/deps/imgui/imgui_demo.cpp index 707c14d38..0130cdb41 100644 --- a/core/deps/imgui/imgui_demo.cpp +++ b/core/deps/imgui/imgui_demo.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 // (demo code) // Help: @@ -7,9 +7,14 @@ // - Need help integrating Dear ImGui in your codebase? // - Read Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started // - Read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. -// Read imgui.cpp for more details, documentation and comments. +// Read top of imgui.cpp and imgui.h for many details, documentation, comments, links. // Get the latest version at https://github.com/ocornut/imgui +// How to easily locate code? +// - Use the Item Picker to debug break in code by clicking any widgets: https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Browse an online version the demo with code linked to hovered widgets: https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html +// - Find a visible string and search for it in the code! + //--------------------------------------------------- // PLEASE DO NOT REMOVE THIS FILE FROM YOUR PROJECT! //--------------------------------------------------- @@ -54,8 +59,9 @@ // Because we can't assume anything about your support of maths operators, we cannot use them in imgui_demo.cpp. // Navigating this file: -// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. -// - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. /* @@ -130,6 +136,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. #pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind #pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size @@ -899,13 +906,18 @@ static void ShowDemoWindowWidgets() if (i == 0) ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode((void*)(intptr_t)i, "Child %d", i)) + // Here we use PushID() to generate a unique base ID, and then the "" used as TreeNode id won't conflict. + // An alternative to using 'PushID() + TreeNode("", ...)' to generate a unique ID is to use 'TreeNode((void*)(intptr_t)i, ...)', + // aka generate a dummy pointer-sized value to be hashed. The demo below uses that technique. Both are fine. + ImGui::PushID(i); + if (ImGui::TreeNode("", "Child %d", i)) { ImGui::Text("blah blah"); ImGui::SameLine(); if (ImGui::SmallButton("button")) {} ImGui::TreePop(); } + ImGui::PopID(); } ImGui::TreePop(); } @@ -923,7 +935,10 @@ static void ShowDemoWindowWidgets() ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", &base_flags, ImGuiTreeNodeFlags_OpenOnDoubleClick); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAvailWidth", &base_flags, ImGuiTreeNodeFlags_SpanAvailWidth); ImGui::SameLine(); HelpMarker("Extend hit area to all available width instead of allowing more items to be laid out after the node."); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &base_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanTextWidth", &base_flags, ImGuiTreeNodeFlags_SpanTextWidth); ImGui::SameLine(); HelpMarker("Reduce hit area to the text label and a bit of margin."); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &base_flags, ImGuiTreeNodeFlags_SpanAllColumns); ImGui::SameLine(); HelpMarker("For use in Tables only."); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_AllowOverlap", &base_flags, ImGuiTreeNodeFlags_AllowOverlap); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_Framed", &base_flags, ImGuiTreeNodeFlags_Framed); ImGui::SameLine(); HelpMarker("Draw frame with background (e.g. for CollapsingHeader)"); ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position); ImGui::Checkbox("Test tree node as drag source", &test_drag_and_drop); ImGui::Text("Hello!"); @@ -956,6 +971,12 @@ static void ShowDemoWindowWidgets() ImGui::Text("This is a drag and drop source"); ImGui::EndDragDropSource(); } + if (i == 2) + { + // Item 2 has an additional inline button to help demonstrate SpanTextWidth. + ImGui::SameLine(); + if (ImGui::SmallButton("button")) {} + } if (node_open) { ImGui::BulletText("Blah blah\nBlah Blah"); @@ -1288,6 +1309,7 @@ static void ShowDemoWindowWidgets() } ImGui::EndListBox(); } + ImGui::SameLine(); HelpMarker("Here we are sharing selection state between both boxes."); // Custom size: use all width, 5 items tall ImGui::Text("Full-width:"); @@ -1818,10 +1840,10 @@ static void ShowDemoWindowWidgets() ImGui::Checkbox("Animate", &animate); // Plot as lines and plot as histogram - IMGUI_DEMO_MARKER("Widgets/Plotting/PlotLines, PlotHistogram"); static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; ImGui::PlotLines("Frame Times", arr, IM_ARRAYSIZE(arr)); ImGui::PlotHistogram("Histogram", arr, IM_ARRAYSIZE(arr), 0, NULL, 0.0f, 1.0f, ImVec2(0, 80.0f)); + //ImGui::SameLine(); HelpMarker("Consider using ImPlot instead!"); // Fill an array of contiguous float values to plot // Tip: If your float aren't contiguous but part of a structure, you can pass a pointer to your first float @@ -1871,15 +1893,17 @@ static void ShowDemoWindowWidgets() ImGui::PlotHistogram("Histogram", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0, 80)); ImGui::Separator(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Progress Bars"); + if (ImGui::TreeNode("Progress Bars")) + { // Animate a simple progress bar - IMGUI_DEMO_MARKER("Widgets/Plotting/ProgressBar"); static float progress = 0.0f, progress_dir = 1.0f; - if (animate) - { - progress += progress_dir * 0.4f * ImGui::GetIO().DeltaTime; - if (progress >= +1.1f) { progress = +1.1f; progress_dir *= -1.0f; } - if (progress <= -0.1f) { progress = -0.1f; progress_dir *= -1.0f; } - } + progress += progress_dir * 0.4f * ImGui::GetIO().DeltaTime; + if (progress >= +1.1f) { progress = +1.1f; progress_dir *= -1.0f; } + if (progress <= -0.1f) { progress = -0.1f; progress_dir *= -1.0f; } // Typically we would use ImVec2(-1.0f,0.0f) or ImVec2(-FLT_MIN,0.0f) to use all available width, // or ImVec2(width,0.0f) for a specified width. ImVec2(0.0f,0.0f) uses ItemWidth. @@ -1891,6 +1915,13 @@ static void ShowDemoWindowWidgets() char buf[32]; sprintf(buf, "%d/%d", (int)(progress_saturated * 1753), 1753); ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), buf); + + // Pass an animated negative value, e.g. -1.0f * (float)ImGui::GetTime() is the recommended value. + // Adjust the factor if you want to adjust the animation speed. + ImGui::ProgressBar(-1.0f * (float)ImGui::GetTime(), ImVec2(0.0f, 0.0f), "Searching.."); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Text("Indeterminate"); + ImGui::TreePop(); } @@ -2481,9 +2512,7 @@ static void ShowDemoWindowWidgets() { IM_UNUSED(payload); ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed); - ImGui::BeginTooltip(); - ImGui::Text("Cannot drop here!"); - ImGui::EndTooltip(); + ImGui::SetTooltip("Cannot drop here!"); } ImGui::EndDragDropTarget(); } @@ -5139,7 +5168,8 @@ static void ShowDemoWindowTables() static ImGuiTableFlags flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; static ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAllColumns; - ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &tree_node_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &tree_node_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanTextWidth", &tree_node_flags, ImGuiTreeNodeFlags_SpanTextWidth); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &tree_node_flags, ImGuiTreeNodeFlags_SpanAllColumns); HelpMarker("See \"Columns flags\" section to configure how indentation is applied to individual columns."); @@ -5328,6 +5358,17 @@ static void ShowDemoWindowTables() ImGui::SliderInt("Frozen rows", &frozen_rows, 0, 2); ImGui::CheckboxFlags("Disable header contributing to column width", &column_flags, ImGuiTableColumnFlags_NoHeaderWidth); + if (ImGui::TreeNode("Style settings")) + { + ImGui::SameLine(); + HelpMarker("Giving access to some ImGuiStyle value in this demo for convenience."); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderAngle("style.TableAngledHeadersAngle", &ImGui::GetStyle().TableAngledHeadersAngle, -50.0f, +50.0f); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderFloat2("style.TableAngledHeadersTextAlign", (float*)&ImGui::GetStyle().TableAngledHeadersTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::TreePop(); + } + if (ImGui::BeginTable("table_angled_headers", columns_count, table_flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 12))) { ImGui::TableSetupColumn(column_names[0], ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_NoReorder); @@ -5479,6 +5520,7 @@ static void ShowDemoWindowTables() HelpMarker("Multiple tables with the same identifier will share their settings, width, visibility, order etc."); static ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings; + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); ImGui::CheckboxFlags("ImGuiTableFlags_SizingFixedFit", &flags, ImGuiTableFlags_SizingFixedFit); ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); @@ -6351,7 +6393,7 @@ void ImGui::ShowAboutWindow(bool* p_open) ImGui::Separator(); ImGui::Text("By Omar Cornut and all Dear ImGui contributors."); ImGui::Text("Dear ImGui is licensed under the MIT License, see LICENSE for more information."); - ImGui::Text("If your company uses this, please consider sponsoring the project!"); + ImGui::Text("If your company uses this, please consider funding the project."); static bool show_config_info = false; ImGui::Checkbox("Config/Build Information", &show_config_info); @@ -6616,6 +6658,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SeparatorText("Tables"); ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderAngle("TableAngledHeadersAngle", &style.TableAngledHeadersAngle, -50.0f, +50.0f); + ImGui::SliderFloat2("TableAngledHeadersTextAlign", (float*)&style.TableAngledHeadersTextAlign, 0.0f, 1.0f, "%.2f"); ImGui::SeparatorText("Widgets"); ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); @@ -7789,7 +7832,7 @@ static void ShowExampleAppConstrainedResize(bool* p_open) if (type == 2) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 0), ImVec2(-1, FLT_MAX)); // Resize vertical + lock current width if (type == 3) ImGui::SetNextWindowSizeConstraints(ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Resize horizontal + lock current height if (type == 4) ImGui::SetNextWindowSizeConstraints(ImVec2(400, -1), ImVec2(500, -1)); // Width Between and 400 and 500 - if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 500), ImVec2(-1, FLT_MAX)); // Height at least 400 + if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 400), ImVec2(-1, FLT_MAX)); // Height at least 400 if (type == 6) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::AspectRatio, (void*)&aspect_ratio); // Aspect ratio if (type == 7) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Square); // Always Square if (type == 8) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Step, (void*)&fixed_step); // Fixed Step @@ -7964,6 +8007,14 @@ static void ShowExampleAppWindowTitles(bool*) // [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() //----------------------------------------------------------------------------- +// Add a |_| looking shape +static void PathConcaveShape(ImDrawList* draw_list, float x, float y, float sz) +{ + const ImVec2 pos_norms[] = { { 0.0f, 0.0f }, { 0.3f, 0.0f }, { 0.3f, 0.7f }, { 0.7f, 0.7f }, { 0.7f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f } }; + for (const ImVec2& p : pos_norms) + draw_list->PathLineTo(ImVec2(x + 0.5f + (int)(sz * p.x), y + 0.5f + (int)(sz * p.y))); +} + // Demonstrate using the low-level ImDrawList to draw custom shapes. static void ShowExampleAppCustomRendering(bool* p_open) { @@ -8047,12 +8098,14 @@ static void ShowExampleAppCustomRendering(bool* p_open) float th = (n == 0) ? 1.0f : thickness; draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle - draw_list->AddEllipse(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, sz*0.3f, col, -0.3f, circle_segments, th); x += sz + spacing; // Ellipse + draw_list->AddEllipse(ImVec2(x + sz*0.5f, y + sz*0.5f), ImVec2(sz*0.5f, sz*0.3f), col, -0.3f, circle_segments, th); x += sz + spacing; // Ellipse draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, ImDrawFlags_None, th); x += sz + spacing; // Square draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, ImDrawFlags_None, th); x += sz + spacing; // Square with all rounded corners draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners draw_list->AddTriangle(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col, th);x += sz + spacing; // Triangle //draw_list->AddTriangle(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col, th);x+= sz*0.4f + spacing; // Thin triangle + PathConcaveShape(draw_list, x, y, sz); draw_list->PathStroke(col, ImDrawFlags_Closed, th); x += sz + spacing; // Concave Shape + //draw_list->AddPolyline(concave_shape, IM_ARRAYSIZE(concave_shape), col, ImDrawFlags_Closed, th); draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y), col, th); x += sz + spacing; // Horizontal line (note: drawing a filled rectangle will be faster!) draw_list->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!) draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line @@ -8076,12 +8129,13 @@ static void ShowExampleAppCustomRendering(bool* p_open) // Filled shapes draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, col, ngon_sides); x += sz + spacing; // N-gon draw_list->AddCircleFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, col, circle_segments); x += sz + spacing; // Circle - draw_list->AddEllipseFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, sz * 0.3f, col, -0.3f, circle_segments); x += sz + spacing;// Ellipse + draw_list->AddEllipseFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), ImVec2(sz * 0.5f, sz * 0.3f), col, -0.3f, circle_segments); x += sz + spacing;// Ellipse draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col); x += sz + spacing; // Square draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f); x += sz + spacing; // Square with all rounded corners draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br); x += sz + spacing; // Square with two rounded corners draw_list->AddTriangleFilled(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col); x += sz + spacing; // Triangle //draw_list->AddTriangleFilled(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col); x += sz*0.4f + spacing; // Thin triangle + PathConcaveShape(draw_list, x, y, sz); draw_list->PathFillConcave(col); x += sz + spacing; // Concave shape draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + thickness), col); x += sz + spacing; // Horizontal line (faster than AddLine, but only handle integer thickness) draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + thickness, y + sz), col); x += spacing * 2.0f;// Vertical line (faster than AddLine, but only handle integer thickness) draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + 1, y + 1), col); x += sz; // Pixel (faster than AddLine) @@ -8097,15 +8151,10 @@ static void ShowExampleAppCustomRendering(bool* p_open) draw_list->PathFillConvex(col); x += sz + spacing; - // Cubic Bezier Curve (4 control points): this is concave so not drawing it yet - //draw_list->PathLineTo(ImVec2(x + cp4[0].x, y + cp4[0].y)); - //draw_list->PathBezierCubicCurveTo(ImVec2(x + cp4[1].x, y + cp4[1].y), ImVec2(x + cp4[2].x, y + cp4[2].y), ImVec2(x + cp4[3].x, y + cp4[3].y), curve_segments); - //draw_list->PathFillConvex(col); - //x += sz + spacing; - draw_list->AddRectFilledMultiColor(ImVec2(x, y), ImVec2(x + sz, y + sz), IM_COL32(0, 0, 0, 255), IM_COL32(255, 0, 0, 255), IM_COL32(255, 255, 0, 255), IM_COL32(0, 255, 0, 255)); + x += sz + spacing; - ImGui::Dummy(ImVec2((sz + spacing) * 12.2f, (sz + spacing) * 3.0f)); + ImGui::Dummy(ImVec2((sz + spacing) * 13.2f, (sz + spacing) * 3.0f)); ImGui::PopItemWidth(); ImGui::EndTabItem(); } diff --git a/core/deps/imgui/imgui_draw.cpp b/core/deps/imgui/imgui_draw.cpp index 1319a6e1d..04aba119e 100644 --- a/core/deps/imgui/imgui_draw.cpp +++ b/core/deps/imgui/imgui_draw.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 // (drawing and font code) /* @@ -8,6 +8,7 @@ Index of this file: // [SECTION] STB libraries implementation // [SECTION] Style functions // [SECTION] ImDrawList +// [SECTION] ImTriangulator, ImDrawList concave polygon fill // [SECTION] ImDrawListSplitter // [SECTION] ImDrawData // [SECTION] Helpers ShadeVertsXXX functions @@ -64,6 +65,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision #pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind #pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used @@ -383,6 +385,7 @@ void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) } // Initialize before use in a new frame. We always have a command ready in the buffer. +// In the majority of cases, you would want to call PushClipRect() and PushTextureID() after this. void ImDrawList::_ResetForNewFrame() { // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory. @@ -1217,10 +1220,10 @@ void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, floa } } -void ImDrawList::PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments) +void ImDrawList::PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments) { if (num_segments <= 0) - num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y)); // A bit pessimistic, maybe there's a better computation to do here. + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. _Path.reserve(_Path.Size + (num_segments + 1)); @@ -1229,11 +1232,10 @@ void ImDrawList::PathEllipticalArcTo(const ImVec2& center, float radius_x, float for (int i = 0; i <= num_segments; i++) { const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); - ImVec2 point(ImCos(a) * radius_x, ImSin(a) * radius_y); - const float rel_x = (point.x * cos_rot) - (point.y * sin_rot); - const float rel_y = (point.x * sin_rot) + (point.y * cos_rot); - point.x = rel_x + center.x; - point.y = rel_y + center.y; + ImVec2 point(ImCos(a) * radius.x, ImSin(a) * radius.y); + const ImVec2 rel((point.x * cos_rot) - (point.y * sin_rot), (point.x * sin_rot) + (point.y * cos_rot)); + point.x = rel.x + center.x; + point.y = rel.y + center.y; _Path.push_back(point); } } @@ -1558,31 +1560,31 @@ void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, in } // Ellipse -void ImDrawList::AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot, int num_segments, float thickness) +void ImDrawList::AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments, float thickness) { if ((col & IM_COL32_A_MASK) == 0) return; if (num_segments <= 0) - num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y)); // A bit pessimistic, maybe there's a better computation to do here. + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; - PathEllipticalArcTo(center, radius_x, radius_y, rot, 0.0f, a_max, num_segments - 1); + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); PathStroke(col, true, thickness); } -void ImDrawList::AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot, int num_segments) +void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments) { if ((col & IM_COL32_A_MASK) == 0) return; if (num_segments <= 0) - num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y)); // A bit pessimistic, maybe there's a better computation to do here. + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; - PathEllipticalArcTo(center, radius_x, radius_y, rot, 0.0f, a_max, num_segments - 1); + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); PathFillConvex(col); } @@ -1613,10 +1615,11 @@ void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, if ((col & IM_COL32_A_MASK) == 0) return; + // Accept null ranges + if (text_begin == text_end || text_begin[0] == 0) + return; if (text_end == NULL) text_end = text_begin + strlen(text_begin); - if (text_begin == text_end) - return; // Pull default font/size from the shared ImDrawListSharedData instance if (font == NULL) @@ -1700,6 +1703,316 @@ void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_mi PopTextureID(); } +//----------------------------------------------------------------------------- +// [SECTION] ImTriangulator, ImDrawList concave polygon fill +//----------------------------------------------------------------------------- +// Triangulate concave polygons. Based on "Triangulation by Ear Clipping" paper, O(N^2) complexity. +// Reference: https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf +// Provided as a convenience for user but not used by main library. +//----------------------------------------------------------------------------- +// - ImTriangulator [Internal] +// - AddConcavePolyFilled() +//----------------------------------------------------------------------------- + +enum ImTriangulatorNodeType +{ + ImTriangulatorNodeType_Convex, + ImTriangulatorNodeType_Ear, + ImTriangulatorNodeType_Reflex +}; + +struct ImTriangulatorNode +{ + ImTriangulatorNodeType Type; + int Index; + ImVec2 Pos; + ImTriangulatorNode* Next; + ImTriangulatorNode* Prev; + + void Unlink() { Next->Prev = Prev; Prev->Next = Next; } +}; + +struct ImTriangulatorNodeSpan +{ + ImTriangulatorNode** Data = NULL; + int Size = 0; + + void push_back(ImTriangulatorNode* node) { Data[Size++] = node; } + void find_erase_unsorted(int idx) { for (int i = Size - 1; i >= 0; i--) if (Data[i]->Index == idx) { Data[i] = Data[Size - 1]; Size--; return; } } +}; + +struct ImTriangulator +{ + static int EstimateTriangleCount(int points_count) { return (points_count < 3) ? 0 : points_count - 2; } + static int EstimateScratchBufferSize(int points_count) { return sizeof(ImTriangulatorNode) * points_count + sizeof(ImTriangulatorNode*) * points_count * 2; } + + void Init(const ImVec2* points, int points_count, void* scratch_buffer); + void GetNextTriangle(unsigned int out_triangle[3]); // Return relative indexes for next triangle + + // Internal functions + void BuildNodes(const ImVec2* points, int points_count); + void BuildReflexes(); + void BuildEars(); + void FlipNodeList(); + bool IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const; + void ReclassifyNode(ImTriangulatorNode* node); + + // Internal members + int _TrianglesLeft = 0; + ImTriangulatorNode* _Nodes = NULL; + ImTriangulatorNodeSpan _Ears; + ImTriangulatorNodeSpan _Reflexes; +}; + +// Distribute storage for nodes, ears and reflexes. +// FIXME-OPT: if everything is convex, we could report it to caller and let it switch to an convex renderer +// (this would require first building reflexes to bail to convex if empty, without even building nodes) +void ImTriangulator::Init(const ImVec2* points, int points_count, void* scratch_buffer) +{ + IM_ASSERT(scratch_buffer != NULL && points_count >= 3); + _TrianglesLeft = EstimateTriangleCount(points_count); + _Nodes = (ImTriangulatorNode*)scratch_buffer; // points_count x Node + _Ears.Data = (ImTriangulatorNode**)(_Nodes + points_count); // points_count x Node* + _Reflexes.Data = (ImTriangulatorNode**)(_Nodes + points_count) + points_count; // points_count x Node* + BuildNodes(points, points_count); + BuildReflexes(); + BuildEars(); +} + +void ImTriangulator::BuildNodes(const ImVec2* points, int points_count) +{ + for (int i = 0; i < points_count; i++) + { + _Nodes[i].Type = ImTriangulatorNodeType_Convex; + _Nodes[i].Index = i; + _Nodes[i].Pos = points[i]; + _Nodes[i].Next = _Nodes + i + 1; + _Nodes[i].Prev = _Nodes + i - 1; + } + _Nodes[0].Prev = _Nodes + points_count - 1; + _Nodes[points_count - 1].Next = _Nodes; +} + +void ImTriangulator::BuildReflexes() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (ImTriangleIsClockwise(n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Reflex; + _Reflexes.push_back(n1); + } +} + +void ImTriangulator::BuildEars() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (n1->Type != ImTriangulatorNodeType_Convex) + continue; + if (!IsEar(n1->Prev->Index, n1->Index, n1->Next->Index, n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Ear; + _Ears.push_back(n1); + } +} + +void ImTriangulator::GetNextTriangle(unsigned int out_triangle[3]) +{ + if (_Ears.Size == 0) + { + FlipNodeList(); + + ImTriangulatorNode* node = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, node = node->Next) + node->Type = ImTriangulatorNodeType_Convex; + _Reflexes.Size = 0; + BuildReflexes(); + BuildEars(); + + // If we still don't have ears, it means geometry is degenerated. + if (_Ears.Size == 0) + { + // Return first triangle available, mimicking the behavior of convex fill. + IM_ASSERT(_TrianglesLeft > 0); // Geometry is degenerated + _Ears.Data[0] = _Nodes; + _Ears.Size = 1; + } + } + + ImTriangulatorNode* ear = _Ears.Data[--_Ears.Size]; + out_triangle[0] = ear->Prev->Index; + out_triangle[1] = ear->Index; + out_triangle[2] = ear->Next->Index; + + ear->Unlink(); + if (ear == _Nodes) + _Nodes = ear->Next; + + ReclassifyNode(ear->Prev); + ReclassifyNode(ear->Next); + _TrianglesLeft--; +} + +void ImTriangulator::FlipNodeList() +{ + ImTriangulatorNode* prev = _Nodes; + ImTriangulatorNode* temp = _Nodes; + ImTriangulatorNode* current = _Nodes->Next; + prev->Next = prev; + prev->Prev = prev; + while (current != _Nodes) + { + temp = current->Next; + + current->Next = prev; + prev->Prev = current; + _Nodes->Next = current; + current->Prev = _Nodes; + + prev = current; + current = temp; + } + _Nodes = prev; +} + +// A triangle is an ear is no other vertex is inside it. We can test reflexes vertices only (see reference algorithm) +bool ImTriangulator::IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const +{ + ImTriangulatorNode** p_end = _Reflexes.Data + _Reflexes.Size; + for (ImTriangulatorNode** p = _Reflexes.Data; p < p_end; p++) + { + ImTriangulatorNode* reflex = *p; + if (reflex->Index != i0 && reflex->Index != i1 && reflex->Index != i2) + if (ImTriangleContainsPoint(v0, v1, v2, reflex->Pos)) + return false; + } + return true; +} + +void ImTriangulator::ReclassifyNode(ImTriangulatorNode* n1) +{ + // Classify node + ImTriangulatorNodeType type; + const ImTriangulatorNode* n0 = n1->Prev; + const ImTriangulatorNode* n2 = n1->Next; + if (!ImTriangleIsClockwise(n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Reflex; + else if (IsEar(n0->Index, n1->Index, n2->Index, n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Ear; + else + type = ImTriangulatorNodeType_Convex; + + // Update lists when a type changes + if (type == n1->Type) + return; + if (n1->Type == ImTriangulatorNodeType_Reflex) + _Reflexes.find_erase_unsorted(n1->Index); + else if (n1->Type == ImTriangulatorNodeType_Ear) + _Ears.find_erase_unsorted(n1->Index); + if (type == ImTriangulatorNodeType_Reflex) + _Reflexes.push_back(n1); + else if (type == ImTriangulatorNodeType_Ear) + _Ears.push_back(n1); + n1->Type = type; +} + +// Use ear-clipping algorithm to triangulate a simple polygon (no self-interaction, no holes). +// (Reminder: we don't perform any coarse clipping/culling in ImDrawList layer! +// It is up to caller to ensure not making costly calls that will be outside of visible area. +// As concave fill is noticeably more expensive than other primitives, be mindful of this... +// Caller can build AABB of points, and avoid filling if 'draw_list->_CmdHeader.ClipRect.Overlays(points_bb) == false') +void ImDrawList::AddConcavePolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + ImTriangulator triangulator; + unsigned int triangle[3]; + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2) * 3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (triangle[0] << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (triangle[1] << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (triangle[2] << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2) * 3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx + triangle[0]); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + triangle[1]); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + triangle[2]); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} //----------------------------------------------------------------------------- // [SECTION] ImDrawListSplitter @@ -2672,8 +2985,8 @@ static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) int unscaled_ascent, unscaled_descent, unscaled_line_gap; stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); - const float ascent = ImTrunc(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1)); - const float descent = ImTrunc(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1)); + const float ascent = ImCeil(unscaled_ascent * font_scale); + const float descent = ImFloor(unscaled_descent * font_scale); ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); const float font_off_x = cfg.GlyphOffset.x; const float font_off_y = cfg.GlyphOffset.y + IM_ROUND(dst_font->Ascent); @@ -3768,6 +4081,8 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, Im { x = start_x; y += line_height; + if (y > clip_rect.w) + break; // break out of main loop word_wrap_eol = NULL; s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks continue; diff --git a/core/deps/imgui/imgui_internal.h b/core/deps/imgui/imgui_internal.h index eee791d04..905b8195a 100644 --- a/core/deps/imgui/imgui_internal.h +++ b/core/deps/imgui/imgui_internal.h @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 // (internal structures/api) // You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility. @@ -87,6 +87,8 @@ Index of this file: #pragma clang diagnostic ignored "-Wdouble-promotion" #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision #pragma clang diagnostic ignored "-Wmissing-noreturn" // warning: function 'xxx' could be declared with attribute 'noreturn' +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind @@ -124,7 +126,7 @@ struct ImDrawListSharedData; // Data shared between all ImDrawList instan struct ImGuiColorMod; // Stacked color modifier, backup of modified data so we can restore it struct ImGuiContext; // Main Dear ImGui context struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine -struct ImGuiDataVarInfo; // Variable information (e.g. to avoid style variables from an enum) +struct ImGuiDataVarInfo; // Variable information (e.g. to access style variables from an enum) struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box @@ -146,6 +148,7 @@ struct ImGuiStyleMod; // Stacked style modifier, backup of modifie struct ImGuiTabBar; // Storage for a tab bar struct ImGuiTabItem; // Storage for a tab item (within a tab bar) struct ImGuiTable; // Storage for a table +struct ImGuiTableHeaderData; // Storage for TableAngledHeadersRow() struct ImGuiTableColumn; // Storage for one column of a table struct ImGuiTableInstanceData; // Storage for one instance of a same table struct ImGuiTableTempData; // Temporary storage for one table (one per table in the stack), shared between tables. @@ -179,6 +182,7 @@ typedef int ImGuiSeparatorFlags; // -> enum ImGuiSeparatorFlags_ // F typedef int ImGuiTextFlags; // -> enum ImGuiTextFlags_ // Flags: for TextEx() typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // Flags: for BeginTooltipEx() typedef int ImGuiTypingSelectFlags; // -> enum ImGuiTypingSelectFlags_ // Flags: for GetTypingSelectRequest() +typedef int ImGuiWindowRefreshFlags; // -> enum ImGuiWindowRefreshFlags_ // Flags: for SetNextWindowRefreshPolicy() typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); @@ -404,6 +408,7 @@ IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. +IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. // Helpers: File System #ifdef IMGUI_DISABLE_FILE_FUNCTIONS @@ -498,7 +503,8 @@ IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); -inline float ImTriangleArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ImFabs((a.x * (b.y - c.y)) + (b.x * (c.y - a.y)) + (c.x * (a.y - b.y))) * 0.5f; } +inline float ImTriangleArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ImFabs((a.x * (b.y - c.y)) + (b.x * (c.y - a.y)) + (c.x * (a.y - b.y))) * 0.5f; } +inline bool ImTriangleIsClockwise(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ((b.x - a.x) * (c.y - b.y)) - ((c.x - b.x) * (b.y - a.y)) > 0.0f; } // Helper: ImVec1 (1D vector) // (this odd construct is used to facilitate the transition between 1D and 2D, and the maintenance of some branches/patches) @@ -863,6 +869,7 @@ enum ImGuiInputTextFlagsPrivate_ ImGuiInputTextFlags_Multiline = 1 << 26, // For internal use by InputTextMultiline() ImGuiInputTextFlags_NoMarkEdited = 1 << 27, // For internal use by functions using InputText() before reformatting data ImGuiInputTextFlags_MergedItem = 1 << 28, // For internal use by TempInputText(), will skip calling ItemAdd(). Require bounding-box to strictly match. + ImGuiInputTextFlags_LocalizeDecimalPoint= 1 << 29, // For internal use by InputScalar() and TempInputScalar() }; // Extend ImGuiButtonFlags_ @@ -1111,6 +1118,15 @@ struct IMGUI_API ImGuiInputTextState }; +enum ImGuiWindowRefreshFlags_ +{ + ImGuiWindowRefreshFlags_None = 0, + ImGuiWindowRefreshFlags_TryToAvoidRefresh = 1 << 0, // [EXPERIMENTAL] Try to keep existing contents, USER MUST NOT HONOR BEGIN() RETURNING FALSE AND NOT APPEND. + ImGuiWindowRefreshFlags_RefreshOnHover = 1 << 1, // [EXPERIMENTAL] Always refresh on hover + ImGuiWindowRefreshFlags_RefreshOnFocus = 1 << 2, // [EXPERIMENTAL] Always refresh on focus + // Refresh policy/frequency, Load Balancing etc. +}; + enum ImGuiNextWindowDataFlags_ { ImGuiNextWindowDataFlags_None = 0, @@ -1123,6 +1139,7 @@ enum ImGuiNextWindowDataFlags_ ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, ImGuiNextWindowDataFlags_HasScroll = 1 << 7, ImGuiNextWindowDataFlags_HasChildFlags = 1 << 8, + ImGuiNextWindowDataFlags_HasRefreshPolicy = 1 << 9, }; // Storage for SetNexWindow** functions @@ -1144,6 +1161,7 @@ struct ImGuiNextWindowData void* SizeCallbackUserData; float BgAlphaVal; // Override background alpha ImVec2 MenuBarOffsetMinVal; // (Always on) This is not exposed publicly, so we don't clear it and it doesn't have a corresponding flag (could we? for consistency?) + ImGuiWindowRefreshFlags RefreshFlagsVal; ImGuiNextWindowData() { memset(this, 0, sizeof(*this)); } inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; } @@ -1292,7 +1310,7 @@ struct ImGuiPopupData { ImGuiID PopupId; // Set on OpenPopup() ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() - ImGuiWindow* BackupNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close + ImGuiWindow* RestoreNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close int ParentNavLayer; // Resolved on BeginPopup(). Actually a ImGuiNavLayer type (declared down below), initialized to -1 which is not part of an enum, but serves well-enough as "not any of layers" value int OpenFrameCount; // Set on OpenPopup() ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) @@ -1349,7 +1367,6 @@ enum ImGuiInputSource ImGuiInputSource_Mouse, // Note: may be Mouse or TouchScreen or Pen. See io.MouseSource to distinguish them. ImGuiInputSource_Keyboard, ImGuiInputSource_Gamepad, - ImGuiInputSource_Clipboard, // Currently only used by InputText() ImGuiInputSource_COUNT }; @@ -1578,6 +1595,7 @@ enum ImGuiNavMoveFlags_ ImGuiNavMoveFlags_Activate = 1 << 12, // Activate/select target item. ImGuiNavMoveFlags_NoSelect = 1 << 13, // Don't trigger selection by not setting g.NavJustMovedTo ImGuiNavMoveFlags_NoSetNavHighlight = 1 << 14, // Do not alter the visible state of keyboard vs mouse nav highlight + ImGuiNavMoveFlags_NoClearActiveId = 1 << 15, // (Experimental) Do not clear active id when applying move result }; enum ImGuiNavLayer @@ -1594,10 +1612,10 @@ struct ImGuiNavItemData ImGuiID FocusScopeId; // Init,Move // Best candidate focus scope ID ImRect RectRel; // Init,Move // Best candidate bounding box in window relative space ImGuiItemFlags InFlags; // ????,Move // Best candidate item flags - ImGuiSelectionUserData SelectionUserData;//I+Mov // Best candidate SetNextItemSelectionData() value. float DistBox; // Move // Best candidate box distance to current NavId float DistCenter; // Move // Best candidate center distance to current NavId float DistAxial; // Move // Best candidate axial distance to current NavId + ImGuiSelectionUserData SelectionUserData;//I+Mov // Best candidate SetNextItemSelectionData() value. ImGuiNavItemData() { Clear(); } void Clear() { Window = NULL; ID = FocusScopeId = 0; InFlags = 0; SelectionUserData = -1; DistBox = DistCenter = DistAxial = FLT_MAX; } @@ -2530,6 +2548,7 @@ struct IMGUI_API ImGuiWindow bool Collapsed; // Set when collapsing window to become only title-bar bool WantCollapseToggle; bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) + bool SkipRefresh; // [EXPERIMENTAL] Reuse previous frame drawn contents, Begin() returns false. bool Appearing; // Set during the frame where the window is appearing (or re-appearing) bool Hidden; // Do not display (== HiddenFrames*** > 0) bool IsFallbackWindow; // Set on the "Debug##Default" window. @@ -2769,13 +2788,24 @@ struct ImGuiTableColumn }; // Transient cell data stored per row. -// sizeof() ~ 6 +// sizeof() ~ 6 bytes struct ImGuiTableCellData { ImU32 BgColor; // Actual color ImGuiTableColumnIdx Column; // Column number }; +// Parameters for TableAngledHeadersRowEx() +// This may end up being refactored for more general purpose. +// sizeof() ~ 12 bytes +struct ImGuiTableHeaderData +{ + ImGuiTableColumnIdx Index; // Column index + ImU32 TextColor; + ImU32 BgColor0; + ImU32 BgColor1; +}; + // Per-instance data that needs preserving across frames (seemingly most others do not need to be preserved aside from debug needs. Does that means they could be moved to ImGuiTableTempData?) // sizeof() ~ 24 bytes struct ImGuiTableInstanceData @@ -2861,7 +2891,7 @@ struct IMGUI_API ImGuiTable ImGuiTableSortSpecs SortSpecs; // Public facing sorts specs, this is what we return in TableGetSortSpecs() ImGuiTableColumnIdx SortSpecsCount; ImGuiTableColumnIdx ColumnsEnabledCount; // Number of enabled columns (<= ColumnsCount) - ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns (<= ColumnsCount) + ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns using fixed width (<= ColumnsCount) ImGuiTableColumnIdx DeclColumnsCount; // Count calls to TableSetupColumn() ImGuiTableColumnIdx AngledHeadersCount; // Count columns with angled headers ImGuiTableColumnIdx HoveredColumnBody; // Index of column whose visible region is being hovered. Important: == ColumnsCount when hovering empty region after the right-most column! @@ -2914,12 +2944,13 @@ struct IMGUI_API ImGuiTable // Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table). // - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. // - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. -// sizeof() ~ 120 bytes. +// sizeof() ~ 136 bytes. struct IMGUI_API ImGuiTableTempData { int TableIndex; // Index in g.Tables.Buf[] pool float LastTimeActive; // Last timestamp this structure was used float AngledHeadersExtraWidth; // Used in EndTable() + ImVector AngledHeadersRequests; // Used in TableAngledHeadersRow() ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() ImDrawListSplitter DrawSplitter; @@ -2991,6 +3022,7 @@ namespace ImGui IMGUI_API ImGuiWindow* FindWindowByID(ImGuiID id); IMGUI_API ImGuiWindow* FindWindowByName(const char* name); IMGUI_API void UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window); + IMGUI_API void UpdateWindowSkipRefresh(ImGuiWindow* window); IMGUI_API ImVec2 CalcWindowNextAutoFitSize(ImGuiWindow* window); IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy); IMGUI_API bool IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent); @@ -3016,6 +3048,9 @@ namespace ImGui IMGUI_API int FindWindowDisplayIndex(ImGuiWindow* window); IMGUI_API ImGuiWindow* FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* window); + // Windows: Idle, Refresh Policies [EXPERIMENTAL] + IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); + // Fonts, drawing IMGUI_API void SetCurrentFont(ImFont* font); inline ImFont* GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } @@ -3303,7 +3338,7 @@ namespace ImGui IMGUI_API float TableGetHeaderAngledMaxLabelWidth(); IMGUI_API void TablePushBackgroundChannel(); IMGUI_API void TablePopBackgroundChannel(); - IMGUI_API void TableAngledHeadersRowEx(float angle, float max_label_width = 0.0f); + IMGUI_API void TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count); // Tables: Internals inline ImGuiTable* GetCurrentTable() { ImGuiContext& g = *GImGui; return g.CurrentTable; } diff --git a/core/deps/imgui/imgui_stdlib.cpp b/core/deps/imgui/imgui_stdlib.cpp new file mode 100644 index 000000000..cf69aa89a --- /dev/null +++ b/core/deps/imgui/imgui_stdlib.cpp @@ -0,0 +1,85 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +// See more C++ related extension (fmt, RAII, syntaxis sugar) on Wiki: +// https://github.com/ocornut/imgui/wiki/Useful-Extensions#cness + +#include "imgui.h" +#include "imgui_stdlib.h" + +// Clang warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#endif + +struct InputTextCallback_UserData +{ + std::string* Str; + ImGuiInputTextCallback ChainCallback; + void* ChainCallbackUserData; +}; + +static int InputTextCallback(ImGuiInputTextCallbackData* data) +{ + InputTextCallback_UserData* user_data = (InputTextCallback_UserData*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + // Resize string callback + // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. + std::string* str = user_data->Str; + IM_ASSERT(data->Buf == str->c_str()); + str->resize(data->BufTextLen); + data->Buf = (char*)str->c_str(); + } + else if (user_data->ChainCallback) + { + // Forward to user callback, if any + data->UserData = user_data->ChainCallbackUserData; + return user_data->ChainCallback(data); + } + return 0; +} + +bool ImGui::InputText(const char* label, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextMultiline(const char* label, std::string* str, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextWithHint(label, hint, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif diff --git a/core/deps/imgui/imgui_stdlib.h b/core/deps/imgui/imgui_stdlib.h new file mode 100644 index 000000000..835a808f2 --- /dev/null +++ b/core/deps/imgui/imgui_stdlib.h @@ -0,0 +1,21 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +// See more C++ related extension (fmt, RAII, syntaxis sugar) on Wiki: +// https://github.com/ocornut/imgui/wiki/Useful-Extensions#cness + +#pragma once + +#include + +namespace ImGui +{ + // ImGui::InputText() with std::string + // Because text input needs dynamic resizing, we need to setup a callback to grow the capacity + IMGUI_API bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* user_data = nullptr); + IMGUI_API bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* user_data = nullptr); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* user_data = nullptr); +} diff --git a/core/deps/imgui/imgui_tables.cpp b/core/deps/imgui/imgui_tables.cpp index 260df1a92..6815af5ad 100644 --- a/core/deps/imgui/imgui_tables.cpp +++ b/core/deps/imgui/imgui_tables.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 // (tables and columns code) /* @@ -24,8 +24,9 @@ Index of this file: */ // Navigating this file: -// - In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. -// - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. //----------------------------------------------------------------------------- // [SECTION] Commentary @@ -227,6 +228,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') #pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind #pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked @@ -498,6 +500,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->DeclColumnsCount = table->AngledHeadersCount = 0; if (previous_frame_active + 1 < g.FrameCount) table->IsActiveIdInTable = false; + table->AngledHeadersHeight = 0.0f; temp_data->AngledHeadersExtraWidth = 0.0f; // Using opaque colors facilitate overlapping lines of the grid, otherwise we'd need to improve TableDrawBorders() @@ -1066,6 +1069,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // - ClipRect.Max.x: using WorkMaxX instead of MaxX (aka including padding) makes things more consistent when resizing down, tho slightly detrimental to visibility in very-small column. // - ClipRect.Max.x: using MaxX makes it easier for header to receive hover highlight with no discontinuity and display sorting arrow. // - FIXME-TABLE: We want equal width columns to have equal (ClipRect.Max.x - WorkMinX) width, which means ClipRect.max.x cannot stray off host_clip_rect.Max.x else right-most column may appear shorter. + const float previous_instance_work_min_x = column->WorkMinX; column->WorkMinX = column->MinX + table->CellPaddingX + table->CellSpacingX1; column->WorkMaxX = column->MaxX - table->CellPaddingX - table->CellSpacingX2; // Expected max column->ItemWidth = ImTrunc(column->WidthGiven * 0.65f); @@ -1118,8 +1122,22 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // column->WorkMinX = ImLerp(column->WorkMinX, ImMax(column->StartX, column->MaxX - column->ContentWidthRowsUnfrozen), 0.5f); // Reset content width variables - column->ContentMaxXFrozen = column->ContentMaxXUnfrozen = column->WorkMinX; - column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX; + if (table->InstanceCurrent == 0) + { + column->ContentMaxXFrozen = column->WorkMinX; + column->ContentMaxXUnfrozen = column->WorkMinX; + column->ContentMaxXHeadersUsed = column->WorkMinX; + column->ContentMaxXHeadersIdeal = column->WorkMinX; + } + else + { + // As we store an absolute value to make per-cell updates faster, we need to offset values used for width computation. + const float offset_from_previous_instance = column->WorkMinX - previous_instance_work_min_x; + column->ContentMaxXFrozen += offset_from_previous_instance; + column->ContentMaxXUnfrozen += offset_from_previous_instance; + column->ContentMaxXHeadersUsed += offset_from_previous_instance; + column->ContentMaxXHeadersIdeal += offset_from_previous_instance; + } // Don't decrement auto-fit counters until container window got a chance to submit its items if (table->HostSkipItems == false) @@ -1240,7 +1258,7 @@ void ImGui::TableUpdateBorders(ImGuiTable* table) ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); const float hit_half_width = TABLE_RESIZE_SEPARATOR_HALF_THICKNESS; const float hit_y1 = (table->FreezeRowsCount >= 1 ? table->OuterRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight; - const float hit_y2_body = ImMax(table->OuterRect.Max.y, hit_y1 + table_instance->LastOuterHeight); + const float hit_y2_body = ImMax(table->OuterRect.Max.y, hit_y1 + table_instance->LastOuterHeight - table->AngledHeadersHeight); const float hit_y2_head = hit_y1 + table_instance->LastTopHeadersRowHeight; for (int order_n = 0; order_n < table->ColumnsCount; order_n++) @@ -1890,7 +1908,7 @@ void ImGui::TableEndRow(ImGuiTable* table) if (is_visible) { // Update data for TableGetHoveredRow() - if (table->HoveredColumnBody != -1 && g.IO.MousePos.y >= bg_y1 && g.IO.MousePos.y < bg_y2) + if (table->HoveredColumnBody != -1 && g.IO.MousePos.y >= bg_y1 && g.IO.MousePos.y < bg_y2 && table_instance->HoveredRowNext < 0) table_instance->HoveredRowNext = table->CurrentRow; // Decide of background color for the row @@ -3153,15 +3171,43 @@ void ImGui::TableHeader(const char* label) } // Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets. -// FIXME: highlight without ImGuiTableFlags_HighlightHoveredColumn // FIXME: No hit-testing/button on the angled header. void ImGui::TableAngledHeadersRow() { ImGuiContext& g = *GImGui; - TableAngledHeadersRowEx(g.Style.TableAngledHeadersAngle, 0.0f); + ImGuiTable* table = g.CurrentTable; + ImGuiTableTempData* temp_data = table->TempData; + temp_data->AngledHeadersRequests.resize(0); + temp_data->AngledHeadersRequests.reserve(table->ColumnsEnabledCount); + + // Which column needs highlight? + const ImGuiID row_id = GetID("##AngledHeaders"); + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + int highlight_column_n = table->HighlightColumnHeader; + if (highlight_column_n == -1 && table->HoveredColumnBody != -1) + if (table_instance->HoveredRowLast == 0 && table->HoveredColumnBorder == -1 && (g.ActiveId == 0 || g.ActiveId == row_id || (table->IsActiveIdInTable || g.DragDropActive))) + highlight_column_n = table->HoveredColumnBody; + + // Build up request + ImU32 col_header_bg = GetColorU32(ImGuiCol_TableHeaderBg); + ImU32 col_text = GetColorU32(ImGuiCol_Text); + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if ((column->Flags & ImGuiTableColumnFlags_AngledHeader) == 0) // Note: can't rely on ImGuiTableColumnFlags_IsVisible test here. + continue; + ImGuiTableHeaderData request = { (ImGuiTableColumnIdx)column_n, col_text, col_header_bg, (column_n == highlight_column_n) ? GetColorU32(ImGuiCol_Header) : 0 }; + temp_data->AngledHeadersRequests.push_back(request); + } + + // Render row + TableAngledHeadersRowEx(row_id, g.Style.TableAngledHeadersAngle, 0.0f, temp_data->AngledHeadersRequests.Data, temp_data->AngledHeadersRequests.Size); } -void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) +// Important: data must be fed left to right +void ImGui::TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count) { ImGuiContext& g = *GImGui; ImGuiTable* table = g.CurrentTable; @@ -3185,7 +3231,7 @@ void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) // Calculate our base metrics and set angled headers data _before_ the first call to TableNextRow() // FIXME-STYLE: Would it be better for user to submit 'max_label_width' or 'row_height' ? One can be derived from the other. const float header_height = g.FontSize + g.Style.CellPadding.x * 2.0f; - const float row_height = ImFabs(ImRotate(ImVec2(max_label_width, flip_label ? +header_height : -header_height), cos_a, sin_a).y); + const float row_height = ImTrunc(ImFabs(ImRotate(ImVec2(max_label_width, flip_label ? +header_height : -header_height), cos_a, sin_a).y)); table->AngledHeadersHeight = row_height; table->AngledHeadersSlope = (sin_a != 0.0f) ? (cos_a / sin_a) : 0.0f; const ImVec2 header_angled_vector = unit_right * (row_height / -sin_a); // vector from bottom-left to top-left, and from bottom-right to top-right @@ -3203,28 +3249,22 @@ void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) draw_list->AddRectFilled(ImVec2(table->BgClipRect.Min.x, row_r.Min.y), ImVec2(table->BgClipRect.Max.x, row_r.Max.y), GetColorU32(ImGuiCol_TableHeaderBg, 0.25f)); // FIXME-STYLE: Change row background with an arbitrary color. PushClipRect(ImVec2(clip_rect_min_x, table->BgClipRect.Min.y), table->BgClipRect.Max, true); // Span all columns - const ImGuiID row_id = GetID("##AngledHeaders"); ButtonBehavior(row_r, row_id, NULL, NULL); KeepAliveID(row_id); - ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); - int highlight_column_n = table->HighlightColumnHeader; - if (highlight_column_n == -1 && table->HoveredColumnBody != -1) - if (table_instance->HoveredRowLast == 0 && table->HoveredColumnBorder == -1 && (g.ActiveId == 0 || g.ActiveId == row_id || (table->IsActiveIdInTable || g.DragDropActive))) - highlight_column_n = table->HoveredColumnBody; + const float ascent_scaled = g.Font->Ascent * (g.FontSize / g.Font->FontSize); // FIXME: Standardize those scaling factors better + const float line_off_for_ascent_x = (ImMax((g.FontSize - ascent_scaled) * 0.5f, 0.0f) / -sin_a) * (flip_label ? -1.0f : 1.0f); + const ImVec2 padding = g.Style.CellPadding; // We will always use swapped component + const ImVec2 align = g.Style.TableAngledHeadersTextAlign; // Draw background and labels in first pass, then all borders. float max_x = 0.0f; - ImVec2 padding = g.Style.CellPadding; // We will always use swapped component for (int pass = 0; pass < 2; pass++) - for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + for (int order_n = 0; order_n < data_count; order_n++) { - if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) - continue; - const int column_n = table->DisplayOrderToIndex[order_n]; + const ImGuiTableHeaderData* request = &data[order_n]; + const int column_n = request->Index; ImGuiTableColumn* column = &table->Columns[column_n]; - if ((column->Flags & ImGuiTableColumnFlags_AngledHeader) == 0) // Note: can't rely on ImGuiTableColumnFlags_IsVisible test here. - continue; ImVec2 bg_shape[4]; bg_shape[0] = ImVec2(column->MaxX, row_r.Max.y); @@ -3234,9 +3274,8 @@ void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) if (pass == 0) { // Draw shape - draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], GetColorU32(ImGuiCol_TableHeaderBg)); - if (column_n == highlight_column_n) - draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], GetColorU32(ImGuiCol_Header)); // Highlight on hover + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request->BgColor0); + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request->BgColor1); // Optional highlight max_x = ImMax(max_x, bg_shape[3].x); // Draw label @@ -3244,8 +3283,17 @@ void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) // - Handle multiple lines manually, as we want each lines to follow on the horizontal border, rather than see a whole block rotated. const char* label_name = TableGetColumnName(table, column_n); const char* label_name_end = FindRenderedTextEnd(label_name); - const float line_off_step_x = g.FontSize / -sin_a; - float line_off_curr_x = 0.0f; + const float line_off_step_x = (g.FontSize / -sin_a); + const int label_lines = ImTextCountLines(label_name, label_name_end); + + // Left<>Right alignment + float line_off_curr_x = flip_label ? (label_lines - 1) * line_off_step_x : 0.0f; + float line_off_for_align_x = ImMax((((column->MaxX - column->MinX) - padding.x * 2.0f) - (label_lines * line_off_step_x)), 0.0f) * align.x; + line_off_curr_x += line_off_for_align_x - line_off_for_ascent_x; + + // Register header width + column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX + ImCeil(label_lines * line_off_step_x - line_off_for_align_x); + while (label_name < label_name_end) { const char* label_name_eol = strchr(label_name, '\n'); @@ -3258,22 +3306,26 @@ void ImGui::TableAngledHeadersRowEx(float angle, float max_label_width) float clip_height = ImMin(label_size.y, column->ClipRect.Max.x - column->WorkMinX - line_off_curr_x); ImRect clip_r(window->ClipRect.Min, window->ClipRect.Min + ImVec2(clip_width, clip_height)); int vtx_idx_begin = draw_list->_VtxCurrentIdx; + PushStyleColor(ImGuiCol_Text, request->TextColor); RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, clip_r.Max.x, label_name, label_name_eol, &label_size); + PopStyleColor(); int vtx_idx_end = draw_list->_VtxCurrentIdx; + // Up<>Down alignment + const float available_space = ImMax(clip_width - label_size.x + ImAbs(padding.x * cos_a) * 2.0f - ImAbs(padding.y * sin_a) * 2.0f, 0.0f); + const float vertical_offset = available_space * align.y * (flip_label ? -1.0f : 1.0f); + // Rotate and offset label - ImVec2 pivot_in = ImVec2(window->ClipRect.Min.x, window->ClipRect.Min.y + label_size.y); + ImVec2 pivot_in = ImVec2(window->ClipRect.Min.x - vertical_offset, window->ClipRect.Min.y + label_size.y); ImVec2 pivot_out = ImVec2(column->WorkMinX, row_r.Max.y); - line_off_curr_x += line_off_step_x; + line_off_curr_x += flip_label ? -line_off_step_x : line_off_step_x; pivot_out += unit_right * padding.y; if (flip_label) pivot_out += unit_right * (clip_width - ImMax(0.0f, clip_width - label_size.x)); - pivot_out.x += flip_label ? line_off_curr_x - line_off_step_x : line_off_curr_x; + pivot_out.x += flip_label ? line_off_curr_x + line_off_step_x : line_off_curr_x; ShadeVertsTransformPos(draw_list, vtx_idx_begin, vtx_idx_end, pivot_in, label_cos_a, label_sin_a, pivot_out); // Rotate and offset - //if (g.IO.KeyShift) { ImDrawList* fg_dl = GetForegroundDrawList(); vtx_idx_begin = fg_dl->_VtxCurrentIdx; fg_dl->AddRect(clip_r.Min, clip_r.Max, IM_COL32(0, 255, 0, 255), 0.0f, 0, 2.0f); ShadeVertsTransformPos(fg_dl, vtx_idx_begin, fg_dl->_VtxCurrentIdx, pivot_in, label_cos_a, label_sin_a, pivot_out); } + //if (g.IO.KeyShift) { ImDrawList* fg_dl = GetForegroundDrawList(); vtx_idx_begin = fg_dl->_VtxCurrentIdx; fg_dl->AddRect(clip_r.Min, clip_r.Max, IM_COL32(0, 255, 0, 255), 0.0f, 0, 1.0f); ShadeVertsTransformPos(fg_dl, vtx_idx_begin, fg_dl->_VtxCurrentIdx, pivot_in, label_cos_a, label_sin_a, pivot_out); } - // Register header width - column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX + ImCeil(line_off_curr_x); label_name = label_name_eol + 1; } } diff --git a/core/deps/imgui/imgui_widgets.cpp b/core/deps/imgui/imgui_widgets.cpp index 5ce24b417..f4c1a900d 100644 --- a/core/deps/imgui/imgui_widgets.cpp +++ b/core/deps/imgui/imgui_widgets.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.90.4 +// dear imgui, v1.90.6 // (widgets code) /* @@ -75,6 +75,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') #pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind #pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked @@ -122,9 +123,9 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); //------------------------------------------------------------------------- // For InputTextEx() -static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source); -static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); -static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); +static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); //------------------------------------------------------------------------- // [SECTION] Widgets: Text, etc. @@ -508,7 +509,7 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool #ifdef IMGUI_ENABLE_TEST_ENGINE // Alternate registration spot, for when caller didn't use ItemAdd() - if (id != 0 && g.LastItemData.ID != id) + if (g.LastItemData.ID != id) IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL); #endif @@ -536,6 +537,8 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id; if (hovered) { + IM_ASSERT(id != 0); // Lazily check inside rare path. + // Poll mouse buttons // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId. // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine. @@ -1312,24 +1315,47 @@ void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* over if (!ItemAdd(bb, 0)) return; - // Render - fraction = ImSaturate(fraction); - RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); - const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y); - RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding); + // Fraction < 0.0f will display an indeterminate progress bar animation + // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works. + const bool is_indeterminate = (fraction < 0.0f); + if (!is_indeterminate) + fraction = ImSaturate(fraction); - // Default displaying the fraction as percentage string, but user can override it - char overlay_buf[32]; - if (!overlay) + // Out of courtesy we accept a NaN fraction without crashing + float fill_n0 = 0.0f; + float fill_n1 = (fraction == fraction) ? fraction : 0.0f; + + if (is_indeterminate) { - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); - overlay = overlay_buf; + const float fill_width_n = 0.2f; + fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n; + fill_n1 = ImSaturate(fill_n0 + fill_width_n); + fill_n0 = ImSaturate(fill_n0); } - ImVec2 overlay_size = CalcTextSize(overlay, NULL); - if (overlay_size.x > 0.0f) - RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); + // Render + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); + RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding); + + // Default displaying the fraction as percentage string, but user can override it + // Don't display text for indeterminate bars by default + char overlay_buf[32]; + if (!is_indeterminate || overlay != NULL) + { + if (!overlay) + { + ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); + overlay = overlay_buf; + } + + ImVec2 overlay_size = CalcTextSize(overlay, NULL); + if (overlay_size.x > 0.0f) + { + float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x; + RenderTextClipped(ImVec2(ImClamp(text_x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); + } + } } void ImGui::Bullet() @@ -3450,7 +3476,7 @@ bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImG DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format); ImStrTrimBlanks(data_buf); - ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; + ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; bool value_changed = false; if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags)) @@ -3495,6 +3521,7 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. + flags |= (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; bool value_changed = false; if (p_step == NULL) @@ -3940,9 +3967,8 @@ void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, cons } // Return false to discard a character. -static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source) +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard) { - IM_ASSERT(input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Clipboard); unsigned int c = *p_char; // Filter non-printable (NB: isprint is unreliable! see #2467) @@ -3957,7 +3983,7 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, Im apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted. } - if (input_source != ImGuiInputSource_Clipboard) + if (input_source_is_clipboard == false) { // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817) if (c == 127) @@ -3973,7 +3999,7 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, Im return false; // Generic named filters - if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific))) + if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))) { // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'. // The standard mandate that programs starts in the "C" locale where the decimal point is '.'. @@ -3983,7 +4009,7 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, Im // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions. ImGuiContext& g = *ctx; const unsigned c_decimal_point = (unsigned int)g.IO.PlatformLocaleDecimalPoint; - if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific)) + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)) if (c == '.' || c == ',') c = c_decimal_point; @@ -4442,7 +4468,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (Shortcut(ImGuiKey_Tab, id, ImGuiInputFlags_Repeat)) { unsigned int c = '\t'; // Insert TAB - if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) state->OnKeyPressed((int)c); } // FIXME: Implement Shift+Tab @@ -4465,7 +4491,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ unsigned int c = (unsigned int)io.InputQueueCharacters[n]; if (c == '\t') // Skip Tab, see above. continue; - if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) state->OnKeyPressed((int)c); } @@ -4548,7 +4574,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ else if (!is_readonly) { unsigned int c = '\n'; // Insert new line - if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) state->OnKeyPressed((int)c); } } @@ -4615,7 +4641,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { unsigned int c; s += ImTextCharFromUtf8(&c, s, NULL); - if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Clipboard)) + if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true)) continue; clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; } @@ -6203,13 +6229,17 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l label_end = FindRenderedTextEnd(label); const ImVec2 label_size = CalcTextSize(label, label_end, false); + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing + const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it + const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow + // We vertically grow up to current line height up the typical widget height. const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL); ImRect frame_bb; frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; frame_bb.Min.y = window->DC.CursorPos.y; - frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; + frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanTextWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x; frame_bb.Max.y = window->DC.CursorPos.y + frame_height; if (display_frame) { @@ -6219,16 +6249,13 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l frame_bb.Max.x += IM_TRUNC(window->WindowPadding.x * 0.5f); } - const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing - const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it - const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapsing ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); ItemSize(ImVec2(text_width, frame_height), padding.y); // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing ImRect interact_bb = frame_bb; - if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0) - interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f; + if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanTextWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0) + interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f); // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. const float backup_clip_rect_min_x = window->ClipRect.Min.x; @@ -6958,6 +6985,7 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y); RenderText(label_pos, label); window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); + AlignTextToFramePadding(); } BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle); diff --git a/core/deps/libadrenotools b/core/deps/libadrenotools new file mode 160000 index 000000000..5deac9f1a --- /dev/null +++ b/core/deps/libadrenotools @@ -0,0 +1 @@ +Subproject commit 5deac9f1ab2bd833ad664bc3386ac1e8998cecb3 diff --git a/core/deps/libchdr b/core/deps/libchdr index 7239eab39..86b272076 160000 --- a/core/deps/libchdr +++ b/core/deps/libchdr @@ -1 +1 @@ -Subproject commit 7239eab39c961a27959cfb58c8877cb2ab1fbf2d +Subproject commit 86b272076d542287d3f03952e7d4efe283e815bf diff --git a/core/deps/rcheevos b/core/deps/rcheevos new file mode 160000 index 000000000..e88f74992 --- /dev/null +++ b/core/deps/rcheevos @@ -0,0 +1 @@ +Subproject commit e88f74992527b9ade48ae1591378ec2cf363bef9 diff --git a/core/emulator.cpp b/core/emulator.cpp index 9e7a352f9..b33e87c6e 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -37,7 +37,6 @@ #include "network/ggpo.h" #include "hw/mem/mem_watch.h" #include "network/net_handshake.h" -#include "rend/gui.h" #include "network/naomi_network.h" #include "serialize.h" #include "hw/pvr/pvr.h" @@ -45,6 +44,9 @@ #include "oslib/storage.h" #include "wsi/context.h" #include +#ifndef LIBRETRO +#include "ui/gui.h" +#endif settings_t settings; @@ -449,6 +451,8 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) { hostfs::FileInfo info = hostfs::storage().getFileInfo(settings.content.path); settings.content.fileName = info.name; + if (settings.content.title.empty()) + settings.content.title = get_file_basename(info.name); } } else @@ -487,7 +491,7 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) nvmem::loadHle(); NOTICE_LOG(BOOT, "Did not load BIOS, using reios"); if (!config::UseReios && config::UseReios.isReadOnly()) - gui_display_notification("This game requires a real BIOS", 15000); + os_notify("This game requires a real BIOS", 15000); } } else @@ -506,6 +510,8 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) InitDrive(""); } } + if (settings.content.path.empty()) + settings.content.title = "Dreamcast BIOS"; if (progress) progress->progress = 1.0f; @@ -526,10 +532,18 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) // Must be done after the maple devices are created and EEPROM is accessible naomi_cart_ConfigureEEPROM(); } +#ifdef USE_RACHIEVEMENTS + // RA probably isn't expecting to travel back in the past so disable it + if (config::GGPOEnable) + config::EnableAchievements.override(false); + // Hardcore mode disables all cheats, under/overclocking, load state, lua and forces dynarec on + settings.raHardcoreMode = config::EnableAchievements && config::AchievementsHardcoreMode + && !NaomiNetworkSupported(); +#endif cheatManager.reset(settings.content.gameId); if (cheatManager.isWidescreen()) { - gui_display_notification("Widescreen cheat activated", 1000); + os_notify("Widescreen cheat activated", 2000); config::ScreenStretching.override(134); // 4:3 -> 16:9 } // reload settings so that all settings can be overridden @@ -538,10 +552,12 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) settings.input.fastForwardMode = false; if (!settings.content.path.empty()) { +#ifndef LIBRETRO if (config::GGPOEnable) dc_loadstate(-1); else if (config::AutoLoadState && !NaomiNetworkSupported() && !settings.naomi.multiboard) dc_loadstate(config::SavestateSlot); +#endif } EventManager::event(Event::Start); @@ -600,9 +616,11 @@ void Emulator::unloadGame() } catch (...) { } if (state == Loaded || state == Error) { +#ifndef LIBRETRO if (state == Loaded && config::AutoSaveState && !settings.content.path.empty() && !settings.naomi.multiboard && !config::GGPOEnable && !NaomiNetworkSupported()) - dc_savestate(config::SavestateSlot); + gui_saveState(false); +#endif try { dc_reset(true); } catch (const FlycastException& e) { @@ -614,6 +632,7 @@ void Emulator::unloadGame() settings.content.path.clear(); settings.content.gameId.clear(); settings.content.fileName.clear(); + settings.content.title.clear(); settings.platform.system = DC_PLATFORM_DREAMCAST; state = Init; EventManager::event(Event::Terminate); @@ -686,6 +705,10 @@ void Emulator::requestReset() void loadGameSpecificSettings() { + // Graphics context isn't available yet in libretro + if (GraphicsContext::Instance() != nullptr && GraphicsContext::Instance()->isAMD()) + config::NativeDepthInterpolation.override(true); + if (settings.platform.isConsole()) { reios_disk_id(); @@ -703,8 +726,13 @@ void loadGameSpecificSettings() // Reload per-game settings config::Settings::instance().load(true); - if (config::GGPOEnable) + if (config::GGPOEnable || settings.raHardcoreMode) config::Sh4Clock.override(200); + if (settings.raHardcoreMode) + { + config::WidescreenGameHacks.override(false); + config::DynarecEnabled.override(true); + } } void Emulator::step() @@ -755,42 +783,30 @@ void Emulator::setNetworkState(bool online) config::Sh4Clock.override(200); sh4_cpu.ResetCache(); } + EventManager::event(Event::Network); } settings.input.fastForwardMode &= !online; } -EventManager EventManager::Instance; - void EventManager::registerEvent(Event event, Callback callback, void *param) { unregisterEvent(event, callback, param); - auto it = callbacks.find(event); - if (it != callbacks.end()) - it->second.push_back(std::make_pair(callback, param)); - else - callbacks.insert({ event, { std::make_pair(callback, param) } }); + auto& vector = callbacks[static_cast(event)]; + vector.push_back(std::make_pair(callback, param)); } void EventManager::unregisterEvent(Event event, Callback callback, void *param) { - auto it = callbacks.find(event); - if (it == callbacks.end()) - return; - - auto it2 = std::find(it->second.begin(), it->second.end(), std::make_pair(callback, param)); - if (it2 == it->second.end()) - return; - - it->second.erase(it2); + auto& vector = callbacks[static_cast(event)]; + auto it = std::find(vector.begin(), vector.end(), std::make_pair(callback, param)); + if (it != vector.end()) + vector.erase(it); } void EventManager::broadcastEvent(Event event) { - auto it = callbacks.find(event); - if (it == callbacks.end()) - return; - - for (auto& pair : it->second) + auto& vector = callbacks[static_cast(event)]; + for (auto& pair : vector) pair.first(event, pair.second); } @@ -842,6 +858,7 @@ void Emulator::start() { const std::lock_guard lock(mutex); threadResult = std::async(std::launch::async, [this] { + ThreadName _("Flycast-emu"); InitAudio(); try { diff --git a/core/emulator.h b/core/emulator.h index 09800e2a0..6802b00e8 100644 --- a/core/emulator.h +++ b/core/emulator.h @@ -23,10 +23,11 @@ #include #include -#include +#include #include #include #include +#include void loadGameSpecificSettings(); void SaveSettings(); @@ -35,9 +36,11 @@ int flycast_init(int argc, char* argv[]); void dc_reset(bool hard); // for tests only void flycast_term(); void dc_exit(); -void dc_savestate(int index = 0); +void dc_savestate(int index = 0, const u8 *pngData = nullptr, u32 pngSize = 0); void dc_loadstate(int index = 0); void dc_loadstate(Deserializer& deser); +time_t dc_getStateCreationDate(int index); +void dc_getStateScreenshot(int index, std::vector& pngData); enum class Event { Start, @@ -46,6 +49,9 @@ enum class Event { Terminate, LoadState, VBlank, + Network, + DiskChange, + max = DiskChange }; class EventManager @@ -54,26 +60,29 @@ public: using Callback = void (*)(Event, void *); static void listen(Event event, Callback callback, void *param = nullptr) { - Instance.registerEvent(event, callback, param); + instance().registerEvent(event, callback, param); } static void unlisten(Event event, Callback callback, void *param = nullptr) { - Instance.unregisterEvent(event, callback, param); + instance().unregisterEvent(event, callback, param); } static void event(Event event) { - Instance.broadcastEvent(event); + instance().broadcastEvent(event); } private: EventManager() = default; + static EventManager& instance() { + static EventManager _instance; + return _instance; + } void registerEvent(Event event, Callback callback, void *param); void unregisterEvent(Event event, Callback callback, void *param); void broadcastEvent(Event event); - static EventManager Instance; - std::map>> callbacks; + std::array>, static_cast(Event::max) + 1> callbacks; }; struct LoadProgress diff --git a/core/hw/aica/aica_if.cpp b/core/hw/aica/aica_if.cpp index 8d6ba1b55..92bfa83f5 100644 --- a/core/hw/aica/aica_if.cpp +++ b/core/hw/aica/aica_if.cpp @@ -581,8 +581,7 @@ void deserialize(Deserializer& deser) deser >> VREG; deser >> ARMRST; deser >> rtc_EN; - if (deser.version() >= Deserializer::V9) - deser >> RealTimeClock; + deser >> RealTimeClock; deser >> aica_reg; diff --git a/core/hw/aica/sgc_if.cpp b/core/hw/aica/sgc_if.cpp index 013cebea4..7b5c4755e 100755 --- a/core/hw/aica/sgc_if.cpp +++ b/core/hw/aica/sgc_if.cpp @@ -149,80 +149,88 @@ class VmuBeep public: void init() { - beepOn = 0; - beepPeriod = 0; - beepCounter = 0; - beepValue = 0; + active = false; + att = 256; + waveIdx.full = 0; + waveStep = 0; } - void update(int on, int period) + void update(int period, int on) { - if (on == 0 || period == 0 || on < period) - { - beepOn = 0; - beepPeriod = 0; + if (on == 0 || period == 0 || on >= period) { + active = false; } else { - // The maple doc may be wrong on this. It looks like the raw values of T1LR and T1LC are set. - // So the period is (256 - T1LR) / (32768 / 6) - // and the duty cycle is (256 - T1LC) / (32768 / 6) - beepOn = (256 - on) * 8; - beepPeriod = (256 - period) * 8; - beepCounter = 0; + // on (duty cycle) is ignored + active = true; + // 6 MHz clock + int freq = 6000000 / 6 / period; + waveStep = freq * 1024 / 2698; } } SampleType getSample() { - constexpr int Slope = 500; - if (beepPeriod == 0) - { - if (beepValue > 0) - beepValue = std::max(0, beepValue - Slope); - else if (beepValue < 0) - beepValue = std::min(0, beepValue + Slope); - } - else - { - if (beepCounter <= beepOn) - beepValue = std::min(16383, beepValue + Slope); - else - beepValue = std::max(-16384, beepValue - Slope); - beepCounter = (beepCounter + 1) % beepPeriod; - } + if (!active && att >= 256) + return 0; - return beepValue; + waveIdx.full += waveStep; + waveIdx.ip %= std::size(wave); + int nextIdx = (waveIdx.ip + 1) % std::size(wave); + SampleType s = (FPMul(wave[waveIdx.ip], (int)(1024 - waveIdx.fp), 10) + FPMul(wave[nextIdx], (int)waveIdx.fp, 10)) * 2; + + s = FPMul(s, tl_lut[att], 15); + if (active) + att = std::max(att - 2, 0); + else + att = std::min(att + 2, 256); + return s; } void serialize(Serializer& ser) { - ser << beepOn; - ser << beepPeriod; - ser << beepCounter; + ser << active; + ser << att; + ser << waveIdx; + ser << waveStep; } void deserialize(Deserializer& deser) { - if (deser.version() >= Deserializer::V22) + if (deser.version() < Deserializer::V49) { - deser >> beepOn; - deser >> beepPeriod; - deser >> beepCounter; + if (deser.version() >= Deserializer::V22) + { + deser.skip(); // beepOn + deser.skip(); // beepPeriod + deser.skip(); // beepCounter + } + init(); } else { - beepOn = 0; - beepPeriod = 0; - beepCounter = 0; + deser >> active; + deser >> att; + deser >> waveIdx; + deser >> waveStep; } } private: - int beepOn = 0; - int beepPeriod = 0; - int beepCounter = 0; - SampleType beepValue = 0; + bool active = false; + int att = 256; + fp_22_10 waveIdx {}; + int waveStep = 0; + + // 2698 Hz + static constexpr SampleType wave[] = { 503, -3519, + -7540, -8214, -8209, -8214, -8209, -5199, -1172, 2843, + 6873, 8207, 8214, 8209, 8212, 5866, 1840, -2175, + -6203, -8210, -8215, -8209, -8214, -6533, -2516, 1507, + 5526, 8212, 8210, 8212, 8210, 7206, 3187, -841, + -4856, -8215, -8209, -8214, -8208, -7882, -3850, 162, + 4187, 8213, 8208, 8213, 8209, 8211, 4525 }; }; static VmuBeep beep; diff --git a/core/hw/flashrom/nvmem.cpp b/core/hw/flashrom/nvmem.cpp index d6c61b02a..e6cfa0d50 100644 --- a/core/hw/flashrom/nvmem.cpp +++ b/core/hw/flashrom/nvmem.cpp @@ -76,8 +76,7 @@ static void add_isp_to_nvmem(DCFlashChip *flash) for (u32 i = FLASH_USER_INET + 5; i <= 0xbf; i++) flash->WriteBlock(FLASH_PT_USER, i, block); - flash_isp1_block isp1; - memset(&isp1, 0, sizeof(isp1)); + flash_isp1_block isp1{}; isp1._unknown[3] = 1; memcpy(isp1.sega, "SEGA", 4); strcpy(isp1.username, "flycast1"); @@ -96,8 +95,7 @@ static void add_isp_to_nvmem(DCFlashChip *flash) block[60] = 1; flash->WriteBlock(FLASH_PT_USER, FLASH_USER_ISP1 + 5, block); - flash_isp2_block isp2; - memset(&isp2, 0, sizeof(isp2)); + flash_isp2_block isp2{}; memcpy(isp2.sega, "SEGA", 4); strcpy(isp2.username, "flycast2"); strcpy(isp2.password, "password"); @@ -373,36 +371,8 @@ void serialize(Serializer& ser) void deserialize(Deserializer& deser) { - if (deser.version() <= Deserializer::VLAST_LIBRETRO) - { - deser.skip(); // size - deser.skip(); // mask - - // Legacy libretro savestate - if (settings.platform.isArcade()) - sys_nvmem->Deserialize(deser); - - deser.skip(); // sys_nvmem/sys_rom->size - deser.skip(); // sys_nvmem/sys_rom->mask - if (settings.platform.isConsole()) - { - sys_nvmem->Deserialize(deser); - } - else if (settings.platform.isAtomiswave()) - { - deser >> static_cast(sys_rom)->state; - deser.deserialize(sys_rom->data, sys_rom->size); - } - else - { - deser.skip(); - } - } - else - { - sys_rom->Deserialize(deser); - sys_nvmem->Deserialize(deser); - } + sys_rom->Deserialize(deser); + sys_nvmem->Deserialize(deser); } } diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index a32b4f527..1a29572ea 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -1412,8 +1412,6 @@ void deserialize(Deserializer& deser) deser.skip(Deserializer::V44); // set_mode_offset (repeat) deser >> ata_cmd; deser >> cdda; - if (deser.version() < Deserializer::V10) - cdda.status = (bool)cdda.status ? cdda_t::Playing : cdda_t::NoInfo; deser >> gd_state; deser >> gd_disk_type; deser >> data_write_mode; @@ -1426,8 +1424,6 @@ void deserialize(Deserializer& deser) deser >> SecNumber; deser >> GDStatus; deser >> ByteCount; - if (deser.version() <= Deserializer::VLAST_LIBRETRO) - deser.skip(); // GDROM_TICK } } diff --git a/core/hw/holly/sb.cpp b/core/hw/holly/sb.cpp index 12e4c5a8c..5966141d4 100644 --- a/core/hw/holly/sb.cpp +++ b/core/hw/holly/sb.cpp @@ -14,6 +14,7 @@ #include "emulator.h" #include "hw/bba/bba.h" #include "serialize.h" +#include u32 sb_regs[0x540]; HollyRegisters hollyRegs; @@ -704,18 +705,7 @@ void sb_serialize(Serializer& ser) void sb_deserialize(Deserializer& deser) { - if (deser.version() <= Deserializer::VLAST_LIBRETRO) - { - for (u32& reg : sb_regs) - { - deser.skip(); // regs.data[i].flags - deser >> reg; - } - } - else - { - deser >> sb_regs; - } + deser >> sb_regs; if (deser.version() < Deserializer::V33) deser >> SB_ISTNRM; if (deser.version() >= Deserializer::V24) @@ -729,9 +719,6 @@ void sb_deserialize(Deserializer& deser) deser.skip(); // SB_FFST_rc; deser.skip(); // SB_FFST; } - if (deser.version() >= Deserializer::V15) - deser >> SB_ADST; - else - SB_ADST = 0; + deser >> SB_ADST; } } diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 3148db8d2..5d89c25b7 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -7,7 +7,6 @@ #include "oslib/oslib.h" #include "hw/aica/sgc_if.h" #include "cfg/option.h" -#include "rend/gui.h" #include #include #include @@ -154,7 +153,15 @@ struct maple_sega_controller: maple_base //2 (Maximum current consumption) w16(get_device_current(1)); - return cmd == MDC_DeviceRequest ? MDRS_DeviceStatus : MDRS_DeviceStatusAll; + if (cmd == MDC_AllStatusReq) + { + const char *extra = "Version 1.010,1998/09/28,315-6211-AB ,Analog Module : The 4th Edition.5/8 +DF"; + wptr(extra, strlen(extra)); + return MDRS_DeviceStatusAll; + } + else { + return MDRS_DeviceStatus; + } //controller condition case MDCF_GetCondition: @@ -441,7 +448,15 @@ struct maple_sega_vmu: maple_base //2 w16(0x0082); // 13 mA - return cmd == MDC_DeviceRequest ? MDRS_DeviceStatus : MDRS_DeviceStatusAll; + if (cmd == MDC_AllStatusReq) + { + const char *extra = "Version 1.005,1999/04/15,315-6208-03,SEGA Visual Memory System BIOS Produced by "; + wptr(extra, strlen(extra)); + return MDRS_DeviceStatusAll; + } + else { + return MDRS_DeviceStatus; + } //in[0] is function used //out[0] is function used @@ -1730,7 +1745,7 @@ struct RFIDReaderWriter : maple_base cardLocked = false; cardInserted = false; NOTICE_LOG(MAPLE, "RFID card %d unlocked", player_num); - gui_display_notification("Card ejected", 2000); + os_notify("Card ejected", 2000); return (MapleDeviceRV)0xfe; case 0xB1: // write to card diff --git a/core/hw/maple/maple_devs.h b/core/hw/maple/maple_devs.h index 284d11a7c..7f97e5af4 100755 --- a/core/hw/maple/maple_devs.h +++ b/core/hw/maple/maple_devs.h @@ -142,8 +142,7 @@ struct maple_device ser << player_num; } virtual void deserialize(Deserializer& deser) { - if (deser.version() >= Deserializer::V14) - deser >> player_num; + deser >> player_num; } virtual MapleDeviceType get_device_type() = 0; diff --git a/core/hw/modem/modem.cpp b/core/hw/modem/modem.cpp index 93fda73d8..f3d6e904d 100644 --- a/core/hw/modem/modem.cpp +++ b/core/hw/modem/modem.cpp @@ -28,10 +28,7 @@ #include "network/picoppp.h" #include "serialize.h" #include "cfg/option.h" - -#ifndef NDEBUG -#include "oslib/oslib.h" -#endif +#include "stdclass.h" #include #define MODEM_COUNTRY_RES 0 @@ -127,7 +124,7 @@ static u64 last_dial_time; static bool data_sent; #ifndef NDEBUG -static double last_comm_stats; +static u64 last_comm_stats; static int sent_bytes; static int recvd_bytes; static FILE *recv_fp; @@ -137,7 +134,7 @@ static FILE *sent_fp; static int modem_sched_func(int tag, int cycles, int jitter, void *arg) { #ifndef NDEBUG - if (os_GetSeconds() - last_comm_stats >= 2) + if (getTimeMs() - last_comm_stats >= 2000) { if (last_comm_stats != 0) { @@ -147,7 +144,7 @@ static int modem_sched_func(int tag, int cycles, int jitter, void *arg) sent_bytes = 0; recvd_bytes = 0; } - last_comm_stats = os_GetSeconds(); + last_comm_stats = getTimeMs(); } #endif int callback_cycles = 0; diff --git a/core/hw/naomi/card_reader.cpp b/core/hw/naomi/card_reader.cpp index b914c39a7..8dffa1cf1 100644 --- a/core/hw/naomi/card_reader.cpp +++ b/core/hw/naomi/card_reader.cpp @@ -21,7 +21,6 @@ #include "hw/sh4/modules/modules.h" #include "hw/maple/maple_cfg.h" #include "hw/maple/maple_devs.h" -#include "rend/gui.h" #include #include #include @@ -288,7 +287,7 @@ protected: case CARD_EJECT: NOTICE_LOG(NAOMI, "Card ejected"); if (cardInserted) - gui_display_notification("Card ejected", 2000); + os_notify("Card ejected", 2000); cardInserted = false; status1 = getStatus1(); break; @@ -582,7 +581,7 @@ private: case CARD_EJECT: NOTICE_LOG(NAOMI, "Card ejected"); if (cardInserted) - gui_display_notification("Card ejected", 2000); + os_notify("Card ejected", 2000); cardInserted = false; break; case CARD_NEW: diff --git a/core/hw/naomi/naomi.cpp b/core/hw/naomi/naomi.cpp index 8e51c5993..8eb2b0c98 100644 --- a/core/hw/naomi/naomi.cpp +++ b/core/hw/naomi/naomi.cpp @@ -30,7 +30,7 @@ #include "serialize.h" #include "network/output.h" #include "hw/sh4/modules/modules.h" -#include "rend/gui.h" +#include "oslib/oslib.h" #include "printer.h" #include "hw/flashrom/x76f100.h" @@ -409,10 +409,7 @@ void naomi_Deserialize(Deserializer& deser) deser.skip(); // reg_dimm_parameterh; deser.skip(); // reg_dimm_status; } - if (deser.version() < Deserializer::V11) - deser.skip(); - else if (deser.version() >= Deserializer::V14) - deser >> aw_maple_devs; + deser >> aw_maple_devs; if (deser.version() >= Deserializer::V20) { deser >> coin_chute_time; @@ -515,7 +512,7 @@ struct DriveSimPipe : public SerialPort::Pipe { char message[16]; sprintf(message, "Speed: %3d", speed); - gui_display_notification(message, 1000); + os_notify(message, 1000); } } buffer.clear(); diff --git a/core/hw/naomi/naomi_cart.cpp b/core/hw/naomi/naomi_cart.cpp index b10bf3e75..588def40e 100644 --- a/core/hw/naomi/naomi_cart.cpp +++ b/core/hw/naomi/naomi_cart.cpp @@ -68,7 +68,7 @@ static bool loadBios(const char *filename, Archive *child_archive, Archive *pare const BIOS_t *bios = &BIOS[biosid]; - std::string arch_name(filename); + std::string arch_name(bios->filename != nullptr ? bios->filename : filename); std::string path = hostfs::findNaomiBios(arch_name + ".zip"); if (path.empty()) path = hostfs::findNaomiBios(arch_name + ".7z"); @@ -123,6 +123,7 @@ static bool loadBios(const char *filename, Archive *child_archive, Archive *pare break; case EepromBE16: { + // FIXME memory leak naomi_default_eeprom = (u8 *)malloc(bios->blobs[romid].length); if (naomi_default_eeprom == nullptr) throw NaomiCartException("Memory allocation failed"); @@ -627,8 +628,10 @@ void naomi_cart_LoadRom(const std::string& path, const std::string& fileName, Lo bool systemSP = memcmp(bootId.boardName, "SystemSP", 8) == 0; std::string gameId = trim_trailing_ws(std::string(bootId.gameTitle[systemSP ? 1 : 0], &bootId.gameTitle[systemSP ? 1 : 0][32])); std::string romName; - if (CurrentCartridge->game != nullptr) + if (CurrentCartridge->game != nullptr) { romName = CurrentCartridge->game->name; + settings.content.title = CurrentCartridge->game->description; + } if (gameId == "SAMPLE GAME MAX LONG NAME-") { // Use better game names @@ -775,7 +778,7 @@ void naomi_cart_serialize(Serializer& ser) void naomi_cart_deserialize(Deserializer& deser) { - if (CurrentCartridge != nullptr && (!settings.platform.isAtomiswave() || deser.version() >= Deserializer::V10_LIBRETRO)) + if (CurrentCartridge != nullptr) CurrentCartridge->Deserialize(deser); touchscreen::deserialize(deser); printer::deserialize(deser); @@ -885,7 +888,7 @@ void* NaomiCartridge::GetDmaPtr(u32& size) { if ((DmaOffset & 0x1fffffff) >= RomSize) { - INFO_LOG(NAOMI, "Error: DmaOffset >= RomSize"); + INFO_LOG(NAOMI, "Error: DmaOffset (%x) >= RomSize (%x)", DmaOffset, RomSize); size = 0; return nullptr; } diff --git a/core/hw/naomi/naomi_m3comm.cpp b/core/hw/naomi/naomi_m3comm.cpp index cd53bdb92..29a928fd3 100644 --- a/core/hw/naomi/naomi_m3comm.cpp +++ b/core/hw/naomi/naomi_m3comm.cpp @@ -30,7 +30,7 @@ #include "hw/sh4/sh4_mem.h" #include "network/naomi_network.h" #include "emulator.h" -#include "rend/gui.h" +#include "oslib/oslib.h" #include #include @@ -72,7 +72,7 @@ void NaomiM3Comm::closeNetwork() void NaomiM3Comm::connectNetwork() { - gui_display_notification("Network started", 5000); + os_notify("Network started", 5000); packet_number = 0; slot_count = naomiNetwork.getSlotCount(); slot_id = naomiNetwork.getSlotId(); diff --git a/core/hw/naomi/naomi_roms.cpp b/core/hw/naomi/naomi_roms.cpp index d7c5fc9ab..a2b092a16 100644 --- a/core/hw/naomi/naomi_roms.cpp +++ b/core/hw/naomi/naomi_roms.cpp @@ -88,51 +88,51 @@ const BIOS_t BIOS[] = //ROM_SYSTEM_BIOS( 1, "bios1", "epr-21576g (Japan)" ) { 0, "epr-21576g.ic27", 0x000000, 0x200000, 0xd2a1c6bf }, //ROM_SYSTEM_BIOS( 2, "bios2", "epr-21576e (Japan)" ) - //{ 0, "epr-21576e.ic27", 0x000000, 0x200000 }, + //{ 0, "epr-21576e.ic27", 0x000000, 0x200000, 0x08c0add7 }, //ROM_SYSTEM_BIOS( 3, "bios3", "epr-21576d (Japan)" ) - //{ 0, "epr-21576d.ic27", 0x000000, 0x200000 }, + //{ 0, "epr-21576d.ic27", 0x000000, 0x200000, 0x3b2afa7b }, //ROM_SYSTEM_BIOS( 4, "bios4", "epr-21576c (Japan)" ) - //{ 0, "epr-21576c.ic27", 0x000000, 0x200000 }, // BAD DUMP + //{ 0, "epr-21576c.ic27", 0x000000, 0x200000, 0x4599ad13 }, // BAD DUMP //ROM_SYSTEM_BIOS( 5, "bios5", "epr-21576b (Japan)" ) - //{ 0, "epr-21576b.ic27", 0x000000, 0x200000 }, + //{ 0, "epr-21576b.ic27", 0x000000, 0x200000, 0x755a6e07 }, //ROM_SYSTEM_BIOS( 6, "bios6", "epr-21576a (Japan)" ) - //{ 0, "epr-21576a.ic27", 0x000000, 0x200000 }, + //{ 0, "epr-21576a.ic27", 0x000000, 0x200000, 0xcedfe439 }, //ROM_SYSTEM_BIOS( 7, "bios7", "epr-21576 (Japan)" ) - //{ 0, "epr-21576.ic27", 0x000000, 0x200000 }, + //{ 0, "epr-21576.ic27", 0x000000, 0x200000, 0x9dad3495 }, //ROM_SYSTEM_BIOS( 8, "bios8", "epr-21578h (Export)" ) { 2, "epr-21578h.ic27", 0x000000, 0x200000, 0x7b452946 }, //ROM_SYSTEM_BIOS( 9, "bios9", "epr-21578g (Export)" ) { 2, "epr-21578g.ic27", 0x000000, 0x200000, 0x55413214 }, //ROM_SYSTEM_BIOS( 10, "bios10", "epr-21578f (Export)" ) - //{ 2, "epr-21578f.ic27", 0x000000, 0x200000 }, + //{ 2, "epr-21578f.ic27", 0x000000, 0x200000, 0x628a27fd }, //ROM_SYSTEM_BIOS( 11, "bios11", "epr-21578e (Export)" ) - //{ 2, "epr-21578e.ic27", 0x000000, 0x200000 }, + //{ 2, "epr-21578e.ic27", 0x000000, 0x200000, 0x087f09a3 }, //ROM_SYSTEM_BIOS( 12, "bios12", "epr-21578d (Export)" ) - //{ 2, "epr-21578d.ic27", 0x000000, 0x200000 }, + //{ 2, "epr-21578d.ic27", 0x000000, 0x200000, 0xdfd5f42a }, //ROM_SYSTEM_BIOS( 13, "bios13", "epr-21578a (Export)" ) - //{ 2, "epr-21578a.ic27", 0x000000, 0x200000 }, + //{ 2, "epr-21578a.ic27", 0x000000, 0x200000, 0x6c9aad83 }, //ROM_SYSTEM_BIOS( 14, "bios14", "epr-21577h (USA)" ) { 1, "epr-21577h.ic27", 0x000000, 0x200000, 0xfdf17452 }, //ROM_SYSTEM_BIOS( 15, "bios15", "epr-21577g (USA)" ) { 1, "epr-21577g.ic27", 0x000000, 0x200000, 0x25f64af7 }, //ROM_SYSTEM_BIOS( 16, "bios16", "epr-21577e (USA)" ) - //{ 1, "epr-21577e.ic27", 0x000000, 0x200000 }, + //{ 1, "epr-21577e.ic27", 0x000000, 0x200000, 0xcf36e97b }, //ROM_SYSTEM_BIOS( 17, "bios17", "epr-21577d (USA)" ) - //{ 1, "epr-21577d.ic27", 0x000000, 0x200000 }, + //{ 1, "epr-21577d.ic27", 0x000000, 0x200000, 0x60ddcbbe }, //ROM_SYSTEM_BIOS( 18, "bios18", "epr-21577a (USA)" ) - //{ 1, "epr-21577a.ic27", 0x000000, 0x200000 }, + //{ 1, "epr-21577a.ic27", 0x000000, 0x200000, 0x969dc491 }, //ROM_SYSTEM_BIOS( 19, "bios19", "epr-21579d (Korea)" ) { 3, "epr-21579d.ic27", 0x000000, 0x200000, 0x33513691 }, //ROM_SYSTEM_BIOS( 20, "bios20", "epr-21579 (Korea)" ) - //{ 3, "epr-21579.ic27", 0x000000, 0x200000 }, + //{ 3, "epr-21579.ic27", 0x000000, 0x200000, 0x71f9c918 }, //ROM_SYSTEM_BIOS( 21, "bios21", "Set4 Dev BIOS" ) - //{ 3, "boot_rom_64b8.ic606", 0x000000, 0x080000 }, + //{ 3, "boot_rom_64b8.ic606", 0x000000, 0x080000, 0x7a50fab9 }, //ROM_SYSTEM_BIOS( 22, "bios22", "Dev BIOS v1.10" ) - //{ 3, "develop110.ic27", 0x000000, 0x200000 }, + //{ 3, "develop110.ic27", 0x000000, 0x200000, 0xde7cfdb0 }, //ROM_SYSTEM_BIOS( 23, "bios23", "Dev BIOS (Nov 1998)" ) - //{ 3, "develop.ic27", 0x000000, 0x200000 }, + //{ 3, "develop.ic27", 0x000000, 0x200000, 0x309a196a }, //ROM_SYSTEM_BIOS( 24, "bios24", "Development ROM Board" ) - //{ 3, "zukinver0930.ic25", 0x000000, 0x200000 }, + //{ 3, "zukinver0930.ic25", 0x000000, 0x200000, 0x58e17c23 }, //ROM_SYSTEM_BIOS( 25, "bios25", "epr-21576h (multi-region hack)" ) // The default dipswitch configuration selects Korea for the multiregion hacked BIOS // See hw/maple/maple_jvs.cpp @@ -176,6 +176,17 @@ const BIOS_t BIOS[] = { 2, "mb_eeprom_exp.ic54s", 0x000, 0x800, 0x947ddfad, EepromBE16 }, }, }, + { + "naomi-jp-d", // for marstv + { + //ROM_SYSTEM_BIOS( 3, "bios3", "epr-21576d (Japan)" ) + { 0, "epr-21576d.ic27", 0x000000, 0x200000, 0x3b2afa7b }, + { 1, "epr-21576d.ic27", 0x000000, 0x200000, 0x3b2afa7b }, + { 2, "epr-21576d.ic27", 0x000000, 0x200000, 0x3b2afa7b }, + { 3, "epr-21576d.ic27", 0x000000, 0x200000, 0x3b2afa7b }, + }, + "naomi", + }, { nullptr } @@ -2375,11 +2386,11 @@ const Game Games[] = // Mars TV (JPN) { "marstv", - NULL, + nullptr, "Mars TV", 0x08000000, 0x280b8ef5, - NULL, + "naomi-jp-d", M2, ROT0, { @@ -2400,7 +2411,6 @@ const Game Games[] = { "mpr-22990.ic13s", 0x6800000, 0x800000 }, { "mpr-22991.ic14s", 0x7000000, 0x800000 }, { "mpr-22992.ic15s", 0x7800000, 0x800000 }, - { NULL, 0, 0 }, } }, // Mazan: Flash of the Blade (MAZ2 Ver. A) diff --git a/core/hw/naomi/naomi_roms.h b/core/hw/naomi/naomi_roms.h index 3277a32eb..c67f78692 100644 --- a/core/hw/naomi/naomi_roms.h +++ b/core/hw/naomi/naomi_roms.h @@ -62,6 +62,7 @@ struct BIOS_t u32 crc; BlobType blob_type; } blobs[MAX_GAME_FILES]; + const char* filename; // if different from name }; extern const BIOS_t BIOS[]; diff --git a/core/hw/naomi/printer.cpp b/core/hw/naomi/printer.cpp index df7551307..3c5edc0e1 100644 --- a/core/hw/naomi/printer.cpp +++ b/core/hw/naomi/printer.cpp @@ -20,7 +20,7 @@ #include "stdclass.h" #include "printer.h" #include "serialize.h" -#include "rend/gui.h" +#include "oslib/oslib.h" #include #include #include @@ -827,11 +827,11 @@ private: state = Default; if (bitmapWriter && bitmapWriter->isDirty()) { + // TODO save to ~/Pictures instead std::string s = get_writable_data_path(settings.content.gameId + "-results.png"); bitmapWriter->save(s); bitmapWriter.reset(); - s = "Print out saved to " + s; - gui_display_notification(s.c_str(), 5000); + os_notify("Print out saved", 5000, s.c_str()); NOTICE_LOG(NAOMI, "%s", s.c_str()); } break; @@ -1198,7 +1198,7 @@ std::string get_writable_data_path(const std::string& s) return "./" + s; } -void gui_display_notification(char const*, int) { +void os_notify(char const*, int, char const*) { } int main(int argc, char *argv[]) diff --git a/core/hw/naomi/systemsp.cpp b/core/hw/naomi/systemsp.cpp index f356a6f09..9cc51b366 100644 --- a/core/hw/naomi/systemsp.cpp +++ b/core/hw/naomi/systemsp.cpp @@ -1300,7 +1300,7 @@ class MedalIOManager : public DefaultIOManager } // OUT-1 (CN10 17-24) - void setCN10_17_24(u8 v) + void setCN10_17_24(u8 v) override { // 0: sw.lamp c // 1: jp solenoid diff --git a/core/hw/pvr/Renderer_if.cpp b/core/hw/pvr/Renderer_if.cpp index aa2b51442..86ce77c7d 100644 --- a/core/hw/pvr/Renderer_if.cpp +++ b/core/hw/pvr/Renderer_if.cpp @@ -536,11 +536,7 @@ void rend_serialize(Serializer& ser) } void rend_deserialize(Deserializer& deser) { - if ((deser.version() >= Deserializer::V12_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO) - || deser.version() >= Deserializer::V12) - deser >> fb_w_cur; - else - fb_w_cur = 1; + deser >> fb_w_cur; if (deser.version() >= Deserializer::V20) { deser >> render_called; diff --git a/core/hw/pvr/Renderer_if.h b/core/hw/pvr/Renderer_if.h index 71d306711..d7f7f17a8 100644 --- a/core/hw/pvr/Renderer_if.h +++ b/core/hw/pvr/Renderer_if.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include "ta_ctx.h" +#include extern u32 FrameCount; @@ -62,6 +63,11 @@ struct Renderer virtual bool Render() = 0; virtual void RenderFramebuffer(const FramebufferInfo& info) = 0; virtual bool RenderLastFrame() { return false; } + // Get the last rendered frame pixel data in RGB format + // The returned image is rotated and scaled (upward orientation and square pixels) + // If both width and height are zero, the internal render resolution will be used. + // Otherwise either width or height will be used as the maximum width or height respectively. + virtual bool GetLastFrame(std::vector& data, int& width, int& height) { return false; } virtual bool Present() { return true; } diff --git a/core/hw/pvr/pvr.cpp b/core/hw/pvr/pvr.cpp index 79478de0b..22bead33e 100644 --- a/core/hw/pvr/pvr.cpp +++ b/core/hw/pvr/pvr.cpp @@ -104,8 +104,7 @@ void deserialize(Deserializer& deser) deser >> taRenderPass; else taRenderPass = 0; - if (deser.version() >= Deserializer::V11 || (deser.version() >= Deserializer::V10_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO)) - DeserializeTAContext(deser); + DeserializeTAContext(deser); if (!deser.rollback()) vram.deserialize(deser); diff --git a/core/hw/pvr/pvr_mem.cpp b/core/hw/pvr/pvr_mem.cpp index aafc3c62e..a45c88870 100644 --- a/core/hw/pvr/pvr_mem.cpp +++ b/core/hw/pvr/pvr_mem.cpp @@ -190,10 +190,7 @@ void YUV_deserialize(Deserializer& deser) deser >> YUV_y_curr; deser >> YUV_x_size; deser >> YUV_y_size; - if (deser.version() >= Deserializer::V16) - deser >> YUV_index; - else - YUV_index = 0; + deser >> YUV_index; } void YUV_reset() diff --git a/core/hw/pvr/spg.cpp b/core/hw/pvr/spg.cpp index ff3b6f320..5fd93bc86 100755 --- a/core/hw/pvr/spg.cpp +++ b/core/hw/pvr/spg.cpp @@ -1,13 +1,13 @@ -#include #include "spg.h" #include "hw/holly/holly_intc.h" #include "hw/holly/sb.h" #include "hw/sh4/sh4_sched.h" -#include "oslib/oslib.h" #include "hw/maple/maple_if.h" #include "serialize.h" #include "network/ggpo.h" #include "hw/pvr/Renderer_if.h" +#include "stdclass.h" +#include #ifdef TEST_AUTOMATION #include "input/gamepad_device.h" @@ -21,7 +21,7 @@ static u32 prv_cur_scanline = -1; #if !defined(NDEBUG) || defined(DEBUGFAST) static u32 vblk_cnt; -static float last_fps; +static u64 last_fps; #endif // 27 mhz pixel clock @@ -156,17 +156,18 @@ static int spg_line_sched(int tag, int cycles, int jitter, void *arg) rend_vblank(); - double now = os_GetSeconds() * 1000000.0; + u64 now = getTimeMs(); cpu_time_idx = (cpu_time_idx + 1) % cpu_cycles.size(); if (cpu_cycles[cpu_time_idx] != 0) { u32 cycle_span = (u32)(sh4_sched_now64() - cpu_cycles[cpu_time_idx]); - double time_span = now - real_times[cpu_time_idx]; - double cpu_speed = ((double)cycle_span / time_span) / (SH4_MAIN_CLOCK / 100000000); - SH4FastEnough = cpu_speed >= 85.0; + u64 time_span = now - real_times[cpu_time_idx]; + float cpu_speed = ((float)cycle_span / time_span) / (SH4_MAIN_CLOCK / 100000); + SH4FastEnough = cpu_speed >= 85.f; } - else + else { SH4FastEnough = false; + } cpu_cycles[cpu_time_idx] = sh4_sched_now64(); real_times[cpu_time_idx] = now; @@ -176,15 +177,15 @@ static int spg_line_sched(int tag, int cycles, int jitter, void *arg) #if !defined(NDEBUG) || defined(DEBUGFAST) vblk_cnt++; - if ((os_GetSeconds()-last_fps)>2) + if (getTimeMs() - last_fps >= 2000) { static int Last_FC; - double ts=os_GetSeconds()-last_fps; - double spd_fps=(FrameCount-Last_FC)/ts; - double spd_vbs=vblk_cnt/ts; - double spd_cpu=spd_vbs*Frame_Cycles; - spd_cpu/=1000000; //mrhz kthx - double fullvbs=(spd_vbs/spd_cpu)*200; + double ts = ((double)getTimeMs() - last_fps) / 1000.0; + double spd_fps = (FrameCount - Last_FC) / ts; + double spd_vbs = vblk_cnt / ts; + double spd_cpu = spd_vbs * Frame_Cycles; + spd_cpu /= 1000000.0; //mrhz kthx + double fullvbs = (spd_vbs / spd_cpu) * 200.0; Last_FC=FrameCount; @@ -210,13 +211,13 @@ static int spg_line_sched(int tag, int cycles, int jitter, void *arg) double full_rps = spd_fps + fskip / ts; - INFO_LOG(COMMON, "%s/%c - %4.2f - %4.2f - V: %4.2f (%.2f, %s%s%4.2f) R: %4.2f+%4.2f", - VER_SHORTNAME,'n',mspdf,spd_cpu*100/200,spd_vbs, - spd_vbs/full_rps,mode,res,fullvbs, - spd_fps,fskip/ts); + INFO_LOG(COMMON, "SPG - %4.2f - %4.2f - V: %4.2f (%.2f, %s%s%4.2f) R: %4.2f+%4.2f", + mspdf, spd_cpu * 100 / 200, spd_vbs, + spd_vbs / full_rps, mode, res, fullvbs, + spd_fps, fskip / ts); - fskip=0; - last_fps=os_GetSeconds(); + fskip = 0; + last_fps = getTimeMs(); } #endif } @@ -314,19 +315,11 @@ void spg_Deserialize(Deserializer& deser) if (deser.version() < Deserializer::V30) deser.skip(); // in_vblank deser >> clc_pvr_scanline; - if (deser.version() >= Deserializer::V12) - { - deser >> maple_int_pending; - if (deser.version() >= Deserializer::V14) - { - deser >> pvr_numscanlines; - deser >> prv_cur_scanline; - deser >> Line_Cycles; - deser >> Frame_Cycles; - deser >> lightgun_line; - deser >> lightgun_hpos; - } - } - if (deser.version() < Deserializer::V14) - CalculateSync(); + deser >> maple_int_pending; + deser >> pvr_numscanlines; + deser >> prv_cur_scanline; + deser >> Line_Cycles; + deser >> Frame_Cycles; + deser >> lightgun_line; + deser >> lightgun_hpos; } diff --git a/core/hw/pvr/ta_ctx.cpp b/core/hw/pvr/ta_ctx.cpp index 58c23d0d1..dd64b05a4 100644 --- a/core/hw/pvr/ta_ctx.cpp +++ b/core/hw/pvr/ta_ctx.cpp @@ -249,8 +249,7 @@ static void deserializeContext(Deserializer& deser, TA_context **pctx) tad_context& tad = (*pctx)->tad; deser.deserialize(tad.thd_root, size); tad.thd_data = tad.thd_root + size; - if ((deser.version() >= Deserializer::V12 && deser.version() < Deserializer::V26) - || (deser.version() >= Deserializer::V12_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO)) + if (deser.version() < Deserializer::V26) { u32 render_pass_count; deser >> render_pass_count; diff --git a/core/hw/sh4/modules/fastmmu.cpp b/core/hw/sh4/modules/fastmmu.cpp index e599d03b5..bc9c08487 100644 --- a/core/hw/sh4/modules/fastmmu.cpp +++ b/core/hw/sh4/modules/fastmmu.cpp @@ -20,6 +20,7 @@ #include "hw/sh4/sh4_if.h" #include "hw/sh4/sh4_core.h" #include "types.h" +#include "stdclass.h" #ifdef FAST_MMU @@ -156,7 +157,7 @@ int main(int argc, char *argv[]) addrs.push_back(random()); asids.push_back(666); } - double start = os_GetSeconds(); + u64 start = getTimeMs(); int success = 0; const int loops = 100000; for (int i = 0; i < loops; i++) @@ -170,8 +171,8 @@ int main(int argc, char *argv[]) success++; } } - double end = os_GetSeconds(); - printf("Lookup time: %f ms. Success rate %f max_len %d\n", (end - start) * 1000.0 / addrs.size(), (double)success / addrs.size() / loops, 0/*max_length*/); + u64 end = getTimeMs(); + printf("Lookup time: %f ms. Success rate %f max_len %d\n", ((double)end - start) / addrs.size(), (double)success / addrs.size() / loops, 0/*max_length*/); } #endif diff --git a/core/hw/sh4/modules/mmu.cpp b/core/hw/sh4/modules/mmu.cpp index 54ec72d51..483125fbb 100644 --- a/core/hw/sh4/modules/mmu.cpp +++ b/core/hw/sh4/modules/mmu.cpp @@ -591,8 +591,6 @@ void mmu_deserialize(Deserializer& deser) deser >> UTLB; deser >> ITLB; - if (deser.version() >= Deserializer::V11 - || (deser.version() >= Deserializer::V11_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO)) - deser >> sq_remap; + deser >> sq_remap; deser.skip(64 * 4, Deserializer::V23); // ITLB_LRU_USE } diff --git a/core/hw/sh4/sh4_mmr.cpp b/core/hw/sh4/sh4_mmr.cpp index 5d1579830..5372003a0 100644 --- a/core/hw/sh4/sh4_mmr.cpp +++ b/core/hw/sh4/sh4_mmr.cpp @@ -692,59 +692,24 @@ void serialize(Serializer& ser) sh4_sched_serialize(ser); } -template -static void register_deserialize_libretro(T& regs, Deserializer& deser) -{ - for (auto& reg : regs) - { - deser.skip(); // regs.data[i].flags - deser >> reg; - } -} - void deserialize(Deserializer& deser) { deser >> OnChipRAM; - if (deser.version() <= Deserializer::VLAST_LIBRETRO) - { - register_deserialize_libretro(CCN, deser); - register_deserialize_libretro(UBC, deser); - register_deserialize_libretro(BSC, deser); - register_deserialize_libretro(DMAC, deser); - register_deserialize_libretro(CPG, deser); - register_deserialize_libretro(RTC, deser); - register_deserialize_libretro(INTC, deser); - register_deserialize_libretro(TMU, deser); - register_deserialize_libretro(SCI, deser); - register_deserialize_libretro(SCIF, deser); - } - else - { - deser >> CCN; - deser >> UBC; - deser >> BSC; - deser >> DMAC; - deser >> CPG; - deser >> RTC; - deser >> INTC; - deser >> TMU; - deser >> SCI; - deser >> SCIF; - } + deser >> CCN; + deser >> UBC; + deser >> BSC; + deser >> DMAC; + deser >> CPG; + deser >> RTC; + deser >> INTC; + deser >> TMU; + deser >> SCI; + deser >> SCIF; + SCIFSerialPort::Instance().deserialize(deser); - if (deser.version() >= Deserializer::V9 - // Note (lr): was added in V11 fa49de29 24/12/2020 but ver not updated until V12 (13/4/2021) - || (deser.version() >= Deserializer::V11_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO)) - icache.Deserialize(deser); - else - icache.Reset(true); - if (deser.version() >= Deserializer::V10 - // Note (lr): was added in V11 2eb66879 27/12/2020 but ver not updated until V12 (13/4/2021) - || (deser.version() >= Deserializer::V11_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO)) - ocache.Deserialize(deser); - else - ocache.Reset(true); + icache.Deserialize(deser); + ocache.Deserialize(deser); if (!deser.rollback()) mem_b.deserialize(deser); @@ -778,11 +743,7 @@ void deserialize2(Deserializer& deser) if (deser.version() <= Deserializer::V32) { deser >> SCIF_SCFSR2; - if (deser.version() >= Deserializer::V11 - || (deser.version() >= Deserializer::V11_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO)) - deser >> SCIF_SCSCR2; - else - SCIF_SCSCR2.full = 0; + deser >> SCIF_SCSCR2; deser >> BSC_PDTRA; } diff --git a/core/imgread/common.cpp b/core/imgread/common.cpp index 6aa6853f5..b99ed778f 100644 --- a/core/imgread/common.cpp +++ b/core/imgread/common.cpp @@ -350,6 +350,7 @@ bool DiscSwap(const std::string& path) { if (!doDiscSwap(path)) throw FlycastException("This media cannot be loaded"); + EventManager::event(Event::DiskChange); // Drive is busy after the lid was closed sns_asc = 4; sns_ascq = 1; diff --git a/core/input/gamepad.h b/core/input/gamepad.h index e428b62c1..88bce6fe7 100644 --- a/core/input/gamepad.h +++ b/core/input/gamepad.h @@ -51,6 +51,7 @@ enum DreamcastKey EMU_BTN_LOADSTATE, EMU_BTN_SAVESTATE, EMU_BTN_BYPASS_KB, + EMU_BTN_SCREENSHOT, // Real axes DC_AXIS_TRIGGERS = 0x1000000, diff --git a/core/input/gamepad_device.cpp b/core/input/gamepad_device.cpp index 8fdbc1b7e..7ca658414 100644 --- a/core/input/gamepad_device.cpp +++ b/core/input/gamepad_device.cpp @@ -19,8 +19,8 @@ #include "gamepad_device.h" #include "cfg/cfg.h" -#include "oslib/oslib.h" -#include "rend/gui.h" +#include "stdclass.h" +#include "ui/gui.h" #include "emulator.h" #include "hw/maple/maple_devs.h" #include "mouse.h" @@ -99,6 +99,10 @@ bool GamepadDevice::handleButtonInput(int port, DreamcastKey key, bool pressed) if (pressed) gui_saveState(); break; + case EMU_BTN_SCREENSHOT: + if (pressed) + gui_takeScreenshot(); + break; case DC_AXIS_LT: if (port >= 0) lt[port] = pressed ? 0xffff : 0; @@ -152,7 +156,7 @@ bool GamepadDevice::handleButtonInput(int port, DreamcastKey key, bool pressed) bool GamepadDevice::gamepad_btn_input(u32 code, bool pressed) { if (_input_detected != nullptr && _detecting_button - && os_GetSeconds() >= _detection_start_time && pressed) + && getTimeMs() >= _detection_start_time && pressed) { _input_detected(code, false, false); _input_detected = nullptr; @@ -207,7 +211,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) { bool positive = value >= 0; if (_input_detected != NULL && _detecting_axis - && os_GetSeconds() >= _detection_start_time && std::abs(value) >= 16384) + && getTimeMs() >= _detection_start_time && std::abs(value) >= 16384) { _input_detected(code, true, positive); _input_detected = nullptr; @@ -505,7 +509,7 @@ void GamepadDevice::detect_btn_input(input_detected_cb button_pressed) _input_detected = button_pressed; _detecting_button = true; _detecting_axis = false; - _detection_start_time = os_GetSeconds() + 0.2; + _detection_start_time = getTimeMs() + 200; } void GamepadDevice::detect_axis_input(input_detected_cb axis_moved) @@ -513,7 +517,7 @@ void GamepadDevice::detect_axis_input(input_detected_cb axis_moved) _input_detected = axis_moved; _detecting_button = false; _detecting_axis = true; - _detection_start_time = os_GetSeconds() + 0.2; + _detection_start_time = getTimeMs() + 200; } void GamepadDevice::detectButtonOrAxisInput(input_detected_cb input_changed) @@ -521,7 +525,7 @@ void GamepadDevice::detectButtonOrAxisInput(input_detected_cb input_changed) _input_detected = input_changed; _detecting_button = true; _detecting_axis = true; - _detection_start_time = os_GetSeconds() + 0.2; + _detection_start_time = getTimeMs() + 200; } #ifdef TEST_AUTOMATION diff --git a/core/input/gamepad_device.h b/core/input/gamepad_device.h index 4941f1c7f..aef633870 100644 --- a/core/input/gamepad_device.h +++ b/core/input/gamepad_device.h @@ -177,7 +177,7 @@ private: int _maple_port; bool _detecting_button = false; bool _detecting_axis = false; - double _detection_start_time = 0.0; + u64 _detection_start_time = 0; input_detected_cb _input_detected; bool _remappable; u32 digitalToAnalogState[4]; diff --git a/core/input/keyboard_device.h b/core/input/keyboard_device.h index c6cbb5d29..682408556 100644 --- a/core/input/keyboard_device.h +++ b/core/input/keyboard_device.h @@ -20,7 +20,7 @@ #include "types.h" #include "cfg/option.h" #include "gamepad_device.h" -#include "rend/gui.h" +#include "ui/gui.h" #include extern u8 kb_key[4][6]; // normal keys pressed @@ -61,6 +61,7 @@ public: set_button(DC_AXIS_LEFT, 13); // J set_button(DC_AXIS_RIGHT, 15); // L set_button(DC_BTN_D, 4); // Q (Coin) + set_button(EMU_BTN_SCREENSHOT, 69); // F12 dirty = false; } diff --git a/core/input/mapping.cpp b/core/input/mapping.cpp index 336fc8bd4..fdea33989 100644 --- a/core/input/mapping.cpp +++ b/core/input/mapping.cpp @@ -61,6 +61,7 @@ button_list[] = { EMU_BTN_LOADSTATE, "emulator", "btn_jump_state" }, { EMU_BTN_SAVESTATE, "emulator", "btn_quick_save" }, { EMU_BTN_BYPASS_KB, "emulator", "btn_bypass_kb" }, + { EMU_BTN_SCREENSHOT, "emulator", "btn_screenshot" }, }; static struct diff --git a/core/input/mapping.h b/core/input/mapping.h index 4e8925005..8adac51af 100644 --- a/core/input/mapping.h +++ b/core/input/mapping.h @@ -36,6 +36,7 @@ public: name = other.name; dead_zone = other.dead_zone; saturation = other.saturation; + rumblePower = other.rumblePower; for (int port = 0; port < 4; port++) { buttons[port] = other.buttons[port]; diff --git a/core/input/mouse.cpp b/core/input/mouse.cpp index 6e171e512..7efdf933f 100644 --- a/core/input/mouse.cpp +++ b/core/input/mouse.cpp @@ -18,7 +18,7 @@ */ #include "mouse.h" #include "cfg/option.h" -#include "rend/gui.h" +#include "ui/gui.h" // Mouse buttons // bit 0: Button C diff --git a/core/linux-dist/dispmanx.cpp b/core/linux-dist/dispmanx.cpp deleted file mode 100644 index adafdf984..000000000 --- a/core/linux-dist/dispmanx.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#if defined(SUPPORT_DISPMANX) -#include "dispmanx.h" -#include "types.h" -#include "wsi/context.h" -#include "cfg/option.h" - -#include -#include - -void dispmanx_window_create() -{ - DISPMANX_DISPLAY_HANDLE_T dispman_display; - DISPMANX_UPDATE_HANDLE_T dispman_update; - DISPMANX_ELEMENT_HANDLE_T dispman_element; - VC_RECT_T src_rect; - VC_RECT_T dst_rect; - VC_DISPMANX_ALPHA_T dispman_alpha; - uint32_t screen_width; - uint32_t screen_height; - uint32_t window_width; - uint32_t window_height; - - dispman_alpha.flags = DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS; - dispman_alpha.opacity = 0xFF; - dispman_alpha.mask = 0; - - graphics_get_display_size(0 /* LCD */, &screen_width, &screen_height); - - window_width = cfgLoadInt("window", "width", 0); - window_height = cfgLoadInt("window", "height", 0); - - if(window_width < 1) - window_width = screen_width; - if(window_height < 1) - window_height = screen_height; - - src_rect.x = 0; - src_rect.y = 0; - src_rect.width = window_width << 16; - src_rect.height = window_height << 16; - - if (config::DispmanxMaintainAspect) - { - float screen_aspect = (float)screen_width / screen_height; - float window_aspect = (float)window_width / window_height; - if(screen_aspect > window_aspect) - { - dst_rect.width = window_width * screen_height / window_height; - dst_rect.height = screen_height; - } - else - { - dst_rect.width = screen_width; - dst_rect.height = window_height * screen_width / window_width; - } - dst_rect.x = (screen_width - dst_rect.width) / 2; - dst_rect.y = (screen_height - dst_rect.height) / 2; - } - else - { - dst_rect.x = 0; - dst_rect.y = 0; - dst_rect.width = screen_width; - dst_rect.height = screen_height; - } - - dispman_display = vc_dispmanx_display_open( 0 /* LCD */); - dispman_update = vc_dispmanx_update_start( 0 ); - dispman_element = vc_dispmanx_element_add(dispman_update, dispman_display, - 0 /*layer*/, &dst_rect, 0 /*src*/, - &src_rect, DISPMANX_PROTECTION_NONE, - &dispman_alpha /*alpha*/, 0 /*clamp*/, (DISPMANX_TRANSFORM_T)0 /*transform*/); - - static EGL_DISPMANX_WINDOW_T native_window; - native_window.element = dispman_element; - native_window.width = window_width; - native_window.height = window_height; - vc_dispmanx_update_submit_sync( dispman_update ); - - initRenderApi(&native_window, (void *)dispman_display); -} -#endif diff --git a/core/linux-dist/dispmanx.h b/core/linux-dist/dispmanx.h deleted file mode 100644 index db95adfa8..000000000 --- a/core/linux-dist/dispmanx.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -extern void dispmanx_window_create(); diff --git a/core/linux-dist/evdev_gamepad.h b/core/linux-dist/evdev_gamepad.h index 263868453..aae93e552 100644 --- a/core/linux-dist/evdev_gamepad.h +++ b/core/linux-dist/evdev_gamepad.h @@ -1,7 +1,7 @@ #pragma once #include "evdev.h" #include "input/gamepad_device.h" -#include "oslib/oslib.h" +#include "stdclass.h" #include #include @@ -81,7 +81,7 @@ public: void rumble(float power, float inclination, u32 duration_ms) override { vib_inclination = inclination * power; - vib_stop_time = os_GetSeconds() + duration_ms / 1000.0; + vib_stop_time = getTimeMs() + duration_ms; do_rumble(power, duration_ms); } @@ -89,7 +89,7 @@ public: { if (vib_inclination > 0) { - int rem_time = (vib_stop_time - os_GetSeconds()) * 1000; + int rem_time = vib_stop_time - getTimeMs(); if (rem_time <= 0) vib_inclination = 0; else @@ -308,7 +308,7 @@ private: std::string _devnode; int _rumble_effect_id = -1; float vib_inclination = 0; - double vib_stop_time = 0; + u64 vib_stop_time = 0; std::map axis_min_values; std::map axis_ranges; static std::map> evdev_gamepads; diff --git a/core/linux-dist/main.cpp b/core/linux-dist/main.cpp index e970f880a..83a18bc76 100644 --- a/core/linux-dist/main.cpp +++ b/core/linux-dist/main.cpp @@ -3,10 +3,10 @@ #endif #include "types.h" -#if defined(__unix__) || defined(__SWITCH__) +#if defined(__unix__) #include "log/LogManager.h" #include "emulator.h" -#include "rend/mainui.h" +#include "ui/mainui.h" #include "oslib/directory.h" #include "oslib/oslib.h" #include "stdclass.h" @@ -16,14 +16,6 @@ #include #include -#if defined(__SWITCH__) -#include "nswitch.h" -#endif - -#if defined(SUPPORT_DISPMANX) - #include "dispmanx.h" -#endif - #if defined(SUPPORT_X11) #include "x11.h" #endif @@ -32,50 +24,10 @@ #include "sdl/sdl.h" #endif -#if defined(USE_EVDEV) - #include "evdev.h" -#endif - #ifdef USE_BREAKPAD #include "breakpad/client/linux/handler/exception_handler.h" #endif -void os_SetupInput() -{ -#if defined(USE_EVDEV) - input_evdev_init(); -#endif - -#if defined(SUPPORT_X11) - input_x11_init(); -#endif - -#if defined(USE_SDL) - input_sdl_init(); -#endif -} - -void os_TermInput() -{ -#if defined(USE_EVDEV) - input_evdev_close(); -#endif -#if defined(USE_SDL) - input_sdl_quit(); -#endif -} - -void UpdateInputState() -{ - #if defined(USE_EVDEV) - input_evdev_handle(); - #endif - - #if defined(USE_SDL) - input_sdl_handle(); - #endif -} - void os_DoEvents() { #if defined(SUPPORT_X11) @@ -84,39 +36,12 @@ void os_DoEvents() #endif } -void os_SetWindowText(const char * text) -{ - #if defined(SUPPORT_X11) - x11_window_set_text(text); - #endif - #if defined(USE_SDL) - sdl_window_set_text(text); - #endif -} - -void os_CreateWindow() -{ - #if defined(SUPPORT_DISPMANX) - dispmanx_window_create(); - #endif - #if defined(SUPPORT_X11) - x11_window_create(); - #endif - #if defined(USE_SDL) - sdl_window_create(); - #endif -} - void common_linux_setup(); // Find the user config directory. // $HOME/.config/flycast on linux -std::string find_user_config_dir() +static std::string find_user_config_dir() { -#ifdef __SWITCH__ - flycast::mkdir("/flycast", 0755); - return "/flycast/"; -#else std::string xdg_home; if (nowide::getenv("XDG_CONFIG_HOME") != nullptr) // If XDG_CONFIG_HOME is set explicitly, we'll use that instead of $HOME/.config @@ -140,17 +65,12 @@ std::string find_user_config_dir() } // Unable to detect config dir, use the current folder return "."; -#endif } // Find the user data directory. // $HOME/.local/share/flycast on linux -std::string find_user_data_dir() +static std::string find_user_data_dir() { -#ifdef __SWITCH__ - flycast::mkdir("/flycast/data", 0755); - return "/flycast/data/"; -#else std::string xdg_home; if (nowide::getenv("XDG_DATA_HOME") != nullptr) // If XDG_DATA_HOME is set explicitly, we'll use that instead of $HOME/.local/share @@ -174,10 +94,8 @@ std::string find_user_data_dir() } // Unable to detect data dir, use the current folder return "."; -#endif } -#ifndef __SWITCH__ static void addDirectoriesFromPath(std::vector& dirs, const std::string& path, const std::string& suffix) { std::string::size_type pos = 0; @@ -193,7 +111,6 @@ static void addDirectoriesFromPath(std::vector& dirs, const std::st if (pos < path.length()) dirs.push_back(path.substr(pos) + suffix); } -#endif // Find a file in the user and system config directories. // The following folders are checked in this order: @@ -204,13 +121,10 @@ static void addDirectoriesFromPath(std::vector& dirs, const std::st // /etc/flycast/ // /etc/xdg/flycast/ // . -std::vector find_system_config_dirs() +static std::vector find_system_config_dirs() { std::vector dirs; -#ifdef __SWITCH__ - dirs.push_back("/flycast/"); -#else std::string xdg_home; if (nowide::getenv("XDG_CONFIG_HOME") != nullptr) // If XDG_CONFIG_HOME is set explicitly, we'll use that instead of $HOME/.config @@ -235,7 +149,6 @@ std::vector find_system_config_dirs() dirs.push_back("/etc/flycast/"); // This isn't part of the XDG spec, but much more common than /etc/xdg/ dirs.push_back("/etc/xdg/flycast/"); } -#endif dirs.push_back("./"); return dirs; @@ -252,13 +165,10 @@ std::vector find_system_config_dirs() // <$FLYCAST_BIOS_PATH> // ./ // ./data -std::vector find_system_data_dirs() +static std::vector find_system_data_dirs() { std::vector dirs; -#ifdef __SWITCH__ - dirs.push_back("/flycast/data/"); -#else std::string xdg_home; if (nowide::getenv("XDG_DATA_HOME") != nullptr) // If XDG_DATA_HOME is set explicitly, we'll use that instead of $HOME/.local/share @@ -288,7 +198,6 @@ std::vector find_system_data_dirs() std::string path = (std::string)nowide::getenv("FLYCAST_BIOS_PATH"); addDirectoriesFromPath(dirs, path, "/"); } -#endif dirs.push_back("./"); dirs.push_back("data/"); @@ -323,11 +232,6 @@ static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, int main(int argc, char* argv[]) { selfPath = argv[0]; -#if defined(__SWITCH__) - socketInitializeDefault(); - nxlinkStdio(); - //appletSetFocusHandlingMode(AppletFocusHandlingMode_NoSuspend); -#endif #if defined(USE_BREAKPAD) google_breakpad::MinidumpDescriptor descriptor("/tmp"); google_breakpad::ExceptionHandler eh(descriptor, nullptr, dumpCallback, nullptr, true, -1); @@ -353,9 +257,7 @@ int main(int argc, char* argv[]) } #endif -#if defined(__unix__) common_linux_setup(); -#endif if (flycast_init(argc, argv)) die("Flycast initialization failed\n"); @@ -366,29 +268,16 @@ int main(int argc, char* argv[]) mainui_loop(); -#if defined(SUPPORT_X11) - x11_window_destroy(); -#endif -#if defined(USE_SDL) - sdl_window_destroy(); -#endif - flycast_term(); os_UninstallFaultHandler(); -#if defined(__SWITCH__) - socketExit(); -#endif - return 0; } -#if defined(__unix__) [[noreturn]] void os_DebugBreak() { raise(SIGTRAP); std::abort(); } -#endif -#endif // __unix__ || __SWITCH__ +#endif // __unix__ diff --git a/core/linux-dist/x11.cpp b/core/linux-dist/x11.cpp index f80c1febb..f7e3c329f 100644 --- a/core/linux-dist/x11.cpp +++ b/core/linux-dist/x11.cpp @@ -6,7 +6,7 @@ #include "types.h" #include "cfg/cfg.h" #include "x11.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "input/gamepad.h" #include "input/mouse.h" #include "icon.h" @@ -21,6 +21,8 @@ #define DEFAULT_WINDOW_WIDTH 640 #define DEFAULT_WINDOW_HEIGHT 480 +static void x11_window_set_text(const char *text); + static Window x11_win; Display *x11_disp; @@ -356,7 +358,7 @@ void x11_window_create() } } -void x11_window_set_text(const char* text) +static void x11_window_set_text(const char* text) { if (x11_win) { diff --git a/core/linux-dist/x11.h b/core/linux-dist/x11.h index 75c176531..7013db703 100644 --- a/core/linux-dist/x11.h +++ b/core/linux-dist/x11.h @@ -1,11 +1,10 @@ #pragma once -extern void input_x11_init(); -extern void event_x11_handle(); -extern void input_x11_handle(); -extern void x11_window_create(); -extern void x11_window_set_text(const char* text); -extern void x11_window_destroy(); +void input_x11_init(); +void event_x11_handle(); +void input_x11_handle(); +void x11_window_create(); +void x11_window_destroy(); // numbers const int KEY_1 = 10; diff --git a/core/linux/common.cpp b/core/linux/common.cpp index d3b2651d3..0ca150af0 100644 --- a/core/linux/common.cpp +++ b/core/linux/common.cpp @@ -11,10 +11,10 @@ #if defined(__linux__) && !defined(__ANDROID__) #include #endif -#if !defined(TARGET_BSD) && !defined(__ANDROID__) && defined(TARGET_VIDEOCORE) - #include -#endif #include +#ifdef __linux__ +#include +#endif #include "oslib/host_context.h" @@ -22,6 +22,7 @@ #include "rend/TexCache.h" #include "hw/mem/addrspace.h" #include "hw/mem/mem_watch.h" +#include "emulator.h" #ifdef __SWITCH__ #include @@ -119,14 +120,6 @@ void os_UninstallFaultHandler() } #endif // !defined(TARGET_NO_EXCEPTIONS) -double os_GetSeconds() -{ - timeval a; - gettimeofday (&a,0); - static u64 tvs_base=a.tv_sec; - return a.tv_sec-tvs_base+a.tv_usec/1000000.0; -} - #if !defined(__unix__) && !defined(LIBRETRO) && !defined(__SWITCH__) [[noreturn]] void os_DebugBreak() { @@ -134,11 +127,15 @@ double os_GetSeconds() } #endif -void enable_runfast() +// RunFast mode is the combination of the following conditions: +// * the VFP11 coprocessor is in flush-to-zero mode +// * the VFP11 coprocessor is in default NaN mode +// * all exception enable bits are cleared. +static void enable_runfast() { - #if HOST_CPU==CPU_ARM && !defined(ARMCC) - static const unsigned int x = 0x04086060; - static const unsigned int y = 0x03000000; +#if HOST_CPU == CPU_ARM && !defined(ARMCC) + static const unsigned int x = 0x04086060; // reset and disable FP exceptions, flush-to-zero, default NaN mode + static const unsigned int y = 0x03000000; // round to zero int r; asm volatile ( "fmrx %0, fpscr \n\t" //r0 = FPSCR @@ -150,45 +147,58 @@ void enable_runfast() ); DEBUG_LOG(BOOT, "ARM VFP-Run Fast (NFP) enabled !"); - #endif +#endif } -void linux_fix_personality() { -#if defined(__linux__) && !defined(__ANDROID__) +// Some old CPUs lack the NX (no exec) flag so READ_IMPLIES_EXEC is set by default on these platforms. +// However resetting the flag isn't going to magically change the way the CPU works. So I wonder how useful this is. +// It's not needed on modern 64-bit architectures anyway. +static void linux_fix_personality() +{ +#if defined(__linux__) && !defined(__ANDROID__) && (HOST_CPU == CPU_X86 || HOST_CPU == CPU_ARM) DEBUG_LOG(BOOT, "Personality: %08X", personality(0xFFFFFFFF)); personality(~READ_IMPLIES_EXEC & personality(0xFFFFFFFF)); DEBUG_LOG(BOOT, "Updated personality: %08X", personality(0xFFFFFFFF)); #endif } -void linux_rpi2_init() { -#if !defined(TARGET_BSD) && !defined(__ANDROID__) && defined(TARGET_VIDEOCORE) - void* handle; - void (*rpi_bcm_init)(void); - - handle = dlopen("libbcm_host.so", RTLD_LAZY); - - if (handle) { - DEBUG_LOG(BOOT, "found libbcm_host"); - *(void**) (&rpi_bcm_init) = dlsym(handle, "bcm_host_init"); - if (rpi_bcm_init) { - DEBUG_LOG(BOOT, "rpi2: bcm_init"); - rpi_bcm_init(); - } - } -#endif +#if defined(__unix__) && !defined(LIBRETRO) && !defined(__ANDROID__) +static void sigintHandler(int) +{ + dc_exit(); } +#endif void common_linux_setup() { linux_fix_personality(); - linux_rpi2_init(); enable_runfast(); os_InstallFaultHandler(); - signal(SIGINT, exit); +#if defined(__unix__) && !defined(LIBRETRO) && !defined(__ANDROID__) + // exit cleanly on ^C + signal(SIGINT, sigintHandler); +#endif DEBUG_LOG(BOOT, "Linux paging: %ld %08X %08X", sysconf(_SC_PAGESIZE), PAGE_SIZE, PAGE_MASK); verify(PAGE_MASK==(sysconf(_SC_PAGESIZE)-1)); } + +#ifndef __APPLE__ + +void os_SetThreadName(const char *name) +{ +#ifdef __linux__ + if (strlen(name) > 16) + { + static char tmp[17]; + strncpy(tmp, name, 16); + name = tmp; + } + pthread_setname_np(pthread_self(), name); +#endif +} + +#endif + #endif // __unix__ or __APPLE__ or __SWITCH__ diff --git a/core/log/LogManager.cpp b/core/log/LogManager.cpp index d86102ac4..dacaac706 100644 --- a/core/log/LogManager.cpp +++ b/core/log/LogManager.cpp @@ -175,10 +175,12 @@ LogManager::~LogManager() // in the form 00:00:000. static std::string GetTimeFormatted() { - double now = os_GetSeconds(); - u32 minutes = (u32)now / 60; - u32 seconds = (u32)now % 60; - u32 ms = (now - (u32)now) * 1000; + u64 now = getTimeMs(); + u32 ms = (u32)(now % 1000); + now /= 1000; + u32 seconds = (u32)(now % 60); + now /= 60; + u32 minutes = (u32)now; return StringFromFormat("%02d:%02d:%03d", minutes, seconds, ms); } diff --git a/core/lua/lua.cpp b/core/lua/lua.cpp index da69edb79..194eb1542 100644 --- a/core/lua/lua.cpp +++ b/core/lua/lua.cpp @@ -21,7 +21,8 @@ #ifdef USE_LUA #include #include -#include "rend/gui.h" +#include "ui/gui.h" +#include "ui/gui_util.h" #include "hw/mem/addrspace.h" #include "cfg/option.h" #include "emulator.h" @@ -43,7 +44,7 @@ using lock_guard = std::lock_guard; static void emuEventCallback(Event event, void *) { - if (L == nullptr) + if (L == nullptr || settings.raHardcoreMode) return; lock_guard lock(mutex); try { @@ -71,6 +72,12 @@ static void emuEventCallback(Event event, void *) case Event::VBlank: key = "vblank"; break; + case Event::Network: + key = "network"; + break; + case Event::DiskChange: + key = "diskChange"; + break; } if (v[key].isFunction()) v[key](); @@ -363,7 +370,7 @@ static void beginWindow(const char *title, int x, int y, int w, int h) ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); ImGui::SetNextWindowPos(ImVec2(x, y)); - ImGui::SetNextWindowSize(ImVec2(w * settings.display.uiScale, h * settings.display.uiScale)); + ImGui::SetNextWindowSize(ScaledVec2(w, h)); ImGui::SetNextWindowBgAlpha(0.7f); ImGui::Begin(title, NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f)); @@ -389,7 +396,7 @@ static void uiTextRightAligned(const std::string& text) static void uiBargraph(float v) { - ImGui::ProgressBar(v, ImVec2(-1, 10.f * settings.display.uiScale), ""); + ImGui::ProgressBar(v, ImVec2(-1, uiScaled(10.f)), ""); } static int uiButton(lua_State *L) @@ -440,7 +447,7 @@ static void luaRegister(lua_State *L) gui_open_settings(); })) .addFunction("exit", dc_exit) - .addFunction("displayNotification", gui_display_notification) + .addFunction("displayNotification", os_notify) .endNamespace() .beginNamespace("config") @@ -619,6 +626,7 @@ void init() EventManager::listen(Event::Terminate, emuEventCallback); EventManager::listen(Event::LoadState, emuEventCallback); EventManager::listen(Event::VBlank, emuEventCallback); + EventManager::listen(Event::Network, emuEventCallback); doExec(initFile); } @@ -633,6 +641,7 @@ void term() EventManager::unlisten(Event::Terminate, emuEventCallback); EventManager::unlisten(Event::LoadState, emuEventCallback); EventManager::unlisten(Event::VBlank, emuEventCallback); + EventManager::unlisten(Event::Network, emuEventCallback); lua_close(L); L = nullptr; } diff --git a/core/network/ggpo.cpp b/core/network/ggpo.cpp index 2f5f9bb4f..0672af39b 100644 --- a/core/network/ggpo.cpp +++ b/core/network/ggpo.cpp @@ -23,10 +23,9 @@ #include "input/keyboard_device.h" #include "input/mouse.h" #include "cfg/option.h" +#include "oslib/oslib.h" #include -void UpdateInputState(); - namespace ggpo { @@ -35,7 +34,7 @@ bool inRollback; static void getLocalInput(MapleInputState inputState[4]) { if (!config::ThreadedRendering) - UpdateInputState(); + os_UpdateInputState(); std::lock_guard lock(relPosMutex); for (int player = 0; player < 4; player++) { @@ -73,7 +72,8 @@ static void getLocalInput(MapleInputState inputState[4]) #ifdef USE_GGPO #include "ggponet.h" #include "emulator.h" -#include "rend/gui.h" +#include "ui/gui.h" +#include "ui/gui_util.h" #include "hw/mem/mem_watch.h" #include #include @@ -216,19 +216,19 @@ static bool on_event(GGPOEvent *info) switch (info->code) { case GGPO_EVENTCODE_CONNECTED_TO_PEER: INFO_LOG(NETWORK, "Connected to peer %d", info->u.connected.player); - gui_display_notification("Connected to peer", 2000); + os_notify("Connected to peer", 2000); break; case GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER: INFO_LOG(NETWORK, "Synchronizing with peer %d", info->u.synchronizing.player); - gui_display_notification("Synchronizing with peer", 2000); + os_notify("Synchronizing with peer", 2000); break; case GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER: INFO_LOG(NETWORK, "Synchronized with peer %d", info->u.synchronized.player); - gui_display_notification("Synchronized with peer", 2000); + os_notify("Synchronized with peer", 2000); break; case GGPO_EVENTCODE_RUNNING: INFO_LOG(NETWORK, "Running"); - gui_display_notification("Running", 2000); + os_notify("Running", 2000); synchronized = true; break; case GGPO_EVENTCODE_DISCONNECTED_FROM_PEER: @@ -242,11 +242,11 @@ static bool on_event(GGPOEvent *info) break; case GGPO_EVENTCODE_CONNECTION_INTERRUPTED: INFO_LOG(NETWORK, "Connection interrupted with player %d", info->u.connection_interrupted.player); - gui_display_notification("Connection interrupted", 2000); + os_notify("Connection interrupted", 2000); break; case GGPO_EVENTCODE_CONNECTION_RESUMED: INFO_LOG(NETWORK, "Connection resumed with player %d", info->u.connection_resumed.player); - gui_display_notification("Connection resumed", 2000); + os_notify("Connection resumed", 2000); break; } return true; @@ -706,7 +706,7 @@ bool nextFrame() // may call save_game_state do { if (!config::ThreadedRendering) - UpdateInputState(); + os_UpdateInputState(); Inputs inputs; inputs.kcode = ~kcode[0]; if (rt[0] >= 0x4000) @@ -783,6 +783,7 @@ std::future startNetwork() synchronized = false; return std::async(std::launch::async, []{ { + ThreadName _("GGPO-start"); std::lock_guard lock(ggpoMutex); #ifdef SYNC_TEST startSession(0, 0); @@ -857,17 +858,17 @@ void displayStats() GGPONetworkStats stats; ggpo_get_network_stats(ggpoSession, remotePlayer, &stats); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0); ImGui::SetNextWindowPos(ImVec2(10, 10)); - ImGui::SetNextWindowSize(ImVec2(95 * settings.display.uiScale, 0)); + ImGui::SetNextWindowSize(ScaledVec2(95, 0)); ImGui::SetNextWindowBgAlpha(0.7f); ImGui::Begin("##ggpostats", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f)); + ImguiStyleColor _2(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f)); // Send Queue ImGui::Text("Send Q"); - ImGui::ProgressBar(stats.network.send_queue_len / 10.f, ImVec2(-1, 10.f * settings.display.uiScale), ""); + ImGui::ProgressBar(stats.network.send_queue_len / 10.f, ImVec2(-1, uiScaled(10.f)), ""); // Frame Delay ImGui::Text("Delay"); @@ -889,7 +890,7 @@ void displayStats() // yellow ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(.9f, .9f, .1f, 1)); ImGui::Text("Predicted"); - ImGui::ProgressBar(stats.sync.predicted_frames / 7.f, ImVec2(-1, 10.f * settings.display.uiScale), ""); + ImGui::ProgressBar(stats.sync.predicted_frames / 7.f, ImVec2(-1, uiScaled(10.f)), ""); if (stats.sync.predicted_frames >= 5) ImGui::PopStyleColor(); @@ -898,16 +899,14 @@ void displayStats() if (timesync > 0) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0, 0, 1)); ImGui::Text("Behind"); - ImGui::ProgressBar(0.5f + stats.timesync.local_frames_behind / 16.f, ImVec2(-1, 10.f * settings.display.uiScale), ""); + ImGui::ProgressBar(0.5f + stats.timesync.local_frames_behind / 16.f, ImVec2(-1, uiScaled(10.f)), ""); if (timesync > 0) { ImGui::PopStyleColor(); timesyncOccurred--; } - ImGui::PopStyleColor(); ImGui::End(); - ImGui::PopStyleVar(2); } void endOfFrame() diff --git a/core/network/naomi_network.cpp b/core/network/naomi_network.cpp index 758cea437..43500056d 100644 --- a/core/network/naomi_network.cpp +++ b/core/network/naomi_network.cpp @@ -19,7 +19,7 @@ #include "naomi_network.h" #include "hw/naomi/naomi_flashrom.h" #include "cfg/option.h" -#include "rend/gui.h" +#include "oslib/oslib.h" #include #include @@ -104,7 +104,7 @@ bool NaomiNetwork::startNetwork() std::string notif = slaves.empty() ? "Waiting for players..." : std::to_string(slaves.size()) + " player(s) connected. Waiting..."; - gui_display_notification(notif.c_str(), timeout.count() * 2000); + os_notify(notif.c_str(), timeout.count() * 2000); poll(); @@ -125,12 +125,12 @@ bool NaomiNetwork::startNetwork() nextPeer = slaves[0].addr; - gui_display_notification("Starting game", 2000); + os_notify("Starting game", 2000); SetNaomiNetworkConfig(0); return true; } - gui_display_notification("No player connected", 8000); + os_notify("No player connected", 8000); } else { @@ -164,7 +164,7 @@ bool NaomiNetwork::startNetwork() } NOTICE_LOG(NETWORK, "Connecting to server"); - gui_display_notification("Connecting to server", 10000); + os_notify("Connecting to server", 10000); steady_clock::time_point start_time = steady_clock::now(); while (!networkStopping && !_startNow && steady_clock::now() - start_time < timeout) @@ -249,7 +249,7 @@ bool NaomiNetwork::receive(const sockaddr_in *addr, const Packet *packet, u32 si nextPeer.sin_port = packet->sync.nextNodePort; nextPeer.sin_addr.s_addr = packet->sync.nextNodeIp == 0 ? addr->sin_addr.s_addr : packet->sync.nextNodeIp; std::string notif = "Connected as slot " + std::to_string(slotId); - gui_display_notification(notif.c_str(), 2000); + os_notify(notif.c_str(), 2000); } break; diff --git a/core/network/naomi_network.h b/core/network/naomi_network.h index b84675e5c..378c0f420 100644 --- a/core/network/naomi_network.h +++ b/core/network/naomi_network.h @@ -22,6 +22,7 @@ #include "miniupnp.h" #include "cfg/option.h" #include "emulator.h" +#include "oslib/oslib.h" #include #include @@ -44,6 +45,7 @@ public: networkStopping = false; _startNow = false; return std::async(std::launch::async, [this] { + ThreadName _("NaomiNetwork-start"); bool res = startNetwork(); emu.setNetworkState(res); return res; diff --git a/core/network/picoppp.cpp b/core/network/picoppp.cpp index 5519157d0..83b15a641 100644 --- a/core/network/picoppp.cpp +++ b/core/network/picoppp.cpp @@ -26,9 +26,6 @@ #include "stdclass.h" //#define BBA_PCAPNG_DUMP -#ifdef BBA_PCAPNG_DUMP -#include "oslib/oslib.h" -#endif #ifdef __MINGW32__ #define _POSIX_SOURCE @@ -51,6 +48,7 @@ extern "C" { #include "miniupnp.h" #include "cfg/option.h" #include "emulator.h" +#include "oslib/oslib.h" #include #include @@ -765,38 +763,34 @@ static void check_dns_entries() if (public_ip.addr == 0) { - if (!dns_query_start) + u32 ip; + pico_string_to_ipv4(RESOLVER1_OPENDNS_COM, &ip); + pico_ip4 tmpdns { ip }; + if (dns_query_start == 0) { dns_query_start = PICO_TIME_MS(); - struct pico_ip4 tmpdns; - pico_string_to_ipv4(RESOLVER1_OPENDNS_COM, &tmpdns.addr); get_host_by_name("myip.opendns.com", tmpdns); } + else if (get_dns_answer(&public_ip, tmpdns) == 0) + { + dns_query_attempts = 0; + dns_query_start = 0; + char myip[16]; + pico_ipv4_to_string(myip, public_ip.addr); + INFO_LOG(MODEM, "My IP is %s", myip); + } else { - struct pico_ip4 tmpdns; - pico_string_to_ipv4(RESOLVER1_OPENDNS_COM, &tmpdns.addr); - if (get_dns_answer(&public_ip, tmpdns) == 0) + if (PICO_TIME_MS() - dns_query_start > 1000) { - dns_query_attempts = 0; - dns_query_start = 0; - char myip[16]; - pico_ipv4_to_string(myip, public_ip.addr); - INFO_LOG(MODEM, "My IP is %s", myip); - } - else - { - if (PICO_TIME_MS() - dns_query_start > 1000) + if (++dns_query_attempts >= 5) { - if (++dns_query_attempts >= 5) - { - public_ip.addr = 0xffffffff; // Bogus but not null - dns_query_attempts = 0; - } - else - // Retry - dns_query_start = 0; + public_ip.addr = 0xffffffff; // Bogus but not null + dns_query_attempts = 0; } + else + // Retry + dns_query_start = 0; } } } @@ -900,7 +894,7 @@ static void dumpFrame(const u8 *frame, u32 size) fwrite(&roundedSize, sizeof(roundedSize), 1, pcapngDump); u32 ifId = 0; fwrite(&ifId, sizeof(ifId), 1, pcapngDump); - u64 now = (u64)(os_GetSeconds() * 1000000.0); + u64 now = getTimeMs() * 1000; fwrite((u32 *)&now + 1, 4, 1, pcapngDump); fwrite(&now, 4, 1, pcapngDump); fwrite(&size, sizeof(size), 1, pcapngDump); @@ -969,6 +963,7 @@ static void *pico_thread_func(void *) std::future upnp = std::async(std::launch::async, [ports]() { // Initialize miniupnpc and map network ports + ThreadName _("UPNP-init"); MiniUPnP upnp; if (ports != nullptr && config::EnableUPnP) { @@ -1163,7 +1158,7 @@ static void *pico_thread_func(void *) return NULL; } -static cThread pico_thread(pico_thread_func, NULL); +static cThread pico_thread(pico_thread_func, nullptr, "PicoTCP"); bool start_pico() { diff --git a/core/nullDC.cpp b/core/nullDC.cpp index 15d2f3e8b..5129cd830 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -5,15 +5,40 @@ #include "cfg/cfg.h" #include "cfg/option.h" #include "log/LogManager.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "oslib/oslib.h" +#include "oslib/directory.h" #include "debug/gdb_server.h" #include "archive/rzip.h" -#include "rend/mainui.h" +#include "ui/mainui.h" #include "input/gamepad_device.h" #include "lua/lua.h" #include "stdclass.h" #include "serialize.h" +#include + +struct SavestateHeader +{ + void init() + { + memcpy(magic, MAGIC, sizeof(magic)); + creationDate = time(nullptr); + version = Deserializer::Current; + pngSize = 0; + } + + bool isValid() const { + return !memcmp(magic, MAGIC, sizeof(magic)); + } + + char magic[8]; + u64 creationDate; + u32 version; + u32 pngSize; + // png data + + static constexpr const char *MAGIC = "FLYSAVE1"; +}; int flycast_init(int argc, char* argv[]) { @@ -65,7 +90,7 @@ int flycast_init(int argc, char* argv[]) void dc_exit() { try { - emu.stop(); + emu.unloadGame(); } catch (...) { } mainui_stop(); } @@ -86,11 +111,12 @@ void flycast_term() gui_cancel_load(); lua::term(); emu.term(); + os_DestroyWindow(); gui_term(); os_TermInput(); } -void dc_savestate(int index) +void dc_savestate(int index, const u8 *pngData, u32 pngSize) { if (settings.network.online) return; @@ -102,7 +128,7 @@ void dc_savestate(int index) if (data == nullptr) { WARN_LOG(SAVESTATE, "Failed to save state - could not malloc %d bytes", (int)ser.size()); - gui_display_notification("Save state failed - memory full", 2000); + os_notify("Save state failed - memory full", 5000); return; } @@ -110,87 +136,105 @@ void dc_savestate(int index) dc_serialize(ser); std::string filename = hostfs::getSavestatePath(index, true); -#if 0 FILE *f = nowide::fopen(filename.c_str(), "wb"); - - if ( f == NULL ) + if (f == nullptr) { WARN_LOG(SAVESTATE, "Failed to save state - could not open %s for writing", filename.c_str()); - gui_display_notification("Cannot open save file", 2000); + os_notify("Cannot open save file", 5000); free(data); return; } + RZipFile zipFile; + SavestateHeader header; + header.init(); + header.pngSize = pngSize; + if (std::fwrite(&header, sizeof(header), 1, f) != 1) + goto fail; + if (pngSize > 0 && std::fwrite(pngData, 1, pngSize, f) != pngSize) + goto fail; + +#if 0 + // Uncompressed savestate std::fwrite(data, 1, ser.size(), f); std::fclose(f); #else - RZipFile zipFile; - if (!zipFile.Open(filename, true)) - { - WARN_LOG(SAVESTATE, "Failed to save state - could not open %s for writing", filename.c_str()); - gui_display_notification("Cannot open save file", 2000); - free(data); - return; - } + if (!zipFile.Open(f, true)) + goto fail; if (zipFile.Write(data, ser.size()) != ser.size()) - { - WARN_LOG(SAVESTATE, "Failed to save state - error writing %s", filename.c_str()); - gui_display_notification("Error saving state", 2000); - zipFile.Close(); - free(data); - return; - } + goto fail; zipFile.Close(); #endif free(data); NOTICE_LOG(SAVESTATE, "Saved state to %s size %d", filename.c_str(), (int)ser.size()); - gui_display_notification("State saved", 1000); + os_notify("State saved", 2000); + return; + +fail: + WARN_LOG(SAVESTATE, "Failed to save state - error writing %s", filename.c_str()); + os_notify("Error saving state", 5000); + if (zipFile.rawFile() != nullptr) + zipFile.Close(); + else + std::fclose(f); + free(data); + // delete failed savestate? } void dc_loadstate(int index) { + if (settings.raHardcoreMode) + return; u32 total_size = 0; - FILE *f = nullptr; std::string filename = hostfs::getSavestatePath(index, false); - RZipFile zipFile; - if (zipFile.Open(filename, false)) + FILE *f = nowide::fopen(filename.c_str(), "rb"); + if (f == nullptr) { + WARN_LOG(SAVESTATE, "Failed to load state - could not open %s for reading", filename.c_str()); + os_notify("Save state not found", 2000); + return; + } + SavestateHeader header; + if (std::fread(&header, sizeof(header), 1, f) == 1) + { + if (!header.isValid()) + // seek to beginning of file if this isn't a valid header (legacy savestate) + std::fseek(f, 0, SEEK_SET); + else + // skip png data + std::fseek(f, header.pngSize, SEEK_CUR); + } + else { + // probably not a valid savestate but we'll fail later + std::fseek(f, 0, SEEK_SET); + } + + if (index == -1 && config::GGPOEnable) + { + long pos = std::ftell(f); + MD5Sum().add(f) + .getDigest(settings.network.md5.savestate); + std::fseek(f, pos, SEEK_SET); + } + RZipFile zipFile; + if (zipFile.Open(f, false)) { total_size = (u32)zipFile.Size(); - if (index == -1 && config::GGPOEnable) - { - f = zipFile.rawFile(); - long pos = std::ftell(f); - MD5Sum().add(f) - .getDigest(settings.network.md5.savestate); - std::fseek(f, pos, SEEK_SET); - f = nullptr; - } } else { - f = nowide::fopen(filename.c_str(), "rb"); - - if ( f == NULL ) - { - WARN_LOG(SAVESTATE, "Failed to load state - could not open %s for reading", filename.c_str()); - gui_display_notification("Save state not found", 2000); - return; - } - if (index == -1 && config::GGPOEnable) - MD5Sum().add(f) - .getDigest(settings.network.md5.savestate); + long pos = std::ftell(f); std::fseek(f, 0, SEEK_END); - total_size = (u32)std::ftell(f); - std::fseek(f, 0, SEEK_SET); + total_size = (u32)std::ftell(f) - pos; + std::fseek(f, pos, SEEK_SET); } void *data = malloc(total_size); if (data == nullptr) { WARN_LOG(SAVESTATE, "Failed to load state - could not malloc %d bytes", total_size); - gui_display_notification("Failed to load state - memory full", 2000); - if (f != nullptr) + os_notify("Failed to load state - memory full", 5000); + if (zipFile.rawFile() == nullptr) std::fclose(f); else zipFile.Close(); @@ -198,20 +242,20 @@ void dc_loadstate(int index) } size_t read_size; - if (f == nullptr) + if (zipFile.rawFile() != nullptr) { read_size = zipFile.Read(data, total_size); zipFile.Close(); } else { - read_size = fread(data, 1, total_size, f); + read_size = std::fread(data, 1, total_size, f); std::fclose(f); } if (read_size != total_size) { WARN_LOG(SAVESTATE, "Failed to load state - I/O error"); - gui_display_notification("Failed to load state - I/O error", 2000); + os_notify("Failed to load state - I/O error", 5000); free(data); return; } @@ -221,6 +265,7 @@ void dc_loadstate(int index) dc_loadstate(deser); NOTICE_LOG(SAVESTATE, "Loaded state ver %d from %s size %d", deser.version(), filename.c_str(), total_size); if (deser.size() != total_size) + // Note: this isn't true for RA savestates WARN_LOG(SAVESTATE, "Savestate size %d but only %d bytes used", total_size, (int)deser.size()); } catch (const Deserializer::Exception& e) { ERROR_LOG(SAVESTATE, "%s", e.what()); @@ -230,4 +275,41 @@ void dc_loadstate(int index) EventManager::event(Event::LoadState); } +time_t dc_getStateCreationDate(int index) +{ + std::string filename = hostfs::getSavestatePath(index, false); + FILE *f = nowide::fopen(filename.c_str(), "rb"); + if (f == nullptr) + return 0; + SavestateHeader header; + if (std::fread(&header, sizeof(header), 1, f) != 1 || !header.isValid()) + { + std::fclose(f); + struct stat st; + if (flycast::stat(filename.c_str(), &st) == 0) + return st.st_mtime; + else + return 0; + } + std::fclose(f); + return (time_t)header.creationDate; +} + +void dc_getStateScreenshot(int index, std::vector& pngData) +{ + pngData.clear(); + std::string filename = hostfs::getSavestatePath(index, false); + FILE *f = nowide::fopen(filename.c_str(), "rb"); + if (f == nullptr) + return; + SavestateHeader header; + if (std::fread(&header, sizeof(header), 1, f) == 1 && header.isValid() && header.pngSize != 0) + { + pngData.resize(header.pngSize); + if (std::fread(pngData.data(), 1, pngData.size(), f) != pngData.size()) + pngData.clear(); + } + std::fclose(f); +} + #endif diff --git a/core/rend/boxart/http_client.cpp b/core/oslib/http_client.cpp similarity index 75% rename from core/rend/boxart/http_client.cpp rename to core/oslib/http_client.cpp index d68db142f..3742159b1 100644 --- a/core/rend/boxart/http_client.cpp +++ b/core/oslib/http_client.cpp @@ -68,6 +68,86 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return 200; } +static int post(const std::string& url, const char *headers, const u8 *payload, u32 payloadSize, std::vector& reply) +{ + char scheme[16], host[256], path[256]; + URL_COMPONENTS components{}; + components.dwStructSize = sizeof(components); + components.lpszScheme = scheme; + components.dwSchemeLength = sizeof(scheme) / sizeof(scheme[0]); + components.lpszHostName = host; + components.dwHostNameLength = sizeof(host) / sizeof(host[0]); + components.lpszUrlPath = path; + components.dwUrlPathLength = sizeof(path) / sizeof(path[0]); + + if (!InternetCrackUrlA(url.c_str(), url.length(), 0, &components)) + return 500; + + bool https = !strcmp(scheme, "https"); + + int rc = 500; + HINTERNET ic = InternetConnect(hInet, host, components.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); + if (ic == NULL) + return rc; + + HINTERNET hreq = HttpOpenRequest(ic, "POST", path, NULL, NULL, NULL, https ? INTERNET_FLAG_SECURE : 0, 0); + if (hreq == NULL) { + InternetCloseHandle(ic); + return rc; + } + if (payloadSize > 0) + { + char clen[128]; + snprintf(clen, sizeof(clen), "Content-Length: %d\r\n", payloadSize); + HttpAddRequestHeaders(hreq, clen, -1L, HTTP_ADDREQ_FLAG_ADD_IF_NEW); + } + if (!HttpSendRequest(hreq, headers, -1, (void *)payload, payloadSize)) + WARN_LOG(NETWORK, "HttpSendRequest Error %d", GetLastError()); + else + { + DWORD status; + DWORD size = sizeof(status); + DWORD index = 0; + if (!HttpQueryInfo(hreq, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status, &size, &index)) + WARN_LOG(NETWORK, "HttpQueryInfo Error %d", GetLastError()); + else + { + rc = status; + reply.clear(); + u8 buffer[4096]; + DWORD bytesRead = sizeof(buffer); + while (true) + { + if (!InternetReadFile(hreq, buffer, sizeof(buffer), &bytesRead)) + { + WARN_LOG(NETWORK, "InternetReadFile failed: %lx", GetLastError()); + InternetCloseHandle(hreq); + rc = 500; + break; + } + if (bytesRead == 0) + break; + reply.insert(reply.end(), buffer, buffer + bytesRead); + } + } + } + + InternetCloseHandle(hreq); + InternetCloseHandle(ic); + + return rc; +} + +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) +{ + char buf[512]; + if (contentType != nullptr) { + sprintf(buf, "Content-Type: %s", contentType); + contentType = buf; + } + return post(url, contentType, (const u8 *)payload, strlen(payload), reply); +} + int post(const std::string& url, const std::vector& fields) { static const std::string boundary("----flycast-boundary-8304529454"); @@ -122,49 +202,9 @@ int post(const std::string& url, const std::vector& fields) } content += "--" + boundary + "--\r\n"; - char scheme[16], host[256], path[256]; - URL_COMPONENTS components{}; - components.dwStructSize = sizeof(components); - components.lpszScheme = scheme; - components.dwSchemeLength = sizeof(scheme) / sizeof(scheme[0]); - components.lpszHostName = host; - components.dwHostNameLength = sizeof(host) / sizeof(host[0]); - components.lpszUrlPath = path; - components.dwUrlPathLength = sizeof(path) / sizeof(path[0]); - - if (!InternetCrackUrlA(url.c_str(), url.length(), 0, &components)) - return 500; - - bool https = !strcmp(scheme, "https"); - - int rc = 500; - HINTERNET ic = InternetConnect(hInet, host, components.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); - if (ic == NULL) - return rc; - - HINTERNET hreq = HttpOpenRequest(ic, "POST", path, NULL, NULL, NULL, https ? INTERNET_FLAG_SECURE : 0, 0); - if (hreq == NULL) { - InternetCloseHandle(ic); - return rc; - } + std::vector reply; std::string header("Content-Type: multipart/form-data; boundary=" + boundary); - if (!HttpSendRequest(hreq, header.c_str(), -1, &content[0], content.length())) - WARN_LOG(NETWORK, "HttpSendRequest Error %d", GetLastError()); - else - { - DWORD status; - DWORD size = sizeof(status); - DWORD index = 0; - if (!HttpQueryInfo(hreq, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status, &size, &index)) - WARN_LOG(NETWORK, "HttpQueryInfo Error %d", GetLastError()); - else - rc = status; - } - - InternetCloseHandle(hreq); - InternetCloseHandle(ic); - - return rc; + return post(url, header.c_str(), (const u8 *)&content[0], content.length(), reply); } void term() @@ -195,7 +235,7 @@ static size_t receiveData(void *buffer, size_t size, size_t nmemb, std::vector& content, std::string& contentType) +static CURL *makeCurlEasy(const std::string& url) { CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_USERAGENT, "Flycast/1.0"); @@ -206,6 +246,13 @@ int get(const std::string& url, std::vector& content, std::string& contentTy curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + return curl; +} + +int get(const std::string& url, std::vector& content, std::string& contentType) +{ + CURL *curl = makeCurlEasy(url); + std::vector recvBuffer; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &recvBuffer); @@ -227,17 +274,41 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return (int)httpCode; } -int post(const std::string& url, const std::vector& fields) +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { - CURL *curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_USERAGENT, "Flycast/1.0"); - curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + CURL *curl = makeCurlEasy(url); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); - curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); + curl_slist *headers = nullptr; + if (contentType != nullptr) + { + headers = curl_slist_append(headers, ("Content-Type: " + std::string(contentType)).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + std::vector recvBuffer; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &recvBuffer); + CURLcode res = curl_easy_perform(curl); + + long httpCode = 500; + if (res == CURLE_OK) + { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + reply = recvBuffer; + } + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + return (int)httpCode; +} + +int post(const std::string& url, const std::vector& fields) +{ + CURL *curl = makeCurlEasy(url); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_mime *mime = curl_mime_init(curl); for (const auto& field : fields) @@ -263,7 +334,6 @@ int post(const std::string& url, const std::vector& fields) curl_easy_cleanup(curl); return (int)httpCode; - } void term() diff --git a/core/rend/boxart/http_client.h b/core/oslib/http_client.h similarity index 95% rename from core/rend/boxart/http_client.h rename to core/oslib/http_client.h index 846abc126..c71fb668f 100644 --- a/core/rend/boxart/http_client.h +++ b/core/oslib/http_client.h @@ -50,6 +50,7 @@ struct PostField }; int post(const std::string& url, const std::vector& fields); +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply); static inline bool success(int status) { return status >= 200 && status < 300; diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index 3a078bae7..976620a30 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -25,6 +25,21 @@ #ifndef _WIN32 #include #endif +#if defined(USE_SDL) +#include "sdl/sdl.h" +#else + #if defined(SUPPORT_X11) + #include "linux-dist/x11.h" + #endif + #if defined(USE_EVDEV) + #include "linux-dist/evdev.h" + #endif +#endif +#if defined(_WIN32) && !defined(TARGET_UWP) +#include "windows/rawinput.h" +#include +#endif +#include "profiler/fc_profiler.h" namespace hostfs { @@ -137,11 +152,190 @@ std::string getTextureDumpPath() return get_writable_data_path("texdump/"); } +#if defined(__unix__) && !defined(__ANDROID__) + +static std::string runCommand(const std::string& cmd) +{ + char buf[1024] {}; + FILE *fp = popen(cmd.c_str(), "r"); + if (fp == nullptr) { + INFO_LOG(COMMON, "popen failed: %d", errno); + return ""; + } + std::string result; + while (fgets(buf, sizeof(buf), fp) != nullptr) + result += trim_trailing_ws(buf, "\n"); + + int rc; + if ((rc = pclose(fp)) != 0) { + INFO_LOG(COMMON, "Command error: %d", rc); + return ""; + } + + return result; +} + +static std::string getScreenshotsPath() +{ + std::string picturesPath = runCommand("xdg-user-dir PICTURES"); + if (!picturesPath.empty()) + return picturesPath; + const char *home = nowide::getenv("HOME"); + if (home != nullptr) + return home; + else + return "."; +} + +#elif defined(TARGET_UWP) +//TODO move to shell/uwp? +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::Storage; + +void saveScreenshot(const std::string& name, const std::vector& data) +{ + try { + StorageFolder^ folder = KnownFolders::PicturesLibrary; // or SavedPictures? + if (folder == nullptr) { + INFO_LOG(COMMON, "KnownFolders::PicturesLibrary is null"); + throw FlycastException("Can't find Pictures library"); + } + nowide::wstackstring wstr; + wchar_t *wname = wstr.convert(name.c_str()); + String^ msname = ref new String(wname); + ArrayReference arrayRef(const_cast(&data[0]), data.size()); + + IAsyncOperation^ op = folder->CreateFileAsync(msname, CreationCollisionOption::FailIfExists); + cResetEvent asyncEvent; + op->Completed = ref new AsyncOperationCompletedHandler( + [&asyncEvent, &arrayRef](IAsyncOperation^ op, AsyncStatus) { + IAsyncAction^ action = FileIO::WriteBytesAsync(op->GetResults(), arrayRef); + action->Completed = ref new AsyncActionCompletedHandler( + [&asyncEvent](IAsyncAction^, AsyncStatus){ + asyncEvent.Set(); + }); + }); + asyncEvent.Wait(); + } + catch (COMException^ e) { + WARN_LOG(COMMON, "Save screenshot failed: %S", e->Message->Data()); + throw FlycastException(""); + } +} + +#elif defined(_WIN32) && !defined(TARGET_UWP) + +static std::string getScreenshotsPath() +{ + wchar_t *screenshotPath; + if (FAILED(SHGetKnownFolderPath(FOLDERID_Screenshots, KF_FLAG_DEFAULT, NULL, &screenshotPath))) + return get_writable_config_path(""); + nowide::stackstring path; + std::string ret; + if (path.convert(screenshotPath) == nullptr) + ret = get_writable_config_path(""); + else + ret = path.get(); + CoTaskMemFree(screenshotPath); + + return ret; +} + +#else + +std::string getScreenshotsPath(); + +#endif + +#if !defined(__ANDROID__) && !defined(TARGET_UWP) && !defined(TARGET_IPHONE) && !defined(__SWITCH__) + +void saveScreenshot(const std::string& name, const std::vector& data) +{ + std::string path = getScreenshotsPath(); + path += "/" + name; + FILE *f = nowide::fopen(path.c_str(), "wb"); + if (f == nullptr) + throw FlycastException(path); + if (std::fwrite(&data[0], data.size(), 1, f) != 1) { + std::fclose(f); + unlink(path.c_str()); + throw FlycastException(path); + } + std::fclose(f); +} + +#endif + +} // namespace hostfs + +void os_CreateWindow() +{ +#if defined(USE_SDL) + sdl_window_create(); +#elif defined(SUPPORT_X11) + x11_window_create(); +#endif +} + +void os_DestroyWindow() +{ +#if defined(USE_SDL) + sdl_window_destroy(); +#elif defined(SUPPORT_X11) + x11_window_destroy(); +#endif +} + +void os_SetupInput() +{ +#if defined(USE_SDL) + input_sdl_init(); +#else + #if defined(SUPPORT_X11) + input_x11_init(); + #endif + #if defined(USE_EVDEV) + input_evdev_init(); + #endif +#endif +#if defined(_WIN32) && !defined(TARGET_UWP) + if (config::UseRawInput) + rawinput::init(); +#endif +} + +void os_TermInput() +{ +#if defined(USE_SDL) + input_sdl_quit(); +#else + #if defined(USE_EVDEV) + input_evdev_close(); + #endif +#endif +#if defined(_WIN32) && !defined(TARGET_UWP) + if (config::UseRawInput) + rawinput::term(); +#endif +} + +void os_UpdateInputState() +{ + FC_PROFILE_SCOPE; + +#if defined(USE_SDL) + input_sdl_handle(); +#else + #if defined(USE_EVDEV) + input_evdev_handle(); + #endif +#endif } #ifdef USE_BREAKPAD -#include "rend/boxart/http_client.h" +#include "http_client.h" #include "version.h" #include "log/InMemoryListener.h" #include "wsi/context.h" diff --git a/core/oslib/oslib.h b/core/oslib/oslib.h index e6ad9c2e3..985f6cb77 100644 --- a/core/oslib/oslib.h +++ b/core/oslib/oslib.h @@ -1,19 +1,34 @@ #pragma once #include "types.h" +#include #if defined(__SWITCH__) #include #endif -void os_SetWindowText(const char* text); -double os_GetSeconds(); - void os_DoEvents(); void os_CreateWindow(); +void os_DestroyWindow(); void os_SetupInput(); void os_TermInput(); +void os_UpdateInputState(); void os_InstallFaultHandler(); void os_UninstallFaultHandler(); void os_RunInstance(int argc, const char *argv[]); +void os_SetThreadName(const char *name); +void os_notify(const char *msg, int durationMs = 2000, const char *details = nullptr); + +// raii thread name setter +class ThreadName +{ +public: + ThreadName(const char *name) { + os_SetThreadName(name); + } + ~ThreadName() { + // default name + os_SetThreadName("flycast"); + } +}; #ifdef _MSC_VER #include @@ -46,6 +61,7 @@ namespace hostfs std::string getTextureDumpPath(); std::string getShaderCachePath(const std::string& filename); + void saveScreenshot(const std::string& name, const std::vector& data); } static inline void *allocAligned(size_t alignment, size_t size) diff --git a/core/oslib/storage.cpp b/core/oslib/storage.cpp index e3c7055c5..69e0ec259 100644 --- a/core/oslib/storage.cpp +++ b/core/oslib/storage.cpp @@ -184,7 +184,8 @@ public: #ifndef _WIN32 struct stat st; if (flycast::stat(path.c_str(), &st) != 0) { - INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno); + if (errno != ENOENT) + INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno); throw StorageException("Cannot stat " + path); } info.isDirectory = S_ISDIR(st.st_mode); diff --git a/core/rend/CustomTexture.cpp b/core/rend/CustomTexture.cpp index 6edcfb0ed..fd082cdd0 100644 --- a/core/rend/CustomTexture.cpp +++ b/core/rend/CustomTexture.cpp @@ -284,7 +284,7 @@ void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, voi FILE *f = nowide::fopen((const char *)context, "wb"); if (f == nullptr) { - WARN_LOG(RENDERER, "Dump texture: can't save to file %s: error %d", context, errno); + WARN_LOG(RENDERER, "Dump texture: can't save to file %s: error %d", (const char *)context, errno); } else { diff --git a/core/rend/CustomTexture.h b/core/rend/CustomTexture.h index 9eb60e1c6..29223ee8e 100644 --- a/core/rend/CustomTexture.h +++ b/core/rend/CustomTexture.h @@ -28,7 +28,7 @@ class CustomTexture { public: - CustomTexture() : loader_thread(loader_thread_func, this) {} + CustomTexture() : loader_thread(loader_thread_func, this, "CustomTexLoader") {} ~CustomTexture() { Terminate(); } u8* LoadCustomTexture(u32 hash, int& width, int& height); void LoadCustomTextureAsync(BaseTextureCacheData *texture_data); diff --git a/core/rend/dx11/dx11_driver.h b/core/rend/dx11/dx11_driver.h index 20bf86999..446de5926 100644 --- a/core/rend/dx11/dx11_driver.h +++ b/core/rend/dx11/dx11_driver.h @@ -17,10 +17,10 @@ along with Flycast. If not, see . */ #pragma once -#include "rend/imgui_driver.h" +#include "ui/imgui_driver.h" #include "imgui_impl_dx11.h" #include "dx11context.h" -#include "rend/gui.h" +#include "ui/gui.h" #include class DX11Driver final : public ImGuiDriver @@ -57,12 +57,12 @@ public: ImTextureID getTexture(const std::string& name) override { auto it = textures.find(name); if (it != textures.end()) - return (ImTextureID)it->second.textureView.get(); + return (ImTextureID)&it->second.imTexture; else return ImTextureID{}; } - ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) override + ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) override { Texture& texture = textures[name]; texture.texture.reset(); @@ -86,8 +86,14 @@ public: theDX11Context.getDevice()->CreateShaderResourceView(texture.texture, &viewDesc, &texture.textureView.get()); theDX11Context.getDeviceContext()->UpdateSubresource(texture.texture, 0, nullptr, data, width * 4, width * 4 * height); + texture.imTexture.shaderResourceView = texture.textureView.get(); + texture.imTexture.pointSampling = nearestSampling; - return (ImTextureID)texture.textureView.get(); + return (ImTextureID)&texture.imTexture; + } + + void deleteTexture(const std::string& name) override { + textures.erase(name); } private: @@ -95,6 +101,7 @@ private: { ComPtr texture; ComPtr textureView; + ImTextureDX11 imTexture; }; bool frameRendered = false; diff --git a/core/rend/dx11/dx11_overlay.cpp b/core/rend/dx11/dx11_overlay.cpp index dd62d5105..ff507c56e 100644 --- a/core/rend/dx11/dx11_overlay.cpp +++ b/core/rend/dx11/dx11_overlay.cpp @@ -56,7 +56,7 @@ void DX11Overlay::draw(u32 width, u32 height, bool vmu, bool crosshair) vmuTextures[i].reset(); continue; } - if (vmuTextures[i] == nullptr || vmu_lcd_changed[i]) + if (vmuTextures[i] == nullptr || this->vmuLastChanged[i] != ::vmuLastChanged[i]) { vmuTextureViews[i].reset(); vmuTextures[i].reset(); @@ -82,8 +82,8 @@ void DX11Overlay::draw(u32 width, u32 height, bool vmu, bool crosshair) for (int y = 0; y < 32; y++) memcpy(&data[y * 48], &vmu_lcd_data[i][(31 - y) * 48], sizeof(u32) * 48); deviceContext->UpdateSubresource(vmuTextures[i], 0, nullptr, data, 48 * 4, 48 * 4 * 32); + this->vmuLastChanged[i] = ::vmuLastChanged[i]; } - vmu_lcd_changed[i] = false; } float x, y; float w = vmu_width; diff --git a/core/rend/dx11/dx11_overlay.h b/core/rend/dx11/dx11_overlay.h index fbdf54c20..73ff81bcc 100644 --- a/core/rend/dx11/dx11_overlay.h +++ b/core/rend/dx11/dx11_overlay.h @@ -35,6 +35,7 @@ public: this->deviceContext = deviceContext; this->samplers = samplers; quad.init(device, deviceContext, shaders); + vmuLastChanged.fill({}); } void term() @@ -60,6 +61,7 @@ private: ComPtr xhairTextureView; std::array, 8> vmuTextures; std::array, 8> vmuTextureViews; + std::array vmuLastChanged {}; Quad quad; Samplers *samplers; BlendStates blendStates; diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index 4a2d79765..623529782 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -20,7 +20,7 @@ #include "dx11context.h" #include "hw/pvr/ta.h" #include "hw/pvr/pvr_mem.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "rend/tileclip.h" #include "rend/sorter.h" @@ -1345,6 +1345,99 @@ void DX11Renderer::writeFramebufferToVRAM() WriteFramebuffer<2, 1, 0, 3>(width, height, (u8 *)tmp_buf.data(), texAddress, pvrrc.fb_W_CTRL, linestride, xClip, yClip); } +bool DX11Renderer::GetLastFrame(std::vector& data, int& width, int& height) +{ + if (!frameRenderedOnce) + return false; + + if (width != 0) { + height = width / aspectRatio; + } + else if (height != 0) { + width = aspectRatio * height; + } + else + { + width = this->width; + height = this->height; + if (config::Rotate90) + std::swap(width, height); + // We need square pixels for PNG + int w = aspectRatio * height; + if (width > w) + height = width / aspectRatio; + else + width = w; + } + + ComPtr dstTex; + ComPtr dstRenderTarget; + createTexAndRenderTarget(dstTex, dstRenderTarget, width, height); + + ID3D11ShaderResourceView *nullResView = nullptr; + deviceContext->PSSetShaderResources(0, 1, &nullResView); + deviceContext->OMSetRenderTargets(1, &dstRenderTarget.get(), nullptr); + D3D11_VIEWPORT vp{}; + vp.Width = (FLOAT)width; + vp.Height = (FLOAT)height; + vp.MinDepth = 0.f; + vp.MaxDepth = 1.f; + deviceContext->RSSetViewports(1, &vp); + const D3D11_RECT r = { 0, 0, (LONG)width, (LONG)height }; + deviceContext->RSSetScissorRects(1, &r); + deviceContext->OMSetBlendState(blendStates.getState(false), nullptr, 0xffffffff); + deviceContext->GSSetShader(nullptr, nullptr, 0); + deviceContext->HSSetShader(nullptr, nullptr, 0); + deviceContext->DSSetShader(nullptr, nullptr, 0); + deviceContext->CSSetShader(nullptr, nullptr, 0); + + quad->draw(fbTextureView, samplers->getSampler(true), nullptr, -1.f, -1.f, 2.f, 2.f, config::Rotate90); + +#ifndef LIBRETRO + deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); +#else + ID3D11RenderTargetView *nullView = nullptr; + deviceContext->OMSetRenderTargets(1, &nullView, nullptr); +#endif + D3D11_TEXTURE2D_DESC desc; + dstTex->GetDesc(&desc); + desc.Usage = D3D11_USAGE_STAGING; + desc.BindFlags = 0; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + ComPtr stagingTex; + HRESULT hr = device->CreateTexture2D(&desc, nullptr, &stagingTex.get()); + if (FAILED(hr)) + { + WARN_LOG(RENDERER, "Staging screenshot texture creation failed"); + return false; + } + deviceContext->CopyResource(stagingTex, dstTex); + + D3D11_MAPPED_SUBRESOURCE mappedSubres; + hr = deviceContext->Map(stagingTex, 0, D3D11_MAP_READ, 0, &mappedSubres); + if (FAILED(hr)) + { + WARN_LOG(RENDERER, "Failed to map staging screenshot texture"); + return false; + } + const u8* const src = (const u8 *)mappedSubres.pData; + for (int y = 0; y < height; y++) + { + const u8 *p = src + y * mappedSubres.RowPitch; + for (int x = 0; x < width; x++, p += 4) + { + data.push_back(p[2]); + data.push_back(p[1]); + data.push_back(p[0]); + } + } + deviceContext->Unmap(stagingTex, 0); + + return true; +} + void DX11Renderer::renderVideoRouting() { #ifdef VIDEO_ROUTING diff --git a/core/rend/dx11/dx11_renderer.h b/core/rend/dx11/dx11_renderer.h index e5f067a77..6304cb652 100644 --- a/core/rend/dx11/dx11_renderer.h +++ b/core/rend/dx11/dx11_renderer.h @@ -53,6 +53,7 @@ struct DX11Renderer : public Renderer bool RenderLastFrame() override; void DrawOSD(bool clear_screen) override; BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; + bool GetLastFrame(std::vector& data, int& width, int& height) override; protected: struct VertexConstants diff --git a/core/rend/dx11/dx11context.cpp b/core/rend/dx11/dx11context.cpp index 26f71739e..0e0d5cf4d 100644 --- a/core/rend/dx11/dx11context.cpp +++ b/core/rend/dx11/dx11context.cpp @@ -64,6 +64,7 @@ bool DX11Context::init(bool keepCurrentWindow) ComPtr dxgiFactory6; ComPtr dxgiAdapter; HRESULT hr; + allowTearing = false; hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void **)&dxgiFactory.get()); if (SUCCEEDED(hr)) { @@ -71,6 +72,10 @@ bool DX11Context::init(bool keepCurrentWindow) if (dxgiFactory6) { dxgiFactory6->EnumAdapterByGpuPreference(0, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, __uuidof(IDXGIAdapter), (void **)&dxgiAdapter.get()); + UINT tearing; + if (SUCCEEDED(dxgiFactory6->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &tearing, + sizeof(tearing))) && tearing != 0) + allowTearing = true; dxgiFactory6.reset(); } } @@ -127,6 +132,8 @@ bool DX11Context::init(bool keepCurrentWindow) desc.BufferCount = 2; desc.SampleDesc.Count = 1; desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + if (allowTearing) + desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; #ifdef TARGET_UWP desc.Width = settings.display.width; @@ -157,6 +164,8 @@ bool DX11Context::init(bool keepCurrentWindow) desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; desc.SampleDesc.Count = 1; desc.SampleDesc.Quality = 0; + if (allowTearing) + desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; hr = dxgiFactory->CreateSwapChain(pDevice, &desc, &swapchain.get()); } @@ -221,26 +230,23 @@ void DX11Context::Present() frameRendered = false; bool swapOnVSync = !settings.input.fastForwardMode && config::VSync; HRESULT hr; - if (!swapchain) - { + if (!swapchain) { hr = DXGI_ERROR_DEVICE_RESET; } - else if (swapOnVSync) - { + else if (swapOnVSync) { int swapInterval = std::min(4, std::max(1, (int)(settings.display.refreshRate / 60))); hr = swapchain->Present(swapInterval, 0); } - else - { - hr = swapchain->Present(0, DXGI_PRESENT_DO_NOT_WAIT); + else { + hr = swapchain->Present(0, allowTearing ? DXGI_PRESENT_ALLOW_TEARING : DXGI_PRESENT_DO_NOT_WAIT); } - if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) - { + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { WARN_LOG(RENDERER, "Present failed: device removed/reset"); handleDeviceLost(); } - else if (hr != DXGI_ERROR_WAS_STILL_DRAWING && FAILED(hr)) + else if (hr != DXGI_ERROR_WAS_STILL_DRAWING && FAILED(hr)) { WARN_LOG(RENDERER, "Present failed %x", hr); + } } void DX11Context::EndImGuiFrame() @@ -260,10 +266,6 @@ void DX11Context::EndImGuiFrame() if (crosshairsNeeded() || config::FloatVMUs) overlay.draw(settings.display.width, settings.display.height, config::FloatVMUs, true); } - else - { - overlay.draw(settings.display.width, settings.display.height, true, false); - } ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); } frameRendered = true; @@ -279,9 +281,9 @@ void DX11Context::resize() pDeviceContext->OMSetRenderTargets(1, &nullRTV, nullptr); renderTargetView.reset(); #ifdef TARGET_UWP - HRESULT hr = swapchain->ResizeBuffers(2, settings.display.width, settings.display.height, DXGI_FORMAT_R8G8B8A8_UNORM, 0); + HRESULT hr = swapchain->ResizeBuffers(2, settings.display.width, settings.display.height, DXGI_FORMAT_R8G8B8A8_UNORM, allowTearing ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0); #else - HRESULT hr = swapchain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH); + HRESULT hr = swapchain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH | (allowTearing ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0)); if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { handleDeviceLost(); diff --git a/core/rend/dx11/dx11context.h b/core/rend/dx11/dx11context.h index 9067d85cf..260264545 100644 --- a/core/rend/dx11/dx11context.h +++ b/core/rend/dx11/dx11context.h @@ -86,6 +86,7 @@ private: ComPtr renderTargetView; bool overlayOnly = false; DX11Overlay overlay; + bool allowTearing = false; bool swapOnVSync = false; bool frameRendered = false; std::string adapterDesc; diff --git a/core/rend/dx9/d3d_overlay.cpp b/core/rend/dx9/d3d_overlay.cpp index c26f1a975..b62c2ecb7 100644 --- a/core/rend/dx9/d3d_overlay.cpp +++ b/core/rend/dx9/d3d_overlay.cpp @@ -50,7 +50,7 @@ void D3DOverlay::draw(u32 width, u32 height, bool vmu, bool crosshair) texture.reset(); continue; } - if (texture == nullptr || vmu_lcd_changed[i]) + if (texture == nullptr || this->vmuLastChanged[i] != ::vmuLastChanged[i]) { texture.reset(); device->CreateTexture(48, 32, 1, D3DUSAGE_DYNAMIC, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &texture.get(), 0); @@ -61,8 +61,8 @@ void D3DOverlay::draw(u32 width, u32 height, bool vmu, bool crosshair) for (int y = 0; y < 32; y++) memcpy(dst + y * rect.Pitch, vmu_lcd_data[i] + (31 - y) * 48, 48 * 4); texture->UnlockRect(0); + this->vmuLastChanged[i] = ::vmuLastChanged[i]; } - vmu_lcd_changed[i] = false; } float x; if (i & 2) diff --git a/core/rend/dx9/d3d_overlay.h b/core/rend/dx9/d3d_overlay.h index 8288cdf76..3852de40e 100644 --- a/core/rend/dx9/d3d_overlay.h +++ b/core/rend/dx9/d3d_overlay.h @@ -28,6 +28,7 @@ class D3DOverlay public: void init(const ComPtr& device) { this->device = device; + vmuLastChanged.fill({}); } void term() { @@ -45,11 +46,12 @@ private: struct Vertex { - float pos[3]; - float uv[2]; + float pos[3]; + float uv[2]; }; ComPtr device; ComPtr xhairTexture; std::array, 8> vmuTextures; + std::array vmuLastChanged {}; }; diff --git a/core/rend/dx9/d3d_renderer.cpp b/core/rend/dx9/d3d_renderer.cpp index 8c9b758be..63085ff42 100644 --- a/core/rend/dx9/d3d_renderer.cpp +++ b/core/rend/dx9/d3d_renderer.cpp @@ -20,7 +20,7 @@ #include "hw/pvr/ta.h" #include "hw/pvr/pvr_mem.h" #include "rend/tileclip.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "rend/sorter.h" const u32 DstBlendGL[] @@ -1422,6 +1422,105 @@ void D3DRenderer::writeFramebufferToVRAM() WriteFramebuffer<2, 1, 0, 3>(width, height, (u8 *)tmp_buf.data(), texAddress, pvrrc.fb_W_CTRL, linestride, xClip, yClip); } +bool D3DRenderer::GetLastFrame(std::vector& data, int& width, int& height) +{ + if (!frameRenderedOnce || !theDXContext.isReady()) + return false; + + if (width != 0) { + height = width / aspectRatio; + } + else if (height != 0) { + width = aspectRatio * height; + } + else + { + width = this->width; + height = this->height; + if (config::Rotate90) + std::swap(width, height); + // We need square pixels for PNG + int w = aspectRatio * height; + if (width > w) + height = width / aspectRatio; + else + width = w; + } + + backbuffer.reset(); + device->GetRenderTarget(0, &backbuffer.get()); + + // Target texture and surface + ComPtr target; + device->CreateTexture(width, height, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &target.get(), NULL); + ComPtr surface; + target->GetSurfaceLevel(0, &surface.get()); + device->SetRenderTarget(0, surface); + // Draw + devCache.SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE); + device->SetPixelShader(NULL); + device->SetVertexShader(NULL); + device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + device->SetRenderState(D3DRS_ZENABLE, FALSE); + device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); + device->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE); + device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); + device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); + + glm::mat4 identity = glm::identity(); + glm::mat4 projection = glm::translate(glm::vec3(-1.f / width, 1.f / height, 0)); + if (config::Rotate90) + projection *= glm::rotate((float)M_PI_2, glm::vec3(0, 0, 1)); + + device->SetTransform(D3DTS_WORLD, (const D3DMATRIX *)&identity[0][0]); + device->SetTransform(D3DTS_VIEW, (const D3DMATRIX *)&identity[0][0]); + device->SetTransform(D3DTS_PROJECTION, (const D3DMATRIX *)&projection[0][0]); + + device->SetFVF(D3DFVF_XYZ | D3DFVF_TEX1); + D3DVIEWPORT9 viewport{}; + viewport.Width = width; + viewport.Height = height; + viewport.MaxZ = 1; + bool rc = SUCCEEDED(device->SetViewport(&viewport)); + verify(rc); + float coords[] { + -1, 1, 0.5f, 0, 0, + -1, -1, 0.5f, 0, 1, + 1, 1, 0.5f, 1, 0, + 1, -1, 0.5f, 1, 1, + }; + device->SetTexture(0, framebufferTexture); + device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, coords, sizeof(float) * 5); + + // Copy back + ComPtr offscreenSurface; + rc = SUCCEEDED(device->CreateOffscreenPlainSurface(width, height, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &offscreenSurface.get(), nullptr)); + verify(rc); + rc = SUCCEEDED(device->GetRenderTargetData(surface, offscreenSurface)); + verify(rc); + + D3DLOCKED_RECT rect; + RECT lockRect { 0, 0, (long)width, (long)height }; + rc = SUCCEEDED(offscreenSurface->LockRect(&rect, &lockRect, D3DLOCK_READONLY)); + verify(rc); + data.clear(); + data.reserve(width * height * 3); + for (int y = 0; y < height; y++) + { + const u8 *src = (const u8 *)rect.pBits + y * rect.Pitch; + for (int x = 0; x < width; x++, src += 4) + { + data.push_back(src[2]); + data.push_back(src[1]); + data.push_back(src[0]); + } + } + rc = SUCCEEDED(offscreenSurface->UnlockRect()); + device->SetRenderTarget(0, backbuffer); + + return true; +} + Renderer* rend_DirectX9() { return new D3DRenderer(); diff --git a/core/rend/dx9/d3d_renderer.h b/core/rend/dx9/d3d_renderer.h index 36e360cd1..bdea5cbe9 100644 --- a/core/rend/dx9/d3d_renderer.h +++ b/core/rend/dx9/d3d_renderer.h @@ -25,7 +25,7 @@ #include "rend/transform_matrix.h" #include "d3d_texture.h" #include "d3d_shaders.h" -#include "rend/imgui_driver.h" +#include "ui/imgui_driver.h" class RenderStateCache { @@ -116,6 +116,7 @@ struct D3DRenderer : public Renderer void preReset(); void postReset(); void RenderFramebuffer(const FramebufferInfo& info) override; + bool GetLastFrame(std::vector& data, int& width, int& height) override; private: enum ModifierVolumeMode { Xor, Or, Inclusion, Exclusion, ModeCount }; diff --git a/core/rend/dx9/dx9_driver.h b/core/rend/dx9/dx9_driver.h index 20e615c5a..4e271b40d 100644 --- a/core/rend/dx9/dx9_driver.h +++ b/core/rend/dx9/dx9_driver.h @@ -17,7 +17,7 @@ along with Flycast. If not, see . */ #pragma once -#include "rend/imgui_driver.h" +#include "ui/imgui_driver.h" #include "imgui_impl_dx9.h" #include "dxcontext.h" #include @@ -53,20 +53,21 @@ public: frameRendered = true; } - ImTextureID getTexture(const std::string& name) override { + ImTextureID getTexture(const std::string& name) override + { auto it = textures.find(name); if (it != textures.end()) - return (ImTextureID)it->second.get(); + return (ImTextureID)&it->second.imTexture; else return ImTextureID{}; } - ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) override + ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) override { - ComPtr& texture = textures[name]; - texture.reset(); - HRESULT hr = theDXContext.getDevice()->CreateTexture(width, height, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &texture.get(), 0); - if (FAILED(hr) || !texture) + Texture& texture = textures[name]; + texture.tex.reset(); + HRESULT hr = theDXContext.getDevice()->CreateTexture(width, height, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &texture.tex.get(), 0); + if (FAILED(hr) || !texture.tex) { WARN_LOG(RENDERER, "CreateTexture failed (%d x %d): error %x", width, height, hr); textures.erase(name); @@ -76,7 +77,7 @@ public: width *= 4; D3DLOCKED_RECT rect; - texture->LockRect(0, &rect, nullptr, 0); + texture.tex->LockRect(0, &rect, nullptr, 0); u8 *dst = (u8 *)rect.pBits; const u8 *src = data; for (int y = 0; y < height; y++) @@ -92,12 +93,23 @@ public: dst += rect.Pitch; src += width; } - texture->UnlockRect(0); + texture.tex->UnlockRect(0); + texture.imTexture.d3dTexture = texture.tex.get(); + texture.imTexture.pointSampling = nearestSampling; - return (ImTextureID)texture.get(); + return (ImTextureID)&texture.imTexture; + } + + void deleteTexture(const std::string& name) override { + textures.erase(name); } private: bool frameRendered = false; - std::unordered_map> textures; + struct Texture + { + ComPtr tex; + ImTextureDX9 imTexture; + }; + std::unordered_map textures; }; diff --git a/core/rend/dx9/dxcontext.cpp b/core/rend/dx9/dxcontext.cpp index 7dc79a9b6..f57306725 100644 --- a/core/rend/dx9/dxcontext.cpp +++ b/core/rend/dx9/dxcontext.cpp @@ -200,10 +200,6 @@ void DXContext::EndImGuiFrame() if (crosshairsNeeded() || config::FloatVMUs) overlay.draw(settings.display.width, settings.display.height, config::FloatVMUs, true); } - else - { - overlay.draw(settings.display.width, settings.display.height, true, false); - } ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); pDevice->EndScene(); } diff --git a/core/rend/game_scanner.h b/core/rend/game_scanner.h deleted file mode 100644 index 8dd4b5df9..000000000 --- a/core/rend/game_scanner.h +++ /dev/null @@ -1,203 +0,0 @@ -/* - Copyright 2020 flyinghead - - This file is part of flycast. - - flycast 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. - - flycast 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 flycast. If not, see . - */ -#pragma once -#include -#include -#include -#include - -#include "types.h" -#include "stdclass.h" -#include "hw/naomi/naomi_roms.h" -#include "oslib/storage.h" -#include "cfg/option.h" - -struct GameMedia { - std::string name; // Display name - std::string path; // Full path to rom. May be an encoded uri - std::string fileName; // Last component of the path, decoded - std::string gameName; // for arcade games only, description from the rom list -}; - -static bool operator<(const GameMedia &left, const GameMedia &right) -{ - return left.name < right.name; -} - -class GameScanner -{ - std::vector game_list; - std::vector arcade_game_list; - std::mutex mutex; - std::mutex threadMutex; - std::unique_ptr scan_thread; - bool scan_done = false; - bool running = false; - std::unordered_map arcade_games; - std::unordered_set arcade_gdroms; - - void insert_game(const GameMedia& game) - { - std::lock_guard guard(mutex); - game_list.insert(std::upper_bound(game_list.begin(), game_list.end(), game), game); - } - - void insert_arcade_game(const GameMedia& game) - { - arcade_game_list.insert(std::upper_bound(arcade_game_list.begin(), arcade_game_list.end(), game), game); - } - - void add_game_directory(const std::string& path) - { - hostfs::DirectoryTree tree(path); - std::string emptyParentPath; - for (const hostfs::FileInfo& item : tree) - { - if (!running) - break; - - if (game_list.empty()) - { - // This won't work for android content uris - size_t slash = get_last_slash_pos(item.path); - std::string parentPath; - if (slash != 0 && slash != std::string::npos) - parentPath = item.path.substr(0, slash); - else - parentPath = item.path; - if (parentPath != emptyParentPath) - { - ++empty_folders_scanned; - emptyParentPath = parentPath; - if (empty_folders_scanned > 1000) - content_path_looks_incorrect = true; - } - } - else - { - content_path_looks_incorrect = false; - } - - if (item.name.substr(0, 2) == "._") - // Ignore Mac OS turds - continue; - std::string fileName(item.name); - std::string gameName(get_file_basename(item.name)); - std::string extension = get_file_extension(item.name); - if (extension == "zip" || extension == "7z") - { - string_tolower(gameName); - auto it = arcade_games.find(gameName); - if (it == arcade_games.end()) - continue; - gameName = it->second->description; - fileName = fileName + " (" + gameName + ")"; - insert_arcade_game(GameMedia{ fileName, item.path, item.name, gameName }); - continue; - } - else if (extension == "bin" || extension == "lst" || extension == "dat") - { - if (!config::HideLegacyNaomiRoms) - insert_arcade_game(GameMedia{ fileName, item.path, item.name, gameName }); - continue; - } - else if (extension == "chd" || extension == "gdi") - { - // Hide arcade gdroms - std::string basename = gameName; - string_tolower(basename); - if (arcade_gdroms.count(basename) != 0) - continue; - } - else if (extension != "cdi" && extension != "cue") - continue; - insert_game(GameMedia{ fileName, item.path, item.name, gameName }); - } - } - -public: - ~GameScanner() - { - stop(); - } - void refresh() - { - stop(); - scan_done = false; - } - - void stop() - { - std::lock_guard guard(threadMutex); - running = false; - empty_folders_scanned = 0; - content_path_looks_incorrect = false; - if (scan_thread && scan_thread->joinable()) - scan_thread->join(); - } - - void fetch_game_list() - { - std::lock_guard guard(threadMutex); - if (scan_done || running) - return; - if (scan_thread && scan_thread->joinable()) - scan_thread->join(); - running = true; - scan_thread = std::unique_ptr( - new std::thread([this]() - { - if (arcade_games.empty()) - for (int gameid = 0; Games[gameid].name != nullptr; gameid++) - { - const Game *game = &Games[gameid]; - arcade_games[game->name] = game; - if (game->gdrom_name != nullptr) - arcade_gdroms.insert(game->gdrom_name); - } - { - std::lock_guard guard(mutex); - game_list.clear(); - } - arcade_game_list.clear(); - for (const auto& path : config::ContentPath.get()) - { - try { - add_game_directory(path); - } catch (const hostfs::StorageException& e) { - // ignore - } - if (!running) - break; - } - { - std::lock_guard guard(mutex); - game_list.insert(game_list.end(), arcade_game_list.begin(), arcade_game_list.end()); - } - if (running) - scan_done = true; - running = false; - })); - } - - std::mutex& get_mutex() { return mutex; } - const std::vector& get_game_list() { return game_list; } - unsigned int empty_folders_scanned = 0; - bool content_path_looks_incorrect = false; -}; diff --git a/core/rend/gl4/gles.cpp b/core/rend/gl4/gles.cpp index e09987c7c..ef65cf9c8 100644 --- a/core/rend/gl4/gles.cpp +++ b/core/rend/gl4/gles.cpp @@ -711,8 +711,8 @@ struct OpenGL4Renderer : OpenGLRenderer if (!config::EmulateFramebuffer) { - DrawOSD(false); frameRendered = true; + DrawOSD(false); renderVideoRouting(); } restoreCurrentFramebuffer(); @@ -729,23 +729,13 @@ struct OpenGL4Renderer : OpenGLRenderer bool renderFrame(int width, int height); -#ifdef LIBRETRO void DrawOSD(bool clearScreen) override { - void DrawVmuTexture(u8 vmu_screen_number, int width, int height); - void DrawGunCrosshair(u8 port, int width, int height); - - if (settings.platform.isConsole()) - { - for (int vmu_screen_number = 0 ; vmu_screen_number < 4 ; vmu_screen_number++) - if (vmu_lcd_status[vmu_screen_number * 2]) - DrawVmuTexture(vmu_screen_number, width, height); - } - - for (int lightgun_port = 0 ; lightgun_port < 4 ; lightgun_port++) - DrawGunCrosshair(lightgun_port, width, height); - } + drawVmusAndCrosshairs(width, height); +#ifndef LIBRETRO + gui_display_osd(); #endif + } }; //setup diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index d4a06ab9e..b69c607e0 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -6,6 +6,7 @@ #include "rend/transform_matrix.h" #ifdef LIBRETRO #include "postprocess.h" +#include "vmu_xhair.h" #endif #include @@ -790,7 +791,7 @@ bool OpenGLRenderer::renderLastFrame() -1.f, -1.f, 1.f, 0.f, 1.f, -1.f, 1.f, 1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 1.f, 1.f, - 1.f, -1.f, 1.f, 1.f, 0.f, + 1.f, 1.f, 1.f, 1.f, 0.f, }; sverts[0] = sverts[5] = -1.f + gl.ofbo.shiftX * 2.f / framebuffer->getWidth(); sverts[10] = sverts[15] = sverts[0] + 2; @@ -817,131 +818,164 @@ bool OpenGLRenderer::renderLastFrame() return true; } -#ifdef LIBRETRO -#include "vmu_xhair.h" - -static GLuint vmuTextureId[4] {}; -static GLuint lightgunTextureId[4] {}; - -static void updateVmuTexture(int vmu_screen_number) +bool OpenGLRenderer::GetLastFrame(std::vector& data, int& width, int& height) { - if (vmuTextureId[vmu_screen_number] == 0) - { - vmuTextureId[vmu_screen_number] = glcache.GenTexture(); - glcache.BindTexture(GL_TEXTURE_2D, vmuTextureId[vmu_screen_number]); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + GlFramebuffer *framebuffer = gl.ofbo2.ready ? gl.ofbo2.framebuffer.get() : gl.ofbo.framebuffer.get(); + if (framebuffer == nullptr) + return false; + if (width != 0) { + height = width / gl.ofbo.aspectRatio; + } + else if (height != 0) { + width = gl.ofbo.aspectRatio * height; } else - glcache.BindTexture(GL_TEXTURE_2D, vmuTextureId[vmu_screen_number]); - - - const u32 *data = vmu_lcd_data[vmu_screen_number * 2]; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VMU_SCREEN_WIDTH, VMU_SCREEN_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - - vmu_lcd_changed[vmu_screen_number * 2] = false; -} - -void DrawVmuTexture(u8 vmu_screen_number, int width, int height) -{ - float x = 8.f * width / 640.f; - float y = 8.f * height / 480.f; - float w = (float)VMU_SCREEN_WIDTH * vmu_screen_params[vmu_screen_number].vmu_screen_size_mult * 4.f / 3.f / gl.ofbo.aspectRatio * width / 640.f; - float h = (float)VMU_SCREEN_HEIGHT * vmu_screen_params[vmu_screen_number].vmu_screen_size_mult * height / 480.f; - - if (vmu_lcd_changed[vmu_screen_number * 2] || vmuTextureId[vmu_screen_number] == 0) - updateVmuTexture(vmu_screen_number); - - switch (vmu_screen_params[vmu_screen_number].vmu_screen_position) { - case UPPER_LEFT: - break; - case UPPER_RIGHT: - x = width - x - w; - break; - case LOWER_LEFT: - y = height - y - h; - break; - case LOWER_RIGHT: - x = width - x - w; - y = height - y - h; - break; + width = framebuffer->getWidth(); + height = framebuffer->getHeight(); + if (config::Rotate90) + std::swap(width, height); + // We need square pixels for PNG + int w = gl.ofbo.aspectRatio * height; + if (width > w) + height = width / gl.ofbo.aspectRatio; + else + width = w; } - float x1 = (x + w) * 2 / width - 1; - float y1 = -(y + h) * 2 / height + 1; - x = x * 2 / width - 1; - y = -y * 2 / height + 1; - float vertices[20] = { - x, y1, 1.f, 0.f, 0.f, - x, y, 1.f, 0.f, 1.f, - x1, y1, 1.f, 1.f, 0.f, - x1, y, 1.f, 1.f, 1.f, - }; - glcache.Enable(GL_BLEND); - glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - drawQuad(vmuTextureId[vmu_screen_number], false, false, vertices); -} -static void updateLightGunTexture(int port) -{ - s32 x,y ; - u8 temp_tex_buffer[LIGHTGUN_CROSSHAIR_SIZE*LIGHTGUN_CROSSHAIR_SIZE*4]; - u8 *dst = temp_tex_buffer; - u8 *src = NULL ; + GlFramebuffer dstFramebuffer(width, height, false, false); - if (lightgunTextureId[port] == 0) + glViewport(0, 0, width, height); + glcache.Disable(GL_BLEND); + verify(framebuffer->getTexture() != 0); + const float *vertices = nullptr; + if (config::Rotate90) { - lightgunTextureId[port] = glcache.GenTexture(); - glcache.BindTexture(GL_TEXTURE_2D, lightgunTextureId[port]); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + static float rvertices[4][5] = { + { -1.f, 1.f, 1.f, 1.f, 0.f }, + { -1.f, -1.f, 1.f, 1.f, 1.f }, + { 1.f, 1.f, 1.f, 0.f, 0.f }, + { 1.f, -1.f, 1.f, 0.f, 1.f }, + }; + vertices = &rvertices[0][0]; } - else - glcache.BindTexture(GL_TEXTURE_2D, lightgunTextureId[port]); + drawQuad(framebuffer->getTexture(), config::Rotate90, false, vertices); - u8* colour = &( lightgun_palette[ lightgun_params[port].colour * 3 ] ); - - for ( y = LIGHTGUN_CROSSHAIR_SIZE-1 ; y >= 0 ; y--) + glBindFramebuffer(GL_FRAMEBUFFER, 0); + data.resize(width * height * 3); + dstFramebuffer.bind(GL_READ_FRAMEBUFFER); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + if (gl.is_gles) { - src = lightgun_img_crosshair + (y*LIGHTGUN_CROSSHAIR_SIZE) ; - - for ( x = 0 ; x < LIGHTGUN_CROSSHAIR_SIZE ; x++) + // GL_RGB not supported + std::vector tmp(width * height * 4); + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, tmp.data()); + u8 *dst = data.data(); + const u8 *src = tmp.data(); + while (src <= &tmp.back()) { - if ( src[x] ) - { - *dst++ = colour[0] ; - *dst++ = colour[1] ; - *dst++ = colour[2] ; - *dst++ = 0xFF ; - } - else - { - *dst++ = 0 ; - *dst++ = 0 ; - *dst++ = 0 ; - *dst++ = 0 ; - } + *dst++ = *src++; + *dst++ = *src++; + *dst++ = *src++; + src++; } } + else { + glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, data.data()); + } + restoreCurrentFramebuffer(); + glCheck(); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, LIGHTGUN_CROSSHAIR_SIZE, LIGHTGUN_CROSSHAIR_SIZE, 0, GL_RGBA, GL_UNSIGNED_BYTE, temp_tex_buffer); - - lightgun_params[port].dirty = false; + return true; } -void DrawGunCrosshair(u8 port, int width, int height) +static GLuint vmuTextureId[8] {}; +static GLuint lightgunTextureId {}; +static u64 vmuLastUpdated[8] {}; + +static void updateVmuTexture(int vmuIndex) { - if (lightgun_params[port].offscreen || lightgun_params[port].colour == 0) + if (vmuTextureId[vmuIndex] == 0) + { + vmuTextureId[vmuIndex] = glcache.GenTexture(); + glcache.BindTexture(GL_TEXTURE_2D, vmuTextureId[vmuIndex]); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + else + glcache.BindTexture(GL_TEXTURE_2D, vmuTextureId[vmuIndex]); + + const u32 *data = vmu_lcd_data[vmuIndex]; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 48, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + + vmuLastUpdated[vmuIndex] = vmuLastChanged[vmuIndex]; +} + +static void drawVmuTexture(u8 vmuIndex, int width, int height) +{ + const float *color = nullptr; +#ifndef LIBRETRO + const float vmu_padding = 8.f * settings.display.uiScale; + const float w = 96.f * settings.display.uiScale; + const float h = 64.f * settings.display.uiScale; + + float x; + float y; + if (vmuIndex & 2) + x = width - vmu_padding - w; + else + x = vmu_padding; + if (vmuIndex & 4) + { + y = height - vmu_padding - h; + if (vmuIndex & 1) + y -= vmu_padding + h; + } + else + { + y = vmu_padding; + if (vmuIndex & 1) + y += vmu_padding + h; + } + const float blend_factor[4] = { 0.75f, 0.75f, 0.75f, 0.75f }; + color = blend_factor; +#else + if (vmuIndex & 1) return; + const float vmu_padding_x = 8.f * width / 640.f * 4.f / 3.f / gl.ofbo.aspectRatio; + const float vmu_padding_y = 8.f * height / 480.f; + const float w = (float)VMU_SCREEN_WIDTH * width / 640.f * 4.f / 3.f / gl.ofbo.aspectRatio + * vmu_screen_params[vmuIndex / 2].vmu_screen_size_mult; + const float h = (float)VMU_SCREEN_HEIGHT * height / 480.f + * vmu_screen_params[vmuIndex / 2].vmu_screen_size_mult; - float w = lightgun_crosshair_size * 4.f / 3.f / gl.ofbo.aspectRatio * config::RenderResolution / 480.f; - float h = lightgun_crosshair_size * config::RenderResolution / 480.f; - auto [x, y] = getCrosshairPosition(port); - x -= w / 2; - y -= h / 2; + float x; + float y; - if (lightgun_params[port].dirty || lightgunTextureId[port] == 0) - updateLightGunTexture(port); + switch (vmu_screen_params[vmuIndex / 2].vmu_screen_position) + { + case UPPER_LEFT: + default: + x = vmu_padding_x; + y = vmu_padding_y; + break; + case UPPER_RIGHT: + x = width - vmu_padding_x - w; + y = vmu_padding_y; + break; + case LOWER_LEFT: + x = vmu_padding_x; + y = height - vmu_padding_y - h; + break; + case LOWER_RIGHT: + x = width - vmu_padding_x - w; + y = height - vmu_padding_y - h; + break; + } +#endif + + if (vmuLastChanged[vmuIndex] != vmuLastUpdated[vmuIndex] || vmuTextureId[vmuIndex] == 0) + updateVmuTexture(vmuIndex); float x1 = (x + w) * 2 / width - 1; float y1 = -(y + h) * 2 / height + 1; @@ -955,14 +989,92 @@ void DrawGunCrosshair(u8 port, int width, int height) }; glcache.Enable(GL_BLEND); glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - drawQuad(lightgunTextureId[port], false, false, vertices); + drawQuad(vmuTextureId[vmuIndex], false, false, vertices, color); +} + +static void updateLightGunTexture() +{ + if (lightgunTextureId == 0) + { + lightgunTextureId = glcache.GenTexture(); + glcache.BindTexture(GL_TEXTURE_2D, lightgunTextureId); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, getCrosshairTextureData()); + } +} + +static void drawGunCrosshair(u8 port, int width, int height) +{ + if (config::CrosshairColor[port] == 0) + return; + if (settings.platform.isConsole() + && config::MapleMainDevices[port] != MDT_LightGun) + return; + + auto [x, y] = getCrosshairPosition(port); +#ifdef LIBRETRO + float halfWidth = lightgun_crosshair_size / 2.f / config::ScreenStretching * 100.f * config::RenderResolution / 480.f; + float halfHeight = lightgun_crosshair_size / 2.f * config::RenderResolution / 480.f; + x /= config::ScreenStretching / 100.f; +#else + float halfWidth = config::CrosshairSize * settings.display.uiScale / 2.f; + float halfHeight = halfWidth; +#endif + + updateLightGunTexture(); + + float x1 = (x + halfWidth) * 2 / width - 1; + float y1 = -(y + halfHeight) * 2 / height + 1; + x = (x - halfWidth) * 2 / width - 1; + y = -(y - halfHeight) * 2 / height + 1; + float vertices[20] = { + x, y1, 1.f, 0.f, 0.f, + x, y, 1.f, 0.f, 1.f, + x1, y1, 1.f, 1.f, 0.f, + x1, y, 1.f, 1.f, 1.f, + }; + const float color[4] = { + (config::CrosshairColor[port] & 0xff) / 255.f, + ((config::CrosshairColor[port] >> 8) & 0xff) / 255.f, + ((config::CrosshairColor[port] >> 16) & 0xff) / 255.f, + ((config::CrosshairColor[port] >> 24) & 0xff) / 255.f + }; + glcache.Enable(GL_BLEND); + glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + drawQuad(lightgunTextureId, false, false, vertices, color); +} + +void drawVmusAndCrosshairs(int width, int height) +{ +#ifndef LIBRETRO + width = settings.display.width; + height = settings.display.height; + glViewport(0, 0, width, height); + glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); + const bool showVmus = config::FloatVMUs; +#else + const bool showVmus = true; +#endif + + if (settings.platform.isConsole() && showVmus) + { + for (int i = 0; i < 8 ; i++) + if (vmu_lcd_status[i]) + drawVmuTexture(i, width, height); + } + + if (crosshairsNeeded()) { + for (int i = 0 ; i < 4 ; i++) + drawGunCrosshair(i, width, height); + } + glCheck(); } void termVmuLightgun() { glcache.DeleteTextures(std::size(vmuTextureId), vmuTextureId); memset(vmuTextureId, 0, sizeof(vmuTextureId)); - glcache.DeleteTextures(std::size(lightgunTextureId), lightgunTextureId); - memset(lightgunTextureId, 0, sizeof(lightgunTextureId)); + glcache.DeleteTextures(1, &lightgunTextureId); + lightgunTextureId = 0; } -#endif diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index 213ed62dd..e547c1689 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -2,7 +2,7 @@ #include "gles.h" #include "hw/pvr/ta.h" #ifndef LIBRETRO -#include "rend/gui.h" +#include "ui/gui.h" #else #include "rend/gles/postprocess.h" #include "vmu_xhair.h" @@ -516,9 +516,9 @@ void termGLCommon() gl.ofbo2.framebuffer.reset(); gl.fbscaling.framebuffer.reset(); gl.videorouting.framebuffer.reset(); + termVmuLightgun(); #ifdef LIBRETRO postProcessor.term(); - termVmuLightgun(); #endif } @@ -989,9 +989,11 @@ static void gl_create_resources() findGLVersion(); +#ifndef LIBRETRO if (gl.gl_major >= 3) // will be used later. Better fail fast verify(glGenVertexArrays != nullptr); +#endif //create vbos gl.vbo.geometry = std::make_unique(GL_ARRAY_BUFFER); @@ -1120,21 +1122,9 @@ static void updatePaletteTexture(GLenum texture_slot) void OpenGLRenderer::DrawOSD(bool clear_screen) { -#ifdef LIBRETRO - void DrawVmuTexture(u8 vmu_screen_number, int width, int height); - void DrawGunCrosshair(u8 port, int width, int height); + drawVmusAndCrosshairs(width, height); - if (settings.platform.isConsole()) - { - for (int vmu_screen_number = 0 ; vmu_screen_number < 4 ; vmu_screen_number++) - if (vmu_lcd_status[vmu_screen_number * 2]) - DrawVmuTexture(vmu_screen_number, width, height); - } - - for (int lightgun_port = 0 ; lightgun_port < 4 ; lightgun_port++) - DrawGunCrosshair(lightgun_port, width, height); - -#else +#ifndef LIBRETRO gui_display_osd(); #ifdef __ANDROID__ if (gl.OSD_SHADER.osd_tex == 0) @@ -1510,8 +1500,8 @@ bool OpenGLRenderer::Render() if (!config::EmulateFramebuffer) { - DrawOSD(false); frameRendered = true; + DrawOSD(false); renderVideoRouting(); } diff --git a/core/rend/gles/gles.h b/core/rend/gles/gles.h index 9f3f0c076..ce1bd6423 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -7,7 +7,7 @@ #include "glcache.h" #include "rend/shader_util.h" #ifndef LIBRETRO -#include "rend/imgui_driver.h" +#include "ui/imgui_driver.h" #endif #include @@ -519,6 +519,7 @@ struct OpenGLRenderer : Renderer return ret; } + bool GetLastFrame(std::vector& data, int& width, int& height) override; void DrawOSD(bool clear_screen) override; @@ -570,7 +571,7 @@ protected: void initQuad(); void termQuad(); -void drawQuad(GLuint texId, bool rotate = false, bool swapY = false, float *coords = nullptr); +void drawQuad(GLuint texId, bool rotate = false, bool swapY = false, const float *coords = nullptr, const float *color = nullptr); extern const char* ShaderCompatSource; extern const char *VertexCompatShader; @@ -585,7 +586,9 @@ public: } }; +void drawVmusAndCrosshairs(int width, int height); +void termVmuLightgun(); + #ifdef LIBRETRO extern "C" struct retro_hw_render_callback hw_render; -void termVmuLightgun(); #endif diff --git a/core/rend/gles/gltex.cpp b/core/rend/gles/gltex.cpp index 18af7462b..e71e8992f 100644 --- a/core/rend/gles/gltex.cpp +++ b/core/rend/gles/gltex.cpp @@ -315,28 +315,22 @@ void glReadFramebuffer(const FramebufferInfo& info) GLuint init_output_framebuffer(int width, int height) { if (gl.ofbo.framebuffer != nullptr - && (width != gl.ofbo.framebuffer->getWidth() || height != gl.ofbo.framebuffer->getHeight() - // if the rotate90 setting has changed - || (gl.gl_major >= 3 && (gl.ofbo.framebuffer->getTexture() == 0) == config::Rotate90))) + && (width != gl.ofbo.framebuffer->getWidth() || height != gl.ofbo.framebuffer->getHeight())) { gl.ofbo.framebuffer.reset(); } if (gl.ofbo.framebuffer == nullptr) { - GLuint texture = 0; - if (config::Rotate90) - { - // Create a texture for rendering to - texture = glcache.GenTexture(); - glcache.BindTexture(GL_TEXTURE_2D, texture); + // Create a texture for rendering to + GLuint texture = glcache.GenTexture(); + glcache.BindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); gl.ofbo.framebuffer = std::make_unique(width, height, true, texture); glcache.Disable(GL_SCISSOR_TEST); diff --git a/core/rend/gles/opengl_driver.cpp b/core/rend/gles/opengl_driver.cpp index 48b259277..001999cc1 100644 --- a/core/rend/gles/opengl_driver.cpp +++ b/core/rend/gles/opengl_driver.cpp @@ -19,10 +19,9 @@ #include "opengl_driver.h" #include "imgui_impl_opengl3.h" #include "wsi/gl_context.h" -#include "rend/osd.h" -#include "rend/gui.h" #include "glcache.h" #include "gles.h" +#include "hw/pvr/Renderer_if.h" #ifndef GL_CLAMP_TO_BORDER #define GL_CLAMP_TO_BORDER 0x812D @@ -31,41 +30,15 @@ #define GL_TEXTURE_BORDER_COLOR 0x1004 #endif -static constexpr int vmu_coords[8][2] = { - { 0 , 0 }, - { 0 , 0 }, - { 1 , 0 }, - { 1 , 0 }, - { 0 , 1 }, - { 0 , 1 }, - { 1 , 1 }, - { 1 , 1 }, -}; -constexpr int VMU_WIDTH = 70 * 48 / 32; -constexpr int VMU_HEIGHT = 70; -constexpr int VMU_PADDING = 8; - OpenGLDriver::OpenGLDriver() { - for (auto& tex : vmu_lcd_tex_ids) - tex = ImTextureID(); ImGui_ImplOpenGL3_Init(); - EventManager::listen(Event::Resume, emuEventCallback, this); - EventManager::listen(Event::Terminate, emuEventCallback, this); } OpenGLDriver::~OpenGLDriver() { - EventManager::unlisten(Event::Resume, emuEventCallback, this); - EventManager::unlisten(Event::Terminate, emuEventCallback, this); - std::vector texIds; - texIds.reserve(std::size(vmu_lcd_tex_ids) + 1 + textures.size()); - for (ImTextureID texId : vmu_lcd_tex_ids) - if (texId != ImTextureID()) - texIds.push_back((GLuint)(uintptr_t)texId); - if (crosshairTexId != ImTextureID()) - texIds.push_back((GLuint)(uintptr_t)crosshairTexId); + texIds.reserve(textures.size()); for (const auto& it : textures) texIds.push_back((GLuint)(uintptr_t)it.second); if (!texIds.empty()) @@ -73,86 +46,6 @@ OpenGLDriver::~OpenGLDriver() ImGui_ImplOpenGL3_Shutdown(); } -void OpenGLDriver::displayVmus() -{ - if (!gameStarted) - return; - ImGui::SetNextWindowBgAlpha(0); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); - - ImGui::Begin("vmu-window", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs - | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoFocusOnAppearing); - const float width = VMU_WIDTH * settings.display.uiScale; - const float height = VMU_HEIGHT * settings.display.uiScale; - const float padding = VMU_PADDING * settings.display.uiScale; - for (int i = 0; i < 8; i++) - { - if (!vmu_lcd_status[i]) - continue; - - if (vmu_lcd_changed[i] || vmu_lcd_tex_ids[i] == ImTextureID()) - vmu_lcd_tex_ids[i] = updateTexture("__vmu" + std::to_string(i), (const u8 *)vmu_lcd_data[i], 48, 32); - - int x = vmu_coords[i][0]; - int y = vmu_coords[i][1]; - ImVec2 pos; - if (x == 0) - pos.x = padding; - else - pos.x = ImGui::GetIO().DisplaySize.x - width - padding; - if (y == 0) - { - pos.y = padding; - if (i & 1) - pos.y += height + padding; - } - else - { - pos.y = ImGui::GetIO().DisplaySize.y - height - padding; - if (i & 1) - pos.y -= height + padding; - } - ImVec2 pos_b(pos.x + width, pos.y + height); - ImGui::GetWindowDrawList()->AddImage(vmu_lcd_tex_ids[i], pos, pos_b, ImVec2(0, 1), ImVec2(1, 0), 0xC0ffffff); - } - ImGui::End(); -} - -void OpenGLDriver::displayCrosshairs() -{ - if (!gameStarted) - return; - if (!crosshairsNeeded()) - return; - - if (crosshairTexId == ImTextureID()) - crosshairTexId = updateTexture("__crosshair", (const u8 *)getCrosshairTextureData(), 16, 16); - - ImGui::SetNextWindowBgAlpha(0); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); - - ImGui::Begin("xhair-window", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs - | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoFocusOnAppearing); - for (u32 i = 0; i < config::CrosshairColor.size(); i++) - { - if (config::CrosshairColor[i] == 0) - continue; - if (settings.platform.isConsole() && config::MapleMainDevices[i] != MDT_LightGun) - continue; - - ImVec2 pos; - std::tie(pos.x, pos.y) = getCrosshairPosition(i); - pos.x -= (config::CrosshairSize * settings.display.uiScale) / 2.f; - pos.y += (config::CrosshairSize * settings.display.uiScale) / 2.f; - ImVec2 pos_b(pos.x + config::CrosshairSize * settings.display.uiScale, pos.y - config::CrosshairSize * settings.display.uiScale); - - ImGui::GetWindowDrawList()->AddImage(crosshairTexId, pos, pos_b, ImVec2(0, 1), ImVec2(1, 0), config::CrosshairColor[i]); - } - ImGui::End(); -} - void OpenGLDriver::newFrame() { ImGui_ImplOpenGL3_NewFrame(); @@ -160,6 +53,17 @@ void OpenGLDriver::newFrame() void OpenGLDriver::renderDrawData(ImDrawData* drawData, bool gui_open) { + if (gui_open) + { +#ifndef TARGET_IPHONE + glBindFramebuffer(GL_FRAMEBUFFER, 0); +#endif + glcache.Disable(GL_SCISSOR_TEST); + glcache.ClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + if (renderer != nullptr) + renderer->RenderLastFrame(); + } ImGui_ImplOpenGL3_RenderDrawData(drawData); if (gui_open) frameRendered = true; @@ -172,15 +76,21 @@ void OpenGLDriver::present() frameRendered = false; } -ImTextureID OpenGLDriver::updateTexture(const std::string& name, const u8 *data, int width, int height) +ImTextureID OpenGLDriver::updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) { ImTextureID oldId = getTexture(name); if (oldId != ImTextureID()) glcache.DeleteTextures(1, (GLuint *)&oldId); GLuint texId = glcache.GenTexture(); - glcache.BindTexture(GL_TEXTURE_2D, texId); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glcache.BindTexture(GL_TEXTURE_2D, texId); + if (nearestSampling) { + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + else { + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } if (gl.border_clamp_supported) { float color[] = { 0.0f, 0.0f, 0.0f, 0.0f }; @@ -197,3 +107,12 @@ ImTextureID OpenGLDriver::updateTexture(const std::string& name, const u8 *data, return textures[name] = (ImTextureID)(u64)texId; } + +void OpenGLDriver::deleteTexture(const std::string& name) +{ + auto it = textures.find(name); + if (it != textures.end()) { + glcache.DeleteTextures(1, (GLuint *)&it->second); + textures.erase(it); + } +} diff --git a/core/rend/gles/opengl_driver.h b/core/rend/gles/opengl_driver.h index 9588a9965..77c63b9f7 100644 --- a/core/rend/gles/opengl_driver.h +++ b/core/rend/gles/opengl_driver.h @@ -17,8 +17,7 @@ along with Flycast. If not, see . */ #pragma once -#include "rend/imgui_driver.h" -#include "emulator.h" +#include "ui/imgui_driver.h" #include class OpenGLDriver final : public ImGuiDriver @@ -27,9 +26,6 @@ public: OpenGLDriver(); ~OpenGLDriver() override; - void displayVmus() override; - void displayCrosshairs() override; - void newFrame() override; void renderDrawData(ImDrawData* drawData, bool gui_open) override; void present() override; @@ -38,7 +34,8 @@ public: frameRendered = true; } - ImTextureID getTexture(const std::string& name) override { + ImTextureID getTexture(const std::string& name) override + { auto it = textures.find(name); if (it != textures.end()) return it->second; @@ -46,30 +43,10 @@ public: return ImTextureID{}; } - ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) override; + ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) override; + void deleteTexture(const std::string& name) override; private: - void emuEvent(Event event) - { - switch (event) - { - case Event::Resume: - gameStarted = true; - break; - case Event::Terminate: - gameStarted = false; - break; - default: - break; - } - } - static void emuEventCallback(Event event, void *p) { - ((OpenGLDriver *)p)->emuEvent(event); - } - - ImTextureID vmu_lcd_tex_ids[8]; - ImTextureID crosshairTexId = ImTextureID(); - bool gameStarted = false; bool frameRendered = false; std::unordered_map textures; }; diff --git a/core/rend/gles/quad.cpp b/core/rend/gles/quad.cpp index 5419b9340..8c6047e97 100644 --- a/core/rend/gles/quad.cpp +++ b/core/rend/gles/quad.cpp @@ -38,10 +38,11 @@ static const char* FragmentShader = R"( in mediump vec2 vtx_uv; uniform sampler2D tex; +uniform mediump vec4 tint; void main() { - gl_FragColor = texture(tex, vtx_uv); + gl_FragColor = tint * texture(tex, vtx_uv); } )"; @@ -63,7 +64,9 @@ protected: }; static GLuint shader; +static GLint tintUniform; static GLuint rot90shader; +static GLint rot90TintUniform; static QuadVertexArray quadVertexArray; static QuadVertexArray quadVertexArraySwapY; static std::unique_ptr quadBuffer; @@ -88,11 +91,13 @@ void initQuad() shader = gl_CompileAndLink(vertexShader.generate().c_str(), fragmentGlsl.c_str()); GLint tex = glGetUniformLocation(shader, "tex"); glUniform1i(tex, 0); // texture 0 + tintUniform = glGetUniformLocation(shader, "tint"); vertexShader.setConstant("ROTATE", 1); rot90shader = gl_CompileAndLink(vertexShader.generate().c_str(), fragmentGlsl.c_str()); tex = glGetUniformLocation(rot90shader, "tex"); glUniform1i(tex, 0); // texture 0 + rot90TintUniform = glGetUniformLocation(rot90shader, "tint"); } if (quadIndexBuffer == nullptr) { @@ -145,7 +150,7 @@ void termQuad() } // coords is an optional array of 20 floats (4 vertices with x,y,z,u,v each) -void drawQuad(GLuint texId, bool rotate, bool swapY, float *coords) +void drawQuad(GLuint texId, bool rotate, bool swapY, const float *coords, const float *color) { glcache.Disable(GL_SCISSOR_TEST); glcache.Disable(GL_DEPTH_TEST); @@ -157,6 +162,12 @@ void drawQuad(GLuint texId, bool rotate, bool swapY, float *coords) glActiveTexture(GL_TEXTURE0); glcache.BindTexture(GL_TEXTURE_2D, texId); + if (color == nullptr) { + static constexpr float white[4] { 1.f, 1.f, 1.f, 1.f }; + color = white; + } + glUniform4fv(rotate ? rot90TintUniform : tintUniform, 1, color); + if (coords == nullptr) { if (swapY) diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp deleted file mode 100644 index 542f62f93..000000000 --- a/core/rend/gui.cpp +++ /dev/null @@ -1,3478 +0,0 @@ -/* - Copyright 2019 flyinghead - - This file is part of Flycast. - - Flycast 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. - - Flycast 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 Flycast. If not, see . - */ -#include "gui.h" -#include "osd.h" -#include "cfg/cfg.h" -#include "hw/maple/maple_if.h" -#include "hw/maple/maple_devs.h" -#include "imgui.h" -#include "network/net_handshake.h" -#include "network/ggpo.h" -#include "wsi/context.h" -#include "input/gamepad_device.h" -#include "gui_util.h" -#include "game_scanner.h" -#include "version.h" -#include "oslib/oslib.h" -#include "audio/audiostream.h" -#include "imgread/common.h" -#include "log/LogManager.h" -#include "emulator.h" -#include "rend/mainui.h" -#include "lua/lua.h" -#include "gui_chat.h" -#include "imgui_driver.h" -#if FC_PROFILER -#include "implot.h" -#endif -#include "boxart/boxart.h" -#include "profiler/fc_profiler.h" -#include "hw/naomi/card_reader.h" -#include "oslib/resources.h" -#if defined(USE_SDL) -#include "sdl/sdl.h" -#endif - -#ifdef __ANDROID__ -#include "gui_android.h" -#endif - -#ifdef _WIN32 -#include -#else -#include -#endif -#include -#include - -static bool game_started; - -int insetLeft, insetRight, insetTop, insetBottom; -std::unique_ptr imguiDriver; - -static bool inited = false; -GuiState gui_state = GuiState::Main; -static bool commandLineStart; -static u32 mouseButtons; -static int mouseX, mouseY; -static float mouseWheel; -static std::string error_msg; -static bool error_msg_shown; -static std::string osd_message; -static double osd_message_end; -static std::mutex osd_message_mutex; -static void (*showOnScreenKeyboard)(bool show); -static bool keysUpNextFrame[512]; -static bool uiUserScaleUpdated; - -static void reset_vmus(); -void error_popup(); - -static GameScanner scanner; -static BackgroundGameLoader gameLoader; -static Boxart boxart; -static Chat chat; -static std::recursive_mutex guiMutex; -using LockGuard = std::lock_guard; - -static void emuEventCallback(Event event, void *) -{ - switch (event) - { - case Event::Resume: - game_started = true; - break; - case Event::Start: - GamepadDevice::load_system_mappings(); - break; - case Event::Terminate: - GamepadDevice::load_system_mappings(); - game_started = false; - break; - default: - break; - } -} - -void gui_init() -{ - if (inited) - return; - inited = true; - - // Setup Dear ImGui context - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); -#if FC_PROFILER - ImPlot::CreateContext(); -#endif - ImGuiIO& io = ImGui::GetIO(); (void)io; - io.BackendFlags |= ImGuiBackendFlags_HasGamepad; - - io.IniFilename = NULL; - - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls - io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls - - EventManager::listen(Event::Resume, emuEventCallback); - EventManager::listen(Event::Start, emuEventCallback); - EventManager::listen(Event::Terminate, emuEventCallback); - ggpo::receiveChatMessages([](int playerNum, const std::string& msg) { chat.receive(playerNum, msg); }); -} - -static ImGuiKey keycodeToImGuiKey(u8 keycode) -{ - switch (keycode) - { - case 0x2B: return ImGuiKey_Tab; - case 0x50: return ImGuiKey_LeftArrow; - case 0x4F: return ImGuiKey_RightArrow; - case 0x52: return ImGuiKey_UpArrow; - case 0x51: return ImGuiKey_DownArrow; - case 0x4B: return ImGuiKey_PageUp; - case 0x4E: return ImGuiKey_PageDown; - case 0x4A: return ImGuiKey_Home; - case 0x4D: return ImGuiKey_End; - case 0x49: return ImGuiKey_Insert; - case 0x4C: return ImGuiKey_Delete; - case 0x2A: return ImGuiKey_Backspace; - case 0x2C: return ImGuiKey_Space; - case 0x28: return ImGuiKey_Enter; - case 0x29: return ImGuiKey_Escape; - case 0x04: return ImGuiKey_A; - case 0x06: return ImGuiKey_C; - case 0x19: return ImGuiKey_V; - case 0x1B: return ImGuiKey_X; - case 0x1C: return ImGuiKey_Y; - case 0x1D: return ImGuiKey_Z; - case 0xE0: - case 0xE4: - return ImGuiMod_Ctrl; - case 0xE1: - case 0xE5: - return ImGuiMod_Shift; - case 0xE3: - case 0xE7: - return ImGuiMod_Super; - default: return ImGuiKey_None; - } -} - -void gui_initFonts() -{ - static float uiScale; - - verify(inited); - -#if !defined(TARGET_UWP) && !defined(__SWITCH__) - settings.display.uiScale = std::max(1.f, settings.display.dpi / 100.f * 0.75f); - // Limit scaling on small low-res screens - if (settings.display.width <= 640 || settings.display.height <= 480) - settings.display.uiScale = std::min(1.4f, settings.display.uiScale); -#endif - settings.display.uiScale *= config::UIScaling / 100.f; - if (settings.display.uiScale == uiScale && ImGui::GetIO().Fonts->IsBuilt()) - return; - uiScale = settings.display.uiScale; - - // Setup Dear ImGui style - ImGui::GetStyle() = ImGuiStyle{}; - ImGui::StyleColorsDark(); - ImGui::GetStyle().TabRounding = 0; - ImGui::GetStyle().ItemSpacing = ImVec2(8, 8); // from 8,4 - ImGui::GetStyle().ItemInnerSpacing = ImVec2(4, 6); // from 4,4 -#if defined(__ANDROID__) || defined(TARGET_IPHONE) || defined(__SWITCH__) - ImGui::GetStyle().TouchExtraPadding = ImVec2(1, 1); // from 0,0 -#endif - if (settings.display.uiScale > 1) - ImGui::GetStyle().ScaleAllSizes(settings.display.uiScale); - - static const ImWchar ranges[] = - { - 0x0020, 0xFFFF, // All chars - 0, - }; - - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->Clear(); - const float fontSize = 17.f * settings.display.uiScale; - size_t dataSize; - std::unique_ptr data = resource::load("fonts/Roboto-Medium.ttf", dataSize); - verify(data != nullptr); - io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, fontSize, nullptr, ranges); - ImFontConfig font_cfg; - font_cfg.MergeMode = true; -#ifdef _WIN32 - u32 cp = GetACP(); - std::string fontDir = std::string(nowide::getenv("SYSTEMROOT")) + "\\Fonts\\"; - switch (cp) - { - case 932: // Japanese - { - font_cfg.FontNo = 2; // UIGothic - ImFont* font = io.Fonts->AddFontFromFileTTF((fontDir + "msgothic.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesJapanese()); - font_cfg.FontNo = 2; // Meiryo UI - if (font == nullptr) - io.Fonts->AddFontFromFileTTF((fontDir + "Meiryo.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesJapanese()); - } - break; - case 949: // Korean - { - ImFont* font = io.Fonts->AddFontFromFileTTF((fontDir + "Malgun.ttf").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesKorean()); - if (font == nullptr) - { - font_cfg.FontNo = 2; // Dotum - io.Fonts->AddFontFromFileTTF((fontDir + "Gulim.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesKorean()); - } - } - break; - case 950: // Traditional Chinese - { - font_cfg.FontNo = 1; // Microsoft JhengHei UI Regular - ImFont* font = io.Fonts->AddFontFromFileTTF((fontDir + "Msjh.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseTraditionalOfficial()); - font_cfg.FontNo = 0; - if (font == nullptr) - io.Fonts->AddFontFromFileTTF((fontDir + "MSJH.ttf").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseTraditionalOfficial()); - } - break; - case 936: // Simplified Chinese - io.Fonts->AddFontFromFileTTF((fontDir + "Simsun.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseSimplifiedOfficial()); - break; - default: - break; - } -#elif defined(__APPLE__) && !defined(TARGET_IPHONE) - std::string fontDir = std::string("/System/Library/Fonts/"); - - extern std::string os_Locale(); - std::string locale = os_Locale(); - - if (locale.find("ja") == 0) // Japanese - { - io.Fonts->AddFontFromFileTTF((fontDir + "ヒラギノ角ゴシック W4.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesJapanese()); - } - else if (locale.find("ko") == 0) // Korean - { - io.Fonts->AddFontFromFileTTF((fontDir + "AppleSDGothicNeo.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesKorean()); - } - else if (locale.find("zh-Hant") == 0) // Traditional Chinese - { - io.Fonts->AddFontFromFileTTF((fontDir + "PingFang.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseTraditionalOfficial()); - } - else if (locale.find("zh-Hans") == 0) // Simplified Chinese - { - io.Fonts->AddFontFromFileTTF((fontDir + "PingFang.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseSimplifiedOfficial()); - } -#elif defined(__ANDROID__) - if (getenv("FLYCAST_LOCALE") != nullptr) - { - const ImWchar *glyphRanges = nullptr; - std::string locale = getenv("FLYCAST_LOCALE"); - if (locale.find("ja") == 0) // Japanese - glyphRanges = io.Fonts->GetGlyphRangesJapanese(); - else if (locale.find("ko") == 0) // Korean - glyphRanges = io.Fonts->GetGlyphRangesKorean(); - else if (locale.find("zh_TW") == 0 - || locale.find("zh_HK") == 0) // Traditional Chinese - glyphRanges = GetGlyphRangesChineseTraditionalOfficial(); - else if (locale.find("zh_CN") == 0) // Simplified Chinese - glyphRanges = GetGlyphRangesChineseSimplifiedOfficial(); - - if (glyphRanges != nullptr) - io.Fonts->AddFontFromFileTTF("/system/fonts/NotoSansCJK-Regular.ttc", fontSize, &font_cfg, glyphRanges); - } - - // TODO Linux, iOS, ... -#endif - NOTICE_LOG(RENDERER, "Screen DPI is %.0f, size %d x %d. Scaling by %.2f", settings.display.dpi, settings.display.width, settings.display.height, settings.display.uiScale); -} - -void gui_keyboard_input(u16 wc) -{ - ImGuiIO& io = ImGui::GetIO(); - if (io.WantCaptureKeyboard) - io.AddInputCharacter(wc); -} - -void gui_keyboard_inputUTF8(const std::string& s) -{ - ImGuiIO& io = ImGui::GetIO(); - if (io.WantCaptureKeyboard) - io.AddInputCharactersUTF8(s.c_str()); -} - -void gui_keyboard_key(u8 keyCode, bool pressed) -{ - if (!inited) - return; - ImGuiKey key = keycodeToImGuiKey(keyCode); - if (key == ImGuiKey_None) - return; - if (!pressed && ImGui::IsKeyDown(key)) - { - keysUpNextFrame[keyCode] = true; - return; - } - ImGuiIO& io = ImGui::GetIO(); - io.AddKeyEvent(key, pressed); -} - -bool gui_keyboard_captured() -{ - ImGuiIO& io = ImGui::GetIO(); - return io.WantCaptureKeyboard; -} - -bool gui_mouse_captured() -{ - ImGuiIO& io = ImGui::GetIO(); - return io.WantCaptureMouse; -} - -void gui_set_mouse_position(int x, int y) -{ - mouseX = std::round(x * settings.display.pointScale); - mouseY = std::round(y * settings.display.pointScale); -} - -void gui_set_mouse_button(int button, bool pressed) -{ - if (pressed) - mouseButtons |= 1 << button; - else - mouseButtons &= ~(1 << button); -} - -void gui_set_mouse_wheel(float delta) -{ - mouseWheel += delta; -} - -static void gui_newFrame() -{ - imguiDriver->newFrame(); - ImGui::GetIO().DisplaySize.x = settings.display.width; - ImGui::GetIO().DisplaySize.y = settings.display.height; - - ImGuiIO& io = ImGui::GetIO(); - - if (mouseX < 0 || mouseX >= settings.display.width || mouseY < 0 || mouseY >= settings.display.height) - io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); - else - io.AddMousePosEvent(mouseX, mouseY); - static bool delayTouch; -#if defined(__ANDROID__) || defined(TARGET_IPHONE) || defined(__SWITCH__) - // Delay touch by one frame to allow widgets to be hovered before click - // This is required for widgets using ImGuiButtonFlags_AllowItemOverlap such as TabItem's - if (!delayTouch && (mouseButtons & (1 << 0)) != 0 && !io.MouseDown[ImGuiMouseButton_Left]) - delayTouch = true; - else - delayTouch = false; -#endif - if (io.WantCaptureMouse) - { - io.AddMouseWheelEvent(0, -mouseWheel / 16); - mouseWheel = 0; - } - if (!delayTouch) - io.AddMouseButtonEvent(ImGuiMouseButton_Left, (mouseButtons & (1 << 0)) != 0); - io.AddMouseButtonEvent(ImGuiMouseButton_Right, (mouseButtons & (1 << 1)) != 0); - io.AddMouseButtonEvent(ImGuiMouseButton_Middle, (mouseButtons & (1 << 2)) != 0); - io.AddMouseButtonEvent(3, (mouseButtons & (1 << 3)) != 0); - - // shows a popup navigation window even in game because of the OSD - //io.AddKeyEvent(ImGuiKey_GamepadFaceLeft, ((kcode[0] & DC_BTN_X) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadFaceRight, ((kcode[0] & DC_BTN_B) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadFaceUp, ((kcode[0] & DC_BTN_Y) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadFaceDown, ((kcode[0] & DC_BTN_A) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadDpadLeft, ((kcode[0] & DC_DPAD_LEFT) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadDpadRight, ((kcode[0] & DC_DPAD_RIGHT) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadDpadUp, ((kcode[0] & DC_DPAD_UP) == 0)); - io.AddKeyEvent(ImGuiKey_GamepadDpadDown, ((kcode[0] & DC_DPAD_DOWN) == 0)); - - float analog; - analog = joyx[0] < 0 ? -(float)joyx[0] / 32768.f : 0.f; - io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickLeft, analog > 0.1f, analog); - analog = joyx[0] > 0 ? (float)joyx[0] / 32768.f : 0.f; - io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickRight, analog > 0.1f, analog); - analog = joyy[0] < 0 ? -(float)joyy[0] / 32768.f : 0.f; - io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickUp, analog > 0.1f, analog); - analog = joyy[0] > 0 ? (float)joyy[0] / 32768.f : 0.f; - io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickDown, analog > 0.1f, analog); - - ImGui::GetStyle().Colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); - - if (showOnScreenKeyboard != nullptr) - showOnScreenKeyboard(io.WantTextInput); -#ifdef USE_SDL - else - { - if (io.WantTextInput && !SDL_IsTextInputActive()) - { - SDL_StartTextInput(); - } - else if (!io.WantTextInput && SDL_IsTextInputActive()) - { - SDL_StopTextInput(); - } - } -#endif -} - -static void delayedKeysUp() -{ - ImGuiIO& io = ImGui::GetIO(); - for (u32 i = 0; i < std::size(keysUpNextFrame); i++) - if (keysUpNextFrame[i]) - io.AddKeyEvent(keycodeToImGuiKey(i), false); - memset(keysUpNextFrame, 0, sizeof(keysUpNextFrame)); -} - -static void gui_endFrame(bool gui_open) -{ - ImGui::Render(); - imguiDriver->renderDrawData(ImGui::GetDrawData(), gui_open); - delayedKeysUp(); -} - -void gui_setOnScreenKeyboardCallback(void (*callback)(bool show)) -{ - showOnScreenKeyboard = callback; -} - -void gui_set_insets(int left, int right, int top, int bottom) -{ - insetLeft = left; - insetRight = right; - insetTop = top; - insetBottom = bottom; -} - -#if 0 -#include "oslib/timeseries.h" -#include -TimeSeries renderTimes; -TimeSeries vblankTimes; - -void gui_plot_render_time(int width, int height) -{ - std::vector v = renderTimes.data(); - ImGui::PlotLines("Render Times", v.data(), v.size(), 0, "", 0.0, 1.0 / 30.0, ImVec2(300, 50)); - ImGui::Text("StdDev: %.1f%%", renderTimes.stddev() * 100.f / 0.01666666667f); - v = vblankTimes.data(); - ImGui::PlotLines("VBlank", v.data(), v.size(), 0, "", 0.0, 1.0 / 30.0, ImVec2(300, 50)); - ImGui::Text("StdDev: %.1f%%", vblankTimes.stddev() * 100.f / 0.01666666667f); -} -#endif - -void gui_open_settings() -{ - const LockGuard lock(guiMutex); - if (gui_state == GuiState::Closed && !settings.naomi.slave) - { - if (!ggpo::active()) - { - HideOSD(); - try { - emu.stop(); - gui_setState(GuiState::Commands); - } catch (const FlycastException& e) { - gui_stop_game(e.what()); - } - } - else - { - chat.toggle(); - } - } - else if (gui_state == GuiState::VJoyEdit) - { - gui_setState(GuiState::VJoyEditCommands); - } - else if (gui_state == GuiState::Loading) - { - gameLoader.cancel(); - } - else if (gui_state == GuiState::Commands) - { - gui_setState(GuiState::Closed); - GamepadDevice::load_system_mappings(); - emu.start(); - } -} - -void gui_start_game(const std::string& path) -{ - const LockGuard lock(guiMutex); - if (gui_state != GuiState::Main && gui_state != GuiState::Closed && gui_state != GuiState::Commands) - return; - emu.unloadGame(); - reset_vmus(); - chat.reset(); - - scanner.stop(); - gui_setState(GuiState::Loading); - gameLoader.load(path); -} - -void gui_stop_game(const std::string& message) -{ - const LockGuard lock(guiMutex); - if (!commandLineStart) - { - // Exit to main menu - emu.unloadGame(); - gui_setState(GuiState::Main); - reset_vmus(); - if (!message.empty()) - gui_error("Flycast has stopped.\n\n" + message); - } - else - { - if (!message.empty()) - ERROR_LOG(COMMON, "Flycast has stopped: %s", message.c_str()); - // Exit emulator - dc_exit(); - } -} - -static bool savestateAllowed() -{ - return !settings.content.path.empty() && !settings.network.online && !settings.naomi.multiboard; -} - -static void gui_display_commands() -{ - imguiDriver->displayVmus(); - - centerNextWindow(); - ImGui::SetNextWindowSize(ScaledVec2(330, 0)); - - ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); - - { - if (card_reader::barcodeAvailable()) - { - char cardBuf[64] {}; - strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1); - if (ImGui::InputText("Card", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr)) - card_reader::barcodeSetCard(cardBuf); - } - - DisabledScope scope(!savestateAllowed()); - - // Load State - if (ImGui::Button("Load State", ScaledVec2(110, 50)) && savestateAllowed()) - { - gui_setState(GuiState::Closed); - dc_loadstate(config::SavestateSlot); - } - ImGui::SameLine(); - - // Slot # - std::string slot = "Slot " + std::to_string((int)config::SavestateSlot + 1); - if (ImGui::Button(slot.c_str(), ImVec2(80 * settings.display.uiScale - ImGui::GetStyle().FramePadding.x, 50 * settings.display.uiScale))) - ImGui::OpenPopup("slot_select_popup"); - if (ImGui::BeginPopup("slot_select_popup")) - { - for (int i = 0; i < 10; i++) - if (ImGui::Selectable(std::to_string(i + 1).c_str(), config::SavestateSlot == i, 0, - ImVec2(ImGui::CalcTextSize("Slot 8").x, 0))) { - config::SavestateSlot = i; - SaveSettings(); - } - ImGui::EndPopup(); - } - ImGui::SameLine(); - - // Save State - if (ImGui::Button("Save State", ScaledVec2(110, 50)) && savestateAllowed()) - { - gui_setState(GuiState::Closed); - dc_savestate(config::SavestateSlot); - } - } - - ImGui::Columns(2, "buttons", false); - - // Settings - if (ImGui::Button("Settings", ScaledVec2(150, 50))) - { - gui_setState(GuiState::Settings); - } - ImGui::NextColumn(); - if (ImGui::Button("Resume", ScaledVec2(150, 50))) - { - GamepadDevice::load_system_mappings(); - gui_setState(GuiState::Closed); - } - - ImGui::NextColumn(); - - // Insert/Eject Disk - const char *disk_label = libGDR_GetDiscType() == Open ? "Insert Disk" : "Eject Disk"; - if (ImGui::Button(disk_label, ScaledVec2(150, 50))) - { - if (libGDR_GetDiscType() == Open) - { - gui_setState(GuiState::SelectDisk); - } - else - { - DiscOpenLid(); - gui_setState(GuiState::Closed); - } - } - ImGui::NextColumn(); - - // Cheats - { - DisabledScope scope(settings.network.online); - - if (ImGui::Button("Cheats", ScaledVec2(150, 50)) && !settings.network.online) - gui_setState(GuiState::Cheats); - } - ImGui::Columns(1, nullptr, false); - - // Exit - if (ImGui::Button(commandLineStart ? "Exit" : "Close Game", ScaledVec2(300, 50) - + ImVec2(ImGui::GetStyle().ColumnsMinSpacing + ImGui::GetStyle().FramePadding.x * 2 - 1, 0))) - { - gui_stop_game(); - } - - ImGui::End(); -} - -inline static void header(const char *title) -{ - ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)); // Left - ImGui::PushStyleVar(ImGuiStyleVar_DisabledAlpha, 1.0f); - ImGui::BeginDisabled(); - ImGui::ButtonEx(title, ImVec2(-1, 0)); - ImGui::EndDisabled(); - ImGui::PopStyleVar(); - ImGui::PopStyleVar(); -} - -const char *maple_device_types[] = -{ - "None", - "Sega Controller", - "Light Gun", - "Keyboard", - "Mouse", - "Twin Stick", - "Arcade/Ascii Stick", - "Maracas Controller", - "Fishing Controller", - "Pop'n Music controller", - "Racing Controller", - "Densha de Go! Controller", -// "Dreameye", -}; - -const char *maple_expansion_device_types[] = -{ - "None", - "Sega VMU", - "Vibration Pack", - "Microphone", -}; - -static const char *maple_device_name(MapleDeviceType type) -{ - switch (type) - { - case MDT_SegaController: - return maple_device_types[1]; - case MDT_LightGun: - return maple_device_types[2]; - case MDT_Keyboard: - return maple_device_types[3]; - case MDT_Mouse: - return maple_device_types[4]; - case MDT_TwinStick: - return maple_device_types[5]; - case MDT_AsciiStick: - return maple_device_types[6]; - case MDT_MaracasController: - return maple_device_types[7]; - case MDT_FishingController: - return maple_device_types[8]; - case MDT_PopnMusicController: - return maple_device_types[9]; - case MDT_RacingController: - return maple_device_types[10]; - case MDT_DenshaDeGoController: - return maple_device_types[11]; - case MDT_Dreameye: -// return maple_device_types[12]; - case MDT_None: - default: - return maple_device_types[0]; - } -} - -static MapleDeviceType maple_device_type_from_index(int idx) -{ - switch (idx) - { - case 1: - return MDT_SegaController; - case 2: - return MDT_LightGun; - case 3: - return MDT_Keyboard; - case 4: - return MDT_Mouse; - case 5: - return MDT_TwinStick; - case 6: - return MDT_AsciiStick; - case 7: - return MDT_MaracasController; - case 8: - return MDT_FishingController; - case 9: - return MDT_PopnMusicController; - case 10: - return MDT_RacingController; - case 11: - return MDT_DenshaDeGoController; - case 12: - return MDT_Dreameye; - case 0: - default: - return MDT_None; - } -} - -static const char *maple_expansion_device_name(MapleDeviceType type) -{ - switch (type) - { - case MDT_SegaVMU: - return maple_expansion_device_types[1]; - case MDT_PurupuruPack: - return maple_expansion_device_types[2]; - case MDT_Microphone: - return maple_expansion_device_types[3]; - case MDT_None: - default: - return maple_expansion_device_types[0]; - } -} - -const char *maple_ports[] = { "None", "A", "B", "C", "D", "All" }; - -struct Mapping { - DreamcastKey key; - const char *name; -}; - -const Mapping dcButtons[] = { - { EMU_BTN_NONE, "Directions" }, - { DC_DPAD_UP, "Up" }, - { DC_DPAD_DOWN, "Down" }, - { DC_DPAD_LEFT, "Left" }, - { DC_DPAD_RIGHT, "Right" }, - - { DC_AXIS_UP, "Thumbstick Up" }, - { DC_AXIS_DOWN, "Thumbstick Down" }, - { DC_AXIS_LEFT, "Thumbstick Left" }, - { DC_AXIS_RIGHT, "Thumbstick Right" }, - - { DC_AXIS2_UP, "R.Thumbstick Up" }, - { DC_AXIS2_DOWN, "R.Thumbstick Down" }, - { DC_AXIS2_LEFT, "R.Thumbstick Left" }, - { DC_AXIS2_RIGHT, "R.Thumbstick Right" }, - - { DC_AXIS3_UP, "Axis 3 Up" }, - { DC_AXIS3_DOWN, "Axis 3 Down" }, - { DC_AXIS3_LEFT, "Axis 3 Left" }, - { DC_AXIS3_RIGHT, "Axis 3 Right" }, - - { DC_DPAD2_UP, "DPad2 Up" }, - { DC_DPAD2_DOWN, "DPad2 Down" }, - { DC_DPAD2_LEFT, "DPad2 Left" }, - { DC_DPAD2_RIGHT, "DPad2 Right" }, - - { EMU_BTN_NONE, "Buttons" }, - { DC_BTN_A, "A" }, - { DC_BTN_B, "B" }, - { DC_BTN_X, "X" }, - { DC_BTN_Y, "Y" }, - { DC_BTN_C, "C" }, - { DC_BTN_D, "D" }, - { DC_BTN_Z, "Z" }, - - { EMU_BTN_NONE, "Triggers" }, - { DC_AXIS_LT, "Left Trigger" }, - { DC_AXIS_RT, "Right Trigger" }, - { DC_AXIS_LT2, "Left Trigger 2" }, - { DC_AXIS_RT2, "Right Trigger 2" }, - - { EMU_BTN_NONE, "System Buttons" }, - { DC_BTN_START, "Start" }, - { DC_BTN_RELOAD, "Reload" }, - - { EMU_BTN_NONE, "Emulator" }, - { EMU_BTN_MENU, "Menu" }, - { EMU_BTN_ESCAPE, "Exit" }, - { EMU_BTN_FFORWARD, "Fast-forward" }, - { EMU_BTN_LOADSTATE, "Load State" }, - { EMU_BTN_SAVESTATE, "Save State" }, - { EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" }, - - { EMU_BTN_NONE, nullptr } -}; - -const Mapping arcadeButtons[] = { - { EMU_BTN_NONE, "Directions" }, - { DC_DPAD_UP, "Up" }, - { DC_DPAD_DOWN, "Down" }, - { DC_DPAD_LEFT, "Left" }, - { DC_DPAD_RIGHT, "Right" }, - - { DC_AXIS_UP, "Thumbstick Up" }, - { DC_AXIS_DOWN, "Thumbstick Down" }, - { DC_AXIS_LEFT, "Thumbstick Left" }, - { DC_AXIS_RIGHT, "Thumbstick Right" }, - - { DC_AXIS2_UP, "R.Thumbstick Up" }, - { DC_AXIS2_DOWN, "R.Thumbstick Down" }, - { DC_AXIS2_LEFT, "R.Thumbstick Left" }, - { DC_AXIS2_RIGHT, "R.Thumbstick Right" }, - - { EMU_BTN_NONE, "Buttons" }, - { DC_BTN_A, "Button 1" }, - { DC_BTN_B, "Button 2" }, - { DC_BTN_C, "Button 3" }, - { DC_BTN_X, "Button 4" }, - { DC_BTN_Y, "Button 5" }, - { DC_BTN_Z, "Button 6" }, - { DC_DPAD2_LEFT, "Button 7" }, - { DC_DPAD2_RIGHT, "Button 8" }, -// { DC_DPAD2_RIGHT, "Button 9" }, // TODO - - { EMU_BTN_NONE, "Triggers" }, - { DC_AXIS_LT, "Left Trigger" }, - { DC_AXIS_RT, "Right Trigger" }, - { DC_AXIS_LT2, "Left Trigger 2" }, - { DC_AXIS_RT2, "Right Trigger 2" }, - - { EMU_BTN_NONE, "System Buttons" }, - { DC_BTN_START, "Start" }, - { DC_BTN_RELOAD, "Reload" }, - { DC_BTN_D, "Coin" }, - { DC_DPAD2_UP, "Service" }, - { DC_DPAD2_DOWN, "Test" }, - { DC_BTN_INSERT_CARD, "Insert Card" }, - - { EMU_BTN_NONE, "Emulator" }, - { EMU_BTN_MENU, "Menu" }, - { EMU_BTN_ESCAPE, "Exit" }, - { EMU_BTN_FFORWARD, "Fast-forward" }, - { EMU_BTN_LOADSTATE, "Load State" }, - { EMU_BTN_SAVESTATE, "Save State" }, - { EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" }, - - { EMU_BTN_NONE, nullptr } -}; - -static MapleDeviceType maple_expansion_device_type_from_index(int idx) -{ - switch (idx) - { - case 1: - return MDT_SegaVMU; - case 2: - return MDT_PurupuruPack; - case 3: - return MDT_Microphone; - case 0: - default: - return MDT_None; - } -} - -static std::shared_ptr mapped_device; -static u32 mapped_code; -static bool analogAxis; -static bool positiveDirection; -static double map_start_time; -static bool arcade_button_mode; -static u32 gamepad_port; - -static void unmapControl(const std::shared_ptr& mapping, u32 gamepad_port, DreamcastKey key) -{ - mapping->clear_button(gamepad_port, key); - mapping->clear_axis(gamepad_port, key); -} - -static DreamcastKey getOppositeDirectionKey(DreamcastKey key) -{ - switch (key) - { - case DC_DPAD_UP: - return DC_DPAD_DOWN; - case DC_DPAD_DOWN: - return DC_DPAD_UP; - case DC_DPAD_LEFT: - return DC_DPAD_RIGHT; - case DC_DPAD_RIGHT: - return DC_DPAD_LEFT; - case DC_DPAD2_UP: - return DC_DPAD2_DOWN; - case DC_DPAD2_DOWN: - return DC_DPAD2_UP; - case DC_DPAD2_LEFT: - return DC_DPAD2_RIGHT; - case DC_DPAD2_RIGHT: - return DC_DPAD2_LEFT; - case DC_AXIS_UP: - return DC_AXIS_DOWN; - case DC_AXIS_DOWN: - return DC_AXIS_UP; - case DC_AXIS_LEFT: - return DC_AXIS_RIGHT; - case DC_AXIS_RIGHT: - return DC_AXIS_LEFT; - case DC_AXIS2_UP: - return DC_AXIS2_DOWN; - case DC_AXIS2_DOWN: - return DC_AXIS2_UP; - case DC_AXIS2_LEFT: - return DC_AXIS2_RIGHT; - case DC_AXIS2_RIGHT: - return DC_AXIS2_LEFT; - case DC_AXIS3_UP: - return DC_AXIS3_DOWN; - case DC_AXIS3_DOWN: - return DC_AXIS3_UP; - case DC_AXIS3_LEFT: - return DC_AXIS3_RIGHT; - case DC_AXIS3_RIGHT: - return DC_AXIS3_LEFT; - default: - return EMU_BTN_NONE; - } -} -static void detect_input_popup(const Mapping *mapping) -{ - ImVec2 padding = ScaledVec2(20, 20); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, padding); - if (ImGui::BeginPopupModal("Map Control", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) - { - ImGui::Text("Waiting for control '%s'...", mapping->name); - double now = os_GetSeconds(); - ImGui::Text("Time out in %d s", (int)(5 - (now - map_start_time))); - if (mapped_code != (u32)-1) - { - std::shared_ptr input_mapping = mapped_device->get_input_mapping(); - if (input_mapping != NULL) - { - unmapControl(input_mapping, gamepad_port, mapping->key); - if (analogAxis) - { - input_mapping->set_axis(gamepad_port, mapping->key, mapped_code, positiveDirection); - DreamcastKey opposite = getOppositeDirectionKey(mapping->key); - // Map the axis opposite direction to the corresponding opposite dc button or axis, - // but only if the opposite direction axis isn't used and the dc button or axis isn't mapped. - if (opposite != EMU_BTN_NONE - && input_mapping->get_axis_id(gamepad_port, mapped_code, !positiveDirection) == EMU_BTN_NONE - && input_mapping->get_axis_code(gamepad_port, opposite).first == (u32)-1 - && input_mapping->get_button_code(gamepad_port, opposite) == (u32)-1) - input_mapping->set_axis(gamepad_port, opposite, mapped_code, !positiveDirection); - } - else - input_mapping->set_button(gamepad_port, mapping->key, mapped_code); - } - mapped_device = NULL; - ImGui::CloseCurrentPopup(); - } - else if (now - map_start_time >= 5) - { - mapped_device = NULL; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - ImGui::PopStyleVar(2); -} - -static void displayLabelOrCode(const char *label, u32 code, const char *suffix = "") -{ - if (label != nullptr) - ImGui::Text("%s%s", label, suffix); - else - ImGui::Text("[%d]%s", code, suffix); -} - -static void displayMappedControl(const std::shared_ptr& gamepad, DreamcastKey key) -{ - std::shared_ptr input_mapping = gamepad->get_input_mapping(); - u32 code = input_mapping->get_button_code(gamepad_port, key); - if (code != (u32)-1) - { - displayLabelOrCode(gamepad->get_button_name(code), code); - return; - } - std::pair pair = input_mapping->get_axis_code(gamepad_port, key); - code = pair.first; - if (code != (u32)-1) - { - displayLabelOrCode(gamepad->get_axis_name(code), code, pair.second ? "+" : "-"); - return; - } -} - -static void controller_mapping_popup(const std::shared_ptr& gamepad) -{ - fullScreenWindow(true); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - if (ImGui::BeginPopupModal("Controller Mapping", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) - { - const ImGuiStyle& style = ImGui::GetStyle(); - const float winWidth = ImGui::GetIO().DisplaySize.x - insetLeft - insetRight - (style.WindowBorderSize + style.WindowPadding.x) * 2; - const float col_width = (winWidth - style.GrabMinSize - style.ItemSpacing.x - - (ImGui::CalcTextSize("Map").x + style.FramePadding.x * 2.0f + style.ItemSpacing.x) - - (ImGui::CalcTextSize("Unmap").x + style.FramePadding.x * 2.0f + style.ItemSpacing.x)) / 2; - const float scaling = settings.display.uiScale; - - static int map_system; - static int item_current_map_idx = 0; - static int last_item_current_map_idx = 2; - - std::shared_ptr input_mapping = gamepad->get_input_mapping(); - if (input_mapping == NULL || ImGui::Button("Done", ScaledVec2(100, 30))) - { - ImGui::CloseCurrentPopup(); - gamepad->save_mapping(map_system); - last_item_current_map_idx = 2; - ImGui::EndPopup(); - ImGui::PopStyleVar(); - return; - } - ImGui::SetItemDefaultFocus(); - - float portWidth = 0; - if (gamepad->maple_port() == MAPLE_PORTS) - { - ImGui::SameLine(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, (30 * scaling - ImGui::GetFontSize()) / 2)); - portWidth = ImGui::CalcTextSize("AA").x + ImGui::GetStyle().ItemSpacing.x * 2.0f + ImGui::GetFontSize(); - ImGui::SetNextItemWidth(portWidth); - if (ImGui::BeginCombo("Port", maple_ports[gamepad_port + 1])) - { - for (u32 j = 0; j < MAPLE_PORTS; j++) - { - bool is_selected = gamepad_port == j; - if (ImGui::Selectable(maple_ports[j + 1], &is_selected)) - gamepad_port = j; - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - portWidth += ImGui::CalcTextSize("Port").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x; - ImGui::PopStyleVar(); - } - float comboWidth = ImGui::CalcTextSize("Dreamcast Controls").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.x * 4; - float gameConfigWidth = 0; - if (!settings.content.gameId.empty()) - gameConfigWidth = ImGui::CalcTextSize(gamepad->isPerGameMapping() ? "Delete Game Config" : "Make Game Config").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x * 2; - ImGui::SameLine(0, ImGui::GetContentRegionAvail().x - comboWidth - gameConfigWidth - ImGui::GetStyle().ItemSpacing.x - 100 * scaling * 2 - portWidth); - - ImGui::AlignTextToFramePadding(); - - if (!settings.content.gameId.empty()) - { - if (gamepad->isPerGameMapping()) - { - if (ImGui::Button("Delete Game Config", ScaledVec2(0, 30))) - { - gamepad->setPerGameMapping(false); - if (!gamepad->find_mapping(map_system)) - gamepad->resetMappingToDefault(arcade_button_mode, true); - } - } - else - { - if (ImGui::Button("Make Game Config", ScaledVec2(0, 30))) - gamepad->setPerGameMapping(true); - } - ImGui::SameLine(); - } - if (ImGui::Button("Reset...", ScaledVec2(100, 30))) - ImGui::OpenPopup("Confirm Reset"); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ScaledVec2(20, 20)); - if (ImGui::BeginPopupModal("Confirm Reset", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) - { - ImGui::Text("Are you sure you want to reset the mappings to default?"); - static bool hitbox; - if (arcade_button_mode) - { - ImGui::Text("Controller Type:"); - if (ImGui::RadioButton("Gamepad", !hitbox)) - hitbox = false; - ImGui::SameLine(); - if (ImGui::RadioButton("Arcade / Hit Box", hitbox)) - hitbox = true; - } - ImGui::NewLine(); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20 * scaling, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(10, 10)); - if (ImGui::Button("Yes")) - { - gamepad->resetMappingToDefault(arcade_button_mode, !hitbox); - gamepad->save_mapping(map_system); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("No")) - ImGui::CloseCurrentPopup(); - ImGui::PopStyleVar(2); - - ImGui::EndPopup(); - } - ImGui::PopStyleVar(1); - - ImGui::SameLine(); - - const char* items[] = { "Dreamcast Controls", "Arcade Controls" }; - - if (last_item_current_map_idx == 2 && game_started) - // Select the right mappings for the current game - item_current_map_idx = settings.platform.isArcade() ? 1 : 0; - - // Here our selection data is an index. - - ImGui::SetNextItemWidth(comboWidth); - // Make the combo height the same as the Done and Reset buttons - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, (30 * scaling - ImGui::GetFontSize()) / 2)); - ImGui::Combo("##arcadeMode", &item_current_map_idx, items, IM_ARRAYSIZE(items)); - ImGui::PopStyleVar(); - if (last_item_current_map_idx != 2 && item_current_map_idx != last_item_current_map_idx) - { - gamepad->save_mapping(map_system); - } - const Mapping *systemMapping = dcButtons; - if (item_current_map_idx == 0) - { - arcade_button_mode = false; - map_system = DC_PLATFORM_DREAMCAST; - systemMapping = dcButtons; - } - else if (item_current_map_idx == 1) - { - arcade_button_mode = true; - map_system = DC_PLATFORM_NAOMI; - systemMapping = arcadeButtons; - } - - if (item_current_map_idx != last_item_current_map_idx) - { - if (!gamepad->find_mapping(map_system)) - if (map_system == DC_PLATFORM_DREAMCAST || !gamepad->find_mapping(DC_PLATFORM_DREAMCAST)) - gamepad->resetMappingToDefault(arcade_button_mode, true); - input_mapping = gamepad->get_input_mapping(); - - last_item_current_map_idx = item_current_map_idx; - } - - char key_id[32]; - - ImGui::BeginChild(ImGui::GetID("buttons"), ImVec2(0, 0), ImGuiChildFlags_FrameStyle, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); - - for (; systemMapping->name != nullptr; systemMapping++) - { - if (systemMapping->key == EMU_BTN_NONE) - { - ImGui::Columns(1, nullptr, false); - header(systemMapping->name); - ImGui::Columns(3, "bindings", false); - ImGui::SetColumnWidth(0, col_width); - ImGui::SetColumnWidth(1, col_width); - continue; - } - sprintf(key_id, "key_id%d", systemMapping->key); - ImGui::PushID(key_id); - - const char *game_btn_name = nullptr; - if (arcade_button_mode) - { - game_btn_name = GetCurrentGameButtonName(systemMapping->key); - if (game_btn_name == nullptr) - game_btn_name = GetCurrentGameAxisName(systemMapping->key); - } - if (game_btn_name != nullptr && game_btn_name[0] != '\0') - ImGui::Text("%s - %s", systemMapping->name, game_btn_name); - else - ImGui::Text("%s", systemMapping->name); - - ImGui::NextColumn(); - displayMappedControl(gamepad, systemMapping->key); - - ImGui::NextColumn(); - if (ImGui::Button("Map")) - { - map_start_time = os_GetSeconds(); - ImGui::OpenPopup("Map Control"); - mapped_device = gamepad; - mapped_code = -1; - gamepad->detectButtonOrAxisInput([](u32 code, bool analog, bool positive) - { - mapped_code = code; - analogAxis = analog; - positiveDirection = positive; - }); - } - detect_input_popup(systemMapping); - ImGui::SameLine(); - if (ImGui::Button("Unmap")) - { - input_mapping = gamepad->get_input_mapping(); - unmapControl(input_mapping, gamepad_port, systemMapping->key); - } - ImGui::NextColumn(); - ImGui::PopID(); - } - ImGui::Columns(1, nullptr, false); - scrollWhenDraggingOnVoid(); - windowDragScroll(); - - ImGui::EndChild(); - error_popup(); - ImGui::EndPopup(); - } - ImGui::PopStyleVar(); -} - -static void gamepadSettingsPopup(const std::shared_ptr& gamepad) -{ - centerNextWindow(); - ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(450.f, 300.f))); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) - { - if (ImGui::Button("Done", ScaledVec2(100, 30))) - { - gamepad->save_mapping(); - // Update both console and arcade profile/mapping - int rumblePower = gamepad->get_rumble_power(); - float deadzone = gamepad->get_dead_zone(); - float saturation = gamepad->get_saturation(); - int otherPlatform = settings.platform.isConsole() ? DC_PLATFORM_NAOMI : DC_PLATFORM_DREAMCAST; - if (!gamepad->find_mapping(otherPlatform)) - if (otherPlatform == DC_PLATFORM_DREAMCAST || !gamepad->find_mapping(DC_PLATFORM_DREAMCAST)) - gamepad->resetMappingToDefault(otherPlatform != DC_PLATFORM_DREAMCAST, true); - std::shared_ptr mapping = gamepad->get_input_mapping(); - if (mapping != nullptr) - { - if (gamepad->is_rumble_enabled() && rumblePower != mapping->rumblePower) { - mapping->rumblePower = rumblePower; - mapping->set_dirty(); - } - if (gamepad->has_analog_stick()) - { - if (deadzone != mapping->dead_zone) { - mapping->dead_zone = deadzone; - mapping->set_dirty(); - } - if (saturation != mapping->saturation) { - mapping->saturation = saturation; - mapping->set_dirty(); - } - } - if (mapping->is_dirty()) - gamepad->save_mapping(otherPlatform); - } - gamepad->find_mapping(); - - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - ImGui::PopStyleVar(); - return; - } - ImGui::NewLine(); - if (gamepad->is_virtual_gamepad()) - { - header("Haptic"); - OptionSlider("Power", config::VirtualGamepadVibration, 0, 60, "Haptic feedback power"); - } - else if (gamepad->is_rumble_enabled()) - { - header("Rumble"); - int power = gamepad->get_rumble_power(); - ImGui::SetNextItemWidth(300 * settings.display.uiScale); - if (ImGui::SliderInt("Power", &power, 0, 100, "%d%%")) - gamepad->set_rumble_power(power); - ImGui::SameLine(); - ShowHelpMarker("Rumble power"); - } - if (gamepad->has_analog_stick()) - { - header("Thumbsticks"); - int deadzone = std::round(gamepad->get_dead_zone() * 100.f); - ImGui::SetNextItemWidth(300 * settings.display.uiScale); - if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%")) - gamepad->set_dead_zone(deadzone / 100.f); - ImGui::SameLine(); - ShowHelpMarker("Minimum deflection to register as input"); - int saturation = std::round(gamepad->get_saturation() * 100.f); - ImGui::SetNextItemWidth(300 * settings.display.uiScale); - if (ImGui::SliderInt("Saturation", &saturation, 50, 200, "%d%%")) - gamepad->set_saturation(saturation / 100.f); - ImGui::SameLine(); - ShowHelpMarker("Value sent to the game at 100% thumbstick deflection. " - "Values greater than 100% will saturate before full deflection of the thumbstick."); - } - - ImGui::EndPopup(); - } - ImGui::PopStyleVar(); -} - -void error_popup() -{ - if (!error_msg_shown && !error_msg.empty()) - { - ImVec2 padding = ScaledVec2(20, 20); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, padding); - ImGui::OpenPopup("Error"); - if (ImGui::BeginPopupModal("Error", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar)) - { - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 400.f * settings.display.uiScale); - ImGui::TextWrapped("%s", error_msg.c_str()); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(16, 3)); - float currentwidth = ImGui::GetContentRegionAvail().x; - ImGui::SetCursorPosX((currentwidth - 80.f * settings.display.uiScale) / 2.f + ImGui::GetStyle().WindowPadding.x); - if (ImGui::Button("OK", ScaledVec2(80.f, 0))) - { - error_msg.clear(); - ImGui::CloseCurrentPopup(); - } - ImGui::SetItemDefaultFocus(); - ImGui::PopStyleVar(); - ImGui::PopTextWrapPos(); - ImGui::EndPopup(); - } - ImGui::PopStyleVar(); - ImGui::PopStyleVar(); - error_msg_shown = true; - } -} - -static void contentpath_warning_popup() -{ - static bool show_contentpath_selection; - - if (scanner.content_path_looks_incorrect) - { - ImGui::OpenPopup("Incorrect Content Location?"); - if (ImGui::BeginPopupModal("Incorrect Content Location?", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) - { - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 400.f * settings.display.uiScale); - ImGui::TextWrapped(" Scanned %d folders but no game can be found! ", scanner.empty_folders_scanned); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(16, 3)); - float currentwidth = ImGui::GetContentRegionAvail().x; - ImGui::SetCursorPosX((currentwidth - 100.f * settings.display.uiScale) / 2.f + ImGui::GetStyle().WindowPadding.x - 55.f * settings.display.uiScale); - if (ImGui::Button("Reselect", ScaledVec2(100.f, 0))) - { - scanner.content_path_looks_incorrect = false; - ImGui::CloseCurrentPopup(); - show_contentpath_selection = true; - } - - ImGui::SameLine(); - ImGui::SetCursorPosX((currentwidth - 100.f * settings.display.uiScale) / 2.f + ImGui::GetStyle().WindowPadding.x + 55.f * settings.display.uiScale); - if (ImGui::Button("Cancel", ScaledVec2(100.f, 0))) - { - scanner.content_path_looks_incorrect = false; - ImGui::CloseCurrentPopup(); - scanner.stop(); - config::ContentPath.get().clear(); - } - ImGui::SetItemDefaultFocus(); - ImGui::PopStyleVar(); - ImGui::EndPopup(); - } - } - if (show_contentpath_selection) - { - scanner.stop(); - ImGui::OpenPopup("Select Directory"); - select_file_popup("Select Directory", [](bool cancelled, std::string selection) - { - show_contentpath_selection = false; - if (!cancelled) - { - config::ContentPath.get().clear(); - config::ContentPath.get().push_back(selection); - } - scanner.refresh(); - return true; - }); - } -} - -static inline void gui_debug_tab() -{ - if (ImGui::BeginTabItem("Debug")) - { - ImVec2 normal_padding = ImGui::GetStyle().FramePadding; - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - header("Logging"); - { - LogManager *logManager = LogManager::GetInstance(); - for (LogTypes::LOG_TYPE type = LogTypes::AICA; type < LogTypes::NUMBER_OF_LOGS; type = (LogTypes::LOG_TYPE)(type + 1)) - { - bool enabled = logManager->IsEnabled(type, logManager->GetLogLevel()); - std::string name = std::string(logManager->GetShortName(type)) + " - " + logManager->GetFullName(type); - if (ImGui::Checkbox(name.c_str(), &enabled) && logManager->GetLogLevel() > LogTypes::LWARNING) { - logManager->SetEnable(type, enabled); - cfgSaveBool("log", logManager->GetShortName(type), enabled); - } - } - ImGui::Spacing(); - - static const char *levels[] = { "Notice", "Error", "Warning", "Info", "Debug" }; - if (ImGui::BeginCombo("Log Verbosity", levels[logManager->GetLogLevel() - 1], ImGuiComboFlags_None)) - { - for (std::size_t i = 0; i < std::size(levels); i++) - { - bool is_selected = logManager->GetLogLevel() - 1 == (int)i; - if (ImGui::Selectable(levels[i], &is_selected)) { - logManager->SetLogLevel((LogTypes::LOG_LEVELS)(i + 1)); - cfgSaveInt("log", "Verbosity", i + 1); - } - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - } -#if FC_PROFILER - ImGui::Spacing(); - header("Profiling"); - { - - OptionCheckbox("Enable", config::ProfilerEnabled, "Enable the profiler."); - if (!config::ProfilerEnabled) - { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); - } - OptionCheckbox("Display", config::ProfilerDrawToGUI, "Draw the profiler output in an overlay."); - OptionCheckbox("Output to terminal", config::ProfilerOutputTTY, "Write the profiler output to the terminal"); - // TODO frame warning time - if (!config::ProfilerEnabled) - { - ImGui::PopItemFlag(); - ImGui::PopStyleVar(); - } - } -#endif - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } -} - -static void addContentPath(const std::string& path) -{ - auto& contentPath = config::ContentPath.get(); - if (std::count(contentPath.begin(), contentPath.end(), path) == 0) - { - scanner.stop(); - contentPath.push_back(path); - scanner.refresh(); - } -} - -static float calcComboWidth(const char *biggestLabel) { - return ImGui::CalcTextSize(biggestLabel).x + ImGui::GetStyle().FramePadding.x * 2.0f + ImGui::GetFrameHeight(); -} - -static void gui_display_settings() -{ - static bool maple_devices_changed; - - fullScreenWindow(false); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - - ImGui::Begin("Settings", NULL, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NoResize - | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); - ImVec2 normal_padding = ImGui::GetStyle().FramePadding; - - if (ImGui::Button("Done", ScaledVec2(100, 30))) - { - if (uiUserScaleUpdated) - { - uiUserScaleUpdated = false; - mainui_reinit(); - } - if (game_started) - gui_setState(GuiState::Commands); - else - gui_setState(GuiState::Main); - if (maple_devices_changed) - { - maple_devices_changed = false; - if (game_started && settings.platform.isConsole()) - { - maple_ReconnectDevices(); - reset_vmus(); - } - } - SaveSettings(); - } - if (game_started) - { - ImGui::SameLine(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(16 * settings.display.uiScale, normal_padding.y)); - if (config::Settings::instance().hasPerGameConfig()) - { - if (ImGui::Button("Delete Game Config", ScaledVec2(0, 30))) - { - config::Settings::instance().setPerGameConfig(false); - config::Settings::instance().load(false); - loadGameSpecificSettings(); - } - } - else - { - if (ImGui::Button("Make Game Config", ScaledVec2(0, 30))) - config::Settings::instance().setPerGameConfig(true); - } - ImGui::PopStyleVar(); - } - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(16, 6)); - - if (ImGui::BeginTabBar("settings", ImGuiTabBarFlags_NoTooltip)) - { - if (ImGui::BeginTabItem("General")) - { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - { - DisabledScope scope(settings.platform.isArcade()); - - const char *languages[] = { "Japanese", "English", "German", "French", "Spanish", "Italian", "Default" }; - OptionComboBox("Language", config::Language, languages, std::size(languages), - "The language as configured in the Dreamcast BIOS"); - - const char *broadcast[] = { "NTSC", "PAL", "PAL/M", "PAL/N", "Default" }; - OptionComboBox("Broadcast", config::Broadcast, broadcast, std::size(broadcast), - "TV broadcasting standard for non-VGA modes"); - } - - const char *consoleRegion[] = { "Japan", "USA", "Europe", "Default" }; - const char *arcadeRegion[] = { "Japan", "USA", "Export", "Korea" }; - const char **region = settings.platform.isArcade() ? arcadeRegion : consoleRegion; - OptionComboBox("Region", config::Region, region, std::size(consoleRegion), - "BIOS region"); - - const char *cable[] = { "VGA", "RGB Component", "TV Composite" }; - { - DisabledScope scope(config::Cable.isReadOnly() || settings.platform.isArcade()); - - const char *value = config::Cable == 0 ? cable[0] - : config::Cable > 0 && config::Cable <= (int)std::size(cable) ? cable[config::Cable - 1] - : "?"; - if (ImGui::BeginCombo("Cable", value, ImGuiComboFlags_None)) - { - for (int i = 0; i < IM_ARRAYSIZE(cable); i++) - { - bool is_selected = i == 0 ? config::Cable <= 1 : config::Cable - 1 == i; - if (ImGui::Selectable(cable[i], &is_selected)) - config::Cable = i == 0 ? 0 : i + 1; - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - ShowHelpMarker("Video connection type"); - } - -#if !defined(TARGET_IPHONE) - ImVec2 size; - size.x = 0.0f; - size.y = (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().FramePadding.y * 2.f) - * (config::ContentPath.get().size() + 1) ;//+ ImGui::GetStyle().FramePadding.y * 2.f; - - if (BeginListBox("Content Location", size, ImGuiWindowFlags_NavFlattened)) - { - int to_delete = -1; - for (u32 i = 0; i < config::ContentPath.get().size(); i++) - { - ImGui::PushID(config::ContentPath.get()[i].c_str()); - ImGui::AlignTextToFramePadding(); - ImGui::Text("%s", config::ContentPath.get()[i].c_str()); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("X").x - ImGui::GetStyle().FramePadding.x); - if (ImGui::Button("X")) - to_delete = i; - ImGui::PopID(); - } - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(24, 3)); -#ifdef __ANDROID__ - if (ImGui::Button("Add")) - { - hostfs::addStorage(true, false, [](bool cancelled, std::string selection) { - if (!cancelled) - addContentPath(selection); - }); - } -#else - if (ImGui::Button("Add")) - ImGui::OpenPopup("Select Directory"); - select_file_popup("Select Directory", [](bool cancelled, std::string selection) { - if (!cancelled) - addContentPath(selection); - return true; - }); -#endif - ImGui::SameLine(); - if (ImGui::Button("Rescan Content")) - scanner.refresh(); - ImGui::PopStyleVar(); - scrollWhenDraggingOnVoid(); - - ImGui::EndListBox(); - if (to_delete >= 0) - { - scanner.stop(); - config::ContentPath.get().erase(config::ContentPath.get().begin() + to_delete); - scanner.refresh(); - } - } - ImGui::SameLine(); - ShowHelpMarker("The directories where your games are stored"); - - size.y = ImGui::GetTextLineHeightWithSpacing() * 1.25f + ImGui::GetStyle().FramePadding.y * 2.0f; - -#if defined(__linux__) && !defined(__ANDROID__) - if (BeginListBox("Data Directory", size, ImGuiWindowFlags_NavFlattened)) - { - ImGui::AlignTextToFramePadding(); - ImGui::Text("%s", get_writable_data_path("").c_str()); - ImGui::EndListBox(); - } - ImGui::SameLine(); - ShowHelpMarker("The directory containing BIOS files, as well as saved VMUs and states"); -#else - if (BeginListBox("Home Directory", size, ImGuiWindowFlags_NavFlattened)) - { - ImGui::AlignTextToFramePadding(); - ImGui::Text("%s", get_writable_config_path("").c_str()); -#ifdef __ANDROID__ - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Change").x - ImGui::GetStyle().FramePadding.x); - if (ImGui::Button("Change")) - gui_setState(GuiState::Onboarding); -#endif -#ifdef TARGET_MAC - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Reveal in Finder").x - ImGui::GetStyle().FramePadding.x); - if (ImGui::Button("Reveal in Finder")) - { - char temp[512]; - sprintf(temp, "open \"%s\"", get_writable_config_path("").c_str()); - system(temp); - } -#endif - ImGui::EndListBox(); - } - ImGui::SameLine(); - ShowHelpMarker("The directory where Flycast saves configuration files and VMUs. BIOS files should be in a subfolder named \"data\""); -#endif // !linux -#endif // !TARGET_IPHONE - - OptionCheckbox("Box Art Game List", config::BoxartDisplayMode, - "Display game cover art in the game list."); - OptionCheckbox("Fetch Box Art", config::FetchBoxart, - "Fetch cover images from TheGamesDB.net."); - if (OptionSlider("UI Scaling", config::UIScaling, 50, 200, "Adjust the size of UI elements and fonts.", "%d%%")) - uiUserScaleUpdated = true; - if (uiUserScaleUpdated) - { - ImGui::SameLine(); - if (ImGui::Button("Apply")) { - mainui_reinit(); - uiUserScaleUpdated = false; - } - } - - if (OptionCheckbox("Hide Legacy Naomi Roms", config::HideLegacyNaomiRoms, - "Hide .bin, .dat and .lst files from the content browser")) - scanner.refresh(); - ImGui::Text("Automatic State:"); - OptionCheckbox("Load", config::AutoLoadState, - "Load the last saved state of the game when starting"); - ImGui::SameLine(); - OptionCheckbox("Save", config::AutoSaveState, - "Save the state of the game when stopping"); - OptionCheckbox("Naomi Free Play", config::ForceFreePlay, "Configure Naomi games in Free Play mode."); - - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Controls")) - { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - header("Physical Devices"); - { - if (ImGui::BeginTable("physicalDevices", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings)) - { - ImGui::TableSetupColumn("System", ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); - - const float portComboWidth = calcComboWidth("None"); - const ImVec4 gray{ 0.5f, 0.5f, 0.5f, 1.f }; - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(gray, "System"); - - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(gray, "Name"); - - ImGui::TableSetColumnIndex(2); - ImGui::TextColored(gray, "Port"); - - for (int i = 0; i < GamepadDevice::GetGamepadCount(); i++) - { - std::shared_ptr gamepad = GamepadDevice::GetGamepad(i); - if (!gamepad) - continue; - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%s", gamepad->api_name().c_str()); - - ImGui::TableSetColumnIndex(1); - ImGui::Text("%s", gamepad->name().c_str()); - - ImGui::TableSetColumnIndex(2); - char port_name[32]; - sprintf(port_name, "##mapleport%d", i); - ImGui::PushID(port_name); - ImGui::SetNextItemWidth(portComboWidth); - if (ImGui::BeginCombo(port_name, maple_ports[gamepad->maple_port() + 1])) - { - for (int j = -1; j < (int)std::size(maple_ports) - 1; j++) - { - bool is_selected = gamepad->maple_port() == j; - if (ImGui::Selectable(maple_ports[j + 1], &is_selected)) - gamepad->set_maple_port(j); - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - - ImGui::EndCombo(); - } - - ImGui::TableSetColumnIndex(3); - ImGui::SameLine(0, 8 * settings.display.uiScale); - if (gamepad->remappable() && ImGui::Button("Map")) - { - gamepad_port = 0; - ImGui::OpenPopup("Controller Mapping"); - } - - controller_mapping_popup(gamepad); - -#ifdef __ANDROID__ - if (gamepad->is_virtual_gamepad()) - { - if (ImGui::Button("Edit Layout")) - { - vjoy_start_editing(); - gui_setState(GuiState::VJoyEdit); - } - } -#endif - if (gamepad->is_rumble_enabled() || gamepad->has_analog_stick() -#ifdef __ANDROID__ - || gamepad->is_virtual_gamepad() -#endif - ) - { - ImGui::SameLine(0, 16 * settings.display.uiScale); - if (ImGui::Button("Settings")) - ImGui::OpenPopup("Gamepad Settings"); - gamepadSettingsPopup(gamepad); - } - ImGui::PopID(); - } - ImGui::EndTable(); - } - } - - ImGui::Spacing(); - OptionSlider("Mouse sensitivity", config::MouseSensitivity, 1, 500); -#if defined(_WIN32) && !defined(TARGET_UWP) - OptionCheckbox("Use Raw Input", config::UseRawInput, "Supports multiple pointing devices (mice, light guns) and keyboards"); -#endif - - ImGui::Spacing(); - header("Dreamcast Devices"); - { - bool is_there_any_xhair = false; - if (ImGui::BeginTable("dreamcastDevices", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings, - ImVec2(0, 0), 8 * settings.display.uiScale)) - { - const float mainComboWidth = calcComboWidth(maple_device_types[11]); // densha de go! controller - const float expComboWidth = calcComboWidth(maple_expansion_device_types[2]); // vibration pack - - for (int bus = 0; bus < MAPLE_PORTS; bus++) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Port %c", bus + 'A'); - - ImGui::TableSetColumnIndex(1); - char device_name[32]; - sprintf(device_name, "##device%d", bus); - float w = ImGui::CalcItemWidth() / 3; - ImGui::PushItemWidth(w); - ImGui::SetNextItemWidth(mainComboWidth); - if (ImGui::BeginCombo(device_name, maple_device_name(config::MapleMainDevices[bus]), ImGuiComboFlags_None)) - { - for (int i = 0; i < IM_ARRAYSIZE(maple_device_types); i++) - { - bool is_selected = config::MapleMainDevices[bus] == maple_device_type_from_index(i); - if (ImGui::Selectable(maple_device_types[i], &is_selected)) - { - config::MapleMainDevices[bus] = maple_device_type_from_index(i); - maple_devices_changed = true; - } - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - int port_count = 0; - switch (config::MapleMainDevices[bus]) { - case MDT_SegaController: - port_count = 2; - break; - case MDT_LightGun: - case MDT_TwinStick: - case MDT_AsciiStick: - case MDT_RacingController: - port_count = 1; - break; - default: break; - } - for (int port = 0; port < port_count; port++) - { - ImGui::TableSetColumnIndex(2 + port); - sprintf(device_name, "##device%d.%d", bus, port + 1); - ImGui::PushID(device_name); - ImGui::SetNextItemWidth(expComboWidth); - if (ImGui::BeginCombo(device_name, maple_expansion_device_name(config::MapleExpansionDevices[bus][port]), ImGuiComboFlags_None)) - { - for (int i = 0; i < IM_ARRAYSIZE(maple_expansion_device_types); i++) - { - bool is_selected = config::MapleExpansionDevices[bus][port] == maple_expansion_device_type_from_index(i); - if (ImGui::Selectable(maple_expansion_device_types[i], &is_selected)) - { - config::MapleExpansionDevices[bus][port] = maple_expansion_device_type_from_index(i); - maple_devices_changed = true; - } - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::PopID(); - } - if (config::MapleMainDevices[bus] == MDT_LightGun) - { - ImGui::TableSetColumnIndex(3); - sprintf(device_name, "##device%d.xhair", bus); - ImGui::PushID(device_name); - u32 color = config::CrosshairColor[bus]; - float xhairColor[4] { - (color & 0xff) / 255.f, - ((color >> 8) & 0xff) / 255.f, - ((color >> 16) & 0xff) / 255.f, - ((color >> 24) & 0xff) / 255.f - }; - bool colorChanged = ImGui::ColorEdit4("Crosshair color", xhairColor, ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf - | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoLabel); - ImGui::SameLine(); - bool enabled = color != 0; - if (ImGui::Checkbox("Crosshair", &enabled) || colorChanged) - { - if (enabled) - { - config::CrosshairColor[bus] = (u8)(std::round(xhairColor[0] * 255.f)) - | ((u8)(std::round(xhairColor[1] * 255.f)) << 8) - | ((u8)(std::round(xhairColor[2] * 255.f)) << 16) - | ((u8)(std::round(xhairColor[3] * 255.f)) << 24); - if (config::CrosshairColor[bus] == 0) - config::CrosshairColor[bus] = 0xC0FFFFFF; - } - else - { - config::CrosshairColor[bus] = 0; - } - } - is_there_any_xhair |= enabled; - ImGui::PopID(); - } - ImGui::PopItemWidth(); - } - ImGui::EndTable(); - } - { - DisabledScope scope(!is_there_any_xhair); - OptionSlider("Crosshair Size", config::CrosshairSize, 10, 100); - } - OptionCheckbox("Per Game VMU A1", config::PerGameVmu, "When enabled, each game has its own VMU on port 1 of controller A."); - } - - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Video")) - { - int renderApi; - bool perPixel; - switch (config::RendererType) - { - default: - case RenderType::OpenGL: - renderApi = 0; - perPixel = false; - break; - case RenderType::OpenGL_OIT: - renderApi = 0; - perPixel = true; - break; - case RenderType::Vulkan: - renderApi = 1; - perPixel = false; - break; - case RenderType::Vulkan_OIT: - renderApi = 1; - perPixel = true; - break; - case RenderType::DirectX9: - renderApi = 2; - perPixel = false; - break; - case RenderType::DirectX11: - renderApi = 3; - perPixel = false; - break; - case RenderType::DirectX11_OIT: - renderApi = 3; - perPixel = true; - break; - } - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - const bool has_per_pixel = GraphicsContext::Instance()->hasPerPixel(); - header("Transparent Sorting"); - { - int renderer = perPixel ? 2 : config::PerStripSorting ? 1 : 0; - ImGui::Columns(has_per_pixel ? 3 : 2, "renderers", false); - ImGui::RadioButton("Per Triangle", &renderer, 0); - ImGui::SameLine(); - ShowHelpMarker("Sort transparent polygons per triangle. Fast but may produce graphical glitches"); - ImGui::NextColumn(); - ImGui::RadioButton("Per Strip", &renderer, 1); - ImGui::SameLine(); - ShowHelpMarker("Sort transparent polygons per strip. Faster but may produce graphical glitches"); - if (has_per_pixel) - { - ImGui::NextColumn(); - ImGui::RadioButton("Per Pixel", &renderer, 2); - ImGui::SameLine(); - ShowHelpMarker("Sort transparent polygons per pixel. Slower but accurate"); - } - ImGui::Columns(1, NULL, false); - switch (renderer) - { - case 0: - perPixel = false; - config::PerStripSorting.set(false); - break; - case 1: - perPixel = false; - config::PerStripSorting.set(true); - break; - case 2: - perPixel = true; - break; - } - } - ImGui::Spacing(); - ImGuiStyle& style = ImGui::GetStyle(); - float innerSpacing = style.ItemInnerSpacing.x; - - header("Rendering Options"); - { - ImGui::Text("Automatic Frame Skipping:"); - ImGui::Columns(3, "autoskip", false); - OptionRadioButton("Disabled", config::AutoSkipFrame, 0, "No frame skipping"); - ImGui::NextColumn(); - OptionRadioButton("Normal", config::AutoSkipFrame, 1, "Skip a frame when the GPU and CPU are both running slow"); - ImGui::NextColumn(); - OptionRadioButton("Maximum", config::AutoSkipFrame, 2, "Skip a frame when the GPU is running slow"); - ImGui::Columns(1, nullptr, false); - - OptionCheckbox("Shadows", config::ModifierVolumes, - "Enable modifier volumes, usually used for shadows"); - OptionCheckbox("Fog", config::Fog, "Enable fog effects"); - OptionCheckbox("Widescreen", config::Widescreen, - "Draw geometry outside of the normal 4:3 aspect ratio. May produce graphical glitches in the revealed areas.\nAspect Fit and shows the full 16:9 content."); - { - DisabledScope scope(!config::Widescreen); - - ImGui::Indent(); - OptionCheckbox("Super Widescreen", config::SuperWidescreen, - "Use the full width of the screen or window when its aspect ratio is greater than 16:9.\nAspect Fill and remove black bars."); - ImGui::Unindent(); - } - OptionCheckbox("Widescreen Game Cheats", config::WidescreenGameHacks, - "Modify the game so that it displays in 16:9 anamorphic format and use horizontal screen stretching. Only some games are supported."); - - const std::array aniso{ 1, 2, 4, 8, 16 }; - const std::array anisoText{ "Disabled", "2x", "4x", "8x", "16x" }; - u32 afSelected = 0; - for (u32 i = 0; i < aniso.size(); i++) - { - if (aniso[i] == config::AnisotropicFiltering) - afSelected = i; - } - - ImGui::PushItemWidth(ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f); - if (ImGui::BeginCombo("##Anisotropic Filtering", anisoText[afSelected].c_str(), ImGuiComboFlags_NoArrowButton)) - { - for (u32 i = 0; i < aniso.size(); i++) - { - bool is_selected = aniso[i] == config::AnisotropicFiltering; - if (ImGui::Selectable(anisoText[i].c_str(), is_selected)) - config::AnisotropicFiltering = aniso[i]; - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::PopItemWidth(); - ImGui::SameLine(0, innerSpacing); - - if (ImGui::ArrowButton("##Decrease Anisotropic Filtering", ImGuiDir_Left)) - { - if (afSelected > 0) - config::AnisotropicFiltering = aniso[afSelected - 1]; - } - ImGui::SameLine(0, innerSpacing); - if (ImGui::ArrowButton("##Increase Anisotropic Filtering", ImGuiDir_Right)) - { - if (afSelected < aniso.size() - 1) - config::AnisotropicFiltering = aniso[afSelected + 1]; - } - ImGui::SameLine(0, style.ItemInnerSpacing.x); - - ImGui::Text("Anisotropic Filtering"); - ImGui::SameLine(); - ShowHelpMarker("Higher values make textures viewed at oblique angles look sharper, but are more demanding on the GPU. This option only has a visible impact on mipmapped textures."); - - ImGui::Text("Texture Filtering:"); - ImGui::Columns(3, "textureFiltering", false); - OptionRadioButton("Default", config::TextureFiltering, 0, "Use the game's default texture filtering"); - ImGui::NextColumn(); - OptionRadioButton("Force Nearest-Neighbor", config::TextureFiltering, 1, "Force nearest-neighbor filtering for all textures. Crisper appearance, but may cause various rendering issues. This option usually does not affect performance."); - ImGui::NextColumn(); - OptionRadioButton("Force Linear", config::TextureFiltering, 2, "Force linear filtering for all textures. Smoother appearance, but may cause various rendering issues. This option usually does not affect performance."); - ImGui::Columns(1, nullptr, false); - -#ifndef TARGET_IPHONE - OptionCheckbox("VSync", config::VSync, "Synchronizes the frame rate with the screen refresh rate. Recommended"); - if (isVulkan(config::RendererType)) - { - ImGui::Indent(); - { - DisabledScope scope(!config::VSync); - - OptionCheckbox("Duplicate frames", config::DupeFrames, "Duplicate frames on high refresh rate monitors (120 Hz and higher)"); - } - ImGui::Unindent(); - } -#endif - OptionCheckbox("Show FPS Counter", config::ShowFPS, "Show on-screen frame/sec counter"); - OptionCheckbox("Show VMU In-game", config::FloatVMUs, "Show the VMU LCD screens while in-game"); - OptionCheckbox("Rotate Screen 90°", config::Rotate90, "Rotate the screen 90° counterclockwise"); - OptionCheckbox("Delay Frame Swapping", config::DelayFrameSwapping, - "Useful to avoid flashing screen or glitchy videos. Not recommended on slow platforms"); - OptionCheckbox("Fix Upscale Bleeding Edge", config::FixUpscaleBleedingEdge, - "Helps with texture bleeding case when upscaling. Disabling it can help if pixels are warping when upscaling in 2D games (MVC2, CVS, KOF, etc.)"); - OptionCheckbox("Native Depth Interpolation", config::NativeDepthInterpolation, - "Helps with texture corruption and depth issues on AMD GPUs. Can also help Intel GPUs in some cases."); - OptionCheckbox("Full Framebuffer Emulation", config::EmulateFramebuffer, - "Fully accurate VRAM framebuffer emulation. Helps games that directly access the framebuffer for special effects. " - "Very slow and incompatible with upscaling and wide screen."); - constexpr int apiCount = 0 - #ifdef USE_VULKAN - + 1 - #endif - #ifdef USE_DX9 - + 1 - #endif - #ifdef USE_OPENGL - + 1 - #endif - #ifdef USE_DX11 - + 1 - #endif - ; - - if (apiCount > 1) - { - ImGui::Text("Graphics API:"); - ImGui::Columns(apiCount, "renderApi", false); -#ifdef USE_OPENGL - ImGui::RadioButton("OpenGL", &renderApi, 0); - ImGui::NextColumn(); -#endif -#ifdef USE_VULKAN -#ifdef __APPLE__ - ImGui::RadioButton("Vulkan (Metal)", &renderApi, 1); - ImGui::SameLine(0, style.ItemInnerSpacing.x); - ShowHelpMarker("MoltenVK: An implementation of Vulkan that runs on Apple's Metal graphics framework"); -#else - ImGui::RadioButton("Vulkan", &renderApi, 1); -#endif // __APPLE__ - ImGui::NextColumn(); -#endif -#ifdef USE_DX9 - ImGui::RadioButton("DirectX 9", &renderApi, 2); - ImGui::NextColumn(); -#endif -#ifdef USE_DX11 - ImGui::RadioButton("DirectX 11", &renderApi, 3); - ImGui::NextColumn(); -#endif - ImGui::Columns(1, nullptr, false); - } - - const std::array scalings{ 0.5f, 1.f, 1.5f, 2.f, 2.5f, 3.f, 4.f, 4.5f, 5.f, 6.f, 7.f, 8.f, 9.f }; - const std::array scalingsText{ "Half", "Native", "x1.5", "x2", "x2.5", "x3", "x4", "x4.5", "x5", "x6", "x7", "x8", "x9" }; - std::array vres; - std::array resLabels; - u32 selected = 0; - for (u32 i = 0; i < scalings.size(); i++) - { - vres[i] = scalings[i] * 480; - if (vres[i] == config::RenderResolution) - selected = i; - if (!config::Widescreen) - resLabels[i] = std::to_string((int)(scalings[i] * 640)) + "x" + std::to_string((int)(scalings[i] * 480)); - else - resLabels[i] = std::to_string((int)(scalings[i] * 480 * 16 / 9)) + "x" + std::to_string((int)(scalings[i] * 480)); - resLabels[i] += " (" + scalingsText[i] + ")"; - } - - ImGui::PushItemWidth(ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f); - if (ImGui::BeginCombo("##Resolution", resLabels[selected].c_str(), ImGuiComboFlags_NoArrowButton)) - { - for (u32 i = 0; i < scalings.size(); i++) - { - bool is_selected = vres[i] == config::RenderResolution; - if (ImGui::Selectable(resLabels[i].c_str(), is_selected)) - config::RenderResolution = vres[i]; - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::PopItemWidth(); - ImGui::SameLine(0, innerSpacing); - - if (ImGui::ArrowButton("##Decrease Res", ImGuiDir_Left)) - { - if (selected > 0) - config::RenderResolution = vres[selected - 1]; - } - ImGui::SameLine(0, innerSpacing); - if (ImGui::ArrowButton("##Increase Res", ImGuiDir_Right)) - { - if (selected < vres.size() - 1) - config::RenderResolution = vres[selected + 1]; - } - ImGui::SameLine(0, style.ItemInnerSpacing.x); - - ImGui::Text("Internal Resolution"); - ImGui::SameLine(); - ShowHelpMarker("Internal render resolution. Higher is better, but more demanding on the GPU. Values higher than your display resolution (but no more than double your display resolution) can be used for supersampling, which provides high-quality antialiasing without reducing sharpness."); - - OptionSlider("Horizontal Stretching", config::ScreenStretching, 100, 250, - "Stretch the screen horizontally", "%d%%"); - OptionArrowButtons("Frame Skipping", config::SkipFrame, 0, 6, - "Number of frames to skip between two actually rendered frames"); - } - if (perPixel) - { - ImGui::Spacing(); - header("Per Pixel Settings"); - - const std::array bufSizes{ 512_MB, 1_GB, 2_GB, 4_GB }; - const std::array bufSizesText{ "512 MB", "1 GB", "2 GB", "4 GB" }; - ImGui::PushItemWidth(ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f); - u32 selected = 0; - for (; selected < bufSizes.size(); selected++) - if (bufSizes[selected] == config::PixelBufferSize) - break; - if (selected == bufSizes.size()) - selected = 0; - if (ImGui::BeginCombo("##PixelBuffer", bufSizesText[selected].c_str(), ImGuiComboFlags_NoArrowButton)) - { - for (u32 i = 0; i < bufSizes.size(); i++) - { - bool is_selected = i == selected; - if (ImGui::Selectable(bufSizesText[i].c_str(), is_selected)) - config::PixelBufferSize = bufSizes[i]; - if (is_selected) { - ImGui::SetItemDefaultFocus(); - selected = i; - } - } - ImGui::EndCombo(); - } - ImGui::PopItemWidth(); - ImGui::SameLine(0, innerSpacing); - - if (ImGui::ArrowButton("##Decrease BufSize", ImGuiDir_Left)) - { - if (selected > 0) - config::PixelBufferSize = bufSizes[selected - 1]; - } - ImGui::SameLine(0, innerSpacing); - if (ImGui::ArrowButton("##Increase BufSize", ImGuiDir_Right)) - { - if (selected < bufSizes.size() - 1) - config::PixelBufferSize = bufSizes[selected + 1]; - } - ImGui::SameLine(0, style.ItemInnerSpacing.x); - - ImGui::Text("Pixel Buffer Size"); - ImGui::SameLine(); - ShowHelpMarker("The size of the pixel buffer. May need to be increased when upscaling by a large factor."); - - OptionSlider("Maximum Layers", config::PerPixelLayers, 8, 128, - "Maximum number of transparent layers. May need to be increased for some complex scenes. Decreasing it may improve performance."); - } - ImGui::Spacing(); - header("Render to Texture"); - { - OptionCheckbox("Copy to VRAM", config::RenderToTextureBuffer, - "Copy rendered-to textures back to VRAM. Slower but accurate"); - } - ImGui::Spacing(); - header("Texture Upscaling"); - { -#ifdef _OPENMP - OptionArrowButtons("Texture Upscaling", config::TextureUpscale, 1, 8, - "Upscale textures with the xBRZ algorithm. Only on fast platforms and for certain 2D games", "x%d"); - OptionSlider("Texture Max Size", config::MaxFilteredTextureSize, 8, 1024, - "Textures larger than this dimension squared will not be upscaled"); - OptionArrowButtons("Max Threads", config::MaxThreads, 1, 8, - "Maximum number of threads to use for texture upscaling. Recommended: number of physical cores minus one"); -#endif - OptionCheckbox("Load Custom Textures", config::CustomTextures, - "Load custom/high-res textures from data/textures/"); - } -#ifdef VIDEO_ROUTING -#ifdef __APPLE__ - header("Video Routing (Syphon)"); -#elif defined(_WIN32) - ((renderApi == 0) || (renderApi == 3)) ? header("Video Routing (Spout)") : header("Video Routing (Only available with OpenGL or DirectX 11)"); -#endif - { -#ifdef _WIN32 - DisabledScope scope(!((renderApi == 0) || (renderApi == 3))); -#endif - OptionCheckbox("Send video content to another program", config::VideoRouting, - "e.g. Route GPU texture to OBS Studio directly instead of using CPU intensive Display/Window Capture"); - - { - DisabledScope scope(!config::VideoRouting); - OptionCheckbox("Scale down before sending", config::VideoRoutingScale, "Could increase performance when sharing a smaller texture, YMMV"); - { - DisabledScope scope(!config::VideoRoutingScale); - static int vres = config::VideoRoutingVRes; - if (ImGui::InputInt("Output vertical resolution", &vres)) - { - config::VideoRoutingVRes = vres; - } - } - ImGui::Text("Output texture size: %d x %d", config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width, config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); - } - } -#endif - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - - switch (renderApi) - { - case 0: - config::RendererType = perPixel ? RenderType::OpenGL_OIT : RenderType::OpenGL; - break; - case 1: - config::RendererType = perPixel ? RenderType::Vulkan_OIT : RenderType::Vulkan; - break; - case 2: - config::RendererType = RenderType::DirectX9; - break; - case 3: - config::RendererType = perPixel ? RenderType::DirectX11_OIT : RenderType::DirectX11; - break; - } - } - if (ImGui::BeginTabItem("Audio")) - { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - OptionCheckbox("Enable DSP", config::DSPEnabled, - "Enable the Dreamcast Digital Sound Processor. Only recommended on fast platforms"); - OptionCheckbox("Enable VMU Sounds", config::VmuSound, "Play VMU beeps when enabled."); - - if (OptionSlider("Volume Level", config::AudioVolume, 0, 100, "Adjust the emulator's audio level", "%d%%")) - { - config::AudioVolume.calcDbPower(); - }; -#ifdef __ANDROID__ - if (config::AudioBackend.get() == "auto" || config::AudioBackend.get() == "android") - OptionCheckbox("Automatic Latency", config::AutoLatency, - "Automatically set audio latency. Recommended"); -#endif - if (!config::AutoLatency - || (config::AudioBackend.get() != "auto" && config::AudioBackend.get() != "android")) - { - int latency = (int)roundf(config::AudioBufferSize * 1000.f / 44100.f); - ImGui::SliderInt("Latency", &latency, 12, 512, "%d ms"); - config::AudioBufferSize = (int)roundf(latency * 44100.f / 1000.f); - ImGui::SameLine(); - ShowHelpMarker("Sets the maximum audio latency. Not supported by all audio drivers."); - } - - AudioBackend *backend = nullptr; - std::string backend_name = config::AudioBackend; - if (backend_name != "auto") - { - backend = AudioBackend::getBackend(config::AudioBackend); - if (backend != nullptr) - backend_name = backend->slug; - } - - AudioBackend *current_backend = backend; - if (ImGui::BeginCombo("Audio Driver", backend_name.c_str(), ImGuiComboFlags_None)) - { - bool is_selected = (config::AudioBackend.get() == "auto"); - if (ImGui::Selectable("auto - Automatic driver selection", &is_selected)) - config::AudioBackend.set("auto"); - - for (u32 i = 0; i < AudioBackend::getCount(); i++) - { - backend = AudioBackend::getBackend(i); - is_selected = (config::AudioBackend.get() == backend->slug); - - if (is_selected) - current_backend = backend; - - if (ImGui::Selectable((backend->slug + " - " + backend->name).c_str(), &is_selected)) - config::AudioBackend.set(backend->slug); - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - ShowHelpMarker("The audio driver to use"); - - if (current_backend != nullptr) - { - // get backend specific options - int option_count; - const AudioBackend::Option *options = current_backend->getOptions(&option_count); - - for (int o = 0; o < option_count; o++) - { - std::string value = cfgLoadStr(current_backend->slug, options->name, ""); - - if (options->type == AudioBackend::Option::integer) - { - int val = stoi(value); - if (ImGui::SliderInt(options->caption.c_str(), &val, options->minValue, options->maxValue)) - { - std::string s = std::to_string(val); - cfgSaveStr(current_backend->slug, options->name, s); - } - } - else if (options->type == AudioBackend::Option::checkbox) - { - bool check = value == "1"; - if (ImGui::Checkbox(options->caption.c_str(), &check)) - cfgSaveStr(current_backend->slug, options->name, - check ? "1" : "0"); - } - else if (options->type == AudioBackend::Option::list) - { - if (ImGui::BeginCombo(options->caption.c_str(), value.c_str(), ImGuiComboFlags_None)) - { - bool is_selected = false; - for (const auto& cur : options->values) - { - is_selected = value == cur; - if (ImGui::Selectable(cur.c_str(), &is_selected)) - cfgSaveStr(current_backend->slug, options->name, cur); - - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - } - else { - WARN_LOG(RENDERER, "Unknown option"); - } - - options++; - } - } - - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Network")) - { - ImGuiStyle& style = ImGui::GetStyle(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - - header("Network Type"); - { - DisabledScope scope(game_started); - - int netType = 0; - if (config::GGPOEnable) - netType = 1; - else if (config::NetworkEnable) - netType = 2; - else if (config::BattleCableEnable) - netType = 3; - ImGui::Columns(4, "networkType", false); - ImGui::RadioButton("Disabled", &netType, 0); - ImGui::NextColumn(); - ImGui::RadioButton("GGPO", &netType, 1); - ImGui::SameLine(0, style.ItemInnerSpacing.x); - ShowHelpMarker("Enable networking using GGPO"); - ImGui::NextColumn(); - ImGui::RadioButton("Naomi", &netType, 2); - ImGui::SameLine(0, style.ItemInnerSpacing.x); - ShowHelpMarker("Enable networking for supported Naomi and Atomiswave games"); - ImGui::NextColumn(); - ImGui::RadioButton("Battle Cable", &netType, 3); - ImGui::SameLine(0, style.ItemInnerSpacing.x); - ShowHelpMarker("Emulate the Taisen (Battle) null modem cable for games that support it"); - ImGui::Columns(1, nullptr, false); - - config::GGPOEnable = false; - config::NetworkEnable = false; - config::BattleCableEnable = false; - switch (netType) { - case 1: - config::GGPOEnable = true; - break; - case 2: - config::NetworkEnable = true; - break; - case 3: - config::BattleCableEnable = true; - break; - } - } - if (config::GGPOEnable || config::NetworkEnable || config::BattleCableEnable) { - ImGui::Spacing(); - header("Configuration"); - } - { - if (config::GGPOEnable) - { - config::NetworkEnable = false; - OptionCheckbox("Play as Player 1", config::ActAsServer, - "Deselect to play as player 2"); - char server_name[256]; - strcpy(server_name, config::NetworkServer.get().c_str()); - ImGui::InputText("Peer", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("Your peer IP address and optional port"); - config::NetworkServer.set(server_name); - OptionSlider("Frame Delay", config::GGPODelay, 0, 20, - "Sets Frame Delay, advisable for sessions with ping >100 ms"); - - ImGui::Text("Left Thumbstick:"); - OptionRadioButton("Disabled", config::GGPOAnalogAxes, 0, "Left thumbstick not used"); - ImGui::SameLine(); - OptionRadioButton("Horizontal", config::GGPOAnalogAxes, 1, "Use the left thumbstick horizontal axis only"); - ImGui::SameLine(); - OptionRadioButton("Full", config::GGPOAnalogAxes, 2, "Use the left thumbstick horizontal and vertical axes"); - - OptionCheckbox("Enable Chat", config::GGPOChat, "Open the chat window when a chat message is received"); - if (config::GGPOChat) - { - OptionCheckbox("Enable Chat Window Timeout", config::GGPOChatTimeoutToggle, "Automatically close chat window after 20 seconds"); - if (config::GGPOChatTimeoutToggle) - { - char chatTimeout[256]; - sprintf(chatTimeout, "%d", (int)config::GGPOChatTimeout); - ImGui::InputText("Chat Window Timeout (seconds)", chatTimeout, sizeof(chatTimeout), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("Sets duration that chat window stays open after new message is received."); - config::GGPOChatTimeout.set(atoi(chatTimeout)); - } - } - OptionCheckbox("Network Statistics", config::NetworkStats, - "Display network statistics on screen"); - } - else if (config::NetworkEnable) - { - OptionCheckbox("Act as Server", config::ActAsServer, - "Create a local server for Naomi network games"); - if (!config::ActAsServer) - { - char server_name[256]; - strcpy(server_name, config::NetworkServer.get().c_str()); - ImGui::InputText("Server", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("The server to connect to. Leave blank to find a server automatically on the default port"); - config::NetworkServer.set(server_name); - } - char localPort[256]; - sprintf(localPort, "%d", (int)config::LocalPort); - ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("The local UDP port to use"); - config::LocalPort.set(atoi(localPort)); - } - else if (config::BattleCableEnable) - { - char server_name[256]; - strcpy(server_name, config::NetworkServer.get().c_str()); - ImGui::InputText("Peer", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("The peer to connect to. Leave blank to find a player automatically on the default port"); - config::NetworkServer.set(server_name); - char localPort[256]; - sprintf(localPort, "%d", (int)config::LocalPort); - ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("The local UDP port to use"); - config::LocalPort.set(atoi(localPort)); - } - } - ImGui::Spacing(); - header("Network Options"); - { - OptionCheckbox("Enable UPnP", config::EnableUPnP, "Automatically configure your network router for netplay"); - OptionCheckbox("Broadcast Digital Outputs", config::NetworkOutput, "Broadcast digital outputs and force-feedback state on TCP port 8000. " - "Compatible with the \"-output network\" MAME option. Arcade games only."); - { - DisabledScope scope(game_started); - - OptionCheckbox("Broadband Adapter Emulation", config::EmulateBBA, - "Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem"); - } - } -#ifdef NAOMI_MULTIBOARD - ImGui::Spacing(); - header("Multiboard Screens"); - { - //OptionRadioButton("Disabled", config::MultiboardSlaves, 0, "Multiboard disabled (when optional)"); - OptionRadioButton("1 (Twin)", config::MultiboardSlaves, 1, "One screen configuration (F355 Twin)"); - ImGui::SameLine(); - OptionRadioButton("3 (Deluxe)", config::MultiboardSlaves, 2, "Three screens configuration"); - } -#endif - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Advanced")) - { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - header("CPU Mode"); - { - ImGui::Columns(2, "cpu_modes", false); - OptionRadioButton("Dynarec", config::DynarecEnabled, true, - "Use the dynamic recompiler. Recommended in most cases"); - ImGui::NextColumn(); - OptionRadioButton("Interpreter", config::DynarecEnabled, false, - "Use the interpreter. Very slow but may help in case of a dynarec problem"); - ImGui::Columns(1, NULL, false); - - OptionSlider("SH4 Clock", config::Sh4Clock, 100, 300, - "Over/Underclock the main SH4 CPU. Default is 200 MHz. Other values may crash, freeze or trigger unexpected nuclear reactions.", - "%d MHz"); - } - ImGui::Spacing(); - header("Other"); - { - OptionCheckbox("HLE BIOS", config::UseReios, "Force high-level BIOS emulation"); - OptionCheckbox("Multi-threaded emulation", config::ThreadedRendering, - "Run the emulated CPU and GPU on different threads"); -#ifndef __ANDROID - OptionCheckbox("Serial Console", config::SerialConsole, - "Dump the Dreamcast serial console to stdout"); -#endif - { - DisabledScope scope(game_started); - OptionCheckbox("Dreamcast 32MB RAM Mod", config::RamMod32MB, - "Enables 32MB RAM Mod for Dreamcast. May affect compatibility"); - } - OptionCheckbox("Dump Textures", config::DumpTextures, - "Dump all textures into data/texdump/"); - - bool logToFile = cfgLoadBool("log", "LogToFile", false); - bool newLogToFile = logToFile; - ImGui::Checkbox("Log to File", &newLogToFile); - if (logToFile != newLogToFile) - { - cfgSaveBool("log", "LogToFile", newLogToFile); - LogManager::Shutdown(); - LogManager::Init(); - } - ImGui::SameLine(); - ShowHelpMarker("Log debug information to flycast.log"); -#ifdef SENTRY_UPLOAD - OptionCheckbox("Automatically Report Crashes", config::UploadCrashLogs, - "Automatically upload crash reports to sentry.io to help in troubleshooting. No personal information is included."); -#endif - } - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - - #ifdef USE_LUA - header("Lua Scripting"); - { - char LuaFileName[256]; - - strcpy(LuaFileName, config::LuaFileName.get().c_str()); - ImGui::InputText("Lua Filename", LuaFileName, sizeof(LuaFileName), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); - ImGui::SameLine(); - ShowHelpMarker("Specify lua filename to use. Should be located in Flycast config directory. Defaults to flycast.lua when empty."); - config::LuaFileName = LuaFileName; - - } - #endif - } - -#if !defined(NDEBUG) || defined(DEBUGFAST) || FC_PROFILER - gui_debug_tab(); -#endif - - if (ImGui::BeginTabItem("About")) - { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - header("Flycast"); - { - ImGui::Text("Version: %s", GIT_VERSION); - ImGui::Text("Git Hash: %s", GIT_HASH); - ImGui::Text("Build Date: %s", BUILD_DATE); - } - ImGui::Spacing(); - header("Platform"); - { - ImGui::Text("CPU: %s", -#if HOST_CPU == CPU_X86 - "x86" -#elif HOST_CPU == CPU_ARM - "ARM" -#elif HOST_CPU == CPU_MIPS - "MIPS" -#elif HOST_CPU == CPU_X64 - "x86/64" -#elif HOST_CPU == CPU_GENERIC - "Generic" -#elif HOST_CPU == CPU_ARM64 - "ARM64" -#else - "Unknown" -#endif - ); - ImGui::Text("Operating System: %s", -#ifdef __ANDROID__ - "Android" -#elif defined(__unix__) - "Linux" -#elif defined(__APPLE__) -#ifdef TARGET_IPHONE - "iOS" -#else - "macOS" -#endif -#elif defined(TARGET_UWP) - "Windows Universal Platform" -#elif defined(_WIN32) - "Windows" -#elif defined(__SWITCH__) - "Switch" -#else - "Unknown" -#endif - ); -#ifdef TARGET_IPHONE - const char *getIosJitStatus(); - ImGui::Text("JIT Status: %s", getIosJitStatus()); -#endif - } - ImGui::Spacing(); - if (isOpenGL(config::RendererType)) - header("OpenGL"); - else if (isVulkan(config::RendererType)) - header("Vulkan"); - else if (isDirectX(config::RendererType)) - header("DirectX"); - ImGui::Text("Driver Name: %s", GraphicsContext::Instance()->getDriverName().c_str()); - ImGui::Text("Version: %s", GraphicsContext::Instance()->getDriverVersion().c_str()); - - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - ImGui::PopStyleVar(); - - scrollWhenDraggingOnVoid(); - windowDragScroll(); - ImGui::End(); - ImGui::PopStyleVar(); -} - -void gui_display_notification(const char *msg, int duration) -{ - std::lock_guard lock(osd_message_mutex); - osd_message = msg; - osd_message_end = os_GetSeconds() + (double)duration / 1000.0; -} - -static std::string get_notification() -{ - std::lock_guard lock(osd_message_mutex); - if (!osd_message.empty() && os_GetSeconds() >= osd_message_end) - osd_message.clear(); - return osd_message; -} - -inline static void gui_display_demo() -{ - ImGui::ShowDemoWindow(); -} - -static void gameTooltip(const std::string& tip) -{ - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 25.0f); - ImGui::TextUnformatted(tip.c_str()); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } -} - -static bool getGameImage(const GameBoxart& art, ImTextureID& textureId, bool allowLoad) -{ - textureId = ImTextureID{}; - if (art.boxartPath.empty()) - return false; - - // Get the boxart texture. Load it if needed. - textureId = imguiDriver->getTexture(art.boxartPath); - if (textureId == ImTextureID() && allowLoad) - { - int width, height; - u8 *imgData = loadImage(art.boxartPath, width, height); - if (imgData != nullptr) - { - try { - textureId = imguiDriver->updateTextureAndAspectRatio(art.boxartPath, imgData, width, height); - } catch (...) { - // vulkan can throw during resizing - } - free(imgData); - } - return true; - } - return false; -} - -static bool gameImageButton(ImTextureID textureId, const std::string& tooltip, ImVec2 size) -{ - float ar = imguiDriver->getAspectRatio(textureId); - ImVec2 uv0 { 0.f, 0.f }; - ImVec2 uv1 { 1.f, 1.f }; - if (ar > 1) - { - uv0.y = -(ar - 1) / 2; - uv1.y = 1 + (ar - 1) / 2; - } - else if (ar != 0) - { - ar = 1 / ar; - uv0.x = -(ar - 1) / 2; - uv1.x = 1 + (ar - 1) / 2; - } - bool pressed = ImGui::ImageButton("", textureId, size - ImGui::GetStyle().FramePadding * 2, uv0, uv1); - gameTooltip(tooltip); - - return pressed; -} - -static void gui_display_content() -{ - fullScreenWindow(false); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); - - ImGui::Begin("##main", NULL, ImGuiWindowFlags_NoDecoration); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); - ImGui::AlignTextToFramePadding(); - ImGui::Indent(10 * settings.display.uiScale); - ImGui::Text("GAMES"); - ImGui::Unindent(10 * settings.display.uiScale); - - static ImGuiTextFilter filter; -#if !defined(__ANDROID__) && !defined(TARGET_IPHONE) && !defined(TARGET_UWP) && !defined(__SWITCH__) - ImGui::SameLine(0, 32 * settings.display.uiScale); - filter.Draw("Filter", ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x - 32 * settings.display.uiScale - - ImGui::CalcTextSize("Settings").x - ImGui::GetStyle().FramePadding.x * 2.0f - ImGui::GetStyle().ItemSpacing.x); -#endif - if (gui_state != GuiState::SelectDisk) - { -#ifdef TARGET_UWP - void gui_load_game(); - ImGui::SameLine(ImGui::GetContentRegionMax().x - ImGui::CalcTextSize("Settings").x - - ImGui::GetStyle().FramePadding.x * 4.0f - ImGui::GetStyle().ItemSpacing.x - ImGui::CalcTextSize("Load...").x); - if (ImGui::Button("Load...")) - gui_load_game(); - ImGui::SameLine(); -#elif defined(__SWITCH__) - ImGui::SameLine(ImGui::GetContentRegionMax().x - ImGui::CalcTextSize("Settings").x - - ImGui::GetStyle().FramePadding.x * 4.0f - ImGui::GetStyle().ItemSpacing.x - ImGui::CalcTextSize("Exit").x); - if (ImGui::Button("Exit")) - dc_exit(); - ImGui::SameLine(); -#else - ImGui::SameLine(ImGui::GetContentRegionMax().x - ImGui::CalcTextSize("Settings").x - ImGui::GetStyle().FramePadding.x * 2.0f); -#endif - if (ImGui::Button("Settings")) - gui_setState(GuiState::Settings); - } - ImGui::PopStyleVar(); - - scanner.fetch_game_list(); - - // Only if Filter and Settings aren't focused... ImGui::SetNextWindowFocus(); - ImGui::BeginChild(ImGui::GetID("library"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); - { - const float totalWidth = ImGui::GetContentRegionMax().x - (!ImGui::GetCurrentWindow()->ScrollbarY ? ImGui::GetStyle().ScrollbarSize : 0); - const int itemsPerLine = std::max(totalWidth / (150 * settings.display.uiScale + ImGui::GetStyle().ItemSpacing.x), 1); - const float responsiveBoxSize = totalWidth / itemsPerLine - ImGui::GetStyle().FramePadding.x * 2; - const ImVec2 responsiveBoxVec2 = ImVec2(responsiveBoxSize, responsiveBoxSize); - - if (config::BoxartDisplayMode) - ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f, 0.5f)); - else - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20)); - - int counter = 0; - int loadedImages = 0; - if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS")) - { - ImGui::PushID("bios"); - bool pressed; - if (config::BoxartDisplayMode) - { - ImTextureID textureId{}; - GameMedia game; - GameBoxart art = boxart.getBoxart(game); - if (getGameImage(art, textureId, loadedImages < 10)) - loadedImages++; - if (textureId != ImTextureID()) - pressed = gameImageButton(textureId, "Dreamcast BIOS", responsiveBoxVec2); - else - pressed = ImGui::Button("Dreamcast BIOS", responsiveBoxVec2); - } - else - { - pressed = ImGui::Selectable("Dreamcast BIOS"); - } - if (pressed) - gui_start_game(""); - ImGui::PopID(); - counter++; - } - { - scanner.get_mutex().lock(); - for (const auto& game : scanner.get_game_list()) - { - if (gui_state == GuiState::SelectDisk) - { - std::string extension = get_file_extension(game.path); - if (extension != "gdi" && extension != "chd" - && extension != "cdi" && extension != "cue") - // Only dreamcast disks - continue; - } - std::string gameName = game.name; - GameBoxart art; - if (config::BoxartDisplayMode) - { - art = boxart.getBoxart(game); - gameName = art.name; - } - if (filter.PassFilter(gameName.c_str())) - { - ImGui::PushID(game.path.c_str()); - bool pressed; - if (config::BoxartDisplayMode) - { - if (counter % itemsPerLine != 0) - ImGui::SameLine(); - counter++; - ImTextureID textureId{}; - // Get the boxart texture. Load it if needed (max 10 per frame). - if (getGameImage(art, textureId, loadedImages < 10)) - loadedImages++; - if (textureId != ImTextureID()) - pressed = gameImageButton(textureId, game.name, responsiveBoxVec2); - else - { - pressed = ImGui::Button(gameName.c_str(), responsiveBoxVec2); - gameTooltip(game.name); - } - } - else - { - pressed = ImGui::Selectable(gameName.c_str()); - } - if (pressed) - { - if (gui_state == GuiState::SelectDisk) - { - settings.content.path = game.path; - try { - DiscSwap(game.path); - gui_setState(GuiState::Closed); - } catch (const FlycastException& e) { - gui_error(e.what()); - } - } - else - { - std::string gamePath(game.path); - scanner.get_mutex().unlock(); - gui_start_game(gamePath); - scanner.get_mutex().lock(); - ImGui::PopID(); - break; - } - } - ImGui::PopID(); - } - } - scanner.get_mutex().unlock(); - } - ImGui::PopStyleVar(); - } - scrollWhenDraggingOnVoid(); - windowDragScroll(); - ImGui::EndChild(); - ImGui::End(); - ImGui::PopStyleVar(); - ImGui::PopStyleVar(); - - contentpath_warning_popup(); -} - -static bool systemdir_selected_callback(bool cancelled, std::string selection) -{ - if (cancelled) - { - gui_setState(GuiState::Main); - return true; - } - selection += "/"; - - std::string data_path = selection + "data/"; - if (!file_exists(data_path)) - { - if (!make_directory(data_path)) - { - WARN_LOG(BOOT, "Cannot create 'data' directory: %s", data_path.c_str()); - gui_error("Invalid selection:\nFlycast cannot write to this directory."); - return false; - } - } - else - { - // Test - std::string testPath = data_path + "writetest.txt"; - FILE *file = fopen(testPath.c_str(), "w"); - if (file == nullptr) - { - WARN_LOG(BOOT, "Cannot write in the 'data' directory"); - gui_error("Invalid selection:\nFlycast cannot write to this directory."); - return false; - } - fclose(file); - unlink(testPath.c_str()); - } - set_user_config_dir(selection); - add_system_data_dir(selection); - set_user_data_dir(data_path); - - if (cfgOpen()) - { - config::Settings::instance().load(false); - // Make sure the renderer type doesn't change mid-flight - config::RendererType = RenderType::OpenGL; - gui_setState(GuiState::Main); - if (config::ContentPath.get().empty()) - { - scanner.stop(); - config::ContentPath.get().push_back(selection); - } - SaveSettings(); - } - return true; -} - -static void gui_display_onboarding() -{ - ImGui::OpenPopup("Select System Directory"); - select_file_popup("Select System Directory", &systemdir_selected_callback); -} - -static std::future networkStatus; - -static void gui_network_start() -{ - centerNextWindow(); - ImGui::SetNextWindowSize(ScaledVec2(330, 180)); - - ImGui::Begin("##network", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(20.f * settings.display.uiScale); - - if (networkStatus.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) - { - ImGui::Text("Starting..."); - try { - if (networkStatus.get()) - gui_setState(GuiState::Closed); - else - gui_stop_game(); - } catch (const FlycastException& e) { - gui_stop_game(e.what()); - } - } - else - { - ImGui::Text("Starting Network..."); - if (NetworkHandshake::instance->canStartNow()) - ImGui::Text("Press Start to start the game now."); - } - ImGui::Text("%s", get_notification().c_str()); - - float currentwidth = ImGui::GetContentRegionAvail().x; - ImGui::SetCursorPosX((currentwidth - 100.f * settings.display.uiScale) / 2.f + ImGui::GetStyle().WindowPadding.x); - ImGui::SetCursorPosY(126.f * settings.display.uiScale); - if (ImGui::Button("Cancel", ScaledVec2(100.f, 0)) && NetworkHandshake::instance != nullptr) - { - NetworkHandshake::instance->stop(); - try { - networkStatus.get(); - } - catch (const FlycastException& e) { - } - gui_stop_game(); - } - ImGui::PopStyleVar(); - - ImGui::End(); - - if ((kcode[0] & DC_BTN_START) == 0 && NetworkHandshake::instance != nullptr) - NetworkHandshake::instance->startNow(); -} - -static void gui_display_loadscreen() -{ - centerNextWindow(); - ImGui::SetNextWindowSize(ScaledVec2(330, 180)); - - ImGui::Begin("##loading", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(20.f * settings.display.uiScale); - try { - const char *label = gameLoader.getProgress().label; - if (label == nullptr) - { - if (gameLoader.ready()) - label = "Starting..."; - else - label = "Loading..."; - } - - if (gameLoader.ready()) - { - if (NetworkHandshake::instance != nullptr) - { - networkStatus = NetworkHandshake::instance->start(); - gui_setState(GuiState::NetworkStart); - } - else - { - gui_setState(GuiState::Closed); - ImGui::Text("%s", label); - } - } - else - { - ImGui::Text("%s", label); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f)); - ImGui::ProgressBar(gameLoader.getProgress().progress, ImVec2(-1, 20.f * settings.display.uiScale), ""); - ImGui::PopStyleColor(); - - float currentwidth = ImGui::GetContentRegionAvail().x; - ImGui::SetCursorPosX((currentwidth - 100.f * settings.display.uiScale) / 2.f + ImGui::GetStyle().WindowPadding.x); - ImGui::SetCursorPosY(126.f * settings.display.uiScale); - if (ImGui::Button("Cancel", ScaledVec2(100.f, 0))) - gameLoader.cancel(); - } - } catch (const FlycastException& ex) { - ERROR_LOG(BOOT, "%s", ex.what()); -#ifdef TEST_AUTOMATION - die("Game load failed"); -#endif - gui_stop_game(ex.what()); - } - ImGui::PopStyleVar(); - - ImGui::End(); -} - -void gui_display_ui() -{ - FC_PROFILE_SCOPE; - const LockGuard lock(guiMutex); - - if (gui_state == GuiState::Closed || gui_state == GuiState::VJoyEdit) - return; - if (gui_state == GuiState::Main) - { - if (!settings.content.path.empty() || settings.naomi.slave) - { -#ifndef __ANDROID__ - commandLineStart = true; -#endif - gui_start_game(settings.content.path); - return; - } - } - - gui_newFrame(); - ImGui::NewFrame(); - error_msg_shown = false; - bool gui_open = gui_is_open(); - - switch (gui_state) - { - case GuiState::Settings: - gui_display_settings(); - break; - case GuiState::Commands: - gui_display_commands(); - break; - case GuiState::Main: - //gui_display_demo(); - gui_display_content(); - break; - case GuiState::Closed: - break; - case GuiState::Onboarding: - gui_display_onboarding(); - break; - case GuiState::VJoyEdit: - break; - case GuiState::VJoyEditCommands: -#ifdef __ANDROID__ - gui_display_vjoy_commands(); -#endif - break; - case GuiState::SelectDisk: - gui_display_content(); - break; - case GuiState::Loading: - gui_display_loadscreen(); - break; - case GuiState::NetworkStart: - gui_network_start(); - break; - case GuiState::Cheats: - gui_cheats(); - break; - default: - die("Unknown UI state"); - break; - } - error_popup(); - gui_endFrame(gui_open); - - if (gui_state == GuiState::Closed) - emu.start(); -} - -static float LastFPSTime; -static int lastFrameCount = 0; -static float fps = -1; - -static std::string getFPSNotification() -{ - if (config::ShowFPS) - { - double now = os_GetSeconds(); - if (now - LastFPSTime >= 1.0) { - fps = (MainFrameCount - lastFrameCount) / (now - LastFPSTime); - LastFPSTime = now; - lastFrameCount = MainFrameCount; - } - if (fps >= 0.f && fps < 9999.f) { - char text[32]; - snprintf(text, sizeof(text), "F:%.1f%s", fps, settings.input.fastForwardMode ? " >>" : ""); - - return std::string(text); - } - } - return std::string(settings.input.fastForwardMode ? ">>" : ""); -} - -void gui_display_osd() -{ - if (gui_state == GuiState::VJoyEdit) - return; - std::string message = get_notification(); - if (message.empty()) - message = getFPSNotification(); - -// if (!message.empty() || config::FloatVMUs || crosshairsNeeded() || (ggpo::active() && config::NetworkStats)) - { - gui_newFrame(); - ImGui::NewFrame(); - - if (!message.empty()) - { - ImGui::SetNextWindowBgAlpha(0); - ImGui::SetNextWindowPos(ImVec2(0, ImGui::GetIO().DisplaySize.y), ImGuiCond_Always, ImVec2(0.f, 1.f)); // Lower left corner - ImGui::SetNextWindowSize(ImVec2(ImGui::GetIO().DisplaySize.x, 0)); - - ImGui::Begin("##osd", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav - | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground); - ImGui::SetWindowFontScale(1.5); - ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", message.c_str()); - ImGui::End(); - } - imguiDriver->displayCrosshairs(); - if (config::FloatVMUs) - imguiDriver->displayVmus(); -// gui_plot_render_time(settings.display.width, settings.display.height); - if (ggpo::active()) - { - if (config::NetworkStats) - ggpo::displayStats(); - chat.display(); - } - lua::overlay(); - - gui_endFrame(gui_is_open()); - } -} - -void gui_display_profiler() -{ -#if FC_PROFILER - gui_newFrame(); - ImGui::NewFrame(); - - ImGui::Begin("Profiler", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBackground); - - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); - - std::unique_lock lock(fc_profiler::ProfileThread::s_allThreadsLock); - - for(const fc_profiler::ProfileThread* profileThread : fc_profiler::ProfileThread::s_allThreads) - { - char text[256]; - std::snprintf(text, 256, "%.3f : Thread %s", (float)profileThread->cachedTime, profileThread->threadName.c_str()); - ImGui::TreeNode(text); - - ImGui::Indent(); - fc_profiler::drawGUI(profileThread->cachedResultTree); - ImGui::Unindent(); - } - - ImGui::PopStyleColor(); - - for (const fc_profiler::ProfileThread* profileThread : fc_profiler::ProfileThread::s_allThreads) - { - fc_profiler::drawGraph(*profileThread); - } - - ImGui::End(); - - gui_endFrame(true); -#endif -} - -void gui_open_onboarding() -{ - gui_setState(GuiState::Onboarding); -} - -void gui_cancel_load() -{ - gameLoader.cancel(); -} - -void gui_term() -{ - if (inited) - { - inited = false; - scanner.stop(); - ImGui::DestroyContext(); - EventManager::unlisten(Event::Resume, emuEventCallback); - EventManager::unlisten(Event::Start, emuEventCallback); - EventManager::unlisten(Event::Terminate, emuEventCallback); - gui_save(); - } -} - -void fatal_error(const char* text, ...) -{ - va_list args; - - char temp[2048]; - va_start(args, text); - vsnprintf(temp, sizeof(temp), text, args); - va_end(args); - ERROR_LOG(COMMON, "%s", temp); - - gui_display_notification(temp, 2000); -} - -extern bool subfolders_read; - -void gui_refresh_files() -{ - scanner.refresh(); - subfolders_read = false; -} - -static void reset_vmus() -{ - for (u32 i = 0; i < std::size(vmu_lcd_status); i++) - vmu_lcd_status[i] = false; -} - -void gui_error(const std::string& what) -{ - error_msg = what; -} - -void gui_save() -{ - boxart.saveDatabase(); -} - -void gui_loadState() -{ - const LockGuard lock(guiMutex); - if (gui_state == GuiState::Closed && savestateAllowed()) - { - try { - emu.stop(); - dc_loadstate(config::SavestateSlot); - emu.start(); - } catch (const FlycastException& e) { - gui_stop_game(e.what()); - } - } -} - -void gui_saveState() -{ - const LockGuard lock(guiMutex); - if (gui_state == GuiState::Closed && savestateAllowed()) - { - try { - emu.stop(); - dc_savestate(config::SavestateSlot); - emu.start(); - } catch (const FlycastException& e) { - gui_stop_game(e.what()); - } - } -} - -void gui_setState(GuiState newState) -{ - gui_state = newState; - if (newState == GuiState::Closed) - { - // If the game isn't rendering any frame, these flags won't be updated and keyboard/mouse input will be ignored. - // So we force them false here. They will be set in the next ImGUI::NewFrame() anyway - ImGuiIO& io = ImGui::GetIO(); - io.WantCaptureKeyboard = false; - io.WantCaptureMouse = false; - } -} - -#ifdef TARGET_UWP -// Ugly but a good workaround for MS stupidity -// UWP doesn't allow the UI thread to wait on a thread/task. When an std::future is ready, it is possible -// that the task has not yet completed. Calling std::future::get() at this point will throw an exception -// AND destroy the std::future at the same time, rendering it invalid and discarding the future result. -bool __cdecl Concurrency::details::_Task_impl_base::_IsNonBlockingThread() { - return false; -} -#endif diff --git a/core/rend/gui_cheats.cpp b/core/rend/gui_cheats.cpp deleted file mode 100644 index db88e24dd..000000000 --- a/core/rend/gui_cheats.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/* - Copyright 2021 flyinghead - - This file is part of Flycast. - - Flycast 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. - - Flycast 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 Flycast. If not, see . -*/ -#include "gui.h" -#include "imgui.h" -#include "gui_util.h" -#include "cheats.h" -#ifdef __ANDROID__ -#include "oslib/storage.h" -#endif - -static bool addingCheat; - -static void addCheat() -{ - static char cheatName[64]; - static char cheatCode[128]; - centerNextWindow(); - ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(600.f, 400.f))); - - ImGui::Begin("##main", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar - | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); - ImGui::AlignTextToFramePadding(); - ImGui::Indent(10 * settings.display.uiScale); - ImGui::Text("ADD CHEAT"); - - ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Cancel").x - ImGui::GetStyle().FramePadding.x * 4.f - - ImGui::CalcTextSize("OK").x - ImGui::GetStyle().ItemSpacing.x); - if (ImGui::Button("Cancel")) - addingCheat = false; - ImGui::SameLine(); - if (ImGui::Button("OK")) - { - try { - cheatManager.addGameSharkCheat(cheatName, cheatCode); - addingCheat = false; - cheatName[0] = 0; - cheatCode[0] = 0; - } catch (const FlycastException& e) { - gui_error(e.what()); - } - } - - ImGui::Unindent(10 * settings.display.uiScale); - ImGui::PopStyleVar(); - - ImGui::BeginChild(ImGui::GetID("input"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_NavFlattened); - { - ImGui::InputText("Name", cheatName, sizeof(cheatName), 0, nullptr, nullptr); - ImGui::InputTextMultiline("Code", cheatCode, sizeof(cheatCode), ImVec2(0, ImGui::GetTextLineHeight() * 8), 0, nullptr, nullptr); - } - ImGui::EndChild(); - ImGui::End(); -} - -static void cheatFileSelected(bool cancelled, std::string path) -{ - if (!cancelled) - cheatManager.loadCheatFile(path); -} - -void gui_cheats() -{ - if (addingCheat) - { - addCheat(); - return; - } - centerNextWindow(); - ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(600.f, 400.f))); - - ImGui::Begin("##main", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar - | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); - ImGui::AlignTextToFramePadding(); - ImGui::Indent(10 * settings.display.uiScale); - ImGui::Text("CHEATS"); - - ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Add").x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().FramePadding.x * 6.f - - ImGui::CalcTextSize("Load").x - ImGui::GetStyle().ItemSpacing.x * 2); - if (ImGui::Button("Add")) - addingCheat = true; - ImGui::SameLine(); -#ifdef __ANDROID__ - if (ImGui::Button("Load")) - hostfs::addStorage(false, true, cheatFileSelected); -#else - if (ImGui::Button("Load")) - ImGui::OpenPopup("Select cheat file"); - select_file_popup("Select cheat file", [](bool cancelled, std::string selection) - { - cheatFileSelected(cancelled, selection); - return true; - }, true, "cht"); -#endif - - ImGui::SameLine(); - if (ImGui::Button("Close")) - gui_setState(GuiState::Commands); - - ImGui::Unindent(10 * settings.display.uiScale); - ImGui::PopStyleVar(); - - ImGui::BeginChild(ImGui::GetID("cheats"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); - { - if (cheatManager.cheatCount() == 0) - ImGui::Text("(No cheat loaded)"); - else - for (size_t i = 0; i < cheatManager.cheatCount(); i++) - { - ImGui::PushID(("cheat" + std::to_string(i)).c_str()); - bool v = cheatManager.cheatEnabled(i); - if (ImGui::Checkbox(cheatManager.cheatDescription(i).c_str(), &v)) - cheatManager.enableCheat(i, v); - ImGui::PopID(); - } - } - scrollWhenDraggingOnVoid(); - windowDragScroll(); - - ImGui::EndChild(); - ImGui::End(); -} diff --git a/core/rend/osd.cpp b/core/rend/osd.cpp index a4496551d..9722b92f4 100644 --- a/core/rend/osd.cpp +++ b/core/rend/osd.cpp @@ -175,7 +175,7 @@ u8 *loadOSDButtons(int &width, int &height) u32 vmu_lcd_data[8][48 * 32]; bool vmu_lcd_status[8]; -bool vmu_lcd_changed[8]; +u64 vmuLastChanged[8]; void push_vmu_screen(int bus_id, int bus_port, u8* buffer) { @@ -195,7 +195,7 @@ void push_vmu_screen(int bus_id, int bus_port, u8* buffer) #ifndef LIBRETRO vmu_lcd_status[vmu_id] = true; #endif - vmu_lcd_changed[vmu_id] = true; + vmuLastChanged[vmu_id] = getTimeMs(); } static const int lightgunCrosshairData[16 * 16] = diff --git a/core/rend/osd.h b/core/rend/osd.h index 8680189c9..b3a819d6d 100644 --- a/core/rend/osd.h +++ b/core/rend/osd.h @@ -37,7 +37,7 @@ void HideOSD(); // VMUs extern u32 vmu_lcd_data[8][48 * 32]; extern bool vmu_lcd_status[8]; -extern bool vmu_lcd_changed[8]; +extern u64 vmuLastChanged[8]; void push_vmu_screen(int bus_id, int bus_port, u8* buffer); @@ -59,5 +59,5 @@ static inline bool crosshairsNeeded() static inline void blankVmus() { memset(vmu_lcd_data, 0, sizeof(vmu_lcd_data)); - memset(vmu_lcd_changed, true, sizeof(vmu_lcd_changed)); + memset(vmuLastChanged, 0, sizeof(vmuLastChanged)); } diff --git a/core/rend/vulkan/adreno.cpp b/core/rend/vulkan/adreno.cpp new file mode 100644 index 000000000..928964a48 --- /dev/null +++ b/core/rend/vulkan/adreno.cpp @@ -0,0 +1,209 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#include "build.h" +#if defined(__ANDROID__) && !defined(LIBRETRO) && HOST_CPU == CPU_ARM64 +#include "adreno.h" +#include +#include "cfg/option.h" +#include +#include "json.hpp" +using namespace nlohmann; +#include "archive/ZipArchive.h" +#include "oslib/directory.h" +#include "stdclass.h" + +std::string getNativeLibraryPath(); +std::string getFilesPath(); + +const std::string DRIVER_PATH = "/gpu_driver/"; +static void *libvulkanHandle; + +static json loadDriverMeta() +{ + std::string fullPath = getFilesPath() + DRIVER_PATH + "meta.json"; + FILE *f = nowide::fopen(fullPath.c_str(), "rt"); + if (f == nullptr) { + WARN_LOG(RENDERER, "Can't open %s", fullPath.c_str()); + return json{}; + } + std::string content(4096, '\0'); + size_t l = fread(content.data(), 1, content.size(), f); + fclose(f); + if (l <= 0) { + WARN_LOG(RENDERER, "Can't read %s", fullPath.c_str()); + return json{}; + } + content.resize(l); + try { + return json::parse(content); + } catch (const json::exception& e) { + WARN_LOG(COMMON, "Corrupted meta.json file: %s", e.what()); + return json{}; + } +} + +static std::string getLibraryName() +{ + json v = loadDriverMeta(); + std::string name; + try { + v.at("libraryName").get_to(name); + } catch (const json::exception& e) { + } + return name; +} + +PFN_vkGetInstanceProcAddr loadVulkanDriver() +{ + // If the user has selected a custom driver, try to load it + if (config::CustomGpuDriver) + { + std::string libName = getLibraryName(); + if (!libName.empty()) + { + std::string driverPath = getFilesPath() + DRIVER_PATH; + std::string tmpLibDir = getFilesPath() + "/tmp/"; + mkdir(tmpLibDir.c_str(), 0755); + //std::string redirectDir = get_writable_data_path(""); + libvulkanHandle = adrenotools_open_libvulkan( + RTLD_NOW | RTLD_LOCAL, + ADRENOTOOLS_DRIVER_CUSTOM /* | ADRENOTOOLS_DRIVER_FILE_REDIRECT */, + tmpLibDir.c_str(), + getNativeLibraryPath().c_str(), + driverPath.c_str(), + libName.c_str(), + nullptr, //redirectDir.c_str(), + nullptr); + if (libvulkanHandle == nullptr) { + char *error = dlerror(); + WARN_LOG(RENDERER, "Failed to load custom Vulkan driver %s%s: %s", driverPath.c_str(), libName.c_str(), error ? error : ""); + } + } + } + if (libvulkanHandle == nullptr) + { + libvulkanHandle = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); + if (libvulkanHandle == nullptr) + { + char *error = dlerror(); + WARN_LOG(RENDERER, "Failed to load system Vulkan driver: %s", error ? error : ""); + return nullptr; + } + } + + return reinterpret_cast(dlsym(libvulkanHandle, "vkGetInstanceProcAddr")); +} + +void unloadVulkanDriver() +{ + if (libvulkanHandle != nullptr) { + dlclose(libvulkanHandle); + libvulkanHandle = nullptr; + } +} + +bool getCustomGpuDriverInfo(std::string& name, std::string& description, std::string& vendor, std::string& version) +{ + json j = loadDriverMeta(); + try { + j.at("name").get_to(name); + } catch (const json::exception& e) { + return false; + } + try { + j.at("description").get_to(description); + } catch (const json::exception& e) { + description = ""; + } + try { + j.at("vendor").get_to(vendor); + } catch (const json::exception& e) { + vendor = ""; + } + try { + j.at("driverVersion").get_to(version); + } catch (const json::exception& e) { + version = ""; + } + + return true; +} + +void uploadCustomGpuDriver(const std::string& zipPath) +{ + FILE *zipf = nowide::fopen(zipPath.c_str(), "rb"); + if (zipf == nullptr) + throw FlycastException("Can't open zip file"); + ZipArchive archive; + if (!archive.Open(zipf)) + throw FlycastException("Invalid zip file"); + std::string fullPath = getFilesPath() + DRIVER_PATH; + flycast::mkdir(fullPath.c_str(), 0755); + // Clean driver directory + DIR *dir = flycast::opendir(fullPath.c_str()); + if (dir != nullptr) + { + while (true) + { + dirent *direntry = flycast::readdir(dir); + if (direntry == nullptr) + break; + std::string name = direntry->d_name; + if (name == "." || name == "..") + continue; + name = fullPath + name; + unlink(name.c_str()); + } + } + // Extract and save files + for (size_t i = 0; ; i++) + { + ArchiveFile *afile = archive.OpenFileByIndex(i); + if (afile == nullptr) + break; + FILE *f = fopen((fullPath + afile->getName()).c_str(), "wb"); + if (f == nullptr) { + delete afile; + throw FlycastException("Can't save files"); + } + u8 buf[8_KB]; + while (true) + { + u32 len = afile->Read(buf, sizeof(buf)); + if (len < 0) + { + fclose(f); + delete afile; + throw FlycastException("Can't read zip"); + } + if (len == 0) + break; + if (fwrite(buf, 1, len, f) != len) + { + fclose(f); + delete afile; + throw FlycastException("Can't save files"); + } + } + fclose(f); + delete afile; + } +} + +#endif // __ANDROID__ && !LIBRETRO && arm64 diff --git a/core/rend/vulkan/adreno.h b/core/rend/vulkan/adreno.h new file mode 100644 index 000000000..261b0af8c --- /dev/null +++ b/core/rend/vulkan/adreno.h @@ -0,0 +1,25 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#pragma once +#include "vulkan.h" + +PFN_vkGetInstanceProcAddr loadVulkanDriver(); +void unloadVulkanDriver(); +bool getCustomGpuDriverInfo(std::string& name, std::string& description, std::string& vendor, std::string& version); +void uploadCustomGpuDriver(const std::string& zipPath); diff --git a/core/rend/vulkan/buffer.h b/core/rend/vulkan/buffer.h index 4f443a75e..0d56238ce 100644 --- a/core/rend/vulkan/buffer.h +++ b/core/rend/vulkan/buffer.h @@ -67,11 +67,11 @@ struct BufferData allocation.UnmapMemory(); } - void *MapMemory() + void *MapMemory() const { return allocation.MapMemory(); } - void UnmapMemory() + void UnmapMemory() const { allocation.UnmapMemory(); } diff --git a/core/rend/vulkan/commandpool.cpp b/core/rend/vulkan/commandpool.cpp index c9424aa62..5e34110d5 100644 --- a/core/rend/vulkan/commandpool.cpp +++ b/core/rend/vulkan/commandpool.cpp @@ -22,6 +22,7 @@ void CommandPool::Init(size_t chainSize) { this->chainSize = chainSize; + device = VulkanContext::Instance()->GetDevice(); if (commandPools.size() > chainSize) { commandPools.resize(chainSize); @@ -31,9 +32,9 @@ void CommandPool::Init(size_t chainSize) { while (commandPools.size() < chainSize) { - commandPools.push_back(VulkanContext::Instance()->GetDevice().createCommandPoolUnique( + commandPools.push_back(device.createCommandPoolUnique( vk::CommandPoolCreateInfo(vk::CommandPoolCreateFlagBits::eTransient, VulkanContext::Instance()->GetGraphicsQueueFamilyIndex()))); - fences.push_back(VulkanContext::Instance()->GetDevice().createFenceUnique(vk::FenceCreateInfo(vk::FenceCreateFlagBits::eSignaled))); + fences.push_back(device.createFenceUnique(vk::FenceCreateInfo(vk::FenceCreateFlagBits::eSignaled))); } } freeBuffers.resize(chainSize); @@ -46,7 +47,7 @@ void CommandPool::Term() if (!fences.empty()) { std::vector allFences = vk::uniqueToRaw(fences); - vk::Result res = VulkanContext::Instance()->GetDevice().waitForFences(allFences, true, UINT64_MAX); + vk::Result res = device.waitForFences(allFences, true, UINT64_MAX); if (res != vk::Result::eSuccess) WARN_LOG(RENDERER, "CommandPool::Term: waitForFences failed %d", (int)res); } @@ -63,15 +64,16 @@ void CommandPool::BeginFrame() return; frameStarted = true; index = (index + 1) % chainSize; - vk::Result res = VulkanContext::Instance()->GetDevice().waitForFences(fences[index].get(), true, UINT64_MAX); + vk::Result res = device.waitForFences(fences[index].get(), true, UINT64_MAX); if (res != vk::Result::eSuccess) WARN_LOG(RENDERER, "CommandPool::BeginFrame: waitForFences failed %d", (int)res); std::vector& inFlightBuf = inFlightBuffers[index]; std::vector& freeBuf = freeBuffers[index]; std::move(inFlightBuf.begin(), inFlightBuf.end(), std::back_inserter(freeBuf)); inFlightBuf.clear(); - VulkanContext::Instance()->GetDevice().resetCommandPool(*commandPools[index], vk::CommandPoolResetFlagBits::eReleaseResources); + device.resetCommandPool(*commandPools[index], vk::CommandPoolResetFlagBits::eReleaseResources); inFlightObjects[index].clear(); + lastBuffers.clear(); } void CommandPool::EndFrame() @@ -80,16 +82,30 @@ void CommandPool::EndFrame() return; frameStarted = false; std::vector commandBuffers = vk::uniqueToRaw(inFlightBuffers[index]); - VulkanContext::Instance()->GetDevice().resetFences(fences[index].get()); + if (!commandBuffers.empty()) + { + // sort buffers: !last, last + size_t len = commandBuffers.size() - 1; + while (len != 0) + { + for (size_t i = 0; i < len; i++) + if (lastBuffers[i] && !lastBuffers[i + 1]) { + std::vector::swap(lastBuffers[i], lastBuffers[i + 1]); + std::swap(commandBuffers[i], commandBuffers[i + 1]); + } + len--; + } + } + device.resetFences(fences[index].get()); VulkanContext::Instance()->SubmitCommandBuffers(commandBuffers, *fences[index]); } -vk::CommandBuffer CommandPool::Allocate() +vk::CommandBuffer CommandPool::Allocate(bool submitLast) { if (freeBuffers[index].empty()) { inFlightBuffers[index].emplace_back(std::move( - VulkanContext::Instance()->GetDevice().allocateCommandBuffersUnique(vk::CommandBufferAllocateInfo(*commandPools[index], vk::CommandBufferLevel::ePrimary, 1)) + device.allocateCommandBuffersUnique(vk::CommandBufferAllocateInfo(*commandPools[index], vk::CommandBufferLevel::ePrimary, 1)) .front())); } else @@ -97,13 +113,14 @@ vk::CommandBuffer CommandPool::Allocate() inFlightBuffers[index].emplace_back(std::move(freeBuffers[index].back())); freeBuffers[index].pop_back(); } + lastBuffers.push_back(submitLast); return *inFlightBuffers[index].back(); } void CommandPool::EndFrameAndWait() { EndFrame(); - vk::Result res = VulkanContext::Instance()->GetDevice().waitForFences(fences[index].get(), true, UINT64_MAX); + vk::Result res = device.waitForFences(fences[index].get(), true, UINT64_MAX); if (res != vk::Result::eSuccess) WARN_LOG(RENDERER, "CommandPool::waitForCommandCompletion: waitForFences failed %d", (int)res); inFlightObjects[index].clear(); diff --git a/core/rend/vulkan/commandpool.h b/core/rend/vulkan/commandpool.h index 3a56d623d..3f42f662a 100644 --- a/core/rend/vulkan/commandpool.h +++ b/core/rend/vulkan/commandpool.h @@ -32,10 +32,9 @@ public: void BeginFrame(); void EndFrame(); void EndFrameAndWait(); - vk::CommandBuffer Allocate(); + vk::CommandBuffer Allocate(bool submitLast = false); - int GetIndex() const - { + int GetIndex() const { return index; } @@ -47,10 +46,12 @@ private: int index = 0; std::vector> freeBuffers; std::vector> inFlightBuffers; + std::vector lastBuffers; std::vector commandPools; std::vector fences; // size should be the same as used by client: 2 for renderer (texCommandPool) size_t chainSize; std::vector>> inFlightObjects; bool frameStarted = false; + vk::Device device{}; }; diff --git a/core/rend/vulkan/drawer.cpp b/core/rend/vulkan/drawer.cpp index 1b6cc7a35..609698545 100644 --- a/core/rend/vulkan/drawer.cpp +++ b/core/rend/vulkan/drawer.cpp @@ -229,7 +229,7 @@ void Drawer::DrawPoly(const vk::CommandBuffer& cmdBuffer, u32 listType, bool sor break; } } - descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, poly, index, *GetMainBuffer(0)->buffer, offset, offsets.lightsOffset, + descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, poly, index, curMainBuffer, offset, offsets.lightsOffset, listType == ListType_Punch_Through); } cmdBuffer.drawIndexed(count, 1, first, 0, 0); @@ -278,8 +278,7 @@ void Drawer::DrawModVols(const vk::CommandBuffer& cmdBuffer, int first, int coun if (count == 0 || pvrrc.modtrig.empty() || !config::ModifierVolumes) return; - vk::Buffer buffer = GetMainBuffer(0)->buffer.get(); - cmdBuffer.bindVertexBuffers(0, buffer, offsets.modVolOffset); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, offsets.modVolOffset); SetScissor(cmdBuffer, baseScissor); ModifierVolumeParam* params = &pvrrc.global_param_mvo[first]; @@ -305,7 +304,7 @@ void Drawer::DrawModVols(const vk::CommandBuffer& cmdBuffer, int first, int coun pipeline = pipelineManager->GetModifierVolumePipeline(ModVolMode::Xor, param.isp.CullMode, param.isNaomi2()); // XOR'ing (closed volume) cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); - descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, param, first + cmv, *GetMainBuffer(0)->buffer, offsets.naomi2ModVolOffset); + descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, param, first + cmv, curMainBuffer, offsets.naomi2ModVolOffset); cmdBuffer.draw(param.count * 3, 1, param.first * 3, 0); @@ -318,7 +317,7 @@ void Drawer::DrawModVols(const vk::CommandBuffer& cmdBuffer, int first, int coun mod_base = -1; } } - cmdBuffer.bindVertexBuffers(0, buffer, {0}); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, {0}); std::array pushConstants = { 1 - FPU_SHAD_SCALE.scale_factor / 256.f, 0, 0, 0, 0 }; cmdBuffer.pushConstants(pipelineManager->GetPipelineLayout(), vk::ShaderStageFlagBits::eFragment, 0, pushConstants); @@ -351,6 +350,7 @@ void Drawer::UploadMainBuffer(const VertexShaderUniforms& vertexUniforms, const BufferData *buffer = GetMainBuffer(packer.size()); packer.upload(*buffer); + curMainBuffer = buffer->buffer.get(); } bool Drawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) @@ -393,14 +393,13 @@ bool Drawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) UploadMainBuffer(vtxUniforms, fragUniforms); // Update per-frame descriptor set and bind it - descriptorSets.updateUniforms(GetMainBuffer(0)->buffer.get(), (u32)offsets.vertexUniformOffset, (u32)offsets.fragmentUniformOffset, + descriptorSets.updateUniforms(curMainBuffer, (u32)offsets.vertexUniformOffset, (u32)offsets.fragmentUniformOffset, fogTexture->GetImageView(), paletteTexture->GetImageView()); descriptorSets.bindPerFrameDescriptorSets(cmdBuffer); // Bind vertex and index buffers - const vk::Buffer buffer = GetMainBuffer(0)->buffer.get(); - cmdBuffer.bindVertexBuffers(0, buffer, {0}); - cmdBuffer.bindIndexBuffer(buffer, offsets.indexOffset, vk::IndexType::eUint32); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, {0}); + cmdBuffer.bindIndexBuffer(curMainBuffer, offsets.indexOffset, vk::IndexType::eUint32); // Make sure to push constants even if not used std::array pushConstants = { 0, 0, 0, 0, 0 }; @@ -430,6 +429,7 @@ bool Drawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) DrawList(cmdBuffer, ListType_Translucent, false, pvrrc.global_param_tr, previous_pass.tr_count, current_pass.tr_count); previous_pass = current_pass; } + curMainBuffer = nullptr; return !pvrrc.isRTT; } @@ -464,7 +464,7 @@ vk::CommandBuffer TextureDrawer::BeginRenderPass() vk::Device device = context->GetDevice(); NewImage(); - vk::CommandBuffer commandBuffer = commandPool->Allocate(); + vk::CommandBuffer commandBuffer = commandPool->Allocate(true); commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); if (!depthAttachment || widthPow2 > depthAttachment->getExtent().width || heightPow2 > depthAttachment->getExtent().height) @@ -614,7 +614,6 @@ void ScreenDrawer::Init(SamplerManager *samplerManager, ShaderManager *shaderMan depthAttachment.reset(); transitionNeeded.clear(); clearNeeded.clear(); - frameRendered = false; } this->viewport = viewport; if (!depthAttachment) @@ -707,36 +706,43 @@ void ScreenDrawer::Init(SamplerManager *samplerManager, ShaderManager *shaderMan vk::CommandBuffer ScreenDrawer::BeginRenderPass() { - NewImage(); - vk::CommandBuffer commandBuffer = commandPool->Allocate(); - commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - - if (transitionNeeded[GetCurrentImage()]) + if (!renderPassStarted) { - setImageLayout(commandBuffer, colorAttachments[GetCurrentImage()]->GetImage(), vk::Format::eR8G8B8A8Unorm, - 1, vk::ImageLayout::eUndefined, - config::EmulateFramebuffer ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal); - transitionNeeded[GetCurrentImage()] = false; - } + NewImage(); + frameRendered = false; + vk::CommandBuffer commandBuffer = commandPool->Allocate(true); + commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - vk::RenderPass renderPass = clearNeeded[GetCurrentImage()] || pvrrc.clearFramebuffer ? *renderPassClear : *renderPassLoad; - clearNeeded[GetCurrentImage()] = false; - const std::array clear_colors = { vk::ClearColorValue(std::array { 0.f, 0.f, 0.f, 1.f }), vk::ClearDepthStencilValue { 0.f, 0 } }; - commandBuffer.beginRenderPass(vk::RenderPassBeginInfo(renderPass, *framebuffers[GetCurrentImage()], - vk::Rect2D( { 0, 0 }, viewport), clear_colors), vk::SubpassContents::eInline); - commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, (float)viewport.width, (float)viewport.height, 1.0f, 0.0f)); + if (transitionNeeded[GetCurrentImage()]) + { + setImageLayout(commandBuffer, colorAttachments[GetCurrentImage()]->GetImage(), vk::Format::eR8G8B8A8Unorm, + 1, vk::ImageLayout::eUndefined, + config::EmulateFramebuffer ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal); + transitionNeeded[GetCurrentImage()] = false; + } + + vk::RenderPass renderPass = clearNeeded[GetCurrentImage()] || pvrrc.clearFramebuffer ? *renderPassClear : *renderPassLoad; + clearNeeded[GetCurrentImage()] = false; + const std::array clear_colors = { vk::ClearColorValue(std::array { 0.f, 0.f, 0.f, 1.f }), vk::ClearDepthStencilValue { 0.f, 0 } }; + commandBuffer.beginRenderPass(vk::RenderPassBeginInfo(renderPass, *framebuffers[GetCurrentImage()], + vk::Rect2D( { 0, 0 }, viewport), clear_colors), vk::SubpassContents::eInline); + currentCommandBuffer = commandBuffer; + renderPassStarted = true; + } + currentCommandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, (float)viewport.width, (float)viewport.height, 1.0f, 0.0f)); matrices.CalcMatrices(&pvrrc, viewport.width, viewport.height); SetBaseScissor(viewport); - commandBuffer.setScissor(0, baseScissor); - currentCommandBuffer = commandBuffer; + currentCommandBuffer.setScissor(0, baseScissor); - return commandBuffer; + return currentCommandBuffer; } void ScreenDrawer::EndRenderPass() { + if (!renderPassStarted) + return; currentCommandBuffer.endRenderPass(); if (config::EmulateFramebuffer) { diff --git a/core/rend/vulkan/drawer.h b/core/rend/vulkan/drawer.h index eb8986220..a22867ed8 100644 --- a/core/rend/vulkan/drawer.h +++ b/core/rend/vulkan/drawer.h @@ -52,6 +52,48 @@ protected: } } + BufferData* GetMainBuffer(u32 size, vk::BufferUsageFlags extraFlags = {}) + { + const vk::BufferUsageFlags usageFlags + { vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eUniformBuffer | extraFlags }; + BufferData *buffer; + if (!mainBuffers.empty()) + { + buffer = mainBuffers.back().release(); + mainBuffers.pop_back(); + if (buffer->bufferSize < size) + { + // FIXME vf4evob still complains about buffer in use after 2 frames. Due to swap chain size of 3 + commandPool->addToFlight(new Deleter(buffer)); + u32 newSize = (u32)buffer->bufferSize; + while (newSize < size) + newSize *= 2; + INFO_LOG(RENDERER, "Increasing main buffer size %zd -> %d", buffer->bufferSize, newSize); + buffer = new BufferData(newSize, usageFlags); + } + } + else { + buffer = new BufferData(std::max(512 * 1024u, size), usageFlags); + } + + class BufferHolder : public Deletable + { + public: + BufferHolder(BufferData *buffer, BaseDrawer *drawer) : buffer(buffer), drawer(drawer) {} + + ~BufferHolder() override { + drawer->mainBuffers.emplace_back(buffer); + } + + private: + BufferData *buffer; + BaseDrawer *drawer; + }; + commandPool->addToFlight(new BufferHolder(buffer, this)); + + return buffer; + } + template T MakeFragmentUniforms() { @@ -167,6 +209,7 @@ protected: vk::Rect2D currentScissor; TransformMatrix matrices; CommandPool *commandPool = nullptr; + std::vector> mainBuffers; }; class Drawer : public BaseDrawer @@ -181,7 +224,9 @@ public: } bool Draw(const Texture *fogTexture, const Texture *paletteTexture); - virtual void EndRenderPass() { renderPass++; } + virtual void EndRenderPass() { + renderPassStarted = false; + } vk::CommandBuffer GetCurrentCommandBuffer() const { return currentCommandBuffer; } protected: @@ -196,7 +241,6 @@ protected: perStripSorting = config::PerStripSorting; pipelineManager->Reset(); } - renderPass = 0; } void Init(SamplerManager *samplerManager, PipelineManager *pipelineManager) @@ -209,29 +253,9 @@ protected: int GetCurrentImage() const { return imageIndex; } - BufferData* GetMainBuffer(u32 size) - { - u32 bufferIndex = imageIndex + renderPass * GetSwapChainSize(); - while (mainBuffers.size() <= bufferIndex) - { - mainBuffers.push_back(std::make_unique(std::max(512 * 1024u, size), - vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eUniformBuffer)); - } - if (mainBuffers[bufferIndex]->bufferSize < size) - { - u32 newSize = (u32)mainBuffers[bufferIndex]->bufferSize; - while (newSize < size) - newSize *= 2; - INFO_LOG(RENDERER, "Increasing main buffer size %d -> %d", (u32)mainBuffers[bufferIndex]->bufferSize, newSize); - commandPool->addToFlight(new Deleter(mainBuffers[bufferIndex].release())); - mainBuffers[bufferIndex] = std::make_unique(newSize, - vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eUniformBuffer); - } - return mainBuffers[bufferIndex].get(); - } - vk::CommandBuffer currentCommandBuffer; SamplerManager *samplerManager = nullptr; + bool renderPassStarted = false; private: void SortTriangles(); @@ -242,7 +266,6 @@ private: void UploadMainBuffer(const VertexShaderUniforms& vertexUniforms, const FragmentShaderUniforms& fragmentUniforms); int imageIndex = 0; - int renderPass = 0; struct { vk::DeviceSize indexOffset = 0; vk::DeviceSize modVolOffset = 0; @@ -256,7 +279,7 @@ private: vk::DeviceSize lightsOffset = 0; } offsets; DescriptorSets descriptorSets; - std::vector> mainBuffers; + vk::Buffer curMainBuffer; PipelineManager *pipelineManager = nullptr; bool perStripSorting = false; bool dithering = false; @@ -282,6 +305,7 @@ public: void EndRenderPass() override; bool PresentFrame() { + EndRenderPass(); if (!frameRendered) return false; frameRendered = false; diff --git a/core/rend/vulkan/oit/oit_drawer.cpp b/core/rend/vulkan/oit/oit_drawer.cpp index ddc538a65..f769f4a87 100644 --- a/core/rend/vulkan/oit/oit_drawer.cpp +++ b/core/rend/vulkan/oit/oit_drawer.cpp @@ -118,7 +118,7 @@ void OITDrawer::DrawPoly(const vk::CommandBuffer& cmdBuffer, u32 listType, bool break; } } - descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, poly, polyNumber, *GetMainBuffer(0)->buffer, offset, offsets.lightsOffset, + descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, poly, polyNumber, curMainBuffer, offset, offsets.lightsOffset, listType == ListType_Punch_Through); } @@ -145,8 +145,7 @@ void OITDrawer::DrawModifierVolumes(const vk::CommandBuffer& cmdBuffer, int firs if (count == 0 || pvrrc.modtrig.empty() || !config::ModifierVolumes) return; - vk::Buffer buffer = GetMainBuffer(0)->buffer.get(); - cmdBuffer.bindVertexBuffers(0, buffer, offsets.modVolOffset); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, offsets.modVolOffset); SetScissor(cmdBuffer, baseScissor); const ModifierVolumeParam *params = &modVolParams[first]; @@ -187,7 +186,7 @@ void OITDrawer::DrawModifierVolumes(const vk::CommandBuffer& cmdBuffer, int firs cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); vk::DeviceSize uniformOffset = Translucent ? offsets.naomi2TrModVolOffset : offsets.naomi2ModVolOffset; - descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, param, first + cmv, *GetMainBuffer(0)->buffer, uniformOffset); + descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, param, first + cmv, curMainBuffer, uniformOffset); cmdBuffer.draw(param.count * 3, 1, param.first * 3, 0); @@ -215,7 +214,7 @@ void OITDrawer::DrawModifierVolumes(const vk::CommandBuffer& cmdBuffer, int firs } } } - cmdBuffer.bindVertexBuffers(0, buffer, {0}); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, {0}); } void OITDrawer::UploadMainBuffer(const OITDescriptorSets::VertexShaderUniforms& vertexUniforms, @@ -259,6 +258,15 @@ void OITDrawer::UploadMainBuffer(const OITDescriptorSets::VertexShaderUniforms& BufferData *buffer = GetMainBuffer(packer.size()); packer.upload(*buffer); + curMainBuffer = buffer->buffer.get(); +} + +vk::Framebuffer OITTextureDrawer::getFramebuffer(int renderPass, int renderPassCount) +{ + if (renderPass < renderPassCount - 1) + return *tempFramebuffers[(renderPassCount - 1 - renderPass) % 2]; + else + return *framebuffer; } bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) @@ -270,6 +278,7 @@ bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) needAttachmentTransition = false; // Not convinced that this is really needed but it makes validation layers happy for (auto& attachment : colorAttachments) + // FIXME should be eTransferSrcOptimal if fullFB (screen) or copy to vram (rtt) -> 1 validation error at startup setImageLayout(cmdBuffer, attachment->GetImage(), vk::Format::eR8G8B8A8Unorm, 1, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); for (auto& attachment : depthAttachments) @@ -322,8 +331,7 @@ bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) quadBuffer->Update(); // Update per-frame descriptor set and bind it - const vk::Buffer mainBuffer = GetMainBuffer(0)->buffer.get(); - descriptorSets.updateUniforms(mainBuffer, (u32)offsets.vertexUniformOffset, (u32)offsets.fragmentUniformOffset, + descriptorSets.updateUniforms(curMainBuffer, (u32)offsets.vertexUniformOffset, (u32)offsets.fragmentUniformOffset, fogTexture->GetImageView(), (u32)offsets.polyParamsOffset, (u32)offsets.polyParamsSize, depthAttachments[0]->GetStencilView(), depthAttachments[0]->GetImageView(), paletteTexture->GetImageView(), oitBuffers); @@ -332,8 +340,8 @@ bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) descriptorSets.updateColorInputDescSet(1, colorAttachments[1]->GetImageView()); // Bind vertex and index buffers - cmdBuffer.bindVertexBuffers(0, mainBuffer, {0}); - cmdBuffer.bindIndexBuffer(mainBuffer, offsets.indexOffset, vk::IndexType::eUint32); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, {0}); + cmdBuffer.bindIndexBuffer(curMainBuffer, offsets.indexOffset, vk::IndexType::eUint32); // Make sure to push constants even if not used OITDescriptorSets::PushConstants pushConstants = { }; @@ -368,11 +376,7 @@ bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) const bool initialPass = render_pass == 0; const bool finalPass = render_pass == (int)pvrrc.render_passes.size() - 1; - vk::Framebuffer targetFramebuffer; - if (!finalPass) - targetFramebuffer = *tempFramebuffers[(pvrrc.render_passes.size() - 1 - render_pass) % 2]; - else - targetFramebuffer = GetFinalFramebuffer(); + vk::Framebuffer targetFramebuffer = getFramebuffer(render_pass, pvrrc.render_passes.size()); cmdBuffer.beginRenderPass( vk::RenderPassBeginInfo(pipelineManager->GetRenderPass(initialPass, finalPass, initialPass && pvrrc.clearFramebuffer), targetFramebuffer, viewport, clear_colors), @@ -402,11 +406,12 @@ bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) // Final subpass cmdBuffer.nextSubpass(vk::SubpassContents::eInline); - descriptorSets.bindColorInputDescSet(cmdBuffer, (pvrrc.render_passes.size() - 1 - render_pass) % 2); + // Bind the input attachment (OP+PT) + descriptorSets.bindColorInputDescSet(cmdBuffer, 1 - getFramebufferIndex()); - if (initialPass && !pvrrc.isRTT && clearNeeded[GetCurrentImage()]) + if (initialPass && !pvrrc.isRTT && clearNeeded[getFramebufferIndex()]) { - clearNeeded[GetCurrentImage()] = false; + clearNeeded[getFramebufferIndex()] = false; SetScissor(cmdBuffer, viewport); cmdBuffer.clearAttachments(vk::ClearAttachment(vk::ImageAspectFlagBits::eColor, 0, clear_colors[0]), vk::ClearRect(viewport, 0, 1)); @@ -443,22 +448,22 @@ bool OITDrawer::Draw(const Texture *fogTexture, const Texture *paletteTexture) if (!finalPass) { // Re-bind vertex and index buffers - cmdBuffer.bindVertexBuffers(0, mainBuffer, {0}); - cmdBuffer.bindIndexBuffer(mainBuffer, offsets.indexOffset, vk::IndexType::eUint32); + cmdBuffer.bindVertexBuffers(0, curMainBuffer, {0}); + cmdBuffer.bindIndexBuffer(curMainBuffer, offsets.indexOffset, vk::IndexType::eUint32); // Tr depth-only pass DrawList(cmdBuffer, ListType_Translucent, current_pass.autosort, Pass::Depth, pvrrc.global_param_tr, previous_pass.tr_count, current_pass.tr_count); - - cmdBuffer.endRenderPass(); } + cmdBuffer.endRenderPass(); previous_pass = current_pass; } + curMainBuffer = nullptr; return !pvrrc.isRTT; } -void OITDrawer::MakeBuffers(int width, int height) +void OITDrawer::MakeBuffers(int width, int height, vk::ImageUsageFlags colorUsage) { oitBuffers->Init(width, height); @@ -467,40 +472,55 @@ void OITDrawer::MakeBuffers(int width, int height) maxWidth = std::max(maxWidth, width); maxHeight = std::max(maxHeight, height); - GetContext()->WaitIdle(); + for (auto& framebuffer : tempFramebuffers) { + if (framebuffer) + commandPool->addToFlight(new Deleter(std::move(framebuffer))); + } + + vk::Device device = GetContext()->GetDevice(); + vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment + | colorUsage; for (auto& attachment : colorAttachments) { - attachment.reset(); + if (attachment) + commandPool->addToFlight(new Deleter(std::move(attachment))); attachment = std::make_unique( - GetContext()->GetPhysicalDevice(), GetContext()->GetDevice()); - attachment->Init(maxWidth, maxHeight, vk::Format::eR8G8B8A8Unorm, - vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, - "COLOR ATTACHMENT " + std::to_string(colorAttachments.size() - 1)); + GetContext()->GetPhysicalDevice(), device); + attachment->Init(maxWidth, maxHeight, vk::Format::eR8G8B8A8Unorm, usage, + "COLOR ATTACHMENT " + std::to_string(&attachment - &colorAttachments[0])); } for (auto& attachment : depthAttachments) { - attachment.reset(); + if (attachment) + commandPool->addToFlight(new Deleter(std::move(attachment))); attachment = std::make_unique( - GetContext()->GetPhysicalDevice(), GetContext()->GetDevice()); + GetContext()->GetPhysicalDevice(), device); attachment->Init(maxWidth, maxHeight, GetContext()->GetDepthFormat(), vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eInputAttachment, - "DEPTH ATTACHMENT " + std::to_string(colorAttachments.size() - 1)); + "DEPTH ATTACHMENT" + std::to_string(&attachment - &depthAttachments[0])); } needAttachmentTransition = true; std::array attachments = { - colorAttachments[1]->GetImageView(), colorAttachments[0]->GetImageView(), + colorAttachments[1]->GetImageView(), depthAttachments[0]->GetImageView(), depthAttachments[1]->GetImageView(), }; vk::FramebufferCreateInfo createInfo(vk::FramebufferCreateFlags(), pipelineManager->GetRenderPass(true, true), attachments, maxWidth, maxHeight, 1); - tempFramebuffers[0] = GetContext()->GetDevice().createFramebufferUnique(createInfo); + tempFramebuffers[0] = device.createFramebufferUnique(createInfo); attachments[0] = attachments[1]; - attachments[1] = colorAttachments[1]->GetImageView(); - tempFramebuffers[1] = GetContext()->GetDevice().createFramebufferUnique(createInfo); + attachments[1] = colorAttachments[0]->GetImageView(); + tempFramebuffers[1] = device.createFramebufferUnique(createInfo); +} + +vk::Framebuffer OITScreenDrawer::getFramebuffer(int renderPass, int renderPassCount) +{ + framebufferIndex = 1 - framebufferIndex; + vk::Framebuffer framebuffer = tempFramebuffers[framebufferIndex].get(); + return framebuffer; } void OITScreenDrawer::MakeFramebuffers(const vk::Extent2D& viewport) @@ -509,35 +529,12 @@ void OITScreenDrawer::MakeFramebuffers(const vk::Extent2D& viewport) this->viewport.offset.y = 0; this->viewport.extent = viewport; - MakeBuffers(viewport.width, viewport.height); - framebuffers.clear(); - finalColorAttachments.clear(); - transitionNeeded.clear(); - clearNeeded.clear(); + // make sure all attachments have the same dimensions + maxWidth = 0; + maxHeight = 0; + MakeBuffers(viewport.width, viewport.height, config::EmulateFramebuffer ? vk::ImageUsageFlagBits::eTransferSrc : vk::ImageUsageFlagBits::eSampled); - vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment; - if (config::EmulateFramebuffer) - usage |= vk::ImageUsageFlagBits::eTransferSrc; - else - usage |= vk::ImageUsageFlagBits::eSampled; - while (finalColorAttachments.size() < GetSwapChainSize()) - { - finalColorAttachments.push_back(std::make_unique( - GetContext()->GetPhysicalDevice(), GetContext()->GetDevice())); - finalColorAttachments.back()->Init(viewport.width, viewport.height, vk::Format::eR8G8B8A8Unorm, - usage, "FINAL ATTACHMENT " + std::to_string(finalColorAttachments.size() - 1)); - std::array attachments = { - finalColorAttachments.back()->GetImageView(), - colorAttachments[0]->GetImageView(), - depthAttachments[0]->GetImageView(), - depthAttachments[1]->GetImageView(), - }; - vk::FramebufferCreateInfo createInfo(vk::FramebufferCreateFlags(), screenPipelineManager->GetRenderPass(true, true), - attachments, viewport.width, viewport.height, 1); - framebuffers.push_back(GetContext()->GetDevice().createFramebufferUnique(createInfo)); - transitionNeeded.push_back(true); - clearNeeded.push_back(true); - } + clearNeeded = { true, true }; } vk::CommandBuffer OITTextureDrawer::NewFrame() @@ -561,10 +558,10 @@ vk::CommandBuffer OITTextureDrawer::NewFrame() VulkanContext *context = GetContext(); vk::Device device = context->GetDevice(); - vk::CommandBuffer commandBuffer = commandPool->Allocate(); + vk::CommandBuffer commandBuffer = commandPool->Allocate(true); commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - MakeBuffers(widthPow2, heightPow2); + MakeBuffers(widthPow2, heightPow2, config::RenderToTextureBuffer ? vk::ImageUsageFlagBits::eTransferSrc : vk::ImageUsageFlagBits::eSampled); vk::ImageView colorImageView; vk::ImageLayout colorImageCurrentLayout; @@ -623,12 +620,13 @@ vk::CommandBuffer OITTextureDrawer::NewFrame() std::array imageViews = { colorImageView, - colorAttachments[0]->GetImageView(), + colorAttachments[1]->GetImageView(), depthAttachments[0]->GetImageView(), depthAttachments[1]->GetImageView(), }; - framebuffers.resize(GetSwapChainSize()); - framebuffers[GetCurrentImage()] = device.createFramebufferUnique(vk::FramebufferCreateInfo(vk::FramebufferCreateFlags(), + if (framebuffer) + commandPool->addToFlight(new Deleter(std::move(framebuffer))); + framebuffer = device.createFramebufferUnique(vk::FramebufferCreateInfo(vk::FramebufferCreateFlags(), rttPipelineManager->GetRenderPass(true, true), imageViews, widthPow2, heightPow2, 1)); commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, (float)upscaledWidth, (float)upscaledHeight, 1.0f, 0.0f)); @@ -644,8 +642,6 @@ vk::CommandBuffer OITTextureDrawer::NewFrame() void OITTextureDrawer::EndFrame() { - currentCommandBuffer.endRenderPass(); - u32 clippedWidth = pvrrc.getFramebufferWidth(); u32 clippedHeight = pvrrc.getFramebufferHeight(); @@ -692,30 +688,24 @@ void OITTextureDrawer::EndFrame() texture->dirty = 0; texture->unprotectVRam(); } - OITDrawer::EndFrame(); } vk::CommandBuffer OITScreenDrawer::NewFrame() { - frameRendered = false; - NewImage(); - vk::CommandBuffer commandBuffer = commandPool->Allocate(); - commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - - if (transitionNeeded[GetCurrentImage()]) + if (!frameStarted) { - setImageLayout(commandBuffer, finalColorAttachments[GetCurrentImage()]->GetImage(), vk::Format::eR8G8B8A8Unorm, 1, - vk::ImageLayout::eUndefined, - config::EmulateFramebuffer ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal); - transitionNeeded[GetCurrentImage()] = false; + frameStarted = true; + frameRendered = false; + NewImage(); + currentCommandBuffer = commandPool->Allocate(true); + currentCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); } matrices.CalcMatrices(&pvrrc, viewport.extent.width, viewport.extent.height); SetBaseScissor(viewport.extent); - commandBuffer.setScissor(0, baseScissor); - commandBuffer.setViewport(0, vk::Viewport((float)viewport.offset.x, (float)viewport.offset.y, (float)viewport.extent.width, (float)viewport.extent.height, 1.0f, 0.0f)); - currentCommandBuffer = commandBuffer; + currentCommandBuffer.setScissor(0, baseScissor); + currentCommandBuffer.setViewport(0, vk::Viewport((float)viewport.offset.x, (float)viewport.offset.y, (float)viewport.extent.width, (float)viewport.extent.height, 1.0f, 0.0f)); - return commandBuffer; + return currentCommandBuffer; } diff --git a/core/rend/vulkan/oit/oit_drawer.h b/core/rend/vulkan/oit/oit_drawer.h index 382a43b20..0376127e4 100644 --- a/core/rend/vulkan/oit/oit_drawer.h +++ b/core/rend/vulkan/oit/oit_drawer.h @@ -41,10 +41,9 @@ public: bool Draw(const Texture *fogTexture, const Texture *paletteTexture); virtual vk::CommandBuffer NewFrame() = 0; - virtual void EndFrame() { renderPass++; }; + virtual void EndFrame() = 0; protected: - u32 GetSwapChainSize() { return 2; } void Init(SamplerManager *samplerManager, OITPipelineManager *pipelineManager, OITBuffers *oitBuffers) { this->pipelineManager = pipelineManager; @@ -73,48 +72,26 @@ protected: maxHeight = 0; } - int GetCurrentImage() const { return imageIndex; } - - void NewImage() - { + void NewImage() { descriptorSets.nextFrame(); - imageIndex = (imageIndex + 1) % GetSwapChainSize(); - renderPass = 0; } - BufferData* GetMainBuffer(u32 size) - { - u32 bufferIndex = imageIndex + renderPass * GetSwapChainSize(); - while (mainBuffers.size() <= bufferIndex) - { - mainBuffers.push_back(std::make_unique(std::max(512 * 1024u, size), - vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eUniformBuffer - | vk::BufferUsageFlagBits::eStorageBuffer)); - } - if (mainBuffers[bufferIndex]->bufferSize < size) - { - u32 newSize = (u32)mainBuffers[bufferIndex]->bufferSize; - while (newSize < size) - newSize *= 2; - INFO_LOG(RENDERER, "Increasing main buffer size %d -> %d", (u32)mainBuffers[bufferIndex]->bufferSize, newSize); - // FIXME vf4evob still complains about buffer in use after 2 frames! due to swap chain size of 3 - // even releasing using the vk context doesn't work - commandPool->addToFlight(new Deleter(mainBuffers[bufferIndex].release())); - mainBuffers[bufferIndex] = std::make_unique(newSize, - vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eUniformBuffer - | vk::BufferUsageFlagBits::eStorageBuffer); - } - return mainBuffers[bufferIndex].get(); + BufferData* GetMainBuffer(u32 size) { + return BaseDrawer::GetMainBuffer(size, vk::BufferUsageFlagBits::eStorageBuffer); } - void MakeBuffers(int width, int height); - virtual vk::Framebuffer GetFinalFramebuffer() const = 0; + void MakeBuffers(int width, int height, vk::ImageUsageFlags colorUsage = {}); + virtual vk::Framebuffer getFramebuffer(int renderPass, int renderPassCount) = 0; + virtual int getFramebufferIndex() { return 0; } vk::Rect2D viewport; std::array, 2> colorAttachments; std::array, 2> depthAttachments; + std::array tempFramebuffers; vk::CommandBuffer currentCommandBuffer; std::vector clearNeeded; + int maxWidth = 0; + int maxHeight = 0; private: void DrawPoly(const vk::CommandBuffer& cmdBuffer, u32 listType, bool autosort, Pass pass, @@ -143,18 +120,13 @@ private: std::unique_ptr quadBuffer; - std::array tempFramebuffers; - OITPipelineManager *pipelineManager = nullptr; SamplerManager *samplerManager = nullptr; OITBuffers *oitBuffers = nullptr; - int maxWidth = 0; - int maxHeight = 0; bool needAttachmentTransition = false; - int imageIndex = 0; - int renderPass = 0; + bool needDepthTransition = false; OITDescriptorSets descriptorSets; - std::vector> mainBuffers; + vk::Buffer curMainBuffer; bool dithering = false; }; @@ -175,8 +147,6 @@ public: void Term() { screenPipelineManager.reset(); - framebuffers.clear(); - finalColorAttachments.clear(); OITDrawer::Term(); } @@ -184,10 +154,11 @@ public: void EndFrame() override { - currentCommandBuffer.endRenderPass(); - if (config::EmulateFramebuffer) - { - scaleAndWriteFramebuffer(currentCommandBuffer, finalColorAttachments[GetCurrentImage()].get()); + if (!frameStarted) + return; + frameStarted = false; + if (config::EmulateFramebuffer) { + scaleAndWriteFramebuffer(currentCommandBuffer, colorAttachments[framebufferIndex].get()); } else { @@ -196,17 +167,17 @@ public: aspectRatio = getOutputFramebufferAspectRatio(); } currentCommandBuffer = nullptr; - OITDrawer::EndFrame(); frameRendered = true; } bool PresentFrame() { + EndFrame(); if (!frameRendered) return false; frameRendered = false; - GetContext()->PresentFrame(finalColorAttachments[GetCurrentImage()]->GetImage(), - finalColorAttachments[GetCurrentImage()]->GetImageView(), viewport.extent, aspectRatio); + GetContext()->PresentFrame(colorAttachments[framebufferIndex]->GetImage(), + colorAttachments[framebufferIndex]->GetImageView(), viewport.extent, aspectRatio); return true; } @@ -214,17 +185,17 @@ public: vk::CommandBuffer GetCurrentCommandBuffer() const { return currentCommandBuffer; } protected: - vk::Framebuffer GetFinalFramebuffer() const override { return *framebuffers[GetCurrentImage()]; } + vk::Framebuffer getFramebuffer(int renderPass, int renderPassCount) override; + int getFramebufferIndex() override { return framebufferIndex; } private: void MakeFramebuffers(const vk::Extent2D& viewport); - std::vector> finalColorAttachments; - std::vector framebuffers; std::unique_ptr screenPipelineManager; - std::vector transitionNeeded; bool frameRendered = false; float aspectRatio = 0.f; + bool frameStarted = false; + int framebufferIndex = 0; }; class OITTextureDrawer : public OITDrawer @@ -242,8 +213,8 @@ public: } void Term() { + framebuffer.reset(); colorAttachment.reset(); - framebuffers.clear(); rttPipelineManager.reset(); OITDrawer::Term(); } @@ -252,7 +223,7 @@ public: protected: vk::CommandBuffer NewFrame() override; - vk::Framebuffer GetFinalFramebuffer() const override { return *framebuffers[GetCurrentImage()]; } + vk::Framebuffer getFramebuffer(int renderPass, int renderPassCount) override; private: u32 textureAddr = 0; @@ -260,8 +231,8 @@ private: Texture *texture = nullptr; vk::Image colorImage; std::unique_ptr colorAttachment; - std::vector framebuffers; std::unique_ptr rttPipelineManager; + vk::UniqueFramebuffer framebuffer; TextureCache *textureCache = nullptr; }; diff --git a/core/rend/vulkan/oit/oit_pipeline.h b/core/rend/vulkan/oit/oit_pipeline.h index 39ae192f2..da32397bb 100644 --- a/core/rend/vulkan/oit/oit_pipeline.h +++ b/core/rend/vulkan/oit/oit_pipeline.h @@ -95,8 +95,8 @@ public: u32 polyParamsOffset, u32 polyParamsSize, vk::ImageView stencilImageView, vk::ImageView depthImageView, vk::ImageView paletteImageView, OITBuffers *oitBuffers) { - if (!perFrameDescSet) - perFrameDescSet = perFrameAlloc.alloc(); + perFrameDescSet = perFrameAlloc.alloc(); + perPolyDescSets.clear(); std::vector bufferInfos; bufferInfos.emplace_back(buffer, vertexUniformOffset, sizeof(VertexShaderUniforms)); @@ -144,8 +144,7 @@ public: void updateColorInputDescSet(int index, vk::ImageView colorImageView) { - if (!colorInputDescSets[index]) - colorInputDescSets[index] = colorInputAlloc.alloc(); + colorInputDescSets[index] = colorInputAlloc.alloc(); vk::DescriptorImageInfo colorImageInfo(vk::Sampler(), colorImageView, vk::ImageLayout::eShaderReadOnlyOptimal); vk::WriteDescriptorSet writeDescriptorSet(colorInputDescSets[index], 0, 0, vk::DescriptorType::eInputAttachment, colorImageInfo); diff --git a/core/rend/vulkan/oit/oit_renderer.cpp b/core/rend/vulkan/oit/oit_renderer.cpp index d85547aea..a8d3d37c0 100644 --- a/core/rend/vulkan/oit/oit_renderer.cpp +++ b/core/rend/vulkan/oit/oit_renderer.cpp @@ -53,6 +53,7 @@ public: { DEBUG_LOG(RENDERER, "OITVulkanRenderer::Term"); GetContext()->WaitIdle(); + texCommandPool.Term(); screenDrawer.Term(); textureDrawer.Term(); oitBuffers.Term(); @@ -61,6 +62,13 @@ public: BaseVulkanRenderer::Term(); } + void Process(TA_context* ctx) override + { + if (ctx->rend.isRTT) + screenDrawer.EndFrame(); + BaseVulkanRenderer::Process(ctx); + } + bool Render() override { try { @@ -81,7 +89,8 @@ public: } drawer->Draw(fogTexture.get(), paletteTexture.get()); - drawer->EndFrame(); + if (config::EmulateFramebuffer || pvrrc.isRTT) + drawer->EndFrame(); return !pvrrc.isRTT; } catch (const vk::SystemError& e) { diff --git a/core/rend/vulkan/oit/oit_renderpass.cpp b/core/rend/vulkan/oit/oit_renderpass.cpp index b19f6fc8c..db198206a 100644 --- a/core/rend/vulkan/oit/oit_renderpass.cpp +++ b/core/rend/vulkan/oit/oit_renderpass.cpp @@ -22,15 +22,16 @@ vk::UniqueRenderPass RenderPasses::MakeRenderPass(bool initial, bool last, bool loadClear) { + vk::AttachmentDescription attach0 = GetAttachment0Description(initial, last, loadClear); std::array attachmentDescriptions = { // Swap chain image - GetAttachment0Description(initial, last, loadClear), + attach0, // OP+PT color attachment vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR8G8B8A8Unorm, vk::SampleCountFlagBits::e1, initial ? vk::AttachmentLoadOp::eClear : vk::AttachmentLoadOp::eLoad, last ? vk::AttachmentStoreOp::eDontCare : vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, - initial ? vk::ImageLayout::eUndefined : vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eShaderReadOnlyOptimal), + initial ? vk::ImageLayout::eUndefined : attach0.initialLayout, attach0.finalLayout), // OP+PT depth attachment vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), GetContext()->GetDepthFormat(), vk::SampleCountFlagBits::e1, initial ? vk::AttachmentLoadOp::eClear : vk::AttachmentLoadOp::eLoad, diff --git a/core/rend/vulkan/oit/oit_renderpass.h b/core/rend/vulkan/oit/oit_renderpass.h index fb400e838..e51ee797b 100644 --- a/core/rend/vulkan/oit/oit_renderpass.h +++ b/core/rend/vulkan/oit/oit_renderpass.h @@ -46,16 +46,20 @@ protected: return vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR8G8B8A8Unorm, vk::SampleCountFlagBits::e1, loadClear ? vk::AttachmentLoadOp::eClear : vk::AttachmentLoadOp::eLoad, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, - config::EmulateFramebuffer && last ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal, + config::EmulateFramebuffer && initial ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal, config::EmulateFramebuffer && last ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal); } virtual std::vector GetSubpassDependencies() const { - std::vector deps; - deps.emplace_back(2, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader, - vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eShaderRead, vk::DependencyFlagBits::eByRegion); - return deps; + if (config::EmulateFramebuffer) + return { { 2, VK_SUBPASS_EXTERNAL, + vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eTransfer | vk::PipelineStageFlagBits::eHost, + vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eHostRead, vk::DependencyFlagBits::eByRegion } }; + else + return { { 2, VK_SUBPASS_EXTERNAL, + vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader, + vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eShaderRead, vk::DependencyFlagBits::eByRegion } }; } private: @@ -76,14 +80,12 @@ protected: std::vector GetSubpassDependencies() const override { - std::vector deps; if (config::RenderToTextureBuffer) - deps.emplace_back(2, VK_SUBPASS_EXTERNAL, + return { { 2, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eTransfer | vk::PipelineStageFlagBits::eHost, - vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eHostRead); + vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eHostRead } }; else - deps.emplace_back(2, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader, - vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eShaderRead); - return deps; + return { { 2, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader, + vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eShaderRead } }; } }; diff --git a/core/rend/vulkan/overlay.cpp b/core/rend/vulkan/overlay.cpp index 0cbbdc74b..296df3c50 100644 --- a/core/rend/vulkan/overlay.cpp +++ b/core/rend/vulkan/overlay.cpp @@ -41,6 +41,7 @@ void VulkanOverlay::Init(QuadPipeline *pipeline) } xhairDrawer = std::make_unique(); xhairDrawer->Init(pipeline); + vmuLastChanged.fill({}); } void VulkanOverlay::Term() @@ -81,7 +82,7 @@ void VulkanOverlay::Prepare(vk::CommandBuffer cmdBuffer, bool vmu, bool crosshai } continue; } - if (texture != nullptr && !vmu_lcd_changed[i]) + if (texture != nullptr && ::vmuLastChanged[i] == this->vmuLastChanged[i]) continue; if (texture) @@ -90,7 +91,7 @@ void VulkanOverlay::Prepare(vk::CommandBuffer cmdBuffer, bool vmu, bool crosshai #ifdef VK_DEBUG VulkanContext::Instance()->setObjectName((VkImageView)texture->GetImageView(), vk::ImageView::objectType, "VMU " + std::to_string(i)); #endif - vmu_lcd_changed[i] = false; + this->vmuLastChanged[i] = ::vmuLastChanged[i]; } } if (crosshair && !xhairTexture) diff --git a/core/rend/vulkan/overlay.h b/core/rend/vulkan/overlay.h index fa0fab75f..ac222747f 100644 --- a/core/rend/vulkan/overlay.h +++ b/core/rend/vulkan/overlay.h @@ -45,6 +45,7 @@ private: std::array, 8> vmuTextures; std::vector commandBuffers; std::array, 8> drawers; + std::array vmuLastChanged {}; QuadPipeline *pipeline = nullptr; std::unique_ptr xhairTexture; diff --git a/core/rend/vulkan/pipeline.h b/core/rend/vulkan/pipeline.h index 72e32c74d..698984236 100644 --- a/core/rend/vulkan/pipeline.h +++ b/core/rend/vulkan/pipeline.h @@ -41,8 +41,8 @@ public: } void updateUniforms(vk::Buffer buffer, u32 vertexUniformOffset, u32 fragmentUniformOffset, vk::ImageView fogImageView, vk::ImageView paletteImageView) { - if (!perFrameDescSet) - perFrameDescSet = perFrameAlloc.alloc(); + perFrameDescSet = perFrameAlloc.alloc(); + perPolyDescSets.clear(); std::vector bufferInfos; bufferInfos.emplace_back(buffer, vertexUniformOffset, sizeof(VertexShaderUniforms)); diff --git a/core/rend/vulkan/texture.h b/core/rend/vulkan/texture.h index fc8ece958..9a1753f11 100644 --- a/core/rend/vulkan/texture.h +++ b/core/rend/vulkan/texture.h @@ -212,9 +212,14 @@ public: void Clear() { - BaseTextureCache::Clear(); + VulkanContext *context = VulkanContext::Instance(); for (auto& set : inFlightTextures) + { + for (Texture *tex : set) + tex->deferDeleteResource(context); set.clear(); + } + BaseTextureCache::Clear(); } private: diff --git a/core/rend/vulkan/vk_context_lr.cpp b/core/rend/vulkan/vk_context_lr.cpp index 3a293151a..4f999de65 100644 --- a/core/rend/vulkan/vk_context_lr.cpp +++ b/core/rend/vulkan/vk_context_lr.cpp @@ -126,7 +126,7 @@ bool VkCreateDevice(retro_vulkan_context* context, VkInstance instance, VkPhysic vk::PhysicalDeviceFeatures supportedFeatures; physicalDevice.getFeatures(&supportedFeatures); - bool fragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics; + VulkanContext::Instance()->fragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics; VulkanContext::Instance()->samplerAnisotropy = supportedFeatures.samplerAnisotropy; // Enable VK_KHR_dedicated_allocation if available @@ -157,7 +157,7 @@ bool VkCreateDevice(retro_vulkan_context* context, VkInstance instance, VkPhysic vk::DeviceQueueCreateInfo(vk::DeviceQueueCreateFlags(), context->presentation_queue_family_index, 1, &queuePriority), }; vk::PhysicalDeviceFeatures features(*required_features); - if (fragmentStoresAndAtomics) + if (VulkanContext::Instance()->fragmentStoresAndAtomics) features.fragmentStoresAndAtomics = true; if (VulkanContext::Instance()->samplerAnisotropy) features.samplerAnisotropy = true; @@ -373,7 +373,7 @@ void VulkanContext::beginFrame(vk::Extent2D extent) } commandPool.BeginFrame(); const std::array clear_colors = { getBorderColor(), vk::ClearDepthStencilValue{ 0.f, 0 } }; - cmdBuffer = commandPool.Allocate(); + cmdBuffer = commandPool.Allocate(true); cmdBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); overlay->Prepare(cmdBuffer, true, true); diff --git a/core/rend/vulkan/vk_context_lr.h b/core/rend/vulkan/vk_context_lr.h index 8fa3f9de9..6202ec518 100644 --- a/core/rend/vulkan/vk_context_lr.h +++ b/core/rend/vulkan/vk_context_lr.h @@ -27,6 +27,7 @@ #include "wsi/context.h" #include "commandpool.h" #include "overlay.h" +#include static vk::Format findDepthFormat(vk::PhysicalDevice physicalDevice); @@ -43,6 +44,7 @@ public: u32 GetGraphicsQueueFamilyIndex() const { return retro_render_if->queue_index; } void PresentFrame(vk::Image image, vk::ImageView imageView, const vk::Extent2D& extent, float aspectRatio); + bool GetLastFrame(std::vector& data, int& width, int& height) { return false; } vk::PhysicalDevice GetPhysicalDevice() const { return physicalDevice; } vk::Device GetDevice() const { return device; } @@ -89,6 +91,7 @@ public: static VulkanContext *Instance() { return contextInstance; } bool SupportsSamplerAnisotropy() const { return samplerAnisotropy; } bool SupportsDedicatedAllocation() const { return dedicatedAllocationSupported; } + bool hasPerPixel() override { return fragmentStoresAndAtomics; } const VMAllocator& GetAllocator() const { return allocator; } vk::DeviceSize GetMaxMemoryAllocationSize() const { return maxMemoryAllocationSize; } f32 GetMaxSamplerAnisotropy() const { return samplerAnisotropy ? maxSamplerAnisotropy : 1.f; } @@ -126,6 +129,7 @@ public: bool samplerAnisotropy = false; f32 maxSamplerAnisotropy = 0.f; bool dedicatedAllocationSupported = false; + bool fragmentStoresAndAtomics = false; private: u32 vendorID = 0; diff --git a/core/rend/vulkan/vulkan.h b/core/rend/vulkan/vulkan.h index 63c8b24e5..825e7bc65 100644 --- a/core/rend/vulkan/vulkan.h +++ b/core/rend/vulkan/vulkan.h @@ -51,10 +51,14 @@ template class Deleter : public Deletable { public: - Deleter(T *p) : p(p) {} + Deleter() = delete; + explicit Deleter(T& o) : o(o) {} + Deleter(T&& o) : o(std::move(o)) {} ~Deleter() override { - delete p; + if constexpr (std::is_pointer_v) + delete o; } + private: - T *p; + T o; }; diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index a46d48ca7..f10e0fde2 100644 --- a/core/rend/vulkan/vulkan_context.cpp +++ b/core/rend/vulkan/vulkan_context.cpp @@ -22,7 +22,7 @@ #include "vulkan_renderer.h" #include "imgui.h" #include "imgui_impl_vulkan.h" -#include "../gui.h" +#include "ui/gui.h" #ifdef USE_SDL #include #include @@ -33,6 +33,9 @@ #include "oslib/oslib.h" #include "vulkan_driver.h" #include "rend/transform_matrix.h" +#if defined(__ANDROID__) && HOST_CPU == CPU_ARM64 +#include "adreno.h" +#endif #if VULKAN_HPP_DISPATCH_LOADER_DYNAMIC == 1 VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE @@ -139,8 +142,13 @@ bool VulkanContext::InitInstance(const char** extensions, uint32_t extensions_co try { #if VULKAN_HPP_DISPATCH_LOADER_DYNAMIC == 1 + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; +#if defined(__ANDROID__) && HOST_CPU == CPU_ARM64 + vkGetInstanceProcAddr = loadVulkanDriver(); +#else static vk::DynamicLoader dl; - PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); + vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); +#endif if (vkGetInstanceProcAddr == nullptr) { ERROR_LOG(RENDERER, "Vulkan entry point vkGetInstanceProcAddr not found"); return false; @@ -284,8 +292,14 @@ bool VulkanContext::InitInstance(const char** extensions, uint32_t extensions_co void VulkanContext::InitImgui() { - imguiDriver.reset(); - imguiDriver = std::unique_ptr(new VulkanDriver()); + VulkanDriver *vkDriver = dynamic_cast(imguiDriver.get()); + if (vkDriver == nullptr) { + imguiDriver.reset(); + imguiDriver = std::unique_ptr(new VulkanDriver()); + } + else { + vkDriver->reset(); + } ImGui_ImplVulkan_InitInfo initInfo{}; initInfo.Instance = (VkInstance)*instance; initInfo.PhysicalDevice = (VkPhysicalDevice)physicalDevice; @@ -952,6 +966,7 @@ void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, const try { NewFrame(); auto overlayCmdBuffer = PrepareOverlay(config::FloatVMUs, true); + gui_draw_osd(); BeginRenderPass(); @@ -959,6 +974,7 @@ void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, const DrawFrame(imageView, extent, aspectRatio); DrawOverlay(settings.display.uiScale, config::FloatVMUs, true); + imguiDriver->renderDrawData(ImGui::GetDrawData(), false); renderer->DrawOSD(false); EndFrame(overlayCmdBuffer); static_cast(renderer)->RenderVideoRouting(); @@ -1022,10 +1038,6 @@ void VulkanContext::term() renderCompleteSemaphores.clear(); drawFences.clear(); allocator.Term(); -#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) - extern void os_VideoRoutingTermVk(); - os_VideoRoutingTermVk(); -#endif #ifndef USE_SDL surface.reset(); #else @@ -1042,6 +1054,9 @@ void VulkanContext::term() #endif #endif instance.reset(); +#if defined(__ANDROID__) && HOST_CPU == CPU_ARM64 + unloadVulkanDriver(); +#endif } void VulkanContext::DoSwapAutomation() @@ -1217,3 +1232,116 @@ VulkanContext::~VulkanContext() verify(contextInstance == this); contextInstance = nullptr; } + +bool VulkanContext::GetLastFrame(std::vector& data, int& width, int& height) +{ + if (!lastFrameView) + return false; + + if (width != 0) { + height = width / lastFrameAR; + } + else if (height != 0) { + width = lastFrameAR * height; + } + else + { + width = lastFrameExtent.width; + height = lastFrameExtent.height; + if (config::Rotate90) + std::swap(width, height); + // We need square pixels for PNG + int w = lastFrameAR * height; + if (width > w) + height = width / lastFrameAR; + else + width = w; + } + // color attachment + FramebufferAttachment attachment(physicalDevice, *device); + attachment.Init(width, height, vk::Format::eR8G8B8A8Unorm, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, "screenshot"); + // command buffer + vk::UniqueCommandBuffer commandBuffer = std::move(device->allocateCommandBuffersUnique( + vk::CommandBufferAllocateInfo(*commandPools.back(), vk::CommandBufferLevel::ePrimary, 1)).front()); + commandBuffer->begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); + // render pass + vk::AttachmentDescription attachmentDescription = vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR8G8B8A8Unorm, vk::SampleCountFlagBits::e1, + vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal); + vk::AttachmentReference colorReference(0, vk::ImageLayout::eColorAttachmentOptimal); + vk::SubpassDescription subpass(vk::SubpassDescriptionFlags(), vk::PipelineBindPoint::eGraphics, nullptr, colorReference, + nullptr, nullptr); + vk::UniqueRenderPass renderPass = device->createRenderPassUnique(vk::RenderPassCreateInfo(vk::RenderPassCreateFlags(), + attachmentDescription, subpass)); + // framebuffer + vk::ImageView imageView = attachment.GetImageView(); + vk::UniqueFramebuffer framebuffer = device->createFramebufferUnique(vk::FramebufferCreateInfo(vk::FramebufferCreateFlags(), + *renderPass, imageView, width, height, 1)); + vk::ClearValue clearValue; + commandBuffer->beginRenderPass(vk::RenderPassBeginInfo(*renderPass, *framebuffer, vk::Rect2D({0, 0}, {(u32)width, (u32)height}), clearValue), + vk::SubpassContents::eInline); + + // Pipeline + QuadPipeline pipeline(true, config::Rotate90); + pipeline.Init(shaderManager.get(), *renderPass, 0); + pipeline.BindPipeline(*commandBuffer); + + // Draw + QuadVertex vtx[] { + { -1, -1, 0, 0, 0 }, + { 1, -1, 0, 1, 0 }, + { -1, 1, 0, 0, 1 }, + { 1, 1, 0, 1, 1 }, + }; + + vk::Viewport viewport(0, 0, width, height); + commandBuffer->setViewport(0, viewport); + commandBuffer->setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), vk::Extent2D(width, height))); + QuadDrawer drawer; + drawer.Init(&pipeline); + drawer.Draw(*commandBuffer, lastFrameView, vtx, false); + commandBuffer->endRenderPass(); + + // Copy back + vk::BufferImageCopy copyRegion(0, width, height, vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1), vk::Offset3D(0, 0, 0), + vk::Extent3D(width, height, 1)); + commandBuffer->copyImageToBuffer(attachment.GetImage(), vk::ImageLayout::eTransferSrcOptimal, + *attachment.GetBufferData()->buffer, copyRegion); + + vk::BufferMemoryBarrier bufferMemoryBarrier( + vk::AccessFlagBits::eTransferWrite, + vk::AccessFlagBits::eHostRead, + VK_QUEUE_FAMILY_IGNORED, + VK_QUEUE_FAMILY_IGNORED, + *attachment.GetBufferData()->buffer, + 0, + VK_WHOLE_SIZE); + commandBuffer->pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eHost, {}, nullptr, bufferMemoryBarrier, nullptr); + commandBuffer->end(); + + vk::UniqueFence fence = device->createFenceUnique(vk::FenceCreateInfo()); + vk::SubmitInfo submitInfo(nullptr, nullptr, commandBuffer.get(), nullptr); + graphicsQueue.submit(submitInfo, *fence); + + vk::Result res = device->waitForFences(fence.get(), true, UINT64_MAX); + if (res != vk::Result::eSuccess) + WARN_LOG(RENDERER, "VulkanContext::GetLastFrame: waitForFences failed %d", (int)res); + + const u8 *img = (const u8 *)attachment.GetBufferData()->MapMemory(); + data.clear(); + data.reserve(width * height * 3); + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + data.push_back(*img++); + data.push_back(*img++); + data.push_back(*img++); + img++; + } + } + attachment.GetBufferData()->UnmapMemory(); + + return true; +} diff --git a/core/rend/vulkan/vulkan_context.h b/core/rend/vulkan/vulkan_context.h index f17fe4093..c2b000b49 100644 --- a/core/rend/vulkan/vulkan_context.h +++ b/core/rend/vulkan/vulkan_context.h @@ -38,6 +38,7 @@ public: #include "rend/TexCache.h" #include "overlay.h" #include "wsi/context.h" +#include struct ImDrawData; @@ -60,6 +61,7 @@ public: void Present() noexcept; void PresentFrame(vk::Image image, vk::ImageView imageView, const vk::Extent2D& extent, float aspectRatio) noexcept; void PresentLastFrame(); + bool GetLastFrame(std::vector& data, int& width, int& height); vk::PhysicalDevice GetPhysicalDevice() const { return physicalDevice; } vk::Device GetDevice() const { return *device; } diff --git a/core/rend/vulkan/vulkan_driver.h b/core/rend/vulkan/vulkan_driver.h index ea4dd5b71..47b142853 100644 --- a/core/rend/vulkan/vulkan_driver.h +++ b/core/rend/vulkan/vulkan_driver.h @@ -17,7 +17,7 @@ along with Flycast. If not, see . */ #pragma once -#include "rend/imgui_driver.h" +#include "ui/imgui_driver.h" #include "imgui_impl_vulkan.h" #include "vulkan_context.h" #include "texture.h" @@ -26,10 +26,18 @@ class VulkanDriver final : public ImGuiDriver { public: - ~VulkanDriver() { + void reset() override + { + ImGuiDriver::reset(); textures.clear(); linearSampler.reset(); + pointSampler.reset(); ImGui_ImplVulkan_Shutdown(); + justStarted = true; + } + + ~VulkanDriver() { + reset(); } void newFrame() override { @@ -44,32 +52,25 @@ public: try { bool rendering = context->IsRendering(); if (!rendering) - { - if (context->recreateSwapChainIfNeeded()) - return; - context->NewFrame(); - } - vk::CommandBuffer vmuCmdBuffer{}; + context->NewFrame(); // may reset this driver if (!rendering || newFrameStarted) { - vmuCmdBuffer = getContext()->PrepareOverlay(true, false); context->BeginRenderPass(); context->PresentLastFrame(); - context->DrawOverlay(settings.display.uiScale, true, false); } if (!justStarted) // Record Imgui Draw Data and draw funcs into command buffer ImGui_ImplVulkan_RenderDrawData(drawData, (VkCommandBuffer)getCommandBuffer()); justStarted = false; if (!rendering || newFrameStarted) - context->EndFrame(vmuCmdBuffer); + context->EndFrame(); newFrameStarted = false; } catch (const InvalidVulkanContext& err) { } } void present() override { - getContext()->Present(); // may destroy this driver + getContext()->Present(); // may reset this driver } ImTextureID getTexture(const std::string& name) override { @@ -80,26 +81,47 @@ public: return ImTextureID{}; } - ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) override + ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) override { VkTexture vkTex(std::make_unique()); vkTex.texture->tex_type = TextureType::_8888; vkTex.texture->SetCommandBuffer(getCommandBuffer()); vkTex.texture->UploadToGPU(width, height, data, false); vkTex.texture->SetCommandBuffer(nullptr); - if (!linearSampler) + VkSampler sampler; + if (nearestSampling) { - linearSampler = getContext()->GetDevice().createSamplerUnique( - vk::SamplerCreateInfo(vk::SamplerCreateFlags(), - vk::Filter::eLinear, vk::Filter::eLinear, - vk::SamplerMipmapMode::eLinear, - vk::SamplerAddressMode::eClampToBorder, - vk::SamplerAddressMode::eClampToBorder, - vk::SamplerAddressMode::eClampToEdge, 0.0f, false, - 0.f, false, vk::CompareOp::eNever, 0.0f, VK_LOD_CLAMP_NONE, - vk::BorderColor::eFloatTransparentBlack)); + if (!pointSampler) + { + pointSampler = getContext()->GetDevice().createSamplerUnique( + vk::SamplerCreateInfo(vk::SamplerCreateFlags(), + vk::Filter::eNearest, vk::Filter::eNearest, + vk::SamplerMipmapMode::eNearest, + vk::SamplerAddressMode::eClampToBorder, + vk::SamplerAddressMode::eClampToBorder, + vk::SamplerAddressMode::eClampToEdge, 0.0f, false, + 0.f, false, vk::CompareOp::eNever, 0.0f, VK_LOD_CLAMP_NONE, + vk::BorderColor::eFloatTransparentBlack)); + } + sampler = (VkSampler)*pointSampler; } - ImTextureID texId = vkTex.textureId = ImGui_ImplVulkan_AddTexture((VkSampler)*linearSampler, (VkImageView)vkTex.texture->GetImageView(), + else + { + if (!linearSampler) + { + linearSampler = getContext()->GetDevice().createSamplerUnique( + vk::SamplerCreateInfo(vk::SamplerCreateFlags(), + vk::Filter::eLinear, vk::Filter::eLinear, + vk::SamplerMipmapMode::eLinear, + vk::SamplerAddressMode::eClampToBorder, + vk::SamplerAddressMode::eClampToBorder, + vk::SamplerAddressMode::eClampToEdge, 0.0f, false, + 0.f, false, vk::CompareOp::eNever, 0.0f, VK_LOD_CLAMP_NONE, + vk::BorderColor::eFloatTransparentBlack)); + } + sampler = (VkSampler)*linearSampler; + } + ImTextureID texId = vkTex.textureId = ImGui_ImplVulkan_AddTexture(sampler, (VkImageView)vkTex.texture->GetImageView(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); // TODO update existing texture //auto it = textures.find(name); @@ -111,6 +133,27 @@ public: return texId; } + void deleteTexture(const std::string& name) override + { + auto it = textures.find(name); + if (it != textures.end()) + { + class DescSetDeleter : public Deletable + { + public: + DescSetDeleter(VkDescriptorSet descSet) : descSet(descSet) {} + ~DescSetDeleter() { + ImGui_ImplVulkan_RemoveTexture(descSet); + } + VkDescriptorSet descSet; + }; + getContext()->addToFlight(new DescSetDeleter((VkDescriptorSet)it->second.textureId)); + if (it->second.texture != nullptr) + it->second.texture->deferDeleteResource(getContext()); + textures.erase(it); + } + } + private: struct VkTexture { VkTexture() = default; @@ -140,6 +183,7 @@ private: std::unordered_map textures; vk::UniqueSampler linearSampler; + vk::UniqueSampler pointSampler; bool newFrameStarted = false; bool justStarted = true; }; diff --git a/core/rend/vulkan/vulkan_renderer.cpp b/core/rend/vulkan/vulkan_renderer.cpp index 0586d997a..56f053ad0 100644 --- a/core/rend/vulkan/vulkan_renderer.cpp +++ b/core/rend/vulkan/vulkan_renderer.cpp @@ -21,6 +21,282 @@ #include "vulkan.h" #include "vulkan_renderer.h" #include "drawer.h" +#include "hw/pvr/ta.h" +#include "rend/osd.h" +#include "rend/transform_matrix.h" + +bool BaseVulkanRenderer::BaseInit(vk::RenderPass renderPass, int subpass) +{ + texCommandPool.Init(); + fbCommandPool.Init(); + +#if defined(__ANDROID__) && !defined(LIBRETRO) + if (!vjoyTexture) + { + int w, h; + u8 *image_data = loadOSDButtons(w, h); + texCommandPool.BeginFrame(); + vjoyTexture = std::make_unique(); + vjoyTexture->tex_type = TextureType::_8888; + vk::CommandBuffer cmdBuffer = texCommandPool.Allocate(); + cmdBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); + vjoyTexture->SetCommandBuffer(cmdBuffer); + vjoyTexture->UploadToGPU(w, h, image_data, false); + vjoyTexture->SetCommandBuffer(nullptr); + cmdBuffer.end(); + texCommandPool.EndFrame(); + delete [] image_data; + osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass()); + } + if (!osdBuffer) + { + osdBuffer = std::make_unique(sizeof(OSDVertex) * VJOY_VISIBLE * 4, + vk::BufferUsageFlagBits::eVertexBuffer); + } +#endif + quadPipeline = std::make_unique(false, false); + quadPipeline->Init(&shaderManager, renderPass, subpass); + framebufferDrawer = std::make_unique(); + framebufferDrawer->Init(quadPipeline.get()); + + return true; +} + +void BaseVulkanRenderer::Term() +{ + GetContext()->WaitIdle(); + GetContext()->PresentFrame(nullptr, nullptr, vk::Extent2D(), 0); +#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) + os_VideoRoutingTermVk(); +#endif + framebufferDrawer.reset(); + quadPipeline.reset(); + osdBuffer.reset(); + osdPipeline.Term(); + vjoyTexture.reset(); + textureCache.Clear(); + fogTexture = nullptr; + paletteTexture = nullptr; + texCommandPool.Term(); + fbCommandPool.Term(); + framebufferTextures.clear(); + framebufferTexIndex = 0; + shaderManager.term(); +} + +BaseTextureCacheData *BaseVulkanRenderer::GetTexture(TSP tsp, TCW tcw) +{ + Texture* tf = textureCache.getTextureCacheData(tsp, tcw); + + //update if needed + if (tf->NeedsUpdate()) + { + // This kills performance when a frame is skipped and lots of texture updated each frame + //if (textureCache.IsInFlight(tf, true)) + // textureCache.DestroyLater(tf); + tf->SetCommandBuffer(texCommandBuffer); + if (!tf->Update()) + { + tf->SetCommandBuffer(nullptr); + return nullptr; + } + } + else if (tf->IsCustomTextureAvailable()) + { + tf->deferDeleteResource(&texCommandPool); + tf->SetCommandBuffer(texCommandBuffer); + tf->CheckCustomTexture(); + } + tf->SetCommandBuffer(nullptr); + textureCache.SetInFlight(tf); + + return tf; +} + +void BaseVulkanRenderer::Process(TA_context* ctx) +{ + framebufferRendered = false; + if (KillTex) + textureCache.Clear(); + + texCommandPool.BeginFrame(); + textureCache.SetCurrentIndex(texCommandPool.GetIndex()); + textureCache.Cleanup(); + + texCommandBuffer = texCommandPool.Allocate(); + texCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); + + ta_parse(ctx, true); + + // TODO can't update fog or palette twice in multi render + CheckFogTexture(); + CheckPaletteTexture(); + texCommandBuffer.end(); +} + +void BaseVulkanRenderer::ReInitOSD() +{ + texCommandPool.Init(); + fbCommandPool.Init(); +#if defined(__ANDROID__) && !defined(LIBRETRO) + osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass()); +#endif +} + +void BaseVulkanRenderer::DrawOSD(bool clear_screen) +{ +#ifndef LIBRETRO + if (!vjoyTexture) + return; + try { + if (clear_screen) + { + GetContext()->NewFrame(); + GetContext()->BeginRenderPass(); + GetContext()->PresentLastFrame(); + } + const float dc2s_scale_h = settings.display.height / 480.0f; + const float sidebarWidth = (settings.display.width - dc2s_scale_h * 640.0f) / 2; + + std::vector osdVertices = GetOSDVertices(); + const float x1 = 2.0f / (settings.display.width / dc2s_scale_h); + const float y1 = 2.0f / 480; + const float x2 = 1 - 2 * sidebarWidth / settings.display.width; + const float y2 = 1; + for (OSDVertex& vtx : osdVertices) + { + vtx.x = vtx.x * x1 - x2; + vtx.y = vtx.y * y1 - y2; + } + + const vk::CommandBuffer cmdBuffer = GetContext()->GetCurrentCommandBuffer(); + cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, osdPipeline.GetPipeline()); + + osdPipeline.BindDescriptorSets(cmdBuffer); + const vk::Viewport viewport(0, 0, (float)settings.display.width, (float)settings.display.height, 0, 1.f); + cmdBuffer.setViewport(0, viewport); + const vk::Rect2D scissor({ 0, 0 }, { (u32)settings.display.width, (u32)settings.display.height }); + cmdBuffer.setScissor(0, scissor); + osdBuffer->upload((u32)(osdVertices.size() * sizeof(OSDVertex)), osdVertices.data()); + cmdBuffer.bindVertexBuffers(0, osdBuffer->buffer.get(), {0}); + for (u32 i = 0; i < (u32)osdVertices.size(); i += 4) + cmdBuffer.draw(4, 1, i, 0); + if (clear_screen) + GetContext()->EndFrame(); + } catch (const InvalidVulkanContext&) { + } +#endif +} + +void BaseVulkanRenderer::RenderFramebuffer(const FramebufferInfo& info) +{ + framebufferTexIndex = (framebufferTexIndex + 1) % GetContext()->GetSwapChainSize(); + + if (framebufferTextures.size() != GetContext()->GetSwapChainSize()) + framebufferTextures.resize(GetContext()->GetSwapChainSize()); + std::unique_ptr& curTexture = framebufferTextures[framebufferTexIndex]; + if (!curTexture) + { + curTexture = std::make_unique(); + curTexture->tex_type = TextureType::_8888; + } + + fbCommandPool.BeginFrame(); + vk::CommandBuffer commandBuffer = fbCommandPool.Allocate(); + commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); + curTexture->SetCommandBuffer(commandBuffer); + + if (info.fb_r_ctrl.fb_enable == 0 || info.vo_control.blank_video == 1) + { + // Video output disabled + u8 rgba[] { (u8)info.vo_border_col._red, (u8)info.vo_border_col._green, (u8)info.vo_border_col._blue, 255 }; + curTexture->UploadToGPU(1, 1, rgba, false); + } + else + { + PixelBuffer pb; + int width; + int height; + ReadFramebuffer(info, pb, width, height); + + curTexture->UploadToGPU(width, height, (u8*)pb.data(), false); + } + + curTexture->SetCommandBuffer(nullptr); + commandBuffer.end(); + fbCommandPool.EndFrame(); + framebufferRendered = true; +} + +void BaseVulkanRenderer::RenderVideoRouting() +{ +#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) + if (config::VideoRouting) + { + auto device = GetContext()->GetDevice(); + auto srcImage = device.getSwapchainImagesKHR(GetContext()->GetSwapChain())[GetContext()->GetCurrentImageIndex()]; + auto graphicsQueue = device.getQueue(GetContext()->GetGraphicsQueueFamilyIndex(), 0); + + int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width); + int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); + + extern void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h); + os_VideoRoutingPublishFrameTexture(device, srcImage, graphicsQueue, 0, 0, targetWidth, targetHeight); + } + else + { + os_VideoRoutingTermVk(); + } +#endif +} + +void BaseVulkanRenderer::CheckFogTexture() +{ + if (!fogTexture) + { + fogTexture = std::make_unique(); + fogTexture->tex_type = TextureType::_8; + fog_needs_update = true; + } + if (!fog_needs_update || !config::Fog) + return; + fog_needs_update = false; + u8 texData[256]; + MakeFogTexture(texData); + + fogTexture->SetCommandBuffer(texCommandBuffer); + fogTexture->UploadToGPU(128, 2, texData, false); + fogTexture->SetCommandBuffer(nullptr); +} + +void BaseVulkanRenderer::CheckPaletteTexture() +{ + if (!paletteTexture) + { + paletteTexture = std::make_unique(); + paletteTexture->tex_type = TextureType::_8888; + palette_updated = true; + } + if (!palette_updated) + return; + palette_updated = false; + + paletteTexture->SetCommandBuffer(texCommandBuffer); + paletteTexture->UploadToGPU(1024, 1, (u8 *)palette32_ram, false); + paletteTexture->SetCommandBuffer(nullptr); +} + +bool BaseVulkanRenderer::presentFramebuffer() +{ + if (framebufferTexIndex >= (int)framebufferTextures.size()) + return false; + Texture *fbTexture = framebufferTextures[framebufferTexIndex].get(); + if (fbTexture == nullptr) + return false; + GetContext()->PresentFrame(fbTexture->GetImage(), fbTexture->GetImageView(), fbTexture->getSize(), + getDCFramebufferAspectRatio()); + return true; +} class VulkanRenderer final : public BaseVulkanRenderer { @@ -44,12 +320,20 @@ public: { DEBUG_LOG(RENDERER, "VulkanRenderer::Term"); GetContext()->WaitIdle(); + texCommandPool.Term(); // make sure all in-flight buffers are returned screenDrawer.Term(); textureDrawer.Term(); samplerManager.term(); BaseVulkanRenderer::Term(); } + void Process(TA_context* ctx) override + { + if (ctx->rend.isRTT) + screenDrawer.EndRenderPass(); + BaseVulkanRenderer::Process(ctx); + } + bool Render() override { try { @@ -70,7 +354,9 @@ public: } drawer->Draw(fogTexture.get(), paletteTexture.get()); - drawer->EndRenderPass(); + if (config::EmulateFramebuffer || pvrrc.isRTT) + // delay ending the render pass in case of multi render + drawer->EndRenderPass(); return !pvrrc.isRTT; } catch (const vk::SystemError& e) { diff --git a/core/rend/vulkan/vulkan_renderer.h b/core/rend/vulkan/vulkan_renderer.h index fc144ec8e..f22f1230d 100644 --- a/core/rend/vulkan/vulkan_renderer.h +++ b/core/rend/vulkan/vulkan_renderer.h @@ -19,240 +19,33 @@ #pragma once #include "vulkan.h" #include "hw/pvr/Renderer_if.h" -#include "hw/pvr/ta.h" #include "commandpool.h" #include "pipeline.h" -#include "rend/osd.h" -#include "rend/transform_matrix.h" -#ifndef LIBRETRO -#include "rend/gui.h" -#endif +#include "shaders.h" #include #include +void os_VideoRoutingTermVk(); + class BaseVulkanRenderer : public Renderer { protected: - bool BaseInit(vk::RenderPass renderPass, int subpass = 0) - { - texCommandPool.Init(); - fbCommandPool.Init(); - -#if defined(__ANDROID__) && !defined(LIBRETRO) - if (!vjoyTexture) - { - int w, h; - u8 *image_data = loadOSDButtons(w, h); - texCommandPool.BeginFrame(); - vjoyTexture = std::make_unique(); - vjoyTexture->tex_type = TextureType::_8888; - vk::CommandBuffer cmdBuffer = texCommandPool.Allocate(); - cmdBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - vjoyTexture->SetCommandBuffer(cmdBuffer); - vjoyTexture->UploadToGPU(w, h, image_data, false); - vjoyTexture->SetCommandBuffer(nullptr); - cmdBuffer.end(); - texCommandPool.EndFrame(); - delete [] image_data; - osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass()); - } - if (!osdBuffer) - { - osdBuffer = std::make_unique(sizeof(OSDVertex) * VJOY_VISIBLE * 4, - vk::BufferUsageFlagBits::eVertexBuffer); - } -#endif - quadPipeline = std::make_unique(false, false); - quadPipeline->Init(&shaderManager, renderPass, subpass); - framebufferDrawer = std::make_unique(); - framebufferDrawer->Init(quadPipeline.get()); - - return true; - } + bool BaseInit(vk::RenderPass renderPass, int subpass = 0); public: - void Term() override - { - GetContext()->WaitIdle(); - GetContext()->PresentFrame(nullptr, nullptr, vk::Extent2D(), 0); - framebufferDrawer.reset(); - quadPipeline.reset(); - osdBuffer.reset(); - osdPipeline.Term(); - vjoyTexture.reset(); - textureCache.Clear(); - fogTexture = nullptr; - paletteTexture = nullptr; - texCommandPool.Term(); - fbCommandPool.Term(); - framebufferTextures.clear(); - framebufferTexIndex = 0; - shaderManager.term(); + void Term() override; + BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; + void Process(TA_context* ctx) override; + void ReInitOSD(); + void DrawOSD(bool clear_screen) override; + void RenderFramebuffer(const FramebufferInfo& info) override; + void RenderVideoRouting(); + + bool GetLastFrame(std::vector& data, int& width, int& height) override { + return GetContext()->GetLastFrame(data, width, height); } - BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override - { - Texture* tf = textureCache.getTextureCacheData(tsp, tcw); - - //update if needed - if (tf->NeedsUpdate()) - { - // This kills performance when a frame is skipped and lots of texture updated each frame - //if (textureCache.IsInFlight(tf, true)) - // textureCache.DestroyLater(tf); - tf->SetCommandBuffer(texCommandBuffer); - if (!tf->Update()) - { - tf->SetCommandBuffer(nullptr); - return nullptr; - } - } - else if (tf->IsCustomTextureAvailable()) - { - tf->deferDeleteResource(&texCommandPool); - tf->SetCommandBuffer(texCommandBuffer); - tf->CheckCustomTexture(); - } - tf->SetCommandBuffer(nullptr); - textureCache.SetInFlight(tf); - - return tf; - } - - void Process(TA_context* ctx) override - { - if (KillTex) - textureCache.Clear(); - - texCommandPool.BeginFrame(); - textureCache.SetCurrentIndex(texCommandPool.GetIndex()); - textureCache.Cleanup(); - - texCommandBuffer = texCommandPool.Allocate(); - texCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - - ta_parse(ctx, true); - - CheckFogTexture(); - CheckPaletteTexture(); - texCommandBuffer.end(); - } - - void ReInitOSD() - { - texCommandPool.Init(); - fbCommandPool.Init(); -#if defined(__ANDROID__) && !defined(LIBRETRO) - osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass()); -#endif - } - - void DrawOSD(bool clear_screen) override - { -#ifndef LIBRETRO - gui_display_osd(); - if (!vjoyTexture) - return; - try { - if (clear_screen) - { - GetContext()->NewFrame(); - GetContext()->BeginRenderPass(); - GetContext()->PresentLastFrame(); - } - const float dc2s_scale_h = settings.display.height / 480.0f; - const float sidebarWidth = (settings.display.width - dc2s_scale_h * 640.0f) / 2; - - std::vector osdVertices = GetOSDVertices(); - const float x1 = 2.0f / (settings.display.width / dc2s_scale_h); - const float y1 = 2.0f / 480; - const float x2 = 1 - 2 * sidebarWidth / settings.display.width; - const float y2 = 1; - for (OSDVertex& vtx : osdVertices) - { - vtx.x = vtx.x * x1 - x2; - vtx.y = vtx.y * y1 - y2; - } - - const vk::CommandBuffer cmdBuffer = GetContext()->GetCurrentCommandBuffer(); - cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, osdPipeline.GetPipeline()); - - osdPipeline.BindDescriptorSets(cmdBuffer); - const vk::Viewport viewport(0, 0, (float)settings.display.width, (float)settings.display.height, 0, 1.f); - cmdBuffer.setViewport(0, viewport); - const vk::Rect2D scissor({ 0, 0 }, { (u32)settings.display.width, (u32)settings.display.height }); - cmdBuffer.setScissor(0, scissor); - osdBuffer->upload((u32)(osdVertices.size() * sizeof(OSDVertex)), osdVertices.data()); - cmdBuffer.bindVertexBuffers(0, osdBuffer->buffer.get(), {0}); - for (u32 i = 0; i < (u32)osdVertices.size(); i += 4) - cmdBuffer.draw(4, 1, i, 0); - if (clear_screen) - GetContext()->EndFrame(); - } catch (const InvalidVulkanContext&) { - } -#endif - } - - void RenderFramebuffer(const FramebufferInfo& info) override - { - framebufferTexIndex = (framebufferTexIndex + 1) % GetContext()->GetSwapChainSize(); - - if (framebufferTextures.size() != GetContext()->GetSwapChainSize()) - framebufferTextures.resize(GetContext()->GetSwapChainSize()); - std::unique_ptr& curTexture = framebufferTextures[framebufferTexIndex]; - if (!curTexture) - { - curTexture = std::make_unique(); - curTexture->tex_type = TextureType::_8888; - } - - fbCommandPool.BeginFrame(); - vk::CommandBuffer commandBuffer = fbCommandPool.Allocate(); - commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - curTexture->SetCommandBuffer(commandBuffer); - - if (info.fb_r_ctrl.fb_enable == 0 || info.vo_control.blank_video == 1) - { - // Video output disabled - u8 rgba[] { (u8)info.vo_border_col._red, (u8)info.vo_border_col._green, (u8)info.vo_border_col._blue, 255 }; - curTexture->UploadToGPU(1, 1, rgba, false); - } - else - { - PixelBuffer pb; - int width; - int height; - ReadFramebuffer(info, pb, width, height); - - curTexture->UploadToGPU(width, height, (u8*)pb.data(), false); - } - - curTexture->SetCommandBuffer(nullptr); - commandBuffer.end(); - fbCommandPool.EndFrame(); - framebufferRendered = true; - } - - void RenderVideoRouting() - { -#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) - if (config::VideoRouting) - { - auto device = GetContext()->GetDevice(); - auto srcImage = device.getSwapchainImagesKHR(GetContext()->GetSwapChain())[GetContext()->GetCurrentImageIndex()]; - auto graphicsQueue = device.getQueue(GetContext()->GetGraphicsQueueFamilyIndex(), 0); - - int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width); - int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); - - extern void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h); - os_VideoRoutingPublishFrameTexture(device, srcImage, graphicsQueue, 0, 0, targetWidth, targetHeight); - } -#endif - } - - protected: BaseVulkanRenderer() : viewport(640, 480) {} @@ -264,57 +57,9 @@ protected: viewport.height = h; } - void CheckFogTexture() - { - if (!fogTexture) - { - fogTexture = std::make_unique(); - fogTexture->tex_type = TextureType::_8; - fog_needs_update = true; - } - if (!fog_needs_update || !config::Fog) - return; - fog_needs_update = false; - u8 texData[256]; - MakeFogTexture(texData); - fogTexture->SetCommandBuffer(texCommandBuffer); - - fogTexture->UploadToGPU(128, 2, texData, false); - - fogTexture->SetCommandBuffer(nullptr); - } - - void CheckPaletteTexture() - { - if (!paletteTexture) - { - paletteTexture = std::make_unique(); - paletteTexture->tex_type = TextureType::_8888; - forcePaletteUpdate(); - } - if (!palette_updated) - return; - palette_updated = false; - - paletteTexture->SetCommandBuffer(texCommandBuffer); - - paletteTexture->UploadToGPU(1024, 1, (u8 *)palette32_ram, false); - - paletteTexture->SetCommandBuffer(nullptr); - } - - bool presentFramebuffer() - { - if (framebufferTexIndex >= (int)framebufferTextures.size()) - return false; - Texture *fbTexture = framebufferTextures[framebufferTexIndex].get(); - if (fbTexture == nullptr) - return false; - GetContext()->PresentFrame(fbTexture->GetImage(), fbTexture->GetImageView(), fbTexture->getSize(), - getDCFramebufferAspectRatio()); - framebufferRendered = false; - return true; - } + void CheckFogTexture(); + void CheckPaletteTexture(); + bool presentFramebuffer(); ShaderManager shaderManager; std::unique_ptr fogTexture; diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 85cf106a2..48b8259f7 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -14,6 +14,7 @@ #include "hw/maple/maple_devs.h" #include "sdl_gamepad.h" #include "sdl_keyboard.h" +#include "sdl_keyboard_mac.h" #include "wsi/context.h" #include "emulator.h" #include "stdclass.h" @@ -46,7 +47,7 @@ static bool gameRunning; static bool mouseCaptured; static std::string clipboardText; static std::string barcode; -static double lastBarcodeTime; +static u64 lastBarcodeTime; static KeyboardLayout detectKeyboardLayout(); static bool handleBarcodeScanner(const SDL_Event& event); @@ -88,6 +89,14 @@ static void sdl_close_joystick(SDL_JoystickID instance) gamepad->close(); } +static void setWindowTitleGame() +{ + if (settings.naomi.slave) + SDL_SetWindowTitle(window, ("Flycast - Multiboard Slave " + cfgLoadStr("naomi", "BoardId", "")).c_str()); + else + SDL_SetWindowTitle(window, ("Flycast - " + settings.content.title).c_str()); +} + static void captureMouse(bool capture) { if (window == nullptr || !gameRunning) @@ -98,7 +107,7 @@ static void captureMouse(bool capture) SDL_SetRelativeMouseMode(SDL_FALSE); else SDL_ShowCursor(SDL_ENABLE); - SDL_SetWindowTitle(window, "Flycast"); + setWindowTitleGame(); mouseCaptured = false; } else @@ -118,12 +127,15 @@ static void emuEventCallback(Event event, void *) { switch (event) { + case Event::Terminate: + SDL_SetWindowTitle(window, "Flycast"); + break; case Event::Pause: gameRunning = false; if (!config::UseRawInput) SDL_SetRelativeMouseMode(SDL_FALSE); SDL_ShowCursor(SDL_ENABLE); - SDL_SetWindowTitle(window, "Flycast"); + setWindowTitleGame(); break; case Event::Resume: gameRunning = true; @@ -160,7 +172,11 @@ static void checkRawInput() #else if (!sdl_keyboard) { +#ifdef __APPLE__ + sdl_keyboard = std::make_shared(0); +#else sdl_keyboard = std::make_shared(0); +#endif GamepadDevice::Register(sdl_keyboard); } #endif @@ -199,6 +215,9 @@ void input_sdl_init() SDL_SetRelativeMouseMode(SDL_FALSE); + // Event::Start is called on a background thread, so we can't use it to change the window title (macOS) + // However it's followed by Event::Resume which is fine. + EventManager::listen(Event::Terminate, emuEventCallback); EventManager::listen(Event::Pause, emuEventCallback); EventManager::listen(Event::Resume, emuEventCallback); @@ -234,6 +253,9 @@ void input_sdl_init() void input_sdl_quit() { + EventManager::unlisten(Event::Terminate, emuEventCallback); + EventManager::unlisten(Event::Pause, emuEventCallback); + EventManager::unlisten(Event::Resume, emuEventCallback); SDLGamepad::closeAllGamepads(); SDL_QuitSubSystem(SDL_INIT_JOYSTICK); } @@ -540,12 +562,6 @@ void input_sdl_handle() } } -void sdl_window_set_text(const char* text) -{ - if (window != nullptr) - SDL_SetWindowTitle(window, text); -} - static float hdpiScaling = 1.f; static inline void get_window_state() @@ -1157,8 +1173,8 @@ static bool handleBarcodeScanner(const SDL_Event& event) return false; } } - double now = os_GetSeconds(); - if (!barcode.empty() && now - lastBarcodeTime >= 0.5) + u64 now = getTimeMs(); + if (!barcode.empty() && now - lastBarcodeTime >= 500) { INFO_LOG(INPUT, "Barcode timeout"); barcode.clear(); diff --git a/core/sdl/sdl.h b/core/sdl/sdl.h index a79f85f22..c60d083f2 100644 --- a/core/sdl/sdl.h +++ b/core/sdl/sdl.h @@ -6,7 +6,6 @@ void input_sdl_init(); void input_sdl_handle(); void input_sdl_quit(); void sdl_window_create(); -void sdl_window_set_text(const char* text); void sdl_window_destroy(); bool sdl_recreate_window(u32 flags); void sdl_fix_steamdeck_dpi(SDL_Window *window); diff --git a/core/sdl/sdl_gamepad.h b/core/sdl/sdl_gamepad.h index 6bb9e7f99..60782f738 100644 --- a/core/sdl/sdl_gamepad.h +++ b/core/sdl/sdl_gamepad.h @@ -1,7 +1,7 @@ #pragma once #include "input/gamepad_device.h" #include "input/mouse.h" -#include "oslib/oslib.h" +#include "stdclass.h" #include "sdl.h" template @@ -208,6 +208,7 @@ public: #endif hasAnalogStick = SDL_JoystickNumAxes(sdl_joystick) > 0; + set_maple_port(maple_port); } bool gamepad_axis_input(u32 code, int value) override @@ -217,6 +218,12 @@ public: return GamepadDevice::gamepad_axis_input(code, value); } + void set_maple_port(int port) override + { + GamepadDevice::set_maple_port(port); + SDL_JoystickSetPlayerIndex(sdl_joystick, port <= 3 ? port : -1); + } + u16 getRumbleIntensity(float power) { return (u16)std::min(power * 65535.f / std::pow(1.06f, 100.f - rumblePower), 65535.f); } @@ -226,7 +233,7 @@ public: if (rumbleEnabled) { vib_inclination = inclination * power; - vib_stop_time = os_GetSeconds() + duration_ms / 1000.0; + vib_stop_time = getTimeMs() + duration_ms; u16 intensity = getRumbleIntensity(power); SDL_JoystickRumble(sdl_joystick, intensity, intensity, duration_ms); @@ -238,7 +245,7 @@ public: return; if (vib_inclination > 0) { - int rem_time = (vib_stop_time - os_GetSeconds()) * 1000; + int rem_time = vib_stop_time - getTimeMs(); if (rem_time <= 0) vib_inclination = 0; else @@ -411,7 +418,7 @@ public: } protected: - double vib_stop_time = 0; + u64 vib_stop_time = 0; SDL_JoystickID sdl_joystick_instance; private: diff --git a/core/sdl/sdl_keyboard.h b/core/sdl/sdl_keyboard.h index b524ef563..8455acd6b 100644 --- a/core/sdl/sdl_keyboard.h +++ b/core/sdl/sdl_keyboard.h @@ -2,25 +2,6 @@ #include "input/keyboard_device.h" #include "sdl.h" -#ifdef __APPLE__ -#include -#include -#include -// Rumbling Taptic Engine by Private MultitouchSupport.framework -extern "C" { -typedef void *MTDeviceRef; -bool MTDeviceIsAvailable(void); -MTDeviceRef MTDeviceCreateDefault(void); -OSStatus MTDeviceGetDeviceID(MTDeviceRef, uint64_t*) __attribute__ ((weak_import)); -CFTypeRef MTActuatorCreateFromDeviceID(UInt64 deviceID); -IOReturn MTActuatorOpen(CFTypeRef actuatorRef); -IOReturn MTActuatorClose(CFTypeRef actuatorRef); -IOReturn MTActuatorActuate(CFTypeRef actuatorRef, SInt32 actuationID, UInt32 unknown1, Float32 unknown2, Float32 unknown3); -bool MTActuatorIsOpen(CFTypeRef actuatorRef); -enum ActuatePattern { minimal = 3, weak = 5, medium = 4, strong = 6 }; -} -#endif - class SDLKeyboardDevice : public KeyboardDevice { public: @@ -54,12 +35,6 @@ public: } else input_mapper = getDefaultMapping(); - -#ifdef __APPLE__ - uint64_t deviceID; - if ( MTDeviceIsAvailable() && MTDeviceGetDeviceID(MTDeviceCreateDefault(), &deviceID) == 0 && (vib_device = MTActuatorCreateFromDeviceID(deviceID)) != NULL && MTActuatorOpen(vib_device) == kIOReturnSuccess) - rumbleEnabled = true; -#endif } const char *get_button_name(u32 code) override @@ -70,59 +45,6 @@ public: return name; } -#ifdef __APPLE__ - void rumble(float power, float inclination, u32 duration_ms) override - { - if (rumbleEnabled) - { - vib_stop_time = os_GetSeconds() + duration_ms / 1000.0; - - __block int pattern; - if (power >= 0.75) - pattern = ActuatePattern::strong; - else if (power >= 0.5) - pattern = ActuatePattern::medium; - else if (power >= 0.25) - pattern = ActuatePattern::weak; - else if (power > 0) - pattern = ActuatePattern::minimal; - else - { - while(!vib_timer_stack.empty()) - { - dispatch_source_cancel(vib_timer_stack.top()); - vib_timer_stack.pop(); - } - return; - } - // Since the Actuator API does not support duration - // using a interval timer with `10ms * rumblePower percentage` to fake it - __block dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); - vib_timer_stack.push(_timer); - dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC * rumblePower / 100.f, 0); - - dispatch_source_set_event_handler(_timer, ^{ - if ( vib_stop_time - os_GetSeconds() < 0 ) - { - dispatch_source_cancel(_timer); - return; - } - MTActuatorActuate(vib_device, pattern, 0, 0.0, 0.0); - }); - - dispatch_resume(_timer); - } - } - - ~SDLKeyboardDevice() { - if (rumbleEnabled) - { - MTActuatorClose(vib_device); - CFRelease(vib_device); - } - } -#endif - void input(SDL_Scancode scancode, bool pressed) { u8 keycode; @@ -132,11 +54,4 @@ public: keycode = (u8)scancode; KeyboardDevice::input(keycode, pressed, 0); } - -#ifdef __APPLE__ -private: - std::stack vib_timer_stack; - CFTypeRef vib_device = NULL; - double vib_stop_time = 0; -#endif }; diff --git a/core/sdl/sdl_keyboard_mac.h b/core/sdl/sdl_keyboard_mac.h new file mode 100644 index 000000000..732f8e8b0 --- /dev/null +++ b/core/sdl/sdl_keyboard_mac.h @@ -0,0 +1,112 @@ +/* + Copyright 2022 edw + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#pragma once +#ifdef __APPLE__ +#include "sdl_keyboard.h" +#include "stdclass.h" +#include +#include +#include + +// Rumbling Taptic Engine by Private MultitouchSupport.framework +extern "C" { +typedef void *MTDeviceRef; +bool MTDeviceIsAvailable(void); +MTDeviceRef MTDeviceCreateDefault(void); +OSStatus MTDeviceGetDeviceID(MTDeviceRef, uint64_t*) __attribute__ ((weak_import)); +CFTypeRef MTActuatorCreateFromDeviceID(UInt64 deviceID); +IOReturn MTActuatorOpen(CFTypeRef actuatorRef); +IOReturn MTActuatorClose(CFTypeRef actuatorRef); +IOReturn MTActuatorActuate(CFTypeRef actuatorRef, SInt32 actuationID, UInt32 unknown1, Float32 unknown2, Float32 unknown3); +bool MTActuatorIsOpen(CFTypeRef actuatorRef); +enum ActuatePattern { minimal = 3, weak = 5, medium = 4, strong = 6 }; +} + +class SDLMacKeyboard : public SDLKeyboardDevice +{ +public: + SDLMacKeyboard(int maple_port) : SDLKeyboardDevice(maple_port) + { + uint64_t deviceID; + if (MTDeviceIsAvailable() + && MTDeviceGetDeviceID(MTDeviceCreateDefault(), &deviceID) == 0 + && (vib_device = MTActuatorCreateFromDeviceID(deviceID)) != NULL + && MTActuatorOpen(vib_device) == kIOReturnSuccess) + rumbleEnabled = true; + } + + void rumble(float power, float inclination, u32 duration_ms) override + { + if (!rumbleEnabled) + return; + + vib_stop_time = getTimeMs() + duration_ms; + + __block int pattern; + if (power >= 0.75) + pattern = ActuatePattern::strong; + else if (power >= 0.5) + pattern = ActuatePattern::medium; + else if (power >= 0.25) + pattern = ActuatePattern::weak; + else if (power > 0) + pattern = ActuatePattern::minimal; + else + { + while(!vib_timer_stack.empty()) + { + dispatch_source_cancel(vib_timer_stack.top()); + vib_timer_stack.pop(); + } + return; + } + // Since the Actuator API does not support duration + // using a interval timer with `10ms * rumblePower percentage` to fake it + __block dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + vib_timer_stack.push(_timer); + dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC * rumblePower / 100.f, 0); + + dispatch_source_set_event_handler(_timer, ^{ + if (vib_stop_time < getTimeMs()) + { + dispatch_source_cancel(_timer); + return; + } + MTActuatorActuate(vib_device, pattern, 0, 0.0, 0.0); + }); + + dispatch_resume(_timer); + } + + ~SDLMacKeyboard() + { + if (rumbleEnabled) + { + MTActuatorClose(vib_device); + CFRelease(vib_device); + } + } + +private: + std::stack vib_timer_stack; + CFTypeRef vib_device = NULL; + u64 vib_stop_time = 0; +}; + +#endif // _APPLE_ diff --git a/core/serialize.cpp b/core/serialize.cpp index 1f3efe04a..029d5cdf9 100644 --- a/core/serialize.cpp +++ b/core/serialize.cpp @@ -16,6 +16,7 @@ #include "hw/bba/bba.h" #include "cfg/option.h" #include "imgread/common.h" +#include "achievements/achievements.h" void dc_serialize(Serializer& ser) { @@ -50,66 +51,13 @@ void dc_serialize(Serializer& ser) naomi_cart_serialize(ser); gd_hle_state.Serialize(ser); + achievements::serialize(ser); DEBUG_LOG(SAVESTATE, "Saved %d bytes", (u32)ser.size()); } -static void dc_deserialize_libretro(Deserializer& deser) -{ - aica::deserialize(deser); - - sb_deserialize(deser); - - nvmem::deserialize(deser); - - gdrom::deserialize(deser); - - mcfg_DeserializeDevices(deser); - - pvr::deserialize(deser); - - sh4::deserialize(deser); - - if (deser.version() >= Deserializer::V13_LIBRETRO) - deser.skip(); // settings.network.EmulateBBA - config::EmulateBBA.override(false); - - ModemDeserialize(deser); - - sh4::deserialize2(deser); - - libGDR_deserialize(deser); - - deser.skip(); // FLASH_SIZE - deser.skip(); // BBSRAM_SIZE - deser.skip(); // BIOS_SIZE - deser.skip(); // RAM_SIZE - deser.skip(); // ARAM_SIZE - deser.skip(); // VRAM_SIZE - deser.skip(); // RAM_MASK - deser.skip(); // ARAM_MASK - deser.skip(); // VRAM_MASK - - naomi_Deserialize(deser); - - deser >> config::Broadcast.get(); - deser >> config::Cable.get(); - deser >> config::Region.get(); - - naomi_cart_deserialize(deser); - gd_hle_state.Deserialize(deser); - - DEBUG_LOG(SAVESTATE, "Loaded %d bytes (libretro compat)", (u32)deser.size()); -} - void dc_deserialize(Deserializer& deser) { - if (deser.version() >= Deserializer::V9_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO) - { - dc_deserialize_libretro(deser); - sh4_sched_ffts(); - return; - } DEBUG_LOG(SAVESTATE, "Loading state version %d", deser.version()); aica::deserialize(deser); @@ -126,10 +74,7 @@ void dc_deserialize(Deserializer& deser) sh4::deserialize(deser); - if (deser.version() >= Deserializer::V13) - deser >> config::EmulateBBA.get(); - else - config::EmulateBBA.override(false); + deser >> config::EmulateBBA.get(); if (config::EmulateBBA) bba_Deserialize(deser); ModemDeserialize(deser); @@ -149,7 +94,64 @@ void dc_deserialize(Deserializer& deser) naomi_cart_deserialize(deser); gd_hle_state.Deserialize(deser); + achievements::deserialize(deser); sh4_sched_ffts(); DEBUG_LOG(SAVESTATE, "Loaded %d bytes", (u32)deser.size()); } + +Deserializer::Deserializer(const void *data, size_t limit, bool rollback) + : SerializeBase(limit, rollback), data((const u8 *)data) +{ + if (!memcmp(data, "RASTATE\001", 8)) + { + // RetroArch savestates now have several sections: MEM, ACHV, RPLY, etc. + const u8 *p = this->data + 8; + limit -= 8; + while (limit > 8) + { + const u8 *section = p; + u32 sectionSize = *(const u32 *)&p[4]; + p += 8; + limit -= 8; + if (!memcmp(section, "MEM ", 4)) + { + // That's the part we're interested in + this->data = p; + this->limit = sectionSize; + break; + } + sectionSize = (sectionSize + 7) & ~7; // align to 8 bytes + if (limit < sectionSize) { + limit = 0; + break; + } + p += sectionSize; + limit -= sectionSize; + } + if (limit <= 8) + throw Exception("Can't find MEM section in RetroArch savestate"); + } + deserialize(_version); + if (_version < V16) + throw Exception("Unsupported version"); + if (_version > Current) + throw Exception("Version too recent"); + + if(_version >= V42 && settings.platform.isConsole()) + { + u32 ramSize; + deserialize(ramSize); + if (ramSize != settings.platform.ram_size) + throw Exception("Selected RAM Size doesn't match Save State"); + } +} + +Serializer::Serializer(void *data, size_t limit, bool rollback) + : SerializeBase(limit, rollback), data((u8 *)data) +{ + Version v = Current; + serialize(v); + if (settings.platform.isConsole()) + serialize(settings.platform.ram_size); +} diff --git a/core/serialize.h b/core/serialize.h index 5b9d38506..5650122d0 100644 --- a/core/serialize.h +++ b/core/serialize.h @@ -26,22 +26,7 @@ class SerializeBase { public: enum Version : int32_t { - V9_LIBRETRO = 8, - V10_LIBRETRO, - V11_LIBRETRO, - V12_LIBRETRO, - V13_LIBRETRO, - VLAST_LIBRETRO = V13_LIBRETRO, - - V8 = 803, - V9, - V10, - V11, - V12, - V13, - V14, - V15, - V16, + V16 = 811, V17, V18, V19, @@ -74,7 +59,9 @@ public: V46, V47, V48, - Current = V48, + V49, + V50, + Current = V50, Next = Current + 1, }; @@ -100,30 +87,7 @@ public: Exception(const char *msg) : std::runtime_error(msg) {} }; - Deserializer(const void *data, size_t limit, bool rollback = false) - : SerializeBase(limit, rollback), data((const u8 *)data) - { - if (!memcmp(data, "RASTATE\001", 8)) - { - // RetroArch savestate: a 16-byte header is now added here because why not? - this->data += 16; - this->limit -= 16; - } - deserialize(_version); - if (_version < V9_LIBRETRO || (_version > V13_LIBRETRO && _version < V8)) - throw Exception("Unsupported version"); - if (_version > Current) - throw Exception("Version too recent"); - - if(_version >= V42 && settings.platform.isConsole()) - { - u32 ramSize; - deserialize(ramSize); - if (ramSize != settings.platform.ram_size) { - throw Exception("Selected RAM Size doesn't match Save State"); - } - } - } + Deserializer(const void *data, size_t limit, bool rollback = false); template void deserialize(T& obj) @@ -179,14 +143,7 @@ public: Serializer() : Serializer(nullptr, std::numeric_limits::max(), false) {} - Serializer(void *data, size_t limit, bool rollback = false) - : SerializeBase(limit, rollback), data((u8 *)data) - { - Version v = Current; - serialize(v); - if (settings.platform.isConsole()) - serialize(settings.platform.ram_size); - } + Serializer(void *data, size_t limit, bool rollback = false); template void serialize(const T& obj) diff --git a/core/stdclass.cpp b/core/stdclass.cpp index b46215226..32b26bd3d 100644 --- a/core/stdclass.cpp +++ b/core/stdclass.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -130,7 +131,10 @@ bool make_directory(const std::string& path) void cThread::Start() { verify(!thread.joinable()); - thread = std::thread(entry, param); + thread = std::thread([this]() { + ThreadName _(name); + entry(param); + }); } void cThread::WaitToEnd() @@ -208,3 +212,12 @@ void RamRegion::free() freeAligned(data); data = nullptr; } + +u64 getTimeMs() +{ + using the_clock = std::chrono::steady_clock; + std::chrono::time_point now = the_clock::now(); + static std::chrono::time_point start = now; + + return std::chrono::duration_cast(now - start).count(); +} diff --git a/core/stdclass.h b/core/stdclass.h index 36fe655be..6c7d209fd 100644 --- a/core/stdclass.h +++ b/core/stdclass.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include #ifdef __ANDROID__ #include @@ -28,12 +30,13 @@ private: typedef void* ThreadEntryFP(void* param); ThreadEntryFP* entry; void* param; + const char *name; public: std::thread thread; - cThread(ThreadEntryFP* function, void* param) - :entry(function), param(param) {} + cThread(ThreadEntryFP* function, void* param, const char *name) + :entry(function), param(param), name(name) {} ~cThread() { WaitToEnd(); } void Start(); void WaitToEnd(); @@ -196,3 +199,41 @@ public: return v; } }; + +u64 getTimeMs(); + +class ThreadRunner +{ +public: + void init() { + threadId = std::this_thread::get_id(); + } + void runOnThread(std::function func) + { + if (threadId == std::this_thread::get_id()) { + func(); + } + else { + LockGuard _(mutex); + tasks.push_back(func); + } + } + void execTasks() + { + assert(threadId == std::this_thread::get_id()); + std::vector> localTasks; + { + LockGuard _(mutex); + std::swap(localTasks, tasks); + } + for (auto& func : localTasks) + func(); + } + +private: + using LockGuard = std::lock_guard; + + std::thread::id threadId; + std::vector> tasks; + std::mutex mutex; +}; diff --git a/core/types.h b/core/types.h index 318879fbd..e518762a0 100644 --- a/core/types.h +++ b/core/types.h @@ -80,9 +80,6 @@ inline static void JITWriteProtect(bool enabled) { #include "log/Log.h" -#define VER_EMUNAME "Flycast" -#define VER_SHORTNAME VER_EMUNAME - #ifndef _MSC_VER #define stricmp strcasecmp #endif @@ -178,6 +175,7 @@ struct settings_t std::string path; std::string gameId; std::string fileName; + std::string title; } content; struct { @@ -212,7 +210,7 @@ struct settings_t int drivingSimSlave; } naomi; - bool disableRenderer; + bool raHardcoreMode; }; extern settings_t settings; diff --git a/core/ui/IconsFontAwesome6.h b/core/ui/IconsFontAwesome6.h new file mode 100644 index 000000000..a9e11503c --- /dev/null +++ b/core/ui/IconsFontAwesome6.h @@ -0,0 +1,1406 @@ +// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py +// for C and C++ +// from codepoints https://github.com/FortAwesome/Font-Awesome/raw/6.x/metadata/icons.yml +// for use with font https://github.com/FortAwesome/Font-Awesome/blob/6.x/webfonts/fa-regular-400.ttf, https://github.com/FortAwesome/Font-Awesome/blob/6.x/webfonts/fa-solid-900.ttf + +#pragma once + +#define FONT_ICON_FILE_NAME_FAR "fa-regular-400.ttf" +#define FONT_ICON_FILE_NAME_FAS "fa-solid-900.ttf" + +#define ICON_MIN_FA 0xe005 +#define ICON_MAX_16_FA 0xf8ff +#define ICON_MAX_FA 0xf8ff + +#define ICON_FA_0 "0" // U+0030 +#define ICON_FA_1 "1" // U+0031 +#define ICON_FA_2 "2" // U+0032 +#define ICON_FA_3 "3" // U+0033 +#define ICON_FA_4 "4" // U+0034 +#define ICON_FA_5 "5" // U+0035 +#define ICON_FA_6 "6" // U+0036 +#define ICON_FA_7 "7" // U+0037 +#define ICON_FA_8 "8" // U+0038 +#define ICON_FA_9 "9" // U+0039 +#define ICON_FA_A "A" // U+0041 +#define ICON_FA_ADDRESS_BOOK "\xef\x8a\xb9" // U+f2b9 +#define ICON_FA_ADDRESS_CARD "\xef\x8a\xbb" // U+f2bb +#define ICON_FA_ALIGN_CENTER "\xef\x80\xb7" // U+f037 +#define ICON_FA_ALIGN_JUSTIFY "\xef\x80\xb9" // U+f039 +#define ICON_FA_ALIGN_LEFT "\xef\x80\xb6" // U+f036 +#define ICON_FA_ALIGN_RIGHT "\xef\x80\xb8" // U+f038 +#define ICON_FA_ANCHOR "\xef\x84\xbd" // U+f13d +#define ICON_FA_ANCHOR_CIRCLE_CHECK "\xee\x92\xaa" // U+e4aa +#define ICON_FA_ANCHOR_CIRCLE_EXCLAMATION "\xee\x92\xab" // U+e4ab +#define ICON_FA_ANCHOR_CIRCLE_XMARK "\xee\x92\xac" // U+e4ac +#define ICON_FA_ANCHOR_LOCK "\xee\x92\xad" // U+e4ad +#define ICON_FA_ANGLE_DOWN "\xef\x84\x87" // U+f107 +#define ICON_FA_ANGLE_LEFT "\xef\x84\x84" // U+f104 +#define ICON_FA_ANGLE_RIGHT "\xef\x84\x85" // U+f105 +#define ICON_FA_ANGLE_UP "\xef\x84\x86" // U+f106 +#define ICON_FA_ANGLES_DOWN "\xef\x84\x83" // U+f103 +#define ICON_FA_ANGLES_LEFT "\xef\x84\x80" // U+f100 +#define ICON_FA_ANGLES_RIGHT "\xef\x84\x81" // U+f101 +#define ICON_FA_ANGLES_UP "\xef\x84\x82" // U+f102 +#define ICON_FA_ANKH "\xef\x99\x84" // U+f644 +#define ICON_FA_APPLE_WHOLE "\xef\x97\x91" // U+f5d1 +#define ICON_FA_ARCHWAY "\xef\x95\x97" // U+f557 +#define ICON_FA_ARROW_DOWN "\xef\x81\xa3" // U+f063 +#define ICON_FA_ARROW_DOWN_1_9 "\xef\x85\xa2" // U+f162 +#define ICON_FA_ARROW_DOWN_9_1 "\xef\xa2\x86" // U+f886 +#define ICON_FA_ARROW_DOWN_A_Z "\xef\x85\x9d" // U+f15d +#define ICON_FA_ARROW_DOWN_LONG "\xef\x85\xb5" // U+f175 +#define ICON_FA_ARROW_DOWN_SHORT_WIDE "\xef\xa2\x84" // U+f884 +#define ICON_FA_ARROW_DOWN_UP_ACROSS_LINE "\xee\x92\xaf" // U+e4af +#define ICON_FA_ARROW_DOWN_UP_LOCK "\xee\x92\xb0" // U+e4b0 +#define ICON_FA_ARROW_DOWN_WIDE_SHORT "\xef\x85\xa0" // U+f160 +#define ICON_FA_ARROW_DOWN_Z_A "\xef\xa2\x81" // U+f881 +#define ICON_FA_ARROW_LEFT "\xef\x81\xa0" // U+f060 +#define ICON_FA_ARROW_LEFT_LONG "\xef\x85\xb7" // U+f177 +#define ICON_FA_ARROW_POINTER "\xef\x89\x85" // U+f245 +#define ICON_FA_ARROW_RIGHT "\xef\x81\xa1" // U+f061 +#define ICON_FA_ARROW_RIGHT_ARROW_LEFT "\xef\x83\xac" // U+f0ec +#define ICON_FA_ARROW_RIGHT_FROM_BRACKET "\xef\x82\x8b" // U+f08b +#define ICON_FA_ARROW_RIGHT_LONG "\xef\x85\xb8" // U+f178 +#define ICON_FA_ARROW_RIGHT_TO_BRACKET "\xef\x82\x90" // U+f090 +#define ICON_FA_ARROW_RIGHT_TO_CITY "\xee\x92\xb3" // U+e4b3 +#define ICON_FA_ARROW_ROTATE_LEFT "\xef\x83\xa2" // U+f0e2 +#define ICON_FA_ARROW_ROTATE_RIGHT "\xef\x80\x9e" // U+f01e +#define ICON_FA_ARROW_TREND_DOWN "\xee\x82\x97" // U+e097 +#define ICON_FA_ARROW_TREND_UP "\xee\x82\x98" // U+e098 +#define ICON_FA_ARROW_TURN_DOWN "\xef\x85\x89" // U+f149 +#define ICON_FA_ARROW_TURN_UP "\xef\x85\x88" // U+f148 +#define ICON_FA_ARROW_UP "\xef\x81\xa2" // U+f062 +#define ICON_FA_ARROW_UP_1_9 "\xef\x85\xa3" // U+f163 +#define ICON_FA_ARROW_UP_9_1 "\xef\xa2\x87" // U+f887 +#define ICON_FA_ARROW_UP_A_Z "\xef\x85\x9e" // U+f15e +#define ICON_FA_ARROW_UP_FROM_BRACKET "\xee\x82\x9a" // U+e09a +#define ICON_FA_ARROW_UP_FROM_GROUND_WATER "\xee\x92\xb5" // U+e4b5 +#define ICON_FA_ARROW_UP_FROM_WATER_PUMP "\xee\x92\xb6" // U+e4b6 +#define ICON_FA_ARROW_UP_LONG "\xef\x85\xb6" // U+f176 +#define ICON_FA_ARROW_UP_RIGHT_DOTS "\xee\x92\xb7" // U+e4b7 +#define ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE "\xef\x82\x8e" // U+f08e +#define ICON_FA_ARROW_UP_SHORT_WIDE "\xef\xa2\x85" // U+f885 +#define ICON_FA_ARROW_UP_WIDE_SHORT "\xef\x85\xa1" // U+f161 +#define ICON_FA_ARROW_UP_Z_A "\xef\xa2\x82" // U+f882 +#define ICON_FA_ARROWS_DOWN_TO_LINE "\xee\x92\xb8" // U+e4b8 +#define ICON_FA_ARROWS_DOWN_TO_PEOPLE "\xee\x92\xb9" // U+e4b9 +#define ICON_FA_ARROWS_LEFT_RIGHT "\xef\x81\xbe" // U+f07e +#define ICON_FA_ARROWS_LEFT_RIGHT_TO_LINE "\xee\x92\xba" // U+e4ba +#define ICON_FA_ARROWS_ROTATE "\xef\x80\xa1" // U+f021 +#define ICON_FA_ARROWS_SPIN "\xee\x92\xbb" // U+e4bb +#define ICON_FA_ARROWS_SPLIT_UP_AND_LEFT "\xee\x92\xbc" // U+e4bc +#define ICON_FA_ARROWS_TO_CIRCLE "\xee\x92\xbd" // U+e4bd +#define ICON_FA_ARROWS_TO_DOT "\xee\x92\xbe" // U+e4be +#define ICON_FA_ARROWS_TO_EYE "\xee\x92\xbf" // U+e4bf +#define ICON_FA_ARROWS_TURN_RIGHT "\xee\x93\x80" // U+e4c0 +#define ICON_FA_ARROWS_TURN_TO_DOTS "\xee\x93\x81" // U+e4c1 +#define ICON_FA_ARROWS_UP_DOWN "\xef\x81\xbd" // U+f07d +#define ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT "\xef\x81\x87" // U+f047 +#define ICON_FA_ARROWS_UP_TO_LINE "\xee\x93\x82" // U+e4c2 +#define ICON_FA_ASTERISK "*" // U+002a +#define ICON_FA_AT "@" // U+0040 +#define ICON_FA_ATOM "\xef\x97\x92" // U+f5d2 +#define ICON_FA_AUDIO_DESCRIPTION "\xef\x8a\x9e" // U+f29e +#define ICON_FA_AUSTRAL_SIGN "\xee\x82\xa9" // U+e0a9 +#define ICON_FA_AWARD "\xef\x95\x99" // U+f559 +#define ICON_FA_B "B" // U+0042 +#define ICON_FA_BABY "\xef\x9d\xbc" // U+f77c +#define ICON_FA_BABY_CARRIAGE "\xef\x9d\xbd" // U+f77d +#define ICON_FA_BACKWARD "\xef\x81\x8a" // U+f04a +#define ICON_FA_BACKWARD_FAST "\xef\x81\x89" // U+f049 +#define ICON_FA_BACKWARD_STEP "\xef\x81\x88" // U+f048 +#define ICON_FA_BACON "\xef\x9f\xa5" // U+f7e5 +#define ICON_FA_BACTERIA "\xee\x81\x99" // U+e059 +#define ICON_FA_BACTERIUM "\xee\x81\x9a" // U+e05a +#define ICON_FA_BAG_SHOPPING "\xef\x8a\x90" // U+f290 +#define ICON_FA_BAHAI "\xef\x99\xa6" // U+f666 +#define ICON_FA_BAHT_SIGN "\xee\x82\xac" // U+e0ac +#define ICON_FA_BAN "\xef\x81\x9e" // U+f05e +#define ICON_FA_BAN_SMOKING "\xef\x95\x8d" // U+f54d +#define ICON_FA_BANDAGE "\xef\x91\xa2" // U+f462 +#define ICON_FA_BANGLADESHI_TAKA_SIGN "\xee\x8b\xa6" // U+e2e6 +#define ICON_FA_BARCODE "\xef\x80\xaa" // U+f02a +#define ICON_FA_BARS "\xef\x83\x89" // U+f0c9 +#define ICON_FA_BARS_PROGRESS "\xef\xa0\xa8" // U+f828 +#define ICON_FA_BARS_STAGGERED "\xef\x95\x90" // U+f550 +#define ICON_FA_BASEBALL "\xef\x90\xb3" // U+f433 +#define ICON_FA_BASEBALL_BAT_BALL "\xef\x90\xb2" // U+f432 +#define ICON_FA_BASKET_SHOPPING "\xef\x8a\x91" // U+f291 +#define ICON_FA_BASKETBALL "\xef\x90\xb4" // U+f434 +#define ICON_FA_BATH "\xef\x8b\x8d" // U+f2cd +#define ICON_FA_BATTERY_EMPTY "\xef\x89\x84" // U+f244 +#define ICON_FA_BATTERY_FULL "\xef\x89\x80" // U+f240 +#define ICON_FA_BATTERY_HALF "\xef\x89\x82" // U+f242 +#define ICON_FA_BATTERY_QUARTER "\xef\x89\x83" // U+f243 +#define ICON_FA_BATTERY_THREE_QUARTERS "\xef\x89\x81" // U+f241 +#define ICON_FA_BED "\xef\x88\xb6" // U+f236 +#define ICON_FA_BED_PULSE "\xef\x92\x87" // U+f487 +#define ICON_FA_BEER_MUG_EMPTY "\xef\x83\xbc" // U+f0fc +#define ICON_FA_BELL "\xef\x83\xb3" // U+f0f3 +#define ICON_FA_BELL_CONCIERGE "\xef\x95\xa2" // U+f562 +#define ICON_FA_BELL_SLASH "\xef\x87\xb6" // U+f1f6 +#define ICON_FA_BEZIER_CURVE "\xef\x95\x9b" // U+f55b +#define ICON_FA_BICYCLE "\xef\x88\x86" // U+f206 +#define ICON_FA_BINOCULARS "\xef\x87\xa5" // U+f1e5 +#define ICON_FA_BIOHAZARD "\xef\x9e\x80" // U+f780 +#define ICON_FA_BITCOIN_SIGN "\xee\x82\xb4" // U+e0b4 +#define ICON_FA_BLENDER "\xef\x94\x97" // U+f517 +#define ICON_FA_BLENDER_PHONE "\xef\x9a\xb6" // U+f6b6 +#define ICON_FA_BLOG "\xef\x9e\x81" // U+f781 +#define ICON_FA_BOLD "\xef\x80\xb2" // U+f032 +#define ICON_FA_BOLT "\xef\x83\xa7" // U+f0e7 +#define ICON_FA_BOLT_LIGHTNING "\xee\x82\xb7" // U+e0b7 +#define ICON_FA_BOMB "\xef\x87\xa2" // U+f1e2 +#define ICON_FA_BONE "\xef\x97\x97" // U+f5d7 +#define ICON_FA_BONG "\xef\x95\x9c" // U+f55c +#define ICON_FA_BOOK "\xef\x80\xad" // U+f02d +#define ICON_FA_BOOK_ATLAS "\xef\x95\x98" // U+f558 +#define ICON_FA_BOOK_BIBLE "\xef\x99\x87" // U+f647 +#define ICON_FA_BOOK_BOOKMARK "\xee\x82\xbb" // U+e0bb +#define ICON_FA_BOOK_JOURNAL_WHILLS "\xef\x99\xaa" // U+f66a +#define ICON_FA_BOOK_MEDICAL "\xef\x9f\xa6" // U+f7e6 +#define ICON_FA_BOOK_OPEN "\xef\x94\x98" // U+f518 +#define ICON_FA_BOOK_OPEN_READER "\xef\x97\x9a" // U+f5da +#define ICON_FA_BOOK_QURAN "\xef\x9a\x87" // U+f687 +#define ICON_FA_BOOK_SKULL "\xef\x9a\xb7" // U+f6b7 +#define ICON_FA_BOOK_TANAKH "\xef\xa0\xa7" // U+f827 +#define ICON_FA_BOOKMARK "\xef\x80\xae" // U+f02e +#define ICON_FA_BORDER_ALL "\xef\xa1\x8c" // U+f84c +#define ICON_FA_BORDER_NONE "\xef\xa1\x90" // U+f850 +#define ICON_FA_BORDER_TOP_LEFT "\xef\xa1\x93" // U+f853 +#define ICON_FA_BORE_HOLE "\xee\x93\x83" // U+e4c3 +#define ICON_FA_BOTTLE_DROPLET "\xee\x93\x84" // U+e4c4 +#define ICON_FA_BOTTLE_WATER "\xee\x93\x85" // U+e4c5 +#define ICON_FA_BOWL_FOOD "\xee\x93\x86" // U+e4c6 +#define ICON_FA_BOWL_RICE "\xee\x8b\xab" // U+e2eb +#define ICON_FA_BOWLING_BALL "\xef\x90\xb6" // U+f436 +#define ICON_FA_BOX "\xef\x91\xa6" // U+f466 +#define ICON_FA_BOX_ARCHIVE "\xef\x86\x87" // U+f187 +#define ICON_FA_BOX_OPEN "\xef\x92\x9e" // U+f49e +#define ICON_FA_BOX_TISSUE "\xee\x81\x9b" // U+e05b +#define ICON_FA_BOXES_PACKING "\xee\x93\x87" // U+e4c7 +#define ICON_FA_BOXES_STACKED "\xef\x91\xa8" // U+f468 +#define ICON_FA_BRAILLE "\xef\x8a\xa1" // U+f2a1 +#define ICON_FA_BRAIN "\xef\x97\x9c" // U+f5dc +#define ICON_FA_BRAZILIAN_REAL_SIGN "\xee\x91\xac" // U+e46c +#define ICON_FA_BREAD_SLICE "\xef\x9f\xac" // U+f7ec +#define ICON_FA_BRIDGE "\xee\x93\x88" // U+e4c8 +#define ICON_FA_BRIDGE_CIRCLE_CHECK "\xee\x93\x89" // U+e4c9 +#define ICON_FA_BRIDGE_CIRCLE_EXCLAMATION "\xee\x93\x8a" // U+e4ca +#define ICON_FA_BRIDGE_CIRCLE_XMARK "\xee\x93\x8b" // U+e4cb +#define ICON_FA_BRIDGE_LOCK "\xee\x93\x8c" // U+e4cc +#define ICON_FA_BRIDGE_WATER "\xee\x93\x8e" // U+e4ce +#define ICON_FA_BRIEFCASE "\xef\x82\xb1" // U+f0b1 +#define ICON_FA_BRIEFCASE_MEDICAL "\xef\x91\xa9" // U+f469 +#define ICON_FA_BROOM "\xef\x94\x9a" // U+f51a +#define ICON_FA_BROOM_BALL "\xef\x91\x98" // U+f458 +#define ICON_FA_BRUSH "\xef\x95\x9d" // U+f55d +#define ICON_FA_BUCKET "\xee\x93\x8f" // U+e4cf +#define ICON_FA_BUG "\xef\x86\x88" // U+f188 +#define ICON_FA_BUG_SLASH "\xee\x92\x90" // U+e490 +#define ICON_FA_BUGS "\xee\x93\x90" // U+e4d0 +#define ICON_FA_BUILDING "\xef\x86\xad" // U+f1ad +#define ICON_FA_BUILDING_CIRCLE_ARROW_RIGHT "\xee\x93\x91" // U+e4d1 +#define ICON_FA_BUILDING_CIRCLE_CHECK "\xee\x93\x92" // U+e4d2 +#define ICON_FA_BUILDING_CIRCLE_EXCLAMATION "\xee\x93\x93" // U+e4d3 +#define ICON_FA_BUILDING_CIRCLE_XMARK "\xee\x93\x94" // U+e4d4 +#define ICON_FA_BUILDING_COLUMNS "\xef\x86\x9c" // U+f19c +#define ICON_FA_BUILDING_FLAG "\xee\x93\x95" // U+e4d5 +#define ICON_FA_BUILDING_LOCK "\xee\x93\x96" // U+e4d6 +#define ICON_FA_BUILDING_NGO "\xee\x93\x97" // U+e4d7 +#define ICON_FA_BUILDING_SHIELD "\xee\x93\x98" // U+e4d8 +#define ICON_FA_BUILDING_UN "\xee\x93\x99" // U+e4d9 +#define ICON_FA_BUILDING_USER "\xee\x93\x9a" // U+e4da +#define ICON_FA_BUILDING_WHEAT "\xee\x93\x9b" // U+e4db +#define ICON_FA_BULLHORN "\xef\x82\xa1" // U+f0a1 +#define ICON_FA_BULLSEYE "\xef\x85\x80" // U+f140 +#define ICON_FA_BURGER "\xef\xa0\x85" // U+f805 +#define ICON_FA_BURST "\xee\x93\x9c" // U+e4dc +#define ICON_FA_BUS "\xef\x88\x87" // U+f207 +#define ICON_FA_BUS_SIMPLE "\xef\x95\x9e" // U+f55e +#define ICON_FA_BUSINESS_TIME "\xef\x99\x8a" // U+f64a +#define ICON_FA_C "C" // U+0043 +#define ICON_FA_CABLE_CAR "\xef\x9f\x9a" // U+f7da +#define ICON_FA_CAKE_CANDLES "\xef\x87\xbd" // U+f1fd +#define ICON_FA_CALCULATOR "\xef\x87\xac" // U+f1ec +#define ICON_FA_CALENDAR "\xef\x84\xb3" // U+f133 +#define ICON_FA_CALENDAR_CHECK "\xef\x89\xb4" // U+f274 +#define ICON_FA_CALENDAR_DAY "\xef\x9e\x83" // U+f783 +#define ICON_FA_CALENDAR_DAYS "\xef\x81\xb3" // U+f073 +#define ICON_FA_CALENDAR_MINUS "\xef\x89\xb2" // U+f272 +#define ICON_FA_CALENDAR_PLUS "\xef\x89\xb1" // U+f271 +#define ICON_FA_CALENDAR_WEEK "\xef\x9e\x84" // U+f784 +#define ICON_FA_CALENDAR_XMARK "\xef\x89\xb3" // U+f273 +#define ICON_FA_CAMERA "\xef\x80\xb0" // U+f030 +#define ICON_FA_CAMERA_RETRO "\xef\x82\x83" // U+f083 +#define ICON_FA_CAMERA_ROTATE "\xee\x83\x98" // U+e0d8 +#define ICON_FA_CAMPGROUND "\xef\x9a\xbb" // U+f6bb +#define ICON_FA_CANDY_CANE "\xef\x9e\x86" // U+f786 +#define ICON_FA_CANNABIS "\xef\x95\x9f" // U+f55f +#define ICON_FA_CAPSULES "\xef\x91\xab" // U+f46b +#define ICON_FA_CAR "\xef\x86\xb9" // U+f1b9 +#define ICON_FA_CAR_BATTERY "\xef\x97\x9f" // U+f5df +#define ICON_FA_CAR_BURST "\xef\x97\xa1" // U+f5e1 +#define ICON_FA_CAR_ON "\xee\x93\x9d" // U+e4dd +#define ICON_FA_CAR_REAR "\xef\x97\x9e" // U+f5de +#define ICON_FA_CAR_SIDE "\xef\x97\xa4" // U+f5e4 +#define ICON_FA_CAR_TUNNEL "\xee\x93\x9e" // U+e4de +#define ICON_FA_CARAVAN "\xef\xa3\xbf" // U+f8ff +#define ICON_FA_CARET_DOWN "\xef\x83\x97" // U+f0d7 +#define ICON_FA_CARET_LEFT "\xef\x83\x99" // U+f0d9 +#define ICON_FA_CARET_RIGHT "\xef\x83\x9a" // U+f0da +#define ICON_FA_CARET_UP "\xef\x83\x98" // U+f0d8 +#define ICON_FA_CARROT "\xef\x9e\x87" // U+f787 +#define ICON_FA_CART_ARROW_DOWN "\xef\x88\x98" // U+f218 +#define ICON_FA_CART_FLATBED "\xef\x91\xb4" // U+f474 +#define ICON_FA_CART_FLATBED_SUITCASE "\xef\x96\x9d" // U+f59d +#define ICON_FA_CART_PLUS "\xef\x88\x97" // U+f217 +#define ICON_FA_CART_SHOPPING "\xef\x81\xba" // U+f07a +#define ICON_FA_CASH_REGISTER "\xef\x9e\x88" // U+f788 +#define ICON_FA_CAT "\xef\x9a\xbe" // U+f6be +#define ICON_FA_CEDI_SIGN "\xee\x83\x9f" // U+e0df +#define ICON_FA_CENT_SIGN "\xee\x8f\xb5" // U+e3f5 +#define ICON_FA_CERTIFICATE "\xef\x82\xa3" // U+f0a3 +#define ICON_FA_CHAIR "\xef\x9b\x80" // U+f6c0 +#define ICON_FA_CHALKBOARD "\xef\x94\x9b" // U+f51b +#define ICON_FA_CHALKBOARD_USER "\xef\x94\x9c" // U+f51c +#define ICON_FA_CHAMPAGNE_GLASSES "\xef\x9e\x9f" // U+f79f +#define ICON_FA_CHARGING_STATION "\xef\x97\xa7" // U+f5e7 +#define ICON_FA_CHART_AREA "\xef\x87\xbe" // U+f1fe +#define ICON_FA_CHART_BAR "\xef\x82\x80" // U+f080 +#define ICON_FA_CHART_COLUMN "\xee\x83\xa3" // U+e0e3 +#define ICON_FA_CHART_GANTT "\xee\x83\xa4" // U+e0e4 +#define ICON_FA_CHART_LINE "\xef\x88\x81" // U+f201 +#define ICON_FA_CHART_PIE "\xef\x88\x80" // U+f200 +#define ICON_FA_CHART_SIMPLE "\xee\x91\xb3" // U+e473 +#define ICON_FA_CHECK "\xef\x80\x8c" // U+f00c +#define ICON_FA_CHECK_DOUBLE "\xef\x95\xa0" // U+f560 +#define ICON_FA_CHECK_TO_SLOT "\xef\x9d\xb2" // U+f772 +#define ICON_FA_CHEESE "\xef\x9f\xaf" // U+f7ef +#define ICON_FA_CHESS "\xef\x90\xb9" // U+f439 +#define ICON_FA_CHESS_BISHOP "\xef\x90\xba" // U+f43a +#define ICON_FA_CHESS_BOARD "\xef\x90\xbc" // U+f43c +#define ICON_FA_CHESS_KING "\xef\x90\xbf" // U+f43f +#define ICON_FA_CHESS_KNIGHT "\xef\x91\x81" // U+f441 +#define ICON_FA_CHESS_PAWN "\xef\x91\x83" // U+f443 +#define ICON_FA_CHESS_QUEEN "\xef\x91\x85" // U+f445 +#define ICON_FA_CHESS_ROOK "\xef\x91\x87" // U+f447 +#define ICON_FA_CHEVRON_DOWN "\xef\x81\xb8" // U+f078 +#define ICON_FA_CHEVRON_LEFT "\xef\x81\x93" // U+f053 +#define ICON_FA_CHEVRON_RIGHT "\xef\x81\x94" // U+f054 +#define ICON_FA_CHEVRON_UP "\xef\x81\xb7" // U+f077 +#define ICON_FA_CHILD "\xef\x86\xae" // U+f1ae +#define ICON_FA_CHILD_COMBATANT "\xee\x93\xa0" // U+e4e0 +#define ICON_FA_CHILD_DRESS "\xee\x96\x9c" // U+e59c +#define ICON_FA_CHILD_REACHING "\xee\x96\x9d" // U+e59d +#define ICON_FA_CHILDREN "\xee\x93\xa1" // U+e4e1 +#define ICON_FA_CHURCH "\xef\x94\x9d" // U+f51d +#define ICON_FA_CIRCLE "\xef\x84\x91" // U+f111 +#define ICON_FA_CIRCLE_ARROW_DOWN "\xef\x82\xab" // U+f0ab +#define ICON_FA_CIRCLE_ARROW_LEFT "\xef\x82\xa8" // U+f0a8 +#define ICON_FA_CIRCLE_ARROW_RIGHT "\xef\x82\xa9" // U+f0a9 +#define ICON_FA_CIRCLE_ARROW_UP "\xef\x82\xaa" // U+f0aa +#define ICON_FA_CIRCLE_CHECK "\xef\x81\x98" // U+f058 +#define ICON_FA_CIRCLE_CHEVRON_DOWN "\xef\x84\xba" // U+f13a +#define ICON_FA_CIRCLE_CHEVRON_LEFT "\xef\x84\xb7" // U+f137 +#define ICON_FA_CIRCLE_CHEVRON_RIGHT "\xef\x84\xb8" // U+f138 +#define ICON_FA_CIRCLE_CHEVRON_UP "\xef\x84\xb9" // U+f139 +#define ICON_FA_CIRCLE_DOLLAR_TO_SLOT "\xef\x92\xb9" // U+f4b9 +#define ICON_FA_CIRCLE_DOT "\xef\x86\x92" // U+f192 +#define ICON_FA_CIRCLE_DOWN "\xef\x8d\x98" // U+f358 +#define ICON_FA_CIRCLE_EXCLAMATION "\xef\x81\xaa" // U+f06a +#define ICON_FA_CIRCLE_H "\xef\x91\xbe" // U+f47e +#define ICON_FA_CIRCLE_HALF_STROKE "\xef\x81\x82" // U+f042 +#define ICON_FA_CIRCLE_INFO "\xef\x81\x9a" // U+f05a +#define ICON_FA_CIRCLE_LEFT "\xef\x8d\x99" // U+f359 +#define ICON_FA_CIRCLE_MINUS "\xef\x81\x96" // U+f056 +#define ICON_FA_CIRCLE_NODES "\xee\x93\xa2" // U+e4e2 +#define ICON_FA_CIRCLE_NOTCH "\xef\x87\x8e" // U+f1ce +#define ICON_FA_CIRCLE_PAUSE "\xef\x8a\x8b" // U+f28b +#define ICON_FA_CIRCLE_PLAY "\xef\x85\x84" // U+f144 +#define ICON_FA_CIRCLE_PLUS "\xef\x81\x95" // U+f055 +#define ICON_FA_CIRCLE_QUESTION "\xef\x81\x99" // U+f059 +#define ICON_FA_CIRCLE_RADIATION "\xef\x9e\xba" // U+f7ba +#define ICON_FA_CIRCLE_RIGHT "\xef\x8d\x9a" // U+f35a +#define ICON_FA_CIRCLE_STOP "\xef\x8a\x8d" // U+f28d +#define ICON_FA_CIRCLE_UP "\xef\x8d\x9b" // U+f35b +#define ICON_FA_CIRCLE_USER "\xef\x8a\xbd" // U+f2bd +#define ICON_FA_CIRCLE_XMARK "\xef\x81\x97" // U+f057 +#define ICON_FA_CITY "\xef\x99\x8f" // U+f64f +#define ICON_FA_CLAPPERBOARD "\xee\x84\xb1" // U+e131 +#define ICON_FA_CLIPBOARD "\xef\x8c\xa8" // U+f328 +#define ICON_FA_CLIPBOARD_CHECK "\xef\x91\xac" // U+f46c +#define ICON_FA_CLIPBOARD_LIST "\xef\x91\xad" // U+f46d +#define ICON_FA_CLIPBOARD_QUESTION "\xee\x93\xa3" // U+e4e3 +#define ICON_FA_CLIPBOARD_USER "\xef\x9f\xb3" // U+f7f3 +#define ICON_FA_CLOCK "\xef\x80\x97" // U+f017 +#define ICON_FA_CLOCK_ROTATE_LEFT "\xef\x87\x9a" // U+f1da +#define ICON_FA_CLONE "\xef\x89\x8d" // U+f24d +#define ICON_FA_CLOSED_CAPTIONING "\xef\x88\x8a" // U+f20a +#define ICON_FA_CLOUD "\xef\x83\x82" // U+f0c2 +#define ICON_FA_CLOUD_ARROW_DOWN "\xef\x83\xad" // U+f0ed +#define ICON_FA_CLOUD_ARROW_UP "\xef\x83\xae" // U+f0ee +#define ICON_FA_CLOUD_BOLT "\xef\x9d\xac" // U+f76c +#define ICON_FA_CLOUD_MEATBALL "\xef\x9c\xbb" // U+f73b +#define ICON_FA_CLOUD_MOON "\xef\x9b\x83" // U+f6c3 +#define ICON_FA_CLOUD_MOON_RAIN "\xef\x9c\xbc" // U+f73c +#define ICON_FA_CLOUD_RAIN "\xef\x9c\xbd" // U+f73d +#define ICON_FA_CLOUD_SHOWERS_HEAVY "\xef\x9d\x80" // U+f740 +#define ICON_FA_CLOUD_SHOWERS_WATER "\xee\x93\xa4" // U+e4e4 +#define ICON_FA_CLOUD_SUN "\xef\x9b\x84" // U+f6c4 +#define ICON_FA_CLOUD_SUN_RAIN "\xef\x9d\x83" // U+f743 +#define ICON_FA_CLOVER "\xee\x84\xb9" // U+e139 +#define ICON_FA_CODE "\xef\x84\xa1" // U+f121 +#define ICON_FA_CODE_BRANCH "\xef\x84\xa6" // U+f126 +#define ICON_FA_CODE_COMMIT "\xef\x8e\x86" // U+f386 +#define ICON_FA_CODE_COMPARE "\xee\x84\xba" // U+e13a +#define ICON_FA_CODE_FORK "\xee\x84\xbb" // U+e13b +#define ICON_FA_CODE_MERGE "\xef\x8e\x87" // U+f387 +#define ICON_FA_CODE_PULL_REQUEST "\xee\x84\xbc" // U+e13c +#define ICON_FA_COINS "\xef\x94\x9e" // U+f51e +#define ICON_FA_COLON_SIGN "\xee\x85\x80" // U+e140 +#define ICON_FA_COMMENT "\xef\x81\xb5" // U+f075 +#define ICON_FA_COMMENT_DOLLAR "\xef\x99\x91" // U+f651 +#define ICON_FA_COMMENT_DOTS "\xef\x92\xad" // U+f4ad +#define ICON_FA_COMMENT_MEDICAL "\xef\x9f\xb5" // U+f7f5 +#define ICON_FA_COMMENT_SLASH "\xef\x92\xb3" // U+f4b3 +#define ICON_FA_COMMENT_SMS "\xef\x9f\x8d" // U+f7cd +#define ICON_FA_COMMENTS "\xef\x82\x86" // U+f086 +#define ICON_FA_COMMENTS_DOLLAR "\xef\x99\x93" // U+f653 +#define ICON_FA_COMPACT_DISC "\xef\x94\x9f" // U+f51f +#define ICON_FA_COMPASS "\xef\x85\x8e" // U+f14e +#define ICON_FA_COMPASS_DRAFTING "\xef\x95\xa8" // U+f568 +#define ICON_FA_COMPRESS "\xef\x81\xa6" // U+f066 +#define ICON_FA_COMPUTER "\xee\x93\xa5" // U+e4e5 +#define ICON_FA_COMPUTER_MOUSE "\xef\xa3\x8c" // U+f8cc +#define ICON_FA_COOKIE "\xef\x95\xa3" // U+f563 +#define ICON_FA_COOKIE_BITE "\xef\x95\xa4" // U+f564 +#define ICON_FA_COPY "\xef\x83\x85" // U+f0c5 +#define ICON_FA_COPYRIGHT "\xef\x87\xb9" // U+f1f9 +#define ICON_FA_COUCH "\xef\x92\xb8" // U+f4b8 +#define ICON_FA_COW "\xef\x9b\x88" // U+f6c8 +#define ICON_FA_CREDIT_CARD "\xef\x82\x9d" // U+f09d +#define ICON_FA_CROP "\xef\x84\xa5" // U+f125 +#define ICON_FA_CROP_SIMPLE "\xef\x95\xa5" // U+f565 +#define ICON_FA_CROSS "\xef\x99\x94" // U+f654 +#define ICON_FA_CROSSHAIRS "\xef\x81\x9b" // U+f05b +#define ICON_FA_CROW "\xef\x94\xa0" // U+f520 +#define ICON_FA_CROWN "\xef\x94\xa1" // U+f521 +#define ICON_FA_CRUTCH "\xef\x9f\xb7" // U+f7f7 +#define ICON_FA_CRUZEIRO_SIGN "\xee\x85\x92" // U+e152 +#define ICON_FA_CUBE "\xef\x86\xb2" // U+f1b2 +#define ICON_FA_CUBES "\xef\x86\xb3" // U+f1b3 +#define ICON_FA_CUBES_STACKED "\xee\x93\xa6" // U+e4e6 +#define ICON_FA_D "D" // U+0044 +#define ICON_FA_DATABASE "\xef\x87\x80" // U+f1c0 +#define ICON_FA_DELETE_LEFT "\xef\x95\x9a" // U+f55a +#define ICON_FA_DEMOCRAT "\xef\x9d\x87" // U+f747 +#define ICON_FA_DESKTOP "\xef\x8e\x90" // U+f390 +#define ICON_FA_DHARMACHAKRA "\xef\x99\x95" // U+f655 +#define ICON_FA_DIAGRAM_NEXT "\xee\x91\xb6" // U+e476 +#define ICON_FA_DIAGRAM_PREDECESSOR "\xee\x91\xb7" // U+e477 +#define ICON_FA_DIAGRAM_PROJECT "\xef\x95\x82" // U+f542 +#define ICON_FA_DIAGRAM_SUCCESSOR "\xee\x91\xba" // U+e47a +#define ICON_FA_DIAMOND "\xef\x88\x99" // U+f219 +#define ICON_FA_DIAMOND_TURN_RIGHT "\xef\x97\xab" // U+f5eb +#define ICON_FA_DICE "\xef\x94\xa2" // U+f522 +#define ICON_FA_DICE_D20 "\xef\x9b\x8f" // U+f6cf +#define ICON_FA_DICE_D6 "\xef\x9b\x91" // U+f6d1 +#define ICON_FA_DICE_FIVE "\xef\x94\xa3" // U+f523 +#define ICON_FA_DICE_FOUR "\xef\x94\xa4" // U+f524 +#define ICON_FA_DICE_ONE "\xef\x94\xa5" // U+f525 +#define ICON_FA_DICE_SIX "\xef\x94\xa6" // U+f526 +#define ICON_FA_DICE_THREE "\xef\x94\xa7" // U+f527 +#define ICON_FA_DICE_TWO "\xef\x94\xa8" // U+f528 +#define ICON_FA_DISEASE "\xef\x9f\xba" // U+f7fa +#define ICON_FA_DISPLAY "\xee\x85\xa3" // U+e163 +#define ICON_FA_DIVIDE "\xef\x94\xa9" // U+f529 +#define ICON_FA_DNA "\xef\x91\xb1" // U+f471 +#define ICON_FA_DOG "\xef\x9b\x93" // U+f6d3 +#define ICON_FA_DOLLAR_SIGN "$" // U+0024 +#define ICON_FA_DOLLY "\xef\x91\xb2" // U+f472 +#define ICON_FA_DONG_SIGN "\xee\x85\xa9" // U+e169 +#define ICON_FA_DOOR_CLOSED "\xef\x94\xaa" // U+f52a +#define ICON_FA_DOOR_OPEN "\xef\x94\xab" // U+f52b +#define ICON_FA_DOVE "\xef\x92\xba" // U+f4ba +#define ICON_FA_DOWN_LEFT_AND_UP_RIGHT_TO_CENTER "\xef\x90\xa2" // U+f422 +#define ICON_FA_DOWN_LONG "\xef\x8c\x89" // U+f309 +#define ICON_FA_DOWNLOAD "\xef\x80\x99" // U+f019 +#define ICON_FA_DRAGON "\xef\x9b\x95" // U+f6d5 +#define ICON_FA_DRAW_POLYGON "\xef\x97\xae" // U+f5ee +#define ICON_FA_DROPLET "\xef\x81\x83" // U+f043 +#define ICON_FA_DROPLET_SLASH "\xef\x97\x87" // U+f5c7 +#define ICON_FA_DRUM "\xef\x95\xa9" // U+f569 +#define ICON_FA_DRUM_STEELPAN "\xef\x95\xaa" // U+f56a +#define ICON_FA_DRUMSTICK_BITE "\xef\x9b\x97" // U+f6d7 +#define ICON_FA_DUMBBELL "\xef\x91\x8b" // U+f44b +#define ICON_FA_DUMPSTER "\xef\x9e\x93" // U+f793 +#define ICON_FA_DUMPSTER_FIRE "\xef\x9e\x94" // U+f794 +#define ICON_FA_DUNGEON "\xef\x9b\x99" // U+f6d9 +#define ICON_FA_E "E" // U+0045 +#define ICON_FA_EAR_DEAF "\xef\x8a\xa4" // U+f2a4 +#define ICON_FA_EAR_LISTEN "\xef\x8a\xa2" // U+f2a2 +#define ICON_FA_EARTH_AFRICA "\xef\x95\xbc" // U+f57c +#define ICON_FA_EARTH_AMERICAS "\xef\x95\xbd" // U+f57d +#define ICON_FA_EARTH_ASIA "\xef\x95\xbe" // U+f57e +#define ICON_FA_EARTH_EUROPE "\xef\x9e\xa2" // U+f7a2 +#define ICON_FA_EARTH_OCEANIA "\xee\x91\xbb" // U+e47b +#define ICON_FA_EGG "\xef\x9f\xbb" // U+f7fb +#define ICON_FA_EJECT "\xef\x81\x92" // U+f052 +#define ICON_FA_ELEVATOR "\xee\x85\xad" // U+e16d +#define ICON_FA_ELLIPSIS "\xef\x85\x81" // U+f141 +#define ICON_FA_ELLIPSIS_VERTICAL "\xef\x85\x82" // U+f142 +#define ICON_FA_ENVELOPE "\xef\x83\xa0" // U+f0e0 +#define ICON_FA_ENVELOPE_CIRCLE_CHECK "\xee\x93\xa8" // U+e4e8 +#define ICON_FA_ENVELOPE_OPEN "\xef\x8a\xb6" // U+f2b6 +#define ICON_FA_ENVELOPE_OPEN_TEXT "\xef\x99\x98" // U+f658 +#define ICON_FA_ENVELOPES_BULK "\xef\x99\xb4" // U+f674 +#define ICON_FA_EQUALS "=" // U+003d +#define ICON_FA_ERASER "\xef\x84\xad" // U+f12d +#define ICON_FA_ETHERNET "\xef\x9e\x96" // U+f796 +#define ICON_FA_EURO_SIGN "\xef\x85\x93" // U+f153 +#define ICON_FA_EXCLAMATION "!" // U+0021 +#define ICON_FA_EXPAND "\xef\x81\xa5" // U+f065 +#define ICON_FA_EXPLOSION "\xee\x93\xa9" // U+e4e9 +#define ICON_FA_EYE "\xef\x81\xae" // U+f06e +#define ICON_FA_EYE_DROPPER "\xef\x87\xbb" // U+f1fb +#define ICON_FA_EYE_LOW_VISION "\xef\x8a\xa8" // U+f2a8 +#define ICON_FA_EYE_SLASH "\xef\x81\xb0" // U+f070 +#define ICON_FA_F "F" // U+0046 +#define ICON_FA_FACE_ANGRY "\xef\x95\x96" // U+f556 +#define ICON_FA_FACE_DIZZY "\xef\x95\xa7" // U+f567 +#define ICON_FA_FACE_FLUSHED "\xef\x95\xb9" // U+f579 +#define ICON_FA_FACE_FROWN "\xef\x84\x99" // U+f119 +#define ICON_FA_FACE_FROWN_OPEN "\xef\x95\xba" // U+f57a +#define ICON_FA_FACE_GRIMACE "\xef\x95\xbf" // U+f57f +#define ICON_FA_FACE_GRIN "\xef\x96\x80" // U+f580 +#define ICON_FA_FACE_GRIN_BEAM "\xef\x96\x82" // U+f582 +#define ICON_FA_FACE_GRIN_BEAM_SWEAT "\xef\x96\x83" // U+f583 +#define ICON_FA_FACE_GRIN_HEARTS "\xef\x96\x84" // U+f584 +#define ICON_FA_FACE_GRIN_SQUINT "\xef\x96\x85" // U+f585 +#define ICON_FA_FACE_GRIN_SQUINT_TEARS "\xef\x96\x86" // U+f586 +#define ICON_FA_FACE_GRIN_STARS "\xef\x96\x87" // U+f587 +#define ICON_FA_FACE_GRIN_TEARS "\xef\x96\x88" // U+f588 +#define ICON_FA_FACE_GRIN_TONGUE "\xef\x96\x89" // U+f589 +#define ICON_FA_FACE_GRIN_TONGUE_SQUINT "\xef\x96\x8a" // U+f58a +#define ICON_FA_FACE_GRIN_TONGUE_WINK "\xef\x96\x8b" // U+f58b +#define ICON_FA_FACE_GRIN_WIDE "\xef\x96\x81" // U+f581 +#define ICON_FA_FACE_GRIN_WINK "\xef\x96\x8c" // U+f58c +#define ICON_FA_FACE_KISS "\xef\x96\x96" // U+f596 +#define ICON_FA_FACE_KISS_BEAM "\xef\x96\x97" // U+f597 +#define ICON_FA_FACE_KISS_WINK_HEART "\xef\x96\x98" // U+f598 +#define ICON_FA_FACE_LAUGH "\xef\x96\x99" // U+f599 +#define ICON_FA_FACE_LAUGH_BEAM "\xef\x96\x9a" // U+f59a +#define ICON_FA_FACE_LAUGH_SQUINT "\xef\x96\x9b" // U+f59b +#define ICON_FA_FACE_LAUGH_WINK "\xef\x96\x9c" // U+f59c +#define ICON_FA_FACE_MEH "\xef\x84\x9a" // U+f11a +#define ICON_FA_FACE_MEH_BLANK "\xef\x96\xa4" // U+f5a4 +#define ICON_FA_FACE_ROLLING_EYES "\xef\x96\xa5" // U+f5a5 +#define ICON_FA_FACE_SAD_CRY "\xef\x96\xb3" // U+f5b3 +#define ICON_FA_FACE_SAD_TEAR "\xef\x96\xb4" // U+f5b4 +#define ICON_FA_FACE_SMILE "\xef\x84\x98" // U+f118 +#define ICON_FA_FACE_SMILE_BEAM "\xef\x96\xb8" // U+f5b8 +#define ICON_FA_FACE_SMILE_WINK "\xef\x93\x9a" // U+f4da +#define ICON_FA_FACE_SURPRISE "\xef\x97\x82" // U+f5c2 +#define ICON_FA_FACE_TIRED "\xef\x97\x88" // U+f5c8 +#define ICON_FA_FAN "\xef\xa1\xa3" // U+f863 +#define ICON_FA_FAUCET "\xee\x80\x85" // U+e005 +#define ICON_FA_FAUCET_DRIP "\xee\x80\x86" // U+e006 +#define ICON_FA_FAX "\xef\x86\xac" // U+f1ac +#define ICON_FA_FEATHER "\xef\x94\xad" // U+f52d +#define ICON_FA_FEATHER_POINTED "\xef\x95\xab" // U+f56b +#define ICON_FA_FERRY "\xee\x93\xaa" // U+e4ea +#define ICON_FA_FILE "\xef\x85\x9b" // U+f15b +#define ICON_FA_FILE_ARROW_DOWN "\xef\x95\xad" // U+f56d +#define ICON_FA_FILE_ARROW_UP "\xef\x95\xb4" // U+f574 +#define ICON_FA_FILE_AUDIO "\xef\x87\x87" // U+f1c7 +#define ICON_FA_FILE_CIRCLE_CHECK "\xee\x96\xa0" // U+e5a0 +#define ICON_FA_FILE_CIRCLE_EXCLAMATION "\xee\x93\xab" // U+e4eb +#define ICON_FA_FILE_CIRCLE_MINUS "\xee\x93\xad" // U+e4ed +#define ICON_FA_FILE_CIRCLE_PLUS "\xee\x92\x94" // U+e494 +#define ICON_FA_FILE_CIRCLE_QUESTION "\xee\x93\xaf" // U+e4ef +#define ICON_FA_FILE_CIRCLE_XMARK "\xee\x96\xa1" // U+e5a1 +#define ICON_FA_FILE_CODE "\xef\x87\x89" // U+f1c9 +#define ICON_FA_FILE_CONTRACT "\xef\x95\xac" // U+f56c +#define ICON_FA_FILE_CSV "\xef\x9b\x9d" // U+f6dd +#define ICON_FA_FILE_EXCEL "\xef\x87\x83" // U+f1c3 +#define ICON_FA_FILE_EXPORT "\xef\x95\xae" // U+f56e +#define ICON_FA_FILE_IMAGE "\xef\x87\x85" // U+f1c5 +#define ICON_FA_FILE_IMPORT "\xef\x95\xaf" // U+f56f +#define ICON_FA_FILE_INVOICE "\xef\x95\xb0" // U+f570 +#define ICON_FA_FILE_INVOICE_DOLLAR "\xef\x95\xb1" // U+f571 +#define ICON_FA_FILE_LINES "\xef\x85\x9c" // U+f15c +#define ICON_FA_FILE_MEDICAL "\xef\x91\xb7" // U+f477 +#define ICON_FA_FILE_PDF "\xef\x87\x81" // U+f1c1 +#define ICON_FA_FILE_PEN "\xef\x8c\x9c" // U+f31c +#define ICON_FA_FILE_POWERPOINT "\xef\x87\x84" // U+f1c4 +#define ICON_FA_FILE_PRESCRIPTION "\xef\x95\xb2" // U+f572 +#define ICON_FA_FILE_SHIELD "\xee\x93\xb0" // U+e4f0 +#define ICON_FA_FILE_SIGNATURE "\xef\x95\xb3" // U+f573 +#define ICON_FA_FILE_VIDEO "\xef\x87\x88" // U+f1c8 +#define ICON_FA_FILE_WAVEFORM "\xef\x91\xb8" // U+f478 +#define ICON_FA_FILE_WORD "\xef\x87\x82" // U+f1c2 +#define ICON_FA_FILE_ZIPPER "\xef\x87\x86" // U+f1c6 +#define ICON_FA_FILL "\xef\x95\xb5" // U+f575 +#define ICON_FA_FILL_DRIP "\xef\x95\xb6" // U+f576 +#define ICON_FA_FILM "\xef\x80\x88" // U+f008 +#define ICON_FA_FILTER "\xef\x82\xb0" // U+f0b0 +#define ICON_FA_FILTER_CIRCLE_DOLLAR "\xef\x99\xa2" // U+f662 +#define ICON_FA_FILTER_CIRCLE_XMARK "\xee\x85\xbb" // U+e17b +#define ICON_FA_FINGERPRINT "\xef\x95\xb7" // U+f577 +#define ICON_FA_FIRE "\xef\x81\xad" // U+f06d +#define ICON_FA_FIRE_BURNER "\xee\x93\xb1" // U+e4f1 +#define ICON_FA_FIRE_EXTINGUISHER "\xef\x84\xb4" // U+f134 +#define ICON_FA_FIRE_FLAME_CURVED "\xef\x9f\xa4" // U+f7e4 +#define ICON_FA_FIRE_FLAME_SIMPLE "\xef\x91\xaa" // U+f46a +#define ICON_FA_FISH "\xef\x95\xb8" // U+f578 +#define ICON_FA_FISH_FINS "\xee\x93\xb2" // U+e4f2 +#define ICON_FA_FLAG "\xef\x80\xa4" // U+f024 +#define ICON_FA_FLAG_CHECKERED "\xef\x84\x9e" // U+f11e +#define ICON_FA_FLAG_USA "\xef\x9d\x8d" // U+f74d +#define ICON_FA_FLASK "\xef\x83\x83" // U+f0c3 +#define ICON_FA_FLASK_VIAL "\xee\x93\xb3" // U+e4f3 +#define ICON_FA_FLOPPY_DISK "\xef\x83\x87" // U+f0c7 +#define ICON_FA_FLORIN_SIGN "\xee\x86\x84" // U+e184 +#define ICON_FA_FOLDER "\xef\x81\xbb" // U+f07b +#define ICON_FA_FOLDER_CLOSED "\xee\x86\x85" // U+e185 +#define ICON_FA_FOLDER_MINUS "\xef\x99\x9d" // U+f65d +#define ICON_FA_FOLDER_OPEN "\xef\x81\xbc" // U+f07c +#define ICON_FA_FOLDER_PLUS "\xef\x99\x9e" // U+f65e +#define ICON_FA_FOLDER_TREE "\xef\xa0\x82" // U+f802 +#define ICON_FA_FONT "\xef\x80\xb1" // U+f031 +#define ICON_FA_FONT_AWESOME "\xef\x8a\xb4" // U+f2b4 +#define ICON_FA_FOOTBALL "\xef\x91\x8e" // U+f44e +#define ICON_FA_FORWARD "\xef\x81\x8e" // U+f04e +#define ICON_FA_FORWARD_FAST "\xef\x81\x90" // U+f050 +#define ICON_FA_FORWARD_STEP "\xef\x81\x91" // U+f051 +#define ICON_FA_FRANC_SIGN "\xee\x86\x8f" // U+e18f +#define ICON_FA_FROG "\xef\x94\xae" // U+f52e +#define ICON_FA_FUTBOL "\xef\x87\xa3" // U+f1e3 +#define ICON_FA_G "G" // U+0047 +#define ICON_FA_GAMEPAD "\xef\x84\x9b" // U+f11b +#define ICON_FA_GAS_PUMP "\xef\x94\xaf" // U+f52f +#define ICON_FA_GAUGE "\xef\x98\xa4" // U+f624 +#define ICON_FA_GAUGE_HIGH "\xef\x98\xa5" // U+f625 +#define ICON_FA_GAUGE_SIMPLE "\xef\x98\xa9" // U+f629 +#define ICON_FA_GAUGE_SIMPLE_HIGH "\xef\x98\xaa" // U+f62a +#define ICON_FA_GAVEL "\xef\x83\xa3" // U+f0e3 +#define ICON_FA_GEAR "\xef\x80\x93" // U+f013 +#define ICON_FA_GEARS "\xef\x82\x85" // U+f085 +#define ICON_FA_GEM "\xef\x8e\xa5" // U+f3a5 +#define ICON_FA_GENDERLESS "\xef\x88\xad" // U+f22d +#define ICON_FA_GHOST "\xef\x9b\xa2" // U+f6e2 +#define ICON_FA_GIFT "\xef\x81\xab" // U+f06b +#define ICON_FA_GIFTS "\xef\x9e\x9c" // U+f79c +#define ICON_FA_GLASS_WATER "\xee\x93\xb4" // U+e4f4 +#define ICON_FA_GLASS_WATER_DROPLET "\xee\x93\xb5" // U+e4f5 +#define ICON_FA_GLASSES "\xef\x94\xb0" // U+f530 +#define ICON_FA_GLOBE "\xef\x82\xac" // U+f0ac +#define ICON_FA_GOLF_BALL_TEE "\xef\x91\x90" // U+f450 +#define ICON_FA_GOPURAM "\xef\x99\xa4" // U+f664 +#define ICON_FA_GRADUATION_CAP "\xef\x86\x9d" // U+f19d +#define ICON_FA_GREATER_THAN ">" // U+003e +#define ICON_FA_GREATER_THAN_EQUAL "\xef\x94\xb2" // U+f532 +#define ICON_FA_GRIP "\xef\x96\x8d" // U+f58d +#define ICON_FA_GRIP_LINES "\xef\x9e\xa4" // U+f7a4 +#define ICON_FA_GRIP_LINES_VERTICAL "\xef\x9e\xa5" // U+f7a5 +#define ICON_FA_GRIP_VERTICAL "\xef\x96\x8e" // U+f58e +#define ICON_FA_GROUP_ARROWS_ROTATE "\xee\x93\xb6" // U+e4f6 +#define ICON_FA_GUARANI_SIGN "\xee\x86\x9a" // U+e19a +#define ICON_FA_GUITAR "\xef\x9e\xa6" // U+f7a6 +#define ICON_FA_GUN "\xee\x86\x9b" // U+e19b +#define ICON_FA_H "H" // U+0048 +#define ICON_FA_HAMMER "\xef\x9b\xa3" // U+f6e3 +#define ICON_FA_HAMSA "\xef\x99\xa5" // U+f665 +#define ICON_FA_HAND "\xef\x89\x96" // U+f256 +#define ICON_FA_HAND_BACK_FIST "\xef\x89\x95" // U+f255 +#define ICON_FA_HAND_DOTS "\xef\x91\xa1" // U+f461 +#define ICON_FA_HAND_FIST "\xef\x9b\x9e" // U+f6de +#define ICON_FA_HAND_HOLDING "\xef\x92\xbd" // U+f4bd +#define ICON_FA_HAND_HOLDING_DOLLAR "\xef\x93\x80" // U+f4c0 +#define ICON_FA_HAND_HOLDING_DROPLET "\xef\x93\x81" // U+f4c1 +#define ICON_FA_HAND_HOLDING_HAND "\xee\x93\xb7" // U+e4f7 +#define ICON_FA_HAND_HOLDING_HEART "\xef\x92\xbe" // U+f4be +#define ICON_FA_HAND_HOLDING_MEDICAL "\xee\x81\x9c" // U+e05c +#define ICON_FA_HAND_LIZARD "\xef\x89\x98" // U+f258 +#define ICON_FA_HAND_MIDDLE_FINGER "\xef\xa0\x86" // U+f806 +#define ICON_FA_HAND_PEACE "\xef\x89\x9b" // U+f25b +#define ICON_FA_HAND_POINT_DOWN "\xef\x82\xa7" // U+f0a7 +#define ICON_FA_HAND_POINT_LEFT "\xef\x82\xa5" // U+f0a5 +#define ICON_FA_HAND_POINT_RIGHT "\xef\x82\xa4" // U+f0a4 +#define ICON_FA_HAND_POINT_UP "\xef\x82\xa6" // U+f0a6 +#define ICON_FA_HAND_POINTER "\xef\x89\x9a" // U+f25a +#define ICON_FA_HAND_SCISSORS "\xef\x89\x97" // U+f257 +#define ICON_FA_HAND_SPARKLES "\xee\x81\x9d" // U+e05d +#define ICON_FA_HAND_SPOCK "\xef\x89\x99" // U+f259 +#define ICON_FA_HANDCUFFS "\xee\x93\xb8" // U+e4f8 +#define ICON_FA_HANDS "\xef\x8a\xa7" // U+f2a7 +#define ICON_FA_HANDS_ASL_INTERPRETING "\xef\x8a\xa3" // U+f2a3 +#define ICON_FA_HANDS_BOUND "\xee\x93\xb9" // U+e4f9 +#define ICON_FA_HANDS_BUBBLES "\xee\x81\x9e" // U+e05e +#define ICON_FA_HANDS_CLAPPING "\xee\x86\xa8" // U+e1a8 +#define ICON_FA_HANDS_HOLDING "\xef\x93\x82" // U+f4c2 +#define ICON_FA_HANDS_HOLDING_CHILD "\xee\x93\xba" // U+e4fa +#define ICON_FA_HANDS_HOLDING_CIRCLE "\xee\x93\xbb" // U+e4fb +#define ICON_FA_HANDS_PRAYING "\xef\x9a\x84" // U+f684 +#define ICON_FA_HANDSHAKE "\xef\x8a\xb5" // U+f2b5 +#define ICON_FA_HANDSHAKE_ANGLE "\xef\x93\x84" // U+f4c4 +#define ICON_FA_HANDSHAKE_SIMPLE "\xef\x93\x86" // U+f4c6 +#define ICON_FA_HANDSHAKE_SIMPLE_SLASH "\xee\x81\x9f" // U+e05f +#define ICON_FA_HANDSHAKE_SLASH "\xee\x81\xa0" // U+e060 +#define ICON_FA_HANUKIAH "\xef\x9b\xa6" // U+f6e6 +#define ICON_FA_HARD_DRIVE "\xef\x82\xa0" // U+f0a0 +#define ICON_FA_HASHTAG "#" // U+0023 +#define ICON_FA_HAT_COWBOY "\xef\xa3\x80" // U+f8c0 +#define ICON_FA_HAT_COWBOY_SIDE "\xef\xa3\x81" // U+f8c1 +#define ICON_FA_HAT_WIZARD "\xef\x9b\xa8" // U+f6e8 +#define ICON_FA_HEAD_SIDE_COUGH "\xee\x81\xa1" // U+e061 +#define ICON_FA_HEAD_SIDE_COUGH_SLASH "\xee\x81\xa2" // U+e062 +#define ICON_FA_HEAD_SIDE_MASK "\xee\x81\xa3" // U+e063 +#define ICON_FA_HEAD_SIDE_VIRUS "\xee\x81\xa4" // U+e064 +#define ICON_FA_HEADING "\xef\x87\x9c" // U+f1dc +#define ICON_FA_HEADPHONES "\xef\x80\xa5" // U+f025 +#define ICON_FA_HEADPHONES_SIMPLE "\xef\x96\x8f" // U+f58f +#define ICON_FA_HEADSET "\xef\x96\x90" // U+f590 +#define ICON_FA_HEART "\xef\x80\x84" // U+f004 +#define ICON_FA_HEART_CIRCLE_BOLT "\xee\x93\xbc" // U+e4fc +#define ICON_FA_HEART_CIRCLE_CHECK "\xee\x93\xbd" // U+e4fd +#define ICON_FA_HEART_CIRCLE_EXCLAMATION "\xee\x93\xbe" // U+e4fe +#define ICON_FA_HEART_CIRCLE_MINUS "\xee\x93\xbf" // U+e4ff +#define ICON_FA_HEART_CIRCLE_PLUS "\xee\x94\x80" // U+e500 +#define ICON_FA_HEART_CIRCLE_XMARK "\xee\x94\x81" // U+e501 +#define ICON_FA_HEART_CRACK "\xef\x9e\xa9" // U+f7a9 +#define ICON_FA_HEART_PULSE "\xef\x88\x9e" // U+f21e +#define ICON_FA_HELICOPTER "\xef\x94\xb3" // U+f533 +#define ICON_FA_HELICOPTER_SYMBOL "\xee\x94\x82" // U+e502 +#define ICON_FA_HELMET_SAFETY "\xef\xa0\x87" // U+f807 +#define ICON_FA_HELMET_UN "\xee\x94\x83" // U+e503 +#define ICON_FA_HIGHLIGHTER "\xef\x96\x91" // U+f591 +#define ICON_FA_HILL_AVALANCHE "\xee\x94\x87" // U+e507 +#define ICON_FA_HILL_ROCKSLIDE "\xee\x94\x88" // U+e508 +#define ICON_FA_HIPPO "\xef\x9b\xad" // U+f6ed +#define ICON_FA_HOCKEY_PUCK "\xef\x91\x93" // U+f453 +#define ICON_FA_HOLLY_BERRY "\xef\x9e\xaa" // U+f7aa +#define ICON_FA_HORSE "\xef\x9b\xb0" // U+f6f0 +#define ICON_FA_HORSE_HEAD "\xef\x9e\xab" // U+f7ab +#define ICON_FA_HOSPITAL "\xef\x83\xb8" // U+f0f8 +#define ICON_FA_HOSPITAL_USER "\xef\xa0\x8d" // U+f80d +#define ICON_FA_HOT_TUB_PERSON "\xef\x96\x93" // U+f593 +#define ICON_FA_HOTDOG "\xef\xa0\x8f" // U+f80f +#define ICON_FA_HOTEL "\xef\x96\x94" // U+f594 +#define ICON_FA_HOURGLASS "\xef\x89\x94" // U+f254 +#define ICON_FA_HOURGLASS_END "\xef\x89\x93" // U+f253 +#define ICON_FA_HOURGLASS_HALF "\xef\x89\x92" // U+f252 +#define ICON_FA_HOURGLASS_START "\xef\x89\x91" // U+f251 +#define ICON_FA_HOUSE "\xef\x80\x95" // U+f015 +#define ICON_FA_HOUSE_CHIMNEY "\xee\x8e\xaf" // U+e3af +#define ICON_FA_HOUSE_CHIMNEY_CRACK "\xef\x9b\xb1" // U+f6f1 +#define ICON_FA_HOUSE_CHIMNEY_MEDICAL "\xef\x9f\xb2" // U+f7f2 +#define ICON_FA_HOUSE_CHIMNEY_USER "\xee\x81\xa5" // U+e065 +#define ICON_FA_HOUSE_CHIMNEY_WINDOW "\xee\x80\x8d" // U+e00d +#define ICON_FA_HOUSE_CIRCLE_CHECK "\xee\x94\x89" // U+e509 +#define ICON_FA_HOUSE_CIRCLE_EXCLAMATION "\xee\x94\x8a" // U+e50a +#define ICON_FA_HOUSE_CIRCLE_XMARK "\xee\x94\x8b" // U+e50b +#define ICON_FA_HOUSE_CRACK "\xee\x8e\xb1" // U+e3b1 +#define ICON_FA_HOUSE_FIRE "\xee\x94\x8c" // U+e50c +#define ICON_FA_HOUSE_FLAG "\xee\x94\x8d" // U+e50d +#define ICON_FA_HOUSE_FLOOD_WATER "\xee\x94\x8e" // U+e50e +#define ICON_FA_HOUSE_FLOOD_WATER_CIRCLE_ARROW_RIGHT "\xee\x94\x8f" // U+e50f +#define ICON_FA_HOUSE_LAPTOP "\xee\x81\xa6" // U+e066 +#define ICON_FA_HOUSE_LOCK "\xee\x94\x90" // U+e510 +#define ICON_FA_HOUSE_MEDICAL "\xee\x8e\xb2" // U+e3b2 +#define ICON_FA_HOUSE_MEDICAL_CIRCLE_CHECK "\xee\x94\x91" // U+e511 +#define ICON_FA_HOUSE_MEDICAL_CIRCLE_EXCLAMATION "\xee\x94\x92" // U+e512 +#define ICON_FA_HOUSE_MEDICAL_CIRCLE_XMARK "\xee\x94\x93" // U+e513 +#define ICON_FA_HOUSE_MEDICAL_FLAG "\xee\x94\x94" // U+e514 +#define ICON_FA_HOUSE_SIGNAL "\xee\x80\x92" // U+e012 +#define ICON_FA_HOUSE_TSUNAMI "\xee\x94\x95" // U+e515 +#define ICON_FA_HOUSE_USER "\xee\x86\xb0" // U+e1b0 +#define ICON_FA_HRYVNIA_SIGN "\xef\x9b\xb2" // U+f6f2 +#define ICON_FA_HURRICANE "\xef\x9d\x91" // U+f751 +#define ICON_FA_I "I" // U+0049 +#define ICON_FA_I_CURSOR "\xef\x89\x86" // U+f246 +#define ICON_FA_ICE_CREAM "\xef\xa0\x90" // U+f810 +#define ICON_FA_ICICLES "\xef\x9e\xad" // U+f7ad +#define ICON_FA_ICONS "\xef\xa1\xad" // U+f86d +#define ICON_FA_ID_BADGE "\xef\x8b\x81" // U+f2c1 +#define ICON_FA_ID_CARD "\xef\x8b\x82" // U+f2c2 +#define ICON_FA_ID_CARD_CLIP "\xef\x91\xbf" // U+f47f +#define ICON_FA_IGLOO "\xef\x9e\xae" // U+f7ae +#define ICON_FA_IMAGE "\xef\x80\xbe" // U+f03e +#define ICON_FA_IMAGE_PORTRAIT "\xef\x8f\xa0" // U+f3e0 +#define ICON_FA_IMAGES "\xef\x8c\x82" // U+f302 +#define ICON_FA_INBOX "\xef\x80\x9c" // U+f01c +#define ICON_FA_INDENT "\xef\x80\xbc" // U+f03c +#define ICON_FA_INDIAN_RUPEE_SIGN "\xee\x86\xbc" // U+e1bc +#define ICON_FA_INDUSTRY "\xef\x89\xb5" // U+f275 +#define ICON_FA_INFINITY "\xef\x94\xb4" // U+f534 +#define ICON_FA_INFO "\xef\x84\xa9" // U+f129 +#define ICON_FA_ITALIC "\xef\x80\xb3" // U+f033 +#define ICON_FA_J "J" // U+004a +#define ICON_FA_JAR "\xee\x94\x96" // U+e516 +#define ICON_FA_JAR_WHEAT "\xee\x94\x97" // U+e517 +#define ICON_FA_JEDI "\xef\x99\xa9" // U+f669 +#define ICON_FA_JET_FIGHTER "\xef\x83\xbb" // U+f0fb +#define ICON_FA_JET_FIGHTER_UP "\xee\x94\x98" // U+e518 +#define ICON_FA_JOINT "\xef\x96\x95" // U+f595 +#define ICON_FA_JUG_DETERGENT "\xee\x94\x99" // U+e519 +#define ICON_FA_K "K" // U+004b +#define ICON_FA_KAABA "\xef\x99\xab" // U+f66b +#define ICON_FA_KEY "\xef\x82\x84" // U+f084 +#define ICON_FA_KEYBOARD "\xef\x84\x9c" // U+f11c +#define ICON_FA_KHANDA "\xef\x99\xad" // U+f66d +#define ICON_FA_KIP_SIGN "\xee\x87\x84" // U+e1c4 +#define ICON_FA_KIT_MEDICAL "\xef\x91\xb9" // U+f479 +#define ICON_FA_KITCHEN_SET "\xee\x94\x9a" // U+e51a +#define ICON_FA_KIWI_BIRD "\xef\x94\xb5" // U+f535 +#define ICON_FA_L "L" // U+004c +#define ICON_FA_LAND_MINE_ON "\xee\x94\x9b" // U+e51b +#define ICON_FA_LANDMARK "\xef\x99\xaf" // U+f66f +#define ICON_FA_LANDMARK_DOME "\xef\x9d\x92" // U+f752 +#define ICON_FA_LANDMARK_FLAG "\xee\x94\x9c" // U+e51c +#define ICON_FA_LANGUAGE "\xef\x86\xab" // U+f1ab +#define ICON_FA_LAPTOP "\xef\x84\x89" // U+f109 +#define ICON_FA_LAPTOP_CODE "\xef\x97\xbc" // U+f5fc +#define ICON_FA_LAPTOP_FILE "\xee\x94\x9d" // U+e51d +#define ICON_FA_LAPTOP_MEDICAL "\xef\xa0\x92" // U+f812 +#define ICON_FA_LARI_SIGN "\xee\x87\x88" // U+e1c8 +#define ICON_FA_LAYER_GROUP "\xef\x97\xbd" // U+f5fd +#define ICON_FA_LEAF "\xef\x81\xac" // U+f06c +#define ICON_FA_LEFT_LONG "\xef\x8c\x8a" // U+f30a +#define ICON_FA_LEFT_RIGHT "\xef\x8c\xb7" // U+f337 +#define ICON_FA_LEMON "\xef\x82\x94" // U+f094 +#define ICON_FA_LESS_THAN "<" // U+003c +#define ICON_FA_LESS_THAN_EQUAL "\xef\x94\xb7" // U+f537 +#define ICON_FA_LIFE_RING "\xef\x87\x8d" // U+f1cd +#define ICON_FA_LIGHTBULB "\xef\x83\xab" // U+f0eb +#define ICON_FA_LINES_LEANING "\xee\x94\x9e" // U+e51e +#define ICON_FA_LINK "\xef\x83\x81" // U+f0c1 +#define ICON_FA_LINK_SLASH "\xef\x84\xa7" // U+f127 +#define ICON_FA_LIRA_SIGN "\xef\x86\x95" // U+f195 +#define ICON_FA_LIST "\xef\x80\xba" // U+f03a +#define ICON_FA_LIST_CHECK "\xef\x82\xae" // U+f0ae +#define ICON_FA_LIST_OL "\xef\x83\x8b" // U+f0cb +#define ICON_FA_LIST_UL "\xef\x83\x8a" // U+f0ca +#define ICON_FA_LITECOIN_SIGN "\xee\x87\x93" // U+e1d3 +#define ICON_FA_LOCATION_ARROW "\xef\x84\xa4" // U+f124 +#define ICON_FA_LOCATION_CROSSHAIRS "\xef\x98\x81" // U+f601 +#define ICON_FA_LOCATION_DOT "\xef\x8f\x85" // U+f3c5 +#define ICON_FA_LOCATION_PIN "\xef\x81\x81" // U+f041 +#define ICON_FA_LOCATION_PIN_LOCK "\xee\x94\x9f" // U+e51f +#define ICON_FA_LOCK "\xef\x80\xa3" // U+f023 +#define ICON_FA_LOCK_OPEN "\xef\x8f\x81" // U+f3c1 +#define ICON_FA_LOCUST "\xee\x94\xa0" // U+e520 +#define ICON_FA_LUNGS "\xef\x98\x84" // U+f604 +#define ICON_FA_LUNGS_VIRUS "\xee\x81\xa7" // U+e067 +#define ICON_FA_M "M" // U+004d +#define ICON_FA_MAGNET "\xef\x81\xb6" // U+f076 +#define ICON_FA_MAGNIFYING_GLASS "\xef\x80\x82" // U+f002 +#define ICON_FA_MAGNIFYING_GLASS_ARROW_RIGHT "\xee\x94\xa1" // U+e521 +#define ICON_FA_MAGNIFYING_GLASS_CHART "\xee\x94\xa2" // U+e522 +#define ICON_FA_MAGNIFYING_GLASS_DOLLAR "\xef\x9a\x88" // U+f688 +#define ICON_FA_MAGNIFYING_GLASS_LOCATION "\xef\x9a\x89" // U+f689 +#define ICON_FA_MAGNIFYING_GLASS_MINUS "\xef\x80\x90" // U+f010 +#define ICON_FA_MAGNIFYING_GLASS_PLUS "\xef\x80\x8e" // U+f00e +#define ICON_FA_MANAT_SIGN "\xee\x87\x95" // U+e1d5 +#define ICON_FA_MAP "\xef\x89\xb9" // U+f279 +#define ICON_FA_MAP_LOCATION "\xef\x96\x9f" // U+f59f +#define ICON_FA_MAP_LOCATION_DOT "\xef\x96\xa0" // U+f5a0 +#define ICON_FA_MAP_PIN "\xef\x89\xb6" // U+f276 +#define ICON_FA_MARKER "\xef\x96\xa1" // U+f5a1 +#define ICON_FA_MARS "\xef\x88\xa2" // U+f222 +#define ICON_FA_MARS_AND_VENUS "\xef\x88\xa4" // U+f224 +#define ICON_FA_MARS_AND_VENUS_BURST "\xee\x94\xa3" // U+e523 +#define ICON_FA_MARS_DOUBLE "\xef\x88\xa7" // U+f227 +#define ICON_FA_MARS_STROKE "\xef\x88\xa9" // U+f229 +#define ICON_FA_MARS_STROKE_RIGHT "\xef\x88\xab" // U+f22b +#define ICON_FA_MARS_STROKE_UP "\xef\x88\xaa" // U+f22a +#define ICON_FA_MARTINI_GLASS "\xef\x95\xbb" // U+f57b +#define ICON_FA_MARTINI_GLASS_CITRUS "\xef\x95\xa1" // U+f561 +#define ICON_FA_MARTINI_GLASS_EMPTY "\xef\x80\x80" // U+f000 +#define ICON_FA_MASK "\xef\x9b\xba" // U+f6fa +#define ICON_FA_MASK_FACE "\xee\x87\x97" // U+e1d7 +#define ICON_FA_MASK_VENTILATOR "\xee\x94\xa4" // U+e524 +#define ICON_FA_MASKS_THEATER "\xef\x98\xb0" // U+f630 +#define ICON_FA_MATTRESS_PILLOW "\xee\x94\xa5" // U+e525 +#define ICON_FA_MAXIMIZE "\xef\x8c\x9e" // U+f31e +#define ICON_FA_MEDAL "\xef\x96\xa2" // U+f5a2 +#define ICON_FA_MEMORY "\xef\x94\xb8" // U+f538 +#define ICON_FA_MENORAH "\xef\x99\xb6" // U+f676 +#define ICON_FA_MERCURY "\xef\x88\xa3" // U+f223 +#define ICON_FA_MESSAGE "\xef\x89\xba" // U+f27a +#define ICON_FA_METEOR "\xef\x9d\x93" // U+f753 +#define ICON_FA_MICROCHIP "\xef\x8b\x9b" // U+f2db +#define ICON_FA_MICROPHONE "\xef\x84\xb0" // U+f130 +#define ICON_FA_MICROPHONE_LINES "\xef\x8f\x89" // U+f3c9 +#define ICON_FA_MICROPHONE_LINES_SLASH "\xef\x94\xb9" // U+f539 +#define ICON_FA_MICROPHONE_SLASH "\xef\x84\xb1" // U+f131 +#define ICON_FA_MICROSCOPE "\xef\x98\x90" // U+f610 +#define ICON_FA_MILL_SIGN "\xee\x87\xad" // U+e1ed +#define ICON_FA_MINIMIZE "\xef\x9e\x8c" // U+f78c +#define ICON_FA_MINUS "\xef\x81\xa8" // U+f068 +#define ICON_FA_MITTEN "\xef\x9e\xb5" // U+f7b5 +#define ICON_FA_MOBILE "\xef\x8f\x8e" // U+f3ce +#define ICON_FA_MOBILE_BUTTON "\xef\x84\x8b" // U+f10b +#define ICON_FA_MOBILE_RETRO "\xee\x94\xa7" // U+e527 +#define ICON_FA_MOBILE_SCREEN "\xef\x8f\x8f" // U+f3cf +#define ICON_FA_MOBILE_SCREEN_BUTTON "\xef\x8f\x8d" // U+f3cd +#define ICON_FA_MONEY_BILL "\xef\x83\x96" // U+f0d6 +#define ICON_FA_MONEY_BILL_1 "\xef\x8f\x91" // U+f3d1 +#define ICON_FA_MONEY_BILL_1_WAVE "\xef\x94\xbb" // U+f53b +#define ICON_FA_MONEY_BILL_TRANSFER "\xee\x94\xa8" // U+e528 +#define ICON_FA_MONEY_BILL_TREND_UP "\xee\x94\xa9" // U+e529 +#define ICON_FA_MONEY_BILL_WAVE "\xef\x94\xba" // U+f53a +#define ICON_FA_MONEY_BILL_WHEAT "\xee\x94\xaa" // U+e52a +#define ICON_FA_MONEY_BILLS "\xee\x87\xb3" // U+e1f3 +#define ICON_FA_MONEY_CHECK "\xef\x94\xbc" // U+f53c +#define ICON_FA_MONEY_CHECK_DOLLAR "\xef\x94\xbd" // U+f53d +#define ICON_FA_MONUMENT "\xef\x96\xa6" // U+f5a6 +#define ICON_FA_MOON "\xef\x86\x86" // U+f186 +#define ICON_FA_MORTAR_PESTLE "\xef\x96\xa7" // U+f5a7 +#define ICON_FA_MOSQUE "\xef\x99\xb8" // U+f678 +#define ICON_FA_MOSQUITO "\xee\x94\xab" // U+e52b +#define ICON_FA_MOSQUITO_NET "\xee\x94\xac" // U+e52c +#define ICON_FA_MOTORCYCLE "\xef\x88\x9c" // U+f21c +#define ICON_FA_MOUND "\xee\x94\xad" // U+e52d +#define ICON_FA_MOUNTAIN "\xef\x9b\xbc" // U+f6fc +#define ICON_FA_MOUNTAIN_CITY "\xee\x94\xae" // U+e52e +#define ICON_FA_MOUNTAIN_SUN "\xee\x94\xaf" // U+e52f +#define ICON_FA_MUG_HOT "\xef\x9e\xb6" // U+f7b6 +#define ICON_FA_MUG_SAUCER "\xef\x83\xb4" // U+f0f4 +#define ICON_FA_MUSIC "\xef\x80\x81" // U+f001 +#define ICON_FA_N "N" // U+004e +#define ICON_FA_NAIRA_SIGN "\xee\x87\xb6" // U+e1f6 +#define ICON_FA_NETWORK_WIRED "\xef\x9b\xbf" // U+f6ff +#define ICON_FA_NEUTER "\xef\x88\xac" // U+f22c +#define ICON_FA_NEWSPAPER "\xef\x87\xaa" // U+f1ea +#define ICON_FA_NOT_EQUAL "\xef\x94\xbe" // U+f53e +#define ICON_FA_NOTDEF "\xee\x87\xbe" // U+e1fe +#define ICON_FA_NOTE_STICKY "\xef\x89\x89" // U+f249 +#define ICON_FA_NOTES_MEDICAL "\xef\x92\x81" // U+f481 +#define ICON_FA_O "O" // U+004f +#define ICON_FA_OBJECT_GROUP "\xef\x89\x87" // U+f247 +#define ICON_FA_OBJECT_UNGROUP "\xef\x89\x88" // U+f248 +#define ICON_FA_OIL_CAN "\xef\x98\x93" // U+f613 +#define ICON_FA_OIL_WELL "\xee\x94\xb2" // U+e532 +#define ICON_FA_OM "\xef\x99\xb9" // U+f679 +#define ICON_FA_OTTER "\xef\x9c\x80" // U+f700 +#define ICON_FA_OUTDENT "\xef\x80\xbb" // U+f03b +#define ICON_FA_P "P" // U+0050 +#define ICON_FA_PAGER "\xef\xa0\x95" // U+f815 +#define ICON_FA_PAINT_ROLLER "\xef\x96\xaa" // U+f5aa +#define ICON_FA_PAINTBRUSH "\xef\x87\xbc" // U+f1fc +#define ICON_FA_PALETTE "\xef\x94\xbf" // U+f53f +#define ICON_FA_PALLET "\xef\x92\x82" // U+f482 +#define ICON_FA_PANORAMA "\xee\x88\x89" // U+e209 +#define ICON_FA_PAPER_PLANE "\xef\x87\x98" // U+f1d8 +#define ICON_FA_PAPERCLIP "\xef\x83\x86" // U+f0c6 +#define ICON_FA_PARACHUTE_BOX "\xef\x93\x8d" // U+f4cd +#define ICON_FA_PARAGRAPH "\xef\x87\x9d" // U+f1dd +#define ICON_FA_PASSPORT "\xef\x96\xab" // U+f5ab +#define ICON_FA_PASTE "\xef\x83\xaa" // U+f0ea +#define ICON_FA_PAUSE "\xef\x81\x8c" // U+f04c +#define ICON_FA_PAW "\xef\x86\xb0" // U+f1b0 +#define ICON_FA_PEACE "\xef\x99\xbc" // U+f67c +#define ICON_FA_PEN "\xef\x8c\x84" // U+f304 +#define ICON_FA_PEN_CLIP "\xef\x8c\x85" // U+f305 +#define ICON_FA_PEN_FANCY "\xef\x96\xac" // U+f5ac +#define ICON_FA_PEN_NIB "\xef\x96\xad" // U+f5ad +#define ICON_FA_PEN_RULER "\xef\x96\xae" // U+f5ae +#define ICON_FA_PEN_TO_SQUARE "\xef\x81\x84" // U+f044 +#define ICON_FA_PENCIL "\xef\x8c\x83" // U+f303 +#define ICON_FA_PEOPLE_ARROWS "\xee\x81\xa8" // U+e068 +#define ICON_FA_PEOPLE_CARRY_BOX "\xef\x93\x8e" // U+f4ce +#define ICON_FA_PEOPLE_GROUP "\xee\x94\xb3" // U+e533 +#define ICON_FA_PEOPLE_LINE "\xee\x94\xb4" // U+e534 +#define ICON_FA_PEOPLE_PULLING "\xee\x94\xb5" // U+e535 +#define ICON_FA_PEOPLE_ROBBERY "\xee\x94\xb6" // U+e536 +#define ICON_FA_PEOPLE_ROOF "\xee\x94\xb7" // U+e537 +#define ICON_FA_PEPPER_HOT "\xef\xa0\x96" // U+f816 +#define ICON_FA_PERCENT "%" // U+0025 +#define ICON_FA_PERSON "\xef\x86\x83" // U+f183 +#define ICON_FA_PERSON_ARROW_DOWN_TO_LINE "\xee\x94\xb8" // U+e538 +#define ICON_FA_PERSON_ARROW_UP_FROM_LINE "\xee\x94\xb9" // U+e539 +#define ICON_FA_PERSON_BIKING "\xef\xa1\x8a" // U+f84a +#define ICON_FA_PERSON_BOOTH "\xef\x9d\x96" // U+f756 +#define ICON_FA_PERSON_BREASTFEEDING "\xee\x94\xba" // U+e53a +#define ICON_FA_PERSON_BURST "\xee\x94\xbb" // U+e53b +#define ICON_FA_PERSON_CANE "\xee\x94\xbc" // U+e53c +#define ICON_FA_PERSON_CHALKBOARD "\xee\x94\xbd" // U+e53d +#define ICON_FA_PERSON_CIRCLE_CHECK "\xee\x94\xbe" // U+e53e +#define ICON_FA_PERSON_CIRCLE_EXCLAMATION "\xee\x94\xbf" // U+e53f +#define ICON_FA_PERSON_CIRCLE_MINUS "\xee\x95\x80" // U+e540 +#define ICON_FA_PERSON_CIRCLE_PLUS "\xee\x95\x81" // U+e541 +#define ICON_FA_PERSON_CIRCLE_QUESTION "\xee\x95\x82" // U+e542 +#define ICON_FA_PERSON_CIRCLE_XMARK "\xee\x95\x83" // U+e543 +#define ICON_FA_PERSON_DIGGING "\xef\xa1\x9e" // U+f85e +#define ICON_FA_PERSON_DOTS_FROM_LINE "\xef\x91\xb0" // U+f470 +#define ICON_FA_PERSON_DRESS "\xef\x86\x82" // U+f182 +#define ICON_FA_PERSON_DRESS_BURST "\xee\x95\x84" // U+e544 +#define ICON_FA_PERSON_DROWNING "\xee\x95\x85" // U+e545 +#define ICON_FA_PERSON_FALLING "\xee\x95\x86" // U+e546 +#define ICON_FA_PERSON_FALLING_BURST "\xee\x95\x87" // U+e547 +#define ICON_FA_PERSON_HALF_DRESS "\xee\x95\x88" // U+e548 +#define ICON_FA_PERSON_HARASSING "\xee\x95\x89" // U+e549 +#define ICON_FA_PERSON_HIKING "\xef\x9b\xac" // U+f6ec +#define ICON_FA_PERSON_MILITARY_POINTING "\xee\x95\x8a" // U+e54a +#define ICON_FA_PERSON_MILITARY_RIFLE "\xee\x95\x8b" // U+e54b +#define ICON_FA_PERSON_MILITARY_TO_PERSON "\xee\x95\x8c" // U+e54c +#define ICON_FA_PERSON_PRAYING "\xef\x9a\x83" // U+f683 +#define ICON_FA_PERSON_PREGNANT "\xee\x8c\x9e" // U+e31e +#define ICON_FA_PERSON_RAYS "\xee\x95\x8d" // U+e54d +#define ICON_FA_PERSON_RIFLE "\xee\x95\x8e" // U+e54e +#define ICON_FA_PERSON_RUNNING "\xef\x9c\x8c" // U+f70c +#define ICON_FA_PERSON_SHELTER "\xee\x95\x8f" // U+e54f +#define ICON_FA_PERSON_SKATING "\xef\x9f\x85" // U+f7c5 +#define ICON_FA_PERSON_SKIING "\xef\x9f\x89" // U+f7c9 +#define ICON_FA_PERSON_SKIING_NORDIC "\xef\x9f\x8a" // U+f7ca +#define ICON_FA_PERSON_SNOWBOARDING "\xef\x9f\x8e" // U+f7ce +#define ICON_FA_PERSON_SWIMMING "\xef\x97\x84" // U+f5c4 +#define ICON_FA_PERSON_THROUGH_WINDOW "\xee\x96\xa9" // U+e5a9 +#define ICON_FA_PERSON_WALKING "\xef\x95\x94" // U+f554 +#define ICON_FA_PERSON_WALKING_ARROW_LOOP_LEFT "\xee\x95\x91" // U+e551 +#define ICON_FA_PERSON_WALKING_ARROW_RIGHT "\xee\x95\x92" // U+e552 +#define ICON_FA_PERSON_WALKING_DASHED_LINE_ARROW_RIGHT "\xee\x95\x93" // U+e553 +#define ICON_FA_PERSON_WALKING_LUGGAGE "\xee\x95\x94" // U+e554 +#define ICON_FA_PERSON_WALKING_WITH_CANE "\xef\x8a\x9d" // U+f29d +#define ICON_FA_PESETA_SIGN "\xee\x88\xa1" // U+e221 +#define ICON_FA_PESO_SIGN "\xee\x88\xa2" // U+e222 +#define ICON_FA_PHONE "\xef\x82\x95" // U+f095 +#define ICON_FA_PHONE_FLIP "\xef\xa1\xb9" // U+f879 +#define ICON_FA_PHONE_SLASH "\xef\x8f\x9d" // U+f3dd +#define ICON_FA_PHONE_VOLUME "\xef\x8a\xa0" // U+f2a0 +#define ICON_FA_PHOTO_FILM "\xef\xa1\xbc" // U+f87c +#define ICON_FA_PIGGY_BANK "\xef\x93\x93" // U+f4d3 +#define ICON_FA_PILLS "\xef\x92\x84" // U+f484 +#define ICON_FA_PIZZA_SLICE "\xef\xa0\x98" // U+f818 +#define ICON_FA_PLACE_OF_WORSHIP "\xef\x99\xbf" // U+f67f +#define ICON_FA_PLANE "\xef\x81\xb2" // U+f072 +#define ICON_FA_PLANE_ARRIVAL "\xef\x96\xaf" // U+f5af +#define ICON_FA_PLANE_CIRCLE_CHECK "\xee\x95\x95" // U+e555 +#define ICON_FA_PLANE_CIRCLE_EXCLAMATION "\xee\x95\x96" // U+e556 +#define ICON_FA_PLANE_CIRCLE_XMARK "\xee\x95\x97" // U+e557 +#define ICON_FA_PLANE_DEPARTURE "\xef\x96\xb0" // U+f5b0 +#define ICON_FA_PLANE_LOCK "\xee\x95\x98" // U+e558 +#define ICON_FA_PLANE_SLASH "\xee\x81\xa9" // U+e069 +#define ICON_FA_PLANE_UP "\xee\x88\xad" // U+e22d +#define ICON_FA_PLANT_WILT "\xee\x96\xaa" // U+e5aa +#define ICON_FA_PLATE_WHEAT "\xee\x95\x9a" // U+e55a +#define ICON_FA_PLAY "\xef\x81\x8b" // U+f04b +#define ICON_FA_PLUG "\xef\x87\xa6" // U+f1e6 +#define ICON_FA_PLUG_CIRCLE_BOLT "\xee\x95\x9b" // U+e55b +#define ICON_FA_PLUG_CIRCLE_CHECK "\xee\x95\x9c" // U+e55c +#define ICON_FA_PLUG_CIRCLE_EXCLAMATION "\xee\x95\x9d" // U+e55d +#define ICON_FA_PLUG_CIRCLE_MINUS "\xee\x95\x9e" // U+e55e +#define ICON_FA_PLUG_CIRCLE_PLUS "\xee\x95\x9f" // U+e55f +#define ICON_FA_PLUG_CIRCLE_XMARK "\xee\x95\xa0" // U+e560 +#define ICON_FA_PLUS "+" // U+002b +#define ICON_FA_PLUS_MINUS "\xee\x90\xbc" // U+e43c +#define ICON_FA_PODCAST "\xef\x8b\x8e" // U+f2ce +#define ICON_FA_POO "\xef\x8b\xbe" // U+f2fe +#define ICON_FA_POO_STORM "\xef\x9d\x9a" // U+f75a +#define ICON_FA_POOP "\xef\x98\x99" // U+f619 +#define ICON_FA_POWER_OFF "\xef\x80\x91" // U+f011 +#define ICON_FA_PRESCRIPTION "\xef\x96\xb1" // U+f5b1 +#define ICON_FA_PRESCRIPTION_BOTTLE "\xef\x92\x85" // U+f485 +#define ICON_FA_PRESCRIPTION_BOTTLE_MEDICAL "\xef\x92\x86" // U+f486 +#define ICON_FA_PRINT "\xef\x80\xaf" // U+f02f +#define ICON_FA_PUMP_MEDICAL "\xee\x81\xaa" // U+e06a +#define ICON_FA_PUMP_SOAP "\xee\x81\xab" // U+e06b +#define ICON_FA_PUZZLE_PIECE "\xef\x84\xae" // U+f12e +#define ICON_FA_Q "Q" // U+0051 +#define ICON_FA_QRCODE "\xef\x80\xa9" // U+f029 +#define ICON_FA_QUESTION "?" // U+003f +#define ICON_FA_QUOTE_LEFT "\xef\x84\x8d" // U+f10d +#define ICON_FA_QUOTE_RIGHT "\xef\x84\x8e" // U+f10e +#define ICON_FA_R "R" // U+0052 +#define ICON_FA_RADIATION "\xef\x9e\xb9" // U+f7b9 +#define ICON_FA_RADIO "\xef\xa3\x97" // U+f8d7 +#define ICON_FA_RAINBOW "\xef\x9d\x9b" // U+f75b +#define ICON_FA_RANKING_STAR "\xee\x95\xa1" // U+e561 +#define ICON_FA_RECEIPT "\xef\x95\x83" // U+f543 +#define ICON_FA_RECORD_VINYL "\xef\xa3\x99" // U+f8d9 +#define ICON_FA_RECTANGLE_AD "\xef\x99\x81" // U+f641 +#define ICON_FA_RECTANGLE_LIST "\xef\x80\xa2" // U+f022 +#define ICON_FA_RECTANGLE_XMARK "\xef\x90\x90" // U+f410 +#define ICON_FA_RECYCLE "\xef\x86\xb8" // U+f1b8 +#define ICON_FA_REGISTERED "\xef\x89\x9d" // U+f25d +#define ICON_FA_REPEAT "\xef\x8d\xa3" // U+f363 +#define ICON_FA_REPLY "\xef\x8f\xa5" // U+f3e5 +#define ICON_FA_REPLY_ALL "\xef\x84\xa2" // U+f122 +#define ICON_FA_REPUBLICAN "\xef\x9d\x9e" // U+f75e +#define ICON_FA_RESTROOM "\xef\x9e\xbd" // U+f7bd +#define ICON_FA_RETWEET "\xef\x81\xb9" // U+f079 +#define ICON_FA_RIBBON "\xef\x93\x96" // U+f4d6 +#define ICON_FA_RIGHT_FROM_BRACKET "\xef\x8b\xb5" // U+f2f5 +#define ICON_FA_RIGHT_LEFT "\xef\x8d\xa2" // U+f362 +#define ICON_FA_RIGHT_LONG "\xef\x8c\x8b" // U+f30b +#define ICON_FA_RIGHT_TO_BRACKET "\xef\x8b\xb6" // U+f2f6 +#define ICON_FA_RING "\xef\x9c\x8b" // U+f70b +#define ICON_FA_ROAD "\xef\x80\x98" // U+f018 +#define ICON_FA_ROAD_BARRIER "\xee\x95\xa2" // U+e562 +#define ICON_FA_ROAD_BRIDGE "\xee\x95\xa3" // U+e563 +#define ICON_FA_ROAD_CIRCLE_CHECK "\xee\x95\xa4" // U+e564 +#define ICON_FA_ROAD_CIRCLE_EXCLAMATION "\xee\x95\xa5" // U+e565 +#define ICON_FA_ROAD_CIRCLE_XMARK "\xee\x95\xa6" // U+e566 +#define ICON_FA_ROAD_LOCK "\xee\x95\xa7" // U+e567 +#define ICON_FA_ROAD_SPIKES "\xee\x95\xa8" // U+e568 +#define ICON_FA_ROBOT "\xef\x95\x84" // U+f544 +#define ICON_FA_ROCKET "\xef\x84\xb5" // U+f135 +#define ICON_FA_ROTATE "\xef\x8b\xb1" // U+f2f1 +#define ICON_FA_ROTATE_LEFT "\xef\x8b\xaa" // U+f2ea +#define ICON_FA_ROTATE_RIGHT "\xef\x8b\xb9" // U+f2f9 +#define ICON_FA_ROUTE "\xef\x93\x97" // U+f4d7 +#define ICON_FA_RSS "\xef\x82\x9e" // U+f09e +#define ICON_FA_RUBLE_SIGN "\xef\x85\x98" // U+f158 +#define ICON_FA_RUG "\xee\x95\xa9" // U+e569 +#define ICON_FA_RULER "\xef\x95\x85" // U+f545 +#define ICON_FA_RULER_COMBINED "\xef\x95\x86" // U+f546 +#define ICON_FA_RULER_HORIZONTAL "\xef\x95\x87" // U+f547 +#define ICON_FA_RULER_VERTICAL "\xef\x95\x88" // U+f548 +#define ICON_FA_RUPEE_SIGN "\xef\x85\x96" // U+f156 +#define ICON_FA_RUPIAH_SIGN "\xee\x88\xbd" // U+e23d +#define ICON_FA_S "S" // U+0053 +#define ICON_FA_SACK_DOLLAR "\xef\xa0\x9d" // U+f81d +#define ICON_FA_SACK_XMARK "\xee\x95\xaa" // U+e56a +#define ICON_FA_SAILBOAT "\xee\x91\x85" // U+e445 +#define ICON_FA_SATELLITE "\xef\x9e\xbf" // U+f7bf +#define ICON_FA_SATELLITE_DISH "\xef\x9f\x80" // U+f7c0 +#define ICON_FA_SCALE_BALANCED "\xef\x89\x8e" // U+f24e +#define ICON_FA_SCALE_UNBALANCED "\xef\x94\x95" // U+f515 +#define ICON_FA_SCALE_UNBALANCED_FLIP "\xef\x94\x96" // U+f516 +#define ICON_FA_SCHOOL "\xef\x95\x89" // U+f549 +#define ICON_FA_SCHOOL_CIRCLE_CHECK "\xee\x95\xab" // U+e56b +#define ICON_FA_SCHOOL_CIRCLE_EXCLAMATION "\xee\x95\xac" // U+e56c +#define ICON_FA_SCHOOL_CIRCLE_XMARK "\xee\x95\xad" // U+e56d +#define ICON_FA_SCHOOL_FLAG "\xee\x95\xae" // U+e56e +#define ICON_FA_SCHOOL_LOCK "\xee\x95\xaf" // U+e56f +#define ICON_FA_SCISSORS "\xef\x83\x84" // U+f0c4 +#define ICON_FA_SCREWDRIVER "\xef\x95\x8a" // U+f54a +#define ICON_FA_SCREWDRIVER_WRENCH "\xef\x9f\x99" // U+f7d9 +#define ICON_FA_SCROLL "\xef\x9c\x8e" // U+f70e +#define ICON_FA_SCROLL_TORAH "\xef\x9a\xa0" // U+f6a0 +#define ICON_FA_SD_CARD "\xef\x9f\x82" // U+f7c2 +#define ICON_FA_SECTION "\xee\x91\x87" // U+e447 +#define ICON_FA_SEEDLING "\xef\x93\x98" // U+f4d8 +#define ICON_FA_SERVER "\xef\x88\xb3" // U+f233 +#define ICON_FA_SHAPES "\xef\x98\x9f" // U+f61f +#define ICON_FA_SHARE "\xef\x81\xa4" // U+f064 +#define ICON_FA_SHARE_FROM_SQUARE "\xef\x85\x8d" // U+f14d +#define ICON_FA_SHARE_NODES "\xef\x87\xa0" // U+f1e0 +#define ICON_FA_SHEET_PLASTIC "\xee\x95\xb1" // U+e571 +#define ICON_FA_SHEKEL_SIGN "\xef\x88\x8b" // U+f20b +#define ICON_FA_SHIELD "\xef\x84\xb2" // U+f132 +#define ICON_FA_SHIELD_CAT "\xee\x95\xb2" // U+e572 +#define ICON_FA_SHIELD_DOG "\xee\x95\xb3" // U+e573 +#define ICON_FA_SHIELD_HALVED "\xef\x8f\xad" // U+f3ed +#define ICON_FA_SHIELD_HEART "\xee\x95\xb4" // U+e574 +#define ICON_FA_SHIELD_VIRUS "\xee\x81\xac" // U+e06c +#define ICON_FA_SHIP "\xef\x88\x9a" // U+f21a +#define ICON_FA_SHIRT "\xef\x95\x93" // U+f553 +#define ICON_FA_SHOE_PRINTS "\xef\x95\x8b" // U+f54b +#define ICON_FA_SHOP "\xef\x95\x8f" // U+f54f +#define ICON_FA_SHOP_LOCK "\xee\x92\xa5" // U+e4a5 +#define ICON_FA_SHOP_SLASH "\xee\x81\xb0" // U+e070 +#define ICON_FA_SHOWER "\xef\x8b\x8c" // U+f2cc +#define ICON_FA_SHRIMP "\xee\x91\x88" // U+e448 +#define ICON_FA_SHUFFLE "\xef\x81\xb4" // U+f074 +#define ICON_FA_SHUTTLE_SPACE "\xef\x86\x97" // U+f197 +#define ICON_FA_SIGN_HANGING "\xef\x93\x99" // U+f4d9 +#define ICON_FA_SIGNAL "\xef\x80\x92" // U+f012 +#define ICON_FA_SIGNATURE "\xef\x96\xb7" // U+f5b7 +#define ICON_FA_SIGNS_POST "\xef\x89\xb7" // U+f277 +#define ICON_FA_SIM_CARD "\xef\x9f\x84" // U+f7c4 +#define ICON_FA_SINK "\xee\x81\xad" // U+e06d +#define ICON_FA_SITEMAP "\xef\x83\xa8" // U+f0e8 +#define ICON_FA_SKULL "\xef\x95\x8c" // U+f54c +#define ICON_FA_SKULL_CROSSBONES "\xef\x9c\x94" // U+f714 +#define ICON_FA_SLASH "\xef\x9c\x95" // U+f715 +#define ICON_FA_SLEIGH "\xef\x9f\x8c" // U+f7cc +#define ICON_FA_SLIDERS "\xef\x87\x9e" // U+f1de +#define ICON_FA_SMOG "\xef\x9d\x9f" // U+f75f +#define ICON_FA_SMOKING "\xef\x92\x8d" // U+f48d +#define ICON_FA_SNOWFLAKE "\xef\x8b\x9c" // U+f2dc +#define ICON_FA_SNOWMAN "\xef\x9f\x90" // U+f7d0 +#define ICON_FA_SNOWPLOW "\xef\x9f\x92" // U+f7d2 +#define ICON_FA_SOAP "\xee\x81\xae" // U+e06e +#define ICON_FA_SOCKS "\xef\x9a\x96" // U+f696 +#define ICON_FA_SOLAR_PANEL "\xef\x96\xba" // U+f5ba +#define ICON_FA_SORT "\xef\x83\x9c" // U+f0dc +#define ICON_FA_SORT_DOWN "\xef\x83\x9d" // U+f0dd +#define ICON_FA_SORT_UP "\xef\x83\x9e" // U+f0de +#define ICON_FA_SPA "\xef\x96\xbb" // U+f5bb +#define ICON_FA_SPAGHETTI_MONSTER_FLYING "\xef\x99\xbb" // U+f67b +#define ICON_FA_SPELL_CHECK "\xef\xa2\x91" // U+f891 +#define ICON_FA_SPIDER "\xef\x9c\x97" // U+f717 +#define ICON_FA_SPINNER "\xef\x84\x90" // U+f110 +#define ICON_FA_SPLOTCH "\xef\x96\xbc" // U+f5bc +#define ICON_FA_SPOON "\xef\x8b\xa5" // U+f2e5 +#define ICON_FA_SPRAY_CAN "\xef\x96\xbd" // U+f5bd +#define ICON_FA_SPRAY_CAN_SPARKLES "\xef\x97\x90" // U+f5d0 +#define ICON_FA_SQUARE "\xef\x83\x88" // U+f0c8 +#define ICON_FA_SQUARE_ARROW_UP_RIGHT "\xef\x85\x8c" // U+f14c +#define ICON_FA_SQUARE_CARET_DOWN "\xef\x85\x90" // U+f150 +#define ICON_FA_SQUARE_CARET_LEFT "\xef\x86\x91" // U+f191 +#define ICON_FA_SQUARE_CARET_RIGHT "\xef\x85\x92" // U+f152 +#define ICON_FA_SQUARE_CARET_UP "\xef\x85\x91" // U+f151 +#define ICON_FA_SQUARE_CHECK "\xef\x85\x8a" // U+f14a +#define ICON_FA_SQUARE_ENVELOPE "\xef\x86\x99" // U+f199 +#define ICON_FA_SQUARE_FULL "\xef\x91\x9c" // U+f45c +#define ICON_FA_SQUARE_H "\xef\x83\xbd" // U+f0fd +#define ICON_FA_SQUARE_MINUS "\xef\x85\x86" // U+f146 +#define ICON_FA_SQUARE_NFI "\xee\x95\xb6" // U+e576 +#define ICON_FA_SQUARE_PARKING "\xef\x95\x80" // U+f540 +#define ICON_FA_SQUARE_PEN "\xef\x85\x8b" // U+f14b +#define ICON_FA_SQUARE_PERSON_CONFINED "\xee\x95\xb7" // U+e577 +#define ICON_FA_SQUARE_PHONE "\xef\x82\x98" // U+f098 +#define ICON_FA_SQUARE_PHONE_FLIP "\xef\xa1\xbb" // U+f87b +#define ICON_FA_SQUARE_PLUS "\xef\x83\xbe" // U+f0fe +#define ICON_FA_SQUARE_POLL_HORIZONTAL "\xef\x9a\x82" // U+f682 +#define ICON_FA_SQUARE_POLL_VERTICAL "\xef\x9a\x81" // U+f681 +#define ICON_FA_SQUARE_ROOT_VARIABLE "\xef\x9a\x98" // U+f698 +#define ICON_FA_SQUARE_RSS "\xef\x85\x83" // U+f143 +#define ICON_FA_SQUARE_SHARE_NODES "\xef\x87\xa1" // U+f1e1 +#define ICON_FA_SQUARE_UP_RIGHT "\xef\x8d\xa0" // U+f360 +#define ICON_FA_SQUARE_VIRUS "\xee\x95\xb8" // U+e578 +#define ICON_FA_SQUARE_XMARK "\xef\x8b\x93" // U+f2d3 +#define ICON_FA_STAFF_SNAKE "\xee\x95\xb9" // U+e579 +#define ICON_FA_STAIRS "\xee\x8a\x89" // U+e289 +#define ICON_FA_STAMP "\xef\x96\xbf" // U+f5bf +#define ICON_FA_STAPLER "\xee\x96\xaf" // U+e5af +#define ICON_FA_STAR "\xef\x80\x85" // U+f005 +#define ICON_FA_STAR_AND_CRESCENT "\xef\x9a\x99" // U+f699 +#define ICON_FA_STAR_HALF "\xef\x82\x89" // U+f089 +#define ICON_FA_STAR_HALF_STROKE "\xef\x97\x80" // U+f5c0 +#define ICON_FA_STAR_OF_DAVID "\xef\x9a\x9a" // U+f69a +#define ICON_FA_STAR_OF_LIFE "\xef\x98\xa1" // U+f621 +#define ICON_FA_STERLING_SIGN "\xef\x85\x94" // U+f154 +#define ICON_FA_STETHOSCOPE "\xef\x83\xb1" // U+f0f1 +#define ICON_FA_STOP "\xef\x81\x8d" // U+f04d +#define ICON_FA_STOPWATCH "\xef\x8b\xb2" // U+f2f2 +#define ICON_FA_STOPWATCH_20 "\xee\x81\xaf" // U+e06f +#define ICON_FA_STORE "\xef\x95\x8e" // U+f54e +#define ICON_FA_STORE_SLASH "\xee\x81\xb1" // U+e071 +#define ICON_FA_STREET_VIEW "\xef\x88\x9d" // U+f21d +#define ICON_FA_STRIKETHROUGH "\xef\x83\x8c" // U+f0cc +#define ICON_FA_STROOPWAFEL "\xef\x95\x91" // U+f551 +#define ICON_FA_SUBSCRIPT "\xef\x84\xac" // U+f12c +#define ICON_FA_SUITCASE "\xef\x83\xb2" // U+f0f2 +#define ICON_FA_SUITCASE_MEDICAL "\xef\x83\xba" // U+f0fa +#define ICON_FA_SUITCASE_ROLLING "\xef\x97\x81" // U+f5c1 +#define ICON_FA_SUN "\xef\x86\x85" // U+f185 +#define ICON_FA_SUN_PLANT_WILT "\xee\x95\xba" // U+e57a +#define ICON_FA_SUPERSCRIPT "\xef\x84\xab" // U+f12b +#define ICON_FA_SWATCHBOOK "\xef\x97\x83" // U+f5c3 +#define ICON_FA_SYNAGOGUE "\xef\x9a\x9b" // U+f69b +#define ICON_FA_SYRINGE "\xef\x92\x8e" // U+f48e +#define ICON_FA_T "T" // U+0054 +#define ICON_FA_TABLE "\xef\x83\x8e" // U+f0ce +#define ICON_FA_TABLE_CELLS "\xef\x80\x8a" // U+f00a +#define ICON_FA_TABLE_CELLS_COLUMN_LOCK "\xee\x99\xb8" // U+e678 +#define ICON_FA_TABLE_CELLS_LARGE "\xef\x80\x89" // U+f009 +#define ICON_FA_TABLE_CELLS_ROW_LOCK "\xee\x99\xba" // U+e67a +#define ICON_FA_TABLE_COLUMNS "\xef\x83\x9b" // U+f0db +#define ICON_FA_TABLE_LIST "\xef\x80\x8b" // U+f00b +#define ICON_FA_TABLE_TENNIS_PADDLE_BALL "\xef\x91\x9d" // U+f45d +#define ICON_FA_TABLET "\xef\x8f\xbb" // U+f3fb +#define ICON_FA_TABLET_BUTTON "\xef\x84\x8a" // U+f10a +#define ICON_FA_TABLET_SCREEN_BUTTON "\xef\x8f\xba" // U+f3fa +#define ICON_FA_TABLETS "\xef\x92\x90" // U+f490 +#define ICON_FA_TACHOGRAPH_DIGITAL "\xef\x95\xa6" // U+f566 +#define ICON_FA_TAG "\xef\x80\xab" // U+f02b +#define ICON_FA_TAGS "\xef\x80\xac" // U+f02c +#define ICON_FA_TAPE "\xef\x93\x9b" // U+f4db +#define ICON_FA_TARP "\xee\x95\xbb" // U+e57b +#define ICON_FA_TARP_DROPLET "\xee\x95\xbc" // U+e57c +#define ICON_FA_TAXI "\xef\x86\xba" // U+f1ba +#define ICON_FA_TEETH "\xef\x98\xae" // U+f62e +#define ICON_FA_TEETH_OPEN "\xef\x98\xaf" // U+f62f +#define ICON_FA_TEMPERATURE_ARROW_DOWN "\xee\x80\xbf" // U+e03f +#define ICON_FA_TEMPERATURE_ARROW_UP "\xee\x81\x80" // U+e040 +#define ICON_FA_TEMPERATURE_EMPTY "\xef\x8b\x8b" // U+f2cb +#define ICON_FA_TEMPERATURE_FULL "\xef\x8b\x87" // U+f2c7 +#define ICON_FA_TEMPERATURE_HALF "\xef\x8b\x89" // U+f2c9 +#define ICON_FA_TEMPERATURE_HIGH "\xef\x9d\xa9" // U+f769 +#define ICON_FA_TEMPERATURE_LOW "\xef\x9d\xab" // U+f76b +#define ICON_FA_TEMPERATURE_QUARTER "\xef\x8b\x8a" // U+f2ca +#define ICON_FA_TEMPERATURE_THREE_QUARTERS "\xef\x8b\x88" // U+f2c8 +#define ICON_FA_TENGE_SIGN "\xef\x9f\x97" // U+f7d7 +#define ICON_FA_TENT "\xee\x95\xbd" // U+e57d +#define ICON_FA_TENT_ARROW_DOWN_TO_LINE "\xee\x95\xbe" // U+e57e +#define ICON_FA_TENT_ARROW_LEFT_RIGHT "\xee\x95\xbf" // U+e57f +#define ICON_FA_TENT_ARROW_TURN_LEFT "\xee\x96\x80" // U+e580 +#define ICON_FA_TENT_ARROWS_DOWN "\xee\x96\x81" // U+e581 +#define ICON_FA_TENTS "\xee\x96\x82" // U+e582 +#define ICON_FA_TERMINAL "\xef\x84\xa0" // U+f120 +#define ICON_FA_TEXT_HEIGHT "\xef\x80\xb4" // U+f034 +#define ICON_FA_TEXT_SLASH "\xef\xa1\xbd" // U+f87d +#define ICON_FA_TEXT_WIDTH "\xef\x80\xb5" // U+f035 +#define ICON_FA_THERMOMETER "\xef\x92\x91" // U+f491 +#define ICON_FA_THUMBS_DOWN "\xef\x85\xa5" // U+f165 +#define ICON_FA_THUMBS_UP "\xef\x85\xa4" // U+f164 +#define ICON_FA_THUMBTACK "\xef\x82\x8d" // U+f08d +#define ICON_FA_TICKET "\xef\x85\x85" // U+f145 +#define ICON_FA_TICKET_SIMPLE "\xef\x8f\xbf" // U+f3ff +#define ICON_FA_TIMELINE "\xee\x8a\x9c" // U+e29c +#define ICON_FA_TOGGLE_OFF "\xef\x88\x84" // U+f204 +#define ICON_FA_TOGGLE_ON "\xef\x88\x85" // U+f205 +#define ICON_FA_TOILET "\xef\x9f\x98" // U+f7d8 +#define ICON_FA_TOILET_PAPER "\xef\x9c\x9e" // U+f71e +#define ICON_FA_TOILET_PAPER_SLASH "\xee\x81\xb2" // U+e072 +#define ICON_FA_TOILET_PORTABLE "\xee\x96\x83" // U+e583 +#define ICON_FA_TOILETS_PORTABLE "\xee\x96\x84" // U+e584 +#define ICON_FA_TOOLBOX "\xef\x95\x92" // U+f552 +#define ICON_FA_TOOTH "\xef\x97\x89" // U+f5c9 +#define ICON_FA_TORII_GATE "\xef\x9a\xa1" // U+f6a1 +#define ICON_FA_TORNADO "\xef\x9d\xaf" // U+f76f +#define ICON_FA_TOWER_BROADCAST "\xef\x94\x99" // U+f519 +#define ICON_FA_TOWER_CELL "\xee\x96\x85" // U+e585 +#define ICON_FA_TOWER_OBSERVATION "\xee\x96\x86" // U+e586 +#define ICON_FA_TRACTOR "\xef\x9c\xa2" // U+f722 +#define ICON_FA_TRADEMARK "\xef\x89\x9c" // U+f25c +#define ICON_FA_TRAFFIC_LIGHT "\xef\x98\xb7" // U+f637 +#define ICON_FA_TRAILER "\xee\x81\x81" // U+e041 +#define ICON_FA_TRAIN "\xef\x88\xb8" // U+f238 +#define ICON_FA_TRAIN_SUBWAY "\xef\x88\xb9" // U+f239 +#define ICON_FA_TRAIN_TRAM "\xee\x96\xb4" // U+e5b4 +#define ICON_FA_TRANSGENDER "\xef\x88\xa5" // U+f225 +#define ICON_FA_TRASH "\xef\x87\xb8" // U+f1f8 +#define ICON_FA_TRASH_ARROW_UP "\xef\xa0\xa9" // U+f829 +#define ICON_FA_TRASH_CAN "\xef\x8b\xad" // U+f2ed +#define ICON_FA_TRASH_CAN_ARROW_UP "\xef\xa0\xaa" // U+f82a +#define ICON_FA_TREE "\xef\x86\xbb" // U+f1bb +#define ICON_FA_TREE_CITY "\xee\x96\x87" // U+e587 +#define ICON_FA_TRIANGLE_EXCLAMATION "\xef\x81\xb1" // U+f071 +#define ICON_FA_TROPHY "\xef\x82\x91" // U+f091 +#define ICON_FA_TROWEL "\xee\x96\x89" // U+e589 +#define ICON_FA_TROWEL_BRICKS "\xee\x96\x8a" // U+e58a +#define ICON_FA_TRUCK "\xef\x83\x91" // U+f0d1 +#define ICON_FA_TRUCK_ARROW_RIGHT "\xee\x96\x8b" // U+e58b +#define ICON_FA_TRUCK_DROPLET "\xee\x96\x8c" // U+e58c +#define ICON_FA_TRUCK_FAST "\xef\x92\x8b" // U+f48b +#define ICON_FA_TRUCK_FIELD "\xee\x96\x8d" // U+e58d +#define ICON_FA_TRUCK_FIELD_UN "\xee\x96\x8e" // U+e58e +#define ICON_FA_TRUCK_FRONT "\xee\x8a\xb7" // U+e2b7 +#define ICON_FA_TRUCK_MEDICAL "\xef\x83\xb9" // U+f0f9 +#define ICON_FA_TRUCK_MONSTER "\xef\x98\xbb" // U+f63b +#define ICON_FA_TRUCK_MOVING "\xef\x93\x9f" // U+f4df +#define ICON_FA_TRUCK_PICKUP "\xef\x98\xbc" // U+f63c +#define ICON_FA_TRUCK_PLANE "\xee\x96\x8f" // U+e58f +#define ICON_FA_TRUCK_RAMP_BOX "\xef\x93\x9e" // U+f4de +#define ICON_FA_TTY "\xef\x87\xa4" // U+f1e4 +#define ICON_FA_TURKISH_LIRA_SIGN "\xee\x8a\xbb" // U+e2bb +#define ICON_FA_TURN_DOWN "\xef\x8e\xbe" // U+f3be +#define ICON_FA_TURN_UP "\xef\x8e\xbf" // U+f3bf +#define ICON_FA_TV "\xef\x89\xac" // U+f26c +#define ICON_FA_U "U" // U+0055 +#define ICON_FA_UMBRELLA "\xef\x83\xa9" // U+f0e9 +#define ICON_FA_UMBRELLA_BEACH "\xef\x97\x8a" // U+f5ca +#define ICON_FA_UNDERLINE "\xef\x83\x8d" // U+f0cd +#define ICON_FA_UNIVERSAL_ACCESS "\xef\x8a\x9a" // U+f29a +#define ICON_FA_UNLOCK "\xef\x82\x9c" // U+f09c +#define ICON_FA_UNLOCK_KEYHOLE "\xef\x84\xbe" // U+f13e +#define ICON_FA_UP_DOWN "\xef\x8c\xb8" // U+f338 +#define ICON_FA_UP_DOWN_LEFT_RIGHT "\xef\x82\xb2" // U+f0b2 +#define ICON_FA_UP_LONG "\xef\x8c\x8c" // U+f30c +#define ICON_FA_UP_RIGHT_AND_DOWN_LEFT_FROM_CENTER "\xef\x90\xa4" // U+f424 +#define ICON_FA_UP_RIGHT_FROM_SQUARE "\xef\x8d\x9d" // U+f35d +#define ICON_FA_UPLOAD "\xef\x82\x93" // U+f093 +#define ICON_FA_USER "\xef\x80\x87" // U+f007 +#define ICON_FA_USER_ASTRONAUT "\xef\x93\xbb" // U+f4fb +#define ICON_FA_USER_CHECK "\xef\x93\xbc" // U+f4fc +#define ICON_FA_USER_CLOCK "\xef\x93\xbd" // U+f4fd +#define ICON_FA_USER_DOCTOR "\xef\x83\xb0" // U+f0f0 +#define ICON_FA_USER_GEAR "\xef\x93\xbe" // U+f4fe +#define ICON_FA_USER_GRADUATE "\xef\x94\x81" // U+f501 +#define ICON_FA_USER_GROUP "\xef\x94\x80" // U+f500 +#define ICON_FA_USER_INJURED "\xef\x9c\xa8" // U+f728 +#define ICON_FA_USER_LARGE "\xef\x90\x86" // U+f406 +#define ICON_FA_USER_LARGE_SLASH "\xef\x93\xba" // U+f4fa +#define ICON_FA_USER_LOCK "\xef\x94\x82" // U+f502 +#define ICON_FA_USER_MINUS "\xef\x94\x83" // U+f503 +#define ICON_FA_USER_NINJA "\xef\x94\x84" // U+f504 +#define ICON_FA_USER_NURSE "\xef\xa0\xaf" // U+f82f +#define ICON_FA_USER_PEN "\xef\x93\xbf" // U+f4ff +#define ICON_FA_USER_PLUS "\xef\x88\xb4" // U+f234 +#define ICON_FA_USER_SECRET "\xef\x88\x9b" // U+f21b +#define ICON_FA_USER_SHIELD "\xef\x94\x85" // U+f505 +#define ICON_FA_USER_SLASH "\xef\x94\x86" // U+f506 +#define ICON_FA_USER_TAG "\xef\x94\x87" // U+f507 +#define ICON_FA_USER_TIE "\xef\x94\x88" // U+f508 +#define ICON_FA_USER_XMARK "\xef\x88\xb5" // U+f235 +#define ICON_FA_USERS "\xef\x83\x80" // U+f0c0 +#define ICON_FA_USERS_BETWEEN_LINES "\xee\x96\x91" // U+e591 +#define ICON_FA_USERS_GEAR "\xef\x94\x89" // U+f509 +#define ICON_FA_USERS_LINE "\xee\x96\x92" // U+e592 +#define ICON_FA_USERS_RAYS "\xee\x96\x93" // U+e593 +#define ICON_FA_USERS_RECTANGLE "\xee\x96\x94" // U+e594 +#define ICON_FA_USERS_SLASH "\xee\x81\xb3" // U+e073 +#define ICON_FA_USERS_VIEWFINDER "\xee\x96\x95" // U+e595 +#define ICON_FA_UTENSILS "\xef\x8b\xa7" // U+f2e7 +#define ICON_FA_V "V" // U+0056 +#define ICON_FA_VAN_SHUTTLE "\xef\x96\xb6" // U+f5b6 +#define ICON_FA_VAULT "\xee\x8b\x85" // U+e2c5 +#define ICON_FA_VECTOR_SQUARE "\xef\x97\x8b" // U+f5cb +#define ICON_FA_VENUS "\xef\x88\xa1" // U+f221 +#define ICON_FA_VENUS_DOUBLE "\xef\x88\xa6" // U+f226 +#define ICON_FA_VENUS_MARS "\xef\x88\xa8" // U+f228 +#define ICON_FA_VEST "\xee\x82\x85" // U+e085 +#define ICON_FA_VEST_PATCHES "\xee\x82\x86" // U+e086 +#define ICON_FA_VIAL "\xef\x92\x92" // U+f492 +#define ICON_FA_VIAL_CIRCLE_CHECK "\xee\x96\x96" // U+e596 +#define ICON_FA_VIAL_VIRUS "\xee\x96\x97" // U+e597 +#define ICON_FA_VIALS "\xef\x92\x93" // U+f493 +#define ICON_FA_VIDEO "\xef\x80\xbd" // U+f03d +#define ICON_FA_VIDEO_SLASH "\xef\x93\xa2" // U+f4e2 +#define ICON_FA_VIHARA "\xef\x9a\xa7" // U+f6a7 +#define ICON_FA_VIRUS "\xee\x81\xb4" // U+e074 +#define ICON_FA_VIRUS_COVID "\xee\x92\xa8" // U+e4a8 +#define ICON_FA_VIRUS_COVID_SLASH "\xee\x92\xa9" // U+e4a9 +#define ICON_FA_VIRUS_SLASH "\xee\x81\xb5" // U+e075 +#define ICON_FA_VIRUSES "\xee\x81\xb6" // U+e076 +#define ICON_FA_VOICEMAIL "\xef\xa2\x97" // U+f897 +#define ICON_FA_VOLCANO "\xef\x9d\xb0" // U+f770 +#define ICON_FA_VOLLEYBALL "\xef\x91\x9f" // U+f45f +#define ICON_FA_VOLUME_HIGH "\xef\x80\xa8" // U+f028 +#define ICON_FA_VOLUME_LOW "\xef\x80\xa7" // U+f027 +#define ICON_FA_VOLUME_OFF "\xef\x80\xa6" // U+f026 +#define ICON_FA_VOLUME_XMARK "\xef\x9a\xa9" // U+f6a9 +#define ICON_FA_VR_CARDBOARD "\xef\x9c\xa9" // U+f729 +#define ICON_FA_W "W" // U+0057 +#define ICON_FA_WALKIE_TALKIE "\xef\xa3\xaf" // U+f8ef +#define ICON_FA_WALLET "\xef\x95\x95" // U+f555 +#define ICON_FA_WAND_MAGIC "\xef\x83\x90" // U+f0d0 +#define ICON_FA_WAND_MAGIC_SPARKLES "\xee\x8b\x8a" // U+e2ca +#define ICON_FA_WAND_SPARKLES "\xef\x9c\xab" // U+f72b +#define ICON_FA_WAREHOUSE "\xef\x92\x94" // U+f494 +#define ICON_FA_WATER "\xef\x9d\xb3" // U+f773 +#define ICON_FA_WATER_LADDER "\xef\x97\x85" // U+f5c5 +#define ICON_FA_WAVE_SQUARE "\xef\xa0\xbe" // U+f83e +#define ICON_FA_WEIGHT_HANGING "\xef\x97\x8d" // U+f5cd +#define ICON_FA_WEIGHT_SCALE "\xef\x92\x96" // U+f496 +#define ICON_FA_WHEAT_AWN "\xee\x8b\x8d" // U+e2cd +#define ICON_FA_WHEAT_AWN_CIRCLE_EXCLAMATION "\xee\x96\x98" // U+e598 +#define ICON_FA_WHEELCHAIR "\xef\x86\x93" // U+f193 +#define ICON_FA_WHEELCHAIR_MOVE "\xee\x8b\x8e" // U+e2ce +#define ICON_FA_WHISKEY_GLASS "\xef\x9e\xa0" // U+f7a0 +#define ICON_FA_WIFI "\xef\x87\xab" // U+f1eb +#define ICON_FA_WIND "\xef\x9c\xae" // U+f72e +#define ICON_FA_WINDOW_MAXIMIZE "\xef\x8b\x90" // U+f2d0 +#define ICON_FA_WINDOW_MINIMIZE "\xef\x8b\x91" // U+f2d1 +#define ICON_FA_WINDOW_RESTORE "\xef\x8b\x92" // U+f2d2 +#define ICON_FA_WINE_BOTTLE "\xef\x9c\xaf" // U+f72f +#define ICON_FA_WINE_GLASS "\xef\x93\xa3" // U+f4e3 +#define ICON_FA_WINE_GLASS_EMPTY "\xef\x97\x8e" // U+f5ce +#define ICON_FA_WON_SIGN "\xef\x85\x99" // U+f159 +#define ICON_FA_WORM "\xee\x96\x99" // U+e599 +#define ICON_FA_WRENCH "\xef\x82\xad" // U+f0ad +#define ICON_FA_X "X" // U+0058 +#define ICON_FA_X_RAY "\xef\x92\x97" // U+f497 +#define ICON_FA_XMARK "\xef\x80\x8d" // U+f00d +#define ICON_FA_XMARKS_LINES "\xee\x96\x9a" // U+e59a +#define ICON_FA_Y "Y" // U+0059 +#define ICON_FA_YEN_SIGN "\xef\x85\x97" // U+f157 +#define ICON_FA_YIN_YANG "\xef\x9a\xad" // U+f6ad +#define ICON_FA_Z "Z" // U+005a diff --git a/core/rend/boxart/boxart.cpp b/core/ui/boxart/boxart.cpp similarity index 93% rename from core/rend/boxart/boxart.cpp rename to core/ui/boxart/boxart.cpp index 7ec447d7d..dc3f42c32 100644 --- a/core/rend/boxart/boxart.cpp +++ b/core/ui/boxart/boxart.cpp @@ -19,9 +19,24 @@ #include "boxart.h" #include "gamesdb.h" #include "../game_scanner.h" +#include "oslib/oslib.h" +#include "cfg/option.h" #include GameBoxart Boxart::getBoxart(const GameMedia& media) +{ + loadDatabase(); + GameBoxart boxart; + { + std::lock_guard guard(mutex); + auto it = games.find(media.fileName); + if (it != games.end()) + boxart = it->second; + } + return boxart; +} + +GameBoxart Boxart::getBoxartAndLoad(const GameMedia& media) { loadDatabase(); GameBoxart boxart; @@ -62,6 +77,7 @@ void Boxart::fetchBoxart() if (toFetch.empty()) return; fetching = std::async(std::launch::async, [this]() { + ThreadName _("BoxArt-scraper"); if (offlineScraper == nullptr) { offlineScraper = std::unique_ptr(new OfflineScraper()); diff --git a/core/rend/boxart/boxart.h b/core/ui/boxart/boxart.h similarity index 96% rename from core/rend/boxart/boxart.h rename to core/ui/boxart/boxart.h index a19680b23..2f91dc424 100644 --- a/core/rend/boxart/boxart.h +++ b/core/ui/boxart/boxart.h @@ -32,6 +32,7 @@ struct GameMedia; class Boxart { public: + GameBoxart getBoxartAndLoad(const GameMedia& media); GameBoxart getBoxart(const GameMedia& media); void saveDatabase(bool internal = false); diff --git a/core/rend/boxart/gamesdb.cpp b/core/ui/boxart/gamesdb.cpp similarity index 94% rename from core/rend/boxart/gamesdb.cpp rename to core/ui/boxart/gamesdb.cpp index b15d40692..8a092d7d5 100644 --- a/core/rend/boxart/gamesdb.cpp +++ b/core/ui/boxart/gamesdb.cpp @@ -17,9 +17,8 @@ along with Flycast. If not, see . */ #include "gamesdb.h" -#include "http_client.h" +#include "oslib/http_client.h" #include "stdclass.h" -#include "oslib/oslib.h" #include "emulator.h" #define APIKEY "3fcc5e726a129924972be97abfd577ac5311f8f12398a9d9bcb5a377d4656fa8" @@ -74,9 +73,9 @@ void TheGamesDb::copyFile(const std::string& from, const std::string& to) json TheGamesDb::httpGet(const std::string& url) { - if (os_GetSeconds() < blackoutPeriod) + if (getTimeMs() < blackoutPeriod) throw std::runtime_error(""); - blackoutPeriod = 0.0; + blackoutPeriod = 0; DEBUG_LOG(COMMON, "TheGameDb: GET %s", url.c_str()); std::vector receivedData; @@ -84,9 +83,9 @@ json TheGamesDb::httpGet(const std::string& url) bool success = http::success(status); if (status == 403) // hit rate-limit cap - blackoutPeriod = os_GetSeconds() + 60.0; + blackoutPeriod = getTimeMs() + 60 * 1000; else if (!success) - blackoutPeriod = os_GetSeconds() + 1.0; + blackoutPeriod = getTimeMs() + 1000; if (!success || receivedData.empty()) throw std::runtime_error("http error"); @@ -226,12 +225,14 @@ void TheGamesDb::parseBoxart(GameBoxart& item, const json& j, int gameId) { copyFile(cached->second, filename); item.setBoxartPath(filename); + item.boxartUrl = url; } else { if (downloadImage(url, filename)) { item.setBoxartPath(filename); + item.boxartUrl = url; boxartCache[url] = filename; } } @@ -321,7 +322,8 @@ void TheGamesDb::scrape(GameBoxart& item) return; fetchPlatforms(); - if (!item.uniqueId.empty()) + // Ignore default disk ids used by kos and katana + if (!item.uniqueId.empty() && item.uniqueId != "T0000" && item.uniqueId != "T0000M") { std::string url = makeUrl("Games/ByGameUniqueID") + "&fields=overview,uids&include=boxart&filter%5Bplatform%5D=" + std::to_string(dreamcastPlatformId) + "&uid=" + http::urlEncode(item.uniqueId); @@ -379,7 +381,7 @@ void TheGamesDb::fetchByUids(std::vector& items) void TheGamesDb::scrape(std::vector& items) { - if (os_GetSeconds() < blackoutPeriod) + if (getTimeMs() < blackoutPeriod) throw std::runtime_error(""); blackoutPeriod = 0.0; @@ -394,8 +396,11 @@ void TheGamesDb::scrape(std::vector& items) else if (item.gamePath.empty()) { std::string localPath = makeUniqueFilename("dreamcast_logo_grey.png"); - if (downloadImage("https://flyinghead.github.io/flycast-builds/dreamcast_logo_grey.png", localPath)) + std::string biosArtUrl{ "https://flyinghead.github.io/flycast-builds/dreamcast_logo_grey.png" }; + if (downloadImage(biosArtUrl, localPath)) { item.setBoxartPath(localPath); + item.boxartUrl = biosArtUrl; + } } item.scraped = true; } diff --git a/core/rend/boxart/gamesdb.h b/core/ui/boxart/gamesdb.h similarity index 98% rename from core/rend/boxart/gamesdb.h rename to core/ui/boxart/gamesdb.h index 90e179074..4d8445971 100644 --- a/core/rend/boxart/gamesdb.h +++ b/core/ui/boxart/gamesdb.h @@ -47,7 +47,7 @@ private: int dreamcastPlatformId = 0; int arcadePlatformId = 0; - double blackoutPeriod = 0.0; + u64 blackoutPeriod = 0; std::map boxartCache; // key: url, value: local file path }; diff --git a/core/rend/boxart/pvrparser.h b/core/ui/boxart/pvrparser.h similarity index 97% rename from core/rend/boxart/pvrparser.h rename to core/ui/boxart/pvrparser.h index 3cf699553..989f52e24 100644 --- a/core/rend/boxart/pvrparser.h +++ b/core/ui/boxart/pvrparser.h @@ -40,7 +40,7 @@ enum PvrDataFormat { PvrSquareTwiddledMipmapsAlt = 0x12, }; -static bool pvrParse(const u8 *data, u32 len, u32& width, u32& height, std::vector& out) +static inline bool pvrParse(const u8 *data, u32 len, u32& width, u32& height, std::vector& out) { if (len < 16) return false; diff --git a/core/rend/boxart/scraper.cpp b/core/ui/boxart/scraper.cpp similarity index 99% rename from core/rend/boxart/scraper.cpp rename to core/ui/boxart/scraper.cpp index 0c905ba71..91d97513a 100644 --- a/core/rend/boxart/scraper.cpp +++ b/core/ui/boxart/scraper.cpp @@ -17,7 +17,7 @@ along with Flycast. If not, see . */ #include "scraper.h" -#include "http_client.h" +#include "oslib/http_client.h" #include "stdclass.h" #include "emulator.h" #include "imgread/common.h" @@ -148,7 +148,7 @@ void OfflineScraper::scrape(GameBoxart& item) FILE *f = nowide::fopen((const char *)context, "wb"); if (f == nullptr) { - WARN_LOG(COMMON, "can't create local file %s: error %d", context, errno); + WARN_LOG(COMMON, "can't create local file %s: error %d", (const char *)context, errno); } else { diff --git a/core/rend/boxart/scraper.h b/core/ui/boxart/scraper.h similarity index 96% rename from core/rend/boxart/scraper.h rename to core/ui/boxart/scraper.h index d6a8f81ce..321095500 100644 --- a/core/rend/boxart/scraper.h +++ b/core/ui/boxart/scraper.h @@ -38,6 +38,7 @@ struct GameBoxart std::string gamePath; std::string boxartPath; + std::string boxartUrl; bool parsed = false; bool scraped = false; @@ -56,6 +57,7 @@ struct GameBoxart { "release_date", releaseDate }, { "overview", overview }, { "boxart_path", boxartPath }, + { "boxart_url", boxartUrl }, { "parsed", parsed }, { "scraped", scraped }, }; @@ -84,6 +86,7 @@ struct GameBoxart loadProperty(releaseDate, j, "release_date"); loadProperty(overview, j, "overview"); loadProperty(boxartPath, j, "boxart_path"); + loadProperty(boxartUrl, j, "boxart_url"); loadProperty(parsed, j, "parsed"); loadProperty(scraped, j, "scraped"); } diff --git a/core/ui/discord.cpp b/core/ui/discord.cpp new file mode 100644 index 000000000..02ffd8dc1 --- /dev/null +++ b/core/ui/discord.cpp @@ -0,0 +1,127 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#include "types.h" +#include "emulator.h" +#include "cfg/option.h" +#include "gui.h" +#include "discord_rpc.h" +#include +#include + +#define FLYCAST_APPID "1212789289851559946" + +class DiscordPresence +{ +public: + DiscordPresence() + { + EventManager::listen(Event::Start, handleEmuEvent, this); + EventManager::listen(Event::Terminate, handleEmuEvent, this); + EventManager::listen(Event::Resume, handleEmuEvent, this); + EventManager::listen(Event::Network, handleEmuEvent, this); + } + + ~DiscordPresence() + { + shutdown(); + EventManager::unlisten(Event::Start, handleEmuEvent, this); + EventManager::unlisten(Event::Terminate, handleEmuEvent, this); + EventManager::unlisten(Event::Resume, handleEmuEvent, this); + EventManager::unlisten(Event::Network, handleEmuEvent, this); + } + +private: + void initialize() + { + if (!initialized) + Discord_Initialize(FLYCAST_APPID, nullptr, 0, nullptr); + initialized = true; + } + + void shutdown() + { + if (initialized) + Discord_Shutdown(); + initialized = false; + } + + void sendPresence() + { + initialize(); + DiscordRichPresence discordPresence{}; + discordPresence.state = settings.content.title.c_str(); + discordPresence.startTimestamp = startTimestamp; + std::string imageUrl = gui_getCurGameBoxartUrl(); + if (!imageUrl.empty()) + { + discordPresence.largeImageKey = imageUrl.c_str(); + discordPresence.largeImageText = settings.content.title.c_str(); + discordPresence.smallImageKey = "icon-512"; + discordPresence.smallImageText = "Flycast is a Dreamcast, Naomi and Atomiswave emulator"; + } + else + { + discordPresence.largeImageKey = "icon-512"; + discordPresence.largeImageText = "Flycast is a Dreamcast, Naomi and Atomiswave emulator"; + } + if (settings.network.online) + discordPresence.details = "Online"; + Discord_UpdatePresence(&discordPresence); + } + + static void handleEmuEvent(Event event, void *p) + { + if (settings.naomi.slave || settings.naomi.drivingSimSlave != 0) + return; + DiscordPresence *inst = (DiscordPresence *)p; + switch (event) + { + case Event::Start: + inst->startTimestamp = time(nullptr); + [[fallthrough]]; + case Event::Network: + if (config::DiscordPresence) + inst->sendPresence(); + break; + case Event::Resume: + if (config::DiscordPresence && !inst->initialized) + // Discord presence enabled + inst->sendPresence(); + else if (!config::DiscordPresence && inst->initialized) + { + // Discord presence disabled + Discord_ClearPresence(); + inst->shutdown(); + } + break; + case Event::Terminate: + if (inst->initialized) + Discord_ClearPresence(); + inst->startTimestamp = 0; + break; + default: + break; + } + } + + bool initialized = false; + int64_t startTimestamp = 0; +}; + +static DiscordPresence discordPresence; diff --git a/core/ui/game_scanner.cpp b/core/ui/game_scanner.cpp new file mode 100644 index 000000000..49666726e --- /dev/null +++ b/core/ui/game_scanner.cpp @@ -0,0 +1,161 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#include "game_scanner.h" +#include "stdclass.h" +#include "oslib/oslib.h" +#include "oslib/storage.h" +#include "cfg/option.h" + +static bool operator<(const GameMedia &left, const GameMedia &right) +{ + return left.name < right.name; +} + +void GameScanner::insert_game(const GameMedia& game) +{ + LockGuard _(mutex); + game_list.insert(std::upper_bound(game_list.begin(), game_list.end(), game), game); +} + +void GameScanner::insert_arcade_game(const GameMedia& game) +{ + arcade_game_list.insert(std::upper_bound(arcade_game_list.begin(), arcade_game_list.end(), game), game); +} + +void GameScanner::add_game_directory(const std::string& path) +{ + hostfs::DirectoryTree tree(path); + std::string emptyParentPath; + for (const hostfs::FileInfo& item : tree) + { + if (!running) + break; + + if (game_list.empty()) + { + // This won't work for android content uris + size_t slash = get_last_slash_pos(item.path); + std::string parentPath; + if (slash != 0 && slash != std::string::npos) + parentPath = item.path.substr(0, slash); + else + parentPath = item.path; + if (parentPath != emptyParentPath) + { + ++empty_folders_scanned; + emptyParentPath = parentPath; + if (empty_folders_scanned > 1000) + content_path_looks_incorrect = true; + } + } + else + { + content_path_looks_incorrect = false; + } + + if (item.name.substr(0, 2) == "._") + // Ignore Mac OS turds + continue; + std::string fileName(item.name); + std::string gameName(get_file_basename(item.name)); + std::string extension = get_file_extension(item.name); + if (extension == "zip" || extension == "7z") + { + string_tolower(gameName); + auto it = arcade_games.find(gameName); + if (it == arcade_games.end()) + continue; + gameName = it->second->description; + fileName = fileName + " (" + gameName + ")"; + insert_arcade_game(GameMedia{ fileName, item.path, item.name, gameName }); + continue; + } + else if (extension == "bin" || extension == "lst" || extension == "dat") + { + if (!config::HideLegacyNaomiRoms) + insert_arcade_game(GameMedia{ fileName, item.path, item.name, gameName }); + continue; + } + else if (extension == "chd" || extension == "gdi") + { + // Hide arcade gdroms + std::string basename = gameName; + string_tolower(basename); + if (arcade_gdroms.count(basename) != 0) + continue; + } + else if (extension != "cdi" && extension != "cue") + continue; + insert_game(GameMedia{ fileName, item.path, item.name, gameName }); + } +} + +void GameScanner::stop() +{ + LockGuard _(threadMutex); + running = false; + empty_folders_scanned = 0; + content_path_looks_incorrect = false; + if (scan_thread && scan_thread->joinable()) + scan_thread->join(); +} + +void GameScanner::fetch_game_list() +{ + LockGuard _(threadMutex); + if (scan_done || running) + return; + if (scan_thread && scan_thread->joinable()) + scan_thread->join(); + running = true; + scan_thread = std::make_unique([this]() + { + ThreadName _("GameScanner"); + if (arcade_games.empty()) + for (int gameid = 0; Games[gameid].name != nullptr; gameid++) + { + const Game *game = &Games[gameid]; + arcade_games[game->name] = game; + if (game->gdrom_name != nullptr) + arcade_gdroms.insert(game->gdrom_name); + } + { + LockGuard _(mutex); + game_list.clear(); + } + arcade_game_list.clear(); + for (const auto& path : config::ContentPath.get()) + { + try { + add_game_directory(path); + } catch (const hostfs::StorageException& e) { + // ignore + } + if (!running) + break; + } + { + LockGuard _(mutex); + game_list.insert(game_list.end(), arcade_game_list.begin(), arcade_game_list.end()); + } + if (running) + scan_done = true; + running = false; + }); +} diff --git a/core/ui/game_scanner.h b/core/ui/game_scanner.h new file mode 100644 index 000000000..f6f5901d8 --- /dev/null +++ b/core/ui/game_scanner.h @@ -0,0 +1,71 @@ +/* + Copyright 2020 flyinghead + + This file is part of flycast. + + flycast 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. + + flycast 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 flycast. If not, see . + */ +#pragma once +#include "types.h" +#include "hw/naomi/naomi_roms.h" +#include +#include +#include +#include +#include +#include + +struct GameMedia { + std::string name; // Display name + std::string path; // Full path to rom. May be an encoded uri + std::string fileName; // Last component of the path, decoded + std::string gameName; // for arcade games only, description from the rom list +}; + +class GameScanner +{ + std::vector game_list; + std::vector arcade_game_list; + std::mutex mutex; + std::mutex threadMutex; + std::unique_ptr scan_thread; + bool scan_done = false; + bool running = false; + std::unordered_map arcade_games; + std::unordered_set arcade_gdroms; + using LockGuard = std::lock_guard; + + void insert_game(const GameMedia& game); + void insert_arcade_game(const GameMedia& game); + void add_game_directory(const std::string& path); + +public: + ~GameScanner() + { + stop(); + } + void refresh() + { + stop(); + scan_done = false; + } + + void stop(); + void fetch_game_list(); + + std::mutex& get_mutex() { return mutex; } + const std::vector& get_game_list() { return game_list; } + unsigned int empty_folders_scanned = 0; + bool content_path_looks_incorrect = false; +}; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp new file mode 100644 index 000000000..bcaf568ab --- /dev/null +++ b/core/ui/gui.cpp @@ -0,0 +1,3749 @@ +/* + Copyright 2019 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . + */ +#include "gui.h" +#include "rend/osd.h" +#include "cfg/cfg.h" +#include "hw/maple/maple_if.h" +#include "hw/maple/maple_devs.h" +#include "imgui.h" +#include "imgui_stdlib.h" +#include "network/net_handshake.h" +#include "network/ggpo.h" +#include "wsi/context.h" +#include "input/gamepad_device.h" +#include "gui_util.h" +#include "game_scanner.h" +#include "version.h" +#include "oslib/oslib.h" +#include "audio/audiostream.h" +#include "imgread/common.h" +#include "log/LogManager.h" +#include "emulator.h" +#include "mainui.h" +#include "lua/lua.h" +#include "gui_chat.h" +#include "imgui_driver.h" +#if FC_PROFILER +#include "implot.h" +#endif +#include "boxart/boxart.h" +#include "profiler/fc_profiler.h" +#include "hw/naomi/card_reader.h" +#include "oslib/resources.h" +#include "achievements/achievements.h" +#include "gui_achievements.h" +#include "IconsFontAwesome6.h" +#include "oslib/storage.h" +#include +#include "hw/pvr/Renderer_if.h" +#if defined(USE_SDL) +#include "sdl/sdl.h" +#endif + +#ifdef __ANDROID__ +#include "gui_android.h" +#if HOST_CPU == CPU_ARM64 && USE_VULKAN +#include "rend/vulkan/adreno.h" +#endif +#endif + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include + +static bool game_started; + +int insetLeft, insetRight, insetTop, insetBottom; +std::unique_ptr imguiDriver; + +static bool inited = false; +GuiState gui_state = GuiState::Main; +static bool commandLineStart; +static u32 mouseButtons; +static int mouseX, mouseY; +static float mouseWheel; +static std::string error_msg; +static bool error_msg_shown; +static std::string osd_message; +static u64 osd_message_end; +static std::mutex osd_message_mutex; +static void (*showOnScreenKeyboard)(bool show); +static bool keysUpNextFrame[512]; +static bool uiUserScaleUpdated; + +static void reset_vmus(); +void error_popup(); + +static GameScanner scanner; +static BackgroundGameLoader gameLoader; +static Boxart boxart; +static Chat chat; +static std::recursive_mutex guiMutex; +using LockGuard = std::lock_guard; + +ImFont *largeFont; +static Toast toast; +static ThreadRunner uiThreadRunner; + +static void emuEventCallback(Event event, void *) +{ + switch (event) + { + case Event::Resume: + game_started = true; + break; + case Event::Start: + GamepadDevice::load_system_mappings(); + break; + case Event::Terminate: + GamepadDevice::load_system_mappings(); + game_started = false; + break; + default: + break; + } +} + +void gui_init() +{ + if (inited) + return; + inited = true; + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); +#if FC_PROFILER + ImPlot::CreateContext(); +#endif + ImGuiIO& io = ImGui::GetIO(); (void)io; + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + io.IniFilename = NULL; + + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + + EventManager::listen(Event::Resume, emuEventCallback); + EventManager::listen(Event::Start, emuEventCallback); + EventManager::listen(Event::Terminate, emuEventCallback); + ggpo::receiveChatMessages([](int playerNum, const std::string& msg) { chat.receive(playerNum, msg); }); +} + +static ImGuiKey keycodeToImGuiKey(u8 keycode) +{ + switch (keycode) + { + case 0x2B: return ImGuiKey_Tab; + case 0x50: return ImGuiKey_LeftArrow; + case 0x4F: return ImGuiKey_RightArrow; + case 0x52: return ImGuiKey_UpArrow; + case 0x51: return ImGuiKey_DownArrow; + case 0x4B: return ImGuiKey_PageUp; + case 0x4E: return ImGuiKey_PageDown; + case 0x4A: return ImGuiKey_Home; + case 0x4D: return ImGuiKey_End; + case 0x49: return ImGuiKey_Insert; + case 0x4C: return ImGuiKey_Delete; + case 0x2A: return ImGuiKey_Backspace; + case 0x2C: return ImGuiKey_Space; + case 0x28: return ImGuiKey_Enter; + case 0x29: return ImGuiKey_Escape; + case 0x04: return ImGuiKey_A; + case 0x06: return ImGuiKey_C; + case 0x19: return ImGuiKey_V; + case 0x1B: return ImGuiKey_X; + case 0x1C: return ImGuiKey_Y; + case 0x1D: return ImGuiKey_Z; + case 0xE0: + case 0xE4: + return ImGuiMod_Ctrl; + case 0xE1: + case 0xE5: + return ImGuiMod_Shift; + case 0xE3: + case 0xE7: + return ImGuiMod_Super; + default: return ImGuiKey_None; + } +} + +void gui_initFonts() +{ + static float uiScale; + + verify(inited); + uiThreadRunner.init(); + +#if !defined(TARGET_UWP) && !defined(__SWITCH__) + settings.display.uiScale = std::max(1.f, settings.display.dpi / 100.f * 0.75f); + // Limit scaling on small low-res screens + if (settings.display.width <= 640 || settings.display.height <= 480) + settings.display.uiScale = std::min(1.2f, settings.display.uiScale); +#endif + settings.display.uiScale *= config::UIScaling / 100.f; + if (settings.display.uiScale == uiScale && ImGui::GetIO().Fonts->IsBuilt()) + return; + uiScale = settings.display.uiScale; + + // Setup Dear ImGui style + ImGui::GetStyle() = ImGuiStyle{}; + ImGui::StyleColorsDark(); + ImGui::GetStyle().TabRounding = 0; + ImGui::GetStyle().ItemSpacing = ImVec2(8, 8); // from 8,4 + ImGui::GetStyle().ItemInnerSpacing = ImVec2(4, 6); // from 4,4 +#if defined(__ANDROID__) || defined(TARGET_IPHONE) || defined(__SWITCH__) + ImGui::GetStyle().TouchExtraPadding = ImVec2(1, 1); // from 0,0 +#endif + if (settings.display.uiScale > 1) + ImGui::GetStyle().ScaleAllSizes(settings.display.uiScale); + + static const ImWchar ranges[] = + { + 0x0020, 0xFFFF, // All chars + 0, + }; + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); + largeFont = nullptr; + const float fontSize = uiScaled(17.f); + size_t dataSize; + std::unique_ptr data = resource::load("fonts/Roboto-Medium.ttf", dataSize); + verify(data != nullptr); + io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, fontSize, nullptr, ranges); + ImFontConfig font_cfg; + font_cfg.MergeMode = true; +#ifdef _WIN32 + u32 cp = GetACP(); + std::string fontDir = std::string(nowide::getenv("SYSTEMROOT")) + "\\Fonts\\"; + switch (cp) + { + case 932: // Japanese + { + font_cfg.FontNo = 2; // UIGothic + ImFont* font = io.Fonts->AddFontFromFileTTF((fontDir + "msgothic.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesJapanese()); + font_cfg.FontNo = 2; // Meiryo UI + if (font == nullptr) + io.Fonts->AddFontFromFileTTF((fontDir + "Meiryo.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesJapanese()); + } + break; + case 949: // Korean + { + ImFont* font = io.Fonts->AddFontFromFileTTF((fontDir + "Malgun.ttf").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesKorean()); + if (font == nullptr) + { + font_cfg.FontNo = 2; // Dotum + io.Fonts->AddFontFromFileTTF((fontDir + "Gulim.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesKorean()); + } + } + break; + case 950: // Traditional Chinese + { + font_cfg.FontNo = 1; // Microsoft JhengHei UI Regular + ImFont* font = io.Fonts->AddFontFromFileTTF((fontDir + "Msjh.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseTraditionalOfficial()); + font_cfg.FontNo = 0; + if (font == nullptr) + io.Fonts->AddFontFromFileTTF((fontDir + "MSJH.ttf").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseTraditionalOfficial()); + } + break; + case 936: // Simplified Chinese + io.Fonts->AddFontFromFileTTF((fontDir + "Simsun.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseSimplifiedOfficial()); + break; + default: + break; + } +#elif defined(__APPLE__) && !defined(TARGET_IPHONE) + std::string fontDir = std::string("/System/Library/Fonts/"); + + extern std::string os_Locale(); + std::string locale = os_Locale(); + + if (locale.find("ja") == 0) // Japanese + { + io.Fonts->AddFontFromFileTTF((fontDir + "ヒラギノ角ゴシック W4.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesJapanese()); + } + else if (locale.find("ko") == 0) // Korean + { + io.Fonts->AddFontFromFileTTF((fontDir + "AppleSDGothicNeo.ttc").c_str(), fontSize, &font_cfg, io.Fonts->GetGlyphRangesKorean()); + } + else if (locale.find("zh-Hant") == 0) // Traditional Chinese + { + io.Fonts->AddFontFromFileTTF((fontDir + "PingFang.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseTraditionalOfficial()); + } + else if (locale.find("zh-Hans") == 0) // Simplified Chinese + { + io.Fonts->AddFontFromFileTTF((fontDir + "PingFang.ttc").c_str(), fontSize, &font_cfg, GetGlyphRangesChineseSimplifiedOfficial()); + } +#elif defined(__ANDROID__) + if (getenv("FLYCAST_LOCALE") != nullptr) + { + const ImWchar *glyphRanges = nullptr; + std::string locale = getenv("FLYCAST_LOCALE"); + if (locale.find("ja") == 0) // Japanese + glyphRanges = io.Fonts->GetGlyphRangesJapanese(); + else if (locale.find("ko") == 0) // Korean + glyphRanges = io.Fonts->GetGlyphRangesKorean(); + else if (locale.find("zh_TW") == 0 + || locale.find("zh_HK") == 0) // Traditional Chinese + glyphRanges = GetGlyphRangesChineseTraditionalOfficial(); + else if (locale.find("zh_CN") == 0) // Simplified Chinese + glyphRanges = GetGlyphRangesChineseSimplifiedOfficial(); + + if (glyphRanges != nullptr) + io.Fonts->AddFontFromFileTTF("/system/fonts/NotoSansCJK-Regular.ttc", fontSize, &font_cfg, glyphRanges); + } + + // TODO Linux, iOS, ... +#endif + // Font Awesome symbols (added to default font) + data = resource::load("fonts/" FONT_ICON_FILE_NAME_FAS, dataSize); + verify(data != nullptr); + font_cfg.FontNo = 0; + static ImWchar faRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, fontSize, &font_cfg, faRanges); + // Large font without Asian glyphs + data = resource::load("fonts/Roboto-Regular.ttf", dataSize); + verify(data != nullptr); + const float largeFontSize = uiScaled(21.f); + largeFont = io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, largeFontSize, nullptr, ranges); + + NOTICE_LOG(RENDERER, "Screen DPI is %.0f, size %d x %d. Scaling by %.2f", settings.display.dpi, settings.display.width, settings.display.height, settings.display.uiScale); +} + +void gui_keyboard_input(u16 wc) +{ + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureKeyboard) + io.AddInputCharacter(wc); +} + +void gui_keyboard_inputUTF8(const std::string& s) +{ + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureKeyboard) + io.AddInputCharactersUTF8(s.c_str()); +} + +void gui_keyboard_key(u8 keyCode, bool pressed) +{ + if (!inited) + return; + ImGuiKey key = keycodeToImGuiKey(keyCode); + if (key == ImGuiKey_None) + return; + if (!pressed && ImGui::IsKeyDown(key)) + { + keysUpNextFrame[keyCode] = true; + return; + } + ImGuiIO& io = ImGui::GetIO(); + io.AddKeyEvent(key, pressed); +} + +bool gui_keyboard_captured() +{ + ImGuiIO& io = ImGui::GetIO(); + return io.WantCaptureKeyboard; +} + +bool gui_mouse_captured() +{ + ImGuiIO& io = ImGui::GetIO(); + return io.WantCaptureMouse; +} + +void gui_set_mouse_position(int x, int y) +{ + mouseX = std::round(x * settings.display.pointScale); + mouseY = std::round(y * settings.display.pointScale); +} + +void gui_set_mouse_button(int button, bool pressed) +{ + if (pressed) + mouseButtons |= 1 << button; + else + mouseButtons &= ~(1 << button); +} + +void gui_set_mouse_wheel(float delta) +{ + mouseWheel += delta; +} + +static void gui_newFrame() +{ + imguiDriver->newFrame(); + ImGui::GetIO().DisplaySize.x = settings.display.width; + ImGui::GetIO().DisplaySize.y = settings.display.height; + + ImGuiIO& io = ImGui::GetIO(); + + if (mouseX < 0 || mouseX >= settings.display.width || mouseY < 0 || mouseY >= settings.display.height) + io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + else + io.AddMousePosEvent(mouseX, mouseY); + static bool delayTouch; +#if defined(__ANDROID__) || defined(TARGET_IPHONE) || defined(__SWITCH__) + // Delay touch by one frame to allow widgets to be hovered before click + // This is required for widgets using ImGuiButtonFlags_AllowItemOverlap such as TabItem's + if (!delayTouch && (mouseButtons & (1 << 0)) != 0 && !io.MouseDown[ImGuiMouseButton_Left]) + delayTouch = true; + else + delayTouch = false; +#endif + if (io.WantCaptureMouse) + { + io.AddMouseWheelEvent(0, -mouseWheel / 16); + mouseWheel = 0; + } + if (!delayTouch) + io.AddMouseButtonEvent(ImGuiMouseButton_Left, (mouseButtons & (1 << 0)) != 0); + io.AddMouseButtonEvent(ImGuiMouseButton_Right, (mouseButtons & (1 << 1)) != 0); + io.AddMouseButtonEvent(ImGuiMouseButton_Middle, (mouseButtons & (1 << 2)) != 0); + io.AddMouseButtonEvent(3, (mouseButtons & (1 << 3)) != 0); + + // shows a popup navigation window even in game because of the OSD + //io.AddKeyEvent(ImGuiKey_GamepadFaceLeft, ((kcode[0] & DC_BTN_X) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadFaceRight, ((kcode[0] & DC_BTN_B) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadFaceUp, ((kcode[0] & DC_BTN_Y) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadFaceDown, ((kcode[0] & DC_BTN_A) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadDpadLeft, ((kcode[0] & DC_DPAD_LEFT) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadDpadRight, ((kcode[0] & DC_DPAD_RIGHT) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadDpadUp, ((kcode[0] & DC_DPAD_UP) == 0)); + io.AddKeyEvent(ImGuiKey_GamepadDpadDown, ((kcode[0] & DC_DPAD_DOWN) == 0)); + + float analog; + analog = joyx[0] < 0 ? -(float)joyx[0] / 32768.f : 0.f; + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickLeft, analog > 0.1f, analog); + analog = joyx[0] > 0 ? (float)joyx[0] / 32768.f : 0.f; + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickRight, analog > 0.1f, analog); + analog = joyy[0] < 0 ? -(float)joyy[0] / 32768.f : 0.f; + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickUp, analog > 0.1f, analog); + analog = joyy[0] > 0 ? (float)joyy[0] / 32768.f : 0.f; + io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickDown, analog > 0.1f, analog); + + ImGui::GetStyle().Colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + + if (showOnScreenKeyboard != nullptr) + showOnScreenKeyboard(io.WantTextInput); +#ifdef USE_SDL + else + { + if (io.WantTextInput && !SDL_IsTextInputActive()) + { + SDL_StartTextInput(); + } + else if (!io.WantTextInput && SDL_IsTextInputActive()) + { + SDL_StopTextInput(); + } + } +#endif +} + +static void delayedKeysUp() +{ + ImGuiIO& io = ImGui::GetIO(); + for (u32 i = 0; i < std::size(keysUpNextFrame); i++) + if (keysUpNextFrame[i]) + io.AddKeyEvent(keycodeToImGuiKey(i), false); + memset(keysUpNextFrame, 0, sizeof(keysUpNextFrame)); +} + +static void gui_endFrame(bool gui_open) +{ + imguiDriver->renderDrawData(ImGui::GetDrawData(), gui_open); + delayedKeysUp(); +} + +void gui_setOnScreenKeyboardCallback(void (*callback)(bool show)) +{ + showOnScreenKeyboard = callback; +} + +void gui_set_insets(int left, int right, int top, int bottom) +{ + insetLeft = left; + insetRight = right; + insetTop = top; + insetBottom = bottom; +} + +#if 0 +#include "oslib/timeseries.h" +#include +TimeSeries renderTimes; +TimeSeries vblankTimes; + +void gui_plot_render_time(int width, int height) +{ + std::vector v = renderTimes.data(); + ImGui::PlotLines("Render Times", v.data(), v.size(), 0, "", 0.0, 1.0 / 30.0, ImVec2(300, 50)); + ImGui::Text("StdDev: %.1f%%", renderTimes.stddev() * 100.f / 0.01666666667f); + v = vblankTimes.data(); + ImGui::PlotLines("VBlank", v.data(), v.size(), 0, "", 0.0, 1.0 / 30.0, ImVec2(300, 50)); + ImGui::Text("StdDev: %.1f%%", vblankTimes.stddev() * 100.f / 0.01666666667f); +} +#endif + +void gui_open_settings() +{ + const LockGuard lock(guiMutex); + if (gui_state == GuiState::Closed && !settings.naomi.slave) + { + if (!ggpo::active()) + { + HideOSD(); + try { + emu.stop(); + gui_setState(GuiState::Commands); + } catch (const FlycastException& e) { + gui_stop_game(e.what()); + } + } + else + { + chat.toggle(); + } + } + else if (gui_state == GuiState::VJoyEdit) + { + gui_setState(GuiState::VJoyEditCommands); + } + else if (gui_state == GuiState::Loading) + { + gameLoader.cancel(); + } + else if (gui_state == GuiState::Commands) + { + gui_setState(GuiState::Closed); + GamepadDevice::load_system_mappings(); + emu.start(); + } +} + +void gui_start_game(const std::string& path) +{ + const LockGuard lock(guiMutex); + if (gui_state != GuiState::Main && gui_state != GuiState::Closed && gui_state != GuiState::Commands) + return; + emu.unloadGame(); + reset_vmus(); + chat.reset(); + + scanner.stop(); + gui_setState(GuiState::Loading); + gameLoader.load(path); +} + +void gui_stop_game(const std::string& message) +{ + const LockGuard lock(guiMutex); + if (!commandLineStart) + { + // Exit to main menu + emu.unloadGame(); + gui_setState(GuiState::Main); + reset_vmus(); + if (!message.empty()) + gui_error("Flycast has stopped.\n\n" + message); + } + else + { + if (!message.empty()) + ERROR_LOG(COMMON, "Flycast has stopped: %s", message.c_str()); + // Exit emulator + dc_exit(); + } +} + +static bool savestateAllowed() +{ + return !settings.content.path.empty() && !settings.network.online && !settings.naomi.multiboard; +} + +static void appendVectorData(void *context, void *data, int size) +{ + std::vector& v = *(std::vector *)context; + const u8 *bytes = (const u8 *)data; + v.insert(v.end(), bytes, bytes + size); +} + +static void getScreenshot(std::vector& data, int width = 0) +{ + data.clear(); + std::vector rawData; + int height = 0; + if (renderer == nullptr || !renderer->GetLastFrame(rawData, width, height)) + return; + stbi_flip_vertically_on_write(0); + stbi_write_png_to_func(appendVectorData, &data, width, height, 3, &rawData[0], 0); +} + +#ifdef _WIN32 +static struct tm *localtime_r(const time_t *_clock, struct tm *_result) +{ + return localtime_s(_result, _clock) ? nullptr : _result; +} +#endif + +static std::string timeToString(time_t time) +{ + tm t; + if (localtime_r(&time, &t) == nullptr) + return {}; + std::string s(32, '\0'); + s.resize(snprintf(s.data(), 32, "%04d/%02d/%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)); + return s; +} + +static void savestate() +{ + // TODO save state async: png compression, savestate file compression/write + std::vector pngData; + getScreenshot(pngData, 640); + dc_savestate(config::SavestateSlot, pngData.empty() ? nullptr : &pngData[0], pngData.size()); + ImguiStateTexture savestatePic; + savestatePic.invalidate(); +} + +static void gui_display_commands() +{ + fullScreenWindow(false); + ImGui::SetNextWindowBgAlpha(0.8f); + ImguiStyleVar _{ImGuiStyleVar_WindowBorderSize, 0}; + + ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoDecoration); + { + ImguiStyleVar _{ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)}; // left aligned + + float columnWidth = std::min(200.f, + (ImGui::GetContentRegionAvail().x - uiScaled(100 + 150) - ImGui::GetStyle().FramePadding.x * 2) + / 2 / uiScaled(1)); + float buttonWidth = 150.f; // not scaled + bool lowWidth = ImGui::GetContentRegionAvail().x < uiScaled(100 + buttonWidth * 3) + + ImGui::GetStyle().FramePadding.x * 2 + ImGui::GetStyle().ItemSpacing.x * 2; + if (lowWidth) + buttonWidth = std::min(150.f, + (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x * 2) + / 3 / uiScaled(1)); + bool lowHeight = ImGui::GetContentRegionAvail().y < uiScaled(100 + 50 * 2 + buttonWidth * 3 / 4) + ImGui::GetTextLineHeightWithSpacing() * 2 + + ImGui::GetStyle().ItemSpacing.y * 2 + ImGui::GetStyle().WindowPadding.y; + + GameMedia game; + game.path = settings.content.path; + game.fileName = settings.content.fileName; + GameBoxart art = boxart.getBoxart(game); + ImguiFileTexture tex(art.boxartPath); + // TODO use placeholder image if not available + tex.draw(ScaledVec2(100, 100)); + + ImGui::SameLine(); + if (!lowHeight) + { + ImGui::BeginChild("game_info", ScaledVec2(0, 100.f), ImGuiChildFlags_Border, ImGuiWindowFlags_None); + ImGui::PushFont(largeFont); + ImGui::Text("%s", art.name.c_str()); + ImGui::PopFont(); + { + ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f)); + ImGui::TextWrapped("%s", art.fileName.c_str()); + } + ImGui::EndChild(); + } + + if (lowWidth) { + ImGui::Columns(3, "buttons", false); + } + else + { + ImGui::Columns(4, "buttons", false); + ImGui::SetColumnWidth(0, uiScaled(100.f) + ImGui::GetStyle().ItemSpacing.x); + ImGui::SetColumnWidth(1, uiScaled(columnWidth)); + ImGui::SetColumnWidth(2, uiScaled(columnWidth)); + const ImVec2 vmuPos = ImGui::GetStyle().WindowPadding + ScaledVec2(0.f, 100.f) + + ImVec2(insetLeft, ImGui::GetStyle().ItemSpacing.y); + ImguiVmuTexture::displayVmus(vmuPos); + ImGui::NextColumn(); + } + ImguiStyleVar _1{ImGuiStyleVar_FramePadding, ScaledVec2(12.f, 3.f)}; + + // Resume + if (ImGui::Button(ICON_FA_PLAY " Resume", ScaledVec2(buttonWidth, 50))) + { + GamepadDevice::load_system_mappings(); + gui_setState(GuiState::Closed); + } + // Cheats + { + DisabledScope _{settings.network.online || settings.raHardcoreMode}; + + if (ImGui::Button(ICON_FA_MASK " Cheats", ScaledVec2(buttonWidth, 50)) && !settings.network.online) + gui_setState(GuiState::Cheats); + } + // Achievements + { + DisabledScope _{!achievements::isActive()}; + + if (ImGui::Button(ICON_FA_TROPHY " Achievements", ScaledVec2(buttonWidth, 50)) && achievements::isActive()) + gui_setState(GuiState::Achievements); + } + // Barcode + if (card_reader::barcodeAvailable()) + { + ImGui::Text("Barcode Card"); + char cardBuf[64] {}; + strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1); + ImGui::SetNextItemWidth(uiScaled(buttonWidth)); + if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr)) + card_reader::barcodeSetCard(cardBuf); + } + + ImGui::NextColumn(); + + // Insert/Eject Disk + const char *disk_label = libGDR_GetDiscType() == Open ? ICON_FA_COMPACT_DISC " Insert Disk" : ICON_FA_COMPACT_DISC " Eject Disk"; + if (ImGui::Button(disk_label, ScaledVec2(buttonWidth, 50))) + { + if (libGDR_GetDiscType() == Open) { + gui_setState(GuiState::SelectDisk); + } + else { + DiscOpenLid(); + gui_setState(GuiState::Closed); + } + } + // Settings + if (ImGui::Button(ICON_FA_GEAR " Settings", ScaledVec2(buttonWidth, 50))) + gui_setState(GuiState::Settings); + + // Exit + if (ImGui::Button(commandLineStart ? ICON_FA_POWER_OFF " Exit" : ICON_FA_POWER_OFF " Close Game", ScaledVec2(buttonWidth, 50))) + gui_stop_game(); + + ImGui::NextColumn(); + { + DisabledScope _{!savestateAllowed()}; + ImguiStateTexture savestatePic; + time_t savestateDate = dc_getStateCreationDate(config::SavestateSlot); + + // Load State + { + DisabledScope _{settings.raHardcoreMode || savestateDate == 0}; + if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT " Load State", ScaledVec2(buttonWidth, 50)) && savestateAllowed()) + { + gui_setState(GuiState::Closed); + dc_loadstate(config::SavestateSlot); + } + } + + // Save State + if (ImGui::Button(ICON_FA_DOWNLOAD " Save State", ScaledVec2(buttonWidth, 50)) && savestateAllowed()) + { + gui_setState(GuiState::Closed); + savestate(); + } + + { + // Help with navigation with gamepad/keyboard + ImguiStyleVar _{ImGuiStyleVar_ItemSpacing, ImVec2(0.f, 2.f)}; + ImGui::Spacing(); + } + // Slot # + if (ImGui::ArrowButton("##prev-slot", ImGuiDir_Left)) + { + if (config::SavestateSlot == 0) + config::SavestateSlot = 9; + else + config::SavestateSlot--; + SaveSettings(); + } + std::string slot = "Slot " + std::to_string((int)config::SavestateSlot + 1); + float spacingW = (uiScaled(buttonWidth) - ImGui::GetFrameHeight() * 2 - ImGui::CalcTextSize(slot.c_str()).x) / 2; + ImGui::SameLine(0, spacingW); + ImGui::Text("%s", slot.c_str()); + ImGui::SameLine(0, spacingW); + if (ImGui::ArrowButton("##next-slot", ImGuiDir_Right)) + { + if (config::SavestateSlot == 9) + config::SavestateSlot = 0; + else + config::SavestateSlot++; + SaveSettings(); + } + { + ImVec4 gray(0.75f, 0.75f, 0.75f, 1.f); + if (savestateDate == 0) + ImGui::TextColored(gray, "Empty"); + else + ImGui::TextColored(gray, "%s", timeToString(savestateDate).c_str()); + } + savestatePic.draw(ScaledVec2(buttonWidth, 0.f)); + } + + ImGui::Columns(1, nullptr, false); + } + ImGui::End(); +} + +inline static void header(const char *title) +{ + ImguiStyleVar _(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)); // Left + ImguiStyleVar _1(ImGuiStyleVar_DisabledAlpha, 1.0f); + ImGui::BeginDisabled(); + ImGui::ButtonEx(title, ImVec2(-1, 0)); + ImGui::EndDisabled(); +} + +const char *maple_device_types[] = +{ + "None", + "Sega Controller", + "Light Gun", + "Keyboard", + "Mouse", + "Twin Stick", + "Arcade/Ascii Stick", + "Maracas Controller", + "Fishing Controller", + "Pop'n Music controller", + "Racing Controller", + "Densha de Go! Controller", +// "Dreameye", +}; + +const char *maple_expansion_device_types[] = +{ + "None", + "Sega VMU", + "Vibration Pack", + "Microphone", +}; + +static const char *maple_device_name(MapleDeviceType type) +{ + switch (type) + { + case MDT_SegaController: + return maple_device_types[1]; + case MDT_LightGun: + return maple_device_types[2]; + case MDT_Keyboard: + return maple_device_types[3]; + case MDT_Mouse: + return maple_device_types[4]; + case MDT_TwinStick: + return maple_device_types[5]; + case MDT_AsciiStick: + return maple_device_types[6]; + case MDT_MaracasController: + return maple_device_types[7]; + case MDT_FishingController: + return maple_device_types[8]; + case MDT_PopnMusicController: + return maple_device_types[9]; + case MDT_RacingController: + return maple_device_types[10]; + case MDT_DenshaDeGoController: + return maple_device_types[11]; + case MDT_Dreameye: +// return maple_device_types[12]; + case MDT_None: + default: + return maple_device_types[0]; + } +} + +static MapleDeviceType maple_device_type_from_index(int idx) +{ + switch (idx) + { + case 1: + return MDT_SegaController; + case 2: + return MDT_LightGun; + case 3: + return MDT_Keyboard; + case 4: + return MDT_Mouse; + case 5: + return MDT_TwinStick; + case 6: + return MDT_AsciiStick; + case 7: + return MDT_MaracasController; + case 8: + return MDT_FishingController; + case 9: + return MDT_PopnMusicController; + case 10: + return MDT_RacingController; + case 11: + return MDT_DenshaDeGoController; + case 12: + return MDT_Dreameye; + case 0: + default: + return MDT_None; + } +} + +static const char *maple_expansion_device_name(MapleDeviceType type) +{ + switch (type) + { + case MDT_SegaVMU: + return maple_expansion_device_types[1]; + case MDT_PurupuruPack: + return maple_expansion_device_types[2]; + case MDT_Microphone: + return maple_expansion_device_types[3]; + case MDT_None: + default: + return maple_expansion_device_types[0]; + } +} + +const char *maple_ports[] = { "None", "A", "B", "C", "D", "All" }; + +struct Mapping { + DreamcastKey key; + const char *name; +}; + +const Mapping dcButtons[] = { + { EMU_BTN_NONE, "Directions" }, + { DC_DPAD_UP, "Up" }, + { DC_DPAD_DOWN, "Down" }, + { DC_DPAD_LEFT, "Left" }, + { DC_DPAD_RIGHT, "Right" }, + + { DC_AXIS_UP, "Thumbstick Up" }, + { DC_AXIS_DOWN, "Thumbstick Down" }, + { DC_AXIS_LEFT, "Thumbstick Left" }, + { DC_AXIS_RIGHT, "Thumbstick Right" }, + + { DC_AXIS2_UP, "R.Thumbstick Up" }, + { DC_AXIS2_DOWN, "R.Thumbstick Down" }, + { DC_AXIS2_LEFT, "R.Thumbstick Left" }, + { DC_AXIS2_RIGHT, "R.Thumbstick Right" }, + + { DC_AXIS3_UP, "Axis 3 Up" }, + { DC_AXIS3_DOWN, "Axis 3 Down" }, + { DC_AXIS3_LEFT, "Axis 3 Left" }, + { DC_AXIS3_RIGHT, "Axis 3 Right" }, + + { DC_DPAD2_UP, "DPad2 Up" }, + { DC_DPAD2_DOWN, "DPad2 Down" }, + { DC_DPAD2_LEFT, "DPad2 Left" }, + { DC_DPAD2_RIGHT, "DPad2 Right" }, + + { EMU_BTN_NONE, "Buttons" }, + { DC_BTN_A, "A" }, + { DC_BTN_B, "B" }, + { DC_BTN_X, "X" }, + { DC_BTN_Y, "Y" }, + { DC_BTN_C, "C" }, + { DC_BTN_D, "D" }, + { DC_BTN_Z, "Z" }, + + { EMU_BTN_NONE, "Triggers" }, + { DC_AXIS_LT, "Left Trigger" }, + { DC_AXIS_RT, "Right Trigger" }, + { DC_AXIS_LT2, "Left Trigger 2" }, + { DC_AXIS_RT2, "Right Trigger 2" }, + + { EMU_BTN_NONE, "System Buttons" }, + { DC_BTN_START, "Start" }, + { DC_BTN_RELOAD, "Reload" }, + + { EMU_BTN_NONE, "Emulator" }, + { EMU_BTN_MENU, "Menu" }, + { EMU_BTN_ESCAPE, "Exit" }, + { EMU_BTN_FFORWARD, "Fast-forward" }, + { EMU_BTN_LOADSTATE, "Load State" }, + { EMU_BTN_SAVESTATE, "Save State" }, + { EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" }, + { EMU_BTN_SCREENSHOT, "Save Screenshot" }, + + { EMU_BTN_NONE, nullptr } +}; + +const Mapping arcadeButtons[] = { + { EMU_BTN_NONE, "Directions" }, + { DC_DPAD_UP, "Up" }, + { DC_DPAD_DOWN, "Down" }, + { DC_DPAD_LEFT, "Left" }, + { DC_DPAD_RIGHT, "Right" }, + + { DC_AXIS_UP, "Thumbstick Up" }, + { DC_AXIS_DOWN, "Thumbstick Down" }, + { DC_AXIS_LEFT, "Thumbstick Left" }, + { DC_AXIS_RIGHT, "Thumbstick Right" }, + + { DC_AXIS2_UP, "R.Thumbstick Up" }, + { DC_AXIS2_DOWN, "R.Thumbstick Down" }, + { DC_AXIS2_LEFT, "R.Thumbstick Left" }, + { DC_AXIS2_RIGHT, "R.Thumbstick Right" }, + + { EMU_BTN_NONE, "Buttons" }, + { DC_BTN_A, "Button 1" }, + { DC_BTN_B, "Button 2" }, + { DC_BTN_C, "Button 3" }, + { DC_BTN_X, "Button 4" }, + { DC_BTN_Y, "Button 5" }, + { DC_BTN_Z, "Button 6" }, + { DC_DPAD2_LEFT, "Button 7" }, + { DC_DPAD2_RIGHT, "Button 8" }, +// { DC_DPAD2_RIGHT, "Button 9" }, // TODO + + { EMU_BTN_NONE, "Triggers" }, + { DC_AXIS_LT, "Left Trigger" }, + { DC_AXIS_RT, "Right Trigger" }, + { DC_AXIS_LT2, "Left Trigger 2" }, + { DC_AXIS_RT2, "Right Trigger 2" }, + + { EMU_BTN_NONE, "System Buttons" }, + { DC_BTN_START, "Start" }, + { DC_BTN_RELOAD, "Reload" }, + { DC_BTN_D, "Coin" }, + { DC_DPAD2_UP, "Service" }, + { DC_DPAD2_DOWN, "Test" }, + { DC_BTN_INSERT_CARD, "Insert Card" }, + + { EMU_BTN_NONE, "Emulator" }, + { EMU_BTN_MENU, "Menu" }, + { EMU_BTN_ESCAPE, "Exit" }, + { EMU_BTN_FFORWARD, "Fast-forward" }, + { EMU_BTN_LOADSTATE, "Load State" }, + { EMU_BTN_SAVESTATE, "Save State" }, + { EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" }, + { EMU_BTN_SCREENSHOT, "Save Screenshot" }, + + { EMU_BTN_NONE, nullptr } +}; + +static MapleDeviceType maple_expansion_device_type_from_index(int idx) +{ + switch (idx) + { + case 1: + return MDT_SegaVMU; + case 2: + return MDT_PurupuruPack; + case 3: + return MDT_Microphone; + case 0: + default: + return MDT_None; + } +} + +static std::shared_ptr mapped_device; +static u32 mapped_code; +static bool analogAxis; +static bool positiveDirection; +static u64 map_start_time; +static bool arcade_button_mode; +static u32 gamepad_port; + +static void unmapControl(const std::shared_ptr& mapping, u32 gamepad_port, DreamcastKey key) +{ + mapping->clear_button(gamepad_port, key); + mapping->clear_axis(gamepad_port, key); +} + +static DreamcastKey getOppositeDirectionKey(DreamcastKey key) +{ + switch (key) + { + case DC_DPAD_UP: + return DC_DPAD_DOWN; + case DC_DPAD_DOWN: + return DC_DPAD_UP; + case DC_DPAD_LEFT: + return DC_DPAD_RIGHT; + case DC_DPAD_RIGHT: + return DC_DPAD_LEFT; + case DC_DPAD2_UP: + return DC_DPAD2_DOWN; + case DC_DPAD2_DOWN: + return DC_DPAD2_UP; + case DC_DPAD2_LEFT: + return DC_DPAD2_RIGHT; + case DC_DPAD2_RIGHT: + return DC_DPAD2_LEFT; + case DC_AXIS_UP: + return DC_AXIS_DOWN; + case DC_AXIS_DOWN: + return DC_AXIS_UP; + case DC_AXIS_LEFT: + return DC_AXIS_RIGHT; + case DC_AXIS_RIGHT: + return DC_AXIS_LEFT; + case DC_AXIS2_UP: + return DC_AXIS2_DOWN; + case DC_AXIS2_DOWN: + return DC_AXIS2_UP; + case DC_AXIS2_LEFT: + return DC_AXIS2_RIGHT; + case DC_AXIS2_RIGHT: + return DC_AXIS2_LEFT; + case DC_AXIS3_UP: + return DC_AXIS3_DOWN; + case DC_AXIS3_DOWN: + return DC_AXIS3_UP; + case DC_AXIS3_LEFT: + return DC_AXIS3_RIGHT; + case DC_AXIS3_RIGHT: + return DC_AXIS3_LEFT; + default: + return EMU_BTN_NONE; + } +} +static void detect_input_popup(const Mapping *mapping) +{ + ImVec2 padding = ScaledVec2(20, 20); + ImguiStyleVar _(ImGuiStyleVar_WindowPadding, padding); + ImguiStyleVar _1(ImGuiStyleVar_ItemSpacing, padding); + if (ImGui::BeginPopupModal("Map Control", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::Text("Waiting for control '%s'...", mapping->name); + u64 now = getTimeMs(); + ImGui::Text("Time out in %d s", (int)(5 - (now - map_start_time) / 1000)); + if (mapped_code != (u32)-1) + { + std::shared_ptr input_mapping = mapped_device->get_input_mapping(); + if (input_mapping != NULL) + { + unmapControl(input_mapping, gamepad_port, mapping->key); + if (analogAxis) + { + input_mapping->set_axis(gamepad_port, mapping->key, mapped_code, positiveDirection); + DreamcastKey opposite = getOppositeDirectionKey(mapping->key); + // Map the axis opposite direction to the corresponding opposite dc button or axis, + // but only if the opposite direction axis isn't used and the dc button or axis isn't mapped. + if (opposite != EMU_BTN_NONE + && input_mapping->get_axis_id(gamepad_port, mapped_code, !positiveDirection) == EMU_BTN_NONE + && input_mapping->get_axis_code(gamepad_port, opposite).first == (u32)-1 + && input_mapping->get_button_code(gamepad_port, opposite) == (u32)-1) + input_mapping->set_axis(gamepad_port, opposite, mapped_code, !positiveDirection); + } + else + input_mapping->set_button(gamepad_port, mapping->key, mapped_code); + } + mapped_device = NULL; + ImGui::CloseCurrentPopup(); + } + else if (now - map_start_time >= 5000) + { + mapped_device = NULL; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +static void displayLabelOrCode(const char *label, u32 code, const char *suffix = "") +{ + if (label != nullptr) + ImGui::Text("%s%s", label, suffix); + else + ImGui::Text("[%d]%s", code, suffix); +} + +static void displayMappedControl(const std::shared_ptr& gamepad, DreamcastKey key) +{ + std::shared_ptr input_mapping = gamepad->get_input_mapping(); + u32 code = input_mapping->get_button_code(gamepad_port, key); + if (code != (u32)-1) + { + displayLabelOrCode(gamepad->get_button_name(code), code); + return; + } + std::pair pair = input_mapping->get_axis_code(gamepad_port, key); + code = pair.first; + if (code != (u32)-1) + { + displayLabelOrCode(gamepad->get_axis_name(code), code, pair.second ? "+" : "-"); + return; + } +} + +static void controller_mapping_popup(const std::shared_ptr& gamepad) +{ + fullScreenWindow(true); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + if (ImGui::BeginPopupModal("Controller Mapping", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + const ImGuiStyle& style = ImGui::GetStyle(); + const float winWidth = ImGui::GetIO().DisplaySize.x - insetLeft - insetRight - (style.WindowBorderSize + style.WindowPadding.x) * 2; + const float col_width = (winWidth - style.GrabMinSize - style.ItemSpacing.x + - (ImGui::CalcTextSize("Map").x + style.FramePadding.x * 2.0f + style.ItemSpacing.x) + - (ImGui::CalcTextSize("Unmap").x + style.FramePadding.x * 2.0f + style.ItemSpacing.x)) / 2; + + static int map_system; + static int item_current_map_idx = 0; + static int last_item_current_map_idx = 2; + + std::shared_ptr input_mapping = gamepad->get_input_mapping(); + if (input_mapping == NULL || ImGui::Button("Done", ScaledVec2(100, 30))) + { + ImGui::CloseCurrentPopup(); + gamepad->save_mapping(map_system); + last_item_current_map_idx = 2; + ImGui::EndPopup(); + return; + } + ImGui::SetItemDefaultFocus(); + + float portWidth = 0; + if (gamepad->maple_port() == MAPLE_PORTS) + { + ImGui::SameLine(); + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, (uiScaled(30) - ImGui::GetFontSize()) / 2)); + portWidth = ImGui::CalcTextSize("AA").x + ImGui::GetStyle().ItemSpacing.x * 2.0f + ImGui::GetFontSize(); + ImGui::SetNextItemWidth(portWidth); + if (ImGui::BeginCombo("Port", maple_ports[gamepad_port + 1])) + { + for (u32 j = 0; j < MAPLE_PORTS; j++) + { + bool is_selected = gamepad_port == j; + if (ImGui::Selectable(maple_ports[j + 1], &is_selected)) + gamepad_port = j; + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + portWidth += ImGui::CalcTextSize("Port").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x; + } + float comboWidth = ImGui::CalcTextSize("Dreamcast Controls").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.x * 4; + float gameConfigWidth = 0; + if (!settings.content.gameId.empty()) + gameConfigWidth = ImGui::CalcTextSize(gamepad->isPerGameMapping() ? "Delete Game Config" : "Make Game Config").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SameLine(0, ImGui::GetContentRegionAvail().x - comboWidth - gameConfigWidth - ImGui::GetStyle().ItemSpacing.x - uiScaled(100) * 2 - portWidth); + + ImGui::AlignTextToFramePadding(); + + if (!settings.content.gameId.empty()) + { + if (gamepad->isPerGameMapping()) + { + if (ImGui::Button("Delete Game Config", ScaledVec2(0, 30))) + { + gamepad->setPerGameMapping(false); + if (!gamepad->find_mapping(map_system)) + gamepad->resetMappingToDefault(arcade_button_mode, true); + } + } + else + { + if (ImGui::Button("Make Game Config", ScaledVec2(0, 30))) + gamepad->setPerGameMapping(true); + } + ImGui::SameLine(); + } + if (ImGui::Button("Reset...", ScaledVec2(100, 30))) + ImGui::OpenPopup("Confirm Reset"); + + { + ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ScaledVec2(20, 20)); + if (ImGui::BeginPopupModal("Confirm Reset", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::Text("Are you sure you want to reset the mappings to default?"); + static bool hitbox; + if (arcade_button_mode) + { + ImGui::Text("Controller Type:"); + if (ImGui::RadioButton("Gamepad", !hitbox)) + hitbox = false; + ImGui::SameLine(); + if (ImGui::RadioButton("Arcade / Hit Box", hitbox)) + hitbox = true; + } + ImGui::NewLine(); + ImguiStyleVar _(ImGuiStyleVar_ItemSpacing, ImVec2(uiScaled(20), ImGui::GetStyle().ItemSpacing.y)); + ImguiStyleVar _1(ImGuiStyleVar_FramePadding, ScaledVec2(10, 10)); + if (ImGui::Button("Yes")) + { + gamepad->resetMappingToDefault(arcade_button_mode, !hitbox); + gamepad->save_mapping(map_system); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No")) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); + } + } + + ImGui::SameLine(); + + const char* items[] = { "Dreamcast Controls", "Arcade Controls" }; + + if (last_item_current_map_idx == 2 && game_started) + // Select the right mappings for the current game + item_current_map_idx = settings.platform.isArcade() ? 1 : 0; + + // Here our selection data is an index. + + ImGui::SetNextItemWidth(comboWidth); + // Make the combo height the same as the Done and Reset buttons + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, (uiScaled(30) - ImGui::GetFontSize()) / 2)); + ImGui::Combo("##arcadeMode", &item_current_map_idx, items, IM_ARRAYSIZE(items)); + ImGui::PopStyleVar(); + if (last_item_current_map_idx != 2 && item_current_map_idx != last_item_current_map_idx) + { + gamepad->save_mapping(map_system); + } + const Mapping *systemMapping = dcButtons; + if (item_current_map_idx == 0) + { + arcade_button_mode = false; + map_system = DC_PLATFORM_DREAMCAST; + systemMapping = dcButtons; + } + else if (item_current_map_idx == 1) + { + arcade_button_mode = true; + map_system = DC_PLATFORM_NAOMI; + systemMapping = arcadeButtons; + } + + if (item_current_map_idx != last_item_current_map_idx) + { + if (!gamepad->find_mapping(map_system)) + if (map_system == DC_PLATFORM_DREAMCAST || !gamepad->find_mapping(DC_PLATFORM_DREAMCAST)) + gamepad->resetMappingToDefault(arcade_button_mode, true); + input_mapping = gamepad->get_input_mapping(); + + last_item_current_map_idx = item_current_map_idx; + } + + char key_id[32]; + + ImGui::BeginChild(ImGui::GetID("buttons"), ImVec2(0, 0), ImGuiChildFlags_FrameStyle, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); + + for (; systemMapping->name != nullptr; systemMapping++) + { + if (systemMapping->key == EMU_BTN_NONE) + { + ImGui::Columns(1, nullptr, false); + header(systemMapping->name); + ImGui::Columns(3, "bindings", false); + ImGui::SetColumnWidth(0, col_width); + ImGui::SetColumnWidth(1, col_width); + continue; + } + sprintf(key_id, "key_id%d", systemMapping->key); + ImguiID _(key_id); + + const char *game_btn_name = nullptr; + if (arcade_button_mode) + { + game_btn_name = GetCurrentGameButtonName(systemMapping->key); + if (game_btn_name == nullptr) + game_btn_name = GetCurrentGameAxisName(systemMapping->key); + } + if (game_btn_name != nullptr && game_btn_name[0] != '\0') + ImGui::Text("%s - %s", systemMapping->name, game_btn_name); + else + ImGui::Text("%s", systemMapping->name); + + ImGui::NextColumn(); + displayMappedControl(gamepad, systemMapping->key); + + ImGui::NextColumn(); + if (ImGui::Button("Map")) + { + map_start_time = getTimeMs(); + ImGui::OpenPopup("Map Control"); + mapped_device = gamepad; + mapped_code = -1; + gamepad->detectButtonOrAxisInput([](u32 code, bool analog, bool positive) + { + mapped_code = code; + analogAxis = analog; + positiveDirection = positive; + }); + } + detect_input_popup(systemMapping); + ImGui::SameLine(); + if (ImGui::Button("Unmap")) + { + input_mapping = gamepad->get_input_mapping(); + unmapControl(input_mapping, gamepad_port, systemMapping->key); + } + ImGui::NextColumn(); + } + ImGui::Columns(1, nullptr, false); + scrollWhenDraggingOnVoid(); + windowDragScroll(); + + ImGui::EndChild(); + error_popup(); + ImGui::EndPopup(); + } +} + +static void gamepadSettingsPopup(const std::shared_ptr& gamepad) +{ + centerNextWindow(); + ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(450.f, 300.f))); + + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + if (ImGui::Button("Done", ScaledVec2(100, 30))) + { + gamepad->save_mapping(); + // Update both console and arcade profile/mapping + int rumblePower = gamepad->get_rumble_power(); + float deadzone = gamepad->get_dead_zone(); + float saturation = gamepad->get_saturation(); + int otherPlatform = settings.platform.isConsole() ? DC_PLATFORM_NAOMI : DC_PLATFORM_DREAMCAST; + if (!gamepad->find_mapping(otherPlatform)) + if (otherPlatform == DC_PLATFORM_DREAMCAST || !gamepad->find_mapping(DC_PLATFORM_DREAMCAST)) + gamepad->resetMappingToDefault(otherPlatform != DC_PLATFORM_DREAMCAST, true); + std::shared_ptr mapping = gamepad->get_input_mapping(); + if (mapping != nullptr) + { + if (gamepad->is_rumble_enabled() && rumblePower != mapping->rumblePower) { + mapping->rumblePower = rumblePower; + mapping->set_dirty(); + } + if (gamepad->has_analog_stick()) + { + if (deadzone != mapping->dead_zone) { + mapping->dead_zone = deadzone; + mapping->set_dirty(); + } + if (saturation != mapping->saturation) { + mapping->saturation = saturation; + mapping->set_dirty(); + } + } + if (mapping->is_dirty()) + gamepad->save_mapping(otherPlatform); + } + gamepad->find_mapping(); + + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return; + } + ImGui::NewLine(); + if (gamepad->is_virtual_gamepad()) + { + header("Haptic"); + OptionSlider("Power", config::VirtualGamepadVibration, 0, 60, "Haptic feedback power"); + } + else if (gamepad->is_rumble_enabled()) + { + header("Rumble"); + int power = gamepad->get_rumble_power(); + ImGui::SetNextItemWidth(uiScaled(300)); + if (ImGui::SliderInt("Power", &power, 0, 100, "%d%%")) + gamepad->set_rumble_power(power); + ImGui::SameLine(); + ShowHelpMarker("Rumble power"); + } + if (gamepad->has_analog_stick()) + { + header("Thumbsticks"); + int deadzone = std::round(gamepad->get_dead_zone() * 100.f); + ImGui::SetNextItemWidth(uiScaled(300)); + if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%")) + gamepad->set_dead_zone(deadzone / 100.f); + ImGui::SameLine(); + ShowHelpMarker("Minimum deflection to register as input"); + int saturation = std::round(gamepad->get_saturation() * 100.f); + ImGui::SetNextItemWidth(uiScaled(300)); + if (ImGui::SliderInt("Saturation", &saturation, 50, 200, "%d%%")) + gamepad->set_saturation(saturation / 100.f); + ImGui::SameLine(); + ShowHelpMarker("Value sent to the game at 100% thumbstick deflection. " + "Values greater than 100% will saturate before full deflection of the thumbstick."); + } + ImGui::EndPopup(); + } +} + +void error_popup() +{ + if (!error_msg_shown && !error_msg.empty()) + { + ImVec2 padding = ScaledVec2(20, 20); + ImguiStyleVar _(ImGuiStyleVar_WindowPadding, padding); + ImguiStyleVar _1(ImGuiStyleVar_ItemSpacing, padding); + ImGui::OpenPopup("Error"); + if (ImGui::BeginPopupModal("Error", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar)) + { + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + uiScaled(400.f)); + ImGui::TextWrapped("%s", error_msg.c_str()); + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(16, 3)); + float currentwidth = ImGui::GetContentRegionAvail().x; + ImGui::SetCursorPosX((currentwidth - uiScaled(80.f)) / 2.f + ImGui::GetStyle().WindowPadding.x); + if (ImGui::Button("OK", ScaledVec2(80.f, 0))) + { + error_msg.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SetItemDefaultFocus(); + ImGui::PopTextWrapPos(); + ImGui::EndPopup(); + } + error_msg_shown = true; + } +} + +static void contentpath_warning_popup() +{ + static bool show_contentpath_selection; + + if (scanner.content_path_looks_incorrect) + { + ImGui::OpenPopup("Incorrect Content Location?"); + if (ImGui::BeginPopupModal("Incorrect Content Location?", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + uiScaled(400.f)); + ImGui::TextWrapped(" Scanned %d folders but no game can be found! ", scanner.empty_folders_scanned); + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(16, 3)); + float currentwidth = ImGui::GetContentRegionAvail().x; + ImGui::SetCursorPosX((currentwidth - uiScaled(100.f)) / 2.f + ImGui::GetStyle().WindowPadding.x - uiScaled(55.f)); + if (ImGui::Button("Reselect", ScaledVec2(100.f, 0))) + { + scanner.content_path_looks_incorrect = false; + ImGui::CloseCurrentPopup(); + show_contentpath_selection = true; + } + + ImGui::SameLine(); + ImGui::SetCursorPosX((currentwidth - uiScaled(100.f)) / 2.f + ImGui::GetStyle().WindowPadding.x + uiScaled(55.f)); + if (ImGui::Button("Cancel", ScaledVec2(100.f, 0))) + { + scanner.content_path_looks_incorrect = false; + ImGui::CloseCurrentPopup(); + scanner.stop(); + config::ContentPath.get().clear(); + } + ImGui::SetItemDefaultFocus(); + ImGui::EndPopup(); + } + } + if (show_contentpath_selection) + { + scanner.stop(); + ImGui::OpenPopup("Select Directory"); + select_file_popup("Select Directory", [](bool cancelled, std::string selection) + { + show_contentpath_selection = false; + if (!cancelled) + { + config::ContentPath.get().clear(); + config::ContentPath.get().push_back(selection); + } + scanner.refresh(); + return true; + }); + } +} + +static void gui_debug_tab() +{ + header("Logging"); + { + LogManager *logManager = LogManager::GetInstance(); + for (LogTypes::LOG_TYPE type = LogTypes::AICA; type < LogTypes::NUMBER_OF_LOGS; type = (LogTypes::LOG_TYPE)(type + 1)) + { + bool enabled = logManager->IsEnabled(type, logManager->GetLogLevel()); + std::string name = std::string(logManager->GetShortName(type)) + " - " + logManager->GetFullName(type); + if (ImGui::Checkbox(name.c_str(), &enabled) && logManager->GetLogLevel() > LogTypes::LWARNING) { + logManager->SetEnable(type, enabled); + cfgSaveBool("log", logManager->GetShortName(type), enabled); + } + } + ImGui::Spacing(); + + static const char *levels[] = { "Notice", "Error", "Warning", "Info", "Debug" }; + if (ImGui::BeginCombo("Log Verbosity", levels[logManager->GetLogLevel() - 1], ImGuiComboFlags_None)) + { + for (std::size_t i = 0; i < std::size(levels); i++) + { + bool is_selected = logManager->GetLogLevel() - 1 == (int)i; + if (ImGui::Selectable(levels[i], &is_selected)) { + logManager->SetLogLevel((LogTypes::LOG_LEVELS)(i + 1)); + cfgSaveInt("log", "Verbosity", i + 1); + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } +#if FC_PROFILER + ImGui::Spacing(); + header("Profiling"); + { + + OptionCheckbox("Enable", config::ProfilerEnabled, "Enable the profiler."); + if (!config::ProfilerEnabled) + { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + } + OptionCheckbox("Display", config::ProfilerDrawToGUI, "Draw the profiler output in an overlay."); + OptionCheckbox("Output to terminal", config::ProfilerOutputTTY, "Write the profiler output to the terminal"); + // TODO frame warning time + if (!config::ProfilerEnabled) + { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); + } + } +#endif +} + +static void addContentPath(const std::string& path) +{ + auto& contentPath = config::ContentPath.get(); + if (std::count(contentPath.begin(), contentPath.end(), path) == 0) + { + scanner.stop(); + contentPath.push_back(path); + scanner.refresh(); + } +} + +static float calcComboWidth(const char *biggestLabel) { + return ImGui::CalcTextSize(biggestLabel).x + ImGui::GetStyle().FramePadding.x * 2.0f + ImGui::GetFrameHeight(); +} + +static void gui_settings_general() +{ + { + DisabledScope scope(settings.platform.isArcade()); + + const char *languages[] = { "Japanese", "English", "German", "French", "Spanish", "Italian", "Default" }; + OptionComboBox("Language", config::Language, languages, std::size(languages), + "The language as configured in the Dreamcast BIOS"); + + const char *broadcast[] = { "NTSC", "PAL", "PAL/M", "PAL/N", "Default" }; + OptionComboBox("Broadcast", config::Broadcast, broadcast, std::size(broadcast), + "TV broadcasting standard for non-VGA modes"); + } + + const char *consoleRegion[] = { "Japan", "USA", "Europe", "Default" }; + const char *arcadeRegion[] = { "Japan", "USA", "Export", "Korea" }; + const char **region = settings.platform.isArcade() ? arcadeRegion : consoleRegion; + OptionComboBox("Region", config::Region, region, std::size(consoleRegion), + "BIOS region"); + + const char *cable[] = { "VGA", "RGB Component", "TV Composite" }; + { + DisabledScope scope(config::Cable.isReadOnly() || settings.platform.isArcade()); + + const char *value = config::Cable == 0 ? cable[0] + : config::Cable > 0 && config::Cable <= (int)std::size(cable) ? cable[config::Cable - 1] + : "?"; + if (ImGui::BeginCombo("Cable", value, ImGuiComboFlags_None)) + { + for (int i = 0; i < IM_ARRAYSIZE(cable); i++) + { + bool is_selected = i == 0 ? config::Cable <= 1 : config::Cable - 1 == i; + if (ImGui::Selectable(cable[i], &is_selected)) + config::Cable = i == 0 ? 0 : i + 1; + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + ShowHelpMarker("Video connection type"); + } + +#if !defined(TARGET_IPHONE) + ImVec2 size; + size.x = 0.0f; + size.y = (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().FramePadding.y * 2.f) + * (config::ContentPath.get().size() + 1) ;//+ ImGui::GetStyle().FramePadding.y * 2.f; + + if (BeginListBox("Content Location", size, ImGuiWindowFlags_NavFlattened)) + { + int to_delete = -1; + for (u32 i = 0; i < config::ContentPath.get().size(); i++) + { + ImguiID _(config::ContentPath.get()[i].c_str()); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", config::ContentPath.get()[i].c_str()); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("X").x - ImGui::GetStyle().FramePadding.x); + if (ImGui::Button("X")) + to_delete = i; + } + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(24, 3)); +#ifdef __ANDROID__ + if (ImGui::Button("Add")) + { + hostfs::addStorage(true, false, [](bool cancelled, std::string selection) { + if (!cancelled) + addContentPath(selection); + }); + } +#else + if (ImGui::Button("Add")) + ImGui::OpenPopup("Select Directory"); + select_file_popup("Select Directory", [](bool cancelled, std::string selection) { + if (!cancelled) + addContentPath(selection); + return true; + }); +#endif + ImGui::SameLine(); + if (ImGui::Button("Rescan Content")) + scanner.refresh(); + scrollWhenDraggingOnVoid(); + + ImGui::EndListBox(); + if (to_delete >= 0) + { + scanner.stop(); + config::ContentPath.get().erase(config::ContentPath.get().begin() + to_delete); + scanner.refresh(); + } + } + ImGui::SameLine(); + ShowHelpMarker("The directories where your games are stored"); + + size.y = ImGui::GetTextLineHeightWithSpacing() * 1.25f + ImGui::GetStyle().FramePadding.y * 2.0f; + +#if defined(__linux__) && !defined(__ANDROID__) + if (BeginListBox("Data Directory", size, ImGuiWindowFlags_NavFlattened)) + { + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", get_writable_data_path("").c_str()); + ImGui::EndListBox(); + } + ImGui::SameLine(); + ShowHelpMarker("The directory containing BIOS files, as well as saved VMUs and states"); +#else + if (BeginListBox("Home Directory", size, ImGuiWindowFlags_NavFlattened)) + { + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", get_writable_config_path("").c_str()); +#ifdef __ANDROID__ + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Change").x - ImGui::GetStyle().FramePadding.x); + if (ImGui::Button("Change")) + gui_setState(GuiState::Onboarding); +#endif +#ifdef TARGET_MAC + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Reveal in Finder").x - ImGui::GetStyle().FramePadding.x); + if (ImGui::Button("Reveal in Finder")) + { + char temp[512]; + sprintf(temp, "open \"%s\"", get_writable_config_path("").c_str()); + system(temp); + } +#endif + ImGui::EndListBox(); + } + ImGui::SameLine(); + ShowHelpMarker("The directory where Flycast saves configuration files and VMUs. BIOS files should be in a subfolder named \"data\""); +#endif // !linux +#endif // !TARGET_IPHONE + + OptionCheckbox("Box Art Game List", config::BoxartDisplayMode, + "Display game cover art in the game list."); + OptionCheckbox("Fetch Box Art", config::FetchBoxart, + "Fetch cover images from TheGamesDB.net."); + if (OptionSlider("UI Scaling", config::UIScaling, 50, 200, "Adjust the size of UI elements and fonts.", "%d%%")) + uiUserScaleUpdated = true; + if (uiUserScaleUpdated) + { + ImGui::SameLine(); + if (ImGui::Button("Apply")) { + mainui_reinit(); + uiUserScaleUpdated = false; + } + } + + if (OptionCheckbox("Hide Legacy Naomi Roms", config::HideLegacyNaomiRoms, + "Hide .bin, .dat and .lst files from the content browser")) + scanner.refresh(); + ImGui::Text("Automatic State:"); + OptionCheckbox("Load", config::AutoLoadState, + "Load the last saved state of the game when starting"); + ImGui::SameLine(); + OptionCheckbox("Save", config::AutoSaveState, + "Save the state of the game when stopping"); + OptionCheckbox("Naomi Free Play", config::ForceFreePlay, "Configure Naomi games in Free Play mode."); +#if USE_DISCORD + OptionCheckbox("Discord Presence", config::DiscordPresence, "Show which game you are playing on Discord"); +#endif +#ifdef USE_RACHIEVEMENTS + OptionCheckbox("Enable RetroAchievements", config::EnableAchievements, "Track your game achievements using RetroAchievements.org"); + { + DisabledScope _(!config::EnableAchievements); + ImGui::Indent(); + OptionCheckbox("Hardcore Mode", config::AchievementsHardcoreMode, + "Enable RetroAchievements hardcore mode. Using cheats and loading a state are not allowed in this mode."); + ImGui::InputText("Username", &config::AchievementsUserName.get(), + achievements::isLoggedOn() ? ImGuiInputTextFlags_ReadOnly : ImGuiInputTextFlags_None, nullptr, nullptr); + if (config::EnableAchievements) + { + static std::future futureLogin; + achievements::init(); + if (achievements::isLoggedOn()) + { + ImGui::Text("Authentication successful"); + if (futureLogin.valid()) + futureLogin.get(); + if (ImGui::Button("Logout", ScaledVec2(100, 0))) + achievements::logout(); + } + else + { + static char password[256]; + ImGui::InputText("Password", password, sizeof(password), ImGuiInputTextFlags_Password, nullptr, nullptr); + if (futureLogin.valid()) + { + if (futureLogin.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) { + ImGui::Text("Authenticating..."); + } + else + { + try { + futureLogin.get(); + } catch (const FlycastException& e) { + gui_error(e.what()); + } + } + } + if (ImGui::Button("Login", ScaledVec2(100, 0)) && !futureLogin.valid()) + { + futureLogin = achievements::login(config::AchievementsUserName.get().c_str(), password); + memset(password, 0, sizeof(password)); + } + } + } + ImGui::Unindent(); + } +#endif +} + +static void gui_settings_controls(bool& maple_devices_changed) +{ + header("Physical Devices"); + { + if (ImGui::BeginTable("physicalDevices", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings)) + { + ImGui::TableSetupColumn("System", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); + + const float portComboWidth = calcComboWidth("None"); + const ImVec4 gray{ 0.5f, 0.5f, 0.5f, 1.f }; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(gray, "System"); + + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(gray, "Name"); + + ImGui::TableSetColumnIndex(2); + ImGui::TextColored(gray, "Port"); + + for (int i = 0; i < GamepadDevice::GetGamepadCount(); i++) + { + std::shared_ptr gamepad = GamepadDevice::GetGamepad(i); + if (!gamepad) + continue; + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", gamepad->api_name().c_str()); + + ImGui::TableSetColumnIndex(1); + ImGui::Text("%s", gamepad->name().c_str()); + + ImGui::TableSetColumnIndex(2); + char port_name[32]; + sprintf(port_name, "##mapleport%d", i); + ImguiID _(port_name); + ImGui::SetNextItemWidth(portComboWidth); + if (ImGui::BeginCombo(port_name, maple_ports[gamepad->maple_port() + 1])) + { + for (int j = -1; j < (int)std::size(maple_ports) - 1; j++) + { + bool is_selected = gamepad->maple_port() == j; + if (ImGui::Selectable(maple_ports[j + 1], &is_selected)) + gamepad->set_maple_port(j); + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + } + + ImGui::TableSetColumnIndex(3); + ImGui::SameLine(0, uiScaled(8)); + if (gamepad->remappable() && ImGui::Button("Map")) + { + gamepad_port = 0; + ImGui::OpenPopup("Controller Mapping"); + } + + controller_mapping_popup(gamepad); + +#ifdef __ANDROID__ + if (gamepad->is_virtual_gamepad()) + { + if (ImGui::Button("Edit Layout")) + { + vjoy_start_editing(); + gui_setState(GuiState::VJoyEdit); + } + } +#endif + if (gamepad->is_rumble_enabled() || gamepad->has_analog_stick() +#ifdef __ANDROID__ + || gamepad->is_virtual_gamepad() +#endif + ) + { + ImGui::SameLine(0, uiScaled(16)); + if (ImGui::Button("Settings")) + ImGui::OpenPopup("Gamepad Settings"); + gamepadSettingsPopup(gamepad); + } + } + ImGui::EndTable(); + } + } + + ImGui::Spacing(); + OptionSlider("Mouse sensitivity", config::MouseSensitivity, 1, 500); +#if defined(_WIN32) && !defined(TARGET_UWP) + OptionCheckbox("Use Raw Input", config::UseRawInput, "Supports multiple pointing devices (mice, light guns) and keyboards"); +#endif + + ImGui::Spacing(); + header("Dreamcast Devices"); + { + bool is_there_any_xhair = false; + if (ImGui::BeginTable("dreamcastDevices", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings, + ImVec2(0, 0), uiScaled(8))) + { + const float mainComboWidth = calcComboWidth(maple_device_types[11]); // densha de go! controller + const float expComboWidth = calcComboWidth(maple_expansion_device_types[2]); // vibration pack + + for (int bus = 0; bus < MAPLE_PORTS; bus++) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Port %c", bus + 'A'); + + ImGui::TableSetColumnIndex(1); + char device_name[32]; + sprintf(device_name, "##device%d", bus); + float w = ImGui::CalcItemWidth() / 3; + ImGui::PushItemWidth(w); + ImGui::SetNextItemWidth(mainComboWidth); + if (ImGui::BeginCombo(device_name, maple_device_name(config::MapleMainDevices[bus]), ImGuiComboFlags_None)) + { + for (int i = 0; i < IM_ARRAYSIZE(maple_device_types); i++) + { + bool is_selected = config::MapleMainDevices[bus] == maple_device_type_from_index(i); + if (ImGui::Selectable(maple_device_types[i], &is_selected)) + { + config::MapleMainDevices[bus] = maple_device_type_from_index(i); + maple_devices_changed = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + int port_count = 0; + switch (config::MapleMainDevices[bus]) { + case MDT_SegaController: + port_count = 2; + break; + case MDT_LightGun: + case MDT_TwinStick: + case MDT_AsciiStick: + case MDT_RacingController: + port_count = 1; + break; + default: break; + } + for (int port = 0; port < port_count; port++) + { + ImGui::TableSetColumnIndex(2 + port); + sprintf(device_name, "##device%d.%d", bus, port + 1); + ImguiID _(device_name); + ImGui::SetNextItemWidth(expComboWidth); + if (ImGui::BeginCombo(device_name, maple_expansion_device_name(config::MapleExpansionDevices[bus][port]), ImGuiComboFlags_None)) + { + for (int i = 0; i < IM_ARRAYSIZE(maple_expansion_device_types); i++) + { + bool is_selected = config::MapleExpansionDevices[bus][port] == maple_expansion_device_type_from_index(i); + if (ImGui::Selectable(maple_expansion_device_types[i], &is_selected)) + { + config::MapleExpansionDevices[bus][port] = maple_expansion_device_type_from_index(i); + maple_devices_changed = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + if (config::MapleMainDevices[bus] == MDT_LightGun) + { + ImGui::TableSetColumnIndex(3); + sprintf(device_name, "##device%d.xhair", bus); + ImguiID _(device_name); + u32 color = config::CrosshairColor[bus]; + float xhairColor[4] { + (color & 0xff) / 255.f, + ((color >> 8) & 0xff) / 255.f, + ((color >> 16) & 0xff) / 255.f, + ((color >> 24) & 0xff) / 255.f + }; + bool colorChanged = ImGui::ColorEdit4("Crosshair color", xhairColor, ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf + | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoLabel); + ImGui::SameLine(); + bool enabled = color != 0; + if (ImGui::Checkbox("Crosshair", &enabled) || colorChanged) + { + if (enabled) + { + config::CrosshairColor[bus] = (u8)(std::round(xhairColor[0] * 255.f)) + | ((u8)(std::round(xhairColor[1] * 255.f)) << 8) + | ((u8)(std::round(xhairColor[2] * 255.f)) << 16) + | ((u8)(std::round(xhairColor[3] * 255.f)) << 24); + if (config::CrosshairColor[bus] == 0) + config::CrosshairColor[bus] = 0xC0FFFFFF; + } + else + { + config::CrosshairColor[bus] = 0; + } + } + is_there_any_xhair |= enabled; + } + ImGui::PopItemWidth(); + } + ImGui::EndTable(); + } + { + DisabledScope scope(!is_there_any_xhair); + OptionSlider("Crosshair Size", config::CrosshairSize, 10, 100); + } + OptionCheckbox("Per Game VMU A1", config::PerGameVmu, "When enabled, each game has its own VMU on port 1 of controller A."); + } +} + +static void gui_settings_video() +{ + int renderApi; + bool perPixel; + switch (config::RendererType) + { + default: + case RenderType::OpenGL: + renderApi = 0; + perPixel = false; + break; + case RenderType::OpenGL_OIT: + renderApi = 0; + perPixel = true; + break; + case RenderType::Vulkan: + renderApi = 1; + perPixel = false; + break; + case RenderType::Vulkan_OIT: + renderApi = 1; + perPixel = true; + break; + case RenderType::DirectX9: + renderApi = 2; + perPixel = false; + break; + case RenderType::DirectX11: + renderApi = 3; + perPixel = false; + break; + case RenderType::DirectX11_OIT: + renderApi = 3; + perPixel = true; + break; + } + + constexpr int apiCount = 0 + #ifdef USE_VULKAN + + 1 + #endif + #ifdef USE_DX9 + + 1 + #endif + #ifdef USE_OPENGL + + 1 + #endif + #ifdef USE_DX11 + + 1 + #endif + ; + + float innerSpacing = ImGui::GetStyle().ItemInnerSpacing.x; + if (apiCount > 1) + { + header("Graphics API"); + { + ImGui::Columns(apiCount, "renderApi", false); +#ifdef USE_OPENGL + ImGui::RadioButton("OpenGL", &renderApi, 0); + ImGui::NextColumn(); +#endif +#ifdef USE_VULKAN +#ifdef __APPLE__ + ImGui::RadioButton("Vulkan (Metal)", &renderApi, 1); + ImGui::SameLine(0, innerSpacing); + ShowHelpMarker("MoltenVK: An implementation of Vulkan that runs on Apple's Metal graphics framework"); +#else + ImGui::RadioButton("Vulkan", &renderApi, 1); +#endif // __APPLE__ + ImGui::NextColumn(); +#endif +#ifdef USE_DX9 + ImGui::RadioButton("DirectX 9", &renderApi, 2); + ImGui::NextColumn(); +#endif +#ifdef USE_DX11 + ImGui::RadioButton("DirectX 11", &renderApi, 3); + ImGui::NextColumn(); +#endif + ImGui::Columns(1, nullptr, false); + } + } + header("Transparent Sorting"); + { + const bool has_per_pixel = GraphicsContext::Instance()->hasPerPixel(); + int renderer = perPixel ? 2 : config::PerStripSorting ? 1 : 0; + ImGui::Columns(has_per_pixel ? 3 : 2, "renderers", false); + ImGui::RadioButton("Per Triangle", &renderer, 0); + ImGui::SameLine(); + ShowHelpMarker("Sort transparent polygons per triangle. Fast but may produce graphical glitches"); + ImGui::NextColumn(); + ImGui::RadioButton("Per Strip", &renderer, 1); + ImGui::SameLine(); + ShowHelpMarker("Sort transparent polygons per strip. Faster but may produce graphical glitches"); + if (has_per_pixel) + { + ImGui::NextColumn(); + ImGui::RadioButton("Per Pixel", &renderer, 2); + ImGui::SameLine(); + ShowHelpMarker("Sort transparent polygons per pixel. Slower but accurate"); + } + ImGui::Columns(1, NULL, false); + switch (renderer) + { + case 0: + perPixel = false; + config::PerStripSorting.set(false); + break; + case 1: + perPixel = false; + config::PerStripSorting.set(true); + break; + case 2: + perPixel = true; + break; + } + } + ImGui::Spacing(); + + header("Rendering Options"); + { + const std::array scalings{ 0.5f, 1.f, 1.5f, 2.f, 2.5f, 3.f, 4.f, 4.5f, 5.f, 6.f, 7.f, 8.f, 9.f }; + const std::array scalingsText{ "Half", "Native", "x1.5", "x2", "x2.5", "x3", "x4", "x4.5", "x5", "x6", "x7", "x8", "x9" }; + std::array vres; + std::array resLabels; + u32 selected = 0; + for (u32 i = 0; i < scalings.size(); i++) + { + vres[i] = scalings[i] * 480; + if (vres[i] == config::RenderResolution) + selected = i; + if (!config::Widescreen) + resLabels[i] = std::to_string((int)(scalings[i] * 640)) + "x" + std::to_string((int)(scalings[i] * 480)); + else + resLabels[i] = std::to_string((int)(scalings[i] * 480 * 16 / 9)) + "x" + std::to_string((int)(scalings[i] * 480)); + resLabels[i] += " (" + scalingsText[i] + ")"; + } + + ImGui::PushItemWidth(ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f); + if (ImGui::BeginCombo("##Resolution", resLabels[selected].c_str(), ImGuiComboFlags_NoArrowButton)) + { + for (u32 i = 0; i < scalings.size(); i++) + { + bool is_selected = vres[i] == config::RenderResolution; + if (ImGui::Selectable(resLabels[i].c_str(), is_selected)) + config::RenderResolution = vres[i]; + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + ImGui::SameLine(0, innerSpacing); + + if (ImGui::ArrowButton("##Decrease Res", ImGuiDir_Left)) + { + if (selected > 0) + config::RenderResolution = vres[selected - 1]; + } + ImGui::SameLine(0, innerSpacing); + if (ImGui::ArrowButton("##Increase Res", ImGuiDir_Right)) + { + if (selected < vres.size() - 1) + config::RenderResolution = vres[selected + 1]; + } + ImGui::SameLine(0, innerSpacing); + + ImGui::Text("Internal Resolution"); + ImGui::SameLine(); + ShowHelpMarker("Internal render resolution. Higher is better, but more demanding on the GPU. Values higher than your display resolution (but no more than double your display resolution) can be used for supersampling, which provides high-quality antialiasing without reducing sharpness."); + +#ifndef TARGET_IPHONE + OptionCheckbox("VSync", config::VSync, "Synchronizes the frame rate with the screen refresh rate. Recommended"); + if (isVulkan(config::RendererType)) + { + ImGui::Indent(); + { + DisabledScope scope(!config::VSync); + + OptionCheckbox("Duplicate frames", config::DupeFrames, "Duplicate frames on high refresh rate monitors (120 Hz and higher)"); + } + ImGui::Unindent(); + } +#endif + OptionCheckbox("Show VMU In-game", config::FloatVMUs, "Show the VMU LCD screens while in-game"); + OptionCheckbox("Full Framebuffer Emulation", config::EmulateFramebuffer, + "Fully accurate VRAM framebuffer emulation. Helps games that directly access the framebuffer for special effects. " + "Very slow and incompatible with upscaling and wide screen."); + OptionCheckbox("Load Custom Textures", config::CustomTextures, + "Load custom/high-res textures from data/textures/"); + } + ImGui::Spacing(); + header("Aspect Ratio"); + { + OptionCheckbox("Widescreen", config::Widescreen, + "Draw geometry outside of the normal 4:3 aspect ratio. May produce graphical glitches in the revealed areas.\nAspect Fit and shows the full 16:9 content."); + { + DisabledScope scope(!config::Widescreen); + + ImGui::Indent(); + OptionCheckbox("Super Widescreen", config::SuperWidescreen, + "Use the full width of the screen or window when its aspect ratio is greater than 16:9.\nAspect Fill and remove black bars."); + ImGui::Unindent(); + } + OptionCheckbox("Widescreen Game Cheats", config::WidescreenGameHacks, + "Modify the game so that it displays in 16:9 anamorphic format and use horizontal screen stretching. Only some games are supported."); + OptionSlider("Horizontal Stretching", config::ScreenStretching, 100, 250, + "Stretch the screen horizontally", "%d%%"); + OptionCheckbox("Rotate Screen 90°", config::Rotate90, "Rotate the screen 90° counterclockwise"); + } + if (perPixel) + { + ImGui::Spacing(); + header("Per Pixel Settings"); + + const std::array bufSizes{ 512_MB, 1_GB, 2_GB, 4_GB }; + const std::array bufSizesText{ "512 MB", "1 GB", "2 GB", "4 GB" }; + ImGui::PushItemWidth(ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f); + u32 selected = 0; + for (; selected < bufSizes.size(); selected++) + if (bufSizes[selected] == config::PixelBufferSize) + break; + if (selected == bufSizes.size()) + selected = 0; + if (ImGui::BeginCombo("##PixelBuffer", bufSizesText[selected].c_str(), ImGuiComboFlags_NoArrowButton)) + { + for (u32 i = 0; i < bufSizes.size(); i++) + { + bool is_selected = i == selected; + if (ImGui::Selectable(bufSizesText[i].c_str(), is_selected)) + config::PixelBufferSize = bufSizes[i]; + if (is_selected) { + ImGui::SetItemDefaultFocus(); + selected = i; + } + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + ImGui::SameLine(0, innerSpacing); + + if (ImGui::ArrowButton("##Decrease BufSize", ImGuiDir_Left)) + { + if (selected > 0) + config::PixelBufferSize = bufSizes[selected - 1]; + } + ImGui::SameLine(0, innerSpacing); + if (ImGui::ArrowButton("##Increase BufSize", ImGuiDir_Right)) + { + if (selected < bufSizes.size() - 1) + config::PixelBufferSize = bufSizes[selected + 1]; + } + ImGui::SameLine(0, innerSpacing); + + ImGui::Text("Pixel Buffer Size"); + ImGui::SameLine(); + ShowHelpMarker("The size of the pixel buffer. May need to be increased when upscaling by a large factor."); + + OptionSlider("Maximum Layers", config::PerPixelLayers, 8, 128, + "Maximum number of transparent layers. May need to be increased for some complex scenes. Decreasing it may improve performance."); + } + ImGui::Spacing(); + header("Performance"); + { + ImGui::Text("Automatic Frame Skipping:"); + ImGui::Columns(3, "autoskip", false); + OptionRadioButton("Disabled", config::AutoSkipFrame, 0, "No frame skipping"); + ImGui::NextColumn(); + OptionRadioButton("Normal", config::AutoSkipFrame, 1, "Skip a frame when the GPU and CPU are both running slow"); + ImGui::NextColumn(); + OptionRadioButton("Maximum", config::AutoSkipFrame, 2, "Skip a frame when the GPU is running slow"); + ImGui::Columns(1, nullptr, false); + + OptionArrowButtons("Frame Skipping", config::SkipFrame, 0, 6, + "Number of frames to skip between two actually rendered frames"); + OptionCheckbox("Shadows", config::ModifierVolumes, + "Enable modifier volumes, usually used for shadows"); + OptionCheckbox("Fog", config::Fog, "Enable fog effects"); + } + ImGui::Spacing(); + header("Advanced"); + { + OptionCheckbox("Delay Frame Swapping", config::DelayFrameSwapping, + "Useful to avoid flashing screen or glitchy videos. Not recommended on slow platforms"); + OptionCheckbox("Fix Upscale Bleeding Edge", config::FixUpscaleBleedingEdge, + "Helps with texture bleeding case when upscaling. Disabling it can help if pixels are warping when upscaling in 2D games (MVC2, CVS, KOF, etc.)"); + OptionCheckbox("Native Depth Interpolation", config::NativeDepthInterpolation, + "Helps with texture corruption and depth issues on AMD GPUs. Can also help Intel GPUs in some cases."); + OptionCheckbox("Copy Rendered Textures to VRAM", config::RenderToTextureBuffer, + "Copy rendered-to textures back to VRAM. Slower but accurate"); + const std::array aniso{ 1, 2, 4, 8, 16 }; + const std::array anisoText{ "Disabled", "2x", "4x", "8x", "16x" }; + u32 afSelected = 0; + for (u32 i = 0; i < aniso.size(); i++) + { + if (aniso[i] == config::AnisotropicFiltering) + afSelected = i; + } + + ImGui::PushItemWidth(ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f); + if (ImGui::BeginCombo("##Anisotropic Filtering", anisoText[afSelected].c_str(), ImGuiComboFlags_NoArrowButton)) + { + for (u32 i = 0; i < aniso.size(); i++) + { + bool is_selected = aniso[i] == config::AnisotropicFiltering; + if (ImGui::Selectable(anisoText[i].c_str(), is_selected)) + config::AnisotropicFiltering = aniso[i]; + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + ImGui::SameLine(0, innerSpacing); + + if (ImGui::ArrowButton("##Decrease Anisotropic Filtering", ImGuiDir_Left)) + { + if (afSelected > 0) + config::AnisotropicFiltering = aniso[afSelected - 1]; + } + ImGui::SameLine(0, innerSpacing); + if (ImGui::ArrowButton("##Increase Anisotropic Filtering", ImGuiDir_Right)) + { + if (afSelected < aniso.size() - 1) + config::AnisotropicFiltering = aniso[afSelected + 1]; + } + ImGui::SameLine(0, innerSpacing); + + ImGui::Text("Anisotropic Filtering"); + ImGui::SameLine(); + ShowHelpMarker("Higher values make textures viewed at oblique angles look sharper, but are more demanding on the GPU. This option only has a visible impact on mipmapped textures."); + + ImGui::Text("Texture Filtering:"); + ImGui::Columns(3, "textureFiltering", false); + OptionRadioButton("Default", config::TextureFiltering, 0, "Use the game's default texture filtering"); + ImGui::NextColumn(); + OptionRadioButton("Force Nearest-Neighbor", config::TextureFiltering, 1, "Force nearest-neighbor filtering for all textures. Crisper appearance, but may cause various rendering issues. This option usually does not affect performance."); + ImGui::NextColumn(); + OptionRadioButton("Force Linear", config::TextureFiltering, 2, "Force linear filtering for all textures. Smoother appearance, but may cause various rendering issues. This option usually does not affect performance."); + ImGui::Columns(1, nullptr, false); + + OptionCheckbox("Show FPS Counter", config::ShowFPS, "Show on-screen frame/sec counter"); + } + ImGui::Spacing(); + header("Texture Upscaling"); + { +#ifdef _OPENMP + OptionArrowButtons("Texture Upscaling", config::TextureUpscale, 1, 8, + "Upscale textures with the xBRZ algorithm. Only on fast platforms and for certain 2D games", "x%d"); + OptionSlider("Texture Max Size", config::MaxFilteredTextureSize, 8, 1024, + "Textures larger than this dimension squared will not be upscaled"); + OptionArrowButtons("Max Threads", config::MaxThreads, 1, 8, + "Maximum number of threads to use for texture upscaling. Recommended: number of physical cores minus one"); +#endif + } +#ifdef VIDEO_ROUTING +#ifdef __APPLE__ + header("Video Routing (Syphon)"); +#elif defined(_WIN32) + ((renderApi == 0) || (renderApi == 3)) ? header("Video Routing (Spout)") : header("Video Routing (Only available with OpenGL or DirectX 11)"); +#endif + { +#ifdef _WIN32 + DisabledScope scope(!((renderApi == 0) || (renderApi == 3))); +#endif + OptionCheckbox("Send video content to another program", config::VideoRouting, + "e.g. Route GPU texture to OBS Studio directly instead of using CPU intensive Display/Window Capture"); + + { + DisabledScope scope(!config::VideoRouting); + OptionCheckbox("Scale down before sending", config::VideoRoutingScale, "Could increase performance when sharing a smaller texture, YMMV"); + { + DisabledScope scope(!config::VideoRoutingScale); + static int vres = config::VideoRoutingVRes; + if (ImGui::InputInt("Output vertical resolution", &vres)) + { + config::VideoRoutingVRes = vres; + } + } + ImGui::Text("Output texture size: %d x %d", config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width, config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); + } + } +#endif + + switch (renderApi) + { + case 0: + config::RendererType = perPixel ? RenderType::OpenGL_OIT : RenderType::OpenGL; + break; + case 1: + config::RendererType = perPixel ? RenderType::Vulkan_OIT : RenderType::Vulkan; + break; + case 2: + config::RendererType = RenderType::DirectX9; + break; + case 3: + config::RendererType = perPixel ? RenderType::DirectX11_OIT : RenderType::DirectX11; + break; + } +} + +static void gui_settings_audio() +{ + OptionCheckbox("Enable DSP", config::DSPEnabled, + "Enable the Dreamcast Digital Sound Processor. Only recommended on fast platforms"); + OptionCheckbox("Enable VMU Sounds", config::VmuSound, "Play VMU beeps when enabled."); + + if (OptionSlider("Volume Level", config::AudioVolume, 0, 100, "Adjust the emulator's audio level", "%d%%")) + { + config::AudioVolume.calcDbPower(); + }; +#ifdef __ANDROID__ + if (config::AudioBackend.get() == "auto" || config::AudioBackend.get() == "android") + OptionCheckbox("Automatic Latency", config::AutoLatency, + "Automatically set audio latency. Recommended"); +#endif + if (!config::AutoLatency + || (config::AudioBackend.get() != "auto" && config::AudioBackend.get() != "android")) + { + int latency = (int)roundf(config::AudioBufferSize * 1000.f / 44100.f); + ImGui::SliderInt("Latency", &latency, 12, 512, "%d ms"); + config::AudioBufferSize = (int)roundf(latency * 44100.f / 1000.f); + ImGui::SameLine(); + ShowHelpMarker("Sets the maximum audio latency. Not supported by all audio drivers."); + } + + AudioBackend *backend = nullptr; + std::string backend_name = config::AudioBackend; + if (backend_name != "auto") + { + backend = AudioBackend::getBackend(config::AudioBackend); + if (backend != nullptr) + backend_name = backend->slug; + } + + AudioBackend *current_backend = backend; + if (ImGui::BeginCombo("Audio Driver", backend_name.c_str(), ImGuiComboFlags_None)) + { + bool is_selected = (config::AudioBackend.get() == "auto"); + if (ImGui::Selectable("auto - Automatic driver selection", &is_selected)) + config::AudioBackend.set("auto"); + + for (u32 i = 0; i < AudioBackend::getCount(); i++) + { + backend = AudioBackend::getBackend(i); + is_selected = (config::AudioBackend.get() == backend->slug); + + if (is_selected) + current_backend = backend; + + if (ImGui::Selectable((backend->slug + " - " + backend->name).c_str(), &is_selected)) + config::AudioBackend.set(backend->slug); + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + ShowHelpMarker("The audio driver to use"); + + if (current_backend != nullptr) + { + // get backend specific options + int option_count; + const AudioBackend::Option *options = current_backend->getOptions(&option_count); + + for (int o = 0; o < option_count; o++) + { + std::string value = cfgLoadStr(current_backend->slug, options->name, ""); + + if (options->type == AudioBackend::Option::integer) + { + int val = stoi(value); + if (ImGui::SliderInt(options->caption.c_str(), &val, options->minValue, options->maxValue)) + { + std::string s = std::to_string(val); + cfgSaveStr(current_backend->slug, options->name, s); + } + } + else if (options->type == AudioBackend::Option::checkbox) + { + bool check = value == "1"; + if (ImGui::Checkbox(options->caption.c_str(), &check)) + cfgSaveStr(current_backend->slug, options->name, + check ? "1" : "0"); + } + else if (options->type == AudioBackend::Option::list) + { + if (ImGui::BeginCombo(options->caption.c_str(), value.c_str(), ImGuiComboFlags_None)) + { + bool is_selected = false; + for (const auto& cur : options->values) + { + is_selected = value == cur; + if (ImGui::Selectable(cur.c_str(), &is_selected)) + cfgSaveStr(current_backend->slug, options->name, cur); + + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + else { + WARN_LOG(RENDERER, "Unknown option"); + } + + options++; + } + } +} + +static void gui_settings_network() +{ + ImGuiStyle& style = ImGui::GetStyle(); + header("Network Type"); + { + DisabledScope scope(game_started); + + int netType = 0; + if (config::GGPOEnable) + netType = 1; + else if (config::NetworkEnable) + netType = 2; + else if (config::BattleCableEnable) + netType = 3; + ImGui::Columns(4, "networkType", false); + ImGui::RadioButton("Disabled", &netType, 0); + ImGui::NextColumn(); + ImGui::RadioButton("GGPO", &netType, 1); + ImGui::SameLine(0, style.ItemInnerSpacing.x); + ShowHelpMarker("Enable networking using GGPO"); + ImGui::NextColumn(); + ImGui::RadioButton("Naomi", &netType, 2); + ImGui::SameLine(0, style.ItemInnerSpacing.x); + ShowHelpMarker("Enable networking for supported Naomi and Atomiswave games"); + ImGui::NextColumn(); + ImGui::RadioButton("Battle Cable", &netType, 3); + ImGui::SameLine(0, style.ItemInnerSpacing.x); + ShowHelpMarker("Emulate the Taisen (Battle) null modem cable for games that support it"); + ImGui::Columns(1, nullptr, false); + + config::GGPOEnable = false; + config::NetworkEnable = false; + config::BattleCableEnable = false; + switch (netType) { + case 1: + config::GGPOEnable = true; + break; + case 2: + config::NetworkEnable = true; + break; + case 3: + config::BattleCableEnable = true; + break; + } + } + if (config::GGPOEnable || config::NetworkEnable || config::BattleCableEnable) { + ImGui::Spacing(); + header("Configuration"); + } + { + if (config::GGPOEnable) + { + config::NetworkEnable = false; + OptionCheckbox("Play as Player 1", config::ActAsServer, + "Deselect to play as player 2"); + ImGui::InputText("Peer", &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("Your peer IP address and optional port"); + OptionSlider("Frame Delay", config::GGPODelay, 0, 20, + "Sets Frame Delay, advisable for sessions with ping >100 ms"); + + ImGui::Text("Left Thumbstick:"); + OptionRadioButton("Disabled", config::GGPOAnalogAxes, 0, "Left thumbstick not used"); + ImGui::SameLine(); + OptionRadioButton("Horizontal", config::GGPOAnalogAxes, 1, "Use the left thumbstick horizontal axis only"); + ImGui::SameLine(); + OptionRadioButton("Full", config::GGPOAnalogAxes, 2, "Use the left thumbstick horizontal and vertical axes"); + + OptionCheckbox("Enable Chat", config::GGPOChat, "Open the chat window when a chat message is received"); + if (config::GGPOChat) + { + OptionCheckbox("Enable Chat Window Timeout", config::GGPOChatTimeoutToggle, "Automatically close chat window after 20 seconds"); + if (config::GGPOChatTimeoutToggle) + { + char chatTimeout[256]; + sprintf(chatTimeout, "%d", (int)config::GGPOChatTimeout); + ImGui::InputText("Chat Window Timeout (seconds)", chatTimeout, sizeof(chatTimeout), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("Sets duration that chat window stays open after new message is received."); + config::GGPOChatTimeout.set(atoi(chatTimeout)); + } + } + OptionCheckbox("Network Statistics", config::NetworkStats, + "Display network statistics on screen"); + } + else if (config::NetworkEnable) + { + OptionCheckbox("Act as Server", config::ActAsServer, + "Create a local server for Naomi network games"); + if (!config::ActAsServer) + { + ImGui::InputText("Server", &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("The server to connect to. Leave blank to find a server automatically on the default port"); + } + char localPort[256]; + sprintf(localPort, "%d", (int)config::LocalPort); + ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("The local UDP port to use"); + config::LocalPort.set(atoi(localPort)); + } + else if (config::BattleCableEnable) + { + ImGui::InputText("Peer", &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("The peer to connect to. Leave blank to find a player automatically on the default port"); + char localPort[256]; + sprintf(localPort, "%d", (int)config::LocalPort); + ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("The local UDP port to use"); + config::LocalPort.set(atoi(localPort)); + } + } + ImGui::Spacing(); + header("Network Options"); + { + OptionCheckbox("Enable UPnP", config::EnableUPnP, "Automatically configure your network router for netplay"); + OptionCheckbox("Broadcast Digital Outputs", config::NetworkOutput, "Broadcast digital outputs and force-feedback state on TCP port 8000. " + "Compatible with the \"-output network\" MAME option. Arcade games only."); + { + DisabledScope scope(game_started); + + OptionCheckbox("Broadband Adapter Emulation", config::EmulateBBA, + "Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem"); + } + } +#ifdef NAOMI_MULTIBOARD + ImGui::Spacing(); + header("Multiboard Screens"); + { + //OptionRadioButton("Disabled", config::MultiboardSlaves, 0, "Multiboard disabled (when optional)"); + OptionRadioButton("1 (Twin)", config::MultiboardSlaves, 1, "One screen configuration (F355 Twin)"); + ImGui::SameLine(); + OptionRadioButton("3 (Deluxe)", config::MultiboardSlaves, 2, "Three screens configuration"); + } +#endif +} + +static void gui_settings_advanced() +{ + header("CPU Mode"); + { + ImGui::Columns(2, "cpu_modes", false); + OptionRadioButton("Dynarec", config::DynarecEnabled, true, + "Use the dynamic recompiler. Recommended in most cases"); + ImGui::NextColumn(); + OptionRadioButton("Interpreter", config::DynarecEnabled, false, + "Use the interpreter. Very slow but may help in case of a dynarec problem"); + ImGui::Columns(1, NULL, false); + + OptionSlider("SH4 Clock", config::Sh4Clock, 100, 300, + "Over/Underclock the main SH4 CPU. Default is 200 MHz. Other values may crash, freeze or trigger unexpected nuclear reactions.", + "%d MHz"); + } + ImGui::Spacing(); + header("Other"); + { + OptionCheckbox("HLE BIOS", config::UseReios, "Force high-level BIOS emulation"); + OptionCheckbox("Multi-threaded emulation", config::ThreadedRendering, + "Run the emulated CPU and GPU on different threads"); +#ifndef __ANDROID + OptionCheckbox("Serial Console", config::SerialConsole, + "Dump the Dreamcast serial console to stdout"); +#endif + { + DisabledScope scope(game_started); + OptionCheckbox("Dreamcast 32MB RAM Mod", config::RamMod32MB, + "Enables 32MB RAM Mod for Dreamcast. May affect compatibility"); + } + OptionCheckbox("Dump Textures", config::DumpTextures, + "Dump all textures into data/texdump/"); + + bool logToFile = cfgLoadBool("log", "LogToFile", false); + bool newLogToFile = logToFile; + ImGui::Checkbox("Log to File", &newLogToFile); + if (logToFile != newLogToFile) + { + cfgSaveBool("log", "LogToFile", newLogToFile); + LogManager::Shutdown(); + LogManager::Init(); + } + ImGui::SameLine(); + ShowHelpMarker("Log debug information to flycast.log"); +#ifdef SENTRY_UPLOAD + OptionCheckbox("Automatically Report Crashes", config::UploadCrashLogs, + "Automatically upload crash reports to sentry.io to help in troubleshooting. No personal information is included."); +#endif + } + +#ifdef USE_LUA + header("Lua Scripting"); + { + ImGui::InputText("Lua Filename", &config::LuaFileName.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("Specify lua filename to use. Should be located in Flycast config directory. Defaults to flycast.lua when empty."); + } +#endif +} + +static void gui_settings_about() +{ + header("Flycast"); + { + ImGui::Text("Version: %s", GIT_VERSION); + ImGui::Text("Git Hash: %s", GIT_HASH); + ImGui::Text("Build Date: %s", BUILD_DATE); + } + ImGui::Spacing(); + header("Platform"); + { + ImGui::Text("CPU: %s", +#if HOST_CPU == CPU_X86 + "x86" +#elif HOST_CPU == CPU_ARM + "ARM" +#elif HOST_CPU == CPU_MIPS + "MIPS" +#elif HOST_CPU == CPU_X64 + "x86/64" +#elif HOST_CPU == CPU_GENERIC + "Generic" +#elif HOST_CPU == CPU_ARM64 + "ARM64" +#else + "Unknown" +#endif + ); + ImGui::Text("Operating System: %s", +#ifdef __ANDROID__ + "Android" +#elif defined(__unix__) + "Linux" +#elif defined(__APPLE__) +#ifdef TARGET_IPHONE + "iOS" +#else + "macOS" +#endif +#elif defined(TARGET_UWP) + "Windows Universal Platform" +#elif defined(_WIN32) + "Windows" +#elif defined(__SWITCH__) + "Switch" +#else + "Unknown" +#endif + ); +#ifdef TARGET_IPHONE + const char *getIosJitStatus(); + ImGui::Text("JIT Status: %s", getIosJitStatus()); +#endif + } + ImGui::Spacing(); + if (isOpenGL(config::RendererType)) + header("OpenGL"); + else if (isVulkan(config::RendererType)) + header("Vulkan"); + else if (isDirectX(config::RendererType)) + header("DirectX"); + ImGui::Text("Driver Name: %s", GraphicsContext::Instance()->getDriverName().c_str()); + ImGui::Text("Version: %s", GraphicsContext::Instance()->getDriverVersion().c_str()); + +#if defined(__ANDROID__) && HOST_CPU == CPU_ARM64 && USE_VULKAN + if (isVulkan(config::RendererType)) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); + if (config::CustomGpuDriver) + { + std::string name, description, vendor, version; + if (getCustomGpuDriverInfo(name, description, vendor, version)) + { + ImGui::Text("Custom Driver:"); + ImGui::Indent(); + ImGui::Text("%s - %s", name.c_str(), description.c_str()); + ImGui::Text("%s - %s", vendor.c_str(), version.c_str()); + ImGui::Unindent(); + } + + if (ImGui::Button("Use Default Driver")) { + config::CustomGpuDriver = false; + ImGui::OpenPopup("Reset Vulkan"); + } + } + else if (ImGui::Button("Upload Custom Driver")) + ImGui::OpenPopup("Select custom GPU driver"); + + static bool driverDirty; + const auto& callback = [](bool cancelled, std::string selection) { + if (!cancelled) { + try { + uploadCustomGpuDriver(selection); + config::CustomGpuDriver = true; + driverDirty = true; + } catch (const FlycastException& e) { + gui_error(e.what()); + config::CustomGpuDriver = false; + } + } + return true; + }; + select_file_popup("Select custom GPU driver", callback, true, "zip"); + + if (driverDirty) { + ImGui::OpenPopup("Reset Vulkan"); + driverDirty = false; + } + + ImguiStyleVar _1(ImGuiStyleVar_WindowPadding, ScaledVec2(20, 20)); + if (ImGui::BeginPopupModal("Reset Vulkan", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar)) + { + ImGui::Text("Do you want to reset Vulkan to use new driver?"); + ImGui::NewLine(); + ImguiStyleVar _(ImGuiStyleVar_ItemSpacing, ImVec2(uiScaled(20), ImGui::GetStyle().ItemSpacing.y)); + ImguiStyleVar _1(ImGuiStyleVar_FramePadding, ScaledVec2(10, 10)); + if (ImGui::Button("Yes")) + { + mainui_reinit(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + } +#endif +} + +static void gui_display_settings() +{ + static bool maple_devices_changed; + + fullScreenWindow(false); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + + ImGui::Begin("Settings", NULL, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + ImVec2 normal_padding = ImGui::GetStyle().FramePadding; + + if (ImGui::Button("Done", ScaledVec2(100, 30))) + { + if (uiUserScaleUpdated) + { + uiUserScaleUpdated = false; + mainui_reinit(); + } + if (game_started) + gui_setState(GuiState::Commands); + else + gui_setState(GuiState::Main); + if (maple_devices_changed) + { + maple_devices_changed = false; + if (game_started && settings.platform.isConsole()) + { + maple_ReconnectDevices(); + reset_vmus(); + } + } + SaveSettings(); + } + if (game_started) + { + ImGui::SameLine(); + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ImVec2(uiScaled(16), normal_padding.y)); + if (config::Settings::instance().hasPerGameConfig()) + { + if (ImGui::Button("Delete Game Config", ScaledVec2(0, 30))) + { + config::Settings::instance().setPerGameConfig(false); + config::Settings::instance().load(false); + loadGameSpecificSettings(); + } + } + else + { + if (ImGui::Button("Make Game Config", ScaledVec2(0, 30))) + config::Settings::instance().setPerGameConfig(true); + } + } + + if (ImGui::GetContentRegionAvail().x >= uiScaled(650.f)) + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(16, 6)); + else + // low width + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(4, 6)); + + if (ImGui::BeginTabBar("settings", ImGuiTabBarFlags_NoTooltip)) + { + if (ImGui::BeginTabItem(ICON_FA_TOOLBOX " General")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_general(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(ICON_FA_GAMEPAD " Controls")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_controls(maple_devices_changed); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(ICON_FA_DISPLAY " Video")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_video(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(ICON_FA_MUSIC " Audio")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_audio(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(ICON_FA_WIFI " Network")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_network(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(ICON_FA_MICROCHIP " Advanced")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_advanced(); + ImGui::EndTabItem(); + } +#if !defined(NDEBUG) || defined(DEBUGFAST) || FC_PROFILER + if (ImGui::BeginTabItem(ICON_FA_BUG " Debug")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_debug_tab(); + ImGui::EndTabItem(); + } +#endif + if (ImGui::BeginTabItem(ICON_FA_CIRCLE_INFO " About")) + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding); + gui_settings_about(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::PopStyleVar(); + + scrollWhenDraggingOnVoid(); + windowDragScroll(); + ImGui::End(); +} + +void os_notify(const char *msg, int durationMs, const char *details) +{ + if (gui_state != GuiState::Closed) + { + std::lock_guard _{osd_message_mutex}; + osd_message = msg; + osd_message_end = getTimeMs() + durationMs; + } + else { + toast.show(msg, details != nullptr ? details : "", durationMs); + } +} + +static std::string get_notification() +{ + std::lock_guard lock(osd_message_mutex); + if (!osd_message.empty() && getTimeMs() >= osd_message_end) + osd_message.clear(); + return osd_message; +} + +inline static void gui_display_demo() +{ + ImGui::ShowDemoWindow(); +} + +static void gameTooltip(const std::string& tip) +{ + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 25.0f); + ImGui::TextUnformatted(tip.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +static bool gameImageButton(ImguiTexture& texture, const std::string& tooltip, ImVec2 size, const std::string& gameName) +{ + bool pressed = texture.button("", size, gameName); + gameTooltip(tooltip); + + return pressed; +} + +static void gui_display_content() +{ + fullScreenWindow(false); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0); + + ImGui::Begin("##main", NULL, ImGuiWindowFlags_NoDecoration); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); + ImGui::AlignTextToFramePadding(); + ImGui::Indent(uiScaled(10)); + ImGui::Text("GAMES"); + ImGui::Unindent(uiScaled(10)); + + static ImGuiTextFilter filter; + const float settingsBtnW = iconButtonWidth(ICON_FA_GEAR, "Settings"); +#if !defined(__ANDROID__) && !defined(TARGET_IPHONE) && !defined(TARGET_UWP) && !defined(__SWITCH__) + ImGui::SameLine(0, uiScaled(32)); + filter.Draw("Filter", ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x - uiScaled(32) + - settingsBtnW - ImGui::GetStyle().ItemSpacing.x); +#endif + if (gui_state != GuiState::SelectDisk) + { +#ifdef TARGET_UWP + void gui_load_game(); + ImGui::SameLine(ImGui::GetContentRegionMax().x - settingsBtnW + - ImGui::GetStyle().FramePadding.x * 2.0f - ImGui::GetStyle().ItemSpacing.x - ImGui::CalcTextSize("Load...").x); + if (ImGui::Button("Load...")) + gui_load_game(); + ImGui::SameLine(); +#elif defined(__SWITCH__) + ImGui::SameLine(ImGui::GetContentRegionMax().x - settingsBtnW + - ImGui::GetStyle().ItemSpacing.x - iconButtonWidth(ICON_FA_POWER_OFF, "Exit")); + if (iconButton(ICON_FA_POWER_OFF, "Exit")) + dc_exit(); + ImGui::SameLine(); +#else + ImGui::SameLine(ImGui::GetContentRegionMax().x - settingsBtnW); +#endif + if (iconButton(ICON_FA_GEAR, "Settings")) + gui_setState(GuiState::Settings); + } + ImGui::PopStyleVar(); + + scanner.fetch_game_list(); + + // Only if Filter and Settings aren't focused... ImGui::SetNextWindowFocus(); + ImGui::BeginChild(ImGui::GetID("library"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); + { + const float totalWidth = ImGui::GetContentRegionMax().x - (!ImGui::GetCurrentWindow()->ScrollbarY ? ImGui::GetStyle().ScrollbarSize : 0); + const int itemsPerLine = std::max(totalWidth / (uiScaled(150) + ImGui::GetStyle().ItemSpacing.x), 1); + const float responsiveBoxSize = totalWidth / itemsPerLine - ImGui::GetStyle().FramePadding.x * 2; + const ImVec2 responsiveBoxVec2 = ImVec2(responsiveBoxSize, responsiveBoxSize); + + if (config::BoxartDisplayMode) + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f, 0.5f)); + else + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20)); + + int counter = 0; + if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS")) + { + ImguiID _("bios"); + bool pressed; + if (config::BoxartDisplayMode) + { + GameMedia game; + GameBoxart art = boxart.getBoxartAndLoad(game); + ImguiFileTexture tex(art.boxartPath); + pressed = gameImageButton(tex, "Dreamcast BIOS", responsiveBoxVec2, "Dreamcast BIOS"); + } + else + { + pressed = ImGui::Selectable("Dreamcast BIOS"); + } + if (pressed) + gui_start_game(""); + counter++; + } + { + scanner.get_mutex().lock(); + for (const auto& game : scanner.get_game_list()) + { + if (gui_state == GuiState::SelectDisk) + { + std::string extension = get_file_extension(game.path); + if (extension != "gdi" && extension != "chd" + && extension != "cdi" && extension != "cue") + // Only dreamcast disks + continue; + } + std::string gameName = game.name; + GameBoxart art; + if (config::BoxartDisplayMode) + { + art = boxart.getBoxartAndLoad(game); + gameName = art.name; + } + if (filter.PassFilter(gameName.c_str())) + { + ImguiID _(game.path.c_str()); + bool pressed = false; + if (config::BoxartDisplayMode) + { + if (counter % itemsPerLine != 0) + ImGui::SameLine(); + counter++; + // Put the image inside a child window so we can detect when it's fully clipped and doesn't need to be loaded + if (ImGui::BeginChild("img", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_NavFlattened)) + { + ImguiFileTexture tex(art.boxartPath); + pressed = gameImageButton(tex, game.name, responsiveBoxVec2, gameName); + } + ImGui::EndChild(); + } + else + { + pressed = ImGui::Selectable(gameName.c_str()); + } + if (pressed) + { + if (gui_state == GuiState::SelectDisk) + { + settings.content.path = game.path; + try { + DiscSwap(game.path); + gui_setState(GuiState::Closed); + } catch (const FlycastException& e) { + gui_error(e.what()); + } + } + else + { + if (!config::BoxartDisplayMode) + art = boxart.getBoxart(game); + settings.content.title = art.name; + if (settings.content.title.empty() || settings.content.title == game.fileName) + settings.content.title = get_file_basename(game.fileName); + std::string gamePath(game.path); + scanner.get_mutex().unlock(); + gui_start_game(gamePath); + scanner.get_mutex().lock(); + break; + } + } + } + } + scanner.get_mutex().unlock(); + } + ImGui::PopStyleVar(); + } + scrollWhenDraggingOnVoid(); + windowDragScroll(); + ImGui::EndChild(); + ImGui::End(); + + contentpath_warning_popup(); +} + +static bool systemdir_selected_callback(bool cancelled, std::string selection) +{ + if (cancelled) + { + gui_setState(GuiState::Main); + return true; + } + selection += "/"; + + std::string data_path = selection + "data/"; + if (!file_exists(data_path)) + { + if (!make_directory(data_path)) + { + WARN_LOG(BOOT, "Cannot create 'data' directory: %s", data_path.c_str()); + gui_error("Invalid selection:\nFlycast cannot write to this directory."); + return false; + } + } + else + { + // Test + std::string testPath = data_path + "writetest.txt"; + FILE *file = fopen(testPath.c_str(), "w"); + if (file == nullptr) + { + WARN_LOG(BOOT, "Cannot write in the 'data' directory"); + gui_error("Invalid selection:\nFlycast cannot write to this directory."); + return false; + } + fclose(file); + unlink(testPath.c_str()); + } + set_user_config_dir(selection); + add_system_data_dir(selection); + set_user_data_dir(data_path); + + if (cfgOpen()) + { + config::Settings::instance().load(false); + // Make sure the renderer type doesn't change mid-flight + config::RendererType = RenderType::OpenGL; + gui_setState(GuiState::Main); + if (config::ContentPath.get().empty()) + { + scanner.stop(); + config::ContentPath.get().push_back(selection); + } + SaveSettings(); + } + return true; +} + +static void gui_display_onboarding() +{ + ImGui::OpenPopup("Select System Directory"); + select_file_popup("Select System Directory", &systemdir_selected_callback); +} + +static std::future networkStatus; + +static void gui_network_start() +{ + centerNextWindow(); + ImGui::SetNextWindowSize(ScaledVec2(330, 180)); + + ImGui::Begin("##network", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); + + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(uiScaled(20.f)); + + if (networkStatus.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) + { + ImGui::Text("Starting..."); + try { + if (networkStatus.get()) + gui_setState(GuiState::Closed); + else + gui_stop_game(); + } catch (const FlycastException& e) { + gui_stop_game(e.what()); + } + } + else + { + ImGui::Text("Starting Network..."); + if (NetworkHandshake::instance->canStartNow()) + ImGui::Text("Press Start to start the game now."); + } + ImGui::Text("%s", get_notification().c_str()); + + float currentwidth = ImGui::GetContentRegionAvail().x; + ImGui::SetCursorPosX((currentwidth - uiScaled(100.f)) / 2.f + ImGui::GetStyle().WindowPadding.x); + ImGui::SetCursorPosY(uiScaled(126.f)); + if (ImGui::Button("Cancel", ScaledVec2(100.f, 0)) && NetworkHandshake::instance != nullptr) + { + NetworkHandshake::instance->stop(); + try { + networkStatus.get(); + } + catch (const FlycastException& e) { + } + gui_stop_game(); + } + ImGui::End(); + + if ((kcode[0] & DC_BTN_START) == 0 && NetworkHandshake::instance != nullptr) + NetworkHandshake::instance->startNow(); +} + +static void gui_display_loadscreen() +{ + centerNextWindow(); + ImGui::SetNextWindowSize(ScaledVec2(330, 180)); + + ImGui::Begin("##loading", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); + + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(uiScaled(20.f)); + try { + const char *label = gameLoader.getProgress().label; + if (label == nullptr) + { + if (gameLoader.ready()) + label = "Starting..."; + else + label = "Loading..."; + } + + if (gameLoader.ready()) + { + if (NetworkHandshake::instance != nullptr) + { + networkStatus = NetworkHandshake::instance->start(); + gui_setState(GuiState::NetworkStart); + } + else + { + gui_setState(GuiState::Closed); + ImGui::Text("%s", label); + } + } + else + { + ImGui::Text("%s", label); + { + ImguiStyleColor _(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f)); + ImGui::ProgressBar(gameLoader.getProgress().progress, ImVec2(-1, uiScaled(20.f)), ""); + } + + float currentwidth = ImGui::GetContentRegionAvail().x; + ImGui::SetCursorPosX((currentwidth - uiScaled(100.f)) / 2.f + ImGui::GetStyle().WindowPadding.x); + ImGui::SetCursorPosY(uiScaled(126.f)); + if (ImGui::Button("Cancel", ScaledVec2(100.f, 0))) + gameLoader.cancel(); + } + } catch (const FlycastException& ex) { + ERROR_LOG(BOOT, "%s", ex.what()); +#ifdef TEST_AUTOMATION + die("Game load failed"); +#endif + gui_stop_game(ex.what()); + } + ImGui::End(); +} + +void gui_display_ui() +{ + FC_PROFILE_SCOPE; + const LockGuard lock(guiMutex); + + if (gui_state == GuiState::Closed || gui_state == GuiState::VJoyEdit) + return; + if (gui_state == GuiState::Main) + { + if (!settings.content.path.empty() || settings.naomi.slave) + { +#ifndef __ANDROID__ + commandLineStart = true; +#endif + gui_start_game(settings.content.path); + return; + } + } + + gui_newFrame(); + ImGui::NewFrame(); + error_msg_shown = false; + bool gui_open = gui_is_open(); + + switch (gui_state) + { + case GuiState::Settings: + gui_display_settings(); + break; + case GuiState::Commands: + gui_display_commands(); + break; + case GuiState::Main: + //gui_display_demo(); + gui_display_content(); + break; + case GuiState::Closed: + break; + case GuiState::Onboarding: + gui_display_onboarding(); + break; + case GuiState::VJoyEdit: + break; + case GuiState::VJoyEditCommands: +#ifdef __ANDROID__ + gui_display_vjoy_commands(); +#endif + break; + case GuiState::SelectDisk: + gui_display_content(); + break; + case GuiState::Loading: + gui_display_loadscreen(); + break; + case GuiState::NetworkStart: + gui_network_start(); + break; + case GuiState::Cheats: + gui_cheats(); + break; + case GuiState::Achievements: +#ifdef USE_RACHIEVEMENTS + achievements::achievementList(); + break; +#endif + default: + die("Unknown UI state"); + break; + } + error_popup(); + ImGui::Render(); + gui_endFrame(gui_open); + uiThreadRunner.execTasks(); + ImguiFileTexture::resetLoadCount(); + + if (gui_state == GuiState::Closed) + emu.start(); +} + +static u64 LastFPSTime; +static int lastFrameCount = 0; +static float fps = -1; + +static std::string getFPSNotification() +{ + if (config::ShowFPS) + { + u64 now = getTimeMs(); + if (now - LastFPSTime >= 1000) { + fps = ((float)MainFrameCount - lastFrameCount) * 1000.f / (now - LastFPSTime); + LastFPSTime = now; + lastFrameCount = MainFrameCount; + } + if (fps >= 0.f && fps < 9999.f) { + char text[32]; + snprintf(text, sizeof(text), "F:%4.1f%s", fps, settings.input.fastForwardMode ? " >>" : ""); + + return std::string(text); + } + } + return std::string(settings.input.fastForwardMode ? ">>" : ""); +} + +void gui_draw_osd() +{ + if (gui_state == GuiState::VJoyEdit) + return; + gui_newFrame(); + ImGui::NewFrame(); + +#ifdef USE_RACHIEVEMENTS + if (!achievements::notifier.draw()) +#endif + if (!toast.draw()) + { + std::string message = getFPSNotification(); + if (!message.empty()) + { + const float maxW = uiScaled(640.f); + ImDrawList *dl = ImGui::GetForegroundDrawList(); + const ScaledVec2 padding(5.f, 5.f); + const ImVec2 size = largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, maxW, &message.front(), &message.back() + 1) + + padding * 2.f; + ImVec2 pos(insetLeft, ImGui::GetIO().DisplaySize.y - size.y); + constexpr float alpha = 0.7f; + const ImU32 bg_col = alphaOverride(0x00202020, alpha / 2.f); + dl->AddRectFilled(pos, pos + size, bg_col, 0.f); + pos += padding; + const ImU32 col = alphaOverride(0x0000FFFF, alpha); + dl->AddText(largeFont, largeFont->FontSize, pos, col, &message.front(), &message.back() + 1, maxW); + } + } + + if (ggpo::active()) + { + if (config::NetworkStats) + ggpo::displayStats(); + chat.display(); + } + if (!settings.raHardcoreMode) + lua::overlay(); + ImGui::Render(); + uiThreadRunner.execTasks(); +} + +void gui_display_osd() +{ + gui_draw_osd(); + gui_endFrame(gui_is_open()); +} + +void gui_display_profiler() +{ +#if FC_PROFILER + gui_newFrame(); + ImGui::NewFrame(); + + ImGui::Begin("Profiler", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBackground); + + { + ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + + std::unique_lock lock(fc_profiler::ProfileThread::s_allThreadsLock); + + for(const fc_profiler::ProfileThread* profileThread : fc_profiler::ProfileThread::s_allThreads) + { + char text[256]; + std::snprintf(text, 256, "%.3f : Thread %s", (float)profileThread->cachedTime, profileThread->threadName.c_str()); + ImGui::TreeNode(text); + + ImGui::Indent(); + fc_profiler::drawGUI(profileThread->cachedResultTree); + ImGui::Unindent(); + } + } + + for (const fc_profiler::ProfileThread* profileThread : fc_profiler::ProfileThread::s_allThreads) + { + fc_profiler::drawGraph(*profileThread); + } + + ImGui::End(); + ImGui::Render(); + gui_endFrame(true); +#endif +} + +void gui_open_onboarding() +{ + gui_setState(GuiState::Onboarding); +} + +void gui_cancel_load() +{ + gameLoader.cancel(); +} + +void gui_term() +{ + if (inited) + { + inited = false; + scanner.stop(); + ImGui::DestroyContext(); + EventManager::unlisten(Event::Resume, emuEventCallback); + EventManager::unlisten(Event::Start, emuEventCallback); + EventManager::unlisten(Event::Terminate, emuEventCallback); + gui_save(); + } +} + +void fatal_error(const char* text, ...) +{ + va_list args; + + char temp[2048]; + va_start(args, text); + vsnprintf(temp, sizeof(temp), text, args); + va_end(args); + ERROR_LOG(COMMON, "%s", temp); + + os_notify("Fatal Error", 20000, temp); +} + +extern bool subfolders_read; + +void gui_refresh_files() +{ + scanner.refresh(); + subfolders_read = false; +} + +static void reset_vmus() +{ + for (u32 i = 0; i < std::size(vmu_lcd_status); i++) + vmu_lcd_status[i] = false; +} + +void gui_error(const std::string& what) +{ + error_msg = what; +} + +void gui_save() +{ + boxart.saveDatabase(); +} + +void gui_loadState() +{ + const LockGuard lock(guiMutex); + if (gui_state == GuiState::Closed && savestateAllowed()) + { + try { + emu.stop(); + dc_loadstate(config::SavestateSlot); + emu.start(); + } catch (const FlycastException& e) { + gui_stop_game(e.what()); + } + } +} + +void gui_saveState(bool stopRestart) +{ + const LockGuard lock(guiMutex); + if (gui_state == GuiState::Closed && savestateAllowed()) + { + try { + if (stopRestart) + emu.stop(); + savestate(); + if (stopRestart) + emu.start(); + } catch (const FlycastException& e) { + if (stopRestart) + gui_stop_game(e.what()); + else + WARN_LOG(COMMON, "gui_saveState: %s", e.what()); + } + } +} + +void gui_setState(GuiState newState) +{ + gui_state = newState; + if (newState == GuiState::Closed) + { + // If the game isn't rendering any frame, these flags won't be updated and keyboard/mouse input will be ignored. + // So we force them false here. They will be set in the next ImGUI::NewFrame() anyway + ImGuiIO& io = ImGui::GetIO(); + io.WantCaptureKeyboard = false; + io.WantCaptureMouse = false; + } +} + +std::string gui_getCurGameBoxartUrl() +{ + GameMedia game; + game.fileName = settings.content.fileName; + game.path = settings.content.path; + GameBoxart art = boxart.getBoxart(game); + return art.boxartUrl; +} + +void gui_takeScreenshot() +{ + if (!game_started) + return; + uiThreadRunner.runOnThread([]() { + std::string date = timeToString(time(nullptr)); + std::replace(date.begin(), date.end(), '/', '-'); + std::replace(date.begin(), date.end(), ':', '-'); + std::string name = "Flycast-" + date + ".png"; + + std::vector data; + getScreenshot(data); + if (data.empty()) { + os_notify("No screenshot available", 2000); + } + else + { + try { + hostfs::saveScreenshot(name, data); + os_notify("Screenshot saved", 2000, name.c_str()); + } catch (const FlycastException& e) { + os_notify("Error saving screenshot", 5000, e.what()); + } + } + }); +} + +#ifdef TARGET_UWP +// Ugly but a good workaround for MS stupidity +// UWP doesn't allow the UI thread to wait on a thread/task. When an std::future is ready, it is possible +// that the task has not yet completed. Calling std::future::get() at this point will throw an exception +// AND destroy the std::future at the same time, rendering it invalid and discarding the future result. +bool __cdecl Concurrency::details::_Task_impl_base::_IsNonBlockingThread() { + return false; +} +#endif diff --git a/core/rend/gui.h b/core/ui/gui.h similarity index 93% rename from core/rend/gui.h rename to core/ui/gui.h index cfd03a0f4..9c23c38b0 100644 --- a/core/rend/gui.h +++ b/core/ui/gui.h @@ -25,7 +25,7 @@ void gui_init(); void gui_initFonts(); void gui_open_settings(); void gui_display_ui(); -void gui_display_notification(const char *msg, int duration); +void gui_draw_osd(); void gui_display_osd(); void gui_display_profiler(); void gui_open_onboarding(); @@ -49,7 +49,9 @@ void gui_error(const std::string& what); void gui_setOnScreenKeyboardCallback(void (*callback)(bool show)); void gui_save(); void gui_loadState(); -void gui_saveState(); +void gui_saveState(bool stopRestart = true); +std::string gui_getCurGameBoxartUrl(); +void gui_takeScreenshot(); enum class GuiState { Closed, @@ -62,7 +64,8 @@ enum class GuiState { SelectDisk, Loading, NetworkStart, - Cheats + Cheats, + Achievements, }; extern GuiState gui_state; diff --git a/core/ui/gui_achievements.cpp b/core/ui/gui_achievements.cpp new file mode 100644 index 000000000..c60cf0a82 --- /dev/null +++ b/core/ui/gui_achievements.cpp @@ -0,0 +1,359 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#ifdef USE_RACHIEVEMENTS +#include "gui_achievements.h" +#include "gui.h" +#include "gui_util.h" +#include "imgui_driver.h" +#include "stdclass.h" +#include "achievements/achievements.h" +#include "IconsFontAwesome6.h" +#include +#include + +extern ImFont *largeFont; +extern int insetLeft; + +namespace achievements +{ + +Notification notifier; + +static constexpr u64 DISPLAY_TIME = 5000; +static constexpr u64 START_ANIM_TIME = 500; +static constexpr u64 END_ANIM_TIME = 1000; +static constexpr u64 NEVER_ENDS = 1000000000000; + +void Notification::notify(Type type, const std::string& image, const std::string& text1, + const std::string& text2, const std::string& text3) +{ + verify(type != Challenge && type != Leaderboard); + std::lock_guard _(mutex); + u64 now = getTimeMs(); + if (type == Progress) + { + if (!text1.empty()) + { + if (this->type == None) + { + // New progress + startTime = now; + endTime = NEVER_ENDS; + } + } + else + { + // Hide progress + endTime = now; + } + } + else { + startTime = now; + endTime = startTime + DISPLAY_TIME; + } + this->type = type; + this->image = { image }; + text[0] = text1; + text[1] = text2; + text[2] = text3; +} + +void Notification::showChallenge(const std::string& image) +{ + std::lock_guard _(mutex); + ImguiFileTexture texture{ image }; + if (std::find(challenges.begin(), challenges.end(), texture) != challenges.end()) + return; + challenges.push_back(texture); + if (this->type == None) + { + this->type = Challenge; + startTime = getTimeMs(); + endTime = NEVER_ENDS; + } +} + +void Notification::hideChallenge(const std::string& image) +{ + std::lock_guard _(mutex); + auto it = std::find(challenges.begin(), challenges.end(), image); + if (it == challenges.end()) + return; + challenges.erase(it); + if (this->type == Challenge && challenges.empty()) + endTime = getTimeMs(); +} + +void Notification::showLeaderboard(u32 id, const std::string& text) +{ + std::lock_guard _(mutex); + auto it = leaderboards.find(id); + if (it == leaderboards.end()) + { + if (leaderboards.empty()) + { + this->type = Leaderboard; + startTime = getTimeMs(); + endTime = NEVER_ENDS; + } + leaderboards[id] = text; + } + else { + it->second = text; + } +} + +void Notification::hideLeaderboard(u32 id) +{ + std::lock_guard _(mutex); + auto it = leaderboards.find(id); + if (it == leaderboards.end()) + return; + leaderboards.erase(it); + if (this->type == Leaderboard && leaderboards.empty()) + endTime = getTimeMs(); +} + +bool Notification::draw() +{ + std::lock_guard _(mutex); + if (type == None) + return false; + u64 now = getTimeMs(); + if (now > endTime + END_ANIM_TIME) + { + if (!leaderboards.empty()) + { + // Show current leaderboards + type = Leaderboard; + startTime = getTimeMs(); + endTime = NEVER_ENDS; + } + else if (!challenges.empty()) + { + // Show current challenge indicators + type = Challenge; + startTime = getTimeMs(); + endTime = NEVER_ENDS; + } + else + { + // Hide notification + type = None; + return false; + } + } + float alpha = 1.f; + if (now > endTime) + // Fade out + alpha = (std::cos((now - endTime) / (float)END_ANIM_TIME * (float)M_PI) + 1.f) / 2.f; + float animY = 0.f; + if (now - startTime < START_ANIM_TIME) + // Slide up + animY = (std::cos((now - startTime) / (float)START_ANIM_TIME * (float)M_PI) + 1.f) / 2.f; + + const ImVec2 padding = ImGui::GetStyle().WindowPadding; + ImDrawList *dl = ImGui::GetForegroundDrawList(); + const ImU32 bg_col = alphaOverride(ImGui::GetColorU32(ImGuiCol_WindowBg), alpha / 2.f); + const ImU32 borderCol = alphaOverride(ImGui::GetColorU32(ImGuiCol_Border), alpha); + if (type == Challenge) + { + const ScaledVec2 size(60.f, 60.f); + const float hspacing = ImGui::GetStyle().ItemSpacing.x; + ImVec2 totalSize = padding * 2 + size; + totalSize.x += (size.x + hspacing) * (challenges.size() - 1); + ImVec2 pos(insetLeft, ImGui::GetIO().DisplaySize.y - totalSize.y * (1.f - animY)); + dl->AddRectFilled(pos, pos + totalSize, bg_col, 0.f); + dl->AddRect(pos, pos + totalSize, borderCol, 0.f); + + pos += padding; + for (auto& img : challenges) { + img.draw(dl, pos, size, alpha); + pos.x += hspacing + size.x; + } + } + else if (type == Leaderboard) + { + ImFont *font = ImGui::GetFont(); + const ImVec2 padding = ImGui::GetStyle().FramePadding; + // iterate from the end + ImVec2 pos(insetLeft + padding.x, ImGui::GetIO().DisplaySize.y - padding.y); + for (auto it = leaderboards.rbegin(); it != leaderboards.rend(); ++it) + { + const std::string& text = it->second; + ImVec2 size = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.f, text.c_str()); + ImVec2 psize = size + padding * 2; + pos.y -= psize.y; + dl->AddRectFilled(pos, pos + psize, bg_col, 0.f); + ImVec2 tpos = pos + padding; + const ImU32 col = alphaOverride(0xffffff, alpha); + dl->AddText(font, font->FontSize, tpos, col, &text.front(), &text.back() + 1, FLT_MAX); + pos.y -= padding.y; + } + } + else + { + const float hspacing = ImGui::GetStyle().ItemSpacing.x; + const float vspacing = ImGui::GetStyle().ItemSpacing.y; + const ScaledVec2 imgSize = image.getId() != ImTextureID{} ? ScaledVec2(80.f, 80.f) : ScaledVec2(); + // text size + const float maxW = std::min(ImGui::GetIO().DisplaySize.x, uiScaled(640.f)) - padding.x + - (imgSize.x != 0.f ? imgSize.x + hspacing : padding.x); + ImFont *regularFont = ImGui::GetFont(); + ImVec2 textSize[3] {}; + ImVec2 totalSize(0.f, padding.y * 2); + for (size_t i = 0; i < std::size(text); i++) + { + if (text[i].empty()) + continue; + const ImFont *font = i == 0 ? largeFont : regularFont; + textSize[i] = font->CalcTextSizeA(font->FontSize, FLT_MAX, maxW, text[i].c_str()); + totalSize.x = std::max(totalSize.x, textSize[i].x); + totalSize.y += textSize[i].y; + } + float topMargin = 0.f; + // image / left padding + if (imgSize.x != 0.f) + { + if (totalSize.y < imgSize.y) + topMargin = (imgSize.y - totalSize.y) / 2.f; + totalSize.x += imgSize.x + hspacing; + totalSize.y = std::max(totalSize.y, imgSize.y); + } + else { + totalSize.x += padding.x; + } + // right padding + totalSize.x += padding.x; + // border + totalSize += ImVec2(2.f, 2.f); + // draw background, border + ImVec2 pos(insetLeft, ImGui::GetIO().DisplaySize.y - totalSize.y * (1.f - animY)); + dl->AddRectFilled(pos, pos + totalSize, bg_col, 0.f); + dl->AddRect(pos, pos + totalSize, borderCol, 0.f); + + // draw image and text + pos += ImVec2(1.f, 1.f); // border + if (imgSize.x != 0.f) { + image.draw(dl, pos, imgSize, alpha); + pos.x += imgSize.x + hspacing; + } + else { + pos.x += padding.x; + } + pos.y += topMargin; + for (size_t i = 0; i < std::size(text); i++) + { + if (text[i].empty()) + continue; + const ImFont *font = i == 0 ? largeFont : regularFont; + const ImU32 col = alphaOverride(i == 0 ? 0xffffff : 0x00ffff, alpha); + dl->AddText(font, font->FontSize, pos, col, &text[i].front(), &text[i].back() + 1, maxW); + pos.y += textSize[i].y + vspacing; + } + } + + return true; +} + +void achievementList() +{ + fullScreenWindow(false); + ImguiStyleVar _(ImGuiStyleVar_WindowBorderSize, 0); + + ImGui::Begin("##achievements", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize); + + { + float w = ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().ItemSpacing.x * 2 - ImGui::GetStyle().WindowPadding.x + - uiScaled(80.f + 20.f * 2); // image width and button frame padding + Game game = getCurrentGame(); + ImguiFileTexture tex(game.image); + tex.draw(ScaledVec2(80.f, 80.f)); + ImGui::SameLine(); + ImGui::BeginChild("game_info", ImVec2(w, uiScaled(80.f)), ImGuiChildFlags_None, ImGuiWindowFlags_None); + ImGui::PushFont(largeFont); + ImGui::Text("%s", game.title.c_str()); + ImGui::PopFont(); + std::stringstream ss; + ss << "You have unlocked " << game.unlockedAchievements << " of " << game.totalAchievements + << " achievements and " << game.points << " of " << game.totalPoints << " points."; + { + ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f)); + ImGui::TextWrapped("%s", ss.str().c_str()); + } + if (settings.raHardcoreMode) + ImGui::Text("Hardcore Mode"); + ImGui::EndChild(); + + ImGui::SameLine(); + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); + if (ImGui::Button("Close")) + gui_setState(GuiState::Commands); + } + + // ImGuiWindowFlags_NavFlattened prevents the child window from getting the focus and thus the list can't be scrolled with a keyboard or gamepad. + if (ImGui::BeginChild(ImGui::GetID("ach_list"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling)) + { + std::vector achList = getAchievementList(); + int id = 0; + std::string category; + for (const auto& ach : achList) + { + if (ach.category != category) + { + category = ach.category; + ImGui::Indent(uiScaled(10)); + if (category == "Locked" || category == "Active Challenges" || category == "Almost There") + ImGui::Text(ICON_FA_LOCK); + else if (category == "Unlocked" || category == "Recently Unlocked") + ImGui::Text(ICON_FA_LOCK_OPEN); + ImGui::SameLine(); + ImGui::PushFont(largeFont); + ImGui::Text("%s", category.c_str()); + ImGui::PopFont(); + ImGui::Unindent(uiScaled(10)); + } + ImguiID _("achiev" + std::to_string(id++)); + ImguiFileTexture tex(ach.image); + tex.draw(ScaledVec2(80.f, 80.f)); + ImGui::SameLine(); + ImGui::BeginChild(ImGui::GetID("ach_item"), ImVec2(0, 0), ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None); + ImGui::PushFont(largeFont); + ImGui::Text("%s", ach.title.c_str()); + ImGui::PopFont(); + + { + ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f)); + ImGui::TextWrapped("%s", ach.description.c_str()); + ImGui::TextWrapped("%s", ach.status.c_str()); + } + + scrollWhenDraggingOnVoid(); + ImGui::EndChild(); + } + } + scrollWhenDraggingOnVoid(); + windowDragScroll(); + + ImGui::EndChild(); + ImGui::End(); +} + +} // namespace achievements +#endif // USE_RACHIEVEMENTS diff --git a/core/ui/gui_achievements.h b/core/ui/gui_achievements.h new file mode 100644 index 000000000..c41587799 --- /dev/null +++ b/core/ui/gui_achievements.h @@ -0,0 +1,65 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#include "types.h" +#include "gui_util.h" +#include +#include +#include + +namespace achievements +{ + +class Notification +{ +public: + enum Type { + None, + Login, + GameLoaded, + Unlocked, + Progress, + Mastery, + Challenge, // internal use + Leaderboard, // internal use + Error + }; + void notify(Type type, const std::string& image, const std::string& text1, + const std::string& text2 = {}, const std::string& text3 = {}); + void showChallenge(const std::string& image); + void hideChallenge(const std::string& image); + void showLeaderboard(u32 id, const std::string& text); + void hideLeaderboard(u32 id); + bool draw(); + +private: + u64 startTime = 0; + u64 endTime = 0; + Type type = Type::None; + ImguiFileTexture image; + std::string text[3]; + std::mutex mutex; + std::vector challenges; + std::map leaderboards; +}; + +extern Notification notifier; + +void achievementList(); + +} diff --git a/core/rend/gui_android.cpp b/core/ui/gui_android.cpp similarity index 100% rename from core/rend/gui_android.cpp rename to core/ui/gui_android.cpp diff --git a/core/rend/gui_android.h b/core/ui/gui_android.h similarity index 100% rename from core/rend/gui_android.h rename to core/ui/gui_android.h diff --git a/core/rend/gui_chat.h b/core/ui/gui_chat.h similarity index 96% rename from core/rend/gui_chat.h rename to core/ui/gui_chat.h index 093c78c62..04fd4509e 100644 --- a/core/rend/gui_chat.h +++ b/core/ui/gui_chat.h @@ -78,8 +78,8 @@ public: if (!visible) return; - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0); ImGui::SetNextWindowPos(ImVec2(settings.display.width / 2, settings.display.height) - ScaledVec2(200.f, 220.f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ScaledVec2(400, 220), ImGuiCond_FirstUseEver); ImGui::SetNextWindowBgAlpha(0.7f); @@ -121,7 +121,6 @@ public: ImGui::SetItemDefaultFocus(); } ImGui::End(); - ImGui::PopStyleVar(2); } void receive(int playerNum, const std::string& msg) diff --git a/core/ui/gui_cheats.cpp b/core/ui/gui_cheats.cpp new file mode 100644 index 000000000..93a8d6725 --- /dev/null +++ b/core/ui/gui_cheats.cpp @@ -0,0 +1,139 @@ +/* + Copyright 2021 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#include "gui.h" +#include "imgui.h" +#include "gui_util.h" +#include "cheats.h" +#include "IconsFontAwesome6.h" +#ifdef __ANDROID__ +#include "oslib/storage.h" +#endif + +static void addCheat() +{ + static char cheatName[64]; + static char cheatCode[128]; + centerNextWindow(); + ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(600.f, 400.f))); + ImguiStyleVar _(ImGuiStyleVar_WindowBorderSize, 1); + + if (ImGui::BeginPopupModal("addCheat", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize)) + { + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); + ImGui::AlignTextToFramePadding(); + ImGui::Indent(uiScaled(10)); + ImGui::Text("ADD CHEAT"); + + ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Cancel").x - ImGui::GetStyle().FramePadding.x * 4.f + - ImGui::CalcTextSize("OK").x - ImGui::GetStyle().ItemSpacing.x); + if (ImGui::Button("Cancel")) + ImGui::CloseCurrentPopup(); + ImGui::SameLine(); + if (ImGui::Button("OK")) + { + try { + cheatManager.addGameSharkCheat(cheatName, cheatCode); + ImGui::CloseCurrentPopup(); + cheatName[0] = 0; + cheatCode[0] = 0; + } catch (const FlycastException& e) { + gui_error(e.what()); + } + } + + ImGui::Unindent(uiScaled(10)); + } + + ImGui::BeginChild(ImGui::GetID("input"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_NavFlattened); + { + ImGui::InputText("Name", cheatName, sizeof(cheatName), 0, nullptr, nullptr); + ImGui::InputTextMultiline("Code", cheatCode, sizeof(cheatCode), ImVec2(0, ImGui::GetTextLineHeight() * 8), 0, nullptr, nullptr); + } + ImGui::EndChild(); + ImGui::EndPopup(); + } +} + +static void cheatFileSelected(bool cancelled, std::string path) +{ + if (!cancelled) + cheatManager.loadCheatFile(path); +} + +void gui_cheats() +{ + fullScreenWindow(false); + ImguiStyleVar _(ImGuiStyleVar_WindowBorderSize, 0); + + ImGui::Begin("##main", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); + + { + ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8)); + ImGui::AlignTextToFramePadding(); + ImGui::Indent(uiScaled(10)); + ImGui::Text(ICON_FA_MASK " CHEATS"); + + ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Add").x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().FramePadding.x * 6.f + - ImGui::CalcTextSize("Load").x - ImGui::GetStyle().ItemSpacing.x * 2); + if (ImGui::Button("Add")) + ImGui::OpenPopup("addCheat"); + addCheat(); + ImGui::SameLine(); +#ifdef __ANDROID__ + if (ImGui::Button("Load")) + hostfs::addStorage(false, true, cheatFileSelected); +#else + if (ImGui::Button("Load")) + ImGui::OpenPopup("Select cheat file"); + select_file_popup("Select cheat file", [](bool cancelled, std::string selection) + { + cheatFileSelected(cancelled, selection); + return true; + }, true, "cht"); +#endif + + ImGui::SameLine(); + if (ImGui::Button("Close")) + gui_setState(GuiState::Commands); + + ImGui::Unindent(uiScaled(10)); + } + + ImGui::BeginChild(ImGui::GetID("cheats"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); + { + if (cheatManager.cheatCount() == 0) + ImGui::Text("(No cheat loaded)"); + else + for (size_t i = 0; i < cheatManager.cheatCount(); i++) + { + ImguiID _(("cheat" + std::to_string(i)).c_str()); + bool v = cheatManager.cheatEnabled(i); + if (ImGui::Checkbox(cheatManager.cheatDescription(i).c_str(), &v)) + cheatManager.enableCheat(i, v); + } + } + scrollWhenDraggingOnVoid(); + windowDragScroll(); + + ImGui::EndChild(); + ImGui::End(); +} diff --git a/core/rend/gui_util.cpp b/core/ui/gui_util.cpp similarity index 82% rename from core/rend/gui_util.cpp rename to core/ui/gui_util.cpp index c836f7b9e..d9ee7f84b 100644 --- a/core/rend/gui_util.cpp +++ b/core/ui/gui_util.cpp @@ -25,10 +25,11 @@ #include "types.h" #include "stdclass.h" #include "oslib/storage.h" +#include "imgui_driver.h" #include "imgui.h" #include "imgui_internal.h" -#define STBI_ONLY_JPEG -#define STBI_ONLY_PNG +#include "stdclass.h" +#include "rend/osd.h" #include static std::string select_current_directory = "**home**"; @@ -37,6 +38,7 @@ static std::vector folderFiles; bool subfolders_read; extern int insetLeft, insetRight, insetTop, insetBottom; +extern ImFont *largeFont; void error_popup(); namespace hostfs @@ -51,7 +53,7 @@ void select_file_popup(const char *prompt, StringCallback callback, bool selectFile, const std::string& selectExtension) { fullScreenWindow(true); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); if (ImGui::BeginPopup(prompt, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize )) { @@ -105,51 +107,48 @@ void select_file_popup(const char *prompt, StringCallback callback, title = select_current_directory; ImGui::Text("%s", title.c_str()); - ImGui::BeginChild(ImGui::GetID("dir_list"), ImVec2(0, - 30 * settings.display.uiScale - ImGui::GetStyle().ItemSpacing.y), + ImGui::BeginChild(ImGui::GetID("dir_list"), ImVec2(0, - uiScaled(30) - ImGui::GetStyle().ItemSpacing.y), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20)); - - if (!select_current_directory.empty() && select_current_directory != "/") { - if (ImGui::Selectable(".. Up to Parent Directory")) - { - subfolders_read = false; - select_current_directory = hostfs::storage().getParentPath(select_current_directory); - } - } + ImguiStyleVar _(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20)); - for (const auto& entry : subfolders) - { - if (ImGui::Selectable(entry.name.c_str())) + if (!select_current_directory.empty() && select_current_directory != "/") { - subfolders_read = false; - select_current_directory = entry.path; + if (ImGui::Selectable(".. Up to Parent Directory")) + { + subfolders_read = false; + select_current_directory = hostfs::storage().getParentPath(select_current_directory); + } } - } - ImGui::PushStyleColor(ImGuiCol_Text, { 1, 1, 1, selectFile ? 1.f : 0.3f }); - for (const auto& entry : folderFiles) - { - if (selectFile) - { - if (ImGui::Selectable(entry.name.c_str())) - { - subfolders_read = false; - if (callback(false, entry.path)) - ImGui::CloseCurrentPopup(); - } - } - else - { - ImGui::Text("%s", entry.name.c_str()); - } - } - ImGui::PopStyleColor(); - - scrollWhenDraggingOnVoid(); - windowDragScroll(); - ImGui::PopStyleVar(); + for (const auto& entry : subfolders) + { + if (ImGui::Selectable(entry.name.c_str())) + { + subfolders_read = false; + select_current_directory = entry.path; + } + } + ImguiStyleColor _1(ImGuiCol_Text, { 1, 1, 1, selectFile ? 1.f : 0.3f }); + for (const auto& entry : folderFiles) + { + if (selectFile) + { + if (ImGui::Selectable(entry.name.c_str())) + { + subfolders_read = false; + if (callback(false, entry.path)) + ImGui::CloseCurrentPopup(); + } + } + else + { + ImGui::Text("%s", entry.name.c_str()); + } + } + scrollWhenDraggingOnVoid(); + windowDragScroll(); + } ImGui::EndChild(); if (!selectFile) { @@ -172,7 +171,6 @@ void select_file_popup(const char *prompt, StringCallback callback, error_popup(); ImGui::EndPopup(); } - ImGui::PopStyleVar(); } // See https://github.com/ocornut/imgui/issues/3379 @@ -520,25 +518,24 @@ template bool OptionSlider(const char *name, config::Option& option, bool OptionArrowButtons(const char *name, config::Option& option, int min, int max, const char *help, const char *format) { const float innerSpacing = ImGui::GetStyle().ItemInnerSpacing.x; - ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)); // Left - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_FrameBg]); - float width = ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f; - std::string id = "##" + std::string(name); - ImGui::PushStyleVar(ImGuiStyleVar_DisabledAlpha, 1.0f); - ImGui::BeginDisabled(); - int size = snprintf(nullptr, 0, format, (int)option); - std::string value; - if (size >= 0) + const std::string id = "##" + std::string(name); { - value.resize(size + 1); - snprintf(value.data(), size + 1, format, (int)option); - value.resize(size); + ImguiStyleVar _(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)); // Left + ImguiStyleColor _1(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_FrameBg]); + const float width = ImGui::CalcItemWidth() - innerSpacing * 2.0f - ImGui::GetFrameHeight() * 2.0f; + ImguiStyleVar _2(ImGuiStyleVar_DisabledAlpha, 1.0f); + ImGui::BeginDisabled(); + int size = snprintf(nullptr, 0, format, (int)option); + std::string value; + if (size >= 0) + { + value.resize(size + 1); + snprintf(value.data(), size + 1, format, (int)option); + value.resize(size); + } + ImGui::ButtonEx((value + id).c_str(), ImVec2(width, 0)); + ImGui::EndDisabled(); } - ImGui::ButtonEx((value + id).c_str(), ImVec2(width, 0)); - ImGui::EndDisabled(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); ImGui::SameLine(0.0f, innerSpacing); ImGui::PushButtonRepeat(true); @@ -614,8 +611,8 @@ void fullScreenWindow(bool modal) { if (!modal) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0); if (insetLeft > 0) { @@ -645,7 +642,6 @@ void fullScreenWindow(bool modal) ImGui::Begin("##insetBottom", NULL, ImGuiWindowFlags_NoDecoration); ImGui::End(); } - ImGui::PopStyleVar(2); } ImGui::SetNextWindowPos(ImVec2(insetLeft, insetTop)); ImGui::SetNextWindowSize(ImVec2(ImGui::GetIO().DisplaySize.x - insetLeft - insetRight, ImGui::GetIO().DisplaySize.y - insetTop - insetBottom)); @@ -697,7 +693,71 @@ void windowDragScroll() } } -u8 *loadImage(const std::string& path, int& width, int& height) +static void setUV(float ar, ImVec2& uv0, ImVec2& uv1) +{ + uv0 = { 0.f, 0.f }; + uv1 = { 1.f, 1.f }; + if (ar > 1) + { + uv0.y = -(ar - 1) / 2; + uv1.y = 1 + (ar - 1) / 2; + } + else if (ar != 0) + { + ar = 1 / ar; + uv0.x = -(ar - 1) / 2; + uv1.x = 1 + (ar - 1) / 2; + } +} + +void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col) +{ + ImTextureID id = getId(); + if (id == ImTextureID{}) + ImGui::Dummy(size); + else + { + const float ar = imguiDriver->getAspectRatio(id); + ImVec2 drawSize(size); + if (size.x == 0.f) + drawSize.x = size.y * ar; + else if (size.y == 0.f) + drawSize.y = size.x / ar; + ImVec2 uv0, uv1; + setUV(ar / drawSize.x * drawSize.y, uv0, uv1); + ImGui::Image(id, drawSize, uv0, uv1, tint_col, border_col); + } +} + +void ImguiTexture::draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha) +{ + ImTextureID id = getId(); + if (id == ImTextureID{}) + return; + const float ar = imguiDriver->getAspectRatio(id); + ImVec2 uv0, uv1; + setUV(ar / size.x * size.y, uv0, uv1); + u32 col = alphaOverride(0xffffff, alpha); + drawList->AddImage(id, pos, pos + size, uv0, uv1, col); +} + +bool ImguiTexture::button(const char* str_id, const ImVec2& image_size, const std::string& title, + const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImTextureID id = getId(); + if (id == ImTextureID{}) + return ImGui::Button(title.c_str(), image_size); + else + { + const float ar = imguiDriver->getAspectRatio(id); + const ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2; + ImVec2 uv0, uv1; + setUV(ar / size.x * size.y, uv0, uv1); + return ImGui::ImageButton(str_id, id, size, uv0, uv1, bg_col, tint_col); + } +} + +static u8 *loadImage(const std::string& path, int& width, int& height) { FILE *file = nowide::fopen(path.c_str(), "rb"); if (file == nullptr) @@ -710,6 +770,135 @@ u8 *loadImage(const std::string& path, int& width, int& height) return imgData; } +int ImguiFileTexture::textureLoadCount; + +ImTextureID ImguiFileTexture::getId() +{ + if (path.empty()) + return {}; + ImTextureID id = imguiDriver->getTexture(path); + if (id == ImTextureID() && textureLoadCount < 10) + { + textureLoadCount++; + int width, height; + u8 *imgData = loadImage(path, width, height); + if (imgData != nullptr) + { + try { + id = imguiDriver->updateTextureAndAspectRatio(path, imgData, width, height, nearestSampling); + } catch (...) { + // vulkan can throw during resizing + } + free(imgData); + } + } + return id; +} + +std::future ImguiStateTexture::asyncLoad; + +bool ImguiStateTexture::exists() +{ + std::string path = hostfs::getSavestatePath(config::SavestateSlot, false); + try { + hostfs::storage().getFileInfo(path); + return true; + } catch (...) { + return false; + } +} + +ImTextureID ImguiStateTexture::getId() +{ + std::string path = hostfs::getSavestatePath(config::SavestateSlot, false); + ImTextureID texid = imguiDriver->getTexture(path); + if (texid != ImTextureID()) + return texid; + if (asyncLoad.valid()) + { + if (asyncLoad.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) + return {}; + LoadedPic loadedPic = asyncLoad.get(); + if (loadedPic.data != nullptr) + { + try { + texid = imguiDriver->updateTextureAndAspectRatio(path, loadedPic.data, loadedPic.width, loadedPic.height, nearestSampling); + } catch (...) { + // vulkan can throw during resizing + } + free(loadedPic.data); + } + return texid; + } + asyncLoad = std::async(std::launch::async, []() { + LoadedPic loadedPic{}; + // load savestate info + std::vector pngData; + dc_getStateScreenshot(config::SavestateSlot, pngData); + if (pngData.empty()) + return loadedPic; + + int channels; + stbi_set_flip_vertically_on_load(0); + loadedPic.data = stbi_load_from_memory(&pngData[0], pngData.size(), &loadedPic.width, &loadedPic.height, &channels, STBI_rgb_alpha); + + return loadedPic; + }); + return {}; +} + +void ImguiStateTexture::invalidate() +{ + if (imguiDriver) + { + std::string path = hostfs::getSavestatePath(config::SavestateSlot, false); + imguiDriver->deleteTexture(path); + } +} + +std::array ImguiVmuTexture::Vmus { 0, 1, 2, 3, 4, 5, 6, 7 }; +constexpr float VMU_WIDTH = 96.f; +constexpr float VMU_HEIGHT = 64.f; +constexpr float VMU_PADDING = 8.f; + +ImTextureID ImguiVmuTexture::getId() +{ + if (!vmu_lcd_status[index]) + return {}; + if (idPath.empty()) + idPath = ":vmu:" + std::to_string(index); + ImTextureID texid = imguiDriver->getTexture(idPath); + if (texid == ImTextureID() || vmuLastChanged != ::vmuLastChanged[index]) + { + try { + texid = imguiDriver->updateTexture(idPath, (const u8 *)vmu_lcd_data[index], 48, 32, true); + vmuLastChanged = ::vmuLastChanged[index]; + } catch (...) { + } + } + return texid; +} + +void ImguiVmuTexture::displayVmus(const ImVec2& pos) +{ + const ScaledVec2 size(VMU_WIDTH, VMU_HEIGHT); + const float padding = uiScaled(VMU_PADDING); + ImDrawList *dl = ImGui::GetForegroundDrawList(); + ImVec2 cpos(pos + ScaledVec2(2.f, 0)); // 96 pixels wide + 2 * 2 -> 100 + for (int i = 0; i < 8; i++) + { + if (!vmu_lcd_status[i]) + continue; + + ImTextureID texid = Vmus[i].getId(); + if (texid == ImTextureID()) + continue; + ImVec2 pos_b = cpos + size; + dl->AddImage(texid, cpos, pos_b, ImVec2(0, 1), ImVec2(1, 0), 0x80ffffff); + cpos.y += size.y + padding; + } +} + // Custom version of ImGui::BeginListBox that allows passing window flags bool BeginListBox(const char* label, const ImVec2& size_arg, ImGuiWindowFlags windowFlags) { @@ -751,3 +940,68 @@ bool BeginListBox(const char* label, const ImVec2& size_arg, ImGuiWindowFlags wi BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle, windowFlags); return true; } + +void Toast::show(const std::string& title, const std::string& message, u32 durationMs) +{ + const u64 now = getTimeMs(); + std::lock_guard _{mutex}; + // no start anim if still visible + if (now > endTime + END_ANIM_TIME) + startTime = getTimeMs(); + endTime = now + durationMs; + this->title = title; + this->message = message; +} + +bool Toast::draw() +{ + const u64 now = getTimeMs(); + std::lock_guard _{mutex}; + if (now > endTime + END_ANIM_TIME) { + title.clear(); + message.clear(); + } + if (title.empty() && message.empty()) + return false; + float alpha = 1.f; + if (now > endTime) + // Fade out + alpha = (std::cos((now - endTime) / (float)END_ANIM_TIME * (float)M_PI) + 1.f) / 2.f; + + const ImVec2 displaySize(ImGui::GetIO().DisplaySize); + const float maxW = std::min(uiScaled(640.f), displaySize.x); + ImFont *regularFont = ImGui::GetFont(); + const ImVec2 titleSize = title.empty() ? ImVec2() + : largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, maxW, &title.front(), &title.back() + 1); + const ImVec2 msgSize = message.empty() ? ImVec2() + : regularFont->CalcTextSizeA(regularFont->FontSize, FLT_MAX, maxW, &message.front(), &message.back() + 1); + const ScaledVec2 padding(5.f, 4.f); + const ScaledVec2 spacing(0.f, 2.f); + ImVec2 totalSize(std::max(titleSize.x, msgSize.x), titleSize.y + msgSize.y); + totalSize += padding * 2.f + spacing * (float)(!title.empty() && !message.empty()); + + ImVec2 pos(insetLeft, displaySize.y - totalSize.y); + if (now - startTime < START_ANIM_TIME) + // Slide up + pos.y += totalSize.y * (std::cos((now - startTime) / (float)START_ANIM_TIME * (float)M_PI) + 1.f) / 2.f; + ImDrawList *dl = ImGui::GetForegroundDrawList(); + const ImU32 bg_col = alphaOverride(ImGui::GetColorU32(ImGuiCol_WindowBg), alpha / 2.f); + dl->AddRectFilled(pos, pos + totalSize, bg_col, 0.f); + const ImU32 col = alphaOverride(ImGui::GetColorU32(ImGuiCol_Border), alpha); + dl->AddRect(pos, pos + totalSize, col, 0.f); + + pos += padding; + if (!title.empty()) + { + const ImU32 col = alphaOverride(ImGui::GetColorU32(ImGuiCol_Text), alpha); + dl->AddText(largeFont, largeFont->FontSize, pos, col, &title.front(), &title.back() + 1, maxW); + pos.y += spacing.y + titleSize.y; + } + if (!message.empty()) + { + const ImU32 col = alphaOverride(0xFF00FFFF, alpha); // yellow + dl->AddText(regularFont, regularFont->FontSize, pos, col, &message.front(), &message.back() + 1, maxW); + } + + return true; +} diff --git a/core/rend/gui_util.h b/core/ui/gui_util.h similarity index 53% rename from core/rend/gui_util.h rename to core/ui/gui_util.h index 931c07bd9..8bb28f6ed 100644 --- a/core/rend/gui_util.h +++ b/core/ui/gui_util.h @@ -24,11 +24,13 @@ #include "imgui_internal.h" #include "gui.h" #include "emulator.h" +#include "oslib/oslib.h" #include #include #include #include +#include typedef bool (*StringCallback)(bool cancelled, std::string selection); @@ -58,16 +60,6 @@ static inline void centerNextWindow() ImGuiCond_Always, ImVec2(0.5f, 0.5f)); } -static inline bool operator==(const ImVec2& l, const ImVec2& r) -{ - return l.x == r.x && l.y == r.y; -} - -static inline bool operator!=(const ImVec2& l, const ImVec2& r) -{ - return !(l == r); -} - void fullScreenWindow(bool modal); void windowDragScroll(); @@ -78,6 +70,7 @@ public: { progress.reset(); future = std::async(std::launch::async, [this, path] { + ThreadName _("GameLoader"); emu.loadGame(path.c_str(), &progress); }); } @@ -117,31 +110,21 @@ private: std::future future; }; +static inline float uiScaled(float f) { + return f * settings.display.uiScale; +} + struct ScaledVec2 : public ImVec2 { ScaledVec2() : ImVec2() {} ScaledVec2(float x, float y) - : ImVec2(x * settings.display.uiScale, y * settings.display.uiScale) {} + : ImVec2(uiScaled(x), uiScaled(y)) {} }; inline static ImVec2 min(const ImVec2& l, const ImVec2& r) { return ImVec2(std::min(l.x, r.x), std::min(l.y, r.y)); } -inline static ImVec2 operator+(const ImVec2& l, const ImVec2& r) { - return ImVec2(l.x + r.x, l.y + r.y); -} -inline static ImVec2 operator-(const ImVec2& l, const ImVec2& r) { - return ImVec2(l.x - r.x, l.y - r.y); -} -inline static ImVec2 operator*(const ImVec2& v, float f) { - return ImVec2(v.x * f, v.y * f); -} -inline static ImVec2 operator/(const ImVec2& v, float f) { - return ImVec2(v.x / f, v.y / f); -} - -u8 *loadImage(const std::string& path, int& width, int& height); class DisabledScope { @@ -171,3 +154,159 @@ private: }; bool BeginListBox(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiWindowFlags windowFlags = 0); + +class ImguiID +{ +public: + ImguiID(const std::string& id) + : ImguiID(id.c_str()) {} + ImguiID(const char *id) { + ImGui::PushID(id); + } + ~ImguiID() { + ImGui::PopID(); + } +}; + +class ImguiStyleVar +{ +public: + ImguiStyleVar(ImGuiStyleVar idx, const ImVec2& val) { + ImGui::PushStyleVar(idx, val); + } + ImguiStyleVar(ImGuiStyleVar idx, float val) { + ImGui::PushStyleVar(idx, val); + } + ~ImguiStyleVar() { + ImGui::PopStyleVar(); + } +}; + +class ImguiStyleColor +{ +public: + ImguiStyleColor(ImGuiCol idx, const ImVec4& col) { + ImGui::PushStyleColor(idx, col); + } + ImguiStyleColor(ImGuiCol idx, ImU32 col) { + ImGui::PushStyleColor(idx, col); + } + ~ImguiStyleColor() { + ImGui::PopStyleColor(); + } +}; + +class ImguiTexture +{ +public: + void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1), + const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha = 1.f); + bool button(const char* str_id, const ImVec2& image_size, const std::string& title = {}, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), + const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + + operator ImTextureID() { + return getId(); + } + void setNearestSampling(bool nearestSampling) { + this->nearestSampling = nearestSampling; + } + + virtual ImTextureID getId() = 0; + virtual ~ImguiTexture() = default; + +protected: + bool nearestSampling = false; +}; + +class ImguiFileTexture : public ImguiTexture +{ +public: + ImguiFileTexture() = default; + ImguiFileTexture(const std::string& path) : ImguiTexture(), path(path) {} + + bool operator==(const ImguiFileTexture& other) const { + return other.path == path; + } + ImTextureID getId() override; + + static void resetLoadCount() { + textureLoadCount = 0; + } + +private: + std::string path; + static int textureLoadCount; +}; + +class ImguiStateTexture : public ImguiTexture +{ +public: + ImTextureID getId() override; + + bool exists(); + void invalidate(); + +private: + struct LoadedPic + { + u8 *data; + int width; + int height; + }; + static std::future asyncLoad; +}; + +class ImguiVmuTexture : public ImguiTexture +{ +public: + ImguiVmuTexture(int index = 0) : index(index) {} + + // draw all active vmus in a single column at the given position + static void displayVmus(const ImVec2& pos); + ImTextureID getId() override; + +private: + int index = 0; + std::string idPath; + u64 vmuLastChanged = 0; + + static std::array Vmus; +}; + +static inline bool iconButton(const char *icon, const std::string& label, const ImVec2& size = {}) +{ + ImguiStyleVar _{ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)}; // left aligned + std::string s(5 + label.size(), '\0'); + s.resize(sprintf(s.data(), "%s %s", icon, label.c_str())); + return ImGui::Button(s.c_str(), size); +} + +static inline float iconButtonWidth(const char *icon, const std::string& label) +{ + // TODO avoid doing stuff twice + std::string s(5 + label.size(), '\0'); + s.resize(sprintf(s.data(), "%s %s", icon, label.c_str())); + return ImGui::CalcTextSize(s.c_str()).x + ImGui::GetStyle().FramePadding.x * 2; +} + +static inline ImU32 alphaOverride(ImU32 color, float alpha) { + return (color & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); +} + +class Toast +{ +public: + void show(const std::string& title, const std::string& message, u32 durationMs); + bool draw(); + +private: + static constexpr u64 START_ANIM_TIME = 500; + static constexpr u64 END_ANIM_TIME = 1000; + + std::string title; + std::string message; + u64 startTime = 0; + u64 endTime = 0; + std::mutex mutex; +}; diff --git a/core/rend/imgui_driver.h b/core/ui/imgui_driver.h similarity index 74% rename from core/rend/imgui_driver.h rename to core/ui/imgui_driver.h index 3d17cef1c..bc60b3a25 100644 --- a/core/rend/imgui_driver.h +++ b/core/ui/imgui_driver.h @@ -21,6 +21,7 @@ #include "gui.h" #include #include +#include class ImGuiDriver { @@ -30,36 +31,43 @@ public: } virtual ~ImGuiDriver() = default; + virtual void reset() { + aspectRatios.clear(); + } + virtual void newFrame() = 0; virtual void renderDrawData(ImDrawData* drawData, bool gui_open) = 0; - virtual void displayVmus() {} - virtual void displayCrosshairs() {} - virtual void present() = 0; virtual void setFrameRendered() {} virtual ImTextureID getTexture(const std::string& name) = 0; - virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) = 0; + virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) = 0; + virtual void deleteTexture(const std::string& name) = 0; - ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height) + ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) { - ImTextureID textureId = updateTexture(name, data, width, height); + ImTextureID textureId = updateTexture(name, data, width, height, nearestSampling); if (textureId != ImTextureID()) aspectRatios[textureId] = (float)width / height; return textureId; } - float getAspectRatio(ImTextureID textureId) { + float getAspectRatio(ImTextureID textureId) + { auto it = aspectRatios.find(textureId); if (it != aspectRatios.end()) return it->second; else - return 1; + return 1.f; + } + void updateAspectRatio(ImTextureID textureId, float aspectRatio) { + if (textureId != ImTextureID()) + aspectRatios[textureId] = aspectRatio; } private: - std::unordered_map aspectRatios; + std::unordered_map aspectRatios; // TODO move this out }; extern std::unique_ptr imguiDriver; diff --git a/core/rend/mainui.cpp b/core/ui/mainui.cpp similarity index 98% rename from core/rend/mainui.cpp rename to core/ui/mainui.cpp index b5b174be7..8191679e4 100644 --- a/core/rend/mainui.cpp +++ b/core/ui/mainui.cpp @@ -34,14 +34,12 @@ static bool mainui_enabled; u32 MainFrameCount; static bool forceReinit; -void UpdateInputState(); - bool mainui_rend_frame() { FC_PROFILE_SCOPE; os_DoEvents(); - UpdateInputState(); + os_UpdateInputState(); if (gui_is_open() || gui_state == GuiState::VJoyEdit) { @@ -85,6 +83,7 @@ void mainui_term() void mainui_loop() { + ThreadName _("Flycast-rend"); mainui_enabled = true; mainui_init(); RenderType currentRenderer = config::RendererType; diff --git a/core/rend/mainui.h b/core/ui/mainui.h similarity index 100% rename from core/rend/mainui.h rename to core/ui/mainui.h diff --git a/core/windows/fault_handler.cpp b/core/windows/fault_handler.cpp index e30715499..ac7ce0903 100644 --- a/core/windows/fault_handler.cpp +++ b/core/windows/fault_handler.cpp @@ -169,18 +169,3 @@ void os_UninstallFaultHandler() #endif SetUnhandledExceptionFilter(prevExceptionHandler); } - -double os_GetSeconds() -{ - static double qpfd = []() { - LARGE_INTEGER qpf; - QueryPerformanceFrequency(&qpf); - return 1.0 / qpf.QuadPart; }(); - - LARGE_INTEGER time_now; - - QueryPerformanceCounter(&time_now); - static LARGE_INTEGER time_now_base = time_now; - - return (time_now.QuadPart - time_now_base.QuadPart) * qpfd; -} diff --git a/core/windows/winmain.cpp b/core/windows/winmain.cpp index 9924374c0..a49446d47 100644 --- a/core/windows/winmain.cpp +++ b/core/windows/winmain.cpp @@ -27,9 +27,7 @@ #include #include #include "cfg/option.h" -#include "rend/gui.h" -#else -#include "rawinput.h" +#include "ui/gui.h" #endif #include "oslib/oslib.h" #include "stdclass.h" @@ -37,7 +35,7 @@ #include "log/LogManager.h" #include "sdl/sdl.h" #include "emulator.h" -#include "rend/mainui.h" +#include "ui/mainui.h" #include "oslib/directory.h" #ifdef USE_BREAKPAD #include "breakpad/client/windows/handler/exception_handler.h" @@ -51,26 +49,6 @@ #include #include -void os_SetupInput() -{ - input_sdl_init(); - -#ifndef TARGET_UWP - if (config::UseRawInput) - rawinput::init(); -#endif -} - -void os_TermInput() -{ - input_sdl_quit(); - -#ifndef TARGET_UWP - if (config::UseRawInput) - rawinput::term(); -#endif -} - static void setupPath() { #ifndef TARGET_UWP @@ -110,23 +88,6 @@ static void setupPath() #endif } -void UpdateInputState() -{ - FC_PROFILE_SCOPE; - - input_sdl_handle(); -} - -void os_CreateWindow() -{ - sdl_window_create(); -} - -void os_SetWindowText(const char* text) -{ - sdl_window_set_text(text); -} - static void reserveBottomMemory() { #if defined(_WIN64) && defined(_DEBUG) @@ -433,8 +394,6 @@ int main(int argc, char* argv[]) mainui_loop(); - sdl_window_destroy(); - flycast_term(); os_UninstallFaultHandler(); @@ -508,6 +467,26 @@ void os_RunInstance(int argc, const char *argv[]) } } +void os_SetThreadName(const char *name) +{ +#ifndef TARGET_UWP + nowide::wstackstring wname; + if (wname.convert(name)) + { + static HRESULT (*SetThreadDescription)(HANDLE, PCWSTR); + if (SetThreadDescription == nullptr) + { + // supported in Windows 10, version 1607 or Windows Server 2016 + HINSTANCE libh = LoadLibraryW(L"KernelBase.dll"); + if (libh != NULL) + SetThreadDescription = (HRESULT (*)(HANDLE, PCWSTR))GetProcAddress(libh, "SetThreadDescription"); + } + if (SetThreadDescription != nullptr) + SetThreadDescription(GetCurrentThread(), wname.get()); + } +#endif +} + #ifdef VIDEO_ROUTING #include "SpoutSender.h" #include "SpoutDX.h" diff --git a/core/wsi/gl_context.h b/core/wsi/gl_context.h index 03c9c59a1..0f1e2ae11 100644 --- a/core/wsi/gl_context.h +++ b/core/wsi/gl_context.h @@ -88,7 +88,7 @@ private: #include "sdl.h" -#elif defined(__ANDROID__) || defined(SUPPORT_DISPMANX) || defined(SUPPORT_X11) +#elif defined(__ANDROID__) || defined(SUPPORT_X11) #include "egl.h" diff --git a/core/wsi/sdl.cpp b/core/wsi/sdl.cpp index e187a8e6a..dddc940a5 100644 --- a/core/wsi/sdl.cpp +++ b/core/wsi/sdl.cpp @@ -20,7 +20,7 @@ */ #if defined(USE_SDL) #include "gl_context.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "sdl/sdl.h" #include "cfg/option.h" diff --git a/fonts/Roboto-Regular.ttf.zip b/fonts/Roboto-Regular.ttf.zip new file mode 100644 index 000000000..6bc0533e8 Binary files /dev/null and b/fonts/Roboto-Regular.ttf.zip differ diff --git a/fonts/fa-solid-900.ttf.zip b/fonts/fa-solid-900.ttf.zip new file mode 100644 index 000000000..449985a45 Binary files /dev/null and b/fonts/fa-solid-900.ttf.zip differ diff --git a/shell/android-studio/build.gradle b/shell/android-studio/build.gradle index 55902fd12..565f8c24e 100644 --- a/shell/android-studio/build.gradle +++ b/shell/android-studio/build.gradle @@ -1,9 +1,4 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.3.0' apply false - id 'com.android.library' version '8.3.0' apply false -} - -task clean(type: Delete) { - delete rootProject.buildDir +alias(libs.plugins.android.application) apply false } \ No newline at end of file diff --git a/shell/android-studio/flycast/build.gradle b/shell/android-studio/flycast/build.gradle index 7bd6c0390..09148ef49 100644 --- a/shell/android-studio/flycast/build.gradle +++ b/shell/android-studio/flycast/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.android.application' + alias(libs.plugins.android.application) } def getVersionName = { -> @@ -73,13 +73,17 @@ android { excludes += ['META-INF/DEPENDENCIES'] } } + packaging { + // This is necessary for libadrenotools custom driver loading + jniLibs.useLegacyPackaging = true + } } dependencies { - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'org.apache.httpcomponents.client5:httpclient5:5.0.3' - implementation 'org.slf4j:slf4j-android:1.7.35' + implementation libs.appcompat + implementation libs.commons.lang3 + implementation libs.httpclient5 + implementation libs.slf4j.android implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: []) - implementation 'androidx.documentfile:documentfile:1.0.1' + implementation libs.documentfile } diff --git a/shell/android-studio/flycast/src/main/AndroidManifest.xml b/shell/android-studio/flycast/src/main/AndroidManifest.xml index 63c30b1fd..5b9f6e183 100644 --- a/shell/android-studio/flycast/src/main/AndroidManifest.xml +++ b/shell/android-studio/flycast/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ - + + 0) { + baos.write(buffer, 0, length); + } + is.close(); + baos.close(); + reply[0] = baos.toByteArray(); + + return response.getCode(); + } catch (MalformedURLException e) { + Log.e("flycast", "Malformed URL", e); + } catch (IOException e) { + Log.e("flycast", "I/O error", e); + } catch (SecurityException e) { + Log.e("flycast", "Security error", e); + } catch (Throwable t) { + Log.e("flycast", "Unknown error", t); + } + return 500; + } public int post(String urlString, String[] fieldNames, String[] fieldValues, String[] contentTypes) { try { diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 0692c7c9a..553a83578 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -6,13 +6,13 @@ #include "hw/naomi/naomi_cart.h" #include "audio/audiostream.h" #include "imgread/common.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "rend/osd.h" #include "cfg/cfg.h" #include "log/LogManager.h" #include "wsi/context.h" #include "emulator.h" -#include "rend/mainui.h" +#include "ui/mainui.h" #include "cfg/option.h" #include "stdclass.h" #include "oslib/oslib.h" @@ -99,27 +99,8 @@ void os_DoEvents() { } -void os_CreateWindow() -{ -} - -void UpdateInputState() -{ -} - void common_linux_setup(); -void os_SetupInput() -{ -} -void os_TermInput() -{ -} - -void os_SetWindowText(char const *Text) -{ -} - #if defined(USE_BREAKPAD) static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) { @@ -211,7 +192,7 @@ extern "C" JNIEXPORT jstring JNICALL Java_com_reicast_emulator_emu_JNIdc_initEnv else { static std::string crashPath; - static cThread uploadThread(uploadCrashThread, &crashPath); + static cThread uploadThread(uploadCrashThread, &crashPath, "SentryUpload"); crashPath = get_writable_config_path(""); uploadThread.Start(); } @@ -337,7 +318,7 @@ static void *render_thread_func(void *) return NULL; } -static cThread render_thread(render_thread_func, NULL); +static cThread render_thread(render_thread_func, nullptr, "Flycast-rend"); extern "C" JNIEXPORT void JNICALL Java_com_reicast_emulator_emu_JNIdc_rendinitNative(JNIEnv * env, jobject obj, jobject surface, jint width, jint height) { @@ -653,3 +634,19 @@ extern "C" void abort_message(const char* format, ...) ERROR_LOG(BOOT, "%s", buffer); abort(); } + +std::string getNativeLibraryPath() +{ + JNIEnv *env = jni::env(); + jmethodID getNativeLibDir = env->GetMethodID(env->GetObjectClass(g_activity), "getNativeLibDir", "()Ljava/lang/String;"); + jni::String nativeLibDir(jni::env()->CallObjectMethod(g_activity, getNativeLibDir)); + return nativeLibDir; +} + +std::string getFilesPath() +{ + JNIEnv *env = jni::env(); + jmethodID getInternalFilesDir = env->GetMethodID(env->GetObjectClass(g_activity), "getInternalFilesDir", "()Ljava/lang/String;"); + jni::String filesDir(jni::env()->CallObjectMethod(g_activity, getInternalFilesDir)); + return filesDir; +} diff --git a/shell/android-studio/flycast/src/main/jni/src/android_keyboard.h b/shell/android-studio/flycast/src/main/jni/src/android_keyboard.h index 087af3a5f..f8bc27b83 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_keyboard.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_keyboard.h @@ -688,8 +688,8 @@ private: return false; } - double now = os_GetSeconds(); - if (!barcode.empty() && now - lastBarcodeTime >= 0.5) + u64 now = getTimeMs(); + if (!barcode.empty() && now - lastBarcodeTime >= 500) { INFO_LOG(INPUT, "Barcode timeout"); barcode.clear(); @@ -721,5 +721,5 @@ private: int modifiers = 0; std::string barcode; - double lastBarcodeTime = 0.0; + u64 lastBarcodeTime = 0; }; diff --git a/shell/android-studio/flycast/src/main/jni/src/android_storage.h b/shell/android-studio/flycast/src/main/jni/src/android_storage.h index 165842996..af7ee59b8 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_storage.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_storage.h @@ -38,6 +38,7 @@ public: jgetSubPath = env->GetMethodID(clazz, "getSubPath", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); jgetFileInfo = env->GetMethodID(clazz, "getFileInfo", "(Ljava/lang/String;)Lcom/flycast/emulator/FileInfo;"); jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZ)V"); + jsaveScreenshot = env->GetMethodID(clazz, "saveScreenshot", "(Ljava/lang/String;[B)V"); } bool isKnownPath(const std::string& path) override { @@ -137,6 +138,15 @@ public: } } + void saveScreenshot(const std::string& name, const std::vector& data) + { + jni::String jname(name); + jni::ByteArray jdata(data.size()); + jdata.setData(&data[0]); + jni::env()->CallVoidMethod(jstorage, jsaveScreenshot, (jstring)jname, (jbyteArray)jdata); + checkException(); + } + private: void checkException() { @@ -184,6 +194,7 @@ private: jmethodID jaddStorage; jmethodID jgetSubPath; jmethodID jgetFileInfo; + jmethodID jsaveScreenshot; // FileInfo accessors lazily initialized to avoid having to load the class jmethodID jgetName = nullptr; jmethodID jgetPath = nullptr; @@ -201,6 +212,11 @@ Storage& customStorage() return *androidStorage; } +void saveScreenshot(const std::string& name, const std::vector& data) +{ + return static_cast(customStorage()).saveScreenshot(name, data); +} + } // namespace hostfs extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_AndroidStorage_addStorageCallback(JNIEnv *env, jobject obj, jstring path) diff --git a/shell/android-studio/flycast/src/main/jni/src/http_client.h b/shell/android-studio/flycast/src/main/jni/src/http_client.h index 83072c95e..602bf21ef 100644 --- a/shell/android-studio/flycast/src/main/jni/src/http_client.h +++ b/shell/android-studio/flycast/src/main/jni/src/http_client.h @@ -17,7 +17,7 @@ along with Flycast. If not, see . */ #pragma once -#include "rend/boxart/http_client.h" +#include "oslib/http_client.h" #include "jni_util.h" namespace http { @@ -25,6 +25,7 @@ namespace http { static jobject HttpClient; static jmethodID openUrlMid; static jmethodID postMid; + static jmethodID postRawMid; void init() { } @@ -35,8 +36,10 @@ namespace http { jni::ObjectArray contentOut(1); jni::ObjectArray contentTypeOut(1); - int httpStatus = jni::env()->CallIntMethod(HttpClient, openUrlMid, (jstring)jurl, (jobjectArray)contentOut, - (jobjectArray)contentTypeOut); + int httpStatus = jni::env()->CallIntMethod(HttpClient, openUrlMid, + static_cast(jurl), + static_cast(contentOut), + static_cast(contentTypeOut)); content = contentOut[0]; contentType = contentTypeOut[0]; @@ -44,6 +47,23 @@ namespace http { return httpStatus; } + int post(const std::string &url, const char *payload, const char *contentType, std::vector& reply) + { + jni::String jurl(url); + jni::String jpayload(payload); + jni::String jcontentType(contentType); + jni::ObjectArray replyOut(1); + + int httpStatus = jni::env()->CallIntMethod(HttpClient, postRawMid, + static_cast(jurl), + static_cast(jpayload), + static_cast(jcontentType), + static_cast(replyOut)); + reply = replyOut[0]; + + return httpStatus; + } + int post(const std::string &url, const std::vector& fields) { jni::String jurl(url); @@ -58,7 +78,11 @@ namespace http { contentTypes.setAt(i, fields[i].contentType); } - int httpStatus = jni::env()->CallIntMethod(HttpClient, postMid, (jstring)jurl, (jobjectArray)names, (jobjectArray)values, (jobjectArray)contentTypes); + int httpStatus = jni::env()->CallIntMethod(HttpClient, postMid, + static_cast(jurl), + static_cast(names), + static_cast(values), + static_cast(contentTypes)); return httpStatus; } @@ -73,4 +97,5 @@ extern "C" JNIEXPORT void JNICALL Java_com_reicast_emulator_emu_HttpClient_nativ http::HttpClient = env->NewGlobalRef(obj); http::openUrlMid = env->GetMethodID(env->GetObjectClass(obj), "openUrl", "(Ljava/lang/String;[[B[Ljava/lang/String;)I"); http::postMid = env->GetMethodID(env->GetObjectClass(obj), "post", "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)I"); + http::postRawMid = env->GetMethodID(env->GetObjectClass(obj), "post", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[[B)I"); } diff --git a/shell/android-studio/gradle.properties b/shell/android-studio/gradle.properties index a03b35489..534063aa9 100644 --- a/shell/android-studio/gradle.properties +++ b/shell/android-studio/gradle.properties @@ -8,8 +8,8 @@ # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK diff --git a/shell/android-studio/gradle/libs.versions.toml b/shell/android-studio/gradle/libs.versions.toml new file mode 100644 index 000000000..e0052ae4d --- /dev/null +++ b/shell/android-studio/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +agp = "8.4.1" +appcompat = "1.3.1" +commonsLang3 = "3.12.0" +documentfile = "1.0.1" +httpclient5 = "5.0.3" +slf4jAndroid = "1.7.35" + +[libraries] +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" } +documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } +httpclient5 = { module = "org.apache.httpcomponents.client5:httpclient5", version.ref = "httpclient5" } +slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/shell/android-studio/gradle/wrapper/gradle-wrapper.properties b/shell/android-studio/gradle/wrapper/gradle-wrapper.properties index 3c224774d..725e2248b 100644 --- a/shell/android-studio/gradle/wrapper/gradle-wrapper.properties +++ b/shell/android-studio/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 05 22:55:18 CEST 2023 +#Sun May 19 21:28:41 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/shell/android-studio/settings.gradle b/shell/android-studio/settings.gradle index 3654d9115..f4b446937 100644 --- a/shell/android-studio/settings.gradle +++ b/shell/android-studio/settings.gradle @@ -1,6 +1,12 @@ pluginManagement { repositories { - google() + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() gradlePluginPortal() } diff --git a/shell/apple/common/http_client.mm b/shell/apple/common/http_client.mm index e1f411533..e577face7 100644 --- a/shell/apple/common/http_client.mm +++ b/shell/apple/common/http_client.mm @@ -17,7 +17,7 @@ along with Flycast. If not, see . */ #import -#include "rend/boxart/http_client.h" +#include "oslib/http_client.h" namespace http { @@ -46,6 +46,40 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return [httpResponse statusCode]; } +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) +{ + NSString *nsurl = [NSString stringWithCString:url.c_str() + encoding:[NSString defaultCStringEncoding]]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:nsurl]]; + [request setHTTPMethod:@"POST"]; + [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; + [request setHTTPShouldHandleCookies:NO]; + + size_t payloadSize = strlen(payload); + [request setHTTPBody:[NSData dataWithBytes:payload length:payloadSize]]; + NSString *postLength = [NSString stringWithFormat:@"%ld", (unsigned long)payloadSize]; + [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; + NSString *nscontentType = contentType != nullptr ? [NSString stringWithCString:contentType + encoding:[NSString defaultCStringEncoding]] + : @"application/x-www-form-urlencoded"; + [request setValue:nscontentType forHTTPHeaderField:@"Content-Type"]; + + NSURLResponse *response = nil; + NSError *error = nil; + NSData *data = [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + if (error != nil) + return 500; + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + + reply.clear(); + reply.insert(reply.begin(), (const u8 *)[data bytes], (const u8 *)[data bytes] + [data length]); + + return [httpResponse statusCode]; +} + int post(const std::string& url, const std::vector& fields) { NSString *nsurl = [NSString stringWithCString:url.c_str() diff --git a/shell/apple/common/util.mm b/shell/apple/common/util.mm new file mode 100644 index 000000000..2e282aa60 --- /dev/null +++ b/shell/apple/common/util.mm @@ -0,0 +1,27 @@ +/* + Copyright 2023 flyinghead + + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . +*/ +#import +#include "oslib/oslib.h" + +void os_SetThreadName(const char *name) +{ + NSString *nsname = [NSString stringWithCString:name + encoding:[NSString defaultCStringEncoding]]; + [[NSThread currentThread] setName:nsname]; +} diff --git a/shell/apple/emulator-ios/emulator/AppDelegate.mm b/shell/apple/emulator-ios/emulator/AppDelegate.mm index cc568b84f..592f5311e 100644 --- a/shell/apple/emulator-ios/emulator/AppDelegate.mm +++ b/shell/apple/emulator-ios/emulator/AppDelegate.mm @@ -23,10 +23,12 @@ #import "AppDelegate.h" #import +#include +#include #include "emulator.h" #include "log/LogManager.h" #include "cfg/option.h" -#include "rend/gui.h" +#include "ui/gui.h" static bool emulatorRunning; @@ -50,6 +52,11 @@ static bool emulatorRunning; if (error != nil) NSLog(@"AVAudioSession.setActive: %@", error); + if (getppid() != 1) { + /* Make LLDB ignore EXC_BAD_ACCESS for debugging */ + task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, 0); + } + return YES; } diff --git a/shell/apple/emulator-ios/emulator/EmulatorView.mm b/shell/apple/emulator-ios/emulator/EmulatorView.mm index e10491774..d77e2eba0 100644 --- a/shell/apple/emulator-ios/emulator/EmulatorView.mm +++ b/shell/apple/emulator-ios/emulator/EmulatorView.mm @@ -20,7 +20,7 @@ #import "EmulatorView.h" #include "types.h" -#include "rend/gui.h" +#include "ui/gui.h" #include "ios_gamepad.h" @implementation EmulatorView { diff --git a/shell/apple/emulator-ios/emulator/FlycastViewController.mm b/shell/apple/emulator-ios/emulator/FlycastViewController.mm index 07cd93a42..fb437bead 100644 --- a/shell/apple/emulator-ios/emulator/FlycastViewController.mm +++ b/shell/apple/emulator-ios/emulator/FlycastViewController.mm @@ -33,7 +33,7 @@ #include "types.h" #include "wsi/context.h" -#include "rend/mainui.h" +#include "ui/mainui.h" #include "emulator.h" #include "log/LogManager.h" #include "stdclass.h" @@ -54,8 +54,6 @@ std::map> IOSGamepad::controllers; std::map> IOSKeyboard::keyboards; std::map> IOSMouse::mice; -void common_linux_setup(); - static bool lockedPointer; static void updatePointerLock(Event event, void *) { @@ -210,7 +208,7 @@ static void updateAudioSession(Event event, void *) } #endif - common_linux_setup(); + os_InstallFaultHandler(); flycast_init(0, nullptr); config::ContentPath.get().clear(); @@ -738,11 +736,11 @@ void pickIosFile() const char *getIosJitStatus() { - static double lastCheckTime; - if (!iosJitAuthorized && os_GetSeconds() - lastCheckTime > 10.0) + static u64 lastCheckTime; + if (!iosJitAuthorized && getTimeMs() - lastCheckTime > 10000) { [flycastViewController altKitStart]; - lastCheckTime = os_GetSeconds(); + lastCheckTime = getTimeMs(); } return iosJitStatus.c_str(); } diff --git a/shell/apple/emulator-ios/emulator/ios_gamepad.h b/shell/apple/emulator-ios/emulator/ios_gamepad.h index 5b4b33317..3d768c713 100644 --- a/shell/apple/emulator-ios/emulator/ios_gamepad.h +++ b/shell/apple/emulator-ios/emulator/ios_gamepad.h @@ -23,7 +23,7 @@ #include #include "input/gamepad_device.h" #include "input/mouse.h" -#include "rend/gui.h" +#include "ui/gui.h" enum IOSButton { IOS_BTN_A = 1, @@ -139,14 +139,14 @@ public: class IOSGamepad : public GamepadDevice { public: - IOSGamepad(int port, GCController *controller) : GamepadDevice(port, "iOS"), gcController(controller) + IOSGamepad(int port, GCController *controller, int index) : GamepadDevice(port, "iOS"), gcController(controller) { set_maple_port(port); if (gcController.vendorName != nullptr) _name = [gcController.vendorName UTF8String]; else _name = "MFi Gamepad"; - //_unique_id = ? + _unique_id = "ios-" + std::to_string(index); INFO_LOG(INPUT, "iOS: Opened joystick %d: '%s'", port, _name.c_str()); loadMapping(); @@ -459,8 +459,9 @@ public: return; if (controller.extendedGamepad == nullptr && controller.gamepad == nullptr) return; - int port = std::min((int)controllers.size(), 3); - controllers[controller] = std::make_shared(port, controller); + int index = (int)controllers.size(); + int port = std::min(index, 3); + controllers[controller] = std::make_shared(port, controller, index); GamepadDevice::Register(controllers[controller]); } diff --git a/shell/apple/emulator-ios/emulator/ios_main.mm b/shell/apple/emulator-ios/emulator/ios_main.mm index a602d3e07..0c1fc8c02 100644 --- a/shell/apple/emulator-ios/emulator/ios_main.mm +++ b/shell/apple/emulator-ios/emulator/ios_main.mm @@ -18,10 +18,9 @@ along with Flycast. If not, see . */ #import - +#include "types.h" +#include #include -#include -#include int darw_printf(const char* text,...) { @@ -40,24 +39,6 @@ int darw_printf(const char* text,...) void os_DoEvents() { } -void os_SetWindowText(const char* t) { -} - -void os_CreateWindow() { - if (getppid() != 1) { - /* Make LLDB ignore EXC_BAD_ACCESS for debugging */ - task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, 0); - } -} - -void UpdateInputState() { -} - -void os_SetupInput() { -} -void os_TermInput() { -} - std::string os_Locale(){ return [[[NSLocale preferredLanguages] objectAtIndex:0] UTF8String]; } @@ -65,3 +46,15 @@ std::string os_Locale(){ std::string os_PrecomposedString(std::string string){ return [[[NSString stringWithUTF8String:string.c_str()] precomposedStringWithCanonicalMapping] UTF8String]; } + +namespace hostfs +{ + +void saveScreenshot(const std::string& name, const std::vector& data) +{ + NSData* imageData = [NSData dataWithBytes:&data[0] length:data.size()]; + UIImage* pngImage = [UIImage imageWithData:imageData]; + UIImageWriteToSavedPhotosAlbum(pngImage, nil, nil, nil); +} + +} diff --git a/shell/apple/emulator-ios/plist.in b/shell/apple/emulator-ios/plist.in index 5d7330ccc..e86c2f7cd 100644 --- a/shell/apple/emulator-ios/plist.in +++ b/shell/apple/emulator-ios/plist.in @@ -70,6 +70,8 @@ NSMicrophoneUsageDescription Flycast requires microphone access to emulate the Dreamcast microphone + NSPhotoLibraryAddUsageDescription + Flycast can save screenshots to your Photo library. UISupportsDocumentBrowser LSSupportsOpeningDocumentsInPlace diff --git a/shell/apple/emulator-osx/emulator-osx/SDLApplicationDelegate.mm b/shell/apple/emulator-osx/emulator-osx/SDLApplicationDelegate.mm index 1ebbe7e9d..0b2dd71d0 100644 --- a/shell/apple/emulator-osx/emulator-osx/SDLApplicationDelegate.mm +++ b/shell/apple/emulator-osx/emulator-osx/SDLApplicationDelegate.mm @@ -9,7 +9,7 @@ #include "emulator.h" #include /* for MAXPATHLEN */ #include -#include "rend/gui.h" +#include "ui/gui.h" #include "oslib/oslib.h" #ifdef USE_BREAKPAD diff --git a/shell/apple/emulator-osx/emulator-osx/osx-main.mm b/shell/apple/emulator-osx/emulator-osx/osx-main.mm index 6b3e34ab3..fe58521c1 100644 --- a/shell/apple/emulator-osx/emulator-osx/osx-main.mm +++ b/shell/apple/emulator-osx/emulator-osx/osx-main.mm @@ -21,7 +21,7 @@ #include "stdclass.h" #include "oslib/oslib.h" #include "emulator.h" -#include "rend/mainui.h" +#include "ui/mainui.h" #include int darw_printf(const char* text, ...) @@ -47,10 +47,6 @@ int darw_printf(const char* text, ...) return 0; } -void os_SetWindowText(const char * text) { - puts(text); -} - void os_DoEvents() { #if defined(USE_SDL) NSMenuItem *editMenuItem = [[NSApp mainMenu] itemAtIndex:1]; @@ -58,43 +54,6 @@ void os_DoEvents() { #endif } -void UpdateInputState() { -#if defined(USE_SDL) - input_sdl_handle(); -#endif -} - -void os_CreateWindow() { -#ifdef DEBUG - int ret = task_set_exception_ports( - mach_task_self(), - EXC_MASK_BAD_ACCESS, - MACH_PORT_NULL, - EXCEPTION_DEFAULT, - 0); - - if (ret != KERN_SUCCESS) { - printf("task_set_exception_ports: %s\n", mach_error_string(ret)); - } -#endif - sdl_window_create(); -} - -void os_SetupInput() -{ -#if defined(USE_SDL) - input_sdl_init(); -#endif -} - -void os_TermInput() -{ -#if defined(USE_SDL) - input_sdl_quit(); -#endif -} - -void common_linux_setup(); static int emu_flycast_init(); static void emu_flycast_term() @@ -172,7 +131,6 @@ extern "C" int SDL_main(int argc, char *argv[]) mainui_loop(); - sdl_window_destroy(); emu_flycast_term(); os_UninstallFaultHandler(); @@ -182,7 +140,7 @@ extern "C" int SDL_main(int argc, char *argv[]) static int emu_flycast_init() { LogManager::Init(); - common_linux_setup(); + os_InstallFaultHandler(); NSArray *arguments = [[NSProcessInfo processInfo] arguments]; unsigned long argc = [arguments count]; char **argv = (char **)malloc(argc * sizeof(char*)); @@ -201,6 +159,19 @@ static int emu_flycast_init() for (unsigned long i = 0; i < paramCount; i++) free(argv[i]); free(argv); + +#if defined(DEBUG) || defined(DEBUGFAST) + int ret = task_set_exception_ports( + mach_task_self(), + EXC_MASK_BAD_ACCESS, + MACH_PORT_NULL, + EXCEPTION_DEFAULT, + 0); + + if (ret != KERN_SUCCESS) { + printf("task_set_exception_ports: %s\n", mach_error_string(ret)); + } +#endif return rc; } @@ -284,3 +255,14 @@ void os_VideoRoutingTermVk() [syphonMtlServer release]; syphonMtlServer = NULL; } + +namespace hostfs +{ + +std::string getScreenshotsPath() +{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES); + return [[paths objectAtIndex:0] UTF8String]; +} + +} diff --git a/shell/apple/generate_xcode_project.command b/shell/apple/generate_xcode_project.command index 13a16475d..311c39ef1 100755 --- a/shell/apple/generate_xcode_project.command +++ b/shell/apple/generate_xcode_project.command @@ -34,5 +34,5 @@ fi cmake -B build -DCMAKE_BUILD_TYPE=Release $option -DCMAKE_XCODE_GENERATE_SCHEME=YES -G "Xcode" nl=$'\n' -sed -i '' -E "s/launchStyle/customLLDBInitFile = \"\$(SRCROOT)\/shell\/apple\/\\${lldbinitfolder}\/LLDBInitFile\"\\${nl}launchStyle/g" build/flycast.xcodeproj/xcshareddata/xcschemes/flycast.xcscheme +/usr/bin/sed -i '' -E "s/launchStyle/customLLDBInitFile = \"\$(SRCROOT)\/shell\/apple\/\\${lldbinitfolder}\/LLDBInitFile\"\\${nl}launchStyle/g" build/flycast.xcodeproj/xcshareddata/xcschemes/flycast.xcscheme open build/flycast.xcodeproj diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index aece2c6c4..ca2c8209f 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -64,6 +64,7 @@ #include "cfg/option.h" #include "version.h" #include "rend/transform_matrix.h" +#include "oslib/oslib.h" constexpr char slash = path_default_slash_c(); @@ -125,6 +126,9 @@ static bool platformIsDreamcast = true; static bool platformIsArcade = false; static bool threadedRenderingEnabled = true; static bool oitEnabled = false; +#if defined(HAVE_OIT) || defined(HAVE_VULKAN) || defined(HAVE_D3D11) +static bool perPixelChecked = false; +#endif static bool autoSkipFrameEnabled = false; #ifdef _OPENMP static bool textureUpscaleEnabled = false; @@ -193,7 +197,6 @@ static retro_rumble_interface rumble; static void refresh_devices(bool first_startup); static void init_disk_control_interface(); static bool read_m3u(const char *file); -void gui_display_notification(const char *msg, int duration); static void updateVibration(u32 port, float power, float inclination, u32 durationMs); static std::string game_data; @@ -218,7 +221,6 @@ static std::vector disk_paths; static std::vector disk_labels; static bool disc_tray_open = false; -void UpdateInputState(); static bool set_variable_visibility(void); void retro_set_video_refresh(retro_video_refresh_t cb) @@ -1013,7 +1015,7 @@ static void update_variables(bool first_startup) | 0xff000000; vmu_lcd_status[i * 2] = false; - vmu_lcd_changed[i * 2] = true; + vmuLastChanged[i * 2] = getTimeMs(); vmu_screen_params[i].vmu_screen_position = UPPER_LEFT; vmu_screen_params[i].vmu_screen_size_mult = 1; vmu_screen_params[i].vmu_pixel_on_R = VMU_SCREEN_COLOR_MAP[VMU_DEFAULT_ON].r; @@ -1167,7 +1169,7 @@ void retro_run() emu.start(); poll_cb(); - UpdateInputState(); + os_UpdateInputState(); bool fastforward = false; if (environ_cb(RETRO_ENVIRONMENT_GET_FASTFORWARDING, &fastforward)) settings.input.fastForwardMode = fastforward; @@ -1187,7 +1189,7 @@ void retro_run() } } catch (const FlycastException& e) { ERROR_LOG(COMMON, "%s", e.what()); - gui_display_notification(e.what(), 5000); + os_notify(e.what(), 5000); environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, NULL); } @@ -1212,7 +1214,7 @@ static bool loadGame() emu.loadGame(game_data.c_str()); } catch (const FlycastException& e) { ERROR_LOG(BOOT, "%s", e.what()); - gui_display_notification(e.what(), 5000); + os_notify(e.what(), 5000); retro_unload_game(); return false; } @@ -1241,6 +1243,42 @@ void retro_reset() emu.start(); } +#if defined(HAVE_OIT) || defined(HAVE_VULKAN) || defined(HAVE_D3D11) +void check_per_pixel_opt(void) +{ + // Check if per-pixel is supported, if not we hide the option + if (!GraphicsContext::Instance()->hasPerPixel()) + { + for (unsigned i = 0; option_defs_us[i].key != NULL; i++) + { + // Looking for the alpha sorting core option... + if (!strcmp(option_defs_us[i].key, CORE_OPTION_NAME "_alpha_sorting")) + { + for (unsigned j = 0; option_defs_us[i].values[j].value != NULL; j++) + { + // ... then for the per-pixel choice... + if (!strcmp(option_defs_us[i].values[j].value, "per-pixel (accurate)")) + { + // ... null it out... + option_defs_us[i].values[j] = { NULL, NULL }; + + // ... and finally refresh core options. + bool optionCategoriesSupported = false; + libretro_set_core_options(environ_cb, &optionCategoriesSupported); + categoriesSupported |= optionCategoriesSupported; + + break; + } + } + break; + } + } + NOTICE_LOG(RENDERER, "Current renderer does not support 'Per-Pixel' Alpha Sorting."); + } + perPixelChecked = true; +} +#endif + #if defined(HAVE_OPENGL) || defined(HAVE_OPENGLES) static void context_reset() { @@ -1251,6 +1289,10 @@ static void context_reset() rend_term_renderer(); theGLContext.init(); rend_init_renderer(); +#ifdef HAVE_OIT + if (!perPixelChecked) + check_per_pixel_opt(); +#endif } static void context_destroy() @@ -1812,6 +1854,8 @@ static void retro_vk_context_reset() theVulkanContext.init((retro_hw_render_interface_vulkan *)vulkan); rend_term_renderer(); rend_init_renderer(); + if (!perPixelChecked) + check_per_pixel_opt(); } static void retro_vk_context_destroy() @@ -1946,6 +1990,8 @@ static void dx11_context_reset() else if (config::RendererType != RenderType::DirectX11_OIT) config::RendererType = RenderType::DirectX11; rend_init_renderer(); + if (!perPixelChecked) + check_per_pixel_opt(); } static void dx11_context_destroy() @@ -1985,7 +2031,7 @@ bool retro_load_game(const struct retro_game_info *game) if (environ_cb(RETRO_ENVIRONMENT_GET_JIT_CAPABLE, &can_jit) && !can_jit) { // jit is required both for performance and for audio. trying to run // without the jit will cause a crash. - gui_display_notification("Cannot run without JIT", 5000); + os_notify("Cannot run without JIT", 5000); return false; } #endif @@ -2455,11 +2501,6 @@ void retro_rend_present() is_dupe = false; } -static uint32_t get_time_ms() -{ - return (uint32_t)(os_GetSeconds() * 1000.0); -} - static void get_analog_stick( retro_input_state_t input_state_cb, int player_index, int stick, @@ -2916,14 +2957,14 @@ static void UpdateInputState(u32 port) } if (rumble.set_rumble_state != NULL && vib_stop_time[port] > 0) { - if (get_time_ms() >= vib_stop_time[port]) + if (getTimeMs() >= vib_stop_time[port]) { vib_stop_time[port] = 0; rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, 0); } else if (vib_delta[port] > 0.0) { - u32 rem_time = vib_stop_time[port] - get_time_ms(); + u32 rem_time = vib_stop_time[port] - getTimeMs(); rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, 65535 * vib_strength[port] * rem_time * vib_delta[port]); } } @@ -3315,7 +3356,7 @@ static void UpdateInputState(u32 port) } } -void UpdateInputState() +void os_UpdateInputState() { UpdateInputState(0); UpdateInputState(1); @@ -3331,7 +3372,7 @@ static void updateVibration(u32 port, float power, float inclination, u32 durati vib_strength[port] = power; rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, (u16)(65535 * power)); - vib_stop_time[port] = get_time_ms() + durationMs; + vib_stop_time[port] = getTimeMs() + durationMs; vib_delta[port] = inclination; } @@ -3661,12 +3702,10 @@ static bool read_m3u(const char *file) return disk_index != 0; } -void gui_display_notification(const char *msg, int duration) +void os_notify(const char *msg, int durationMs, const char *details) { retro_message retromsg; retromsg.msg = msg; - retromsg.frames = duration / 17; + retromsg.frames = durationMs / 17; environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &retromsg); } - -void os_RunInstance(int argc, const char *argv[]) { } diff --git a/shell/libretro/oslib.cpp b/shell/libretro/oslib.cpp index 4f7bfa872..cf70ad53e 100644 --- a/shell/libretro/oslib.cpp +++ b/shell/libretro/oslib.cpp @@ -130,12 +130,7 @@ std::string getTextureDumpPath() } -void dc_savestate(int index = 0) -{ - die("unsupported"); -} - -void dc_loadstate(int index = 0) -{ - die("unsupported"); +#ifdef _WIN32 +void os_SetThreadName(const char *name) { } +#endif diff --git a/core/linux/libnx_vmem.cpp b/shell/switch/libnx_vmem.cpp similarity index 100% rename from core/linux/libnx_vmem.cpp rename to shell/switch/libnx_vmem.cpp diff --git a/shell/switch/switch_gamepad.h b/shell/switch/switch_gamepad.h index b21c7b96e..e6956eff1 100644 --- a/shell/switch/switch_gamepad.h +++ b/shell/switch/switch_gamepad.h @@ -59,23 +59,23 @@ public: memcpy(&vibValues[1], &vibValues[0], sizeof(HidVibrationValue)); hidSendVibrationValues(getDeviceHandle(), vibValues, 2); if (power != 0.f) - vib_stop_time = os_GetSeconds() + duration_ms / 1000.0; + vib_stop_time = getTimeMs() + duration_ms; else - vib_stop_time = 0.0; + vib_stop_time = 0; } void update_rumble() override { if (!rumbleEnabled || vib_stop_time == 0.0) return; - int rem_time = (vib_stop_time - os_GetSeconds()) * 1000; + int rem_time = vib_stop_time - getTimeMs(); if (rem_time <= 0) { HidVibrationValue vibValues[2]{}; vibValues[0].freq_low = vibValues[1].freq_low = 160.f; vibValues[0].freq_high = vibValues[1].freq_high = 320.f; hidSendVibrationValues(getDeviceHandle(), vibValues, 2); - vib_stop_time = 0.0; + vib_stop_time = 0; } } diff --git a/shell/switch/switch_main.cpp b/shell/switch/switch_main.cpp new file mode 100644 index 000000000..2d710796a --- /dev/null +++ b/shell/switch/switch_main.cpp @@ -0,0 +1,72 @@ +/* + This file is part of Flycast. + + Flycast 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. + + Flycast 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 Flycast. If not, see . + */ +#ifndef LIBRETRO +#include "nswitch.h" +#include "stdclass.h" +#include "log/LogManager.h" +#include "emulator.h" +#include "ui/mainui.h" +#include "oslib/directory.h" +#include +#include + +int main(int argc, char *argv[]) +{ + socketInitializeDefault(); + nxlinkStdio(); + //appletSetFocusHandlingMode(AppletFocusHandlingMode_NoSuspend); + + LogManager::Init(); + + // Set directories + flycast::mkdir("/flycast", 0755); + flycast::mkdir("/flycast/data", 0755); + set_user_config_dir("/flycast/"); + set_user_data_dir("/flycast/data/"); + + add_system_config_dir("/flycast"); + add_system_config_dir("./"); + add_system_data_dir("/flycast/data/"); + add_system_data_dir("./"); + add_system_data_dir("data/"); + + if (flycast_init(argc, argv)) + die("Flycast initialization failed"); + + mainui_loop(); + + flycast_term(); + + socketExit(); + + return 0; +} + +void os_DoEvents() +{ +} + +namespace hostfs +{ + +void saveScreenshot(const std::string& name, const std::vector& data) +{ + throw FlycastException("Not supported on Switch"); +} + +} +#endif //!LIBRETRO diff --git a/shell/switch/ucontext.h b/shell/switch/ucontext.h index c2b2312c0..c14f1ae09 100644 --- a/shell/switch/ucontext.h +++ b/shell/switch/ucontext.h @@ -1,5 +1,5 @@ #pragma once -#include +#include "nswitch.h" typedef struct { diff --git a/shell/uwp/http_client.cpp b/shell/uwp/http_client.cpp index af787c925..68de14302 100644 --- a/shell/uwp/http_client.cpp +++ b/shell/uwp/http_client.cpp @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ -#include "rend/boxart/http_client.h" +#include "oslib/http_client.h" #include "stdclass.h" namespace http { @@ -89,7 +89,60 @@ int get(const std::string& url, std::vector& content, std::string& contentTy } } -int post(const std::string& url, const std::vector& fields) { +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) +{ + nowide::wstackstring wurl; + if (!wurl.convert(url.c_str())) + return 500; + nowide::wstackstring wpayload; + if (!wpayload.convert(payload)) + return 500; + nowide::wstackstring wcontentType; + if (contentType != nullptr && !wcontentType.convert(contentType)) + return 500; + try + { + Uri^ uri = ref new Uri(ref new String(wurl.get())); + HttpStringContent^ content = ref new HttpStringContent(ref new String(wpayload.get())); + content->Headers->ContentLength = strlen(payload); + if (contentType != nullptr) + content->Headers->ContentType = ref new HttpMediaTypeHeaderValue(ref new String(wcontentType.get())); + + IAsyncOperationWithProgress^ op = httpClient->PostAsync(uri, content); + cResetEvent asyncEvent; + op->Completed = ref new AsyncOperationWithProgressCompletedHandler( + [&asyncEvent](IAsyncOperationWithProgress^, AsyncStatus) { + asyncEvent.Set(); + }); + asyncEvent.Wait(); + HttpResponseMessage^ resp = op->GetResults(); + + if (resp->IsSuccessStatusCode) + { + IHttpContent^ httpContent = resp->Content; + IAsyncOperationWithProgress^ readOp = httpContent->ReadAsBufferAsync(); + asyncEvent.Reset(); + readOp->Completed = ref new AsyncOperationWithProgressCompletedHandler( + [&asyncEvent](IAsyncOperationWithProgress^, AsyncStatus) { + asyncEvent.Set(); + }); + asyncEvent.Wait(); + IBuffer^ buffer = readOp->GetResults(); + + Array^ array = ref new Array(buffer->Length); + DataReader::FromBuffer(buffer)->ReadBytes(array); + reply = std::vector(array->begin(), array->end()); + } + return (int)resp->StatusCode; + } + catch (Exception^ e) + { + WARN_LOG(COMMON, "http::post error %.*S", e->Message->Length(), e->Message->Data()); + return 500; + } +} + +int post(const std::string & url, const std::vector&fields) { // not implemented return 500; } diff --git a/tests/src/serialize_test.cpp b/tests/src/serialize_test.cpp index a057e375f..168699ce6 100644 --- a/tests/src/serialize_test.cpp +++ b/tests/src/serialize_test.cpp @@ -32,7 +32,7 @@ TEST_F(SerializeTest, SizeTest) std::vector data(30000000); Serializer ser(data.data(), data.size()); dc_serialize(ser); - ASSERT_EQ(28191438u, ser.size()); + ASSERT_EQ(28191443u, ser.size()); } diff --git a/tests/src/test_stubs.cpp b/tests/src/test_stubs.cpp index 0b6b16001..aa3793ace 100644 --- a/tests/src/test_stubs.cpp +++ b/tests/src/test_stubs.cpp @@ -15,43 +15,11 @@ HWND getNativeHwnd() } #endif -void os_SetupInput() -{ -} -void os_TermInput() -{ -} - -void UpdateInputState() -{ -} - void os_DoEvents() { } -void os_CreateWindow() -{ -} - void os_RunInstance(int argc, const char *argv[]) { } #endif - -#ifdef _WIN32 -#include - -static LARGE_INTEGER qpf; -static double qpfd; -//Helper functions -double os_GetSeconds() -{ - static bool initme = (QueryPerformanceFrequency(&qpf), qpfd=1/(double)qpf.QuadPart); - LARGE_INTEGER time_now; - - QueryPerformanceCounter(&time_now); - static LARGE_INTEGER time_now_base = time_now; - return (time_now.QuadPart - time_now_base.QuadPart)*qpfd; -} -#endif