Skip to content

Commit c7e64bb

Browse files
raquelpecesEvergreen
authored andcommitted
Prevent ResourceHandle ValidityBit Collisions over Time
1 parent 1cc5804 commit c7e64bb

2 files changed

Lines changed: 166 additions & 77 deletions

File tree

Packages/com.unity.render-pipelines.core/Runtime/RenderGraph/RenderGraphResources.cs

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,28 @@ internal enum RenderGraphResourceType
2020
// Note on handles validity.
2121
// PassData classes used during render graph passes are pooled and because of that, when users don't fill them completely,
2222
// they can contain stale handles from a previous render graph execution that could still be considered valid if we only checked the index.
23-
// In order to avoid using those, we incorporate the execution index in a 16 bits hash to make sure the handle is coming from the current execution.
23+
// In order to avoid using those, we incorporate the execution index in a hash to make sure the handle is coming from the current execution.
2424
// If not, it's considered invalid.
25-
// We store this validity mask in the upper 16 bits of the index.
26-
const uint kValidityMask = 0xFFFF0000;
25+
// The validity is stored in a separate field, allowing for a full 32-bit validity check. (2^32 =~ 4B (at 60fps it is more than 2 years) 4,294,967,295 unique execution indices before a collision)
26+
// The m_VersionIndex field contains both index and version:
27+
// - Index (lower 16 bits = 65,535 unique indices)
28+
// - Version (upper 16 bits with one for not versioned. 15 bits = 32767 unique versions).
29+
// - Bit 31 is used as a "not versioned" flag for faster checking.
2730
const uint kIndexMask = 0xFFFF;
31+
const uint kVersionMask = 0x7FFF0000;
32+
const int kVersionShift = 16;
33+
const uint kNotVersionedBit = 0x80000000;
2834

29-
private readonly uint m_Value;
30-
private readonly int m_Version;
35+
private readonly uint m_VersionIndex;
36+
private readonly uint m_Validity;
3137
private readonly RenderGraphResourceType m_Type;
3238

33-
static uint s_CurrentValidBit = 1 << 16;
34-
static uint s_SharedResourceValidBit = 0x7FFF << 16;
39+
static uint s_CurrentValidBit = 1;
3540

