From 6f0581032b57c6664bae1281e88e16491b615a79 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 13 May 2024 15:47:34 +0200 Subject: [PATCH] save screenshot. add screenshot to savestates Retrieve last frame rgb data (gl, vk, dx9, dx11). Specific save screenshot code for android, iOS and UWP. Add Save Screenshot emu key (F12 by default) vk: defer deletion of in-flight textures when texture cache is cleared. vk: fix issue when updating imgui textures after a render pass has begun (achievements) vk: palette texture not updated after a state has been loaded. gl: Move opengl-specific stuff into opengl imgui driver. savestate: Add non compressed header, following by screenshot png data, before actual savestate. Issue #842 --- CMakeLists.txt | 1 - core/archive/rzip.cpp | 30 +- core/archive/rzip.h | 2 + core/emulator.cpp | 6 +- core/emulator.h | 6 +- core/hw/pvr/Renderer_if.h | 4 + core/input/gamepad.h | 1 + core/input/gamepad_device.cpp | 4 + core/input/keyboard_device.h | 1 + core/input/mapping.cpp | 1 + core/nullDC.cpp | 188 +++++++---- core/oslib/oslib.cpp | 116 +++++++ core/oslib/oslib.h | 2 + core/rend/dx11/dx11_driver.h | 4 + core/rend/dx11/dx11_renderer.cpp | 80 +++++ core/rend/dx11/dx11_renderer.h | 1 + core/rend/dx9/d3d_renderer.cpp | 90 ++++++ core/rend/dx9/d3d_renderer.h | 1 + core/rend/dx9/dx9_driver.h | 6 +- core/rend/gles/gldraw.cpp | 64 +++- core/rend/gles/gles.cpp | 4 +- core/rend/gles/gles.h | 3 +- core/rend/gles/gltex.cpp | 24 +- core/rend/gles/opengl_driver.cpp | 37 +++ core/rend/gles/opengl_driver.h | 5 + core/rend/gles/quad.cpp | 2 +- core/rend/vulkan/buffer.h | 4 +- core/rend/vulkan/texture.h | 7 +- core/rend/vulkan/vk_context_lr.h | 2 + core/rend/vulkan/vulkan_context.cpp | 106 +++++++ core/rend/vulkan/vulkan_context.h | 2 + core/rend/vulkan/vulkan_driver.h | 21 ++ core/rend/vulkan/vulkan_renderer.cpp | 275 ++++++++++++++++ core/rend/vulkan/vulkan_renderer.h | 294 +----------------- core/serialize.cpp | 56 ++++ core/serialize.h | 33 +- core/stdclass.h | 38 +++ core/ui/gui.cpp | 187 ++++++++--- core/ui/gui.h | 4 +- core/ui/gui_achievements.cpp | 8 +- core/ui/gui_achievements.h | 4 +- core/ui/gui_util.cpp | 172 ++++++++-- core/ui/gui_util.h | 61 +++- core/ui/imgui_driver.cpp | 110 ------- core/ui/imgui_driver.h | 49 ++- core/ui/mainui.cpp | 2 +- .../com/flycast/emulator/AndroidStorage.java | 36 +++ .../src/main/jni/src/android_storage.h | 16 + shell/apple/emulator-ios/emulator/ios_main.mm | 13 + shell/apple/emulator-ios/plist.in | 2 + .../emulator-osx/emulator-osx/osx-main.mm | 11 + shell/libretro/oslib.cpp | 10 - 52 files changed, 1562 insertions(+), 644 deletions(-) delete mode 100644 core/ui/imgui_driver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 640375fe1..d2c6fc0a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1294,7 +1294,6 @@ if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE core/ui/game_scanner.cpp core/ui/game_scanner.h - core/ui/imgui_driver.cpp core/ui/imgui_driver.h core/ui/gui.cpp core/ui/gui.h 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/emulator.cpp b/core/emulator.cpp index 9af7f6f5b..d41b93eaa 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -550,10 +550,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); @@ -612,9 +614,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) { diff --git a/core/emulator.h b/core/emulator.h index f23835441..6802b00e8 100644 --- a/core/emulator.h +++ b/core/emulator.h @@ -27,6 +27,7 @@ #include #include #include +#include void loadGameSpecificSettings(); void SaveSettings(); @@ -35,10 +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); -std::string dc_getStateUpdateDate(int index); +time_t dc_getStateCreationDate(int index); +void dc_getStateScreenshot(int index, std::vector& pngData); enum class Event { Start, diff --git a/core/hw/pvr/Renderer_if.h b/core/hw/pvr/Renderer_if.h index 71d306711..191368630 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,9 @@ 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) + virtual bool GetLastFrame(std::vector& data, int& width, int& height) { return false; } virtual bool Present() { return true; } 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 610ef5b85..7ca658414 100644 --- a/core/input/gamepad_device.cpp +++ b/core/input/gamepad_device.cpp @@ -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; diff --git a/core/input/keyboard_device.h b/core/input/keyboard_device.h index 1ae17ce35..682408556 100644 --- a/core/input/keyboard_device.h +++ b/core/input/keyboard_device.h @@ -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/nullDC.cpp b/core/nullDC.cpp index be8955bf2..5741127a2 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -17,6 +17,29 @@ #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[]) { #if defined(TEST_AUTOMATION) @@ -67,7 +90,7 @@ int flycast_init(int argc, char* argv[]) void dc_exit() { try { - emu.stop(); + emu.unloadGame(); } catch (...) { } mainui_stop(); } @@ -85,15 +108,15 @@ void SaveSettings() void flycast_term() { - os_DestroyWindow(); 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; @@ -113,10 +136,8 @@ 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", 5000); @@ -124,31 +145,41 @@ void dc_savestate(int index) 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", 5000); - 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", 5000); - 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", 2000); + return; + +fail: + WARN_LOG(SAVESTATE, "Failed to save state - error writing %s", filename.c_str()); + gui_display_notification("Error saving state", 5000); + if (zipFile.rawFile() != nullptr) + zipFile.Close(); + else + std::fclose(f); + free(data); + // delete failed savestate? } void dc_loadstate(int index) @@ -156,46 +187,54 @@ 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()); + gui_display_notification("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", 5000); - if (f != nullptr) + if (zipFile.rawFile() == nullptr) std::fclose(f); else zipFile.Close(); @@ -203,14 +242,14 @@ 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) @@ -226,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()); @@ -235,25 +275,41 @@ void dc_loadstate(int index) EventManager::event(Event::LoadState); } -#ifdef _WIN32 -static struct tm *localtime_r(const time_t *_clock, struct tm *_result) -{ - return localtime_s(_result, _clock) ? nullptr : _result; -} -#endif - -std::string dc_getStateUpdateDate(int index) +time_t dc_getStateCreationDate(int index) { std::string filename = hostfs::getSavestatePath(index, false); - struct stat st; - if (flycast::stat(filename.c_str(), &st) != 0) - return {}; - tm t; - if (localtime_r(&st.st_mtime, &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; + 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/oslib/oslib.cpp b/core/oslib/oslib.cpp index ad3b0561b..834ade0ef 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -37,6 +37,7 @@ #endif #if defined(_WIN32) && !defined(TARGET_UWP) #include "windows/rawinput.h" +#include #endif #include "profiler/fc_profiler.h" @@ -151,8 +152,123 @@ 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(); + } + 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) diff --git a/core/oslib/oslib.h b/core/oslib/oslib.h index 8744af19e..8c64fd4df 100644 --- a/core/oslib/oslib.h +++ b/core/oslib/oslib.h @@ -1,5 +1,6 @@ #pragma once #include "types.h" +#include #if defined(__SWITCH__) #include #endif @@ -59,6 +60,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/rend/dx11/dx11_driver.h b/core/rend/dx11/dx11_driver.h index 5807c2f01..446de5926 100644 --- a/core/rend/dx11/dx11_driver.h +++ b/core/rend/dx11/dx11_driver.h @@ -92,6 +92,10 @@ public: return (ImTextureID)&texture.imTexture; } + void deleteTexture(const std::string& name) override { + textures.erase(name); + } + private: struct Texture { diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index b52e8fd3f..2cb9e9357 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -1345,6 +1345,86 @@ 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; + + 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); + + deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); + + 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/dx9/d3d_renderer.cpp b/core/rend/dx9/d3d_renderer.cpp index 42d892395..ac58ab35a 100644 --- a/core/rend/dx9/d3d_renderer.cpp +++ b/core/rend/dx9/d3d_renderer.cpp @@ -1422,6 +1422,96 @@ 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; + + 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 07787564c..bdea5cbe9 100644 --- a/core/rend/dx9/d3d_renderer.h +++ b/core/rend/dx9/d3d_renderer.h @@ -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 e1fe7527d..4e271b40d 100644 --- a/core/rend/dx9/dx9_driver.h +++ b/core/rend/dx9/dx9_driver.h @@ -97,7 +97,11 @@ public: texture.imTexture.d3dTexture = texture.tex.get(); texture.imTexture.pointSampling = nearestSampling; - return (ImTextureID)&texture; + return (ImTextureID)&texture.imTexture; + } + + void deleteTexture(const std::string& name) override { + textures.erase(name); } private: diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index a2384fe03..9108bfeae 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -790,7 +790,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,6 +817,68 @@ bool OpenGLRenderer::renderLastFrame() return true; } +bool OpenGLRenderer::GetLastFrame(std::vector& data, int& width, int& height) +{ + GlFramebuffer *framebuffer = gl.ofbo2.ready ? gl.ofbo2.framebuffer.get() : gl.ofbo.framebuffer.get(); + if (framebuffer == nullptr) + return false; + 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; + + GlFramebuffer dstFramebuffer(width, height, false, false); + + glViewport(0, 0, width, height); + glcache.Disable(GL_BLEND); + verify(framebuffer->getTexture() != 0); + const float *vertices = nullptr; + if (config::Rotate90) + { + 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]; + } + drawQuad(framebuffer->getTexture(), config::Rotate90, false, vertices); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + data.resize(width * height * 3); + dstFramebuffer.bind(GL_READ_FRAMEBUFFER); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + if (gl.is_gles) + { + // 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()) + { + *dst++ = *src++; + *dst++ = *src++; + *dst++ = *src++; + src++; + } + } + else { + glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, data.data()); + } + restoreCurrentFramebuffer(); + glCheck(); + + return true; +} + #ifdef LIBRETRO #include "vmu_xhair.h" diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index 54117246c..aa88bf258 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -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); @@ -1510,8 +1512,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 3063bd341..f7ebc131e 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -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); extern const char* ShaderCompatSource; extern const char *VertexCompatShader; 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 3d63a2570..425dfd778 100644 --- a/core/rend/gles/opengl_driver.cpp +++ b/core/rend/gles/opengl_driver.cpp @@ -69,6 +69,34 @@ OpenGLDriver::~OpenGLDriver() ImGui_ImplOpenGL3_Shutdown(); } +void OpenGLDriver::reset() +{ + ImGuiDriver::reset(); + for (auto& tex : vmu_lcd_tex_ids) + tex = ImTextureID{}; + vmuLastChanged.fill({}); +} + +void OpenGLDriver::updateVmuTextures() +{ + for (int i = 0; i < 8; i++) + { + if (!vmu_lcd_status[i]) + continue; + + if (this->vmuLastChanged[i] != ::vmuLastChanged[i] || vmu_lcd_tex_ids[i] == ImTextureID()) + { + try { + vmu_lcd_tex_ids[i] = updateTexture(":vmugl:" + std::to_string(i), (const u8 *)vmu_lcd_data[i], 48, 32, true); + } catch (...) { + continue; + } + if (vmu_lcd_tex_ids[i] != ImTextureID()) + this->vmuLastChanged[i] = ::vmuLastChanged[i]; + } + } +} + void OpenGLDriver::displayVmus() { if (!gameStarted) @@ -190,3 +218,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 59c41d146..d9262d541 100644 --- a/core/rend/gles/opengl_driver.h +++ b/core/rend/gles/opengl_driver.h @@ -33,6 +33,7 @@ public: void newFrame() override; void renderDrawData(ImDrawData* drawData, bool gui_open) override; void present() override; + void reset() override; void setFrameRendered() override { frameRendered = true; @@ -47,6 +48,7 @@ public: } 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) @@ -66,8 +68,11 @@ private: static void emuEventCallback(Event event, void *p) { ((OpenGLDriver *)p)->emuEvent(event); } + void updateVmuTextures(); ImTextureID crosshairTexId = ImTextureID(); + ImTextureID vmu_lcd_tex_ids[8] {}; + std::array vmuLastChanged {}; 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..c14c8f794 100644 --- a/core/rend/gles/quad.cpp +++ b/core/rend/gles/quad.cpp @@ -145,7 +145,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) { glcache.Disable(GL_SCISSOR_TEST); glcache.Disable(GL_DEPTH_TEST); 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/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.h b/core/rend/vulkan/vk_context_lr.h index ddfeec52f..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; } diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index e58252c71..6aede4e8e 100644 --- a/core/rend/vulkan/vulkan_context.cpp +++ b/core/rend/vulkan/vulkan_context.cpp @@ -966,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(); @@ -973,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(); @@ -1230,3 +1232,107 @@ VulkanContext::~VulkanContext() verify(contextInstance == this); contextInstance = nullptr; } + +bool VulkanContext::GetLastFrame(std::vector& data, int& width, int& height) +{ + if (!lastFrameView) + return false; + + 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 0a8bc30cc..47b142853 100644 --- a/core/rend/vulkan/vulkan_driver.h +++ b/core/rend/vulkan/vulkan_driver.h @@ -133,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; diff --git a/core/rend/vulkan/vulkan_renderer.cpp b/core/rend/vulkan/vulkan_renderer.cpp index 0586d997a..d32a70e0d 100644 --- a/core/rend/vulkan/vulkan_renderer.cpp +++ b/core/rend/vulkan/vulkan_renderer.cpp @@ -21,6 +21,281 @@ #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) +{ + 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 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()); + framebufferRendered = false; + return true; +} class VulkanRenderer final : public BaseVulkanRenderer { diff --git a/core/rend/vulkan/vulkan_renderer.h b/core/rend/vulkan/vulkan_renderer.h index f31b2328f..f22f1230d 100644 --- a/core/rend/vulkan/vulkan_renderer.h +++ b/core/rend/vulkan/vulkan_renderer.h @@ -19,14 +19,9 @@ #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 "ui/gui.h" -#endif +#include "shaders.h" #include #include @@ -36,232 +31,21 @@ 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); -#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(); + 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); - } - else - { - os_VideoRoutingTermVk(); - } -#endif - } - - protected: BaseVulkanRenderer() : viewport(640, 480) {} @@ -273,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/serialize.cpp b/core/serialize.cpp index 9094e164b..87bc2c44c 100644 --- a/core/serialize.cpp +++ b/core/serialize.cpp @@ -99,3 +99,59 @@ void dc_deserialize(Deserializer& deser) 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 2c9ebb813..5650122d0 100644 --- a/core/serialize.h +++ b/core/serialize.h @@ -87,29 +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 < 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"); - } - } + Deserializer(const void *data, size_t limit, bool rollback = false); template void deserialize(T& obj) @@ -165,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.h b/core/stdclass.h index d5efa9e82..6c7d209fd 100644 --- a/core/stdclass.h +++ b/core/stdclass.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include #ifdef __ANDROID__ #include @@ -199,3 +201,39 @@ public: }; 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/ui/gui.cpp b/core/ui/gui.cpp index 3860735c2..b448dfa1f 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -50,6 +50,8 @@ #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 @@ -101,6 +103,7 @@ using LockGuard = std::lock_guard; ImFont *largeFont; static Toast toast; +static ThreadRunner uiThreadRunner; static void emuEventCallback(Event event, void *) { @@ -190,12 +193,13 @@ 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.4f, settings.display.uiScale); + 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()) @@ -470,7 +474,6 @@ static void delayedKeysUp() static void gui_endFrame(bool gui_open) { - ImGui::Render(); imguiDriver->renderDrawData(ImGui::GetDrawData(), gui_open); delayedKeysUp(); } @@ -581,6 +584,50 @@ 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) +{ + data.clear(); + std::vector rawData; + int width, height; + 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() +{ + std::vector pngData; + getScreenshot(pngData); + dc_savestate(config::SavestateSlot, pngData.empty() ? nullptr : &pngData[0], pngData.size()); + ImguiStateTexture savestatePic; + savestatePic.invalidate(); +} + static void gui_display_commands() { fullScreenWindow(false); @@ -595,33 +642,38 @@ static void gui_display_commands() (ImGui::GetContentRegionAvail().x - uiScaled(100 + 150) - ImGui::GetStyle().FramePadding.x * 2) / 2 / uiScaled(1)); float buttonWidth = 150.f; // not scaled - bool lowW = ImGui::GetContentRegionAvail().x < (uiScaled(100 + buttonWidth * 3) - + ImGui::GetStyle().FramePadding.x * 2 + ImGui::GetStyle().ItemSpacing.x * 2); - if (lowW) + 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); - ImguiTexture tex(art.boxartPath); + ImguiFileTexture tex(art.boxartPath); // TODO use placeholder image if not available tex.draw(ScaledVec2(100, 100)); ImGui::SameLine(); - ImGui::BeginChild("game_info", ScaledVec2(0, 100.f), ImGuiChildFlags_Border, ImGuiWindowFlags_None); - ImGui::PushFont(largeFont); - ImGui::Text("%s", art.name.c_str()); - ImGui::PopFont(); + if (!lowHeight) { - ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f)); - ImGui::TextWrapped("%s", art.fileName.c_str()); + 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(); } - ImGui::EndChild(); - if (lowW) { + if (lowWidth) { ImGui::Columns(3, "buttons", false); } else @@ -632,7 +684,7 @@ static void gui_display_commands() ImGui::SetColumnWidth(2, uiScaled(columnWidth)); const ImVec2 vmuPos = ImGui::GetStyle().WindowPadding + ScaledVec2(0.f, 100.f) + ImVec2(insetLeft, ImGui::GetStyle().ItemSpacing.y); - imguiDriver->displayVmus(vmuPos); + ImguiVmuTexture::displayVmus(vmuPos); ImGui::NextColumn(); } ImguiStyleVar _1{ImGuiStyleVar_FramePadding, ScaledVec2(12.f, 3.f)}; @@ -657,6 +709,17 @@ static void gui_display_commands() 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 @@ -674,6 +737,7 @@ static void gui_display_commands() // 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(); @@ -681,10 +745,12 @@ static void gui_display_commands() ImGui::NextColumn(); { DisabledScope _{!savestateAllowed()}; + ImguiStateTexture savestatePic; + time_t savestateDate = dc_getStateCreationDate(config::SavestateSlot); + // Load State { - DisabledScope _{settings.raHardcoreMode}; - // 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); @@ -696,7 +762,7 @@ static void gui_display_commands() if (ImGui::Button(ICON_FA_DOWNLOAD " Save State", ScaledVec2(buttonWidth, 50)) && savestateAllowed()) { gui_setState(GuiState::Closed); - dc_savestate(config::SavestateSlot); + savestate(); } { @@ -720,30 +786,20 @@ static void gui_display_commands() ImGui::SameLine(0, spacingW); if (ImGui::ArrowButton("##next-slot", ImGuiDir_Right)) { - if (config::SavestateSlot == 9) - config::SavestateSlot = 0; - else + if (config::SavestateSlot == 9) + config::SavestateSlot = 0; + else config::SavestateSlot++; - SaveSettings(); + SaveSettings(); } - std::string date = dc_getStateUpdateDate(config::SavestateSlot); { ImVec4 gray(0.75f, 0.75f, 0.75f, 1.f); - if (date.empty()) + if (savestateDate == 0) ImGui::TextColored(gray, "Empty"); else - ImGui::TextColored(gray, "%s", date.c_str()); + ImGui::TextColored(gray, "%s", timeToString(savestateDate).c_str()); } - } - // Barcode - if (card_reader::barcodeAvailable()) - { - ImGui::NewLine(); - ImGui::Text("Barcode Card"); - char cardBuf[64] {}; - strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1); - if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr)) - card_reader::barcodeSetCard(cardBuf); + savestatePic.draw(ScaledVec2(buttonWidth, 0.f)); } ImGui::Columns(1, nullptr, false); @@ -929,6 +985,7 @@ const Mapping dcButtons[] = { { 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 } }; @@ -982,6 +1039,7 @@ const Mapping arcadeButtons[] = { { 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 } }; @@ -3088,7 +3146,7 @@ static void gui_display_content() { GameMedia game; GameBoxart art = boxart.getBoxartAndLoad(game); - ImguiTexture tex(art.boxartPath); + ImguiFileTexture tex(art.boxartPath); pressed = gameImageButton(tex, "Dreamcast BIOS", responsiveBoxVec2, "Dreamcast BIOS"); } else @@ -3130,7 +3188,7 @@ static void gui_display_content() // 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)) { - ImguiTexture tex(art.boxartPath); + ImguiFileTexture tex(art.boxartPath); pressed = gameImageButton(tex, game.name, responsiveBoxVec2, gameName); } ImGui::EndChild(); @@ -3417,7 +3475,10 @@ void gui_display_ui() break; } error_popup(); + ImGui::Render(); gui_endFrame(gui_open); + uiThreadRunner.execTasks(); + ImguiFileTexture::resetLoadCount(); if (gui_state == GuiState::Closed) emu.start(); @@ -3447,7 +3508,7 @@ static std::string getFPSNotification() return std::string(settings.input.fastForwardMode ? ">>" : ""); } -void gui_display_osd() +void gui_draw_osd() { if (gui_state == GuiState::VJoyEdit) return; @@ -3488,7 +3549,13 @@ void gui_display_osd() } if (!settings.raHardcoreMode) lua::overlay(); + ImGui::Render(); + uiThreadRunner.execTasks(); +} +void gui_display_osd() +{ + gui_draw_osd(); gui_endFrame(gui_is_open()); } @@ -3523,7 +3590,7 @@ void gui_display_profiler() } ImGui::End(); - + ImGui::Render(); gui_endFrame(true); #endif } @@ -3604,17 +3671,22 @@ void gui_loadState() } } -void gui_saveState() +void gui_saveState(bool stopRestart) { const LockGuard lock(guiMutex); if (gui_state == GuiState::Closed && savestateAllowed()) { try { - emu.stop(); - dc_savestate(config::SavestateSlot); - emu.start(); + if (stopRestart) + emu.stop(); + savestate(); + if (stopRestart) + emu.start(); } catch (const FlycastException& e) { - gui_stop_game(e.what()); + if (stopRestart) + gui_stop_game(e.what()); + else + WARN_LOG(COMMON, "gui_saveState: %s", e.what()); } } } @@ -3641,6 +3713,33 @@ std::string gui_getCurGameBoxartUrl() 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()) { + gui_display_notification("No screenshot available", 2000); + } + else + { + try { + hostfs::saveScreenshot(name, data); + gui_display_notification("Screenshot saved", 2000, name.c_str()); + } catch (const FlycastException& e) { + gui_display_notification("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 diff --git a/core/ui/gui.h b/core/ui/gui.h index 593ba32b8..ced181f0e 100644 --- a/core/ui/gui.h +++ b/core/ui/gui.h @@ -26,6 +26,7 @@ void gui_initFonts(); void gui_open_settings(); void gui_display_ui(); void gui_display_notification(const char *msg, int duration, const char *details = nullptr); +void gui_draw_osd(); void gui_display_osd(); void gui_display_profiler(); void gui_open_onboarding(); @@ -49,8 +50,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, diff --git a/core/ui/gui_achievements.cpp b/core/ui/gui_achievements.cpp index 7a8d84b29..c60cf0a82 100644 --- a/core/ui/gui_achievements.cpp +++ b/core/ui/gui_achievements.cpp @@ -77,7 +77,7 @@ void Notification::notify(Type type, const std::string& image, const std::string void Notification::showChallenge(const std::string& image) { std::lock_guard _(mutex); - ImguiTexture texture{ image }; + ImguiFileTexture texture{ image }; if (std::find(challenges.begin(), challenges.end(), texture) != challenges.end()) return; challenges.push_back(texture); @@ -183,7 +183,7 @@ bool Notification::draw() dl->AddRect(pos, pos + totalSize, borderCol, 0.f); pos += padding; - for (const auto& img : challenges) { + for (auto& img : challenges) { img.draw(dl, pos, size, alpha); pos.x += hspacing + size.x; } @@ -283,7 +283,7 @@ void achievementList() 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(); - ImguiTexture tex(game.image); + 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); @@ -330,7 +330,7 @@ void achievementList() ImGui::Unindent(uiScaled(10)); } ImguiID _("achiev" + std::to_string(id++)); - ImguiTexture tex(ach.image); + 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); diff --git a/core/ui/gui_achievements.h b/core/ui/gui_achievements.h index a0ea9658a..c41587799 100644 --- a/core/ui/gui_achievements.h +++ b/core/ui/gui_achievements.h @@ -51,10 +51,10 @@ private: u64 startTime = 0; u64 endTime = 0; Type type = Type::None; - ImguiTexture image; + ImguiFileTexture image; std::string text[3]; std::mutex mutex; - std::vector challenges; + std::vector challenges; std::map leaderboards; }; diff --git a/core/ui/gui_util.cpp b/core/ui/gui_util.cpp index 8621fb231..a037d0e78 100644 --- a/core/ui/gui_util.cpp +++ b/core/ui/gui_util.cpp @@ -29,6 +29,8 @@ #include "imgui.h" #include "imgui_internal.h" #include "stdclass.h" +#include "rend/osd.h" +#include static std::string select_current_directory = "**home**"; static std::vector subfolders; @@ -691,13 +693,6 @@ void windowDragScroll() } } -ImTextureID ImguiTexture::getId() const -{ - if (path.empty()) - return {}; - return imguiDriver->getOrLoadTexture(path); -} - static void setUV(float ar, ImVec2& uv0, ImVec2& uv1) { uv0 = { 0.f, 0.f }; @@ -715,49 +710,182 @@ static void setUV(float ar, ImVec2& uv0, ImVec2& uv1) } } -void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col) const +void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col) { ImTextureID id = getId(); if (id == ImTextureID{}) ImGui::Dummy(size); else { - float ar = imguiDriver->getAspectRatio(id); + 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, uv0, uv1); - ImGui::Image(id, size, uv0, uv1, tint_col, border_col); + 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) const +void ImguiTexture::draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha) { ImTextureID id = getId(); if (id == ImTextureID{}) return; - float ar = imguiDriver->getAspectRatio(id); + const float ar = imguiDriver->getAspectRatio(id); ImVec2 uv0, uv1; - setUV(ar, uv0, uv1); - ImVec2 pos_b = pos + size; + setUV(ar / size.x * size.y, uv0, uv1); u32 col = alphaOverride(0xffffff, alpha); - drawList->AddImage(id, pos, pos_b, uv0, uv1, col); + 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) const + const ImVec4& bg_col, const ImVec4& tint_col) { ImTextureID id = getId(); if (id == ImTextureID{}) return ImGui::Button(title.c_str(), image_size); else { - float ar = imguiDriver->getAspectRatio(id); + const float ar = imguiDriver->getAspectRatio(id); + const ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2; ImVec2 uv0, uv1; - setUV(ar, uv0, uv1); - ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2; + 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) + return nullptr; + + int channels; + stbi_set_flip_vertically_on_load(0); + u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha); + std::fclose(file); + 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; +} + +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()) + { + // load savestate info + std::vector pngData; + dc_getStateScreenshot(config::SavestateSlot, pngData); + if (pngData.empty()) + return {}; + + int width, height, channels; + stbi_set_flip_vertically_on_load(0); + u8 *imgData = stbi_load_from_memory(&pngData[0], pngData.size(), &width, &height, &channels, STBI_rgb_alpha); + if (imgData != nullptr) + { + try { + texid = imguiDriver->updateTextureAndAspectRatio(path, imgData, width, height, nearestSampling); + } catch (...) { + // vulkan can throw during resizing + } + free(imgData); + } + } + return texid; +} + +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) { @@ -827,8 +955,9 @@ bool Toast::draw() // 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 float maxW = uiScaled(640.f); const ImVec2 titleSize = title.empty() ? ImVec2() : largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, maxW, &title.front(), &title.back() + 1); const ImVec2 msgSize = message.empty() ? ImVec2() @@ -838,7 +967,6 @@ bool Toast::draw() ImVec2 totalSize(std::max(titleSize.x, msgSize.x), titleSize.y + msgSize.y); totalSize += padding * 2.f + spacing * (float)(!title.empty() && !message.empty()); - const ImVec2 displaySize(ImGui::GetIO().DisplaySize); ImVec2 pos(insetLeft, displaySize.y - totalSize.y); if (now - startTime < START_ANIM_TIME) // Slide up diff --git a/core/ui/gui_util.h b/core/ui/gui_util.h index c52f1956c..957ce994e 100644 --- a/core/ui/gui_util.h +++ b/core/ui/gui_util.h @@ -199,27 +199,70 @@ public: class ImguiTexture { public: - ImguiTexture() = default; - ImguiTexture(const std::string& path) : path(path) {} - void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1), - const ImVec4& border_col = ImVec4(0, 0, 0, 0)) const; - void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha = 1.f) const; + 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)) const; - - ImTextureID getId() const; + const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); operator ImTextureID() { return getId(); } + void setNearestSampling(bool nearestSampling) { + this->nearestSampling = nearestSampling; + } - bool operator==(const ImguiTexture& other) const { + 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(); +}; + +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 = {}) diff --git a/core/ui/imgui_driver.cpp b/core/ui/imgui_driver.cpp deleted file mode 100644 index e6b36b3f4..000000000 --- a/core/ui/imgui_driver.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - 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 "imgui_driver.h" -#include "gui_util.h" -#include "rend/osd.h" -#define STBI_ONLY_JPEG -#define STBI_ONLY_PNG -#include - -constexpr float VMU_WIDTH = 96.f; -constexpr float VMU_HEIGHT = 64.f; -constexpr float VMU_PADDING = 8.f; - -void ImGuiDriver::reset() -{ - aspectRatios.clear(); - for (auto& tex : vmu_lcd_tex_ids) - tex = ImTextureID{}; - textureLoadCount = 0; - vmuLastChanged.fill({}); -} - -static u8 *loadImage(const std::string& path, int& width, int& height) -{ - FILE *file = nowide::fopen(path.c_str(), "rb"); - if (file == nullptr) - return nullptr; - - int channels; - stbi_set_flip_vertically_on_load(0); - u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha); - std::fclose(file); - return imgData; -} - -ImTextureID ImGuiDriver::getOrLoadTexture(const std::string& path, bool nearestSampling) -{ - ImTextureID id = getTexture(path); - if (id == ImTextureID() && textureLoadCount < 10) - { - textureLoadCount++; - int width, height; - u8 *imgData = loadImage(path, width, height); - if (imgData != nullptr) - { - try { - id = updateTextureAndAspectRatio(path, imgData, width, height, nearestSampling); - } catch (...) { - // vulkan can throw during resizing - } - free(imgData); - } - } - return id; -} - -void ImGuiDriver::updateVmuTextures() -{ - for (int i = 0; i < 8; i++) - { - if (!vmu_lcd_status[i]) - continue; - - if (this->vmuLastChanged[i] != ::vmuLastChanged[i] || vmu_lcd_tex_ids[i] == ImTextureID()) - { - try { - vmu_lcd_tex_ids[i] = updateTexture("__vmu" + std::to_string(i), (const u8 *)vmu_lcd_data[i], 48, 32, true); - } catch (...) { - continue; - } - if (vmu_lcd_tex_ids[i] != ImTextureID()) - this->vmuLastChanged[i] = ::vmuLastChanged[i]; - } - } -} - -void ImGuiDriver::displayVmus(const ImVec2& pos) -{ - updateVmuTextures(); - 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; - - ImVec2 pos_b = cpos + size; - dl->AddImage(vmu_lcd_tex_ids[i], cpos, pos_b, ImVec2(0, 1), ImVec2(1, 0), 0x80ffffff); - cpos.y += size.y + padding; - } -} - diff --git a/core/ui/imgui_driver.h b/core/ui/imgui_driver.h index 3a77a7f6e..68d20e599 100644 --- a/core/ui/imgui_driver.h +++ b/core/ui/imgui_driver.h @@ -30,54 +30,47 @@ public: gui_initFonts(); } virtual ~ImGuiDriver() = default; - virtual void reset(); + + virtual void reset() { + aspectRatios.clear(); + } virtual void newFrame() = 0; virtual void renderDrawData(ImDrawData* drawData, bool gui_open) = 0; virtual void displayVmus() {} // TODO OpenGL only. Get rid of it virtual void displayCrosshairs() {} // same - // draw all active vmus in a single column at the given position - void displayVmus(const ImVec2& pos); - - void doPresent() { - textureLoadCount = 0; - present(); - } + virtual void present() = 0; virtual void setFrameRendered() {} - float getAspectRatio(ImTextureID textureId) { - auto it = aspectRatios.find(textureId); - if (it != aspectRatios.end()) - return it->second; - else - return 1; - } - - ImTextureID getOrLoadTexture(const std::string& path, bool nearestSampling = false); - -protected: virtual ImTextureID getTexture(const std::string& name) = 0; virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) = 0; - virtual void present() = 0; - void updateVmuTextures(); + virtual void deleteTexture(const std::string& name) = 0; - ImTextureID vmu_lcd_tex_ids[8] {}; - std::array vmuLastChanged {}; - -private: ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) { - textureLoadCount++; ImTextureID textureId = updateTexture(name, data, width, height, nearestSampling); if (textureId != ImTextureID()) aspectRatios[textureId] = (float)width / height; return textureId; } - std::unordered_map aspectRatios; - int textureLoadCount = 0; + float getAspectRatio(ImTextureID textureId) + { + auto it = aspectRatios.find(textureId); + if (it != aspectRatios.end()) + return it->second; + else + return 1.f; + } + void updateAspectRatio(ImTextureID textureId, float aspectRatio) { + if (textureId != ImTextureID()) + aspectRatios[textureId] = aspectRatio; + } + +private: + std::unordered_map aspectRatios; // TODO move this out }; extern std::unique_ptr imguiDriver; diff --git a/core/ui/mainui.cpp b/core/ui/mainui.cpp index 7a9a84192..8191679e4 100644 --- a/core/ui/mainui.cpp +++ b/core/ui/mainui.cpp @@ -96,7 +96,7 @@ void mainui_loop() if (imguiDriver == nullptr) forceReinit = true; else - imguiDriver->doPresent(); + imguiDriver->present(); if (config::RendererType != currentRenderer || forceReinit) { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java index ce56bdaeb..0858f9928 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java @@ -24,17 +24,22 @@ import android.content.ContentUris; import android.content.CursorLoader; import android.content.Intent; import android.database.Cursor; +import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.util.Log; import androidx.documentfile.provider.DocumentFile; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.List; public class AndroidStorage { @@ -386,4 +391,35 @@ public class AndroidStorage { cursor.close(); return result; } + + public void saveScreenshot(String name, byte data[]) + { + File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + File file = new File(path, name); + + try { + // Make sure the Pictures directory exists. + path.mkdirs(); + + OutputStream os = new FileOutputStream(file); + try { + os.write(data); + } catch (IOException e) { + try { os.close(); } catch (IOException e1) {} + file.delete(); + throw e; + } + os.close(); + + // Tell the media scanner about the new file so that it is + // immediately available to the user. + MediaScannerConnection.scanFile(activity, + new String[] { file.toString() }, null, null); + } catch (IOException e) { + // Unable to create file, likely because external storage is + // not currently mounted. + Log.w("flycast", "saveScreenshot: Error writing " + file, e); + throw new RuntimeException(e.getMessage()); + } + } } 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/apple/emulator-ios/emulator/ios_main.mm b/shell/apple/emulator-ios/emulator/ios_main.mm index 0fe36432c..20cc34677 100644 --- a/shell/apple/emulator-ios/emulator/ios_main.mm +++ b/shell/apple/emulator-ios/emulator/ios_main.mm @@ -19,6 +19,7 @@ */ #import +#include #include int darw_printf(const char* text,...) @@ -45,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/osx-main.mm b/shell/apple/emulator-osx/emulator-osx/osx-main.mm index 089ce81f4..fe58521c1 100644 --- a/shell/apple/emulator-osx/emulator-osx/osx-main.mm +++ b/shell/apple/emulator-osx/emulator-osx/osx-main.mm @@ -255,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/libretro/oslib.cpp b/shell/libretro/oslib.cpp index cbb46b3bc..cf70ad53e 100644 --- a/shell/libretro/oslib.cpp +++ b/shell/libretro/oslib.cpp @@ -130,16 +130,6 @@ 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) { }