fc-gpui-core 0.11.1

GPU-accelerated UI framework with system tray, global hotkeys, notifications, and daemon mode support (fork of Zed's GPUI)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
//! Resource budget configuration for GPUI applications.
//!
//! Different application types have vastly different resource needs. A full IDE
//! benefits from large caches and atlas textures, while a tray icon or a small
//! popover panel can run with significantly less memory.
//!
//! [`AppResourceProfile`] allows callers to tune internal cache sizes and GPU
//! resource allocation at application startup via
//! [`Application::with_resource_profile`](crate::Application::with_resource_profile).
//!
//! # Quick Start
//!
//! ```rust,ignore
//! use gpui::AppProfile;
//!
//! gpui::Application::new()
//!     .with_resource_profile(AppProfile::Minimal)
//!     .run(|cx| {
//!         // open your tiny window...
//!     });
//! ```
//!
//! # Memory Characteristics
//!
//! - **Text layout cache** (`line_layout_cache_max_entries`): Controls the
//!   worst-case heap memory of the global line-layout cache. The cache grows on
//!   demand; this value only caps how large it can get before eviction kicks in.
//!   The cache stores both unwrapped and wrapped line layouts, each with its own
//!   entry limit. At ~1.2 KB per entry, 10 000 entries in one cache can occupy
//!   up to ~12 MB at peak.
//!
//! - **Atlas initial size** (`atlas_initial_size`): Controls the dimensions of
//!   GPU textures allocated for the glyph/image atlas. Each 1024×1024 BGRA
//!   texture costs 4 MB of GPU memory; a 512×512 texture costs 1 MB. These are
//!   **GPU** resources, not regular heap allocations.
//!
//! - **Instance buffer initial size** (`instance_buffer_initial_size`):
//!   Controls the initial capacity of renderer instance buffers where supported.
//!   Renderers may grow beyond this value automatically when a scene requires it.
//!
//! - **Instance buffer max size** (`instance_buffer_max_size`):
//!   Caps how far a renderer may grow the instance buffer for a single frame.
//!   Batch upload binds each primitive array as one storage buffer, so this
//!   watermark keeps Minimal/Utility profiles from following Zed's 256 MiB
//!   desktop ceiling.

use crate::{DevicePixels, Size};

/// Predefined application profiles for common use cases.
///
/// Use these as a convenient starting point. For fine-grained control, use
/// [`AppProfile::Custom`] with a manually constructed [`AppResourceProfile`].
#[derive(Clone, Debug, Default)]
pub enum AppProfile {
    /// Full desktop application (IDE, editor, browser).
    ///
    /// Large caches, standard atlas size. This is the default if no profile is
    /// specified.
    #[default]
    Desktop,

    /// Lightweight utility window (settings panel, dialog box).
    ///
    /// Moderate cache sizes, standard atlas.
    Utility,

    /// Minimal resource usage (tray icon, status bar, notification popup).
    ///
    /// Small caches, smaller atlas textures. Optimized for long-running
    /// processes with minimal UI.
    Minimal,

    /// Fully custom resource budget.
    Custom(AppResourceProfile),
}

impl AppProfile {
    /// Convert this profile into a concrete [`AppResourceProfile`].
    pub fn to_resource_profile(&self) -> AppResourceProfile {
        match self {
            AppProfile::Desktop => AppResourceProfile::desktop(),
            AppProfile::Utility => AppResourceProfile::utility(),
            AppProfile::Minimal => AppResourceProfile::minimal(),
            AppProfile::Custom(profile) => profile.clone(),
        }
    }
}

impl From<AppProfile> for AppResourceProfile {
    fn from(profile: AppProfile) -> Self {
        profile.to_resource_profile()
    }
}

/// Fine-grained resource budget for a GPUI application.
///
/// All fields have sensible defaults corresponding to [`AppProfile::Desktop`].
#[derive(Clone, Debug)]
pub struct AppResourceProfile {
    /// Text layout cache configuration.
    pub text: TextResourceBudget,

    /// GPU / Atlas resource configuration.
    pub gpu: GpuResourceBudget,

    /// Initial capacity (in bytes) of the per-thread element arena.
    ///
    /// The element arena is a bump allocator used during each frame's layout
    /// and paint phases. It is cleared after every frame, so the initial size
    /// only determines how much memory is pre-allocated to avoid mid-frame
    /// reallocations.
    ///
    /// Default: 1 048 576 (1 MiB). For minimal applications, 256 KiB or
    /// 512 KiB is recommended.
    pub element_arena_size: usize,
}

impl AppResourceProfile {
    /// Preset for full desktop applications.
    pub fn desktop() -> Self {
        Self {
            text: TextResourceBudget {
                line_layout_cache_max_entries: 10_000,
                line_layout_cache_low_watermark: 5_000,
                raster_bounds_cache_max_entries: None,
            },
            gpu: GpuResourceBudget {
                atlas_initial_size: 1024,
                instance_buffer_initial_size: 2 * 1024 * 1024,
                instance_buffer_max_size: 256 * 1024 * 1024,
            },
            element_arena_size: 1024 * 1024,
        }
    }

    /// Preset for utility/tool windows.
    pub fn utility() -> Self {
        Self {
            text: TextResourceBudget {
                line_layout_cache_max_entries: 3_000,
                line_layout_cache_low_watermark: 2_000,
                raster_bounds_cache_max_entries: Some(5_000),
            },
            gpu: GpuResourceBudget {
                atlas_initial_size: 1024,
                instance_buffer_initial_size: 1024 * 1024,
                instance_buffer_max_size: 64 * 1024 * 1024,
            },
            element_arena_size: 512 * 1024,
        }
    }

    /// Preset for minimal/tray applications.
    pub fn minimal() -> Self {
        Self {
            text: TextResourceBudget {
                line_layout_cache_max_entries: 500,
                line_layout_cache_low_watermark: 400,
                raster_bounds_cache_max_entries: Some(2_000),
            },
            gpu: GpuResourceBudget {
                atlas_initial_size: 512,
                instance_buffer_initial_size: 512 * 1024,
                instance_buffer_max_size: 16 * 1024 * 1024,
            },
            element_arena_size: 256 * 1024,
        }
    }
}

impl Default for AppResourceProfile {
    fn default() -> Self {
        Self::desktop()
    }
}

/// Text system resource budget.
#[derive(Clone, Debug)]
pub struct TextResourceBudget {
    /// Maximum number of entries in the global line-layout cache.
    ///
    /// When the cache reaches this limit, older entries are evicted down to the
    /// low watermark. Default: 10 000.
    ///
    /// Each entry occupies approximately 1.2 KB on average (varies with text
    /// length and font complexity).
    pub line_layout_cache_max_entries: usize,

    /// Target number of entries to retain after eviction.
    ///
    /// When the cache exceeds `line_layout_cache_max_entries`, it evicts the
    /// oldest entries until only `low_watermark` remain. A larger gap between
    /// max and low watermark reduces eviction frequency but allows more peak
    /// memory usage. Default: 5 000.
    pub line_layout_cache_low_watermark: usize,

    /// Maximum number of entries in the glyph raster-bounds cache.
    ///
    /// This cache maps `RenderGlyphParams → Bounds<DevicePixels>`. Each entry
    /// is small (~120 bytes), but without a limit the cache can grow unbounded
    /// when many distinct font/size/glyph combinations are used. When the
    /// limit is reached the entire cache is cleared (a simple generational
    /// strategy, since re-computing raster bounds is inexpensive compared to
    /// full text shaping).
    ///
    /// Default: `None` (legacy behavior). Preset profiles may set a finite
    /// limit.
    pub raster_bounds_cache_max_entries: Option<usize>,
}

#[cfg_attr(
    not(any(target_os = "linux", target_os = "freebsd", test)),
    allow(dead_code)
)]
const MIN_INSTANCE_BUFFER_CAPACITY: u64 = 16;

/// GPU resource budget.
///
/// `instance_buffer_max_size` is new in 0.9. Existing struct literals that listed
/// only atlas and initial-buffer fields will not compile. Use
/// [`Self::new`] or `..GpuResourceBudget::default()` to keep a two-field update
/// path; presets already fill the ceiling.
#[derive(Clone, Debug)]
pub struct GpuResourceBudget {
    /// Initial width and height (in device pixels) for newly created atlas
    /// textures.
    ///
    /// Atlas textures are allocated lazily when glyphs or images are first
    /// rasterized. This controls the **minimum** size of each texture. If a
    /// single glyph or image is larger, the texture will be sized to fit.
    ///
    /// Default: 1024 (meaning 1024×1024). For minimal applications, 512 is
    /// recommended.
    ///
    /// **Memory impact** (per texture):
    /// - Monochrome (R8): `size × size × 1` bytes
    /// - Polychrome (BGRA): `size × size × 4` bytes
    pub atlas_initial_size: u32,

    /// Initial renderer instance buffer capacity in bytes, where supported.
    ///
    /// Instance buffers hold per-frame draw data uploaded to the GPU. This is an
    /// initial budget, not a hard cap: renderers may grow the buffer on demand
    /// for complex scenes. Lower values reduce idle GPU memory for small apps,
    /// while larger values avoid early reallocations for heavy UIs.
    ///
    /// Default: 2 MiB for desktop applications.
    pub instance_buffer_initial_size: usize,

