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
- 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
overlay draw, smoothing-mask face pass, grid keepalive draw, heightmap draw,
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`
directly, and Linux FPS title reporting now uses an injected callback, but
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
app shell.
- `pp_panopainter_ui` still depends on `pp_legacy_app`.
- `Canvas`, `NodeCanvas`, and `NodeStrokePreview` still own too much live
OpenGL execution around the renderer boundary, even though `NodeCanvas`
display resolve, cache-to-screen composite, and post-draw mask/grid/current-
mode sequencing now route through retained draw-merge helpers.
display resolve, cache-to-screen composite, post-draw mask/grid/current-mode
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
rather than thin composition/binding surfaces.
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
global singleton reach, raw observer pointers, retained static worker
ownership in several app families, and ad hoc mutex/condition-variable
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`,
explicit result/status objects, and a few concepts, but the live app still
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
`execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw
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:
- `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
Status: Ready
Status: In Progress
Why now:
`App` still owns static render/UI queues, mutexes, condition variables, and
thread ids. That makes thread safety hard to reason about and keeps platform
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:
- `src/app.h`
- `src/app.cpp`
@@ -570,6 +582,9 @@ Current slice:
- `LegacyPlatformServices::prepare_storage_paths()` now routes Apple path
preparation through a narrow local helper instead of reading `App::I`
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
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();
render_thread_id_ = std::this_thread::get_id();
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;
pp::app::AppQueueDrainPlan drain_plan;
{
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());
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();
@@ -141,14 +144,16 @@ void AppRuntime::ui_thread_main(App& app)
float t_fps_counter = 0;
float t_reloader = 0;
int rendered_frames = 0;
while (ui_running_)
while (ui_running_ && !stop_token.stop_requested())
{
std::deque<AppTask> working_list;
{
std::unique_lock<std::mutex> lock(ui_task_mutex_);
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms),
[this] { return ui_tasklist_.empty() && ui_running_ ? false : true; });
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), [this, &stop_token]
{
return stop_token.stop_requested() || !ui_running_ || !ui_tasklist_.empty();
});
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();
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;
}
@@ -232,6 +240,8 @@ void AppRuntime::render_thread_stop()
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
if (plan.mark_not_running)
render_running_ = false;
if (plan.join_thread)
render_thread_.request_stop();
if (plan.notify_worker)
render_cv_.notify_all();
if (plan.join_thread)
@@ -242,7 +252,10 @@ void AppRuntime::ui_thread_start(App& app)
{
const auto plan = pp::app::plan_app_thread_start();
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;
}
@@ -251,6 +264,8 @@ void AppRuntime::ui_thread_stop()
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
if (plan.mark_not_running)
ui_running_ = false;
if (plan.join_thread)
ui_thread_.request_stop();
if (plan.notify_worker)
ui_cv_.notify_all();
if (plan.join_thread)

View File

@@ -43,12 +43,12 @@ public:
void notify_ui_worker() noexcept;
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_stop();
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_stop();
@@ -202,14 +202,14 @@ private:
std::deque<AppTask> render_tasklist_;
std::mutex render_task_mutex_;
std::condition_variable render_cv_;
std::thread render_thread_;
std::jthread render_thread_;
std::thread::id render_thread_id_;
bool render_running_ = false;
std::deque<AppTask> ui_tasklist_;
std::mutex ui_task_mutex_;
std::condition_variable ui_cv_;
std::thread ui_thread_;
std::jthread ui_thread_;
std::thread::id ui_thread_id_;
bool ui_running_ = false;
bool request_redraw_ = false;

View File

@@ -94,6 +94,22 @@ struct LegacyCanvasDrawMergeLayerCompositeExecution {
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 {
std::function<void()> setup;
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(
const LegacyCanvasDrawMergeTemporaryCompositeExecution& execution)
{

View File

@@ -468,12 +468,6 @@ void NodeCanvas::draw()
m_canvas->m_layers[layer_index]->m_opacity == .0f || !faces))
continue;
if (use_blend)
{
m_blender_rtt.bindFramebuffer();
m_blender_rtt.clear();
}
int z = (int)(m_canvas->m_layers.size() - i);
auto plane_mvp_z = proj * camera *
glm::scale(glm::vec3(z + 1)) *
@@ -481,174 +475,177 @@ void NodeCanvas::draw()
m_canvas->m_plane_transform[plane_index] *
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)
{
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,
});
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);
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_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
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();
}
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
// draw dirty area

View File

@@ -61,6 +61,16 @@ bool AppleDocumentPlatformServices::set_clipboard_text(std::string_view text) co
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
{
if (family_ == PlatformFamily::ios)
@@ -173,6 +183,26 @@ void AppleDocumentPlatformServices::set_cursor_visible(bool visible) const
#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
{
#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()> 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)> share_file;
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;
};
@@ -43,9 +45,14 @@ public:
[[nodiscard]] std::string format_working_directory_path(std::string_view path) const;
[[nodiscard]] std::string clipboard_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 share_file(std::string_view path) 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;
private:

View File

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