mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
GPU: the upload cache budget can be set per context and warns once when full, instead of evicting layer textures silently (#687)
This commit is contained in:
1 parent
9c5c187292
commit
d8ff1189dc
2 files changed
+63
-3
No files matched your search
@@ -37,7 +37,7 @@ const EXT_ENTRIES: &[&str] = &["classic3d"];
|
||||
/// 8/16 bpc are emulated by clamping and quantising, as on the CPU).
|
||||
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
|
||||
|
||||
/// Uploaded layer buffers kept on the GPU (bytes).
|
||||
/// Uploaded layer buffers kept on the GPU by default (bytes; [`GpuContext::set_upload_budget`]).
|
||||
const UPLOAD_BUDGET: usize = 1536 << 20;
|
||||
|
||||
/// Working textures kept for reuse once their last image is dropped (bytes).
|
||||
@@ -296,6 +296,8 @@ pub struct GpuContext {
|
||||
dummy_tex: wgpu::TextureView,
|
||||
dummy_buf: wgpu::Buffer,
|
||||
uploads: Mutex<Uploads>,
|
||||
upload_budget: std::sync::atomic::AtomicUsize,
|
||||
upload_full_warned: std::sync::atomic::AtomicBool,
|
||||
pool: Arc<Mutex<Pool>>,
|
||||
/// CPU ↔ GPU traffic (diagnostics: `bench --gpu`).
|
||||
pub(crate) transfers: Transfers,
|
||||
@@ -641,6 +643,8 @@ impl GpuContext {
|
||||
dummy,
|
||||
dummy_buf,
|
||||
uploads: Mutex::new(Uploads::default()),
|
||||
upload_budget: std::sync::atomic::AtomicUsize::new(UPLOAD_BUDGET),
|
||||
upload_full_warned: Default::default(),
|
||||
pool: Default::default(),
|
||||
staging: Default::default(),
|
||||
transfers: Default::default(),
|
||||
@@ -869,6 +873,17 @@ impl GpuContext {
|
||||
(GpuImage { texture, width: w, height: h, _lease: Some(lease), levels: None }, was_reused)
|
||||
}
|
||||
|
||||
/// Bytes of uploaded layer textures the context keeps (default 1536 MB). An application that
|
||||
/// knows the device's memory can raise it (static layers of a big frame upload once) or lower
|
||||
/// it (a small GPU); the least recently used textures go first.
|
||||
pub fn set_upload_budget(&self, bytes: usize) {
|
||||
self.upload_budget.store(bytes.max(1 << 20), std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn upload_budget(&self) -> usize {
|
||||
self.upload_budget.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Uploads and readbacks so far (images; uploads of cached layers count once).
|
||||
pub fn transfer_stats(&self) -> TransferStats {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
@@ -925,7 +940,8 @@ impl GpuContext {
|
||||
}
|
||||
u.bytes += bytes;
|
||||
// Drop textures of freed buffers, then the least recently used over budget.
|
||||
if u.bytes > UPLOAD_BUDGET {
|
||||
let budget = self.upload_budget();
|
||||
if u.bytes > budget {
|
||||
let dead: Vec<usize> = u.map.iter().filter(|(_, e)| e.buf.strong_count() == 0).map(|(k, _)| *k).collect();
|
||||
for k in dead {
|
||||
if let Some(e) = u.map.remove(&k) {
|
||||
@@ -933,7 +949,14 @@ impl GpuContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
while u.bytes > UPLOAD_BUDGET {
|
||||
if u.bytes > budget && !self.upload_full_warned.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
log::warn!(
|
||||
target: "effectcraft_gpu",
|
||||
"GPU upload cache full ({} MB): evicting layer textures, which upload again when next used; a frame whose static layers exceed it re-uploads them every frame",
|
||||
budget >> 20
|
||||
);
|
||||
}
|
||||
while u.bytes > budget {
|
||||
let Some(k) = u.map.iter().min_by_key(|(_, e)| e.last_use).map(|(k, _)| *k) else { break };
|
||||
if let Some(e) = u.map.remove(&k) {
|
||||
u.bytes -= e.bytes;
|
||||
|
||||
@@ -942,3 +942,40 @@ fn texture_reuse_does_not_change_output() {
|
||||
assert!(off.is_some_and(|off| off.data == on.data), "texture reuse changed the frame");
|
||||
check("texture reuse vs CPU", compare_at(&s, opts(), t), 0.0);
|
||||
}
|
||||
|
||||
/// Static layer buffers (here adjustment-layer footprints) upload once while they fit the upload budget, and upload again every frame
|
||||
/// when they do not (least recently used first); the frame is the same either way.
|
||||
#[test]
|
||||
fn upload_budget_bounds_the_layer_textures_kept() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let mut s = Scene::new(BitDepth::Bpc8);
|
||||
for (i, c) in [[0.8, 0.2, 0.1], [0.1, 0.3, 0.9]].into_iter().enumerate() {
|
||||
let mut l = s.solid(c, 200, 200);
|
||||
set(&mut l, "transform/position", v3(60.0 + 30.0 * i as f64, 40.0));
|
||||
// Adjustment layers: their footprints are cached layer buffers the GPU uploads.
|
||||
l.switches.adjustment = true;
|
||||
s.effect(&mut l, "ec.color.levels", &[("gamma", n(1.1 + 0.2 * i as f64))]);
|
||||
s.push(l);
|
||||
}
|
||||
let frame = |cache: &effectcraft_render::cache::LayerCache, k: u64| {
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
r.cache = Some(cache);
|
||||
let before = g.ctx.transfer_stats().upload_bytes;
|
||||
let img = g.render(&r, s.cid, Tick::from_seconds_f64(k as f64 / 30.0)).expect("the GPU renders the frame");
|
||||
(img.data, g.ctx.transfer_stats().upload_bytes - before)
|
||||
};
|
||||
let fits = effectcraft_render::cache::LayerCache::new(64 << 20);
|
||||
let (a, _) = frame(&fits, 0);
|
||||
let (b, up) = frame(&fits, 1);
|
||||
assert_eq!(up, 0, "static layers re-uploaded within the default budget");
|
||||
// A budget smaller than the two 640 KB footprints: each upload evicts the other.
|
||||
let default = g.ctx.upload_budget();
|
||||
g.ctx.set_upload_budget(1 << 20);
|
||||
let tight = effectcraft_render::cache::LayerCache::new(64 << 20);
|
||||
let _ = frame(&tight, 0);
|
||||
let (c, up) = frame(&tight, 1);
|
||||
g.ctx.set_upload_budget(default);
|
||||
assert!(up > 0, "over budget, the footprints must upload again");
|
||||
assert!(a == b && b == c, "the upload budget changed the frame");
|
||||
}
|
||||
Reference in new issue
Block a user