diff --git a/src/.github/commit-instructions.md b/src/.github/commit-instructions.md deleted file mode 100644 index cba523b..0000000 --- a/src/.github/commit-instructions.md +++ /dev/null @@ -1,7 +0,0 @@ -Use this instructions when writing a git commit message - -The first line should be a single line with no more than 50 characters that summary the changes. The second line should be blank. Start at the third line for actual changes. - -The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. - -Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space. The type feat MUST be used when a commit adds a new feature to your application or library. The type fix MUST be used when a commit represents a bug fix for your application. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser) A description MUST immediately follow the colon and space after the typescope prefix. The description is a short summary of the code changes, e.g., fix array parsing issue when multiple spaces were contained in string. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. A commit body is free-form and MAY consist of any number of newline separated paragraphs. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a or # separator, followed by a string value (this is inspired by the git trailer convention). A footer’s token MUST use - in place of whitespace characters, e.g., Acked-by (this helps differentiate the footer section from a multi-paragraph body). An exception is made for BREAKING CHANGE, which MAY also be used as a token. A footer’s value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer tokenseparator pair is observed. Breaking changes MUST be indicated in the typescope prefix of a commit, or as an entry in the footer. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., BREAKING CHANGE environment variables now take precedence over config files. If included in the typescope prefix, breaking changes MUST be indicated by a ! immediately before the . If ! is used, BREAKING CHANGE MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change. Types other than feat and fix MAY be used in your commit messages, e.g., docs update ref docs. The units of information that make up Conventional Commits MUST NOT be treated as case-sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer. \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs index 5a072a9..665967e 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs @@ -758,7 +758,7 @@ public static unsafe partial class MeshProcessor } } - return 0; + return pMeshletData->groups.Count - 1; } /// @@ -846,8 +846,266 @@ public static unsafe partial class MeshProcessor pMeshletData->materialSlotCount = maxMaterialSlot + 1; } - public static void BuildClusterLodHierarchy() + private struct TempBinaryNode { - // TODO: Implement a function that builds a cluster LOD hierarchy for a mesh, which can be used for efficient rendering of large meshes with varying levels of detail. + public AABB bounds; + public float maxParentError; + public int leftChild; + public int rightChild; + public int meshletIndex; + } + + private static int BuildBinaryTree(UnsafeList nodes, UnsafeArray meshletIndices, int start, int end, ReadOnlySpan meshlets) + { + if (start == end - 1) + { + var meshletIndex = meshletIndices[start]; + ref readonly var m = ref meshlets[meshletIndex]; + + var node = new TempBinaryNode + { + bounds = m.boundingBox, + maxParentError = m.parentError, + leftChild = -1, + rightChild = -1, + meshletIndex = meshletIndex + }; + var nodeIndex = nodes.Count; + nodes.Add(node); + return nodeIndex; + } + + // Compute centroid bounds + var centroidMin = new float3(float.MaxValue); + var centroidMax = new float3(float.MinValue); + for (var i = start; i < end; i++) + { + var m = meshlets[meshletIndices[i]]; + var center = m.boundingBox.Center; + centroidMin = math.min(centroidMin, center); + centroidMax = math.max(centroidMax, center); + } + + var extents = centroidMax - centroidMin; + var splitAxis = 0; + if (extents.y > extents.x && extents.y > extents.z) splitAxis = 1; + if (extents.z > extents.x && extents.z > extents.y) splitAxis = 2; + + var splitPoint = centroidMin[splitAxis] + extents[splitAxis] * 0.5f; + + // Partition + var mid = start; + for (var i = start; i < end; i++) + { + var center = meshlets[meshletIndices[i]].boundingBox.Center; + if (center[splitAxis] < splitPoint) + { + var temp = meshletIndices[mid]; + meshletIndices[mid] = meshletIndices[i]; + meshletIndices[i] = temp; + mid++; + } + } + + if (mid == start || mid == end) + { + mid = start + (end - start) / 2; + } + + var left = BuildBinaryTree(nodes, meshletIndices, start, mid, meshlets); + var right = BuildBinaryTree(nodes, meshletIndices, mid, end, meshlets); + + var leftNode = nodes[left]; + var rightNode = nodes[right]; + + var mergedBounds = new AABB( + math.min(leftNode.bounds.Min, rightNode.bounds.Min), + math.max(leftNode.bounds.Max, rightNode.bounds.Max) + ); + + var internalNodeIndex = nodes.Count; + nodes.Add(new TempBinaryNode + { + bounds = mergedBounds, + maxParentError = Math.Max(leftNode.maxParentError, rightNode.maxParentError), + leftChild = left, + rightChild = right, + meshletIndex = -1 + }); + + return internalNodeIndex; + } + + private static void GatherChildren(UnsafeList binaryNodes, int nodeIndex, UnsafeList gathered) + { + gathered.Clear(); + var node = binaryNodes[nodeIndex]; + if (node.leftChild != -1) gathered.Add(node.leftChild); + if (node.rightChild != -1) gathered.Add(node.rightChild); + + while (gathered.Count < 4) + { + var largestInternalIndex = -1; + var maxSurfaceArea = -1.0f; + var listIndexToRemove = -1; + + for (var i = 0; i < gathered.Count; i++) + { + var childIdx = gathered[i]; + var childNode = binaryNodes[childIdx]; + if (childNode.leftChild != -1) // is internal + { + var extents = childNode.bounds.Extents; + var sa = extents.x * extents.y + extents.y * extents.z + extents.z * extents.x; + if (sa > maxSurfaceArea) + { + maxSurfaceArea = sa; + largestInternalIndex = childIdx; + listIndexToRemove = i; + } + } + } + + if (largestInternalIndex == -1) break; // all gathered are leaves + + gathered.RemoveAt(listIndexToRemove); + var largestNode = binaryNodes[largestInternalIndex]; + if (largestNode.leftChild != -1) gathered.Add(largestNode.leftChild); + if (largestNode.rightChild != -1) gathered.Add(largestNode.rightChild); + } + } + + private static int CollapseTo4Ary(UnsafeList binaryNodes, int binaryNodeIndex, UnsafeList hierarchyNodes) + { + var node = binaryNodes[binaryNodeIndex]; + if (node.leftChild == -1) + { + return -1; + } + + using var gathered = new UnsafeList(4, AllocationHandle.FreeList); + GatherChildren(binaryNodes, binaryNodeIndex, gathered); + + var bvhNode = new MeshletHierarchyNode(); + + var minX = new float4(float.PositiveInfinity); + var minY = new float4(float.PositiveInfinity); + var minZ = new float4(float.PositiveInfinity); + var maxX = new float4(float.NegativeInfinity); + var maxY = new float4(float.NegativeInfinity); + var maxZ = new float4(float.NegativeInfinity); + var maxParentError = new float4(0); + var nodeData = new uint4(0xFFFFFFFF); + + var outNodeIndex = hierarchyNodes.Count; + hierarchyNodes.Add(bvhNode); // Reserve slot + + for (var i = 0; i < gathered.Count; i++) + { + var childIdx = gathered[i]; + var childNode = binaryNodes[childIdx]; + + uint data = 0; + if (childNode.leftChild == -1) + { + data = (uint)childNode.meshletIndex; + } + else + { + var child4AryIndex = CollapseTo4Ary(binaryNodes, childIdx, hierarchyNodes); + data = (1u << 31) | (uint)child4AryIndex; + } + + if (i == 0) + { + minX.x = childNode.bounds.Min.x; minY.x = childNode.bounds.Min.y; minZ.x = childNode.bounds.Min.z; + maxX.x = childNode.bounds.Max.x; maxY.x = childNode.bounds.Max.y; maxZ.x = childNode.bounds.Max.z; + maxParentError.x = childNode.maxParentError; + nodeData.x = data; + } + else if (i == 1) + { + minX.y = childNode.bounds.Min.x; minY.y = childNode.bounds.Min.y; minZ.y = childNode.bounds.Min.z; + maxX.y = childNode.bounds.Max.x; maxY.y = childNode.bounds.Max.y; maxZ.y = childNode.bounds.Max.z; + maxParentError.y = childNode.maxParentError; + nodeData.y = data; + } + else if (i == 2) + { + minX.z = childNode.bounds.Min.x; minY.z = childNode.bounds.Min.y; minZ.z = childNode.bounds.Min.z; + maxX.z = childNode.bounds.Max.x; maxY.z = childNode.bounds.Max.y; maxZ.z = childNode.bounds.Max.z; + maxParentError.z = childNode.maxParentError; + nodeData.z = data; + } + else if (i == 3) + { + minX.w = childNode.bounds.Min.x; minY.w = childNode.bounds.Min.y; minZ.w = childNode.bounds.Min.z; + maxX.w = childNode.bounds.Max.x; maxY.w = childNode.bounds.Max.y; maxZ.w = childNode.bounds.Max.z; + maxParentError.w = childNode.maxParentError; + nodeData.w = data; + } + } + + bvhNode.minX = minX; + bvhNode.minY = minY; + bvhNode.minZ = minZ; + bvhNode.maxX = maxX; + bvhNode.maxY = maxY; + bvhNode.maxZ = maxZ; + bvhNode.maxParentError = maxParentError; + bvhNode.nodeData = nodeData; + + hierarchyNodes[outNodeIndex] = bvhNode; + return outNodeIndex; + } + + public static void BuildClusterLodHierarchy(MeshletMeshData* pMeshletData) + { + if (pMeshletData->meshletCount == 0) return; + + using var meshletIndices = new UnsafeArray(pMeshletData->meshletCount, AllocationHandle.FreeList); + for (var i = 0; i < pMeshletData->meshletCount; i++) + { + meshletIndices[i] = i; + } + + var meshletsSpan = new ReadOnlySpan(pMeshletData->meshlets.GetUnsafePtr(), pMeshletData->meshlets.Count); + + using var binaryNodes = new UnsafeList(pMeshletData->meshletCount * 2, AllocationHandle.FreeList); + var rootIndex = BuildBinaryTree(binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletsSpan); + + if (!pMeshletData->hierarchyNodes.IsCreated) + { + pMeshletData->hierarchyNodes = new UnsafeList(pMeshletData->meshletCount, AllocationHandle.Persistent); + } + + if (binaryNodes[rootIndex].leftChild == -1) + { + var bvhNode = new MeshletHierarchyNode(); + bvhNode.minX = new float4(float.PositiveInfinity); + bvhNode.minY = new float4(float.PositiveInfinity); + bvhNode.minZ = new float4(float.PositiveInfinity); + bvhNode.maxX = new float4(float.NegativeInfinity); + bvhNode.maxY = new float4(float.NegativeInfinity); + bvhNode.maxZ = new float4(float.NegativeInfinity); + bvhNode.maxParentError = new float4(0); + bvhNode.nodeData = new uint4(0xFFFFFFFF); + + var childNode = binaryNodes[rootIndex]; + bvhNode.minX.x = childNode.bounds.Min.x; + bvhNode.minY.x = childNode.bounds.Min.y; + bvhNode.minZ.x = childNode.bounds.Min.z; + bvhNode.maxX.x = childNode.bounds.Max.x; + bvhNode.maxY.x = childNode.bounds.Max.y; + bvhNode.maxZ.x = childNode.bounds.Max.z; + bvhNode.maxParentError.x = childNode.maxParentError; + bvhNode.nodeData.x = (uint)childNode.meshletIndex; + + pMeshletData->hierarchyNodes.Add(bvhNode); + } + else + { + CollapseTo4Ary(binaryNodes, rootIndex, pMeshletData->hierarchyNodes); + } } } diff --git a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs index ff9f534..ac9032a 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs @@ -34,7 +34,7 @@ internal static partial class TextureProcessor public int numMipLevels; public int channelCount; - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static Vector2 Hammersley(TFloat i, int N, float* lut) { var x = i / N; @@ -43,23 +43,18 @@ internal static partial class TextureProcessor } // GGX Importance Sampling - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static Vector3 ImportanceSampleGGX(Vector2 Xi, Vector3 N, float roughness) { var a = roughness * roughness; // Disney remap roughness for better visual linearity var phi = 2.0f * PI * Xi.x; - // Clamp the inside of the cosTheta Sqrt to prevent NaN on division precision edges - var cosThetaInner = TFloat.Max((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y), TFloat.Zero); - var cosTheta = TFloat.Sqrt(cosThetaInner); - - // Clamp the inside of sinTheta to prevent sqrt of negative floating-point errors - var sinThetaInner = TFloat.Max(1.0f - cosTheta * cosTheta, TFloat.Zero); - var sinTheta = TFloat.Sqrt(sinThetaInner); + var cosTheta = TFloat.Sqrt((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y)); + var sinTheta = TFloat.Sqrt(1.0f - cosTheta * cosTheta); // Spherical to Cartesian coordinates (Halfway vector) - var (sinPhi, cosPhi) = TFloat.SinCos(phi); + TFloat.SinCos(phi, out var sinPhi, out var cosPhi); var H = MathV.Create(cosPhi * sinTheta, sinPhi * sinTheta, cosTheta); // Tangent space to World space @@ -73,13 +68,13 @@ internal static partial class TextureProcessor return MathV.Normalize(sampleVec); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static float3 CubemapUVToDir(int face, float u, float v) { var sc = 2.0f * u - 1.0f; var tc = 1.0f - 2.0f * v; - float x = 0, y = 0, z = 0; + float x = 0.0f, y = 0.0f, z = 0.0f; switch (face) { case 0: x = 1.0f; y = tc; z = -sc; break; @@ -93,7 +88,7 @@ internal static partial class TextureProcessor return normalize(float3(x, y, z)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] private static Vector3 SampleCubemap(float* img, int edge, int c, Vector3 dir) { var absX = TFloat.Abs(dir.x); @@ -140,6 +135,7 @@ internal static partial class TextureProcessor return MathV.GatherVector3(img, idx.GetUnsafePtr(), 1); } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] public void Execute(int loopIndex, ref readonly JobExecutionContext ctx) { var m = 0; @@ -226,7 +222,7 @@ internal static partial class TextureProcessor } var totalWeight = 0.0f; - var prefilteredColor = float3(0, 0, 0); + var prefilteredColor = float3(0.0f, 0.0f, 0.0f); for (var i = 0; i < TFloat.LaneWidth; i++) { diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 8535aad..d734a92 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -27,7 +27,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs index 5d4f673..61f8155 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs @@ -317,7 +317,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase _resources.Remove(handle.ID, handle.Generation); #if DEBUG || GHOST_EDITOR - _resourceName.Remove(handle, out var name); + _resourceName.Remove(handle, out _); #endif } diff --git a/src/Runtime/Ghost.Graphics/Core/Mesh.cs b/src/Runtime/Ghost.Graphics/Core/Mesh.cs index 9ef37cc..d410226 100644 --- a/src/Runtime/Ghost.Graphics/Core/Mesh.cs +++ b/src/Runtime/Ghost.Graphics/Core/Mesh.cs @@ -41,10 +41,20 @@ public struct MeshletGroup [StructLayout(LayoutKind.Sequential)] public struct MeshletHierarchyNode { - public SphereBounds boundingSphere; // 16 bytes - public AABB boundingBox; // 24 bytes - public float maxParentError; // maximum error in this subtree - public uint nodeData; // packed leaf/internal metadata + public float4 minX; + public float4 minY; + public float4 minZ; + public float4 maxX; + public float4 maxY; + public float4 maxZ; + public float4 maxParentError; + + // x,y,z,w correspond to children 0,1,2,3. + // MSB (1 << 31) indicates it's an Internal Node. + // If MSB is 0, the remaining 31 bits are the MeshletIndex. + // If MSB is 1, the remaining 31 bits are the child MeshletHierarchyNode index. + // 0xFFFFFFFF means invalid/empty slot. + public uint4 nodeData; } [StructLayout(LayoutKind.Sequential)] @@ -183,18 +193,6 @@ public struct Mesh : IResourceReleasable get; internal set; } - internal Mesh(ReadOnlySpan vertices, ReadOnlySpan indices, Handle vertexBuffer, Handle indexBuffer) - { - Vertices = new UnsafeList(vertices.Length, AllocationHandle.Persistent); - Indices = new UnsafeList(indices.Length, AllocationHandle.Persistent); - Vertices.CopyFrom(vertices); - Indices.CopyFrom(indices); - VertexBuffer = vertexBuffer; - IndexBuffer = indexBuffer; - - this.ComputeBounds(); - } - public void ReleaseCpuResources() { _vertices.Dispose(); diff --git a/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs b/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs index 875f100..0f67e3e 100644 --- a/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs +++ b/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs @@ -110,51 +110,55 @@ public sealed partial class ResourceManager : IDisposable /// /// Creates a new mesh from the specified vertex and index data. /// - /// A UnsafeList containing the vertices that define the geometry of the mesh. Must contain at least one vertex. - /// A UnsafeList containing the indices that specify how vertices are connected to form primitives. Must contain at least one index. + /// A UnsafeList containing the vertices that define the geometry of the mesh. + /// A UnsafeList containing the indices that specify how vertices are connected to form primitives. + /// Indicates whether the mesh is expected to be updated frequently. If true, the underlying GPU buffers will be created with upload heap type for better CPU write performance. + /// The name of the mesh. /// An representing the newly created mesh. - public unsafe Handle CreateMesh(UnsafeList vertices, UnsafeList indices) + public unsafe Handle CreateMesh(UnsafeList vertices, UnsafeList indices, bool dynamic = false, string? name = null) { Logger.DebugAssert(!_disposed); + var vertexBufferDesc = new BufferDesc + { + Size = (uint)(vertices.Count * sizeof(Vertex)), + Stride = (uint)sizeof(Vertex), + Usage = BufferUsage.Vertex | BufferUsage.ShaderResource | BufferUsage.Raw, + HeapType = dynamic ? HeapType.Upload : HeapType.Default, + }; + + var indexBufferDesc = new BufferDesc + { + Size = (uint)(indices.Count * sizeof(uint)), + Stride = sizeof(uint), + Usage = BufferUsage.Index | BufferUsage.ShaderResource | BufferUsage.Raw, + HeapType = dynamic ? HeapType.Upload : HeapType.Default, + }; + + var meshDataBufferDesc = new BufferDesc + { + Size = (uint)sizeof(MeshData), + Stride = (uint)sizeof(MeshData), + Usage = BufferUsage.Raw | BufferUsage.ShaderResource, + HeapType = dynamic ? HeapType.Upload : HeapType.Default, + }; + + var hasName = name != null; + var vertexBuffer = _resourceAllocator.CreateBuffer(in vertexBufferDesc, hasName ? $"{name}_VertexBuffer" : "VertexBuffer"); + var indexBuffer = _resourceAllocator.CreateBuffer(in indexBufferDesc, hasName ? $"{name}_IndexBuffer" : "IndexBuffer"); + var meshDataBuffer = _resourceAllocator.CreateBuffer(in meshDataBufferDesc, hasName ? $"{name}_MeshDataBuffer" : "MeshDataBuffer"); + + var mesh = new Mesh + { + Vertices = vertices, + Indices = indices, + VertexBuffer = vertexBuffer, + IndexBuffer = indexBuffer, + MeshDataBuffer = meshDataBuffer, + }; + lock (_meshWriteLock) { - var vertexBufferDesc = new BufferDesc - { - Size = (uint)(vertices.Count * sizeof(Vertex)), - Stride = (uint)sizeof(Vertex), - Usage = BufferUsage.Vertex | BufferUsage.ShaderResource | BufferUsage.Raw, - HeapType = HeapType.Default, - }; - - var indexBufferDesc = new BufferDesc - { - Size = (uint)(indices.Count * sizeof(uint)), - Stride = sizeof(uint), - Usage = BufferUsage.Index | BufferUsage.ShaderResource | BufferUsage.Raw, - HeapType = HeapType.Default, - }; - - var objectBufferDesc = new BufferDesc - { - Size = (uint)sizeof(MeshData), - Stride = (uint)sizeof(MeshData), - Usage = BufferUsage.Raw | BufferUsage.ShaderResource, - HeapType = HeapType.Default, - }; - - var vertexBuffer = _resourceAllocator.CreateBuffer(in vertexBufferDesc, "VertexBuffer"); - var indexBuffer = _resourceAllocator.CreateBuffer(in indexBufferDesc, "IndexBuffer"); - var objectBuffer = _resourceAllocator.CreateBuffer(in objectBufferDesc, "ObjectBuffer"); - - var mesh = new Mesh - { - Vertices = vertices, - Indices = indices, - VertexBuffer = vertexBuffer, - IndexBuffer = indexBuffer, - MeshDataBuffer = objectBuffer, - }; var id = _meshes.Add(mesh, out var generation); return new Handle(id, generation); @@ -165,20 +169,20 @@ public sealed partial class ResourceManager : IDisposable /// Creates a new material instance using the specified shader. /// /// The identifier of the shader to associate with the new material. + /// The name of the material. /// An representing the newly created material. - public Handle CreateMaterial(Handle shader) + public Handle CreateMaterial(Handle shader, string? name = null) { Logger.DebugAssert(!_disposed); var material = new Material(); + if (material.SetShader(shader, this, _resourceDatabase, _resourceAllocator) != Error.None) + { + return Handle.Invalid; + } lock (_materialWriteLock) { - if (material.SetShader(shader, this, _resourceDatabase, _resourceAllocator) != Error.None) - { - return Handle.Invalid; - } - var id = _materials.Add(material, out var generation); return new Handle(id, generation); }