diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index eceb4e251..3785bade1 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -55,6 +55,8 @@ add_library(common
string_util.h
thirdparty/thread_pool.cpp
thirdparty/thread_pool.h
+ threading.cpp
+ threading.h
timer.cpp
timer.h
types.h
diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj
index e9e5935ff..5a905689e 100644
--- a/src/common/common.vcxproj
+++ b/src/common/common.vcxproj
@@ -64,6 +64,7 @@
true
+
@@ -131,6 +132,7 @@
true
+
diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters
index 22d0cf5a2..cec6b11db 100644
--- a/src/common/common.vcxproj.filters
+++ b/src/common/common.vcxproj.filters
@@ -137,6 +137,7 @@
+
@@ -250,6 +251,7 @@
+
diff --git a/src/common/http_downloader.cpp b/src/common/http_downloader.cpp
index 8fe209f68..b44dd5db2 100644
--- a/src/common/http_downloader.cpp
+++ b/src/common/http_downloader.cpp
@@ -37,7 +37,7 @@ void HTTPDownloader::CreateRequest(std::string url, Request::Callback callback)
req->type = Request::Type::Get;
req->url = std::move(url);
req->callback = std::move(callback);
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
std::unique_lock lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
@@ -57,7 +57,7 @@ void HTTPDownloader::CreatePostRequest(std::string url, std::string post_data, R
req->url = std::move(url);
req->post_data = std::move(post_data);
req->callback = std::move(callback);
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
std::unique_lock lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
@@ -76,7 +76,7 @@ void HTTPDownloader::LockedPollRequests(std::unique_lock& lock)
InternalPollRequests();
- const Common::Timer::Value current_time = Common::Timer::GetValue();
+ const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
u32 active_requests = 0;
u32 unstarted_requests = 0;
diff --git a/src/common/http_downloader_curl.cpp b/src/common/http_downloader_curl.cpp
index abcfbd774..3809a4750 100644
--- a/src/common/http_downloader_curl.cpp
+++ b/src/common/http_downloader_curl.cpp
@@ -81,7 +81,7 @@ void HTTPDownloaderCurl::ProcessRequest(Request* req)
if (pthread_sigmask(SIG_BLOCK, &new_block_mask, &old_block_mask) != 0)
Log_WarningPrint("Failed to block SIGPIPE");
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
int ret = curl_easy_perform(req->handle);
if (ret == CURLE_OK)
{
@@ -144,7 +144,7 @@ bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request)
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
m_thread_pool->Schedule(std::bind(&HTTPDownloaderCurl::ProcessRequest, this, req));
return true;
}
diff --git a/src/common/http_downloader_uwp.cpp b/src/common/http_downloader_uwp.cpp
index 0f1ff98a7..3a3573008 100644
--- a/src/common/http_downloader_uwp.cpp
+++ b/src/common/http_downloader_uwp.cpp
@@ -74,7 +74,7 @@ bool HTTPDownloaderUWP::StartRequest(HTTPDownloader::Request* request)
try
{
req->state.store(Request::State::Receiving);
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
const HttpResponseMessage response(req->request_async.get());
req->status_code = static_cast(response.StatusCode());
@@ -146,7 +146,7 @@ bool HTTPDownloaderUWP::StartRequest(HTTPDownloader::Request* request)
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
return true;
}
diff --git a/src/common/http_downloader_winhttp.cpp b/src/common/http_downloader_winhttp.cpp
index b99dede86..1aa95fb15 100644
--- a/src/common/http_downloader_winhttp.cpp
+++ b/src/common/http_downloader_winhttp.cpp
@@ -177,7 +177,7 @@ void CALLBACK HTTPDownloaderWinHttp::HTTPStatusCallback(HINTERNET hRequest, DWOR
const u32 new_size = req->io_position + dwStatusInformationLength;
Assert(new_size <= req->data.size());
req->data.resize(new_size);
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
@@ -275,7 +275,7 @@ bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request)
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
- req->start_time = Common::Timer::GetValue();
+ req->start_time = Common::Timer::GetCurrentValue();
return true;
}
diff --git a/src/common/image.cpp b/src/common/image.cpp
index bd5259b49..6e743f302 100644
--- a/src/common/image.cpp
+++ b/src/common/image.cpp
@@ -2,35 +2,529 @@
#include "byte_stream.h"
#include "file_system.h"
#include "log.h"
+#include "path.h"
+#include "scope_guard.h"
#include "stb_image.h"
#include "stb_image_write.h"
#include "string_util.h"
-Log_SetChannel(Common::Image);
+Log_SetChannel(Image);
-namespace Common {
-bool LoadImageFromFile(Common::RGBA8Image* image, const char* filename)
+using namespace Common;
+
+#if 0
+static bool PNGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
+static bool PNGBufferSaver(const RGBA8Image& image, std::vector* buffer, int quality);
+static bool PNGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp);
+static bool PNGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality);
+
+static bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
+static bool JPEGBufferSaver(const RGBA8Image& image, std::vector* buffer, int quality);
+static bool JPEGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp);
+static bool JPEGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality);
+#endif
+
+static bool STBBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
+static bool STBFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp);
+static bool STBBufferSaverPNG(const RGBA8Image& image, std::vector* buffer, int quality);
+static bool STBBufferSaverJPEG(const RGBA8Image& image, std::vector* buffer, int quality);
+static bool STBFileSaverPNG(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality);
+static bool STBFileSaverJPEG(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality);
+
+struct FormatHandler
+{
+ const char* extension;
+ bool (*buffer_loader)(RGBA8Image*, const void*, size_t);
+ bool (*buffer_saver)(const RGBA8Image&, std::vector*, int);
+ bool (*file_loader)(RGBA8Image*, const char*, std::FILE*);
+ bool (*file_saver)(const RGBA8Image&, const char*, std::FILE*, int);
+};
+
+static constexpr FormatHandler s_format_handlers[] = {
+#if 0
+ {"png", PNGBufferLoader, PNGBufferSaver, PNGFileLoader, PNGFileSaver},
+ {"jpg", JPEGBufferLoader, JPEGBufferSaver, JPEGFileLoader, JPEGFileSaver},
+ {"jpeg", JPEGBufferLoader, JPEGBufferSaver, JPEGFileLoader, JPEGFileSaver},
+#else
+ {"png", STBBufferLoader, STBBufferSaverPNG, STBFileLoader, STBFileSaverPNG},
+ {"jpg", STBBufferLoader, STBBufferSaverJPEG, STBFileLoader, STBFileSaverJPEG},
+ {"jpeg", STBBufferLoader, STBBufferSaverJPEG, STBFileLoader, STBFileSaverJPEG},
+#endif
+};
+
+static const FormatHandler* GetFormatHandler(const std::string_view& extension)
+{
+ for (const FormatHandler& handler : s_format_handlers)
+ {
+ if (StringUtil::Strncasecmp(extension.data(), handler.extension, extension.size()))
+ return &handler;
+ }
+
+ return nullptr;
+}
+
+RGBA8Image::RGBA8Image() = default;
+
+RGBA8Image::RGBA8Image(const RGBA8Image& copy) : Image(copy) {}
+
+RGBA8Image::RGBA8Image(u32 width, u32 height, const u32* pixels) : Image(width, height, pixels) {}
+
+RGBA8Image::RGBA8Image(RGBA8Image&& move) : Image(move) {}
+
+RGBA8Image& RGBA8Image::operator=(const RGBA8Image& copy)
+{
+ Image::operator=(copy);
+ return *this;
+}
+
+RGBA8Image& RGBA8Image::operator=(RGBA8Image&& move)
+{
+ Image::operator=(move);
+ return *this;
+}
+
+bool RGBA8Image::LoadFromFile(const char* filename)
{
auto fp = FileSystem::OpenManagedCFile(filename, "rb");
if (!fp)
+ return false;
+
+ return LoadFromFile(filename, fp.get());
+}
+
+bool RGBA8Image::SaveToFile(const char* filename, int quality) const
+{
+ auto fp = FileSystem::OpenManagedCFile(filename, "wb");
+ if (!fp)
+ return false;
+
+ if (SaveToFile(filename, fp.get(), quality))
+ return true;
+
+ // save failed
+ fp.reset();
+ FileSystem::DeleteFile(filename);
+ return false;
+}
+
+bool RGBA8Image::LoadFromFile(const char* filename, std::FILE* fp)
+{
+ const std::string_view extension(Path::GetExtension(filename));
+ const FormatHandler* handler = GetFormatHandler(extension);
+ if (!handler || !handler->file_loader)
{
+ Log_ErrorPrintf("Unknown extension '%.*s'", static_cast(extension.size()), extension.data());
return false;
}
- int width, height, file_channels;
- u8* pixel_data = stbi_load_from_file(fp.get(), &width, &height, &file_channels, 4);
- if (!pixel_data)
+ return handler->file_loader(this, filename, fp);
+}
+
+bool RGBA8Image::LoadFromBuffer(const char* filename, const void* buffer, size_t buffer_size)
+{
+ const std::string_view extension(Path::GetExtension(filename));
+ const FormatHandler* handler = GetFormatHandler(extension);
+ if (!handler || !handler->buffer_loader)
{
- const char* error_reason = stbi_failure_reason();
- Log_ErrorPrintf("Failed to load image from '%s': %s", filename, error_reason ? error_reason : "unknown error");
+ Log_ErrorPrintf("Unknown extension '%.*s'", static_cast(extension.size()), extension.data());
return false;
}
- image->SetPixels(static_cast(width), static_cast(height), reinterpret_cast(pixel_data));
- stbi_image_free(pixel_data);
+ return handler->buffer_loader(this, buffer, buffer_size);
+}
+
+bool RGBA8Image::SaveToFile(const char* filename, std::FILE* fp, int quality) const
+{
+ const std::string_view extension(Path::GetExtension(filename));
+ const FormatHandler* handler = GetFormatHandler(extension);
+ if (!handler || !handler->file_saver)
+ {
+ Log_ErrorPrintf("Unknown extension '%.*s'", static_cast(extension.size()), extension.data());
+ return false;
+ }
+
+ if (!handler->file_saver(*this, filename, fp, quality))
+ return false;
+
+ return (std::fflush(fp) == 0);
+}
+
+std::optional> RGBA8Image::SaveToBuffer(const char* filename, int quality) const
+{
+ std::optional> ret;
+
+ const std::string_view extension(Path::GetExtension(filename));
+ const FormatHandler* handler = GetFormatHandler(extension);
+ if (!handler || !handler->file_saver)
+ {
+ Log_ErrorPrintf("Unknown extension '%.*s'", static_cast(extension.size()), extension.data());
+ return ret;
+ }
+
+ ret = std::vector();
+ if (!handler->buffer_saver(*this, &ret.value(), quality))
+ ret.reset();
+
+ return ret;
+}
+
+#if 0
+
+static bool PNGCommonLoader(RGBA8Image* image, png_structp png_ptr, png_infop info_ptr, std::vector& new_data,
+ std::vector& row_pointers)
+{
+ png_read_info(png_ptr, info_ptr);
+
+ const u32 width = png_get_image_width(png_ptr, info_ptr);
+ const u32 height = png_get_image_height(png_ptr, info_ptr);
+ const png_byte color_type = png_get_color_type(png_ptr, info_ptr);
+ const png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+
+ // Read any color_type into 8bit depth, RGBA format.
+ // See http://www.libpng.org/pub/png/libpng-manual.txt
+
+ if (bit_depth == 16)
+ png_set_strip_16(png_ptr);
+
+ if (color_type == PNG_COLOR_TYPE_PALETTE)
+ png_set_palette_to_rgb(png_ptr);
+
+ // PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
+ if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+ png_set_expand_gray_1_2_4_to_8(png_ptr);
+
+ if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
+ png_set_tRNS_to_alpha(png_ptr);
+
+ // These color_type don't have an alpha channel then fill it with 0xff.
+ if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
+ png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
+
+ if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
+ png_set_gray_to_rgb(png_ptr);
+
+ png_read_update_info(png_ptr, info_ptr);
+
+ new_data.resize(width * height);
+ row_pointers.reserve(height);
+ for (u32 y = 0; y < height; y++)
+ row_pointers.push_back(reinterpret_cast(new_data.data() + y * width));
+
+ png_read_image(png_ptr, row_pointers.data());
+ image->SetPixels(width, height, std::move(new_data));
return true;
}
-bool LoadImageFromBuffer(Common::RGBA8Image* image, const void* buffer, std::size_t buffer_size)
+bool PNGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
+{
+ png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ if (!png_ptr)
+ return false;
+
+ png_infop info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr)
+ {
+ png_destroy_read_struct(&png_ptr, nullptr, nullptr);
+ return false;
+ }
+
+ ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
+
+ std::vector new_data;
+ std::vector row_pointers;
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ return false;
+
+ png_init_io(png_ptr, fp);
+ return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers);
+}
+
+bool PNGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
+{
+ png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ if (!png_ptr)
+ return false;
+
+ png_infop info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr)
+ {
+ png_destroy_read_struct(&png_ptr, nullptr, nullptr);
+ return false;
+ }
+
+ ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
+
+ std::vector new_data;
+ std::vector row_pointers;
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ return false;
+
+ struct IOData
+ {
+ const u8* buffer;
+ size_t buffer_size;
+ size_t buffer_pos;
+ };
+ IOData data = {static_cast(buffer), buffer_size, 0};
+
+ png_set_read_fn(png_ptr, &data, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
+ IOData* data = static_cast(png_get_io_ptr(png_ptr));
+ const size_t read_size = std::min(data->buffer_size - data->buffer_pos, size);
+ if (read_size > 0)
+ {
+ std::memcpy(data_ptr, data->buffer + data->buffer_pos, read_size);
+ data->buffer_pos += read_size;
+ }
+ });
+
+ return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers);
+}
+
+static void PNGSaveCommon(const RGBA8Image& image, png_structp png_ptr, png_infop info_ptr, int quality)
+{
+ png_set_compression_level(png_ptr, std::clamp(quality / 10, 0, 9));
+ png_set_IHDR(png_ptr, info_ptr, image.GetWidth(), image.GetHeight(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
+ PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+ png_write_info(png_ptr, info_ptr);
+
+ for (u32 y = 0; y < image.GetHeight(); ++y)
+ png_write_row(png_ptr, (png_bytep)image.GetRowPixels(y));
+
+ png_write_end(png_ptr, nullptr);
+}
+
+bool PNGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality)
+{
+ png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ png_infop info_ptr = nullptr;
+ if (!png_ptr)
+ return false;
+
+ ScopedGuard cleanup([&png_ptr, &info_ptr]() {
+ if (png_ptr)
+ png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
+ });
+
+ info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr)
+ return false;
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ return false;
+
+ png_set_write_fn(
+ png_ptr, fp,
+ [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
+ if (std::fwrite(data_ptr, size, 1, static_cast(png_get_io_ptr(png_ptr))) != 1)
+ png_error(png_ptr, "file write error");
+ },
+ [](png_structp png_ptr) {});
+
+ PNGSaveCommon(image, png_ptr, info_ptr, quality);
+ return true;
+}
+
+bool PNGBufferSaver(const RGBA8Image& image, std::vector* buffer, int quality)
+{
+ png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ png_infop info_ptr = nullptr;
+ if (!png_ptr)
+ return false;
+
+ ScopedGuard cleanup([&png_ptr, &info_ptr]() {
+ if (png_ptr)
+ png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
+ });
+
+ info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr)
+ return false;
+
+ buffer->reserve(image.GetWidth() * image.GetHeight() * 2);
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ return false;
+
+ png_set_write_fn(
+ png_ptr, buffer,
+ [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
+ std::vector* buffer = static_cast*>(png_get_io_ptr(png_ptr));
+ const size_t old_pos = buffer->size();
+ buffer->resize(old_pos + size);
+ std::memcpy(buffer->data() + old_pos, data_ptr, size);
+ },
+ [](png_structp png_ptr) {});
+
+ PNGSaveCommon(image, png_ptr, info_ptr, quality);
+ return true;
+}
+
+bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
+{
+ int width, height, file_comps;
+ u8* data = jpgd::decompress_jpeg_image_from_memory(static_cast(buffer), static_cast(buffer_size),
+ &width, &height, &file_comps, 4, 0);
+ if (!data)
+ {
+ Console.Error("jpgd::decompress_jpeg_image_from_memory() failed");
+ return false;
+ }
+
+ image->SetPixels(static_cast(width), static_cast(height), reinterpret_cast(data));
+ std::free(data);
+ return true;
+}
+
+bool JPEGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
+{
+ class FileStream : public jpgd::jpeg_decoder_stream
+ {
+ std::FILE* m_fp;
+ bool m_error_flag = false;
+ bool m_eof_flag = false;
+
+ public:
+ explicit FileStream(std::FILE* fp_) : m_fp(fp_) {}
+
+ int read(jpgd::uint8* pBuf, int max_bytes_to_read, bool* pEOF_flag) override
+ {
+ if (m_eof_flag)
+ {
+ *pEOF_flag = true;
+ return 0;
+ }
+
+ if (m_error_flag)
+ return -1;
+
+ int bytes_read = static_cast(std::fread(pBuf, 1, max_bytes_to_read, m_fp));
+ if (bytes_read < max_bytes_to_read)
+ {
+ if (std::ferror(m_fp))
+ {
+ m_error_flag = true;
+ return -1;
+ }
+
+ m_eof_flag = true;
+ *pEOF_flag = true;
+ }
+
+ return bytes_read;
+ }
+ };
+
+ FileStream stream(fp);
+ int width, height, file_comps;
+ u8* data = jpgd::decompress_jpeg_image_from_stream(&stream, &width, &height, &file_comps, 4, 0);
+ if (!data)
+ {
+ Console.Error("jpgd::decompress_jpeg_image_from_stream() failed");
+ return false;
+ }
+
+ image->SetPixels(static_cast(width), static_cast(height), reinterpret_cast(data));
+ std::free(data);
+ return true;
+}
+
+static bool JPEGCommonSaver(const RGBA8Image& image, jpge::output_stream& stream, int quality)
+{
+ jpge::params params;
+ params.m_quality = quality;
+
+ jpge::jpeg_encoder dst_image;
+ if (!dst_image.init(&stream, image.GetWidth(), image.GetHeight(), 3, params))
+ return false;
+
+ // for RGBA->RGB
+ std::vector row;
+ row.resize(image.GetWidth() * 3);
+
+ for (uint pass_index = 0; pass_index < dst_image.get_total_passes(); pass_index++)
+ {
+ for (u32 i = 0; i < image.GetHeight(); i++)
+ {
+ const u8* row_in = reinterpret_cast(image.GetRowPixels(i));
+ u8* row_out = row.data();
+ for (u32 j = 0; j < image.GetWidth(); j++)
+ {
+ *(row_out++) = *(row_in++);
+ *(row_out++) = *(row_in++);
+ *(row_out++) = *(row_in++);
+ row_in++;
+ }
+
+ if (!dst_image.process_scanline(row.data()))
+ return false;
+ }
+ if (!dst_image.process_scanline(NULL))
+ return false;
+ }
+
+ dst_image.deinit();
+
+ return true;
+}
+
+bool JPEGBufferSaver(const RGBA8Image& image, std::vector* buffer, int quality)
+{
+ class BufferStream : public jpge::output_stream
+ {
+ std::vector* buffer;
+
+ public:
+ explicit BufferStream(std::vector* buffer_) : buffer(buffer_) {}
+
+ bool put_buf(const void* Pbuf, int len) override
+ {
+ const size_t old_size = buffer->size();
+ buffer->resize(buffer->size() + static_cast(len));
+ std::memcpy(buffer->data() + old_size, Pbuf, static_cast(len));
+ return true;
+ }
+ };
+
+ // give enough space to avoid reallocs
+ buffer->reserve(image.GetWidth() * image.GetHeight() * 2);
+
+ BufferStream stream(buffer);
+ return JPEGCommonSaver(image, stream, quality);
+}
+
+bool JPEGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality)
+{
+ class FileStream : public jpge::output_stream
+ {
+ std::FILE* m_fp;
+ bool m_error_flag = false;
+
+ public:
+ explicit FileStream(std::FILE* fp_) : m_fp(fp_) {}
+
+ bool put_buf(const void* Pbuf, int len) override
+ {
+ if (m_error_flag)
+ return false;
+
+ if (std::fwrite(Pbuf, len, 1, m_fp) != 1)
+ {
+ m_error_flag = true;
+ return false;
+ }
+
+ return true;
+ }
+ };
+
+ FileStream stream(fp);
+ return JPEGCommonSaver(image, stream, quality);
+}
+
+#endif
+
+bool STBBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
{
int width, height, file_channels;
u8* pixel_data = stbi_load_from_memory(static_cast(buffer), static_cast(buffer_size), &width,
@@ -47,24 +541,14 @@ bool LoadImageFromBuffer(Common::RGBA8Image* image, const void* buffer, std::siz
return true;
}
-bool LoadImageFromStream(RGBA8Image* image, ByteStream* stream)
+bool STBFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
{
- stbi_io_callbacks iocb;
- iocb.read = [](void* user, char* data, int size) {
- return static_cast(static_cast(user)->Read(data, static_cast(size)));
- };
- iocb.skip = [](void* user, int n) { static_cast(user)->SeekRelative(n); };
- iocb.eof = [](void* user) {
- ByteStream* stream = static_cast(user);
- return (stream->InErrorState() || stream->GetPosition() == stream->GetSize()) ? 1 : 0;
- };
-
int width, height, file_channels;
- u8* pixel_data = stbi_load_from_callbacks(&iocb, stream, &width, &height, &file_channels, 4);
+ u8* pixel_data = stbi_load_from_file(fp, &width, &height, &file_channels, 4);
if (!pixel_data)
{
const char* error_reason = stbi_failure_reason();
- Log_ErrorPrintf("Failed to load image from stream: %s", error_reason ? error_reason : "unknown error");
+ Log_ErrorPrintf("Failed to load image from memory: %s", error_reason ? error_reason : "unknown error");
return false;
}
@@ -73,52 +557,48 @@ bool LoadImageFromStream(RGBA8Image* image, ByteStream* stream)
return true;
}
-bool WriteImageToFile(const RGBA8Image& image, const char* filename)
+bool STBBufferSaverPNG(const RGBA8Image& image, std::vector* buffer, int quality)
{
- const char* extension = std::strrchr(filename, '.');
- if (!extension)
- {
- Log_ErrorPrintf("Unable to determine file extension for '%s'", filename);
- return false;
- }
+ const auto write_func = [](void* context, void* data, int size) {
+ std::vector* buffer = reinterpret_cast*>(data);
+ const u32 len = static_cast(size);
+ buffer->resize(buffer->size() + len);
+ std::memcpy(buffer->data(), data, len);
+ };
- auto fp = FileSystem::OpenManagedCFile(filename, "wb");
- if (!fp)
- return {};
+ return (stbi_write_png_to_func(write_func, buffer, image.GetWidth(), image.GetHeight(), 4, image.GetPixels(),
+ image.GetByteStride()) == 0);
+}
+bool STBBufferSaverJPEG(const RGBA8Image& image, std::vector* buffer, int quality)
+{
+ const auto write_func = [](void* context, void* data, int size) {
+ std::vector* buffer = reinterpret_cast*>(data);
+ const u32 len = static_cast(size);
+ buffer->resize(buffer->size() + len);
+ std::memcpy(buffer->data(), data, len);
+ };
+
+ return (stbi_write_jpg_to_func(write_func, buffer, image.GetWidth(), image.GetHeight(), 4, image.GetPixels(),
+ quality) == 0);
+}
+
+bool STBFileSaverPNG(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality)
+{
const auto write_func = [](void* context, void* data, int size) {
std::fwrite(data, 1, size, static_cast(context));
};
- bool result = false;
- if (StringUtil::Strcasecmp(extension, ".png") == 0)
- {
- result = (stbi_write_png_to_func(write_func, fp.get(), image.GetWidth(), image.GetHeight(), 4, image.GetPixels(),
- image.GetByteStride()) != 0);
- }
- else if (StringUtil::Strcasecmp(extension, ".jpg") == 0)
- {
- result = (stbi_write_jpg_to_func(write_func, fp.get(), image.GetWidth(), image.GetHeight(), 4, image.GetPixels(),
- 95) != 0);
- }
- else if (StringUtil::Strcasecmp(extension, ".tga") == 0)
- {
- result =
- (stbi_write_tga_to_func(write_func, fp.get(), image.GetWidth(), image.GetHeight(), 4, image.GetPixels()) != 0);
- }
- else if (StringUtil::Strcasecmp(extension, ".bmp") == 0)
- {
- result =
- (stbi_write_bmp_to_func(write_func, fp.get(), image.GetWidth(), image.GetHeight(), 4, image.GetPixels()) != 0);
- }
-
- if (!result)
- {
- Log_ErrorPrintf("Unknown extension in filename '%s' or save error: '%s'", filename, extension);
- return false;
- }
-
- return true;
+ return (stbi_write_png_to_func(write_func, fp, image.GetWidth(), image.GetHeight(), 4, image.GetPixels(),
+ image.GetByteStride()) == 0);
}
-} // namespace Common
\ No newline at end of file
+bool STBFileSaverJPEG(const RGBA8Image& image, const char* filename, std::FILE* fp, int quality)
+{
+ const auto write_func = [](void* context, void* data, int size) {
+ std::fwrite(data, 1, size, static_cast(context));
+ };
+
+ return (stbi_write_jpg_to_func(write_func, fp, image.GetWidth(), image.GetHeight(), 4, image.GetPixels(), quality) ==
+ 0);
+}
\ No newline at end of file
diff --git a/src/common/image.h b/src/common/image.h
index 922df6b6b..c8876a21e 100644
--- a/src/common/image.h
+++ b/src/common/image.h
@@ -2,12 +2,11 @@
#include "assert.h"
#include "types.h"
#include
+#include
#include
#include
#include
-class ByteStream;
-
namespace Common {
template
class Image
@@ -86,17 +85,46 @@ public:
std::memcpy(m_pixels.data(), pixels, width * height * sizeof(PixelType));
}
-private:
+ void SetPixels(u32 width, u32 height, std::vector pixels)
+ {
+ m_width = width;
+ m_height = height;
+ m_pixels = std::move(pixels);
+ }
+
+ std::vector TakePixels()
+ {
+ m_width = 0;
+ m_height = 0;
+ return std::move(m_pixels);
+ }
+
+protected:
u32 m_width = 0;
u32 m_height = 0;
std::vector m_pixels;
};
-using RGBA8Image = Image;
+class RGBA8Image : public Image
+{
+public:
+ static constexpr int DEFAULT_SAVE_QUALITY = 85;
-bool LoadImageFromFile(Common::RGBA8Image* image, const char* filename);
-bool LoadImageFromBuffer(Common::RGBA8Image* image, const void* buffer, std::size_t buffer_size);
-bool LoadImageFromStream(Common::RGBA8Image* image, ByteStream* stream);
-bool WriteImageToFile(const Common::RGBA8Image& image, const char* filename);
+ RGBA8Image();
+ RGBA8Image(u32 width, u32 height, const u32* pixels);
+ RGBA8Image(const RGBA8Image& copy);
+ RGBA8Image(RGBA8Image&& move);
-} // namespace Common
\ No newline at end of file
+ RGBA8Image& operator=(const RGBA8Image& copy);
+ RGBA8Image& operator=(RGBA8Image&& move);
+
+ bool LoadFromFile(const char* filename);
+ bool LoadFromFile(const char* filename, std::FILE* fp);
+ bool LoadFromBuffer(const char* filename, const void* buffer, size_t buffer_size);
+
+ bool SaveToFile(const char* filename, int quality = DEFAULT_SAVE_QUALITY) const;
+ bool SaveToFile(const char* filename, std::FILE* fp, int quality = DEFAULT_SAVE_QUALITY) const;
+ std::optional> SaveToBuffer(const char* filename, int quality = DEFAULT_SAVE_QUALITY) const;
+};
+
+} // namespace Common
diff --git a/src/common/log.cpp b/src/common/log.cpp
index cfbfea1d5..a28118257 100644
--- a/src/common/log.cpp
+++ b/src/common/log.cpp
@@ -30,7 +30,7 @@ static std::mutex s_callback_mutex;
static LOGLEVEL s_filter_level = LOGLEVEL_TRACE;
-static Common::Timer::Value s_startTimeStamp = Common::Timer::GetValue();
+static Common::Timer::Value s_startTimeStamp = Common::Timer::GetCurrentValue();
static bool s_console_output_enabled = false;
static String s_console_output_channel_filter;
@@ -123,7 +123,7 @@ static int FormatLogMessageForDisplay(char* buffer, size_t buffer_size, const ch
{
// find time since start of process
const float message_time =
- static_cast(Common::Timer::ConvertValueToSeconds(Common::Timer::GetValue() - s_startTimeStamp));
+ static_cast(Common::Timer::ConvertValueToSeconds(Common::Timer::GetCurrentValue() - s_startTimeStamp));
if (level <= LOGLEVEL_PERF)
{
diff --git a/src/common/lru_cache.h b/src/common/lru_cache.h
index e6f8bcc4b..6c76e66cb 100644
--- a/src/common/lru_cache.h
+++ b/src/common/lru_cache.h
@@ -16,7 +16,10 @@ class LRUCache
using MapType = std::map;
public:
- LRUCache(std::size_t max_capacity = 16) : m_max_capacity(max_capacity) {}
+ LRUCache(std::size_t max_capacity = 16, bool manual_evict = false)
+ : m_max_capacity(max_capacity), m_manual_evict(manual_evict)
+ {
+ }
~LRUCache() = default;
std::size_t GetSize() const { return m_items.size(); }
@@ -31,7 +34,8 @@ public:
Evict(m_items.size() - m_max_capacity);
}
- V* Lookup(const K& key)
+ template
+ V* Lookup(const KeyT& key)
{
auto iter = m_items.find(key);
if (iter == m_items.end())
@@ -41,7 +45,7 @@ public:
return &iter->second.value;
}
- V* Insert(const K& key, V value)
+ V* Insert(K key, V value)
{
ShrinkForNewItem();
@@ -57,7 +61,7 @@ public:
Item it;
it.last_access = ++m_last_counter;
it.value = std::move(value);
- auto ip = m_items.emplace(key, std::move(it));
+ auto ip = m_items.emplace(std::move(key), std::move(it));
return &ip.first->second.value;
}
}
@@ -76,7 +80,8 @@ public:
}
}
- bool Remove(const K& key)
+ template
+ bool Remove(const KeyT& key)
{
auto iter = m_items.find(key);
if (iter == m_items.end())
@@ -86,6 +91,20 @@ public:
return true;
}
+ void SetManualEvict(bool block)
+ {
+ m_manual_evict = block;
+ if (!m_manual_evict)
+ ManualEvict();
+ }
+
+ void ManualEvict()
+ {
+ // evict if we went over
+ while (m_items.size() > m_max_capacity)
+ Evict(m_items.size() - (m_max_capacity - 1));
+ }
+
private:
void ShrinkForNewItem()
{
@@ -98,4 +117,5 @@ private:
MapType m_items;
CounterType m_last_counter = 0;
std::size_t m_max_capacity = 0;
+ bool m_manual_evict = false;
};
\ No newline at end of file
diff --git a/src/common/threading.cpp b/src/common/threading.cpp
new file mode 100644
index 000000000..07428fffc
--- /dev/null
+++ b/src/common/threading.cpp
@@ -0,0 +1,541 @@
+#include "threading.h"
+#include "assert.h"
+#include
+
+#if !defined(_WIN32) && !defined(__APPLE__)
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#endif
+
+#if defined(_WIN32)
+#include "windows_headers.h"
+#include
+#else
+#include
+#include
+#if defined(__linux__)
+#include
+#include
+#include
+
+// glibc < v2.30 doesn't define gettid...
+#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 30
+#include
+#define gettid() syscall(SYS_gettid)
+#endif
+#else
+#include
+#endif
+#endif
+
+#ifdef _WIN32
+// This hacky union would probably fail on some cpu platforms if the contents of FILETIME aren't
+// packed (but for any x86 CPU and microsoft compiler, they will be).
+union FileTimeSucks
+{
+ FILETIME filetime;
+ u64 u64time;
+};
+#endif
+
+#ifdef __APPLE__
+// gets the CPU time used by the current thread (both system and user), in
+// microseconds, returns 0 on failure
+static u64 getthreadtime(thread_port_t thread)
+{
+ mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
+ thread_basic_info_data_t info;
+
+ kern_return_t kr = thread_info(thread, THREAD_BASIC_INFO, (thread_info_t)&info, &count);
+ if (kr != KERN_SUCCESS)
+ return 0;
+
+ // add system and user time
+ return (u64)info.user_time.seconds * (u64)1e6 + (u64)info.user_time.microseconds +
+ (u64)info.system_time.seconds * (u64)1e6 + (u64)info.system_time.microseconds;
+}
+#endif
+
+#ifdef __linux__
+// Helper function to get either either the current cpu usage
+// in called thread or in id thread
+static u64 get_thread_time(void* id = 0)
+{
+ clockid_t cid;
+ if (id)
+ {
+ int err = pthread_getcpuclockid((pthread_t)id, &cid);
+ if (err)
+ return 0;
+ }
+ else
+ {
+ cid = CLOCK_THREAD_CPUTIME_ID;
+ }
+
+ struct timespec ts;
+ int err = clock_gettime(cid, &ts);
+ if (err)
+ return 0;
+
+ return (u64)ts.tv_sec * (u64)1e6 + (u64)ts.tv_nsec / (u64)1e3;
+}
+#endif
+
+void Threading::Timeslice()
+{
+#if defined(_WIN32)
+ ::Sleep(0);
+#elif defined(__APPLE__)
+ sched_yield();
+#else
+ sched_yield();
+#endif
+}
+
+Threading::ThreadHandle::ThreadHandle() = default;
+
+#ifdef _WIN32
+Threading::ThreadHandle::ThreadHandle(const ThreadHandle& handle)
+{
+ if (handle.m_native_handle)
+ {
+ HANDLE new_handle;
+ if (DuplicateHandle(GetCurrentProcess(), (HANDLE)handle.m_native_handle, GetCurrentProcess(), &new_handle,
+ THREAD_QUERY_INFORMATION | THREAD_SET_LIMITED_INFORMATION, FALSE, 0))
+ {
+ m_native_handle = (void*)new_handle;
+ }
+ }
+}
+#else
+Threading::ThreadHandle::ThreadHandle(const ThreadHandle& handle)
+ : m_native_handle(handle.m_native_handle)
+#ifdef __linux__
+ ,
+ m_native_id(handle.m_native_id)
+#endif
+{
+}
+#endif
+
+#ifdef _WIN32
+Threading::ThreadHandle::ThreadHandle(ThreadHandle&& handle) : m_native_handle(handle.m_native_handle)
+{
+ handle.m_native_handle = nullptr;
+}
+#else
+Threading::ThreadHandle::ThreadHandle(ThreadHandle&& handle)
+ : m_native_handle(handle.m_native_handle)
+#ifdef __linux__
+ ,
+ m_native_id(handle.m_native_id)
+#endif
+{
+ handle.m_native_handle = nullptr;
+#ifdef __linux__
+ handle.m_native_id = 0;
+#endif
+}
+#endif
+
+Threading::ThreadHandle::~ThreadHandle()
+{
+#ifdef _WIN32
+ if (m_native_handle)
+ CloseHandle(m_native_handle);
+#endif
+}
+
+Threading::ThreadHandle Threading::ThreadHandle::GetForCallingThread()
+{
+ ThreadHandle ret;
+#ifdef _WIN32
+ ret.m_native_handle =
+ (void*)OpenThread(THREAD_QUERY_INFORMATION | THREAD_SET_LIMITED_INFORMATION, FALSE, GetCurrentThreadId());
+#else
+ ret.m_native_handle = (void*)pthread_self();
+#ifdef __linux__
+ ret.m_native_id = gettid();
+#endif
+#endif
+ return ret;
+}
+
+Threading::ThreadHandle& Threading::ThreadHandle::operator=(ThreadHandle&& handle)
+{
+#ifdef _WIN32
+ if (m_native_handle)
+ CloseHandle((HANDLE)m_native_handle);
+ m_native_handle = handle.m_native_handle;
+ handle.m_native_handle = nullptr;
+#else
+ m_native_handle = handle.m_native_handle;
+ handle.m_native_handle = nullptr;
+#ifdef __linux__
+ m_native_id = handle.m_native_id;
+ handle.m_native_id = 0;
+#endif
+#endif
+ return *this;
+}
+
+Threading::ThreadHandle& Threading::ThreadHandle::operator=(const ThreadHandle& handle)
+{
+#ifdef _WIN32
+ if (m_native_handle)
+ {
+ CloseHandle((HANDLE)m_native_handle);
+ m_native_handle = nullptr;
+ }
+
+ HANDLE new_handle;
+ if (DuplicateHandle(GetCurrentProcess(), (HANDLE)handle.m_native_handle, GetCurrentProcess(), &new_handle,
+ THREAD_QUERY_INFORMATION | THREAD_SET_LIMITED_INFORMATION, FALSE, 0))
+ {
+ m_native_handle = (void*)new_handle;
+ }
+#else
+ m_native_handle = handle.m_native_handle;
+#ifdef __linux__
+ m_native_id = handle.m_native_id;
+#endif
+#endif
+
+ return *this;
+}
+
+u64 Threading::ThreadHandle::GetCPUTime() const
+{
+#if defined(_WIN32)
+#if 0
+ u64 ret = 0;
+ if (m_native_handle)
+ QueryThreadCycleTime((HANDLE)m_native_handle, &ret);
+ return ret;
+#else
+ FileTimeSucks user = {}, kernel = {};
+ FILETIME dummy;
+ GetThreadTimes((HANDLE)m_native_handle, &dummy, &dummy, &kernel.filetime, &user.filetime);
+ return user.u64time + kernel.u64time;
+#endif
+#elif defined(__APPLE__)
+ return getthreadtime(pthread_mach_thread_np((pthread_t)m_native_handle));
+#elif defined(__linux__)
+ return get_thread_time(m_native_handle);
+#else
+ return 0;
+#endif
+}
+
+bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
+{
+#if defined(_WIN32)
+ if (processor_mask == 0)
+ processor_mask = ~processor_mask;
+
+ return (SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)processor_mask) != 0 || GetLastError() != ERROR_SUCCESS);
+#elif defined(__linux__)
+ cpu_set_t set;
+ CPU_ZERO(&set);
+
+ if (processor_mask != 0)
+ {
+ for (u32 i = 0; i < 64; i++)
+ {
+ if (processor_mask & (static_cast(1) << i))
+ {
+ CPU_SET(i, &set);
+ }
+ }
+ }
+ else
+ {
+ long num_processors = sysconf(_SC_NPROCESSORS_CONF);
+ for (long i = 0; i < num_processors; i++)
+ {
+ CPU_SET(i, &set);
+ }
+ }
+
+ return sched_setaffinity((pid_t)m_native_id, sizeof(set), &set) >= 0;
+#else
+ return false;
+#endif
+}
+
+Threading::Thread::Thread() = default;
+
+Threading::Thread::Thread(Thread&& thread) : ThreadHandle(thread), m_stack_size(thread.m_stack_size)
+{
+ thread.m_stack_size = 0;
+}
+
+Threading::Thread::Thread(EntryPoint func) : ThreadHandle()
+{
+ if (!Start(std::move(func)))
+ Panic("Failed to start implicitly started thread.");
+}
+
+Threading::Thread::~Thread()
+{
+ AssertMsg(!m_native_handle, "Thread should be detached or joined at destruction");
+}
+
+void Threading::Thread::SetStackSize(u32 size)
+{
+ AssertMsg(!m_native_handle, "Can't change the stack size on a started thread");
+ m_stack_size = size;
+}
+
+#if defined(_WIN32)
+
+unsigned Threading::Thread::ThreadProc(void* param)
+{
+ std::unique_ptr entry(static_cast(param));
+ (*entry.get())();
+ return 0;
+}
+
+bool Threading::Thread::Start(EntryPoint func)
+{
+ AssertMsg(!m_native_handle, "Can't start an already-started thread");
+
+ std::unique_ptr func_clone(std::make_unique(std::move(func)));
+ unsigned thread_id;
+ m_native_handle =
+ reinterpret_cast(_beginthreadex(nullptr, m_stack_size, ThreadProc, func_clone.get(), 0, &thread_id));
+ if (!m_native_handle)
+ return false;
+
+ // thread started, it'll release the memory
+ func_clone.release();
+ return true;
+}
+
+#elif defined(__linux__)
+
+// For Linux, we have to do a bit of trickery here to get the thread's ID back from
+// the thread itself, because it's not part of pthreads. We use a semaphore to signal
+// when the thread has started, and filled in thread_id_ptr.
+struct ThreadProcParameters
+{
+ Threading::Thread::EntryPoint func;
+ Threading::KernelSemaphore* start_semaphore;
+ unsigned int* thread_id_ptr;
+};
+
+void* Threading::Thread::ThreadProc(void* param)
+{
+ std::unique_ptr entry(static_cast(param));
+ *entry->thread_id_ptr = gettid();
+ entry->start_semaphore->Post();
+ entry->func();
+ return nullptr;
+}
+
+bool Threading::Thread::Start(EntryPoint func)
+{
+ AssertMsg(!m_native_handle, "Can't start an already-started thread");
+
+ KernelSemaphore start_semaphore;
+ std::unique_ptr params(std::make_unique());
+ params->func = std::move(func);
+ params->start_semaphore = &start_semaphore;
+ params->thread_id_ptr = &m_native_id;
+
+ pthread_attr_t attrs;
+ bool has_attributes = false;
+
+ if (m_stack_size != 0)
+ {
+ has_attributes = true;
+ pthread_attr_init(&attrs);
+ }
+ if (m_stack_size != 0)
+ pthread_attr_setstacksize(&attrs, m_stack_size);
+
+ pthread_t handle;
+ const int res = pthread_create(&handle, has_attributes ? &attrs : nullptr, ThreadProc, params.get());
+ if (res != 0)
+ return false;
+
+ // wait until it sets our native id
+ start_semaphore.Wait();
+
+ // thread started, it'll release the memory
+ m_native_handle = (void*)handle;
+ params.release();
+ return true;
+}
+
+#else
+
+void* Threading::Thread::ThreadProc(void* param)
+{
+ std::unique_ptr entry(static_cast(param));
+ (*entry.get())();
+ return nullptr;
+}
+
+bool Threading::Thread::Start(EntryPoint func)
+{
+ pxAssertRel(!m_native_handle, "Can't start an already-started thread");
+
+ std::unique_ptr func_clone(std::make_unique(std::move(func)));
+
+ pthread_attr_t attrs;
+ bool has_attributes = false;
+
+ if (m_stack_size != 0)
+ {
+ has_attributes = true;
+ pthread_attr_init(&attrs);
+ }
+ if (m_stack_size != 0)
+ pthread_attr_setstacksize(&attrs, m_stack_size);
+
+ pthread_t handle;
+ const int res = pthread_create(&handle, has_attributes ? &attrs : nullptr, ThreadProc, func_clone.get());
+ if (res != 0)
+ return false;
+
+ // thread started, it'll release the memory
+ m_native_handle = (void*)handle;
+ func_clone.release();
+ return true;
+}
+
+#endif
+
+void Threading::Thread::Detach()
+{
+ AssertMsg(m_native_handle, "Can't detach without a thread");
+#ifdef _WIN32
+ CloseHandle((HANDLE)m_native_handle);
+ m_native_handle = nullptr;
+#else
+ pthread_detach((pthread_t)m_native_handle);
+ m_native_handle = nullptr;
+#ifdef __linux__
+ m_native_id = 0;
+#endif
+#endif
+}
+
+void Threading::Thread::Join()
+{
+ AssertMsg(m_native_handle, "Can't join without a thread");
+#ifdef _WIN32
+ const DWORD res = WaitForSingleObject((HANDLE)m_native_handle, INFINITE);
+ if (res != WAIT_OBJECT_0)
+ Panic("WaitForSingleObject() for thread join failed");
+
+ CloseHandle((HANDLE)m_native_handle);
+ m_native_handle = nullptr;
+#else
+ void* retval;
+ const int res = pthread_join((pthread_t)m_native_handle, &retval);
+ if (res != 0)
+ Panic("pthread_join() for thread join failed");
+
+ m_native_handle = nullptr;
+#ifdef __linux__
+ m_native_id = 0;
+#endif
+#endif
+}
+
+Threading::ThreadHandle& Threading::Thread::operator=(Thread&& thread)
+{
+ ThreadHandle::operator=(thread);
+ m_stack_size = thread.m_stack_size;
+ thread.m_stack_size = 0;
+ return *this;
+}
+
+u64 Threading::GetThreadCpuTime()
+{
+#if defined(_WIN32)
+#if 0
+ u64 ret = 0;
+ QueryThreadCycleTime(GetCurrentThread(), &ret);
+ return ret;
+#else
+ FileTimeSucks user = {}, kernel = {};
+ FILETIME dummy;
+ GetThreadTimes(GetCurrentThread(), &dummy, &dummy, &kernel.filetime, &user.filetime);
+ return user.u64time + kernel.u64time;
+#endif
+#elif defined(__APPLE__)
+ return getthreadtime(pthread_mach_thread_np(pthread_self()));
+#else
+ return get_thread_time(nullptr);
+#endif
+}
+
+u64 Threading::GetThreadTicksPerSecond()
+{
+#if defined(_WIN32)
+#if 0
+ // On x86, despite what the MS documentation says, this basically appears to be rdtsc.
+ // So, the frequency is our base clock speed (and stable regardless of power management).
+ static u64 frequency = 0;
+ if (unlikely(frequency == 0))
+ frequency = x86caps.CachedMHz() * u64(1000000);
+ return frequency;
+#else
+ return 10000000;
+#endif
+#elif defined(__APPLE__)
+ return 1000000;
+
+#else
+ return 1000000;
+#endif
+}
+
+void Threading::SetNameOfCurrentThread(const char* name)
+{
+ // This feature needs Windows headers and MSVC's SEH support:
+
+#if defined(_WIN32) && defined(_MSC_VER)
+
+ // This code sample was borrowed form some obscure MSDN article.
+ // In a rare bout of sanity, it's an actual Microsoft-published hack
+ // that actually works!
+
+ static const int MS_VC_EXCEPTION = 0x406D1388;
+
+#pragma pack(push, 8)
+ struct THREADNAME_INFO
+ {
+ DWORD dwType; // Must be 0x1000.
+ LPCSTR szName; // Pointer to name (in user addr space).
+ DWORD dwThreadID; // Thread ID (-1=caller thread).
+ DWORD dwFlags; // Reserved for future use, must be zero.
+ };
+#pragma pack(pop)
+
+ THREADNAME_INFO info;
+ info.dwType = 0x1000;
+ info.szName = name;
+ info.dwThreadID = GetCurrentThreadId();
+ info.dwFlags = 0;
+
+ __try
+ {
+ RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ }
+#elif defined(__linux__)
+ // Extract of manpage: "The name can be up to 16 bytes long, and should be
+ // null-terminated if it contains fewer bytes."
+ prctl(PR_SET_NAME, name, 0, 0, 0);
+#else
+ pthread_set_name_np(pthread_self(), name);
+#endif
+}
diff --git a/src/common/threading.h b/src/common/threading.h
new file mode 100644
index 000000000..62094fe0d
--- /dev/null
+++ b/src/common/threading.h
@@ -0,0 +1,121 @@
+#pragma once
+#include "types.h"
+
+#if defined(__APPLE__)
+#include
+#elif !defined(_WIN32)
+#include
+#endif
+
+#include
+#include
+
+namespace Threading {
+extern u64 GetThreadCpuTime();
+extern u64 GetThreadTicksPerSecond();
+
+/// Set the name of the current thread
+extern void SetNameOfCurrentThread(const char* name);
+
+// Releases a timeslice to other threads.
+extern void Timeslice();
+
+// --------------------------------------------------------------------------------------
+// ThreadHandle
+// --------------------------------------------------------------------------------------
+// Abstracts an OS's handle to a thread, closing the handle when necessary. Currently,
+// only used for getting the CPU time for a thread.
+//
+class ThreadHandle
+{
+public:
+ ThreadHandle();
+ ThreadHandle(ThreadHandle&& handle);
+ ThreadHandle(const ThreadHandle& handle);
+ ~ThreadHandle();
+
+ /// Returns a new handle for the calling thread.
+ static ThreadHandle GetForCallingThread();
+
+ ThreadHandle& operator=(ThreadHandle&& handle);
+ ThreadHandle& operator=(const ThreadHandle& handle);
+
+ operator void*() const { return m_native_handle; }
+ operator bool() const { return (m_native_handle != nullptr); }
+
+ /// Returns the amount of CPU time consumed by the thread, at the GetThreadTicksPerSecond() frequency.
+ u64 GetCPUTime() const;
+
+ /// Sets the affinity for a thread to the specified processors.
+ /// Obviously, only works up to 64 processors.
+ bool SetAffinity(u64 processor_mask) const;
+
+protected:
+ void* m_native_handle = nullptr;
+
+ // We need the thread ID for affinity adjustments on Linux.
+#if defined(__linux__)
+ unsigned int m_native_id = 0;
+#endif
+};
+
+// --------------------------------------------------------------------------------------
+// Thread
+// --------------------------------------------------------------------------------------
+// Abstracts a native thread in a lightweight manner. Provides more functionality than
+// std::thread (allowing stack size adjustments).
+//
+class Thread : public ThreadHandle
+{
+public:
+ using EntryPoint = std::function;
+
+ Thread();
+ Thread(Thread&& thread);
+ Thread(const Thread&) = delete;
+ Thread(EntryPoint func);
+ ~Thread();
+
+ ThreadHandle& operator=(Thread&& thread);
+ ThreadHandle& operator=(const Thread& handle) = delete;
+
+ ALWAYS_INLINE bool Joinable() const { return (m_native_handle != nullptr); }
+ ALWAYS_INLINE u32 GetStackSize() const { return m_stack_size; }
+
+ /// Sets the stack size for the thread. Do not call if the thread has already been started.
+ void SetStackSize(u32 size);
+
+ bool Start(EntryPoint func);
+ void Detach();
+ void Join();
+
+protected:
+#ifdef _WIN32
+ static unsigned __stdcall ThreadProc(void* param);
+#else
+ static void* ThreadProc(void* param);
+#endif
+
+ u32 m_stack_size = 0;
+};
+
+/// A semaphore that may not have a fast userspace path
+/// (Used in other semaphore-based algorithms where the semaphore is just used for its thread sleep/wake ability)
+class KernelSemaphore
+{
+#if defined(_WIN32)
+ void* m_sema;
+#elif defined(__APPLE__)
+ semaphore_t m_sema;
+#else
+ sem_t m_sema;
+#endif
+public:
+ KernelSemaphore();
+ ~KernelSemaphore();
+ void Post();
+ void Wait();
+ bool TryWait();
+};
+
+} // namespace Threading
\ No newline at end of file
diff --git a/src/common/timer.cpp b/src/common/timer.cpp
index 79b49684d..679824f45 100644
--- a/src/common/timer.cpp
+++ b/src/common/timer.cpp
@@ -34,7 +34,7 @@ static HANDLE GetSleepTimer()
return s_sleep_timer;
}
-Timer::Value Timer::GetValue()
+double Timer::GetFrequency()
{
// even if this races, it should still result in the same value..
if (!s_counter_initialized)
@@ -45,6 +45,11 @@ Timer::Value Timer::GetValue()
s_counter_initialized = true;
}
+ return s_counter_frequency;
+}
+
+Timer::Value Timer::GetCurrentValue()
+{
Timer::Value ReturnValue;
QueryPerformanceCounter(reinterpret_cast(&ReturnValue));
return ReturnValue;
@@ -52,44 +57,44 @@ Timer::Value Timer::GetValue()
double Timer::ConvertValueToNanoseconds(Timer::Value value)
{
- return (static_cast(value) / s_counter_frequency);
+ return (static_cast(value) / GetFrequency());
}
double Timer::ConvertValueToMilliseconds(Timer::Value value)
{
- return ((static_cast(value) / s_counter_frequency) / 1000000.0);
+ return ((static_cast(value) / GetFrequency()) / 1000000.0);
}
double Timer::ConvertValueToSeconds(Timer::Value value)
{
- return ((static_cast(value) / s_counter_frequency) / 1000000000.0);
+ return ((static_cast(value) / GetFrequency()) / 1000000000.0);
}
Timer::Value Timer::ConvertSecondsToValue(double s)
{
- return static_cast((s * 1000000000.0) * s_counter_frequency);
+ return static_cast((s * 1000000000.0) * GetFrequency());
}
Timer::Value Timer::ConvertMillisecondsToValue(double ms)
{
- return static_cast((ms * 1000000.0) * s_counter_frequency);
+ return static_cast((ms * 1000000.0) * GetFrequency());
}
Timer::Value Timer::ConvertNanosecondsToValue(double ns)
{
- return static_cast(ns * s_counter_frequency);
+ return static_cast(ns * GetFrequency());
}
void Timer::SleepUntil(Value value, bool exact)
{
if (exact)
{
- while (GetValue() < value)
+ while (GetCurrentValue() < value)
SleepUntil(value, false);
}
else
{
- const std::int64_t diff = static_cast(value - GetValue());
+ const std::int64_t diff = static_cast(value - GetCurrentValue());
if (diff <= 0)
return;
@@ -120,7 +125,12 @@ void Timer::SleepUntil(Value value, bool exact)
#else
-Timer::Value Timer::GetValue()
+double Timer::GetFrequency()
+{
+ return 1.0;
+}
+
+Timer::Value Timer::GetCurrentValue()
{
struct timespec tv;
clock_gettime(CLOCK_MONOTONIC, &tv);
@@ -161,14 +171,14 @@ void Timer::SleepUntil(Value value, bool exact)
{
if (exact)
{
- while (GetValue() < value)
+ while (GetCurrentValue() < value)
SleepUntil(value, false);
}
else
{
// Apple doesn't have TIMER_ABSTIME, so fall back to nanosleep in such a case.
#ifdef __APPLE__
- const Value current_time = GetValue();
+ const Value current_time = GetCurrentValue();
if (value <= current_time)
return;
@@ -195,58 +205,82 @@ Timer::Timer()
void Timer::Reset()
{
- m_tvStartValue = GetValue();
+ m_tvStartValue = GetCurrentValue();
}
double Timer::GetTimeSeconds() const
{
- return ConvertValueToSeconds(GetValue() - m_tvStartValue);
+ return ConvertValueToSeconds(GetCurrentValue() - m_tvStartValue);
}
double Timer::GetTimeMilliseconds() const
{
- return ConvertValueToMilliseconds(GetValue() - m_tvStartValue);
+ return ConvertValueToMilliseconds(GetCurrentValue() - m_tvStartValue);
}
double Timer::GetTimeNanoseconds() const
{
- return ConvertValueToNanoseconds(GetValue() - m_tvStartValue);
+ return ConvertValueToNanoseconds(GetCurrentValue() - m_tvStartValue);
+}
+
+double Timer::GetTimeSecondsAndReset()
+{
+ const Value value = GetCurrentValue();
+ const double ret = ConvertValueToSeconds(value - m_tvStartValue);
+ m_tvStartValue = value;
+ return ret;
+}
+
+double Timer::GetTimeMillisecondsAndReset()
+{
+ const Value value = GetCurrentValue();
+ const double ret = ConvertValueToMilliseconds(value - m_tvStartValue);
+ m_tvStartValue = value;
+ return ret;
+}
+
+double Timer::GetTimeNanosecondsAndReset()
+{
+ const Value value = GetCurrentValue();
+ const double ret = ConvertValueToNanoseconds(value - m_tvStartValue);
+ m_tvStartValue = value;
+ return ret;
}
void Timer::BusyWait(std::uint64_t ns)
{
- const Value start = GetValue();
+ const Value start = GetCurrentValue();
const Value end = start + ConvertNanosecondsToValue(static_cast(ns));
if (end < start)
{
// overflow, unlikely
- while (GetValue() > end)
+ while (GetCurrentValue() > end)
;
}
- while (GetValue() < end)
+ while (GetCurrentValue() < end)
;
}
void Timer::HybridSleep(std::uint64_t ns, std::uint64_t min_sleep_time)
{
- const std::uint64_t start = GetValue();
+ const std::uint64_t start = GetCurrentValue();
const std::uint64_t end = start + ConvertNanosecondsToValue(static_cast(ns));
if (end < start)
{
// overflow, unlikely
- while (GetValue() > end)
+ while (GetCurrentValue() > end)
;
}
- std::uint64_t current = GetValue();
+ std::uint64_t current = GetCurrentValue();
while (current < end)
{
const std::uint64_t remaining = end - current;
if (remaining >= min_sleep_time)
NanoSleep(min_sleep_time);
- current = GetValue();
+ current = GetCurrentValue();
}
}
diff --git a/src/common/timer.h b/src/common/timer.h
index c105f3d50..45e31df1b 100644
--- a/src/common/timer.h
+++ b/src/common/timer.h
@@ -10,7 +10,9 @@ public:
Timer();
- static Value GetValue();
+ static double GetFrequency();
+ static Value GetCurrentValue();
+
static double ConvertValueToSeconds(Value value);
static double ConvertValueToMilliseconds(Value value);
static double ConvertValueToNanoseconds(Value value);
@@ -23,11 +25,18 @@ public:
static void SleepUntil(Value value, bool exact);
void Reset();
+ void ResetTo(Value value) { m_tvStartValue = value; }
+
+ Value GetStartValue() const { return m_tvStartValue; }
double GetTimeSeconds() const;
double GetTimeMilliseconds() const;
double GetTimeNanoseconds() const;
+ double GetTimeSecondsAndReset();
+ double GetTimeMillisecondsAndReset();
+ double GetTimeNanosecondsAndReset();
+
private:
Value m_tvStartValue;
};
diff --git a/src/core/gpu_backend.cpp b/src/core/gpu_backend.cpp
index e9e441ac6..64198c192 100644
--- a/src/core/gpu_backend.cpp
+++ b/src/core/gpu_backend.cpp
@@ -215,7 +215,7 @@ void GPUBackend::RunGPULoop()
u32 read_ptr = m_command_fifo_read_ptr.load();
if (read_ptr == write_ptr)
{
- const Common::Timer::Value current_time = Common::Timer::GetValue();
+ const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
if (Common::Timer::ConvertValueToNanoseconds(current_time - last_command_time) < SPIN_TIME_NS)
continue;
@@ -263,7 +263,7 @@ void GPUBackend::RunGPULoop()
}
}
- last_command_time = allow_sleep ? 0 : Common::Timer::GetValue();
+ last_command_time = allow_sleep ? 0 : Common::Timer::GetCurrentValue();
m_command_fifo_read_ptr.store(read_ptr);
}
}
diff --git a/src/core/gpu_hw.cpp b/src/core/gpu_hw.cpp
index 08be657c9..7601f425f 100644
--- a/src/core/gpu_hw.cpp
+++ b/src/core/gpu_hw.cpp
@@ -1475,7 +1475,7 @@ void GPU_HW::DrawRendererStats(bool is_idle_frame)
GPU_HW::ShaderCompileProgressTracker::ShaderCompileProgressTracker(std::string title, u32 total)
: m_title(std::move(title)), m_min_time(Common::Timer::ConvertSecondsToValue(1.0)),
- m_update_interval(Common::Timer::ConvertSecondsToValue(0.1)), m_start_time(Common::Timer::GetValue()),
+ m_update_interval(Common::Timer::ConvertSecondsToValue(0.1)), m_start_time(Common::Timer::GetCurrentValue()),
m_last_update_time(0), m_progress(0), m_total(total)
{
}
@@ -1484,7 +1484,7 @@ void GPU_HW::ShaderCompileProgressTracker::Increment()
{
m_progress++;
- const u64 tv = Common::Timer::GetValue();
+ const u64 tv = Common::Timer::GetCurrentValue();
if ((tv - m_start_time) >= m_min_time && (tv - m_last_update_time) >= m_update_interval)
{
g_host_interface->DisplayLoadingScreen(m_title.c_str(), 0, static_cast(m_total), static_cast(m_progress));
diff --git a/src/core/host_display.cpp b/src/core/host_display.cpp
index d0469fd3b..456c41cb8 100644
--- a/src/core/host_display.cpp
+++ b/src/core/host_display.cpp
@@ -36,7 +36,7 @@ bool HostDisplay::ShouldSkipDisplayingFrame()
if (m_display_frame_interval == 0.0f)
return false;
- const u64 now = Common::Timer::GetValue();
+ const u64 now = Common::Timer::GetCurrentValue();
const double diff = Common::Timer::ConvertValueToSeconds(now - m_last_frame_displayed_time);
if (diff < m_display_frame_interval)
return true;
diff --git a/src/core/imgui_fullscreen.cpp b/src/core/imgui_fullscreen.cpp
index 1c3919740..46ce3049e 100644
--- a/src/core/imgui_fullscreen.cpp
+++ b/src/core/imgui_fullscreen.cpp
@@ -1678,7 +1678,7 @@ void AddNotification(float duration, std::string title, std::string text, std::s
notif.title = std::move(title);
notif.text = std::move(text);
notif.badge_path = std::move(image_path);
- notif.start_time = Common::Timer::GetValue();
+ notif.start_time = Common::Timer::GetCurrentValue();
s_notifications.push_back(std::move(notif));
}
@@ -1694,7 +1694,7 @@ void DrawNotifications(ImVec2& position, float spacing)
static constexpr float EASE_IN_TIME = 0.6f;
static constexpr float EASE_OUT_TIME = 0.6f;
- const Common::Timer::Value current_time = Common::Timer::GetValue();
+ const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
const float horizontal_padding = ImGuiFullscreen::LayoutScale(20.0f);
const float vertical_padding = ImGuiFullscreen::LayoutScale(10.0f);
diff --git a/src/core/namco_guncon.cpp b/src/core/namco_guncon.cpp
index 9c88a79d7..dbdb77886 100644
--- a/src/core/namco_guncon.cpp
+++ b/src/core/namco_guncon.cpp
@@ -283,7 +283,7 @@ void NamcoGunCon::LoadSettings(const char* section)
{
m_crosshair_image_path = std::move(path);
if (m_crosshair_image_path.empty() ||
- !Common::LoadImageFromFile(&m_crosshair_image, m_crosshair_image_path.c_str()))
+ !m_crosshair_image.LoadFromFile(m_crosshair_image_path.c_str()))
{
m_crosshair_image.Invalidate();
}
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 38f193eff..69cbd9016 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -1585,7 +1585,7 @@ void UpdateThrottlePeriod()
void ResetThrottler()
{
- s_next_frame_time = Common::Timer::GetValue();
+ s_next_frame_time = Common::Timer::GetCurrentValue();
}
void Throttle()
@@ -1606,7 +1606,7 @@ void Throttle()
#endif
// Use unsigned for defined overflow/wrap-around.
- const Common::Timer::Value time = Common::Timer::GetValue();
+ const Common::Timer::Value time = Common::Timer::GetCurrentValue();
const double sleep_time = (s_next_frame_time >= time) ?
Common::Timer::ConvertValueToNanoseconds(s_next_frame_time - time) :
-Common::Timer::ConvertValueToNanoseconds(time - s_next_frame_time);
@@ -1630,7 +1630,7 @@ void RunFrames()
const u32 max_frames_to_run = 2;
u32 frames_run = 0;
- Common::Timer::Value value = Common::Timer::GetValue();
+ Common::Timer::Value value = Common::Timer::GetCurrentValue();
while (frames_run < max_frames_to_run)
{
if (value < s_next_frame_time)
@@ -1639,7 +1639,7 @@ void RunFrames()
RunFrame();
frames_run++;
- value = Common::Timer::GetValue();
+ value = Common::Timer::GetCurrentValue();
}
if (frames_run != 1)
diff --git a/src/core/texture_replacements.cpp b/src/core/texture_replacements.cpp
index 66a7d3f19..533c5978b 100644
--- a/src/core/texture_replacements.cpp
+++ b/src/core/texture_replacements.cpp
@@ -105,7 +105,7 @@ void TextureReplacements::DumpVRAMWrite(u32 width, u32 height, const void* pixel
}
Log_InfoPrintf("Dumping %ux%u VRAM write to '%s'", width, height, filename.c_str());
- if (!Common::WriteImageToFile(image, filename.c_str()))
+ if (!image.SaveToFile(filename.c_str()))
Log_ErrorPrintf("Failed to dump %ux%u VRAM write to '%s'", width, height, filename.c_str());
}
@@ -266,7 +266,7 @@ const TextureReplacementTexture* TextureReplacements::LoadTexture(const std::str
return &it->second;
Common::RGBA8Image image;
- if (!Common::LoadImageFromFile(&image, filename.c_str()))
+ if (!image.LoadFromFile(filename.c_str()))
{
Log_ErrorPrintf("Failed to load '%s'", filename.c_str());
return nullptr;
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index f6c2e8603..f19594bd9 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -1652,7 +1652,7 @@ void CommonHostInterface::AddControllerRumble(u32 controller_index, u32 num_moto
rumble.num_motors = std::min(num_motors, ControllerRumbleState::MAX_MOTORS);
rumble.last_strength.fill(0.0f);
rumble.update_callback = std::move(callback);
- rumble.last_update_time = Common::Timer::GetValue();
+ rumble.last_update_time = Common::Timer::GetCurrentValue();
m_controller_vibration_motors.push_back(std::move(rumble));
}
@@ -1666,7 +1666,7 @@ void CommonHostInterface::UpdateControllerRumble()
// This is because the rumble update is synchronous, and with bluetooth latency can severely impact fast forward
// performance.
static constexpr float UPDATE_FREQUENCY = 1000.0f;
- const u64 time = Common::Timer::GetValue();
+ const u64 time = Common::Timer::GetCurrentValue();
for (ControllerRumbleState& rumble : m_controller_vibration_motors)
{
diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp
index 8284adee3..2879c9481 100644
--- a/src/frontend-common/fullscreen_ui.cpp
+++ b/src/frontend-common/fullscreen_ui.cpp
@@ -481,19 +481,27 @@ void DestroyResources()
static std::unique_ptr LoadTexture(const char* path, bool from_package)
{
- std::unique_ptr stream;
+ std::vector data;
if (from_package)
- stream = g_host_interface->OpenPackageFile(path, BYTESTREAM_OPEN_READ);
+ {
+ std::unique_ptr stream = g_host_interface->OpenPackageFile(path, BYTESTREAM_OPEN_READ);
+ if (stream)
+ data = ByteStream::ReadBinaryStream(stream.get(), false);
+ }
else
- stream = ByteStream::OpenFile(path, BYTESTREAM_OPEN_READ);
- if (!stream)
+ {
+ std::optional> odata(FileSystem::ReadBinaryFile(path));
+ if (!odata.has_value())
+ data = std::move(odata.value());
+ }
+ if (data.empty())
{
Log_ErrorPrintf("Failed to open texture resource '%s'", path);
return {};
}
Common::RGBA8Image image;
- if (!Common::LoadImageFromStream(&image, stream.get()) && image.IsValid())
+ if (!image.LoadFromBuffer(path, data.data(), data.size()) && image.IsValid())
{
Log_ErrorPrintf("Failed to read texture resource '%s'", path);
return {};