Skip to content

Conversation

@icculus
Copy link
Collaborator

@icculus icculus commented Nov 10, 2025

This is an attempt at a limited-but-lower-overhead Properties object that could be useful for throwaway SDL_PropertiesID...namely, the type we use for SDL_Create*WithProperties functions.

And example of updating testyuv.c to use it:

diff --git a/test/testyuv.c b/test/testyuv.c
index 81ed2b734..93b11f6ef 100644
--- a/test/testyuv.c
+++ b/test/testyuv.c
@@ -619,12 +619,16 @@ int main(int argc, char **argv)
 
     output[0] = SDL_CreateTextureFromSurface(renderer, original);
     output[1] = SDL_CreateTextureFromSurface(renderer, converted);
-    props = SDL_CreateProperties();
-    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, yuv_colorspace);
-    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, yuv_format);
-    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STREAMING);
-    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, original->w);
-    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, original->h);
+    {
+        const SDL_TemporaryPropertyItem texture_prop_items[] = {
+            { SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = yuv_colorspace } },
+            { SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = yuv_format } },
+            { SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = SDL_TEXTUREACCESS_STREAMING } },
+            { SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = original->w } },
+            { SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = original->h } }
+        };
+        props = SDL_CreateTemporaryProperties(texture_prop_items, SDL_arraysize(texture_prop_items));
+    }
     output[2] = SDL_CreateTextureWithProperties(renderer, props);
     SDL_DestroyProperties(props);
     if (!output[0] || !output[1] || !output[2]) {

If following the rules for these objects (read-only, thread unsafe, etc), they could be used in other situations, as they can be used anywhere an SDL_PropertiesID is expected.

Fixes #14436.

@slouken
Copy link
Collaborator

slouken commented Nov 10, 2025

Let's hold this for 3.6

@slouken slouken added this to the 3.6.0 milestone Nov 10, 2025
@icculus icculus force-pushed the sdl3-temporary-properties branch from 84e5dba to bff3fde Compare January 2, 2026 21:34
@icculus icculus force-pushed the sdl3-temporary-properties branch from bff3fde to 446e676 Compare January 2, 2026 21:36
@icculus
Copy link
Collaborator Author

icculus commented Jan 2, 2026

Rebased to fix conflicts and changed the \since documentation to 3.6.0.

Let's decide if we want to do this.

@slouken
Copy link
Collaborator

slouken commented Jan 2, 2026

I'm kind of inclined not to do this. It adds some complexity to SDL, it doesn't significantly reduce the complexity of the application code, and properties are intended by design not to be in performance critical paths.

Maybe we should take the original example and compare the allocations before and after to see how much this improves things?

@icculus
Copy link
Collaborator Author

icculus commented Jan 2, 2026

Maybe we should take the original example and compare the allocations before and after to see how much this improves things?

This diff to log allocations and locks...

diff --git a/src/stdlib/SDL_malloc.c b/src/stdlib/SDL_malloc.c
index 5fb7c1c9b..2eb3c4d4c 100644
--- a/src/stdlib/SDL_malloc.c
+++ b/src/stdlib/SDL_malloc.c
@@ -6452,6 +6452,7 @@ void *SDL_malloc(size_t size)
 {
     void *mem;
 
+printf("SDL_malloc(%d)", (int) size);
     if (!size) {
         size = 1;
     }
@@ -6463,6 +6464,8 @@ void *SDL_malloc(size_t size)
         SDL_OutOfMemory();
     }
 
+printf(" -> %p\n", mem);
+
     return mem;
 }
 
@@ -6470,6 +6473,8 @@ void *SDL_calloc(size_t nmemb, size_t size)
 {
     void *mem;
 
+printf("SDL_calloc(%d, %d)", (int) nmemb, (int) size);
+
     if (!nmemb || !size) {
         nmemb = 1;
         size = 1;
@@ -6482,6 +6487,7 @@ void *SDL_calloc(size_t nmemb, size_t size)
         SDL_OutOfMemory();
     }
 
+printf(" -> %p\n", mem);
     return mem;
 }
 
@@ -6489,6 +6495,8 @@ void *SDL_realloc(void *ptr, size_t size)
 {
     void *mem;
 
+printf("SDL_realloc(%p, %d)", ptr, (int) size);
+
     if (!size) {
         size = 1;
     }
@@ -6500,6 +6508,8 @@ void *SDL_realloc(void *ptr, size_t size)
         SDL_OutOfMemory();
     }
 
