Use blend gate plan for canvas copy decisions

This commit is contained in:
2026-06-03 18:37:58 +02:00
parent bc5b39057d
commit b576143afb
7 changed files with 73 additions and 19 deletions

View File

@@ -37,7 +37,7 @@ and validation command.
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture | | PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter | | Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden | | 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, 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 and destination-copy coverage, and GPU parity |
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects | | Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation ## Layers And Animation

View File

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

View File

@@ -882,7 +882,8 @@ the app calls through renderer services for the whole compositing path.
mapping from persisted layer and brush blend indices to that planner, including mapping from persisted layer and brush blend indices to that planner, including
fallback behavior for unknown nonzero indices. Both `Canvas::draw_merge` and fallback behavior for unknown nonzero indices. Both `Canvas::draw_merge` and
`NodeCanvas` panorama rendering consume that shared gate, so the live app no `NodeCanvas` panorama rendering consume that shared gate, so the live app no
longer has duplicate local blend-trigger logic. 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 The OpenGL shader initialization path now stores a renderer-neutral
`RenderDeviceFeatures` snapshot converted by `pp_renderer_gl`, and those live `RenderDeviceFeatures` snapshot converted by `pp_renderer_gl`, and those live
canvas gates consume that snapshot instead of rebuilding feature flags from canvas gates consume that snapshot instead of rebuilding feature flags from
@@ -1618,6 +1619,10 @@ Results:
feature snapshot through the legacy shader manager, and live canvas blend feature snapshot through the legacy shader manager, and live canvas blend
gates consume that `RenderDeviceFeatures` value instead of hand-built gates consume that `RenderDeviceFeatures` value instead of hand-built
framebuffer-fetch/texture-copy flags. 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 equirectangular import drawing and depth export rendering now route - Canvas equirectangular import drawing and depth export rendering now route
depth/blend state and active texture units through the renderer GL backend depth/blend state and active texture units through the renderer GL backend
mapping. mapping.

View File

@@ -49,7 +49,7 @@ pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept
return ShaderManager::render_device_features(); return ShaderManager::render_device_features();
} }
bool draw_merge_needs_shader_blend( pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
int width, int width,
int height, int height,
const std::vector<std::shared_ptr<Layer>>& layers, const std::vector<std::shared_ptr<Layer>>& layers,
@@ -75,7 +75,15 @@ bool draw_merge_needs_shader_blend(
.has_stroke_blend_mode = brush != nullptr, .has_stroke_blend_mode = brush != nullptr,
.stroke_blend_mode = brush ? brush->m_blend_mode : 0, .stroke_blend_mode = brush ? brush->m_blend_mode : 0,
}); });
return plan ? plan.value().shader_blend : true; 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() GLenum unsigned_byte_component_type()
@@ -1121,11 +1129,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); 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; const auto& b = m_current_stroke->m_brush;
const bool use_blend = draw_merge_needs_shader_blend( const auto blend_gate = draw_merge_blend_gate_plan(
m_width, m_width,
m_height, m_height,
m_layers, m_layers,
m_current_stroke ? m_current_stroke->m_brush.get() : nullptr); 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 // if not using shader blend, use gl rasterizer blend
glDisable(depth_test_state()); glDisable(depth_test_state());
@@ -1289,7 +1299,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_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, 1.f); ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
ShaderManager::u_mat4(kShaderUniform::MVP, ortho); ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
m_sampler.bind(2); m_sampler.bind(2);
ShaderManager::u_int(kShaderUniform::TexBG, 2); ShaderManager::u_int(kShaderUniform::TexBG, 2);
@@ -1297,7 +1307,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
set_active_texture_unit(0); set_active_texture_unit(0);
m_merge_rtt.bindTexture(); m_merge_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_merge_tex.bind(); m_merge_tex.bind();
@@ -1306,7 +1316,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
m_plane.draw_fill(); m_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_merge_tex.unbind(); m_merge_tex.unbind();

View File

