Add stroke composite feedback planner

This commit is contained in:
2026-06-03 18:07:08 +02:00
parent 94a6877e7c
commit 2ec11e5099
9 changed files with 531 additions and 3 deletions

View File

@@ -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

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-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

View File

@@ -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.

View File

@@ -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<std::size_t> 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<std::size_t>::success(static_cast<std::size_t>(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<StrokeCompositePlan> plan_stroke_composite(
pp::renderer::RenderDeviceFeatures features,
StrokeCompositeRequest request) noexcept
{
if (!is_valid_blend_mode(request.layer_blend_mode)) {
return pp::foundation::Result<StrokeCompositePlan>::failure(
pp::foundation::Status::invalid_argument("unknown layer blend mode"));
}
if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) {
return pp::foundation::Result<StrokeCompositePlan>::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<StrokeCompositePlan>::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<StrokeCompositePlan>::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";
}
}

View File

@@ -4,10 +4,17 @@
#include "paint/blend.h"
#include "renderer_api/renderer_api.h"
#include <cstdint>
#include <span>
namespace pp::paint_renderer {
enum class StrokeCompositePath : std::uint8_t {
fixed_function_blend,
framebuffer_fetch,
ping_pong_textures,
};
struct LayerCompositeView {
std::span<const pp::paint::Rgba> 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<pp::paint::Rgba> 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<StrokeCompositePlan> plan_stroke_composite(
pp::renderer::RenderDeviceFeatures features,
StrokeCompositeRequest request) noexcept;
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
}

View File

@@ -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

View File

@@ -2,14 +2,24 @@
#include "test_harness.h"
#include <cmath>
#include <string_view>
#include <vector>
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<BlendMode>(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<StrokeBlendMode>(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<StrokeCompositePath>(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();
}

View File

@@ -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)

View File

@@ -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<std::uint32_t>(args.width),
.height = static_cast<std::uint32_t>(args.height),
},
.target_format = args.depth_target
? pp::renderer::TextureFormat::depth24_stencil8
: pp::renderer::TextureFormat::rgba8,
.target_usage = usage,
.layer_blend_mode = static_cast<pp::paint::BlendMode>(args.layer_blend_mode),
.stroke_blend_mode = static_cast<pp::paint::StrokeBlendMode>(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);
}