+printf(" -> %p\n", mem);
+
     return mem;
 }
 
@@ -6509,6 +6519,8 @@ void SDL_free(void *ptr)
         return;
     }
 
+printf("SDL_free(%p)\n", ptr);
+
     s_mem.free_func(ptr);
     DECREMENT_ALLOCATION_COUNT();
 }
diff --git a/src/thread/pthread/SDL_sysmutex.c b/src/thread/pthread/SDL_sysmutex.c
index 3e04d2150..5d0dedb22 100644
--- a/src/thread/pthread/SDL_sysmutex.c
+++ b/src/thread/pthread/SDL_sysmutex.c
@@ -60,6 +60,7 @@ void SDL_DestroyMutex(SDL_Mutex *mutex)
 
 void SDL_LockMutex(SDL_Mutex *mutex) SDL_NO_THREAD_SAFETY_ANALYSIS // clang doesn't know about NULL mutexes
 {
+printf("LockMutex(%p)\n", mutex);
     if (mutex) {
 #ifdef FAKE_RECURSIVE_MUTEX
         pthread_t this_thread = pthread_self();
@@ -125,6 +126,7 @@ bool SDL_TryLockMutex(SDL_Mutex *mutex)
 
 void SDL_UnlockMutex(SDL_Mutex *mutex) SDL_NO_THREAD_SAFETY_ANALYSIS // clang doesn't know about NULL mutexes
 {
+printf("UnlockMutex(%p)\n", mutex);
     if (mutex) {
 #ifdef FAKE_RECURSIVE_MUTEX
         // We can only unlock the mutex if we own it
diff --git a/src/thread/pthread/SDL_sysrwlock.c b/src/thread/pthread/SDL_sysrwlock.c
index ed7f5482e..ecdd1a8ba 100644
--- a/src/thread/pthread/SDL_sysrwlock.c
+++ b/src/thread/pthread/SDL_sysrwlock.c
@@ -55,6 +55,7 @@ void SDL_DestroyRWLock(SDL_RWLock *rwlock)
 
 void SDL_LockRWLockForReading(SDL_RWLock *rwlock) SDL_NO_THREAD_SAFETY_ANALYSIS  // clang doesn't know about NULL mutexes
 {
+printf("LockRWLockForReading(%p)\n", rwlock);
     if (rwlock) {
         const int rc = pthread_rwlock_rdlock(&rwlock->id);
         SDL_assert(rc == 0);  // assume we're in a lot of trouble if this assert fails.
@@ -63,6 +64,7 @@ void SDL_LockRWLockForReading(SDL_RWLock *rwlock) SDL_NO_THREAD_SAFETY_ANALYSIS
 
 void SDL_LockRWLockForWriting(SDL_RWLock *rwlock) SDL_NO_THREAD_SAFETY_ANALYSIS  // clang doesn't know about NULL mutexes
 {
+printf("LockRWLockForWriting(%p)\n", rwlock);
     if (rwlock) {
         const int rc = pthread_rwlock_wrlock(&rwlock->id);
         SDL_assert(rc == 0);  // assume we're in a lot of trouble if this assert fails.
@@ -105,6 +107,7 @@ bool SDL_TryLockRWLockForWriting(SDL_RWLock *rwlock)
 
 void SDL_UnlockRWLock(SDL_RWLock *rwlock) SDL_NO_THREAD_SAFETY_ANALYSIS  // clang doesn't know about NULL mutexes
 {
+printf("UnlockRWLock(%p)\n", rwlock);
     if (rwlock) {
         const int rc = pthread_rwlock_unlock(&rwlock->id);
         SDL_assert(rc == 0);  // assume we're in a lot of trouble if this assert fails.

...and this test program...

#include <stdio.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

int main(int argc, char **argv)
{
    SDL_Init(0);
printf("TEST START\n");
#if 1
	SDL_PropertiesID props = SDL_CreateProperties();
	SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, argv);
	SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
	SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL);
	SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL);
	SDL_SetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN, true);
#else
    const SDL_TemporaryPropertyItem prop_items[] = {
        { SDL_PROP_PROCESS_CREATE_ARGS_POINTER, SDL_PROPERTY_TYPE_POINTER, { .p = argv } },
	    { SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = SDL_PROCESS_STDIO_NULL } },
	    { SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = SDL_PROCESS_STDIO_NULL } },
	    { SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROPERTY_TYPE_NUMBER, { .n = SDL_PROCESS_STDIO_NULL } },
	    { SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN, SDL_PROPERTY_TYPE_BOOLEAN, { .b = true } }
    };
    SDL_PropertiesID props = SDL_CreateTemporaryProperties(prop_items, SDL_arraysize(prop_items));
#endif
    SDL_DestroyProperties(props);
printf("TEST END\n");
    SDL_Quit();
    return 0;
}

So this just creates a Properties set, adds some stuff to it, and deletes it.

Here's the relevant output for the normal path:

TEST START
SDL_calloc(1, 24) -> 0x5ff078d5db70
SDL_calloc(1, 40) -> 0x5ff078d5fd70
SDL_calloc(1, 64) -> 0x5ff078d5fda0
SDL_calloc(4, 24) -> 0x5ff078d5fdf0
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
LockRWLockForWriting(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
SDL_calloc(1, 40) -> 0x5ff078d5fe60
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
LockMutex(0x5ff078d5fd70)
LockRWLockForWriting((nil))
UnlockRWLock((nil))
SDL_malloc(24) -> 0x5ff078d5db50
LockRWLockForWriting((nil))
UnlockRWLock((nil))
UnlockMutex(0x5ff078d5fd70)
SDL_calloc(1, 40) -> 0x5ff078d5fe90
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
LockMutex(0x5ff078d5fd70)
LockRWLockForWriting((nil))
UnlockRWLock((nil))
SDL_malloc(32) -> 0x5ff078d5de20
LockRWLockForWriting((nil))
UnlockRWLock((nil))
UnlockMutex(0x5ff078d5fd70)
SDL_calloc(1, 40) -> 0x5ff078d5fec0
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
LockMutex(0x5ff078d5fd70)
LockRWLockForWriting((nil))
UnlockRWLock((nil))
SDL_malloc(33) -> 0x5ff078d5db20
LockRWLockForWriting((nil))
UnlockRWLock((nil))
UnlockMutex(0x5ff078d5fd70)
SDL_calloc(1, 40) -> 0x5ff078d5fef0
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
LockMutex(0x5ff078d5fd70)
LockRWLockForWriting((nil))
UnlockRWLock((nil))
SDL_malloc(33) -> 0x5ff078d5ff20
LockRWLockForWriting((nil))
SDL_calloc(8, 24) -> 0x5ff078d5ff50
SDL_free(0x5ff078d5fdf0)
UnlockRWLock((nil))
UnlockMutex(0x5ff078d5fd70)
SDL_calloc(1, 40) -> 0x5ff078d60020
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
LockMutex(0x5ff078d5fd70)
LockRWLockForWriting((nil))
UnlockRWLock((nil))
SDL_malloc(30) -> 0x5ff078d60050
LockRWLockForWriting((nil))
UnlockRWLock((nil))
UnlockMutex(0x5ff078d5fd70)
LockRWLockForReading(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
SDL_free(0x5ff078d5db50)
SDL_free(0x5ff078d5fe60)
SDL_free(0x5ff078d60050)
SDL_free(0x5ff078d60020)
SDL_free(0x5ff078d5ff20)
SDL_free(0x5ff078d5fef0)
SDL_free(0x5ff078d5db20)
SDL_free(0x5ff078d5fec0)
SDL_free(0x5ff078d5de20)
SDL_free(0x5ff078d5fe90)
SDL_free(0x5ff078d5ff50)
SDL_free(0x5ff078d5fda0)
SDL_free(0x5ff078d5fd70)
SDL_free(0x5ff078d5db70)
LockRWLockForWriting(0x5ff078d565f0)
UnlockRWLock(0x5ff078d565f0)
TEST END

Here's the output for the temporary properties version:

TEST START
SDL_calloc(1, 24) -> 0x5b6ec286ab70
LockRWLockForReading(0x5b6ec28635f0)
UnlockRWLock(0x5b6ec28635f0)
LockRWLockForWriting(0x5b6ec28635f0)
UnlockRWLock(0x5b6ec28635f0)
LockRWLockForReading(0x5b6ec28635f0)
UnlockRWLock(0x5b6ec28635f0)
SDL_free(0x5b6ec286ab70)
LockRWLockForWriting(0x5b6ec28635f0)
UnlockRWLock(0x5b6ec28635f0)
TEST END

(the calloc() and free() are to make the property object; all those locks are to manage the hashtable of SDL_PropertiesIDs. We could avoid those if we reserve some piece of the ID space for temporary objects and never put them in the global table at all, but let's not get ahead of ourselves.)

Since this is just creation/destruction and not actually using a properties group (to avoid catching allocations and locks done by SDL_CreateProcessWithProperties() or whatever), here's what a single use looks like:

SDL_CopyProperties() is almost identical between the two (one extra lock or so). This is because temporary objects are read-only and can't be copied into, so most of this specific overhead is dealing with the normal Properties object you'd be copying into.

SDL_Get*Property() looks like this in normal operation:

LockRWLockForReading(0x5eec3ba7d5f0)
UnlockRWLock(0x5eec3ba7d5f0)
LockMutex(0x5eec3ba86d70)
LockRWLockForReading((nil))
UnlockRWLock((nil))
UnlockMutex(0x5eec3ba86d70)

...and like this with a temporary property set:

LockRWLockForReading(0x63f6b3e3e5f0)
UnlockRWLock(0x63f6b3e3e5f0)

(which is, again, the global property lock while it converts the SDL_PropertiesID into a struct.)

So it's definitely less locking and malloc pressure, but I'm also willing to believe that it isn't actually a bottleneck in real world use, and don't have serious data on that at the moment.

@slouken
Copy link
Collaborator

slouken commented Jan 2, 2026

@willvale, do you have a real world example that shows the benefit (or lack thereof) of this PR?

@willvale
Copy link

willvale commented Jan 4, 2026

I'm just making some changes so that I can try this out - been distracted integrating/fixing another open source library!

@willvale
Copy link

willvale commented Jan 4, 2026

See below for my results for SDL_CreateProcessWithProperties: It's very worthwhile in a debug build (consistently saves a third of a second) but any difference is lost in the noise in a release build. Pretty much as you'd expect.

My specific use (currently) is when I'm building game data which includes ink scripts, I need to shell out to a couple of tools to compile each script. Normally I run the data build as part of launching the game and aim to keep the time of that build as low as possible so I can iterate quickly with a full tools pipeline.

So for me, having a measurably faster debug configuration is worthwhile as that's the one I usually run. But it's not a deal-breaker.

Opinionated bit:

_Really, the reason I opened the ticket was to try and discourage blanket use of SDL_Properties as they're surprisingly expensive. IMO when considering if an API should take properties, the default should be "no" unless it's something that's called once at startup or on e.g. a mode change, and needs to take and/or return platform-dependent properties.

At the moment the uses which look worrying are in SDL_IOStream as mentioned above, SDL_Thread, and the renderer textures. But the more code that relies on this mechanism, the harder it will be to change course if it ends up causing death by 1000 cuts._

Full fat properties:

[main/Run/Build all/Build/Story/Process] Created process 0x034c7920 using C:\Users\will\sil\build\x32\bin\debug\inklecate.exe
[main/Run/Build all/Build/Story/Process] mem::Scope[SDL]: 35 allocs for 16285 bytes, 25 frees
[main/Run/Build all/Build/Story/Process] Created process 0x034c7920 using C:\Users\will\sil\build\x32\bin\debug\ink-to-bin.exe
[main/Run/Build all/Build/Story/Process] mem::Scope[SDL]: 34 allocs for 16177 bytes, 24 frees

Debug build actual timings for CreateProperties/CreateProcessWithProperties/DestroyProperties
20 trials after warmup, average time = 749.84ms
20 trials after warmup, average time = 722.72ms

Release build actual timings for CreateProperties/CreateProcessWithProperties/DestroyProperties
20 trials after warmup, average time = 71.03ms
20 trials after warmup, average time = 47.60ms

Temporary properties:

[main/Run/Build all/Build/Story/Process] Created process 0x037c3920 using C:\Users\will\sil\build\x32\bin\debug\inklecate.exe
[main/Run/Build all/Build/Story/Process] mem::Scope[SDL]: 21 allocs for 15733 bytes, 11 frees
[main/Run/Build all/Build/Story/Process] Created process 0x037c3920 using C:\Users\will\sil\build\x32\bin\debug\ink-to-bin.exe
[main/Run/Build all/Build/Story/Process] mem::Scope[SDL]: 20 allocs for 15625 bytes, 10 frees

Debug build actual timings for CreateTemporaryProperties/CreateProcessWithProperties/DestroyProperties
20 trials after warmup, average time = 407.92ms
20 trials after warmup, average time = 398.07ms

Release build actual timings CreateTemporaryProperties/CreateProcessWithProperties/DestroyProperties
20 trials after warmup, average time = 74.67ms
20 trials after warmup, average time = 61.15ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proliferation & performance of SDL_properties

3 participants