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:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 20:50:40 -04:00
1 parent fee978c7e4
commit c9163e98ad
5 files changed
+134 -20

No files matched your search

+1 -1
View File
@@ -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};
+44
View File
@@ -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();