Phase 3c: bound the raster-layer GPU texture cache + show VRAM in F3
`raster_layer_cache` (one ~w·h·16-byte Rgba16Float CanvasPair per keyframe) had no size cap — scrubbing a long timeline grew VRAM without bound (~33 MB/frame at 1080p), the largest unbounded consumer. Added a recency LRU (RASTER_LAYER_CACHE_MAX = 12): `ensure_layer_texture` bumps the frame to most-recent and evicts the oldest past the budget; the shown frame (and any rendered this pass) is always most-recent so it's never the victim. Evicted textures re-upload cheaply from the resident/faulted-in pixels on revisit. `remove_layer_texture` keeps the LRU in sync. F3 debug overlay now reports the tracked VRAM (raster cache MB + frame count), pushed from the GPU brush whenever the cache changes (wgpu exposes no allocator query).
This commit is contained in:
parent
62d6cd0c84
commit
17d7395229
|
|
@ -39,6 +39,33 @@ pub fn update_prepare_timing(
|
||||||
t.composite_ms = composite_ms;
|
t.composite_ms = composite_ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// GPU memory the editor tracks itself (wgpu has no allocator query). Currently the
|
||||||
|
/// raster-layer texture cache — the only unbounded-by-default VRAM consumer.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct GpuMemoryStats {
|
||||||
|
pub raster_cache_entries: usize,
|
||||||
|
pub raster_cache_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
static GPU_MEMORY: OnceLock<Mutex<GpuMemoryStats>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Called by the GPU brush whenever the raster-layer cache changes.
|
||||||
|
pub fn update_gpu_memory(raster_cache_entries: usize, raster_cache_bytes: usize) {
|
||||||
|
let cell = GPU_MEMORY.get_or_init(|| Mutex::new(GpuMemoryStats::default()));
|
||||||
|
if let Ok(mut s) = cell.lock() {
|
||||||
|
s.raster_cache_entries = raster_cache_entries;
|
||||||
|
s.raster_cache_bytes = raster_cache_bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_gpu_memory() -> GpuMemoryStats {
|
||||||
|
GPU_MEMORY
|
||||||
|
.get_or_init(|| Mutex::new(GpuMemoryStats::default()))
|
||||||
|
.lock()
|
||||||
|
.map(|s| s.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
const DEVICE_REFRESH_INTERVAL: Duration = Duration::from_secs(2); // Refresh devices every 2 seconds
|
const DEVICE_REFRESH_INTERVAL: Duration = Duration::from_secs(2); // Refresh devices every 2 seconds
|
||||||
const MEMORY_REFRESH_INTERVAL: Duration = Duration::from_millis(500); // Refresh memory every 500ms
|
const MEMORY_REFRESH_INTERVAL: Duration = Duration::from_millis(500); // Refresh memory every 500ms
|
||||||
|
|
||||||
|
|
@ -52,6 +79,7 @@ pub struct DebugStats {
|
||||||
pub frame_time_ms: f32, // Current frame time in milliseconds
|
pub frame_time_ms: f32, // Current frame time in milliseconds
|
||||||
pub memory_physical_mb: usize,
|
pub memory_physical_mb: usize,
|
||||||
pub memory_virtual_mb: usize,
|
pub memory_virtual_mb: usize,
|
||||||
|
pub gpu_memory: GpuMemoryStats,
|
||||||
pub gpu_name: String,
|
pub gpu_name: String,
|
||||||
pub gpu_backend: String,
|
pub gpu_backend: String,
|
||||||
pub gpu_driver: String,
|
pub gpu_driver: String,
|
||||||
|
|
@ -218,6 +246,7 @@ impl DebugStatsCollector {
|
||||||
frame_time_ms,
|
frame_time_ms,
|
||||||
memory_physical_mb,
|
memory_physical_mb,
|
||||||
memory_virtual_mb,
|
memory_virtual_mb,
|
||||||
|
gpu_memory: get_gpu_memory(),
|
||||||
gpu_name,
|
gpu_name,
|
||||||
gpu_backend,
|
gpu_backend,
|
||||||
gpu_driver,
|
gpu_driver,
|
||||||
|
|
@ -286,6 +315,11 @@ pub fn render_debug_overlay(ctx: &egui::Context, stats: &DebugStats) {
|
||||||
ui.colored_label(egui::Color32::YELLOW, format!("Memory: ({}µs)", stats.timing_memory_us));
|
ui.colored_label(egui::Color32::YELLOW, format!("Memory: ({}µs)", stats.timing_memory_us));
|
||||||
ui.label(format!("Physical: {} MB", stats.memory_physical_mb));
|
ui.label(format!("Physical: {} MB", stats.memory_physical_mb));
|
||||||
ui.label(format!("Virtual: {} MB", stats.memory_virtual_mb));
|
ui.label(format!("Virtual: {} MB", stats.memory_virtual_mb));
|
||||||
|
ui.label(format!(
|
||||||
|
"VRAM (raster cache): {:.1} MB ({} frames)",
|
||||||
|
stats.gpu_memory.raster_cache_bytes as f64 / (1024.0 * 1024.0),
|
||||||
|
stats.gpu_memory.raster_cache_entries,
|
||||||
|
));
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1049,6 +1049,11 @@ pub struct GpuBrushEngine {
|
||||||
/// once when `RasterKeyframe::texture_dirty` is set, then reused every frame.
|
/// once when `RasterKeyframe::texture_dirty` is set, then reused every frame.
|
||||||
/// Separate from `canvases` so tool teardown never accidentally removes them.
|
/// Separate from `canvases` so tool teardown never accidentally removes them.
|
||||||
pub raster_layer_cache: HashMap<Uuid, CanvasPair>,
|
pub raster_layer_cache: HashMap<Uuid, CanvasPair>,
|
||||||
|
/// Recency order for `raster_layer_cache` (least-recent first), bumped on every
|
||||||
|
/// `ensure_layer_texture` so the visible frames stay at the back. Scrubbing a long
|
||||||
|
/// timeline would otherwise grow this map without bound (~w·h·16 bytes of VRAM per
|
||||||
|
/// entry); we evict the oldest once it exceeds `RASTER_LAYER_CACHE_MAX`.
|
||||||
|
raster_layer_lru: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CPU-side parameters uniform for the compute shader.
|
/// CPU-side parameters uniform for the compute shader.
|
||||||
|
|
@ -1066,6 +1071,12 @@ struct DabParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuBrushEngine {
|
impl GpuBrushEngine {
|
||||||
|
/// Max number of idle raster-layer textures kept resident in VRAM. Scrubbing a
|
||||||
|
/// long timeline would otherwise cache one ~`w·h·16`-byte pair per visited frame
|
||||||
|
/// without bound; the least-recently-used are evicted past this (re-uploaded on
|
||||||
|
/// revisit from the faulted-in pixels).
|
||||||
|
const RASTER_LAYER_CACHE_MAX: usize = 12;
|
||||||
|
|
||||||
/// Create the pipeline. Returns `Err` if the device lacks the required
|
/// Create the pipeline. Returns `Err` if the device lacks the required
|
||||||
/// storage-texture capability for `Rgba16Float`.
|
/// storage-texture capability for `Rgba16Float`.
|
||||||
pub fn new(device: &wgpu::Device) -> Self {
|
pub fn new(device: &wgpu::Device) -> Self {
|
||||||
|
|
@ -1160,6 +1171,7 @@ impl GpuBrushEngine {
|
||||||
canvases: HashMap::new(),
|
canvases: HashMap::new(),
|
||||||
displacement_bufs: HashMap::new(),
|
displacement_bufs: HashMap::new(),
|
||||||
raster_layer_cache: HashMap::new(),
|
raster_layer_cache: HashMap::new(),
|
||||||
|
raster_layer_lru: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1533,6 +1545,31 @@ impl GpuBrushEngine {
|
||||||
}
|
}
|
||||||
self.raster_layer_cache.insert(kf_id, canvas);
|
self.raster_layer_cache.insert(kf_id, canvas);
|
||||||
}
|
}
|
||||||
|
// Bump recency (the frame was used this render) and evict the least-recently
|
||||||
|
// used textures over budget. The current frame is pushed to the back, so it
|
||||||
|
// (and every other frame rendered this pass) is never the eviction victim.
|
||||||
|
if let Some(pos) = self.raster_layer_lru.iter().position(|id| *id == kf_id) {
|
||||||
|
self.raster_layer_lru.remove(pos);
|
||||||
|
}
|
||||||
|
self.raster_layer_lru.push(kf_id);
|
||||||
|
let mut evicted = false;
|
||||||
|
while self.raster_layer_lru.len() > Self::RASTER_LAYER_CACHE_MAX {
|
||||||
|
let old = self.raster_layer_lru.remove(0);
|
||||||
|
self.raster_layer_cache.remove(&old);
|
||||||
|
evicted = true;
|
||||||
|
}
|
||||||
|
if needs_new || evicted {
|
||||||
|
self.report_raster_cache_vram();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimated VRAM footprint of `raster_layer_cache` (two `Rgba16Float` textures =
|
||||||
|
/// `w·h·16` bytes per entry), published to the F3 debug overlay.
|
||||||
|
fn report_raster_cache_vram(&self) {
|
||||||
|
let bytes: usize = self.raster_layer_cache.values()
|
||||||
|
.map(|c| (c.width as usize) * (c.height as usize) * 16)
|
||||||
|
.sum();
|
||||||
|
crate::debug_overlay::update_gpu_memory(self.raster_layer_cache.len(), bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the cached display texture for a raster layer keyframe.
|
/// Get the cached display texture for a raster layer keyframe.
|
||||||
|
|
@ -1542,7 +1579,12 @@ impl GpuBrushEngine {
|
||||||
|
|
||||||
/// Remove the cached texture for a raster layer keyframe (e.g. when deleted).
|
/// Remove the cached texture for a raster layer keyframe (e.g. when deleted).
|
||||||
pub fn remove_layer_texture(&mut self, kf_id: &Uuid) {
|
pub fn remove_layer_texture(&mut self, kf_id: &Uuid) {
|
||||||
self.raster_layer_cache.remove(kf_id);
|
if self.raster_layer_cache.remove(kf_id).is_some() {
|
||||||
|
if let Some(pos) = self.raster_layer_lru.iter().position(|id| id == kf_id) {
|
||||||
|
self.raster_layer_lru.remove(pos);
|
||||||
|
}
|
||||||
|
self.report_raster_cache_vram();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composite the accumulated-dab scratch buffer C over the source A, writing the
|
/// Composite the accumulated-dab scratch buffer C over the source A, writing the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue