Skip to content

Releases: landelare/ue5coro

1.9

20 Jun 04:11
Compare
Choose a tag to compare
1.9

The folder structure of the repository and releases changed, there are multiple plugins now.
Tested on Unreal Engine 5.1.1, 5.2.0, 5.2.1, with preliminary support for 5.3 and BuildSettingsVersion.V4.
README.md was updated with new installation instructions.

UE5Coro

  • Delegates are now directly co_awaitable. All of them. See the documentation for more details.
  • TCoroutine::WasSuccessful was added to complement IsDone. It's true if and only if the coroutine successfully ran to completion.
    • If you're using exceptions, an unhandled exception leads to IsDone() && !WasSuccessful().
  • UE5CoroGAS support.

Improvements:

  • Coroutines returning TCoroutine start a little faster and use slightly less memory.
  • Latent awaiters have marginally lower CPU overhead when co_awaited.

Fixes:

  • Fixed that coroutines returning TCoroutine and having multiple FForceLatentCoroutine parameters compiled. They weren't supposed to.
  • Worked around an MSVC regression in CoroutineHandleTest.cpp.

Deprecations:

  • TCoroutine::OnCompletion has been removed entirely.
  • VS2019 compatibility is no longer tested or maintained.
  • UE5.0 compatibility is no longer tested or maintained.

UE5CoroGAS

A brand-new and optional plugin to integrate with Unreal's Gameplay Ability System.
This plugin is currently experimental. Although it has been tested, its API is subject to change without deprecations for now.

  • UUE5CoroGameplayAbility: A coroutine-based UGameplayAbility implementation that deals with most lifetime control methods for you and translates them to and from native C++ coroutine behavior.
    • There's a new awaiter only available from this class (Task()) that lets you co_await many built-in tasks with a more convenient syntax than Latent::UntilDelegate.
  • UUE5CoroAbilityTask: The same, but for UAbilityTask.
  • UUE5CoroSimpleAbilityTask: A convenience base class with two premade delegates for coroutine success/failure.

All of these are drop-in replacements for their engine base classes. Normal coroutine behavior is tweaked to match what BP-authored abilities or tasks would do. There's a full list of these behavior changes in the documentation.

1.8

23 Apr 01:45
Compare
Choose a tag to compare
1.8

Tested against UE 5.0.3, 5.1.1, and 5.2.0p2.

Integrated cancellation support:

  • TCoroutine<>::Cancel requests the coroutine to cancel after the next co_await, clean up its locals, and return instead of resuming. The return value will be T().
    • This is processed automatically in every TCoroutine without you having to write additional code to handle being canceled.
      At the same time, existing code is unaffected because nothing could have called Cancel() yet.
  • co_await UE5Coro::FinishNowIfCanceled(); can be used in case there's nothing else to co_await, e.g., in a tight loop.
  • For advanced scenarios, the presence of a UE5Coro::FCancellationGuard delays the processing of incoming cancellation requests until the last one has gone out of scope. Making one on the first line of a coroutine body essentially opts it out of user-requested cancellation.
    • This does not affect cancellations caused by delete from the latent action manager.
    • Latent::Cancel() will check() if it's co_awaited with an active FCancellationGuard.
  • For even more advanced usage, UE5Coro::IsCurrentCoroutineCanceled() checks if there was an incoming cancellation request but does not process it.
    Do not use it to manually process cancellations (if (IsCurrentCoroutineCanceled()) co_return;), prefer FinishNowIfCanceled.

