asyncResourceGatherer: add new module (#36)
Some checks are pending
Build & Test (Arch) / Arch: Build and Test (gcc) (push) Waiting to run
Build & Test (Arch) / Arch: Build and Test (clang) (push) Waiting to run
Build & Test / nix (hyprgraphics) (push) Waiting to run
Build & Test / nix (hyprgraphics-with-tests) (push) Waiting to run

Adds a new module based on the hyprlock gatherer

---------

Co-authored-by: Mihai Fufezan <mihai@fufexan.net>
This commit is contained in:
Vaxry 2025-09-17 14:42:36 +02:00 committed by GitHub
parent aa9d14963b
commit b86c4d9ed3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 476 additions and 8 deletions

View file

@ -17,7 +17,7 @@ jobs:
run: | run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu 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 - name: Build hyprgraphics with gcc
run: | run: |
@ -44,7 +44,7 @@ jobs:
run: | run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu 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 - name: Build hyprgraphics with clang
run: | run: |

View file

@ -54,6 +54,7 @@ pkg_check_modules(
IMPORTED_TARGET IMPORTED_TARGET
pixman-1 pixman-1
cairo cairo
pangocairo
hyprutils hyprutils
libjpeg libjpeg
libwebp libwebp
@ -113,6 +114,13 @@ add_test(
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprgraphics_image "image") COMMAND hyprgraphics_image "image")
add_dependencies(tests hyprgraphics_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 # Installation
install(TARGETS hyprgraphics) install(TARGETS hyprgraphics)

12
flake.lock generated
View file

@ -10,11 +10,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1749135356, "lastModified": 1756117388,
"narHash": "sha256-Q8mAKMDsFbCEuq7zoSlcTuxgbIBVhfIYpX0RjE32PS0=", "narHash": "sha256-oRDel6pNl/T2tI+nc/USU9ZP9w08dxtl7hiZxa0C/Wc=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprutils", "repo": "hyprutils",
"rev": "e36db00dfb3a3d3fdcc4069cb292ff60d2699ccb", "rev": "b2ae3204845f5f2f79b4703b441252d8ad2ecfd0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -25,11 +25,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1748929857, "lastModified": 1757745802,
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -0,0 +1,38 @@
#pragma once
#include <thread>
#include <atomic>
#include <vector>
#include <unordered_map>
#include <condition_variable>
#include "../cairo/CairoSurface.hpp"
#include "./resources/AsyncResource.hpp"
#include <hyprutils/memory/Atomic.hpp>
namespace Hyprgraphics {
class CAsyncResourceGatherer {
public:
CAsyncResourceGatherer();
~CAsyncResourceGatherer();
void enqueue(Hyprutils::Memory::CAtomicSharedPointer<IAsyncResource> resource);
private:
std::thread m_gatherThread;
struct {
std::mutex requestMutex;
std::condition_variable requestsCV;
bool exit = false;
bool needsToProcess = false;
} m_asyncLoopState;
std::vector<Hyprutils::Memory::CAtomicSharedPointer<IAsyncResource>> m_targetsToLoad;
std::mutex m_targetsToLoadMutex;
//
void asyncAssetSpinLock();
void wakeUpMainThread();
};
}

View file

@ -0,0 +1,33 @@
#pragma once
#include <hyprutils/memory/UniquePtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
#include <hyprutils/signal/Signal.hpp>
#include "../../cairo/CairoSurface.hpp"
#include <atomic>
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<bool> 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<CCairoSurface> cairoSurface;
Hyprutils::Math::Vector2D pixelSize;
} m_asset;
};
}

View file

@ -0,0 +1,27 @@
#pragma once
#include "AsyncResource.hpp"
#include "../../color/Color.hpp"
#include <optional>
#include <hyprutils/math/Vector2D.hpp>
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;
};
};

View file

@ -0,0 +1,36 @@
#pragma once
#include "AsyncResource.hpp"
#include "../../color/Color.hpp"
#include <optional>
#include <hyprutils/math/Vector2D.hpp>
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<Hyprutils::Math::Vector2D> maxSize = std::nullopt;
};
CTextResource(STextResourceData&& data);
virtual ~CTextResource() = default;
virtual void render();
private:
STextResourceData m_data;
};
};

View file

@ -12,6 +12,7 @@
libjxl, libjxl,
libspng, libspng,
libwebp, libwebp,
pango,
pixman, pixman,
version ? "git", version ? "git",
doCheck ? false, doCheck ? false,
@ -55,6 +56,7 @@ in
libjxl libjxl
libspng libspng
libwebp libwebp
pango
pixman pixman
]; ];

View file

@ -0,0 +1,63 @@
#include <hyprgraphics/resource/AsyncResourceGatherer.hpp>
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<IAsyncResource> resource) {
{
std::lock_guard<std::mutex> 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();
}
}
}

View file

@ -0,0 +1,21 @@
#include <hyprgraphics/resource/resources/ImageResource.hpp>
#include <hyprgraphics/image/Image.hpp>
#include <hyprutils/memory/Atomic.hpp>
#include <hyprutils/memory/Casts.hpp>
#include <cairo/cairo.h>
#include <pango/pangocairo.h>
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{};
}

View file

@ -0,0 +1,101 @@
#include <hyprgraphics/resource/resources/TextResource.hpp>
#include <hyprutils/memory/Atomic.hpp>
#include <hyprutils/memory/Casts.hpp>
#include <cairo/cairo.h>
#include <pango/pangocairo.h>
using namespace Hyprgraphics;
using namespace Hyprutils::Memory;
CTextResource::CTextResource(CTextResource::STextResourceData&& data) : m_data(std::move(data)) {
;
}
void CTextResource::render() {
auto CAIROSURFACE = makeUnique<CCairoSurface>(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<int>(m_data.maxSize->x * PANGO_SCALE));
layoutHeight = std::min(layoutHeight, sc<int>(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<CCairoSurface>(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);
}

139
tests/arg.cpp Normal file
View file

@ -0,0 +1,139 @@
#include <algorithm>
#include <print>
#include <format>
#include <filesystem>
#include <fstream>
#include <vector>
#include <hyprgraphics/resource/AsyncResourceGatherer.hpp>
#include <hyprgraphics/resource/resources/TextResource.hpp>
#include <hyprgraphics/resource/resources/ImageResource.hpp>
#include <hyprutils/memory/UniquePtr.hpp>
#include <hyprutils/memory/Atomic.hpp>
#include <hyprutils/math/Vector2D.hpp>
#include "shared.hpp"
using namespace Hyprutils::Memory;
using namespace Hyprutils::Math;
using namespace Hyprgraphics;
#define UP CUniquePointer
static UP<CAsyncResourceGatherer> 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<CAtomicSharedPointer<IAsyncResource>> 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>(CTextResource::STextResourceData{.text = text, .fontSize = 72, .maxSize = max.x == 0 ? std::nullopt : std::optional<Vector2D>(max)});
CAtomicSharedPointer<IAsyncResource> 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<CImageResource>(path);
CAtomicSharedPointer<IAsyncResource> 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<CAsyncResourceGatherer>();
EXPECT(renderText("Hello World"), true);
EXPECT(renderText("<b><i>Test markup</i></b>"), 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;
}