Compare commits

..

9 Commits

25 changed files with 3378 additions and 207 deletions

View File

@@ -355,6 +355,7 @@ if(PP_BUILD_APP)
pp_assets
pp_document
pp_paint
pp_paint_renderer
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
@@ -392,6 +393,7 @@ if(PP_BUILD_APP)
pp_assets
pp_document
pp_paint
pp_paint_renderer
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)

View File

@@ -1,7 +1,7 @@
# PanoPainter Capability Map
Status: live
Last updated: 2026-06-02
Last updated: 2026-06-03
This map is the preservation checklist for the modernization. When a component
is extracted, update the relevant rows with the owning component, test label,
@@ -32,12 +32,12 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Brush settings serialization | `Brush`, `Serializer` | `pp_paint`, `pp_assets` | Round-trip and boundary values |
| Brush settings serialization and stroke-panel controls | `Brush`, `Serializer`, `NodePanelStroke` | `pp_paint`, `pp_assets`, `pp_app_core`, `pp_panopainter_ui` | Round-trip and boundary values; stroke slider/toggle/blend/reset planning and invalid setting tests |
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and GPU golden |
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors plus GPU parity |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, 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, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke-feedback destination-copy coverage, 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

@@ -40,7 +40,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing |
| DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, and the `BrushUiServices` boundary, but the live adapter still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings execution is owned by injected brush/app/asset/UI services with no legacy brush adapter |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, and `BrushStrokeControlServices`, but the live adapter still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, and refreshes legacy quick/stroke/color widgets | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter |
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter |
| DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter |
| DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter |
@@ -53,6 +53,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter |
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `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. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current stroke shader feedback decision, and live `Canvas::stroke_draw` uses it for main-brush, dual-brush, and stroke-pad destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, and thumbnail layer blending still use legacy OpenGL canvas execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_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`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | 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

@@ -511,6 +511,15 @@ changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
settings refreshes used by the live brush, quick, color, and floating panel
callbacks. Brush UI execution now dispatches through `BrushUiServices` before
the legacy `Brush`/panel adapter mutates brush state or loads brush resources.
`pano_cli plan-brush-texture-list` exposes app-core planning for brush/pattern
texture add, remove, and reorder actions, and `NodePanelBrush` now dispatches
those actions through `BrushTextureListServices` before the legacy image
load/save and UI-list adapter continues.
`pano_cli plan-brush-stroke-control` exposes app-core planning for the live
stroke panel's slider, checkbox, blend-mode, tip-aspect reset, and default
brush reset commands. `NodePanelStroke` now dispatches those controls through
`BrushStrokeControlServices` before the legacy `Canvas::I`/`Brush`/stroke-panel
adapter continues.
`pano_cli plan-canvas-tool` exposes app-core planning for draw/erase/line,
camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar
commands. Canvas tool execution now dispatches through `CanvasToolServices`
@@ -853,6 +862,38 @@ nested passes, texture I/O or blits inside a pass, and unclosed passes. It
also validates executable command dependencies, including shader-before-uniform
and shader-plus-mesh before draw within each render pass, and rejects invalid
texture/sampler bind slots in malformed recorded streams.
The renderer-neutral API now also plans complex paint feedback strategies for
future stroke/layer compositing work: framebuffer-fetch-capable backends can
read destination color directly, while other backends must use ping-pong render
targets backed by texture copy or render-target blit support. This is exposed
through `pano_cli plan-paint-feedback` and tracked by DEBT-0036 until the live
paint renderer consumes the plan.
`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::draw_merge` now uses this planner for its existing
shader-blend gate for layer and primary-brush blend modes while preserving the
legacy trigger policy; actual canvas stroke execution, dual-brush feedback, and
pattern feedback are still legacy OpenGL and remain tracked by DEBT-0036 until
the app calls through renderer services for the whole compositing path.
`pp_paint_renderer::plan_canvas_blend_gate` now also owns the compatibility
mapping from persisted layer and brush blend indices to that planner, including
fallback behavior for unknown nonzero indices. Both `Canvas::draw_merge` and
`NodeCanvas` panorama rendering consume that shared gate, so the live app no
longer has duplicate local blend-trigger logic or duplicate destination-copy
versus framebuffer-fetch decisions in those paths.
The OpenGL shader initialization path now stores a renderer-neutral
`RenderDeviceFeatures` snapshot converted by `pp_renderer_gl`, and those live
canvas gates consume that snapshot instead of rebuilding feature flags from
individual `ShaderManager` extension booleans.
`pp_paint_renderer::plan_canvas_stroke_feedback` now models the current stroke
shader's required destination feedback without changing the legacy shader math.
Live `Canvas::stroke_draw` consumes that plan for main-brush, dual-brush, and
stroke-pad destination-copy versus framebuffer-fetch decisions; thumbnail layer
blending is the remaining direct canvas feedback branch before a fuller live
paint-renderer execution boundary can take over.
The existing renderer classes are not yet fully
behind the renderer interfaces.
@@ -1098,8 +1139,15 @@ Results:
render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/
upload/texture-copy/readback/frame-capture/blit command capture, draw
mesh-input capture, explicit draw-range capture, and invalid catalog
rejection.
rejection. The same suite now covers complex paint feedback planning for
framebuffer-fetch backends, ping-pong texture-copy/blit fallbacks, simple
no-feedback blends, invalid render-target usage, unsupported backends, and
depth-target rejection.
- `pp_paint_renderer_compositor_tests` passed.
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.
@@ -1226,15 +1274,29 @@ Results:
live animation-panel planning as JSON automation.
- `pp_app_core_brush_ui_tests` passed, covering brush color channel validation,
invalid color rejection, texture-path validation, preset-brush availability,
preserve-current-color intent, stroke-settings refresh intent, service
dispatch ordering, texture/preset execution payloads, and invalid execution
payload rejection.
preserve-current-color intent, stroke-settings refresh intent, texture-list
add target path planning, user-texture removal intent, clamped reorder intent,
stroke-control slider/toggle/blend/reset planning, service dispatch ordering,
texture/preset/list/stroke-control execution payloads, execution failure
preservation, and invalid execution payload rejection.
- `pano_cli_plan_brush_operation_color_smoke`,
`pano_cli_plan_brush_operation_texture_smoke`,
`pano_cli_plan_brush_operation_preset_smoke`,
`pano_cli_plan_brush_operation_rejects_bad_color`, and
`pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live
brush/color/preset UI planning as JSON automation.
- `pano_cli_plan_brush_texture_list_add_smoke`,
`pano_cli_plan_brush_texture_list_remove_user_smoke`,
`pano_cli_plan_brush_texture_list_move_edge_smoke`, and
`pano_cli_plan_brush_texture_list_rejects_bad_source` passed and expose live
brush/pattern texture-list planning as JSON automation.
- `pano_cli_plan_brush_stroke_control_float_smoke`,
`pano_cli_plan_brush_stroke_control_toggle_smoke`,
`pano_cli_plan_brush_stroke_control_blend_smoke`,
`pano_cli_plan_brush_stroke_control_reset_smoke`,
`pano_cli_plan_brush_stroke_control_rejects_bad_setting`, and
`pano_cli_plan_brush_stroke_control_rejects_bad_blend` passed and expose live
stroke-panel slider/toggle/blend/reset planning as JSON automation.
- `pp_app_core_grid_ui_tests` passed, covering heightmap pick/load/reload/clear
planning, lightmap capability and limit checks, missing-heightmap no-op
behavior, and commit canvas gating.
@@ -1552,6 +1614,25 @@ Results:
- Canvas layer merge rendering and explicit layer-merge compositing now route
depth/blend state, active texture units, fallback 2D texture unbinds, and
merge framebuffer copy targets through the renderer GL backend mapping.
- Canvas draw-merge shader-blend selection now consumes the extracted
`pp_paint_renderer` stroke composite planner for current layer and primary
brush blend modes, while preserving legacy OpenGL compositing execution under
DEBT-0036.
- `NodeCanvas` panorama rendering now consumes the same tested
`pp_paint_renderer` canvas blend-gate planner as `Canvas::draw_merge`, so
layer and primary-brush blend-trigger compatibility is centralized.
- Shader initialization now publishes the OpenGL backend's renderer-neutral
feature snapshot through the legacy shader manager, and live canvas blend
gates consume that `RenderDeviceFeatures` value instead of hand-built
framebuffer-fetch/texture-copy flags.
- Canvas draw-merge and `NodeCanvas` panorama shader-blend paths now use the
shared canvas blend-gate plan to decide whether they can read destination
color through framebuffer fetch or must copy the destination texture before
the legacy OpenGL blend draw.
- Canvas main-brush, dual-brush, and stroke-pad draw paths now use the tested
`pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch
supplies destination color or the legacy OpenGL path must copy the target
texture before drawing.
- Canvas equirectangular import drawing and depth export rendering now route
depth/blend state and active texture units through the renderer GL backend
mapping.

View File