Additional features and improvements:

  • UE5Coro::Race() behaves similarly to WhenAny, but the first coroutine to finish cancels the others. Unlike WhenAny, this only works on TCoroutines, not anything awaitable.
  • TCoroutines are now valid keys for collections: they come with comparison operators (==, !=, < in C++17; ==, <=> in C++20), GetTypeHash, and a specialization of std::hash.
    • The order of TCoroutines is meaningless, but it is strict and total.
  • Added new static shortcuts on TCoroutine to get already-completed coroutines.
  • Latent::Cancel() is now free-threaded. The cancellation will happen on the game thread regardless of where it was co_awaited.
  • Async::MoveTo...Thread() is now more efficient if it targets the same thread it's already running on, continuing immediately and synchronously.
    • If this is not desired, Async::Yield() is added: it always suspends and resumes later on the same kind of thread that it was co_awaited on (game thread to game thread, AnyThread to AnyThread, high priority to high priority, etc.).
  • Latent coroutines no longer need to explicitly end on the game thread; this is now automatically done behind the scenes if needed. Note that locals' destructors run on the current thread BEFORE the coroutine ends.
  • If a latent coroutine does end (or self-cancel) on the game thread, its completion and cleanup are now processed immediately instead of on the latent action manager's next poll. Timings don't change for BP, but this improves responsiveness when ContinueWith is used or coroutines are chained with co_await.

Breaking/behavioral changes:

  • If a latent coroutine ends on the game thread, due to the immediate cleanup mentioned above, locals' destructors (notably, ON_SCOPE_EXIT) now run before the latent exec pin is triggered in BP, with their effects observable from BP.
  • Latent coroutines used to be able to co_await to other threads and run indefinitely after a latent action manager deleted their underlying BP latent action; it would only be processed once execution returned to the game thread.
    This will now get processed at the next co_await on ALL threads: if canceled by a delete, the coroutine will move back to the game thread instead of resuming and clean up there.
    FCancellationGuard does NOT guard against this: the latent action is gone and the coroutine would not be able to continue normally, leading to a memory leak that's avoided this way.
  • Behavior has slightly changed if a non-latent coroutine is co_awaiting a latent action (e.g., Chain()/ChainEx()) that the engine decides to delete early. This used to force cleanup of the awaiter coroutine no matter what, but it's now translated to a regular cancellation that's processed immediately and CAN be guarded against.
  • A coroutine's destruction is now assumed to be noexcept (i.e., destructors should not throw). This is highly unlikely to affect even code that uses exceptions, and completely irrelevant if UE's default exception settings are used.

Fixes:

  • Fixed UWorld async query awaiters using lvalue overloads even if an rvalue was co_awaited, resulting in an unnecessary copy of the result TArray.

Deprecations/removals:

  • This is the last version that's tested against UE 5.0 and the VS2019 toolchain.
  • The debug-only resume stack no longer exists and has been removed from UE5Coro.natvis. UE5Coro::Private::FPromise::Resume entries from the regular call stack can be used to obtain the same information.

1.7

15 Mar 18:07
Compare
Choose a tag to compare
1.7

Tested against UE 5.0.3 and 5.1.1, with preliminary support for 5.2.

Async coroutine overhaul:

  • Async coroutines can now co_return values. #7
  • The canonical return type of async coroutines is now UE5Coro::TCoroutine<T> for a coroutine co_returning T. T is optional. All of these implicitly convert to TCoroutine<>, meaning any return type or void.
  • co_awaiting a TCoroutine<T> results in T.
  • FAsyncCoroutine remains, but it's now nothing more than a USTRUCT wrapper for TCoroutine<>. It's still needed for latent UFUNCTIONs, etc. due to engine limitations. It implicitly converts from/to TCoroutine<>, which is the safer and preferred type.

Additional new features:

  • New animation-related awaiters for various montage events, notifies, etc. in the UE5Coro::Anim namespace.
  • ContinueWith and ContinueWithWeak are more robust, thread-safe replacements of OnCompletion() that handle the coroutine completing before they're called, and support return values. Functors for TCoroutine<T> can take no parameters, T, const T&, or anything that converts. Functors for TCoroutine<> must be parameterless. For ContinueWithWeak, the object may optionally be passed as the first parameter, enabling member functions to be bound.
  • FForceLatentCoroutine is a new USTRUCT that lets you force latent execution mode without having to specify a valid FLatentActionInfo.

