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;