From b570567b8819b5eaecacba615dee9d3e9bdbbdfb Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Fri, 9 Oct 2026 06:10:16 +0200 Subject: [PATCH] Give the Vulkan allocator one owning thread Readback buffers are allocated when the readback is recorded rather than on the render thread, so only the thread that records frames touches VMA. The allocator is created externally synchronized, and VMA is built with no-op mutexes and plain counters instead of std::mutex and std::atomic. Headless runs Vulkan offscreen in builds without SDL too, and CI runs the headless tests on lavapipe, skipping the pixel tests a hardware backend doesn't match the software references on. sleep_ms and sleep_us go, with the thread manager test waiting on an event instead, and so does the unused portable-file-dialogs header. --- .github/workflows/build.yml | 8 +- CMakeLists.txt | 2 +- Common/GPU/Vulkan/VulkanContext.cpp | 3 + Common/GPU/Vulkan/VulkanContext.h | 2 +- Common/GPU/Vulkan/VulkanFrameData.h | 2 + Common/GPU/Vulkan/VulkanQueueRunner.cpp | 47 +- Common/GPU/Vulkan/VulkanQueueRunner.h | 5 + Common/GPU/Vulkan/VulkanRenderManager.cpp | 2 + Common/TimeUtil.cpp | 48 - Common/TimeUtil.h | 7 - Core/HLE/sceNetAdhocMatching.cpp | 11 +- ext/portable-file-dialogs/COPYING | 14 - .../portable-file-dialogs.h | 1887 ----------------- ext/vma/vk_mem_alloc.cpp | 57 + headless/Headless.cpp | 11 +- headless/README.md | 2 +- test.py | 50 + unittest/TestThreadManager.cpp | 18 +- 18 files changed, 173 insertions(+), 2003 deletions(-) delete mode 100644 ext/portable-file-dialogs/COPYING delete mode 100644 ext/portable-file-dialogs/portable-file-dialogs.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 272f508fb6..dff37d3725 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -345,7 +345,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update -y -qq - sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libcurl4-openssl-dev + sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libcurl4-openssl-dev mesa-vulkan-drivers - name: Install macOS dependencies if: runner.os == 'macOS' @@ -390,6 +390,12 @@ jobs: done exit $fail + # Runs the Vulkan backend's render and compile threads on lavapipe. + - name: Execute headless tests (Vulkan) + if: matrix.os == 'ubuntu-26.04' + working-directory: ${{ env.GITHUB_WORKSPACE }} + run: python3 test.py -g --graphics=vulkan --known-failures=vulkan + # x86-64 only: the reference images don't match what the software renderer produces on arm64, # 23 of the 30 dumps differ there, one of them by an MSE of 100. That's a real difference and # worth chasing, but it predates the arm64 runner - it just never had a CI job to show up in. diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ba8b06931..60b4a03933 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -668,7 +668,7 @@ endif(USE_FFMPEG) if(LIBRETRO) elseif(HEADLESS_CROSS OR WIN32 OR ANDROID OR IOS) - # No SDL3 here: the headless tools run with --graphics=software only. + # No SDL3 here: the headless tools run with --graphics=software or vulkan only. add_definitions(-DHEADLESS_NO_SDL) else() # The headless tools create their GL contexts with SDL. diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index 7cb54041ac..ad53ed7aea 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -926,6 +926,9 @@ VkResult VulkanContext::CreateDevice(int physical_device, const std::vector DeleteCallback; // This is a bit repetitive... // // Thread safety: the queueing functions can be called from any thread - the render thread queues -// deletes too (VulkanDescSetPool::Recreate from FlushDescSets, ResizeReadbackBuffer). They push onto +// deletes too (VulkanDescSetPool::Recreate from FlushDescSets). They push onto // a lock-free MPSC queue. Take() and PerformDeletes() belong to one thread, the main thread, which // moves the queued items into the per-kind vectors. class VulkanDeleteList { diff --git a/Common/GPU/Vulkan/VulkanFrameData.h b/Common/GPU/Vulkan/VulkanFrameData.h index f37338a162..fbd5cc38c6 100644 --- a/Common/GPU/Vulkan/VulkanFrameData.h +++ b/Common/GPU/Vulkan/VulkanFrameData.h @@ -55,6 +55,8 @@ struct CachedReadback { VmaAllocation allocation; VkDeviceSize bufferSize; bool isCoherent; + // frameId of the frame that created it; holds no data until that frame comes round again. + uint64_t createdFrame; void Destroy(VulkanContext *vulkan); }; diff --git a/Common/GPU/Vulkan/VulkanQueueRunner.cpp b/Common/GPU/Vulkan/VulkanQueueRunner.cpp index 8d4f9fb01c..200a22148e 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.cpp +++ b/Common/GPU/Vulkan/VulkanQueueRunner.cpp @@ -1703,6 +1703,26 @@ void VulkanQueueRunner::ResizeReadbackBuffer(CachedReadback *readback, VkDeviceS readback->isCoherent = (memoryType.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0; } +CachedReadback *VulkanQueueRunner::PrepareReadback(FrameData &frameData, VKRFramebuffer *src, int width, int height, bool delayed) { + // TODO: Handle different readback formats! + const VkDeviceSize size = sizeof(uint32_t) * (VkDeviceSize)width * (VkDeviceSize)height; + CachedReadback *cached = &syncReadback_; + if (delayed) { + ReadbackKey key; + key.framebuf = src; + key.width = width; + key.height = height; + if (!frameData.readbacks_.Get(key, &cached)) { + cached = new CachedReadback(); + cached->bufferSize = 0; + cached->createdFrame = frameData.frameId; + frameData.readbacks_.Insert(key, cached); + } + } + ResizeReadbackBuffer(cached, size); + return cached; +} + void VulkanQueueRunner::PerformReadback(const VKRStep &step, VkCommandBuffer cmd, FrameData &frameData) { VkImage image; VkImageLayout copyLayout; @@ -1736,28 +1756,7 @@ void VulkanQueueRunner::PerformReadback(const VKRStep &step, VkCommandBuffer cmd recordBarrier_.Flush(cmd); - // TODO: Handle different readback formats! - u32 readbackSizeInBytes = sizeof(uint32_t) * step.readback.srcRect.extent.width * step.readback.srcRect.extent.height; - - CachedReadback *cached = nullptr; - - if (step.readback.delayed) { - ReadbackKey key; - key.framebuf = step.readback.src; - key.width = step.readback.srcRect.extent.width; - key.height = step.readback.srcRect.extent.height; - - // See if there's already a buffer we can reuse - if (!frameData.readbacks_.Get(key, &cached)) { - cached = new CachedReadback(); - cached->bufferSize = 0; - frameData.readbacks_.Insert(key, cached); - } - } else { - cached = &syncReadback_; - } - - ResizeReadbackBuffer(cached, readbackSizeInBytes); + CachedReadback *cached = step.readback.dst; VkBufferImageCopy region{}; region.imageOffset = { step.readback.srcRect.offset.x, step.readback.srcRect.offset.y, 0 }; @@ -1792,8 +1791,6 @@ void VulkanQueueRunner::PerformReadbackImage(const VKRStep &step, VkCommandBuffe recordBarrier_.TransitionColorImageAuto(step.readback_image.image, &layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, 1, 1); recordBarrier_.Flush(cmd); - ResizeReadbackBuffer(&syncReadback_, sizeof(uint32_t) * step.readback_image.srcRect.extent.width * step.readback_image.srcRect.extent.height); - VkBufferImageCopy region{}; region.imageOffset = { step.readback_image.srcRect.offset.x, step.readback_image.srcRect.offset.y, 0 }; region.imageExtent = { step.readback_image.srcRect.extent.width, step.readback_image.srcRect.extent.height, 1 }; @@ -1827,7 +1824,7 @@ bool VulkanQueueRunner::CopyReadbackBuffer(FrameData &frameData, VKRFramebuffer key.width = width; key.height = height; CachedReadback *cached; - if (frameData.readbacks_.Get(key, &cached)) { + if (frameData.readbacks_.Get(key, &cached) && cached->createdFrame != frameData.frameId) { readback = cached; } else { // Didn't have a cached image ready yet diff --git a/Common/GPU/Vulkan/VulkanQueueRunner.h b/Common/GPU/Vulkan/VulkanQueueRunner.h index 39988c97cc..dc8fb35031 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.h +++ b/Common/GPU/Vulkan/VulkanQueueRunner.h @@ -187,6 +187,7 @@ struct VKRStep { VkRect2D srcRect; VkImageAspectFlags aspectMask; bool delayed; + CachedReadback *dst; } readback; struct { VkImage image; @@ -245,6 +246,10 @@ public: return (int)depth * 3 + (int)color; } + // Readback buffers are allocated here on the recording thread, so the render thread never + // touches the allocator. src == nullptr or !delayed means the sync readback buffer. + CachedReadback *PrepareReadback(FrameData &frameData, VKRFramebuffer *src, int width, int height, bool delayed); + // src == 0 means to copy from the sync readback buffer. bool CopyReadbackBuffer(FrameData &frameData, VKRFramebuffer *src, int width, int height, Draw::DataFormat srcFormat, Draw::DataFormat destFormat, int pixelStride, uint8_t *pixels); diff --git a/Common/GPU/Vulkan/VulkanRenderManager.cpp b/Common/GPU/Vulkan/VulkanRenderManager.cpp index 79ec6d2fc8..728d9b5921 100644 --- a/Common/GPU/Vulkan/VulkanRenderManager.cpp +++ b/Common/GPU/Vulkan/VulkanRenderManager.cpp @@ -1147,6 +1147,7 @@ bool VulkanRenderManager::CopyFramebufferToMemory(VKRFramebuffer *src, VkImageAs step->readback.srcRect.offset = { x, y }; step->readback.srcRect.extent = { (uint32_t)w, (uint32_t)h }; step->readback.delayed = mode == Draw::ReadbackMode::OLD_DATA_OK; + step->readback.dst = queueRunner_.PrepareReadback(frameData_[vulkan_->GetCurFrame()], src, w, h, step->readback.delayed); step->dependencies.insert(src); step->tag = tag; steps_.push_back(step); @@ -1207,6 +1208,7 @@ void VulkanRenderManager::CopyImageToMemorySync(VkImage image, int mipLevel, int step->readback_image.srcRect.extent = { (uint32_t)w, (uint32_t)h }; step->readback_image.mipLevel = mipLevel; step->tag = tag; + queueRunner_.PrepareReadback(frameData_[vulkan_->GetCurFrame()], nullptr, w, h, false); steps_.push_back(step); FlushSync(); diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index 0a3bfc2561..9045dc7b5e 100644 --- a/Common/TimeUtil.cpp +++ b/Common/TimeUtil.cpp @@ -6,20 +6,11 @@ #include "Common/TimeUtil.h" #include "Common/Log.h" -#ifdef HAVE_LIBNX -#include -#endif // HAVE_LIBNX - -#ifdef __EMSCRIPTEN__ -#include -#endif // __EMSCRIPTEN__ - #ifdef _WIN32 #include "CommonWindows.h" #include #else #include -#include #endif #include @@ -257,45 +248,6 @@ double Instant::ElapsedSeconds() const { #endif -#ifndef __LIBRETRO__ -// The headless tools and tests only; see TimeUtil.h. -void sleep_ms(int ms, const char *reason) { - if (ms <= 0) { - return; - } -#if SLEEP_LOG_ENABLED - INFO_LOG(Log::System, "Sleep %d ms: %s", ms, reason); -#endif -#ifdef _WIN32 - Sleep(ms); -#elif defined(HAVE_LIBNX) - svcSleepThread(ms * 1000000); -#elif defined(__EMSCRIPTEN__) - emscripten_sleep(ms); -#else - usleep(ms * 1000); -#endif -} - -void sleep_us(int us, const char *reason) { - if (us <= 0) { - return; - } -#if SLEEP_LOG_ENABLED - INFO_LOG(Log::System, "Sleep %d us: %s", us, reason); -#endif -#ifdef _WIN32 - Sleep(us / 1000); -#elif defined(HAVE_LIBNX) - svcSleepThread(us * 1000); -#elif defined(__EMSCRIPTEN__) - emscripten_sleep(us / 1000); -#else - usleep(us); -#endif -} -#endif - // Return the current time formatted as Minutes:Seconds:Milliseconds // in the form 00:00:000. void GetCurrentTimeFormatted(char formattedTime[13]) { diff --git a/Common/TimeUtil.h b/Common/TimeUtil.h index 32838fec5a..8abcc0f959 100644 --- a/Common/TimeUtil.h +++ b/Common/TimeUtil.h @@ -19,13 +19,6 @@ double from_time_raw_relative(uint64_t raw_time); double time_now_unix_utc(); double time_to_unix_utc(double timeNowSeconds); -#ifndef __LIBRETRO__ -// Sleeps, for the standalone frontends, headless tools and tests. The core never sleeps and -// doesn't have this, so a sleep can't creep back into it. -void sleep_ms(int ms, const char *reason); -void sleep_us(int us, const char *reason); -#endif - void GetCurrentTimeFormatted(char formattedTime[13]); // Most accurate timer possible - no extra double conversions. Only for spans. diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 1cf67f1410..5372be22b2 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -1131,18 +1131,9 @@ static void MatchingEventPass(SceNetAdhocMatchingContext *context, int matchingI // Log Matching Events INFO_LOG(Log::sceNet, "EventLoop[%d]: Matching Event [%d=%s][%s] OptSize=%d", matchingId, msg->opcode, getMatchingEventStr(msg->opcode), mac2str(&msg->mac).c_str(), msg->optlen); - // Unlock to prevent race-condition with other threads due to recursive lock - // // Call Event Handler - //context->handler(context->id, msg->opcode, &msg->mac, msg->optlen, opt); // Notify Event Handlers - notifyMatchingHandler(context, msg, opt, bufAddr, bufLen, args); // If we're using shared Buffer & Args for All Events We should wait for the Mipscall to be fully executed before processing the next event. GTA VCS need this delay/sleep. + notifyMatchingHandler(context, msg, opt, bufAddr, bufLen, args); - // Give some time before executing the next mipscall to prevent event ACCEPT(6)->ESTABLISH(7) getting reversed After Action ESTABLISH(7)->ACCEPT(6) - // Must Not be delayed too long to prevent desync/disconnect. Not longer than the delays on callback's HLE? - //sleep_ms(10); //sceKernelDelayThread(10000); - - // Lock again - // // Pop event stack from front (this should be queue instead of stack?) context->event_stack = msg->next; free(msg); diff --git a/ext/portable-file-dialogs/COPYING b/ext/portable-file-dialogs/COPYING deleted file mode 100644 index 8b014d64ae..0000000000 --- a/ext/portable-file-dialogs/COPYING +++ /dev/null @@ -1,14 +0,0 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2004 Sam Hocevar - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - diff --git a/ext/portable-file-dialogs/portable-file-dialogs.h b/ext/portable-file-dialogs/portable-file-dialogs.h deleted file mode 100644 index 1fc79a2910..0000000000 --- a/ext/portable-file-dialogs/portable-file-dialogs.h +++ /dev/null @@ -1,1887 +0,0 @@ -// -// Portable File Dialogs -// -// Copyright © 2018–2022 Sam Hocevar -// -// This library is free software. It comes without any warranty, to -// the extent permitted by applicable law. You can redistribute it -// and/or modify it under the terms of the Do What the Fuck You Want -// to Public License, Version 2, as published by the WTFPL Task Force. -// See http://www.wtfpl.net/ for more details. -// - -#pragma once - -#if _WIN32 -#ifndef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN 1 -#endif -#include -#include -#include -#include // IFileDialog -#include -#include -#include // std::async -#include // GetUserProfileDirectory() - -#elif __EMSCRIPTEN__ -#include - -#else -#ifndef _POSIX_C_SOURCE -# define _POSIX_C_SOURCE 2 // for popen() -#endif -#ifdef __APPLE__ -# ifndef _DARWIN_C_SOURCE -# define _DARWIN_C_SOURCE -# endif -#endif -#include // popen() -#include // std::getenv() -#include // fcntl() -#include // read(), pipe(), dup2(), getuid() -#include // ::kill, std::signal -#include // stat() -#include // waitpid() -#include // getpwnam() -#endif - -#include // std::string -#include // std::shared_ptr -#include // std::ostream -#include // std::map -#include // std::set -#include // std::regex -#include // std::mutex, std::this_thread -#include // std::chrono - -// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog -#ifndef PFD_HAS_IFILEDIALOG -# define PFD_HAS_IFILEDIALOG 1 -# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION -# if __GXX_ABI_VERSION <= 1013 -# undef PFD_HAS_IFILEDIALOG -# define PFD_HAS_IFILEDIALOG 0 -# endif -# endif -#endif - -namespace pfd -{ - -enum class button -{ - cancel = -1, - ok, - yes, - no, - abort, - retry, - ignore, -}; - -enum class choice -{ - ok = 0, - ok_cancel, - yes_no, - yes_no_cancel, - retry_cancel, - abort_retry_ignore, -}; - -enum class icon -{ - info = 0, - warning, - error, - question, -}; - -// Additional option flags for various dialog constructors -enum class opt : uint8_t -{ - none = 0, - // For file open, allow multiselect. - multiselect = 0x1, - // For file save, force overwrite and disable the confirmation dialog. - force_overwrite = 0x2, - // For folder select, force path to be the provided argument instead - // of the last opened directory, which is the Microsoft-recommended, - // user-friendly behaviour. - force_path = 0x4, -}; - -inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } -inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } - -// The settings class, only exposing to the user a way to set verbose mode -// and to force a rescan of installed desktop helpers (zenity, kdialog…). -class settings -{ -public: - static bool available(); - - static void verbose(bool value); - static void rescan(); - -protected: - explicit settings(bool resync = false); - - bool check_program(std::string const &program); - - inline bool is_osascript() const; - inline bool is_zenity() const; - inline bool is_kdialog() const; - - enum class flag - { - is_scanned = 0, - is_verbose, - - has_zenity, - has_matedialog, - has_qarma, - has_kdialog, - is_vista, - - max_flag, - }; - - // Static array of flags for internal state - bool const &flags(flag in_flag) const; - - // Non-const getter for the static array of flags - bool &flags(flag in_flag); -}; - -// Internal classes, not to be used by client applications -namespace internal -{ - -// Process wait timeout, in milliseconds -static int const default_wait_timeout = 20; - -class executor -{ - friend class dialog; - -public: - // High level function to get the result of a command - std::string result(int *exit_code = nullptr); - - // High level function to abort - bool kill(); - -#if _WIN32 - void start_func(std::function const &fun); - static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); -#elif __EMSCRIPTEN__ - void start(int exit_code); -#else - void start_process(std::vector const &command); -#endif - - ~executor(); - -protected: - bool ready(int timeout = default_wait_timeout); - void stop(); - -private: - bool m_running = false; - std::string m_stdout; - int m_exit_code = -1; -#if _WIN32 - std::future m_future; - std::set m_windows; - std::condition_variable m_cond; - std::mutex m_mutex; - DWORD m_tid; -#elif __EMSCRIPTEN__ || __NX__ - // FIXME: do something -#else - pid_t m_pid = 0; - int m_fd = -1; -#endif -}; - -class platform -{ -protected: -#if _WIN32 - // Helper class around LoadLibraryA() and GetProcAddress() with some safety - class dll - { - public: - dll(std::string const &name); - ~dll(); - - template class proc - { - public: - proc(dll const &lib, std::string const &sym) - : m_proc(reinterpret_cast((void *)::GetProcAddress(lib.handle, sym.c_str()))) - {} - - operator bool() const { return m_proc != nullptr; } - operator T *() const { return m_proc; } - - private: - T *m_proc; - }; - - private: - HMODULE handle; - }; - - // Helper class around CoInitialize() and CoUnInitialize() - class ole32_dll : public dll - { - public: - ole32_dll(); - ~ole32_dll(); - bool is_initialized(); - - private: - HRESULT m_state; - }; - - // Helper class around CreateActCtx() and ActivateActCtx() - class new_style_context - { - public: - new_style_context(); - ~new_style_context(); - - private: - HANDLE create(); - ULONG_PTR m_cookie = 0; - }; -#endif -}; - -class dialog : protected settings, protected platform -{ -public: - bool ready(int timeout = default_wait_timeout) const; - bool kill() const; - -protected: - explicit dialog(); - - std::vector desktop_helper() const; - static std::string buttons_to_name(choice _choice); - static std::string get_icon_name(icon _icon); - - std::string powershell_quote(std::string const &str) const; - std::string osascript_quote(std::string const &str) const; - std::string shell_quote(std::string const &str) const; - - // Keep handle to executing command - std::shared_ptr m_async; -}; - -class file_dialog : public dialog -{ -protected: - enum type - { - open, - save, - folder, - }; - - file_dialog(type in_type, - std::string const &title, - std::string const &default_path = "", - std::vector const &filters = {}, - opt options = opt::none); - -protected: - std::string string_result(); - std::vector vector_result(); - -#if _WIN32 - static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); -#if PFD_HAS_IFILEDIALOG - std::string select_folder_vista(IFileDialog *ifd, bool force_path); -#endif - - std::wstring m_wtitle; - std::wstring m_wdefault_path; - - std::vector m_vector_result; -#endif -}; - -} // namespace internal - -// -// The path class provides some platform-specific path constants -// - -class path : protected internal::platform -{ -public: - static std::string home(); - static std::string separator(); -}; - -// -// The notify widget -// - -class notify : public internal::dialog -{ -public: - notify(std::string const &title, - std::string const &message, - icon _icon = icon::info); -}; - -// -// The message widget -// - -class message : public internal::dialog -{ -public: - message(std::string const &title, - std::string const &text, - choice _choice = choice::ok_cancel, - icon _icon = icon::info); - - button result(); - -private: - // Some extra logic to map the exit code to button number - std::map m_mappings; -}; - -// -// The open_file, save_file, and open_folder widgets -// - -class open_file : public internal::file_dialog -{ -public: - open_file(std::string const &title, - std::string const &default_path = "", - std::vector const &filters = { "All Files", "*" }, - opt options = opt::none); - -#if defined(__has_cpp_attribute) -#if __has_cpp_attribute(deprecated) - // Backwards compatibility - [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] -#endif -#endif - open_file(std::string const &title, - std::string const &default_path, - std::vector const &filters, - bool allow_multiselect); - - std::vector result(); -}; - -class save_file : public internal::file_dialog -{ -public: - save_file(std::string const &title, - std::string const &default_path = "", - std::vector const &filters = { "All Files", "*" }, - opt options = opt::none); - -#if defined(__has_cpp_attribute) -#if __has_cpp_attribute(deprecated) - // Backwards compatibility - [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] -#endif -#endif - save_file(std::string const &title, - std::string const &default_path, - std::vector const &filters, - bool confirm_overwrite); - - std::string result(); -}; - -class select_folder : public internal::file_dialog -{ -public: - select_folder(std::string const &title, - std::string const &default_path = "", - opt options = opt::none); - - std::string result(); -}; - -// -// Below this are all the method implementations. You may choose to define the -// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except -// in one place. This may reduce compilation times. -// - -#if !defined PFD_SKIP_IMPLEMENTATION - -// internal free functions implementations - -namespace internal -{ - -#if _WIN32 -static inline std::wstring str2wstr(std::string const &str) -{ - int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); - std::wstring ret(len, '\0'); - MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); - return ret; -} - -static inline std::string wstr2str(std::wstring const &str) -{ - int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); - std::string ret(len, '\0'); - WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); - return ret; -} - -static inline bool is_vista() -{ - OSVERSIONINFOEXW osvi; - memset(&osvi, 0, sizeof(osvi)); - DWORDLONG const mask = VerSetConditionMask( - VerSetConditionMask( - VerSetConditionMask( - 0, VER_MAJORVERSION, VER_GREATER_EQUAL), - VER_MINORVERSION, VER_GREATER_EQUAL), - VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); - osvi.dwOSVersionInfoSize = sizeof(osvi); - osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); - osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); - osvi.wServicePackMajor = 0; - - return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; -} -#endif - -// This is necessary until C++20 which will have std::string::ends_with() etc. - -static inline bool ends_with(std::string const &str, std::string const &suffix) -{ - return suffix.size() <= str.size() && - str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; -} - -static inline bool starts_with(std::string const &str, std::string const &prefix) -{ - return prefix.size() <= str.size() && - str.compare(0, prefix.size(), prefix) == 0; -} - -// This is necessary until C++17 which will have std::filesystem::is_directory - -static inline bool is_directory(std::string const &path) -{ -#if _WIN32 - auto attr = GetFileAttributesA(path.c_str()); - return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY); -#elif __EMSCRIPTEN__ - // TODO - return false; -#else - struct stat s; - return stat(path.c_str(), &s) == 0 && S_ISDIR(s.st_mode); -#endif -} - -// This is necessary because getenv is not thread-safe - -static inline std::string getenv(std::string const &str) -{ -#if _MSC_VER - char *buf = nullptr; - size_t size = 0; - if (_dupenv_s(&buf, &size, str.c_str()) == 0 && buf) - { - std::string ret(buf); - free(buf); - return ret; - } - return ""; -#else - auto buf = std::getenv(str.c_str()); - return buf ? buf : ""; -#endif -} - -} // namespace internal - -// settings implementation - -inline settings::settings(bool resync) -{ - flags(flag::is_scanned) &= !resync; - - if (flags(flag::is_scanned)) - return; - - auto pfd_verbose = internal::getenv("PFD_VERBOSE"); - auto match_no = std::regex("(|0|no|false)", std::regex_constants::icase); - if (!std::regex_match(pfd_verbose, match_no)) - flags(flag::is_verbose) = true; - -#if _WIN32 - flags(flag::is_vista) = internal::is_vista(); -#elif !__APPLE__ - flags(flag::has_zenity) = check_program("zenity"); - flags(flag::has_matedialog) = check_program("matedialog"); - flags(flag::has_qarma) = check_program("qarma"); - flags(flag::has_kdialog) = check_program("kdialog"); - - // If multiple helpers are available, try to default to the best one - if (flags(flag::has_zenity) && flags(flag::has_kdialog)) - { - auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP"); - if (desktop_name == std::string("gnome")) - flags(flag::has_kdialog) = false; - else if (desktop_name == std::string("KDE")) - flags(flag::has_zenity) = false; - } -#endif - - flags(flag::is_scanned) = true; -} - -inline bool settings::available() -{ -#if _WIN32 - return true; -#elif __APPLE__ - return true; -#elif __EMSCRIPTEN__ - // FIXME: Return true after implementation is complete. - return false; -#else - settings tmp; - return tmp.flags(flag::has_zenity) || - tmp.flags(flag::has_matedialog) || - tmp.flags(flag::has_qarma) || - tmp.flags(flag::has_kdialog); -#endif -} - -inline void settings::verbose(bool value) -{ - settings().flags(flag::is_verbose) = value; -} - -inline void settings::rescan() -{ - settings(/* resync = */ true); -} - -// Check whether a program is present using “which”. -inline bool settings::check_program(std::string const &program) -{ -#if _WIN32 - (void)program; - return false; -#elif __EMSCRIPTEN__ - (void)program; - return false; -#else - int exit_code = -1; - internal::executor async; - async.start_process({"/bin/sh", "-c", "which " + program}); - async.result(&exit_code); - return exit_code == 0; -#endif -} - -inline bool settings::is_osascript() const -{ -#if __APPLE__ - return true; -#else - return false; -#endif -} - -inline bool settings::is_zenity() const -{ - return flags(flag::has_zenity) || - flags(flag::has_matedialog) || - flags(flag::has_qarma); -} - -inline bool settings::is_kdialog() const -{ - return flags(flag::has_kdialog); -} - -inline bool const &settings::flags(flag in_flag) const -{ - static bool flags[size_t(flag::max_flag)]; - return flags[size_t(in_flag)]; -} - -inline bool &settings::flags(flag in_flag) -{ - return const_cast(static_cast(this)->flags(in_flag)); -} - -// path implementation -inline std::string path::home() -{ -#if _WIN32 - // First try the USERPROFILE environment variable - auto user_profile = internal::getenv("USERPROFILE"); - if (user_profile.size() > 0) - return user_profile; - // Otherwise, try GetUserProfileDirectory() - HANDLE token = nullptr; - DWORD len = MAX_PATH; - char buf[MAX_PATH] = { '\0' }; - if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) - { - dll userenv("userenv.dll"); - dll::proc get_user_profile_directory(userenv, "GetUserProfileDirectoryA"); - get_user_profile_directory(token, buf, &len); - CloseHandle(token); - if (*buf) - return buf; - } -#elif __EMSCRIPTEN__ - return "/"; -#else - // First try the HOME environment variable - auto home = internal::getenv("HOME"); - if (home.size() > 0) - return home; - // Otherwise, try getpwuid_r() - size_t len = 4096; -#if defined(_SC_GETPW_R_SIZE_MAX) - auto size_max = sysconf(_SC_GETPW_R_SIZE_MAX); - if (size_max != -1) - len = size_t(size_max); -#endif - std::vector buf(len); - struct passwd pwd, *result; - if (getpwuid_r(getuid(), &pwd, buf.data(), buf.size(), &result) == 0) - return result->pw_dir; -#endif - return "/"; -} - -inline std::string path::separator() -{ -#if _WIN32 - return "\\"; -#else - return "/"; -#endif -} - -// executor implementation - -inline std::string internal::executor::result(int *exit_code /* = nullptr */) -{ - stop(); - if (exit_code) - *exit_code = m_exit_code; - return m_stdout; -} - -inline bool internal::executor::kill() -{ -#if _WIN32 - if (m_future.valid()) - { - // Close all windows that weren’t open when we started the future - auto previous_windows = m_windows; - EnumWindows(&enum_windows_callback, (LPARAM)this); - for (auto hwnd : m_windows) - if (previous_windows.find(hwnd) == previous_windows.end()) - { - SendMessage(hwnd, WM_CLOSE, 0, 0); - // Also send IDNO in case of a Yes/No or Abort/Retry/Ignore messagebox - SendMessage(hwnd, WM_COMMAND, IDNO, 0); - } - } -#elif __EMSCRIPTEN__ || __NX__ - // FIXME: do something - return false; // cannot kill -#else - ::kill(m_pid, SIGKILL); -#endif - stop(); - return true; -} - -#if _WIN32 -inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) -{ - auto that = (executor *)lParam; - - DWORD pid; - auto tid = GetWindowThreadProcessId(hwnd, &pid); - if (tid == that->m_tid) - that->m_windows.insert(hwnd); - return TRUE; -} -#endif - -#if _WIN32 -inline void internal::executor::start_func(std::function const &fun) -{ - stop(); - - auto trampoline = [fun, this]() - { - // Save our thread id so that the caller can cancel us - m_tid = GetCurrentThreadId(); - EnumWindows(&enum_windows_callback, (LPARAM)this); - m_cond.notify_all(); - return fun(&m_exit_code); - }; - - std::unique_lock lock(m_mutex); - m_future = std::async(std::launch::async, trampoline); - m_cond.wait(lock); - m_running = true; -} - -#elif __EMSCRIPTEN__ -inline void internal::executor::start(int exit_code) -{ - m_exit_code = exit_code; -} - -#else -inline void internal::executor::start_process(std::vector const &command) -{ - stop(); - m_stdout.clear(); - m_exit_code = -1; - - int in[2], out[2]; - if (pipe(in) != 0 || pipe(out) != 0) - return; - - m_pid = fork(); - if (m_pid < 0) - return; - - close(in[m_pid ? 0 : 1]); - close(out[m_pid ? 1 : 0]); - - if (m_pid == 0) - { - dup2(in[0], STDIN_FILENO); - dup2(out[1], STDOUT_FILENO); - - // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) - int fd = open("/dev/null", O_WRONLY); - dup2(fd, STDERR_FILENO); - close(fd); - - std::vector args; - std::transform(command.cbegin(), command.cend(), std::back_inserter(args), - [](std::string const &s) { return const_cast(s.c_str()); }); - args.push_back(nullptr); // null-terminate argv[] - - execvp(args[0], args.data()); - exit(1); - } - - close(in[1]); - m_fd = out[0]; - auto flags = fcntl(m_fd, F_GETFL); - fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); - - m_running = true; -} -#endif - -inline internal::executor::~executor() -{ - stop(); -} - -inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) -{ - if (!m_running) - return true; - -#if _WIN32 - if (m_future.valid()) - { - auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); - if (status != std::future_status::ready) - { - // On Windows, we need to run the message pump. If the async - // thread uses a Windows API dialog, it may be attached to the - // main thread and waiting for messages that only we can dispatch. - MSG msg; - while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - return false; - } - - m_stdout = m_future.get(); - } -#elif __EMSCRIPTEN__ || __NX__ - // FIXME: do something - (void)timeout; -#else - char buf[BUFSIZ]; - ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore - if (received > 0) - { - m_stdout += std::string(buf, received); - return false; - } - - // Reap child process if it is dead. It is possible that the system has already reaped it - // (this happens when the calling application handles or ignores SIG_CHLD) and results in - // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for - // a little while. - int status; - pid_t child = waitpid(m_pid, &status, WNOHANG); - if (child != m_pid && (child >= 0 || errno != ECHILD)) - { - // FIXME: this happens almost always at first iteration - std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); - return false; - } - - close(m_fd); - m_exit_code = WEXITSTATUS(status); -#endif - - m_running = false; - return true; -} - -inline void internal::executor::stop() -{ - // Loop until the user closes the dialog - while (!ready()) - ; -} - -// dll implementation - -#if _WIN32 -inline internal::platform::dll::dll(std::string const &name) - : handle(::LoadLibraryA(name.c_str())) -{} - -inline internal::platform::dll::~dll() -{ - if (handle) - ::FreeLibrary(handle); -} -#endif // _WIN32 - -// ole32_dll implementation - -#if _WIN32 -inline internal::platform::ole32_dll::ole32_dll() - : dll("ole32.dll") -{ - // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. - // See https://github.com/samhocevar/portable-file-dialogs/issues/51 - auto coinit = proc(*this, "CoInitializeEx"); - m_state = coinit(nullptr, COINIT_MULTITHREADED); -} - -inline internal::platform::ole32_dll::~ole32_dll() -{ - if (is_initialized()) - proc(*this, "CoUninitialize")(); -} - -inline bool internal::platform::ole32_dll::is_initialized() -{ - return m_state == S_OK || m_state == S_FALSE; -} -#endif - -// new_style_context implementation - -#if _WIN32 -inline internal::platform::new_style_context::new_style_context() -{ - // Only create one activation context for the whole app lifetime. - static HANDLE hctx = create(); - - if (hctx != INVALID_HANDLE_VALUE) - ActivateActCtx(hctx, &m_cookie); -} - -inline internal::platform::new_style_context::~new_style_context() -{ - DeactivateActCtx(0, m_cookie); -} - -inline HANDLE internal::platform::new_style_context::create() -{ - // This “hack” seems to be necessary for this code to work on windows XP. - // Without it, dialogs do not show and close immediately. GetError() - // returns 0 so I don’t know what causes this. I was not able to reproduce - // this behavior on Windows 7 and 10 but just in case, let it be here for - // those versions too. - // This hack is not required if other dialogs are used (they load comdlg32 - // automatically), only if message boxes are used. - dll comdlg32("comdlg32.dll"); - - // Using approach as shown here: https://stackoverflow.com/a/10444161 - UINT len = ::GetSystemDirectoryA(nullptr, 0); - std::string sys_dir(len, '\0'); - ::GetSystemDirectoryA(&sys_dir[0], len); - - ACTCTXA act_ctx = - { - // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a - // crash with error “default context is already set”. - sizeof(act_ctx), - ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, - "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, nullptr, 0, - }; - - return ::CreateActCtxA(&act_ctx); -} -#endif // _WIN32 - -// dialog implementation - -inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const -{ - return m_async->ready(timeout); -} - -inline bool internal::dialog::kill() const -{ - return m_async->kill(); -} - -inline internal::dialog::dialog() - : m_async(std::make_shared()) -{ -} - -inline std::vector internal::dialog::desktop_helper() const -{ -#if __APPLE__ - return { "osascript" }; -#else - return { flags(flag::has_zenity) ? "zenity" - : flags(flag::has_matedialog) ? "matedialog" - : flags(flag::has_qarma) ? "qarma" - : flags(flag::has_kdialog) ? "kdialog" - : "echo" }; -#endif -} - -inline std::string internal::dialog::buttons_to_name(choice _choice) -{ - switch (_choice) - { - case choice::ok_cancel: return "okcancel"; - case choice::yes_no: return "yesno"; - case choice::yes_no_cancel: return "yesnocancel"; - case choice::retry_cancel: return "retrycancel"; - case choice::abort_retry_ignore: return "abortretryignore"; - /* case choice::ok: */ default: return "ok"; - } -} - -inline std::string internal::dialog::get_icon_name(icon _icon) -{ - switch (_icon) - { - case icon::warning: return "warning"; - case icon::error: return "error"; - case icon::question: return "question"; - // Zenity wants "information" but WinForms wants "info" - /* case icon::info: */ default: -#if _WIN32 - return "info"; -#else - return "information"; -#endif - } -} - -// This is only used for debugging purposes -inline std::ostream& operator <<(std::ostream &s, std::vector const &v) -{ - int not_first = 0; - for (auto &e : v) - s << (not_first++ ? " " : "") << e; - return s; -} - -// Properly quote a string for Powershell: replace ' or " with '' or "" -// FIXME: we should probably get rid of newlines! -// FIXME: the \" sequence seems unsafe, too! -// XXX: this is no longer used but I would like to keep it around just in case -inline std::string internal::dialog::powershell_quote(std::string const &str) const -{ - return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; -} - -// Properly quote a string for osascript: replace \ or " with \\ or \" -// XXX: this also used to replace ' with \' when popen was used, but it would be -// smarter to do shell_quote(osascript_quote(...)) if this is needed again. -inline std::string internal::dialog::osascript_quote(std::string const &str) const -{ - return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; -} - -// Properly quote a string for the shell: just replace ' with '\'' -// XXX: this is no longer used but I would like to keep it around just in case -inline std::string internal::dialog::shell_quote(std::string const &str) const -{ - return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; -} - -// file_dialog implementation - -inline internal::file_dialog::file_dialog(type in_type, - std::string const &title, - std::string const &default_path /* = "" */, - std::vector const &filters /* = {} */, - opt options /* = opt::none */) -{ -#if _WIN32 - std::string filter_list; - std::regex whitespace(" *"); - for (size_t i = 0; i + 1 < filters.size(); i += 2) - { - filter_list += filters[i] + '\0'; - filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; - } - filter_list += '\0'; - - m_async->start_func([this, in_type, title, default_path, filter_list, - options](int *exit_code) -> std::string - { - (void)exit_code; - m_wtitle = internal::str2wstr(title); - m_wdefault_path = internal::str2wstr(default_path); - auto wfilter_list = internal::str2wstr(filter_list); - - // Initialise COM. This is required for the new folder selection window, - // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) - // and to avoid random crashes with GetOpenFileNameW() (see - // https://github.com/samhocevar/portable-file-dialogs/issues/51) - ole32_dll ole32; - - // Folder selection uses a different method - if (in_type == type::folder) - { -#if PFD_HAS_IFILEDIALOG - if (flags(flag::is_vista)) - { - // On Vista and higher we should be able to use IFileDialog for folder selection - IFileDialog *ifd; - HRESULT hr = dll::proc(ole32, "CoCreateInstance") - (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); - - // In case CoCreateInstance fails (which it should not), try legacy approach - if (SUCCEEDED(hr)) - return select_folder_vista(ifd, options & opt::force_path); - } -#endif - - BROWSEINFOW bi; - memset(&bi, 0, sizeof(bi)); - - bi.lpfn = &bffcallback; - bi.lParam = (LPARAM)this; - - if (flags(flag::is_vista)) - { - if (ole32.is_initialized()) - bi.ulFlags |= BIF_NEWDIALOGSTYLE; - bi.ulFlags |= BIF_EDITBOX; - bi.ulFlags |= BIF_STATUSTEXT; - } - - auto *list = SHBrowseForFolderW(&bi); - std::string ret; - if (list) - { - auto buffer = new wchar_t[MAX_PATH]; - SHGetPathFromIDListW(list, buffer); - dll::proc(ole32, "CoTaskMemFree")(list); - ret = internal::wstr2str(buffer); - delete[] buffer; - } - return ret; - } - - OPENFILENAMEW ofn; - memset(&ofn, 0, sizeof(ofn)); - ofn.lStructSize = sizeof(OPENFILENAMEW); - ofn.hwndOwner = GetActiveWindow(); - - ofn.lpstrFilter = wfilter_list.c_str(); - - auto woutput = std::wstring(MAX_PATH * 256, L'\0'); - ofn.lpstrFile = (LPWSTR)woutput.data(); - ofn.nMaxFile = (DWORD)woutput.size(); - if (!m_wdefault_path.empty()) - { - // If a directory was provided, use it as the initial directory. If - // a valid path was provided, use it as the initial file. Otherwise, - // let the Windows API decide. - auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); - if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) - ofn.lpstrInitialDir = m_wdefault_path.c_str(); - else if (m_wdefault_path.size() <= woutput.size()) - //second argument is size of buffer, not length of string - StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); - else - { - ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); - ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); - } - } - ofn.lpstrTitle = m_wtitle.c_str(); - ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; - - dll comdlg32("comdlg32.dll"); - - // Apply new visual style (required for windows XP) - new_style_context ctx; - - if (in_type == type::save) - { - if (!(options & opt::force_overwrite)) - ofn.Flags |= OFN_OVERWRITEPROMPT; - - dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); - if (get_save_file_name(&ofn) == 0) - return ""; - return internal::wstr2str(woutput.c_str()); - } - else - { - if (options & opt::multiselect) - ofn.Flags |= OFN_ALLOWMULTISELECT; - ofn.Flags |= OFN_PATHMUSTEXIST; - - dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); - if (get_open_file_name(&ofn) == 0) - return ""; - } - - std::string prefix; - for (wchar_t const *p = woutput.c_str(); *p; ) - { - auto filename = internal::wstr2str(p); - p += wcslen(p); - // In multiselect mode, we advance p one wchar further and - // check for another filename. If there is one and the - // prefix is empty, it means we just read the prefix. - if ((options & opt::multiselect) && *++p && prefix.empty()) - { - prefix = filename + "/"; - continue; - } - - m_vector_result.push_back(prefix + filename); - } - - return ""; - }); -#elif __EMSCRIPTEN__ - // FIXME: do something - (void)in_type; - (void)title; - (void)default_path; - (void)filters; - (void)options; -#else - auto command = desktop_helper(); - - if (is_osascript()) - { - std::string script = "set ret to choose"; - switch (in_type) - { - case type::save: - script += " file name"; - break; - case type::open: default: - script += " file"; - if (options & opt::multiselect) - script += " with multiple selections allowed"; - break; - case type::folder: - script += " folder"; - break; - } - - if (default_path.size()) - { - if (in_type == type::folder || is_directory(default_path)) - script += " default location "; - else - script += " default name "; - script += osascript_quote(default_path); - } - - script += " with prompt " + osascript_quote(title); - - if (in_type == type::open) - { - // Concatenate all user-provided filter patterns - std::string patterns; - for (size_t i = 0; i < filters.size() / 2; ++i) - patterns += " " + filters[2 * i + 1]; - - // Split the pattern list to check whether "*" is in there; if it - // is, we have to disable filters because there is no mechanism in - // OS X for the user to override the filter. - std::regex sep("\\s+"); - std::string filter_list; - bool has_filter = true; - std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); - std::sregex_token_iterator end; - for ( ; iter != end; ++iter) - { - auto pat = iter->str(); - if (pat == "*" || pat == "*.*") - has_filter = false; - else if (internal::starts_with(pat, "*.")) - filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2)); - } - - if (has_filter && filter_list.size() > 0) - { - // There is a weird AppleScript bug where file extensions of length != 3 are - // ignored, e.g. type{"txt"} works, but type{"json"} does not. Fortunately if - // the whole list starts with a 3-character extension, everything works again. - // We use "///" for such an extension because we are sure it cannot appear in - // an actual filename. - script += " of type {\"///\"" + filter_list + "}"; - } - } - - if (in_type == type::open && (options & opt::multiselect)) - { - script += "\nset s to \"\""; - script += "\nrepeat with i in ret"; - script += "\n set s to s & (POSIX path of i) & \"\\n\""; - script += "\nend repeat"; - script += "\ncopy s to stdout"; - } - else - { - script += "\nPOSIX path of ret"; - } - - command.push_back("-e"); - command.push_back(script); - } - else if (is_zenity()) - { - command.push_back("--file-selection"); - - // If the default path is a directory, make sure it ends with "/" otherwise zenity will - // open the file dialog in the parent directory. - auto filename_arg = "--filename=" + default_path; - if (in_type != type::folder && !ends_with(default_path, "/") && internal::is_directory(default_path)) - filename_arg += "/"; - command.push_back(filename_arg); - - command.push_back("--title"); - command.push_back(title); - command.push_back("--separator=\n"); - - for (size_t i = 0; i < filters.size() / 2; ++i) - { - command.push_back("--file-filter"); - command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); - } - - if (in_type == type::save) - command.push_back("--save"); - if (in_type == type::folder) - command.push_back("--directory"); - if (!(options & opt::force_overwrite)) - command.push_back("--confirm-overwrite"); - if (options & opt::multiselect) - command.push_back("--multiple"); - } - else if (is_kdialog()) - { - switch (in_type) - { - case type::save: command.push_back("--getsavefilename"); break; - case type::open: command.push_back("--getopenfilename"); break; - case type::folder: command.push_back("--getexistingdirectory"); break; - } - if (options & opt::multiselect) - { - command.push_back("--multiple"); - command.push_back("--separate-output"); - } - - command.push_back(default_path); - - std::string filter; - for (size_t i = 0; i < filters.size() / 2; ++i) - filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; - command.push_back(filter); - - command.push_back("--title"); - command.push_back(title); - } - - if (flags(flag::is_verbose)) - std::cerr << "pfd: " << command << std::endl; - - m_async->start_process(command); -#endif -} - -inline std::string internal::file_dialog::string_result() -{ -#if _WIN32 - return m_async->result(); -#else - auto ret = m_async->result(); - // Strip potential trailing newline (zenity). Also strip trailing slash - // added by osascript for consistency with other backends. - while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) - ret.pop_back(); - return ret; -#endif -} - -inline std::vector internal::file_dialog::vector_result() -{ -#if _WIN32 - m_async->result(); - return m_vector_result; -#else - std::vector ret; - auto result = m_async->result(); - for (;;) - { - // Split result along newline characters - auto i = result.find('\n'); - if (i == 0 || i == std::string::npos) - break; - ret.push_back(result.substr(0, i)); - result = result.substr(i + 1, result.size()); - } - return ret; -#endif -} - -#if _WIN32 -// Use a static function to pass as BFFCALLBACK for legacy folder select -inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, - LPARAM, LPARAM pData) -{ - auto inst = (file_dialog *)pData; - switch (uMsg) - { - case BFFM_INITIALIZED: - SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); - break; - } - return 0; -} - -#if PFD_HAS_IFILEDIALOG -inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) -{ - std::string result; - - IShellItem *folder; - - // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) - dll shell32("shell32.dll"); - dll::proc - create_item(shell32, "SHCreateItemFromParsingName"); - - if (!create_item) - return ""; - - auto hr = create_item(m_wdefault_path.c_str(), - nullptr, - IID_PPV_ARGS(&folder)); - - // Set default folder if found. This only sets the default folder. If - // Windows has any info about the most recently selected folder, it - // will display it instead. Generally, calling SetFolder() to set the - // current directory “is not a good or expected user experience and - // should therefore be avoided”: - // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder - if (SUCCEEDED(hr)) - { - if (force_path) - ifd->SetFolder(folder); - else - ifd->SetDefaultFolder(folder); - folder->Release(); - } - - // Set the dialog title and option to select folders - ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM); - ifd->SetTitle(m_wtitle.c_str()); - - hr = ifd->Show(GetActiveWindow()); - if (SUCCEEDED(hr)) - { - IShellItem* item; - hr = ifd->GetResult(&item); - if (SUCCEEDED(hr)) - { - wchar_t* wname = nullptr; - // This is unlikely to fail because we use FOS_FORCEFILESYSTEM, but try - // to output a debug message just in case. - if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &wname))) - { - result = internal::wstr2str(std::wstring(wname)); - dll::proc(ole32_dll(), "CoTaskMemFree")(wname); - } - else - { - if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname))) - { - auto name = internal::wstr2str(std::wstring(wname)); - dll::proc(ole32_dll(), "CoTaskMemFree")(wname); - std::cerr << "pfd: failed to get path for " << name << std::endl; - } - else - std::cerr << "pfd: item of unknown type selected" << std::endl; - } - - item->Release(); - } - } - - ifd->Release(); - - return result; -} -#endif -#endif - -// notify implementation - -inline notify::notify(std::string const &title, - std::string const &message, - icon _icon /* = icon::info */) -{ - if (_icon == icon::question) // Not supported by notifications - _icon = icon::info; - -#if _WIN32 - // Use a static shared pointer for notify_icon so that we can delete - // it whenever we need to display a new one, and we can also wait - // until the program has finished running. - struct notify_icon_data : public NOTIFYICONDATAW - { - ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } - }; - - static std::shared_ptr nid; - - // Release the previous notification icon, if any, and allocate a new - // one. Note that std::make_shared() does value initialization, so there - // is no need to memset the structure. - nid = nullptr; - nid = std::make_shared(); - - // For XP support - nid->cbSize = NOTIFYICONDATAW_V2_SIZE; - nid->hWnd = nullptr; - nid->uID = 0; - - // Flag Description: - // - NIF_ICON The hIcon member is valid. - // - NIF_MESSAGE The uCallbackMessage member is valid. - // - NIF_TIP The szTip member is valid. - // - NIF_STATE The dwState and dwStateMask members are valid. - // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. - // - NIF_GUID Reserved. - nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; - - // Flag Description - // - NIIF_ERROR An error icon. - // - NIIF_INFO An information icon. - // - NIIF_NONE No icon. - // - NIIF_WARNING A warning icon. - // - NIIF_ICON_MASK Version 6.0. Reserved. - // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips - switch (_icon) - { - case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; - case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; - /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; - } - - ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL - { - ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); - return false; - }; - - nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); - ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); - - nid->uTimeout = 5000; - - StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); - StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); - - // Display the new icon - Shell_NotifyIconW(NIM_ADD, nid.get()); -#elif __EMSCRIPTEN__ - // FIXME: do something - (void)title; - (void)message; -#else - auto command = desktop_helper(); - - if (is_osascript()) - { - command.push_back("-e"); - command.push_back("display notification " + osascript_quote(message) + - " with title " + osascript_quote(title)); - } - else if (is_zenity()) - { - command.push_back("--notification"); - command.push_back("--window-icon"); - command.push_back(get_icon_name(_icon)); - command.push_back("--text"); - command.push_back(title + "\n" + message); - } - else if (is_kdialog()) - { - command.push_back("--icon"); - command.push_back(get_icon_name(_icon)); - command.push_back("--title"); - command.push_back(title); - command.push_back("--passivepopup"); - command.push_back(message); - command.push_back("5"); - } - - if (flags(flag::is_verbose)) - std::cerr << "pfd: " << command << std::endl; - - m_async->start_process(command); -#endif -} - -// message implementation - -inline message::message(std::string const &title, - std::string const &text, - choice _choice /* = choice::ok_cancel */, - icon _icon /* = icon::info */) -{ -#if _WIN32 - // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought - // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 - UINT style = MB_SYSTEMMODAL; - switch (_icon) - { - case icon::warning: style |= MB_ICONWARNING; break; - case icon::error: style |= MB_ICONERROR; break; - case icon::question: style |= MB_ICONQUESTION; break; - /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; - } - - switch (_choice) - { - case choice::ok_cancel: style |= MB_OKCANCEL; break; - case choice::yes_no: style |= MB_YESNO; break; - case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; - case choice::retry_cancel: style |= MB_RETRYCANCEL; break; - case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; - /* case choice::ok: */ default: style |= MB_OK; break; - } - - m_mappings[IDCANCEL] = button::cancel; - m_mappings[IDOK] = button::ok; - m_mappings[IDYES] = button::yes; - m_mappings[IDNO] = button::no; - m_mappings[IDABORT] = button::abort; - m_mappings[IDRETRY] = button::retry; - m_mappings[IDIGNORE] = button::ignore; - - m_async->start_func([text, title, style](int* exit_code) -> std::string - { - auto wtext = internal::str2wstr(text); - auto wtitle = internal::str2wstr(title); - // Apply new visual style (required for all Windows versions) - new_style_context ctx; - *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); - return ""; - }); - -#elif __EMSCRIPTEN__ - std::string full_message; - switch (_icon) - { - case icon::warning: full_message = "⚠️"; break; - case icon::error: full_message = "⛔"; break; - case icon::question: full_message = "❓"; break; - /* case icon::info: */ default: full_message = "ℹ"; break; - } - - full_message += ' ' + title + "\n\n" + text; - - // This does not really start an async task; it just passes the - // EM_ASM_INT return value to a fake start() function. - m_async->start(EM_ASM_INT( - { - if ($1) - return window.confirm(UTF8ToString($0)) ? 0 : -1; - alert(UTF8ToString($0)); - return 0; - }, full_message.c_str(), _choice == choice::ok_cancel)); -#else - auto command = desktop_helper(); - - if (is_osascript()) - { - std::string script = "display dialog " + osascript_quote(text) + - " with title " + osascript_quote(title); - auto if_cancel = button::cancel; - switch (_choice) - { - case choice::ok_cancel: - script += "buttons {\"OK\", \"Cancel\"}" - " default button \"OK\"" - " cancel button \"Cancel\""; - break; - case choice::yes_no: - script += "buttons {\"Yes\", \"No\"}" - " default button \"Yes\"" - " cancel button \"No\""; - if_cancel = button::no; - break; - case choice::yes_no_cancel: - script += "buttons {\"Yes\", \"No\", \"Cancel\"}" - " default button \"Yes\"" - " cancel button \"Cancel\""; - break; - case choice::retry_cancel: - script += "buttons {\"Retry\", \"Cancel\"}" - " default button \"Retry\"" - " cancel button \"Cancel\""; - break; - case choice::abort_retry_ignore: - script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" - " default button \"Abort\"" - " cancel button \"Retry\""; - if_cancel = button::retry; - break; - case choice::ok: default: - script += "buttons {\"OK\"}" - " default button \"OK\"" - " cancel button \"OK\""; - if_cancel = button::ok; - break; - } - m_mappings[1] = if_cancel; - m_mappings[256] = if_cancel; // XXX: I think this was never correct - script += " with icon "; - switch (_icon) - { - #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ - "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" - case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; - case icon::warning: script += "caution"; break; - case icon::error: script += "stop"; break; - case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; - #undef PFD_OSX_ICON - } - - command.push_back("-e"); - command.push_back(script); - } - else if (is_zenity()) - { - switch (_choice) - { - case choice::ok_cancel: - command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; - case choice::yes_no: - // Do not use standard --question because it causes “No” to return -1, - // which is inconsistent with the “Yes/No/Cancel” mode below. - command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; - case choice::yes_no_cancel: - command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; - case choice::retry_cancel: - command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; - case choice::abort_retry_ignore: - command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; - case choice::ok: - default: - switch (_icon) - { - case icon::error: command.push_back("--error"); break; - case icon::warning: command.push_back("--warning"); break; - default: command.push_back("--info"); break; - } - } - - command.insert(command.end(), { "--title", title, - "--width=300", "--height=0", // sensible defaults - "--no-markup", // do not interpret text as Pango markup - "--text", text, - "--icon-name=dialog-" + get_icon_name(_icon) }); - } - else if (is_kdialog()) - { - if (_choice == choice::ok) - { - switch (_icon) - { - case icon::error: command.push_back("--error"); break; - case icon::warning: command.push_back("--sorry"); break; - default: command.push_back("--msgbox"); break; - } - } - else - { - std::string flag = "--"; - if (_icon == icon::warning || _icon == icon::error) - flag += "warning"; - flag += "yesno"; - if (_choice == choice::yes_no_cancel) - flag += "cancel"; - command.push_back(flag); - if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) - { - m_mappings[0] = button::yes; - m_mappings[256] = button::no; - } - } - - command.push_back(text); - command.push_back("--title"); - command.push_back(title); - - // Must be after the above part - if (_choice == choice::ok_cancel) - command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); - } - - if (flags(flag::is_verbose)) - std::cerr << "pfd: " << command << std::endl; - - m_async->start_process(command); -#endif -} - -inline button message::result() -{ - int exit_code; - auto ret = m_async->result(&exit_code); - // osascript will say "button returned:Cancel\n" - // and others will just say "Cancel\n" - if (internal::ends_with(ret, "Cancel\n")) - return button::cancel; - if (internal::ends_with(ret, "OK\n")) - return button::ok; - if (internal::ends_with(ret, "Yes\n")) - return button::yes; - if (internal::ends_with(ret, "No\n")) - return button::no; - if (internal::ends_with(ret, "Abort\n")) - return button::abort; - if (internal::ends_with(ret, "Retry\n")) - return button::retry; - if (internal::ends_with(ret, "Ignore\n")) - return button::ignore; - if (m_mappings.count(exit_code) != 0) - return m_mappings[exit_code]; - return exit_code == 0 ? button::ok : button::cancel; -} - -// open_file implementation - -inline open_file::open_file(std::string const &title, - std::string const &default_path /* = "" */, - std::vector const &filters /* = { "All Files", "*" } */, - opt options /* = opt::none */) - : file_dialog(type::open, title, default_path, filters, options) -{ -} - -inline open_file::open_file(std::string const &title, - std::string const &default_path, - std::vector const &filters, - bool allow_multiselect) - : open_file(title, default_path, filters, - (allow_multiselect ? opt::multiselect : opt::none)) -{ -} - -inline std::vector open_file::result() -{ - return vector_result(); -} - -// save_file implementation - -inline save_file::save_file(std::string const &title, - std::string const &default_path /* = "" */, - std::vector const &filters /* = { "All Files", "*" } */, - opt options /* = opt::none */) - : file_dialog(type::save, title, default_path, filters, options) -{ -} - -inline save_file::save_file(std::string const &title, - std::string const &default_path, - std::vector const &filters, - bool confirm_overwrite) - : save_file(title, default_path, filters, - (confirm_overwrite ? opt::none : opt::force_overwrite)) -{ -} - -inline std::string save_file::result() -{ - return string_result(); -} - -// select_folder implementation - -inline select_folder::select_folder(std::string const &title, - std::string const &default_path /* = "" */, - opt options /* = opt::none */) - : file_dialog(type::folder, title, default_path, {}, options) -{ -} - -inline std::string select_folder::result() -{ - return string_result(); -} - -#endif // PFD_SKIP_IMPLEMENTATION - -} // namespace pfd diff --git a/ext/vma/vk_mem_alloc.cpp b/ext/vma/vk_mem_alloc.cpp index 615c41a081..b37e6ff976 100644 --- a/ext/vma/vk_mem_alloc.cpp +++ b/ext/vma/vk_mem_alloc.cpp @@ -11,6 +11,63 @@ using namespace PPSSPP_VK; +// The allocator has one owning thread (see VulkanContext::CreateDevice), so VMA gets no-op +// mutexes and plain counters instead of std::mutex and std::atomic. +#include +#include +#define VMA_CONFIGURATION_USER_INCLUDES_H + +class VmaOwnerMutex { +public: + void Lock() {} + void Unlock() {} + bool TryLock() { return true; } + void LockRead() {} + void UnlockRead() {} + bool TryLockRead() { return true; } + void LockWrite() {} + void UnlockWrite() {} + bool TryLockWrite() { return true; } +}; + +template +class VmaOwnerValue { +public: + VmaOwnerValue() : v_() {} + VmaOwnerValue(T v) : v_(v) {} + VmaOwnerValue(const VmaOwnerValue &) = delete; + VmaOwnerValue &operator=(const VmaOwnerValue &) = delete; + T load() const { return v_; } + void store(T v) { v_ = v; } + T fetch_add(T d) { T o = v_; v_ += d; return o; } + T fetch_sub(T d) { T o = v_; v_ -= d; return o; } + bool compare_exchange_strong(T &expected, T desired) { + if (v_ == expected) { + v_ = desired; + return true; + } + expected = v_; + return false; + } + bool compare_exchange_weak(T &expected, T desired) { return compare_exchange_strong(expected, desired); } + T operator=(T v) { v_ = v; return v; } + operator T() const { return v_; } + T operator++() { return ++v_; } + T operator--() { return --v_; } + T operator++(int) { return v_++; } + T operator--(int) { return v_--; } + T operator+=(T d) { return v_ += d; } + T operator-=(T d) { return v_ -= d; } +private: + T v_; +}; + +#define VMA_MUTEX VmaOwnerMutex +#define VMA_RW_MUTEX VmaOwnerMutex +#define VMA_ATOMIC_UINT32 VmaOwnerValue +#define VMA_ATOMIC_UINT64 VmaOwnerValue +#define VMA_ATOMIC_BOOL VmaOwnerValue + #undef VK_NO_PROTOTYPES #include "vk_mem_alloc.h" #define VK_NO_PROTOTYPES diff --git a/headless/Headless.cpp b/headless/Headless.cpp index aa43e8db9b..04604cc0a9 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -977,16 +977,22 @@ int main(int argc, const char* argv[]) { // We don't bother with a window. graphicsContext = new NullGraphicsContext(); } else { -#if PPSSPP_PLATFORM(ANDROID) || defined(HEADLESS_NO_SDL) +#if PPSSPP_PLATFORM(ANDROID) fprintf(stderr, "Headless graphics context creation is not supported on this platform.\n"); return 1; #else if (gpuCore == GPUCORE_VULKAN) { - // Vulkan renders into images of its own, with no window or swapchain. + // Vulkan renders into images of its own, with no window or swapchain, so it needs no SDL. VulkanGraphicsContext *vulkanContext = new VulkanGraphicsContext(); vulkanContext->SetOffscreen(480, 272); graphicsContext = vulkanContext; deviceSetting = &g_Config.sVulkanDevice; +#if defined(HEADLESS_NO_SDL) + } else { + fprintf(stderr, "Headless graphics context creation is not supported in this build.\n"); + return 1; + } +#else #if PPSSPP_PLATFORM(MAC) && defined(SDL) } else if (gpuCore == GPUCORE_GLES) { // So does OpenGL, into a framebuffer object of its own. @@ -1006,6 +1012,7 @@ int main(int argc, const char* argv[]) { return 1; } } +#endif #endif } diff --git a/headless/README.md b/headless/README.md index ea98d15a7c..9371bbaf06 100644 --- a/headless/README.md +++ b/headless/README.md @@ -84,7 +84,7 @@ The `--graphics` option selects the rendering backend: | `software` | Software rasterizer (most deterministic, recommended for tests) | | `gles` | OpenGL ES (desktop OpenGL on non-Windows) | | `directx11` | Direct3D 11 (Windows only) | -| `vulkan` | Vulkan | +| `vulkan` | Vulkan, offscreen (also in builds without SDL) | The software backend produces deterministic output across runs and is the default. Hardware backends may produce slightly different pixels due to precision differences in shaders and rasterization. diff --git a/test.py b/test.py index 6dfff703a6..b3d9288c91 100755 --- a/test.py +++ b/test.py @@ -554,6 +554,56 @@ known_failures = { "loongarch64": [ "cpu/fpu/fpu", ], + # Vulkan on lavapipe. These compare pixels against software-rendered references that a + # hardware backend doesn't match exactly; the lane is there for the threading, not the pixels. + "vulkan": [ + "gpu/clipping/guardband", + "gpu/clipping/homogeneous", + "gpu/clipping/xycull", + "gpu/commands/blend", + "gpu/commands/blend565", + "gpu/commands/fog", + "gpu/commands/light", + "gpu/complex/complex", + "gpu/depth/precision", + "gpu/depth/transformprecision", + "gpu/dither/dither", + "gpu/exact/coverage", + "gpu/exact/curves", + "gpu/exact/lighting", + "gpu/exact/lines", + "gpu/exact/nearclip", + "gpu/exact/pixel", + "gpu/exact/planes", + "gpu/exact/texture", + "gpu/exact/transform", + "gpu/filtering/linear", + "gpu/filtering/mipmaplinear", + "gpu/filtering/nearest", + "gpu/filtering/precisionlinear2d", + "gpu/filtering/precisionlinear3d", + "gpu/filtering/precisionnearest2d", + "gpu/filtering/precisionnearest3d", + "gpu/ge/get", + "gpu/primitives/bezier", + "gpu/primitives/continue", + "gpu/primitives/immediate", + "gpu/primitives/indices", + "gpu/primitives/spline", + "gpu/reflection/reflection", + "gpu/rendertarget/copy", + "gpu/rendertarget/rendertarget", + "gpu/texfunc/blend", + "gpu/texfunc/decal", + "gpu/texfunc/modulate", + "gpu/texmtx/normals", + "gpu/texmtx/prims", + "gpu/texmtx/source", + "gpu/textures/mipmap", + "gpu/textures/size", + "gpu/triangle/triangle", + "gpu/vertices/morph", + ], } tests_next = [ diff --git a/unittest/TestThreadManager.cpp b/unittest/TestThreadManager.cpp index e3eee548f1..6b58ae2a21 100644 --- a/unittest/TestThreadManager.cpp +++ b/unittest/TestThreadManager.cpp @@ -4,6 +4,7 @@ #include "Common/Log.h" #include "Common/TimeUtil.h" #include "Common/Thread/Barrier.h" +#include "Common/Thread/Event.h" #include "Common/Thread/ThreadManager.h" #include "Common/Thread/Channel.h" #include "Common/Thread/Promise.h" @@ -18,8 +19,11 @@ struct ResultObject { bool ok; }; +// Holds the result back until the parallel loop test is done, so BlockUntilReady really waits. +static Event *g_resultGate; + ResultObject *ResultProducer() { - sleep_ms(250, "test-result"); + g_resultGate->Wait(); printf("result produced: thread %d\n", GetCurrentThreadIdForDebug()); return new ResultObject{ true }; } @@ -36,7 +40,6 @@ bool TestMailbox() { } void rangeFunc(int lower, int upper) { - sleep_ms(30, "test-range"); printf(" - range %d-%d (thread %d)\n", lower, upper, GetCurrentThreadIdForDebug()); } @@ -130,12 +133,12 @@ bool TestThreadManager() { g_threadMan = &manager; + Event resultGate; + g_resultGate = &resultGate; Promise *object(Promise::Spawn(&manager, &ResultProducer, TaskType::IO_BLOCKING)); - if (!TestParallelLoop(&manager)) { - return false; - } - sleep_ms(100, "test-threadman"); + const bool loopOk = TestParallelLoop(&manager); + resultGate.Notify(); ResultObject *result = object->BlockUntilReady(); if (result) { @@ -143,6 +146,9 @@ bool TestThreadManager() { } delete object; + if (!loopOk) { + return false; + } if (!TestMailbox()) { return false;