forked from 1b-agent/opencadstudio-upstream
fix: give the wire arena its own buffer budget
`PersistentWireArena` keeps one instance buffer for the whole resident
set and patches it in place on an edit. Sizing that against the shared
per-chunk budget caps it at 32 MiB, which is 524 288 `WireInstance`s.
A drawing with more than that fails `WireArena::build`, falls back to the
batched path, and re-uploads everything on every edit. On the 198 k-wire
reproducer -- 821 316 instances -- that shows up as:
wire 200-370ms arena-hybrid
instead of the ~12 ms `arena-patch` the same drawing reached before the
limits moved onto the shared budget.
The 32 MiB cap is right for transient chunks, where many are allocated
per frame and any one of them failing loses the frame. The arena's buffer
is a single long-lived allocation that exists precisely to avoid those
re-uploads, and it can afford more: 821 k instances is 53 MB, ~79 MB with
the arena's headroom. 128 MiB leaves it four times smaller than the
505 MiB allocation that crashed a 2 GB card, and it is allocated through
`gpu_upload`, so exhausting VRAM still degrades the frame rather than
aborting.
`OCS_GPU_CHUNK_MIB` overrides both caps, so a test can still force small
buffers everywhere.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
13afc1479f
commit
19490a548b
2 files changed
+41
-7
No files matched your search
@@ -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<usize> = 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<usize> {
|
||||
static CAP: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
|
||||
*CAP.get_or_init(|| {
|
||||
std::env::var("OCS_GPU_CHUNK_MIB")
|
||||
.ok()
|
||||
.and_then(|value| value.trim().parse::<usize>().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<T>(device: &wgpu::Device) -> usize {
|
||||
(arena_budget(device) / std::mem::size_of::<T>().max(1)).max(1)
|
||||
}
|
||||
|
||||
/// Arena storage buffers must also fit in one binding.
|
||||
pub fn max_arena_storage_elements<T>(device: &wgpu::Device) -> usize {
|
||||
(arena_budget(device).min(device.limits().max_storage_buffer_binding_size as usize)
|
||||
/ std::mem::size_of::<T>().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())
|
||||
|
||||
@@ -313,8 +313,10 @@ impl WireArena {
|
||||
const_bgl: &wgpu::BindGroupLayout,
|
||||
mesh_edge: bool,
|
||||
) -> Option<Self> {
|
||||
let instance_limit = super::gpu_budget::max_elements::<WireInstance>(device) as u64;
|
||||
let constant_limit = super::gpu_budget::max_storage_elements::<WireConst>(device) as u64;
|
||||
let instance_limit =
|
||||
super::gpu_budget::max_arena_elements::<WireInstance>(device) as u64;
|
||||
let constant_limit =
|
||||
super::gpu_budget::max_arena_storage_elements::<WireConst>(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<u64, [f32; 2]>,
|
||||
mesh_edge: bool,
|
||||
) -> Option<Self> {
|
||||
let packed_limit = super::gpu_budget::max_elements::<PackedWireInstance>(device) as u64;
|
||||
let packed_limit =
|
||||
super::gpu_budget::max_arena_elements::<PackedWireInstance>(device) as u64;
|
||||
let ranges = handle_ranges(wires)?;
|
||||
let max_instances: usize = wires
|
||||
.iter()
|
||||
|
||||
Reference in new issue
Block a user