gpu: Capturer::offscreen draws one pass that a caller records

Capturer::rgba becomes offscreen() (sizes, error scopes, MSAA, depth,
read-back) plus finish() (background and labels), so another painter,
such as the window's paint callback, can draw into the same pass.

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-16 08:27:17 -04:00
1 parent eae709b3c0
commit 7714afbced
1 file changed
+58 -32
+58 -32
View File
@@ -83,33 +83,49 @@ impl Capturer {
self.target.samples
}
/// The device pictures are drawn on, its queue and the target its passes have, for a caller
/// that draws through its own [`Pipelines`] in [`Capturer::offscreen`].
pub fn device(&self) -> (&wgpu::Device, &wgpu::Queue, GpuTarget) {
(&self.device, &self.queue, self.target)
}
/// RGBA pixels (`w × h`, rows top to bottom) of `scene` with `layers` (the symbols, then
/// the overlay) seen by `cam`, over the scene's background. Widths are taken as pixels.
pub fn rgba(&self, scene: &Scene, layers: &[&Scene], cam: &Camera, w: u32, h: u32) -> Result<Vec<u8>> {
let (d, q) = (&self.device, &self.queue);
let drawn = self.offscreen(w, h, |pass| {
let mut uploaded = [Layer::default(), Layer::default(), Layer::default()];
for (layer, s) in uploaded.iter_mut().zip(std::iter::once(scene).chain(layers.iter().copied())) {
layer.upload(d, q, &GpuScene::from_scene(s, 1.0));
}
self.pipelines.write_frame(q, &Frame::of(cam, w as f32, h as f32, scene.radius));
let [s, y, o] = &uploaded;
self.pipelines.paint(pass, s, y, o);
})?;
finish(&drawn, scene, layers, cam, w, h)
}
/// The premultiplied RGBA pixels (`w × h`, rows top to bottom, transparent where nothing
/// is drawn) of one render pass with the viewport's MSAA and depth, whose contents `paint`
/// records (uploads included). Sizes are checked first; device errors in `paint` and the
/// read-back are caught and returned.
pub fn offscreen(&self, w: u32, h: u32, paint: impl FnOnce(&mut wgpu::RenderPass<'static>)) -> Result<Vec<u8>> {
let max = self.device.limits().max_texture_dimension_2d.min(MAX_SIDE);
if w == 0 || h == 0 || w > max || h > max || u64::from(w) * u64::from(h) > MAX_PIXELS {
return Err(GpuError::Size(format!("picture size {w} x {h} is out of range (1..={max} per side, at most {MAX_PIXELS} pixels)")));
}
let oom = self.device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let validation = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let drawn = self.draw(scene, layers, cam, w, h);
let drawn = self.draw(w, h, paint);
let caught = popped(validation.pop()).or_else(|| popped(oom.pop()));
if let Some(e) = caught {
return Err(GpuError::Failed(e.to_string()));
}
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()))
drawn
}
fn draw(&self, scene: &Scene, layers: &[&Scene], cam: &Camera, w: u32, h: u32) -> Result<Vec<u8>> {
let (d, q) = (&self.device, &self.queue);
let mut uploaded = [Layer::default(), Layer::default(), Layer::default()];
for (layer, s) in uploaded.iter_mut().zip(std::iter::once(scene).chain(layers.iter().copied())) {
layer.upload(d, q, &GpuScene::from_scene(s, 1.0));
}
self.pipelines.write_frame(q, &Frame::of(cam, w as f32, h as f32, scene.radius));
fn draw(&self, w: u32, h: u32, paint: impl FnOnce(&mut wgpu::RenderPass<'static>)) -> Result<Vec<u8>> {
let d = &self.device;
let size = wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 };
let texture = |label: &str, format: wgpu::TextureFormat, samples: u32, usage: wgpu::TextureUsages| {
d.create_texture(&wgpu::TextureDescriptor {
@@ -143,25 +159,26 @@ impl Capturer {
});
let mut encoder = d.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("fc_capture") });
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("fc_capture"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: resolve,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_view,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
let [s, y, o] = &uploaded;
self.pipelines.paint(&mut pass, s, y, o);
let mut pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("fc_capture"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: resolve,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_view,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
.forget_lifetime();
paint(&mut pass);
}
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo { texture: &out, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
@@ -171,7 +188,7 @@ impl Capturer {
},
size,
);
q.submit([encoder.finish()]);
self.queue.submit([encoder.finish()]);
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
@@ -227,6 +244,15 @@ pub fn png(
Ok((png, Renderer::Cpu { why }))
}
/// A picture from [`Capturer::offscreen`]'s pixels of `scene` and `layers`: laid over the
/// scene's background gradient, with the name labels drawn in the CPU rasterizer's font (the
/// window draws them with the interface's).
pub fn finish(drawn: &[u8], scene: &Scene, layers: &[&Scene], cam: &Camera, w: u32, h: u32) -> Result<Vec<u8>> {
let pixels = over_background(drawn, w as usize, h as usize, scene.background.unwrap_or(BACKGROUND));
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()))
}
/// 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;