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) { }