#include "pch.h"

#include "assets/image_pixels.h"
#include "brush.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_canvas_stroke_commit_services.h"
#include "legacy_canvas_stroke_execution_services.h"
#include "legacy_node_stroke_preview_execution_services.h"
#include "paint_renderer/compositor.h"
#include "renderer_api/recording_renderer.h"
#include "test_harness.h"

#include <cmath>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>

// DEBT-0064: planner tests cover retained inline preview helpers without linking the app texture implementation.
void Texture2D::bind() const {}

using pp::foundation::StatusCode;
using pp::paint::BlendMode;
using pp::paint::Rgba;
using pp::paint::StrokeBlendMode;
using pp::assets::decode_png_rgba8;
using pp::paint_renderer::CanvasBlendGateRequest;
using pp::paint_renderer::CanvasStrokeBox;
using pp::paint_renderer::CanvasStrokeCommitRequest;
using pp::paint_renderer::CanvasStrokeCommitStep;
using pp::paint_renderer::CanvasStrokeCommitTextureRole;
using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest;
using pp::paint_renderer::CanvasStrokeMaterialRequest;
using pp::paint_renderer::CanvasStrokePadRegionRequest;
using pp::paint_renderer::CanvasStrokePoint;
using pp::paint_renderer::CanvasStrokeSampleBoundsRequest;
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;
using pp::paint_renderer::composite_document_face;
using pp::paint_renderer::composite_document_frame;
using pp::paint_renderer::export_document_depth_pngs;
using pp::paint_renderer::plan_canvas_blend_gate;
using pp::paint_renderer::plan_canvas_stroke_face_dirty_update;
using pp::paint_renderer::plan_canvas_stroke_feedback;
using pp::paint_renderer::plan_canvas_stroke_commit_sequence;
using pp::paint_renderer::plan_canvas_stroke_material;
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;
using pp::renderer::Extent2D;
using pp::renderer::RecordedRenderCommandKind;
using pp::renderer::RecordingRenderDevice;
using pp::renderer::RenderDeviceFeatures;
using pp::renderer::TextureFormat;
using pp::renderer::TextureUsage;
using pp::document::AnimationFrame;
using pp::document::CanvasDocument;
using pp::document::DocumentLayerConfig;
using pp::document::DocumentSnapshotConfig;
using pp::document::LayerFacePixels;

namespace {

bool almost_equal(float a, float b)
{
    return std::fabs(a - b) < 0.0001F;
}

bool almost_equal(const glm::vec2& a, const glm::vec2& b)
{
    return almost_equal(a.x, b.x) && almost_equal(a.y, b.y);
}

bool has_texture_binding(
    const pp::paint_renderer::CanvasStrokeMaterialPlan& plan,
    CanvasStrokeTextureRole role,
    std::uint8_t slot)
{
    for (std::size_t i = 0; i < plan.texture_binding_count; ++i) {
        if (plan.texture_bindings[i].role == role && plan.texture_bindings[i].slot == slot) {
            return true;
        }
    }
    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;
}

bool has_commit_texture_binding(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan,
    CanvasStrokeCommitTextureRole role,
    std::uint8_t slot)
{
    for (std::size_t i = 0; i < plan.texture_binding_count; ++i) {
        if (plan.texture_bindings[i].role == role && plan.texture_bindings[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);
}

void expect_commit_prefix(pp::tests::Harness& h, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan)
{
    PP_EXPECT(h, plan.step_count == 7U);
    PP_EXPECT(h, plan.steps[0] == CanvasStrokeCommitStep::readback_history_region);
    PP_EXPECT(h, plan.steps[1] == CanvasStrokeCommitStep::update_layer_dirty_state);
    PP_EXPECT(h, plan.steps[2] == CanvasStrokeCommitStep::copy_layer_rtt_to_scratch);
    PP_EXPECT(h, plan.steps[3] == CanvasStrokeCommitStep::bind_commit_inputs);
    PP_EXPECT(h, plan.steps[5] == CanvasStrokeCommitStep::copy_committed_rtt_to_scratch);
    PP_EXPECT(h, plan.steps[6] == CanvasStrokeCommitStep::dilate_edges_draw);
}

std::vector<std::uint8_t> solid_rgba8(
    std::uint32_t width,
    std::uint32_t height,
    std::uint8_t r,
    std::uint8_t g,
    std::uint8_t b,
    std::uint8_t a)
{
    std::vector<std::uint8_t> pixels(
        static_cast<std::size_t>(width) * height * pp::document::rgba8_components);
    for (std::size_t i = 0; i < pixels.size(); i += pp::document::rgba8_components) {
        pixels[i] = r;
        pixels[i + 1U] = g;
        pixels[i + 2U] = b;
        pixels[i + 3U] = a;
    }
    return pixels;
}

LayerFacePixels solid_face_payload(
    std::uint32_t face_index,
    std::uint32_t width,
    std::uint32_t height,
    std::uint8_t r,
    std::uint8_t g,
    std::uint8_t b,
    std::uint8_t a)
{
    return LayerFacePixels {
        .face_index = face_index,
        .x = 0,
        .y = 0,
        .width = width,
        .height = height,
        .rgba8 = solid_rgba8(width, height, r, g, b, a),
    };
}

std::vector<LayerFacePixels> solid_cube_faces(
    std::uint32_t width,
    std::uint32_t height,
    std::uint8_t r,
    std::uint8_t g,
    std::uint8_t b,
    std::uint8_t a)
{
    std::vector<LayerFacePixels> faces;
    faces.reserve(pp::document::cube_face_count);
    for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
        faces.push_back(solid_face_payload(face_index, width, height, r, g, b, a));
    }
    return faces;
}

void composites_visible_layer_with_opacity(pp::tests::Harness& h)
{
    std::vector<Rgba> destination {
        Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
    };
    const std::vector<Rgba> foreground {
        Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.5F },
    };

    const auto status = composite_layer(
        destination,
        Extent2D { .width = 1, .height = 1 },
        LayerCompositeView {
            .pixels = foreground,
            .opacity = 0.5F,
            .visible = true,
            .blend_mode = BlendMode::normal,
        });

    PP_EXPECT(h, status.ok());
    PP_EXPECT(h, almost_equal(destination[0].a, 0.625F));
    PP_EXPECT(h, almost_equal(destination[0].r, 0.44F));
    PP_EXPECT(h, almost_equal(destination[0].g, 0.32F));
    PP_EXPECT(h, almost_equal(destination[0].b, 0.4F));
}

void invisible_and_zero_opacity_layers_are_noops(pp::tests::Harness& h)
{
    const Rgba original { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 0.4F };
    std::vector<Rgba> destination { original };
    const std::vector<Rgba> foreground {
        Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 1.0F },
    };

    PP_EXPECT(h, composite_layer(
        destination,
        Extent2D { .width = 1, .height = 1 },
        LayerCompositeView { .pixels = foreground, .opacity = 1.0F, .visible = false }).ok());
    PP_EXPECT(h, almost_equal(destination[0].r, original.r));
    PP_EXPECT(h, almost_equal(destination[0].g, original.g));
    PP_EXPECT(h, almost_equal(destination[0].b, original.b));
    PP_EXPECT(h, almost_equal(destination[0].a, original.a));

    PP_EXPECT(h, composite_layer(
        destination,
        Extent2D { .width = 1, .height = 1 },
        LayerCompositeView { .pixels = foreground, .opacity = 0.0F, .visible = true }).ok());
    PP_EXPECT(h, almost_equal(destination[0].r, original.r));
    PP_EXPECT(h, almost_equal(destination[0].g, original.g));
    PP_EXPECT(h, almost_equal(destination[0].b, original.b));
    PP_EXPECT(h, almost_equal(destination[0].a, original.a));
}

void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h)
{
    std::vector<Rgba> destination(2);
    const std::vector<Rgba> foreground(1);

    const auto mismatched = composite_layer(
        destination,
        Extent2D { .width = 2, .height = 1 },
        LayerCompositeView { .pixels = foreground });
    const auto bad_opacity = composite_layer(
        destination,
        Extent2D { .width = 2, .height = 1 },
        LayerCompositeView { .pixels = destination, .opacity = 1.5F });
    const auto bad_extent = composite_layer(
        destination,
        Extent2D { .width = 0, .height = 1 },
        LayerCompositeView { .pixels = destination });

    PP_EXPECT(h, !mismatched.ok());
    PP_EXPECT(h, mismatched.code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_opacity.ok());
    PP_EXPECT(h, bad_opacity.code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_extent.ok());
    PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument);
}

void composites_document_face_payloads_in_layer_order(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame base_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 2,
                    .height = 1,
                    .rgba8 = { 255, 0, 0, 255, 0, 255, 0, 255 },
                },
            },
        },
    };
    const AnimationFrame paint_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 1,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 0, 0, 255, 255 },
                },
            },
        },
    };
    const AnimationFrame hidden_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 255, 255, 255, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Base",
            .frames = std::span<const AnimationFrame>(base_frames, 1),
        },
        {
            .name = "Paint",
            .opacity = 0.5F,
            .frames = std::span<const AnimationFrame>(paint_frames, 1),
        },
        {
            .name = "Hidden",
            .visible = false,
            .frames = std::span<const AnimationFrame>(hidden_frames, 1),
        },
    };

    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 2,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 3),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    const auto result = composite_document_face(DocumentFaceCompositeRequest {
        .document = &document.value(),
        .frame_index = 0,
        .face_index = 0,
    });

    PP_EXPECT(h, result);
    if (result) {
        PP_EXPECT(h, result.value().extent.width == 2U);
        PP_EXPECT(h, result.value().extent.height == 1U);
        PP_EXPECT(h, result.value().visited_layer_count == 3U);
        PP_EXPECT(h, result.value().composited_layer_count == 2U);
        PP_EXPECT(h, result.value().face_payload_count == 2U);
        PP_EXPECT(h, result.value().pixels.size() == 2U);
        PP_EXPECT(h, almost_equal(result.value().pixels[0].r, 1.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[0].g, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[0].b, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[0].a, 1.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[1].r, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[1].g, 0.5F));
        PP_EXPECT(h, almost_equal(result.value().pixels[1].b, 0.5F));
        PP_EXPECT(h, almost_equal(result.value().pixels[1].a, 1.0F));
    }
}

void document_face_composite_skips_layers_without_requested_frame(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame short_layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 255, 0, 0, 255 },
                },
            },
        },
    };
    const AnimationFrame animated_layer_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 0, 0, 255, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Short",
            .frames = std::span<const AnimationFrame>(short_layer_frames, 1),
        },
        {
            .name = "Animated",
            .frames = std::span<const AnimationFrame>(animated_layer_frames, 2),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 2),
        .frames = std::span<const AnimationFrame>(root_frames, 2),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);

    const auto result = composite_document_face(DocumentFaceCompositeRequest {
        .document = &document.value(),
        .frame_index = 1,
        .face_index = 0,
    });

    PP_EXPECT(h, result);
    if (result) {
        PP_EXPECT(h, result.value().visited_layer_count == 2U);
        PP_EXPECT(h, result.value().composited_layer_count == 1U);
        PP_EXPECT(h, result.value().face_payload_count == 1U);
        PP_EXPECT(h, almost_equal(result.value().pixels[0].r, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[0].g, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[0].b, 1.0F));
        PP_EXPECT(h, almost_equal(result.value().pixels[0].a, 1.0F));
    }
}

void document_face_composite_rejects_invalid_requests(pp::tests::Harness& h)
{
    const auto no_document = composite_document_face(DocumentFaceCompositeRequest {});

    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const DocumentLayerConfig layers[] {
        { .name = "Layer", .frames = {} },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);

    const auto bad_frame = composite_document_face(DocumentFaceCompositeRequest {
        .document = &document.value(),
        .frame_index = 1,
        .face_index = 0,
    });
    const auto bad_face = composite_document_face(DocumentFaceCompositeRequest {
        .document = &document.value(),
        .frame_index = 0,
        .face_index = 6,
    });

    PP_EXPECT(h, !no_document.ok());
    PP_EXPECT(h, no_document.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_frame.ok());
    PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_face.ok());
    PP_EXPECT(h, bad_face.status().code == StatusCode::out_of_range);
}

void composites_document_frame_cube_faces(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame base_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 255, 0, 0, 255 },
                },
                LayerFacePixels {
                    .face_index = 5,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 0, 0, 255, 255 },
                },
            },
        },
    };
    const AnimationFrame paint_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 5,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 0, 255, 0, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Base",
            .frames = std::span<const AnimationFrame>(base_frames, 1),
        },
        {
            .name = "Paint",
            .opacity = 0.5F,
            .frames = std::span<const AnimationFrame>(paint_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 2),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);

    const auto result = composite_document_frame(DocumentFrameCompositeRequest {
        .document = &document.value(),
        .frame_index = 0,
        .clear_color = Rgba { .r = 0.25F, .g = 0.25F, .b = 0.25F, .a = 1.0F },
    });

    PP_EXPECT(h, result);
    if (result) {
        PP_EXPECT(h, result.value().extent.width == 1U);
        PP_EXPECT(h, result.value().extent.height == 1U);
        PP_EXPECT(h, result.value().faces.size() == pp::document::cube_face_count);
        PP_EXPECT(h, result.value().visited_layer_count == 2U);
        PP_EXPECT(h, result.value().composited_layer_face_count == 3U);
        PP_EXPECT(h, result.value().face_payload_count == 3U);
        PP_EXPECT(h, result.value().faces[0].pixels.size() == 1U);
        PP_EXPECT(h, almost_equal(result.value().faces[0].pixels[0].r, 1.0F));
        PP_EXPECT(h, almost_equal(result.value().faces[0].pixels[0].g, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().faces[0].pixels[0].b, 0.0F));
        PP_EXPECT(h, result.value().faces[1].pixels.size() == 1U);
        PP_EXPECT(h, almost_equal(result.value().faces[1].pixels[0].r, 0.25F));
        PP_EXPECT(h, almost_equal(result.value().faces[1].pixels[0].g, 0.25F));
        PP_EXPECT(h, almost_equal(result.value().faces[1].pixels[0].b, 0.25F));
        PP_EXPECT(h, result.value().faces[5].pixels.size() == 1U);
        PP_EXPECT(h, almost_equal(result.value().faces[5].pixels[0].r, 0.0F));
        PP_EXPECT(h, almost_equal(result.value().faces[5].pixels[0].g, 0.5F));
        PP_EXPECT(h, almost_equal(result.value().faces[5].pixels[0].b, 0.5F));
        PP_EXPECT(h, almost_equal(result.value().faces[5].pixels[0].a, 1.0F));
    }
}

