forked from 1b-agent/fieldcraft-upstream
gpu: captures draw name labels; a shared capturer and a PNG with the CPU as last tier
render::draw_labels draws a scene's labels over finished pixels in the CPU rasterizer's font. capture::shared opens the process's device once by the FIELDCRAFT_GPU policy; capture::png draws on it or on the CPU and reports which (Renderer). Refs #422 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
fee978c7e4
commit
c9163e98ad
5 files changed
+134
-20
No files matched your search
@@ -3,13 +3,14 @@
|
||||
//! and laid over the viewport's background gradient. Every GPU call sits inside error scopes,
|
||||
//! so a device error (out of memory, validation) returns as an error, never a panic.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use fieldcraft_render::{Camera, Rgb, Scene};
|
||||
|
||||
use crate::select::{self, Policy, Tier};
|
||||
use crate::select::{self, Policy};
|
||||
use crate::view::{Frame, GpuScene, GpuTarget, Layer, Pipelines};
|
||||
use crate::{GpuError, Result};
|
||||
use crate::{Renderer, Tier};
|
||||
|
||||
/// Largest picture side and pixel count drawn (the CPU rasterizer's limits).
|
||||
pub const MAX_SIDE: u32 = fieldcraft_render::MAX_DIM as u32;
|
||||
@@ -96,8 +97,10 @@ impl Capturer {
|
||||
if let Some(e) = caught {
|
||||
return Err(GpuError::Failed(e.to_string()));
|
||||
}
|
||||
let pixels = drawn?;
|
||||
Ok(over_background(&pixels, w as usize, h as usize, scene.background.unwrap_or(BACKGROUND)))
|
||||
let pixels = over_background(&drawn?, w as usize, h as usize, scene.background.unwrap_or(BACKGROUND));
|
||||
// Labels in the CPU rasterizer's font (the window draws them with the interface's).
|
||||
let layers: Vec<&Scene> = std::iter::once(scene).chain(layers.iter().copied()).collect();
|
||||
fieldcraft_render::draw_labels(&pixels, w as usize, h as usize, &layers, cam, scene.radius).map_err(|e| GpuError::Failed(e.to_string()))
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene, layers: &[&Scene], cam: &Camera, w: u32, h: u32) -> Result<Vec<u8>> {
|
||||
@@ -184,6 +187,46 @@ impl Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
/// The process's capturer, opened on first use by the `FIELDCRAFT_GPU` policy: `Ok(None)` for
|
||||
/// the CPU rasterizer. Its error is kept, so a missing device is not looked for again.
|
||||
pub fn shared() -> &'static Result<Option<Capturer>> {
|
||||
static SHARED: OnceLock<Result<Option<Capturer>>> = OnceLock::new();
|
||||
SHARED.get_or_init(|| Policy::from_env().and_then(Capturer::open))
|
||||
}
|
||||
|
||||
/// A PNG of `scene` with `layers` seen by `cam`, `w` × `h` pixels, drawn by `capturer` (a
|
||||
/// [`shared`] result): on its GPU, or by the CPU rasterizer when it is `Ok(None)`. Under the
|
||||
/// `auto` policy a GPU failure falls back to the CPU and the reason is reported; under a strict
|
||||
/// one it is the error.
|
||||
pub fn png(
|
||||
capturer: &Result<Option<Capturer>>,
|
||||
policy: Policy,
|
||||
scene: &Scene,
|
||||
layers: &[&Scene],
|
||||
cam: &Camera,
|
||||
w: u32,
|
||||
h: u32,
|
||||
) -> Result<(Vec<u8>, Renderer)> {
|
||||
let gpu = match capturer {
|
||||
Ok(Some(c)) => c.rgba(scene, layers, cam, w, h).map(|px| Some((px, c))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(e.clone()),
|
||||
};
|
||||
let why = match gpu {
|
||||
Ok(Some((px, c))) => {
|
||||
let png =
|
||||
fieldcraft_render::png(w as usize, h as usize, &px).ok_or_else(|| GpuError::Failed("the picture could not be encoded".into()))?;
|
||||
return Ok((png, Renderer::Gpu { adapter: c.adapter.clone(), tier: c.tier, samples: c.samples() }));
|
||||
}
|
||||
Ok(None) => (policy == Policy::Auto).then(|| "no GPU adapter".to_string()),
|
||||
Err(e @ GpuError::Size(_)) => return Err(e),
|
||||
Err(e) if policy == Policy::Auto => Some(e.to_string()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let png = fieldcraft_render::render_layers_png(scene, layers, cam, w as usize, h as usize).map_err(|e| GpuError::Size(e.to_string()))?;
|
||||
Ok((png, Renderer::Cpu { why }))
|
||||
}
|
||||
|
||||
/// Bytes per row of a `w` pixel RGBA copy, padded to wgpu's 256-byte alignment.
|
||||
pub fn padded_row(w: u32) -> u32 {
|
||||
let a = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
|
||||
@@ -29,3 +29,44 @@ pub enum GpuError {
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, GpuError>;
|
||||
|
||||
/// The kind of device a picture was drawn on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Tier {
|
||||
Hardware,
|
||||
Software,
|
||||
}
|
||||
|
||||
impl Tier {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Tier::Hardware => "hardware GPU",
|
||||
Tier::Software => "software GPU",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What drew a picture.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Renderer {
|
||||
Gpu {
|
||||
adapter: String,
|
||||
tier: Tier,
|
||||
samples: u32,
|
||||
},
|
||||
/// The CPU rasterizer, and why when a GPU was wanted (`auto` with no usable GPU).
|
||||
Cpu {
|
||||
why: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// `"hardware"`, `"software"` or `"cpu"`.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Renderer::Gpu { tier: Tier::Hardware, .. } => "hardware",
|
||||
Renderer::Gpu { tier: Tier::Software, .. } => "software",
|
||||
Renderer::Cpu { .. } => "cpu",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,21 +44,7 @@ impl Policy {
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of device a picture was drawn on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Tier {
|
||||
Hardware,
|
||||
Software,
|
||||
}
|
||||
|
||||
impl Tier {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Tier::Hardware => "hardware GPU",
|
||||
Tier::Software => "software GPU",
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use crate::Tier;
|
||||
|
||||
/// An adapter's tier and its rank within it (lower is better).
|
||||
pub fn rank(t: wgpu::DeviceType) -> (Tier, u8) {
|
||||
|
||||
@@ -19,6 +19,6 @@ pub use camera::{Camera, FIT_MARGIN, FULL_VIEW, Mat4, Orientation, StandardView}
|
||||
pub use display::{DisplayEdge, DisplayLine, DisplayMesh, DisplayNode, DisplayTri, SurfaceCurve, SurfaceTri};
|
||||
pub use raster::{
|
||||
Canvas, LABEL_OFFSET, MAX_DIM, MAX_PIXELS, MAX_PRIMITIVES, RenderError, Rgb, Scene, SceneFill, SceneLabel, SceneLine, ScenePoint, SceneTri,
|
||||
fit_size, png, render, render_layers, render_layers_png, render_png, shade,
|
||||
draw_labels, fit_size, png, render, render_layers, render_layers_png, render_png, shade,
|
||||
};
|
||||
pub use vec::{Aabb3, Vec3};
|
||||
@@ -336,6 +336,36 @@ pub fn render_layers(scene: &Scene, overlays: &[&Scene], cam: &Camera, w: usize,
|
||||
downsample(&c, w, h)
|
||||
}
|
||||
|
||||
/// `layers`' labels drawn over finished `w × h` RGBA pixels (a GPU picture), as [`render_layers`]
|
||||
/// draws them: the pixels are drawn at the supersampled size, so pixels away from a label come
|
||||
/// back unchanged. `radius` is the scene's clipping radius.
|
||||
pub fn draw_labels(rgba: &[u8], w: usize, h: usize, layers: &[&Scene], cam: &Camera, radius: f64) -> Result<Vec<u8>, RenderError> {
|
||||
if !size_ok(w, h) || rgba.len() != w * h * 4 {
|
||||
return Err(RenderError::Size { w, h });
|
||||
}
|
||||
let labels: Vec<&SceneLabel> = layers.iter().flat_map(|s| &s.labels).take(MAX_LABELS).collect();
|
||||
if labels.is_empty() {
|
||||
return Ok(rgba.to_vec());
|
||||
}
|
||||
let (sw, sh) = (w * SS, h * SS);
|
||||
let mut c = Canvas::new(sw, sh)?;
|
||||
for y in 0..sh {
|
||||
for x in 0..sw {
|
||||
let (i, o) = (((y / SS) * w + x / SS) * 4, (y * sw + x) * 4);
|
||||
if let (Some(src), Some(dst)) = (rgba.get(i..i + 4), c.rgba.get_mut(o..o + 4)) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
let proj = Projector::new(cam, sw as f64, sh as f64, radius);
|
||||
for l in labels {
|
||||
if let Some(q) = proj.screen(l.p) {
|
||||
label(&mut c, q, &l.text, l.color.f());
|
||||
}
|
||||
}
|
||||
Ok(downsample(&c, w, h)?.rgba)
|
||||
}
|
||||
|
||||
/// Cover a triangle with screen corners `a`, `b`, `c` (x, y, depth) on a `w × h` pixel grid:
|
||||
/// `pixel(x, y, depth)` for every pixel whose centre is inside it (its edges included), the depth
|
||||
/// interpolated. Degenerate and off-grid triangles cover nothing. The renderer and the picking
|
||||
@@ -614,6 +644,20 @@ mod tests {
|
||||
let _ = render(&wild, &cam, 32, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_draw_over_finished_pixels() {
|
||||
let (w, h) = (64, 32);
|
||||
let px: Vec<u8> = (0..w * h).flat_map(|i| [(i % 251) as u8, 40, 200, 255]).collect();
|
||||
let cam = Camera::default();
|
||||
let mut scene = Scene::default();
|
||||
assert_eq!(draw_labels(&px, w, h, &[&scene], &cam, 10.0).unwrap(), px);
|
||||
scene.labels.push(SceneLabel { p: Vec3::ZERO, text: "RP".into(), color: Rgb(0, 0, 0) });
|
||||
let out = draw_labels(&px, w, h, &[&scene], &cam, 10.0).unwrap();
|
||||
let changed = out.chunks(4).zip(px.chunks(4)).filter(|(a, b)| a != b).count();
|
||||
assert!(changed > 10 && changed < w * h / 2, "{changed}");
|
||||
assert!(draw_labels(&px, w + 1, h, &[&scene], &cam, 10.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_draw_over_everything_beside_their_point() {
|
||||
let mut scene = red_triangle();
|
||||
|
||||
Reference in new issue
Block a user