diff --git a/examples/issue-0029.sx b/examples/issue-0029.sx deleted file mode 100644 index c7a5d48..0000000 --- a/examples/issue-0029.sx +++ /dev/null @@ -1,47 +0,0 @@ -// issue-0029: Feature — add explicit destructors to the GPU protocol so -// resources can be freed without leaking. -// -// ── Proposed additions to library/modules/gpu/api.sx ────────────────────── -// -// destroy_shader :: (h: ShaderHandle); -// destroy_buffer :: (h: BufferHandle); -// destroy_texture :: (h: TextureHandle); -// -// ── Why ──────────────────────────────────────────────────────────────────── -// -// Today, library/modules/ui/glyph_cache.sx's `grow()` method recreates -// the atlas texture at a larger size but has no way to release the old -// one — see the comment in metal.sx that explicitly notes the leak. The -// GL path uses glDeleteTextures(1, @self.texture_id); the GPU protocol -// has no equivalent yet. -// -// ── Implementation notes ────────────────────────────────────────────────── -// -// Metal backend: send `release` to the MTLTexture / MTLBuffer / -// MTLRenderPipelineState (or call CFRelease, since these are -// CFTypeRef-compatible). Clear the corresponding slot in -// MetalGPU.textures / buffers / shaders to `null` / 0. -// -// GL backend (future): glDeleteTextures / glDeleteBuffers / glDeleteProgram. -// -// Handle lifecycle: after destroy, the slot in the backend List is freed. -// New allocations can take that slot or grow the list. Caller's handles -// remain valid until destroy. Don't aggressively re-use slots in MVP; -// keep handles append-only with a `null` marker for destroyed entries -// (matches the current shape). -// -// ── Touch points ────────────────────────────────────────────────────────── -// -// library/modules/gpu/api.sx — add 3 protocol method signatures -// library/modules/gpu/metal.sx — implement them (release + null -// the slot) -// library/modules/ui/glyph_cache.sx — call destroy_texture(old_handle) -// in grow() before creating the -// new atlas -// -// ── Syntax constraint ───────────────────────────────────────────────────── -// -// None — straight protocol-method addition. - -#import "modules/std.sx"; -main :: () -> s32 { 0; } diff --git a/library/modules/gpu/api.sx b/library/modules/gpu/api.sx index c25bb17..115d99a 100644 --- a/library/modules/gpu/api.sx +++ b/library/modules/gpu/api.sx @@ -38,6 +38,14 @@ GPU :: protocol { create_texture :: (w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle; update_texture_region :: (tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void); + // Release a GPU resource. Implementations release the backing object and + // null the slot so the handle becomes inert. Calling with handle 0 or + // an already-destroyed handle is a no-op. Handles are not re-used; the + // backing List entry stays at its index with a null sentinel. + destroy_shader :: (sh: ShaderHandle); + destroy_buffer :: (buf: BufferHandle); + destroy_texture :: (tex: TextureHandle); + set_shader :: (sh: ShaderHandle); set_vertex_buffer :: (buf: BufferHandle); set_texture :: (slot: u32, tex: TextureHandle); diff --git a/library/modules/gpu/metal.sx b/library/modules/gpu/metal.sx index 38e2296..8e9d1ea 100644 --- a/library/modules/gpu/metal.sx +++ b/library/modules/gpu/metal.sx @@ -166,6 +166,24 @@ impl GPU for MetalGPU { } } + destroy_shader :: (self: *MetalGPU, sh: ShaderHandle) { + inline if OS == .ios { + metal_destroy_shader_ios(self, sh); + } + } + + destroy_buffer :: (self: *MetalGPU, buf: BufferHandle) { + inline if OS == .ios { + metal_destroy_buffer_ios(self, buf); + } + } + + destroy_texture :: (self: *MetalGPU, tex: TextureHandle) { + inline if OS == .ios { + metal_destroy_texture_ios(self, tex); + } + } + // ── Per-draw state ─────────────────────────────────────────────────── // All operate on `self.encoder`, which is live only between begin_frame // and end_frame. Calling these outside that window is a silent no-op. @@ -557,6 +575,51 @@ metal_update_texture_region_ios :: (self: *MetalGPU, handle: u32, x: s32, y: s32 region, 0, pixels, bytes_per_row); } +// ── Destroy ────────────────────────────────────────────────────────────── +// `release` on a Metal object whose retain count reaches zero deallocates +// it. Our resource Lists hold the only strong reference (Metal returns +// retained-+1 objects from `new*` and our `append` doesn't retain), so a +// single `release` is correct. Slots are null'd so a subsequent lookup +// short-circuits; the List length doesn't shrink (would break later +// handle->index mapping). + +metal_destroy_shader_ios :: (self: *MetalGPU, handle: u32) { + inline if OS != .ios { return; } + if handle == 0 { return; } + h64 : s64 = xx handle; + if h64 > self.shaders.len { return; } + obj := self.shaders.items[handle - 1]; + if obj == null { return; } + msg : (*void, *void) -> void = xx objc_msgSend; + msg(obj, sel_registerName("release".ptr)); + self.shaders.items[handle - 1] = null; +} + +metal_destroy_buffer_ios :: (self: *MetalGPU, handle: u32) { + inline if OS != .ios { return; } + if handle == 0 { return; } + h64 : s64 = xx handle; + if h64 > self.buffers.len { return; } + obj := self.buffers.items[handle - 1]; + if obj == null { return; } + msg : (*void, *void) -> void = xx objc_msgSend; + msg(obj, sel_registerName("release".ptr)); + self.buffers.items[handle - 1] = null; +} + +metal_destroy_texture_ios :: (self: *MetalGPU, handle: u32) { + inline if OS != .ios { return; } + if handle == 0 { return; } + h64 : s64 = xx handle; + if h64 > self.textures.len { return; } + obj := self.textures.items[handle - 1].tex; + if obj == null { return; } + msg : (*void, *void) -> void = xx objc_msgSend; + msg(obj, sel_registerName("release".ptr)); + self.textures.items[handle - 1].tex = null; + self.textures.items[handle - 1].bytes_per_pixel = 0; +} + // ── Per-draw state ─────────────────────────────────────────────────────── metal_set_shader_ios :: (self: *MetalGPU, sh: u32) { diff --git a/library/modules/ui/glyph_cache.sx b/library/modules/ui/glyph_cache.sx index 37b7e5a..ef9bf8d 100755 --- a/library/modules/ui/glyph_cache.sx +++ b/library/modules/ui/glyph_cache.sx @@ -501,9 +501,7 @@ GlyphCache :: struct { // Recreate atlas at the new size. if self.gpu != null { - // No destroy_texture in the GPU protocol yet — old atlas - // leaks in the backend table until process exit. Atlas grow - // is rare so this is acceptable for now. + self.gpu.destroy_texture(self.texture_id); self.texture_id = self.gpu.create_texture(new_w, new_h, .r8, xx new_bitmap); } else { glDeleteTextures(1, @self.texture_id);