Route canvas blend gate through paint renderer

This commit is contained in:
2026-06-03 18:14:37 +02:00
parent 2ec11e5099
commit a89f5e6cf2
5 changed files with 118 additions and 13 deletions

View File

@@ -355,6 +355,7 @@ if(PP_BUILD_APP)
pp_assets pp_assets
pp_document pp_document
pp_paint pp_paint
pp_paint_renderer
pp_renderer_api pp_renderer_api
pp_project_warnings) pp_project_warnings)
if(TARGET pp_renderer_gl) if(TARGET pp_renderer_gl)
@@ -392,6 +393,7 @@ if(PP_BUILD_APP)
pp_assets pp_assets
pp_document pp_document
pp_paint pp_paint
pp_paint_renderer
pp_renderer_api pp_renderer_api
pp_project_warnings) pp_project_warnings)
if(TARGET pp_renderer_gl) if(TARGET pp_renderer_gl)

View File

@@ -1,7 +1,7 @@
# PanoPainter Capability Map # PanoPainter Capability Map
Status: live Status: live
Last updated: 2026-06-02 Last updated: 2026-06-03
This map is the preservation checklist for the modernization. When a component This map is the preservation checklist for the modernization. When a component
is extracted, update the relevant rows with the owning component, test label, 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 | | 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 | | 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 | | 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 | | Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation ## Layers And Animation

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -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 fixed-function blending or needs framebuffer-fetch/ping-pong destination
feedback. `pano_cli plan-stroke-composite` exposes the same decision for feedback. `pano_cli plan-stroke-composite` exposes the same decision for
automation, including layer blend, stroke blend, dual-brush, and pattern-blend automation, including layer blend, stroke blend, dual-brush, and pattern-blend
inputs. Live canvas stroke execution is still legacy OpenGL and remains tracked inputs. Live `Canvas::draw_merge` now uses this planner for its existing
by DEBT-0036 until the app calls through this paint-renderer boundary. 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 The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.
@@ -1595,6 +1598,10 @@ Results:
- Canvas layer merge rendering and explicit layer-merge compositing now route - Canvas layer merge rendering and explicit layer-merge compositing now route
depth/blend state, active texture units, fallback 2D texture unbinds, and depth/blend state, active texture units, fallback 2D texture unbinds, and
merge framebuffer copy targets through the renderer GL backend mapping. 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 - Canvas equirectangular import drawing and depth export rendering now route
depth/blend state and active texture units through the renderer GL backend depth/blend state and active texture units through the renderer GL backend
mapping. mapping.

View File

@@ -4,6 +4,7 @@
#include "app.h" #include "app.h"
#include "texture.h" #include "texture.h"
#include "node_progress_bar.h" #include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
#include <thread> #include <thread>
#include <algorithm> #include <algorithm>
@@ -43,6 +44,104 @@ GLenum rgba_pixel_format()
return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format()); return static_cast<GLenum>(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::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(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<std::shared_ptr<Layer>>& 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() GLenum unsigned_byte_component_type()
{ {
return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type()); return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type());
@@ -1086,14 +1185,11 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f); auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f);
const auto& b = m_current_stroke->m_brush; const auto& b = m_current_stroke->m_brush;
// check if any layer use blend, otherwise draw directly on main framebuffer const bool use_blend = draw_merge_needs_shader_blend(
bool use_blend = false; m_width,
for (auto& l : m_layers) m_height,
{ m_layers,
use_blend |= l->m_blend_mode != 0; m_current_stroke ? m_current_stroke->m_brush.get() : nullptr);
}
if (Canvas::I->m_current_stroke)
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
// if not using shader blend, use gl rasterizer blend // if not using shader blend, use gl rasterizer blend
glDisable(depth_test_state()); glDisable(depth_test_state());