@@ -32,7 +32,7 @@ pp::renderer::RenderDeviceFeatures node_canvas_stroke_composite_features() noexc
return ShaderManager::render_device_features(); return ShaderManager::render_device_features();
} }
bool node_canvas_needs_shader_blend( pp::paint_renderer::CanvasBlendGatePlan node_canvas_blend_gate_plan(
int width, int width,
int height, int height,
const std::vector<std::shared_ptr<Layer>>& layers, const std::vector<std::shared_ptr<Layer>>& layers,
@@ -58,7 +58,15 @@ bool node_canvas_needs_shader_blend(
.has_stroke_blend_mode = brush != nullptr, .has_stroke_blend_mode = brush != nullptr,
.stroke_blend_mode = brush ? brush->m_blend_mode : 0, .stroke_blend_mode = brush ? brush->m_blend_mode : 0,
}); });
return plan ? plan.value().shader_blend : true; 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() void run_history_undo_if_available()
@@ -290,11 +298,13 @@ void NodeCanvas::draw()
} }
else else
{ {
const bool use_blend = node_canvas_needs_shader_blend( const auto blend_gate = node_canvas_blend_gate_plan(
m_cache_rtt.getWidth(), m_cache_rtt.getWidth(),
m_cache_rtt.getHeight(), m_cache_rtt.getHeight(),
m_canvas->m_layers, m_canvas->m_layers,
m_canvas->m_current_stroke ? m_canvas->m_current_stroke->m_brush.get() : nullptr); 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) if (use_blend)
{ {
@@ -485,7 +495,7 @@ void NodeCanvas::draw()
ShaderManager::use(kShader::TextureBlend); ShaderManager::use(kShader::TextureBlend);
ShaderManager::u_int(kShaderUniform::Tex, 0); 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::TexBG, 2);
ShaderManager::u_int(kShaderUniform::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode); ShaderManager::u_int(kShaderUniform::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, 1.f); ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
@@ -493,7 +503,7 @@ void NodeCanvas::draw()
set_active_texture_unit(0); set_active_texture_unit(0);
m_blender_rtt.bindTexture(); m_blender_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_blender_bg.bind(); m_blender_bg.bind();
@@ -503,7 +513,7 @@ void NodeCanvas::draw()
m_face_plane.draw_fill(); m_face_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_blender_bg.unbind(); m_blender_bg.unbind();

View File

@@ -116,11 +116,22 @@ void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& str
gate.requires_render_target_blit = stroke.requires_render_target_blit; gate.requires_render_target_blit = stroke.requires_render_target_blit;
} }
void mark_shader_blend_fallback(CanvasBlendGatePlan& gate) noexcept void mark_shader_blend_fallback(
CanvasBlendGatePlan& gate,
pp::renderer::RenderDeviceFeatures features) noexcept
{ {
gate.shader_blend = true; gate.shader_blend = true;
gate.complex_blend = true; gate.complex_blend = true;
gate.compatibility_fallback = 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;
}
} }
} }
@@ -227,7 +238,7 @@ pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
if (!paint_blend_mode_from_persisted_index(request.layer_blend_modes[i], layer_blend)) { if (!paint_blend_mode_from_persisted_index(request.layer_blend_modes[i], layer_blend)) {
if (request.layer_blend_modes[i] != 0) { if (request.layer_blend_modes[i] != 0) {
gate.first_complex_layer_index = static_cast<int>(i); gate.first_complex_layer_index = static_cast<int>(i);
mark_shader_blend_fallback(gate); mark_shader_blend_fallback(gate, features);
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate); return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
} }
continue; continue;
@@ -259,7 +270,7 @@ pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
if (!stroke_blend_mode_from_persisted_index(request.stroke_blend_mode, stroke_blend)) { if (!stroke_blend_mode_from_persisted_index(request.stroke_blend_mode, stroke_blend)) {
if (request.stroke_blend_mode != 0) { if (request.stroke_blend_mode != 0) {
gate.stroke_complex = true; gate.stroke_complex = true;
mark_shader_blend_fallback(gate); mark_shader_blend_fallback(gate, features);
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate); return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
} }
} else if (stroke_blend != pp::paint::StrokeBlendMode::normal) { } else if (stroke_blend != pp::paint::StrokeBlendMode::normal) {

View File

@@ -317,7 +317,7 @@ void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
{ {
const std::vector<int> unknown_layer { 0, 99 }; const std::vector<int> unknown_layer { 0, 99 };
const auto unknown = plan_canvas_blend_gate( const auto unknown = plan_canvas_blend_gate(
RenderDeviceFeatures {}, RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest { CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 }, .extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer, .layer_blend_modes = unknown_layer,
@@ -328,6 +328,9 @@ void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
PP_EXPECT(h, unknown.value().complex_blend); PP_EXPECT(h, unknown.value().complex_blend);
PP_EXPECT(h, unknown.value().compatibility_fallback); PP_EXPECT(h, unknown.value().compatibility_fallback);
PP_EXPECT(h, unknown.value().first_complex_layer_index == 1); 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 std::vector<int> normal_layers { 0 };
@@ -344,6 +347,21 @@ void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
PP_EXPECT(h, unsupported.value().shader_blend); PP_EXPECT(h, unsupported.value().shader_blend);
PP_EXPECT(h, unsupported.value().stroke_complex); PP_EXPECT(h, unsupported.value().stroke_complex);
PP_EXPECT(h, unsupported.value().compatibility_fallback); 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( const auto dual_pattern = plan_canvas_blend_gate(