Skip to content

Commit

Permalink
fix: exceptions sometimes thrown when accessing R/O meshes from previ…
Browse files Browse the repository at this point in the history
…ew callbacks (#410)

Fixes: #409
  • Loading branch information
bdunderscore authored Sep 22, 2024
1 parent e70e43b commit 64c96e6
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 14 deletions.
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

0 comments on commit 64c96e6

Please sign in to comment.