diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index f7e8329..33fecde 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,12 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` final preview + background capture, composite input binding/draw, and preview texture copy now + route through `legacy_canvas_stroke_preview_services.h`, with semantic preview + composite ordering and texture-slot intent covered by `pp_paint_renderer` + tests. Static preview RTT/texture ownership, checkerboard shader setup, + framebuffer copies, and retained GL callbacks remain in legacy preview code. - 2026-06-13: DEBT-0036 was narrowed again. Stroke sample copy bounds, live face dirty-box accumulation, and preview padding region math now live as tested `pp_paint_renderer` planners and are consumed by retained Canvas and diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 7a09bec..eab7887 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1359,6 +1359,10 @@ callback execution under `DEBT-0036`. Stroke sample copy bounds, live face dirty-box accumulation, and preview padding region math now live as tested `pp_paint_renderer` planners while retained Canvas/preview code still owns GL ordering, RTT/texture binding, history mutation, and final dirty state storage. +`NodeStrokePreview` also now consumes a tested preview composite sequence plan +for mixer intent, and retained preview background capture, composite input +binding/draw, and preview texture copy are centralized behind +`legacy_canvas_stroke_preview_services.h`. It also owns renderer API texture-format to OpenGL internal/pixel/component token mapping, including depth-stencil formats, for future backend texture objects. `Texture2D` 2D texture binding, upload, diff --git a/src/legacy_canvas_stroke_preview_services.h b/src/legacy_canvas_stroke_preview_services.h new file mode 100644 index 0000000..f813a8a --- /dev/null +++ b/src/legacy_canvas_stroke_preview_services.h @@ -0,0 +1,55 @@ +#pragma once + +namespace pp::panopainter { + +struct LegacyStrokePreviewCopySize { + int width = 0; + int height = 0; +}; + +template < + typename SetupCheckerboard, + typename DrawPlane, + typename BindBackgroundTexture, + typename CopyFramebufferToTexture> +void execute_legacy_stroke_preview_background_capture( + SetupCheckerboard&& setup_checkerboard, + DrawPlane&& draw_plane, + BindBackgroundTexture&& bind_background_texture, + CopyFramebufferToTexture&& copy_framebuffer_to_texture, + LegacyStrokePreviewCopySize copy_size) +{ + setup_checkerboard(); + draw_plane(); + bind_background_texture(); + copy_framebuffer_to_texture(0, 0, 0, 0, copy_size.width, copy_size.height); +} + +template < + typename SetupCompositeShader, + typename BindCompositeSamplers, + typename BindCompositeInputs, + typename DrawPlane> +void execute_legacy_stroke_preview_final_composite( + SetupCompositeShader&& setup_composite_shader, + BindCompositeSamplers&& bind_composite_samplers, + BindCompositeInputs&& bind_composite_inputs, + DrawPlane&& draw_plane) +{ + setup_composite_shader(); + bind_composite_samplers(); + bind_composite_inputs(); + draw_plane(); +} + +template +void copy_legacy_stroke_preview_texture( + BindPreviewTexture&& bind_preview_texture, + CopyFramebufferToTexture&& copy_framebuffer_to_texture, + LegacyStrokePreviewCopySize copy_size) +{ + bind_preview_texture(); + copy_framebuffer_to_texture(0, 0, 0, 0, copy_size.width, copy_size.height); +} + +} // namespace pp::panopainter diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 99c94b6..09f872a 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -8,6 +8,7 @@ #include "app.h" #include "legacy_canvas_stroke_composite_services.h" #include "legacy_canvas_stroke_execution_services.h" +#include "legacy_canvas_stroke_preview_services.h" #include "legacy_canvas_stroke_shader_services.h" #include "legacy_canvas_stroke_services.h" #include "legacy_ui_gl_dispatch.h" @@ -451,6 +452,12 @@ void NodeStrokePreview::draw_stroke_immediate() const auto stroke_feedback = stroke_preview_destination_feedback_plan(m_rtt.getWidth(), m_rtt.getHeight()); const bool copy_stroke_destination = !stroke_feedback.reads_destination_color; const auto material = stroke_preview_material_plan(*b, copy_stroke_destination); + const auto preview_composite_plan = pp::paint_renderer::plan_stroke_preview_composite( + pp::paint_renderer::StrokePreviewCompositeRequest { + .uses_mixer = b->m_tip_mix > 0.0f, + .uses_dual = material.composite_pass.use_dual, + .uses_pattern = material.composite_pass.use_pattern, + }); pp::panopainter::setup_legacy_stroke_shader( pp::panopainter::LegacyStrokeShaderSetupUniforms { .resolution = size, @@ -508,21 +515,34 @@ void NodeStrokePreview::draw_stroke_immediate() // CHEKCERBOARD - // copy background color to tex2 - ShaderManager::use(kShader::Checkerboard); - ShaderManager::u_int(kShaderUniform::Colorize, b->m_tip_mix > 0.f || b->m_blend_mode != 0); - float aspect = size.x / size.y; - ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f / aspect, .5f / aspect, -1.f, 1.f)); - m_plane.draw_fill(); - //m_rtt.clear({ .3f, .3f, .3f, 1.f }); - m_tex_background.bind(); - copy_framebuffer_to_texture_2d( - 0, - 0, - 0, - 0, - static_cast(size.x), - static_cast(size.y)); + pp::panopainter::execute_legacy_stroke_preview_background_capture( + [&] { + // copy background color to tex2 + ShaderManager::use(kShader::Checkerboard); + ShaderManager::u_int(kShaderUniform::Colorize, b->m_tip_mix > 0.f || b->m_blend_mode != 0); + float aspect = size.x / size.y; + ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f / aspect, .5f / aspect, -1.f, 1.f)); + }, + [&] { + m_plane.draw_fill(); + }, + [&] { + //m_rtt.clear({ .3f, .3f, .3f, 1.f }); + m_tex_background.bind(); + }, + []( + int src_x, + int src_y, + int dst_x, + int dst_y, + int width, + int height) { + copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height); + }, + pp::panopainter::LegacyStrokePreviewCopySize { + .width = static_cast(size.x), + .height = static_cast(size.y), + }); // DRAW MAIN BRUSH @@ -545,7 +565,7 @@ void NodeStrokePreview::draw_stroke_immediate() b->m_pattern_texture->bind() : unbind_texture_2d(); set_active_texture_unit(3U); - b->m_tip_mix > 0.f ? m_rtt_mixer.bindTexture() : unbind_texture_2d(); + preview_composite_plan.uses_mixer ? m_rtt_mixer.bindTexture() : unbind_texture_2d(); auto frames = stroke_draw_compute(m_stroke, zoom); m_rtt.clear(); for (auto& f : frames) @@ -582,57 +602,73 @@ void NodeStrokePreview::draw_stroke_immediate() // COMPOSITE - pp::panopainter::setup_legacy_stroke_composite_shader( - pp::panopainter::LegacyStrokeCompositeUniforms { - .resolution = size, - .pattern = { - .scale = patt_scale, - .invert = static_cast(b->m_pattern_invert), - .brightness = b->m_pattern_brightness, - .contrast = b->m_pattern_contrast, - .depth = b->m_pattern_depth, - .blend_mode = material.composite_pass.pattern_blend_mode, - .offset = glm::vec2(b->m_pattern_rand_offset ? 0.5f : 0.0f), - }, - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), - .layer_alpha = 1.0f, - .alpha_lock = false, - .mask_enabled = false, - .use_fragcoord = false, - .blend_mode = b->m_blend_mode, - .use_dual = material.composite_pass.use_dual, - .dual_blend_mode = material.composite_pass.dual_blend_mode, - .dual_alpha = material.composite_pass.dual_alpha, - .use_pattern = material.composite_pass.use_pattern, + pp::panopainter::execute_legacy_stroke_preview_final_composite( + [&] { + pp::panopainter::setup_legacy_stroke_composite_shader( + pp::panopainter::LegacyStrokeCompositeUniforms { + .resolution = size, + .pattern = { + .scale = patt_scale, + .invert = static_cast(b->m_pattern_invert), + .brightness = b->m_pattern_brightness, + .contrast = b->m_pattern_contrast, + .depth = b->m_pattern_depth, + .blend_mode = material.composite_pass.pattern_blend_mode, + .offset = glm::vec2(b->m_pattern_rand_offset ? 0.5f : 0.0f), + }, + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + .layer_alpha = 1.0f, + .alpha_lock = false, + .mask_enabled = false, + .use_fragcoord = false, + .blend_mode = b->m_blend_mode, + .use_dual = material.composite_pass.use_dual, + .dual_blend_mode = material.composite_pass.dual_blend_mode, + .dual_alpha = material.composite_pass.dual_alpha, + .use_pattern = material.composite_pass.use_pattern, + }); + }, + [&] { + m_sampler_linear.bind(0); + m_sampler_linear.bind(1); + m_sampler_linear.bind(2); + m_sampler_linear.bind(3); + m_sampler_linear_repeat.bind(4); + }, + [&] { + set_active_texture_unit(0U); + m_tex_background.bind(); + set_active_texture_unit(1U); + m_tex.bind(); + set_active_texture_unit(3U); + m_tex_dual.bind(); + set_active_texture_unit(4U); + b->m_pattern_texture ? + b->m_pattern_texture->bind() : + unbind_texture_2d(); + }, + [&] { + m_plane.draw_fill(); }); - m_sampler_linear.bind(0); - m_sampler_linear.bind(1); - m_sampler_linear.bind(2); - m_sampler_linear.bind(3); - m_sampler_linear_repeat.bind(4); - - set_active_texture_unit(0U); - m_tex_background.bind(); - set_active_texture_unit(1U); - m_tex.bind(); - set_active_texture_unit(3U); - m_tex_dual.bind(); - set_active_texture_unit(4U); - b->m_pattern_texture ? - b->m_pattern_texture->bind() : - unbind_texture_2d(); - m_plane.draw_fill(); - // copy the result to the actual preview - m_tex_preview.bind(); - copy_framebuffer_to_texture_2d( - 0, - 0, - 0, - 0, - static_cast(size.x), - static_cast(size.y)); + pp::panopainter::copy_legacy_stroke_preview_texture( + [&] { + m_tex_preview.bind(); + }, + []( + int src_x, + int src_y, + int dst_x, + int dst_y, + int width, + int height) { + copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height); + }, + pp::panopainter::LegacyStrokePreviewCopySize { + .width = static_cast(size.x), + .height = static_cast(size.y), + }); m_rtt.unbindFramebuffer(); diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp index 4d2b18a..0bc4dc4 100644 --- a/src/paint_renderer/compositor.cpp +++ b/src/paint_renderer/compositor.cpp @@ -1264,6 +1264,52 @@ CanvasStrokeMaterialPlan plan_canvas_stroke_material(CanvasStrokeMaterialRequest return plan; } +StrokePreviewCompositePlan plan_stroke_preview_composite(StrokePreviewCompositeRequest request) noexcept +{ + StrokePreviewCompositePlan plan; + plan.uses_mixer = request.uses_mixer; + plan.uses_dual = request.uses_dual; + plan.uses_pattern = request.uses_pattern; + + auto append_step = [&plan](StrokePreviewCompositeStep step) noexcept { + if (plan.step_count >= plan.steps.size()) { + return; + } + plan.steps[plan.step_count] = step; + ++plan.step_count; + }; + auto bind = [&plan](StrokePreviewTextureRole role, std::uint8_t slot) noexcept { + if (plan.texture_slot_count >= plan.texture_slots.size()) { + return; + } + plan.texture_slots[plan.texture_slot_count] = StrokePreviewTextureSlotPlan { + .role = role, + .slot = slot, + }; + ++plan.texture_slot_count; + }; + + append_step(StrokePreviewCompositeStep::checkerboard_background); + append_step(StrokePreviewCompositeStep::capture_background_texture); + append_step(StrokePreviewCompositeStep::bind_final_composite_inputs); + append_step(StrokePreviewCompositeStep::final_composite_draw); + append_step(StrokePreviewCompositeStep::copy_preview_texture); + + bind(StrokePreviewTextureRole::background, 0); + bind(StrokePreviewTextureRole::stroke, 1); + if (request.uses_dual) { + bind(StrokePreviewTextureRole::dual, 3); + } + if (request.uses_pattern) { + bind(StrokePreviewTextureRole::pattern, 4); + } + if (request.uses_mixer) { + bind(StrokePreviewTextureRole::mixer, 3); + } + + return plan; +} + pp::foundation::Result plan_canvas_blend_gate( pp::renderer::RenderDeviceFeatures features, CanvasBlendGateRequest request) noexcept diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 2496a2c..27fe655 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -109,6 +109,44 @@ struct CanvasStrokeMaterialPlan { std::size_t texture_binding_count = 0; }; +enum class StrokePreviewCompositeStep : std::uint8_t { + checkerboard_background, + capture_background_texture, + bind_final_composite_inputs, + final_composite_draw, + copy_preview_texture, +}; + +enum class StrokePreviewTextureRole : std::uint8_t { + background, + stroke, + mask, + dual, + pattern, + mixer, +}; + +struct StrokePreviewTextureSlotPlan { + StrokePreviewTextureRole role = StrokePreviewTextureRole::background; + std::uint8_t slot = 0; +}; + +struct StrokePreviewCompositeRequest { + bool uses_mixer = false; + bool uses_dual = false; + bool uses_pattern = false; +}; + +struct StrokePreviewCompositePlan { + std::array steps {}; + std::size_t step_count = 0; + std::array texture_slots {}; + std::size_t texture_slot_count = 0; + bool uses_mixer = false; + bool uses_dual = false; + bool uses_pattern = false; +}; + struct CanvasBlendGateRequest { pp::renderer::Extent2D extent {}; std::span layer_blend_modes; @@ -440,6 +478,9 @@ export_document_animation_frames_equirectangular_pngs( [[nodiscard]] CanvasStrokeMaterialPlan plan_canvas_stroke_material( CanvasStrokeMaterialRequest request) noexcept; +[[nodiscard]] StrokePreviewCompositePlan plan_stroke_preview_composite( + StrokePreviewCompositeRequest request) noexcept; + [[nodiscard]] pp::foundation::Result plan_canvas_blend_gate( pp::renderer::RenderDeviceFeatures features, CanvasBlendGateRequest request) noexcept; diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 300a5d4..448b3f4 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -25,6 +25,9 @@ using pp::paint_renderer::CanvasStrokeTextureRole; using pp::paint_renderer::DocumentFaceCompositeRequest; using pp::paint_renderer::DocumentFrameCompositeRequest; using pp::paint_renderer::LayerCompositeView; +using pp::paint_renderer::StrokePreviewCompositeRequest; +using pp::paint_renderer::StrokePreviewCompositeStep; +using pp::paint_renderer::StrokePreviewTextureRole; using pp::paint_renderer::StrokeCompositePath; using pp::paint_renderer::StrokeCompositeRequest; using pp::paint_renderer::composite_layer; @@ -39,6 +42,7 @@ using pp::paint_renderer::plan_canvas_stroke_pad_region; using pp::paint_renderer::plan_canvas_stroke_rasterization; using pp::paint_renderer::plan_canvas_stroke_sample_bounds; using pp::paint_renderer::plan_document_depth_export_render; +using pp::paint_renderer::plan_stroke_preview_composite; using pp::paint_renderer::plan_stroke_composite; using pp::paint_renderer::stroke_composite_path_name; using pp::paint_renderer::stroke_composite_requires_feedback; @@ -74,6 +78,29 @@ bool has_texture_binding( return false; } +bool has_preview_texture_slot( + const pp::paint_renderer::StrokePreviewCompositePlan& plan, + StrokePreviewTextureRole role, + std::uint8_t slot) +{ + for (std::size_t i = 0; i < plan.texture_slot_count; ++i) { + if (plan.texture_slots[i].role == role && plan.texture_slots[i].slot == slot) { + return true; + } + } + return false; +} + +void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::StrokePreviewCompositePlan& plan) +{ + PP_EXPECT(h, plan.step_count == 5U); + PP_EXPECT(h, plan.steps[0] == StrokePreviewCompositeStep::checkerboard_background); + PP_EXPECT(h, plan.steps[1] == StrokePreviewCompositeStep::capture_background_texture); + PP_EXPECT(h, plan.steps[2] == StrokePreviewCompositeStep::bind_final_composite_inputs); + PP_EXPECT(h, plan.steps[3] == StrokePreviewCompositeStep::final_composite_draw); + PP_EXPECT(h, plan.steps[4] == StrokePreviewCompositeStep::copy_preview_texture); +} + std::vector solid_rgba8( std::uint32_t width, std::uint32_t height, @@ -1665,6 +1692,71 @@ void plans_canvas_stroke_dual_material_intent(pp::tests::Harness& h) PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::pattern, 2)); } +void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h) +{ + const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {}); + + expect_preview_sequence(h, plan); + PP_EXPECT(h, !plan.uses_mixer); + PP_EXPECT(h, !plan.uses_dual); + PP_EXPECT(h, !plan.uses_pattern); + PP_EXPECT(h, plan.texture_slot_count == 2U); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1)); + PP_EXPECT(h, !has_preview_texture_slot(plan, StrokePreviewTextureRole::mask, 2)); +} + +void plans_stroke_preview_composite_with_mixer_input(pp::tests::Harness& h) +{ + const auto plan = plan_stroke_preview_composite( + StrokePreviewCompositeRequest { + .uses_mixer = true, + }); + + expect_preview_sequence(h, plan); + PP_EXPECT(h, plan.uses_mixer); + PP_EXPECT(h, !plan.uses_dual); + PP_EXPECT(h, !plan.uses_pattern); + PP_EXPECT(h, plan.texture_slot_count == 3U); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3)); +} + +void plans_stroke_preview_composite_with_dual_input(pp::tests::Harness& h) +{ + const auto plan = plan_stroke_preview_composite( + StrokePreviewCompositeRequest { + .uses_dual = true, + }); + + expect_preview_sequence(h, plan); + PP_EXPECT(h, !plan.uses_mixer); + PP_EXPECT(h, plan.uses_dual); + PP_EXPECT(h, !plan.uses_pattern); + PP_EXPECT(h, plan.texture_slot_count == 3U); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::dual, 3)); +} + +void plans_stroke_preview_composite_with_pattern_input(pp::tests::Harness& h) +{ + const auto plan = plan_stroke_preview_composite( + StrokePreviewCompositeRequest { + .uses_pattern = true, + }); + + expect_preview_sequence(h, plan); + PP_EXPECT(h, !plan.uses_mixer); + PP_EXPECT(h, !plan.uses_dual); + PP_EXPECT(h, plan.uses_pattern); + PP_EXPECT(h, plan.texture_slot_count == 3U); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4)); +} + void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) { const std::vector normal_layers { 0, 0, 0 }; @@ -2086,6 +2178,10 @@ int main() harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans); harness.run("plans_canvas_stroke_material_passes", plans_canvas_stroke_material_passes); harness.run("plans_canvas_stroke_dual_material_intent", plans_canvas_stroke_dual_material_intent); + harness.run("plans_stroke_preview_composite_for_simple_brush", plans_stroke_preview_composite_for_simple_brush); + harness.run("plans_stroke_preview_composite_with_mixer_input", plans_stroke_preview_composite_with_mixer_input); + harness.run("plans_stroke_preview_composite_with_dual_input", plans_stroke_preview_composite_with_dual_input); + harness.run("plans_stroke_preview_composite_with_pattern_input", plans_stroke_preview_composite_with_pattern_input); harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices); harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks); harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);