mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
Cut serial work from raw development and GPU transfers
- RawImage::develop demosaics only the default crop with AHD when there is no OpcodeList3 (ahd_window), instead of demosaicing the margin too and then copying the crop out of the full picture on one thread. - Gpu::upload writes through queue.write_buffer_with and copies into the staging buffer in parallel parts. create_buffer_init zeroed a staging buffer and then copied over it, two single-threaded passes over a full-size photo's ~290 MB. - finish_and_read copies the mapped readback out in parallel parts (one thread read the 91 MB of a 24 MP export at ~2 GB/s, ~45 ms). Output is byte-identical (ahd_window_is_the_crop_of_the_whole, uploads_read_back_exactly). Export of a 24 MP ARW to full-size JPEG (examples/export_bench, 75 interleaved runs each on a shared 16-thread Ryzen): main 802 ms median / 614 ms min -> this branch 647 / 525 (-19.3% / -14.5%). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ce8dbc55b
commit
93487af3b3
+56
-6
@@ -499,11 +499,31 @@ impl Gpu {
|
||||
self.over_limit(bytes.len() as u64);
|
||||
return Buf { buf: Some(Tracked::new(self.placeholder())), len };
|
||||
}
|
||||
let buf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytes,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
let usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST;
|
||||
if let Some(size) = wgpu::BufferSize::new(bytes.len() as u64).filter(|s| s.get() % wgpu::COPY_BUFFER_ALIGNMENT == 0) {
|
||||
// through the queue: the bytes are copied once, into a staging buffer the next submission copies from, in
|
||||
// parallel parts (`create_buffer_init` zeroes a staging buffer first and copies over that, two passes on
|
||||
// one thread over a full-size photo's ~290 MB)
|
||||
let buf = self.device.create_buffer(&wgpu::BufferDescriptor { label: None, size: size.get(), usage, mapped_at_creation: false });
|
||||
if let Some(mut view) = self.queue.write_buffer_with(&buf, 0, size) {
|
||||
const PART: usize = 1 << 20;
|
||||
{
|
||||
let (whole, mut tail) = view.slice(..).into_chunks::<PART>();
|
||||
let (src, src_tail) = bytes.as_chunks::<PART>();
|
||||
let mut parts: Vec<_> = whole.into_iter().zip(src).map(Some).collect();
|
||||
lightcraft_raster::par_rows(&mut parts, 1, |_, part| {
|
||||
for (d, s) in part.iter_mut().filter_map(Option::take) {
|
||||
wgpu::WriteOnly::<[u8]>::from(d).copy_from_slice(s);
|
||||
}
|
||||
});
|
||||
tail.copy_from_slice(src_tail);
|
||||
}
|
||||
// (dropping the view schedules the copy)
|
||||
drop(view);
|
||||
return Buf { buf: Some(Tracked::new(buf)), len };
|
||||
}
|
||||
}
|
||||
let buf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: bytes, usage });
|
||||
Buf { buf: Some(Tracked::new(buf)), len }
|
||||
}
|
||||
|
||||
@@ -612,7 +632,17 @@ impl Gpu {
|
||||
Ok(d) => d,
|
||||
Err(e) => return failed(format!("{e:?}")),
|
||||
};
|
||||
let out: Vec<T> = bytemuck::cast_slice(&data).to_vec();
|
||||
// copied out in parallel parts: one thread reads mapped memory slowly (~2 GB/s, so ~45 ms for a 24 MP
|
||||
// export's pixels), and the new vector's pages are first touched there too
|
||||
let src: &[u8] = &data;
|
||||
// (`len` counts 32-bit words: as many `T` as they fill)
|
||||
let mut out = vec![<T as bytemuck::Zeroable>::zeroed(); src.len() / std::mem::size_of::<T>().max(1)];
|
||||
const PART: usize = 1 << 20;
|
||||
lightcraft_raster::par_rows(bytemuck::cast_slice_mut::<T, u8>(&mut out), PART, |k, part| {
|
||||
if let Some(s) = src.get(k * PART..k * PART + part.len()) {
|
||||
part.copy_from_slice(s);
|
||||
}
|
||||
});
|
||||
drop(data);
|
||||
staging.unmap();
|
||||
out
|
||||
@@ -811,6 +841,26 @@ mod tests {
|
||||
Some(stdout)
|
||||
}
|
||||
|
||||
/// An upload reaches the device whole: it is copied into staging memory in parallel parts, the last one
|
||||
/// shorter than the others (and a small upload is that last part alone).
|
||||
#[test]
|
||||
fn uploads_read_back_exactly() {
|
||||
let Some(g) = crate::device() else {
|
||||
lightcraft_testkit::skip!(Gpu, "no GPU device");
|
||||
return;
|
||||
};
|
||||
for len in [1usize, 1000, (3 << 20) / 4 + 777] {
|
||||
let data: Vec<u32> = (0..len as u32).map(|i| i.wrapping_mul(2_654_435_761)).collect();
|
||||
let b = g.upload(&data);
|
||||
let back: Vec<u32> = g.finish_and_read(g.encoder(), &b, len);
|
||||
assert!(back == data, "{len} values");
|
||||
}
|
||||
// read back as wider values (`len` counts 32-bit words), as `read_rgb` does
|
||||
let data: Vec<u32> = (0..3 << 20).collect();
|
||||
let b = g.upload(&data);
|
||||
let back: Vec<[u32; 3]> = g.finish_and_read(g.encoder(), &b, data.len());
|
||||
assert!(back.len() == 1 << 20 && back.as_flattened() == data.as_slice());
|
||||
}
|
||||
/// Issue #1858: once a Vulkan or DX12 device is lost, wgpu 30 panics in `Device::poll` instead of
|
||||
/// returning an error. A render's readback panicked; while it unwound, a buffer's way back to
|
||||
/// the pool polls again from a destructor, and that second panic aborted the app.
|
||||
|
||||
Reference in New Issue
Block a user