From a4f5e5b8fc5aed894f13ffe2b96cbd9c5a11770d Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 24 Sep 2026 13:09:24 -0300 Subject: Allocate SDL's memory from the gui's Zig allocator, and give it all back on the way out SDL allocated with libc malloc. runNative now installs the shared C heap over the gui's gpa with SDL_SetMemoryFunctions before SDL's first call: SDL frees with whichever functions are current, so none of its memory may predate them. SDL allocates from its own threads too, and the gpa is thread-safe in every build (a DebugAllocator in Debug, libc's malloc in release). That put SDL in a Debug build's leak report, and the report filled up: 1769 blocks at exit, because runNative never released its GPU objects, its window or SDL itself. The process was about to exit, but a report that long hides any leak that matters. runNative now releases every pipeline, texture, sampler and buffer the Gui holds, closes the gamepad, destroys the GPU device and the window, and calls SDL_Quit, all through defers. Eight 32-byte blocks are left, and they are SDL's: 3.4.4's VULKAN_INTERNAL_DestroyCommandPool never frees two arrays per command buffer (buffersUsedInPendingTransfers and texturesUsedInPendingTransfers), which SDL's main branch now frees. They go away with the next SDL; a comment at the SDL_Quit says so. The frame-cost harness and a software-present run (PARDES_SOFT_PRESENT) both run and exit cleanly in Debug. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/gui/gui.zig | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 73f17a22..9d785b96 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -180,6 +180,12 @@ test "FreeType and HarfBuzz allocate from the gui allocator and give it all back try std.testing.expectEqual(@as(u8, 2), shaped[4].span); } +/// What SDL allocates from, on its own threads as well as the render thread: +/// the gui's gpa, through the shared C heap, which is thread-safe in every +/// build. runNative hands everything back and calls SDL_Quit on the way out. +var sdl_allocator: std.mem.Allocator = undefined; +const sdl_heap = c_heap.Heap(&sdl_allocator); + const max_fallback_fonts = 1 + fonts.fallback_names.len; const LoadedFallback = struct { face: *c.UIFont, @@ -2152,12 +2158,26 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u else |_| {} }; + // SDL frees with whichever functions are current, so they go in before + // its first call, while nothing of SDL's exists yet. + sdl_allocator = gpa; + if (!c.SDL_SetMemoryFunctions(sdl_heap.malloc, sdl_heap.calloc, sdl_heap.realloc, sdl_heap.free)) { + log.err("SDL_SetMemoryFunctions: {s}", .{c.SDL_GetError()}); + return error.SdlInit; + } _ = c.SDL_SetHint("SDL_JOYSTICK_HIDAPI", "1"); // SDL_HINT_JOYSTICK_HIDAPI _ = c.SDL_SetHint("SDL_JOYSTICK_HIDAPI_STEAMDECK", "1"); // ..._STEAMDECK if (!c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_GAMEPAD)) { log.err("SDL_Init: {s}", .{c.SDL_GetError()}); return error.SdlInit; } + // SDL allocates from gpa (above), so what it still holds at exit is in a + // Debug build's leak report. The window, the GPU device and everything + // made on it go back first (the defers below run before this one). What + // is left is SDL 3.4.4's: its Vulkan backend never frees two 32-byte + // arrays per command buffer (VULKAN_INTERNAL_DestroyCommandPool; SDL's + // main branch frees them). + defer c.SDL_Quit(); var win_flags: c.SDL_WindowFlags = c.SDL_WINDOW_RESIZABLE; if (!test_mode) win_flags |= c.SDL_WINDOW_HIGH_PIXEL_DENSITY; // A test run renders to the capture texture; keep it off the desktop. @@ -2171,6 +2191,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u log.err("SDL_CreateWindow: {s}", .{c.SDL_GetError()}); return error.SdlInit; }; + defer c.SDL_DestroyWindow(window); if (native_wayland) log.info("Wayland opacity: background-only transparent surface", .{}); const arrow_cursor = c.SDL_CreateCursor(&p9_arrow_set, &p9_arrow_mask, 16, 16, 1, 1); defer if (arrow_cursor) |cursor| c.SDL_DestroyCursor(cursor); @@ -2183,6 +2204,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u log.err("SDL_CreateGPUDevice: {s}", .{c.SDL_GetError()}); return error.SdlInit; }; + defer c.SDL_DestroyGPUDevice(device); var soft_present = false; var soft_renderer: ?*c.SDL_Renderer = null; const force_soft = if (env.get("PARDES_SOFT_PRESENT")) |raw| @@ -2346,6 +2368,25 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u .surface_opacity = surface_opacity, .soft_renderer = soft_renderer, }; + // The pipelines, textures and buffers the Gui holds, whatever they were + // regrown to; the image ones are released below. + defer { + c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline); + c.SDL_ReleaseGPUGraphicsPipeline(device, g.overlay_pipeline); + c.SDL_ReleaseGPUGraphicsPipeline(device, g.crt_pipeline); + c.SDL_ReleaseGPUTexture(device, g.atlas_tex); + c.SDL_ReleaseGPUSampler(device, g.atlas_sampler); + c.SDL_ReleaseGPUSampler(device, g.linear_sampler); + c.SDL_ReleaseGPUTransferBuffer(device, g.atlas_xfer); + c.SDL_ReleaseGPUBuffer(device, g.overlay_vbuf); + c.SDL_ReleaseGPUTransferBuffer(device, g.overlay_vxfer); + if (g.vbuf) |buffer| c.SDL_ReleaseGPUBuffer(device, buffer); + if (g.vxfer) |transfer| c.SDL_ReleaseGPUTransferBuffer(device, transfer); + if (g.scene_tex) |texture| c.SDL_ReleaseGPUTexture(device, texture); + if (g.capture_tex) |texture| c.SDL_ReleaseGPUTexture(device, texture); + if (g.capture_xfer) |transfer| c.SDL_ReleaseGPUTransferBuffer(device, transfer); + if (g.gamepad) |pad| c.SDL_CloseGamepad(pad); + } setGuiFontName(&g, "Adwaita Mono"); loadFallbackFonts(&g, gpa); defer deinitFallbackFonts(&g, gpa); -- cgit v1.3