Own runtime threads and thin platform/canvas seams

This commit is contained in:
2026-06-16 07:34:59 +02:00
parent 17b603536b
commit 6f4bd4b26f
10 changed files with 354 additions and 200 deletions

View File

@@ -18,6 +18,24 @@ agent or engineer to remove them without reconstructing context from chat.
## Reductions ## Reductions
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` non-`draw_merged`
per-layer/per-plane retained draw execution now routes through
`execute_legacy_canvas_draw_merge_layer_plane(...)` in
`src/legacy_canvas_draw_merge_services.h` instead of keeping the erase,
temporary paint/composite, regular texture, and optional blend path fully
inline in `NodeCanvas::draw()`; retained layer traversal and broader canvas
renderer orchestration remain.
- 2026-06-16: `DEBT-0003` was narrowed again. `AppRuntime` now owns its
render/UI worker threads as `std::jthread` with explicit stop requests in
`src/app_runtime.cpp` and `src/app_runtime.h` instead of raw `std::thread`
ownership; retained app task call sites, singleton composition, and broader
runtime/platform coupling remain.
- 2026-06-16: `DEBT-0017`/`DEBT-0053` were narrowed again. iOS virtual-keyboard
visibility and prepared-file save handoff now route through explicit Apple
bridge callbacks in `src/platform_apple/apple_platform_services.*` consumed
by `src/platform_legacy/legacy_platform_services.cpp` instead of direct
`App::I` calls in those legacy platform methods; the retained Apple fallback
adapter and broader platform singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` smoothing-mask - 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` smoothing-mask
overlay draw, smoothing-mask face pass, grid keepalive draw, heightmap draw, overlay draw, smoothing-mask face pass, grid keepalive draw, heightmap draw,
and current-mode draw now route through and current-mode draw now route through

View File

@@ -97,21 +97,25 @@ Current architecture mismatches that must be treated as real blockers:
- `src/platform_apple/apple_platform_services.cpp` no longer reaches `App::I` - `src/platform_apple/apple_platform_services.cpp` no longer reaches `App::I`
directly, and Linux FPS title reporting now uses an injected callback, but directly, and Linux FPS title reporting now uses an injected callback, but
retained Apple bridging in `platform_legacy` and other platform/app coupling retained Apple bridging in `platform_legacy` and other platform/app coupling
remain. remain, even though iOS keyboard visibility and prepared-file save handoff
now also route through explicit Apple bridge callbacks.
- `src/platform_legacy/legacy_platform_services.*` is still part of the live - `src/platform_legacy/legacy_platform_services.*` is still part of the live
app shell. app shell.
- `pp_panopainter_ui` still depends on `pp_legacy_app`. - `pp_panopainter_ui` still depends on `pp_legacy_app`.
- `Canvas`, `NodeCanvas`, and `NodeStrokePreview` still own too much live - `Canvas`, `NodeCanvas`, and `NodeStrokePreview` still own too much live
OpenGL execution around the renderer boundary, even though `NodeCanvas` OpenGL execution around the renderer boundary, even though `NodeCanvas`
display resolve, cache-to-screen composite, and post-draw mask/grid/current- display resolve, cache-to-screen composite, post-draw mask/grid/current-mode
mode sequencing now route through retained draw-merge helpers. sequencing, and per-layer/per-plane retained draw execution now route
through retained draw-merge helpers.
- `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files - `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files
rather than thin composition/binding surfaces. rather than thin composition/binding surfaces.
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use - `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
global singleton reach, raw observer pointers, retained static worker global singleton reach, raw observer pointers, retained static worker
ownership in several app families, and ad hoc mutex/condition-variable ownership in several app families, and ad hoc mutex/condition-variable
ownership, even though most previously detached or raw app-facing worker ownership, even though most previously detached or raw app-facing worker
launches now use owned `std::jthread` or service-owned worker queues. launches now use owned `std::jthread` or service-owned worker queues and
`AppRuntime` now owns render/UI workers with explicit `std::jthread`
shutdown semantics.
- Modern C++23 usage exists in extracted components, especially `std::span`, - Modern C++23 usage exists in extracted components, especially `std::span`,
explicit result/status objects, and a few concepts, but the live app still explicit result/status objects, and a few concepts, but the live app still
does not consistently express ownership, thread affinity, or renderer does not consistently express ownership, thread affinity, or renderer

