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

fix: exceptions sometimes thrown when accessing R/O meshes from preview callbacks #410

Merged
merged 1 commit into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- [#410] Added `NDMFSyncContext` API

### Fixed
- [#408] Improved performance of `BuildContext.Serialize`
- [#410] Sometimes R/O meshes cannot be accessed from preview context

### Changed
- [#408] Unserialized assets will be serialized after the Transforming phase completes (before e.g. VRCFury runs)
Expand Down
16 changes: 10 additions & 6 deletions Editor/ChangeStream/PropertyMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using nadena.dev.ndmf.preview;
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
Expand All @@ -19,12 +20,15 @@ internal class PropertyMonitor

internal void MaybeStartRefreshTimer()
{
_activeRefreshTask = Task.Factory.StartNew(
CheckAllObjectsLoop,
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()
);
using (var scope = NDMFSyncContext.Scope())
{
_activeRefreshTask = Task.Factory.StartNew(
CheckAllObjectsLoop,
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()
);
}
}

public enum PropertyMonitorEvent
Expand Down
19 changes: 11 additions & 8 deletions Editor/PreviewSystem/Rendering/ProxyPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,17 @@ public ProxyPipeline(ProxyObjectCache proxyCache, IEnumerable<IRenderFilter> fil
{
_generation = (priorPipeline?._generation ?? 0) + 1;
InvalidateAction = Invalidate;

_buildTask = Task.Factory.StartNew(
_ => Build(proxyCache, filters, priorPipeline),
null,
CancellationToken.None,
0,
TaskScheduler.FromCurrentSynchronizationContext()
).Unwrap();

using (var scope = NDMFSyncContext.Scope())
{
_buildTask = Task.Factory.StartNew(
_ => Build(proxyCache, filters, priorPipeline),
null,
CancellationToken.None,
0,
TaskScheduler.FromCurrentSynchronizationContext()
).Unwrap();
}
}

private async Task Build(ProxyObjectCache proxyCache, IEnumerable<IRenderFilter> filters,
Expand Down
3 changes: 3 additions & 0 deletions Editor/PreviewSystem/Task.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

171 changes: 171 additions & 0 deletions Editor/PreviewSystem/Task/NDMFSyncContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEditor;
using UnityEngine;

namespace nadena.dev.ndmf.preview
{
/// <summary>
/// The default unity synchronization context runs in the context of the Player Loop, which blocks access to R/O
/// mesh data. As such, NDMF provides a synchronization context which runs in the context of
/// `EditorApplication.delayCall` instead.
/// </summary>
public static class NDMFSyncContext
{
public static SynchronizationContext Context = new Impl();

/// <summary>
/// Switches to the NDMF synchronization context, and returns an IDisposable which will restore the prior
/// synchronization context.
/// </summary>
/// <returns></returns>
public static IDisposable Scope()
{
return new SyncContextScope();
}

private class SyncContextScope : IDisposable
{
private readonly SynchronizationContext _prior = SynchronizationContext.Current;

public SyncContextScope()
{
SynchronizationContext.SetSynchronizationContext(Context);
}

public void Dispose()
{
SynchronizationContext.SetSynchronizationContext(_prior);
}
}

private class Impl : SynchronizationContext
{
private readonly object _lock = new();
private readonly EditorApplication.CallbackFunction _turnDelegate;
private int unityMainThreadId = -1;
private readonly List<WorkRequest> asyncQueue = new();
private readonly List<WorkRequest> localQueue = new();
private bool isRegistered, isTurning;

internal Impl()
{
_turnDelegate = Turn;
}

// invoked under _lock
private void RegisterCallback()
{
if (isRegistered) return;
isRegistered = true;
EditorApplication.delayCall += _turnDelegate;
}

public void Turn()
{
lock (_lock)
{
unityMainThreadId = Thread.CurrentThread.ManagedThreadId;
localQueue.AddRange(asyncQueue);
asyncQueue.Clear();
isRegistered = false;
isTurning = true;
}

while (localQueue.Count > 0 && !TaskThrottle.ShouldThrottle)
{
foreach (var ev in localQueue)
{
ev.Run();
}

localQueue.Clear();

if (!TaskThrottle.ShouldThrottle)
{
lock (_lock)
{
localQueue.AddRange(asyncQueue);
asyncQueue.Clear();
}
}
}

lock (_lock)
{
if (localQueue.Count > 0)
{
RegisterCallback();
}

isTurning = false;
}
}

public override void Post(SendOrPostCallback d, object state)
{
lock (_lock)
{
asyncQueue.Add(new WorkRequest { callback = d, state = state });
RegisterCallback();
}
}

public override void Send(SendOrPostCallback d, object state)
{
ManualResetEvent wait = null;
var runLocally = false;
lock (_lock)
{
runLocally = unityMainThreadId == Thread.CurrentThread.ManagedThreadId && isTurning;
if (!runLocally)
{
wait = new ManualResetEvent(false);
asyncQueue.Add(new WorkRequest { callback = d, state = state, waitHandle = wait });
RegisterCallback();
}
}

if (runLocally)
{
try
{
d(state);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
else
{
wait.WaitOne();
}
}
}

private class WorkRequest
{
public SendOrPostCallback callback;
public object state;
public ManualResetEvent waitHandle;

public void Run()
{
try
{
callback(state);
}
catch (Exception e)
{
Debug.LogException(e);
}
finally
{
waitHandle?.Set();
}
}
}
}
}
3 changes: 3 additions & 0 deletions Editor/PreviewSystem/Task/NDMFSyncContext.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions Editor/PreviewSystem/TaskThrottle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ private static void Init()
}

private static int index;

public static bool ShouldThrottle
{
get
{
lock (_taskTime)
{
if (!_taskTime.IsRunning)
{
_taskTime.Start();
return false;
}

return _taskTime.ElapsedMilliseconds > TASK_TIME_LIMIT_MS;
}
}
}

public static async ValueTask MaybeThrottle()
{
Expand Down
Loading