Headless pictures draw on the GPU, with the viewport's pipelines (CPU rasterizer as last tier) (#1555)
Thanks @jyviko.
This commit is contained in:
1 parent
48322f6464
commit
1a0b33fcf3
54 files changed
+3525
-1998
No files matched your search
@@ -0,0 +1,114 @@
|
||||
//! 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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user