@@ -2,6 +2,7 @@
#include "foundation/result.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <string_view>
@@ -21,6 +22,89 @@ enum class BrushUiOperation {
stroke_settings_changed,
};
enum class BrushTextureListOperation {
add_texture,
remove_texture,
move_texture,
};
enum class BrushStrokeControlOperation {
set_float,
set_bool,
set_blend_mode,
reset_tip_aspect,
reset_default_brush,
};
enum class BrushStrokeFloatSetting {
tip_size,
tip_spacing,
tip_flow,
tip_opacity,
tip_angle,
tip_angle_smooth,
tip_mix,
tip_wet,
tip_noise,
tip_hue,
tip_saturation,
tip_value,
jitter_scale,
jitter_angle,
jitter_scatter,
jitter_flow,
jitter_opacity,
jitter_hue,
jitter_saturation,
jitter_value,
jitter_aspect,
dual_size,
dual_spacing,
dual_scatter,
tip_aspect,
dual_opacity,
dual_flow,
dual_rotate,
pattern_scale,
pattern_brightness,
pattern_contrast,
pattern_depth,
};
enum class BrushStrokeBoolSetting {
tip_angle_init,
tip_angle_follow,
tip_flow_pressure,
tip_opacity_pressure,
tip_size_pressure,
jitter_scatter_both_axis,
jitter_aspect_both_axis,
jitter_hsv_each_sample,
tip_invert,
tip_flip_x,
tip_flip_y,
pattern_enabled,
dual_enabled,
dual_scatter_both_axis,
dual_invert,
dual_flip_x,
dual_flip_y,
dual_random_flip,
tip_random_flip_x,
tip_random_flip_y,
pattern_each_sample,
pattern_invert,
pattern_flip_x,
pattern_flip_y,
pattern_random_offset,
};
enum class BrushStrokeBlendSetting {
tip,
dual,
pattern,
};
struct BrushUiPlan {
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
@@ -37,6 +121,38 @@ struct BrushUiPlan {
bool update_brush_ui = false;
};
struct BrushTextureListPlan {
BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
int item_count = 0;
int current_index = -1;
int target_index = -1;
int move_offset = 0;
std::string source_path;
std::string high_path;
std::string thumbnail_path;
std::string brush_name;
bool user_texture = false;
bool deletes_texture_files = false;
bool saves_list = false;
bool notifies_selection = false;
bool converts_brush_alpha = false;
bool no_op = false;
};
struct BrushStrokeControlPlan {
BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float;
BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size;
BrushStrokeBoolSetting bool_setting = BrushStrokeBoolSetting::tip_angle_init;
BrushStrokeBlendSetting blend_setting = BrushStrokeBlendSetting::tip;
float float_value = 0.0F;
bool bool_value = false;
int blend_mode = 0;
bool mutates_brush = false;
bool updates_controls = false;
bool refreshes_preview = false;
bool notifies_stroke_change = false;
};
class BrushUiServices {
public:
virtual ~BrushUiServices() = default;
@@ -47,6 +163,55 @@ public:
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
};
class BrushTextureListServices {
public:
virtual ~BrushTextureListServices() = default;
virtual pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) = 0;
virtual void remove_texture(int index, bool delete_texture_files) = 0;
virtual void move_texture(int from_index, int to_index) = 0;
virtual void select_texture(int index) = 0;
virtual void save_texture_list() = 0;
};
class BrushStrokeControlServices {
public:
virtual ~BrushStrokeControlServices() = default;
virtual void set_float_setting(BrushStrokeFloatSetting setting, float value) = 0;
virtual void set_bool_setting(BrushStrokeBoolSetting setting, bool value) = 0;
virtual void set_blend_mode(BrushStrokeBlendSetting setting, int blend_mode) = 0;
virtual void reset_tip_aspect(float value) = 0;
virtual void reset_default_brush() = 0;
virtual void update_stroke_controls() = 0;
virtual void refresh_stroke_preview() = 0;
virtual void notify_stroke_changed() = 0;
};
[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
std::string_view source_path) noexcept
{
const auto slash = source_path.find_last_of("/\\");
const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
if (name_begin >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
}
const auto dot = source_path.find_last_of('.');
if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
}
return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
}
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
{
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
@@ -56,6 +221,24 @@ public:
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept
{
if (!std::isfinite(value)) {
return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept
{
if (blend_mode < 0 || blend_mode > 63) {
return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
float r,
float g,
@@ -129,6 +312,168 @@ public:
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
BrushStrokeFloatSetting setting,
float value)
{
const auto status = validate_brush_stroke_float(value);
if (!status.ok()) {
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
}
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::set_float;
plan.float_setting = setting;
plan.float_value = value;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
}
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting(
BrushStrokeBoolSetting setting,
bool value) noexcept
{
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::set_bool;
plan.bool_setting = setting;
plan.bool_value = value;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_blend_mode(
BrushStrokeBlendSetting setting,
int blend_mode)
{
const auto status = validate_brush_stroke_blend_mode(blend_mode);
if (!status.ok()) {
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
}
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::set_blend_mode;
plan.blend_setting = setting;
plan.blend_mode = blend_mode;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
}
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept
{
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::reset_tip_aspect;
plan.float_setting = BrushStrokeFloatSetting::tip_aspect;
plan.float_value = value;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return plan;
}
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept
{
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::reset_default_brush;
plan.mutates_brush = true;
plan.updates_controls = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
std::string_view directory_name,
std::string_view data_path,
std::string_view source_path)
{
if (directory_name.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
}
if (data_path.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
}
const auto stem = brush_texture_source_stem(source_path);
if (!stem) {
return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::add_texture;
plan.source_path = std::string(source_path);
plan.brush_name = std::string(stem.value());
plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
+ plan.brush_name + ".png";
plan.user_texture = true;
plan.saves_list = true;
plan.converts_brush_alpha = directory_name == "brushes";
return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
int item_count,
int current_index,
bool current_is_user_texture)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::remove_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
plan.user_texture = current_is_user_texture;
plan.deletes_texture_files = current_is_user_texture;
plan.saves_list = true;
plan.notifies_selection = plan.target_index >= 0;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
int item_count,
int current_index,
int offset)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
if (offset == 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::move_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
plan.move_offset = offset;
plan.saves_list = true;
plan.no_op = plan.target_index == current_index;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
const BrushUiPlan& plan,
BrushUiServices& services)
@@ -168,4 +513,116 @@ public:
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
}
[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan(
const BrushStrokeControlPlan& plan,
BrushStrokeControlServices& services)
{
switch (plan.operation) {
case BrushStrokeControlOperation::set_float:
{
const auto status = validate_brush_stroke_float(plan.float_value);
if (!status.ok()) {
return status;
}
services.set_float_setting(plan.float_setting, plan.float_value);
break;
}
case BrushStrokeControlOperation::set_bool:
services.set_bool_setting(plan.bool_setting, plan.bool_value);
break;
case BrushStrokeControlOperation::set_blend_mode:
{
const auto status = validate_brush_stroke_blend_mode(plan.blend_mode);
if (!status.ok()) {
return status;
}
services.set_blend_mode(plan.blend_setting, plan.blend_mode);
break;
}
case BrushStrokeControlOperation::reset_tip_aspect:
{
const auto status = validate_brush_stroke_float(plan.float_value);
if (!status.ok()) {
return status;
}
services.reset_tip_aspect(plan.float_value);
break;
}
case BrushStrokeControlOperation::reset_default_brush:
services.reset_default_brush();
break;
}
if (plan.updates_controls) {
services.update_stroke_controls();
}
if (plan.refreshes_preview) {
services.refresh_stroke_preview();
}
if (plan.notifies_stroke_change) {
services.notify_stroke_changed();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
const BrushTextureListPlan& plan,
BrushTextureListServices& services)
{
switch (plan.operation) {
case BrushTextureListOperation::add_texture:
{
if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
|| plan.brush_name.empty()) {
return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
}
const auto add_status = services.add_texture_from_source(
plan.source_path,
plan.high_path,
plan.thumbnail_path,
plan.brush_name,
plan.converts_brush_alpha);
if (!add_status.ok()) {
return add_status;
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
case BrushTextureListOperation::remove_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
}
services.remove_texture(plan.current_index, plan.deletes_texture_files);
if (plan.notifies_selection && plan.target_index >= 0) {
services.select_texture(plan.target_index);
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
case BrushTextureListOperation::move_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
}
services.move_texture(plan.current_index, plan.target_index);
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
}
} // namespace pp::app

View File

@@ -16,6 +16,17 @@ namespace {
return static_cast<GLenum>(pp::renderer::gl::extension_string_name());
}
[[nodiscard]] pp::renderer::gl::OpenGlCapabilities shader_manager_capabilities() noexcept
{
pp::renderer::gl::OpenGlCapabilities capabilities;
capabilities.framebuffer_fetch = ShaderManager::ext_framebuffer_fetch;
capabilities.map_buffer_alignment = ShaderManager::ext_map_aligned;
capabilities.float32_textures = ShaderManager::ext_float32;
capabilities.float32_linear = ShaderManager::ext_float32_linear;
capabilities.float16_textures = ShaderManager::ext_float16;
return capabilities;
}
}
void App::initShaders()
@@ -55,6 +66,7 @@ void App::initShaders()
ShaderManager::ext_float32 = capabilities.float32_textures;
ShaderManager::ext_float32_linear = capabilities.float32_linear;
ShaderManager::ext_float16 = capabilities.float16_textures;
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
});
#if __GL__
@@ -63,6 +75,8 @@ void App::initShaders()
ShaderManager::ext_float32 = true;
ShaderManager::ext_float16 = true;
#endif
ShaderManager::set_render_device_features(
pp::renderer::gl::render_device_features(shader_manager_capabilities()));
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");

View File

@@ -4,6 +4,7 @@
#include "app.h"
#include "texture.h"
#include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include <thread>
#include <algorithm>
@@ -43,6 +44,69 @@ GLenum rgba_pixel_format()
return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format());
}
pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept
{
return ShaderManager::render_device_features();
}
pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_stroke_feedback_plan(
int width,
int height) noexcept
{
const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback(
canvas_stroke_composite_features(),
pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasStrokeFeedbackPlan fallback;
fallback.compatibility_fallback = true;
fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures;
fallback.requires_auxiliary_texture = true;
return fallback;
}
pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
int width,
int height,
const std::vector<std::shared_ptr<Layer>>& layers,
const Brush* brush) noexcept
{
std::vector<int> layer_blend_modes;
layer_blend_modes.reserve(layers.size());
for (const auto& layer : layers) {
if (!layer) {
continue;
}
layer_blend_modes.push_back(layer->m_blend_mode);
}
const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
canvas_stroke_composite_features(),
pp::paint_renderer::CanvasBlendGateRequest {
.extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
},
.layer_blend_modes = layer_blend_modes,
.has_stroke_blend_mode = brush != nullptr,
.stroke_blend_mode = brush ? brush->m_blend_mode : 0,
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasBlendGatePlan fallback;
fallback.shader_blend = true;
fallback.complex_blend = true;
fallback.compatibility_fallback = true;
return fallback;
}
GLenum unsigned_byte_component_type()
{
return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type());
@@ -421,9 +485,12 @@ std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(std::array<vert
return ret;
}
glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
glm::vec4 Canvas::stroke_draw_samples(
int i,
std::vector<vertex_t>& P,
bool copy_stroke_destination)
{
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
{
set_active_texture_unit(1);
m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
@@ -441,7 +508,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
glm::vec2 pad(1);
glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height });
glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos));
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
{
glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y,
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
@@ -469,7 +536,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
}
m_brush_shape.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
{
set_active_texture_unit(1);
m_tex[i].unbind();
@@ -587,10 +654,13 @@ void Canvas::stroke_draw()
if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
if (brush->m_pattern_flipy) patt_scale.y *= -1.f;
const auto stroke_feedback = canvas_stroke_feedback_plan(m_width, m_height);
const bool copy_stroke_destination = !stroke_feedback.reads_destination_color;
glDisable(blend_state());
ShaderManager::use(kShader::Stroke);
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg
ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern
ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer
@@ -648,7 +718,7 @@ void Canvas::stroke_draw()
ShaderManager::u_vec4(kShaderUniform::Col, f.col);
ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
auto box_sample = stroke_draw_samples(i, P);
auto box_sample = stroke_draw_samples(i, P, copy_stroke_destination);
m_tmp[i].unbindFramebuffer();
@@ -675,7 +745,7 @@ void Canvas::stroke_draw()
// work on documents that doesn't have the padding, so on document loading.
ShaderManager::use(kShader::StrokePad);
ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
{
set_active_texture_unit(1);
ShaderManager::u_int(kShaderUniform::TexBG, 1);
@@ -705,7 +775,7 @@ void Canvas::stroke_draw()
m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());
m_tmp[i].bindFramebuffer();
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
{
glm::vec2 o = glm::max({0, 0}, xy(b) - pad);
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
@@ -716,7 +786,7 @@ void Canvas::stroke_draw()
m_brush_shape.draw_fill();
m_tmp[i].unbindFramebuffer();
}
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_stroke_destination)
{
unbind_texture_2d();
}
@@ -747,7 +817,7 @@ void Canvas::stroke_draw()
if (P.size() < 3)
continue;
m_tmp_dual[i].bindFramebuffer();
auto box_sample = stroke_draw_samples(i, P);
auto box_sample = stroke_draw_samples(i, P, copy_stroke_destination);
m_tmp_dual[i].unbindFramebuffer();
// this mode overflows the main brush boundries
@@ -1086,14 +1156,13 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f);
const auto& b = m_current_stroke->m_brush;
// check if any layer use blend, otherwise draw directly on main framebuffer
bool use_blend = false;
for (auto& l : m_layers)
{
use_blend |= l->m_blend_mode != 0;
}
if (Canvas::I->m_current_stroke)
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
const auto blend_gate = draw_merge_blend_gate_plan(
m_width,
m_height,
m_layers,
m_current_stroke ? m_current_stroke->m_brush.get() : nullptr);
const bool use_blend = blend_gate.shader_blend;
const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;
// if not using shader blend, use gl rasterizer blend
glDisable(depth_test_state());
@@ -1257,7 +1326,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_blend_destination)
{
m_sampler.bind(2);
ShaderManager::u_int(kShaderUniform::TexBG, 2);
@@ -1265,7 +1334,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
set_active_texture_unit(0);
m_merge_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_merge_tex.bind();
@@ -1274,7 +1343,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
m_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_merge_tex.unbind();

View File

@@ -205,7 +205,7 @@ public:
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const;
// return rect {origin, size}
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P);
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P, bool copy_stroke_destination);
std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
void stroke_draw();
void stroke_end();

View File

