mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:41:25 +00:00
Preview: render at most two frames on the GPU at once so video memory stays flat (#3028)
This commit is contained in:
1 parent
58ca9c1c1b
commit
29619dfe67
3 files changed
+80
-5
No files matched your search
@@ -26,7 +26,7 @@
|
||||
|
||||
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Condvar, Mutex, PoisonError};
|
||||
|
||||
use effectcraft_engine::project::{Comp, ItemId, ItemKind, LayerSource, Node, Project, PropGroup};
|
||||
use effectcraft_engine::render::disk_cache::{self, DiskCache};
|
||||
@@ -47,6 +47,46 @@ const MIN_GPU_BUDGET: usize = 64 << 20;
|
||||
const GPU_PRESSURE_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
const GPU_RECOVERY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// GPU frames that render at once, however many frame threads there are. Each one holds its own
|
||||
/// working textures, so 16 at once ran an RTX 3090 out of video memory (#3028).
|
||||
const MAX_GPU_RENDERS: usize = 2;
|
||||
|
||||
/// Counts the GPU renders in flight; the rest of the frame threads wait their turn.
|
||||
struct GpuSlots {
|
||||
busy: Mutex<usize>,
|
||||
freed: Condvar,
|
||||
}
|
||||
|
||||
/// One GPU render slot, given back on drop.
|
||||
struct GpuSlot<'a>(&'a GpuSlots);
|
||||
|
||||
static GPU_SLOTS: GpuSlots = GpuSlots::new();
|
||||
|
||||
impl GpuSlots {
|
||||
const fn new() -> Self {
|
||||
Self { busy: Mutex::new(0), freed: Condvar::new() }
|
||||
}
|
||||
|
||||
/// Wait until fewer than `max` renders are in flight, then take a slot. The browser has no
|
||||
/// threads to wait on: it never blocks.
|
||||
fn acquire(&self, max: usize) -> GpuSlot<'_> {
|
||||
let mut busy = self.busy.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
while !cfg!(target_arch = "wasm32") && *busy >= max {
|
||||
busy = self.freed.wait(busy).unwrap_or_else(PoisonError::into_inner);
|
||||
}
|
||||
*busy += 1;
|
||||
GpuSlot(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GpuSlot<'_> {
|
||||
fn drop(&mut self) {
|
||||
let mut busy = self.0.busy.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
*busy = busy.saturating_sub(1);
|
||||
self.0.freed.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
/// A rendered viewer frame: CPU pixels or a GPU texture.
|
||||
#[derive(Clone)]
|
||||
pub enum FrameImage {
|
||||
@@ -1458,7 +1498,10 @@ impl Worker {
|
||||
r.cache = Some(&job.src.layer_cache);
|
||||
r.accel = Some(g as &dyn effectcraft_engine::render::Accelerator);
|
||||
r.active_accel()?;
|
||||
let f = g.within_memory(|| g.render_display(&r, job.comp, job.t));
|
||||
let f = {
|
||||
let _slot = GPU_SLOTS.acquire(MAX_GPU_RENDERS);
|
||||
g.within_memory(|| g.render_display(&r, job.comp, job.t))
|
||||
};
|
||||
effectcraft_engine::render::Accelerator::frame_end(g);
|
||||
match f {
|
||||
Ok(f) => {
|
||||
@@ -1497,7 +1540,11 @@ impl Worker {
|
||||
let auto = if cfg!(target_arch = "wasm32") { None } else { r.frame_auto(a, job.comp, true) };
|
||||
if auto.is_none_or(|(p, key)| p.choose(key)) {
|
||||
let t0 = web_time::Instant::now();
|
||||
match g.within_memory(|| g.render_display(&r, job.comp, job.t)) {
|
||||
let shown = {
|
||||
let _slot = GPU_SLOTS.acquire(MAX_GPU_RENDERS);
|
||||
g.within_memory(|| g.render_display(&r, job.comp, job.t))
|
||||
};
|
||||
match shown {
|
||||
Ok(Some(f)) => {
|
||||
self.gpu_succeeded();
|
||||
if let Some((p, key)) = auto {
|
||||
@@ -1530,7 +1577,11 @@ impl Worker {
|
||||
}
|
||||
if let Some(g) = gpu {
|
||||
// The GPU composites and the frame is read back.
|
||||
return match g.within_memory(|| r.comp_frame(job.comp, job.t)) {
|
||||
let composited = {
|
||||
let _slot = GPU_SLOTS.acquire(MAX_GPU_RENDERS);
|
||||
g.within_memory(|| r.comp_frame(job.comp, job.t))
|
||||
};
|
||||
return match composited {
|
||||
Ok(img) => {
|
||||
self.gpu_succeeded();
|
||||
FrameImage::Cpu(Arc::new(to_color_image(&img)))
|
||||
|
||||
@@ -1113,3 +1113,25 @@ fn timing_markers_and_expressions_still_change_the_frames() {
|
||||
s.execute("layer.addMarker", json!({"comp": outer.0, "layers": [solid], "time": 0.75})).unwrap();
|
||||
assert_ne!(at(&s), o2, "an expression can read the marker, so the frames change");
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn gpu_renders_in_flight_stay_within_the_cap() {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
let slots = GpuSlots::new();
|
||||
let now = AtomicUsize::new(0);
|
||||
let peak = AtomicUsize::new(0);
|
||||
std::thread::scope(|s| {
|
||||
for _ in 0..16 {
|
||||
s.spawn(|| {
|
||||
let _slot = slots.acquire(2);
|
||||
let n = now.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
peak.fetch_max(n, Ordering::SeqCst);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
now.fetch_sub(1, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
});
|
||||
assert!(peak.load(Ordering::SeqCst) <= 2);
|
||||
assert_eq!(*slots.busy.lock().unwrap_or_else(PoisonError::into_inner), 0);
|
||||
}
|
||||
@@ -362,7 +362,9 @@ as NaNs (a kernel that skips pixels fails the oracle) and validation errors pani
|
||||
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. Outside
|
||||
tests, device errors are logged rather than fatal. A viewer frame that runs out of video memory
|
||||
(`Gpu::within_memory`) renders on the CPU, the uploaded layers and pooled textures are freed, and
|
||||
the RAM preview keeps half as many GPU frames from then on. The same guard catches a frame's
|
||||
the RAM preview keeps half as many GPU frames from then on. However many frame threads the viewer
|
||||
runs, at most two of them render on the GPU at once and the rest wait their turn, so their working
|
||||
textures don't add up to more video memory than the GPU has (#3028). The same guard catches a frame's
|
||||
validation errors (an invalid texture, often left by a failed allocation): that frame renders on
|
||||
the CPU and the kept textures are freed, but the compositor stays on and, with no allocation
|
||||
failure before it, the RAM preview keeps its GPU frames (#1319). A frame whose render panics is
|
||||
|
||||
Reference in new issue
Block a user