bevy_ratatui_camera 0.16.0

A bevy plugin for rendering your bevy app to the terminal using ratatui.
Documentation
use bevy::{
    core_pipeline::{
        core_2d::graph::{Core2d, Node2d},
        core_3d::graph::{Core3d, Node3d},
    },
    ecs::query::QueryItem,
    prelude::*,
    render::{
        RenderApp,
        render_asset::RenderAssets,
        render_graph::{
            NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
        },
        render_resource::{
            Buffer, CommandEncoderDescriptor, Extent3d, TexelCopyBufferInfo, TexelCopyBufferLayout,
            Texture,
        },
        renderer::{RenderContext, RenderDevice, RenderQueue},
        texture::GpuImage,
        view::ViewDepthTexture,
    },
};

use crate::{
    camera_image_pipe::calculate_buffer_size,
    camera_readback::{RatatuiCameraSender, RatatuiDepthSender, RatatuiSobelSender},
};

pub struct RatatuiCameraNodePlugin;

impl Plugin for RatatuiCameraNodePlugin {
    fn build(&self, app: &mut App) {
        let render_app = app.sub_app_mut(RenderApp);

        render_app
            .add_render_graph_node::<ViewNodeRunner<RatatuiCameraNode>>(Core3d, RatatuiCameraLabel);
        render_app.add_render_graph_edge(Core3d, Node3d::Upscaling, RatatuiCameraLabel);

        render_app
            .add_render_graph_node::<ViewNodeRunner<RatatuiCameraNode>>(Core2d, RatatuiCameraLabel);
        render_app.add_render_graph_edge(Core2d, Node2d::Upscaling, RatatuiCameraLabel);
    }
}

#[derive(Default)]
pub struct RatatuiCameraNode;

#[derive(RenderLabel, Clone, Debug, Eq, Hash, PartialEq)]
pub struct RatatuiCameraLabel;

impl ViewNode for RatatuiCameraNode {
    type ViewQuery = (
        &'static ViewDepthTexture,
        &'static RatatuiCameraSender,
        Option<&'static RatatuiDepthSender>,
        Option<&'static RatatuiSobelSender>,
    );

    fn run<'w>(
        &self,
        _graph: &mut RenderGraphContext,
        render_context: &mut RenderContext<'w>,
        (depth_texture, camera_sender, depth_sender, sobel_sender): QueryItem<
            'w,
            '_,
            Self::ViewQuery,
        >,
        world: &'w World,
    ) -> Result<(), NodeRunError> {
        let gpu_images = world.get_resource::<RenderAssets<GpuImage>>().unwrap();

        let src_image = gpu_images.get(&camera_sender.sender_image).unwrap();
        copy_texture_to_buffer(
            render_context,
            world,
            &src_image.texture,
            &camera_sender.buffer,
        );

        if let Some(depth_sender) = depth_sender {
            let expected_buffer_size = calculate_buffer_size(
                depth_texture.texture.width(),
                depth_texture.texture.height(),
            );
            if expected_buffer_size == depth_sender.buffer.size() {
                copy_texture_to_buffer(
                    render_context,
                    world,
                    &depth_texture.texture,
                    &depth_sender.buffer,
                );
            }
        }

        if let Some(sobel_sender) = sobel_sender {
            let src_image_sobel = gpu_images.get(&sobel_sender.sender_image).unwrap();
            copy_texture_to_buffer(
                render_context,
                world,
                &src_image_sobel.texture,
                &sobel_sender.buffer,
            );
        }

        Ok(())
    }
}

fn copy_texture_to_buffer(
    render_context: &mut RenderContext,
    world: &World,
    src_texture: &Texture,
    buffer: &Buffer,
) {
    let mut encoder = render_context
        .render_device()
        .create_command_encoder(&CommandEncoderDescriptor::default());

    let block_dimensions = src_texture.format().block_dimensions();
    let block_size = src_texture.format().block_copy_size(None).unwrap();

    let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row(
        (src_texture.width() as usize / block_dimensions.0 as usize) * block_size as usize,
    );

    let texture_extent = Extent3d {
        width: src_texture.width(),
        height: src_texture.height(),
        depth_or_array_layers: 1,
    };

    encoder.copy_texture_to_buffer(
        src_texture.as_image_copy(),
        TexelCopyBufferInfo {
            buffer,
            layout: TexelCopyBufferLayout {
                offset: 0,
                rows_per_image: None,
                bytes_per_row: Some(
                    std::num::NonZeroU32::new(padded_bytes_per_row as u32)
                        .unwrap()
                        .into(),
                ),
            },
        },
        texture_extent,
    );

    let render_queue = world.get_resource::<RenderQueue>().unwrap();
    render_queue.submit(std::iter::once(encoder.finish()));
}