diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 9d8481d..8b5c518 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -37,7 +37,7 @@ and validation command. | 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, 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 draw-merge gate coverage, and GPU parity | +| 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, and GPU parity | | Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects | ## Layers And Animation diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 8856432..22780d5 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -53,7 +53,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | -| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `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. The live `Canvas::draw_merge` shader-blend gate now calls `pp_paint_renderer::plan_stroke_composite` for the existing layer and primary-brush blend decision, but live stroke compositing, dual-brush feedback, and pattern feedback still use the legacy OpenGL canvas path | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `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 | +| 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. `pp_paint_renderer::plan_canvas_blend_gate` now 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 for their existing shader-blend gates. Actual live stroke compositing, dual-brush feedback, and pattern feedback still use the legacy OpenGL canvas path | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `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 diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index d4e55d1..cf7e135 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -878,6 +878,11 @@ 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. The existing renderer classes are not yet fully behind the renderer interfaces. @@ -1602,6 +1607,9 @@ Results: `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. - Canvas equirectangular import drawing and depth export rendering now route depth/blend state and active texture units through the renderer GL backend mapping. diff --git a/src/canvas.cpp b/src/canvas.cpp index 0219fe9..108e5d1 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -44,36 +44,6 @@ GLenum rgba_pixel_format() return static_cast(pp::renderer::gl::rgba_pixel_format()); } -bool to_paint_blend_mode(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; - } -} - -bool to_stroke_blend_mode(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; - } -} - pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept { return pp::renderer::RenderDeviceFeatures { @@ -82,64 +52,33 @@ pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept }; } -bool stroke_composite_plan_needs_shader_blend( - int width, - int height, - pp::paint::BlendMode layer_blend_mode, - pp::paint::StrokeBlendMode stroke_blend_mode) noexcept -{ - const auto plan = pp::paint_renderer::plan_stroke_composite( - canvas_stroke_composite_features(), - pp::paint_renderer::StrokeCompositeRequest { - .extent = pp::renderer::Extent2D { - .width = static_cast(std::max(width, 0)), - .height = static_cast(std::max(height, 0)), - }, - .layer_blend_mode = layer_blend_mode, - .stroke_blend_mode = stroke_blend_mode, - }); - return plan ? plan.value().complex_blend : true; -} - bool draw_merge_needs_shader_blend( int width, int height, const std::vector>& layers, const Brush* brush) noexcept { + std::vector layer_blend_modes; + layer_blend_modes.reserve(layers.size()); for (const auto& layer : layers) { if (!layer) { continue; } - pp::paint::BlendMode layer_blend = pp::paint::BlendMode::normal; - if (!to_paint_blend_mode(layer->m_blend_mode, layer_blend)) { - if (layer->m_blend_mode != 0) { - return true; - } - continue; - } - if (stroke_composite_plan_needs_shader_blend( - width, - height, - layer_blend, - pp::paint::StrokeBlendMode::normal)) { - return true; - } + layer_blend_modes.push_back(layer->m_blend_mode); } - if (brush) { - pp::paint::StrokeBlendMode stroke_blend = pp::paint::StrokeBlendMode::normal; - if (!to_stroke_blend_mode(brush->m_blend_mode, stroke_blend)) { - return brush->m_blend_mode != 0; - } - return stroke_composite_plan_needs_shader_blend( - width, - height, - pp::paint::BlendMode::normal, - stroke_blend); - } - - return false; + 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::max(width, 0)), + .height = static_cast(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, + }); + return plan ? plan.value().shader_blend : true; } GLenum unsigned_byte_component_type() diff --git a/src/node_canvas.cpp b/src/node_canvas.cpp index 782f2fb..03f09f2 100644 --- a/src/node_canvas.cpp +++ b/src/node_canvas.cpp @@ -1,6 +1,9 @@ #include "pch.h" +#include #include +#include +#include #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,43 @@ void unbind_texture_2d() glBindTexture(pp::renderer::gl::texture_2d_target(), 0); } +pp::renderer::RenderDeviceFeatures node_canvas_stroke_composite_features() noexcept +{ + return pp::renderer::RenderDeviceFeatures { + .framebuffer_fetch = ShaderManager::ext_framebuffer_fetch, + .texture_copy = !ShaderManager::ext_framebuffer_fetch, + }; +} + +bool node_canvas_needs_shader_blend( + int width, + int height, + const std::vector>& layers, + const Brush* brush) noexcept +{ + std::vector 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::max(width, 0)), + .height = static_cast(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, + }); + return plan ? plan.value().shader_blend : true; +} + void run_history_undo_if_available() { const auto plan = pp::app::plan_history_undo(static_cast(ActionManager::I.m_actions.size())); @@ -252,14 +293,11 @@ 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 bool use_blend = node_canvas_needs_shader_blend( + 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); if (use_blend) { diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp index 1474565..55d0b1f 100644 --- a/src/paint_renderer/compositor.cpp +++ b/src/paint_renderer/compositor.cpp @@ -40,6 +40,36 @@ namespace { 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 expected_pixel_count(pp::renderer::Extent2D extent) noexcept { const auto extent_status = pp::renderer::validate_extent(extent); @@ -77,6 +107,22 @@ namespace { return StrokeCompositePath::fixed_function_blend; } +void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept +{ + gate.path = stroke.path; + gate.reads_destination_color = stroke.reads_destination_color; + 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 mark_shader_blend_fallback(CanvasBlendGatePlan& gate) noexcept +{ + gate.shader_blend = true; + gate.complex_blend = true; + gate.compatibility_fallback = true; +} + } pp::foundation::Status composite_layer( @@ -170,6 +216,82 @@ pp::foundation::Result plan_stroke_composite( return pp::foundation::Result::success(plan); } +pp::foundation::Result 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(i); + mark_shader_blend_fallback(gate); + return pp::foundation::Result::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(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::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); + return pp::foundation::Result::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::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::success(gate); +} + const char* stroke_composite_path_name(StrokeCompositePath path) noexcept { switch (path) { diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 37defd6..8546b91 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -50,6 +50,30 @@ struct StrokeCompositePlan { bool requires_explicit_transition = false; }; +struct CanvasBlendGateRequest { + pp::renderer::Extent2D extent {}; + std::span 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; +}; + [[nodiscard]] pp::foundation::Status composite_layer( std::span destination, pp::renderer::Extent2D extent, @@ -65,6 +89,10 @@ struct StrokeCompositePlan { pp::renderer::RenderDeviceFeatures features, StrokeCompositeRequest request) noexcept; +[[nodiscard]] pp::foundation::Result plan_canvas_blend_gate( + pp::renderer::RenderDeviceFeatures features, + CanvasBlendGateRequest request) noexcept; + [[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept; } diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 8853e9f..0b5ed38 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -10,9 +10,11 @@ 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_stroke_composite; using pp::paint_renderer::stroke_composite_path_name; using pp::paint_renderer::stroke_composite_requires_feedback; @@ -259,6 +261,108 @@ void rejects_bad_stroke_composite_plans(pp::tests::Harness& h) PP_EXPECT(h, stroke_composite_path_name(static_cast(255)) == std::string_view("unknown")); } +void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) +{ + const std::vector 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 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().requires_texture_copy); + } +} + +void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h) +{ + const std::vector unknown_layer { 0, 99 }; + const auto unknown = plan_canvas_blend_gate( + RenderDeviceFeatures {}, + 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); + } + + const std::vector 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); + } + + 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); + } +} + } int main() @@ -270,5 +374,7 @@ int main() 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); return harness.finish(); }