View File

@@ -137,6 +137,10 @@ Current slice:
draw, heightmap draw, and current-mode draw now also route through draw, heightmap draw, and current-mode draw now also route through
`execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw `execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw
orchestration is still inline. orchestration is still inline.
- `NodeCanvas` non-`draw_merged` per-layer/per-plane retained draw execution
now also routes through `execute_legacy_canvas_draw_merge_layer_plane(...)`,
but the node still owns substantial live layer traversal and renderer state
orchestration.
Write scope: Write scope:
- `src/node_stroke_preview.cpp` - `src/node_stroke_preview.cpp`
@@ -313,13 +317,21 @@ Mini-model packet:
#### ARC-APP-004 - Move Render/UI Queues Into An Owned App Runtime Service #### ARC-APP-004 - Move Render/UI Queues Into An Owned App Runtime Service
Status: Ready Status: In Progress
Why now: Why now:
`App` still owns static render/UI queues, mutexes, condition variables, and `App` still owns static render/UI queues, mutexes, condition variables, and
thread ids. That makes thread safety hard to reason about and keeps platform thread ids. That makes thread safety hard to reason about and keeps platform
entrypoints coupled to the singleton. entrypoints coupled to the singleton.
Current slice:
- render/UI queues, mutexes, condition variables, and thread identity already
live in `AppRuntime`
- `AppRuntime` render/UI worker ownership now also uses `std::jthread` plus
explicit stop requests instead of raw `std::thread`
- retained `App` composition, task call sites, and platform/runtime entrypoint
coupling are still not fully reduced behind the runtime contract
Write scope: Write scope:
- `src/app.h` - `src/app.h`
- `src/app.cpp` - `src/app.cpp`
@@ -570,6 +582,9 @@ Current slice:
- `LegacyPlatformServices::prepare_storage_paths()` now routes Apple path - `LegacyPlatformServices::prepare_storage_paths()` now routes Apple path
preparation through a narrow local helper instead of reading `App::I` preparation through a narrow local helper instead of reading `App::I`
directly in that method body directly in that method body
- iOS virtual-keyboard visibility and prepared-file save handoff now also route
through explicit Apple bridge callbacks instead of direct `App::I` calls in
`LegacyPlatformServices`
- retained Apple callback injection and broader `platform_legacy` singleton - retained Apple callback injection and broader `platform_legacy` singleton
reach are still open reach are still open

View File