3641
public int index
37-
{
42+
{
3843
[MethodImpl(MethodImplOptions.AggressiveInlining)]
39-
get { return (int)(m_Value & kIndexMask); }
44+
get { return (int)(m_VersionIndex & kIndexMask); }
4045
}
4146
public int iType
4247
{
@@ -46,34 +51,38 @@ public int iType
4651
public int version
4752
{
4853
[MethodImpl(MethodImplOptions.AggressiveInlining)]
49-
get { return m_Version; }
54+
get
55+
{
56+
return (m_VersionIndex & kNotVersionedBit) != 0 ? -1 : (int)((m_VersionIndex & kVersionMask) >> kVersionShift);
57+
}
5058
}
5159
public RenderGraphResourceType type
5260
{
5361
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5462
get { return m_Type; }
5563
}
5664

57-
internal ResourceHandle(int value, RenderGraphResourceType type, bool shared)
65+
internal ResourceHandle(int index, RenderGraphResourceType type, bool shared)
5866
{
59-
Debug.Assert(value <= 0xFFFF);
60-
m_Value = ((uint)value & kIndexMask) | (shared ? s_SharedResourceValidBit : s_CurrentValidBit);
67+
Debug.Assert(index > 0 && index <= 0xFFFF, "ResourceHandle: Invalid index, values should be >0 && <65536");
68+
m_VersionIndex = ((uint)index & kIndexMask) | kNotVersionedBit;
69+
m_Validity = s_CurrentValidBit;
6170
m_Type = type;
62-
m_Version = -1;
6371
}
6472

6573
internal ResourceHandle(in ResourceHandle h, int version)
6674
{
67-
this.m_Value = h.m_Value;
68-
this.m_Type = h.type;
69-
this.m_Version = version;
75+
Debug.Assert(version >= 0 && version <= 0x7FFF, "ResourceHandle: Invalid version, values should be >=0 && <32768");
76+
uint versionBits = ((uint)version << kVersionShift) & kVersionMask;
77+
m_VersionIndex = (h.m_VersionIndex & kIndexMask) | versionBits;
78+
m_Validity = h.m_Validity;
79+
m_Type = h.type;
7080
}
7181

7282
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7383
public bool IsValid()
7484
{
75-
var validity = m_Value & kValidityMask;
76-
return validity != 0 && (validity == s_CurrentValidBit || validity == s_SharedResourceValidBit);
85+
return m_Validity != 0 && (m_Validity == s_CurrentValidBit);
7786
}
7887

7988
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -82,8 +91,8 @@ public bool IsNull()
8291
if (index == 0)
8392
{
8493
// Make sure everything is zero
85-
Debug.Assert(m_Value == 0);
86-
Debug.Assert(m_Version == 0);
94+
Debug.Assert(m_VersionIndex == 0);
95+
Debug.Assert(m_Validity == 0);
8796
return true;
8897
}
8998
return false;
@@ -92,20 +101,20 @@ public bool IsNull()
92101
static public void NewFrame(int executionIndex)
93102
{
94103
uint previousValidBit = s_CurrentValidBit;
95-
// Scramble frame count to avoid collision when wrapping around.
96-
s_CurrentValidBit = (uint)(((executionIndex >> 16) ^ (executionIndex & 0xffff) * 58546883) << 16);
104+
105+
var hasher = HashFNV1A32.Create();
106+
hasher.Append(executionIndex);
107+
s_CurrentValidBit = (uint)hasher.value;
97108
// In case the current valid bit is 0, even though perfectly valid, 0 represents an invalid handle, hence we'll
98109
// trigger an invalid state incorrectly. To account for this, we actually skip 0 as a viable s_CurrentValidBit and
99110
// start from 1 again.
100-
// In the same spirit, s_SharedResourceValidBit is reserved for shared textures so we should never use it otherwise
101-
// resources could be considered valid at frame N+1 (because shared) even though they aren't.
102-
if (s_CurrentValidBit == 0 || s_CurrentValidBit == s_SharedResourceValidBit)
111+
if (s_CurrentValidBit == 0)
103112
{
104113
// We need to make sure we don't pick the same value twice.
105114
uint value = 1;
106-
while (previousValidBit == (value << 16))
115+
while (previousValidBit == value)
107116
value++;
108-
s_CurrentValidBit = (value << 16);
117+
s_CurrentValidBit = value;
109118
}
110119
}
111120

@@ -114,14 +123,14 @@ public bool IsVersioned
114123
[MethodImpl(MethodImplOptions.AggressiveInlining)]
115124
get
116125
{
117-
return m_Version >= 0;
126+
return (m_VersionIndex & kNotVersionedBit) == 0;
118127
}
119128
}
120129

121130
[MethodImpl(MethodImplOptions.AggressiveInlining)]
122131
public bool Equals(ResourceHandle hdl)
123132
{
124-
return hdl.m_Value == this.m_Value && hdl.m_Version == this.m_Version && hdl.type == this.type;
133+
return hdl.m_VersionIndex == this.m_VersionIndex && hdl.m_Validity == this.m_Validity && hdl.type == this.type;
125134
}
126135

127136
public static bool operator ==(ResourceHandle lhs, ResourceHandle rhs) => lhs.Equals(rhs);
@@ -133,8 +142,8 @@ public bool Equals(ResourceHandle hdl)
133142
public override int GetHashCode()
134143
{
135144
var hashCode = HashFNV1A32.Create();
136-
hashCode.Append(m_Value);
137-
hashCode.Append(m_Version);
145+
hashCode.Append(m_VersionIndex);
146+
hashCode.Append(m_Validity);
138147
hashCode.Append(m_Type);
139148
return hashCode.value;
140149
}

Packages/com.unity.render-pipelines.core/Tests/Editor/RenderGraphTests.cs

Lines changed: 125 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,62 +1414,142 @@ public void RenderGraphMultisampledShaderResolvePassMustHaveOneColorAttachment()
14141414
m_Camera.Render();
14151415
}
14161416