void document_frame_composite_rejects_invalid_requests(pp::tests::Harness& h)
{
    const auto no_document = composite_document_frame(DocumentFrameCompositeRequest {});

    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const DocumentLayerConfig layers[] {
        { .name = "Layer", .frames = {} },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);

    const auto bad_frame = composite_document_frame(DocumentFrameCompositeRequest {
        .document = &document.value(),
        .frame_index = 1,
    });

    PP_EXPECT(h, !no_document.ok());
    PP_EXPECT(h, no_document.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_frame.ok());
    PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
}

void uploads_document_frame_faces_to_renderer_api(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 255, 0, 0, 255 },
                },
                LayerFacePixels {
                    .face_index = 3,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 0, 255, 0, 128 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);

    RecordingRenderDevice device;
    const auto result = pp::paint_renderer::upload_document_frame_faces(
        device,
        pp::paint_renderer::DocumentFrameUploadRequest {
            .document = &document.value(),
            .frame_index = 0,
        });

    PP_EXPECT(h, result);
    if (result) {
        PP_EXPECT(h, result.value().texture_count == pp::document::cube_face_count);
        PP_EXPECT(h, result.value().transition_count == pp::document::cube_face_count);
        PP_EXPECT(h, result.value().uploaded_bytes == 24U);
        PP_EXPECT(h, result.value().composite.face_payload_count == 2U);
        PP_EXPECT(h, result.value().face_textures[0] != nullptr);
        PP_EXPECT(h, result.value().face_textures[0]->desc().extent.width == 1U);
        PP_EXPECT(h, result.value().face_textures[0]->desc().format == TextureFormat::rgba8);
    }

    std::size_t upload_count = 0;
    std::size_t transition_count = 0;
    for (const auto& command : device.commands()) {
        if (command.kind == RecordedRenderCommandKind::upload_texture) {
            ++upload_count;
            PP_EXPECT(h, command.upload_bytes == 4U);
            PP_EXPECT(h, command.readback_region.width == 1U);
            PP_EXPECT(h, command.texture_desc.format == TextureFormat::rgba8);
        }
        if (command.kind == RecordedRenderCommandKind::transition_texture) {
            ++transition_count;
            PP_EXPECT(h, command.before_state == pp::renderer::TextureState::upload_destination);
            PP_EXPECT(h, command.after_state == pp::renderer::TextureState::shader_read);
        }
    }
    PP_EXPECT(h, device.commands().size() == 12U);
    PP_EXPECT(h, upload_count == pp::document::cube_face_count);
    PP_EXPECT(h, transition_count == pp::document::cube_face_count);
}

void records_document_frame_upload_report(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 1,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 0, 0, 255, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto report = pp::paint_renderer::record_document_frame_upload(
        pp::paint_renderer::DocumentFrameUploadRequest {
            .document = &document.value(),
            .frame_index = 0,
        });

    PP_EXPECT(h, report);
    if (report) {
        PP_EXPECT(h, report.value().upload.texture_count == pp::document::cube_face_count);
        PP_EXPECT(h, report.value().upload.transition_count == pp::document::cube_face_count);
        PP_EXPECT(h, report.value().upload.uploaded_bytes == 24U);
        PP_EXPECT(h, report.value().upload.composite.face_payload_count == 1U);
        PP_EXPECT(h, report.value().command_count == 12U);
        PP_EXPECT(h, report.value().upload_command_count == pp::document::cube_face_count);
        PP_EXPECT(h, report.value().transition_command_count == pp::document::cube_face_count);
    }
}

void exports_document_frame_faces_as_pngs(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 4,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 64, 128, 255, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto exported = pp::paint_renderer::export_document_frame_face_pngs(
        pp::paint_renderer::DocumentFrameCompositeRequest {
            .document = &document.value(),
            .frame_index = 0,
            .clear_color = {},
        });

    PP_EXPECT(h, exported);
    if (!exported) {
        return;
    }

    PP_EXPECT(h, exported.value().face_count == pp::document::cube_face_count);
    PP_EXPECT(h, exported.value().encoded_bytes > 0U);
    PP_EXPECT(h, exported.value().composite.face_payload_count == 1U);

    const auto decoded = pp::assets::decode_png_rgba8(exported.value().face_pngs[4]);
    PP_EXPECT(h, decoded);
    if (decoded) {
        PP_EXPECT(h, decoded.value().width == 1U);
        PP_EXPECT(h, decoded.value().height == 1U);
        PP_EXPECT(h, decoded.value().pixels.size() == 4U);
        PP_EXPECT(h, decoded.value().pixels[0] == 64U);
        PP_EXPECT(h, decoded.value().pixels[1] == 128U);
        PP_EXPECT(h, decoded.value().pixels[2] == 255U);
        PP_EXPECT(h, decoded.value().pixels[3] == 255U);
    }
}

void prepares_document_frame_export_readiness_report(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 2,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 255, 128, 0, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
        DocumentFrameCompositeRequest {
            .document = &document.value(),
            .frame_index = 0,
            .clear_color = {},
        });

    PP_EXPECT(h, readiness);
    if (!readiness) {
        return;
    }

    PP_EXPECT(h, readiness.value().recorded_upload.upload.texture_count == pp::document::cube_face_count);
    PP_EXPECT(h, readiness.value().recorded_upload.upload.uploaded_bytes == 24U);
    PP_EXPECT(h, readiness.value().recorded_upload.command_count == 12U);
    PP_EXPECT(h, readiness.value().recorded_upload.upload_command_count == pp::document::cube_face_count);
    PP_EXPECT(h, readiness.value().recorded_upload.transition_command_count == pp::document::cube_face_count);
    PP_EXPECT(h, readiness.value().face_pngs.face_count == pp::document::cube_face_count);
    PP_EXPECT(h, readiness.value().face_pngs.encoded_bytes > 0U);
    PP_EXPECT(h, readiness.value().face_pngs.composite.face_payload_count == 1U);

    const auto decoded = pp::assets::decode_png_rgba8(readiness.value().face_pngs.face_pngs[2]);
    PP_EXPECT(h, decoded);
    if (decoded) {
        PP_EXPECT(h, decoded.value().pixels[0] == 255U);
        PP_EXPECT(h, decoded.value().pixels[1] == 128U);
        PP_EXPECT(h, decoded.value().pixels[2] == 0U);
        PP_EXPECT(h, decoded.value().pixels[3] == 255U);
    }
}

void exports_document_frame_as_equirectangular_png(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 4,
                    .rgba8 = { 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255 },
                },
                LayerFacePixels {
                    .face_index = 1,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 4,
                    .rgba8 = { 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255 },
                },
                LayerFacePixels {
                    .face_index = 2,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 4,
                    .rgba8 = { 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255 },
                },
                LayerFacePixels {
                    .face_index = 3,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 4,
                    .rgba8 = { 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255 },
                },
                LayerFacePixels {
                    .face_index = 4,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 4,
                    .rgba8 = { 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255 },
                },
                LayerFacePixels {
                    .face_index = 5,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 4,
                    .rgba8 = { 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 4,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto exported = pp::paint_renderer::export_document_frame_equirectangular_png(
        DocumentFrameCompositeRequest {
            .document = &document.value(),
            .frame_index = 0,
        });

    PP_EXPECT(h, exported);
    if (!exported) {
        return;
    }

    PP_EXPECT(h, exported.value().face_extent.width == 1U);
    PP_EXPECT(h, exported.value().face_extent.height == 4U);
    PP_EXPECT(h, exported.value().equirectangular_extent.width == 4U);
    PP_EXPECT(h, exported.value().equirectangular_extent.height == 8U);
    PP_EXPECT(h, exported.value().face_payload_count == pp::document::cube_face_count);
    PP_EXPECT(h, exported.value().encoded_bytes > 0U);

    const auto decoded = pp::assets::decode_png_rgba8(exported.value().png);
    PP_EXPECT(h, decoded);
    if (!decoded) {
        return;
    }

    PP_EXPECT(h, decoded.value().width == 4U);
    PP_EXPECT(h, decoded.value().height == 8U);
    PP_EXPECT(h, decoded.value().pixels[0] == 255U);
    PP_EXPECT(h, decoded.value().pixels[1] == 0U);
    PP_EXPECT(h, decoded.value().pixels[2] == 255U);
    const auto bottom = (static_cast<std::size_t>(decoded.value().height) - 1U)
        * decoded.value().width * pp::document::rgba8_components;
    PP_EXPECT(h, decoded.value().pixels[bottom] == 0U);
    PP_EXPECT(h, decoded.value().pixels[bottom + 1U] == 255U);
    PP_EXPECT(h, decoded.value().pixels[bottom + 2U] == 255U);
}

void exports_document_frame_as_equirectangular_jpeg_with_xmp(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = solid_cube_faces(1, 4, 25, 125, 225, 255),
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 4,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto exported = pp::paint_renderer::export_document_frame_equirectangular_jpeg(
        DocumentFrameCompositeRequest {
            .document = &document.value(),
            .frame_index = 0,
        },
        90);

    PP_EXPECT(h, exported);
    if (!exported) {
        return;
    }

    PP_EXPECT(h, exported.value().face_extent.width == 1U);
    PP_EXPECT(h, exported.value().face_extent.height == 4U);
    PP_EXPECT(h, exported.value().equirectangular_extent.width == 4U);
    PP_EXPECT(h, exported.value().equirectangular_extent.height == 8U);
    PP_EXPECT(h, exported.value().face_payload_count == pp::document::cube_face_count);
    PP_EXPECT(h, exported.value().encoded_bytes == exported.value().jpeg.size());
    PP_EXPECT(h, exported.value().xmp_injected);

    const std::string_view text(
        reinterpret_cast<const char*>(exported.value().jpeg.data()),
        exported.value().jpeg.size());
    PP_EXPECT(h, text.find("GPano:ProjectionType") != std::string_view::npos);
    PP_EXPECT(h, text.find("equirectangular") != std::string_view::npos);

    const auto decoded = pp::assets::decode_jpeg_rgba8(exported.value().jpeg);
    PP_EXPECT(h, decoded);
    if (!decoded) {
        return;
    }

    PP_EXPECT(h, decoded.value().width == 4U);
    PP_EXPECT(h, decoded.value().height == 8U);
}

void exports_document_layers_as_equirectangular_pngs(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame base_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = solid_cube_faces(1, 4, 255, 0, 0, 255),
        },
    };
    const AnimationFrame hidden_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = solid_cube_faces(1, 4, 0, 0, 255, 255),
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Base",
            .frames = std::span<const AnimationFrame>(base_frames, 1),
        },
        {
            .name = "HiddenPaint",
            .visible = false,
            .opacity = 0.0F,
            .frames = std::span<const AnimationFrame>(hidden_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 4,
        .layers = std::span<const DocumentLayerConfig>(layers, 2),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto exported = pp::paint_renderer::export_document_layers_equirectangular_pngs(
        pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {
            .document = &document.value(),
            .frame_index = 0,
        });
    PP_EXPECT(h, exported);
    if (!exported) {
        return;
    }

    PP_EXPECT(h, exported.value().layer_count == 2U);
    PP_EXPECT(h, exported.value().layers.size() == 2U);
    PP_EXPECT(h, exported.value().layers[0].layer_name == "Base");
    PP_EXPECT(h, exported.value().layers[1].layer_name == "HiddenPaint");
    PP_EXPECT(h, exported.value().layers[0].face_payload_count == pp::document::cube_face_count);
    PP_EXPECT(h, exported.value().layers[1].face_payload_count == pp::document::cube_face_count);
    PP_EXPECT(h, exported.value().encoded_bytes > 0U);

    const auto decoded = pp::assets::decode_png_rgba8(exported.value().layers[1].png);
    PP_EXPECT(h, decoded);
    if (!decoded) {
        return;
    }

    PP_EXPECT(h, decoded.value().width == 4U);
    PP_EXPECT(h, decoded.value().height == 8U);
    PP_EXPECT(h, decoded.value().pixels[0] == 0U);
    PP_EXPECT(h, decoded.value().pixels[1] == 0U);
    PP_EXPECT(h, decoded.value().pixels[2] == 255U);
}

void exports_document_animation_frames_as_equirectangular_pngs(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = solid_cube_faces(1, 4, 255, 0, 0, 255),
        },
        {
            .duration_ms = 100,
            .face_pixels = solid_cube_faces(1, 4, 0, 255, 0, 255),
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Paint",
            .frames = std::span<const AnimationFrame>(layer_frames, 2),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 4,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 2),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto exported = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
        pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {
            .document = &document.value(),
        });
    PP_EXPECT(h, exported);
    if (!exported) {
        return;
    }

    PP_EXPECT(h, exported.value().frame_count == 2U);
    PP_EXPECT(h, exported.value().frames.size() == 2U);
    PP_EXPECT(h, exported.value().frames[0].frame_index == 0U);
    PP_EXPECT(h, exported.value().frames[1].frame_index == 1U);
    PP_EXPECT(h, exported.value().encoded_bytes > 0U);

    const auto decoded = pp::assets::decode_png_rgba8(exported.value().frames[1].png);
    PP_EXPECT(h, decoded);
    if (!decoded) {
        return;
    }

    PP_EXPECT(h, decoded.value().width == 4U);
    PP_EXPECT(h, decoded.value().height == 8U);
    PP_EXPECT(h, decoded.value().pixels[0] == 0U);
    PP_EXPECT(h, decoded.value().pixels[1] == 255U);
    PP_EXPECT(h, decoded.value().pixels[2] == 0U);
}

void plans_document_depth_export_renderer_work(pp::tests::Harness& h)
{
    std::vector<LayerFacePixels> visible_faces;
    visible_faces.push_back(solid_face_payload(0, 1, 1, 255, 0, 0, 255));
    visible_faces.push_back(solid_face_payload(2, 1, 1, 0, 0, 255, 255));

    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame visible_layer_frames[] {
        { .duration_ms = 100, .face_pixels = visible_faces },
    };
    const AnimationFrame hidden_layer_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = solid_cube_faces(1, 1, 255, 255, 0, 255),
        },
    };
    const AnimationFrame empty_layer_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Visible",
            .frames = std::span<const AnimationFrame>(visible_layer_frames, 1),
        },
        {
            .name = "Hidden",
            .visible = false,
            .frames = std::span<const AnimationFrame>(hidden_layer_frames, 1),
        },
        {
            .name = "Transparent",
            .opacity = 0.0F,
            .frames = std::span<const AnimationFrame>(hidden_layer_frames, 1),
        },
        {
            .name = "EmptyVisible",
            .frames = std::span<const AnimationFrame>(empty_layer_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 4),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto plan = plan_document_depth_export_render(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &document.value(),
            .frame_index = 0,
        });
    PP_EXPECT(h, plan);
    if (!plan) {
        return;
    }

    PP_EXPECT(h, plan.value().output_extent.width == 1024U);
    PP_EXPECT(h, plan.value().output_extent.height == 1024U);
    PP_EXPECT(h, plan.value().merged_face_draw_count == pp::document::cube_face_count);
    PP_EXPECT(h, plan.value().visited_layer_count == 4U);
    PP_EXPECT(h, plan.value().visible_layer_count == 2U);
    PP_EXPECT(h, plan.value().face_payload_count == 2U);
    PP_EXPECT(h, plan.value().layer_depth_draw_count == 2U);
    PP_EXPECT(h, plan.value().uses_perspective_camera);
    PP_EXPECT(h, plan.value().requires_renderer_readback);
}