Breaking/behavioral changes, deprecations:

  • If you're not using the convenient UE5Coro.h header, you'll probably need to adjust some of your #includes. Notably, you shouldn't be directly #including UE5Coro/AsyncCoroutine.h anymore.
  • TCoroutine<>::OnCompletion(), née FAsyncCoroutine is deprecated, will not receive support for coroutine return values, and will be removed in a future version.
  • TCoroutines now keep a part of the coroutine's state alive after it's completed, notably its return value, which dramatically increases runtime safety.
    This comes at the cost of some unfortunate extra overhead when starting (but not resuming) coroutines. The performance impact is negligible unless you're starting thousands per tick. If you do, TGenerator might be an alternative, which can additionally benefit from compiler HALO.

Fixes:

  • Further changes to WhenAny/WhenAll behavior: an awaiter or coroutine getting destroyed while being waited on this way counts as it completing, instead of the coroutine getting "stuck" and potentially leaking memory. This is not ideal, but there are engine/language limitations with exceptions not being available.
  • Fixed a crash when co_awaiting a TFuture that's already complete. #9
  • Fixed a crash when an exception is not caught and thrown out of a coroutine. You'll probably still crash, but not because of this anymore.
  • Latent::Timeline now clamps its Length parameter to SMALL_NUMBER to prevent a division by zero and any resulting NaNs. This is done silently by default. If ENABLE_NAN_DIAGNOSTIC is turned on, it will be a diagnostic message or ensure based on the value of GEnsureOnNANDiagnostic (i.e., how every other NaN diagnostic works). Negative lengths are treated the same way and replaced with positive SMALL_NUMBER instead.
  • Latent::Chain (C++20 only) is affected by a compiler bug on older versions of MSVC (VS2019), which will now generate a compile-time warning. Clang and newer versions of MSVC (VS2022) are not affected.

1.6.2

27 Feb 17:56
Compare
Choose a tag to compare
  • Latent coroutines now refuse to start with a null callback target (e.g. from a default-constructed FLatentActionInfo) instead of starting but unexpectedly not proceeding past (usually) the first co_await.
  • Latent coroutines that are destroyed on the game thread but are currently running on another thread (which is fully supported) are now cleaned up immediately when they return instead of soon after. This also provides more relevant call stacks.
  • Fixed a potential crash that can happen if a latent coroutine is destroyed while suspended and waiting for a TTask.
  • Fixed a memory leak and crash caused by calling FAsyncCoroutine::Wait with a finite timeout that does time out.
  • Fixed the Latent::Timeline family of coroutines passing incorrect values to the provided function.
  • Fixed an exotic false-positive ensure that can happen if exceptions are enabled.

1.6.1

07 Feb 10:19
Compare
Choose a tag to compare

Fixed the UE5Coro::TAwaitable concept (C++20 only) not telling lvalues and rvalues apart. TAwaitable<TFuture<int>&> is now correctly NOT satisfied, but TAwaitable<TFuture<int>> is.

The internal type UE5Coro::Private::FPromise (the base class of all FAsyncCoroutine promises) has received a "nice" incrementing ID in debug only. This is displayed by UE5Coro.natvis.

Fixed a crash bug in WhenAny and WhenAll. The fix required a breaking change in how they're used: noncopyable awaiters (mostly the ones returned by functions in the Latent namespace) will need to be explicitly moved into the call with MoveTemp/std::move/etc.

  • In C++20, invalid calls will generate a compiler error at the call site.
  • In C++17, invalid calls will hit a static_assert in WhenAny/WhenAll along with additional compiler errors coming from UE5Coro::Private. The error's notes hopefully contain the call site.

Note that WhenAny and WhenAll consume and co_await every input parameter; awaiters that aren't reusable may not be used after they're indirectly co_awaited this way. This hasn't changed.