    /// Maximum renderer instance buffer capacity in bytes, where supported.
    ///
    /// Batch upload sizes the GPU buffer to fit the whole scene at frame start.
    /// This is the growth ceiling after device limits are applied. Desktop
    /// matches Zed's 256 MiB native cap; Utility and Minimal stay lower so a
    /// tray or dialog profile cannot balloon to a full-editor working set.
    ///
    /// Values below [`Self::instance_buffer_initial_size`] are raised to that
    /// initial budget.
    ///
    /// Callers that only set atlas and initial size should use [`Self::new`]
    /// instead of a struct literal.
    pub instance_buffer_max_size: usize,
}

impl Default for GpuResourceBudget {
    fn default() -> Self {
        AppResourceProfile::desktop().gpu
    }
}

impl GpuResourceBudget {
    /// Build a budget from atlas and initial instance-buffer sizes.
    ///
    /// The growth ceiling defaults to Zed's 256 MiB native cap — the implicit
    /// limit before this field existed — so two-argument construction stays
    /// source-compatible. Presets still apply tighter Utility/Minimal caps.
    pub fn new(atlas_initial_size: u32, instance_buffer_initial_size: usize) -> Self {
        Self {
            atlas_initial_size,
            instance_buffer_initial_size,
            instance_buffer_max_size: 256 * 1024 * 1024,
        }
    }

    /// Returns the atlas initial size as a [`Size<DevicePixels>`] for renderer use.
    pub fn atlas_size(&self) -> Size<DevicePixels> {
        let s = self.atlas_initial_size as i32;
        Size {
            width: DevicePixels(s),
            height: DevicePixels(s),
        }
    }

    /// Returns a renderer-safe instance-buffer capacity for a requested budget.
    ///
    /// Renderers require at least 16 bytes for a dynamic buffer binding, while
    /// a device may impose a smaller upper bound than the requested profile.
    #[doc(hidden)]
    pub fn normalize_instance_buffer_capacity(
        instance_buffer_initial_size: usize,
        max_buffer_size: u64,
    ) -> u64 {
        (instance_buffer_initial_size as u64)
            .max(MIN_INSTANCE_BUFFER_CAPACITY)
            .min(max_buffer_size)
    }

    /// Returns `(initial, max)` instance-buffer capacities after applying the
    /// profile watermarks and a device/binding ceiling.
    ///
    /// `device_max_buffer_size` should already be the minimum of the device
    /// `max_buffer_size`, `max_storage_buffer_binding_size`, and any renderer
    /// hard cap. The profile max is never below the profile initial size.
    #[doc(hidden)]
    pub fn normalize_instance_buffer_limits(
        instance_buffer_initial_size: usize,
        instance_buffer_max_size: usize,
        device_max_buffer_size: u64,
    ) -> (u64, u64) {
        let device_max = device_max_buffer_size.max(MIN_INSTANCE_BUFFER_CAPACITY);
        let profile_max = (instance_buffer_max_size as u64)
            .max(instance_buffer_initial_size as u64)
            .max(MIN_INSTANCE_BUFFER_CAPACITY)
            .min(device_max);
        let initial =
            Self::normalize_instance_buffer_capacity(instance_buffer_initial_size, profile_max);
        (initial, profile_max)
    }

    /// Next instance-buffer capacity that can hold `required` bytes.
    ///
    /// Growth doubles the current profile-backed allocation and will jump to
    /// the next power of two of `required` when a single frame exceeds that,
    /// but never past `max_capacity`. Returns `None` when the scene cannot
    /// fit the profile/device watermark.
    #[doc(hidden)]
    pub fn next_instance_buffer_capacity(
        current: u64,
        required: u64,
        max_capacity: u64,
    ) -> Option<u64> {
        if required > max_capacity {
            return None;
        }
        if current >= required {
            return Some(current);
        }
        let grown = current
            .saturating_mul(2)
            .max(required.next_power_of_two())
            .min(max_capacity);
        (grown >= required).then_some(grown)
    }

    /// Bytes reserved for one storage-buffer binding of `data_len` bytes.
    ///
    /// wgpu rejects zero-sized bindings, so empty primitive arrays still
    /// reserve [`MIN_INSTANCE_BUFFER_CAPACITY`] and then align.
    #[doc(hidden)]
    pub fn aligned_instance_allocation(data_len: u64, alignment: u64) -> u64 {
        data_len
            .max(MIN_INSTANCE_BUFFER_CAPACITY)
            .next_multiple_of(alignment.max(1))
    }
}