@@ -52,20 +52,23 @@ void AppRuntime::render_thread_tick(App& app)
} }
} }
void AppRuntime::render_thread_main(App& app) void AppRuntime::render_thread_main(App& app, std::stop_token stop_token)
{ {
BT_SetTerminate(); BT_SetTerminate();
render_thread_id_ = std::this_thread::get_id(); render_thread_id_ = std::this_thread::get_id();
render_running_ = pp::app::plan_app_thread_start().mark_running; render_running_ = pp::app::plan_app_thread_start().mark_running;
while (render_running_) while (render_running_ && !stop_token.stop_requested())
{ {
std::deque<AppTask> working_list; std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan; pp::app::AppQueueDrainPlan drain_plan;
{ {
std::unique_lock<std::mutex> lock(render_task_mutex_); std::unique_lock<std::mutex> lock(render_task_mutex_);
render_cv_.wait(lock, [this] { return render_tasklist_.empty() && render_running_ ? false : true; }); render_cv_.wait(lock, [this, &stop_token]
{
return stop_token.stop_requested() || !render_running_ || !render_tasklist_.empty();
});
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size()); drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
working_list = std::move(render_tasklist_); working_list = std::move(render_tasklist_);
} }
@@ -124,7 +127,7 @@ void AppRuntime::ui_thread_tick(App& app)
} }
} }
void AppRuntime::ui_thread_main(App& app) void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token)
{ {
BT_SetTerminate(); BT_SetTerminate();
@@ -141,14 +144,16 @@ void AppRuntime::ui_thread_main(App& app)
float t_fps_counter = 0; float t_fps_counter = 0;
float t_reloader = 0; float t_reloader = 0;
int rendered_frames = 0; int rendered_frames = 0;
while (ui_running_) while (ui_running_ && !stop_token.stop_requested())
{ {
std::deque<AppTask> working_list; std::deque<AppTask> working_list;
{ {
std::unique_lock<std::mutex> lock(ui_task_mutex_); std::unique_lock<std::mutex> lock(ui_task_mutex_);
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), [this, &stop_token]
[this] { return ui_tasklist_.empty() && ui_running_ ? false : true; }); {
return stop_token.stop_requested() || !ui_running_ || !ui_tasklist_.empty();
});
working_list = std::move(ui_tasklist_); working_list = std::move(ui_tasklist_);
} }
@@ -223,7 +228,10 @@ void AppRuntime::render_thread_start(App& app)
{ {
const auto plan = pp::app::plan_app_thread_start(); const auto plan = pp::app::plan_app_thread_start();
if (plan.start_thread) if (plan.start_thread)
render_thread_ = std::thread(&AppRuntime::render_thread_main, this, std::ref(app)); render_thread_ = std::jthread([this, &app](std::stop_token stop_token)
{
render_thread_main(app, stop_token);
});
render_running_ = plan.mark_running; render_running_ = plan.mark_running;
} }
@@ -232,6 +240,8 @@ void AppRuntime::render_thread_stop()
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable()); const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
if (plan.mark_not_running) if (plan.mark_not_running)
render_running_ = false; render_running_ = false;
if (plan.join_thread)
render_thread_.request_stop();
if (plan.notify_worker) if (plan.notify_worker)
render_cv_.notify_all(); render_cv_.notify_all();
if (plan.join_thread) if (plan.join_thread)
@@ -242,7 +252,10 @@ void AppRuntime::ui_thread_start(App& app)
{ {
const auto plan = pp::app::plan_app_thread_start(); const auto plan = pp::app::plan_app_thread_start();
if (plan.start_thread) if (plan.start_thread)
ui_thread_ = std::thread(&AppRuntime::ui_thread_main, this, std::ref(app)); ui_thread_ = std::jthread([this, &app](std::stop_token stop_token)
{
ui_thread_main(app, stop_token);
});
ui_running_ = plan.mark_running; ui_running_ = plan.mark_running;
} }
@@ -251,6 +264,8 @@ void AppRuntime::ui_thread_stop()
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable()); const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
if (plan.mark_not_running) if (plan.mark_not_running)
ui_running_ = false; ui_running_ = false;
if (plan.join_thread)
ui_thread_.request_stop();
if (plan.notify_worker) if (plan.notify_worker)
ui_cv_.notify_all(); ui_cv_.notify_all();
if (plan.join_thread) if (plan.join_thread)

View File

