Plan stroke preview feedback copies

This commit is contained in:
2026-06-03 19:42:15 +02:00
parent 6b92d0bfea
commit fa1493b843
6 changed files with 56 additions and 14 deletions

View File

@@ -429,6 +429,7 @@ if(PP_BUILD_APP)
pp_legacy_engine pp_legacy_engine
pp_project_options pp_project_options
PRIVATE PRIVATE
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

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

View File

@@ -841,7 +841,8 @@ error codes, state queries, framebuffer targets, texture binding targets, and
active texture units to `pp_renderer_gl`. active texture units to `pp_renderer_gl`.
`NodeStrokePreview` brush preview rendering now delegates depth/scissor/blend `NodeStrokePreview` brush preview rendering now delegates depth/scissor/blend
state, viewport/clear-color queries, active texture units, 2D texture targets, 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 Legacy `Texture2D`, `TextureManager`, `Sampler`, and `RTT` public headers no
longer expose raw OpenGL enum defaults; default texture formats, sampler longer expose raw OpenGL enum defaults; default texture formats, sampler
filters/wraps, and render-target formats are resolved through backend-owned 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 Live `Canvas::stroke_draw` consumes that plan for main-brush, dual-brush, and
stroke-pad destination-copy versus framebuffer-fetch decisions. Thumbnail layer stroke-pad destination-copy versus framebuffer-fetch decisions. Thumbnail layer
blending now consumes the same canvas destination-feedback decision for its blending now consumes the same canvas destination-feedback decision for its
legacy `TextureBlend` path; the full thumbnail compositing execution remains legacy `TextureBlend` path. `NodeStrokePreview` uses the same destination
legacy OpenGL until a fuller live paint-renderer boundary can take over. 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 The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.

View File

@@ -6,7 +6,40 @@
#include "bezier.h" #include "bezier.h"
#include "canvas.h" #include "canvas.h"
#include "app.h" #include "app.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
#include <algorithm>
#include <cstdint>
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::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(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_int NodeStrokePreview::s_instances{ 0 };
std::atomic_bool NodeStrokePreview::s_running{ false }; 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(); gl.restore();
} }
glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Texture2D& blend_tex) glm::vec4 NodeStrokePreview::stroke_draw_samples(
std::array<vertex_t, 4>& 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)); glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
blend_tex.bind(); // bg, copy of framebuffer (copied before drawing) blend_tex.bind(); // bg, copy of framebuffer (copied before drawing)
@@ -169,7 +205,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
glm::vec2 pad(1); glm::vec2 pad(1);
glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, size); 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)); 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 // this is also used by the mixer
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, tex_pos.x, tex_pos.y, 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<vertex_t, 4>& P, Tex
} }
m_brush_shape.draw_fill(); m_brush_shape.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
glActiveTexture(pp::renderer::gl::active_texture_unit(1U)); glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
blend_tex.unbind(); blend_tex.unbind();
@@ -353,7 +389,9 @@ void NodeStrokePreview::draw_stroke_immediate()
glDisable(pp::renderer::gl::blend_state()); glDisable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Stroke); ShaderManager::use(kShader::Stroke);
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush 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::TexBG, 1); // bg
ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern
ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer 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_vec4(kShaderUniform::Col, { 0, 0, 0, 1 });
ShaderManager::u_float(kShaderUniform::Alpha, f.flow); ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); 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 // copy raw stroke to tex
@@ -435,7 +473,7 @@ void NodeStrokePreview::draw_stroke_immediate()
glActiveTexture(pp::renderer::gl::active_texture_unit(0U)); glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
b->m_tip_texture->bind(); b->m_tip_texture->bind();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
glActiveTexture(pp::renderer::gl::active_texture_unit(1U)); glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
m_tex.bind(); // tmp swap for blending 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_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::Alpha, glm::max(f.flow, m_min_flow));
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); 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)); glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
m_rtt_mixer.unbindTexture(); m_rtt_mixer.unbindTexture();

View File

@@ -49,7 +49,7 @@ public:
virtual void clear_context() override; virtual void clear_context() override;
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz); void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
// return rect {origin, size} // return rect {origin, size}
glm::vec4 stroke_draw_samples(std::array<vertex_t, 4>& P, Texture2D& blend_tex); glm::vec4 stroke_draw_samples(std::array<vertex_t, 4>& P, Texture2D& blend_tex, bool copy_stroke_destination);
std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke, float zoom) const; std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke, float zoom) const;
void draw_stroke(); void draw_stroke();
void draw_stroke_immediate(); void draw_stroke_immediate();