SDL icon indicating copy to clipboard operation
SDL copied to clipboard

Memory Leak - SDL_CreateWindow

Open jarroddavis68 opened this issue 2 years ago • 4 comments

Hi, I am using SDL3 with Delphi. I started using SDL_SetMemoryFunctions so that all allocations flow through Delphi and can take advantage of built-in leak detection. Ok, so If I do something like this:

function SDLMallocFunc(size: NativeUInt): Pointer; cdecl;
begin
  GetMem(Result, size);
end;

function SDLCallocFunc(count, size: NativeUInt): Pointer; cdecl;
var
  totalSize: NativeUInt;
begin
  totalSize := count * size;
  GetMem(Result, totalSize);
  FillChar(Result^, totalSize, 0);
end;

function SDLReallocFunc(ptr: Pointer; size: NativeUInt): Pointer; cdecl;
begin
  ReallocMem(ptr, size);
  Result := ptr;
end;

procedure SDLFreeFunc(ptr: Pointer); cdecl;
begin
  FreeMem(ptr);
end;

var
  win: PSDL_Window;
begin
  ReportMemoryLeaksOnShutdown := True;
  SDL_SetMemoryFunctions(@SDLMallocFunc, @SDLCallocFunc, @SDLReallocFunc, @SDLFreeFunc);
  win := SDL_CreateWindow('test', 640, 480, 0);
  if Assigned(win) then
  begin
    SDL_DestroyWindow(win);
  end;
  SDL_Quit;
end;

On exit, I will get the following:

Unexpected Memory Leak
An unexpected memory leak has occurred. The unexpected small block leaks are:

217 - 232 bytes: Unknown x 1

I've been working on my project today, doing other SDL related things and memory management through those functions has been working (I set break points and I can see each being called). When I got to adding SDL_CreateWindow, I noticed the leak. Looked at the SDL_CreateWindow/SDL_DestroyWindow C code and on the surface, all seems ok (C/C++ is not my main language however, soooo yea).

Any ideas?

WinPro 11 v22H2 (latest build) 64bit Delphi 11.3 Patch 1 Target: win64

Thanks.

jarroddavis68 avatar Apr 11 '23 22:04 jarroddavis68

So, there seem to be 15 allocations after win := SDL_CreateWindow('test', 640, 480, 0); and immediately calling SDL_DestroyWindow(win) there are 4 calls to release memory, thus 11 allocations seem to be left dangling? I get these numbers by inc/dec a counter as they pass through the custom mem manager.

jarroddavis68 avatar Apr 12 '23 23:04 jarroddavis68

Opening and closing multiple windows using SDL_CreateWindow/SDL_DestroyWindow can cause memory leaks. However, when using SDL_CreateWindowFrom with a window created using the Win32 API, no leaks were observed. It is possible that all memory allocations are being processed through SDL, including the custom handler, and some deallocations are either missing or not being called through the SDL API, resulting in potential leaks. In cases where deallocations are handled by the C/C++ runtime and not the custom implementation, there may not be any memory leaks on the C/C++ side, and thus they may go unnoticed and unreported.

jarroddavis68 avatar Apr 20 '23 18:04 jarroddavis68

There are some allocations that are only cleaned up in SDL_Quit(), have you checked after calling that?

slouken avatar Apr 24 '23 23:04 slouken

There are some allocations that are only cleaned up in SDL_Quit(), have you checked after calling that?

The memory leak detection mechanism in Delphi reports memory leaks at the end of program execution, which would be after the SDL_Quit function call. To test this mechanism, I added code to track memory allocation in custom callbacks and released any remaining memory after SDL_Quit. This approach worked initially, but when SDL_CreateWindow/SDL_DestroyWindow were called multiple times, the problem became more severe. I then removed the tracking code and tried using SDL_CreateWindowFrom to create windows from a HWND created via CreateWindowEx. This resolved the issue.

There seems to be something going on that warrants an investigation. I use Delphi, so I'm not familiar with any leak detection for c/c++. But you could set up the custom callbacks and simply track the allocations to get an idea of the leaks on the C side?

