Plan canvas stroke feedback copies

This commit is contained in:
2026-06-03 18:52:37 +02:00
parent b576143afb
commit 2ac2c45b11
8 changed files with 174 additions and 14 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 and destination-copy 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, 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 | | 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 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 | | 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 ## Closed Debt

View File

@@ -888,6 +888,12 @@ 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
individual `ShaderManager` extension booleans. 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 The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.
@@ -1623,6 +1629,10 @@ Results:
shared canvas blend-gate plan to decide whether they can read destination shared canvas blend-gate plan to decide whether they can read destination
color through framebuffer fetch or must copy the destination texture before color through framebuffer fetch or must copy the destination texture before
the legacy OpenGL blend draw. 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 - 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,6 +49,27 @@ pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept
return ShaderManager::render_device_features(); 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( pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
int width, int width,
int height, int height,
@@ -464,9 +485,12 @@ std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(std::array<vert
return ret; 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); set_active_texture_unit(1);
m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing) m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
@@ -484,7 +508,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
glm::vec2 pad(1); glm::vec2 pad(1);
glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height }); 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)); 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, glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y,
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y); tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
@@ -512,7 +536,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
} }
m_brush_shape.draw_fill(); m_brush_shape.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
set_active_texture_unit(1); set_active_texture_unit(1);
m_tex[i].unbind(); m_tex[i].unbind();
@@ -630,10 +654,13 @@ void Canvas::stroke_draw()
if (brush->m_pattern_flipx) patt_scale.x *= -1.f; if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
if (brush->m_pattern_flipy) patt_scale.y *= -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()); glDisable(blend_state());
ShaderManager::use(kShader::Stroke); ShaderManager::use(kShader::Stroke);
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush 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::TexBG, 1); // bg
ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern
ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer
@@ -691,7 +718,7 @@ void Canvas::stroke_draw()
ShaderManager::u_vec4(kShaderUniform::Col, f.col); ShaderManager::u_vec4(kShaderUniform::Col, f.col);
ShaderManager::u_float(kShaderUniform::Alpha, f.flow); ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); 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(); m_tmp[i].unbindFramebuffer();
@@ -718,7 +745,7 @@ void Canvas::stroke_draw()
// work on documents that doesn't have the padding, so on document loading. // work on documents that doesn't have the padding, so on document loading.
ShaderManager::use(kShader::StrokePad); ShaderManager::use(kShader::StrokePad);
ShaderManager::u_vec4(kShaderUniform::Col, pad_color); ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
set_active_texture_unit(1); set_active_texture_unit(1);
ShaderManager::u_int(kShaderUniform::TexBG, 1); ShaderManager::u_int(kShaderUniform::TexBG, 1);
@@ -748,7 +775,7 @@ void Canvas::stroke_draw()
m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size()); m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());
m_tmp[i].bindFramebuffer(); 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 o = glm::max({0, 0}, xy(b) - pad);
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o; glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
@@ -759,7 +786,7 @@ void Canvas::stroke_draw()
m_brush_shape.draw_fill(); m_brush_shape.draw_fill();
m_tmp[i].unbindFramebuffer(); m_tmp[i].unbindFramebuffer();
} }
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
unbind_texture_2d(); unbind_texture_2d();
} }
@@ -790,7 +817,7 @@ void Canvas::stroke_draw()
if (P.size() < 3) if (P.size() < 3)
continue; continue;
m_tmp_dual[i].bindFramebuffer(); 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(); m_tmp_dual[i].unbindFramebuffer();
// this mode overflows the main brush boundries // this mode overflows the main brush boundries

View File

@@ -205,7 +205,7 @@ public:
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz); 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; 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} // 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; std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
void stroke_draw(); void stroke_draw();
void stroke_end(); void stroke_end();

View File

@@ -110,12 +110,21 @@ namespace {
void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept
{ {
gate.path = stroke.path; gate.path = stroke.path;
gate.reads_destination_color = stroke.reads_destination_color; gate.reads_destination_color = stroke.path == StrokeCompositePath::framebuffer_fetch;
gate.requires_auxiliary_texture = stroke.requires_auxiliary_texture; gate.requires_auxiliary_texture = stroke.requires_auxiliary_texture;
gate.requires_texture_copy = stroke.requires_texture_copy; gate.requires_texture_copy = stroke.requires_texture_copy;
gate.requires_render_target_blit = stroke.requires_render_target_blit; 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( void mark_shader_blend_fallback(
CanvasBlendGatePlan& gate, CanvasBlendGatePlan& gate,
pp::renderer::RenderDeviceFeatures features) noexcept pp::renderer::RenderDeviceFeatures features) noexcept
@@ -303,6 +312,45 @@ pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate); 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 const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
{ {
switch (path) { switch (path) {

View File

@@ -74,6 +74,15 @@ struct CanvasBlendGatePlan {
bool requires_render_target_blit = 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( [[nodiscard]] pp::foundation::Status composite_layer(
std::span<pp::paint::Rgba> destination, std::span<pp::paint::Rgba> destination,
pp::renderer::Extent2D extent, pp::renderer::Extent2D extent,
@@ -93,6 +102,10 @@ struct CanvasBlendGatePlan {
pp::renderer::RenderDeviceFeatures features, pp::renderer::RenderDeviceFeatures features,
CanvasBlendGateRequest request) noexcept; 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; [[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
} }

View File

@@ -15,6 +15,7 @@ using pp::paint_renderer::StrokeCompositePath;
using pp::paint_renderer::StrokeCompositeRequest; using pp::paint_renderer::StrokeCompositeRequest;
using pp::paint_renderer::composite_layer; using pp::paint_renderer::composite_layer;
using pp::paint_renderer::plan_canvas_blend_gate; 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::plan_stroke_composite;
using pp::paint_renderer::stroke_composite_path_name; using pp::paint_renderer::stroke_composite_path_name;
using pp::paint_renderer::stroke_composite_requires_feedback; using pp::paint_renderer::stroke_composite_requires_feedback;
@@ -309,6 +310,7 @@ void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
PP_EXPECT(h, stroke.value().stroke_complex); PP_EXPECT(h, stroke.value().stroke_complex);
PP_EXPECT(h, stroke.value().first_complex_layer_index == -1); 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().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !stroke.value().reads_destination_color);
PP_EXPECT(h, stroke.value().requires_texture_copy); PP_EXPECT(h, stroke.value().requires_texture_copy);
} }
} }
@@ -381,6 +383,64 @@ void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
} }
} }
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() int main()
@@ -394,5 +454,7 @@ int main()
harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans); 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("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("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(); return harness.finish();
} }