forked from 1b-agent/fieldcraft-upstream
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:
1 parent
eae709b3c0
commit
7714afbced
1 file changed
+58
-32
+58
-32
@@ -83,33 +83,49 @@ impl Capturer {
|
|||||||
self.target.samples
|
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
|
/// 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.
|
/// 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>> {
|
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);
|
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 {
|
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)")));
|
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 oom = self.device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
|
||||||
let validation = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
|
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()));
|
let caught = popped(validation.pop()).or_else(|| popped(oom.pop()));
|
||||||
if let Some(e) = caught {
|
if let Some(e) = caught {
|
||||||
return Err(GpuError::Failed(e.to_string()));
|
return Err(GpuError::Failed(e.to_string()));
|
||||||
}
|
}
|
||||||
let pixels = over_background(&drawn?, w as usize, h as usize, scene.background.unwrap_or(BACKGROUND));
|
drawn
|
||||||
// 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>> {
|
fn draw(&self, w: u32, h: u32, paint: impl FnOnce(&mut wgpu::RenderPass<'static>)) -> Result<Vec<u8>> {
|
||||||
let (d, q) = (&self.device, &self.queue);
|
let d = &self.device;
|
||||||
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 size = wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 };
|
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| {
|
let texture = |label: &str, format: wgpu::TextureFormat, samples: u32, usage: wgpu::TextureUsages| {
|
||||||
d.create_texture(&wgpu::TextureDescriptor {
|
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 encoder = d.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("fc_capture") });
|
||||||
{
|
{
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut pass = encoder
|
||||||
label: Some("fc_capture"),
|
.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
label: Some("fc_capture"),
|
||||||
view,
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
depth_slice: None,
|
view,
|
||||||
resolve_target: resolve,
|
depth_slice: None,
|
||||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
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_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
|
view: &depth_view,
|
||||||
stencil_ops: None,
|
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,
|
timestamp_writes: None,
|
||||||
multiview_mask: None,
|
occlusion_query_set: None,
|
||||||
});
|
multiview_mask: None,
|
||||||
let [s, y, o] = &uploaded;
|
})
|
||||||
self.pipelines.paint(&mut pass, s, y, o);
|
.forget_lifetime();
|
||||||
|
paint(&mut pass);
|
||||||
}
|
}
|
||||||
encoder.copy_texture_to_buffer(
|
encoder.copy_texture_to_buffer(
|
||||||
wgpu::TexelCopyTextureInfo { texture: &out, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
wgpu::TexelCopyTextureInfo { texture: &out, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
||||||
@@ -171,7 +188,7 @@ impl Capturer {
|
|||||||
},
|
},
|
||||||
size,
|
size,
|
||||||
);
|
);
|
||||||
q.submit([encoder.finish()]);
|
self.queue.submit([encoder.finish()]);
|
||||||
let slice = buffer.slice(..);
|
let slice = buffer.slice(..);
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||||
@@ -227,6 +244,15 @@ pub fn png(
|
|||||||
Ok((png, Renderer::Cpu { why }))
|
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.
|
/// Bytes per row of a `w` pixel RGBA copy, padded to wgpu's 256-byte alignment.
|
||||||
pub fn padded_row(w: u32) -> u32 {
|
pub fn padded_row(w: u32) -> u32 {
|
||||||
let a = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
let a = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||||
|
|||||||
Reference in new issue
Block a user