Centralize canvas blend gate planning

This commit is contained in:
2026-06-03 18:20:01 +02:00
parent a89f5e6cf2
commit 1369a9048e
8 changed files with 327 additions and 86 deletions

View File

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

View File

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

View File

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

View File

@@ -44,36 +44,6 @@ GLenum rgba_pixel_format()
return static_cast<GLenum>(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::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(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<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;
}
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::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,
});
return plan ? plan.value().shader_blend : true;
}
GLenum unsigned_byte_component_type()

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,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<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,
});
return plan ? plan.value().shader_blend : true;
}
void run_history_undo_if_available()
{
const auto plan = pp::app::plan_history_undo(static_cast<int>(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)
{

View File

@@ -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<std::size_t> 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<StrokeCompositePlan> plan_stroke_composite(
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);
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);
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);
}
const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
{
switch (path) {

View File

@@ -50,6 +50,30 @@ struct StrokeCompositePlan {
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;
};
[[nodiscard]] pp::foundation::Status composite_layer(
std::span<pp::paint::Rgba> destination,
pp::renderer::Extent2D extent,
@@ -65,6 +89,10 @@ struct StrokeCompositePlan {
pp::renderer::RenderDeviceFeatures features,
StrokeCompositeRequest request) noexcept;
[[nodiscard]] pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
pp::renderer::RenderDeviceFeatures features,
CanvasBlendGateRequest request) noexcept;
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
}

View File

@@ -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<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().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 {},
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<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);
}
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();
}