@@ -43,12 +43,12 @@ public:
void notify_ui_worker() noexcept; void notify_ui_worker() noexcept;
void render_thread_tick(App& app); void render_thread_tick(App& app);
void render_thread_main(App& app); void render_thread_main(App& app, std::stop_token stop_token);
void render_thread_start(App& app); void render_thread_start(App& app);
void render_thread_stop(); void render_thread_stop();
void ui_thread_tick(App& app); void ui_thread_tick(App& app);
void ui_thread_main(App& app); void ui_thread_main(App& app, std::stop_token stop_token);
void ui_thread_start(App& app); void ui_thread_start(App& app);
void ui_thread_stop(); void ui_thread_stop();
@@ -202,14 +202,14 @@ private:
std::deque<AppTask> render_tasklist_; std::deque<AppTask> render_tasklist_;
std::mutex render_task_mutex_; std::mutex render_task_mutex_;
std::condition_variable render_cv_; std::condition_variable render_cv_;
std::thread render_thread_; std::jthread render_thread_;
std::thread::id render_thread_id_; std::thread::id render_thread_id_;
bool render_running_ = false; bool render_running_ = false;
std::deque<AppTask> ui_tasklist_; std::deque<AppTask> ui_tasklist_;
std::mutex ui_task_mutex_; std::mutex ui_task_mutex_;
std::condition_variable ui_cv_; std::condition_variable ui_cv_;
std::thread ui_thread_; std::jthread ui_thread_;
std::thread::id ui_thread_id_; std::thread::id ui_thread_id_;
bool ui_running_ = false; bool ui_running_ = false;
bool request_redraw_ = false; bool request_redraw_ = false;

View File

