Add paint feedback strategy planner

This commit is contained in:
2026-06-03 17:58:24 +02:00
parent dc23a5648d
commit 94a6877e7c
8 changed files with 360 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 | | 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 | | PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter | | Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and GPU golden | | 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 plus GPU parity | | 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 | | Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation ## Layers And Animation

View File

@@ -53,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-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter |
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api` 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 ## Closed Debt

View File

@@ -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 also validates executable command dependencies, including shader-before-uniform
and shader-plus-mesh before draw within each render pass, and rejects invalid and shader-plus-mesh before draw within each render pass, and rejects invalid
texture/sampler bind slots in malformed recorded streams. 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 The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.
@@ -1107,7 +1113,10 @@ Results:
render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/
upload/texture-copy/readback/frame-capture/blit command capture, draw upload/texture-copy/readback/frame-capture/blit command capture, draw
mesh-input capture, explicit draw-range capture, and invalid catalog 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_paint_renderer_compositor_tests` passed.
- `pp_ui_core_color_tests` passed. - `pp_ui_core_color_tests` passed.
- `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_value_tests` passed.

View File

@@ -823,6 +823,65 @@ pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc desti
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
pp::foundation::Result<PaintFeedbackPlan> 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<PaintFeedbackPlan>::failure(target_status);
}
if (!has_texture_usage(target_desc.usage, TextureUsage::render_target)) {
return pp::foundation::Result<PaintFeedbackPlan>::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<PaintFeedbackPlan>::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<PaintFeedbackPlan>::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<PaintFeedbackPlan>::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<PaintFeedbackPlan>::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<PaintFeedbackPlan>::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<PaintFeedbackPlan>::success(plan);
}
const char* texture_format_name(TextureFormat format) noexcept const char* texture_format_name(TextureFormat format) noexcept
{ {
switch (format) { switch (format) {
@@ -887,6 +946,20 @@ const char* blit_filter_name(BlitFilter filter) noexcept
return "unknown"; 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 const char* blend_factor_name(BlendFactor factor) noexcept
{ {
switch (factor) { switch (factor) {

View File

@@ -132,6 +132,12 @@ enum class BlitFilter : std::uint8_t {
linear, linear,
}; };
enum class PaintFeedbackPath : std::uint8_t {
none,
framebuffer_fetch,
ping_pong_textures,
};
enum class BlendFactor : std::uint8_t { enum class BlendFactor : std::uint8_t {
zero, zero,
one, one,
@@ -236,6 +242,19 @@ struct RenderDeviceFeatures {
bool float32_render_targets = false; 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 { class ITexture2D {
public: public:
virtual ~ITexture2D() = default; virtual ~ITexture2D() = default;
@@ -393,10 +412,15 @@ public:
[[nodiscard]] pp::foundation::Status validate_blit_descs( [[nodiscard]] pp::foundation::Status validate_blit_descs(
TextureDesc source, TextureDesc source,
TextureDesc destination) noexcept; TextureDesc destination) noexcept;
[[nodiscard]] pp::foundation::Result<PaintFeedbackPlan> 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_format_name(TextureFormat format) noexcept;
[[nodiscard]] const char* texture_state_name(TextureState state) noexcept; [[nodiscard]] const char* texture_state_name(TextureState state) noexcept;
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; [[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
[[nodiscard]] const char* blit_filter_name(BlitFilter filter) 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_factor_name(BlendFactor factor) noexcept;
[[nodiscard]] const char* blend_op_name(BlendOp op) noexcept; [[nodiscard]] const char* blend_op_name(BlendOp op) noexcept;
[[nodiscard]] const char* compare_op_name(CompareOp op) noexcept; [[nodiscard]] const char* compare_op_name(CompareOp op) noexcept;

View File

@@ -1192,6 +1192,36 @@ if(TARGET pano_cli)
LABELS "app;paint;integration;desktop-fast;fuzz" LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) 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 add_test(NAME pano_cli_plan_canvas_tool_draw_smoke
COMMAND pano_cli plan-canvas-tool --kind draw) COMMAND pano_cli plan-canvas-tool --kind draw)
set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES

View File

@@ -34,6 +34,7 @@ using pp::renderer::IRenderTarget;
using pp::renderer::IRenderTrace; using pp::renderer::IRenderTrace;
using pp::renderer::IShaderProgram; using pp::renderer::IShaderProgram;
using pp::renderer::MeshDesc; using pp::renderer::MeshDesc;
using pp::renderer::PaintFeedbackPath;
using pp::renderer::PrimitiveTopology; using pp::renderer::PrimitiveTopology;
using pp::renderer::ReadbackRegion; using pp::renderer::ReadbackRegion;
using pp::renderer::RecordedRenderCommandKind; using pp::renderer::RecordedRenderCommandKind;
@@ -66,6 +67,8 @@ using pp::renderer::max_texture_dimension;
using pp::renderer::max_texture_slots; using pp::renderer::max_texture_slots;
using pp::renderer::max_trace_label_bytes; using pp::renderer::max_trace_label_bytes;
using pp::renderer::panopainter_shader_catalog; 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::primitive_topology_name;
using pp::renderer::readback_byte_size; using pp::renderer::readback_byte_size;
using pp::renderer::recorded_render_command_kind_name; 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<BlitFilter>(255)) == std::string_view("unknown")); PP_EXPECT(h, blit_filter_name(static_cast<BlitFilter>(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<PaintFeedbackPath>(255)) == std::string_view("unknown"));
}
void validates_texture_copy_contract(pp::tests::Harness& h) void validates_texture_copy_contract(pp::tests::Harness& h)
{ {
const TextureDesc rgba_desc { const TextureDesc rgba_desc {
@@ -2696,6 +2787,7 @@ int main()
harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes); harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes);
harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes); harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes);
harness.run("validates_blit_contract", validates_blit_contract); 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_texture_copy_contract", validates_texture_copy_contract);
harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_blend_contract", validates_blend_contract);
harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_depth_contract", validates_depth_contract);

View File

@@ -335,6 +335,18 @@ struct PlanBrushStrokeControlArgs {
int blend_mode = 0; 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 { struct PlanGridOperationArgs {
std::string kind = "pick"; std::string kind = "pick";
std::string path; 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-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-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-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 --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-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" << " 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; 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<std::uint32_t>(args.width),
.height = static_cast<std::uint32_t>(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( pp::foundation::Status parse_plan_canvas_tool_args(
int argc, int argc,
char** argv, char** argv,
@@ -7939,6 +8063,10 @@ int main(int argc, char** argv)
return plan_brush_stroke_control(argc, argv); return plan_brush_stroke_control(argc, argv);
} }
if (command == "plan-paint-feedback") {
return plan_paint_feedback(argc, argv);
}
if (command == "plan-canvas-tool") { if (command == "plan-canvas-tool") {
return plan_canvas_tool(argc, argv); return plan_canvas_tool(argc, argv);
} }