#[cfg(test)]
mod tests {
    use super::{AppResourceProfile, GpuResourceBudget};

    #[test]
    fn instance_buffer_capacity_uses_the_requested_budget_within_device_limits() {
        assert_eq!(
            GpuResourceBudget::normalize_instance_buffer_capacity(768 * 1024, 8 * 1024 * 1024),
            768 * 1024
        );
        assert_eq!(
            GpuResourceBudget::normalize_instance_buffer_capacity(0, 8 * 1024 * 1024),
            16
        );
        assert_eq!(
            GpuResourceBudget::normalize_instance_buffer_capacity(
                16 * 1024 * 1024,
                8 * 1024 * 1024,
            ),
            8 * 1024 * 1024
        );
    }

    #[test]
    fn instance_buffer_limits_keep_profile_watermarks_under_the_device_cap() {
        let (initial, max) = GpuResourceBudget::normalize_instance_buffer_limits(
            512 * 1024,
            16 * 1024 * 1024,
            8 * 1024 * 1024,
        );
        assert_eq!(initial, 512 * 1024);
        assert_eq!(max, 8 * 1024 * 1024);
    }

    #[test]
    fn instance_buffer_limits_raise_a_max_below_the_initial_budget() {
        let (initial, max) = GpuResourceBudget::normalize_instance_buffer_limits(
            2 * 1024 * 1024,
            1024,
            8 * 1024 * 1024,
        );
        assert_eq!(initial, 2 * 1024 * 1024);
        assert_eq!(max, 2 * 1024 * 1024);
    }

    #[test]
    fn instance_buffer_growth_starts_from_the_profile_allocation() {
        assert_eq!(
            GpuResourceBudget::next_instance_buffer_capacity(
                512 * 1024,
                600 * 1024,
                16 * 1024 * 1024
            ),
            Some(1024 * 1024)
        );
        assert_eq!(
            GpuResourceBudget::next_instance_buffer_capacity(
                512 * 1024,
                3 * 1024 * 1024,
                16 * 1024 * 1024,
            ),
            Some(4 * 1024 * 1024)
        );
        assert_eq!(
            GpuResourceBudget::next_instance_buffer_capacity(
                512 * 1024,
                32 * 1024 * 1024,
                16 * 1024 * 1024,
            ),
            None
        );
    }

    #[test]
    fn preset_profiles_cap_batch_upload_below_zed_desktop_ceiling() {
        let desktop = AppResourceProfile::desktop().gpu;
        let utility = AppResourceProfile::utility().gpu;
        let minimal = AppResourceProfile::minimal().gpu;
        assert_eq!(desktop.instance_buffer_max_size, 256 * 1024 * 1024);
        assert_eq!(utility.instance_buffer_max_size, 64 * 1024 * 1024);
        assert_eq!(minimal.instance_buffer_max_size, 16 * 1024 * 1024);
        assert!(minimal.instance_buffer_max_size < utility.instance_buffer_max_size);
        assert!(utility.instance_buffer_max_size < desktop.instance_buffer_max_size);
        assert!(desktop.instance_buffer_initial_size <= desktop.instance_buffer_max_size);
    }

    #[test]
    fn new_and_default_keep_a_two_field_construction_path() {
        let custom = GpuResourceBudget::new(768, 768 * 1024);
        assert_eq!(custom.atlas_initial_size, 768);
        assert_eq!(custom.instance_buffer_initial_size, 768 * 1024);
        assert_eq!(custom.instance_buffer_max_size, 256 * 1024 * 1024);

        let patched = GpuResourceBudget {
            atlas_initial_size: 512,
            instance_buffer_initial_size: 512 * 1024,
            ..GpuResourceBudget::default()
        };
        assert_eq!(patched.instance_buffer_max_size, 256 * 1024 * 1024);
        assert_eq!(
            GpuResourceBudget::default().instance_buffer_initial_size,
            AppResourceProfile::desktop()
                .gpu
                .instance_buffer_initial_size
        );
    }

    #[test]
    fn aligned_instance_allocation_reserves_the_binding_minimum() {
        assert_eq!(GpuResourceBudget::aligned_instance_allocation(0, 256), 256);
        assert_eq!(GpuResourceBudget::aligned_instance_allocation(16, 256), 256);
        assert_eq!(
            GpuResourceBudget::aligned_instance_allocation(300, 256),
            512
        );
    }
}