From b86c4d9ed353073e764fef286423c5cc6fb9318b Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:42:36 +0200 Subject: [PATCH] asyncResourceGatherer: add new module (#36) Adds a new module based on the hyprlock gatherer --------- Co-authored-by: Mihai Fufezan --- .github/workflows/arch.yml | 4 +- CMakeLists.txt | 8 + flake.lock | 12 +- .../resource/AsyncResourceGatherer.hpp | 38 +++++ .../resource/resources/AsyncResource.hpp | 33 +++++ .../resource/resources/ImageResource.hpp | 27 ++++ .../resource/resources/TextResource.hpp | 36 +++++ nix/default.nix | 2 + src/resource/AsyncResourceGatherer.cpp | 63 ++++++++ src/resource/resources/ImageResource.cpp | 21 +++ src/resource/resources/TextResource.cpp | 101 +++++++++++++ tests/arg.cpp | 139 ++++++++++++++++++ 12 files changed, 476 insertions(+), 8 deletions(-) create mode 100644 include/hyprgraphics/resource/AsyncResourceGatherer.hpp create mode 100644 include/hyprgraphics/resource/resources/AsyncResource.hpp create mode 100644 include/hyprgraphics/resource/resources/ImageResource.hpp create mode 100644 include/hyprgraphics/resource/resources/TextResource.hpp create mode 100644 src/resource/AsyncResourceGatherer.cpp create mode 100644 src/resource/resources/ImageResource.cpp create mode 100644 src/resource/resources/TextResource.cpp create mode 100644 tests/arg.cpp diff --git a/.github/workflows/arch.yml b/.github/workflows/arch.yml index 56311b3..e51a03d 100644 --- a/.github/workflows/arch.yml +++ b/.github/workflows/arch.yml @@ -17,7 +17,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp libpng + pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pango pixman cairo hyprutils libjpeg-turbo libjxl libwebp libpng ttf-dejavu - name: Build hyprgraphics with gcc run: | @@ -44,7 +44,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp libpng + pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pango pixman cairo hyprutils libjpeg-turbo libjxl libwebp libpng ttf-dejavu - name: Build hyprgraphics with clang run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index c96c7b3..f3a7084 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ pkg_check_modules( IMPORTED_TARGET pixman-1 cairo + pangocairo hyprutils libjpeg libwebp @@ -113,6 +114,13 @@ add_test( WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprgraphics_image "image") add_dependencies(tests hyprgraphics_image) +add_executable(hyprgraphics_arg "tests/arg.cpp") +target_link_libraries(hyprgraphics_arg PRIVATE hyprgraphics PkgConfig::deps) +add_test( + NAME "ARG" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND hyprgraphics_arg "image") +add_dependencies(tests hyprgraphics_arg) # Installation install(TARGETS hyprgraphics) diff --git a/flake.lock b/flake.lock index 8798ba3..43d2b3d 100644 --- a/flake.lock +++ b/flake.lock @@ -10,11 +10,11 @@ ] }, "locked": { - "lastModified": 1749135356, - "narHash": "sha256-Q8mAKMDsFbCEuq7zoSlcTuxgbIBVhfIYpX0RjE32PS0=", + "lastModified": 1756117388, + "narHash": "sha256-oRDel6pNl/T2tI+nc/USU9ZP9w08dxtl7hiZxa0C/Wc=", "owner": "hyprwm", "repo": "hyprutils", - "rev": "e36db00dfb3a3d3fdcc4069cb292ff60d2699ccb", + "rev": "b2ae3204845f5f2f79b4703b441252d8ad2ecfd0", "type": "github" }, "original": { @@ -25,11 +25,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748929857, - "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", + "lastModified": 1757745802, + "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", + "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", "type": "github" }, "original": { diff --git a/include/hyprgraphics/resource/AsyncResourceGatherer.hpp b/include/hyprgraphics/resource/AsyncResourceGatherer.hpp new file mode 100644 index 0000000..1d3655d --- /dev/null +++ b/include/hyprgraphics/resource/AsyncResourceGatherer.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "../cairo/CairoSurface.hpp" +#include "./resources/AsyncResource.hpp" +#include + +namespace Hyprgraphics { + class CAsyncResourceGatherer { + public: + CAsyncResourceGatherer(); + ~CAsyncResourceGatherer(); + + void enqueue(Hyprutils::Memory::CAtomicSharedPointer resource); + + private: + std::thread m_gatherThread; + + struct { + std::mutex requestMutex; + std::condition_variable requestsCV; + + bool exit = false; + bool needsToProcess = false; + } m_asyncLoopState; + + std::vector> m_targetsToLoad; + std::mutex m_targetsToLoadMutex; + + // + void asyncAssetSpinLock(); + void wakeUpMainThread(); + }; +} diff --git a/include/hyprgraphics/resource/resources/AsyncResource.hpp b/include/hyprgraphics/resource/resources/AsyncResource.hpp new file mode 100644 index 0000000..1479612 --- /dev/null +++ b/include/hyprgraphics/resource/resources/AsyncResource.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include "../../cairo/CairoSurface.hpp" +#include + +namespace Hyprgraphics { + class IAsyncResource { + public: + IAsyncResource() = default; + virtual ~IAsyncResource() = default; + + virtual void render() = 0; + + struct { + // this signal fires on the worker thread. **Really** consider making this signal handler call something to wake your + // main event loop up and do things there. + Hyprutils::Signal::CSignalT<> finished; + } m_events; + + // you probably shouldn't use this but it's here just in case. + std::atomic m_ready = false; + + struct { + // This pointer can be made not thread safe as after .finished the worker thread will not touch it anymore + // and before that you shouldnt touch it either + Hyprutils::Memory::CSharedPointer cairoSurface; + Hyprutils::Math::Vector2D pixelSize; + } m_asset; + }; +} diff --git a/include/hyprgraphics/resource/resources/ImageResource.hpp b/include/hyprgraphics/resource/resources/ImageResource.hpp new file mode 100644 index 0000000..cf9260f --- /dev/null +++ b/include/hyprgraphics/resource/resources/ImageResource.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "AsyncResource.hpp" +#include "../../color/Color.hpp" + +#include + +#include + +namespace Hyprgraphics { + class CImageResource : public IAsyncResource { + public: + enum eTextAlignmentMode : uint8_t { + TEXT_ALIGN_LEFT = 0, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, + }; + + CImageResource(const std::string& path); + virtual ~CImageResource() = default; + + virtual void render(); + + private: + std::string m_path; + }; +}; diff --git a/include/hyprgraphics/resource/resources/TextResource.hpp b/include/hyprgraphics/resource/resources/TextResource.hpp new file mode 100644 index 0000000..c481adb --- /dev/null +++ b/include/hyprgraphics/resource/resources/TextResource.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "AsyncResource.hpp" +#include "../../color/Color.hpp" + +#include + +#include + +namespace Hyprgraphics { + class CTextResource : public IAsyncResource { + public: + enum eTextAlignmentMode : uint8_t { + TEXT_ALIGN_LEFT = 0, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, + }; + + struct STextResourceData { + std::string text = "Sample Text"; + std::string font = "Sans Serif"; + size_t fontSize = 16; + CColor color = CColor{CColor::SSRGB{.r = 1.F, .g = 1.F, .b = 1.F}}; + eTextAlignmentMode align = TEXT_ALIGN_LEFT; + std::optional maxSize = std::nullopt; + }; + + CTextResource(STextResourceData&& data); + virtual ~CTextResource() = default; + + virtual void render(); + + private: + STextResourceData m_data; + }; +}; diff --git a/nix/default.nix b/nix/default.nix index a755d15..52328e0 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -12,6 +12,7 @@ libjxl, libspng, libwebp, + pango, pixman, version ? "git", doCheck ? false, @@ -55,6 +56,7 @@ in libjxl libspng libwebp + pango pixman ]; diff --git a/src/resource/AsyncResourceGatherer.cpp b/src/resource/AsyncResourceGatherer.cpp new file mode 100644 index 0000000..7ea058f --- /dev/null +++ b/src/resource/AsyncResourceGatherer.cpp @@ -0,0 +1,63 @@ +#include + +using namespace Hyprgraphics; + +CAsyncResourceGatherer::CAsyncResourceGatherer() { + m_gatherThread = std::thread([this]() { asyncAssetSpinLock(); }); +} + +CAsyncResourceGatherer::~CAsyncResourceGatherer() { + m_asyncLoopState.exit = true; + wakeUpMainThread(); + + if (m_gatherThread.joinable()) + m_gatherThread.join(); +} + +void CAsyncResourceGatherer::wakeUpMainThread() { + m_asyncLoopState.needsToProcess = true; + m_asyncLoopState.requestsCV.notify_all(); +} + +void CAsyncResourceGatherer::enqueue(Hyprutils::Memory::CAtomicSharedPointer resource) { + { + std::lock_guard lg(m_targetsToLoadMutex); + m_targetsToLoad.emplace_back(resource); + } + + wakeUpMainThread(); +} + +void CAsyncResourceGatherer::asyncAssetSpinLock() { + while (!m_asyncLoopState.exit) { + + std::unique_lock lk(m_asyncLoopState.requestMutex); + if (!m_asyncLoopState.needsToProcess) // avoid a lock if a thread managed to request something already since we .unlock()ed + m_asyncLoopState.requestsCV.wait_for(lk, std::chrono::seconds(5), [this] { return m_asyncLoopState.needsToProcess; }); // wait for events + + if (m_asyncLoopState.exit) + break; + + m_asyncLoopState.needsToProcess = false; + lk.unlock(); + m_targetsToLoadMutex.lock(); + + if (m_targetsToLoad.empty()) { + m_targetsToLoadMutex.unlock(); + continue; + } + + auto requests = m_targetsToLoad; + m_targetsToLoad.clear(); + + m_targetsToLoadMutex.unlock(); + + // process requests + for (auto& r : requests) { + r->render(); + + r->m_ready = true; + r->m_events.finished.emit(); + } + } +} \ No newline at end of file diff --git a/src/resource/resources/ImageResource.cpp b/src/resource/resources/ImageResource.cpp new file mode 100644 index 0000000..2b82c5c --- /dev/null +++ b/src/resource/resources/ImageResource.cpp @@ -0,0 +1,21 @@ +#include +#include +#include +#include + +#include +#include + +using namespace Hyprgraphics; +using namespace Hyprutils::Memory; + +CImageResource::CImageResource(const std::string& path) : m_path(path) { + ; +} + +void CImageResource::render() { + auto image = CImage(m_path); + + m_asset.cairoSurface = image.cairoSurface(); + m_asset.pixelSize = m_asset.cairoSurface && m_asset.cairoSurface->cairo() ? m_asset.cairoSurface->size() : Hyprutils::Math::Vector2D{}; +} \ No newline at end of file diff --git a/src/resource/resources/TextResource.cpp b/src/resource/resources/TextResource.cpp new file mode 100644 index 0000000..ff16a34 --- /dev/null +++ b/src/resource/resources/TextResource.cpp @@ -0,0 +1,101 @@ +#include +#include +#include + +#include +#include + +using namespace Hyprgraphics; +using namespace Hyprutils::Memory; + +CTextResource::CTextResource(CTextResource::STextResourceData&& data) : m_data(std::move(data)) { + ; +} + +void CTextResource::render() { + auto CAIROSURFACE = makeUnique(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1920, 1080 /* dummy value */)); + auto CAIRO = cairo_create(CAIROSURFACE->cairo()); + + PangoLayout* layout = pango_cairo_create_layout(CAIRO); + + PangoFontDescription* fontDesc = pango_font_description_from_string(m_data.font.c_str()); + pango_font_description_set_size(fontDesc, m_data.fontSize * PANGO_SCALE); + pango_layout_set_font_description(layout, fontDesc); + pango_font_description_free(fontDesc); + + cairo_font_options_t* options = cairo_font_options_create(); + cairo_font_options_set_antialias(options, CAIRO_ANTIALIAS_GOOD); + pango_cairo_context_set_font_options(pango_layout_get_context(layout), options); + cairo_font_options_destroy(options); + + PangoAlignment pangoAlign = PANGO_ALIGN_LEFT; + + switch (m_data.align) { + case TEXT_ALIGN_LEFT: break; + case TEXT_ALIGN_CENTER: pangoAlign = PANGO_ALIGN_CENTER; break; + case TEXT_ALIGN_RIGHT: pangoAlign = PANGO_ALIGN_RIGHT; break; + default: break; + } + + pango_layout_set_alignment(layout, pangoAlign); + + PangoAttrList* attrList = nullptr; + GError* gError = nullptr; + char* buf = nullptr; + if (pango_parse_markup(m_data.text.c_str(), -1, 0, &attrList, &buf, nullptr, &gError)) + pango_layout_set_text(layout, buf, -1); + else { + g_error_free(gError); + pango_layout_set_text(layout, m_data.text.c_str(), -1); + } + + if (!attrList) + attrList = pango_attr_list_new(); + + if (buf) + free(buf); + + pango_attr_list_insert(attrList, pango_attr_scale_new(1)); + pango_layout_set_attributes(layout, attrList); + pango_attr_list_unref(attrList); + + int layoutWidth, layoutHeight; + pango_layout_get_size(layout, &layoutWidth, &layoutHeight); + + if (m_data.maxSize) { + layoutWidth = std::min(layoutWidth, sc(m_data.maxSize->x * PANGO_SCALE)); + layoutHeight = std::min(layoutHeight, sc(m_data.maxSize->y * PANGO_SCALE)); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + pango_layout_set_width(layout, layoutWidth); + pango_layout_set_height(layout, layoutHeight); + } + + // TODO: avoid this? + cairo_destroy(CAIRO); + + CAIROSURFACE.reset(); + + m_asset.cairoSurface = makeShared(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, layoutWidth / PANGO_SCALE, layoutHeight / PANGO_SCALE)); + CAIRO = cairo_create(m_asset.cairoSurface->cairo()); + + // clear the pixmap + cairo_save(CAIRO); + cairo_set_operator(CAIRO, CAIRO_OPERATOR_CLEAR); + cairo_paint(CAIRO); + cairo_restore(CAIRO); + + // render the thing + const auto RGB = m_data.color.asRgb(); + cairo_set_source_rgba(CAIRO, RGB.r, RGB.g, RGB.b, 1.F); + + cairo_move_to(CAIRO, 0, 0); + pango_cairo_show_layout(CAIRO, layout); + + g_object_unref(layout); + + cairo_surface_flush(m_asset.cairoSurface->cairo()); + + m_asset.pixelSize = {layoutWidth / (double)PANGO_SCALE, layoutHeight / (double)PANGO_SCALE}; + + cairo_destroy(CAIRO); +} \ No newline at end of file diff --git a/tests/arg.cpp b/tests/arg.cpp new file mode 100644 index 0000000..e6054ef --- /dev/null +++ b/tests/arg.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "shared.hpp" + +using namespace Hyprutils::Memory; +using namespace Hyprutils::Math; +using namespace Hyprgraphics; + +#define UP CUniquePointer + +static UP g_asyncResourceGatherer; + +static struct { + std::mutex wakeupMutex; + std::condition_variable wakeup; + + bool exit = false; + bool needsToProcess = false; + + int loadedAssets = 0; + + std::mutex resourcesMutex; + std::vector> resources; +} state; + +// + +static bool renderText(const std::string& text, Vector2D max = {}) { + // this stinks a bit but it's due to our ASP impl. + auto resource = + makeAtomicShared(CTextResource::STextResourceData{.text = text, .fontSize = 72, .maxSize = max.x == 0 ? std::nullopt : std::optional(max)}); + CAtomicSharedPointer resourceGeneric(resource); + + g_asyncResourceGatherer->enqueue(resourceGeneric); + + state.resourcesMutex.lock(); + state.resources.emplace_back(std::move(resourceGeneric)); + state.resourcesMutex.unlock(); + + resource->m_events.finished.listenStatic([]() { + state.needsToProcess = true; + state.wakeup.notify_all(); + }); + + std::println("Enqueued \"{}\" successfully.", text); + + return true; +} + +static bool renderImage(const std::string& path) { + // this stinks a bit but it's due to our ASP impl. + auto resource = makeAtomicShared(path); + CAtomicSharedPointer resourceGeneric(resource); + + g_asyncResourceGatherer->enqueue(resourceGeneric); + + state.resourcesMutex.lock(); + state.resources.emplace_back(std::move(resourceGeneric)); + state.resourcesMutex.unlock(); + + resource->m_events.finished.listenStatic([]() { + state.needsToProcess = true; + state.wakeup.notify_all(); + }); + + std::println("Enqueued \"{}\" successfully.", path); + + return true; +} + +int main(int argc, char** argv, char** envp) { + int ret = 0; + + g_asyncResourceGatherer = makeUnique(); + + EXPECT(renderText("Hello World"), true); + EXPECT(renderText("Test markup"), true); + EXPECT(renderText("Test ellipsis!!!!!", {512, 190}), + true); + EXPECT(renderImage("./resource/images/hyprland.png"), true); + + while (!state.exit) { + std::unique_lock lk(state.wakeupMutex); + if (!state.needsToProcess) // avoid a lock if a thread managed to request something already since we .unlock()ed + state.wakeup.wait_for(lk, std::chrono::seconds(5), [] { return state.needsToProcess; }); // wait for events + + if (state.exit) + break; + + state.needsToProcess = false; + + state.resourcesMutex.lock(); + + const bool SHOULD_EXIT = std::ranges::all_of(state.resources, [](const auto& e) { return !!e->m_ready; }); + + state.resourcesMutex.unlock(); + + if (SHOULD_EXIT) + break; + + lk.unlock(); + } + + // all assets should be done, let's render them + size_t idx = 0; + for (const auto& r : state.resources) { + const auto TEST_DIR = std::filesystem::current_path().string() + "/test_output"; + + // try to write it for inspection + if (!std::filesystem::exists(TEST_DIR)) + std::filesystem::create_directory(TEST_DIR); + + std::string name = std::format("render-arg-{}", idx); + + EXPECT(!!r->m_asset.cairoSurface->cairo(), true); + + //NOLINTNEXTLINE + if (!r->m_asset.cairoSurface->cairo()) + continue; + + EXPECT(cairo_surface_write_to_png(r->m_asset.cairoSurface->cairo(), (TEST_DIR + "/" + name + ".png").c_str()), CAIRO_STATUS_SUCCESS); + + idx++; + } + + g_asyncResourceGatherer.reset(); + + return ret; +}