@@ -1,6 +1,9 @@
#include "pch.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <vector>
#include "app_core/canvas_tool_ui.h"
#include "app_core/history_ui.h"
@@ -8,6 +11,7 @@
#include "log.h"
#include "node_canvas.h"
#include "node_image_texture.h"
#include "paint_renderer/compositor.h"
#include "settings.h"
#include "renderer_gl/opengl_capabilities.h"
@@ -23,6 +27,48 @@ void unbind_texture_2d()
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
}
pp::renderer::RenderDeviceFeatures node_canvas_stroke_composite_features() noexcept
{
return ShaderManager::render_device_features();
}
pp::paint_renderer::CanvasBlendGatePlan node_canvas_blend_gate_plan(
int width,
int height,
const std::vector<std::shared_ptr<Layer>>& layers,
const Brush* brush) noexcept
{
std::vector<int> layer_blend_modes;
layer_blend_modes.reserve(layers.size());
for (const auto& layer : layers) {
if (!layer) {
continue;
}
layer_blend_modes.push_back(layer->m_blend_mode);
}
const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
node_canvas_stroke_composite_features(),
pp::paint_renderer::CanvasBlendGateRequest {
.extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
},
.layer_blend_modes = layer_blend_modes,
.has_stroke_blend_mode = brush != nullptr,
.stroke_blend_mode = brush ? brush->m_blend_mode : 0,
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasBlendGatePlan fallback;
fallback.shader_blend = true;
fallback.complex_blend = true;
fallback.compatibility_fallback = true;
return fallback;
}
void run_history_undo_if_available()
{
const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
@@ -252,14 +298,13 @@ void NodeCanvas::draw()
}
else
{
// check if any layer use blend, otherwise draw directly on main framebuffer
bool use_blend = false;
for (size_t i = 0; i < m_canvas->m_layers.size(); i++)
{
use_blend |= m_canvas->m_layers[i]->m_blend_mode != 0;
}
if (Canvas::I->m_current_stroke)
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
const auto blend_gate = node_canvas_blend_gate_plan(
m_cache_rtt.getWidth(),
m_cache_rtt.getHeight(),
m_canvas->m_layers,
m_canvas->m_current_stroke ? m_canvas->m_current_stroke->m_brush.get() : nullptr);
const bool use_blend = blend_gate.shader_blend;
const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;
if (use_blend)
{
@@ -450,7 +495,7 @@ void NodeCanvas::draw()
ShaderManager::use(kShader::TextureBlend);
ShaderManager::u_int(kShaderUniform::Tex, 0);
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_blend_destination)
ShaderManager::u_int(kShaderUniform::TexBG, 2);
ShaderManager::u_int(kShaderUniform::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
@@ -458,7 +503,7 @@ void NodeCanvas::draw()
set_active_texture_unit(0);
m_blender_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_blender_bg.bind();
@@ -468,7 +513,7 @@ void NodeCanvas::draw()
m_face_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch)
if (copy_blend_destination)
{
set_active_texture_unit(2);
m_blender_bg.unbind();

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_panel_brush.h"
#include "app_core/brush_ui.h"
#include "asset.h"
#include "texture.h"
@@ -75,6 +76,116 @@ Node* NodePanelBrush::clone_instantiate() const
return new NodePanelBrush();
}
void NodePanelBrush::execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan)
{
class LegacyBrushTextureListServices final : public pp::app::BrushTextureListServices {
public:
explicit LegacyBrushTextureListServices(NodePanelBrush& panel) noexcept
: panel_(panel)
{
}
pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) override
{
Image img;
if (!img.load_file(std::string(source_path))) {
return pp::foundation::Status::invalid_argument("brush texture source could not be loaded");
}
if (converts_brush_alpha) {
img.gayscale_alpha();
}
auto thumbnail_image = img.resize(64, 64).resize_squared(glm::u8vec4(255));
thumbnail_image.save_png(std::string(thumbnail_path));
img.save_png(std::string(high_path));
NodeButtonBrush* brush = new NodeButtonBrush;
panel_.m_container->add_child(brush);
brush->init();
brush->create();
brush->loaded();
const auto thumbnail_path_string = std::string(thumbnail_path);
brush->set_icon(thumbnail_path_string.c_str());
brush->thumb_path = std::string(thumbnail_path);
brush->high_path = std::string(high_path);
brush->brush_name = std::string(brush_name);
brush->m_user_brush = true;
brush->on_click = std::bind(&NodePanelBrush::handle_click, &panel_, std::placeholders::_1);
return pp::foundation::Status::success();
}
void remove_texture(int index, bool delete_texture_files) override
{
auto* brush = brush_at(index);
if (!brush) {
return;
}
if (delete_texture_files) {
Asset::delete_file(brush->thumb_path);
Asset::delete_file(brush->high_path);
}
if (panel_.m_current == brush) {
panel_.m_current = nullptr;
}
panel_.m_container->remove_child(brush);
}
void move_texture(int from_index, int to_index) override
{
if (auto* brush = brush_at(from_index)) {
panel_.m_container->move_child(brush, to_index);
}
}
void select_texture(int index) override
{
if (panel_.m_current) {
panel_.m_current->m_selected = false;
}
panel_.m_current = brush_at(index);
if (!panel_.m_current) {
return;
}
panel_.m_current->m_selected = true;
if (panel_.on_brush_changed) {
panel_.on_brush_changed(&panel_, index);
}
}
void save_texture_list() override
{
panel_.save();
}
private:
NodeButtonBrush* brush_at(int index) const
{
if (index < 0 || index >= static_cast<int>(panel_.m_container->m_children.size())) {
return nullptr;
}
return static_cast<NodeButtonBrush*>(panel_.m_container->m_children[index].get());
}
NodePanelBrush& panel_;
};
LegacyBrushTextureListServices services(*this);
const auto status = pp::app::execute_brush_texture_list_plan(plan, services);
if (!status.ok()) {
LOG("Brush texture list action failed: %s", status.message);
}
}
void NodePanelBrush::init()
{
init_template_file("data/dialogs/panel-brushes.xml", "tpl-panel-brushes");
@@ -82,41 +193,9 @@ void NodePanelBrush::init()
m_btn_add = find<NodeButtonCustom>("btn-add");
m_btn_add->on_click = [this](Node*) {
App::I->pick_file({ "JPG", "PNG" }, [this](std::string path) {
std::string name, base, ext;
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
std::smatch m;
if (!std::regex_search(path, m, r))
return;
base = m[1].str();
name = m[2].str();
ext = m[3].str();
Image img;
if (!m_dir_name.empty() && img.load_file(path))
{
std::string path_high = App::I->data_path + "/" + m_dir_name + "/" + name + ".png";
std::string path_thumb = App::I->data_path + "/" + m_dir_name + "/thumbs/" + name + ".png";
//img = img.resize_squared(glm::u8vec4(255));
if (m_dir_name == "brushes")
img.gayscale_alpha();
auto thumb = img.resize(64, 64).resize_squared(glm::u8vec4(255));
thumb.save_png(path_thumb);
//auto po2 = img.resize_power2();
img.save_png(path_high);
NodeButtonBrush* brush = new NodeButtonBrush;
m_container->add_child(brush);
brush->init();
brush->create();
brush->loaded();
brush->set_icon(path_thumb.c_str());
brush->thumb_path = path_thumb;
brush->high_path = path_high;
brush->brush_name = name;
brush->m_user_brush = true;
brush->on_click = std::bind(&NodePanelBrush::handle_click, this, std::placeholders::_1);
save();
const auto plan = pp::app::plan_brush_texture_list_add(m_dir_name, App::I->data_path, path);
if (plan) {
execute_texture_list_plan(plan.value());
}
});
};
@@ -126,26 +205,13 @@ void NodePanelBrush::init()
if (m_current)
{
int idx = m_container->get_child_index(m_current);
if (m_current->m_user_brush)
{
// only delete user brushes
Asset::delete_file(m_current->thumb_path);
Asset::delete_file(m_current->high_path);
const auto plan = pp::app::plan_brush_texture_list_remove(
static_cast<int>(m_container->m_children.size()),
idx,
m_current->m_user_brush);
if (plan) {
execute_texture_list_plan(plan.value());
}
m_container->remove_child(m_current);
if (m_container->m_children.size() > 0)
{
idx = std::max(0, std::min(idx, (int)m_container->m_children.size() - 1));
m_current = (NodeButtonBrush*)m_container->m_children[idx].get();
m_current->m_selected = true;
if (on_brush_changed)
on_brush_changed(this, idx);
}
else
{
m_current = nullptr;
}
save();
}
};
@@ -154,9 +220,13 @@ void NodePanelBrush::init()
if (m_current)
{
int idx = m_container->get_child_index(m_current);
idx = std::max(0, std::min(idx - 1, (int)m_container->m_children.size() - 1));
m_container->move_child(m_current, idx);
save();
const auto plan = pp::app::plan_brush_texture_list_move(
static_cast<int>(m_container->m_children.size()),
idx,
-1);
if (plan) {
execute_texture_list_plan(plan.value());
}
}
};
@@ -165,9 +235,13 @@ void NodePanelBrush::init()
if (m_current)
{
int idx = m_container->get_child_index(m_current);
idx = std::max(0, std::min(idx + 1, (int)m_container->m_children.size() - 1));
m_container->move_child(m_current, idx);
save();
const auto plan = pp::app::plan_brush_texture_list_move(
static_cast<int>(m_container->m_children.size()),
idx,
1);
if (plan) {
execute_texture_list_plan(plan.value());
}
}
};

View File

@@ -9,6 +9,10 @@
#include "serializer.h"
#include "node_button.h"
namespace pp::app {
struct BrushTextureListPlan;
}
class NodeButtonBrush : public NodeButtonCustom, public Serializer::Type
{
public:
@@ -38,6 +42,7 @@ class NodePanelBrush : public Node
NodeButtonCustom* m_btn_down;
NodeButtonCustom* m_btn_remove;
bool m_interacted = false;
void execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan);
public:
NodeScroll* m_container;
std::string m_dir_name;

View File

