From 94a6877e7cfd51e559f378aa7d84c66ab036d29d Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 17:58:24 +0200 Subject: [PATCH] Add paint feedback strategy planner --- docs/modernization/capability-map.md | 4 +- docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 11 +- src/renderer_api/renderer_api.cpp | 73 ++++++++++++ src/renderer_api/renderer_api.h | 24 ++++ tests/CMakeLists.txt | 30 +++++ tests/renderer_api/renderer_api_tests.cpp | 92 ++++++++++++++++ tools/pano_cli/main.cpp | 128 ++++++++++++++++++++++ 8 files changed, 360 insertions(+), 3 deletions(-) diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 0806c6f..8cbe3e4 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 and GPU golden | -| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors plus GPU parity | +| 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 | | 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 de80819..f55607e 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -53,6 +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 | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 87f37e9..171056b 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -862,6 +862,12 @@ nested passes, texture I/O or blits inside a pass, and unclosed passes. It also validates executable command dependencies, including shader-before-uniform and shader-plus-mesh before draw within each render pass, and rejects invalid texture/sampler bind slots in malformed recorded streams. +The renderer-neutral API now also plans complex paint feedback strategies for +future stroke/layer compositing work: framebuffer-fetch-capable backends can +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. The existing renderer classes are not yet fully behind the renderer interfaces. @@ -1107,7 +1113,10 @@ Results: render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ upload/texture-copy/readback/frame-capture/blit command capture, draw mesh-input capture, explicit draw-range capture, and invalid catalog - rejection. + rejection. The same suite now covers complex paint feedback planning for + framebuffer-fetch backends, ping-pong texture-copy/blit fallbacks, simple + no-feedback blends, invalid render-target usage, unsupported backends, and + depth-target rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index ef86ac5..d43a89b 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -823,6 +823,65 @@ pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc desti return pp::foundation::Status::success(); } +pp::foundation::Result plan_paint_feedback( + RenderDeviceFeatures features, + TextureDesc target_desc, + bool complex_blend) noexcept +{ + const auto target_status = validate_texture_desc(target_desc); + if (!target_status.ok()) { + return pp::foundation::Result::failure(target_status); + } + + if (!has_texture_usage(target_desc.usage, TextureUsage::render_target)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("paint feedback target must allow render_target usage")); + } + + if (target_desc.format == TextureFormat::depth24_stencil8) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("paint feedback target must be a color texture")); + } + + const auto target_bytes = texture_byte_size(target_desc); + if (!target_bytes.ok()) { + return pp::foundation::Result::failure(target_bytes.status()); + } + + PaintFeedbackPlan plan; + plan.target_desc = target_desc; + plan.target_bytes = target_bytes.value(); + plan.complex_blend = complex_blend; + if (!complex_blend) { + return pp::foundation::Result::success(plan); + } + + plan.reads_destination_color = true; + if (features.framebuffer_fetch) { + plan.path = PaintFeedbackPath::framebuffer_fetch; + plan.requires_explicit_transition = features.explicit_texture_transitions; + return pp::foundation::Result::success(plan); + } + + const bool can_ping_pong = has_texture_usage(target_desc.usage, TextureUsage::sampled) + && has_texture_usage(target_desc.usage, TextureUsage::copy_source) + && has_texture_usage(target_desc.usage, TextureUsage::copy_destination) + && (features.texture_copy || features.render_target_blit); + if (!can_ping_pong) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument( + "complex paint feedback requires framebuffer fetch or sampled copy-capable render targets")); + } + + plan.path = PaintFeedbackPath::ping_pong_textures; + plan.requires_auxiliary_texture = true; + plan.requires_texture_copy = features.texture_copy; + plan.requires_render_target_blit = !features.texture_copy && features.render_target_blit; + plan.requires_explicit_transition = features.explicit_texture_transitions; + plan.auxiliary_desc = target_desc; + return pp::foundation::Result::success(plan); +} + const char* texture_format_name(TextureFormat format) noexcept { switch (format) { @@ -887,6 +946,20 @@ const char* blit_filter_name(BlitFilter filter) noexcept return "unknown"; } +const char* paint_feedback_path_name(PaintFeedbackPath path) noexcept +{ + switch (path) { + case PaintFeedbackPath::none: + return "none"; + case PaintFeedbackPath::framebuffer_fetch: + return "framebuffer_fetch"; + case PaintFeedbackPath::ping_pong_textures: + return "ping_pong_textures"; + } + + return "unknown"; +} + const char* blend_factor_name(BlendFactor factor) noexcept { switch (factor) { diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index 2e24dba..9bebcf3 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -132,6 +132,12 @@ enum class BlitFilter : std::uint8_t { linear, }; +enum class PaintFeedbackPath : std::uint8_t { + none, + framebuffer_fetch, + ping_pong_textures, +}; + enum class BlendFactor : std::uint8_t { zero, one, @@ -236,6 +242,19 @@ struct RenderDeviceFeatures { bool float32_render_targets = false; }; +struct PaintFeedbackPlan { + PaintFeedbackPath path = PaintFeedbackPath::none; + TextureDesc target_desc {}; + TextureDesc auxiliary_desc {}; + std::uint64_t target_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; +}; + class ITexture2D { public: virtual ~ITexture2D() = default; @@ -393,10 +412,15 @@ public: [[nodiscard]] pp::foundation::Status validate_blit_descs( TextureDesc source, TextureDesc destination) noexcept; +[[nodiscard]] pp::foundation::Result plan_paint_feedback( + RenderDeviceFeatures features, + TextureDesc target_desc, + bool complex_blend) noexcept; [[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept; [[nodiscard]] const char* texture_state_name(TextureState state) noexcept; [[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; [[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept; +[[nodiscard]] const char* paint_feedback_path_name(PaintFeedbackPath path) noexcept; [[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept; [[nodiscard]] const char* blend_op_name(BlendOp op) noexcept; [[nodiscard]] const char* compare_op_name(CompareOp op) noexcept; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 51af0c1..efb6637 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1192,6 +1192,36 @@ if(TARGET pano_cli) LABELS "app;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_paint_feedback_framebuffer_fetch_smoke + COMMAND pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only) + set_tests_properties(pano_cli_plan_paint_feedback_framebuffer_fetch_smoke PROPERTIES + LABELS "renderer;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"framebuffer_fetch\".*\"readsDestinationColor\":true.*\"requiresAuxiliaryTexture\":false.*\"requiresExplicitTransition\":true") + + add_test(NAME pano_cli_plan_paint_feedback_ping_pong_copy_smoke + COMMAND pano_cli plan-paint-feedback --texture-copy) + set_tests_properties(pano_cli_plan_paint_feedback_ping_pong_copy_smoke PROPERTIES + LABELS "renderer;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"ping_pong_textures\".*\"requiresAuxiliaryTexture\":true.*\"requiresTextureCopy\":true.*\"requiresRenderTargetBlit\":false") + + add_test(NAME pano_cli_plan_paint_feedback_simple_smoke + COMMAND pano_cli plan-paint-feedback --simple --render-only) + set_tests_properties(pano_cli_plan_paint_feedback_simple_smoke PROPERTIES + LABELS "renderer;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"none\".*\"complexBlend\":false.*\"readsDestinationColor\":false") + + add_test(NAME pano_cli_plan_paint_feedback_rejects_unsupported + COMMAND pano_cli plan-paint-feedback) + set_tests_properties(pano_cli_plan_paint_feedback_rejects_unsupported PROPERTIES + LABELS "renderer;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_paint_feedback_rejects_depth + COMMAND pano_cli plan-paint-feedback --texture-copy --depth) + set_tests_properties(pano_cli_plan_paint_feedback_rejects_depth 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/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index 97645a2..ea4d79f 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -34,6 +34,7 @@ using pp::renderer::IRenderTarget; using pp::renderer::IRenderTrace; using pp::renderer::IShaderProgram; using pp::renderer::MeshDesc; +using pp::renderer::PaintFeedbackPath; using pp::renderer::PrimitiveTopology; using pp::renderer::ReadbackRegion; using pp::renderer::RecordedRenderCommandKind; @@ -66,6 +67,8 @@ using pp::renderer::max_texture_dimension; using pp::renderer::max_texture_slots; using pp::renderer::max_trace_label_bytes; using pp::renderer::panopainter_shader_catalog; +using pp::renderer::paint_feedback_path_name; +using pp::renderer::plan_paint_feedback; using pp::renderer::primitive_topology_name; using pp::renderer::readback_byte_size; using pp::renderer::recorded_render_command_kind_name; @@ -1210,6 +1213,94 @@ void validates_blit_contract(pp::tests::Harness& h) PP_EXPECT(h, blit_filter_name(static_cast(255)) == std::string_view("unknown")); } +void plans_paint_feedback_paths(pp::tests::Harness& h) +{ + const TextureDesc render_target { + .extent = Extent2D { .width = 64, .height = 32 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::render_target + | TextureUsage::sampled + | TextureUsage::copy_source + | TextureUsage::copy_destination, + .debug_name = "paint-target", + }; + const TextureDesc render_only_target { + .extent = Extent2D { .width = 64, .height = 32 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::render_target, + .debug_name = "paint-render-only", + }; + const TextureDesc depth_target { + .extent = Extent2D { .width = 64, .height = 32 }, + .format = TextureFormat::depth24_stencil8, + .usage = TextureUsage::render_target + | TextureUsage::sampled + | TextureUsage::copy_source + | TextureUsage::copy_destination, + .debug_name = "paint-depth", + }; + const RenderDeviceFeatures framebuffer_fetch_features { + .framebuffer_fetch = true, + .explicit_texture_transitions = true, + }; + const RenderDeviceFeatures copy_features { + .texture_copy = true, + }; + const RenderDeviceFeatures blit_features { + .render_target_blit = true, + }; + + const auto simple = plan_paint_feedback(copy_features, render_only_target, false); + PP_EXPECT(h, simple); + if (simple) { + PP_EXPECT(h, simple.value().path == PaintFeedbackPath::none); + PP_EXPECT(h, !simple.value().reads_destination_color); + PP_EXPECT(h, simple.value().target_bytes == 8192U); + } + + const auto fetch = plan_paint_feedback(framebuffer_fetch_features, render_only_target, true); + PP_EXPECT(h, fetch); + if (fetch) { + PP_EXPECT(h, fetch.value().path == PaintFeedbackPath::framebuffer_fetch); + PP_EXPECT(h, fetch.value().reads_destination_color); + PP_EXPECT(h, !fetch.value().requires_auxiliary_texture); + PP_EXPECT(h, !fetch.value().requires_texture_copy); + PP_EXPECT(h, fetch.value().requires_explicit_transition); + } + + const auto ping_pong_copy = plan_paint_feedback(copy_features, render_target, true); + PP_EXPECT(h, ping_pong_copy); + if (ping_pong_copy) { + PP_EXPECT(h, ping_pong_copy.value().path == PaintFeedbackPath::ping_pong_textures); + PP_EXPECT(h, ping_pong_copy.value().requires_auxiliary_texture); + PP_EXPECT(h, ping_pong_copy.value().requires_texture_copy); + PP_EXPECT(h, !ping_pong_copy.value().requires_render_target_blit); + PP_EXPECT(h, ping_pong_copy.value().auxiliary_desc.extent.width == render_target.extent.width); + } + + const auto ping_pong_blit = plan_paint_feedback(blit_features, render_target, true); + PP_EXPECT(h, ping_pong_blit); + if (ping_pong_blit) { + PP_EXPECT(h, ping_pong_blit.value().path == PaintFeedbackPath::ping_pong_textures); + PP_EXPECT(h, !ping_pong_blit.value().requires_texture_copy); + PP_EXPECT(h, ping_pong_blit.value().requires_render_target_blit); + } + + const auto unsupported = plan_paint_feedback(RenderDeviceFeatures {}, render_target, true); + const auto missing_usage = plan_paint_feedback(copy_features, render_only_target, true); + const auto depth = plan_paint_feedback(copy_features, depth_target, true); + + 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, paint_feedback_path_name(PaintFeedbackPath::framebuffer_fetch) == std::string_view("framebuffer_fetch")); + PP_EXPECT(h, paint_feedback_path_name(PaintFeedbackPath::ping_pong_textures) == std::string_view("ping_pong_textures")); + PP_EXPECT(h, paint_feedback_path_name(static_cast(255)) == std::string_view("unknown")); +} + void validates_texture_copy_contract(pp::tests::Harness& h) { const TextureDesc rgba_desc { @@ -2696,6 +2787,7 @@ int main() harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes); harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes); harness.run("validates_blit_contract", validates_blit_contract); + harness.run("plans_paint_feedback_paths", plans_paint_feedback_paths); harness.run("validates_texture_copy_contract", validates_texture_copy_contract); harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_depth_contract", validates_depth_contract); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 8ddb119..3701db6 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -335,6 +335,18 @@ struct PlanBrushStrokeControlArgs { int blend_mode = 0; }; +struct PlanPaintFeedbackArgs { + int width = 64; + int height = 32; + bool complex_blend = true; + 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; @@ -1753,6 +1765,7 @@ void print_help() << " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" << " 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-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" @@ -4809,6 +4822,117 @@ int plan_brush_stroke_control(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_paint_feedback_args( + int argc, + char** argv, + PlanPaintFeedbackArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--width" || key == "--height") { + 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 (value.value() <= 0) { + return pp::foundation::Status::invalid_argument("paint feedback extent must be greater than zero"); + } + if (key == "--width") { + args.width = value.value(); + } else { + args.height = value.value(); + } + } else if (key == "--simple") { + args.complex_blend = false; + } else if (key == "--complex") { + args.complex_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"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_paint_feedback(int argc, char** argv) +{ + PlanPaintFeedbackArgs args; + const auto status = parse_plan_paint_feedback_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-paint-feedback", 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::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 pp::renderer::TextureDesc target { + .extent = pp::renderer::Extent2D { + .width = static_cast(args.width), + .height = static_cast(args.height), + }, + .format = args.depth_target + ? pp::renderer::TextureFormat::depth24_stencil8 + : pp::renderer::TextureFormat::rgba8, + .usage = usage, + .debug_name = "paint-feedback-target", + }; + + const auto plan = pp::renderer::plan_paint_feedback(features, target, args.complex_blend); + if (!plan) { + print_error("plan-paint-feedback", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-paint-feedback\"" + << ",\"state\":{\"width\":" << args.width + << ",\"height\":" << args.height + << ",\"complexBlend\":" << json_bool(args.complex_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::renderer::paint_feedback_path_name(value.path) + << "\",\"targetFormat\":\"" << pp::renderer::texture_format_name(value.target_desc.format) + << "\",\"targetBytes\":" << value.target_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, @@ -7939,6 +8063,10 @@ int main(int argc, char** argv) return plan_brush_stroke_control(argc, argv); } + if (command == "plan-paint-feedback") { + return plan_paint_feedback(argc, argv); + } + if (command == "plan-canvas-tool") { return plan_canvas_tool(argc, argv); }