gpui_core/resource_profile.rs
1//! Resource budget configuration for GPUI applications.
2//!
3//! Different application types have vastly different resource needs. A full IDE
4//! benefits from large caches and atlas textures, while a tray icon or a small
5//! popover panel can run with significantly less memory.
6//!
7//! [`AppResourceProfile`] allows callers to tune internal cache sizes and GPU
8//! resource allocation at application startup via
9//! [`Application::with_resource_profile`](crate::Application::with_resource_profile).
10//!
11//! # Quick Start
12//!
13//! ```rust,ignore
14//! use gpui::AppProfile;
15//!
16//! gpui::Application::new()
17//! .with_resource_profile(AppProfile::Minimal)
18//! .run(|cx| {
19//! // open your tiny window...
20//! });
21//! ```
22//!
23//! # Memory Characteristics
24//!
25//! - **Text layout cache** (`line_layout_cache_max_entries`): Controls the
26//! worst-case heap memory of the global line-layout cache. The cache grows on
27//! demand; this value only caps how large it can get before eviction kicks in.
28//! The cache stores both unwrapped and wrapped line layouts, each with its own
29//! entry limit. At ~1.2 KB per entry, 10 000 entries in one cache can occupy
30//! up to ~12 MB at peak.
31//!
32//! - **Atlas initial size** (`atlas_initial_size`): Controls the dimensions of
33//! GPU textures allocated for the glyph/image atlas. Each 1024×1024 BGRA
34//! texture costs 4 MB of GPU memory; a 512×512 texture costs 1 MB. These are
35//! **GPU** resources, not regular heap allocations.
36//!
37//! - **Instance buffer initial size** (`instance_buffer_initial_size`):
38//! Controls the initial capacity of renderer instance buffers where supported.
39//! Renderers may grow beyond this value automatically when a scene requires it.
40//!
41//! - **Instance buffer max size** (`instance_buffer_max_size`):
42//! Caps how far a renderer may grow the instance buffer for a single frame.
43//! Batch upload binds each primitive array as one storage buffer, so this
44//! watermark keeps Minimal/Utility profiles from following Zed's 256 MiB
45//! desktop ceiling.
46
47use crate::{DevicePixels, Size};
48
49/// Predefined application profiles for common use cases.
50///
51/// Use these as a convenient starting point. For fine-grained control, use
52/// [`AppProfile::Custom`] with a manually constructed [`AppResourceProfile`].
53#[derive(Clone, Debug, Default)]
54pub enum AppProfile {
55 /// Full desktop application (IDE, editor, browser).
56 ///
57 /// Large caches, standard atlas size. This is the default if no profile is
58 /// specified.
59 #[default]
60 Desktop,
61
62 /// Lightweight utility window (settings panel, dialog box).
63 ///
64 /// Moderate cache sizes, standard atlas.
65 Utility,
66
67 /// Minimal resource usage (tray icon, status bar, notification popup).
68 ///
69 /// Small caches, smaller atlas textures. Optimized for long-running
70 /// processes with minimal UI.
71 Minimal,
72
73 /// Fully custom resource budget.
74 Custom(AppResourceProfile),
75}
76
77impl AppProfile {
78 /// Convert this profile into a concrete [`AppResourceProfile`].
79 pub fn to_resource_profile(&self) -> AppResourceProfile {
80 match self {
81 AppProfile::Desktop => AppResourceProfile::desktop(),
82 AppProfile::Utility => AppResourceProfile::utility(),
83 AppProfile::Minimal => AppResourceProfile::minimal(),
84 AppProfile::Custom(profile) => profile.clone(),
85 }
86 }
87}
88
89impl From<AppProfile> for AppResourceProfile {
90 fn from(profile: AppProfile) -> Self {
91 profile.to_resource_profile()
92 }
93}
94
95/// Fine-grained resource budget for a GPUI application.
96///
97/// All fields have sensible defaults corresponding to [`AppProfile::Desktop`].
98#[derive(Clone, Debug)]
99pub struct AppResourceProfile {
100 /// Text layout cache configuration.
101 pub text: TextResourceBudget,
102
103 /// GPU / Atlas resource configuration.
104 pub gpu: GpuResourceBudget,
105
106 /// Initial capacity (in bytes) of the per-thread element arena.
107 ///
108 /// The element arena is a bump allocator used during each frame's layout
109 /// and paint phases. It is cleared after every frame, so the initial size
110 /// only determines how much memory is pre-allocated to avoid mid-frame
111 /// reallocations.
112 ///
113 /// Default: 1 048 576 (1 MiB). For minimal applications, 256 KiB or
114 /// 512 KiB is recommended.
115 pub element_arena_size: usize,
116}
117
118impl AppResourceProfile {
119 /// Preset for full desktop applications.
120 pub fn desktop() -> Self {
121 Self {
122 text: TextResourceBudget {
123 line_layout_cache_max_entries: 10_000,
124 line_layout_cache_low_watermark: 5_000,
125 raster_bounds_cache_max_entries: None,
126 },
127 gpu: GpuResourceBudget {
128 atlas_initial_size: 1024,
129 instance_buffer_initial_size: 2 * 1024 * 1024,
130 instance_buffer_max_size: 256 * 1024 * 1024,
131 },
132 element_arena_size: 1024 * 1024,
133 }
134 }
135
136 /// Preset for utility/tool windows.
137 pub fn utility() -> Self {
138 Self {
139 text: TextResourceBudget {
140 line_layout_cache_max_entries: 3_000,
141 line_layout_cache_low_watermark: 2_000,
142 raster_bounds_cache_max_entries: Some(5_000),
143 },
144 gpu: GpuResourceBudget {
145 atlas_initial_size: 1024,
146 instance_buffer_initial_size: 1024 * 1024,
147 instance_buffer_max_size: 64 * 1024 * 1024,
148 },
149 element_arena_size: 512 * 1024,
150 }
151 }
152
153 /// Preset for minimal/tray applications.
154 pub fn minimal() -> Self {
155 Self {
156 text: TextResourceBudget {
157 line_layout_cache_max_entries: 500,
158 line_layout_cache_low_watermark: 400,
159 raster_bounds_cache_max_entries: Some(2_000),
160 },
161 gpu: GpuResourceBudget {
162 atlas_initial_size: 512,
163 instance_buffer_initial_size: 512 * 1024,
164 instance_buffer_max_size: 16 * 1024 * 1024,
165 },
166 element_arena_size: 256 * 1024,
167 }
168 }
169}
170
171impl Default for AppResourceProfile {
172 fn default() -> Self {
173 Self::desktop()
174 }
175}
176
177/// Text system resource budget.
178#[derive(Clone, Debug)]
179pub struct TextResourceBudget {
180 /// Maximum number of entries in the global line-layout cache.
181 ///
182 /// When the cache reaches this limit, older entries are evicted down to the
183 /// low watermark. Default: 10 000.
184 ///
185 /// Each entry occupies approximately 1.2 KB on average (varies with text
186 /// length and font complexity).
187 pub line_layout_cache_max_entries: usize,
188
189 /// Target number of entries to retain after eviction.
190 ///
191 /// When the cache exceeds `line_layout_cache_max_entries`, it evicts the
192 /// oldest entries until only `low_watermark` remain. A larger gap between
193 /// max and low watermark reduces eviction frequency but allows more peak
194 /// memory usage. Default: 5 000.
195 pub line_layout_cache_low_watermark: usize,
196
197 /// Maximum number of entries in the glyph raster-bounds cache.
198 ///
199 /// This cache maps `RenderGlyphParams → Bounds<DevicePixels>`. Each entry
200 /// is small (~120 bytes), but without a limit the cache can grow unbounded
201 /// when many distinct font/size/glyph combinations are used. When the
202 /// limit is reached the entire cache is cleared (a simple generational
203 /// strategy, since re-computing raster bounds is inexpensive compared to
204 /// full text shaping).
205 ///
206 /// Default: `None` (legacy behavior). Preset profiles may set a finite
207 /// limit.
208 pub raster_bounds_cache_max_entries: Option<usize>,
209}
210
211#[cfg_attr(
212 not(any(target_os = "linux", target_os = "freebsd", test)),
213 allow(dead_code)
214)]
215const MIN_INSTANCE_BUFFER_CAPACITY: u64 = 16;
216
217/// GPU resource budget.
218///
219/// `instance_buffer_max_size` is new in 0.9. Existing struct literals that listed
220/// only atlas and initial-buffer fields will not compile. Use
221/// [`Self::new`] or `..GpuResourceBudget::default()` to keep a two-field update
222/// path; presets already fill the ceiling.
223#[derive(Clone, Debug)]
224pub struct GpuResourceBudget {
225 /// Initial width and height (in device pixels) for newly created atlas
226 /// textures.
227 ///
228 /// Atlas textures are allocated lazily when glyphs or images are first
229 /// rasterized. This controls the **minimum** size of each texture. If a
230 /// single glyph or image is larger, the texture will be sized to fit.
231 ///
232 /// Default: 1024 (meaning 1024×1024). For minimal applications, 512 is
233 /// recommended.
234 ///
235 /// **Memory impact** (per texture):
236 /// - Monochrome (R8): `size × size × 1` bytes
237 /// - Polychrome (BGRA): `size × size × 4` bytes
238 pub atlas_initial_size: u32,
239
240 /// Initial renderer instance buffer capacity in bytes, where supported.
241 ///
242 /// Instance buffers hold per-frame draw data uploaded to the GPU. This is an
243 /// initial budget, not a hard cap: renderers may grow the buffer on demand
244 /// for complex scenes. Lower values reduce idle GPU memory for small apps,
245 /// while larger values avoid early reallocations for heavy UIs.
246 ///
247 /// Default: 2 MiB for desktop applications.
248 pub instance_buffer_initial_size: usize,
249
250 /// Maximum renderer instance buffer capacity in bytes, where supported.
251 ///
252 /// Batch upload sizes the GPU buffer to fit the whole scene at frame start.
253 /// This is the growth ceiling after device limits are applied. Desktop
254 /// matches Zed's 256 MiB native cap; Utility and Minimal stay lower so a
255 /// tray or dialog profile cannot balloon to a full-editor working set.
256 ///
257 /// Values below [`Self::instance_buffer_initial_size`] are raised to that
258 /// initial budget.
259 ///
260 /// Callers that only set atlas and initial size should use [`Self::new`]
261 /// instead of a struct literal.
262 pub instance_buffer_max_size: usize,
263}
264
265impl Default for GpuResourceBudget {
266 fn default() -> Self {
267 AppResourceProfile::desktop().gpu
268 }
269}
270
271impl GpuResourceBudget {
272 /// Build a budget from atlas and initial instance-buffer sizes.
273 ///
274 /// The growth ceiling defaults to Zed's 256 MiB native cap — the implicit
275 /// limit before this field existed — so two-argument construction stays
276 /// source-compatible. Presets still apply tighter Utility/Minimal caps.
277 pub fn new(atlas_initial_size: u32, instance_buffer_initial_size: usize) -> Self {
278 Self {
279 atlas_initial_size,
280 instance_buffer_initial_size,
281 instance_buffer_max_size: 256 * 1024 * 1024,
282 }
283 }
284
285 /// Returns the atlas initial size as a [`Size<DevicePixels>`] for renderer use.
286 pub fn atlas_size(&self) -> Size<DevicePixels> {
287 let s = self.atlas_initial_size as i32;
288 Size {
289 width: DevicePixels(s),
290 height: DevicePixels(s),
291 }
292 }
293
294 /// Returns a renderer-safe instance-buffer capacity for a requested budget.
295 ///
296 /// Renderers require at least 16 bytes for a dynamic buffer binding, while
297 /// a device may impose a smaller upper bound than the requested profile.
298 #[doc(hidden)]
299 pub fn normalize_instance_buffer_capacity(
300 instance_buffer_initial_size: usize,
301 max_buffer_size: u64,
302 ) -> u64 {
303 (instance_buffer_initial_size as u64)
304 .max(MIN_INSTANCE_BUFFER_CAPACITY)
305 .min(max_buffer_size)
306 }
307
308 /// Returns `(initial, max)` instance-buffer capacities after applying the
309 /// profile watermarks and a device/binding ceiling.
310 ///
311 /// `device_max_buffer_size` should already be the minimum of the device
312 /// `max_buffer_size`, `max_storage_buffer_binding_size`, and any renderer
313 /// hard cap. The profile max is never below the profile initial size.
314 #[doc(hidden)]
315 pub fn normalize_instance_buffer_limits(
316 instance_buffer_initial_size: usize,
317 instance_buffer_max_size: usize,
318 device_max_buffer_size: u64,
319 ) -> (u64, u64) {
320 let device_max = device_max_buffer_size.max(MIN_INSTANCE_BUFFER_CAPACITY);
321 let profile_max = (instance_buffer_max_size as u64)
322 .max(instance_buffer_initial_size as u64)
323 .max(MIN_INSTANCE_BUFFER_CAPACITY)
324 .min(device_max);
325 let initial =
326 Self::normalize_instance_buffer_capacity(instance_buffer_initial_size, profile_max);
327 (initial, profile_max)
328 }
329
330 /// Next instance-buffer capacity that can hold `required` bytes.
331 ///
332 /// Growth doubles the current profile-backed allocation and will jump to
333 /// the next power of two of `required` when a single frame exceeds that,
334 /// but never past `max_capacity`. Returns `None` when the scene cannot
335 /// fit the profile/device watermark.
336 #[doc(hidden)]
337 pub fn next_instance_buffer_capacity(
338 current: u64,
339 required: u64,
340 max_capacity: u64,
341 ) -> Option<u64> {
342 if required > max_capacity {
343 return None;
344 }
345 if current >= required {
346 return Some(current);
347 }
348 let grown = current
349 .saturating_mul(2)
350 .max(required.next_power_of_two())
351 .min(max_capacity);
352 (grown >= required).then_some(grown)
353 }
354
355 /// Bytes reserved for one storage-buffer binding of `data_len` bytes.
356 ///
357 /// wgpu rejects zero-sized bindings, so empty primitive arrays still
358 /// reserve [`MIN_INSTANCE_BUFFER_CAPACITY`] and then align.
359 #[doc(hidden)]
360 pub fn aligned_instance_allocation(data_len: u64, alignment: u64) -> u64 {
361 data_len
362 .max(MIN_INSTANCE_BUFFER_CAPACITY)
363 .next_multiple_of(alignment.max(1))
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::{AppResourceProfile, GpuResourceBudget};
370
371 #[test]
372 fn instance_buffer_capacity_uses_the_requested_budget_within_device_limits() {
373 assert_eq!(
374 GpuResourceBudget::normalize_instance_buffer_capacity(768 * 1024, 8 * 1024 * 1024),
375 768 * 1024
376 );
377 assert_eq!(
378 GpuResourceBudget::normalize_instance_buffer_capacity(0, 8 * 1024 * 1024),
379 16
380 );
381 assert_eq!(
382 GpuResourceBudget::normalize_instance_buffer_capacity(
383 16 * 1024 * 1024,
384 8 * 1024 * 1024,
385 ),
386 8 * 1024 * 1024
387 );
388 }
389
390 #[test]
391 fn instance_buffer_limits_keep_profile_watermarks_under_the_device_cap() {
392 let (initial, max) = GpuResourceBudget::normalize_instance_buffer_limits(
393 512 * 1024,
394 16 * 1024 * 1024,
395 8 * 1024 * 1024,
396 );
397 assert_eq!(initial, 512 * 1024);
398 assert_eq!(max, 8 * 1024 * 1024);
399 }
400
401 #[test]
402 fn instance_buffer_limits_raise_a_max_below_the_initial_budget() {
403 let (initial, max) = GpuResourceBudget::normalize_instance_buffer_limits(
404 2 * 1024 * 1024,
405 1024,
406 8 * 1024 * 1024,
407 );
408 assert_eq!(initial, 2 * 1024 * 1024);
409 assert_eq!(max, 2 * 1024 * 1024);
410 }
411
412 #[test]
413 fn instance_buffer_growth_starts_from_the_profile_allocation() {
414 assert_eq!(
415 GpuResourceBudget::next_instance_buffer_capacity(
416 512 * 1024,
417 600 * 1024,
418 16 * 1024 * 1024
419 ),
420 Some(1024 * 1024)
421 );
422 assert_eq!(
423 GpuResourceBudget::next_instance_buffer_capacity(
424 512 * 1024,
425 3 * 1024 * 1024,
426 16 * 1024 * 1024,
427 ),
428 Some(4 * 1024 * 1024)
429 );
430 assert_eq!(
431 GpuResourceBudget::next_instance_buffer_capacity(
432 512 * 1024,
433 32 * 1024 * 1024,
434 16 * 1024 * 1024,
435 ),
436 None
437 );
438 }
439
440 #[test]
441 fn preset_profiles_cap_batch_upload_below_zed_desktop_ceiling() {
442 let desktop = AppResourceProfile::desktop().gpu;
443 let utility = AppResourceProfile::utility().gpu;
444 let minimal = AppResourceProfile::minimal().gpu;
445 assert_eq!(desktop.instance_buffer_max_size, 256 * 1024 * 1024);
446 assert_eq!(utility.instance_buffer_max_size, 64 * 1024 * 1024);
447 assert_eq!(minimal.instance_buffer_max_size, 16 * 1024 * 1024);
448 assert!(minimal.instance_buffer_max_size < utility.instance_buffer_max_size);
449 assert!(utility.instance_buffer_max_size < desktop.instance_buffer_max_size);
450 assert!(desktop.instance_buffer_initial_size <= desktop.instance_buffer_max_size);
451 }
452
453 #[test]
454 fn new_and_default_keep_a_two_field_construction_path() {
455 let custom = GpuResourceBudget::new(768, 768 * 1024);
456 assert_eq!(custom.atlas_initial_size, 768);
457 assert_eq!(custom.instance_buffer_initial_size, 768 * 1024);
458 assert_eq!(custom.instance_buffer_max_size, 256 * 1024 * 1024);
459
460 let patched = GpuResourceBudget {
461 atlas_initial_size: 512,
462 instance_buffer_initial_size: 512 * 1024,
463 ..GpuResourceBudget::default()
464 };
465 assert_eq!(patched.instance_buffer_max_size, 256 * 1024 * 1024);
466 assert_eq!(
467 GpuResourceBudget::default().instance_buffer_initial_size,
468 AppResourceProfile::desktop()
469 .gpu
470 .instance_buffer_initial_size
471 );
472 }
473
474 #[test]
475 fn aligned_instance_allocation_reserves_the_binding_minimum() {
476 assert_eq!(GpuResourceBudget::aligned_instance_allocation(0, 256), 256);
477 assert_eq!(GpuResourceBudget::aligned_instance_allocation(16, 256), 256);
478 assert_eq!(
479 GpuResourceBudget::aligned_instance_allocation(300, 256),
480 512
481 );
482 }
483}