void exports_document_depth_as_png_payloads(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame base_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                solid_face_payload(0, 1, 1, 255, 0, 0, 255),
            },
        },
    };
    const AnimationFrame top_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                solid_face_payload(0, 1, 1, 0, 255, 0, 255),
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Base",
            .frames = std::span<const AnimationFrame>(base_frames, 1),
        },
        {
            .name = "Top",
            .frames = std::span<const AnimationFrame>(top_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 2),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);
    if (!document) {
        return;
    }

    const auto exported = export_document_depth_pngs(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &document.value(),
            .frame_index = 0,
            .output_extent = Extent2D { .width = 1, .height = 1 },
        });
    PP_EXPECT(h, exported);
    if (!exported) {
        return;
    }

    PP_EXPECT(h, exported.value().output_extent.width == 1U);
    PP_EXPECT(h, exported.value().output_extent.height == 1U);
    PP_EXPECT(h, exported.value().image_encoded_bytes > 0U);
    PP_EXPECT(h, exported.value().depth_encoded_bytes > 0U);
    PP_EXPECT(h, exported.value().merged_face_draw_count == pp::document::cube_face_count);
    PP_EXPECT(h, exported.value().layer_depth_draw_count == 2U);
    PP_EXPECT(h, exported.value().visited_layer_count == 2U);
    PP_EXPECT(h, exported.value().visible_layer_count == 2U);
    PP_EXPECT(h, exported.value().face_payload_count == 2U);
    PP_EXPECT(h, exported.value().uses_perspective_camera);

    const auto image = decode_png_rgba8(exported.value().image_png);
    const auto depth = decode_png_rgba8(exported.value().depth_png);
    PP_EXPECT(h, image);
    PP_EXPECT(h, depth);
    if (!image || !depth) {
        return;
    }

    PP_EXPECT(h, image.value().width == 1U);
    PP_EXPECT(h, image.value().height == 1U);
    PP_EXPECT(h, depth.value().width == 1U);
    PP_EXPECT(h, depth.value().height == 1U);
    const std::vector<std::uint8_t> expected_image { 0, 255, 0, 255 };
    const std::vector<std::uint8_t> expected_depth { 170, 170, 170, 255 };
    PP_EXPECT(h, image.value().pixels == expected_image);
    PP_EXPECT(h, depth.value().pixels == expected_depth);
}

void depth_export_payload_boundary_rejects_malformed_face_bytes(pp::tests::Harness& h)
{
    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const AnimationFrame bad_frames[] {
        {
            .duration_ms = 100,
            .face_pixels = {
                LayerFacePixels {
                    .face_index = 0,
                    .x = 0,
                    .y = 0,
                    .width = 1,
                    .height = 1,
                    .rgba8 = { 255, 0, 0 },
                },
            },
        },
    };
    const DocumentLayerConfig layers[] {
        {
            .name = "Broken",
            .frames = std::span<const AnimationFrame>(bad_frames, 1),
        },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, !document.ok());
    PP_EXPECT(h, document.status().code == StatusCode::invalid_argument);
}

void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h)
{
    RecordingRenderDevice device;
    const auto no_document = pp::paint_renderer::upload_document_frame_faces(
        device,
        pp::paint_renderer::DocumentFrameUploadRequest {});
    const auto no_document_readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
        DocumentFrameCompositeRequest {});
    const auto no_document_equirect = pp::paint_renderer::export_document_frame_equirectangular_png(
        DocumentFrameCompositeRequest {});
    const auto no_document_jpeg = pp::paint_renderer::export_document_frame_equirectangular_jpeg(
        DocumentFrameCompositeRequest {});
    const auto no_document_layers = pp::paint_renderer::export_document_layers_equirectangular_pngs(
        pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {});
    const auto no_document_frames = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
        pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {});
    const auto no_document_depth = plan_document_depth_export_render(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {});
    const auto no_document_depth_pngs = export_document_depth_pngs(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {});

    const AnimationFrame root_frames[] {
        { .duration_ms = 100, .face_pixels = {} },
    };
    const DocumentLayerConfig layers[] {
        { .name = "Layer", .frames = {} },
    };
    const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
        .width = 1,
        .height = 1,
        .layers = std::span<const DocumentLayerConfig>(layers, 1),
        .frames = std::span<const AnimationFrame>(root_frames, 1),
        .selection_masks = {},
    });
    PP_EXPECT(h, document);

    const auto bad_frame = pp::paint_renderer::upload_document_frame_faces(
        device,
        pp::paint_renderer::DocumentFrameUploadRequest {
            .document = &document.value(),
            .frame_index = 1,
        });
    const auto bad_frame_readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
        DocumentFrameCompositeRequest {
            .document = &document.value(),
            .frame_index = 1,
        });
    const auto bad_frame_depth = plan_document_depth_export_render(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &document.value(),
            .frame_index = 1,
        });
    const auto bad_extent_depth = plan_document_depth_export_render(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &document.value(),
            .frame_index = 0,
            .output_extent = Extent2D {},
        });
    const auto bad_frame_depth_pngs = export_document_depth_pngs(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &document.value(),
            .frame_index = 1,
        });
    const auto bad_extent_depth_pngs = export_document_depth_pngs(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &document.value(),
            .frame_index = 0,
            .output_extent = Extent2D {},
        });
    const auto bad_jpeg_quality = pp::paint_renderer::export_document_frame_equirectangular_jpeg(
        DocumentFrameCompositeRequest {
            .document = &document.value(),
            .frame_index = 0,
        },
        0);

    PP_EXPECT(h, !no_document.ok());
    PP_EXPECT(h, no_document.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_readiness.ok());
    PP_EXPECT(h, no_document_readiness.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_equirect.ok());
    PP_EXPECT(h, no_document_equirect.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_jpeg.ok());
    PP_EXPECT(h, no_document_jpeg.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_layers.ok());
    PP_EXPECT(h, no_document_layers.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_frames.ok());
    PP_EXPECT(h, no_document_frames.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_depth.ok());
    PP_EXPECT(h, no_document_depth.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !no_document_depth_pngs.ok());
    PP_EXPECT(h, no_document_depth_pngs.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_frame.ok());
    PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_frame_readiness.ok());
    PP_EXPECT(h, bad_frame_readiness.status().code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_frame_depth.ok());
    PP_EXPECT(h, bad_frame_depth.status().code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_extent_depth.ok());
    PP_EXPECT(h, bad_extent_depth.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_frame_depth_pngs.ok());
    PP_EXPECT(h, bad_frame_depth_pngs.status().code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_extent_depth_pngs.ok());
    PP_EXPECT(h, bad_extent_depth_pngs.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_jpeg_quality.ok());
    PP_EXPECT(h, bad_jpeg_quality.status().code == StatusCode::out_of_range);
    PP_EXPECT(h, device.commands().empty());
}

void detects_feedback_requirements(pp::tests::Harness& h)
{
    PP_EXPECT(h, !stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::normal,
        false,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::multiply,
        StrokeBlendMode::normal,
        false,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::overlay,
        false,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::normal,
        true,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::normal,
        false,
        true));
}

void plans_stroke_composite_paths(pp::tests::Harness& h)
{
    const StrokeCompositeRequest simple {
        .extent = Extent2D { .width = 64, .height = 32 },
        .target_usage = TextureUsage::render_target,
    };
    const auto fixed = plan_stroke_composite(
        RenderDeviceFeatures {},
        simple);
    PP_EXPECT(h, fixed);
    if (fixed) {
        PP_EXPECT(h, fixed.value().path == StrokeCompositePath::fixed_function_blend);
        PP_EXPECT(h, !fixed.value().complex_blend);
        PP_EXPECT(h, !fixed.value().reads_destination_color);
        PP_EXPECT(h, fixed.value().target_bytes == 8192U);
        PP_EXPECT(h, fixed.value().estimated_working_bytes == 8192U);
    }

    const StrokeCompositeRequest complex_fetch {
        .extent = Extent2D { .width = 32, .height = 16 },
        .target_usage = TextureUsage::render_target,
        .stroke_blend_mode = StrokeBlendMode::height,
    };
    const auto fetch = plan_stroke_composite(
        RenderDeviceFeatures {
            .framebuffer_fetch = true,
            .explicit_texture_transitions = true,
        },
        complex_fetch);
    PP_EXPECT(h, fetch);
    if (fetch) {
        PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, fetch.value().complex_blend);
        PP_EXPECT(h, fetch.value().reads_destination_color);
        PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
        PP_EXPECT(h, fetch.value().requires_explicit_transition);
        PP_EXPECT(h, fetch.value().target_bytes == 2048U);
    }

    const StrokeCompositeRequest complex_copy {
        .extent = Extent2D { .width = 32, .height = 16 },
        .layer_blend_mode = BlendMode::overlay,
        .dual_brush_blend = true,
    };
    const auto copy = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        complex_copy);
    PP_EXPECT(h, copy);
    if (copy) {
        PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
        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);
        PP_EXPECT(h, copy.value().target_bytes == 2048U);
        PP_EXPECT(h, copy.value().auxiliary_bytes == 2048U);
        PP_EXPECT(h, copy.value().estimated_working_bytes == 4096U);
    }

    const auto blit = plan_stroke_composite(
        RenderDeviceFeatures { .render_target_blit = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .pattern_blend = true,
        });
    PP_EXPECT(h, blit);
    if (blit) {
        PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, !blit.value().requires_texture_copy);
        PP_EXPECT(h, blit.value().requires_render_target_blit);
    }
}

void rejects_bad_stroke_composite_plans(pp::tests::Harness& h)
{
    const auto unsupported = plan_stroke_composite(
        RenderDeviceFeatures {},
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_mode = BlendMode::multiply,
        });
    const auto missing_usage = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .target_usage = TextureUsage::render_target,
            .layer_blend_mode = BlendMode::multiply,
        });
    const auto depth = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .target_format = TextureFormat::depth24_stencil8,
            .layer_blend_mode = BlendMode::multiply,
        });
    const auto bad_blend = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_mode = static_cast<BlendMode>(255),
        });
    const auto bad_stroke_blend = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .stroke_blend_mode = static_cast<StrokeBlendMode>(255),
        });

    PP_EXPECT(h, !unsupported.ok());
    PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !missing_usage.ok());
    PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !depth.ok());
    PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_blend.ok());
    PP_EXPECT(h, bad_blend.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_stroke_blend.ok());
    PP_EXPECT(h, bad_stroke_blend.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::fixed_function_blend) == std::string_view("fixed_function_blend"));
    PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::framebuffer_fetch) == std::string_view("framebuffer_fetch"));
    PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::ping_pong_textures) == std::string_view("ping_pong_textures"));
    PP_EXPECT(h, stroke_composite_path_name(static_cast<StrokeCompositePath>(255)) == std::string_view("unknown"));
}

void plans_canvas_stroke_material_passes(pp::tests::Harness& h)
{
    const auto simple = plan_canvas_stroke_material(CanvasStrokeMaterialRequest {});
    PP_EXPECT(h, simple.texture_binding_count == 1U);
    PP_EXPECT(h, has_texture_binding(simple, CanvasStrokeTextureRole::main_brush_tip, 0));
    PP_EXPECT(h, !simple.stroke_pass.uses_destination_feedback);
    PP_EXPECT(h, !simple.stroke_pass.uses_pattern);
    PP_EXPECT(h, !simple.stroke_pass.uses_mixer);
    PP_EXPECT(h, !simple.dual_pass.enabled);
    PP_EXPECT(h, !simple.composite_pass.use_dual);
    PP_EXPECT(h, !simple.composite_pass.use_pattern);

    const auto eachsample = plan_canvas_stroke_material(
        CanvasStrokeMaterialRequest {
            .destination_feedback_needed = true,
            .pattern_enabled = true,
            .pattern_eachsample = true,
            .wet_blend = true,
            .noise_enabled = true,
        });
    PP_EXPECT(h, eachsample.stroke_pass.uses_destination_feedback);
    PP_EXPECT(h, eachsample.stroke_pass.uses_pattern);
    PP_EXPECT(h, eachsample.stroke_pass.uses_mixer);
    PP_EXPECT(h, !eachsample.composite_pass.use_pattern);
    PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::main_brush_tip, 0));
    PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::destination_feedback, 1));
    PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::pattern, 2));
    PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::mixer, 3));

    const auto composite_pattern = plan_canvas_stroke_material(
        CanvasStrokeMaterialRequest {
            .pattern_enabled = true,
            .pattern_eachsample = false,
            .pattern_blend_mode = 6,
        });
    PP_EXPECT(h, !composite_pattern.stroke_pass.uses_pattern);
    PP_EXPECT(h, composite_pattern.composite_pass.use_pattern);
    PP_EXPECT(h, composite_pattern.composite_pass.pattern_blend_mode == 6);
    PP_EXPECT(h, has_texture_binding(composite_pattern, CanvasStrokeTextureRole::pattern, 2));
}

void plans_canvas_stroke_dual_material_intent(pp::tests::Harness& h)
{
    const auto dual = plan_canvas_stroke_material(
        CanvasStrokeMaterialRequest {
            .pattern_enabled = true,
            .pattern_eachsample = true,
            .dual_brush_enabled = true,
            .dual_blend_mode = 3,
            .pattern_blend_mode = 4,
            .dual_alpha = 0.625F,
        });

    PP_EXPECT(h, dual.stroke_pass.uses_pattern);
    PP_EXPECT(h, dual.dual_pass.enabled);
    PP_EXPECT(h, !dual.dual_pass.uses_pattern);
    PP_EXPECT(h, dual.composite_pass.use_dual);
    PP_EXPECT(h, !dual.composite_pass.use_pattern);
    PP_EXPECT(h, dual.composite_pass.dual_blend_mode == 3);
    PP_EXPECT(h, dual.composite_pass.pattern_blend_mode == 4);
    PP_EXPECT(h, almost_equal(dual.composite_pass.dual_alpha, 0.625F));
    PP_EXPECT(h, has_texture_binding(dual, CanvasStrokeTextureRole::dual_brush_tip, 4));

    const auto dual_composite_pattern = plan_canvas_stroke_material(
        CanvasStrokeMaterialRequest {
            .pattern_enabled = true,
            .pattern_eachsample = false,
            .mix_blend = true,
            .dual_brush_enabled = true,
        });
    PP_EXPECT(h, !dual_composite_pattern.stroke_pass.uses_pattern);
    PP_EXPECT(h, dual_composite_pattern.stroke_pass.uses_mixer);
    PP_EXPECT(h, dual_composite_pattern.dual_pass.enabled);
    PP_EXPECT(h, !dual_composite_pattern.dual_pass.uses_pattern);
    PP_EXPECT(h, dual_composite_pattern.composite_pass.use_dual);
    PP_EXPECT(h, dual_composite_pattern.composite_pass.use_pattern);
    PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::mixer, 3));
    PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::dual_brush_tip, 4));
    PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::pattern, 2));
}

