The behavior with IntPtr and out when using P/Invoke can be understood through how the interop marshalling works in .NET. Essentially, interop marshalling is a runtime mechanism that translates managed data types into unmanaged types and vice versa, depending on how data is passed between managed and unmanaged code. This process involves making sure that the data representation is consistent on both sides.
When using "out IntPtr" in P/Invoke, it means that the unmanaged code will allocate a value for the IntPtr that gets assigned to the caller. For these scenarios, P/Invoke handles the memory directly, and IntPtr is used to keep track of pointers to unmanaged memory allocated by the unmanaged function.
One of the key points is that out or ref on IntPtr means the runtime will handle marshalling the address so that the underlying value can be written directly into the managed memory by the unmanaged call. This is why accessing the address of the IntPtr using &IntPtr is correct because it tells the unmanaged function where in the managed heap it can write the value (the pointer).
On the other hand, when allocating memory explicitly (such as GPU memory allocation), IntPtr.ToPointer() is used to obtain the raw pointer value that represents the address in the unmanaged memory. This difference arises from whether you're manipulating the pointer itself (IntPtr as a container) or manipulating the address it points to (using ToPointer).
For more details on how P/Invoke marshals IntPtr and other types, including how data is passed between managed and unmanaged memory, refer to the Microsoft documentation on interop marshalling and platform invoke, which gives a good overview of how data is handled in these interop scenarios​: