diff --git a/src/scene/pipeline/gpu_budget.rs b/src/scene/pipeline/gpu_budget.rs index 5338a2310..052f1fdb9 100644 --- a/src/scene/pipeline/gpu_budget.rs +++ b/src/scene/pipeline/gpu_budget.rs @@ -8,19 +8,50 @@ fn device_ceiling(device: &wgpu::Device) -> usize { (usize::try_from(device.limits().max_buffer_size).unwrap_or(usize::MAX) / 10) * 9 } -/// Optional per-buffer cap in MiB, useful for testing smaller chunks. -fn cap_bytes() -> usize { - static CAP: std::sync::OnceLock = std::sync::OnceLock::new(); +/// The wire arena keeps one persistent instance buffer for the whole resident +/// set and patches it in place. Capping that at the per-chunk budget pushes any +/// drawing past ~524 k instances onto the batched fallback, which re-uploads +/// everything on every edit — 200 ms a click on a drawing with 821 k. A single +/// long-lived buffer can afford more than a transient chunk, and 128 MiB is +/// still four times smaller than the 505 MiB allocation that crashed a 2 GB +/// card in the first place. +const ARENA_CAP_BYTES: usize = 128 * 1024 * 1024; + +/// `OCS_GPU_CHUNK_MIB`, when set. Overrides both caps so a test can force +/// small buffers. +fn env_cap_bytes() -> Option { + static CAP: std::sync::OnceLock> = std::sync::OnceLock::new(); *CAP.get_or_init(|| { std::env::var("OCS_GPU_CHUNK_MIB") .ok() .and_then(|value| value.trim().parse::().ok()) .filter(|mib| *mib > 0) .and_then(|mib| mib.checked_mul(1024 * 1024)) - .unwrap_or(HARD_CAP_BYTES) }) } +/// Optional per-buffer cap in MiB, useful for testing smaller chunks. +fn cap_bytes() -> usize { + env_cap_bytes().unwrap_or(HARD_CAP_BYTES) +} + +/// Largest byte size the wire arena's persistent buffers may take. +pub fn arena_budget(device: &wgpu::Device) -> usize { + device_ceiling(device).min(env_cap_bytes().unwrap_or(ARENA_CAP_BYTES)) +} + +/// How many `T` fit in one arena buffer. Never returns 0. +pub fn max_arena_elements(device: &wgpu::Device) -> usize { + (arena_budget(device) / std::mem::size_of::().max(1)).max(1) +} + +/// Arena storage buffers must also fit in one binding. +pub fn max_arena_storage_elements(device: &wgpu::Device) -> usize { + (arena_budget(device).min(device.limits().max_storage_buffer_binding_size as usize) + / std::mem::size_of::().max(1)) + .max(1) +} + /// Largest byte size a single buffer built by this renderer may take. pub fn buffer_budget(device: &wgpu::Device) -> usize { device_ceiling(device).min(cap_bytes()) diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index cf9707c58..9b10f76f7 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -313,8 +313,10 @@ impl WireArena { const_bgl: &wgpu::BindGroupLayout, mesh_edge: bool, ) -> Option { - let instance_limit = super::gpu_budget::max_elements::(device) as u64; - let constant_limit = super::gpu_budget::max_storage_elements::(device) as u64; + let instance_limit = + super::gpu_budget::max_arena_elements::(device) as u64; + let constant_limit = + super::gpu_budget::max_arena_storage_elements::(device) as u64; let ranges = handle_ranges(wires)?; let perf = crate::perf::enabled(); let total_started = iced::time::Instant::now(); @@ -971,7 +973,8 @@ impl PackedWireArena { depth_map: &FxHashMap, mesh_edge: bool, ) -> Option { - let packed_limit = super::gpu_budget::max_elements::(device) as u64; + let packed_limit = + super::gpu_budget::max_arena_elements::(device) as u64; let ranges = handle_ranges(wires)?; let max_instances: usize = wires .iter()