void legacy_canvas_stroke_main_pass_texture_dispatch_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto dispatch = pp::panopainter::make_legacy_canvas_stroke_main_pass_texture_dispatch(
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
        },
        [&] {
            steps.emplace_back("bind-tip");
        },
        [&] {
            steps.emplace_back("unbind-tip");
        },
        [&] {
            steps.emplace_back("bind-pattern");
        },
        [&] {
            steps.emplace_back("bind-mixer");
        },
        [&] {
            steps.emplace_back("unbind-mixer");
        });

    pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern,
                .slot = 2,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
                .slot = 3,
            },
        },
        dispatch);

    pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
                .slot = 3,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
        },
        dispatch);

    const std::vector<std::string> expected {
        "activate:0",
        "bind-tip",
        "activate:2",
        "bind-pattern",
        "activate:3",
        "bind-mixer",
        "activate:3",
        "unbind-mixer",
        "activate:0",
        "unbind-tip",
    };
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_stroke_live_pass_sampler_dispatch_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto dispatch = pp::panopainter::make_legacy_canvas_stroke_live_pass_sampler_dispatch(
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
            steps.emplace_back("bind-brush");
        },
        [&] {
            steps.emplace_back("unbind-brush");
        },
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
            steps.emplace_back("bind-stencil");
        },
        [&] {
            steps.emplace_back("unbind-stencil");
        },
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
            steps.emplace_back("bind-pattern");
        },
        [&] {
            steps.emplace_back("unbind-pattern");
        },
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
            steps.emplace_back("bind-mixer");
        },
        [&] {
            steps.emplace_back("unbind-mixer");
        });

    pp::panopainter::bind_legacy_canvas_stroke_sampler_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
                .slot = 1,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern,
                .slot = 2,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
                .slot = 3,
            },
        },
        dispatch);

    pp::panopainter::unbind_legacy_canvas_stroke_sampler_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
                .slot = 3,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern,
                .slot = 2,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
                .slot = 1,
            },
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
        },
        dispatch);

    const std::vector<std::string> expected {
        "activate:0",
        "bind-brush",
        "activate:1",
        "bind-stencil",
        "activate:2",
        "bind-pattern",
        "activate:3",
        "bind-mixer",
        "unbind-mixer",
        "unbind-pattern",
        "unbind-stencil",
        "unbind-brush",
    };
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_stroke_dual_pass_brush_tip_dispatch_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto dispatch = pp::panopainter::make_legacy_canvas_stroke_brush_tip_texture_dispatch(
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
        },
        [&](int) {
            steps.emplace_back("bind-tip");
        },
        [&](int) {
            steps.emplace_back("unbind-tip");
        },
        0);

    pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
        },
        dispatch);

    pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
                .slot = 0,
            },
        },
        dispatch);

    const std::vector<std::string> expected {
        "activate:0",
        "bind-tip",
        "activate:0",
        "unbind-tip",
    };
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_stroke_pad_copy_region_preserves_coordinates(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    pp::panopainter::execute_legacy_canvas_stroke_pad_copy_region(
        pp::paint_renderer::CanvasStrokeCopyRegion {
            .x = 7,
            .y = 11,
            .width = 13,
            .height = 17,
        },
        [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
            steps.emplace_back(
                std::to_string(src_x) + "," +
                std::to_string(src_y) + "," +
                std::to_string(dst_x) + "," +
                std::to_string(dst_y) + "," +
                std::to_string(width) + "," +
                std::to_string(height));
        });

    const std::vector<std::string> expected {
        "7,11,7,11,13,17",
    };
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_stroke_dual_shader_wrapper_preserves_setup(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    pp::panopainter::setup_legacy_canvas_stroke_dual_shader(true);
    steps.emplace_back("ok");
    PP_EXPECT(h, steps.size() == 1U);
}

void legacy_canvas_stroke_pad_destination_dispatch_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto dispatch = pp::panopainter::make_legacy_canvas_stroke_pad_destination_texture_dispatch(
        [&](int slot) {
            steps.emplace_back("activate:" + std::to_string(slot));
        },
        [&](int face_index) {
            steps.emplace_back("bind:" + std::to_string(face_index));
        },
        [&](int face_index) {
            steps.emplace_back("unbind:" + std::to_string(face_index));
        },
        4);

    pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
                .slot = 1,
            },
        },
        dispatch);
    pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
        std::array {
            pp::panopainter::LegacyCanvasStrokeTextureBinding {
                .input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
                .slot = 1,
            },
        },
        dispatch);

    const std::vector<std::string> expected {
        "activate:1",
        "bind:4",
        "activate:1",
        "unbind:4",
    };
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_stroke_pad_face_callbacks_preserve_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto padded_faces = pp::panopainter::execute_legacy_canvas_stroke_pad_face_callbacks(
        std::array {
            pp::panopainter::LegacyCanvasStrokePadFace {
                .index = 2,
                .dirty = true,
                .pass_dirty_box = glm::vec4(1.f),
            },
        },
        pp::renderer::Extent2D { 64U, 32U },
        true,
        [&](std::span<const vertex_t> pad_quad) {
            steps.emplace_back("upload:" + std::to_string(pad_quad.size()));
        },
        [&](int face_index) {
            steps.emplace_back("begin:" + std::to_string(face_index));
        },
        [&](int face_index) {
            steps.emplace_back("bind:" + std::to_string(face_index));
        },
        [&](const pp::paint_renderer::CanvasStrokeCopyRegion& copy_region) {
            steps.emplace_back(
                "copy:" + std::to_string(copy_region.x) + "," +
                std::to_string(copy_region.y) + "," +
                std::to_string(copy_region.width) + "," +
                std::to_string(copy_region.height));
        },
        [&](int face_index) {
            steps.emplace_back("unbind:" + std::to_string(face_index));
        },
        [&] {
            steps.emplace_back("draw");
        },
        [&](int face_index) {
            steps.emplace_back("finish:" + std::to_string(face_index));
        });

    const std::vector<std::string> expected {
        "upload:6",
        "begin:2",
        "bind:2",
        "copy:0,0,64,32",
        "draw",
        "unbind:2",
        "finish:2",
    };
    PP_EXPECT(h, padded_faces == 1U);
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_stroke_dual_pass_request_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto result = pp::panopainter::execute_legacy_canvas_stroke_dual_pass(
        pp::panopainter::LegacyCanvasStrokeDualPassRequest {
            .context = "compositor-test",
            .bind_brush_tip = [&] {
                steps.emplace_back("bind");
            },
            .unbind_brush_tip = [&] {
                steps.emplace_back("unbind");
            },
            .setup_dual_shader = [&] {
                steps.emplace_back("setup");
            },
            .execute_frame_pass = [&] {
                steps.emplace_back("frame");
            },
        });

    const std::vector<std::string> expected {
        "setup",
        "bind",
        "frame",
        "unbind",
    };
    PP_EXPECT(h, result.ok);
    PP_EXPECT(h, result.composed_planes == 0U);
    PP_EXPECT(h, steps == expected);
}

void plans_canvas_stroke_commit_erase_sequence(pp::tests::Harness& h)
{
    const auto plan = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = true,
            .alpha_locked = true,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });

    expect_commit_prefix(h, plan);
    PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::erase_draw);
    PP_EXPECT(h, plan.erase_mode);
    PP_EXPECT(h, plan.alpha_locked);
    PP_EXPECT(h, plan.selection_mask_active);
    PP_EXPECT(h, !plan.uses_dual_stroke);
    PP_EXPECT(h, !plan.uses_pattern);
    PP_EXPECT(h, plan.requires_history_readback);
    PP_EXPECT(h, !plan.updates_layer_bounds);
    PP_EXPECT(h, plan.requires_layer_scratch_copy);
    PP_EXPECT(h, plan.requires_committed_scratch_copy);
    PP_EXPECT(h, plan.requires_dilate);
    PP_EXPECT(h, plan.texture_binding_count == 3U);
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0));
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1));
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2));
}

void plans_canvas_stroke_commit_composite_sequence(pp::tests::Harness& h)
{
    const auto plan = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });

    expect_commit_prefix(h, plan);
    PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::composite_draw);
    PP_EXPECT(h, !plan.erase_mode);
    PP_EXPECT(h, !plan.alpha_locked);
    PP_EXPECT(h, plan.selection_mask_active);
    PP_EXPECT(h, plan.uses_dual_stroke);
    PP_EXPECT(h, plan.uses_pattern);
    PP_EXPECT(h, plan.updates_layer_bounds);
    PP_EXPECT(h, plan.texture_binding_count == 5U);
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0));
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1));
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2));
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::dual_stroke, 3));
    PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4));
}

void retained_stroke_commit_runner_clamps_malformed_step_count(pp::tests::Harness& h)
{
    pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence;
    sequence.step_count = 99U;
    sequence.steps[0] = CanvasStrokeCommitStep::bind_commit_inputs;
    sequence.steps[1] = CanvasStrokeCommitStep::composite_draw;

    int bind_inputs = 0;
    int paint_draws = 0;
    int erase_draws = 0;
    int started = 0;
    int restored = 0;
    int published = 0;
    int timelapse = 0;

    const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
        pp::panopainter::LegacyCanvasStrokeCommitRequest {
            .context = "test",
            .faces = {
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true },
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false },
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true },
            },
            .sequence = sequence,
            .callbacks = {
                .mark_commit_started = [&]() { ++started; },
                .capture_render_state = []() {},
                .prepare_render_state = []() {},
                .restore_render_state = [&]() { ++restored; },
                .publish_history = [&]() { ++published; },
                .capture_timelapse_frame = [&]() { ++timelapse; },
                .bind_layer_framebuffer = [](int) {},
                .capture_history_region = [](int) {},
                .apply_layer_dirty_region = [](int) {},
                .copy_layer_to_commit_destination = [](int) {},
                .bind_commit_inputs = [&](int) { ++bind_inputs; },
                .execute_erase_composite = [&](int) { ++erase_draws; },
                .execute_paint_composite = [&](int) { ++paint_draws; },
                .copy_committed_to_dilate_source = [](int) {},
                .execute_commit_dilate = [](int) {},
                .unbind_layer_framebuffer = [](int) {},
            },
        });

    PP_EXPECT(h, result.ok);
    PP_EXPECT(h, result.committed_faces == 2);
    PP_EXPECT(h, bind_inputs == 2);
    PP_EXPECT(h, paint_draws == 2);
    PP_EXPECT(h, erase_draws == 0);
    PP_EXPECT(h, started == 1);
    PP_EXPECT(h, restored == 1);
    PP_EXPECT(h, published == 1);
    PP_EXPECT(h, timelapse == 1);
}

void retained_stroke_commit_input_binder_uses_sequence_slots(pp::tests::Harness& h)
{
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });

    std::vector<int> active_slots;
    std::vector<CanvasStrokeCommitTextureRole> bound_textures;
    std::vector<std::pair<CanvasStrokeCommitTextureRole, int>> bound_samplers;

    pp::panopainter::bind_legacy_canvas_stroke_commit_inputs(
        sequence,
        [&](int texture_slot) {
            active_slots.push_back(texture_slot);
        },
        [&](CanvasStrokeCommitTextureRole role) {
            bound_textures.push_back(role);
        },
        [&](CanvasStrokeCommitTextureRole role, int texture_slot) {
            bound_samplers.emplace_back(role, texture_slot);
        });

    PP_EXPECT(h, active_slots.size() == 5U);
    PP_EXPECT(h, active_slots[0] == 0);
    PP_EXPECT(h, active_slots[1] == 1);
    PP_EXPECT(h, active_slots[2] == 2);
    PP_EXPECT(h, active_slots[3] == 3);
    PP_EXPECT(h, active_slots[4] == 4);
    PP_EXPECT(h, bound_textures.size() == 5U);
    PP_EXPECT(h, bound_textures[0] == CanvasStrokeCommitTextureRole::layer_scratch);
    PP_EXPECT(h, bound_textures[1] == CanvasStrokeCommitTextureRole::stroke);
    PP_EXPECT(h, bound_textures[2] == CanvasStrokeCommitTextureRole::selection_mask);
    PP_EXPECT(h, bound_textures[3] == CanvasStrokeCommitTextureRole::dual_stroke);
    PP_EXPECT(h, bound_textures[4] == CanvasStrokeCommitTextureRole::pattern);
    PP_EXPECT(h, bound_samplers.size() == 5U);
    PP_EXPECT(h, bound_samplers[0].first == CanvasStrokeCommitTextureRole::layer_scratch);
    PP_EXPECT(h, bound_samplers[0].second == 0);
    PP_EXPECT(h, bound_samplers[4].first == CanvasStrokeCommitTextureRole::pattern);
    PP_EXPECT(h, bound_samplers[4].second == 4);
}

void retained_stroke_commit_face_input_binder_forwards_sequence_slots(pp::tests::Harness& h)
{
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });

    std::vector<int> active_slots;
    std::vector<CanvasStrokeCommitTextureRole> bound_textures;
    std::vector<std::pair<CanvasStrokeCommitTextureRole, int>> bound_samplers;

    pp::panopainter::bind_legacy_canvas_stroke_commit_face_inputs(
        sequence,
        [&](int texture_slot) {
            active_slots.push_back(texture_slot);
        },
        [&](CanvasStrokeCommitTextureRole role) {
            bound_textures.push_back(role);
        },
        [&](CanvasStrokeCommitTextureRole role, int texture_slot) {
            bound_samplers.emplace_back(role, texture_slot);
        });

    PP_EXPECT(h, active_slots.size() == 5U);
    PP_EXPECT(h, bound_textures.size() == 5U);
    PP_EXPECT(h, bound_samplers.size() == 5U);
    PP_EXPECT(h, active_slots[0] == 0);
    PP_EXPECT(h, active_slots[4] == 4);
    PP_EXPECT(h, bound_textures[2] == CanvasStrokeCommitTextureRole::selection_mask);
    PP_EXPECT(h, bound_samplers[3].first == CanvasStrokeCommitTextureRole::dual_stroke);
    PP_EXPECT(h, bound_samplers[3].second == 3);
}