@@ -94,6 +94,22 @@ struct LegacyCanvasDrawMergeLayerCompositeExecution {
std::function<void()> execute_layer_blend; std::function<void()> execute_layer_blend;
}; };
struct LegacyCanvasDrawMergeLayerPlaneExecution {
std::function<void()> bind_blender_framebuffer;
std::function<void()> clear_blender_framebuffer;
std::function<void()> unbind_blender_framebuffer;
std::function<void()> prepare_temporary_erase;
std::function<void(int, float)> draw_temporary_erase_frame;
std::function<void()> cleanup_temporary_erase;
std::function<void()> prepare_temporary_paint;
std::function<void(int, float)> draw_temporary_paint_frame;
std::function<void()> cleanup_temporary_paint;
std::function<void()> prepare_layer_texture;
std::function<void(int, float)> draw_layer_texture_frame;
std::function<void()> cleanup_layer_texture;
std::function<void()> draw_blend;
};
struct LegacyCanvasDrawMergeTemporaryCompositeExecution { struct LegacyCanvasDrawMergeTemporaryCompositeExecution {
std::function<void()> setup; std::function<void()> setup;
std::function<void()> bind_samplers; std::function<void()> bind_samplers;
@@ -419,6 +435,46 @@ inline void execute_legacy_canvas_draw_merge_layer_composite(
} }
} }
inline void execute_legacy_canvas_draw_merge_layer_plane(
bool is_temporary_erase,
bool is_temporary_paint,
bool use_blend,
int first_frame,
int last_frame,
const std::function<float(int)>& frame_alpha,
const LegacyCanvasDrawMergeLayerPlaneExecution& execution)
{
if (use_blend) {
execution.bind_blender_framebuffer();
execution.clear_blender_framebuffer();
}
if (is_temporary_erase) {
execution.prepare_temporary_erase();
for (int frame = first_frame; frame <= last_frame; frame++) {
execution.draw_temporary_erase_frame(frame, frame_alpha(frame));
}
execution.cleanup_temporary_erase();
} else if (is_temporary_paint) {
execution.prepare_temporary_paint();
for (int frame = first_frame; frame <= last_frame; frame++) {
execution.draw_temporary_paint_frame(frame, frame_alpha(frame));
}
execution.cleanup_temporary_paint();
} else {
execution.prepare_layer_texture();
for (int frame = first_frame; frame <= last_frame; frame++) {
execution.draw_layer_texture_frame(frame, frame_alpha(frame));
}
execution.cleanup_layer_texture();
}
if (use_blend) {
execution.unbind_blender_framebuffer();
execution.draw_blend();
}
}
inline void execute_legacy_canvas_draw_merge_temporary_composite( inline void execute_legacy_canvas_draw_merge_temporary_composite(
const LegacyCanvasDrawMergeTemporaryCompositeExecution& execution) const LegacyCanvasDrawMergeTemporaryCompositeExecution& execution)
{ {

View File

@@ -468,12 +468,6 @@ void NodeCanvas::draw()
m_canvas->m_layers[layer_index]->m_opacity == .0f || !faces)) m_canvas->m_layers[layer_index]->m_opacity == .0f || !faces))
continue; continue;
if (use_blend)
{
m_blender_rtt.bindFramebuffer();
m_blender_rtt.clear();
}
int z = (int)(m_canvas->m_layers.size() - i); int z = (int)(m_canvas->m_layers.size() - i);
auto plane_mvp_z = proj * camera * auto plane_mvp_z = proj * camera *
glm::scale(glm::vec3(z + 1)) * glm::scale(glm::vec3(z + 1)) *
@@ -481,174 +475,177 @@ void NodeCanvas::draw()
m_canvas->m_plane_transform[plane_index] * m_canvas->m_plane_transform[plane_index] *
glm::translate(glm::vec3(0, 0, -1)); glm::translate(glm::vec3(0, 0, -1));
if (m_canvas->m_current_stroke && m_canvas->m_current_mode == kCanvasMode::Erase && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index) const auto draw_layer_frame = [&](int frame, float onion_alpha) {
{ ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
m_sampler.bind(0);
m_sampler.bind(1);
m_sampler.bind(2);
//ShaderManager::u_vec2(kShaderUniform::Resolution, zw(m_canvas->m_box) / zoom);
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
pp::panopainter::setup_legacy_stroke_erase_shader(
pp::panopainter::LegacyStrokeEraseUniforms {
.mvp = plane_mvp_z,
.texture_slot = 0,
.stroke_texture_slot = 1,
.mask_texture_slot = 2,
.mask_enabled = m_canvas->m_smask_active,
});
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].bindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
{
const float onion_alpha = pp::app::animation_onion_frame_alpha(onion_range, frame);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity* onion_alpha);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
m_face_plane.draw_fill();
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
}
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).unbindTexture();
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].unbindTexture();
}
else if(m_canvas->m_current_stroke && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index)
{
m_sampler.bind(0);
m_sampler.bind(1);
m_sampler.bind(2);
m_sampler.bind(3);
m_sampler_stencil.bind(4);
glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
if (b->m_pattern_flipx) patt_scale.x *= -1.f;
if (b->m_pattern_flipy) patt_scale.y *= -1.f;
pp::panopainter::setup_legacy_stroke_composite_shader(
pp::panopainter::LegacyStrokeCompositeUniforms {
.resolution = Canvas::I->m_size,
.pattern = {
.scale = patt_scale,
.invert = static_cast<float>(b->m_pattern_invert),
.brightness = b->m_pattern_brightness,
.contrast = b->m_pattern_contrast,
.depth = b->m_pattern_depth,
.blend_mode = b->m_pattern_blend_mode,
.offset = Canvas::I->m_pattern_offset,
},
.mvp = plane_mvp_z,
.layer_alpha = 1.0f,
.alpha_lock = m_canvas->m_layers[layer_index]->m_alpha_locked,
.mask_enabled = m_canvas->m_smask_active,
.use_fragcoord = false,
.blend_mode = b->m_blend_mode,
.use_dual = b->m_dual_enabled,
.dual_blend_mode = b->m_dual_blend_mode,
.dual_alpha = b->m_dual_opacity,
.use_pattern = b->m_pattern_enabled && !b->m_pattern_eachsample,
});
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].bindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_canvas->m_tmp_dual[plane_index].bindTexture();
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
unbind_texture_2d();
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
{
const float onion_alpha = pp::app::animation_onion_frame_alpha(onion_range, frame);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
m_face_plane.draw_fill();
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
}
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_canvas->m_tmp_dual[plane_index].unbindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).unbindTexture();
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].unbindTexture();
}
else
{
m_canvas->m_cam_fov < 20.f ? m_sampler_nearest.bind(0) : m_sampler.bind(0);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms {
.mvp = plane_mvp_z,
.texture_slot = 0,
.alpha = 1.f,
.highlight = m_canvas->m_layers[layer_index]->m_hightlight,
});
for (int frame = onion_range.first_frame; frame <= onion_range.last_frame; frame++)
{
const float onion_alpha = pp::app::animation_onion_frame_alpha(onion_range, frame);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
m_face_plane.draw_fill();
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
}
}
if (use_blend)
{
m_blender_rtt.unbindFramebuffer();
}
// draw the blended
if (use_blend)
{
m_sampler.bind(0);
m_sampler.bind(2);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_blend_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureBlendUniforms {
.mvp = glm::ortho(-1, 1, -1, 1),
.texture_slot = 0,
.destination_texture_slot = 2,
.use_destination_texture = copy_blend_destination,
.blend_mode = m_canvas->m_layers[layer_index]->m_blend_mode,
.alpha = 1.f,
});
set_active_texture_unit(0); set_active_texture_unit(0);
m_blender_rtt.bindTexture(); m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_blender_bg.bind();
copy_framebuffer_to_texture_2d(
0,
0,
0,
0,
m_blender_bg.size().x,
m_blender_bg.size().y);
}
m_face_plane.draw_fill(); m_face_plane.draw_fill();
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_blender_bg.unbind();
}
set_active_texture_unit(0); set_active_texture_unit(0);
m_blender_rtt.unbindTexture(); m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
} };
pp::panopainter::execute_legacy_canvas_draw_merge_layer_plane(
m_canvas->m_current_stroke && m_canvas->m_current_mode == kCanvasMode::Erase && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index,
m_canvas->m_current_stroke && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index,
use_blend,
onion_range.first_frame,
onion_range.last_frame,
[&](int frame) {
return pp::app::animation_onion_frame_alpha(onion_range, frame);
},
{
.bind_blender_framebuffer = [&] {
m_blender_rtt.bindFramebuffer();
},
.clear_blender_framebuffer = [&] {
m_blender_rtt.clear();
},
.unbind_blender_framebuffer = [&] {
m_blender_rtt.unbindFramebuffer();
},
.prepare_temporary_erase = [&] {
m_sampler.bind(0);
m_sampler.bind(1);
m_sampler.bind(2);
//ShaderManager::u_vec2(kShaderUniform::Resolution, zw(m_canvas->m_box) / zoom);
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
pp::panopainter::setup_legacy_stroke_erase_shader(
pp::panopainter::LegacyStrokeEraseUniforms {
.mvp = plane_mvp_z,
.texture_slot = 0,
.stroke_texture_slot = 1,
.mask_texture_slot = 2,
.mask_enabled = m_canvas->m_smask_active,
});
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].bindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
},
.draw_temporary_erase_frame = [&](int frame, float onion_alpha) {
draw_layer_frame(frame, onion_alpha);
},
.cleanup_temporary_erase = [&] {
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).unbindTexture();
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].unbindTexture();
},
.prepare_temporary_paint = [&] {
m_sampler.bind(0);
m_sampler.bind(1);
m_sampler.bind(2);
m_sampler.bind(3);
m_sampler_stencil.bind(4);
glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
if (b->m_pattern_flipx) patt_scale.x *= -1.f;
if (b->m_pattern_flipy) patt_scale.y *= -1.f;
pp::panopainter::setup_legacy_stroke_composite_shader(
pp::panopainter::LegacyStrokeCompositeUniforms {
.resolution = Canvas::I->m_size,
.pattern = {
.scale = patt_scale,
.invert = static_cast<float>(b->m_pattern_invert),
.brightness = b->m_pattern_brightness,
.contrast = b->m_pattern_contrast,
.depth = b->m_pattern_depth,
.blend_mode = b->m_pattern_blend_mode,
.offset = Canvas::I->m_pattern_offset,
},
.mvp = plane_mvp_z,
.layer_alpha = 1.0f,
.alpha_lock = m_canvas->m_layers[layer_index]->m_alpha_locked,
.mask_enabled = m_canvas->m_smask_active,
.use_fragcoord = false,
.blend_mode = b->m_blend_mode,
.use_dual = b->m_dual_enabled,
.dual_blend_mode = b->m_dual_blend_mode,
.dual_alpha = b->m_dual_opacity,
.use_pattern = b->m_pattern_enabled && !b->m_pattern_eachsample,
});
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].bindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_canvas->m_tmp_dual[plane_index].bindTexture();
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
unbind_texture_2d();
},
.draw_temporary_paint_frame = [&](int frame, float onion_alpha) {
draw_layer_frame(frame, onion_alpha);
},
.cleanup_temporary_paint = [&] {
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_canvas->m_tmp_dual[plane_index].unbindTexture();
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).unbindTexture();
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].unbindTexture();
},
.prepare_layer_texture = [&] {
m_canvas->m_cam_fov < 20.f ? m_sampler_nearest.bind(0) : m_sampler.bind(0);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms {
.mvp = plane_mvp_z,
.texture_slot = 0,
.alpha = 1.f,
.highlight = m_canvas->m_layers[layer_index]->m_hightlight,
});
},
.draw_layer_texture_frame = [&](int frame, float onion_alpha) {
draw_layer_frame(frame, onion_alpha);
},
.cleanup_layer_texture = [&] {
},
.draw_blend = [&] {
m_sampler.bind(0);
m_sampler.bind(2);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_blend_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureBlendUniforms {
.mvp = glm::ortho(-1, 1, -1, 1),
.texture_slot = 0,
.destination_texture_slot = 2,
.use_destination_texture = copy_blend_destination,
.blend_mode = m_canvas->m_layers[layer_index]->m_blend_mode,
.alpha = 1.f,
});
set_active_texture_unit(0);
m_blender_rtt.bindTexture();
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_blender_bg.bind();
copy_framebuffer_to_texture_2d(
0,
0,
0,
0,
m_blender_bg.size().x,
m_blender_bg.size().y);
}
m_face_plane.draw_fill();
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_blender_bg.unbind();
}
set_active_texture_unit(0);
m_blender_rtt.unbindTexture();
},
});
#ifdef _DEBUG #ifdef _DEBUG
// draw dirty area // draw dirty area