@@ -1,4 +1,5 @@
#include "pch.h"
#include "app_core/brush_ui.h"
#include "log.h"
#include "node_panel_stroke.h"
#include "canvas.h"
@@ -6,6 +7,137 @@
#include "app.h"
#include "abr.h"
namespace {
class LegacyBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices {
public:
explicit LegacyBrushStrokeControlServices(NodePanelStroke& panel) : panel_(panel) {}
void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override
{
auto& brush = *Canvas::I->m_current_brush;
switch (setting) {
case pp::app::BrushStrokeFloatSetting::tip_size: brush.m_tip_size = value; break;
case pp::app::BrushStrokeFloatSetting::tip_spacing: brush.m_tip_spacing = value; break;
case pp::app::BrushStrokeFloatSetting::tip_flow: brush.m_tip_flow = value; break;
case pp::app::BrushStrokeFloatSetting::tip_opacity: brush.m_tip_opacity = value; break;
case pp::app::BrushStrokeFloatSetting::tip_angle: brush.m_tip_angle = value; break;
case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: brush.m_tip_angle_smooth = value; break;
case pp::app::BrushStrokeFloatSetting::tip_mix: brush.m_tip_mix = value; break;
case pp::app::BrushStrokeFloatSetting::tip_wet: brush.m_tip_wet = value; break;
case pp::app::BrushStrokeFloatSetting::tip_noise: brush.m_tip_noise = value; break;
case pp::app::BrushStrokeFloatSetting::tip_hue: brush.m_tip_hue = value; break;
case pp::app::BrushStrokeFloatSetting::tip_saturation: brush.m_tip_sat = value; break;
case pp::app::BrushStrokeFloatSetting::tip_value: brush.m_tip_val = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_scale: brush.m_jitter_scale = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_angle: brush.m_jitter_angle = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_scatter: brush.m_jitter_scatter = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_flow: brush.m_jitter_flow = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_opacity: brush.m_jitter_opacity = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_hue: brush.m_jitter_hue = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_saturation: brush.m_jitter_sat = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_value: brush.m_jitter_val = value; break;
case pp::app::BrushStrokeFloatSetting::jitter_aspect: brush.m_jitter_aspect = value; break;
case pp::app::BrushStrokeFloatSetting::dual_size: brush.m_dual_size = value; break;
case pp::app::BrushStrokeFloatSetting::dual_spacing: brush.m_dual_spacing = value; break;
case pp::app::BrushStrokeFloatSetting::dual_scatter: brush.m_dual_scatter = value; break;
case pp::app::BrushStrokeFloatSetting::tip_aspect: brush.m_tip_aspect = value; break;
case pp::app::BrushStrokeFloatSetting::dual_opacity: brush.m_dual_opacity = value; break;
case pp::app::BrushStrokeFloatSetting::dual_flow: brush.m_dual_flow = value; break;
case pp::app::BrushStrokeFloatSetting::dual_rotate: brush.m_dual_rotate = value; break;
case pp::app::BrushStrokeFloatSetting::pattern_scale: brush.m_pattern_scale = value; break;
case pp::app::BrushStrokeFloatSetting::pattern_brightness: brush.m_pattern_brightness = value; break;
case pp::app::BrushStrokeFloatSetting::pattern_contrast: brush.m_pattern_contrast = value; break;
case pp::app::BrushStrokeFloatSetting::pattern_depth: brush.m_pattern_depth = value; break;
}
}
void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override
{
auto& brush = *Canvas::I->m_current_brush;
switch (setting) {
case pp::app::BrushStrokeBoolSetting::tip_angle_init: brush.m_tip_angle_init = value; break;
case pp::app::BrushStrokeBoolSetting::tip_angle_follow: brush.m_tip_angle_follow = value; break;
case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: brush.m_tip_flow_pressure = value; break;
case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: brush.m_tip_opacity_pressure = value; break;
case pp::app::BrushStrokeBoolSetting::tip_size_pressure: brush.m_tip_size_pressure = value; break;
case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: brush.m_jitter_scatter_bothaxis = value; break;
case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: brush.m_jitter_aspect_bothaxis = value; break;
case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: brush.m_jitter_hsv_eachsample = value; break;
case pp::app::BrushStrokeBoolSetting::tip_invert: brush.m_tip_invert = value; break;
case pp::app::BrushStrokeBoolSetting::tip_flip_x: brush.m_tip_flipx = value; break;
case pp::app::BrushStrokeBoolSetting::tip_flip_y: brush.m_tip_flipy = value; break;
case pp::app::BrushStrokeBoolSetting::pattern_enabled: brush.m_pattern_enabled = value; break;
case pp::app::BrushStrokeBoolSetting::dual_enabled: brush.m_dual_enabled = value; break;
case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: brush.m_dual_scatter_bothaxis = value; break;
case pp::app::BrushStrokeBoolSetting::dual_invert: brush.m_dual_invert = value; break;
case pp::app::BrushStrokeBoolSetting::dual_flip_x: brush.m_dual_flipx = value; break;
case pp::app::BrushStrokeBoolSetting::dual_flip_y: brush.m_dual_flipy = value; break;
case pp::app::BrushStrokeBoolSetting::dual_random_flip: brush.m_dual_randflip = value; break;
case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: brush.m_tip_randflipx = value; break;
case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: brush.m_tip_randflipy = value; break;
case pp::app::BrushStrokeBoolSetting::pattern_each_sample: brush.m_pattern_eachsample = value; break;
case pp::app::BrushStrokeBoolSetting::pattern_invert: brush.m_pattern_invert = value; break;
case pp::app::BrushStrokeBoolSetting::pattern_flip_x: brush.m_pattern_flipx = value; break;
case pp::app::BrushStrokeBoolSetting::pattern_flip_y: brush.m_pattern_flipy = value; break;
case pp::app::BrushStrokeBoolSetting::pattern_random_offset: brush.m_pattern_rand_offset = value; break;
}
}
void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override
{
auto& brush = *Canvas::I->m_current_brush;
switch (setting) {
case pp::app::BrushStrokeBlendSetting::tip: brush.m_blend_mode = blend_mode; break;
case pp::app::BrushStrokeBlendSetting::dual: brush.m_dual_blend_mode = blend_mode; break;
case pp::app::BrushStrokeBlendSetting::pattern: brush.m_pattern_blend_mode = blend_mode; break;
}
}
void reset_tip_aspect(float value) override
{
panel_.m_tip_aspect->set_value(value);
Canvas::I->m_current_brush->m_tip_aspect = value;
}
void reset_default_brush() override
{
auto brush = std::make_shared<Brush>();
brush->load_tip(
panel_.m_brush_popup->get_texture_path(panel_.m_default_brush_index),
panel_.m_brush_popup->get_thumb_path(panel_.m_default_brush_index));
brush->m_tip_size = 30;
brush->m_tip_flow = .9f;
brush->m_tip_spacing = .1f;
brush->m_tip_opacity = 1.f;
Canvas::I->m_current_brush = brush;
}
void update_stroke_controls() override
{
panel_.update_controls();
}
void refresh_stroke_preview() override
{
if (panel_.m_preview) {
panel_.m_preview->m_brush = Canvas::I->m_current_brush;
}
}
void notify_stroke_changed() override
{
if (panel_.on_stroke_change) {
panel_.on_stroke_change(&panel_);
}
}
private:
NodePanelStroke& panel_;
};
} // namespace
Node* NodePanelStroke::clone_instantiate() const
{
return new NodePanelStroke();
@@ -156,7 +288,8 @@ void NodePanelStroke::init_controls()
//m_presets_popup->m_flood_events = true;
//m_presets_popup->m_capture_children = false;
int br_idx = std::max(m_brush_popup->find_brush("Round-Hard"), 0);
m_default_brush_index = std::max(m_brush_popup->find_brush("Round-Hard"), 0);
const int br_idx = m_default_brush_index;
// init main brush
auto b = std::make_shared<Brush>();
@@ -331,73 +464,73 @@ void NodePanelStroke::init_controls()
m_blend_mode = find<NodeComboBox>("blend-mode");
m_blend_mode->on_select = [this](Node*, int index) {
Canvas::I->m_current_brush->m_blend_mode = index;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, index);
if (plan) {
execute_stroke_control_plan(plan.value());
}
};
init_slider(m_tip_size, "tip-size", &Brush::m_tip_size);
init_slider(m_tip_spacing, "tip-spacing", &Brush::m_tip_spacing);
init_slider(m_tip_flow, "tip-flow", &Brush::m_tip_flow);
init_slider(m_tip_opacity, "tip-opacity", &Brush::m_tip_opacity);
init_slider(m_tip_angle, "tip-angle", &Brush::m_tip_angle);
init_slider(m_tip_angle_smooth, "tip-angle-smooth", &Brush::m_tip_angle_smooth);
init_slider(m_tip_mix, "tip-mix", &Brush::m_tip_mix);
init_slider(m_tip_wet, "tip-wet", &Brush::m_tip_wet);
init_slider(m_tip_noise, "tip-noise", &Brush::m_tip_noise);
init_slider(m_tip_hue, "tip-hue", &Brush::m_tip_hue);
init_slider(m_tip_sat, "tip-sat", &Brush::m_tip_sat);
init_slider(m_tip_val, "tip-val", &Brush::m_tip_val);
init_slider(m_jitter_scale, "jitter-scale", &Brush::m_jitter_scale);
init_slider(m_jitter_angle, "jitter-angle", &Brush::m_jitter_angle);
init_slider(m_jitter_scatter, "jitter-scatter", &Brush::m_jitter_scatter);
init_slider(m_jitter_flow, "jitter-flow", &Brush::m_jitter_flow);
init_slider(m_jitter_opacity, "jitter-opacity", &Brush::m_jitter_opacity);
init_slider(m_jitter_hue, "jitter-hue", &Brush::m_jitter_hue);
init_slider(m_jitter_sat, "jitter-sat", &Brush::m_jitter_sat);
init_slider(m_jitter_val, "jitter-val", &Brush::m_jitter_val);
init_slider(m_jitter_aspect, "jitter-aspect", &Brush::m_jitter_aspect);
init_slider(m_tip_size, "tip-size", pp::app::BrushStrokeFloatSetting::tip_size, &Brush::m_tip_size);
init_slider(m_tip_spacing, "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing, &Brush::m_tip_spacing);
init_slider(m_tip_flow, "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow, &Brush::m_tip_flow);
init_slider(m_tip_opacity, "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity, &Brush::m_tip_opacity);
init_slider(m_tip_angle, "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle, &Brush::m_tip_angle);
init_slider(m_tip_angle_smooth, "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth, &Brush::m_tip_angle_smooth);
init_slider(m_tip_mix, "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix, &Brush::m_tip_mix);
init_slider(m_tip_wet, "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet, &Brush::m_tip_wet);
init_slider(m_tip_noise, "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise, &Brush::m_tip_noise);
init_slider(m_tip_hue, "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue, &Brush::m_tip_hue);
init_slider(m_tip_sat, "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation, &Brush::m_tip_sat);
init_slider(m_tip_val, "tip-val", pp::app::BrushStrokeFloatSetting::tip_value, &Brush::m_tip_val);
init_slider(m_jitter_scale, "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale, &Brush::m_jitter_scale);
init_slider(m_jitter_angle, "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle, &Brush::m_jitter_angle);
init_slider(m_jitter_scatter, "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter, &Brush::m_jitter_scatter);
init_slider(m_jitter_flow, "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow, &Brush::m_jitter_flow);
init_slider(m_jitter_opacity, "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity, &Brush::m_jitter_opacity);
init_slider(m_jitter_hue, "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue, &Brush::m_jitter_hue);
init_slider(m_jitter_sat, "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation, &Brush::m_jitter_sat);
init_slider(m_jitter_val, "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value, &Brush::m_jitter_val);
init_slider(m_jitter_aspect, "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect, &Brush::m_jitter_aspect);
init_checkbox(m_tip_angle_init, "tip-angle-init", &Brush::m_tip_angle_init);
init_checkbox(m_tip_angle_follow, "tip-angle-follow", &Brush::m_tip_angle_follow);
init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", &Brush::m_tip_flow_pressure);
init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", &Brush::m_tip_opacity_pressure);
init_checkbox(m_tip_size_pressure, "tip-size-pressure", &Brush::m_tip_size_pressure);
init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", &Brush::m_jitter_scatter_bothaxis);
init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", &Brush::m_jitter_aspect_bothaxis);
init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", &Brush::m_jitter_hsv_eachsample);
init_checkbox(m_tip_angle_init, "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init, &Brush::m_tip_angle_init);
init_checkbox(m_tip_angle_follow, "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow, &Brush::m_tip_angle_follow);
init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure, &Brush::m_tip_flow_pressure);
init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure, &Brush::m_tip_opacity_pressure);
init_checkbox(m_tip_size_pressure, "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure, &Brush::m_tip_size_pressure);
init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis, &Brush::m_jitter_scatter_bothaxis);
init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis, &Brush::m_jitter_aspect_bothaxis);
init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample, &Brush::m_jitter_hsv_eachsample);
init_checkbox(m_tip_invert, "tip-invert", &Brush::m_tip_invert);
init_checkbox(m_tip_flipx, "tip-flipx", &Brush::m_tip_flipx);
init_checkbox(m_tip_flipy, "tip-flipy", &Brush::m_tip_flipy);
init_checkbox(m_pattern_enabled, "pattern-enabled", &Brush::m_pattern_enabled);
init_checkbox(m_dual_enabled, "dual-enabled", &Brush::m_dual_enabled);
init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", &Brush::m_dual_scatter_bothaxis);
init_checkbox(m_dual_invert, "dual-invert", &Brush::m_dual_invert);
init_checkbox(m_dual_flipx, "dual-flipx", &Brush::m_dual_flipx);
init_checkbox(m_dual_flipy, "dual-flipy", &Brush::m_dual_flipy);
init_checkbox(m_dual_randflip, "dual-randflip", &Brush::m_dual_randflip);
init_checkbox(m_tip_randflipx, "tip-randflipx", &Brush::m_tip_randflipx);
init_checkbox(m_tip_randflipy, "tip-randflipy", &Brush::m_tip_randflipy);
init_checkbox(m_pattern_eachsample, "pattern-eachsample", &Brush::m_pattern_eachsample);
init_checkbox(m_tip_invert, "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert, &Brush::m_tip_invert);
init_checkbox(m_tip_flipx, "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x, &Brush::m_tip_flipx);
init_checkbox(m_tip_flipy, "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y, &Brush::m_tip_flipy);
init_checkbox(m_pattern_enabled, "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled, &Brush::m_pattern_enabled);
init_checkbox(m_dual_enabled, "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled, &Brush::m_dual_enabled);
init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis, &Brush::m_dual_scatter_bothaxis);
init_checkbox(m_dual_invert, "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert, &Brush::m_dual_invert);
init_checkbox(m_dual_flipx, "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x, &Brush::m_dual_flipx);
init_checkbox(m_dual_flipy, "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y, &Brush::m_dual_flipy);
init_checkbox(m_dual_randflip, "dual-randflip", pp::app::BrushStrokeBoolSetting::dual_random_flip, &Brush::m_dual_randflip);
init_checkbox(m_tip_randflipx, "tip-randflipx", pp::app::BrushStrokeBoolSetting::tip_random_flip_x, &Brush::m_tip_randflipx);
init_checkbox(m_tip_randflipy, "tip-randflipy", pp::app::BrushStrokeBoolSetting::tip_random_flip_y, &Brush::m_tip_randflipy);
init_checkbox(m_pattern_eachsample, "pattern-eachsample", pp::app::BrushStrokeBoolSetting::pattern_each_sample, &Brush::m_pattern_eachsample);
init_checkbox(m_pattern_invert, "pattern-invert", &Brush::m_pattern_invert);
init_checkbox(m_pattern_flipx, "pattern-flipx", &Brush::m_pattern_flipx);
init_checkbox(m_pattern_flipy, "pattern-flipy", &Brush::m_pattern_flipy);
init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", &Brush::m_pattern_rand_offset);
init_checkbox(m_pattern_invert, "pattern-invert", pp::app::BrushStrokeBoolSetting::pattern_invert, &Brush::m_pattern_invert);
init_checkbox(m_pattern_flipx, "pattern-flipx", pp::app::BrushStrokeBoolSetting::pattern_flip_x, &Brush::m_pattern_flipx);
init_checkbox(m_pattern_flipy, "pattern-flipy", pp::app::BrushStrokeBoolSetting::pattern_flip_y, &Brush::m_pattern_flipy);
init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", pp::app::BrushStrokeBoolSetting::pattern_random_offset, &Brush::m_pattern_rand_offset);
init_slider(m_dual_size, "dual-size", &Brush::m_dual_size);
init_slider(m_dual_spacing, "dual-spacing", &Brush::m_dual_spacing);
init_slider(m_dual_scatter, "dual-scatter", &Brush::m_dual_scatter);
init_slider(m_tip_aspect, "tip-aspect", &Brush::m_tip_aspect);
init_slider(m_dual_opacity, "dual-opacity", &Brush::m_dual_opacity);
init_slider(m_dual_flow, "dual-flow", &Brush::m_dual_flow);
init_slider(m_dual_rotate, "dual-rotate", &Brush::m_dual_rotate);
init_slider(m_pattern_scale, "pattern-scale", &Brush::m_pattern_scale);
init_slider(m_pattern_brightness, "pattern-brightness", &Brush::m_pattern_brightness);
init_slider(m_pattern_contrast, "pattern-contrast", &Brush::m_pattern_contrast);
init_slider(m_pattern_depth, "pattern-depth", &Brush::m_pattern_depth);
init_slider(m_dual_size, "dual-size", pp::app::BrushStrokeFloatSetting::dual_size, &Brush::m_dual_size);
init_slider(m_dual_spacing, "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing, &Brush::m_dual_spacing);
init_slider(m_dual_scatter, "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter, &Brush::m_dual_scatter);
init_slider(m_tip_aspect, "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect, &Brush::m_tip_aspect);
init_slider(m_dual_opacity, "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity, &Brush::m_dual_opacity);
init_slider(m_dual_flow, "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow, &Brush::m_dual_flow);
init_slider(m_dual_rotate, "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate, &Brush::m_dual_rotate);
init_slider(m_pattern_scale, "pattern-scale", pp::app::BrushStrokeFloatSetting::pattern_scale, &Brush::m_pattern_scale);
init_slider(m_pattern_brightness, "pattern-brightness", pp::app::BrushStrokeFloatSetting::pattern_brightness, &Brush::m_pattern_brightness);
init_slider(m_pattern_contrast, "pattern-contrast", pp::app::BrushStrokeFloatSetting::pattern_contrast, &Brush::m_pattern_contrast);
init_slider(m_pattern_depth, "pattern-depth", pp::app::BrushStrokeFloatSetting::pattern_depth, &Brush::m_pattern_depth);
SliderCurve curve_cubic {
[](float v) { return glm::pow(v, 3.f); },
@@ -469,27 +602,23 @@ void NodePanelStroke::init_controls()
m_tip_aspect_reset = find<NodeButtonCustom>("tip-aspect-reset");
m_tip_aspect_reset->on_click = [this](Node*) {
m_tip_aspect->set_value(0.5);
Canvas::I->m_current_brush->m_tip_aspect = 0.5f;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
execute_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset());
};
m_dual_blend_mode = find<NodeComboBox>("dual-blend-mode");
m_dual_blend_mode->on_select = [this](Node*, int index) {
Canvas::I->m_current_brush->m_dual_blend_mode = index;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::dual, index);
if (plan) {
execute_stroke_control_plan(plan.value());
}
};
m_pattern_blend_mode = find<NodeComboBox>("pattern-blend-mode");
m_pattern_blend_mode->on_select = [this](Node*, int index) {
Canvas::I->m_current_brush->m_pattern_blend_mode = index;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, index);
if (plan) {
execute_stroke_control_plan(plan.value());
}
};
m_preview->m_brush = Canvas::I->m_current_brush;
@@ -518,54 +647,68 @@ void NodePanelStroke::init_controls()
}
m_brush_settings_reset = find<NodeButton>("brush-settings-reset");
m_brush_settings_reset->on_click = [br_idx,this](Node*) {
auto b = std::make_shared<Brush>();
b->load_tip(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx));
//b->load_dual(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx));
//b->load_pattern(m_pattern_popup->get_texture_path(0), m_pattern_popup->get_thumb_path(0));
b->m_tip_size = 30;
b->m_tip_flow = .9f;
b->m_tip_spacing = .1f;
b->m_tip_opacity = 1.f;
Canvas::I->m_current_brush = b;
update_controls();
App::I->brush_update(true, true);
m_brush_settings_reset->on_click = [this](Node*) {
execute_stroke_control_plan(pp::app::plan_brush_default_settings_reset());
};
update_controls();
}
void NodePanelStroke::init_slider(NodeSliderH*& target, const char* id, float Brush::* prop)
void NodePanelStroke::init_slider(
NodeSliderH*& target,
const char* id,
pp::app::BrushStrokeFloatSetting setting,
float Brush::* prop)
{
target = find<NodeSliderH>(id);
target->on_value_changed = std::bind(&NodePanelStroke::handle_slide,
this, prop, std::placeholders::_1, std::placeholders::_2);
this, setting, prop, std::placeholders::_1, std::placeholders::_2);
//m_canvas->m_brush->*prop = target->m_values;
}
void NodePanelStroke::handle_slide(float Brush::* prop, Node* target, float value)
void NodePanelStroke::handle_slide(
pp::app::BrushStrokeFloatSetting setting,
float Brush::* prop,
Node* target,
float value)
{
auto curve = m_curves.find((NodeSliderH*)target);
Canvas::I->m_current_brush.get()->*prop = curve != m_curves.end() ? curve->second.to_value(value) : value;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
const auto brush_value = curve != m_curves.end() ? curve->second.to_value(value) : value;
const auto plan = pp::app::plan_brush_stroke_float_setting(setting, brush_value);
if (plan) {
execute_stroke_control_plan(plan.value());
} else {
Canvas::I->m_current_brush.get()->*prop = brush_value;
}
}
void NodePanelStroke::init_checkbox(NodeCheckBox*& target, const char* id, bool Brush::* prop)
void NodePanelStroke::init_checkbox(
NodeCheckBox*& target,
const char* id,
pp::app::BrushStrokeBoolSetting setting,
bool Brush::* prop)
{
target = find<NodeCheckBox>(id);
target->on_value_changed = std::bind(&NodePanelStroke::handle_checkbox,
this, prop, std::placeholders::_1, std::placeholders::_2);
this, setting, std::placeholders::_2);
Canvas::I->m_current_brush.get()->*prop = target->checked;
}
void NodePanelStroke::handle_checkbox(bool Brush::* prop, Node *target, bool value)
void NodePanelStroke::handle_checkbox(
pp::app::BrushStrokeBoolSetting setting,
bool value)
{
Canvas::I->m_current_brush.get()->*prop = value;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
const auto plan = pp::app::plan_brush_stroke_bool_setting(setting, value);
execute_stroke_control_plan(plan);
}
void NodePanelStroke::execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan)
{
LegacyBrushStrokeControlServices services(*this);
const auto status = pp::app::execute_brush_stroke_control_plan(plan, services);
if (!status.ok()) {
LOG("Brush stroke control action failed: %s", status.message);
}
}
kEventResult NodePanelStroke::handle_event(Event* e)

View File

@@ -9,6 +9,13 @@
#include "node_image.h"
#include "node_panel_brush.h"
namespace pp::app {
struct BrushStrokeControlPlan;
enum class BrushStrokeBoolSetting;
enum class BrushStrokeFloatSetting;
enum class BrushStrokeBlendSetting;
} // namespace pp::app
class NodePanelStroke : public Node
{
public:
@@ -103,6 +110,7 @@ public:
inline float to_slider(float v) { return m_inv(v); }
};
std::map<NodeSliderH*, SliderCurve> m_curves;
int m_default_brush_index = 0;
virtual Node* clone_instantiate() const override;
virtual void clone_finalize(Node* dest) const override;
@@ -116,9 +124,10 @@ public:
void set_size(float value, bool normalized, bool propagate);
void init_fold(const std::string& name);
void init_slider(NodeSliderH*& slider, const char* id, float Brush::* prop);
void handle_slide(float Brush::* prop, Node* target, float value);
void init_slider(NodeSliderH*& slider, const char* id, pp::app::BrushStrokeFloatSetting setting, float Brush::* prop);
void handle_slide(pp::app::BrushStrokeFloatSetting setting, float Brush::* prop, Node* target, float value);
void init_checkbox(NodeCheckBox*& slider, const char* id, bool Brush::* prop);
void handle_checkbox(bool Brush::* prop, Node* target, bool value);
void init_checkbox(NodeCheckBox*& slider, const char* id, pp::app::BrushStrokeBoolSetting setting, bool Brush::* prop);
void handle_checkbox(pp::app::BrushStrokeBoolSetting setting, bool value);
void execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan);
};

View File

@@ -6,6 +6,70 @@ 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]] bool paint_blend_mode_from_persisted_index(int value, pp::paint::BlendMode& out) noexcept
{
switch (value) {
case 0: out = pp::paint::BlendMode::normal; return true;
case 1: out = pp::paint::BlendMode::multiply; return true;
case 2: out = pp::paint::BlendMode::screen; return true;
case 3: out = pp::paint::BlendMode::color_dodge; return true;
case 4: out = pp::paint::BlendMode::overlay; return true;
default: return false;
}
}
[[nodiscard]] bool stroke_blend_mode_from_persisted_index(int value, pp::paint::StrokeBlendMode& out) noexcept
{
switch (value) {
case 0: out = pp::paint::StrokeBlendMode::normal; return true;
case 1: out = pp::paint::StrokeBlendMode::multiply; return true;
case 2: out = pp::paint::StrokeBlendMode::subtract; return true;
case 3: out = pp::paint::StrokeBlendMode::darken; return true;
case 4: out = pp::paint::StrokeBlendMode::overlay; return true;
case 5: out = pp::paint::StrokeBlendMode::color_dodge; return true;
case 6: out = pp::paint::StrokeBlendMode::color_burn; return true;
case 7: out = pp::paint::StrokeBlendMode::linear_burn; return true;
case 8: out = pp::paint::StrokeBlendMode::hard_mix; return true;
case 9: out = pp::paint::StrokeBlendMode::linear_height; return true;
case 10: out = pp::paint::StrokeBlendMode::height; return true;
default: 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 +93,56 @@ 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;
}
void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept
{
gate.path = stroke.path;
gate.reads_destination_color = stroke.path == StrokeCompositePath::framebuffer_fetch;
gate.requires_auxiliary_texture = stroke.requires_auxiliary_texture;
gate.requires_texture_copy = stroke.requires_texture_copy;
gate.requires_render_target_blit = stroke.requires_render_target_blit;
}
void apply_feedback_plan(CanvasStrokeFeedbackPlan& plan, const pp::renderer::PaintFeedbackPlan& feedback) noexcept
{
plan.path = composite_path_from_feedback(feedback.path);
plan.reads_destination_color = plan.path == StrokeCompositePath::framebuffer_fetch;
plan.requires_auxiliary_texture = feedback.requires_auxiliary_texture;
plan.requires_texture_copy = feedback.requires_texture_copy;
plan.requires_render_target_blit = feedback.requires_render_target_blit;
}
void mark_shader_blend_fallback(
CanvasBlendGatePlan& gate,
pp::renderer::RenderDeviceFeatures features) noexcept
{
gate.shader_blend = true;
gate.complex_blend = true;
gate.compatibility_fallback = true;
if (features.framebuffer_fetch) {
gate.path = StrokeCompositePath::framebuffer_fetch;
gate.reads_destination_color = true;
} else if (features.texture_copy || features.render_target_blit) {
gate.path = StrokeCompositePath::ping_pong_textures;
gate.requires_auxiliary_texture = true;
gate.requires_texture_copy = features.texture_copy;
gate.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
}
}
}
pp::foundation::Status composite_layer(
@@ -62,4 +176,193 @@ 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);
}
pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
pp::renderer::RenderDeviceFeatures features,
CanvasBlendGateRequest request) noexcept
{
CanvasBlendGatePlan gate;
for (std::size_t i = 0; i < request.layer_blend_modes.size(); ++i) {
pp::paint::BlendMode layer_blend = pp::paint::BlendMode::normal;
if (!paint_blend_mode_from_persisted_index(request.layer_blend_modes[i], layer_blend)) {
if (request.layer_blend_modes[i] != 0) {
gate.first_complex_layer_index = static_cast<int>(i);
mark_shader_blend_fallback(gate, features);
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
continue;
}
if (layer_blend == pp::paint::BlendMode::normal) {
continue;
}
gate.shader_blend = true;
gate.complex_blend = true;
gate.first_complex_layer_index = static_cast<int>(i);
const auto stroke = plan_stroke_composite(
features,
StrokeCompositeRequest {
.extent = request.extent,
.layer_blend_mode = layer_blend,
});
if (stroke) {
apply_stroke_plan(gate, stroke.value());
} else {
gate.compatibility_fallback = true;
}
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
pp::paint::StrokeBlendMode stroke_blend = pp::paint::StrokeBlendMode::normal;
if (request.has_stroke_blend_mode) {
if (!stroke_blend_mode_from_persisted_index(request.stroke_blend_mode, stroke_blend)) {
if (request.stroke_blend_mode != 0) {
gate.stroke_complex = true;
mark_shader_blend_fallback(gate, features);
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
} else if (stroke_blend != pp::paint::StrokeBlendMode::normal) {
gate.stroke_complex = true;
}
}
gate.dual_brush_complex = request.dual_brush_blend;
gate.pattern_complex = request.pattern_blend;
if (!gate.stroke_complex && !gate.dual_brush_complex && !gate.pattern_complex) {
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
gate.shader_blend = true;
gate.complex_blend = true;
const auto stroke = plan_stroke_composite(
features,
StrokeCompositeRequest {
.extent = request.extent,
.stroke_blend_mode = stroke_blend,
.dual_brush_blend = request.dual_brush_blend,
.pattern_blend = request.pattern_blend,
});
if (stroke) {
apply_stroke_plan(gate, stroke.value());
} else {
gate.compatibility_fallback = true;
}
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
pp::foundation::Result<CanvasStrokeFeedbackPlan> plan_canvas_stroke_feedback(
pp::renderer::RenderDeviceFeatures features,
pp::renderer::Extent2D extent) noexcept
{
const auto extent_status = pp::renderer::validate_extent(extent);
if (!extent_status.ok()) {
return pp::foundation::Result<CanvasStrokeFeedbackPlan>::failure(extent_status);
}
const pp::renderer::TextureDesc target_desc {
.extent = extent,
.format = pp::renderer::TextureFormat::rgba8,
.usage = pp::renderer::TextureUsage::render_target
| pp::renderer::TextureUsage::sampled
| pp::renderer::TextureUsage::copy_source
| pp::renderer::TextureUsage::copy_destination,
.debug_name = "canvas-stroke-feedback-target",
};
const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, true);
if (feedback) {
CanvasStrokeFeedbackPlan plan;
apply_feedback_plan(plan, feedback.value());
return pp::foundation::Result<CanvasStrokeFeedbackPlan>::success(plan);
}
CanvasStrokeFeedbackPlan fallback;
fallback.compatibility_fallback = true;
if (features.framebuffer_fetch) {
fallback.path = StrokeCompositePath::framebuffer_fetch;
fallback.reads_destination_color = true;
} else {
fallback.path = StrokeCompositePath::ping_pong_textures;
fallback.requires_auxiliary_texture = true;
fallback.requires_texture_copy = features.texture_copy;
fallback.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
}
return pp::foundation::Result<CanvasStrokeFeedbackPlan>::success(fallback);
}
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,90 @@ 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;
};
struct CanvasBlendGateRequest {
pp::renderer::Extent2D extent {};
std::span<const int> layer_blend_modes;
bool has_stroke_blend_mode = false;
int stroke_blend_mode = 0;
bool dual_brush_blend = false;
bool pattern_blend = false;
};
struct CanvasBlendGatePlan {
bool shader_blend = false;
bool complex_blend = false;
bool compatibility_fallback = false;
bool stroke_complex = false;
bool dual_brush_complex = false;
bool pattern_complex = false;
int first_complex_layer_index = -1;
StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
bool reads_destination_color = false;
bool requires_auxiliary_texture = false;
bool requires_texture_copy = false;
bool requires_render_target_blit = false;
};
struct CanvasStrokeFeedbackPlan {
StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
bool reads_destination_color = false;
bool requires_auxiliary_texture = false;
bool requires_texture_copy = false;
bool requires_render_target_blit = false;
bool compatibility_fallback = 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]] pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
pp::renderer::RenderDeviceFeatures features,
CanvasBlendGateRequest request) noexcept;
[[nodiscard]] pp::foundation::Result<CanvasStrokeFeedbackPlan> plan_canvas_stroke_feedback(
pp::renderer::RenderDeviceFeatures features,
pp::renderer::Extent2D extent) noexcept;
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
}

View File

@@ -823,6 +823,65 @@ pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc desti
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
{
switch (format) {
@@ -887,6 +946,20 @@ const char* blit_filter_name(BlitFilter filter) noexcept
return "unknown";
}
const char* paint_feedback_path_name(PaintFeedbackPath path) noexcept
{
switch (path) {
case PaintFeedbackPath::none:
return "none";
case PaintFeedbackPath::framebuffer_fetch:
return "framebuffer_fetch";
case PaintFeedbackPath::ping_pong_textures:
return "ping_pong_textures";
}
return "unknown";
}
const char* blend_factor_name(BlendFactor factor) noexcept
{
switch (factor) {

View File

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

View File

@@ -211,6 +211,7 @@ std::int32_t get_opengl_uniform_location(std::uint32_t program, const char* name
std::map<kShader, Shader> ShaderManager::m_shaders;
Shader* ShaderManager::m_current;
pp::renderer::RenderDeviceFeatures ShaderManager::m_render_device_features {};
bool ShaderManager::ext_framebuffer_fetch = false;
bool ShaderManager::ext_float32 = false;
bool ShaderManager::ext_float32_linear = false;
@@ -816,6 +817,16 @@ void ShaderManager::u_float(kShaderUniform id, float f)
m_current->u_float(id, f);
}
void ShaderManager::set_render_device_features(pp::renderer::RenderDeviceFeatures features) noexcept
{
m_render_device_features = features;
}
pp::renderer::RenderDeviceFeatures ShaderManager::render_device_features() noexcept
{
return m_render_device_features;
}
void ShaderManager::invalidate()
{
m_shaders.clear();

View File

@@ -1,4 +1,5 @@
#pragma once
#include "renderer_api/renderer_api.h"
#include "util.h"
bool check_uniform_uniqueness();
@@ -108,6 +109,7 @@ class ShaderManager
{
static std::map<kShader, Shader> m_shaders;
static Shader* m_current;
static pp::renderer::RenderDeviceFeatures m_render_device_features;
public:
static bool ext_framebuffer_fetch;
static bool ext_float32;
@@ -127,5 +129,7 @@ public:
static void u_int(kShaderUniform id, int i);
static void u_int(const char* name, int i);
static void u_float(kShaderUniform id, float f);
static void set_render_device_features(pp::renderer::RenderDeviceFeatures features) noexcept;
static pp::renderer::RenderDeviceFeatures render_device_features() noexcept;
static void invalidate();
};

View File

@@ -1132,6 +1132,126 @@ if(TARGET pano_cli)
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_texture_list_add_smoke
COMMAND pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png)
set_tests_properties(pano_cli_plan_brush_texture_list_add_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"add-texture\".*\"source\":\"C:/Temp/soft.png\".*\"path\":\"data/brushes/soft.png\".*\"thumb\":\"data/brushes/thumbs/soft.png\".*\"brushName\":\"soft\".*\"userTexture\":true.*\"convertsBrushAlpha\":true")
add_test(NAME pano_cli_plan_brush_texture_list_remove_user_smoke
COMMAND pano_cli plan-brush-texture-list --kind remove --item-count 3 --current-index 2 --user-texture)
set_tests_properties(pano_cli_plan_brush_texture_list_remove_user_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"remove-texture\".*\"itemCount\":3.*\"currentIndex\":2.*\"targetIndex\":1.*\"deletesTextureFiles\":true.*\"notifiesSelection\":true")
add_test(NAME pano_cli_plan_brush_texture_list_move_edge_smoke
COMMAND pano_cli plan-brush-texture-list --kind move --item-count 3 --current-index 0 --offset -1)
set_tests_properties(pano_cli_plan_brush_texture_list_move_edge_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"move-texture\".*\"currentIndex\":0.*\"targetIndex\":0.*\"moveOffset\":-1.*\"noOp\":true")
add_test(NAME pano_cli_plan_brush_texture_list_rejects_bad_source
COMMAND pano_cli plan-brush-texture-list --kind add --source no-extension)
set_tests_properties(pano_cli_plan_brush_texture_list_rejects_bad_source PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_stroke_control_float_smoke
COMMAND pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5)
set_tests_properties(pano_cli_plan_brush_stroke_control_float_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-float\".*\"floatSetting\":\"tip-size\".*\"floatValue\":42.5.*\"mutatesBrush\":true.*\"notifiesStrokeChange\":true")
add_test(NAME pano_cli_plan_brush_stroke_control_toggle_smoke
COMMAND pano_cli plan-brush-stroke-control --kind bool --setting dual-enabled --enabled)
set_tests_properties(pano_cli_plan_brush_stroke_control_toggle_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-bool\".*\"boolSetting\":\"dual-enabled\".*\"boolValue\":true.*\"refreshesPreview\":true")
add_test(NAME pano_cli_plan_brush_stroke_control_blend_smoke
COMMAND pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3)
set_tests_properties(pano_cli_plan_brush_stroke_control_blend_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-blend-mode\".*\"blendSetting\":\"pattern\".*\"blendMode\":3")
add_test(NAME pano_cli_plan_brush_stroke_control_reset_smoke
COMMAND pano_cli plan-brush-stroke-control --kind default-reset)
set_tests_properties(pano_cli_plan_brush_stroke_control_reset_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"reset-default-brush\".*\"updatesControls\":true.*\"notifiesStrokeChange\":true")
add_test(NAME pano_cli_plan_brush_stroke_control_rejects_bad_setting
COMMAND pano_cli plan-brush-stroke-control --kind float --setting imaginary --value 1)
set_tests_properties(pano_cli_plan_brush_stroke_control_rejects_bad_setting PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_stroke_control_rejects_bad_blend
COMMAND pano_cli plan-brush-stroke-control --kind blend --setting tip --blend-mode 99)
set_tests_properties(pano_cli_plan_brush_stroke_control_rejects_bad_blend PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_paint_feedback_framebuffer_fetch_smoke
COMMAND pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only)
set_tests_properties(pano_cli_plan_paint_feedback_framebuffer_fetch_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"framebuffer_fetch\".*\"readsDestinationColor\":true.*\"requiresAuxiliaryTexture\":false.*\"requiresExplicitTransition\":true")
add_test(NAME pano_cli_plan_paint_feedback_ping_pong_copy_smoke
COMMAND pano_cli plan-paint-feedback --texture-copy)
set_tests_properties(pano_cli_plan_paint_feedback_ping_pong_copy_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"ping_pong_textures\".*\"requiresAuxiliaryTexture\":true.*\"requiresTextureCopy\":true.*\"requiresRenderTargetBlit\":false")
add_test(NAME pano_cli_plan_paint_feedback_simple_smoke
COMMAND pano_cli plan-paint-feedback --simple --render-only)
set_tests_properties(pano_cli_plan_paint_feedback_simple_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"none\".*\"complexBlend\":false.*\"readsDestinationColor\":false")
add_test(NAME pano_cli_plan_paint_feedback_rejects_unsupported
COMMAND pano_cli plan-paint-feedback)
set_tests_properties(pano_cli_plan_paint_feedback_rejects_unsupported PROPERTIES
LABELS "renderer;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_paint_feedback_rejects_depth
COMMAND pano_cli plan-paint-feedback --texture-copy --depth)
set_tests_properties(pano_cli_plan_paint_feedback_rejects_depth PROPERTIES
LABELS "renderer;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_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

@@ -65,6 +65,150 @@ public:
std::string call_order;
};
class FakeBrushTextureListServices final : public pp::app::BrushTextureListServices {
public:
pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) override
{
if (fail_add) {
call_order += "add-failed;";
return pp::foundation::Status::invalid_argument("fake add failure");
}
adds += 1;
last_source_path = std::string(source_path);
last_high_path = std::string(high_path);
last_thumbnail_path = std::string(thumbnail_path);
last_brush_name = std::string(brush_name);
last_converts_brush_alpha = converts_brush_alpha;
call_order += "add;";
return pp::foundation::Status::success();
}
void remove_texture(int index, bool delete_texture_files) override
{
removes += 1;
last_index = index;
last_deletes_texture_files = delete_texture_files;
call_order += "remove;";
}
void move_texture(int from_index, int to_index) override
{
moves += 1;
last_index = from_index;
last_target_index = to_index;
call_order += "move;";
}
void select_texture(int index) override
{
selections += 1;
last_target_index = index;
call_order += "select;";
}
void save_texture_list() override
{
saves += 1;
call_order += "save;";
}
int adds = 0;
int removes = 0;
int moves = 0;
int selections = 0;
int saves = 0;
int last_index = -1;
int last_target_index = -1;
std::string last_source_path;
std::string last_high_path;
std::string last_thumbnail_path;
std::string last_brush_name;
bool last_converts_brush_alpha = false;
bool last_deletes_texture_files = false;
bool fail_add = false;
std::string call_order;
};
class FakeBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices {
public:
void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override
{
float_sets += 1;
last_float_setting = setting;
last_float_value = value;
call_order += "float;";
}
void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override
{
bool_sets += 1;
last_bool_setting = setting;
last_bool_value = value;
call_order += "bool;";
}
void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override
{
blend_sets += 1;
last_blend_setting = setting;
last_blend_mode = blend_mode;
call_order += "blend;";
}
void reset_tip_aspect(float value) override
{
tip_aspect_resets += 1;
last_float_value = value;
call_order += "tip-aspect;";
}
void reset_default_brush() override
{
default_resets += 1;
call_order += "default;";
}
void update_stroke_controls() override
{
control_updates += 1;
call_order += "controls;";
}
void refresh_stroke_preview() override
{
preview_refreshes += 1;
call_order += "preview;";
}
void notify_stroke_changed() override
{
stroke_notifications += 1;
call_order += "notify;";
}
int float_sets = 0;
int bool_sets = 0;
int blend_sets = 0;
int tip_aspect_resets = 0;
int default_resets = 0;
int control_updates = 0;
int preview_refreshes = 0;
int stroke_notifications = 0;
pp::app::BrushStrokeFloatSetting last_float_setting = pp::app::BrushStrokeFloatSetting::tip_size;
pp::app::BrushStrokeBoolSetting last_bool_setting = pp::app::BrushStrokeBoolSetting::tip_angle_init;
pp::app::BrushStrokeBlendSetting last_blend_setting = pp::app::BrushStrokeBlendSetting::tip;
float last_float_value = 0.0F;
bool last_bool_value = false;
int last_blend_mode = 0;
std::string call_order;
};
void color_plan_validates_all_channels(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F);
@@ -132,6 +276,134 @@ void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness)
PP_EXPECT(harness, !plan.loads_brush_resources);
}
void stroke_control_plans_validate_values_and_reject_breaking_points(pp::tests::Harness& harness)
{
const auto slider = pp::app::plan_brush_stroke_float_setting(
pp::app::BrushStrokeFloatSetting::tip_size,
42.5F);
PP_EXPECT(harness, slider);
if (slider) {
PP_EXPECT(harness, slider.value().operation == pp::app::BrushStrokeControlOperation::set_float);
PP_EXPECT(harness, slider.value().float_setting == pp::app::BrushStrokeFloatSetting::tip_size);
PP_EXPECT(harness, slider.value().float_value == 42.5F);
PP_EXPECT(harness, slider.value().mutates_brush);
PP_EXPECT(harness, slider.value().refreshes_preview);
PP_EXPECT(harness, slider.value().notifies_stroke_change);
}
const auto checkbox = pp::app::plan_brush_stroke_bool_setting(
pp::app::BrushStrokeBoolSetting::pattern_enabled,
true);
PP_EXPECT(harness, checkbox.operation == pp::app::BrushStrokeControlOperation::set_bool);
PP_EXPECT(harness, checkbox.bool_setting == pp::app::BrushStrokeBoolSetting::pattern_enabled);
PP_EXPECT(harness, checkbox.bool_value);
const auto blend = pp::app::plan_brush_stroke_blend_mode(
pp::app::BrushStrokeBlendSetting::dual,
7);
PP_EXPECT(harness, blend);
if (blend) {
PP_EXPECT(harness, blend.value().operation == pp::app::BrushStrokeControlOperation::set_blend_mode);
PP_EXPECT(harness, blend.value().blend_setting == pp::app::BrushStrokeBlendSetting::dual);
PP_EXPECT(harness, blend.value().blend_mode == 7);
}
const auto tip_aspect = pp::app::plan_brush_tip_aspect_reset();
PP_EXPECT(harness, tip_aspect.operation == pp::app::BrushStrokeControlOperation::reset_tip_aspect);
PP_EXPECT(harness, tip_aspect.float_setting == pp::app::BrushStrokeFloatSetting::tip_aspect);
PP_EXPECT(harness, tip_aspect.float_value == 0.5F);
const auto defaults = pp::app::plan_brush_default_settings_reset();
PP_EXPECT(harness, defaults.operation == pp::app::BrushStrokeControlOperation::reset_default_brush);
PP_EXPECT(harness, defaults.updates_controls);
PP_EXPECT(harness, defaults.refreshes_preview);
PP_EXPECT(harness, defaults.notifies_stroke_change);
PP_EXPECT(
harness,
!pp::app::plan_brush_stroke_float_setting(
pp::app::BrushStrokeFloatSetting::tip_flow,
std::nanf("")));
PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, -1));
PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 64));
}
void texture_list_add_plans_target_paths_and_rejects_bad_input(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_brush_texture_list_add(
"brushes",
"D:/Paint/data",
"C:/Users/artist/My Brush.JPG");
PP_EXPECT(harness, plan);
if (plan) {
PP_EXPECT(harness, plan.value().operation == pp::app::BrushTextureListOperation::add_texture);
PP_EXPECT(harness, plan.value().source_path == "C:/Users/artist/My Brush.JPG");
PP_EXPECT(harness, plan.value().brush_name == "My Brush");
PP_EXPECT(harness, plan.value().high_path == "D:/Paint/data/brushes/My Brush.png");
PP_EXPECT(harness, plan.value().thumbnail_path == "D:/Paint/data/brushes/thumbs/My Brush.png");
PP_EXPECT(harness, plan.value().user_texture);
PP_EXPECT(harness, plan.value().saves_list);
PP_EXPECT(harness, plan.value().converts_brush_alpha);
}
const auto pattern = pp::app::plan_brush_texture_list_add(
"patterns",
"D:/Paint/data",
R"(C:\Textures\noise.png)");
PP_EXPECT(harness, pattern);
if (pattern) {
PP_EXPECT(harness, !pattern.value().converts_brush_alpha);
PP_EXPECT(harness, pattern.value().brush_name == "noise");
}
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("", "D:/Paint/data", "a.png"));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "", "a.png"));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "no-extension"));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/dir/"));
}
void texture_list_remove_and_move_plans_handle_edges(pp::tests::Harness& harness)
{
const auto remove_middle = pp::app::plan_brush_texture_list_remove(3, 1, true);
PP_EXPECT(harness, remove_middle);
if (remove_middle) {
PP_EXPECT(harness, remove_middle.value().operation == pp::app::BrushTextureListOperation::remove_texture);
PP_EXPECT(harness, remove_middle.value().current_index == 1);
PP_EXPECT(harness, remove_middle.value().target_index == 1);
PP_EXPECT(harness, remove_middle.value().deletes_texture_files);
PP_EXPECT(harness, remove_middle.value().notifies_selection);
PP_EXPECT(harness, remove_middle.value().saves_list);
}
const auto remove_last = pp::app::plan_brush_texture_list_remove(1, 0, false);
PP_EXPECT(harness, remove_last);
if (remove_last) {
PP_EXPECT(harness, remove_last.value().target_index == -1);
PP_EXPECT(harness, !remove_last.value().deletes_texture_files);
PP_EXPECT(harness, !remove_last.value().notifies_selection);
}
const auto move_up_edge = pp::app::plan_brush_texture_list_move(3, 0, -1);
PP_EXPECT(harness, move_up_edge);
if (move_up_edge) {
PP_EXPECT(harness, move_up_edge.value().operation == pp::app::BrushTextureListOperation::move_texture);
PP_EXPECT(harness, move_up_edge.value().target_index == 0);
PP_EXPECT(harness, move_up_edge.value().no_op);
}
const auto move_down = pp::app::plan_brush_texture_list_move(3, 1, 1);
PP_EXPECT(harness, move_down);
if (move_down) {
PP_EXPECT(harness, move_down.value().target_index == 2);
PP_EXPECT(harness, !move_down.value().no_op);
}
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(0, 0, true));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(2, 2, true));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, 0, 0));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, -1, 1));
}
void executor_dispatches_color_and_refresh(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
@@ -197,6 +469,105 @@ void executor_dispatches_stroke_refresh_only(pp::tests::Harness& harness)
PP_EXPECT(harness, services.call_order == "refresh;");
}
void texture_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& harness)
{
FakeBrushTextureListServices services;
const auto add = pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/Temp/soft.png");
PP_EXPECT(harness, add);
if (add) {
PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(add.value(), services).ok());
}
const auto remove = pp::app::plan_brush_texture_list_remove(3, 2, true);
PP_EXPECT(harness, remove);
if (remove) {
PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(remove.value(), services).ok());
}
const auto move = pp::app::plan_brush_texture_list_move(3, 1, -1);
PP_EXPECT(harness, move);
if (move) {
PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(move.value(), services).ok());
}
PP_EXPECT(harness, services.adds == 1);
PP_EXPECT(harness, services.last_source_path == "C:/Temp/soft.png");
PP_EXPECT(harness, services.last_high_path == "D:/Paint/data/brushes/soft.png");
PP_EXPECT(harness, services.last_thumbnail_path == "D:/Paint/data/brushes/thumbs/soft.png");
PP_EXPECT(harness, services.last_brush_name == "soft");
PP_EXPECT(harness, services.last_converts_brush_alpha);
PP_EXPECT(harness, services.removes == 1);
PP_EXPECT(harness, services.moves == 1);
PP_EXPECT(harness, services.selections == 1);
PP_EXPECT(harness, services.saves == 3);
PP_EXPECT(harness, services.last_deletes_texture_files);
PP_EXPECT(harness, services.call_order == "add;save;remove;select;save;move;save;");
FakeBrushTextureListServices failing_services;
failing_services.fail_add = true;
PP_EXPECT(harness, add);
if (add) {
PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add.value(), failing_services).ok());
}
PP_EXPECT(harness, failing_services.saves == 0);
PP_EXPECT(harness, failing_services.call_order == "add-failed;");
}
void stroke_control_executor_dispatches_and_rejects_bad_payloads(pp::tests::Harness& harness)
{
FakeBrushStrokeControlServices services;
const auto slider = pp::app::plan_brush_stroke_float_setting(
pp::app::BrushStrokeFloatSetting::pattern_depth,
0.75F);
PP_EXPECT(harness, slider);
if (slider) {
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(slider.value(), services).ok());
}
const auto checkbox = pp::app::plan_brush_stroke_bool_setting(
pp::app::BrushStrokeBoolSetting::dual_enabled,
true);
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(checkbox, services).ok());
const auto blend = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 3);
PP_EXPECT(harness, blend);
if (blend) {
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(blend.value(), services).ok());
}
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset(), services).ok());
PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_default_settings_reset(), services).ok());
PP_EXPECT(harness, services.float_sets == 1);
PP_EXPECT(harness, services.last_float_setting == pp::app::BrushStrokeFloatSetting::pattern_depth);
PP_EXPECT(harness, services.last_bool_setting == pp::app::BrushStrokeBoolSetting::dual_enabled);
PP_EXPECT(harness, services.last_blend_setting == pp::app::BrushStrokeBlendSetting::pattern);
PP_EXPECT(harness, services.last_blend_mode == 3);
PP_EXPECT(harness, services.tip_aspect_resets == 1);
PP_EXPECT(harness, services.default_resets == 1);
PP_EXPECT(harness, services.preview_refreshes == 5);
PP_EXPECT(harness, services.control_updates == 1);
PP_EXPECT(harness, services.stroke_notifications == 5);
PP_EXPECT(
harness,
services.call_order == "float;preview;notify;bool;preview;notify;blend;preview;notify;tip-aspect;"
"preview;notify;default;controls;preview;notify;");
FakeBrushStrokeControlServices bad_services;
pp::app::BrushStrokeControlPlan bad_float;
bad_float.operation = pp::app::BrushStrokeControlOperation::set_float;
bad_float.float_value = std::nanf("");
PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_float, bad_services).ok());
pp::app::BrushStrokeControlPlan bad_blend;
bad_blend.operation = pp::app::BrushStrokeControlOperation::set_blend_mode;
bad_blend.blend_mode = 99;
PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_blend, bad_services).ok());
PP_EXPECT(harness, bad_services.call_order.empty());
}
void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
@@ -218,6 +589,20 @@ void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
PP_EXPECT(harness, services.color_sets == 0);
PP_EXPECT(harness, services.texture_sets == 0);
PP_EXPECT(harness, services.refreshes == 0);
FakeBrushTextureListServices list_services;
pp::app::BrushTextureListPlan add;
add.operation = pp::app::BrushTextureListOperation::add_texture;
add.source_path = "source.png";
PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add, list_services).ok());
pp::app::BrushTextureListPlan move;
move.operation = pp::app::BrushTextureListOperation::move_texture;
move.item_count = 1;
move.current_index = 0;
move.target_index = 1;
PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(move, list_services).ok());
PP_EXPECT(harness, list_services.call_order.empty());
}
} // namespace
@@ -229,9 +614,14 @@ int main()
harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot);
harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush);
harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview);
harness.run("stroke control plans validate values and reject breaking points", stroke_control_plans_validate_values_and_reject_breaking_points);
harness.run("texture list add plans target paths and rejects bad input", texture_list_add_plans_target_paths_and_rejects_bad_input);
harness.run("texture list remove and move plans handle edges", texture_list_remove_and_move_plans_handle_edges);
harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh);
harness.run("executor dispatches texture and preset", executor_dispatches_texture_and_preset);
harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only);
harness.run("texture list executor dispatches and preserves failure", texture_list_executor_dispatches_and_preserves_failure);
harness.run("stroke control executor dispatches and rejects bad payloads", stroke_control_executor_dispatches_and_rejects_bad_payloads);
harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
return harness.finish();
}