void retained_stroke_commit_request_builder_preserves_dirty_face_order(pp::tests::Harness& h)
{
    const pp::panopainter::LegacyCanvasStrokeCommitCallbacks callbacks {
        .mark_commit_started = []() {},
        .capture_render_state = []() {},
        .prepare_render_state = []() {},
        .restore_render_state = []() {},
        .publish_history = []() {},
        .capture_timelapse_frame = []() {},
        .bind_layer_framebuffer = [](int) {},
        .capture_history_region = [](int) {},
        .apply_layer_dirty_region = [](int) {},
        .copy_layer_to_commit_destination = [](int) {},
        .bind_commit_inputs = [](int) {},
        .execute_erase_composite = [](int) {},
        .execute_paint_composite = [](int) {},
        .copy_committed_to_dilate_source = [](int) {},
        .execute_commit_dilate = [](int) {},
        .unbind_layer_framebuffer = [](int) {},
    };
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });
    const std::array<pp::panopainter::LegacyCanvasStrokeCommitFace, 6> faces {
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 3, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 4, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 5, .dirty = true },
    };

    const auto request = pp::panopainter::LegacyCanvasStrokeCommitRequest {
        .context = "test",
        .faces = faces,
        .sequence = sequence,
        .callbacks = callbacks,
    };

    PP_EXPECT(h, request.faces[0].index == 0);
    PP_EXPECT(h, request.faces[1].index == 1);
    PP_EXPECT(h, request.faces[3].dirty);
    PP_EXPECT(h, request.faces[5].index == 5);
}

void retained_stroke_commit_request_helper_preserves_dirty_face_order(pp::tests::Harness& h)
{
    const pp::panopainter::LegacyCanvasStrokeCommitCallbacks callbacks {
        .mark_commit_started = []() {},
        .capture_render_state = []() {},
        .prepare_render_state = []() {},
        .restore_render_state = []() {},
        .publish_history = []() {},
        .capture_timelapse_frame = []() {},
        .bind_layer_framebuffer = [](int) {},
        .capture_history_region = [](int) {},
        .apply_layer_dirty_region = [](int) {},
        .copy_layer_to_commit_destination = [](int) {},
        .bind_commit_inputs = [](int) {},
        .execute_erase_composite = [](int) {},
        .execute_paint_composite = [](int) {},
        .copy_committed_to_dilate_source = [](int) {},
        .execute_commit_dilate = [](int) {},
        .unbind_layer_framebuffer = [](int) {},
    };
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = true,
            .alpha_locked = true,
            .selection_mask_active = false,
            .dual_stroke_enabled = false,
            .pattern_enabled = false,
        });
    const std::array<pp::panopainter::LegacyCanvasStrokeCommitFace, 6> faces {
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 3, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 4, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 5, .dirty = false },
    };

    const auto request = pp::panopainter::make_legacy_canvas_stroke_commit_request(
        faces,
        sequence,
        callbacks);

    PP_EXPECT(h, request.context == "Canvas::stroke_commit");
    PP_EXPECT(h, request.faces[0].dirty);
    PP_EXPECT(h, request.faces[1].index == 1);
    PP_EXPECT(h, request.faces[2].dirty);
    PP_EXPECT(h, request.faces[4].dirty);
}

void retained_stroke_commit_sequence_executor_preserves_request_shape(pp::tests::Harness& h)
{
    const pp::panopainter::LegacyCanvasStrokeCommitCallbacks callbacks {
        .mark_commit_started = []() {},
        .capture_render_state = []() {},
        .prepare_render_state = []() {},
        .restore_render_state = []() {},
        .publish_history = []() {},
        .capture_timelapse_frame = []() {},
        .bind_layer_framebuffer = [](int) {},
        .capture_history_region = [](int) {},
        .apply_layer_dirty_region = [](int) {},
        .copy_layer_to_commit_destination = [](int) {},
        .bind_commit_inputs = [](int) {},
        .execute_erase_composite = [](int) {},
        .execute_paint_composite = [](int) {},
        .copy_committed_to_dilate_source = [](int) {},
        .execute_commit_dilate = [](int) {},
        .unbind_layer_framebuffer = [](int) {},
    };
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });
    const std::array<pp::panopainter::LegacyCanvasStrokeCommitFace, 6> faces {
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 3, .dirty = false },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 4, .dirty = true },
        pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 5, .dirty = false },
    };

    const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
        pp::panopainter::make_legacy_canvas_stroke_commit_request(faces, sequence, callbacks));

    PP_EXPECT(h, result.ok);
    PP_EXPECT(h, result.committed_faces == 3);
}

void retained_stroke_commit_dilate_copy_uses_layer_scratch_slot(pp::tests::Harness& h)
{
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = true,
            .alpha_locked = true,
            .selection_mask_active = false,
            .dual_stroke_enabled = false,
            .pattern_enabled = false,
        });

    int setup_calls = 0;
    std::vector<int> active_slots;
    int bind_layer_scratch_calls = 0;
    std::vector<pp::paint_renderer::CanvasStrokeCopyRegion> copy_regions;

    pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source(
        sequence,
        [&]() { ++setup_calls; },
        [&](int texture_slot) { active_slots.push_back(texture_slot); },
        [&]() { ++bind_layer_scratch_calls; },
        [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
            copy_regions.push_back(pp::paint_renderer::CanvasStrokeCopyRegion {
                .x = src_x,
                .y = src_y,
                .width = width,
                .height = height,
            });
            PP_EXPECT(h, dst_x == 0);
            PP_EXPECT(h, dst_y == 0);
        },
        pp::panopainter::LegacyCanvasStrokeCommitCopyExtent {
            .width = 256,
            .height = 128,
        });

    PP_EXPECT(h, setup_calls == 1);
    PP_EXPECT(h, active_slots.size() == 1U);
    PP_EXPECT(h, active_slots[0] == 0);
    PP_EXPECT(h, bind_layer_scratch_calls == 1);
    PP_EXPECT(h, copy_regions.size() == 1U);
    PP_EXPECT(h, copy_regions[0].x == 0);
    PP_EXPECT(h, copy_regions[0].y == 0);
    PP_EXPECT(h, copy_regions[0].width == 256);
    PP_EXPECT(h, copy_regions[0].height == 128);
}

void retained_stroke_commit_runner_preserves_per_face_step_order(pp::tests::Harness& h)
{
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });

    std::vector<std::string> events;
    const auto record = [&](std::string_view event, int face_index = -1) {
        if (face_index >= 0) {
            events.push_back(std::string(event) + ":" + std::to_string(face_index));
        } else {
            events.push_back(std::string(event));
        }
    };

    const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
        pp::panopainter::LegacyCanvasStrokeCommitRequest {
            .context = "test",
            .faces = {
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true },
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false },
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true },
            },
            .sequence = sequence,
            .callbacks = {
                .mark_commit_started = [&]() { record("start"); },
                .capture_render_state = [&]() { record("capture"); },
                .prepare_render_state = [&]() { record("prepare"); },
                .restore_render_state = [&]() { record("restore"); },
                .publish_history = [&]() { record("publish"); },
                .capture_timelapse_frame = [&]() { record("timelapse"); },
                .bind_layer_framebuffer = [&](int face_index) { record("bind-fbo", face_index); },
                .capture_history_region = [&](int face_index) { record("history", face_index); },
                .apply_layer_dirty_region = [&](int face_index) { record("dirty", face_index); },
                .copy_layer_to_commit_destination = [&](int face_index) { record("copy-layer", face_index); },
                .bind_commit_inputs = [&](int face_index) { record("bind-inputs", face_index); },
                .execute_erase_composite = [&](int face_index) { record("erase", face_index); },
                .execute_paint_composite = [&](int face_index) { record("paint", face_index); },
                .copy_committed_to_dilate_source = [&](int face_index) { record("copy-committed", face_index); },
                .execute_commit_dilate = [&](int face_index) { record("dilate", face_index); },
                .unbind_layer_framebuffer = [&](int face_index) { record("unbind-fbo", face_index); },
            },
        });

    const std::vector<std::string> expected {
        "start",
        "capture",
        "prepare",
        "bind-fbo:0",
        "history:0",
        "dirty:0",
        "copy-layer:0",
        "bind-inputs:0",
        "paint:0",
        "copy-committed:0",
        "dilate:0",
        "unbind-fbo:0",
        "bind-fbo:2",
        "history:2",
        "dirty:2",
        "copy-layer:2",
        "bind-inputs:2",
        "paint:2",
        "copy-committed:2",
        "dilate:2",
        "unbind-fbo:2",
        "restore",
        "publish",
        "timelapse",
    };

    PP_EXPECT(h, result.ok);
    PP_EXPECT(h, result.committed_faces == 2);
    PP_EXPECT(h, events == expected);
}

void retained_stroke_commit_callback_builder_preserves_order(pp::tests::Harness& h)
{
    const auto sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = false,
            .alpha_locked = false,
            .selection_mask_active = true,
            .dual_stroke_enabled = true,
            .pattern_enabled = true,
        });

    std::vector<std::string> events;
    const auto record = [&](std::string_view event, int face_index = -1) {
        if (face_index >= 0) {
            events.push_back(std::string(event) + ":" + std::to_string(face_index));
        } else {
            events.push_back(std::string(event));
        }
    };

    const auto callbacks = pp::panopainter::make_legacy_canvas_stroke_commit_callbacks(
        [&]() { record("start"); },
        [&]() { record("capture"); },
        [&]() { record("prepare"); },
        [&]() { record("restore"); },
        [&]() { record("publish"); },
        [&]() { record("timelapse"); },
        [&](int face_index) { record("bind-fbo", face_index); },
        [&](int face_index) { record("history", face_index); },
        [&](int face_index) { record("dirty", face_index); },
        [&](int face_index) { record("copy-layer", face_index); },
        [&](int face_index) { record("bind-inputs", face_index); },
        [&](int face_index) { record("erase", face_index); },
        [&](int face_index) { record("paint", face_index); },
        [&](int face_index) { record("copy-committed", face_index); },
        [&](int face_index) { record("dilate", face_index); },
        [&](int face_index) { record("unbind-fbo", face_index); });

    const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
        pp::panopainter::LegacyCanvasStrokeCommitRequest {
            .context = "test",
            .faces = {
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true },
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false },
                pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true },
            },
            .sequence = sequence,
            .callbacks = callbacks,
        });

    const std::vector<std::string> expected {
        "start",
        "capture",
        "prepare",
        "bind-fbo:0",
        "history:0",
        "dirty:0",
        "copy-layer:0",
        "bind-inputs:0",
        "paint:0",
        "copy-committed:0",
        "dilate:0",
        "unbind-fbo:0",
        "bind-fbo:2",
        "history:2",
        "dirty:2",
        "copy-layer:2",
        "bind-inputs:2",
        "paint:2",
        "copy-committed:2",
        "dilate:2",
        "unbind-fbo:2",
        "restore",
        "publish",
        "timelapse",
    };

    PP_EXPECT(h, result.ok);
    PP_EXPECT(h, result.committed_faces == 2);
    PP_EXPECT(h, events == expected);
}

void retained_stroke_commit_copy_skips_missing_layer_scratch_or_invalid_extent(pp::tests::Harness& h)
{
    pp::paint_renderer::CanvasStrokeCommitSequencePlan missing_scratch;
    missing_scratch.texture_binding_count = 1U;
    missing_scratch.texture_bindings[0] = pp::paint_renderer::CanvasStrokeCommitTextureBindingPlan {
        .role = CanvasStrokeCommitTextureRole::stroke,
        .slot = 4,
    };

    int setup_calls = 0;
    int active_texture_calls = 0;
    int bind_layer_scratch_calls = 0;
    int copy_calls = 0;

    pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source(
        missing_scratch,
        [&]() { ++setup_calls; },
        [&](int) { ++active_texture_calls; },
        [&]() { ++bind_layer_scratch_calls; },
        [&](int, int, int, int, int, int) { ++copy_calls; },
        pp::panopainter::LegacyCanvasStrokeCommitCopyExtent {
            .width = 256,
            .height = 128,
        });

    const auto valid_sequence = plan_canvas_stroke_commit_sequence(
        CanvasStrokeCommitRequest {
            .erase_mode = true,
            .alpha_locked = true,
            .selection_mask_active = false,
            .dual_stroke_enabled = false,
            .pattern_enabled = false,
        });
    pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source(
        valid_sequence,
        [&]() { ++setup_calls; },
        [&](int) { ++active_texture_calls; },
        [&]() { ++bind_layer_scratch_calls; },
        [&](int, int, int, int, int, int) { ++copy_calls; },
        pp::panopainter::LegacyCanvasStrokeCommitCopyExtent {
            .width = 0,
            .height = 128,
        });

    PP_EXPECT(h, setup_calls == 0);
    PP_EXPECT(h, active_texture_calls == 0);
    PP_EXPECT(h, bind_layer_scratch_calls == 0);
    PP_EXPECT(h, copy_calls == 0);
}

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_stroke_preview_composite_with_all_retained_inputs(pp::tests::Harness& h)
{
    const auto plan = plan_stroke_preview_composite(
        StrokePreviewCompositeRequest {
            .uses_mixer = true,
            .uses_dual = true,
            .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 == 5U);
    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));
    PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4));
    PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3));
}

void legacy_node_stroke_preview_feedback_adapter_clamps_invalid_extent(pp::tests::Harness& h)
{
    const auto fetch = pp::panopainter::plan_legacy_node_stroke_preview_feedback(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        32,
        16);
    PP_EXPECT(h, fetch.path == StrokeCompositePath::framebuffer_fetch);
    PP_EXPECT(h, fetch.reads_destination_color);
    PP_EXPECT(h, !fetch.requires_auxiliary_texture);
    PP_EXPECT(h, !fetch.compatibility_fallback);

    const auto fallback = pp::panopainter::plan_legacy_node_stroke_preview_feedback(
        RenderDeviceFeatures { .texture_copy = true },
        -32,
        16);
    PP_EXPECT(h, fallback.path == StrokeCompositePath::ping_pong_textures);
    PP_EXPECT(h, !fallback.reads_destination_color);
    PP_EXPECT(h, fallback.requires_auxiliary_texture);
    PP_EXPECT(h, !fallback.requires_texture_copy);
    PP_EXPECT(h, !fallback.requires_render_target_blit);
    PP_EXPECT(h, fallback.compatibility_fallback);
}

void legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs(pp::tests::Harness& h)
{
    const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_composite(
        true,
        true,
        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 == 5U);
    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));
    PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4));
    PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3));
}

void legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms(pp::tests::Harness& h)
{
    const pp::panopainter::LegacyNodeStrokePreviewMixPassRequest request {
        .resolution = glm::vec2(128.0F, 64.0F),
        .pattern_scale = 0.25F,
        .pattern_flipx = true,
        .pattern_flipy = true,
        .pattern_invert = true,
        .pattern_brightness = 0.6F,
        .pattern_contrast = 0.8F,
        .pattern_depth = 0.9F,
        .pattern_rand_offset = true,
        .pattern_enabled = true,
        .pattern_eachsample = false,
        .tip_wet = 0.3F,
        .tip_mix = 0.7F,
        .tip_noise = 0.2F,
        .dual_enabled = true,
        .dual_blend_mode = 9,
        .pattern_blend_mode = 7,
        .dual_opacity = 0.4F,
        .blend_mode = 5,
    };

    const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass(request);

    PP_EXPECT(h, plan.material.dual_pass.enabled);
    PP_EXPECT(h, !plan.material.stroke_pass.uses_pattern);
    PP_EXPECT(h, plan.material.composite_pass.use_dual);
    PP_EXPECT(h, plan.material.composite_pass.use_pattern);
    PP_EXPECT(h, plan.material.composite_pass.dual_blend_mode == request.dual_blend_mode);
    PP_EXPECT(h, almost_equal(plan.material.composite_pass.dual_alpha, request.dual_opacity));
    PP_EXPECT(h, plan.material.composite_pass.pattern_blend_mode == request.pattern_blend_mode);

    PP_EXPECT(h, almost_equal(plan.shader.resolution, request.resolution));
    PP_EXPECT(h, almost_equal(plan.shader.pattern_scale, glm::vec2(-0.25F, -0.25F)));
    PP_EXPECT(h, almost_equal(plan.shader.pattern_invert, 1.0F));
    PP_EXPECT(h, almost_equal(plan.shader.pattern_brightness, request.pattern_brightness));
    PP_EXPECT(h, almost_equal(plan.shader.pattern_contrast, request.pattern_contrast));
    PP_EXPECT(h, almost_equal(plan.shader.pattern_depth, request.pattern_depth));
    PP_EXPECT(h, plan.shader.pattern_blend_mode == request.pattern_blend_mode);
    PP_EXPECT(h, almost_equal(plan.shader.pattern_offset, glm::vec2(0.5F, 0.5F)));
    PP_EXPECT(h, plan.shader.blend_mode == request.blend_mode);
    PP_EXPECT(h, plan.shader.use_dual == plan.material.composite_pass.use_dual);
    PP_EXPECT(h, plan.shader.dual_blend_mode == plan.material.composite_pass.dual_blend_mode);
    PP_EXPECT(h, almost_equal(plan.shader.dual_alpha, plan.material.composite_pass.dual_alpha));
    PP_EXPECT(h, plan.shader.use_pattern == plan.material.composite_pass.use_pattern);
}

void legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    int observed_plane_index = -1;

    const std::array planes {
        pp::panopainter::LegacyCanvasStrokeMixPassPlane {
            .index = 4,
            .visible = true,
            .has_target = true,
            .opacity = 1.0f,
            .mvp = glm::mat4(1.0f),
        },
    };

    const auto ok = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
        pp::panopainter::LegacyCanvasStrokeMixPassRequest {
            .context = "NodeStrokePreview::stroke_draw_mix",
            .resolution = glm::vec2(128.0F, 64.0F),
            .planes = planes,
            .bind_mix_samplers = [&] {
                steps.emplace_back("bind-mix");
            },
            .unbind_mix_samplers = [&] {
                steps.emplace_back("unbind-mix");
            },
            .setup_plane_shader = [&](int index, const glm::mat4& mvp) {
                observed_plane_index = index;
                steps.emplace_back(
                    "setup:" + std::to_string(index) + ":" + std::to_string(mvp[0][0]));
            },
            .bind_layer_texture = [&](int /*index*/) {
                steps.emplace_back("bind-layer");
            },
            .bind_stroke_texture = [&](int /*index*/) {
                steps.emplace_back("bind-stroke");
            },
            .bind_mask_texture = [&](int /*index*/) {
                steps.emplace_back("bind-mask");
            },
            .draw_plane = [&] {
                steps.emplace_back("draw");
            },
            .unbind_mask_texture = [&](int /*index*/) {
                steps.emplace_back("unbind-mask");
            },
            .unbind_stroke_texture = [&](int /*index*/) {
                steps.emplace_back("unbind-stroke");
            },
            .unbind_layer_texture = [&](int /*index*/) {
                steps.emplace_back("unbind-layer");
            },
        });

    PP_EXPECT(h, ok.ok);
    PP_EXPECT(h, observed_plane_index == 4);

    const std::vector<std::string> expected_steps {
        "bind-mix",
        "setup:4:1.000000",
        "bind-layer",
        "bind-stroke",
        "bind-mask",
        "draw",
        "unbind-mask",
        "unbind-stroke",
        "unbind-layer",
        "unbind-mix",
    };
    PP_EXPECT(h, steps == expected_steps);

    const auto invalid = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
        pp::panopainter::LegacyCanvasStrokeMixPassRequest {
            .context = "NodeStrokePreview::stroke_draw_mix",
            .resolution = glm::vec2(128.0F, 64.0F),
            .planes = planes,
        });
    PP_EXPECT(h, !invalid.ok);
}

void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto run_sequence = [&](bool dual_enabled) {
        steps.clear();
        if (dual_enabled) {
            steps.emplace_back("prepare_dual");
            steps.emplace_back("execute_dual");
        }

        steps.emplace_back("capture_background");
        steps.emplace_back("prepare_main");
        const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass(
            pp::panopainter::LegacyNodeStrokePreviewMainLivePassRequestT<std::uint8_t> {
                .setup_blend_uniforms = [] {},
                .bind_main_pass_textures = [] {},
                .clear_target = [] {},
                .compute_frames = [&] {
                    return std::vector<std::uint8_t> { 1 };
                },
                .before_frame = [](std::uint8_t&) {},
                .setup_sample_shader = [](std::uint8_t&) {},
                .draw_sample = [&] (std::uint8_t&) {
                    steps.emplace_back("execute_main");
                },
                .copy_pass_result = [] {},
                .finish_main_pass = [] {},
            });
        PP_EXPECT(h, ok);
        steps.emplace_back("finish_main");

        pp::panopainter::execute_legacy_stroke_preview_final_composite(
            [&] {
                steps.emplace_back("execute_composite");
            },
            [] {},
            [] {},
            [] {});
        const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
            [] {},
            [&](int, int, int, int, int, int) {
                steps.emplace_back("copy_preview");
            },
            pp::paint_renderer::StrokePreviewCopySize {
                .width = 32,
                .height = 16,
            });
        PP_EXPECT(h, copy_status.ok());
    };

    run_sequence(true);
    const std::vector<std::string> dual_steps {
        "prepare_dual",
        "execute_dual",
        "capture_background",
        "prepare_main",
        "execute_main",
        "finish_main",
        "execute_composite",
        "copy_preview",
    };
    PP_EXPECT(h, steps == dual_steps);

    run_sequence(false);
    const std::vector<std::string> single_steps {
        "capture_background",
        "prepare_main",
        "execute_main",
        "finish_main",
        "execute_composite",
        "copy_preview",
    };
    PP_EXPECT(h, steps == single_steps);
}

void legacy_node_stroke_preview_main_live_pass_preserves_order(pp::tests::Harness& h)
{
    struct Frame {
        int id = 0;
    };

    std::vector<std::string> steps;
    const auto run = [&](std::vector<Frame> frames) {
        steps.clear();
        const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass<Frame>(
            pp::panopainter::LegacyNodeStrokePreviewMainLivePassRequestT<Frame> {
                .setup_blend_uniforms = [&] {
                    steps.emplace_back("setup_blend");
                },
                .bind_main_pass_textures = [&] {
                    steps.emplace_back("bind_textures");
                },
                .clear_target = [&] {
                    steps.emplace_back("clear");
                },
                .compute_frames = [&] {
                    steps.emplace_back("compute");
                    return frames;
                },
                .before_frame = [&](Frame& frame) {
                    steps.emplace_back("before:" + std::to_string(frame.id));
                },
                .setup_sample_shader = [&](Frame& frame) {
                    steps.emplace_back("setup:" + std::to_string(frame.id));
                },
                .draw_sample = [&](Frame& frame) {
                    steps.emplace_back("draw:" + std::to_string(frame.id));
                },
                .copy_pass_result = [&] {
                    steps.emplace_back("copy");
                },
                .finish_main_pass = [&] {
                    steps.emplace_back("finish");
                },
            });
        PP_EXPECT(h, ok);
    };

    run({});
    PP_EXPECT(h, (steps == std::vector<std::string> {
        "setup_blend",
        "bind_textures",
        "compute",
        "clear",
        "copy",
        "finish",
    }));

    run({ Frame { .id = 7 } });
    PP_EXPECT(h, (steps == std::vector<std::string> {
        "setup_blend",
        "bind_textures",
        "compute",
        "clear",
        "before:7",
        "setup:7",
        "draw:7",
        "copy",
        "finish",
    }));

    const bool invalid = pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass<Frame>(
        pp::panopainter::LegacyNodeStrokePreviewMainLivePassRequestT<Frame> {});
    PP_EXPECT(h, !invalid);
}

void legacy_node_stroke_preview_main_pass_texture_dispatch_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    const auto run = [&](bool copy_stroke_destination, bool uses_mixer) {
        steps.clear();
        pp::panopainter::bind_legacy_node_stroke_preview_main_pass_textures(
            pp::panopainter::make_legacy_node_stroke_preview_main_pass_texture_dispatch(
                [&](int texture_slot) {
                    steps.emplace_back("unit:" + std::to_string(texture_slot));
                },
                [&] {
                    steps.emplace_back("bind_tip");
                },
                [&] {
                    steps.emplace_back("bind_destination");
                },
                [&] {
                    steps.emplace_back("bind_pattern");
                },
                [&] {
                    steps.emplace_back("bind_mixer");
                }),
            copy_stroke_destination,
            uses_mixer);
    };

    run(false, false);
    PP_EXPECT(h, (steps == std::vector<std::string> {
        "unit:0",
        "bind_tip",
        "unit:2",
        "bind_pattern",
        "unit:3",
    }));

    run(true, true);
    PP_EXPECT(h, (steps == std::vector<std::string> {
        "unit:0",
        "bind_tip",
        "unit:1",
        "bind_destination",
        "unit:2",
        "bind_pattern",
        "unit:3",
        "bind_mixer",
    }));
}

void legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    pp::panopainter::execute_legacy_stroke_preview_final_composite(
        [&] {
            steps.emplace_back("setup");
        },
        [&] {
            steps.emplace_back("bind_samplers");
        },
        [&] {
            steps.emplace_back("bind_inputs");
        },
        [&] {
            steps.emplace_back("draw");
        });
    PP_EXPECT(h, (steps == std::vector<std::string> {
        "setup",
        "bind_samplers",
        "bind_inputs",
        "draw",
    }));

    steps.clear();
    const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
        [&] {
            steps.emplace_back("bind");
        },
        [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
            steps.emplace_back(
                "copy:" +
                std::to_string(src_x) + "," +
                std::to_string(src_y) + "," +
                std::to_string(dst_x) + "," +
                std::to_string(dst_y) + "," +
                std::to_string(width) + "," +
                std::to_string(height));
        },
        pp::paint_renderer::StrokePreviewCopySize {
            .width = 32,
            .height = 16,
        });
    PP_EXPECT(h, copy_status.ok());
    PP_EXPECT(h, (steps == std::vector<std::string> {
        "bind",
        "copy:0,0,0,0,32,16",
    }));
}

void legacy_node_stroke_preview_stroke_setup_plan_preserves_curve_and_dual_inputs(pp::tests::Harness& h)
{
    const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_stroke_setup(
        pp::panopainter::LegacyNodeStrokePreviewStrokeSetupRequest {
            .preview_size = glm::vec2(120.0F, 80.0F),
            .zoom = 2.0F,
            .brush_tip_size = 30.0F,
            .stroke_max_size_override = 0.0F,
            .pad_override = 17.0F,
            .tip_size_pressure = false,
            .dual_enabled = true,
            .dual_size = 0.25F,
            .pattern_scale = 0.5F,
            .pattern_flipx = true,
            .pattern_flipy = false,
            .preview_point_count = 4,
        });

    PP_EXPECT(h, almost_equal(plan.stroke_max_size, 60.0F));
    PP_EXPECT(h, almost_equal(plan.dual_stroke_max_size, 15.0F));
    PP_EXPECT(h, plan.dual_enabled);
    PP_EXPECT(h, almost_equal(plan.pattern_scale, glm::vec2(-0.5F, 0.5F)));
    PP_EXPECT(h, plan.points.size() == 4U);

    PP_EXPECT(h, almost_equal(plan.points[0].position.x, 17.0F));
    PP_EXPECT(h, almost_equal(plan.points[0].position.y, 80.0F));
    PP_EXPECT(h, almost_equal(plan.points[0].position.z, 0.0F));
    PP_EXPECT(h, almost_equal(plan.points[0].pressure, 0.0F));

    PP_EXPECT(h, almost_equal(plan.points[1].pressure, 0.55F));
    PP_EXPECT(h, almost_equal(plan.points[2].pressure, 1.0F));
    PP_EXPECT(h, almost_equal(plan.points[3].pressure, 0.55F));

    const auto pressure_fallback = pp::panopainter::plan_legacy_node_stroke_preview_stroke_setup(
        pp::panopainter::LegacyNodeStrokePreviewStrokeSetupRequest {
            .preview_size = glm::vec2(120.0F, 80.0F),
            .zoom = 2.0F,
            .brush_tip_size = 30.0F,
            .stroke_max_size_override = 20.0F,
            .pad_override = NAN,
            .tip_size_pressure = true,
            .dual_enabled = false,
            .dual_size = 2.0F,
            .pattern_scale = 0.25F,
            .pattern_flipx = false,
            .pattern_flipy = true,
            .preview_point_count = 0,
        });

    PP_EXPECT(h, almost_equal(pressure_fallback.stroke_max_size, 20.0F));
    PP_EXPECT(h, almost_equal(pressure_fallback.dual_stroke_max_size, 40.0F));
    PP_EXPECT(h, !pressure_fallback.dual_enabled);
    PP_EXPECT(h, almost_equal(pressure_fallback.pattern_scale, glm::vec2(0.25F, -0.25F)));
    PP_EXPECT(h, pressure_fallback.points.empty());
}