View File

@@ -61,6 +61,16 @@ bool AppleDocumentPlatformServices::set_clipboard_text(std::string_view text) co
return false; return false;
} }
void AppleDocumentPlatformServices::set_virtual_keyboard_visible(bool visible) const
{
#if defined(__IOS__)
if (bridge_.set_virtual_keyboard_visible)
bridge_.set_virtual_keyboard_visible(visible);
#else
(void)visible;
#endif
}
void AppleDocumentPlatformServices::pick_image(PickedPathCallback callback) const void AppleDocumentPlatformServices::pick_image(PickedPathCallback callback) const
{ {
if (family_ == PlatformFamily::ios) if (family_ == PlatformFamily::ios)
@@ -173,6 +183,26 @@ void AppleDocumentPlatformServices::set_cursor_visible(bool visible) const
#endif #endif
} }
void AppleDocumentPlatformServices::save_prepared_file(
std::string_view path,
std::string_view suggested_name,
PreparedFileCallback callback) const
{
#if defined(__IOS__)
if (bridge_.save_prepared_file)
{
bridge_.save_prepared_file(
std::string(path),
std::string(suggested_name),
std::move(callback));
}
#else
(void)path;
(void)suggested_name;
(void)callback;
#endif
}
void AppleDocumentPlatformServices::save_ui_state() const void AppleDocumentPlatformServices::save_ui_state() const
{ {
#if defined(__OSX__) #if defined(__OSX__)

View File

@@ -18,9 +18,11 @@ struct AppleDocumentPickerBridge {
std::function<std::string(std::string_view path)> format_working_directory_path; std::function<std::string(std::string_view path)> format_working_directory_path;
std::function<std::string()> clipboard_text; std::function<std::string()> clipboard_text;
std::function<bool(std::string_view text)> set_clipboard_text; std::function<bool(std::string_view text)> set_clipboard_text;
std::function<void(bool visible)> set_virtual_keyboard_visible;
std::function<void(std::string path)> display_file; std::function<void(std::string path)> display_file;
std::function<void(std::string path)> share_file; std::function<void(std::string path)> share_file;
std::function<void(bool visible)> set_cursor_visible; std::function<void(bool visible)> set_cursor_visible;
std::function<void(std::string path, std::string suggested_name, PreparedFileCallback callback)> save_prepared_file;
std::function<void()> save_ui_state; std::function<void()> save_ui_state;
}; };
@@ -43,9 +45,14 @@ public:
[[nodiscard]] std::string format_working_directory_path(std::string_view path) const; [[nodiscard]] std::string format_working_directory_path(std::string_view path) const;
[[nodiscard]] std::string clipboard_text() const; [[nodiscard]] std::string clipboard_text() const;
[[nodiscard]] bool set_clipboard_text(std::string_view text) const; [[nodiscard]] bool set_clipboard_text(std::string_view text) const;
void set_virtual_keyboard_visible(bool visible) const;
void display_file(std::string_view path) const; void display_file(std::string_view path) const;
void share_file(std::string_view path) const; void share_file(std::string_view path) const;
void set_cursor_visible(bool visible) const; void set_cursor_visible(bool visible) const;
void save_prepared_file(
std::string_view path,
std::string_view suggested_name,
PreparedFileCallback callback) const;
void save_ui_state() const; void save_ui_state() const;
private: private:

View File

@@ -135,6 +135,14 @@ public:
const std::string value(text); const std::string value(text);
return [ios_view clipboard_set_string:value]; return [ios_view clipboard_set_string:value];
}; };
bridge.set_virtual_keyboard_visible = [ios_view](bool visible) {
dispatch_async(dispatch_get_main_queue(), ^{
if (visible)
[ios_view show_keyboard];
else
[ios_view hide_keyboard];
});
};
bridge.display_file = [ios_view](std::string path) { bridge.display_file = [ios_view](std::string path) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[ios_view display_file:path]; [ios_view display_file:path];
@@ -157,6 +165,16 @@ public:
[ios_view pick_file:apple_file_types_array(file_types) then:callback]; [ios_view pick_file:apple_file_types_array(file_types) then:callback];
}); });
}; };
bridge.save_prepared_file = [ios_view](
std::string path,
std::string suggested_name,
pp::platform::PreparedFileCallback callback) {
(void)suggested_name;
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view pick_file_save:path];
});
callback(path, true);
};
return bridge; return bridge;
}()); }());
return services; return services;
@@ -330,12 +348,7 @@ public:
void set_virtual_keyboard_visible(bool visible) override void set_virtual_keyboard_visible(bool visible) override
{ {
#ifdef __IOS__ #ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{ active_apple_document_platform_services().set_virtual_keyboard_visible(visible);
if (visible)
[App::I->ios_view show_keyboard];
else
[App::I->ios_view hide_keyboard];
});
#elif __ANDROID__ #elif __ANDROID__
displayKeyboard(visible); displayKeyboard(visible);
#else #else
@@ -734,11 +747,10 @@ public:
const std::string value(path); const std::string value(path);
const std::string name(suggested_name); const std::string name(suggested_name);
#ifdef __IOS__ #ifdef __IOS__
(void)name; active_apple_document_platform_services().save_prepared_file(
dispatch_async(dispatch_get_main_queue(), ^{ value,
[App::I->ios_view pick_file_save:value]; name,
}); std::move(callback));
callback(value, true);
#else #else
(void)name; (void)name;
callback(value, false); callback(value, false);