mirror of
https://github.com/cccb/servicepoint.git
synced 2025-01-18 10:00:14 +01:00
add bevy plugin
This commit is contained in:
parent
3901efcf61
commit
a098e35db9
2935
Cargo.lock
generated
2935
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,8 @@ members = [
|
||||||
"crates/servicepoint",
|
"crates/servicepoint",
|
||||||
"crates/servicepoint_binding_c",
|
"crates/servicepoint_binding_c",
|
||||||
"crates/servicepoint_binding_cs",
|
"crates/servicepoint_binding_cs",
|
||||||
"crates/servicepoint_binding_c/examples/lang_c"
|
"crates/servicepoint_binding_c/examples/lang_c",
|
||||||
|
"crates/servicepoint_bevy"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An origin marks the top left position of a window sent to the display.
|
/// An origin marks the top left position of a window sent to the display.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
pub struct Origin(pub usize, pub usize);
|
pub struct Origin(pub usize, pub usize);
|
||||||
|
|
||||||
impl std::ops::Add<Origin> for Origin {
|
impl std::ops::Add<Origin> for Origin {
|
||||||
|
|
15
crates/servicepoint_bevy/Cargo.toml
Normal file
15
crates/servicepoint_bevy/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "servicepoint_bevy"
|
||||||
|
edition = "2021"
|
||||||
|
license = "Apache-2.0 OR MIT"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
servicepoint = { version = "0.5.0", path = "../servicepoint" }
|
||||||
|
|
||||||
|
bevy = { version = "0.13.2", default-features = false, features = ["bevy_core_pipeline", "bevy_asset", "bevy_render", "bevy_sprite"] }
|
||||||
|
image = { version = "0.24", default-features = false }
|
||||||
|
futures = { version = "0.3" }
|
||||||
|
futures-lite = { version = "2.1" }
|
||||||
|
wgpu = { version = "0.19.1" }
|
||||||
|
bytemuck = { version = "1.13" }
|
8
crates/servicepoint_bevy/README.md
Normal file
8
crates/servicepoint_bevy/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# servicepoint_bevy
|
||||||
|
|
||||||
|
Provides bevy helpers for outputting to the servicepoint display.
|
||||||
|
|
||||||
|
## LICENSE
|
||||||
|
|
||||||
|
The bevy plugin is based on [bevy_image_export](https://github.com/paulkre/bevy_image_export/).
|
||||||
|
The crate inherits the MIT/Apache-2.0 dual license.
|
365
crates/servicepoint_bevy/src/lib.rs
Normal file
365
crates/servicepoint_bevy/src/lib.rs
Normal file
|
@ -0,0 +1,365 @@
|
||||||
|
// Source: https://github.com/paulkre/bevy_image_export/
|
||||||
|
// License: MIT / Apache-2.0
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
ecs::{
|
||||||
|
query::QueryItem,
|
||||||
|
system::{lifetimeless::SRes, SystemParamItem},
|
||||||
|
},
|
||||||
|
prelude::*,
|
||||||
|
render::{
|
||||||
|
camera::{CameraUpdateSystem, RenderTarget, ScalingMode},
|
||||||
|
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||||
|
graph::CameraDriverLabel,
|
||||||
|
render_asset::{
|
||||||
|
PrepareAssetError, RenderAsset, RenderAssetPlugin,
|
||||||
|
RenderAssetUsages, RenderAssets,
|
||||||
|
},
|
||||||
|
render_graph::{
|
||||||
|
Node, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel,
|
||||||
|
},
|
||||||
|
render_resource::{
|
||||||
|
Buffer, BufferDescriptor, BufferUsages, Extent3d, ImageCopyBuffer,
|
||||||
|
ImageDataLayout, MapMode, TextureDescriptor, TextureDimension,
|
||||||
|
TextureFormat, TextureUsages,
|
||||||
|
},
|
||||||
|
renderer::{RenderContext, RenderDevice},
|
||||||
|
Render, RenderApp, RenderSet,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use image::{ImageBuffer, Rgba};
|
||||||
|
use wgpu::Maintain;
|
||||||
|
|
||||||
|
use servicepoint::{
|
||||||
|
BitVec, Command::BitmapLinearWin, CompressionCode::Lzma, Connection,
|
||||||
|
DataRef, Origin, PixelGrid,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Asset, Reflect, Clone, Default)]
|
||||||
|
pub struct ServicePointExportSource {
|
||||||
|
pub image: Handle<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Settings for handling an
|
||||||
|
#[derive(Component, Clone)]
|
||||||
|
pub struct ServicePointExportSettings {
|
||||||
|
pub origin: Origin,
|
||||||
|
pub window_width: usize,
|
||||||
|
pub window_height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GpuImageExportSource {
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub source_handle: Handle<Image>,
|
||||||
|
pub source_size: Extent3d,
|
||||||
|
pub bytes_per_row: u32,
|
||||||
|
pub padded_bytes_per_row: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct ImageExportBundle {
|
||||||
|
pub source: Handle<ServicePointExportSource>,
|
||||||
|
pub settings: ServicePointExportSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin enabling the generation of image sequences.
|
||||||
|
pub struct ServicePointPlugin {
|
||||||
|
pub bind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct ServicePointPluginConnection {
|
||||||
|
pub connection: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||||
|
pub enum ImageExportSystems {
|
||||||
|
SetupImageExport,
|
||||||
|
SetupImageExportFlush,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAsset for ServicePointExportSource {
|
||||||
|
type PreparedAsset = GpuImageExportSource;
|
||||||
|
type Param = (SRes<RenderDevice>, SRes<RenderAssets<Image>>);
|
||||||
|
|
||||||
|
fn asset_usage(&self) -> RenderAssetUsages {
|
||||||
|
RenderAssetUsages::RENDER_WORLD
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_asset(
|
||||||
|
self,
|
||||||
|
(device, images): &mut SystemParamItem<Self::Param>,
|
||||||
|
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> {
|
||||||
|
let gpu_image = images.get(&self.image).unwrap();
|
||||||
|
|
||||||
|
let size = gpu_image.texture.size();
|
||||||
|
let format = &gpu_image.texture_format;
|
||||||
|
let bytes_per_row = (size.width / format.block_dimensions().0)
|
||||||
|
* format.block_copy_size(None).unwrap();
|
||||||
|
let padded_bytes_per_row =
|
||||||
|
RenderDevice::align_copy_bytes_per_row(bytes_per_row as usize)
|
||||||
|
as u32;
|
||||||
|
|
||||||
|
let source_size = gpu_image.texture.size();
|
||||||
|
|
||||||
|
Ok(GpuImageExportSource {
|
||||||
|
buffer: device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("Image Export Buffer"),
|
||||||
|
size: (source_size.height * padded_bytes_per_row) as u64,
|
||||||
|
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}),
|
||||||
|
source_handle: self.image,
|
||||||
|
source_size,
|
||||||
|
bytes_per_row,
|
||||||
|
padded_bytes_per_row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractComponent for ServicePointExportSettings {
|
||||||
|
type QueryData = (&'static Self, &'static Handle<ServicePointExportSource>);
|
||||||
|
type QueryFilter = ();
|
||||||
|
type Out = (Self, Handle<ServicePointExportSource>);
|
||||||
|
|
||||||
|
fn extract_component(
|
||||||
|
(settings, source_handle): QueryItem<'_, Self::QueryData>,
|
||||||
|
) -> Option<Self::Out> {
|
||||||
|
Some((settings.clone(), source_handle.clone_weak()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image_bytes_from_gpu(
|
||||||
|
gpu_source: &GpuImageExportSource,
|
||||||
|
render_device: &Res<RenderDevice>,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let source_size = gpu_source.source_size;
|
||||||
|
|
||||||
|
let slice = gpu_source.buffer.slice(..);
|
||||||
|
|
||||||
|
let (mapping_tx, mapping_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
render_device.map_buffer(&slice, MapMode::Read, move |res| {
|
||||||
|
mapping_tx.send(res).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
render_device.poll(Maintain::Wait);
|
||||||
|
futures_lite::future::block_on(mapping_rx).unwrap().unwrap();
|
||||||
|
|
||||||
|
let vec = slice.get_mapped_range().to_vec();
|
||||||
|
gpu_source.buffer.unmap();
|
||||||
|
|
||||||
|
let bytes_per_row = gpu_source.bytes_per_row as usize;
|
||||||
|
let padded_bytes_per_row = gpu_source.padded_bytes_per_row as usize;
|
||||||
|
if bytes_per_row == padded_bytes_per_row {
|
||||||
|
vec
|
||||||
|
} else {
|
||||||
|
let mut unpadded_bytes = Vec::<u8>::with_capacity(
|
||||||
|
source_size.height as usize * bytes_per_row,
|
||||||
|
);
|
||||||
|
|
||||||
|
for padded_row in vec.chunks(padded_bytes_per_row) {
|
||||||
|
unpadded_bytes.extend_from_slice(&padded_row[..bytes_per_row]);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpadded_bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_buffer_to_connection(
|
||||||
|
export_bundles: Query<(
|
||||||
|
&Handle<ServicePointExportSource>,
|
||||||
|
&ServicePointExportSettings,
|
||||||
|
)>,
|
||||||
|
sources: Res<RenderAssets<ServicePointExportSource>>,
|
||||||
|
render_device: Res<RenderDevice>,
|
||||||
|
connection: Res<ServicePointPluginConnection>,
|
||||||
|
) {
|
||||||
|
for (source_handle, settings) in &export_bundles {
|
||||||
|
let gpu_source = match sources.get(source_handle) {
|
||||||
|
None => continue,
|
||||||
|
Some(source) => source,
|
||||||
|
};
|
||||||
|
|
||||||
|
let image_bytes = get_image_bytes_from_gpu(gpu_source, &render_device);
|
||||||
|
|
||||||
|
let buffer = match ImageBuffer::<Rgba<u8>, _>::from_raw(
|
||||||
|
settings.window_width as u32,
|
||||||
|
settings.window_height as u32,
|
||||||
|
image_bytes,
|
||||||
|
) {
|
||||||
|
None => {
|
||||||
|
println!("Failed creating image buffer");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Some(buffer) => buffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut bit_vec =
|
||||||
|
BitVec::new(settings.window_height * settings.window_width);
|
||||||
|
|
||||||
|
for (index, pixel) in buffer.chunks_exact(4).enumerate() {
|
||||||
|
assert_eq!(pixel.len(), 4);
|
||||||
|
if pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0 {
|
||||||
|
bit_vec.set(index, pixel[3] != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pixels = PixelGrid::load(
|
||||||
|
settings.window_width,
|
||||||
|
settings.window_height,
|
||||||
|
bit_vec.data_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
connection
|
||||||
|
.connection
|
||||||
|
.send(BitmapLinearWin(settings.origin, pixels, Lzma))
|
||||||
|
.expect("send failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for ServicePointPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.configure_sets(
|
||||||
|
PostUpdate,
|
||||||
|
(
|
||||||
|
ImageExportSystems::SetupImageExport,
|
||||||
|
ImageExportSystems::SetupImageExportFlush,
|
||||||
|
)
|
||||||
|
.chain()
|
||||||
|
.before(CameraUpdateSystem),
|
||||||
|
)
|
||||||
|
.register_type::<ServicePointExportSource>()
|
||||||
|
.init_asset::<ServicePointExportSource>()
|
||||||
|
.register_asset_reflect::<ServicePointExportSource>()
|
||||||
|
.add_plugins((
|
||||||
|
RenderAssetPlugin::<ServicePointExportSource>::default(),
|
||||||
|
ExtractComponentPlugin::<ServicePointExportSettings>::default(),
|
||||||
|
))
|
||||||
|
.add_systems(
|
||||||
|
PostUpdate,
|
||||||
|
(
|
||||||
|
apply_deferred
|
||||||
|
.in_set(ImageExportSystems::SetupImageExportFlush),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let render_app = app.sub_app_mut(RenderApp);
|
||||||
|
|
||||||
|
let connection =
|
||||||
|
Connection::open(&self.bind).expect("could not connect to display");
|
||||||
|
|
||||||
|
render_app
|
||||||
|
.insert_resource(ServicePointPluginConnection { connection })
|
||||||
|
.add_systems(
|
||||||
|
Render,
|
||||||
|
send_buffer_to_connection
|
||||||
|
.after(RenderSet::Render)
|
||||||
|
.before(RenderSet::Cleanup),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut graph =
|
||||||
|
render_app.world.get_resource_mut::<RenderGraph>().unwrap();
|
||||||
|
|
||||||
|
graph.add_node(ImageExportLabel, ImageExportNode);
|
||||||
|
graph.add_node_edge(CameraDriverLabel, ImageExportLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_export_bundle(
|
||||||
|
settings: ServicePointExportSettings,
|
||||||
|
scaling_mode: ScalingMode,
|
||||||
|
main_camera: &mut bevy::ecs::system::EntityCommands,
|
||||||
|
images: &mut ResMut<Assets<Image>>,
|
||||||
|
export_sources: &mut ResMut<Assets<ServicePointExportSource>>,
|
||||||
|
) -> ImageExportBundle {
|
||||||
|
// Create an output texture.
|
||||||
|
let size = Extent3d {
|
||||||
|
width: settings.window_width as u32,
|
||||||
|
height: settings.window_height as u32,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
let mut export_texture = Image {
|
||||||
|
texture_descriptor: TextureDescriptor {
|
||||||
|
label: None,
|
||||||
|
size,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format: TextureFormat::Rgba8UnormSrgb,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
usage: TextureUsages::COPY_DST
|
||||||
|
| TextureUsages::COPY_SRC
|
||||||
|
| TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
export_texture.resize(size);
|
||||||
|
|
||||||
|
let export_texture = images.add(export_texture);
|
||||||
|
|
||||||
|
let mut export_camera = Camera2dBundle {
|
||||||
|
camera: Camera {
|
||||||
|
target: RenderTarget::Image(export_texture.clone()),
|
||||||
|
clear_color: ClearColorConfig::Custom(Color::BLACK),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
|
export_camera.projection.scaling_mode = scaling_mode;
|
||||||
|
|
||||||
|
// render main camera to output texture as well
|
||||||
|
main_camera.with_children(|parent| {
|
||||||
|
parent.spawn(export_camera);
|
||||||
|
});
|
||||||
|
|
||||||
|
let export_source = ServicePointExportSource {
|
||||||
|
image: export_texture,
|
||||||
|
};
|
||||||
|
|
||||||
|
ImageExportBundle {
|
||||||
|
source: export_sources.add(export_source),
|
||||||
|
settings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||||
|
pub struct ImageExportLabel;
|
||||||
|
|
||||||
|
pub struct ImageExportNode;
|
||||||
|
|
||||||
|
impl Node for ImageExportNode {
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
_graph: &mut RenderGraphContext,
|
||||||
|
render_context: &mut RenderContext,
|
||||||
|
world: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
for (_, source) in world
|
||||||
|
.resource::<RenderAssets<ServicePointExportSource>>()
|
||||||
|
.iter()
|
||||||
|
{
|
||||||
|
if let Some(gpu_image) = world
|
||||||
|
.resource::<RenderAssets<Image>>()
|
||||||
|
.get(&source.source_handle)
|
||||||
|
{
|
||||||
|
render_context.command_encoder().copy_texture_to_buffer(
|
||||||
|
gpu_image.texture.as_image_copy(),
|
||||||
|
ImageCopyBuffer {
|
||||||
|
buffer: &source.buffer,
|
||||||
|
layout: ImageDataLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(source.padded_bytes_per_row),
|
||||||
|
rows_per_image: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
source.source_size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
27
shell.nix
27
shell.nix
|
@ -1,11 +1,26 @@
|
||||||
{pkgs ? import <nixpkgs> {}}:
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
pkgs.mkShell {
|
|
||||||
nativeBuildInputs = with pkgs.buildPackages; [
|
with pkgs;
|
||||||
|
|
||||||
|
mkShell rec {
|
||||||
|
nativeBuildInputs = [
|
||||||
rustup
|
rustup
|
||||||
pkg-config
|
pkg-config
|
||||||
xe
|
|
||||||
lzma
|
|
||||||
cargo-tarpaulin
|
|
||||||
gnumake
|
gnumake
|
||||||
|
|
||||||
|
# find missing test cases
|
||||||
|
cargo-tarpaulin
|
||||||
];
|
];
|
||||||
|
buildInputs = [
|
||||||
|
# servicepoint2 compression
|
||||||
|
lzma xe
|
||||||
|
|
||||||
|
# for bevy
|
||||||
|
udev alsa-lib vulkan-loader
|
||||||
|
# bevy x11
|
||||||
|
xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr
|
||||||
|
# bevy wayland
|
||||||
|
libxkbcommon wayland
|
||||||
|
];
|
||||||
|
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue