#include "pch.h" #include "canvas.h" #include "app.h" #include "legacy_canvas_stroke_commit_services.h" #include "legacy_canvas_stroke_composite_services.h" #include "legacy_canvas_stroke_execution_services.h" #include "legacy_canvas_stroke_erase_services.h" #include "legacy_canvas_stroke_edge_services.h" #include "legacy_canvas_stroke_shader_services.h" #include "legacy_ui_gl_dispatch.h" #include "renderer_gl/opengl_capabilities.h" #include "util.h" #include #include #include namespace { GLenum blend_state() { return static_cast(pp::renderer::gl::blend_state()); } void set_active_texture_unit(std::uint32_t unit_index) { pp::legacy::ui_gl::activate_texture_unit(unit_index, "Canvas"); } void unbind_texture_2d() { pp::legacy::ui_gl::unbind_texture_2d("Canvas"); } void apply_canvas_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) { pp::legacy::ui_gl::apply_viewport(x, y, width, height, "Canvas"); } pp::renderer::gl::OpenGlViewportRect query_canvas_viewport() { return pp::legacy::ui_gl::query_viewport_rect("Canvas"); } std::array query_canvas_clear_color() { return pp::legacy::ui_gl::query_clear_color("Canvas"); } bool query_canvas_capability(std::uint32_t state) { return pp::legacy::ui_gl::query_capability(state, "Canvas"); } void apply_canvas_clear_color(std::array color) { pp::legacy::ui_gl::set_clear_color(color, "Canvas"); } void apply_canvas_capability(std::uint32_t capability, bool enabled) { pp::legacy::ui_gl::set_capability(capability, enabled, "Canvas"); } pp::paint_renderer::CanvasStrokeMaterialPlan canvas_stroke_material_plan( const Brush& brush, bool destination_feedback_needed) noexcept { return pp::panopainter::plan_legacy_canvas_stroke_material( pp::paint_renderer::CanvasStrokeMaterialRequest { .destination_feedback_needed = destination_feedback_needed, .pattern_enabled = brush.m_pattern_enabled, .pattern_eachsample = brush.m_pattern_eachsample, .wet_blend = brush.m_tip_wet > 0.F, .mix_blend = brush.m_tip_mix > 0.F, .noise_enabled = brush.m_tip_noise > 0.F, .dual_brush_enabled = brush.m_dual_enabled, .dual_blend_mode = brush.m_dual_blend_mode, .pattern_blend_mode = brush.m_pattern_blend_mode, .dual_alpha = brush.m_dual_opacity, }); } struct CanvasStrokeCommitPrelude { pp::renderer::gl::OpenGlViewportRect viewport; std::array clear_color; bool blend; ActionStroke* action; }; void stamp_canvas_stroke_commit_action( Canvas& canvas, ActionStroke* action); void capture_canvas_stroke_commit_layer_state( Canvas& canvas, ActionStroke* action, int i); void apply_canvas_stroke_commit_dirty_mutation( Canvas& canvas, int i); void copy_canvas_stroke_commit_layer_image( Canvas& canvas, int i); template static auto make_canvas_stroke_commit_callbacks( Canvas& canvas, pp::renderer::gl::OpenGlViewportRect vp, std::array cc, bool blend, SetActiveTextureUnit&& set_active_texture_unit, ActionStroke* action, const Stroke* current_stroke, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, const pp::paint_renderer::CanvasStrokeMaterialPlan& stroke_material) { const auto& b = current_stroke->m_brush; auto bind_commit_inputs = [&](int i) { pp::panopainter::bind_legacy_canvas_stroke_commit_face_inputs( sequence, [&](int texture_slot) { set_active_texture_unit(texture_slot); }, [&](pp::paint_renderer::CanvasStrokeCommitTextureRole role) { switch (role) { case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch: canvas.m_tex2[i].bind(); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke: canvas.m_tmp[i].bindTexture(); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask: canvas.m_smask.rtt(i).bindTexture(); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke: canvas.m_tmp_dual[i].bindTexture(); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern: b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); break; } }, [&](pp::paint_renderer::CanvasStrokeCommitTextureRole role, int texture_slot) { switch (role) { case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch: canvas.m_sampler.bind(texture_slot); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke: canvas.m_sampler_nearest.bind(texture_slot); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask: case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke: canvas.m_sampler.bind(texture_slot); break; case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern: canvas.m_sampler_stencil.bind(texture_slot); break; } }); }; return pp::panopainter::make_legacy_canvas_stroke_commit_callbacks( [&]() { canvas.m_dirty = false; canvas.m_dirty_stroke = true; // new stroke ready for timelapse capture App::I->redraw = true; canvas.m_unsaved = true; App::I->title_update(); }, []() {}, [&]() { apply_canvas_viewport(0, 0, canvas.m_width, canvas.m_height); apply_canvas_capability(blend_state(), false); }, [&]() { blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false); apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height); apply_canvas_clear_color(cc); set_active_texture_unit(0); }, [&]() { stamp_canvas_stroke_commit_action(canvas, action); }, [&]() { canvas.stroke_commit_timelapse(); }, [](int) {}, [&](int i) { capture_canvas_stroke_commit_layer_state(canvas, action, i); }, [&](int i) { apply_canvas_stroke_commit_dirty_mutation(canvas, i); }, [&](int i) { copy_canvas_stroke_commit_layer_image(canvas, i); }, [&](int i) { bind_commit_inputs(i); }, [&](int) { pp::panopainter::execute_legacy_canvas_stroke_commit_erase( [&]() { pp::panopainter::setup_legacy_stroke_erase_shader( pp::panopainter::LegacyStrokeEraseUniforms { .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), .texture_slot = 0, .stroke_texture_slot = 1, .mask_texture_slot = 2, .alpha = 1.0f, .mask_enabled = canvas.m_smask_active, }); }, [&]() { canvas.m_plane.draw_fill(); }); }, [&](int) { glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale); if (b->m_pattern_flipx) patt_scale.x *= -1.f; if (b->m_pattern_flipy) patt_scale.y *= -1.f; pp::panopainter::execute_legacy_canvas_stroke_commit_paint( [&]() { pp::panopainter::setup_legacy_stroke_composite_shader( pp::panopainter::LegacyStrokeCompositeUniforms { .resolution = canvas.m_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 = b->m_pattern_blend_mode, .offset = canvas.m_pattern_offset, }, .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), .layer_alpha = 1.0f, .alpha_lock = canvas.m_layers[canvas.m_current_layer_idx]->m_alpha_locked, .mask_enabled = canvas.m_smask_active, .use_fragcoord = false, .blend_mode = b->m_blend_mode, .use_dual = stroke_material.composite_pass.use_dual, .dual_blend_mode = stroke_material.composite_pass.dual_blend_mode, .dual_alpha = stroke_material.composite_pass.dual_alpha, .use_pattern = stroke_material.composite_pass.use_pattern, }); }, [&]() { canvas.m_plane.draw_fill(); }); }, [&](int i) { const pp::panopainter::LegacyStrokeDilateUniforms dilate_uniforms { .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), }; const pp::panopainter::LegacyCanvasStrokeCommitCopyExtent copy_extent { .width = canvas.m_width, .height = canvas.m_height, }; pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source( sequence, [&]() { pp::panopainter::setup_legacy_stroke_dilate_shader(dilate_uniforms); }, [&](int texture_slot) { set_active_texture_unit(texture_slot); }, [&]() { canvas.m_tex2[i].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); }, copy_extent); }, [&](int) { pp::panopainter::execute_legacy_canvas_stroke_commit_dilate([&]() { canvas.m_plane.draw_fill(); }); }, [&](int i) { canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).unbindFramebuffer(); }); } template static auto execute_canvas_stroke_commit_sequence( BuildRequest&& build_request) { return pp::panopainter::execute_legacy_canvas_stroke_commit_sequence( build_request()); } template static auto make_canvas_stroke_commit_request( Canvas& canvas, pp::renderer::gl::OpenGlViewportRect vp, std::array cc, bool blend, SetActiveTextureUnit&& set_active_texture_unit, ActionStroke* action, const Stroke* current_stroke, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, const pp::paint_renderer::CanvasStrokeMaterialPlan& stroke_material) { const auto commit_callbacks = make_canvas_stroke_commit_callbacks( canvas, vp, cc, blend, std::forward(set_active_texture_unit), action, current_stroke, sequence, stroke_material); const std::array faces { pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = canvas.m_dirty_face[0] }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = canvas.m_dirty_face[1] }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = canvas.m_dirty_face[2] }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 3, .dirty = canvas.m_dirty_face[3] }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 4, .dirty = canvas.m_dirty_face[4] }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 5, .dirty = canvas.m_dirty_face[5] }, }; return pp::panopainter::make_legacy_canvas_stroke_commit_request(faces, sequence, commit_callbacks); } template static auto execute_canvas_stroke_commit_request( Canvas& canvas, pp::renderer::gl::OpenGlViewportRect vp, std::array cc, bool blend, SetActiveTextureUnit&& set_active_texture_unit, ActionStroke* action, const Stroke* current_stroke, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, const pp::paint_renderer::CanvasStrokeMaterialPlan& stroke_material) { return execute_canvas_stroke_commit_sequence([&]() { return make_canvas_stroke_commit_request( canvas, vp, cc, blend, std::forward(set_active_texture_unit), action, current_stroke, sequence, stroke_material); }); } template static auto execute_canvas_stroke_commit_dispatch( Canvas& canvas, pp::renderer::gl::OpenGlViewportRect vp, std::array cc, bool blend, SetActiveTextureUnit&& set_active_texture_unit, ActionStroke* action, const Stroke* current_stroke, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, const pp::paint_renderer::CanvasStrokeMaterialPlan& stroke_material) { return execute_canvas_stroke_commit_request( canvas, vp, cc, blend, std::forward(set_active_texture_unit), action, current_stroke, sequence, stroke_material); } CanvasStrokeCommitPrelude make_canvas_stroke_commit_prelude(Canvas& canvas) { CanvasStrokeCommitPrelude prelude { .viewport = query_canvas_viewport(), .clear_color = query_canvas_clear_color(), .blend = query_canvas_capability(blend_state()), .action = new ActionStroke, }; prelude.action->was_saved = !canvas.m_unsaved; return prelude; } pp::paint_renderer::CanvasStrokeCommitSequencePlan make_canvas_stroke_commit_sequence_plan( const Canvas& canvas, kCanvasMode current_mode, int current_layer_idx, bool smask_active, const pp::paint_renderer::CanvasStrokeMaterialPlan& stroke_material) { return pp::paint_renderer::plan_canvas_stroke_commit_sequence( pp::paint_renderer::CanvasStrokeCommitRequest { .erase_mode = current_mode == kCanvasMode::Erase, .alpha_locked = canvas.m_layers[current_layer_idx]->m_alpha_locked, .selection_mask_active = smask_active, .dual_stroke_enabled = stroke_material.composite_pass.use_dual, .pattern_enabled = stroke_material.composite_pass.use_pattern, }); } void stamp_canvas_stroke_commit_action( Canvas& canvas, ActionStroke* action) { action->m_layer_idx = canvas.m_current_layer_idx; action->m_frame_idx = canvas.layer().m_frame_index; action->m_canvas = &canvas; ActionManager::add(action); } void capture_canvas_stroke_commit_layer_state( Canvas& canvas, ActionStroke* action, int i) { canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).bindFramebuffer(); glm::vec2 box_sz = zw(canvas.m_dirty_box[i]) - xy(canvas.m_dirty_box[i]); action->m_image[i] = std::make_unique( static_cast(box_sz.x * box_sz.y * 4)); canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).readPixelsRgba8( static_cast(canvas.m_dirty_box[i].x), static_cast(canvas.m_dirty_box[i].y), static_cast(box_sz.x), static_cast(box_sz.y), action->m_image[i].get()); action->m_box[i] = canvas.m_dirty_box[i]; action->m_old_box[i] = canvas.m_layers[canvas.m_current_layer_idx]->box(i); action->m_old_dirty[i] = canvas.m_layers[canvas.m_current_layer_idx]->face(i); } void apply_canvas_stroke_commit_dirty_mutation( Canvas& canvas, int i) { if (!canvas.m_layers[canvas.m_current_layer_idx]->m_alpha_locked) { auto& lbox = canvas.m_layers[canvas.m_current_layer_idx]->box(i); lbox = glm::vec4( glm::min(xy(canvas.m_dirty_box[i]), xy(lbox)), glm::max(zw(canvas.m_dirty_box[i]), zw(lbox))); } canvas.m_layers[canvas.m_current_layer_idx]->face(i) = true; } void copy_canvas_stroke_commit_layer_image( Canvas& canvas, int i) { set_active_texture_unit(0); canvas.m_tex2[i].bind(); copy_framebuffer_to_texture_2d(0, 0, 0, 0, canvas.m_width, canvas.m_height); canvas.m_tex2[i].unbind(); } } // namespace glm::vec4 Canvas::stroke_draw_samples( int i, std::vector& P, bool copy_stroke_destination) { constexpr std::array destination_texture_binding { pp::panopainter::LegacyCanvasStrokeTextureBinding { .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination, .slot = 1, }, }; const auto result = pp::panopainter::execute_legacy_canvas_stroke_face_sample_polygon( make_stroke_draw_samples_request( i, P, copy_stroke_destination), destination_texture_binding, make_stroke_draw_samples_destination_texture_dispatch(i)); return result.dirty_bounds; } pp::panopainter::LegacyCanvasStrokeTextureInputDispatch Canvas::make_stroke_draw_samples_destination_texture_dispatch( int face_index) { return pp::panopainter::make_legacy_canvas_stroke_destination_texture_dispatch( [&](int texture_slot) { set_active_texture_unit(texture_slot); }, [&, face_index] { m_tex[face_index].bind(); // bg, copy of framebuffer (copied before drawing) }, [&, face_index] { m_tex[face_index].unbind(); }); } pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest Canvas::make_stroke_draw_samples_request( int face_index, std::vector& polygon_vertices, bool copy_stroke_destination) { return pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest { .context = "Canvas::stroke_draw_samples", .target_size = { m_width, m_height }, .polygon_vertices = polygon_vertices, .face_index = face_index, .copy_stroke_destination = copy_stroke_destination, .copy_framebuffer_to_destination_texture = []( int, 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); }, .upload_brush_vertices = [&](int, std::span vertices) { m_brush_shape.update_vertices( const_cast(vertices.data()), static_cast(vertices.size())); }, .draw_brush_shape = [&](int) { m_brush_shape.draw_fill(); }, }; } void Canvas::stroke_commit() { if (!m_dirty || m_layers.empty()) return; const auto prelude = make_canvas_stroke_commit_prelude(*this); const auto& b = m_current_stroke->m_brush; const auto stroke_material = canvas_stroke_material_plan(*b, false); const auto stroke_commit_sequence = make_canvas_stroke_commit_sequence_plan( *this, m_current_mode, m_current_layer_idx, m_smask_active, stroke_material); [[maybe_unused]] const auto commit_result = execute_canvas_stroke_commit_dispatch( *this, prelude.viewport, prelude.clear_color, prelude.blend, [&](int texture_slot) { set_active_texture_unit(texture_slot); }, prelude.action, m_current_stroke.get(), stroke_commit_sequence, stroke_material); }