CoreCLR and .NET Modernization - Unite 2024

One is the consequence of the other. The GC doesn’t run every frame and the frequency it runs depends on the amount of garbage that is generated. Thus, the more allocations, the more frequently the GC kicks in. If your code doesn’t allocate at all every frame, for 1000 frames, the GC won’t be a problem.

That is neat, I had no idea. That changes the scenario a bit, at least to me. With that said, UniTask has been around at least since 2019 (that’s when I started using it) and based on what I see from Unity’s documentation (and from the Unity 2022 project I just tried to use Awaitable and couldn’t resolve it), it seems like the type was introduced in Unity 2023. Correct me if I’m wrong.

At work we are still using Unity 2021 due to legacy SDKs (thanks, Pico). On my personal projects, I stick to LTS, so I’m on 2022. It seems like UniTask (which has been extremely reliable, BTW) will still be a part of the code I write for some time.

2 Likes

Still on the Awaitable subject, I’ve got a question. Unity’s documentation says:

…once awaited, Unity returns the Awaitable object to the internal Awaitable pool. This has one significant implication: you should never await more than once on an Awaitable instance.

UniTask has a similar limitation. You can get around it with extra calls (that involve allocating heap memory), but that’s beside the point.

My question is: what happens if I try to access properties like Awaitable.IsCompleted after awaiting on it? The instance is back at the pool and could be inactive, or it could already be recycled.

first of all, i’ve never seen anyone await a task or anything multiple times, because that generally does not make sense in any case.
second, to answer your question: afaik it’s simply undefined behavior to await it multiple times, because it might or might not be recycled, meaning you’ll get an unfinished task or a result that might very well not be from your intended source, since it can be recycled for a different operation and thus contain that one’s result.

and yes, Awaitable is a more recent addition to Unity, but therefore it’s important to know of it going forward.

My question wasn’t about awaiting twice. I will quote myself here:

I only talked about awaiting twice because it was part Unity’s explanation about the pooling. Thinking about the pooling brought me to the question.

coroutines/awaitables related discussion should go into its own separate thread - just look at this thread’s title ^
(just sayin’)

6 Likes

In the Unite 2024 Roadmap video (at 30:37) they demonstrated the better debugging experience.

2 Likes

Mixed mode debugging works well, which is nice.

I haven’t seen anything about Edit and Continue (which usually works in managed-only debugging, but that’s ok). I’m asking specifically, because I’m not sure whether this works when CoreCLR is hosted in native executable.

Are there any other debugging limitations? @xoofx

1 Like

I salute you o/

(Having been in a similar situation of pushing for the obvious up a unreasonably gigantic mountain (however I gave up), I really felt this post).

4 posts were split to a new topic: Testing framework query

As far as I know, awaiting an Awaitable will return it to the pool. So I would assume that touching it in any way afterwards is invalid, including accessing “IsCompleted” (because something else may claim it from the pool, so the Awaitable object is no longer referring to the same operation).

…Which makes me wonder what happens if you don’t await an Awaitable (i.e. when storing several in a list and checking IsCompleted on them, which you may do with a Task). Does it just “leak”?

1 Like

This sounds super promising! Anything that is intuitive and makes programming more about doing stuff and less about scaffolding is a good friend to a game dev.

Does it have more relaxed constraints than jobs and burst? I don’t see how but it would be nice to not have to deal with the scaffolding of data formatting to speak with monos and the engine.

We tested this yesterday, because having access to an awaitable that has already been awaited be “undefined” seems quite dangerous. When you have awaited and awaitable you get a null reference exception if you try to access it again. The IsCompleted property forwards to a reference to the coroutine/awaitable as can be seen here: UnityCsReference/Runtime/Export/Scripting/AwaitableT.cs at 5e328f0a4a3b155f977c7be4c91899c06f0eca83 · Unity-Technologies/UnityCsReference · GitHub

If you never await (and never try to get the result) I would assume it just leaks yeah.

One thing I find a bit scary with awaitables though is exceptions and running on other threads.
Like the following:

