Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove device indices, only use instances #6889

Closed
icculus opened this issue Dec 23, 2022 · 7 comments
Closed

Remove device indices, only use instances #6889

icculus opened this issue Dec 23, 2022 · 7 comments
Milestone

Comments

@icculus
Copy link
Collaborator

icculus commented Dec 23, 2022

So a thing we do in several places in SDL is use an index:

int total = SDL_GetNumAudioDevices(0);
for (int i = 0; i < total; i++) {
    printf("Audio device #d: %s\n", SDL_GetAudioDeviceName(0, i));
}

When opening a device, we open it by this index number, and in return we are given a device ID (audio), or instance (joystick), which has no further association with the index number.

Device index can change as devices come and go, as can the total number of devices, so things that take the index need to be stable until the next call to SDL_GetNumAudioDevices(), and locks need to be held in various places to make sure the index doesn't change in the middle of using it.

When events references these devices, their which field is the device instance, which might be confusing if all you had was an index number and an SDL_Joystick *...now you're scrambling to figure out what this number means, or worse, just assuming it was the index.

So let's get rid of the indexes.

Let's have one call that fills a buffer with instance IDs, which are associated with a device for as long as they exist (opened or not), while SDL is initiailized. We already track these.

So that for loop would become:

int total;
SDL_AudioDeviceID *devs = SDL_GetAudioDeviceList(0, &total);
for (int i = 0; i < total; i++) {
    printf("Audio device #%d: %s\n", (int) devs[i], SDL_GetAudioDeviceName(0, devs[i]));
}
SDL_free(devs);

So if this list changes during the for-loop, SDL will notice the instance is not a currently-valid item and return a failure (NULL in this case), but life will continue.

devs can be NULL, and SDL_free will do the right thing with it (and total will be zero here).

We delete all the things that are named SDL_JoystickGetWidgetForIndex(int index).

There's more to this, but that's the idea.

@icculus icculus added this to the 3.2.0 milestone Dec 23, 2022
@slouken
Copy link
Collaborator

slouken commented Dec 23, 2022

One of the advantages of this approach is that in cases where locking is needed, like joysticks, etc. the entire list can be returned atomically.

A couple more comments on details:
In general these APIs should be returning IDs of some sort, where 0 is an invalid ID. The list is then guaranteed to be 0 terminated, allowing the caller to pass NULL for the count and do something like this:

SDL_AudioDeviceID *devs = SDL_GetAudioDeviceList(0, NULL);
if (devs) {
    for (int i = 0; devs[i]; i++) {
        printf("Audio device #%d: %s\n", (int) devs[i], SDL_GetAudioDeviceName(0, devs[i]));
    }
    SDL_free(devs);
}

This semantic also guarantees that returning NULL means an internal error occurred, as distinct from a list with only 0 in it, indicating an empty list.

We should double check with @attila-lendvai and folks on #6337 to see if this maps well to language bindings.

@eloj
Copy link
Contributor

eloj commented Dec 28, 2022

Are you sure it's not worth making these accept memory to fill out, rather than doing an internal heap allocation?

This would make the process less atomic, but not in a way that'd be very harmful that I can see; i.e at the point of filling in the memory, the returned enumeration is always equally valid in both designs.

// Modelled on Khronos-style enumerations, e.g
// https://registry.khronos.org/OpenCL/sdk/1.0/docs/man/xhtml/clGetPlatformIDs.html
int num_sensors;
int err = SDL_GetSensors(0, NULL, &num_sensors); // Get count

// Implies num_sensors > 0
if (err == SDL_OK) {
    SDL_SensorID *sensors = whatever_alloc(num_sensors, sizeof(*sensors));
    SDL_GetSensors(num_sensors, sensors, NULL); // Fill out memory
    whatever_free(sensors);
}

I can certainly see arguments against, just raising this to make sure it's been considered and rejected before the API gets locked down.

@slouken
Copy link
Collaborator

slouken commented Dec 28, 2022

Yes, we considered that pattern. Microsoft also uses that for many APIs. The problem is that it's not atomic, and you always risk the count changing between the two calls. The recommended pattern for robustness with that style of API is to actually do the allocations in a loop:

SDL_SensorID *sensors = NULL;
int num_sensors;
int result;
while ((result = SDL_GetSensors(0, sensors, &num_sensors)) == BUFFERTOOSMALL) {
    SDL_SensorID *new_sensors = (SDL_SensorID *)realloc(sensors, num_sensors * sizeof(*sensors));
    if (new_sensors == NULL) {
        /* Handle out of memory condition */
        if (sensors) {
            free(sensors);
        }
        return;
    }
    sensors = new_sensors;
}
if (result == OK) {
    int i;
    for (i = 0; i < num_sensors; ++i) {
        /* Do something with sensors[i] */
    }
}
if (sensors) {
    free(sensors);
}

So, since we already provide memory allocation functions, we decided to make this much easier to use correctly. :)

slouken added a commit that referenced this issue Dec 28, 2022
Instead of indexing into an internal list of devices which requires locking, we return a list of device IDs which can then be queried individually.

Reference: #6889
icculus added a commit that referenced this issue Aug 4, 2023
This rips up the entire SDL audio subsystem! While we still feed the audio device from a separate thread, the audio callback into the app is now gone a totally optional alternative.

Now the app will bind an SDL_AudioStream to a given device and feed data to it. As many streams as one likes can be bound to a device; SDL will mix them all into a single buffer and feed the device from there.

So not only does this function as a basic mixer, it also means that multiple device opens are handled seamlessly (so if you want to open the device for your game, but you also link to a library that provides VoIP and it wants to open the device separately, you don't have to worry about stepping on each other, or that the OS will fail to allow multiple opens of the same device, etc).

Merged from pull request #7704.

Fixes #7379.
Reference Issue #6889.
Reference Issue #6632.
@icculus
Copy link
Collaborator Author

icculus commented Aug 5, 2023

Okay, this is in for audio devices now; sensors, joysticks, gamepads, and display modes were already done.

Things still using this pattern:

  • Haptics (are we keeping this API?)
  • Gamepad Mappings (SDL_GetNumGamepadMappings/SDL_GetGamepadMappingForIndex)
  • Gamepads don't use this, but there's a bunch of \sa things in the header for a non-existent SDL_GetGamepadNameForIndex

@slouken
Copy link
Collaborator

slouken commented Aug 6, 2023

Okay, this is in for audio devices now; sensors, joysticks, gamepads, and display modes were already done.

Things still using this pattern:

  • Haptics (are we keeping this API?)

The jury is out on this one. It's useful for applications that want to use the phone vibration service. It's also useful for games that want to support wheels and haptic flightsticks. I'm tempted to just remove gamepad rumble from the interface and leave it. Thoughts?

  • Gamepad Mappings (SDL_GetNumGamepadMappings/SDL_GetGamepadMappingForIndex)

This can stay as-is. Mappings don't have an ID, and this allows enumerating them.

  • Gamepads don't use this, but there's a bunch of \sa things in the header for a non-existent SDL_GetGamepadNameForIndex

Fixed!

@icculus
Copy link
Collaborator Author

icculus commented Aug 25, 2023

The jury is out on this one. It's useful for applications that want to use the phone vibration service. It's also useful for games that want to support wheels and haptic flightsticks. I'm tempted to just remove gamepad rumble from the interface and leave it. Thoughts?

I guess the question is: is there modern support for this, or is it stuck on a DirectInput 8 interface that never got upgraded?

(And if it never got upgraded, is that because DI8 was Good Enough, or was it where the industry decided this was too niche to continue to support?)

If we're keeping it, I think we definitely make rumble a separate thing. That's what most things want, it's what modern APIs support. Right now the Windows haptic stuff has this weird split between DirectInput8 and XInput, and I suppose that's just to support rumble, and it might be nice to chop that out if nothing else.

@slouken
Copy link
Collaborator

slouken commented Nov 8, 2023

I'm going to close this and open a separate issue to figure out what to do with haptics.

@slouken slouken closed this as completed Nov 8, 2023
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

No branches or pull requests

3 participants