View File

@@ -2,14 +2,27 @@
#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::CanvasBlendGateRequest;
using pp::paint_renderer::StrokeCompositePath;
using pp::paint_renderer::StrokeCompositeRequest;
using pp::paint_renderer::composite_layer;
using pp::paint_renderer::plan_canvas_blend_gate;
using pp::paint_renderer::plan_canvas_stroke_feedback;
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 +110,337 @@ 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"));
}
void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
{
const std::vector<int> normal_layers { 0, 0, 0 };
const auto normal = plan_canvas_blend_gate(
RenderDeviceFeatures {},
CanvasBlendGateRequest {
.extent = Extent2D { .width = 0, .height = 0 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 0,
});
PP_EXPECT(h, normal);
if (normal) {
PP_EXPECT(h, !normal.value().shader_blend);
PP_EXPECT(h, !normal.value().complex_blend);
PP_EXPECT(h, !normal.value().compatibility_fallback);
}
const std::vector<int> layer_blend { 0, 4 };
const auto layer = plan_canvas_blend_gate(
RenderDeviceFeatures { .framebuffer_fetch = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = layer_blend,
});
PP_EXPECT(h, layer);
if (layer) {
PP_EXPECT(h, layer.value().shader_blend);
PP_EXPECT(h, layer.value().complex_blend);
PP_EXPECT(h, layer.value().first_complex_layer_index == 1);
PP_EXPECT(h, layer.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, layer.value().reads_destination_color);
}
const auto stroke = plan_canvas_blend_gate(
RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 10,
});
PP_EXPECT(h, stroke);
if (stroke) {
PP_EXPECT(h, stroke.value().shader_blend);
PP_EXPECT(h, stroke.value().stroke_complex);
PP_EXPECT(h, stroke.value().first_complex_layer_index == -1);
PP_EXPECT(h, stroke.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !stroke.value().reads_destination_color);
PP_EXPECT(h, stroke.value().requires_texture_copy);
}
}
void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
{
const std::vector<int> unknown_layer { 0, 99 };
const auto unknown = plan_canvas_blend_gate(
RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer,
});
PP_EXPECT(h, unknown);
if (unknown) {
PP_EXPECT(h, unknown.value().shader_blend);
PP_EXPECT(h, unknown.value().complex_blend);
PP_EXPECT(h, unknown.value().compatibility_fallback);
PP_EXPECT(h, unknown.value().first_complex_layer_index == 1);
PP_EXPECT(h, unknown.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, unknown.value().requires_auxiliary_texture);
PP_EXPECT(h, unknown.value().requires_texture_copy);
}
const std::vector<int> normal_layers { 0 };
const auto unsupported = plan_canvas_blend_gate(
RenderDeviceFeatures {},
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 10,
});
PP_EXPECT(h, unsupported);
if (unsupported) {
PP_EXPECT(h, unsupported.value().shader_blend);
PP_EXPECT(h, unsupported.value().stroke_complex);
PP_EXPECT(h, unsupported.value().compatibility_fallback);
PP_EXPECT(h, !unsupported.value().requires_texture_copy);
}
const auto unknown_fetch = plan_canvas_blend_gate(
RenderDeviceFeatures { .framebuffer_fetch = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer,
});
PP_EXPECT(h, unknown_fetch);
if (unknown_fetch) {
PP_EXPECT(h, unknown_fetch.value().compatibility_fallback);
PP_EXPECT(h, unknown_fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, unknown_fetch.value().reads_destination_color);
PP_EXPECT(h, !unknown_fetch.value().requires_texture_copy);
}
const auto dual_pattern = plan_canvas_blend_gate(
RenderDeviceFeatures { .render_target_blit = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 16, .height = 16 },
.layer_blend_modes = normal_layers,
.dual_brush_blend = true,
.pattern_blend = true,
});
PP_EXPECT(h, dual_pattern);
if (dual_pattern) {
PP_EXPECT(h, dual_pattern.value().shader_blend);
PP_EXPECT(h, dual_pattern.value().dual_brush_complex);
PP_EXPECT(h, dual_pattern.value().pattern_complex);
PP_EXPECT(h, dual_pattern.value().requires_render_target_blit);
}
}
void plans_canvas_stroke_feedback_paths(pp::tests::Harness& h)
{
const Extent2D extent { .width = 32, .height = 16 };
const auto fetch = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .framebuffer_fetch = true },
extent);
PP_EXPECT(h, fetch);
if (fetch) {
PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, fetch.value().reads_destination_color);
PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
PP_EXPECT(h, !fetch.value().compatibility_fallback);
}
const auto copy = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .texture_copy = true },
extent);
PP_EXPECT(h, copy);
if (copy) {
PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !copy.value().reads_destination_color);
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);
}
const auto blit = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .render_target_blit = true },
extent);
PP_EXPECT(h, blit);
if (blit) {
PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, blit.value().requires_auxiliary_texture);
PP_EXPECT(h, !blit.value().requires_texture_copy);
PP_EXPECT(h, blit.value().requires_render_target_blit);
}
}
void canvas_stroke_feedback_preserves_legacy_fallback(pp::tests::Harness& h)
{
const auto fallback = plan_canvas_stroke_feedback(
RenderDeviceFeatures {},
Extent2D { .width = 32, .height = 16 });
PP_EXPECT(h, fallback);
if (fallback) {
PP_EXPECT(h, fallback.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, fallback.value().requires_auxiliary_texture);
PP_EXPECT(h, !fallback.value().requires_texture_copy);
PP_EXPECT(h, fallback.value().compatibility_fallback);
}
const auto invalid = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .texture_copy = true },
Extent2D { .width = 0, .height = 16 });
PP_EXPECT(h, !invalid.ok());
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
}
}
int main()
@@ -105,5 +449,12 @@ 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);
harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices);
harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
return harness.finish();
}

