321 lines
12 KiB
C++
321 lines
12 KiB
C++
#include "paint_renderer/compositor.h"
|
|
|
|
#include <limits>
|
|
|
|
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<std::size_t> 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<std::size_t>::failure(extent_status);
|
|
}
|
|
|
|
const auto width = static_cast<std::uint64_t>(extent.width);
|
|
const auto height = static_cast<std::uint64_t>(extent.height);
|
|
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("pixel count overflows uint64"));
|
|
}
|
|
|
|
const auto count = width * height;
|
|
if (count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("pixel count exceeds addressable memory"));
|
|
}
|
|
|
|
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(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<pp::paint::Rgba> 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<StrokeCompositePlan> plan_stroke_composite(
|
|
pp::renderer::RenderDeviceFeatures features,
|
|
StrokeCompositeRequest request) noexcept
|
|
{
|
|
if (!is_valid_blend_mode(request.layer_blend_mode)) {
|
|
return pp::foundation::Result<StrokeCompositePlan>::failure(
|
|
pp::foundation::Status::invalid_argument("unknown layer blend mode"));
|
|
}
|
|
|
|
if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) {
|
|
return pp::foundation::Result<StrokeCompositePlan>::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<StrokeCompositePlan>::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<StrokeCompositePlan>::success(plan);
|
|
}
|
|
|
|
pp::foundation::Result<CanvasBlendGatePlan> 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<int>(i);
|
|
mark_shader_blend_fallback(gate, features);
|
|
return pp::foundation::Result<CanvasBlendGatePlan>::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<int>(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<CanvasBlendGatePlan>::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<CanvasBlendGatePlan>::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<CanvasBlendGatePlan>::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<CanvasBlendGatePlan>::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";
|
|
}
|
|
|
|
}
|