void legacy_node_stroke_preview_pass_orchestration_plan_preserves_feedback_material_and_composite_inputs(
    pp::tests::Harness& h)
{
    const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_pass_orchestration(
        pp::panopainter::LegacyNodeStrokePreviewPassOrchestrationRequest {
            .features = RenderDeviceFeatures { .framebuffer_fetch = false },
            .preview_size = glm::vec2(96.0F, 48.0F),
            .pattern_scale = 0.75F,
            .pattern_flipx = true,
            .pattern_flipy = false,
            .pattern_invert = true,
            .pattern_brightness = 0.2F,
            .pattern_contrast = 0.3F,
            .pattern_depth = 0.4F,
            .pattern_rand_offset = true,
            .pattern_enabled = true,
            .pattern_eachsample = false,
            .tip_mix = 0.5F,
            .tip_wet = 0.25F,
            .tip_noise = 0.1F,
            .dual_enabled = true,
            .dual_blend_mode = 2,
            .dual_opacity = 0.6F,
            .pattern_blend_mode = 3,
            .blend_mode = 4,
            .mvp = glm::ortho(-2.0F, 2.0F, -1.0F, 1.0F),
        });

    PP_EXPECT(h, plan.feedback.compatibility_fallback);
    PP_EXPECT(h, !plan.feedback.reads_destination_color);
    PP_EXPECT(h, plan.copy_stroke_destination);
    PP_EXPECT(h, plan.background_colorize);

    PP_EXPECT(h, plan.material.stroke_pass.uses_destination_feedback);
    PP_EXPECT(h, plan.material.composite_pass.use_dual);
    PP_EXPECT(h, plan.material.composite_pass.use_pattern);
    PP_EXPECT(h, plan.material.composite_pass.pattern_blend_mode == 3);
    PP_EXPECT(h, plan.material.composite_pass.dual_blend_mode == 2);
    PP_EXPECT(h, almost_equal(plan.material.composite_pass.dual_alpha, 0.6F));

    PP_EXPECT(h, plan.composite.uses_mixer);
    PP_EXPECT(h, plan.composite.uses_dual == plan.material.composite_pass.use_dual);
    PP_EXPECT(h, plan.composite.uses_pattern == plan.material.composite_pass.use_pattern);
    const std::size_t expected_texture_slots =
        2U +
        (plan.composite.uses_dual ? 1U : 0U) +
        (plan.composite.uses_pattern ? 1U : 0U) +
        (plan.composite.uses_mixer ? 1U : 0U);
    PP_EXPECT(h, plan.composite.texture_slot_count == expected_texture_slots);
    PP_EXPECT(h, has_preview_texture_slot(plan.composite, StrokePreviewTextureRole::background, 0));
    PP_EXPECT(h, has_preview_texture_slot(plan.composite, StrokePreviewTextureRole::stroke, 1));
    PP_EXPECT(h, has_preview_texture_slot(plan.composite, StrokePreviewTextureRole::dual, 3));
    PP_EXPECT(h, has_preview_texture_slot(plan.composite, StrokePreviewTextureRole::mixer, 3));
    PP_EXPECT(h, has_preview_texture_slot(plan.composite, StrokePreviewTextureRole::pattern, 4));

    PP_EXPECT(h, almost_equal(plan.stroke_shader.resolution, glm::vec2(96.0F, 48.0F)));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.pattern.scale, glm::vec2(-0.75F, 0.75F)));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.pattern.invert, 1.0F));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.pattern.brightness, 0.2F));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.pattern.contrast, 0.3F));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.pattern.depth, 0.4F));
    PP_EXPECT(h, plan.stroke_shader.pattern.blend_mode == 3);
    PP_EXPECT(h, almost_equal(plan.stroke_shader.pattern.offset, glm::vec2(0.5F, 0.5F)));
    PP_EXPECT(h, plan.stroke_shader.uses_destination_feedback);
    PP_EXPECT(h, !plan.stroke_shader.uses_pattern);
    PP_EXPECT(h, !plan.stroke_shader.set_opacity);
    PP_EXPECT(h, almost_equal(plan.stroke_shader.mix_alpha, 0.0F));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.wet, 0.0F));
    PP_EXPECT(h, almost_equal(plan.stroke_shader.noise, 0.0F));
}

void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
{
    const std::vector<int> normal_layers { 0, 0, 0 };
    const auto normal = plan_canvas_blend_gate(
        RenderDeviceFeatures {},
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 0, .height = 0 },
            .layer_blend_modes = normal_layers,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 0,
        });
    PP_EXPECT(h, normal);
    if (normal) {
        PP_EXPECT(h, !normal.value().shader_blend);
        PP_EXPECT(h, !normal.value().complex_blend);
        PP_EXPECT(h, !normal.value().compatibility_fallback);
    }

    const std::vector<int> layer_blend { 0, 4 };
    const auto layer = plan_canvas_blend_gate(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = layer_blend,
        });
    PP_EXPECT(h, layer);
    if (layer) {
        PP_EXPECT(h, layer.value().shader_blend);
        PP_EXPECT(h, layer.value().complex_blend);
        PP_EXPECT(h, layer.value().first_complex_layer_index == 1);
        PP_EXPECT(h, layer.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, layer.value().reads_destination_color);
    }

    const auto stroke = plan_canvas_blend_gate(
        RenderDeviceFeatures { .texture_copy = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = normal_layers,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 10,
        });
    PP_EXPECT(h, stroke);
    if (stroke) {
        PP_EXPECT(h, stroke.value().shader_blend);
        PP_EXPECT(h, stroke.value().stroke_complex);
        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().reads_destination_color);
        PP_EXPECT(h, stroke.value().requires_texture_copy);
    }
}

void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
{
    const std::vector<int> unknown_layer { 0, 99 };
    const auto unknown = plan_canvas_blend_gate(
        RenderDeviceFeatures { .texture_copy = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = unknown_layer,
        });
    PP_EXPECT(h, unknown);
    if (unknown) {
        PP_EXPECT(h, unknown.value().shader_blend);
        PP_EXPECT(h, unknown.value().complex_blend);
        PP_EXPECT(h, unknown.value().compatibility_fallback);
        PP_EXPECT(h, unknown.value().first_complex_layer_index == 1);
        PP_EXPECT(h, unknown.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, unknown.value().requires_auxiliary_texture);
        PP_EXPECT(h, unknown.value().requires_texture_copy);
    }

    const std::vector<int> normal_layers { 0 };
    const auto unsupported = plan_canvas_blend_gate(
        RenderDeviceFeatures {},
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = normal_layers,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 10,
        });
    PP_EXPECT(h, unsupported);
    if (unsupported) {
        PP_EXPECT(h, unsupported.value().shader_blend);
        PP_EXPECT(h, unsupported.value().stroke_complex);
        PP_EXPECT(h, unsupported.value().compatibility_fallback);
        PP_EXPECT(h, !unsupported.value().requires_texture_copy);
    }

    const auto unknown_fetch = plan_canvas_blend_gate(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = unknown_layer,
        });
    PP_EXPECT(h, unknown_fetch);
    if (unknown_fetch) {
        PP_EXPECT(h, unknown_fetch.value().compatibility_fallback);
        PP_EXPECT(h, unknown_fetch.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, unknown_fetch.value().reads_destination_color);
        PP_EXPECT(h, !unknown_fetch.value().requires_texture_copy);
    }

    const auto dual_pattern = plan_canvas_blend_gate(
        RenderDeviceFeatures { .render_target_blit = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 16, .height = 16 },
            .layer_blend_modes = normal_layers,
            .dual_brush_blend = true,
            .pattern_blend = true,
        });
    PP_EXPECT(h, dual_pattern);
    if (dual_pattern) {
        PP_EXPECT(h, dual_pattern.value().shader_blend);
        PP_EXPECT(h, dual_pattern.value().dual_brush_complex);
        PP_EXPECT(h, dual_pattern.value().pattern_complex);
        PP_EXPECT(h, dual_pattern.value().requires_render_target_blit);
    }
}

void canvas_blend_gate_combines_layer_and_stroke_complexity(pp::tests::Harness& h)
{
    const std::vector<int> layer_blend_modes { 0, 4, 0 };
    const auto plan = plan_canvas_blend_gate(
        RenderDeviceFeatures { .framebuffer_fetch = true, .texture_copy = true, .render_target_blit = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 64, .height = 32 },
            .layer_blend_modes = layer_blend_modes,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 10,
            .dual_brush_blend = true,
            .pattern_blend = true,
        });

    PP_EXPECT(h, plan);
    if (!plan) {
        return;
    }

    PP_EXPECT(h, plan.value().shader_blend);
    PP_EXPECT(h, plan.value().complex_blend);
    PP_EXPECT(h, plan.value().stroke_complex);
    PP_EXPECT(h, plan.value().dual_brush_complex);
    PP_EXPECT(h, plan.value().pattern_complex);
    PP_EXPECT(h, plan.value().first_complex_layer_index == 1);
    PP_EXPECT(h, plan.value().reads_destination_color);
    PP_EXPECT(h, !plan.value().requires_auxiliary_texture);
    PP_EXPECT(h, !plan.value().requires_texture_copy);
    PP_EXPECT(h, !plan.value().requires_render_target_blit);
    PP_EXPECT(h, plan.value().path == StrokeCompositePath::framebuffer_fetch);
}

void legacy_canvas_stroke_mix_executor_preserves_plane_callback_order(pp::tests::Harness& h)
{
    std::vector<std::string> steps;
    std::array<pp::panopainter::LegacyCanvasStrokeMixPassPlane, 3> planes {
        pp::panopainter::LegacyCanvasStrokeMixPassPlane {
            .index = 4,
            .visible = true,
            .has_target = true,
            .opacity = 0.75f,
            .mvp = glm::mat4(1.0f),
        },
        pp::panopainter::LegacyCanvasStrokeMixPassPlane {
            .index = 5,
            .visible = false,
            .has_target = true,
            .opacity = 0.5f,
            .mvp = glm::mat4(1.0f),
        },
        pp::panopainter::LegacyCanvasStrokeMixPassPlane {
            .index = 6,
            .visible = true,
            .has_target = false,
            .opacity = 0.9f,
            .mvp = glm::mat4(1.0f),
        },
    };

    const auto result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
        pp::panopainter::LegacyCanvasStrokeMixPassRequest {
            .context = "mix-order",
            .resolution = glm::vec2(256.0f, 128.0f),
            .planes = planes,
            .bind_mix_samplers = [&] {
                steps.emplace_back("bind-mix");
            },
            .unbind_mix_samplers = [&] {
                steps.emplace_back("unbind-mix");
            },
            .setup_plane_shader = [&](int index, const glm::mat4& mvp) {
                steps.emplace_back("setup:" + std::to_string(index) + ":" + std::to_string(mvp[0][0]));
            },
            .bind_layer_texture = [&](int index) {
                steps.emplace_back("bind-layer:" + std::to_string(index));
            },
            .bind_stroke_texture = [&](int index) {
                steps.emplace_back("bind-stroke:" + std::to_string(index));
            },
            .bind_mask_texture = [&](int index) {
                steps.emplace_back("bind-mask:" + std::to_string(index));
            },
            .draw_plane = [&] {
                steps.emplace_back("draw");
            },
            .unbind_mask_texture = [&](int index) {
                steps.emplace_back("unbind-mask:" + std::to_string(index));
            },
            .unbind_stroke_texture = [&](int index) {
                steps.emplace_back("unbind-stroke:" + std::to_string(index));
            },
            .unbind_layer_texture = [&](int index) {
                steps.emplace_back("unbind-layer:" + std::to_string(index));
            },
        });

    PP_EXPECT(h, result.ok);
    PP_EXPECT(h, result.composed_planes == 1);

    const std::vector<std::string> expected {
        "bind-mix",
        "setup:4:1.000000",
        "bind-layer:4",
        "bind-stroke:4",
        "bind-mask:4",
        "draw",
        "unbind-mask:4",
        "unbind-stroke:4",
        "unbind-layer:4",
        "unbind-mix",
    };
    PP_EXPECT(h, steps == expected);
}

void legacy_canvas_draw_merge_temporary_erase_helper_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> order;
    const auto execution = pp::panopainter::make_legacy_canvas_draw_merge_temporary_erase_composite(
        [&] { order.emplace_back("setup"); },
        [&] { order.emplace_back("bind_samplers"); },
        [&] { order.emplace_back("bind_textures"); },
        [&] { order.emplace_back("draw"); },
        [&] { order.emplace_back("unbind_textures"); });

    pp::panopainter::execute_legacy_canvas_draw_merge_temporary_composite(execution);

    const std::vector<std::string> expected {
        "setup",
        "bind_samplers",
        "bind_textures",
        "draw",
        "unbind_textures",
    };
    PP_EXPECT(h, order == expected);
}

void legacy_canvas_draw_merge_temporary_paint_helper_preserves_order(pp::tests::Harness& h)
{
    std::vector<std::string> order;
    const auto execution = pp::panopainter::make_legacy_canvas_draw_merge_temporary_paint_composite(
        [&] { order.emplace_back("setup"); },
        [&] { order.emplace_back("bind_samplers"); },
        [&] { order.emplace_back("bind_textures"); },
        [&] { order.emplace_back("draw"); },
        [&] { order.emplace_back("unbind_textures"); });

    pp::panopainter::execute_legacy_canvas_draw_merge_temporary_composite(execution);

    const std::vector<std::string> expected {
        "setup",
        "bind_samplers",
        "bind_textures",
        "draw",
        "unbind_textures",
    };
    PP_EXPECT(h, order == expected);
}

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);
}

void plans_canvas_stroke_rasterization_boundary(pp::tests::Harness& h)
{
    const Extent2D extent { .width = 32, .height = 16 };
    const auto fetch = plan_canvas_stroke_rasterization(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        extent);
    PP_EXPECT(h, fetch);
    if (fetch) {
        PP_EXPECT(h, fetch.value().feedback.path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, !fetch.value().copy_stroke_destination);
        PP_EXPECT(h, fetch.value().can_route_feedback_through_renderer);
        PP_EXPECT(h, !fetch.value().compatibility_fallback);
    }

    const auto copy = plan_canvas_stroke_rasterization(
        RenderDeviceFeatures { .texture_copy = true },
        extent);
    PP_EXPECT(h, copy);
    if (copy) {
        PP_EXPECT(h, copy.value().feedback.path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, copy.value().copy_stroke_destination);
        PP_EXPECT(h, copy.value().can_route_feedback_through_renderer);
        PP_EXPECT(h, !copy.value().compatibility_fallback);
    }

    const auto fallback = plan_canvas_stroke_rasterization(
        RenderDeviceFeatures {},
        extent);
    PP_EXPECT(h, fallback);
    if (fallback) {
        PP_EXPECT(h, fallback.value().feedback.path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, fallback.value().copy_stroke_destination);
        PP_EXPECT(h, !fallback.value().can_route_feedback_through_renderer);
        PP_EXPECT(h, fallback.value().compatibility_fallback);
    }

    const auto invalid = plan_canvas_stroke_rasterization(
        RenderDeviceFeatures { .texture_copy = true },
        Extent2D { .width = 0, .height = 16 });
    PP_EXPECT(h, !invalid.ok());
    PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
}

