fix-deviant(renderer): don't over-allocate Metal buffers by sizeof(T)

Intentional deviation from Ghostty 1.3.1 (DEVIANT_FIXES.md row 34).
When a Metal buffer grows, Zig's renderer/metal/buffer.zig:89 and :146
compute the new size as twice the required *byte* count and then
multiply by @sizeOf(T) again, so large cell buffers allocate
sizeof(T) times more GPU memory than intended. The Rust Metal backend
(since bab49ee) allocates exactly twice the required bytes; this commit
factors the computation into metal_layout::grown_buffer_bytes, adds a
regression test and records the deviation.

Test Plan:
- cargo test -p ghostty-renderer
  grown_buffer_bytes_is_double_the_required_bytes_not_scaled_by_size_of_t
- just check-macos
This commit is contained in:
ddidderr committed 2026-10-12 16:23:54 +02:00
1 parent e536b603b2
commit 80e8502eaf
3 files changed
+28 -3

No files matched your search

+4 -3
View File
@@ -9,7 +9,7 @@ use objc2_metal::{MTLBuffer, MTLDevice as _, MTLResourceOptions};
use super::{Device, Error};
use crate::{
graphics_api::{ApiError, GpuBuffer},
metal_layout::{ResourceOptions, StorageMode},
metal_layout::{ResourceOptions, StorageMode, grown_buffer_bytes},
};
/// Options for initializing a buffer.
@@ -124,8 +124,9 @@ impl<T: Pod> Buffer<T> {
// require (the old buffer is released when it is replaced).
//
// NOTE: Zig multiplies this size by `@sizeOf(T)` a second time,
// over-allocating; we allocate exactly double.
let size = req_bytes * 2;
// over-allocating; we allocate exactly double
// (see `grown_buffer_bytes`).
let size = grown_buffer_bytes(req_bytes);
self.buffer = self
.opts
.device
@@ -478,6 +478,19 @@ pub const PIPELINE_DESCS: [PipelineDesc; 5] = [
},
];
/// Size in bytes of the replacement Metal buffer when `req_bytes` bytes no
/// longer fit (`metal/buffer.zig` `sync`/`syncFromArrayLists`: "enough to
/// hold double what we require").
///
/// Upstream bug fixed: Zig computes `size = req_bytes * 2` and then passes
/// `size * @sizeOf(T)` to `newBufferWithLength:options:`, multiplying by the
/// element size a second time (`sizeof(T)` times too much GPU memory). The
/// length is already in bytes, so this is exactly double.
#[must_use]
pub const fn grown_buffer_bytes(req_bytes: usize) -> usize {
req_bytes * 2
}
#[cfg(test)]
mod tests {
use core::mem::{offset_of, size_of};
@@ -485,6 +498,16 @@ mod tests {
use super::*;
use crate::shader_types::{Bools, PaddingExtend};
#[test]
fn grown_buffer_bytes_is_double_the_required_bytes_not_scaled_by_size_of_t() {
// 100 vertices of a 32-byte type: 3200 bytes required, 6400 allocated
// (Zig would ask Metal for 6400 * 32 bytes).
let req_bytes = 100 * size_of::<CellText>();
assert_eq!(grown_buffer_bytes(req_bytes), req_bytes * 2);
assert_eq!(grown_buffer_bytes(0), 0);
assert_eq!(grown_buffer_bytes(1), 2);
}
#[test]
fn resource_options_bits() {
// MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeShared