mem: List(T) mutations gain optional alloc: Allocator = context.allocator
The chess panel-text regression (text vanished after the first move on macOS) had a single root cause: GlyphCache's entries List, hash table, and shaped_buf grew through `context.allocator` — which during render is the per-frame arena. On the next arena reset the backing died, and subsequent glyph lookups read garbage / wrote into freshly-allocated view-tree memory. Fix is shaped as the user proposed: `List(T)`'s mutations take an optional trailing `alloc: Allocator = context.allocator` argument. No allocator stored on the container, no init ceremony, every existing `list.append(item)` callsite keeps working unchanged. Long-lived owners now write `list.append(item, self.parent_allocator)` and the arena-leak bug becomes impossible to write accidentally. Default-arg substitution previously only fired for identifier callees (`expandCallDefaults` at lower.zig:7978). Extended to the generic struct-method dispatch path (`list.append(...)` lands here) via a new `appendDefaultArgs` helper that lowers fd.params[i].default_expr in the caller's scope and appends to the lowered args slice. Long-lived owners updated to capture `parent_allocator: Allocator` at init and use it for every internal growth: - GlyphCache (the chess bug) — entries, shaped_buf, hash_keys, hash_vals, atlas bitmap. - DockInteraction — drops the existing `push Context` workaround in `ensure_capacity` for the explicit-arg form. - StateStore — entries list + per-entry data buffer. - Gles3Gpu, MetalGPU — shaders, buffers, textures (atlas-grow during render would otherwise leak resources into the frame arena). Also kept: an operator-precedence fix in pipeline.sx (`(self.frame_index & 1) == 0` instead of `self.frame_index & 1 == 0`, which parses as `self.frame_index & (1 == 0)` = always 0). That was a stealth single-arena-only bug that masked the GlyphCache one for a long time. Docs: - specs.md §11 documents `param: T = expr` default parameter values. The parser already supported it — formalised in the spec now. - current/CHECKPOINT-MEM.md logs the change. - CLAUDE.md REJECTED PATTERNS gains a "Long-lived containers growing through context.allocator" section with the `parent_allocator` capture template and the list of existing examples to mirror. 155/155 example tests pass — zero-diff against snapshots since every existing callsite still resolves to `context.allocator`.
This commit is contained in:
@@ -179,6 +179,11 @@ GlyphCache :: struct {
|
||||
last_shape_len: s64;
|
||||
last_shape_size_q: u16;
|
||||
|
||||
// Allocator that owns every dynamically-grown buffer on this cache —
|
||||
// entries list, hash table, shaped_buf. Captured at init time so growth
|
||||
// never accidentally lands in a transient per-frame arena.
|
||||
parent_allocator: Allocator;
|
||||
|
||||
// GPU protocol backend. When set, atlas creation + dirty uploads route
|
||||
// through `gpu` instead of raw GL.
|
||||
gpu: ?GPU = null;
|
||||
@@ -190,6 +195,7 @@ GlyphCache :: struct {
|
||||
// Zero out the entire struct first (parent may be uninitialized with = ---)
|
||||
memset(self, 0, size_of(GlyphCache));
|
||||
self.gpu = saved_gpu;
|
||||
self.parent_allocator = context.allocator;
|
||||
|
||||
// Load font file
|
||||
file_size : s32 = 0;
|
||||
@@ -204,7 +210,7 @@ GlyphCache :: struct {
|
||||
self.font_data_size = file_size;
|
||||
|
||||
// Init stbtt_fontinfo
|
||||
self.font_info = context.allocator.alloc(FONTINFO_SIZE);
|
||||
self.font_info = self.parent_allocator.alloc(FONTINFO_SIZE);
|
||||
memset(self.font_info, 0, FONTINFO_SIZE);
|
||||
stbtt_InitFont(self.font_info, font_data, 0);
|
||||
|
||||
@@ -235,7 +241,7 @@ GlyphCache :: struct {
|
||||
self.atlas_width = GLYPH_ATLAS_W;
|
||||
self.atlas_height = GLYPH_ATLAS_H;
|
||||
bitmap_size : s64 = xx self.atlas_width * xx self.atlas_height;
|
||||
self.bitmap = xx context.allocator.alloc(bitmap_size);
|
||||
self.bitmap = xx self.parent_allocator.alloc(bitmap_size);
|
||||
memset(self.bitmap, 0, bitmap_size);
|
||||
|
||||
// Shelf packer init
|
||||
@@ -251,10 +257,10 @@ GlyphCache :: struct {
|
||||
// Init hash table (256 slots)
|
||||
self.hash_cap = 256;
|
||||
hash_bytes : s64 = self.hash_cap * 4; // u32 per slot
|
||||
self.hash_keys = xx context.allocator.alloc(hash_bytes);
|
||||
self.hash_keys = xx self.parent_allocator.alloc(hash_bytes);
|
||||
memset(self.hash_keys, 0, hash_bytes);
|
||||
val_bytes : s64 = self.hash_cap * 8; // s64 per slot (s32 would suffice but alignment)
|
||||
self.hash_vals = xx context.allocator.alloc(val_bytes);
|
||||
self.hash_vals = xx self.parent_allocator.alloc(val_bytes);
|
||||
|
||||
// Create the atlas texture. In GPU-protocol mode we create empty and
|
||||
// let the first `flush()` push the (zero-initialized) bitmap via
|
||||
@@ -328,7 +334,7 @@ GlyphCache :: struct {
|
||||
advance = advance
|
||||
}
|
||||
};
|
||||
self.entries.append(entry);
|
||||
self.entries.append(entry, self.parent_allocator);
|
||||
self.hash_insert(key, self.entries.len - 1);
|
||||
return @self.entries.items[self.entries.len - 1].glyph;
|
||||
}
|
||||
@@ -371,7 +377,7 @@ GlyphCache :: struct {
|
||||
advance = advance
|
||||
}
|
||||
};
|
||||
self.entries.append(entry);
|
||||
self.entries.append(entry, self.parent_allocator);
|
||||
self.hash_insert(key, self.entries.len - 1);
|
||||
return @self.entries.items[self.entries.len - 1].glyph;
|
||||
}
|
||||
@@ -399,10 +405,10 @@ GlyphCache :: struct {
|
||||
|
||||
self.hash_cap = old_cap * 2;
|
||||
hash_bytes : s64 = self.hash_cap * 4;
|
||||
self.hash_keys = xx context.allocator.alloc(hash_bytes);
|
||||
self.hash_keys = xx self.parent_allocator.alloc(hash_bytes);
|
||||
memset(self.hash_keys, 0, hash_bytes);
|
||||
val_bytes : s64 = self.hash_cap * 8;
|
||||
self.hash_vals = xx context.allocator.alloc(val_bytes);
|
||||
self.hash_vals = xx self.parent_allocator.alloc(val_bytes);
|
||||
|
||||
// Rehash
|
||||
mask := self.hash_cap - 1;
|
||||
@@ -420,8 +426,8 @@ GlyphCache :: struct {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
context.allocator.dealloc(old_keys);
|
||||
context.allocator.dealloc(old_vals);
|
||||
self.parent_allocator.dealloc(old_keys);
|
||||
self.parent_allocator.dealloc(old_vals);
|
||||
}
|
||||
|
||||
// Upload dirty atlas to GPU. On the Metal path, defer the upload to
|
||||
@@ -482,7 +488,7 @@ GlyphCache :: struct {
|
||||
new_w := self.atlas_width * 2;
|
||||
new_h := self.atlas_height * 2;
|
||||
new_size : s64 = xx new_w * xx new_h;
|
||||
new_bitmap : [*]u8 = xx context.allocator.alloc(new_size);
|
||||
new_bitmap : [*]u8 = xx self.parent_allocator.alloc(new_size);
|
||||
memset(new_bitmap, 0, new_size);
|
||||
|
||||
// Copy old rows into new bitmap
|
||||
@@ -494,7 +500,7 @@ GlyphCache :: struct {
|
||||
y += 1;
|
||||
}
|
||||
|
||||
context.allocator.dealloc(self.bitmap);
|
||||
self.parent_allocator.dealloc(self.bitmap);
|
||||
self.bitmap = new_bitmap;
|
||||
self.atlas_width = new_w;
|
||||
self.atlas_height = new_h;
|
||||
@@ -599,7 +605,7 @@ GlyphCache :: struct {
|
||||
x = total,
|
||||
y = 0.0,
|
||||
advance = adv
|
||||
});
|
||||
}, self.parent_allocator);
|
||||
total += adv;
|
||||
i += 1;
|
||||
}
|
||||
@@ -632,7 +638,7 @@ GlyphCache :: struct {
|
||||
x = gx,
|
||||
y = gy,
|
||||
advance = adv
|
||||
});
|
||||
}, self.parent_allocator);
|
||||
total += adv;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user