As an example, the usual pattern to implement a timeout changes like so:

 using namespace UE5Coro;
 auto Awaiter = SomethingLengthy(); // lvalue for the sake of this example
-if (co_await WhenAny(Awaiter, Latent::Seconds(5)))
+if (co_await WhenAny(MoveTemp(Awaiter), Latent::Seconds(5)))
     TimedOut();

The return value of Latent::Seconds(5) is an rvalue to begin with in this example, so it doesn't need special treatment.

To better work with this new requirement, movability/copyability restrictions are relaxed in existing awaitables:

  • The return value of WhenAny/WhenAll is copyable and reusable. Additional co_awaits will continue immediately and synchronously.
  • The return value of Http::ProcessAsync is copyable and partially reusable. Copies are thread safe. Only one coroutine may co_await any copy of the original return value at the same time (this is check()ed), but once it has resumed, any copy may be co_awaited again and again to get the same HTTP result. This matters, e.g., if you attempt to co_await the result separately while it's also being held by a WhenAny.
  • The return value of AsyncLoadPackage is copyable and partially reusable in a similar way (one concurrent co_await, any number of additional ones once it's done), but it's NOT thread safe.
  • Async collision queries from the Latent namespace are copyable and partially reusable in the same way. Game thread only.
  • Attempting to co_await an invalid (e.g. moved-from) or spent TFuture<T> from the engine will check() earlier than it used to. As a reminder, co_awaiting a TFuture consumes it. TFuture itself from the engine is otherwise movable.
  • UE::Tasks::TTask<T> from the engine is copyable and may be co_awaited any number of times. Every co_await will continue after the original task has completed (immediately and synchronously without a task/thread switch if the TTask is already complete before co_await).
  • The return values of most other functions in the Latent namespace are movable and reusable. Additional co_awaits after the first one has completed will continue immediately and synchronously.

Additional awaiters are also relaxed for consistency, although it doesn't make much sense to pass these into WhenAny/WhenAll:

  • The return value of MoveToTask is copyable and reusable. Reusing it will co_await into a new Task every time.
  • The return values of MoveToThread and MoveToGameThread are copyable and reusable. Reusing them will co_await back into the same thread.
  • The return value of MoveToNewThread is copyable and reusable. Reusing it will co_await into a new thread every time.
  • The return value of Latent::Cancel is copyable and reusable. It will cancel any latent mode coroutine that co_awaits it if you decide to pass it around for some weird reason.

Moving from an awaiter of any type that's currently being co_awaited is undefined behavior.

1.6

31 Jan 10:01
Compare
Choose a tag to compare
1.6

TL;DR: coroutines and awaiters that aren't latent actions will ignore pause by default, unless respecting it makes more sense. Numerous new template and non-template async loading awaiters were added, and their co_await results are now more reliable.


This release mostly focuses on unifying async C++ behavior in various situations that aren't handled that clearly by the engine itself. Overall, the changes bring async behavior closer to what you would expect to see in a C++ program outside Unreal Engine and work better with language facilities such as RAII.

This results in multiple technically-breaking changes, but hopefully, most of them will change towards being more predictable and comfortable to use rather than the opposite.

  • Latent mode coroutines continue to respect their caller's tick-while-pause setting, exactly like BP latent nodes do. This hasn't changed.
  • Async mode coroutines themselves have always ignored pause; however they could await latent actions that respected it. Callees will now ignore pause when co_awaited from a non-latent async coroutine.
    This makes RealSeconds, RealTimeline, AsyncLoad, etc., more convenient to use as they won't get "stuck" until the game is unpaused.
  • Latent::Chain and ChainEx now always ignore pause.
  • Latent::UnpausedTimeline and RealTimeline now tick while paused, even if called from a latent mode coroutine that respects pause. They've received a new parameter to respect pause instead.
  • Latent::Timeline and AudioTimeline continue to respect pause, but this is now opt-out with a new parameter. If opted out, the provided function will be called with the same Alpha value since the underlying world times still respect pause.

Async loading received substantial changes and additions:

  • Numerous new functions and overloads were added for typed/untyped, scalar/array, primary asset/soft pointer, etc.
  • As a rule of thumb, if there's a templated and non-templated AsyncLoadSomething, the templated one will provide the loaded objects as the result when co_awaited, while the non-templated one will skip this step. (async class loading being an exception)
  • These functions now take a Priority parameter for extra control over loading.
  • Bugfix: the co_awaited results now more reliably and consistently match the functions' input parameters. A workaround that the engine's own async loading BP nodes use is performed internally; expect results consistent with those nodes.

Additional changes:

  • Latent::Chain and ChainEx will now resume the calling coroutine even if the chained latent action is canceled. Previously, canceled latent actions would simply abandon their caller (which remains the engine's built-in BP behavior).
  • co_awaiting Latent::Chain/ChainEx now results in a bool: true if the chained coroutine finished normally (e.g., it called FLatentResponse::FinishAndTriggerIf) and false if it didn't.
  • A few memory management bugs were fixed in the plugin itself that were triggered by unusual Latent::Chain usages (such as not co_awaiting the result and the latent action getting canceled).
  • Latent::Frames was removed entirely.

1.5

24 Dec 21:48
Compare
Choose a tag to compare
1.5

C++17 with the Coroutines TS is now supported.

1.4

16 Nov 11:06
Compare
Choose a tag to compare
1.4

Tested against UE 5.0, 5.1, with preliminary support for 5.2.
Note that 5.0 is still affected by UE-22342 a.k.a. UE-159823, which in turn affects latent actions.

Features:

  • TFuture is now directly co_awaitable. #include "UE5Coro/AsyncAwaiters.h"
  • co_awaiting WhenAny now returns the (0-based) index of the first awaitable that finished.
  • co_awaiting AsyncLoadObject and AsyncLoadClass now returns the loaded object.
    • AsyncLoadObject is now a template returning the same type as the type parameter to TSoftObjectPtr.
  • Latent::AsyncLoadPackage has been added.
  • Async::MoveToNewThread has been added. It lets you co_await into a newly-started thread with additional control over priority, affinity, etc. instead of using thread pools, without having to implement FRunnable yourself.
  • All 9 async collision queries from UWorld have received coroutine wrappers in the Latent namespace. co_awaiting these will return a result array, with possibly 0 or 1 element depending on the kind of query and its success.
  • Functions taking double time values (Seconds, Timeline) support diagnostic NaN checks (EnsureOnNaNFail cvar, ENABLE_NAN_DIAGNOSTIC)
  • Natvis and additional debug features when compiled with UE5CORO_DEBUG (default on for Debug and Development and off for Shipping, see UE5Coro/Definitions.h)
    • Most of these are deliberately undocumented without any expectation of stability across versions, see the beginning of UE5Coro::Private::FPromise.
    • FAsyncCoroutine::SetDebugName is a new static method that can be called from any coroutine returning FAsyncCoroutine to attach a debug string to it. To simplify caller code, it's available but has no effect when UE5CORO_DEBUG is off so that usages of it don't need to be guarded with #ifs.

Update notes and incompatibilities:

  • co_awaiting UE::Tasks might need #include "UE5Coro/TaskAwaiters.h" after this update to work if you're not using the UE5Coro.h meta-header.
  • Awaiters in the UE5Coro::Latent namespace came with an implicit promise that they'd react to the latent action being aborted within 1 tick and unwind your stack. This has been relaxed to reacting within 2 ticks (in practice, very often still 1).

1.3.2

08 Oct 18:19
Compare
Choose a tag to compare

Fix for global coroutines that have 0 parameters (FAsyncCoroutine NotInAClass();)

1.3.1

27 Sep 16:42
Compare
Choose a tag to compare

Fixed compiler errors on older versions of MSVC and Clang.