Skip to main content

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}