void canvas_stroke_sample_bounds_empty_vertices_have_no_pixels(pp::tests::Harness& h)
{
    const auto plan = plan_canvas_stroke_sample_bounds(
        CanvasStrokeSampleBoundsRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .vertices = {},
        });

    PP_EXPECT(h, !plan.has_pixels);
    PP_EXPECT(h, plan.copy_region.width == 0);
    PP_EXPECT(h, plan.copy_region.height == 0);
}

void canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad(pp::tests::Harness& h)
{
    const CanvasStrokePoint vertices[] {
        { .x = 10.0F, .y = 20.0F },
        { .x = 10.0F, .y = 30.0F },
        { .x = 30.0F, .y = 30.0F },
        { .x = 30.0F, .y = 20.0F },
    };

    const auto plan = plan_canvas_stroke_sample_bounds(
        CanvasStrokeSampleBoundsRequest {
            .extent = Extent2D { .width = 64, .height = 64 },
            .vertices = vertices,
        });

    PP_EXPECT(h, plan.has_pixels);
    PP_EXPECT(h, plan.copy_region.x == 9);
    PP_EXPECT(h, plan.copy_region.y == 19);
    PP_EXPECT(h, plan.copy_region.width == 22);
    PP_EXPECT(h, plan.copy_region.height == 12);
    PP_EXPECT(h, almost_equal(plan.dirty_bounds.min_x, 9.0F));
    PP_EXPECT(h, almost_equal(plan.dirty_bounds.min_y, 19.0F));
    PP_EXPECT(h, almost_equal(plan.dirty_bounds.max_x, 31.0F));
    PP_EXPECT(h, almost_equal(plan.dirty_bounds.max_y, 31.0F));
}

void canvas_stroke_sample_bounds_clamp_out_of_range_vertices(pp::tests::Harness& h)
{
    const CanvasStrokePoint vertices[] {
        { .x = -10.0F, .y = -5.0F },
        { .x = 70.0F, .y = 80.0F },
    };

    const auto plan = plan_canvas_stroke_sample_bounds(
        CanvasStrokeSampleBoundsRequest {
            .extent = Extent2D { .width = 64, .height = 32 },
            .vertices = vertices,
        });

    PP_EXPECT(h, plan.has_pixels);
    PP_EXPECT(h, plan.copy_region.x == 0);
    PP_EXPECT(h, plan.copy_region.y == 0);
    PP_EXPECT(h, plan.copy_region.width == 64);
    PP_EXPECT(h, plan.copy_region.height == 32);
    PP_EXPECT(h, almost_equal(plan.dirty_bounds.max_x, 64.0F));
    PP_EXPECT(h, almost_equal(plan.dirty_bounds.max_y, 32.0F));
}

void canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad(pp::tests::Harness& h)
{
    const auto plan = plan_canvas_stroke_pad_region(
        CanvasStrokePadRegionRequest {
            .extent = Extent2D { .width = 100, .height = 80 },
            .pass_dirty_box = CanvasStrokeBox {
                .min_x = 5.0F,
                .min_y = 10.0F,
                .max_x = 20.0F,
                .max_y = 30.0F,
            },
        });

    PP_EXPECT(h, plan.has_pixels);
    PP_EXPECT(h, plan.copy_region.x == 0);
    PP_EXPECT(h, plan.copy_region.y == 0);
    PP_EXPECT(h, plan.copy_region.width == 40);
    PP_EXPECT(h, plan.copy_region.height == 50);
    PP_EXPECT(h, almost_equal(plan.ndc_quad[0].x, -1.0F));
    PP_EXPECT(h, almost_equal(plan.ndc_quad[0].y, -1.0F));
    PP_EXPECT(h, almost_equal(plan.ndc_quad[2].x, -0.2F));
    PP_EXPECT(h, almost_equal(plan.ndc_quad[2].y, 0.25F));
    PP_EXPECT(h, almost_equal(plan.ndc_quad[5].x, -0.2F));
    PP_EXPECT(h, almost_equal(plan.ndc_quad[5].y, -1.0F));
}

void canvas_stroke_face_dirty_update_includes_committed_dirty_box(pp::tests::Harness& h)
{
    const auto plan = plan_canvas_stroke_face_dirty_update(
        CanvasStrokeFaceDirtyUpdateRequest {
            .extent = Extent2D { .width = 64, .height = 32 },
            .previous_accumulated_dirty_box = CanvasStrokeBox {
                .min_x = 64.0F,
                .min_y = 32.0F,
                .max_x = 0.0F,
                .max_y = 0.0F,
            },
            .previous_pass_dirty_box = CanvasStrokeBox {
                .min_x = 64.0F,
                .min_y = 32.0F,
                .max_x = 0.0F,
                .max_y = 0.0F,
            },
            .sample_dirty_box = CanvasStrokeBox {
                .min_x = -5.0F,
                .min_y = -3.0F,
                .max_x = 80.0F,
                .max_y = 90.0F,
            },
            .include_in_committed_dirty_box = true,
        });

    PP_EXPECT(h, plan.has_dirty_pixels);
    PP_EXPECT(h, plan.committed_dirty);
    PP_EXPECT(h, plan.pass_dirty);
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.min_x, 0.0F));
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.min_y, 0.0F));
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.max_x, 64.0F));
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.max_y, 64.0F));
    PP_EXPECT(h, almost_equal(plan.pass_dirty_box.min_x, -5.0F));
    PP_EXPECT(h, almost_equal(plan.pass_dirty_box.max_y, 90.0F));
}

void canvas_stroke_face_dirty_update_can_skip_committed_dirty_box(pp::tests::Harness& h)
{
    const auto plan = plan_canvas_stroke_face_dirty_update(
        CanvasStrokeFaceDirtyUpdateRequest {
            .extent = Extent2D { .width = 64, .height = 32 },
            .previous_accumulated_dirty_box = CanvasStrokeBox {
                .min_x = 1.0F,
                .min_y = 2.0F,
                .max_x = 3.0F,
                .max_y = 4.0F,
            },
            .previous_pass_dirty_box = CanvasStrokeBox {
                .min_x = 10.0F,
                .min_y = 10.0F,
                .max_x = 20.0F,
                .max_y = 20.0F,
            },
            .sample_dirty_box = CanvasStrokeBox {
                .min_x = 0.0F,
                .min_y = 0.0F,
                .max_x = 30.0F,
                .max_y = 30.0F,
            },
            .include_in_committed_dirty_box = false,
        });

    PP_EXPECT(h, plan.has_dirty_pixels);
    PP_EXPECT(h, !plan.committed_dirty);
    PP_EXPECT(h, plan.pass_dirty);
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.min_x, 1.0F));
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.min_y, 2.0F));
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.max_x, 3.0F));
    PP_EXPECT(h, almost_equal(plan.accumulated_dirty_box.max_y, 4.0F));
    PP_EXPECT(h, almost_equal(plan.pass_dirty_box.min_x, 0.0F));
    PP_EXPECT(h, almost_equal(plan.pass_dirty_box.min_y, 0.0F));
    PP_EXPECT(h, almost_equal(plan.pass_dirty_box.max_x, 30.0F));
    PP_EXPECT(h, almost_equal(plan.pass_dirty_box.max_y, 30.0F));
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity);
    harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops);
    harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity);
    harness.run("composites_document_face_payloads_in_layer_order", composites_document_face_payloads_in_layer_order);
    harness.run("document_face_composite_skips_layers_without_requested_frame", document_face_composite_skips_layers_without_requested_frame);
    harness.run("document_face_composite_rejects_invalid_requests", document_face_composite_rejects_invalid_requests);
    harness.run("composites_document_frame_cube_faces", composites_document_frame_cube_faces);
    harness.run("document_frame_composite_rejects_invalid_requests", document_frame_composite_rejects_invalid_requests);
    harness.run("uploads_document_frame_faces_to_renderer_api", uploads_document_frame_faces_to_renderer_api);
    harness.run("records_document_frame_upload_report", records_document_frame_upload_report);
    harness.run("exports_document_frame_faces_as_pngs", exports_document_frame_faces_as_pngs);
    harness.run("prepares_document_frame_export_readiness_report", prepares_document_frame_export_readiness_report);
    harness.run("exports_document_frame_as_equirectangular_png", exports_document_frame_as_equirectangular_png);
    harness.run(
        "exports_document_frame_as_equirectangular_jpeg_with_xmp",
        exports_document_frame_as_equirectangular_jpeg_with_xmp);
    harness.run("exports_document_layers_as_equirectangular_pngs", exports_document_layers_as_equirectangular_pngs);
    harness.run(
        "exports_document_animation_frames_as_equirectangular_pngs",
        exports_document_animation_frames_as_equirectangular_pngs);
    harness.run("plans_document_depth_export_renderer_work", plans_document_depth_export_renderer_work);
    harness.run("exports_document_depth_as_png_payloads", exports_document_depth_as_png_payloads);
    harness.run(
        "depth export payload boundary rejects malformed face bytes",
        depth_export_payload_boundary_rejects_malformed_face_bytes);
    harness.run("document_frame_upload_rejects_invalid_requests", document_frame_upload_rejects_invalid_requests);
    harness.run("detects_feedback_requirements", detects_feedback_requirements);
    harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths);
    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(
        "legacy_canvas_stroke_main_pass_texture_dispatch_preserves_order",
        legacy_canvas_stroke_main_pass_texture_dispatch_preserves_order);
    harness.run(
        "legacy_canvas_stroke_live_pass_sampler_dispatch_preserves_order",
        legacy_canvas_stroke_live_pass_sampler_dispatch_preserves_order);
    harness.run(
        "legacy_canvas_stroke_dual_pass_brush_tip_dispatch_preserves_order",
        legacy_canvas_stroke_dual_pass_brush_tip_dispatch_preserves_order);
    harness.run(
        "legacy_canvas_stroke_dual_pass_request_preserves_order",
        legacy_canvas_stroke_dual_pass_request_preserves_order);
    harness.run(
        "legacy_canvas_stroke_pad_copy_region_preserves_coordinates",
        legacy_canvas_stroke_pad_copy_region_preserves_coordinates);
    harness.run(
        "legacy_canvas_stroke_pad_destination_dispatch_preserves_order",
        legacy_canvas_stroke_pad_destination_dispatch_preserves_order);
    harness.run("plans_canvas_stroke_commit_erase_sequence", plans_canvas_stroke_commit_erase_sequence);
    harness.run("plans_canvas_stroke_commit_composite_sequence", plans_canvas_stroke_commit_composite_sequence);
    harness.run(
        "retained_stroke_commit_runner_clamps_malformed_step_count",
        retained_stroke_commit_runner_clamps_malformed_step_count);
    harness.run(
        "retained_stroke_commit_input_binder_uses_sequence_slots",
        retained_stroke_commit_input_binder_uses_sequence_slots);
    harness.run(
        "retained_stroke_commit_dilate_copy_uses_layer_scratch_slot",
        retained_stroke_commit_dilate_copy_uses_layer_scratch_slot);
    harness.run(
        "retained_stroke_commit_runner_preserves_per_face_step_order",
        retained_stroke_commit_runner_preserves_per_face_step_order);
    harness.run(
        "retained_stroke_commit_callback_builder_preserves_order",
        retained_stroke_commit_callback_builder_preserves_order);
    harness.run(
        "retained_stroke_commit_copy_skips_missing_layer_scratch_or_invalid_extent",
        retained_stroke_commit_copy_skips_missing_layer_scratch_or_invalid_extent);
    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_stroke_preview_composite_with_all_retained_inputs",
        plans_stroke_preview_composite_with_all_retained_inputs);
    harness.run(
        "legacy_node_stroke_preview_feedback_adapter_clamps_invalid_extent",
        legacy_node_stroke_preview_feedback_adapter_clamps_invalid_extent);
    harness.run(
        "legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs",
        legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs);
    harness.run(
        "legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms",
        legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms);
    harness.run(
        "legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order",
        legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order);
    harness.run(
        "legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order",
        legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order);
    harness.run(
        "legacy_node_stroke_preview_main_live_pass_preserves_order",
        legacy_node_stroke_preview_main_live_pass_preserves_order);
    harness.run(
        "legacy_node_stroke_preview_main_pass_texture_dispatch_preserves_order",
        legacy_node_stroke_preview_main_pass_texture_dispatch_preserves_order);
    harness.run(
        "legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order",
        legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order);
    harness.run(
        "legacy_node_stroke_preview_stroke_setup_plan_preserves_curve_and_dual_inputs",
        legacy_node_stroke_preview_stroke_setup_plan_preserves_curve_and_dual_inputs);
    harness.run(
        "legacy_node_stroke_preview_pass_orchestration_plan_preserves_feedback_material_and_composite_inputs",
        legacy_node_stroke_preview_pass_orchestration_plan_preserves_feedback_material_and_composite_inputs);
    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_combines_layer_and_stroke_complexity", canvas_blend_gate_combines_layer_and_stroke_complexity);
    harness.run(
        "legacy_canvas_stroke_mix_executor_preserves_plane_callback_order",
        legacy_canvas_stroke_mix_executor_preserves_plane_callback_order);
    harness.run(
        "legacy_canvas_draw_merge_temporary_erase_helper_preserves_order",
        legacy_canvas_draw_merge_temporary_erase_helper_preserves_order);
    harness.run(
        "legacy_canvas_draw_merge_temporary_paint_helper_preserves_order",
        legacy_canvas_draw_merge_temporary_paint_helper_preserves_order);
    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);
    harness.run("plans_canvas_stroke_rasterization_boundary", plans_canvas_stroke_rasterization_boundary);
    harness.run(
        "canvas_stroke_sample_bounds_empty_vertices_have_no_pixels",
        canvas_stroke_sample_bounds_empty_vertices_have_no_pixels);
    harness.run(
        "canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad",
        canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad);
    harness.run(
        "canvas_stroke_sample_bounds_clamp_out_of_range_vertices",
        canvas_stroke_sample_bounds_clamp_out_of_range_vertices);
    harness.run(
        "canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad",
        canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad);
    harness.run(
        "canvas_stroke_face_dirty_update_includes_committed_dirty_box",
        canvas_stroke_face_dirty_update_includes_committed_dirty_box);
    harness.run(
        "canvas_stroke_face_dirty_update_can_skip_committed_dirty_box",
        canvas_stroke_face_dirty_update_can_skip_committed_dirty_box);
    return harness.finish();
}
