#include "paint_renderer/compositor.h" #include namespace pp::paint_renderer { namespace { [[nodiscard]] bool is_valid_blend_mode(pp::paint::BlendMode mode) noexcept { switch (mode) { case pp::paint::BlendMode::normal: case pp::paint::BlendMode::multiply: case pp::paint::BlendMode::screen: case pp::paint::BlendMode::color_dodge: case pp::paint::BlendMode::overlay: return true; } return false; } [[nodiscard]] bool is_valid_stroke_blend_mode(pp::paint::StrokeBlendMode mode) noexcept { switch (mode) { case pp::paint::StrokeBlendMode::normal: case pp::paint::StrokeBlendMode::multiply: case pp::paint::StrokeBlendMode::subtract: case pp::paint::StrokeBlendMode::darken: case pp::paint::StrokeBlendMode::overlay: case pp::paint::StrokeBlendMode::color_dodge: case pp::paint::StrokeBlendMode::color_burn: case pp::paint::StrokeBlendMode::linear_burn: case pp::paint::StrokeBlendMode::hard_mix: case pp::paint::StrokeBlendMode::linear_height: case pp::paint::StrokeBlendMode::height: return true; } 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 expected_pixel_count(pp::renderer::Extent2D extent) noexcept { const auto extent_status = pp::renderer::validate_extent(extent); if (!extent_status.ok()) { return pp::foundation::Result::failure(extent_status); } const auto width = static_cast(extent.width); const auto height = static_cast(extent.height); if (width > std::numeric_limits::max() / height) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("pixel count overflows uint64")); } const auto count = width * height; if (count > static_cast(std::numeric_limits::max())) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("pixel count exceeds addressable memory")); } return pp::foundation::Result::success(static_cast(count)); } [[nodiscard]] StrokeCompositePath composite_path_from_feedback(pp::renderer::PaintFeedbackPath path) noexcept { switch (path) { case pp::renderer::PaintFeedbackPath::none: return StrokeCompositePath::fixed_function_blend; case pp::renderer::PaintFeedbackPath::framebuffer_fetch: return StrokeCompositePath::framebuffer_fetch; case pp::renderer::PaintFeedbackPath::ping_pong_textures: return StrokeCompositePath::ping_pong_textures; } 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, pp::renderer::RenderDeviceFeatures features) noexcept { gate.shader_blend = true; gate.complex_blend = 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; } } } pp::foundation::Status composite_layer( std::span destination, pp::renderer::Extent2D extent, LayerCompositeView layer) noexcept { const auto pixel_count = expected_pixel_count(extent); if (!pixel_count) { return pixel_count.status(); } if (destination.size() != pixel_count.value() || layer.pixels.size() != pixel_count.value()) { return pp::foundation::Status::invalid_argument("composite buffers must match the render extent"); } if (layer.opacity < 0.0F || layer.opacity > 1.0F) { return pp::foundation::Status::out_of_range("layer opacity must be between 0 and 1"); } if (!layer.visible || layer.opacity == 0.0F) { return pp::foundation::Status::success(); } for (std::size_t i = 0; i < destination.size(); ++i) { auto stroke = layer.pixels[i]; stroke.a *= layer.opacity; destination[i] = pp::paint::blend_pixels(destination[i], stroke, layer.blend_mode); } return pp::foundation::Status::success(); } bool stroke_composite_requires_feedback( pp::paint::BlendMode layer_blend_mode, pp::paint::StrokeBlendMode stroke_blend_mode, bool dual_brush_blend, bool pattern_blend) noexcept { return layer_blend_mode != pp::paint::BlendMode::normal || stroke_blend_mode != pp::paint::StrokeBlendMode::normal || dual_brush_blend || pattern_blend; } pp::foundation::Result plan_stroke_composite( pp::renderer::RenderDeviceFeatures features, StrokeCompositeRequest request) noexcept { if (!is_valid_blend_mode(request.layer_blend_mode)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("unknown layer blend mode")); } if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("unknown stroke blend mode")); } const pp::renderer::TextureDesc target_desc { .extent = request.extent, .format = request.target_format, .usage = request.target_usage, .debug_name = "stroke-composite-target", }; const auto complex_blend = stroke_composite_requires_feedback( request.layer_blend_mode, request.stroke_blend_mode, request.dual_brush_blend, request.pattern_blend); const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, complex_blend); if (!feedback) { return pp::foundation::Result::failure(feedback.status()); } StrokeCompositePlan plan; plan.path = composite_path_from_feedback(feedback.value().path); plan.feedback = feedback.value(); plan.target_desc = target_desc; plan.target_bytes = feedback.value().target_bytes; plan.auxiliary_bytes = feedback.value().requires_auxiliary_texture ? feedback.value().target_bytes : 0U; plan.estimated_working_bytes = plan.target_bytes + plan.auxiliary_bytes; plan.complex_blend = complex_blend; plan.reads_destination_color = feedback.value().reads_destination_color; plan.requires_auxiliary_texture = feedback.value().requires_auxiliary_texture; plan.requires_texture_copy = feedback.value().requires_texture_copy; plan.requires_render_target_blit = feedback.value().requires_render_target_blit; plan.requires_explicit_transition = feedback.value().requires_explicit_transition; return pp::foundation::Result::success(plan); } pp::foundation::Result 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(i); mark_shader_blend_fallback(gate, features); return pp::foundation::Result::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(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::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, features); return pp::foundation::Result::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::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::success(gate); } const char* stroke_composite_path_name(StrokeCompositePath path) noexcept { switch (path) { case StrokeCompositePath::fixed_function_blend: return "fixed_function_blend"; case StrokeCompositePath::framebuffer_fetch: return "framebuffer_fetch"; case StrokeCompositePath::ping_pong_textures: return "ping_pong_textures"; } return "unknown"; } }