private async void SomeFuncOnMainThreadTouchingUnityApi()
{
    var vectors = new List<Vector3>();
    try
    {
        vectors = await CalculateNewVectors();
    }
    catch (Exception)
    {
        // Log error and continue on, we have some safe backup
        // values we can use.
    }

    // If we threw an exception before running
    // Awaitable.MainThreadAsync, what thread are we on now. Is
    // this legal?
    SomeUnityApi(vectors);
}

private async Awaitable<List<Vector3>> CalculateNewVectors()
{
    var vectors = new List<Vector3>();
    await Awaitable.BackgroundThreadAsync();
    // call some function that ends up throwing

    await Awaitable.MainThreadAsync();

    return vectors;
}

Would have been nice to have some sort of scoped way of restoring the original context in these situations. You can do it with try finally, but that’s annoying. We tried seeing if we could set up an IDisposable wrapper, something like:

public struct ScopedBackgroundThread
    : IDisposable
{
     public void async Dispose()
    {
          await Awaitable.MainThreadAsync();
    }
}

You can’t have an async dispose function, so this doesn’t actually work.

2 Likes

It seems like the decision to make Awaitable a class instead of a struct will limit potential changes in the future. If it were a struct, it could still have the behavior it has now (by implementing it as a wrapper around an internal class), but it would also have been possible to store a “version” value inside it to detect misuse automatically. Since it’s a pooled reference type, you can’t really make use of techniques like that…

You can’t have an async dispose function, so this doesn’t actually work.

IAsyncDisposable exists for this purpose (and supports await using syntax), so you could play around with that.

3 Likes

Cool, I wasn’t aware of this interface. Unfortunately it only solves half the problem (at least based on my limited experiments and experience with C# stuff).

_
                public struct BackgroundThreadScope
                    : IAsyncDisposable
                {
                    // Can't because return value must be Task, Task<T>,
                    // task-like, IAsyncEnumerable<T>, or IAsyncEnumerator<T>
                    public static async BackgroundThreadScope Scope()
                    {
                        await Awaitable.BackgroundThreadAsync();
                        return new BackgroundThreadScope();
                    }

                    public async ValueTask DisposeAsync()
                    {
                        await Awaitable.MainThreadAsync();
                    }
                }

                public async void Func()
                {
                    // Would prefer to be able to use:
                    using var _ = BackgroundThreadScope.Scope();

                    // But guess we'll have to settle for:
                    await Awaitable.BackgroundThreadAsync();
                    await using var scope = new BackgroundThreadScope();
                }
1 Like

The best way to find out is to test the code. My guess would be that it would run on the background thread, because you haven’t asked it to switch back to the main thread yet. That seems to be in line with vanilla C#/.NET behavior with Task. If you would like to guarantee that it runs on the main thread, you could do something like this inside CalculateNewVectors:

try
{
    var vectors = new List<Vector3>();
    await Awaitable.BackgroundThreadAsync();
    // call some function that ends up throwing
}
finally
{
    await Awaitable.MainThreadAsync();
}
return vectors;

But again, testing is the best way to find out.

Hey @xoofx!

Could you please share your own impression on the difference between Mono and CoreCLR for the Editor at this moment? Is it totally a game changer? Is it comparable with other popular engines?
Does it meet your expectations?

What do other people usually say when they try it for the first time? Is it “Wooooh” or “Oh, it’s faster, isn’t it?”.

3 Likes

On this I would also love to know how fast is script compilation time, a nice before vs after on your system would be enough to give us an idea :slight_smile:

From the initial post it looks like they’ll switch from domain reload to code reload.

From my understanding, for small scripts, this will feel almost instantaneous.

What you’ve linked there has nothing to do with the direction they’re saying they are going in.

The technique for managing reloads is using AssemblyLoadContext. This was discussed in a previous thread and mentioned in The Unity Engine Roadmap from Unite 2024.

In this case, they still have to reload whole groups of assemblies, and the cooperative nature of the feature requires libraries to be compatible with this mechanism.

imager

What are they referring to here? Is ‘Code Reload’ just AssemblyLoadContext. Why do they call it Code Reload, makes it a bit confusing.