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

Host buffers extension initial #1012

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

Try
Copy link

@Try Try commented Jul 6, 2024

Hi, following discussion in #1006, decide to create an extension prototype here. Do not have a experience with proposing api extensions, but need to start somewhere :)

Name

AL_SOFT_host_buffers

Overview

This extension allows application to allocate, deallocate and manipulate sound buffers without any active openal context.
Id's of those buffers are valid across openal-soft contexts, allowing seamless switching between different output devices.

Already tested this new api for gen/delete/data is my game-project - this enables to make sound code much more simple and provide well defined behavior when sound is uses across devices (builtin sound + headsets).

New Procedures and Functions

ALenum alGenBuffersHost(ALsizei n, ALuint *buffers);
ALenum alDeleteBuffersHost(ALsizei n, const ALuint *buffers);
ALboolean alIsBufferHost(ALuint buffer);
ALenum alBufferDataHost(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei samplerate);
ALenum alBufferfHost(ALuint buffer, ALenum param, ALfloat value);
ALenum alBuffer3fHost(ALuint buffer, ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
ALenum alBufferfvHost(ALuint buffer, ALenum param, const ALfloat *values);
ALenum alBufferiHost(ALuint buffer, ALenum param, ALint value);
ALenum alBuffer3iHost(ALuint buffer, ALenum param, ALint value1, ALint value2, ALint value3);
ALenum alBufferivHost(ALuint buffer, ALenum param, const ALint *values);
ALenum alGetBufferfHost(ALuint buffer, ALenum param, ALfloat *value);
ALenum alGetBuffer3fHost(ALuint buffer, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
ALenum alGetBufferfvHost(ALuint buffer, ALenum param, ALfloat *values);
ALenum alGetBufferiHost(ALuint buffer, ALenum param, ALint *value);
ALenum alGetBuffer3iHost(ALuint buffer, ALenum param, ALint *value1, ALint *value2, ALint *value3);
ALenum alGetBufferivHost(ALuint buffer, ALenum param, ALint *values);

Those functions generally follow syntax of vanilla openal buffer-functions, for gen/delete/modify, with only change in return of error code. Returning an error-code is necessary, due to alGetError is bound to ALCcontext.

For now keeping thins simple, one alloc/free/upload and basic get/set functions are implemented.

New Tokens

None.

Other information

amount of lock contention when using multiple devices simultaneously.

Addressed by using upper bit if buffer-id, to signify that it belong to host-buffers. For applications that do not use this extension no extra locking should happen on primary path. Only exception now is alSourceQueueBuffers (see implementation details).

  • Interaction with EAX

I'm not really knowledgeable on how EAX should work, By looking in source code EXA seen to be device-depended. Solution for now to set eax_x_ram_mode = EaxStorage::Accessible, assuming that it software emulated EAX.

Calling EAXSetBufferMode on host buffer generate a error AL_INVALID_VALUE

  • Interaction with alObjectLabelDirectEXT

Should not interact, as buffer name stored in device, generates AL_INVALID_NAME error

@kcat
Copy link
Owner

kcat commented Jul 7, 2024

I don't think I like splitting the buffer pool like this, all the extra complexity and it doesn't handle the underlying issue of it not working between different drivers through the router, and leaves no way to know whether two devices or contexts can recognize each others' buffers aside from trying it. Two devices could have the extension, but not be able to share buffers if they're on two different drivers that support the extension independently.

A better option may be to just make the buffer (and effect and filter) lists global, and a function to query if two given contexts or devices share them (this could increase lock contention, but if it's desired functionality, it may be worth the drawback). Or have some kind of share functionality, along the lines of wglShareLists or glXCreateContextAttribsARB's share_context parameter, which can share certain data between separate contexts as long as they're in the same address space and share "the same implementation of OpenGL functions".

@Try
Copy link
Author

Try commented Jul 7, 2024

Hi, @kcat , thanks for quick response!

Two devices could have the extension, but not be able to share buffers

This is not intended to work, in case if application uses 2 different openal implementations. Target use-case is application that uses only openal-soft and need to decouple buffer management from context management.

Or have some kind of share functionality

GLES-like share functionality does not really addressing issues that this extension is intended to solve.
In GLES we can use vertex-buffer-array and provide c-pointer with data instead of buffer. Works only because uniforms/vertex-data/indices are plain c-data. Unfortunately OpenAL cannot reuse it as buffer is not only plain memory.

In Vulkan there are VK_EXT_external_memory_host and VK_KHR_external_memory.

  • VK_EXT_external_memory_host is similar to what I need
  • VK_KHR_external_memory is also capable of handling images and much more difficult to use.

OpenAL also cannot quite reuse those extensions as-is since buffers are opaque types and application doesn't know how-much memory to malloc.

A better option may be to just make the buffer (and effect and filter) lists global,

If that OK, I'm fine with this. In that case ALCdevice::BufferList would be removed in favor of single global one. And I'm proposing then not to introduce new alGenBuffersHost-like api, but reuse al-direct by allowing alGenBuffersDirect to take nullptr as context.

@kcat
Copy link
Owner

kcat commented Jul 8, 2024

Two devices could have the extension, but not be able to share buffers

This is not intended to work, in case if application uses 2 different openal implementations. Target use-case is application that uses only openal-soft and need to decouple buffer management from context management.

Right but my point is, OpenAL Soft could just make the buffers global and implicitly sharable between its own devices, but the app would have no way to know whether two given devices can use the same buffers aside from just trying and see if it errors.

Or have some kind of share functionality

GLES-like share functionality does not really addressing issues that this extension is intended to solve. In GLES we can use vertex-buffer-array and provide c-pointer with data instead of buffer. Works only because uniforms/vertex-data/indices are plain c-data. Unfortunately OpenAL cannot reuse it as buffer is not only plain memory.

I'm not sure what GLES-like share functionality is like, but with GLX and WGL, it is a method of allowing multiple contexts to recognize each others' sharable objects (which don't rely on context-specific state). So you could create an OpenGL context, create another OpenGL context from the same driver and share with the previous context, then creating textures and stuff on one context will let them be accessible to the other. Similarly, you could create an OpenAL context, create another OpenAL context from the same driver and share it with the previous context, then creating buffers (and filters and effect objects) on one context will let them be accessible to the other context.

In Vulkan there are VK_EXT_external_memory_host and VK_KHR_external_memory.

* `VK_EXT_external_memory_host` is similar to what I need

* `VK_KHR_external_memory` is also capable of handling images and much more difficult to use.

OpenAL Soft 1.23.1 supports AL_EXT_STATIC_BUFFER, which allows you to load samples into your own data buffer, then have an OpenAL buffer object use your sample buffer without copying it. You still need to create and prepare buffers for each context, and manage the lifetime of your sample buffer manually (making sure not to free it before all OpenAL buffers using it are deleted), but you don't need to reload and have multiple copies of the sound itself.

A better option may be to just make the buffer (and effect and filter) lists global,

If that OK, I'm fine with this.

It's preferable over the split pool, but not really ideal since it leaves no way to know which contexts can share buffers.

@Try
Copy link
Author

Try commented Jul 16, 2024

Thanks for response!

I'll need to take a look into AL_EXT_STATIC_BUFFER first and see if it's good fit for the job and will be back!

@Try
Copy link
Author

Try commented Jul 21, 2024

After examining AL_EXT_STATIC_BUFFER: while static buffer can be used to achieve the goal, it has development and abstraction cost: game-engine would have to maintain something like a map, that maps {data, AlCdevice} <--> AlBuffer.

It's preferable over the split pool, but not really ideal since it leaves no way to know which contexts can share buffers.

I've pushed new changes for single-pool based solution: no new api-entries, but nullptr is a acceptable parameter for context pointer. However now it introduces a problem of error handling, as there is not way to store last error now.
So, for error handling my solution is to also return it from all alBuffer*Direct functions. This also fixes small design oversight in al-direct, as multi-threaded application still can't relay on alGetError[Direct], unless global locking.
Hopefully this minor change to api is acceptable.

no way to know which contexts can share buffers.

Havent address this yet. So far can propose interface such as:
bool alBufferIsSharable(AlContext* parent, Aluint buffer, AlContext* other) but not sure what implementation should be like... return true?

@Try
Copy link
Author

Try commented Jul 28, 2024

Small bump to review: just want to sync on a direction of how extension is evolving

@kcat
Copy link
Owner

kcat commented Jul 28, 2024

So, for error handling my solution is to also return it from all alBuffer*Direct functions. This also fixes small design oversight in al-direct, as multi-threaded application still can't relay on alGetError[Direct], unless global locking. Hopefully this minor change to api is acceptable.

Unfortunately it isn't, it will break ABI and cause potential problems for existing compiled code that's calling the function without expecting it to have a return value. I don't know the actual effect it'll have, but I'd expect different systems to have different behaviors, making it a potential danger even if it appears to work.

I'm also not a fan of allowing nullptr for the context of *Direct functions since a context may be necessary to know where to forward the call to. Calling AL functions without a valid context isn't safe, it can do anything from noop to crash. The Direct functions in particular lack context validation and checking for performance reasons, so having some of them sometimes accept a null context is creating ambiguity and a potential hazard.

With recent commits, alGetError[Direct] handle a thread-local error state, so you don't need to worry about a thread erroneously intercepting an error generated on another thread.

no way to know which contexts can share buffers.

Havent address this yet. So far can propose interface such as: bool alBufferIsSharable(AlContext* parent, Aluint buffer, AlContext* other) but not sure what implementation should be like... return true?

A buffer ID wouldn't be necessary, if two contexts can share buffers any valid buffer with one context would work with the other.

Though I still prefer the idea of the app having to make a call to actively share resources, either with a function call just after context creation or a new context creation function, in case an implementation would need to do it on explicit request instead of implicitly sharing everywhere it can.

@Try
Copy link
Author

Try commented Jul 30, 2024

I don't know the actual effect it'll have, but I'd expect different systems to have different behaviors, making it a potential danger even if it appears to work.

Depend on calling convention. In source code I've found:

#ifdef _WIN32
 #define AL_APIENTRY __cdecl
#else
 #define AL_APIENTRY
#endif

For __cdecl when function returns an integer, it will be put to EAX register. For void function EAX should be assumed as dirty and not used. However on other systems this code has no defined calling convention, so don't know what to expect.

alGetError[Direct] handle a thread-local error state, so you don't need to worry about a thread

al::tss<ALenum> ALCcontext::mLastThreadError this can only work for context-based api, but indeed for al-direct not an issue.
For sake of host-buffers, one of the goals here to allocate buffer without context, so need other way to return error-code.
This is why at first I've proposed a new set of api's: to have ret-codes and avoid abi issues.

Though I still prefer the idea of the app having to make a call to actively share resources

It's difficult to express in api:

  • how to manage ID's? if buffer with ID=8, from context A is shared with context B where 8 is already taken?
  • how to allocate a buffer? Dummy context?
  • can buffer be shared twice? if not then application has to maintain complex mapping buffer <-> context

One more slightly tangent point to bring, for me would be EAX.
By searching code-base: buffer do have some bookkeeping code for EAX memory per device, but it seem not to be used anywhere.

@kcat
Copy link
Owner

kcat commented Jul 30, 2024

al::tss<ALenum> ALCcontext::mLastThreadError this can only work for context-based api, but indeed for al-direct not an issue. For sake of host-buffers, one of the goals here to allocate buffer without context, so need other way to return error-code.

I am not confident that this will work context-less. A context or device handle is needed to "talk" to a driver, and even though it could work in some situations, it wouldn't be reliable depending on how OpenAL is accessed.

It's difficult to express in api:

* how to manage ID's? if buffer with ID=8, from context A is shared with context B where 8 is already taken?

That wouldn't happen. The "link" would be created during context creation (or immediately after, before any buffers are made). Essentially the BufferLock and BufferList would be in a separate struct that's allocated separately for the ALCdevice, and managed with an intrusive_ptr (effectively a std::shared_ptr, reference counting the list). When a context is created, it would either take a reference to the device's buffer list, or the shared context's buffer list, so that when it needs to lookup/access buffers, it uses the appropriate list that the context has a reference to. Even if the original device that all contexts share with is closed and deleted, since it would be reference counted the buffer list would remain valid until the last context that has access to it is destroyed.

In practice, the shared state would likely include filters and effects too, not just buffers.

* how to allocate a buffer? Dummy context?

Any context that is or will be shared with other contexts.

* can buffer be shared twice? if not then application has to maintain complex mapping `buffer` <-> `context`

The buffer list can be shared as much as desired, as long as it doesn't overflow the reference counter (4 billion for a 32-bit counter).

One more slightly tangent point to bring, for me would be EAX. By searching code-base: buffer do have some bookkeeping code for EAX memory per device, but it seem not to be used anywhere.

The EAX memory is just a dummy counter, to pacify apps that think they're using audio hardware memory. It doesn't actually do anything as far as OpenAL Soft is concerned, all memory is the same, it's just pretending to do what apps expect when using it. There might need to be a bit of a think to ensure the counter doesn't break (maybe include it in the shared state?), but EAX is only for old apps and old apps wouldn't use a new extension for sharing resources, so an app wouldn't be using both EAX and sharing at the same time.

@Try
Copy link
Author

Try commented Jul 31, 2024

I am not confident that this will work context-less.

In current implementation mLastThreadError will cause null-pointer error. This is why return value for context-less api is important.

The "link" would be created during context creation (or immediately after, before any buffers are made).

Unfortunately this is not valid solution. If sharing api is so complex, game-engine can instead fallback on full-software emulation via alBufferCallbackDirectSOFT or alBufferDataStatic.
Maybe I need to clarify problem that I'm working on:
It's desirable for application to have memory-management decoupled from sound-device and sound might be preloaded ahead of context creation. In my case resource-manager is also cannot be easy aware of with what context(s) buffer will be used with.
Buffer cleanup is also not clear as it's now: application cannot delete device, wait a bit, and then create a new, while reusing same buffers.

This maintaining different non-compatible buffer-ID's defeats the purpose. Declaring buffers upfront also defeat the purpose.

A context or device handle is needed to "talk" to a driver,

Maybe I'm losing what is driver here. There are, AFAIK, no 'real' openal drivers anymore. And from application standpoint, ther eis little reason to support and link two different openal implementations sanctimoniously. If there is demand from other developers, we may provide extension only conditionally (only for PC+Mobile).

In practice, the shared state would likely include filters and effects too, not just buffers.

Good point. For now, reason why I'm focusing on buffer is lack of use-case for effects/filters to test. However they might be introduced afterward, as another extension or newer version of the same one.

@kcat
Copy link
Owner

kcat commented Aug 1, 2024

I am not confident that this will work context-less.

In current implementation mLastThreadError will cause null-pointer error. This is why return value for context-less api is important.

I mean conceptually. I don't think a method for this can be made to work without a context that would be satisfactory.

The "link" would be created during context creation (or immediately after, before any buffers are made).

Unfortunately this is not valid solution. If sharing api is so complex, game-engine can instead fallback on full-software emulation via alBufferCallbackDirectSOFT or alBufferDataStatic.

It wouldn't be complex. It would be using an alternative function for alcCreateContext to create a context that takes another ALCcontext to share with, or an alcShareLists function that takes a new context and another one, which is called immediately after creating a context to share buffers between them. Then any buffer that is accessible to one context will be accessible to the other.

Maybe I need to clarify problem that I'm working on: It's desirable for application to have memory-management decoupled from sound-device and sound might be preloaded ahead of context creation. In my case resource-manager is also cannot be easy aware of with what context(s) buffer will be used with. Buffer cleanup is also not clear as it's now: application cannot delete device, wait a bit, and then create a new, while reusing same buffers.

The problem is OpenAL needs a device or context to know what to do with a call. If you want to decouple sound loading from sound devices, it will need to be loaded and managed separately from OpenAL. Perhaps using a combination of context sharing and alBufferDataStatic to make it accessible with OpenAL.

Load sounds into your own memory buffer, along with it's format and any other info you want, and a buffer ID (which is 0 by default). When you create the first context, create it normally and then create buffers for any already-loaded sounds using alGetBuffers and alBufferDataStatic, and store the buffer ID with the sound info. Any additionally-created contexts would be created and set to share with any other still valid context, so all contexts can use the same sound with the same ID. When the last context and device is destroyed/closed, the sounds' buffer IDs would be cleared since it wouldn't have any OpenAL buffer anymore. If another context is then created, the buffers would be recreated the same way, getting new buffer IDs.

In this way, the buffer ID would also act as a flag indicating if the sound buffer is in use by an OpenAL buffer, which would need to be deleted using any existing context before it can be explicitly freed.

A context or device handle is needed to "talk" to a driver,

Maybe I'm losing what is driver here. There are, AFAIK, no 'real' openal drivers anymore. And from application standpoint, ther eis little reason to support and link two different openal implementations sanctimoniously. If there is demand from other developers, we may provide extension only conditionally (only for PC+Mobile).

Generic Software and Creative's hardware drivers still get used. Rapture3D is still around, AFAIK. But even with just OpenAL Soft, if it's used through the router for whatever reason, an OpenAL call won't get to OpenAL Soft without a context or device handle.

@Try
Copy link
Author

Try commented Aug 4, 2024

But even with just OpenAL Soft, if it's used through the router for whatever reason, an OpenAL call won't get to OpenAL Soft without a context or device handle.
The problem is OpenAL needs a device or context to know what to do with a call.

Can you please clarify here: is my understanding correct, that openal uses context pointer to dispatch api-call into correct *.dll (assuming multiple openal-implementations installed in same system).
If so - than it make sense that context-less api cannot exists.

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.

2 participants