From a89f5e6cf248898745988b4e6c44b381e3319950 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 18:14:37 +0200 Subject: [PATCH] Route canvas blend gate through paint renderer --- CMakeLists.txt | 2 + docs/modernization/capability-map.md | 4 +- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 11 ++- src/canvas.cpp | 112 +++++++++++++++++++++++++-- 5 files changed, 118 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ae90848..a90ca11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -355,6 +355,7 @@ if(PP_BUILD_APP) pp_assets pp_document pp_paint + pp_paint_renderer pp_renderer_api pp_project_warnings) if(TARGET pp_renderer_gl) @@ -392,6 +393,7 @@ if(PP_BUILD_APP) pp_assets pp_document pp_paint + pp_paint_renderer pp_renderer_api pp_project_warnings) if(TARGET pp_renderer_gl) diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 4c68f99..9d8481d 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -1,7 +1,7 @@ # PanoPainter Capability Map Status: live -Last updated: 2026-06-02 +Last updated: 2026-06-03 This map is the preservation checklist for the modernization. When a component is extracted, update the relevant rows with the owning component, test label, @@ -37,7 +37,7 @@ and validation command. | PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture | | Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter | | Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden | -| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, and GPU parity | +| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live draw-merge gate coverage, and GPU parity | | Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects | ## Layers And Animation diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 6e03d32..8856432 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -53,7 +53,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | -| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets, but live stroke compositing still uses the legacy OpenGL canvas path | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior | +| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. The live `Canvas::draw_merge` shader-blend gate now calls `pp_paint_renderer::plan_stroke_composite` for the existing layer and primary-brush blend decision, but live stroke compositing, dual-brush feedback, and pattern feedback still use the legacy OpenGL canvas path | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 2a57fa8..d4e55d1 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -873,8 +873,11 @@ stroke composite plan that decides whether a stroke/layer blend can use fixed-function blending or needs framebuffer-fetch/ping-pong destination feedback. `pano_cli plan-stroke-composite` exposes the same decision for automation, including layer blend, stroke blend, dual-brush, and pattern-blend -inputs. Live canvas stroke execution is still legacy OpenGL and remains tracked -by DEBT-0036 until the app calls through this paint-renderer boundary. +inputs. Live `Canvas::draw_merge` now uses this planner for its existing +shader-blend gate for layer and primary-brush blend modes while preserving the +legacy trigger policy; actual canvas stroke execution, dual-brush feedback, and +pattern feedback are still legacy OpenGL and remain tracked by DEBT-0036 until +the app calls through renderer services for the whole compositing path. The existing renderer classes are not yet fully behind the renderer interfaces. @@ -1595,6 +1598,10 @@ Results: - Canvas layer merge rendering and explicit layer-merge compositing now route depth/blend state, active texture units, fallback 2D texture unbinds, and merge framebuffer copy targets through the renderer GL backend mapping. +- Canvas draw-merge shader-blend selection now consumes the extracted + `pp_paint_renderer` stroke composite planner for current layer and primary + brush blend modes, while preserving legacy OpenGL compositing execution under + DEBT-0036. - Canvas equirectangular import drawing and depth export rendering now route depth/blend state and active texture units through the renderer GL backend mapping. diff --git a/src/canvas.cpp b/src/canvas.cpp index 6e4b375..0219fe9 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -4,6 +4,7 @@ #include "app.h" #include "texture.h" #include "node_progress_bar.h" +#include "paint_renderer/compositor.h" #include "renderer_gl/opengl_capabilities.h" #include #include @@ -43,6 +44,104 @@ GLenum rgba_pixel_format() return static_cast(pp::renderer::gl::rgba_pixel_format()); } +bool to_paint_blend_mode(int value, pp::paint::BlendMode& out) noexcept +{ + switch (value) { + case 0: out = pp::paint::BlendMode::normal; return true; + case 1: out = pp::paint::BlendMode::multiply; return true; + case 2: out = pp::paint::BlendMode::screen; return true; + case 3: out = pp::paint::BlendMode::color_dodge; return true; + case 4: out = pp::paint::BlendMode::overlay; return true; + default: return false; + } +} + +bool to_stroke_blend_mode(int value, pp::paint::StrokeBlendMode& out) noexcept +{ + switch (value) { + case 0: out = pp::paint::StrokeBlendMode::normal; return true; + case 1: out = pp::paint::StrokeBlendMode::multiply; return true; + case 2: out = pp::paint::StrokeBlendMode::subtract; return true; + case 3: out = pp::paint::StrokeBlendMode::darken; return true; + case 4: out = pp::paint::StrokeBlendMode::overlay; return true; + case 5: out = pp::paint::StrokeBlendMode::color_dodge; return true; + case 6: out = pp::paint::StrokeBlendMode::color_burn; return true; + case 7: out = pp::paint::StrokeBlendMode::linear_burn; return true; + case 8: out = pp::paint::StrokeBlendMode::hard_mix; return true; + case 9: out = pp::paint::StrokeBlendMode::linear_height; return true; + case 10: out = pp::paint::StrokeBlendMode::height; return true; + default: return false; + } +} + +pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept +{ + return pp::renderer::RenderDeviceFeatures { + .framebuffer_fetch = ShaderManager::ext_framebuffer_fetch, + .texture_copy = !ShaderManager::ext_framebuffer_fetch, + }; +} + +bool stroke_composite_plan_needs_shader_blend( + int width, + int height, + pp::paint::BlendMode layer_blend_mode, + pp::paint::StrokeBlendMode stroke_blend_mode) noexcept +{ + const auto plan = pp::paint_renderer::plan_stroke_composite( + canvas_stroke_composite_features(), + pp::paint_renderer::StrokeCompositeRequest { + .extent = pp::renderer::Extent2D { + .width = static_cast(std::max(width, 0)), + .height = static_cast(std::max(height, 0)), + }, + .layer_blend_mode = layer_blend_mode, + .stroke_blend_mode = stroke_blend_mode, + }); + return plan ? plan.value().complex_blend : true; +} + +bool draw_merge_needs_shader_blend( + int width, + int height, + const std::vector>& layers, + const Brush* brush) noexcept +{ + for (const auto& layer : layers) { + if (!layer) { + continue; + } + pp::paint::BlendMode layer_blend = pp::paint::BlendMode::normal; + if (!to_paint_blend_mode(layer->m_blend_mode, layer_blend)) { + if (layer->m_blend_mode != 0) { + return true; + } + continue; + } + if (stroke_composite_plan_needs_shader_blend( + width, + height, + layer_blend, + pp::paint::StrokeBlendMode::normal)) { + return true; + } + } + + if (brush) { + pp::paint::StrokeBlendMode stroke_blend = pp::paint::StrokeBlendMode::normal; + if (!to_stroke_blend_mode(brush->m_blend_mode, stroke_blend)) { + return brush->m_blend_mode != 0; + } + return stroke_composite_plan_needs_shader_blend( + width, + height, + pp::paint::BlendMode::normal, + stroke_blend); + } + + return false; +} + GLenum unsigned_byte_component_type() { return static_cast(pp::renderer::gl::unsigned_byte_component_type()); @@ -1086,14 +1185,11 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array faces /*= SI auto ortho = glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f); const auto& b = m_current_stroke->m_brush; - // check if any layer use blend, otherwise draw directly on main framebuffer - bool use_blend = false; - for (auto& l : m_layers) - { - use_blend |= l->m_blend_mode != 0; - } - if (Canvas::I->m_current_stroke) - use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0; + const bool use_blend = draw_merge_needs_shader_blend( + m_width, + m_height, + m_layers, + m_current_stroke ? m_current_stroke->m_brush.get() : nullptr); // if not using shader blend, use gl rasterizer blend glDisable(depth_test_state());