diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 8cbe3e4..4c68f99 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -36,8 +36,8 @@ and validation command. | ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR | | 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, GPU golden, and paint feedback path planning | -| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, framebuffer-fetch/ping-pong planning, and GPU parity | +| 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 | | 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 f55607e..6e03d32 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` and `pano_cli plan-paint-feedback` can choose a backend-neutral complex paint feedback strategy for 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`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `ctest --preset desktop-fast --build-config Debug` | Stroke/layer compositing chooses its feedback path through 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, 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 | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 171056b..2a57fa8 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -868,6 +868,13 @@ read destination color directly, while other backends must use ping-pong render targets backed by texture copy or render-target blit support. This is exposed through `pano_cli plan-paint-feedback` and tracked by DEBT-0036 until the live paint renderer consumes the plan. +`pp_paint_renderer` now consumes that lower-level feedback planner through a +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. The existing renderer classes are not yet fully behind the renderer interfaces. @@ -1118,6 +1125,10 @@ Results: no-feedback blends, invalid render-target usage, unsupported backends, and depth-target rejection. - `pp_paint_renderer_compositor_tests` passed. + The suite now covers fixed-function stroke composite planning, + framebuffer-fetch planning, ping-pong texture-copy/blit fallback planning, + dual/pattern blend feedback detection, invalid blend mode rejection, + unsupported backend rejection, and invalid render-target rejection. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_xml_tests` passed. diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp index 9ba965e..1474565 100644 --- a/src/paint_renderer/compositor.cpp +++ b/src/paint_renderer/compositor.cpp @@ -6,6 +6,40 @@ namespace pp::paint_renderer { namespace { +[[nodiscard]] bool is_valid_blend_mode(pp::paint::BlendMode mode) noexcept +{ + switch (mode) { + case pp::paint::BlendMode::normal: + case pp::paint::BlendMode::multiply: + case pp::paint::BlendMode::screen: + case pp::paint::BlendMode::color_dodge: + case pp::paint::BlendMode::overlay: + return true; + } + + return false; +} + +[[nodiscard]] bool is_valid_stroke_blend_mode(pp::paint::StrokeBlendMode mode) noexcept +{ + switch (mode) { + case pp::paint::StrokeBlendMode::normal: + case pp::paint::StrokeBlendMode::multiply: + case pp::paint::StrokeBlendMode::subtract: + case pp::paint::StrokeBlendMode::darken: + case pp::paint::StrokeBlendMode::overlay: + case pp::paint::StrokeBlendMode::color_dodge: + case pp::paint::StrokeBlendMode::color_burn: + case pp::paint::StrokeBlendMode::linear_burn: + case pp::paint::StrokeBlendMode::hard_mix: + case pp::paint::StrokeBlendMode::linear_height: + case pp::paint::StrokeBlendMode::height: + return true; + } + + return false; +} + [[nodiscard]] pp::foundation::Result expected_pixel_count(pp::renderer::Extent2D extent) noexcept { const auto extent_status = pp::renderer::validate_extent(extent); @@ -29,6 +63,20 @@ namespace { return pp::foundation::Result::success(static_cast(count)); } +[[nodiscard]] StrokeCompositePath composite_path_from_feedback(pp::renderer::PaintFeedbackPath path) noexcept +{ + switch (path) { + case pp::renderer::PaintFeedbackPath::none: + return StrokeCompositePath::fixed_function_blend; + case pp::renderer::PaintFeedbackPath::framebuffer_fetch: + return StrokeCompositePath::framebuffer_fetch; + case pp::renderer::PaintFeedbackPath::ping_pong_textures: + return StrokeCompositePath::ping_pong_textures; + } + + return StrokeCompositePath::fixed_function_blend; +} + } pp::foundation::Status composite_layer( @@ -62,4 +110,78 @@ pp::foundation::Status composite_layer( return pp::foundation::Status::success(); } +bool stroke_composite_requires_feedback( + pp::paint::BlendMode layer_blend_mode, + pp::paint::StrokeBlendMode stroke_blend_mode, + bool dual_brush_blend, + bool pattern_blend) noexcept +{ + return layer_blend_mode != pp::paint::BlendMode::normal + || stroke_blend_mode != pp::paint::StrokeBlendMode::normal + || dual_brush_blend + || pattern_blend; +} + +pp::foundation::Result plan_stroke_composite( + pp::renderer::RenderDeviceFeatures features, + StrokeCompositeRequest request) noexcept +{ + if (!is_valid_blend_mode(request.layer_blend_mode)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown layer blend mode")); + } + + if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown stroke blend mode")); + } + + const pp::renderer::TextureDesc target_desc { + .extent = request.extent, + .format = request.target_format, + .usage = request.target_usage, + .debug_name = "stroke-composite-target", + }; + const auto complex_blend = stroke_composite_requires_feedback( + request.layer_blend_mode, + request.stroke_blend_mode, + request.dual_brush_blend, + request.pattern_blend); + const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, complex_blend); + if (!feedback) { + return pp::foundation::Result::failure(feedback.status()); + } + + StrokeCompositePlan plan; + plan.path = composite_path_from_feedback(feedback.value().path); + plan.feedback = feedback.value(); + plan.target_desc = target_desc; + plan.target_bytes = feedback.value().target_bytes; + plan.auxiliary_bytes = feedback.value().requires_auxiliary_texture + ? feedback.value().target_bytes + : 0U; + plan.estimated_working_bytes = plan.target_bytes + plan.auxiliary_bytes; + plan.complex_blend = complex_blend; + plan.reads_destination_color = feedback.value().reads_destination_color; + plan.requires_auxiliary_texture = feedback.value().requires_auxiliary_texture; + plan.requires_texture_copy = feedback.value().requires_texture_copy; + plan.requires_render_target_blit = feedback.value().requires_render_target_blit; + plan.requires_explicit_transition = feedback.value().requires_explicit_transition; + return pp::foundation::Result::success(plan); +} + +const char* stroke_composite_path_name(StrokeCompositePath path) noexcept +{ + switch (path) { + case StrokeCompositePath::fixed_function_blend: + return "fixed_function_blend"; + case StrokeCompositePath::framebuffer_fetch: + return "framebuffer_fetch"; + case StrokeCompositePath::ping_pong_textures: + return "ping_pong_textures"; + } + + return "unknown"; +} + } diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 735b7f2..37defd6 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -4,10 +4,17 @@ #include "paint/blend.h" #include "renderer_api/renderer_api.h" +#include #include namespace pp::paint_renderer { +enum class StrokeCompositePath : std::uint8_t { + fixed_function_blend, + framebuffer_fetch, + ping_pong_textures, +}; + struct LayerCompositeView { std::span pixels; float opacity = 1.0F; @@ -15,9 +22,49 @@ struct LayerCompositeView { pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal; }; +struct StrokeCompositeRequest { + pp::renderer::Extent2D extent {}; + pp::renderer::TextureFormat target_format = pp::renderer::TextureFormat::rgba8; + pp::renderer::TextureUsage target_usage = pp::renderer::TextureUsage::render_target + | pp::renderer::TextureUsage::sampled + | pp::renderer::TextureUsage::copy_source + | pp::renderer::TextureUsage::copy_destination; + pp::paint::BlendMode layer_blend_mode = pp::paint::BlendMode::normal; + pp::paint::StrokeBlendMode stroke_blend_mode = pp::paint::StrokeBlendMode::normal; + bool dual_brush_blend = false; + bool pattern_blend = false; +}; + +struct StrokeCompositePlan { + StrokeCompositePath path = StrokeCompositePath::fixed_function_blend; + pp::renderer::PaintFeedbackPlan feedback {}; + pp::renderer::TextureDesc target_desc {}; + std::uint64_t target_bytes = 0; + std::uint64_t auxiliary_bytes = 0; + std::uint64_t estimated_working_bytes = 0; + bool complex_blend = false; + bool reads_destination_color = false; + bool requires_auxiliary_texture = false; + bool requires_texture_copy = false; + bool requires_render_target_blit = false; + bool requires_explicit_transition = false; +}; + [[nodiscard]] pp::foundation::Status composite_layer( std::span destination, pp::renderer::Extent2D extent, LayerCompositeView layer) noexcept; +[[nodiscard]] bool stroke_composite_requires_feedback( + pp::paint::BlendMode layer_blend_mode, + pp::paint::StrokeBlendMode stroke_blend_mode, + bool dual_brush_blend, + bool pattern_blend) noexcept; + +[[nodiscard]] pp::foundation::Result plan_stroke_composite( + pp::renderer::RenderDeviceFeatures features, + StrokeCompositeRequest request) noexcept; + +[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept; + } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index efb6637..b1db48e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1222,6 +1222,36 @@ if(TARGET pano_cli) LABELS "renderer;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_stroke_composite_fixed_smoke + COMMAND pano_cli plan-stroke-composite --render-only) + set_tests_properties(pano_cli_plan_stroke_composite_fixed_smoke PROPERTIES + LABELS "renderer;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-stroke-composite\".*\"path\":\"fixed_function_blend\".*\"feedbackPath\":\"none\".*\"complexBlend\":false.*\"readsDestinationColor\":false") + + add_test(NAME pano_cli_plan_stroke_composite_framebuffer_fetch_smoke + COMMAND pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only) + set_tests_properties(pano_cli_plan_stroke_composite_framebuffer_fetch_smoke PROPERTIES + LABELS "renderer;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-stroke-composite\".*\"strokeBlend\":\"height\".*\"path\":\"framebuffer_fetch\".*\"feedbackPath\":\"framebuffer_fetch\".*\"complexBlend\":true.*\"requiresExplicitTransition\":true") + + add_test(NAME pano_cli_plan_stroke_composite_ping_pong_copy_smoke + COMMAND pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy) + set_tests_properties(pano_cli_plan_stroke_composite_ping_pong_copy_smoke PROPERTIES + LABELS "renderer;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-stroke-composite\".*\"layerBlend\":\"overlay\".*\"path\":\"ping_pong_textures\".*\"estimatedWorkingBytes\":16384.*\"requiresAuxiliaryTexture\":true.*\"requiresTextureCopy\":true") + + add_test(NAME pano_cli_plan_stroke_composite_rejects_unsupported + COMMAND pano_cli plan-stroke-composite --layer-blend 1) + set_tests_properties(pano_cli_plan_stroke_composite_rejects_unsupported PROPERTIES + LABELS "renderer;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_stroke_composite_rejects_bad_stroke_blend + COMMAND pano_cli plan-stroke-composite --stroke-blend 99 --texture-copy) + set_tests_properties(pano_cli_plan_stroke_composite_rejects_bad_stroke_blend PROPERTIES + LABELS "renderer;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_canvas_tool_draw_smoke COMMAND pano_cli plan-canvas-tool --kind draw) set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 8ffb977..8853e9f 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -2,14 +2,24 @@ #include "test_harness.h" #include +#include #include using pp::foundation::StatusCode; using pp::paint::BlendMode; using pp::paint::Rgba; +using pp::paint::StrokeBlendMode; using pp::paint_renderer::LayerCompositeView; +using pp::paint_renderer::StrokeCompositePath; +using pp::paint_renderer::StrokeCompositeRequest; using pp::paint_renderer::composite_layer; +using pp::paint_renderer::plan_stroke_composite; +using pp::paint_renderer::stroke_composite_path_name; +using pp::paint_renderer::stroke_composite_requires_feedback; using pp::renderer::Extent2D; +using pp::renderer::RenderDeviceFeatures; +using pp::renderer::TextureFormat; +using pp::renderer::TextureUsage; namespace { @@ -97,6 +107,158 @@ void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h) PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument); } +void detects_feedback_requirements(pp::tests::Harness& h) +{ + PP_EXPECT(h, !stroke_composite_requires_feedback( + BlendMode::normal, + StrokeBlendMode::normal, + false, + false)); + PP_EXPECT(h, stroke_composite_requires_feedback( + BlendMode::multiply, + StrokeBlendMode::normal, + false, + false)); + PP_EXPECT(h, stroke_composite_requires_feedback( + BlendMode::normal, + StrokeBlendMode::overlay, + false, + false)); + PP_EXPECT(h, stroke_composite_requires_feedback( + BlendMode::normal, + StrokeBlendMode::normal, + true, + false)); + PP_EXPECT(h, stroke_composite_requires_feedback( + BlendMode::normal, + StrokeBlendMode::normal, + false, + true)); +} + +void plans_stroke_composite_paths(pp::tests::Harness& h) +{ + const StrokeCompositeRequest simple { + .extent = Extent2D { .width = 64, .height = 32 }, + .target_usage = TextureUsage::render_target, + }; + const auto fixed = plan_stroke_composite( + RenderDeviceFeatures {}, + simple); + PP_EXPECT(h, fixed); + if (fixed) { + PP_EXPECT(h, fixed.value().path == StrokeCompositePath::fixed_function_blend); + PP_EXPECT(h, !fixed.value().complex_blend); + PP_EXPECT(h, !fixed.value().reads_destination_color); + PP_EXPECT(h, fixed.value().target_bytes == 8192U); + PP_EXPECT(h, fixed.value().estimated_working_bytes == 8192U); + } + + const StrokeCompositeRequest complex_fetch { + .extent = Extent2D { .width = 32, .height = 16 }, + .target_usage = TextureUsage::render_target, + .stroke_blend_mode = StrokeBlendMode::height, + }; + const auto fetch = plan_stroke_composite( + RenderDeviceFeatures { + .framebuffer_fetch = true, + .explicit_texture_transitions = true, + }, + complex_fetch); + PP_EXPECT(h, fetch); + if (fetch) { + PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch); + PP_EXPECT(h, fetch.value().complex_blend); + PP_EXPECT(h, fetch.value().reads_destination_color); + PP_EXPECT(h, !fetch.value().requires_auxiliary_texture); + PP_EXPECT(h, fetch.value().requires_explicit_transition); + PP_EXPECT(h, fetch.value().target_bytes == 2048U); + } + + const StrokeCompositeRequest complex_copy { + .extent = Extent2D { .width = 32, .height = 16 }, + .layer_blend_mode = BlendMode::overlay, + .dual_brush_blend = true, + }; + const auto copy = plan_stroke_composite( + RenderDeviceFeatures { .texture_copy = true }, + complex_copy); + PP_EXPECT(h, copy); + if (copy) { + PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures); + PP_EXPECT(h, copy.value().requires_auxiliary_texture); + PP_EXPECT(h, copy.value().requires_texture_copy); + PP_EXPECT(h, !copy.value().requires_render_target_blit); + PP_EXPECT(h, copy.value().target_bytes == 2048U); + PP_EXPECT(h, copy.value().auxiliary_bytes == 2048U); + PP_EXPECT(h, copy.value().estimated_working_bytes == 4096U); + } + + const auto blit = plan_stroke_composite( + RenderDeviceFeatures { .render_target_blit = true }, + StrokeCompositeRequest { + .extent = Extent2D { .width = 32, .height = 16 }, + .pattern_blend = true, + }); + PP_EXPECT(h, blit); + if (blit) { + PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures); + PP_EXPECT(h, !blit.value().requires_texture_copy); + PP_EXPECT(h, blit.value().requires_render_target_blit); + } +} + +void rejects_bad_stroke_composite_plans(pp::tests::Harness& h) +{ + const auto unsupported = plan_stroke_composite( + RenderDeviceFeatures {}, + StrokeCompositeRequest { + .extent = Extent2D { .width = 32, .height = 16 }, + .layer_blend_mode = BlendMode::multiply, + }); + const auto missing_usage = plan_stroke_composite( + RenderDeviceFeatures { .texture_copy = true }, + StrokeCompositeRequest { + .extent = Extent2D { .width = 32, .height = 16 }, + .target_usage = TextureUsage::render_target, + .layer_blend_mode = BlendMode::multiply, + }); + const auto depth = plan_stroke_composite( + RenderDeviceFeatures { .texture_copy = true }, + StrokeCompositeRequest { + .extent = Extent2D { .width = 32, .height = 16 }, + .target_format = TextureFormat::depth24_stencil8, + .layer_blend_mode = BlendMode::multiply, + }); + const auto bad_blend = plan_stroke_composite( + RenderDeviceFeatures { .texture_copy = true }, + StrokeCompositeRequest { + .extent = Extent2D { .width = 32, .height = 16 }, + .layer_blend_mode = static_cast(255), + }); + const auto bad_stroke_blend = plan_stroke_composite( + RenderDeviceFeatures { .texture_copy = true }, + StrokeCompositeRequest { + .extent = Extent2D { .width = 32, .height = 16 }, + .stroke_blend_mode = static_cast(255), + }); + + PP_EXPECT(h, !unsupported.ok()); + PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_usage.ok()); + PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !depth.ok()); + PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_blend.ok()); + PP_EXPECT(h, bad_blend.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_stroke_blend.ok()); + PP_EXPECT(h, bad_stroke_blend.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::fixed_function_blend) == std::string_view("fixed_function_blend")); + PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::framebuffer_fetch) == std::string_view("framebuffer_fetch")); + PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::ping_pong_textures) == std::string_view("ping_pong_textures")); + PP_EXPECT(h, stroke_composite_path_name(static_cast(255)) == std::string_view("unknown")); +} + } int main() @@ -105,5 +267,8 @@ int main() harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity); harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops); harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity); + harness.run("detects_feedback_requirements", detects_feedback_requirements); + harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths); + harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans); return harness.finish(); } diff --git a/tools/pano_cli/CMakeLists.txt b/tools/pano_cli/CMakeLists.txt index 87f46ff..9305b0f 100644 --- a/tools/pano_cli/CMakeLists.txt +++ b/tools/pano_cli/CMakeLists.txt @@ -7,6 +7,7 @@ target_link_libraries(pano_cli PRIVATE pp_foundation pp_assets pp_document + pp_paint_renderer pp_renderer_api pp_ui_core) diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 3701db6..f336d0f 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -31,6 +31,7 @@ #include "foundation/parse.h" #include "foundation/result.h" #include "paint/blend.h" +#include "paint_renderer/compositor.h" #include "paint/stroke.h" #include "paint/stroke_script.h" #include "renderer_api/recording_renderer.h" @@ -347,6 +348,21 @@ struct PlanPaintFeedbackArgs { bool depth_target = false; }; +struct PlanStrokeCompositeArgs { + int width = 64; + int height = 32; + int layer_blend_mode = 0; + int stroke_blend_mode = 0; + bool dual_brush_blend = false; + bool pattern_blend = false; + bool framebuffer_fetch = false; + bool explicit_texture_transitions = false; + bool texture_copy = false; + bool render_target_blit = false; + bool render_only_target = false; + bool depth_target = false; +}; + struct PlanGridOperationArgs { std::string kind = "pick"; std::string path; @@ -1766,6 +1782,7 @@ void print_help() << " plan-brush-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n" << " plan-brush-stroke-control --kind float|bool|blend|tip-aspect-reset|default-reset [--setting NAME] [--value N] [--enabled|--disabled] [--blend-mode N]\n" << " plan-paint-feedback [--width N] [--height N] [--simple|--complex] [--framebuffer-fetch] [--texture-copy] [--blit] [--explicit-transitions] [--render-only] [--depth]\n" + << " plan-stroke-composite [--width N] [--height N] [--layer-blend N] [--stroke-blend N] [--dual-blend] [--pattern-blend] [--framebuffer-fetch] [--texture-copy] [--blit] [--explicit-transitions] [--render-only] [--depth]\n" << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" @@ -4933,6 +4950,137 @@ int plan_paint_feedback(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_stroke_composite_args( + int argc, + char** argv, + PlanStrokeCompositeArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--width" || key == "--height" || key == "--layer-blend" || key == "--stroke-blend") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--width" || key == "--height") { + if (value.value() <= 0) { + return pp::foundation::Status::invalid_argument("stroke composite extent must be greater than zero"); + } + if (key == "--width") { + args.width = value.value(); + } else { + args.height = value.value(); + } + } else if (key == "--layer-blend") { + args.layer_blend_mode = value.value(); + } else { + args.stroke_blend_mode = value.value(); + } + } else if (key == "--dual-blend") { + args.dual_brush_blend = true; + } else if (key == "--pattern-blend") { + args.pattern_blend = true; + } else if (key == "--framebuffer-fetch") { + args.framebuffer_fetch = true; + } else if (key == "--explicit-transitions") { + args.explicit_texture_transitions = true; + } else if (key == "--texture-copy") { + args.texture_copy = true; + } else if (key == "--blit") { + args.render_target_blit = true; + } else if (key == "--render-only") { + args.render_only_target = true; + } else if (key == "--depth") { + args.depth_target = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + if (args.layer_blend_mode < 0 || args.layer_blend_mode > 4) { + return pp::foundation::Status::out_of_range("layer blend mode must be in the range [0, 4]"); + } + if (args.stroke_blend_mode < 0 || args.stroke_blend_mode > 10) { + return pp::foundation::Status::out_of_range("stroke blend mode must be in the range [0, 10]"); + } + + return pp::foundation::Status::success(); +} + +int plan_stroke_composite(int argc, char** argv) +{ + PlanStrokeCompositeArgs args; + const auto status = parse_plan_stroke_composite_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-stroke-composite", status.message); + return 2; + } + + pp::renderer::TextureUsage usage = pp::renderer::TextureUsage::render_target; + if (!args.render_only_target) { + usage |= pp::renderer::TextureUsage::sampled; + usage |= pp::renderer::TextureUsage::copy_source; + usage |= pp::renderer::TextureUsage::copy_destination; + } + + const pp::paint_renderer::StrokeCompositeRequest request { + .extent = pp::renderer::Extent2D { + .width = static_cast(args.width), + .height = static_cast(args.height), + }, + .target_format = args.depth_target + ? pp::renderer::TextureFormat::depth24_stencil8 + : pp::renderer::TextureFormat::rgba8, + .target_usage = usage, + .layer_blend_mode = static_cast(args.layer_blend_mode), + .stroke_blend_mode = static_cast(args.stroke_blend_mode), + .dual_brush_blend = args.dual_brush_blend, + .pattern_blend = args.pattern_blend, + }; + const pp::renderer::RenderDeviceFeatures features { + .framebuffer_fetch = args.framebuffer_fetch, + .explicit_texture_transitions = args.explicit_texture_transitions, + .texture_copy = args.texture_copy, + .render_target_blit = args.render_target_blit, + }; + const auto plan = pp::paint_renderer::plan_stroke_composite(features, request); + if (!plan) { + print_error("plan-stroke-composite", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-stroke-composite\"" + << ",\"state\":{\"width\":" << args.width + << ",\"height\":" << args.height + << ",\"layerBlend\":\"" << pp::paint::blend_mode_name(request.layer_blend_mode) + << "\",\"strokeBlend\":\"" << pp::paint::stroke_blend_mode_name(request.stroke_blend_mode) + << "\",\"dualBrushBlend\":" << json_bool(args.dual_brush_blend) + << ",\"patternBlend\":" << json_bool(args.pattern_blend) + << ",\"framebufferFetch\":" << json_bool(args.framebuffer_fetch) + << ",\"explicitTransitions\":" << json_bool(args.explicit_texture_transitions) + << ",\"textureCopy\":" << json_bool(args.texture_copy) + << ",\"blit\":" << json_bool(args.render_target_blit) + << ",\"renderOnlyTarget\":" << json_bool(args.render_only_target) + << ",\"depthTarget\":" << json_bool(args.depth_target) + << "},\"plan\":{\"path\":\"" << pp::paint_renderer::stroke_composite_path_name(value.path) + << "\",\"feedbackPath\":\"" << pp::renderer::paint_feedback_path_name(value.feedback.path) + << "\",\"targetBytes\":" << value.target_bytes + << ",\"auxiliaryBytes\":" << value.auxiliary_bytes + << ",\"estimatedWorkingBytes\":" << value.estimated_working_bytes + << ",\"complexBlend\":" << json_bool(value.complex_blend) + << ",\"readsDestinationColor\":" << json_bool(value.reads_destination_color) + << ",\"requiresAuxiliaryTexture\":" << json_bool(value.requires_auxiliary_texture) + << ",\"requiresTextureCopy\":" << json_bool(value.requires_texture_copy) + << ",\"requiresRenderTargetBlit\":" << json_bool(value.requires_render_target_blit) + << ",\"requiresExplicitTransition\":" << json_bool(value.requires_explicit_transition) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_canvas_tool_args( int argc, char** argv, @@ -8067,6 +8215,10 @@ int main(int argc, char** argv) return plan_paint_feedback(argc, argv); } + if (command == "plan-stroke-composite") { + return plan_stroke_composite(argc, argv); + } + if (command == "plan-canvas-tool") { return plan_canvas_tool(argc, argv); }