浏览代码
Partial progress: Following a trio of Relay+UTP samples, I seem to have something that will bind a transport to Relay and keep the connection open (as evident from the fact that a client can join after what would be the 10s timeout on Relay and successfully get the JoinAllocation, when they wouldn't without keeping the connection open even when binding was correct). However, this current code is very slapdash and sloppy; I'm just committing now as a very rough draft.
/main/staging
Partial progress: Following a trio of Relay+UTP samples, I seem to have something that will bind a transport to Relay and keep the connection open (as evident from the fact that a client can join after what would be the 10s timeout on Relay and successfully get the JoinAllocation, when they wouldn't without keeping the connection open even when binding was correct). However, this current code is very slapdash and sloppy; I'm just committing now as a very rough draft.
/main/staging
nathaniel.buck@unity3d.com
3 年前
当前提交
abd52623
共有 61 个文件被更改,包括 3258 次插入 和 18 次删除
-
34Assets/Scripts/Entities/GameStateManager.cs
-
7Assets/Scripts/LobbyRelaySample.asmdef
-
14Assets/Scripts/Relay/RelayInterface.cs
-
9Packages/packages-lock.json
-
402Assets/Scripts/Relay/RelayUtpSetup.cs
-
11Assets/Scripts/Relay/RelayUtpSetup.cs.meta
-
16ProjectSettings/BurstAotSettings_StandaloneWindows.json
-
6ProjectSettings/CommonBurstAotSettings.json
-
175Packages/com.unity.jobs/CHANGELOG.md
-
7Packages/com.unity.jobs/CHANGELOG.md.meta
-
4Packages/com.unity.jobs/Documentation~/TableOfContents.md
-
382Packages/com.unity.jobs/Documentation~/custom_job_types.md
-
20Packages/com.unity.jobs/Documentation~/filter.yml
-
4Packages/com.unity.jobs/Documentation~/images/pixel.png
-
5Packages/com.unity.jobs/Documentation~/index.md
-
36Packages/com.unity.jobs/Documentation~/scheduling_a_job_from_a_job.md
-
8Packages/com.unity.jobs/Editor.meta
-
32Packages/com.unity.jobs/Editor/CLILeakDetectionSwitcher.cs
-
13Packages/com.unity.jobs/Editor/CLILeakDetectionSwitcher.cs.meta
-
89Packages/com.unity.jobs/Editor/JobsMenu.cs
-
13Packages/com.unity.jobs/Editor/JobsMenu.cs.meta
-
11Packages/com.unity.jobs/Editor/Unity.Jobs.Editor.asmdef
-
7Packages/com.unity.jobs/Editor/Unity.Jobs.Editor.asmdef.meta
-
30Packages/com.unity.jobs/LICENSE.md
-
7Packages/com.unity.jobs/LICENSE.md.meta
-
8Packages/com.unity.jobs/Unity.Jobs.Tests.meta
-
8Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs.meta
-
88Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobStressTests.cs
-
11Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobStressTests.cs.meta
-
519Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobTests.cs
-
11Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobTests.cs.meta
-
156Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobTestsFixture.cs
-
11Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobTestsFixture.cs.meta
-
111Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobTests_CombineDependencies.cs
-
11Packages/com.unity.jobs/Unity.Jobs.Tests/ManagedJobs/JobTests_CombineDependencies.cs.meta
-
226Packages/com.unity.jobs/Unity.Jobs.Tests/NativeListDeferredArrayTests.cs
-
11Packages/com.unity.jobs/Unity.Jobs.Tests/NativeListDeferredArrayTests.cs.meta
-
164Packages/com.unity.jobs/Unity.Jobs.Tests/ParallelFilterJobTests.cs
-
12Packages/com.unity.jobs/Unity.Jobs.Tests/ParallelFilterJobTests.cs.meta
-
21Packages/com.unity.jobs/Unity.Jobs.Tests/Unity.Jobs.Tests.asmdef
-
7Packages/com.unity.jobs/Unity.Jobs.Tests/Unity.Jobs.Tests.asmdef.meta
-
10Packages/com.unity.jobs/Unity.Jobs.meta
-
52Packages/com.unity.jobs/Unity.Jobs/EarlyInitHelpers.cs
-
11Packages/com.unity.jobs/Unity.Jobs/EarlyInitHelpers.cs.meta
-
80Packages/com.unity.jobs/Unity.Jobs/IJobParallelForBatch.cs
-
13Packages/com.unity.jobs/Unity.Jobs/IJobParallelForBatch.cs.meta
-
175Packages/com.unity.jobs/Unity.Jobs/IJobParallelForDefer.cs
-
11Packages/com.unity.jobs/Unity.Jobs/IJobParallelForDefer.cs.meta
-
144Packages/com.unity.jobs/Unity.Jobs/IJobParallelForFilter.cs
-
12Packages/com.unity.jobs/Unity.Jobs/IJobParallelForFilter.cs.meta
-
12Packages/com.unity.jobs/Unity.Jobs/Unity.Jobs.asmdef
-
7Packages/com.unity.jobs/Unity.Jobs/Unity.Jobs.asmdef.meta
-
25Packages/com.unity.jobs/package.json
-
7Packages/com.unity.jobs/package.json.meta
|
|||
using LobbyRelaySample; |
|||
using System.Collections; |
|||
using Unity.Collections; |
|||
using Unity.Jobs; |
|||
using Unity.Networking.Transport; |
|||
using Unity.Networking.Transport.Relay; |
|||
using Unity.Services.Relay; |
|||
using Unity.Services.Relay.Allocations; |
|||
using Unity.Services.Relay.Models; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.Relay |
|||
{ |
|||
/// <summary>
|
|||
/// Responsible for setting up a connection with Relay using UTP, for the lobby host.
|
|||
/// Must be a MonoBehaviour since the binding process doesn't have asynchronous callback options.
|
|||
/// </summary>
|
|||
public abstract class RelayUTPSetup : MonoBehaviour |
|||
{ |
|||
// TODO: Eh, don't need to live here.
|
|||
unsafe protected static RelayAllocationId ConvertFromAllocationIdBytes(byte[] allocationIdBytes) |
|||
{ |
|||
fixed (byte* ptr = allocationIdBytes) |
|||
{ |
|||
return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length); |
|||
} |
|||
} |
|||
|
|||
unsafe protected static RelayConnectionData ConvertConnectionData(byte[] connectionData) |
|||
{ |
|||
fixed (byte* ptr = connectionData) |
|||
{ |
|||
return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length); |
|||
} |
|||
} |
|||
|
|||
unsafe protected static RelayHMACKey ConvertFromHMAC(byte[] hmac) |
|||
{ |
|||
fixed (byte* ptr = hmac) |
|||
{ |
|||
return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class RelayUtpSetup_Host : RelayUTPSetup |
|||
{ |
|||
private LocalLobby m_localLobby; |
|||
private Allocation m_allocation; |
|||
private bool m_isRelayConnected = false; |
|||
public NetworkDriver m_ServerDriver; |
|||
private NativeList<NetworkConnection> m_connections; |
|||
private JobHandle m_updateHandle; |
|||
|
|||
public void DoRelaySetup(LocalLobby localLobby) |
|||
{ |
|||
m_localLobby = localLobby; |
|||
RelayInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnAllocation); |
|||
} |
|||
|
|||
public void OnAllocation(Allocation allocation) |
|||
{ |
|||
m_allocation = allocation; |
|||
// RelayInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
|
|||
//}
|
|||
|
|||
//public void OnRelayCode(string relayCode)
|
|||
//{
|
|||
// m_localLobby.RelayCode = relayCode;
|
|||
// RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
|
|||
//}
|
|||
|
|||
//private void OnJoin(JoinAllocation allocation)
|
|||
//{
|
|||
|
|||
// // TODO: Use the ServerAddress?
|
|||
// m_localLobby.RelayServer = new ServerAddress(m_allocation.RelayServer.IpV4, m_allocation.RelayServer.Port);
|
|||
|
|||
NetworkEndPoint serverEndpoint = NetworkEndPoint.Parse(m_allocation.RelayServer.IpV4, (ushort)m_allocation.RelayServer.Port); |
|||
// UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them
|
|||
RelayAllocationId allocationId = ConvertFromAllocationIdBytes(m_allocation.AllocationIdBytes); |
|||
RelayConnectionData connectionData = ConvertConnectionData(m_allocation.ConnectionData); |
|||
RelayHMACKey key = ConvertFromHMAC(m_allocation.Key); |
|||
|
|||
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref connectionData, ref key); |
|||
relayServerData.ComputeNewNonce(); |
|||
var relayNetworkParameter = new RelayNetworkParameter { ServerData = relayServerData }; |
|||
|
|||
StartCoroutine(ServerBindAndListen(relayNetworkParameter, serverEndpoint)); |
|||
} |
|||
|
|||
private IEnumerator ServerBindAndListen(RelayNetworkParameter relayNetworkParameter, NetworkEndPoint serverEndpoint) |
|||
{ |
|||
// Create the NetworkDriver using the Relay parameters
|
|||
m_ServerDriver = NetworkDriver.Create(new INetworkParameter[] { relayNetworkParameter }); |
|||
m_connections = new NativeList<NetworkConnection>(16, Allocator.Persistent); |
|||
|
|||
// Bind the NetworkDriver to the local endpoint
|
|||
if (m_ServerDriver.Bind(NetworkEndPoint.AnyIpv4) != 0) |
|||
{ |
|||
Debug.LogError("Server failed to bind"); |
|||
} |
|||
else |
|||
{ |
|||
// The binding process is an async operation; wait until bound
|
|||
while (!m_ServerDriver.Bound) |
|||
{ |
|||
m_ServerDriver.ScheduleUpdate().Complete(); |
|||
yield return null; // TODO: Does this not proceed until a client connects as well?
|
|||
} |
|||
|
|||
// Once the driver is bound you can start listening for connection requests
|
|||
if (m_ServerDriver.Listen() != 0) |
|||
{ |
|||
Debug.LogError("Server failed to listen"); |
|||
yield break; |
|||
} |
|||
else |
|||
{ |
|||
Debug.LogWarning("Server is now listening!"); |
|||
m_isRelayConnected = true; |
|||
} |
|||
|
|||
//var serverConnection = driver.Connect(serverEndpoint);
|
|||
|
|||
//while (driver.GetConnectionState(serverConnection) == NetworkConnection.State.Connecting)
|
|||
//{
|
|||
// driver.ScheduleUpdate().Complete();
|
|||
// yield return null;
|
|||
//}
|
|||
//Debug.LogWarning("Should be good now?");
|
|||
|
|||
|
|||
|
|||
//// This successfully sends data, it seems, though it's just to other clients and not actually to the Relay service.
|
|||
//while (true)
|
|||
//{
|
|||
// yield return new WaitForSeconds(1);
|
|||
// DataStreamWriter writer = default;
|
|||
// if (driver.BeginSend(serverConnection, out writer) == 0)
|
|||
// {
|
|||
// writer.WriteByte(0);
|
|||
// driver.EndSend(writer);
|
|||
// Debug.LogWarning("Sent a byte");
|
|||
// }
|
|||
//}
|
|||
|
|||
RelayInterface.GetJoinCodeAsync(m_allocation.AllocationId, OnRelayCode); |
|||
} |
|||
} |
|||
|
|||
public void OnRelayCode(string relayCode) |
|||
{ |
|||
m_localLobby.RelayCode = relayCode; |
|||
//RelayInterface.JoinAsync(m_localLobby.RelayCode, OnRelayJoined);
|
|||
} |
|||
|
|||
private void OnRelayJoined(JoinAllocation allocation) |
|||
{ |
|||
StartCoroutine(DoRelayConnect(allocation)); |
|||
} |
|||
private IEnumerator DoRelayConnect(JoinAllocation allocation) |
|||
{ |
|||
NetworkEndPoint serverEndpoint = NetworkEndPoint.Parse(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port); |
|||
// UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them
|
|||
RelayAllocationId allocationId = ConvertFromAllocationIdBytes(allocation.AllocationIdBytes); |
|||
RelayConnectionData connectionData = ConvertConnectionData(allocation.ConnectionData); |
|||
RelayHMACKey key = ConvertFromHMAC(allocation.Key); |
|||
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref connectionData, ref key); |
|||
relayServerData.ComputeNewNonce(); |
|||
var relayNetworkParameter = new RelayNetworkParameter { ServerData = relayServerData }; |
|||
var driver = NetworkDriver.Create(new INetworkParameter[] { relayNetworkParameter }); |
|||
|
|||
var serverConnection = driver.Connect(serverEndpoint); |
|||
|
|||
Debug.LogWarning("Trying the relay connection now."); |
|||
|
|||
while (driver.GetConnectionState(serverConnection) == NetworkConnection.State.Connecting) |
|||
{ |
|||
driver.ScheduleUpdate().Complete(); |
|||
yield return null; |
|||
} |
|||
Debug.LogWarning("Should be good now?"); |
|||
|
|||
|
|||
|
|||
// This successfully sends data, it seems, though it's just to other clients and not actually to the Relay service.
|
|||
while (true) |
|||
{ |
|||
yield return new WaitForSeconds(1); |
|||
DataStreamWriter writer = default; |
|||
if (driver.BeginSend(serverConnection, out writer) == 0) |
|||
{ |
|||
writer.WriteByte(0); |
|||
driver.EndSend(writer); |
|||
Debug.LogWarning("Sent a byte"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
struct DriverUpdateJob : IJob |
|||
{ |
|||
public NetworkDriver driver; |
|||
public NativeList<NetworkConnection> connections; |
|||
|
|||
public void Execute() |
|||
{ |
|||
// Remove connections which have been destroyed from the list of active connections
|
|||
for (int i = 0; i < connections.Length; ++i) |
|||
{ |
|||
if (!connections[i].IsCreated) |
|||
{ |
|||
connections.RemoveAtSwapBack(i); |
|||
// Index i is a new connection since we did a swap back, check it again
|
|||
--i; |
|||
} |
|||
} |
|||
|
|||
// Accept all new connections
|
|||
while (true) |
|||
{ |
|||
var con = driver.Accept(); |
|||
// "Nothing more to accept" is signaled by returning an invalid connection from accept
|
|||
if (!con.IsCreated) |
|||
break; |
|||
connections.Add(con); |
|||
} |
|||
} |
|||
} |
|||
|
|||
static NetworkConnection ProcessSingleConnection(NetworkDriver.Concurrent driver, NetworkConnection connection) |
|||
{ |
|||
DataStreamReader strm; |
|||
NetworkEvent.Type cmd; |
|||
// Pop all events for the connection
|
|||
while ((cmd = driver.PopEventForConnection(connection, out strm)) != NetworkEvent.Type.Empty) |
|||
{ |
|||
if (cmd == NetworkEvent.Type.Data) |
|||
{ |
|||
// For ping requests we reply with a pong message
|
|||
int id = strm.ReadInt(); |
|||
// Create a temporary DataStreamWriter to keep our serialized pong message
|
|||
if (driver.BeginSend(connection, out var pongData) == 0) |
|||
{ |
|||
pongData.WriteInt(id); |
|||
// Send the pong message with the same id as the ping
|
|||
driver.EndSend(pongData); |
|||
} |
|||
} |
|||
else if (cmd == NetworkEvent.Type.Disconnect) |
|||
{ |
|||
// When disconnected we make sure the connection return false to IsCreated so the next frames
|
|||
// DriverUpdateJob will remove it
|
|||
return default(NetworkConnection); |
|||
} |
|||
} |
|||
|
|||
return connection; |
|||
} |
|||
|
|||
struct PongJob : Unity.Jobs.IJobParallelForDefer |
|||
{ |
|||
public NetworkDriver.Concurrent driver; |
|||
public NativeArray<NetworkConnection> connections; |
|||
|
|||
public void Execute(int i) |
|||
{ |
|||
connections[i] = ProcessSingleConnection(driver, connections[i]); |
|||
} |
|||
} |
|||
|
|||
private void Update() |
|||
{ |
|||
// When connecting to the relay we need to this?
|
|||
if (m_ServerDriver.IsCreated && !m_isRelayConnected) |
|||
{ |
|||
m_ServerDriver.ScheduleUpdate().Complete(); |
|||
|
|||
var updateJob = new DriverUpdateJob {driver = m_ServerDriver, connections = m_connections}; |
|||
updateJob.Schedule().Complete(); |
|||
} |
|||
} |
|||
|
|||
void LateUpdate() |
|||
{ |
|||
// On fast clients we can get more than 4 frames per fixed update, this call prevents warnings about TempJob
|
|||
// allocation longer than 4 frames in those cases
|
|||
if (m_ServerDriver.IsCreated && m_isRelayConnected) |
|||
m_updateHandle.Complete(); |
|||
} |
|||
|
|||
void FixedUpdate() |
|||
{ |
|||
if (m_ServerDriver.IsCreated && m_isRelayConnected) { |
|||
// Wait for the previous frames ping to complete before starting a new one, the Complete in LateUpdate is not
|
|||
// enough since we can get multiple FixedUpdate per frame on slow clients
|
|||
m_updateHandle.Complete(); |
|||
var updateJob = new DriverUpdateJob {driver = m_ServerDriver, connections = m_connections}; |
|||
var pongJob = new PongJob |
|||
{ |
|||
// PongJob is a ParallelFor job, it must use the concurrent NetworkDriver
|
|||
driver = m_ServerDriver.ToConcurrent(), |
|||
// PongJob uses IJobParallelForDeferExtensions, we *must* use AsDeferredJobArray in order to access the
|
|||
// list from the job
|
|||
connections = m_connections.AsDeferredJobArray() |
|||
}; |
|||
// Update the driver should be the first job in the chain
|
|||
m_updateHandle = m_ServerDriver.ScheduleUpdate(); |
|||
// The DriverUpdateJob which accepts new connections should be the second job in the chain, it needs to depend
|
|||
// on the driver update job
|
|||
m_updateHandle = updateJob.Schedule(m_updateHandle); |
|||
// PongJob uses IJobParallelForDeferExtensions, we *must* schedule with a list as first parameter rather than
|
|||
// an int since the job needs to pick up new connections from DriverUpdateJob
|
|||
// The PongJob is the last job in the chain and it must depends on the DriverUpdateJob
|
|||
m_updateHandle = pongJob.Schedule(m_connections, 1, m_updateHandle); |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
|
|||
public class RelayUtpSetup_Client : RelayUTPSetup |
|||
{ |
|||
LocalLobby m_localLobby; |
|||
|
|||
public void JoinRelay(LocalLobby localLobby) |
|||
{ |
|||
m_localLobby = localLobby; |
|||
localLobby.onChanged += OnLobbyChange; |
|||
} |
|||
|
|||
private void OnLobbyChange(LocalLobby lobby) |
|||
{ |
|||
if (m_localLobby.RelayCode != null) |
|||
{ |
|||
RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoin); |
|||
m_localLobby.onChanged -= OnLobbyChange; |
|||
} |
|||
} |
|||
|
|||
private void OnJoin(JoinAllocation allocation) |
|||
{ |
|||
if (allocation == null) |
|||
{ |
|||
// TODO: Error messaging.
|
|||
return; |
|||
} |
|||
|
|||
// Collect and convert the Relay data from the join response
|
|||
var serverEndpoint = NetworkEndPoint.Parse(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port); |
|||
var allocationId = ConvertFromAllocationIdBytes(allocation.AllocationIdBytes); |
|||
var connectionData = ConvertConnectionData(allocation.ConnectionData); |
|||
var hostConnectionData = ConvertConnectionData(allocation.HostConnectionData); |
|||
var key = ConvertFromHMAC(allocation.Key); |
|||
|
|||
// Prepare the RelayNetworkParameter
|
|||
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref hostConnectionData, ref key); |
|||
relayServerData.ComputeNewNonce(); |
|||
|
|||
var relayNetworkParameter = new RelayNetworkParameter { ServerData = relayServerData }; |
|||
StartCoroutine(ServerBindAndListen(relayNetworkParameter, serverEndpoint)); |
|||
} |
|||
|
|||
private IEnumerator ServerBindAndListen(RelayNetworkParameter relayNetworkParameter, NetworkEndPoint serverEndpoint) |
|||
{ |
|||
var driver = NetworkDriver.Create(new INetworkParameter[] { relayNetworkParameter }); |
|||
|
|||
// Bind the NetworkDriver to the available local endpoint.
|
|||
// This will send the bind request to the Relay server
|
|||
if (driver.Bind(NetworkEndPoint.AnyIpv4) != 0) |
|||
{ |
|||
Debug.LogError("Client failed to bind"); |
|||
} |
|||
else |
|||
{ |
|||
while (!driver.Bound) |
|||
{ |
|||
driver.ScheduleUpdate().Complete(); |
|||
yield return null; |
|||
} |
|||
|
|||
// Once the client is bound to the Relay server, you can send a connection request
|
|||
var serverConnection = driver.Connect(serverEndpoint); |
|||
|
|||
while (driver.GetConnectionState(serverConnection) == NetworkConnection.State.Connecting) |
|||
{ |
|||
driver.ScheduleUpdate().Complete(); |
|||
yield return null; |
|||
} |
|||
|
|||
if (driver.GetConnectionState(serverConnection) != NetworkConnection.State.Connected) |
|||
{ |
|||
Debug.LogError("Client failed to connect to server"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 5857e5666b1ecf844b8280729adb6e6e |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"MonoBehaviour": { |
|||
"Version": 3, |
|||
"EnableBurstCompilation": true, |
|||
"EnableOptimisations": true, |
|||
"EnableSafetyChecks": false, |
|||
"EnableDebugInAllBuilds": false, |
|||
"UsePlatformSDKLinker": false, |
|||
"CpuMinTargetX32": 0, |
|||
"CpuMaxTargetX32": 0, |
|||
"CpuMinTargetX64": 0, |
|||
"CpuMaxTargetX64": 0, |
|||
"CpuTargetsX32": 6, |
|||
"CpuTargetsX64": 72 |
|||
} |
|||
} |
|
|||
{ |
|||
"MonoBehaviour": { |
|||
"Version": 3, |
|||
"DisabledWarnings": "" |
|||
} |
|||
} |
|
|||
# Change log |
|||
|
|||
## [0.10.0] - 2021-03-15 |
|||
|
|||
### Changed |
|||
|
|||
* `IJobParallelForDeferExtensions.Schedule` generic `U` constraint from `struct` to `unmanaged`. |
|||
- Updated dependencies for using com.unity.burst@1.5.3 |
|||
|
|||
|
|||
|
|||
## [0.9.0] - 2021-01-26 |
|||
|
|||
### Added |
|||
|
|||
* `CLILeakDetectionSwitcher` to be used by Yamato. |
|||
|
|||
### Changed |
|||
|
|||
* Update minimum editor version to 2020.2.1f1-dots.3 |
|||
|
|||
|
|||
|
|||
## [0.8.0] - 2020-11-13 |
|||
|
|||
### Changed |
|||
|
|||
* Update minimum editor version to 2020.1.2f1 |
|||
*Added tests for generic jobs. |
|||
|
|||
## [0.7.0] - 2020-09-24 |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
## [0.6.0] - 2020-08-26 |
|||
|
|||
* Updated dependencies of this package. |
|||
## [0.5.0] - 2020-08-04 |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
## [0.4.0] - 2020-07-10 |
|||
|
|||
|
|||
### Changed |
|||
|
|||
* Updated minimum Unity Editor version to 2020.1.0b15 (40d9420e7de8) |
|||
|
|||
### Known Issues |
|||
|
|||
* This version is not compatible with 2020.2.0a17. Please update to the forthcoming alpha. |
|||
|
|||
## [0.3.0] - 2020-05-27 |
|||
|
|||
### Changed |
|||
|
|||
* Updated minimum Unity Editor version to 2020.1.0b9 (9c0aec301c8d) |
|||
|
|||
## [0.2.10] - 2020-05-04 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.9] - 2020-04-24 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.8] - 2020-04-08 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.7] - 2020-03-13 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
* The internals of IJobParallelForFilter are now `internal` rather than `public` |
|||
|
|||
|
|||
## [0.2.6] - 2020-03-03 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
* Maintain JobsDebugger menu item value between sessions. |
|||
|
|||
|
|||
## [0.2.5] - 2020-02-17 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.4] - 2020-01-28 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.3] - 2020-01-16 |
|||
|
|||
### Changed |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.2] - 2019-12-16 |
|||
|
|||
**This version requires Unity 2019.3.0f1+** |
|||
|
|||
### Changes |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.1] - 2019-12-03 |
|||
|
|||
### Changes |
|||
|
|||
* Updated dependencies of this package. |
|||
|
|||
|
|||
## [0.2.0] - 2019-11-22 |
|||
|
|||
**This version requires Unity 2019.3 0b11+** |
|||
|
|||
### Changes |
|||
|
|||
* Updated dependencies for this package. |
|||
|
|||
|
|||
## [0.1.1] - 2019-08-06 |
|||
|
|||
### Changes |
|||
|
|||
* Updated dependencies for this package. |
|||
|
|||
|
|||
## [0.1.0] - 2019-07-30 |
|||
|
|||
### Changes |
|||
|
|||
* Updated dependencies for this package. |
|||
|
|||
|
|||
## [0.0.7-preview.13] - 2019-05-24 |
|||
|
|||
### Changes |
|||
|
|||
* Updated dependency for `com.unity.collections` |
|||
|
|||
|
|||
## [0.0.7-preview.12] - 2019-05-16 |
|||
|
|||
### New Features |
|||
|
|||
* IJobParallelForDeferred has been added to allow a parallel for job to be scheduled even if it's for each count will only be known during another jobs execution. |
|||
|
|||
### Upgrade guide |
|||
* Previously IJobParallelFor had a overload with the same IJobParallelForDeferred functionality. This is no longer supported since it was not working in Standalone builds using Burst. Now you need to explicitly implement IJobParallelForDeferred if you want to use the deferred schedule parallel for. |
|||
|
|||
|
|||
## [0.0.7-preview.11] - 2019-05-01 |
|||
|
|||
Change tracking started with this version. |
|
|||
fileFormatVersion: 2 |
|||
guid: ce949e15b369542f1991c23589a224fc |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
<!-- Generated from ../Samples/Packages/com.unity.jobs/Documentation~/toc.yml do not edit. --> |
|||
* [Jobs package](index.md) |
|||
* [Custom job types](custom_job_types.md) |
|||
* [Scheduling a job from a job](scheduling_a_job_from_a_job.md) |
|
|||
--- |
|||
uid: custom-job-types |
|||
--- |
|||
# Custom job types |
|||
|
|||
On the lowest level of the job system, jobs are scheduled by calling one of the `Schedule` functions in [JobsUtility](https://docs.unity3d.com/ScriptReference/Unity.Jobs.LowLevel.Unsafe.JobsUtility.html). The currently existing [job types](https://docs.unity3d.com/ScriptReference/Unity.Jobs.LowLevel.Unsafe.JobType.html) all use these functions, but it is also possible to create specialized job types using the same APIs. |
|||
|
|||
These APIs use unsafe code and have to be crafted carefully, since they can easily introduce unwanted race conditions. If you add your own job types, we strongly recommend to aim for full test coverage. |
|||
|
|||
As an example we have a custom job type `IJobParallelForBatch` (see file: _/Packages/com.unity.jobs/Unity.Jobs/IJobParallelForBatch.cs_). |
|||
|
|||
It works like __IJobParallelFor__, but instead of calling a single execute function per index it calls one execute function per batch being executed. This is useful if you need to do something on more than one item at a time, but still want to do it in parallel. A common scenario for this job type is if you need to create a temporary array and you want to avoid creating each item in the array one at a time. By using IJobParallelFor you can instead create one temporary array per batch. |
|||
|
|||
In the IJobParallelForBatch example, the entry point where the job is actually scheduled looks like this: |
|||
|
|||
```c# |
|||
unsafe static public JobHandle ScheduleBatch<T>(this T jobData, int arrayLength, int minIndicesPerJobCount, JobHandle dependsOn = new JobHandle()) where T : struct, IJobParallelForBatch |
|||
{ |
|||
var scheduleParams = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref jobData), ParallelForBatchJobStruct<T>.Initialize(), dependsOn, ScheduleMode.Batched); |
|||
return JobsUtility.ScheduleParallelFor(ref scheduleParams, arrayLength, minIndicesPerJobCount); |
|||
} |
|||
``` |
|||
|
|||
The first line creates a struct containing the scheduling parameters. When creating it you need to set a pointer to the data which will be copied to the jobs. The reason this is a pointer is that the native code which uses it does not know about the type. |
|||
You also need to pass it a pointer to the __JobReflectionData__ created by calling: |
|||
|
|||
```c# |
|||
JobsUtility.CreateJobReflectionData(typeof(T), JobType.ParallelFor, (ExecuteJobFunction)Execute); |
|||
``` |
|||
|
|||
JobReflection stores information about the struct with the data for the job, such as which __NativeContainers__ it has and how they need to be patched when scheduling a job. It lives on the native side of the engine and the managed code only has access to it though pointers without any information about what the type is. When creating JobReflectionData you need to specify the type of the struct implementing the job, the __JobType__ and the method which will be called to execute the job. The JobReflectionData does not depend on the data in the struct you schedule, only its type, so it should only be created once for all jobs implementing the same interface. There are currently only two job types, __Single__ and __ParallelFor__. Single means the job will only get a single call, ParallelFor means there will be multiple calls to process it; where each call is restricted to a subset of the range of indices to process. Which job type you choose affects which schedule function you are allowed to call. |
|||
|
|||
The third parameter of __JobsUtility.JobScheduleParameters__ is the __JobHandle__ that the scheduled job should depend on. |
|||
|
|||
The final parameter is the schedule mode. There are two scheduling modes to choose from, __Run__ and __Batched__. Batched means one or more jobs will be scheduled to do the processing, while Run means the processing will be done on the main thread before Schedule returns. |
|||
|
|||
Once the schedule parameters are created we actually schedule the job. There are three ways to schedule jobs depending on their type: |
|||
|
|||
```c# |
|||
JobHandle Schedule(ref JobScheduleParameters parameters); |
|||
JobHandle ScheduleParallelFor(ref JobScheduleParameters parameters, int arrayLength, int innerLoopBatchCount); |
|||
JobHandle ScheduleParallelForTransform(ref JobScheduleParameters parameters, IntPtr transfromAccessArray); |
|||
|
|||
``` |
|||
|
|||
Schedule can only be used if the __ScheduleParameters__ are created with __JobType.Single__, the other two schedule functions require __JobType.ParallelFor__. |
|||
The __arrayLength__ and __innerLoopBatchCount__ parameter passed to __ScheduleParallelFor__ are used to determine how many indices the jobs should process and how many indices it should handle in the inner loop (see the section on [Execution and JobRanges](#execution-and-jobranges) for more information on the inner loop count). |
|||
__ScheduleParallelForTransform__ is similar to ScheduleParallelFor, but it also has access to a __TransformAccessArray__ that allows you to modify __Transform__ components on __GameObjects__. The number of indices and batch size is inferred from the TransformAccessArray. |
|||
|
|||
## Execution and JobRanges |
|||
|
|||
After scheduling the job, Unity will call the entry point you specified directly from the native side. It works in a similar way to how __Update__ is called on MonoBehaviours, but from inside a job instead. You only get one call per job and there is either one job, or one job per worker thread; in the case of ParallelFor. |
|||
|
|||
The signature used for Execute is |
|||
|
|||
```c# |
|||
public delegate void ExecuteJobFunction(ref T data, IntPtr additionalPtr, IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex); |
|||
``` |
|||
For Single jobs, only the data is needed and you can just do your processing right away, but for ParallelFor jobs it requires some more work before you can start processing indices. We need to split up the indices into a number of sequential sub-sets that each job will process in parallel. This way we do not process the same thing twice and we are sure that everything gets covered. The memory layout will determine the order of indices. |
|||
|
|||
The JobRanges contain the batches and indices a ParallelFor job is supposed to process. The indices are split into batches based on the batch size, the batches are evenly distributed between the jobs doing the execution in such a way that each job can iterate over a continuous section of memory. The ParallelFor job should call: |
|||
|
|||
```c# |
|||
JobsUtility.GetWorkStealingRange(ref ranges, jobIndex, out begin, out end) |
|||
``` |
|||
|
|||
This continues until it returns `false`, and after calling it process all items with index between __begin__ and __end__. |
|||
The reason you get batches of items, rather than the full set of items the job should process, is that Unity will apply [work stealing](https://en.wikipedia.org/wiki/Work_stealing) if one job completes before the others. Work stealing in this context means that when one job is done it will look at the other jobs running and see if any of them still have a lot of work left. If it finds a job which is not complete it will steal some of the batches that it has not yet started; to dynamically redistribute the work. |
|||
|
|||
Before a ParallelFor job starts processing items it also needs to limit the write access to NativeContainers on the range of items which the job is processing. If it does not do this several jobs can potentially write to the same index which leads to race conditions. The NativeContainers that need to be limited is passed to the job and there is a function to patch them; so they cannot access items outside the correct range. The code to do it looks like this: |
|||
|
|||
```c# |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
JobsUtility.PatchBufferMinMaxRanges(bufferRangePatchData, UnsafeUtility.AddressOf(ref jobData), begin, end - begin); |
|||
#endif |
|||
``` |
|||
|
|||
# Custom NativeContainers |
|||
|
|||
When writing jobs, the data communication between jobs is one of the hardest parts to get right. Just using __NativeArray__ is very limiting. Using __NativeQueue__, __NativeHashMap__ and __NativeMultiHashMap__ and their __Concurrent__ versions solves most scenarios. |
|||
|
|||
For the remaining scenarios it is possible to write your own custom NativeContainers. |
|||
When writing custom containers for [thread synchronization](https://en.wikipedia.org/wiki/Synchronization_(computer_science)#Thread_or_process_synchronization) it is very important to write correct code. We strongly recommend full test coverage for any new containers you add. |
|||
|
|||
As a very simple example of this we will create a __NativeCounter__ that can be incremented in a ParallelFor job through __NativeCounter.Concurrent__ and read in a later job or on the main thread. |
|||
|
|||
Let's start with the basic container type: |
|||
|
|||
```c# |
|||
// Mark this struct as a NativeContainer, usually this would be a generic struct for containers, but a counter does not need to be generic |
|||
// TODO - why does a counter not need to be generic? - explain the argument for this reasoning please. |
|||
[StructLayout(LayoutKind.Sequential)] |
|||
[NativeContainer] |
|||
unsafe public struct NativeCounter |
|||
{ |
|||
// The actual pointer to the allocated count needs to have restrictions relaxed so jobs can be schedled with this container |
|||
[NativeDisableUnsafePtrRestriction] |
|||
int* m_Counter; |
|||
|
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle m_Safety; |
|||
// The dispose sentinel tracks memory leaks. It is a managed type so it is cleared to null when scheduling a job |
|||
// The job cannot dispose the container, and no one else can dispose it until the job has run, so it is ok to not pass it along |
|||
// This attribute is required, without it this NativeContainer cannot be passed to a job; since that would give the job access to a managed object |
|||
[NativeSetClassTypeToNullOnSchedule] |
|||
DisposeSentinel m_DisposeSentinel; |
|||
#endif |
|||
|
|||
// Keep track of where the memory for this was allocated |
|||
Allocator m_AllocatorLabel; |
|||
|
|||
public NativeCounter(Allocator label) |
|||
{ |
|||
// This check is redundant since we always use an int that is blittable. |
|||
// It is here as an example of how to check for type correctness for generic types. |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
if (!UnsafeUtility.IsBlittable<int>()) |
|||
throw new ArgumentException(string.Format("{0} used in NativeQueue<{0}> must be blittable", typeof(int))); |
|||
#endif |
|||
m_AllocatorLabel = label; |
|||
|
|||
// Allocate native memory for a single integer |
|||
m_Counter = (int*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<int>(), 4, label); |
|||
|
|||
// Create a dispose sentinel to track memory leaks. This also creates the AtomicSafetyHandle |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
DisposeSentinel.Create(out m_Safety, out m_DisposeSentinel, 0); |
|||
#endif |
|||
// Initialize the count to 0 to avoid uninitialized data |
|||
Count = 0; |
|||
} |
|||
|
|||
public void Increment() |
|||
{ |
|||
// Verify that the caller has write permission on this data. |
|||
// This is the race condition protection, without these checks the AtomicSafetyHandle is useless |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); |
|||
#endif |
|||
(*m_Counter)++; |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
// Verify that the caller has read permission on this data. |
|||
// This is the race condition protection, without these checks the AtomicSafetyHandle is useless |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckReadAndThrow(m_Safety); |
|||
#endif |
|||
return *m_Counter; |
|||
} |
|||
set |
|||
{ |
|||
// Verify that the caller has write permission on this data. This is the race condition protection, without these checks the AtomicSafetyHandle is useless |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); |
|||
#endif |
|||
*m_Counter = value; |
|||
} |
|||
} |
|||
|
|||
public bool IsCreated |
|||
{ |
|||
get { return m_Counter != null; } |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
// Let the dispose sentinel know that the data has been freed so it does not report any memory leaks |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
DisposeSentinel.Dispose(m_Safety, ref m_DisposeSentinel); |
|||
#endif |
|||
|
|||
UnsafeUtility.Free(m_Counter, m_AllocatorLabel); |
|||
m_Counter = null; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
With this we have a simple NativeContainer where we can get, set, and increment the count. This container can be passed to a job, but it has the same restrictions as NativeArray, which means it cannot be passed to a ParallelFor job with write access. |
|||
|
|||
The next step is to make it usable in ParallelFor. In order to avoid race conditions we want to make sure no-one else is reading it while the ParallelFor is writing to it. To achieve this we create a separate inner struct called Concurrent that can handle multiple writers, but no readers. We make sure NativeCounter.Concurrent can be assigned to from within a normal NativeCounter, since it is not possible for it to live separately outside a NativeCounter. <!-- TODO - why is that? --> |
|||
|
|||
```c# |
|||
[NativeContainer] |
|||
// This attribute is what makes it possible to use NativeCounter.Concurrent in a ParallelFor job |
|||
[NativeContainerIsAtomicWriteOnly] |
|||
unsafe public struct Concurrent |
|||
{ |
|||
// Copy of the pointer from the full NativeCounter |
|||
[NativeDisableUnsafePtrRestriction] |
|||
int* m_Counter; |
|||
|
|||
// Copy of the AtomicSafetyHandle from the full NativeCounter. The dispose sentinel is not copied since this inner struct does not own the memory and is not responsible for freeing it. |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle m_Safety; |
|||
#endif |
|||
|
|||
// This is what makes it possible to assign to NativeCounter.Concurrent from NativeCounter |
|||
public static implicit operator NativeCounter.Concurrent (NativeCounter cnt) |
|||
{ |
|||
NativeCounter.Concurrent concurrent; |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(cnt.m_Safety); |
|||
concurrent.m_Safety = cnt.m_Safety; |
|||
AtomicSafetyHandle.UseSecondaryVersion(ref concurrent.m_Safety); |
|||
#endif |
|||
|
|||
concurrent.m_Counter = cnt.m_Counter; |
|||
return concurrent; |
|||
} |
|||
|
|||
public void Increment() |
|||
{ |
|||
// Increment still needs to check for write permissions |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); |
|||
#endif |
|||
// The actual increment is implemented with an atomic, since it can be incremented by multiple threads at the same time |
|||
Interlocked.Increment(ref *m_Counter); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
With this setup we can schedule ParallelFor with write access to a NativeCounter through the inner Concurrent struct, like this: |
|||
|
|||
```c# |
|||
struct CountZeros : IJobParallelFor |
|||
{ |
|||
[ReadOnly] |
|||
public NativeArray<int> input; |
|||
public NativeCounter.Concurrent counter; |
|||
public void Execute(int i) |
|||
{ |
|||
if (input[i] == 0) |
|||
{ |
|||
counter.Increment(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```c# |
|||
var counter = new NativeCounter(Allocator.Temp); |
|||
var jobData = new CountZeros(); |
|||
jobData.input = input; |
|||
jobData.counter = counter; |
|||
counter.Count = 0; |
|||
|
|||
var handle = jobData.Schedule(input.Length, 8); |
|||
handle.Complete(); |
|||
|
|||
Debug.Log("The array countains " + counter.Count + " zeros"); |
|||
counter.Dispose(); |
|||
``` |
|||
|
|||
## Better cache usage |
|||
|
|||
The NativeCounter from the previous section is a working implementation of a counter, but all jobs in the ParallelFor will access the same atomic to increment the value. This is not optimal as it means the same cache line is used by all threads. |
|||
The way this is generally solved in NativeContainers is to have a local cache per worker thread, which is stored on its own cache line. |
|||
|
|||
The __[NativeSetThreadIndex]__ attribute can inject a worker thread index, the index is guaranteed to be unique while accessing the NativeContainer from the ParallelFor jobs. |
|||
|
|||
In order to make such an optimization here we need to change a few things. The first thing we need to change is the data layout. For performance reasons we need one full cache line per worker thread, rather than a single int to avoid [false sharing](https://en.wikipedia.org/wiki/False_sharing). |
|||
|
|||
We start by adding a constant for the number of ints on a cache line. |
|||
|
|||
```c# |
|||
public const int IntsPerCacheLine = JobsUtility.CacheLineSize / sizeof(int); |
|||
``` |
|||
|
|||
Next we change the amount of memory allocated. |
|||
|
|||
```c# |
|||
// One full cache line (integers per cacheline * size of integer) for each potential worker index, JobsUtility.MaxJobThreadCount |
|||
m_Counter = (int*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<int>()*IntsPerCacheLine*JobsUtility.MaxJobThreadCount, 4, label); |
|||
``` |
|||
|
|||
<!-- TODO: I'm not sure which example you are referring to when you say: main, non-concurrent, version below (is this an example you used on this page or what you would do if you were not using jobified code/ECS etc. It has potential for confusion.) --> |
|||
|
|||
When accessing the counter from the main, non-concurrent, version there can only be one writer so the increment function is fine with the new memory layout. |
|||
For `get` and `set` of the `count` we need to loop over all potential worker indices. |
|||
|
|||
```c# |
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
// Verify that the caller has read permission on this data. |
|||
// This is the race condition protection, without these checks the AtomicSafetyHandle is useless |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckReadAndThrow(m_Safety); |
|||
#endif |
|||
int count = 0; |
|||
for (int i = 0; i < JobsUtility.MaxJobThreadCount; ++i) |
|||
count += m_Counter[IntsPerCacheLine * i]; |
|||
return count; |
|||
} |
|||
set |
|||
{ |
|||
// Verify that the caller has write permission on this data. |
|||
// This is the race condition protection, without these checks the AtomicSafetyHandle is useless |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); |
|||
#endif |
|||
// Clear all locally cached counts, |
|||
// set the first one to the required value |
|||
for (int i = 1; i < JobsUtility.MaxJobThreadCount; ++i) |
|||
m_Counter[IntsPerCacheLine * i] = 0; |
|||
*m_Counter = value; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The final change is the inner Concurrent struct that needs to get the worker index injected into it. Since each worker only runs one job at a time, there is no longer any need to use atomics when only accessing the local count. |
|||
|
|||
```c# |
|||
[NativeContainer] |
|||
[NativeContainerIsAtomicWriteOnly] |
|||
// Let the job system know that it should inject the current worker index into this container |
|||
unsafe public struct Concurrent |
|||
{ |
|||
[NativeDisableUnsafePtrRestriction] |
|||
int* m_Counter; |
|||
|
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle m_Safety; |
|||
#endif |
|||
|
|||
// The current worker thread index; it must use this exact name since it is injected |
|||
[NativeSetThreadIndex] |
|||
int m_ThreadIndex; |
|||
|
|||
public static implicit operator NativeCacheCounter.Concurrent (NativeCacheCounter cnt) |
|||
{ |
|||
NativeCacheCounter.Concurrent concurrent; |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(cnt.m_Safety); |
|||
concurrent.m_Safety = cnt.m_Safety; |
|||
AtomicSafetyHandle.UseSecondaryVersion(ref concurrent.m_Safety); |
|||
#endif |
|||
|
|||
concurrent.m_Counter = cnt.m_Counter; |
|||
concurrent.m_ThreadIndex = 0; |
|||
return concurrent; |
|||
} |
|||
|
|||
public void Increment() |
|||
{ |
|||
#if ENABLE_UNITY_COLLECTIONS_CHECKS |
|||
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); |
|||
#endif |
|||
// No need for atomics any more since we are just incrementing the local count |
|||
++m_Counter[IntsPerCacheLine*m_ThreadIndex]; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Writing the NativeCounter this way significantly reduces the overhead of having multiple threads writing to it. It does, however, come at a price. The cost of getting the count on the main thread has increased significantly since it now needs to check all local caches and sum them up. If you are aware of this and make sure to cache the return values it is usually worth it, but you need to know the limitations of your data structures. So we strongly recommend documenting the performance characteristics. |
|||
|
|||
## Tests |
|||
|
|||
The NativeCounter is not complete, the only thing left is to add tests for it to make sure it is correct and that it does not break in the future. When writing tests you should try to cover as many unusual scenarios as possible. It is also a good idea to add some kind of stress test using jobs to detect race conditions, even if it is unlikely to find all of them. The NativeCounter API is very small so the number of tests required is not huge. |
|||
|
|||
* Both versions of the counter examples above are available at: _/Assets/NativeCounterDemo_. |
|||
* The tests for them can be found at: _/Assets/NativeCounterDemo/Editor/NativeCounterTests.cs_. |
|||
|
|||
## Available attributes |
|||
|
|||
The NativeCounter uses many attributes, but there are a few more available for other types of containers. Here is a list of the available attributes you can use on the NativeContainer struct. |
|||
* [NativeContainer](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeContainerAttribute.html) - marks a struct as a NativeContainer.Required for all native containers. |
|||
* [NativeContainerSupportsMinMaxWriteRestriction](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeContainerSupportsMinMaxWriteRestrictionAttribute.html) - signals that the NativeContainer can restrict its writable ranges to be between a min and max index. This is used when passing the container to an IJobParallelFor to make sure that the job does not write to indices it is not supposed to process. In order to use this the NativeContainer must have the members int __m_Length__, int __m_MinIndex__ and int __m_MaxIndex__ in that order with no other members between them. The container must also throw an exception for writes outside the min/max range. |
|||
* [NativeContainerIsAtomicWriteOnly](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeContainerIsAtomicWriteOnlyAttribute.html) - signals that the NativeContainer uses atomic writes and no reads. By adding this is is possible to pass the NativeContainer to an IJobParallelFor as writable without restrictions on which indices can be written to. |
|||
* [NativeContainerSupportsDeallocateOnJobCompletion](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeContainerSupportsDeallocateOnJobCompletionAttribute.html) - makes the NativeContainer usable with [DeallocateOnJobCompletion](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.DeallocateOnJobCompletionAttribute.html). In order to use this the NativeContainer must have a single allocation in __m_Buffer__, an allocator label in __m_AllocatorLabel__ and a dispose sentinel in __m_DisposeSentinel__. |
|||
* [NativeSetThreadIndex](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeSetThreadIndexAttribute.html) - Patches an int with the thread index of the job. |
|||
|
|||
In addition to these attributes on the native container struct itself there are a few attributes which can be used on members of the native container. |
|||
* [NativeDisableUnsafePtrRestriction](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeDisableUnsafePtrRestrictionAttribute.html) - allows the NativeContainer to be passed to a job even though it contains a pointer, which is usually not allowed. |
|||
* [NativeSetClassTypeToNullOnSchedule](https://docs.unity3d.com/2018.1/Documentation/ScriptReference/Unity.Collections.LowLevel.Unsafe.NativeSetClassTypeToNullOnScheduleAttribute.html) - allows the NativeContainer to be passed to a job even though it contains a managed object. The managed object will be set to `null` on the copy passed to the job. |
|||
|
|
|||
apiRules: |
|||
- exclude: |
|||
# inherited Object methods |
|||
uidRegex: ^System\.Object\..*$ |
|||
type: Method |
|||
- exclude: |
|||
# mentioning types from System.* namespace |
|||
uidRegex: ^System\..*$ |
|||
type: Type |
|||
- exclude: |
|||
hasAttribute: |
|||
uid: System.ObsoleteAttribute |
|||
type: Member |
|||
- exclude: |
|||
hasAttribute: |
|||
uid: System.ObsoleteAttribute |
|||
type: Type |
|||
- exclude: |
|||
uidRegex: Tests$ |
|||
type: Namespace |
|
|||
# Unity Jobs Package |
|||
|
|||
The Jobs Package extends the core Unity Job system with types helpful when using the Entity Component System. |
|||
|
|||
The main documentation for the C# Job System resides in the Unity Manual. See [C# Job System](https://docs.unity3d.com/Manual/JobSystem.html). |
|
|||
# Scheduling a job from a job - why not? |
|||
|
|||
We have a couple of important principles that drive our design. |
|||
|
|||
* Determinism by default: Determinism enables networked games, replay and debugging tools. |
|||
* Safe: Race conditions are immediately reported, this makes writing jobified code significantly more approachable and simple. |
|||
|
|||
These two principles applied result in some choices and restrictions that we enforce. |
|||
|
|||
## Jobs can only be completed on the main thread - but why? |
|||
|
|||
If you were to call __JobHandle.Complete__ that leads to impossible to solve job scheduler deadlocks. |
|||
(We have tried this over the last couple years with the Unity C++ code base, and every single case has resulted in tears and us reverting such patterns in our code.) The deadlocks are rare but provably impossible to solve in all cases, they are heavily dependent on the timing of jobs. |
|||
|
|||
## Jobs can only be scheduled on the main thread - but why? |
|||
|
|||
If you were to simply schedule a job from another job, but not call JobHandle.Complete from the job, then there is no way to guarantee determinism. The main thread has to call JobHandle.Complete(), but who passes that JobHandle to the main thread? How do you know the job that schedules the other job has already executed? |
|||
|
|||
In summary, first instinct is to simply schedule jobs from other jobs, and then wait for jobs within a job. |
|||
Yet experience tells us that this is always a bad idea. So the C# job system does not support it. |
|||
|
|||
## OK, but how do I process workloads where I don't know the exact size upfront? |
|||
|
|||
It's totally fine to schedule jobs conservatively and then simply exit early and do nothing if it turns out the number of actual elements to process, when the job executes, is much less than the conservative number of elements that was determined at schedule time. |
|||
|
|||
In fact this way of doing it leads to deterministic execution, and if the early exit can skip a whole batch of operations it's not really a performance issue. |
|||
Also, there is no possibility of causing internal job scheduler deadlocks. |
|||
|
|||
For this purpose using __IJobParallelForBatch__ as opposed to __IJobParallelFor__ can be very useful since you can exit early on a whole batch. |
|||
``` |
|||
public interface IJobParallelForBatch |
|||
{ |
|||
void Execute(int startIndex, int count); |
|||
} |
|||
``` |
|||
<!-- TODO: CODE EXAMPLE for sorting? --> |
|
|||
fileFormatVersion: 2 |
|||
guid: df44a713789094ce7a8dec834299ed29 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using UnityEditor; |
|||
using Unity.Collections; |
|||
using UnityEngine; |
|||
|
|||
class CLILeakDetectionSwitcher |
|||
{ |
|||
[InitializeOnLoadMethod] |
|||
static void SetLeakDetectionModeFromEnvironment() |
|||
{ |
|||
var nativeLeakDetectionMode = Environment.GetEnvironmentVariable("UNITY_JOBS_NATIVE_LEAK_DETECTION_MODE"); |
|||
if (!string.IsNullOrEmpty(nativeLeakDetectionMode)) |
|||
{ |
|||
switch (nativeLeakDetectionMode) |
|||
{ |
|||
case "0": |
|||
NativeLeakDetection.Mode = NativeLeakDetectionMode.Disabled; |
|||
break; |
|||
case "1": |
|||
NativeLeakDetection.Mode = NativeLeakDetectionMode.Enabled; |
|||
break; |
|||
case "2": |
|||
NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace; |
|||
break; |
|||
default: |
|||
Debug.LogWarning("The environment variable UNITY_JOBS_NATIVE_LEAK_DETECTION_MODE has an invalid value. Please use: 0 = Disabled, 1 = Enabled, 2 = EnabledWithStackTrace."); |
|||
break; |
|||
} |
|||
Debug.Log("Native leak detection mode: " + NativeLeakDetection.Mode); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a3b018ffc4586a441a093c4d954a8cf1 |
|||
timeCreated: 1507328300 |
|||
licenseType: Pro |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEditor; |
|||
using Unity.Collections; |
|||
using Unity.Jobs.LowLevel.Unsafe; |
|||
|
|||
class JobsMenu |
|||
{ |
|||
private static int savedJobWorkerCount = JobsUtility.JobWorkerCount; |
|||
|
|||
const string kUseJobThreads = "Jobs/Use Job Threads"; |
|||
|
|||
[MenuItem(kUseJobThreads, false)] |
|||
static void SwitchUseJobThreads() |
|||
{ |
|||
if (JobsUtility.JobWorkerCount > 0) |
|||
{ |
|||
savedJobWorkerCount = JobsUtility.JobWorkerCount; |
|||
try |
|||
{ |
|||
JobsUtility.JobWorkerCount = 0; |
|||
} |
|||
catch (System.ArgumentOutOfRangeException e) when (e.ParamName == "JobWorkerCount") |
|||
{ |
|||
UnityEngine.Debug.LogWarning("Disabling Job Threads requires Unity Version 2020.1.a15 or newer"); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
JobsUtility.JobWorkerCount = savedJobWorkerCount; |
|||
if (savedJobWorkerCount == 0) |
|||
{ |
|||
JobsUtility.ResetJobWorkerCount(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MenuItem(kUseJobThreads, true)] |
|||
static bool SwitchUseJobThreadsValidate() |
|||
{ |
|||
Menu.SetChecked(kUseJobThreads, (JobsUtility.JobWorkerCount > 0)); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
const string kDebuggerMenu = "Jobs/JobsDebugger"; |
|||
|
|||
[MenuItem(kDebuggerMenu, false)] |
|||
static void SwitchJobsDebugger() |
|||
{ |
|||
JobsUtility.JobDebuggerEnabled = !JobsUtility.JobDebuggerEnabled; |
|||
} |
|||
|
|||
[MenuItem(kDebuggerMenu, true)] |
|||
static bool SwitchJobsDebuggerValidate() |
|||
{ |
|||
Menu.SetChecked(kDebuggerMenu, JobsUtility.JobDebuggerEnabled); |
|||
return true; |
|||
} |
|||
|
|||
const string kLeakOff = "Jobs/Leak Detection/Off"; |
|||
const string kLeakOn = "Jobs/Leak Detection/On"; |
|||
const string kLeakDetectionFull = "Jobs/Leak Detection/Full Stack Traces (Expensive)"; |
|||
|
|||
[MenuItem(kLeakOff)] |
|||
static void SwitchLeaksOff() |
|||
{ |
|||
NativeLeakDetection.Mode = NativeLeakDetectionMode.Disabled; |
|||
} |
|||
|
|||
[MenuItem(kLeakOn)] |
|||
static void SwitchLeaksOn() |
|||
{ |
|||
NativeLeakDetection.Mode = NativeLeakDetectionMode.Enabled; |
|||
} |
|||
|
|||
[MenuItem(kLeakDetectionFull)] |
|||
static void SwitchLeaksFull() |
|||
{ |
|||
NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace; |
|||
} |
|||
|
|||
[MenuItem(kLeakOff, true)] |
|||
static bool SwitchLeaksOffValidate() |
|||
{ |
|||
Menu.SetChecked(kLeakOff, NativeLeakDetection.Mode == NativeLeakDetectionMode.Disabled); |
|||
Menu.SetChecked(kLeakOn, NativeLeakDetection.Mode == NativeLeakDetectionMode.Enabled); |
|||
Menu.SetChecked(kLeakDetectionFull, NativeLeakDetection.Mode == NativeLeakDetectionMode.EnabledWithStackTrace); |
|||
return true; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7a96926915a7746789220056d7c409a5 |
|||
timeCreated: 1507328300 |
|||
licenseType: Pro |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"name": "Unity.Jobs.Editor", |
|||
"references": [ |
|||
"Unity.Jobs" |
|||
], |
|||
"optionalUnityReferences": [], |
|||
"includePlatforms": [ |
|||
"Editor" |
|||
], |
|||
"excludePlatforms": [] |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 261882d2e8b4744e6bb349bdc6a75dc1 |
|||
AssemblyDefinitionImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
Unity Companion License (“License”) |
|||
Software Copyright © 2017-2020 Unity Technologies ApS |
|||
|
|||
Unity Technologies ApS (“Unity”) grants to you a worldwide, non-exclusive, no-charge, and royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the software that is made available under this License (“Software”), subject to the following terms and conditions: |
|||
|
|||
1. Unity Companion Use Only. Exercise of the license granted herein is limited to exercise for the creation, use, and/or distribution of applications, software, or other content pursuant to a valid Unity content authoring and rendering engine software license (“Engine License”). That means while use of the Software is not limited to use in the software licensed under the Engine License, the Software may not be used for any purpose other than the creation, use, and/or distribution of Engine License-dependent applications, software, or other content. No other exercise of the license granted herein is permitted, and in no event may the Software be used for competitive analysis or to develop a competing product or service. |
|||
|
|||
2. No Modification of Engine License. Neither this License nor any exercise of the license granted herein modifies the Engine License in any way. |
|||
|
|||
3. Ownership & Grant Back to You. |
|||
|
|||
3.1 You own your content. In this License, “derivative works” means derivatives of the Software itself--works derived only from the Software by you under this License (for example, modifying the code of the Software itself to improve its efficacy); “derivative works” of the Software do not include, for example, games, apps, or content that you create using the Software. You keep all right, title, and interest to your own content. |
|||
|
|||
3.2 Unity owns its content. While you keep all right, title, and interest to your own content per the above, as between Unity and you, Unity will own all right, title, and interest to all intellectual property rights (including patent, trademark, and copyright) in the Software and derivative works of the Software, and you hereby assign and agree to assign all such rights in those derivative works to Unity. |
|||
|
|||
3.3 You have a license to those derivative works. Subject to this License, Unity grants to you the same worldwide, non-exclusive, no-charge, and royalty-free copyright license to derivative works of the Software you create as is granted to you for the Software under this License. |
|||
|
|||
4. Trademarks. You are not granted any right or license under this License to use any trademarks, service marks, trade names, products names, or branding of Unity or its affiliates (“Trademarks”). Descriptive uses of Trademarks are permitted; see, for example, Unity’s Branding Usage Guidelines at https://unity3d.com/public-relations/brand. |
|||
|
|||
5. Notices & Third-Party Rights. This License, including the copyright notice associated with the Software, must be provided in all substantial portions of the Software and derivative works thereof (or, if that is impracticable, in any other location where such notices are customarily placed). Further, if the Software is accompanied by a Unity “third-party notices” or similar file, you acknowledge and agree that software identified in that file is governed by those separate license terms. |
|||
|
|||
6. DISCLAIMER, LIMITATION OF LIABILITY. THE SOFTWARE AND ANY DERIVATIVE WORKS THEREOF IS PROVIDED ON AN "AS IS" BASIS, AND IS PROVIDED WITHOUT WARRANTY OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND/OR NONINFRINGEMENT. IN NO EVENT SHALL ANY COPYRIGHT HOLDER OR AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES (WHETHER DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL, INCLUDING PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF USE, DATA, OR PROFITS, AND BUSINESS INTERRUPTION), OR OTHER LIABILITY WHATSOEVER, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM OR OUT OF, OR IN CONNECTION WITH, THE SOFTWARE OR ANY DERIVATIVE WORKS THEREOF OR THE USE OF OR OTHER DEALINGS IN SAME, EVEN WHERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|||
|
|||
7. USE IS ACCEPTANCE and License Versions. Your receipt and use of the Software constitutes your acceptance of this License and its terms and conditions. Software released by Unity under this License may be modified or updated and the License with it; upon any such modification or update, you will comply with the terms of the updated License for any use of any of the Software under the updated License. |
|||
|
|||
8. Use in Compliance with Law and Termination. Your exercise of the license granted herein will at all times be in compliance with applicable law and will not infringe any proprietary rights (including intellectual property rights); this License will terminate immediately on any breach by you of this License. |
|||
|
|||
9. Severability. If any provision of this License is held to be unenforceable or invalid, that provision will be enforced to the maximum extent possible and the other provisions will remain in full force and effect. |
|||
|
|||
10. Governing Law and Venue. This License is governed by and construed in accordance with the laws of Denmark, except for its conflict of laws rules; the United Nations Convention on Contracts for the International Sale of Goods will not apply. If you reside (or your principal place of business is) within the United States, you and Unity agree to submit to the personal and exclusive jurisdiction of and venue in the state and federal courts located in San Francisco County, California concerning any dispute arising out of this License (“Dispute”). If you reside (or your principal place of business is) outside the United States, you and Unity agree to submit to the personal and exclusive jurisdiction of and venue in the courts located in Copenhagen, Denmark concerning any Dispute. |
|
|||
fileFormatVersion: 2 |
|||
guid: f9474ce91ede643258663ff3c5392424 |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 05ee6c1e82201411f994177ecda6086f |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: e1d350448ea42c04fb892565ddbd3231 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using NUnit.Framework; |
|||
using Unity.Collections; |
|||
using Unity.Jobs; |
|||
|
|||
namespace Unity.Jobs.Tests.ManagedJobs |
|||
{ |
|||
public class JobStressTests : JobTestsFixture |
|||
{ |
|||
struct JobSetIndexValue : IJobParallelFor |
|||
{ |
|||
public NativeArray<int> value; |
|||
|
|||
public void Execute(int index) |
|||
{ |
|||
value[index] = index; |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void StressTestParallelFor() |
|||
{ |
|||
StressTestParallelForIterations(1, 5000); |
|||
} |
|||
|
|||
public void StressTestParallelForIterations(int amount, int amountOfData) |
|||
{ |
|||
for (var k = 0; k != amount; k++) |
|||
{ |
|||
var len = UnityEngine.Random.Range(1, amountOfData); |
|||
|
|||
JobSetIndexValue job1; |
|||
job1.value = new NativeArray<int>(len, Allocator.TempJob); |
|||
|
|||
JobSetIndexValue job2; |
|||
job2.value = new NativeArray<int>(len, Allocator.TempJob); |
|||
|
|||
var job1Handle = job1.Schedule(len, UnityEngine.Random.Range(1, 1024)); |
|||
var job2Handle = job2.Schedule(len, UnityEngine.Random.Range(1, 1024)); |
|||
|
|||
job2Handle.Complete(); |
|||
job1Handle.Complete(); |
|||
|
|||
for (var i = 0; i < len; i++) |
|||
{ |
|||
Assert.AreEqual(i, job1.value[i]); |
|||
Assert.AreEqual(i, job2.value[i]); |
|||
} |
|||
|
|||
job1.value.Dispose(); |
|||
job2.value.Dispose(); |
|||
} |
|||
} |
|||
|
|||
struct JobSetValue : IJob |
|||
{ |
|||
public int expected; |
|||
public NativeArray<int> value; |
|||
|
|||
public void Execute() |
|||
{ |
|||
value[0] = value[0] + 1; |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void DeepDependencyChain() |
|||
{ |
|||
var array = new NativeArray<int>(1, Allocator.Persistent); |
|||
var jobHandle = new JobHandle(); |
|||
const int depth = 10000; |
|||
for (var i = 0; i < depth; i++) |
|||
{ |
|||
var job = new JobSetValue |
|||
{ |
|||
value = array, |
|||
expected = i |
|||
}; |
|||
jobHandle = job.Schedule(jobHandle); |
|||
} |
|||
|
|||
jobHandle.Complete(); |
|||
Assert.AreEqual(depth, array[0]); |
|||
|
|||
array.Dispose(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 36d0e1be06be93249a0ffa0c8356e69e |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using NUnit.Framework; |
|||
using Unity.Collections; |
|||
using Unity.Collections.LowLevel.Unsafe; |
|||
using Unity.Jobs; |
|||
using Unity.Jobs.LowLevel.Unsafe; |
|||
|
|||
namespace Unity.Jobs.Tests.ManagedJobs |
|||
{ |
|||
#if UNITY_DOTSRUNTIME
|
|||
public class DotsRuntimeFixmeAttribute : IgnoreAttribute |
|||
{ |
|||
public DotsRuntimeFixmeAttribute(string msg = null) : base(msg == null ? "Test should work in DOTS Runtime but currently doesn't. Ignoring until fixed..." : msg) |
|||
{ |
|||
} |
|||
} |
|||
#else
|
|||
public class DotsRuntimeFixmeAttribute : Attribute |
|||
{ |
|||
public DotsRuntimeFixmeAttribute(string msg = null) |
|||
{ |
|||
} |
|||
} |
|||
#endif
|
|||
|
|||
[JobProducerType(typeof(IJobTestExtensions.JobTestProducer<>))] |
|||
public interface IJobTest |
|||
{ |
|||
void Execute(); |
|||
} |
|||
|
|||
public static class IJobTestExtensions |
|||
{ |
|||
internal struct JobTestWrapper<T> where T : struct |
|||
{ |
|||
internal T JobData; |
|||
|
|||
[NativeDisableContainerSafetyRestriction] |
|||
[DeallocateOnJobCompletion] |
|||
internal NativeArray<byte> ProducerResourceToClean; |
|||
} |
|||
|
|||
internal struct JobTestProducer<T> where T : struct, IJobTest |
|||
{ |
|||
static IntPtr s_JobReflectionData; |
|||
|
|||
public static IntPtr Initialize() |
|||
{ |
|||
if (s_JobReflectionData == IntPtr.Zero) |
|||
{ |
|||
#if UNITY_2020_2_OR_NEWER
|
|||
s_JobReflectionData = JobsUtility.CreateJobReflectionData(typeof(JobTestWrapper<T>), typeof(T), (ExecuteJobFunction)Execute); |
|||
#else
|
|||
s_JobReflectionData = JobsUtility.CreateJobReflectionData(typeof(JobTestWrapper<T>), typeof(T), |
|||
JobType.Single, (ExecuteJobFunction)Execute); |
|||
#endif
|
|||
} |
|||
|
|||
return s_JobReflectionData; |
|||
} |
|||
|
|||
public delegate void ExecuteJobFunction(ref JobTestWrapper<T> jobWrapper, IntPtr additionalPtr, IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex); |
|||
public unsafe static void Execute(ref JobTestWrapper<T> jobWrapper, IntPtr additionalPtr, IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex) |
|||
{ |
|||
jobWrapper.JobData.Execute(); |
|||
} |
|||
} |
|||
|
|||
public static unsafe JobHandle ScheduleTest<T>(this T jobData, NativeArray<byte> dataForProducer, JobHandle dependsOn = new JobHandle()) where T : struct, IJobTest |
|||
{ |
|||
JobTestWrapper<T> jobTestWrapper = new JobTestWrapper<T> |
|||
{ |
|||
JobData = jobData, |
|||
ProducerResourceToClean = dataForProducer |
|||
}; |
|||
|
|||
var scheduleParams = new JobsUtility.JobScheduleParameters( |
|||
UnsafeUtility.AddressOf(ref jobTestWrapper), |
|||
JobTestProducer<T>.Initialize(), |
|||
dependsOn, |
|||
#if UNITY_2020_2_OR_NEWER
|
|||
ScheduleMode.Parallel |
|||
#else
|
|||
ScheduleMode.Batched |
|||
#endif
|
|||
); |
|||
|
|||
return JobsUtility.Schedule(ref scheduleParams); |
|||
} |
|||
} |
|||
|
|||