1417-
/*
1418-
// Disabled for now as version management is not exposed to user code
1417+
14191418
[Test]
1420-
public void VersionManagement()
1419+
public void ResourceHandleVersionManagement()
14211420
{
1421+
TextureHandle texture0 = m_RenderGraph.CreateTexture(new TextureDesc(Vector2.one) { colorFormat = GraphicsFormat.R8G8B8A8_UNorm });
14221422

1423-
TextureHandle texture0;
1424-
TextureHandle texture0v1;
1425-
TextureHandle texture0v2;
1426-
texture0 = m_RenderGraph.CreateTexture(new TextureDesc(Vector2.one) { colorFormat = GraphicsFormat.R8G8B8A8_UNorm });
1423+
// Handles are unversioned by default.
1424+
Assert.IsFalse(texture0.handle.IsVersioned);
1425+
Assert.AreEqual(-1, texture0.handle.version);
14271426

1428-
TextureHandle texture1;
1429-
texture1 = m_RenderGraph.CreateTexture(new TextureDesc(Vector2.one) { colorFormat = GraphicsFormat.R8G8B8A8_UNorm });
1427+
int iterations = 1000;
1428+
for (int i = 0; i < iterations; ++i)
1429+
{
1430+
int writePassIndex = m_RenderGraph.m_RenderPasses.Count;
1431+
// Writing bumps version
1432+
using (var builder = m_RenderGraph.AddRasterRenderPass<RenderGraphTestPassData>("Async_TestPassWrite"+i, out var passData))
1433+
{
1434+
builder.SetRenderAttachment(texture0, 0, AccessFlags.Write);
1435+
builder.SetRenderFunc((RenderGraphTestPassData data, RasterGraphContext context) => { });
1436+
}
14301437

1431-
// Handles are unversioned by default. Unversioned handles use an implicit version "the latest version" depending on their
1432-
// usage context.
1433-
Assert.AreEqual(false, texture0.handle.IsVersioned);
1438+
// Access the versioned handle from the render pass
1439+
var writePass = m_RenderGraph.m_RenderPasses[writePassIndex];
1440+
var versionedWriteHandle = writePass.colorBufferAccess[0].textureHandle.handle;
1441+
Assert.IsTrue(versionedWriteHandle.IsVersioned, $"Write handle should be versioned at iteration {i}");
1442+
Assert.AreEqual(i + 1, versionedWriteHandle.version, $"Write version should be {i + 1} at iteration {i}");
14341443

1435-
// Writing bumps the version
1436-
using (var builder = m_RenderGraph.AddRasterRenderPass<RenderGraphTestPassData>("Async_TestPass0", out var passData))
1437-
{
1438-
texture0v1 = builder.UseTexture(texture0, AccessFlags.ReadWrite);
1439-
Assert.AreEqual(true, texture0v1.handle.IsVersioned);
1440-
Assert.AreEqual(1, texture0v1.handle.version);
1441-
builder.SetRenderFunc((RenderGraphTestPassData data, RasterGraphContext context) => { });
1442-
}
1444+
int readPassIndex = m_RenderGraph.m_RenderPasses.Count;
14431445

1444-
// Writing again bumps again
1445-
using (var builder = m_RenderGraph.AddRasterRenderPass<RenderGraphTestPassData>("Async_TestPass1", out var passData))
1446-
{
1447-
texture0v2 = builder.UseTexture(texture0, AccessFlags.ReadWrite);
1448-
Assert.AreEqual(true, texture0v2.handle.IsVersioned);
1449-
Assert.AreEqual(2, texture0v2.handle.version);
1450-
builder.SetRenderFunc((RenderGraphTestPassData data, RasterGraphContext context) => { });
1451-
}
1446+
// Reading leaves the version alone
1447+
using (var builder = m_RenderGraph.AddRasterRenderPass<RenderGraphTestPassData>("Async_TestPassRead"+i, out var passData))
1448+
{
1449+
builder.SetRenderAttachment(texture0, 0, AccessFlags.Read);
1450+
}
14521451

1453-
// Reading leaves the version alone
1454-
using (var builder = m_RenderGraph.AddRasterRenderPass<RenderGraphTestPassData>("Async_TestPass2", out var passData))
1455-
{
1456-
var versioned = builder.UseTexture(texture0, AccessFlags.Read);
1457-
Assert.AreEqual(true, versioned.handle.IsVersioned);
1458-
Assert.AreEqual(2, versioned.handle.version);
1459-
builder.SetRenderFunc((RenderGraphTestPassData data, RasterGraphContext context) => { });
1452+
// Access the versioned handle from the read pass
1453+
var readPass = m_RenderGraph.m_RenderPasses[readPassIndex];
1454+
var versionedReadHandle = readPass.colorBufferAccess[0].textureHandle.handle;
1455+
Assert.IsTrue(versionedReadHandle.IsVersioned, $"Read handle should be versioned at iteration {i}");
1456+
Assert.AreEqual(i + 1, versionedReadHandle.version, $"Read version should remain {i + 1} at iteration {i}");
14601457
}
1458+
}
1459+
1460+
[Test]
1461+
public void ResourceHandleValidityBitManagement()
1462+
{
1463+
// Reset validityBit hash to execution 0
1464+
ResourceHandle.NewFrame(0);
1465+
1466+
// Create a handle and verify it has the current validity bit
1467+
TextureHandle texture0 = m_RenderGraph.CreateTexture(new TextureDesc(Vector2.one) { colorFormat = GraphicsFormat.R8G8B8A8_UNorm });
1468+
Assert.IsTrue(texture0.handle.IsValid(), "Newly created handle should be valid");
1469+
Assert.IsFalse(texture0.handle.IsNull(), "Newly created handle should not be null");
14611470

1462-
// Writing to an old version is not supported it would lead to a divergent version timeline for the resource
1463-
using (var builder = m_RenderGraph.AddRasterRenderPass<RenderGraphTestPassData>("Async_TestPass2", out var passData))
1471+
const int iterations = 216000; // 216k frames is like 1 hour at 60fps
1472+
for (int i = 1; i < iterations; ++i)
14641473
{
1465-
// If you want do achieve this and avoid copying the move should be used
1466-
Assert.Throws<System.InvalidOperationException>(() =>
1467-
{
1468-
var versioned = builder.UseTexture(texture0v1, AccessFlags.ReadWrite);
1469-
});
1470-
builder.SetRenderFunc((RenderGraphTestPassData data, RasterGraphContext context) => { });
1474+
// Simulate a new frame with a new execution index
1475+
ResourceHandle.NewFrame(i);
1476+
1477+
// The stored handle should now be invalid because the validity bit has changed
1478+
Assert.IsFalse(texture0.IsValid(), "Handle from previous frame should be invalid");
14711479
}
1472-
}*/
1480+
}
1481+
1482+
[Test]
1483+
public void ResourceHandleCheckValues()
1484+
{
1485+
// Check NullHandle
1486+
TextureHandle nullHandle = TextureHandle.nullHandle;
1487+
Assert.IsTrue(nullHandle.handle.IsNull(), "TextureHandle.nullHandle should return true to IsNull comparision.");
1488+
1489+
// Check a new created texture
1490+
TextureHandle defaultTexture = m_RenderGraph.CreateTexture(new TextureDesc(Vector2.one) { colorFormat = GraphicsFormat.R8G8B8A8_UNorm });
1491+
// Handles are unversioned by default.
1492+
Assert.IsTrue(defaultTexture.handle.IsValid(), "Newly created handle should be valid");
1493+
Assert.IsFalse(defaultTexture.handle.IsNull(), "Newly created handle should not be null");
1494+
Assert.IsFalse(defaultTexture.handle.IsVersioned, "Newly created handle should not be versioned");
1495+
Assert.AreEqual(-1, defaultTexture.handle.version, "Newly created handle version should be -1");
1496+
1497+
1498+
/* Check limits for version */
1499+
// Version has 15 bits, so the max valid number should be 2^15 - 1 =
1500+
int maxVersion = (int)Math.Pow(2,15) - 1;
1501+
ResourceHandle resourceVersionMax = new ResourceHandle(defaultTexture.handle, maxVersion);
1502+
Assert.IsTrue(resourceVersionMax.IsValid());
1503+
Assert.IsTrue(resourceVersionMax.IsVersioned);
1504+
Assert.AreEqual(maxVersion, resourceVersionMax.version);
1505+
1506+
ResourceHandle resourceVersionMin = new ResourceHandle(defaultTexture.handle, 0);
1507+
Assert.IsTrue(resourceVersionMin.IsValid());
1508+
Assert.IsTrue(resourceVersionMin.IsVersioned);
1509+
Assert.AreEqual(0, resourceVersionMin.version);
1510+
1511+
// Testing Out Of Range values makes Yamato jobs to fail.
1512+
// The test passes because the assert is correctly triggered, but it also fails the job because of the --fail-on-assert command, which is expected to fail the test if any assert is triggered.
1513+
// Need to comment this checks for now, until this is fixed.
1514+
1515+
// ResourceHandle resourceVersionOutOfRangeMax = new ResourceHandle(resourceVersionMax, maxVersion+1);
1516+
// ResourceHandle resourceVersionOutOfRangeMin = new ResourceHandle(resourceVersionMin, -1);
1517+
//
1518+
//#if UNITY_ASSERTIONS
1519+
// // If we increase the version to one more it should log an error
1520+
// LogAssert.Expect(LogType.Assert, "ResourceHandle: Invalid version, values should be >=0 && <32768");
1521+
// LogAssert.Expect(LogType.Assert, "ResourceHandle: Invalid version, values should be >=0 && <32768");
1522+
//#endif
1523+
1524+
1525+
/* Check limits for index */
1526+
int maxIndex = (int)Math.Pow(2,16) - 1;
1527+
ResourceHandle resourceIndexMax = new ResourceHandle(maxIndex, RenderGraphResourceType.Texture, false);
1528+
Assert.IsTrue(resourceIndexMax.IsValid());
1529+
Assert.IsFalse(resourceIndexMax.IsVersioned);
1530+
Assert.AreEqual(maxIndex, resourceIndexMax.index);
1531+
1532+
ResourceHandle resourceIndexMin = new ResourceHandle(1, RenderGraphResourceType.Texture, false);
1533+
Assert.IsTrue(resourceIndexMin.IsValid());
1534+
Assert.IsFalse(resourceIndexMin.IsVersioned);
1535+
Assert.AreEqual(1, resourceIndexMin.index);
1536+
1537+
// Testing Out Of Range values makes Yamato jobs to fail.
1538+
// The test passes because the assert is correctly triggered, but it also fails the job because of the --fail-on-assert command, which is expected to fail the test if any assert is triggered.
1539+
// Need to comment this checks for now, until this is fixed.
1540+
1541+
// ResourceHandle resourceIndexOutOfRangeMax = new ResourceHandle(maxIndex+1, RenderGraphResourceType.Texture, false);
1542+
// ResourceHandle resourceIndexOutOfRange0 = new ResourceHandle(0, RenderGraphResourceType.Texture, false);
1543+
// ResourceHandle resourceIndexOutOfRangeNegative = new ResourceHandle(-1, RenderGraphResourceType.Texture, false);
1544+
//
1545+
//#if UNITY_ASSERTIONS
1546+
// // If we increase the version to one more it should log an error
1547+
// LogAssert.Expect(LogType.Assert, "ResourceHandle: Invalid index, values should be >0 && <65536");
1548+
// LogAssert.Expect(LogType.Assert, "ResourceHandle: Invalid index, values should be >0 && <65536");
1549+
// LogAssert.Expect(LogType.Assert, "ResourceHandle: Invalid index, values should be >0 && <65536");
1550+
//#endif
1551+
1552+
}
14731553

14741554
class RenderGraphAsyncRequestTestData
14751555
{
@@ -2620,7 +2700,7 @@ public void CanMergeBackBufferAndCustomRenderTargets()
26202700
builder.SetRenderFunc(static (UVOriginPassData data, RasterGraphContext context) =>{ });
26212701
builder.AllowPassCulling(false);
26222702
}
2623-
2703+
26242704
var result = m_RenderGraph.CompileNativeRenderGraph(m_RenderGraph.ComputeGraphHash());
26252705
var passes = result.contextData.GetNativePasses();
26262706

0 commit comments

Comments
 (0)