From fa1493b8435add9d3148e40599778a010f9ca05c Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 19:42:15 +0200 Subject: [PATCH] Plan stroke preview feedback copies --- CMakeLists.txt | 1 + docs/modernization/capability-map.md | 2 +- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 9 +++-- src/node_stroke_preview.cpp | 54 +++++++++++++++++++++++----- src/node_stroke_preview.h | 2 +- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a90ca11..4f529f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -429,6 +429,7 @@ if(PP_BUILD_APP) pp_legacy_engine pp_project_options PRIVATE + 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 606bbb6..866c9e3 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -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, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke/thumbnail destination-copy coverage, 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 `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke/thumbnail/brush-preview destination-copy 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 80e2cf5..5125263 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. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw` plus thumbnail layer blending use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, and thumbnail layer compositing still use legacy OpenGL canvas execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_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 | +| 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. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_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 646168b..9b87f67 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -841,7 +841,8 @@ error codes, state queries, framebuffer targets, texture binding targets, and active texture units to `pp_renderer_gl`. `NodeStrokePreview` brush preview rendering now delegates depth/scissor/blend state, viewport/clear-color queries, active texture units, 2D texture targets, -copy targets, and sampler filters/wraps to `pp_renderer_gl`. +copy targets, sampler filters/wraps, and destination-feedback copy/fetch +decisions to `pp_renderer_gl` and `pp_paint_renderer`. Legacy `Texture2D`, `TextureManager`, `Sampler`, and `RTT` public headers no longer expose raw OpenGL enum defaults; default texture formats, sampler filters/wraps, and render-target formats are resolved through backend-owned @@ -893,8 +894,10 @@ shader's required destination feedback without changing the legacy shader math. Live `Canvas::stroke_draw` consumes that plan for main-brush, dual-brush, and stroke-pad destination-copy versus framebuffer-fetch decisions. Thumbnail layer blending now consumes the same canvas destination-feedback decision for its -legacy `TextureBlend` path; the full thumbnail compositing execution remains -legacy OpenGL until a fuller live paint-renderer boundary can take over. +legacy `TextureBlend` path. `NodeStrokePreview` uses the same destination +feedback plan for its live brush-preview copy/fetch decision. The full +thumbnail and brush-preview compositing execution remains legacy OpenGL until a +fuller live paint-renderer boundary can take over. The existing renderer classes are not yet fully behind the renderer interfaces. diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 6390854..d3a5996 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -6,7 +6,40 @@ #include "bezier.h" #include "canvas.h" #include "app.h" +#include "paint_renderer/compositor.h" #include "renderer_gl/opengl_capabilities.h" +#include +#include + +namespace { + +pp::renderer::RenderDeviceFeatures stroke_preview_render_device_features() noexcept +{ + return ShaderManager::render_device_features(); +} + +pp::paint_renderer::CanvasStrokeFeedbackPlan stroke_preview_destination_feedback_plan( + int width, + int height) noexcept +{ + const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback( + stroke_preview_render_device_features(), + pp::renderer::Extent2D { + .width = static_cast(std::max(width, 0)), + .height = static_cast(std::max(height, 0)), + }); + if (plan) { + return plan.value(); + } + + pp::paint_renderer::CanvasStrokeFeedbackPlan fallback; + fallback.compatibility_fallback = true; + fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures; + fallback.requires_auxiliary_texture = true; + return fallback; +} + +} std::atomic_int NodeStrokePreview::s_instances{ 0 }; std::atomic_bool NodeStrokePreview::s_running{ false }; @@ -147,9 +180,12 @@ void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2 gl.restore(); } -glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array& P, Texture2D& blend_tex) +glm::vec4 NodeStrokePreview::stroke_draw_samples( + std::array& P, + Texture2D& blend_tex, + bool copy_stroke_destination) { - if (!ShaderManager::ext_framebuffer_fetch) + if (copy_stroke_destination) { glActiveTexture(pp::renderer::gl::active_texture_unit(1U)); blend_tex.bind(); // bg, copy of framebuffer (copied before drawing) @@ -169,7 +205,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array& P, Tex glm::vec2 pad(1); glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, size); glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(size) - tex_pos)); - if (!ShaderManager::ext_framebuffer_fetch) + if (copy_stroke_destination) { // this is also used by the mixer glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, tex_pos.x, tex_pos.y, @@ -189,7 +225,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array& P, Tex } m_brush_shape.draw_fill(); - if (!ShaderManager::ext_framebuffer_fetch) + if (copy_stroke_destination) { glActiveTexture(pp::renderer::gl::active_texture_unit(1U)); blend_tex.unbind(); @@ -353,7 +389,9 @@ void NodeStrokePreview::draw_stroke_immediate() glDisable(pp::renderer::gl::blend_state()); ShaderManager::use(kShader::Stroke); ShaderManager::u_int(kShaderUniform::Tex, 0); // brush - if (!ShaderManager::ext_framebuffer_fetch) + const auto stroke_feedback = stroke_preview_destination_feedback_plan(m_rtt.getWidth(), m_rtt.getHeight()); + const bool copy_stroke_destination = !stroke_feedback.reads_destination_color; + if (copy_stroke_destination) ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer @@ -388,7 +426,7 @@ void NodeStrokePreview::draw_stroke_immediate() ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 1 }); ShaderManager::u_float(kShaderUniform::Alpha, f.flow); ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); - /*auto rect =*/ stroke_draw_samples(f.shapes, m_tex_dual); + /*auto rect =*/ stroke_draw_samples(f.shapes, m_tex_dual, copy_stroke_destination); } // copy raw stroke to tex @@ -435,7 +473,7 @@ void NodeStrokePreview::draw_stroke_immediate() glActiveTexture(pp::renderer::gl::active_texture_unit(0U)); b->m_tip_texture->bind(); - if (!ShaderManager::ext_framebuffer_fetch) + if (copy_stroke_destination) { glActiveTexture(pp::renderer::gl::active_texture_unit(1U)); m_tex.bind(); // tmp swap for blending @@ -462,7 +500,7 @@ void NodeStrokePreview::draw_stroke_immediate() ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 1 } /*f.col*/); ShaderManager::u_float(kShaderUniform::Alpha, glm::max(f.flow, m_min_flow)); ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); - /*auto rect =*/ stroke_draw_samples(f.shapes, m_tex); + /*auto rect =*/ stroke_draw_samples(f.shapes, m_tex, copy_stroke_destination); } glActiveTexture(pp::renderer::gl::active_texture_unit(3U)); m_rtt_mixer.unbindTexture(); diff --git a/src/node_stroke_preview.h b/src/node_stroke_preview.h index 64f2b39..3d46e2d 100644 --- a/src/node_stroke_preview.h +++ b/src/node_stroke_preview.h @@ -49,7 +49,7 @@ public: virtual void clear_context() override; void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz); // return rect {origin, size} - glm::vec4 stroke_draw_samples(std::array& P, Texture2D& blend_tex); + glm::vec4 stroke_draw_samples(std::array& P, Texture2D& blend_tex, bool copy_stroke_destination); std::vector stroke_draw_compute(Stroke& stroke, float zoom) const; void draw_stroke(); void draw_stroke_immediate();