View File

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

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"
@@ -316,6 +317,52 @@ struct PlanBrushOperationArgs {
bool has_brush = true;
};
struct PlanBrushTextureListArgs {
std::string kind = "add";
std::string directory_name = "brushes";
std::string data_path = "data";
std::string source_path = "source.png";
int item_count = 1;
int current_index = 0;
int offset = 1;
bool current_is_user_texture = false;
};
struct PlanBrushStrokeControlArgs {
std::string kind = "float";
std::string setting = "tip-size";
float value = 1.0F;
bool enabled = true;
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 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;
@@ -1087,6 +1134,125 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep
return "stroke-settings-changed";
}
const char* brush_texture_list_operation_name(pp::app::BrushTextureListOperation operation) noexcept
{
switch (operation) {
case pp::app::BrushTextureListOperation::add_texture:
return "add-texture";
case pp::app::BrushTextureListOperation::remove_texture:
return "remove-texture";
case pp::app::BrushTextureListOperation::move_texture:
return "move-texture";
}
return "add-texture";
}
const char* brush_stroke_control_operation_name(pp::app::BrushStrokeControlOperation operation) noexcept
{
switch (operation) {
case pp::app::BrushStrokeControlOperation::set_float:
return "set-float";
case pp::app::BrushStrokeControlOperation::set_bool:
return "set-bool";
case pp::app::BrushStrokeControlOperation::set_blend_mode:
return "set-blend-mode";
case pp::app::BrushStrokeControlOperation::reset_tip_aspect:
return "reset-tip-aspect";
case pp::app::BrushStrokeControlOperation::reset_default_brush:
return "reset-default-brush";
}
return "set-float";
}
const char* brush_stroke_float_setting_name(pp::app::BrushStrokeFloatSetting setting) noexcept
{
switch (setting) {
case pp::app::BrushStrokeFloatSetting::tip_size: return "tip-size";
case pp::app::BrushStrokeFloatSetting::tip_spacing: return "tip-spacing";
case pp::app::BrushStrokeFloatSetting::tip_flow: return "tip-flow";
case pp::app::BrushStrokeFloatSetting::tip_opacity: return "tip-opacity";
case pp::app::BrushStrokeFloatSetting::tip_angle: return "tip-angle";
case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: return "tip-angle-smooth";
case pp::app::BrushStrokeFloatSetting::tip_mix: return "tip-mix";
case pp::app::BrushStrokeFloatSetting::tip_wet: return "tip-wet";
case pp::app::BrushStrokeFloatSetting::tip_noise: return "tip-noise";
case pp::app::BrushStrokeFloatSetting::tip_hue: return "tip-hue";
case pp::app::BrushStrokeFloatSetting::tip_saturation: return "tip-saturation";
case pp::app::BrushStrokeFloatSetting::tip_value: return "tip-value";
case pp::app::BrushStrokeFloatSetting::jitter_scale: return "jitter-scale";
case pp::app::BrushStrokeFloatSetting::jitter_angle: return "jitter-angle";
case pp::app::BrushStrokeFloatSetting::jitter_scatter: return "jitter-scatter";
case pp::app::BrushStrokeFloatSetting::jitter_flow: return "jitter-flow";
case pp::app::BrushStrokeFloatSetting::jitter_opacity: return "jitter-opacity";
case pp::app::BrushStrokeFloatSetting::jitter_hue: return "jitter-hue";
case pp::app::BrushStrokeFloatSetting::jitter_saturation: return "jitter-saturation";
case pp::app::BrushStrokeFloatSetting::jitter_value: return "jitter-value";
case pp::app::BrushStrokeFloatSetting::jitter_aspect: return "jitter-aspect";
case pp::app::BrushStrokeFloatSetting::dual_size: return "dual-size";
case pp::app::BrushStrokeFloatSetting::dual_spacing: return "dual-spacing";
case pp::app::BrushStrokeFloatSetting::dual_scatter: return "dual-scatter";
case pp::app::BrushStrokeFloatSetting::tip_aspect: return "tip-aspect";
case pp::app::BrushStrokeFloatSetting::dual_opacity: return "dual-opacity";
case pp::app::BrushStrokeFloatSetting::dual_flow: return "dual-flow";
case pp::app::BrushStrokeFloatSetting::dual_rotate: return "dual-rotate";
case pp::app::BrushStrokeFloatSetting::pattern_scale: return "pattern-scale";
case pp::app::BrushStrokeFloatSetting::pattern_brightness: return "pattern-brightness";
case pp::app::BrushStrokeFloatSetting::pattern_contrast: return "pattern-contrast";
case pp::app::BrushStrokeFloatSetting::pattern_depth: return "pattern-depth";
}
return "tip-size";
}
const char* brush_stroke_bool_setting_name(pp::app::BrushStrokeBoolSetting setting) noexcept
{
switch (setting) {
case pp::app::BrushStrokeBoolSetting::tip_angle_init: return "tip-angle-init";
case pp::app::BrushStrokeBoolSetting::tip_angle_follow: return "tip-angle-follow";
case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: return "tip-flow-pressure";
case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: return "tip-opacity-pressure";
case pp::app::BrushStrokeBoolSetting::tip_size_pressure: return "tip-size-pressure";
case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: return "jitter-scatter-both-axis";
case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: return "jitter-aspect-both-axis";
case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: return "jitter-hsv-each-sample";
case pp::app::BrushStrokeBoolSetting::tip_invert: return "tip-invert";
case pp::app::BrushStrokeBoolSetting::tip_flip_x: return "tip-flip-x";
case pp::app::BrushStrokeBoolSetting::tip_flip_y: return "tip-flip-y";
case pp::app::BrushStrokeBoolSetting::pattern_enabled: return "pattern-enabled";
case pp::app::BrushStrokeBoolSetting::dual_enabled: return "dual-enabled";
case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: return "dual-scatter-both-axis";
case pp::app::BrushStrokeBoolSetting::dual_invert: return "dual-invert";
case pp::app::BrushStrokeBoolSetting::dual_flip_x: return "dual-flip-x";
case pp::app::BrushStrokeBoolSetting::dual_flip_y: return "dual-flip-y";
case pp::app::BrushStrokeBoolSetting::dual_random_flip: return "dual-random-flip";
case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: return "tip-random-flip-x";
case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: return "tip-random-flip-y";
case pp::app::BrushStrokeBoolSetting::pattern_each_sample: return "pattern-each-sample";
case pp::app::BrushStrokeBoolSetting::pattern_invert: return "pattern-invert";
case pp::app::BrushStrokeBoolSetting::pattern_flip_x: return "pattern-flip-x";
case pp::app::BrushStrokeBoolSetting::pattern_flip_y: return "pattern-flip-y";
case pp::app::BrushStrokeBoolSetting::pattern_random_offset: return "pattern-random-offset";
}
return "tip-angle-init";
}
const char* brush_stroke_blend_setting_name(pp::app::BrushStrokeBlendSetting setting) noexcept
{
switch (setting) {
case pp::app::BrushStrokeBlendSetting::tip:
return "tip";
case pp::app::BrushStrokeBlendSetting::dual:
return "dual";
case pp::app::BrushStrokeBlendSetting::pattern:
return "pattern";
}
return "tip";
}
const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept
{
switch (operation) {
@@ -1613,6 +1779,10 @@ void print_help()
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n"
<< " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\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-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"
@@ -4283,6 +4453,634 @@ int plan_brush_operation(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_plan_brush_texture_list_args(
int argc,
char** argv,
PlanBrushTextureListArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--kind" || key == "--dir" || key == "--data-path" || key == "--source") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
if (key == "--kind") {
args.kind = argv[++i];
} else if (key == "--dir") {
args.directory_name = argv[++i];
} else if (key == "--data-path") {
args.data_path = argv[++i];
} else {
args.source_path = argv[++i];
}
} else if (key == "--item-count" || key == "--current-index" || key == "--offset") {
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 == "--item-count") {
args.item_count = value.value();
} else if (key == "--current-index") {
args.current_index = value.value();
} else {
args.offset = value.value();
}
} else if (key == "--user-texture") {
args.current_is_user_texture = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Result<pp::app::BrushTextureListPlan> make_brush_texture_list_plan(
const PlanBrushTextureListArgs& args)
{
if (args.kind == "add") {
return pp::app::plan_brush_texture_list_add(args.directory_name, args.data_path, args.source_path);
}
if (args.kind == "remove") {
return pp::app::plan_brush_texture_list_remove(
args.item_count,
args.current_index,
args.current_is_user_texture);
}
if (args.kind == "move" || args.kind == "up" || args.kind == "down") {
const int offset = args.kind == "up" ? -1 : (args.kind == "down" ? 1 : args.offset);
return pp::app::plan_brush_texture_list_move(args.item_count, args.current_index, offset);
}
return pp::foundation::Result<pp::app::BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("unknown brush texture list operation kind"));
}
int plan_brush_texture_list(int argc, char** argv)
{
PlanBrushTextureListArgs args;
const auto status = parse_plan_brush_texture_list_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-brush-texture-list", status.message);
return 2;
}
const auto plan = make_brush_texture_list_plan(args);
if (!plan) {
print_error("plan-brush-texture-list", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-brush-texture-list\""
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
<< "\",\"dir\":\"" << json_escape(args.directory_name)
<< "\",\"dataPath\":\"" << json_escape(args.data_path)
<< "\",\"source\":\"" << json_escape(args.source_path)
<< "\",\"itemCount\":" << args.item_count
<< ",\"currentIndex\":" << args.current_index
<< ",\"offset\":" << args.offset
<< ",\"currentIsUserTexture\":" << json_bool(args.current_is_user_texture)
<< "},\"plan\":{\"operation\":\"" << brush_texture_list_operation_name(value.operation)
<< "\",\"itemCount\":" << value.item_count
<< ",\"currentIndex\":" << value.current_index
<< ",\"targetIndex\":" << value.target_index
<< ",\"moveOffset\":" << value.move_offset
<< ",\"source\":\"" << json_escape(value.source_path)
<< "\",\"path\":\"" << json_escape(value.high_path)
<< "\",\"thumb\":\"" << json_escape(value.thumbnail_path)
<< "\",\"brushName\":\"" << json_escape(value.brush_name)
<< "\",\"userTexture\":" << json_bool(value.user_texture)
<< ",\"deletesTextureFiles\":" << json_bool(value.deletes_texture_files)
<< ",\"savesList\":" << json_bool(value.saves_list)
<< ",\"notifiesSelection\":" << json_bool(value.notifies_selection)
<< ",\"convertsBrushAlpha\":" << json_bool(value.converts_brush_alpha)
<< ",\"noOp\":" << json_bool(value.no_op)
<< "}}\n";
return 0;
}
pp::foundation::Result<pp::app::BrushStrokeFloatSetting> parse_brush_stroke_float_setting(
std::string_view setting)
{
static constexpr std::array<std::pair<std::string_view, pp::app::BrushStrokeFloatSetting>, 32> settings{ {
{ "tip-size", pp::app::BrushStrokeFloatSetting::tip_size },
{ "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing },
{ "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow },
{ "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity },
{ "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle },
{ "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth },
{ "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix },
{ "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet },
{ "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise },
{ "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue },
{ "tip-saturation", pp::app::BrushStrokeFloatSetting::tip_saturation },
{ "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation },
{ "tip-value", pp::app::BrushStrokeFloatSetting::tip_value },
{ "tip-val", pp::app::BrushStrokeFloatSetting::tip_value },
{ "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale },
{ "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle },
{ "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter },
{ "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow },
{ "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity },
{ "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue },
{ "jitter-saturation", pp::app::BrushStrokeFloatSetting::jitter_saturation },
{ "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation },
{ "jitter-value", pp::app::BrushStrokeFloatSetting::jitter_value },
{ "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value },
{ "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect },
{ "dual-size", pp::app::BrushStrokeFloatSetting::dual_size },
{ "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing },
{ "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter },
{ "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect },
{ "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity },
{ "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow },
{ "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate },
} };
for (const auto& entry : settings) {
if (setting == entry.first) {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(entry.second);
}
}
if (setting == "pattern-scale") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_scale);
}
if (setting == "pattern-brightness") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_brightness);
}
if (setting == "pattern-contrast") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_contrast);
}
if (setting == "pattern-depth") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_depth);
}
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke float setting"));
}
pp::foundation::Result<pp::app::BrushStrokeBoolSetting> parse_brush_stroke_bool_setting(
std::string_view setting)
{
static constexpr std::array<std::pair<std::string_view, pp::app::BrushStrokeBoolSetting>, 25> settings{ {
{ "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init },
{ "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow },
{ "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure },
{ "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure },
{ "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure },
{ "jitter-scatter-both-axis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis },
{ "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis },
{ "jitter-aspect-both-axis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis },
{ "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis },
{ "jitter-hsv-each-sample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample },
{ "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample },
{ "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert },
{ "tip-flip-x", pp::app::BrushStrokeBoolSetting::tip_flip_x },
{ "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x },
{ "tip-flip-y", pp::app::BrushStrokeBoolSetting::tip_flip_y },
{ "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y },
{ "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled },
{ "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled },
{ "dual-scatter-both-axis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis },
{ "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis },
{ "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert },
{ "dual-flip-x", pp::app::BrushStrokeBoolSetting::dual_flip_x },
{ "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x },
{ "dual-flip-y", pp::app::BrushStrokeBoolSetting::dual_flip_y },
{ "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y },
} };
for (const auto& entry : settings) {
if (setting == entry.first) {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(entry.second);
}
}
if (setting == "dual-random-flip" || setting == "dual-randflip") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::dual_random_flip);
}
if (setting == "tip-random-flip-x" || setting == "tip-randflipx") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::tip_random_flip_x);
}
if (setting == "tip-random-flip-y" || setting == "tip-randflipy") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::tip_random_flip_y);
}
if (setting == "pattern-each-sample" || setting == "pattern-eachsample") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_each_sample);
}
if (setting == "pattern-invert") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_invert);
}
if (setting == "pattern-flip-x" || setting == "pattern-flipx") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_flip_x);
}
if (setting == "pattern-flip-y" || setting == "pattern-flipy") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_flip_y);
}
if (setting == "pattern-random-offset" || setting == "pattern-rand-offset") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_random_offset);
}
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke bool setting"));
}
pp::foundation::Result<pp::app::BrushStrokeBlendSetting> parse_brush_stroke_blend_setting(
std::string_view setting)
{
if (setting == "tip" || setting == "brush") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::tip);
}
if (setting == "dual") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::dual);
}
if (setting == "pattern") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::pattern);
}
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke blend setting"));
}
pp::foundation::Status parse_plan_brush_stroke_control_args(
int argc,
char** argv,
PlanBrushStrokeControlArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--kind" || key == "--setting") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
if (key == "--kind") {
args.kind = argv[++i];
} else {
args.setting = argv[++i];
}
} else if (key == "--value") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_float_arg(argv[++i]);
if (!value) {
return value.status();
}
args.value = value.value();
} else if (key == "--blend-mode") {
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();
}
args.blend_mode = value.value();
} else if (key == "--enabled") {
args.enabled = true;
} else if (key == "--disabled") {
args.enabled = false;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Result<pp::app::BrushStrokeControlPlan> make_brush_stroke_control_plan(
const PlanBrushStrokeControlArgs& args)
{
if (args.kind == "float" || args.kind == "slider") {
const auto setting = parse_brush_stroke_float_setting(args.setting);
if (!setting) {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::app::plan_brush_stroke_float_setting(setting.value(), args.value);
}
if (args.kind == "bool" || args.kind == "toggle" || args.kind == "checkbox") {
const auto setting = parse_brush_stroke_bool_setting(args.setting);
if (!setting) {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_stroke_bool_setting(setting.value(), args.enabled));
}
if (args.kind == "blend" || args.kind == "blend-mode") {
const auto setting = parse_brush_stroke_blend_setting(args.setting);
if (!setting) {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::app::plan_brush_stroke_blend_mode(setting.value(), args.blend_mode);
}
if (args.kind == "tip-aspect-reset") {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_tip_aspect_reset());
}
if (args.kind == "default-reset" || args.kind == "reset") {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_default_settings_reset());
}
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke control kind"));
}
int plan_brush_stroke_control(int argc, char** argv)
{
PlanBrushStrokeControlArgs args;
const auto status = parse_plan_brush_stroke_control_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-brush-stroke-control", status.message);
return 2;
}
const auto plan = make_brush_stroke_control_plan(args);
if (!plan) {
print_error("plan-brush-stroke-control", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-brush-stroke-control\""
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
<< "\",\"setting\":\"" << json_escape(args.setting)
<< "\",\"value\":" << args.value
<< ",\"enabled\":" << json_bool(args.enabled)
<< ",\"blendMode\":" << args.blend_mode
<< "},\"plan\":{\"operation\":\"" << brush_stroke_control_operation_name(value.operation)
<< "\",\"floatSetting\":\"" << brush_stroke_float_setting_name(value.float_setting)
<< "\",\"boolSetting\":\"" << brush_stroke_bool_setting_name(value.bool_setting)
<< "\",\"blendSetting\":\"" << brush_stroke_blend_setting_name(value.blend_setting)
<< "\",\"floatValue\":" << value.float_value
<< ",\"boolValue\":" << json_bool(value.bool_value)
<< ",\"blendMode\":" << value.blend_mode
<< ",\"mutatesBrush\":" << json_bool(value.mutates_brush)
<< ",\"updatesControls\":" << json_bool(value.updates_controls)
<< ",\"refreshesPreview\":" << json_bool(value.refreshes_preview)
<< ",\"notifiesStrokeChange\":" << json_bool(value.notifies_stroke_change)
<< "}}\n";
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_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,
@@ -7405,6 +8203,22 @@ int main(int argc, char** argv)
return plan_brush_operation(argc, argv);
}
if (command == "plan-brush-texture-list") {
return plan_brush_texture_list(argc, argv);
}
if (command == "plan-brush-stroke-control") {
return plan_brush_stroke_control(argc, argv);
}
if (command == "plan-paint-feedback") {
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);
}