Plan thumbnail blend feedback copies

This commit is contained in:
2026-06-03 19:27:57 +02:00
parent 2ac2c45b11
commit 6b92d0bfea
4 changed files with 20 additions and 14 deletions

View File

@@ -15,7 +15,7 @@ and validation command.
| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke | | Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke |
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke | | Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke |
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | | Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input | | Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input, destination-feedback copy/fetch gate |
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke | | Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
## Image And Export ## Image And Export
@@ -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-feedback 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 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 stroke shader feedback decision, and live `Canvas::stroke_draw` uses it for main-brush, dual-brush, and stroke-pad destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, and thumbnail layer blending 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` 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 |
## Closed Debt ## Closed Debt

View File

@@ -891,9 +891,10 @@ individual `ShaderManager` extension booleans.
`pp_paint_renderer::plan_canvas_stroke_feedback` now models the current stroke `pp_paint_renderer::plan_canvas_stroke_feedback` now models the current stroke
shader's required destination feedback without changing the legacy shader math. 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 is the remaining direct canvas feedback branch before a fuller live blending now consumes the same canvas destination-feedback decision for its
paint-renderer execution boundary can take over. legacy `TextureBlend` path; the full thumbnail 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.
@@ -1633,6 +1634,9 @@ Results:
`pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch `pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch
supplies destination color or the legacy OpenGL path must copy the target supplies destination color or the legacy OpenGL path must copy the target
texture before drawing. texture before drawing.
- Canvas thumbnail layer blending now uses the same canvas destination-feedback
plan for framebuffer-fetch versus texture-copy decisions; the thumbnail draw
itself still executes through retained OpenGL canvas code 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

@@ -44,17 +44,17 @@ GLenum rgba_pixel_format()
return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format()); return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format());
} }
pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept pp::renderer::RenderDeviceFeatures canvas_render_device_features() noexcept
{ {
return ShaderManager::render_device_features(); return ShaderManager::render_device_features();
} }
pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_stroke_feedback_plan( pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_destination_feedback_plan(
int width, int width,
int height) noexcept int height) noexcept
{ {
const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback( const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback(
canvas_stroke_composite_features(), canvas_render_device_features(),
pp::renderer::Extent2D { pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)), .width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)), .height = static_cast<std::uint32_t>(std::max(height, 0)),
@@ -86,7 +86,7 @@ pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
} }
const auto plan = pp::paint_renderer::plan_canvas_blend_gate( const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
canvas_stroke_composite_features(), canvas_render_device_features(),
pp::paint_renderer::CanvasBlendGateRequest { pp::paint_renderer::CanvasBlendGateRequest {
.extent = pp::renderer::Extent2D { .extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)), .width = static_cast<std::uint32_t>(std::max(width, 0)),
@@ -654,7 +654,7 @@ void Canvas::stroke_draw()
if (brush->m_pattern_flipx) patt_scale.x *= -1.f; if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
if (brush->m_pattern_flipy) patt_scale.y *= -1.f; if (brush->m_pattern_flipy) patt_scale.y *= -1.f;
const auto stroke_feedback = canvas_stroke_feedback_plan(m_width, m_height); const auto stroke_feedback = canvas_destination_feedback_plan(m_width, m_height);
const bool copy_stroke_destination = !stroke_feedback.reads_destination_color; const bool copy_stroke_destination = !stroke_feedback.reads_destination_color;
glDisable(blend_state()); glDisable(blend_state());
@@ -2842,6 +2842,8 @@ Image Canvas::thumbnail_generate(int w, int h)
m_face_plane.create<1>(2, 2); m_face_plane.create<1>(2, 2);
Texture2D blendtex; Texture2D blendtex;
blendtex.create(w, h); blendtex.create(w, h);
const auto layer_feedback = canvas_destination_feedback_plan(w, h);
const bool copy_layer_destination = !layer_feedback.reads_destination_color;
// recalculate because of different aspect ratio than the m_proj matrix // recalculate because of different aspect ratio than the m_proj matrix
glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f); glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f);
@@ -2855,7 +2857,7 @@ Image Canvas::thumbnail_generate(int w, int h)
ShaderManager::use(kShader::TextureBlend); ShaderManager::use(kShader::TextureBlend);
ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp); ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
if (!ShaderManager::ext_framebuffer_fetch) if (copy_layer_destination)
{ {
ShaderManager::u_int(kShaderUniform::TexBG, 2); ShaderManager::u_int(kShaderUniform::TexBG, 2);
set_active_texture_unit(2); set_active_texture_unit(2);
@@ -2869,7 +2871,7 @@ Image Canvas::thumbnail_generate(int w, int h)
m_layers[layer_index]->m_opacity == 0.f || m_layers[layer_index]->m_opacity == 0.f ||
!m_layers[layer_index]->face(i)) !m_layers[layer_index]->face(i))
continue; continue;
if (!ShaderManager::ext_framebuffer_fetch) if (copy_layer_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, w, h); glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, w, h);
@@ -2882,7 +2884,7 @@ Image Canvas::thumbnail_generate(int w, int h)
m_layers[layer_index]->rtt(i).unbindTexture(); m_layers[layer_index]->rtt(i).unbindTexture();
} }
if (!ShaderManager::ext_framebuffer_fetch) if (copy_layer_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
blendtex.unbind(); blendtex.unbind();