Add stroke composite feedback planner
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user