jarroddavis68 avatar Apr 25 '23 00:04 jarroddavis68

Upon conducting a reevaluation of the existing issue, I can confirm that it persists. I have refined the test code to monitor memory allocations and deallocate any lingering resources. The system identifies two unresolved allocations: one consuming 88 bytes and the other 220 bytes.

var
  mem: TDictionary<Pointer, NativeUInt>;

function SDLMallocFunc(size: NativeUInt): Pointer; cdecl;
begin
  GetMem(Result, size);
  mem.Add(Result, size);
end;

function SDLCallocFunc(count, size: NativeUInt): Pointer; cdecl;
var
  totalSize: NativeUInt;
begin
  totalSize := count * size;
  GetMem(Result, totalSize);
  FillChar(Result^, totalSize, 0);
  mem.Add(Result, size);
end;

function SDLReallocFunc(ptr: Pointer; size: NativeUInt): Pointer; cdecl;
begin
  mem.Remove(ptr);
  ReallocMem(ptr, size);
  Result := ptr;
  mem.Add(ptr, size);
end;

procedure SDLFreeFunc(ptr: Pointer); cdecl;
begin
  FreeMem(ptr);
  mem.Remove(ptr);
end;

procedure initmem;
begin
  mem := TDictionary<Pointer, NativeUInt>.Create;
end;

procedure donemem;
var
  item: TPair<Pointer, NativeUint>;
begin
  writeln('Dangling allocations: ', mem.Count);

  for item in mem do
  begin
    FreeMem(item.Key);
    WriteLn('Size: ', item.Value, ' bytes');
  end;

  mem.Free;
end;

procedure Test01;
var
  win: PSDL_Window;
begin
  ReportMemoryLeaksOnShutdown := True;
  initmem;
  SDL_SetMemoryFunctions(@SDLMallocFunc, @SDLCallocFunc, @SDLReallocFunc, @SDLFreeFunc);
  SDL_Init(SDL_INIT_EVERYTHING);

  win := SDL_CreateWindow('test1', 640, 480, 0);

  SDL_Delay(1000);

  SDL_DestroyWindow(win);

  SDL_Quit;

  donemem;

end;

jarroddavis68 avatar Sep 19 '23 10:09 jarroddavis68

Are you able to get a stack trace or stop in the debugger when you get allocations of those sizes?

slouken avatar Nov 08 '23 04:11 slouken

Hi, sorry, just saw this message. image interestingly, today I just tried with the build I got from the repo on (12/15/2023), only get 1 dangling allocation. Just doing SDL_Init(SDL_INIT_EVERYTHING) and SDL_Quit(), leaves 88 bytes unallocated. There no longer seems to be any correlation with creating a window. Before, there would be leaks that would increase each time a window is created, this no longer seems to be the case. Which is fantastic. Just 88 bytes. It seems the 88 bytes were from a realloc operation as no other realloc was 88 bytes that I noticed during SDL_Init/SDL_Quit.

jarroddavis68 avatar Dec 16 '23 20:12 jarroddavis68

Great, can you get a call stack for those 88 bytes?

slouken avatar Dec 16 '23 22:12 slouken

Great, can you get a call stack for those 88 bytes?

I'm using Delphi, 64bit SDL DLL. All I have access to is the call stack memory offsets leading to the custom realloc function, in the above image.

jarroddavis68 avatar Dec 16 '23 22:12 jarroddavis68

Ok, found out that whenever one of these flags are added, you will get the 88 byte leak: SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD.

jarroddavis68 avatar Dec 17 '23 00:12 jarroddavis68

It looks like this is an SDL error buffer that's allocated from a WGI service thread. Unfortunately there isn't a good way to know when that thread is finished, so we can free the memory allocated there.

