Files
solidcraft-upstream/crates/engine/src/capture.rs
T

115 lines
4.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! How headless pictures of the model are drawn: a renderer the host installs. The engine
//! describes what a picture shows ([`crate::view::scene`]); drawing it is a GPU job the engine
//! cannot do itself (the GPU crate sits above it), so the application, the CLI and the MCP
//! server install one ([`install`]) that draws with the viewport's own pipelines (or, as a last
//! resort the host chooses, the CPU rasterizer). Every picture names the renderer that drew it.
//! Without a renderer, a picture is an error and a save simply has no thumbnail.
use std::sync::{Arc, RwLock};
use solidcraft_render::{Camera, Scene};
/// A picture: straight RGBA, rows top to bottom, and the renderer that drew it (`gpu`,
/// `gpu-software` or `cpu`).
pub struct Picture {
pub rgba: Vec<u8>,
pub renderer: &'static str,
}
/// A picture as PNG bytes, and the renderer that drew it.
pub struct Png {
pub bytes: Vec<u8>,
pub renderer: &'static str,
}
/// What a reply says when the CPU rasterizer drew the picture.
pub const CPU_WARNING: &str = "drawn by the CPU rasterizer (no GPU adapter qualified, or SOLIDCRAFT_GPU=cpu): surface analysis colours, highlights and legends are not drawn; `solidcraft-cli gpu` lists the adapters";
/// The warning that goes with a picture drawn by `renderer`, if any.
pub fn warning(renderer: &str) -> Option<&'static str> {
(renderer == "cpu").then_some(CPU_WARNING)
}
/// Draws `scene` seen by `cam` at `width` × `height` pixels (`width * height * 4` bytes of RGBA),
/// or says why it could not.
pub type Renderer = dyn Fn(&Scene, &Camera, u32, u32) -> Result<Picture, String> + Send + Sync;
static RENDERER: RwLock<Option<Arc<Renderer>>> = RwLock::new(None);
/// Make `renderer` draw this process's headless pictures (replacing any installed before).
pub fn install(renderer: Arc<Renderer>) {
if let Ok(mut r) = RENDERER.write() {
*r = Some(renderer);
}
}
/// The installed renderer, if any.
pub fn installed() -> Option<Arc<Renderer>> {
RENDERER.read().ok().and_then(|r| r.clone())
}
/// `scene` drawn by the installed renderer.
pub fn rgba(scene: &Scene, cam: &Camera, width: u32, height: u32) -> Result<Picture, String> {
rgba_with(installed().as_deref(), scene, cam, width, height)
}
/// `scene` drawn by the installed renderer, as PNG.
pub fn png(scene: &Scene, cam: &Camera, width: u32, height: u32) -> Result<Png, String> {
png_with(installed().as_deref(), scene, cam, width, height)
}
/// [`rgba`] with `renderer` instead of the installed one.
pub fn rgba_with(renderer: Option<&Renderer>, scene: &Scene, cam: &Camera, width: u32, height: u32) -> Result<Picture, String> {
let renderer = renderer.ok_or("no renderer is installed to draw pictures in this process")?;
let picture = renderer(scene, cam, width, height)?;
if picture.rgba.len() as u64 != u64::from(width) * u64::from(height) * 4 {
return Err(format!("the renderer returned {} bytes for {width} × {height} pixels", picture.rgba.len()));
}
if let Some(w) = warning(picture.renderer) {
log::warn!("{w}");
}
Ok(picture)
}
/// [`png`] with `renderer` instead of the installed one.
pub fn png_with(renderer: Option<&Renderer>, scene: &Scene, cam: &Camera, width: u32, height: u32) -> Result<Png, String> {
let Picture { rgba, renderer } = rgba_with(renderer, scene, cam, width, height)?;
let bytes = solidcraft_render::png(width as usize, height as usize, rgba).ok_or_else(|| "PNG encoding failed".to_string())?;
Ok(Png { bytes, renderer })
}
/// For tests: a stand-in for the renderer the hosts install, a flat picture whose first byte
/// counts the scene's meshes.
#[cfg(test)]
pub(crate) fn install_test_renderer() {
install(Arc::new(|scene: &Scene, _: &Camera, w: u32, h: u32| {
let mut rgba = vec![200; (w * h * 4) as usize];
if let Some(first) = rgba.first_mut() {
*first = scene.meshes.len() as u8;
}
Ok(Picture { rgba, renderer: "gpu" })
}));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn without_a_renderer_a_picture_is_an_error_not_a_crash() {
let e = png_with(None, &Scene::default(), &Camera::default(), 16, 16).err().unwrap();
assert!(e.contains("no renderer"), "{e}");
}
#[test]
fn a_renderer_that_returns_the_wrong_size_is_an_error() {
let short: &Renderer = &|_, _, _, _| Ok(Picture { rgba: vec![0; 3], renderer: "gpu" });
assert!(rgba_with(Some(short), &Scene::default(), &Camera::default(), 2, 2).is_err());
let plain: &Renderer = &|_, _, w, h| Ok(Picture { rgba: vec![255; (w * h * 4) as usize], renderer: "cpu" });
let png = png_with(Some(plain), &Scene::default(), &Camera::default(), 2, 2).unwrap();
assert!(png.bytes.starts_with(b"\x89PNG"));
assert_eq!(png.renderer, "cpu");
assert_eq!((warning("cpu"), warning("gpu"), warning("gpu-software")), (Some(CPU_WARNING), None, None));
}
}