diff --git a/panda/src/event/asyncFuture.cxx b/panda/src/event/asyncFuture.cxx index d8b8b91d97..a6aa341c07 100644 --- a/panda/src/event/asyncFuture.cxx +++ b/panda/src/event/asyncFuture.cxx @@ -389,6 +389,17 @@ wake_task(AsyncTask *task) { } } +/** + * Internal callback called when a CompletionToken created from this future + * completes. + */ +void AsyncFuture:: +token_callback(Completable::Data *data, bool success) { + AsyncFuture *future = (AsyncFuture *)data; + future->set_result(EventParameter(success)); + unref_delete(future); +} + /** * @see AsyncFuture::gather */ diff --git a/panda/src/event/asyncFuture.h b/panda/src/event/asyncFuture.h index acb6b1020f..f9f71472f8 100644 --- a/panda/src/event/asyncFuture.h +++ b/panda/src/event/asyncFuture.h @@ -20,6 +20,7 @@ #include "eventParameter.h" #include "patomic.h" #include "small_vector.h" +#include "completionToken.h" class AsyncTaskManager; class AsyncTask; @@ -58,7 +59,7 @@ class AsyncTask; * * @since 1.10.0 */ -class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount { +class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount, protected Completable::Data { PUBLISHED: INLINE AsyncFuture(); virtual ~AsyncFuture(); @@ -109,6 +110,8 @@ public: private: void wake_task(AsyncTask *task); + static void token_callback(Completable::Data *, bool success); + protected: enum FutureState : patomic_unsigned_lock_free::value_type { // Pending states @@ -136,6 +139,7 @@ protected: friend class AsyncGatheringFuture; friend class AsyncTaskChain; + friend class CompletionToken; friend class PythonTask; public: @@ -199,6 +203,33 @@ private: static TypeHandle _type_handle; }; +#ifndef CPPPARSER +// Allow passing a future into a method accepting a CompletionToken. +template<> +INLINE CompletionToken:: +CompletionToken(AsyncFuture *future) { + if (future != nullptr) { + future->ref(); + _callback._data = future; + if (_callback._data->_function == nullptr) { + _callback._data->_function = &AsyncFuture::token_callback; + } + } +} + +template<> +INLINE CompletionToken:: +CompletionToken(PT(AsyncFuture) future) { + if (future != nullptr) { + _callback._data = future; + if (_callback._data->_function == nullptr) { + _callback._data->_function = &AsyncFuture::token_callback; + } + future.cheat() = nullptr; + } +} +#endif + #include "asyncFuture.I" #endif // !ASYNCFUTURE_H diff --git a/panda/src/putil/CMakeLists.txt b/panda/src/putil/CMakeLists.txt index ecc14b9843..c99dfac383 100644 --- a/panda/src/putil/CMakeLists.txt +++ b/panda/src/putil/CMakeLists.txt @@ -20,6 +20,9 @@ set(P3PUTIL_HEADERS clockObject.h clockObject.I collideMask.h colorSpace.h + completable.I completable.h + completionCounter.I completionCounter.h + completionToken.I completionToken.h copyOnWriteObject.h copyOnWriteObject.I copyOnWritePointer.h copyOnWritePointer.I compareTo.I compareTo.h @@ -86,6 +89,7 @@ set(P3PUTIL_SOURCES callbackObject.cxx clockObject.cxx colorSpace.cxx + completionCounter.cxx copyOnWriteObject.cxx copyOnWritePointer.cxx config_putil.cxx configurable.cxx diff --git a/panda/src/putil/completable.I b/panda/src/putil/completable.I new file mode 100644 index 0000000000..4f44471615 --- /dev/null +++ b/panda/src/putil/completable.I @@ -0,0 +1,61 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completable.I + * @author rdb + * @date 2025-01-22 + */ + +#ifndef CPPPARSER +/** + * + */ +template +INLINE Completable:: +Completable(Callable callback) : + _data(new LambdaData(std::move(callback), [](Data *data, bool do_run) { + LambdaData *self = (LambdaData *)data; + if (do_run) { + std::move(self->_lambda)(); + } + delete self; + })) { +} +#endif + +/** + * + */ +INLINE Completable:: +Completable(Completable &&from) noexcept : + _data(from._data) { + from._data = nullptr; +} + +/** + * + */ +INLINE Completable:: +~Completable() { + Data *data = _data; + if (data != nullptr) { + data->_function.load(std::memory_order_relaxed)(data, false); + } +} + +/** + * + */ +INLINE void Completable:: +operator ()() { + Data *data = _data; + _data = nullptr; + if (data != nullptr) { + data->_function.load(std::memory_order_relaxed)(data, true); + } +} diff --git a/panda/src/putil/completable.h b/panda/src/putil/completable.h new file mode 100644 index 0000000000..a184302d9c --- /dev/null +++ b/panda/src/putil/completable.h @@ -0,0 +1,79 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completable.h + * @author rdb + * @date 2025-01-22 + */ + +#ifndef COMPLETABLE_H +#define COMPLETABLE_H + +#include "pandabase.h" +#include "patomic.h" + +/** + * Stores a type-erased callable that is move-only. May only be called once. + */ +class EXPCL_PANDA_PUTIL Completable { +public: + constexpr Completable() = default; + +#ifndef CPPPARSER + template + INLINE Completable(Callable callback); +#endif + + INLINE Completable(const Completable ©) = delete; + INLINE Completable(Completable &&from) noexcept; + + INLINE void operator ()(); + + INLINE ~Completable(); + +protected: + // There are several design approaches here: + // 1. Optimize for no data block: do not require dynamic allocation of a data + // block in the simple case where the callback data is only the size of a + // single pointer. Store two pointers, one function pointer and a data + // pointer(-sized storage), directly on the class here. + // 2. Optimize for a data block: store the function pointer on the data block, + // always requiring dynamic allocation. + // + // Right now I have opted for 2 because it allows the function pointer to be + // dynamically swapped (used in CompletionCounter), but this decision may + // change in the future. + + struct Data; + typedef void CallbackFunction(Data *, bool); + + struct Data { + patomic _function { nullptr }; + }; + + template + struct LambdaData : public Data { + // Must unfortunately be defined inline, since this struct is protected. + LambdaData(Lambda lambda, CallbackFunction *function) : + _lambda(std::move(lambda)) { + _function = function; + } + + Lambda _lambda; + }; + + Data *_data = nullptr; + + friend class AsyncFuture; + friend class CompletionCounter; + friend class CompletionToken; +}; + +#include "completable.I" + +#endif diff --git a/panda/src/putil/completionCounter.I b/panda/src/putil/completionCounter.I new file mode 100644 index 0000000000..6d59143360 --- /dev/null +++ b/panda/src/putil/completionCounter.I @@ -0,0 +1,97 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionCounter.I + * @author rdb + * @date 2025-01-22 + */ + +/** + * + */ +INLINE CompletionCounter:: +~CompletionCounter() { + CounterData *data = _data; + if (data != nullptr) { + // then() is not called; we still need something that destructs the data + // when done. + auto prev_function = data->_function.exchange(&abandon_callback, std::memory_order_relaxed); + if (prev_function == nullptr) { + // Was already done. + delete data; + } + } +} + +/** + * Returns a new token. May not be called after then(). + */ +INLINE CompletionToken CompletionCounter:: +make_token() { + CompletionToken token; + if (_data == nullptr) { + _data = new CounterData; + _data->_function = &initial_callback; + } + auto old_value = _data->_counter.fetch_add(1); + nassertr(old_value >= 0, token); + token._callback._data = _data; + return token; +} + +/** + * Runs the given callback immediately upon completion. If the counter is + * already done, runs it immediately. This requires an rvalue because it + * consumes the counter, use std::move() if you don't have an rvalue. + * + * The callback will either be called immediately or directly when the last + * token calls complete(), however, it may also be called if a token is + * destroyed. This may happen at unexpected times, such as when the lambda + * holding the token is destroyed prematurely. In this case, however, the + * passed success argument will always be false. + */ +template +INLINE void CompletionCounter:: +then(Callable callable) && { + // Replace the callback pointer with something that calls the given callable + // once the count reaches 0. + CounterData *data = _data; + nassertv(data != nullptr); + _data = nullptr; + if (data->_function.load(std::memory_order_acquire) == nullptr) { + // Already done. + callable((data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0); + delete data; + return; + } + + static_assert(sizeof(Callable) <= sizeof(data->_storage), + "raise storage size in completionCounter.h or reduce lambda captures"); + + new (data->_storage) Callable(std::move(callable)); + + Completable::CallbackFunction *new_function = + [] (Completable::Data *data_ptr, bool success) { + CounterData *data = (CounterData *)data_ptr; + auto prev_count = data->_counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release); + if ((short)(prev_count & 0xffff) > 1) { + return; + } + + Callable *callable = (Callable *)data->_storage; + std::move(*callable)(success && (prev_count & ~0xffff) == 0); + callable->~Callable(); + delete data; + }; + + auto prev_function = data->_function.exchange(new_function, std::memory_order_acq_rel); + if (UNLIKELY(prev_function == nullptr)) { + // Last token finished in the meantime. + new_function(data, (data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0); + } +} diff --git a/panda/src/putil/completionCounter.cxx b/panda/src/putil/completionCounter.cxx new file mode 100644 index 0000000000..2540867e61 --- /dev/null +++ b/panda/src/putil/completionCounter.cxx @@ -0,0 +1,52 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionCounter.cxx + * @author rdb + * @date 2025-01-24 + */ + +#include "completionCounter.h" + +/** + * Called when a token is completed before then() is called. + */ +void CompletionCounter:: +initial_callback(Completable::Data *data_ptr, bool success) { + CounterData &data = *(CounterData *)data_ptr; + auto prev_count = data._counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release); + if ((prev_count & 0xffff) == 1) { + // We're done early. + auto prev_callback = data._function.exchange(nullptr, std::memory_order_acq_rel); + nassertv(prev_callback != nullptr); + + // Someone called then() in the meantime. Call the new callback. The + // refcount will drop below 0 when that's called but they are designed to + // handle that. + if (prev_callback != &initial_callback) { + prev_callback(data_ptr, success && (prev_count & ~0xffff) == 0); + } + } +} + +/** + * Called when a token is completed after this object is destroyed without + * then() being called. + */ +void CompletionCounter:: +abandon_callback(Completable::Data *data_ptr, bool success) { + CounterData &data = *(CounterData *)data_ptr; + auto prev_count = data._counter.fetch_sub(1, std::memory_order_relaxed); + if ((prev_count & 0xffff) <= 1) { + // Done. + auto prev_callback = data._function.exchange(nullptr, std::memory_order_relaxed); + nassertv(prev_callback != nullptr); + nassertv(prev_callback == &abandon_callback); + delete &data; + } +} diff --git a/panda/src/putil/completionCounter.h b/panda/src/putil/completionCounter.h new file mode 100644 index 0000000000..dbb0e2dcfb --- /dev/null +++ b/panda/src/putil/completionCounter.h @@ -0,0 +1,58 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionCounter.h + * @author rdb + * @date 2025-01-22 + */ + +#ifndef COMPLETIONCOUNTER_H +#define COMPLETIONCOUNTER_H + +#include "pandabase.h" +#include "completionToken.h" + +#include + +/** + * Shared counter that generates "completion tokens" incrementing a counter, + * which will decrement the counter once they are finished. After the tokens + * are handed out, a callback may be registered using then(), which will be + * called as soon as the last token is done. + */ +class EXPCL_PANDA_PUTIL CompletionCounter { +public: + constexpr CompletionCounter() = default; + CompletionCounter(const CompletionCounter ©) = delete; + + INLINE ~CompletionCounter(); + + INLINE CompletionToken make_token(); + + template + INLINE void then(Callable callable) &&; + +private: + static void initial_callback(Completable::Data *data, bool success); + static void abandon_callback(Completable::Data *data, bool success); + +protected: + struct CounterData : public Completable::Data { + // Least significant half is counter, most significant half is error count + patomic_signed_lock_free _counter { 0 }; + + // Just raise this if the static_assert fires (or limit the size of your + // lambda captures). + alignas(std::max_align_t) unsigned char _storage[64]; + }; + CounterData *_data = nullptr; +}; + +#include "completionCounter.I" + +#endif diff --git a/panda/src/putil/completionToken.I b/panda/src/putil/completionToken.I new file mode 100644 index 0000000000..aef06a7d4e --- /dev/null +++ b/panda/src/putil/completionToken.I @@ -0,0 +1,42 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionToken.I + * @author rdb + * @date 2025-01-22 + */ + +#ifndef CPPPARSER +/** + * Creates a token that calls the given callback when it's done, passing it + * true on success and false on failure or abandonment. + */ +template +INLINE CompletionToken:: +CompletionToken(Callable callback) { + // Main difference over a Completable is that this will always call the + // callback, even on failure, so that cleanup can be done. + _callback._data = new Completable::LambdaData(std::move(callback), [](Completable::Data *data, bool success) { + Completable::LambdaData *self = (Completable::LambdaData *)data; + std::move(self->_lambda)(success); + delete self; + }); +} +#endif + +/** + * + */ +INLINE void CompletionToken:: +complete(bool success) { + Completable::Data *data = _callback._data; + if (data != nullptr) { + _callback._data = nullptr; + data->_function.load(std::memory_order_relaxed)(data, success); + } +} diff --git a/panda/src/putil/completionToken.h b/panda/src/putil/completionToken.h new file mode 100644 index 0000000000..b73f4fe376 --- /dev/null +++ b/panda/src/putil/completionToken.h @@ -0,0 +1,56 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionToken.h + * @author rdb + * @date 2025-01-22 + */ + +#ifndef COMPLETIONTOKEN_H +#define COMPLETIONTOKEN_H + +#include "pandabase.h" +#include "pnotify.h" +#include "completable.h" + +/** + * A completion token can be created from a callback, future or + * CompletionCounter and can be passed into an asynchronous operation in order + * to receive a signal when it is done. + * + * The asynchronous operation should call complete() on it when it is done, + * with a boolean value indicating success or failure. If the token is + * destroyed prematurely, it is treated as if it called complete(false). + * + * This should be preferred over passing an AsyncFuture into a method since + * a CompletionToken provides both more flexibility in use (due to accepting + * an arbitrary callback) and more safety (since the RAII semantics guarantees + * that the callback is never silently dropped). + * + * The token may only be moved, not copied. + */ +class EXPCL_PANDA_PUTIL CompletionToken { +public: + constexpr CompletionToken() = default; + +#ifndef CPPPARSER + template + INLINE CompletionToken(Callable callback); +#endif + + void complete(bool success); + +protected: + Completable _callback; + + friend class CompletionCounter; +}; + +#include "completionToken.I" + +#endif diff --git a/panda/src/putil/p3putil_composite1.cxx b/panda/src/putil/p3putil_composite1.cxx index f78459eff7..c464d7708c 100644 --- a/panda/src/putil/p3putil_composite1.cxx +++ b/panda/src/putil/p3putil_composite1.cxx @@ -17,6 +17,7 @@ #include "callbackObject.cxx" #include "clockObject.cxx" #include "colorSpace.cxx" +#include "completionCounter.cxx" #include "config_putil.cxx" #include "configurable.cxx" #include "copyOnWriteObject.cxx"