Own brush workers and thin preview/platform seams

This commit is contained in:
2026-06-16 06:54:14 +02:00
parent a76560e3df
commit 56c4743e66
11 changed files with 415 additions and 180 deletions

View File

@@ -5,6 +5,10 @@
#include "app_core/document_platform_io.h"
#include "app_core/document_sharing.h"
#include "platform_api/platform_services.h"
#ifdef __LINUX__
#include <GLFW/glfw3.h>
#include "platform_linux/linux_platform_services.h"
#endif
#include "platform_legacy/legacy_platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
@@ -51,6 +55,21 @@ namespace {
void App::set_platform_services(pp::platform::PlatformServices* services) noexcept
{
platform_services_ = services;
#ifdef __LINUX__
if (services)
{
pp::platform::linux::set_fps_title_callback([this](std::string title) {
if (!glfw_window)
return;
glfwSetWindowTitle(glfw_window, title.c_str());
});
}
else
{
pp::platform::linux::set_fps_title_callback({});
}
#endif
}
pp::platform::PlatformServices* App::platform_services() const noexcept

View File

@@ -7,12 +7,90 @@
#include "node_dialog_export_ppbr.h"
#include "node_panel_brush.h"
#include <condition_variable>
#include <deque>
#include <functional>
#include <string>
#include <mutex>
#include <stop_token>
#include <thread>
namespace pp::panopainter {
namespace {
class LegacyBrushPackageWorker final {
public:
LegacyBrushPackageWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyBrushPackageWorker()
{
shutdown();
}
void post(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_)
return;
tasks_.push_back(std::move(task));
}
cv_.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
}
void run(std::stop_token stop_token)
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [&] {
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
});
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
break;
task = std::move(tasks_.front());
tasks_.pop_front();
}
if (task) {
try {
task();
} catch (...) {
LOG("brush package export worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyBrushPackageWorker& brush_package_worker()
{
static LegacyBrushPackageWorker worker;
return worker;
}
NodePanelBrushPreset::PPBRInfo to_legacy_ppbr_info(
const pp::app::BrushPackageExportRequest& request,
const NodeDialogExportPPBR& dialog)
@@ -44,20 +122,29 @@ public:
{
const auto path_string = std::string(path);
const auto info = to_legacy_ppbr_info(request, dialog_);
auto presets = app_.presets;
if (mode_ == LegacyBrushPackageExportMode::desktop_async_close_and_message) {
auto* app = &app_;
auto* dialog = &dialog_;
std::thread([app, dialog, path_string, info] {
auto dialog = std::static_pointer_cast<NodeDialogExportPPBR>(dialog_.shared_from_this());
brush_package_worker().post([app, presets, dialog, path_string, info] {
BT_SetTerminate();
app->presets->export_ppbr(path_string, info);
pp::panopainter::close_legacy_dialog_node(*dialog);
if (presets) {
presets->export_ppbr(path_string, info);
}
const auto plan = pp::app::plan_brush_package_export_success_dialog(path_string);
app->message_box(plan.title, plan.message, plan.show_cancel);
}).detach();
app->ui_task([dialog, plan] {
if (dialog) {
pp::panopainter::close_legacy_dialog_node(*dialog);
}
App::I->message_box(plan.title, plan.message, plan.show_cancel);
});
});
return;
}
app_.presets->export_ppbr(path_string, info);
if (presets) {
presets->export_ppbr(path_string, info);
}
}
private:

View File

@@ -5,12 +5,90 @@
#include "app.h"
#include "node_panel_brush.h"
#include <condition_variable>
#include <deque>
#include <functional>
#include <string>
#include <mutex>
#include <stop_token>
#include <thread>
namespace pp::panopainter {
namespace {
class LegacyBrushPackageWorker final {
public:
LegacyBrushPackageWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyBrushPackageWorker()
{
shutdown();
}
void post(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_)
return;
tasks_.push_back(std::move(task));
}
cv_.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
}
void run(std::stop_token stop_token)
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [&] {
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
});
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
break;
task = std::move(tasks_.front());
tasks_.pop_front();
}
if (task) {
try {
task();
} catch (...) {
LOG("brush package import worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyBrushPackageWorker& brush_package_worker()
{
static LegacyBrushPackageWorker worker;
return worker;
}
class LegacyBrushPackageImportServices final : public pp::app::BrushPackageImportServices {
public:
explicit LegacyBrushPackageImportServices(App& app) noexcept
@@ -22,12 +100,18 @@ public:
{
auto presets = app_.presets;
const auto path_string = std::string(path);
if (kind == pp::app::BrushPackageImportKind::abr) {
std::thread(&NodePanelBrushPreset::import_abr, presets, path_string).detach();
return;
}
brush_package_worker().post([presets, kind, path_string] {
BT_SetTerminate();
if (!presets) {
return;
}
if (kind == pp::app::BrushPackageImportKind::abr) {
presets->import_abr(path_string);
return;
}
std::thread(&NodePanelBrushPreset::import_ppbr, presets, path_string).detach();
presets->import_ppbr(path_string);
});
}
private:

View File

@@ -4,14 +4,18 @@
#include "../libs/glm/glm/ext/matrix_clip_space.hpp"
#include "legacy_canvas_stroke_shader_services.h"
#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_preview_services.h"
#include "legacy_canvas_stroke_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "paint_renderer/compositor.h"
#include "texture.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <functional>
#include <utility>
#include <vector>
namespace pp::panopainter {
@@ -241,6 +245,91 @@ template <typename Frame>
return true;
}
[[nodiscard]] inline bool execute_legacy_node_stroke_preview_final_composite(
glm::vec2 size,
glm::vec2 pattern_scale,
const Brush& brush,
const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass,
Texture2D& background_texture,
Texture2D& stroke_texture,
Texture2D& dual_texture,
Texture2D& preview_texture,
Sampler& linear_sampler,
Sampler& repeat_sampler,
std::function<void()> bind_pattern_texture,
std::function<void()> draw_composite)
{
if (!bind_pattern_texture || !draw_composite) {
return false;
}
pp::panopainter::execute_legacy_stroke_preview_final_composite(
[&] {
pp::panopainter::setup_legacy_stroke_composite_shader(
pp::panopainter::LegacyStrokeCompositeUniforms {
.resolution = size,
.pattern = {
.scale = pattern_scale,
.invert = static_cast<float>(brush.m_pattern_invert),
.brightness = brush.m_pattern_brightness,
.contrast = brush.m_pattern_contrast,
.depth = brush.m_pattern_depth,
.blend_mode = composite_pass.pattern_blend_mode,
.offset = glm::vec2(brush.m_pattern_rand_offset ? 0.5f : 0.0f),
},
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.layer_alpha = 1.0f,
.alpha_lock = false,
.mask_enabled = false,
.use_fragcoord = false,
.blend_mode = brush.m_blend_mode,
.use_dual = composite_pass.use_dual,
.dual_blend_mode = composite_pass.dual_blend_mode,
.dual_alpha = composite_pass.dual_alpha,
.use_pattern = composite_pass.use_pattern,
});
},
[&] {
linear_sampler.bind(0U);
linear_sampler.bind(1U);
linear_sampler.bind(2U);
linear_sampler.bind(3U);
repeat_sampler.bind(4U);
},
[&] {
pp::legacy::ui_gl::activate_texture_unit(0U, "NodeStrokePreview");
background_texture.bind();
pp::legacy::ui_gl::activate_texture_unit(1U, "NodeStrokePreview");
stroke_texture.bind();
pp::legacy::ui_gl::activate_texture_unit(3U, "NodeStrokePreview");
dual_texture.bind();
pp::legacy::ui_gl::activate_texture_unit(4U, "NodeStrokePreview");
bind_pattern_texture();
},
[&] {
draw_composite();
});
const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[&] {
preview_texture.bind();
},
[](
int src_x,
int src_y,
int dst_x,
int dst_y,
int width,
int height) {
copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
},
pp::paint_renderer::StrokePreviewCopySize {
.width = static_cast<int>(size.x),
.height = static_cast<int>(size.y),
});
return copy_status.ok();
}
struct LegacyNodeStrokePreviewPassOrchestrationRequest {
pp::renderer::RenderDeviceFeatures features {};
glm::vec2 preview_size {};

View File

@@ -4,6 +4,7 @@
#include "assets/brush_package.h"
#include "app_core/brush_ui.h"
#include "legacy_brush_ui_services.h"
#include "legacy_brush_package_import_services.h"
#include "legacy_ui_overlay_services.h"
#include "asset.h"
#include "texture.h"
@@ -600,13 +601,8 @@ void NodePanelBrushPreset::init()
switch (index)
{
case 0: // import file
App::I->pick_file({"abr", "ppbr"}, [this] (std::string path) {
std::thread([this, path] {
BT_SetTerminate();
import_brush(path);
for (auto p : s_panels)
p->m_notification->SetVisibility(p->m_container->m_children.size() == 0);
}).detach();
App::I->pick_file({"abr", "ppbr"}, [presets = std::static_pointer_cast<NodePanelBrushPreset>(shared_from_this())] (std::string path) {
presets->import_brush(path);
});
break;
case 1: // export file
@@ -637,13 +633,8 @@ void NodePanelBrushPreset::init()
};
m_btn_import = find<NodeButton>("import");
m_btn_import->on_click = [this] (Node*) {
App::I->pick_file({ "abr", "ppbr" }, [this](std::string path) {
std::thread([this, path] {
BT_SetTerminate();
import_brush(path);
for (auto p : s_panels)
p->m_notification->SetVisibility(p->m_container->m_children.size() == 0);
}).detach();
App::I->pick_file({ "abr", "ppbr" }, [presets = std::static_pointer_cast<NodePanelBrushPreset>(shared_from_this())](std::string path) {
presets->import_brush(path);
});
};
m_btn_download = find<NodeButton>("download");
@@ -1050,6 +1041,10 @@ bool NodePanelBrushPreset::import_ppbr(const std::string& path)
// brush settings
auto brushes_count = sr.ru32();
std::vector<std::shared_ptr<Brush>> brushes_to_add;
if (brushes_count > 0) {
brushes_to_add.reserve(static_cast<std::size_t>(brushes_count));
}
for (int i = 0; i < brushes_count; i++)
{
auto b = std::make_shared<Brush>();
@@ -1058,16 +1053,23 @@ bool NodePanelBrushPreset::import_ppbr(const std::string& path)
LOG("import_ppbr brush name %s", b->m_name.c_str());
if (b->valid())
{
for (auto p : s_panels)
p->add_brush(b);
brushes_to_add.push_back(b);
}
pb->increment();
}
save();
App::I->stroke->m_brush_popup->reload();
auto owner = std::static_pointer_cast<NodePanelBrushPreset>(shared_from_this());
App::I->ui_task([owner, brushes_to_add = std::move(brushes_to_add), pb]() mutable {
for (const auto& b : brushes_to_add)
{
for (auto p : s_panels)
p->add_brush(b);
}
pp::panopainter::close_legacy_dialog_node(*pb);
owner->save();
App::I->stroke->m_brush_popup->reload();
pp::panopainter::close_legacy_dialog_node(*pb);
});
return true;
}
@@ -1148,7 +1150,8 @@ bool NodePanelBrushPreset::import_abr(const std::string& path)
});
auto brushes = abr.compute_brushes(App::I->data_path);
App::I->ui_task([&]{
auto owner = std::static_pointer_cast<NodePanelBrushPreset>(shared_from_this());
App::I->ui_task([owner, brushes = std::move(brushes), pb]() mutable {
for (const auto& b : brushes)
{
if (b->valid())
@@ -1159,12 +1162,11 @@ bool NodePanelBrushPreset::import_abr(const std::string& path)
}
pb->increment();
}
owner->save();
App::I->stroke->m_brush_popup->reload();
pp::panopainter::close_legacy_dialog_node(*pb);
});
save();
App::I->stroke->m_brush_popup->reload();
pp::panopainter::close_legacy_dialog_node(*pb);
return true;
}
@@ -1178,7 +1180,15 @@ bool NodePanelBrushPreset::import_brush(const std::string& path)
std::string name = m[2].str();
std::string ext = m[3].str();
return ext == "ppbr" ? import_ppbr(path) : import_abr(path);
const auto kind = ext == "ppbr"
? pp::app::BrushPackageImportKind::ppbr
: pp::app::BrushPackageImportKind::abr;
const auto status = pp::panopainter::execute_legacy_brush_package_import(*App::I, kind, path);
if (!status.ok()) {
LOG("Brush package import request failed: %s", status.message);
return false;
}
return true;
}
void NodePanelBrushPreset::clear_brushes()

View File

@@ -82,43 +82,6 @@ constexpr std::uint32_t kMixer = 3U;
constexpr std::uint32_t kReservedLinear = 4U;
}
struct StrokePreviewCompositePassInputs {
glm::vec2 resolution;
glm::vec2 pattern_scale;
const Brush& brush;
const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass;
Texture2D& background_texture;
Texture2D& stroke_texture;
Texture2D& dual_texture;
Sampler& linear_sampler;
Sampler& repeat_sampler;
std::function<void()> draw_composite;
StrokePreviewCompositePassInputs(
glm::vec2 resolution_in,
glm::vec2 pattern_scale_in,
const Brush& brush_in,
const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass_in,
Texture2D& background_texture_in,
Texture2D& stroke_texture_in,
Texture2D& dual_texture_in,
Sampler& linear_sampler_in,
Sampler& repeat_sampler_in,
std::function<void()> draw_composite_in)
: resolution(resolution_in)
, pattern_scale(pattern_scale_in)
, brush(brush_in)
, composite_pass(composite_pass_in)
, background_texture(background_texture_in)
, stroke_texture(stroke_texture_in)
, dual_texture(dual_texture_in)
, linear_sampler(linear_sampler_in)
, repeat_sampler(repeat_sampler_in)
, draw_composite(std::move(draw_composite_in))
{
}
};
pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request(
const Brush& brush,
glm::vec2 resolution) noexcept
@@ -232,65 +195,7 @@ pp::panopainter::LegacyCanvasStrokeMixPassRequest make_stroke_preview_mix_pass_e
[&](int) {
set_active_texture_unit(stroke_preview_composite_slots::kBackground);
background_texture.unbind();
});
}
void copy_stroke_preview_result_to_texture(Texture2D& texture, glm::vec2 size);
void execute_stroke_preview_final_composite_and_copy(
const StrokePreviewCompositePassInputs& inputs,
Texture2D& preview_texture,
glm::vec2 size)
{
pp::panopainter::execute_legacy_stroke_preview_final_composite(
[&] {
pp::panopainter::setup_legacy_stroke_composite_shader(
pp::panopainter::LegacyStrokeCompositeUniforms {
.resolution = inputs.resolution,
.pattern = {
.scale = inputs.pattern_scale,
.invert = static_cast<float>(inputs.brush.m_pattern_invert),
.brightness = inputs.brush.m_pattern_brightness,
.contrast = inputs.brush.m_pattern_contrast,
.depth = inputs.brush.m_pattern_depth,
.blend_mode = inputs.composite_pass.pattern_blend_mode,
.offset = glm::vec2(inputs.brush.m_pattern_rand_offset ? 0.5f : 0.0f),
},
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.layer_alpha = 1.0f,
.alpha_lock = false,
.mask_enabled = false,
.use_fragcoord = false,
.blend_mode = inputs.brush.m_blend_mode,
.use_dual = inputs.composite_pass.use_dual,
.dual_blend_mode = inputs.composite_pass.dual_blend_mode,
.dual_alpha = inputs.composite_pass.dual_alpha,
.use_pattern = inputs.composite_pass.use_pattern,
});
},
[&] {
inputs.linear_sampler.bind(stroke_preview_composite_slots::kBackground);
inputs.linear_sampler.bind(stroke_preview_composite_slots::kStroke);
inputs.linear_sampler.bind(2U);
inputs.linear_sampler.bind(stroke_preview_composite_slots::kDual);
inputs.repeat_sampler.bind(stroke_preview_composite_slots::kPattern);
},
[&] {
set_active_texture_unit(stroke_preview_composite_slots::kBackground);
inputs.background_texture.bind();
set_active_texture_unit(stroke_preview_composite_slots::kStroke);
inputs.stroke_texture.bind();
set_active_texture_unit(stroke_preview_composite_slots::kDual);
inputs.dual_texture.bind();
set_active_texture_unit(stroke_preview_composite_slots::kPattern);
inputs.brush.m_pattern_texture ?
inputs.brush.m_pattern_texture->bind() :
unbind_texture_2d();
},
[&] {
inputs.draw_composite();
});
copy_stroke_preview_result_to_texture(preview_texture, size);
});
}
void copy_stroke_preview_framebuffer_to_texture(
@@ -500,22 +405,6 @@ void execute_stroke_preview_background_capture_pass(
assert(copy_status.ok());
}
void copy_stroke_preview_result_to_texture(Texture2D& preview_texture, glm::vec2 size)
{
const auto result = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[&] {
preview_texture.bind();
},
[](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
},
pp::paint_renderer::StrokePreviewCopySize {
.width = static_cast<int>(size.x),
.height = static_cast<int>(size.y),
});
assert(result.ok());
}
}
std::atomic_int NodeStrokePreview::s_instances{ 0 };
@@ -841,22 +730,26 @@ void NodeStrokePreview::draw_stroke_immediate()
return false;
}
execute_stroke_preview_final_composite_and_copy(
StrokePreviewCompositePassInputs(
size,
glm::vec2(b->m_pattern_scale),
*b,
material.composite_pass,
m_tex_background,
m_tex,
m_tex_dual,
m_sampler_linear,
m_sampler_linear_repeat,
[&] {
m_plane.draw_fill();
}),
const bool final_composite_ok = pp::panopainter::execute_legacy_node_stroke_preview_final_composite(
size,
glm::vec2(b->m_pattern_scale),
*b,
material.composite_pass,
m_tex_background,
m_tex,
m_tex_dual,
m_tex_preview,
size);
m_sampler_linear,
m_sampler_linear_repeat,
[&] {
b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d();
},
[&] {
m_plane.draw_fill();
});
if (!final_composite_ok) {
return false;
}
return true;
}();
assert(sequence_ok);

View File

@@ -2,21 +2,28 @@
#ifdef __LINUX__
#include <string>
#include <GLFW/glfw3.h>
#include "app.h"
#include <utility>
namespace pp::platform::linux {
namespace {
std::function<void(std::string)> g_fps_title_callback;
void linux_update_fps(int frames)
{
App::I->title("PanoPainter - " + std::to_string(frames) + " FPS");
if (!g_fps_title_callback)
return;
g_fps_title_callback("PanoPainter - " + std::to_string(frames) + " FPS");
}
}
void set_fps_title_callback(std::function<void(std::string)> callback)
{
g_fps_title_callback = std::move(callback);
}
void report_rendered_frames(int frames)
{
linux_update_fps(frames);
@@ -26,6 +33,11 @@ void report_rendered_frames(int frames)
#else
namespace pp::platform::linux {
void set_fps_title_callback(std::function<void(std::string)> callback)
{
(void)callback;
}
void report_rendered_frames(int frames)
{
(void)frames;

View File

@@ -1,7 +1,11 @@
#pragma once
#include <functional>
#include <string>
namespace pp::platform::linux {
void set_fps_title_callback(std::function<void(std::string)> callback);
void report_rendered_frames(int frames);
}