For future reference, this is the allocation callstack:

 	SDL3.dll!SDL_ClearError_REAL() Line 79	C
 	SDL3.dll!register_string_error_to_buffer(wchar_t * * error_buffer, const wchar_t * string_error) Line 356	C
 	SDL3.dll!register_global_error(const wchar_t * string_error) Line 382	C
 	SDL3.dll!PLATFORM_hid_init() Line 415	C
 	SDL3.dll!PLATFORM_hid_enumerate(unsigned short vendor_id, unsigned short product_id) Line 919	C
 	SDL3.dll!SDL_hid_enumerate_REAL(unsigned short vendor_id, unsigned short product_id) Line 1439	C
 	SDL3.dll!HIDAPI_UpdateDeviceList() Line 1103	C
 	SDL3.dll!HIDAPI_IsDevicePresent(unsigned short vendor_id, unsigned short product_id, unsigned short version, const char * name) Line 1254	C
 	SDL3.dll!IEventHandler_CRawGameControllerVtbl_InvokeAdded(__FIEventHandler_1_Windows__CGaming__CInput__CRawGameController * This, IInspectable * sender, __x_ABI_CWindows_CGaming_CInput_CIRawGameController * e) Line 423	C
 	Windows.Gaming.Input.dll!00007ffc243c4260()	Unknown
 	Windows.Gaming.Input.dll!00007ffc243c4a82()	Unknown
 	Windows.Gaming.Input.dll!00007ffc243c4d11()	Unknown
 	Windows.Gaming.Input.dll!00007ffc2436b215()	Unknown
 	SHCore.dll!WorkThreadManager::CThread::ThreadProc(void)	Unknown
 	SHCore.dll!WorkThreadManager::CThread::s_ExecuteThreadProc(void *)	Unknown
 	SHCore.dll!<lambda_9844335fc14345151eefcc3593dd6895>::<lambda_invoker_cdecl>(void *)	Unknown
 	kernel32.dll!BaseThreadInitThunk()	Unknown
 	ntdll.dll!RtlUserThreadStart()	Unknown

slouken avatar Dec 17 '23 05:12 slouken

Ok, understand. I d/l and recompiled with this fix, just so you know... those changes, results in 120 bytes leaking. It looks like (so far) it's just this area, so I will just let it be. My custom memory mapping cleans up any dangling memory, but it was bothering me to see it leaking. In fact, since it's a thread issue, maybe as a hack, you can do the same as I am doing, clean it up after SDL_Quit, plus have the added benefit of testing for future leaks. Or maybe it's not a hack and a reliable way to handle it in a situation like this?

jarroddavis68 avatar Dec 17 '23 06:12 jarroddavis68

I'm not sure how those changes would result in 120 bytes leaking. We're doing fewer allocations, not more... ?

slouken avatar Dec 17 '23 16:12 slouken

I'm not sure how those changes would result in 120 bytes leaking. We're doing fewer allocations, not more... ?

Yea, that is what I had assumed too. I just d/l the repo, rebuilt the DLL and when I run my same code above, it now shows 120 bytes on shutdown. I dunno. If you set up and track the allocations like I am doing, you can see what I'm seeing.

jarroddavis68 avatar Dec 17 '23 17:12 jarroddavis68

if you run this code, you can see the leak.

#include <iostream>
#include <SDL3/SDL.h>

#define DEBUG_CRT
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

static inline void DebugCrtInit(long break_alloc)
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_WNDW);
    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_WNDW);
    if (break_alloc != 0)
        _CrtSetBreakAlloc(break_alloc);
}

static inline void DebugCrtDumpLeaks()
{
    _CrtDumpMemoryLeaks();
}

int main()
{
    DebugCrtInit(0);

    int flag = SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMEPAD | SDL_INIT_EVENTS | SDL_INIT_SENSOR | SDL_INIT_CAMERA;
    
    if(SDL_Init(flag) != 0)
    {
        std::cout << "Could not init SDL!\n";
        return 1;
    }
    std::cout << "Succesfully init SDL!\n";

    SDL_Quit();

    //malloc(42);
    DebugCrtDumpLeaks();
    return 0;
}

You can combine flags up until SDL_INIT_HAPTIC, then the leak shows up.

jarroddavis68 avatar Feb 27 '24 19:02 jarroddavis68