diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 8eadca22..8e846b28 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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 diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index ec72a073..04c9df91 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -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 diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index f7b11601..87d073fe 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -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 diff --git a/src/app_runtime.cpp b/src/app_runtime.cpp index f9addee8..37017039 100644 --- a/src/app_runtime.cpp +++ b/src/app_runtime.cpp @@ -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 working_list; pp::app::AppQueueDrainPlan drain_plan; { std::unique_lock 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 working_list; { std::unique_lock 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) diff --git a/src/app_runtime.h b/src/app_runtime.h index b066d112..ea4ee0c4 100644 --- a/src/app_runtime.h +++ b/src/app_runtime.h @@ -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 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 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; diff --git a/src/legacy_canvas_draw_merge_services.h b/src/legacy_canvas_draw_merge_services.h index cfba6cbb..6c1dc15e 100644 --- a/src/legacy_canvas_draw_merge_services.h +++ b/src/legacy_canvas_draw_merge_services.h @@ -94,6 +94,22 @@ struct LegacyCanvasDrawMergeLayerCompositeExecution { std::function execute_layer_blend; }; +struct LegacyCanvasDrawMergeLayerPlaneExecution { + std::function bind_blender_framebuffer; + std::function clear_blender_framebuffer; + std::function unbind_blender_framebuffer; + std::function prepare_temporary_erase; + std::function draw_temporary_erase_frame; + std::function cleanup_temporary_erase; + std::function prepare_temporary_paint; + std::function draw_temporary_paint_frame; + std::function cleanup_temporary_paint; + std::function prepare_layer_texture; + std::function draw_layer_texture_frame; + std::function cleanup_layer_texture; + std::function draw_blend; +}; + struct LegacyCanvasDrawMergeTemporaryCompositeExecution { std::function setup; std::function 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& 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) { diff --git a/src/node_canvas.cpp b/src/node_canvas.cpp index 91576e29..39f8347e 100644 --- a/src/node_canvas.cpp +++ b/src/node_canvas.cpp @@ -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(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(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 diff --git a/src/platform_apple/apple_platform_services.cpp b/src/platform_apple/apple_platform_services.cpp index 1291e633..78f255de 100644 --- a/src/platform_apple/apple_platform_services.cpp +++ b/src/platform_apple/apple_platform_services.cpp @@ -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__) diff --git a/src/platform_apple/apple_platform_services.h b/src/platform_apple/apple_platform_services.h index 9da9030e..48f3316d 100644 --- a/src/platform_apple/apple_platform_services.h +++ b/src/platform_apple/apple_platform_services.h @@ -18,9 +18,11 @@ struct AppleDocumentPickerBridge { std::function format_working_directory_path; std::function clipboard_text; std::function set_clipboard_text; + std::function set_virtual_keyboard_visible; std::function display_file; std::function share_file; std::function set_cursor_visible; + std::function save_prepared_file; std::function 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: diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index fb155148..4dd2d173 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -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);