Skip to content

Commit 7b07233

Browse files
authored
feat: Mesh API for index and vertex buffer reading (#2858)
1 parent e162677 commit 7b07233

File tree

14 files changed

+1268
-335
lines changed

14 files changed

+1268
-335
lines changed

sources/engine/Stride.BepuPhysics/Stride.BepuPhysics/Systems/ShapeCacheSystem.cs

Lines changed: 20 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@
66
using Stride.BepuPhysics.Definitions;
77
using Stride.BepuPhysics.Definitions.Colliders;
88
using Stride.Core;
9-
using Stride.Core.IO;
109
using Stride.Core.Mathematics;
11-
using Stride.Core.Serialization;
12-
using Stride.Core.Serialization.Contents;
1310
using Stride.Graphics;
14-
using Stride.Graphics.Data;
1511
using Stride.Graphics.GeometricPrimitives;
12+
using Stride.Graphics.Semantics;
1613
using Stride.Rendering;
1714
using BufferPool = BepuUtilities.Memory.BufferPool;
1815
using Mesh = BepuPhysics.Collidables.Mesh;
@@ -140,72 +137,6 @@ private static void ExtractHull(DecomposedHulls hullDesc, out VertexPosition3[]
140137
}
141138
}
142139

143-
internal static IEnumerable<(Stride.Rendering.Mesh mesh, byte[] verticesBytes, byte[] indicesBytes)> ExtractMeshes(Model model, IServiceRegistry services)
144-
{
145-
foreach (var meshData in model.Meshes)
146-
{
147-
byte[]? verticesBytes = TryFetchBufferContent(meshData.Draw.VertexBuffers[0].Buffer, services);
148-
byte[]? indicesBytes = TryFetchBufferContent(meshData.Draw.IndexBuffer.Buffer, services);
149-
150-
if(verticesBytes is null || indicesBytes is null || verticesBytes.Length == 0 || indicesBytes.Length == 0)
151-
{
152-
throw new InvalidOperationException(
153-
$"Failed to find mesh buffers while attempting to {nameof(ExtractMeshes)}. " +
154-
$"Make sure that the {nameof(model)} is either an asset on disk, or has its buffer data attached to the buffer through '{nameof(AttachedReference)}'\n");
155-
}
156-
157-
yield return (meshData, verticesBytes, indicesBytes);
158-
}
159-
160-
// Get mesh data from GPU, shared memory or disk
161-
static unsafe byte[]? TryFetchBufferContent(Graphics.Buffer buffer, IServiceRegistry services)
162-
{
163-
var bufRef = AttachedReferenceManager.GetAttachedReference(buffer);
164-
if (bufRef?.Data != null && ((BufferData)bufRef.Data).Content is { } output)
165-
return output;
166-
167-
// Try to load it from disk, a file provider is required, editor does not provide one
168-
if (bufRef?.Url != null && services.GetService<IDatabaseFileProviderService>() is {} provider && provider.FileProvider is not null)
169-
{
170-
// We have to create a new one without providing services to ensure that it dumps the graphics buffer data to the attached reference below
171-
var cleanManager = new ContentManager(provider);
172-
var bufferCopy = cleanManager.Load<Graphics.Buffer>(bufRef.Url);
173-
try
174-
{
175-
return bufferCopy.GetSerializationData().Content;
176-
}
177-
finally
178-
{
179-
cleanManager.Unload(bufRef.Url);
180-
}
181-
}
182-
183-
// When the mesh is created at runtime, or when the file provider is null as can be the case in editor, fetch from GPU
184-
// will most likely break on non-dx11 APIs
185-
if (services.GetService<GraphicsContext>() is { } context)
186-
{
187-
output = new byte[buffer.SizeInBytes];
188-
fixed (byte* window = output)
189-
{
190-
var ptr = new DataPointer(window, output.Length);
191-
if (buffer.Description.Usage == GraphicsResourceUsage.Staging) // Directly if this is a staging resource
192-
{
193-
buffer.GetData(context.CommandList, buffer, ptr);
194-
}
195-
else // inefficient way to use the Copy method using dynamic staging texture
196-
{
197-
using var throughStaging = buffer.ToStaging();
198-
buffer.GetData(context.CommandList, throughStaging, ptr);
199-
}
200-
}
201-
202-
return output;
203-
}
204-
205-
return null;
206-
}
207-
}
208-
209140
internal static unsafe BepuUtilities.Memory.Buffer<Triangle> ExtractBepuMesh(Model model, IServiceRegistry services, BufferPool pool)
210141
{
211142
int totalIndices = 0;
@@ -215,42 +146,23 @@ internal static unsafe BepuUtilities.Memory.Buffer<Triangle> ExtractBepuMesh(Mod
215146
}
216147

217148
pool.Take<Triangle>(totalIndices / 3, out var triangles);
218-
var triangleAsV3 = triangles.As<Vector3>();
219-
int triangleV3Index = 0;
220-
221-
foreach ((Rendering.Mesh mesh, byte[] verticesBytes, byte[] indicesBytes) in ExtractMeshes(model, services))
149+
var bepuTriangles = triangles.As<Vector3>();
150+
var spanLeft = new Span<Vector3>(bepuTriangles.Memory, bepuTriangles.Length);
151+
foreach (var mesh in model.Meshes)
222152
{
223-
var vBindings = mesh.Draw.VertexBuffers[0];
224-
int vStride = vBindings.Declaration.VertexStride;
225-
var position = vBindings.Declaration.EnumerateWithOffsets().First(x => x.VertexElement.SemanticName == VertexElementUsage.Position);
153+
mesh.Draw.IndexBuffer.AsReadable(services, out var indexHelper, out int indexCount);
154+
mesh.Draw.VertexBuffers[0].AsReadable(services, out var vertexHelper, out int vertexCount);
226155

227-
if (position.VertexElement.Format is PixelFormat.R32G32B32_Float or PixelFormat.R32G32B32A32_Float == false)
228-
throw new ArgumentException($"{model}'s vertex position must be declared as float3 or float4");
156+
var copyJob = new VertexBufferHelper.CopyAsTriangleList { IndexBufferHelper = indexHelper };
157+
vertexHelper.Read<PositionSemantic, Vector3, VertexBufferHelper.CopyAsTriangleList>(spanLeft[..indexCount], copyJob);
229158

230-
fixed (byte* vBuffer = &verticesBytes[vBindings.Offset])
231-
fixed (byte* iBuffer = indicesBytes)
232-
{
233-
if (mesh.Draw.IndexBuffer.Is32Bit)
234-
{
235-
foreach (int i in new Span<int>(iBuffer + mesh.Draw.IndexBuffer.Offset, mesh.Draw.IndexBuffer.Count))
236-
{
237-
triangleAsV3[triangleV3Index++] = *(Vector3*)(vBuffer + vStride * i + position.Offset); // start of the buffer, move to the 'i'th vertex, and read from the position field of that vertex
238-
}
239-
}
240-
else
241-
{
242-
foreach (ushort i in new Span<ushort>(iBuffer + mesh.Draw.IndexBuffer.Offset, mesh.Draw.IndexBuffer.Count))
243-
{
244-
triangleAsV3[triangleV3Index++] = *(Vector3*)(vBuffer + vStride * i + position.Offset);
245-
}
246-
}
247-
}
159+
spanLeft = spanLeft[indexCount..];
248160
}
249161

250162
return triangles;
251163
}
252164

253-
private static unsafe void ExtractMeshBuffers(Model model, IServiceRegistry services, out VertexPosition3[] vertices, out int[] indices)
165+
private static void ExtractMeshBuffers(Model model, IServiceRegistry services, out VertexPosition3[] vertices, out int[] indices)
254166
{
255167
int totalVertices = 0, totalIndices = 0;
256168
foreach (var meshData in model.Meshes)
@@ -262,42 +174,19 @@ private static unsafe void ExtractMeshBuffers(Model model, IServiceRegistry serv
262174
vertices = new VertexPosition3[totalVertices];
263175
indices = new int[totalIndices];
264176

265-
int vertexWriteHead = 0;
266-
int indexWriteHead = 0;
267-
foreach ((Rendering.Mesh mesh, byte[] verticesBytes, byte[] indicesBytes) in ExtractMeshes(model, services))
268-
{
269-
int vertMappingStart = vertexWriteHead;
270-
fixed (byte* bytePtr = verticesBytes)
271-
{
272-
var vBindings = mesh.Draw.VertexBuffers[0];
273-
int count = vBindings.Count;
274-
int stride = vBindings.Declaration.VertexStride;
177+
var verticesLeft = MemoryMarshal.Cast<VertexPosition3, Vector3>(vertices.AsSpan());
178+
var indicesLeft = indices.AsSpan();
275179

276-
for (int i = 0, vHead = vBindings.Offset; i < count; i++, vHead += stride)
277-
{
278-
vertices[vertexWriteHead++].Position = *(Vector3*)(bytePtr + vHead);
279-
}
280-
}
180+
foreach (var mesh in model.Meshes)
181+
{
182+
mesh.Draw.IndexBuffer.AsReadable(services, out var indexHelper, out int indexCount);
183+
mesh.Draw.VertexBuffers[0].AsReadable(services, out var vertexHelper, out int vertexCount);
281184

282-
fixed (byte* bytePtr = indicesBytes)
283-
{
284-
var count = mesh.Draw.IndexBuffer.Count;
185+
vertexHelper.Copy<PositionSemantic, Vector3>(verticesLeft[..vertexCount]);
186+
indexHelper.CopyTo(indicesLeft[..indexCount]);
285187

286-
if (mesh.Draw.IndexBuffer.Is32Bit)
287-
{
288-
foreach (int indexBufferValue in new Span<int>(bytePtr + mesh.Draw.IndexBuffer.Offset, count))
289-
{
290-
indices[indexWriteHead++] = vertMappingStart + indexBufferValue;
291-
}
292-
}
293-
else
294-
{
295-
foreach (ushort indexBufferValue in new Span<ushort>(bytePtr + mesh.Draw.IndexBuffer.Offset, count))
296-
{
297-
indices[indexWriteHead++] = vertMappingStart + indexBufferValue;
298-
}
299-
}
300-
}
188+
verticesLeft = verticesLeft[vertexCount..];
189+
indicesLeft = indicesLeft[indexCount..];
301190
}
302191
}
303192

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
#nullable enable
5+
using System;
6+
using System.Runtime.InteropServices;
7+
using Stride.Core;
8+
9+
namespace Stride.Graphics;
10+
11+
/// <example>
12+
/// Reading the indices of a mesh:
13+
/// <code>
14+
/// <![CDATA[
15+
/// Model.Meshes[0].Draw.IndexBuffer.AsReadable(Services, out IndexBufferHelper helper, out int count)
16+
/// var indices = helper.To32Bit();
17+
/// ]]>
18+
/// </code>
19+
/// </example>
20+
public readonly struct IndexBufferHelper
21+
{
22+
/// <summary>
23+
/// Full index buffer, does not account for the binding offset or length
24+
/// </summary>
25+
public readonly byte[] DataOuter;
26+
public readonly IndexBufferBinding Binding;
27+
28+
/// <summary>
29+
/// Effective index buffer, handles the binding offset
30+
/// </summary>
31+
public Span<byte> DataInner => DataOuter.AsSpan(Binding.Offset, Binding.Count * (Binding.Is32Bit ? 4 : 2));
32+
33+
/// <inheritdoc cref="MeshExtension.AsReadable(IndexBufferBinding, IServiceRegistry, out IndexBufferHelper, out int)"/>
34+
public IndexBufferHelper(IndexBufferBinding binding, IServiceRegistry services, out int count)
35+
{
36+
DataOuter = MeshExtension.FetchBufferContentOrThrow(binding.Buffer, services);
37+
Binding = binding;
38+
count = Binding.Count;
39+
}
40+
41+
/// <summary>
42+
/// Branch to read the buffer as a 16 or 32 bit buffer, does not allocate
43+
/// </summary>
44+
/// <example>
45+
/// <code>
46+
/// if (Is32Bit(out var d32, out var d16))
47+
/// {
48+
/// foreach (var value in d32)
49+
/// {
50+
/// // Your logic for 32 bit
51+
/// }
52+
/// }
53+
/// else
54+
/// {
55+
/// foreach (var value in d16)
56+
/// {
57+
/// // Your logic for 16bit
58+
/// }
59+
/// }
60+
/// </code>
61+
/// </example>
62+
public bool Is32Bit(out Span<int> data32, out Span<ushort> data16)
63+
{
64+
if (Binding.Is32Bit)
65+
{
66+
data32 = MemoryMarshal.Cast<byte, int>(DataInner);
67+
data16 = Span<ushort>.Empty;
68+
return true;
69+
}
70+
else
71+
{
72+
data32 = Span<int>.Empty;
73+
data16 = MemoryMarshal.Cast<byte, ushort>(DataInner);
74+
return false;
75+
}
76+
}
77+
78+
/// <summary>
79+
/// Does not allocate if the buffer is already 32 bit,
80+
/// otherwise allocates a new int[] and copies the data into it
81+
/// </summary>
82+
public Span<int> To32Bit()
83+
{
84+
if (Is32Bit(out var d32, out var d16))
85+
{
86+
return d32;
87+
}
88+
else
89+
{
90+
var output = new int[d16.Length];
91+
for (int i = 0; i < d16.Length; i++)
92+
output[i] = d16[i];
93+
94+
return output;
95+
}
96+
}
97+
98+
/// <summary>
99+
/// Does not allocate if the buffer is already 16 bit,
100+
/// otherwise allocates a new ushort[] and copies the data into it
101+
/// </summary>
102+
public Span<ushort> To16Bit()
103+
{
104+
if (Is32Bit(out var d32, out var d16))
105+
{
106+
var output = new ushort[d32.Length];
107+
for (int i = 0; i < d32.Length; i++)
108+
output[i] = (ushort)d32[i];
109+
110+
return output;
111+
}
112+
else
113+
{
114+
return d16;
115+
}
116+
}
117+
118+
public void CopyTo(Span<int> dest)
119+
{
120+
if (Is32Bit(out var d32, out var d16))
121+
{
122+
d32.CopyTo(dest);
123+
}
124+
else
125+
{
126+
for (int i = 0; i < d16.Length; i++)
127+
dest[i] = d16[i];
128+
}
129+
}
130+
131+
public void CopyTo(Span<ushort> dest)
132+
{
133+
if (Is32Bit(out var d32, out var d16))
134+
{
135+
for (int i = 0; i < d32.Length; i++)
136+
dest[i] = (ushort)d32[i];
137+
}
138+
else
139+
{
140+
d16.CopyTo(dest);
141+
}
142+
}
143+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
#nullable enable
5+
using System.Runtime.InteropServices;
6+
7+
namespace Stride.Graphics.Interop;
8+
9+
[StructLayout(LayoutKind.Sequential, Size = 4)]
10+
public struct Byte4(byte x, byte y, byte z, byte w)
11+
{
12+
public byte X = x, Y = y, Z = z, W = w;
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
#nullable enable
5+
using System.Runtime.InteropServices;
6+
7+
namespace Stride.Graphics.Interop;
8+
9+
[StructLayout(LayoutKind.Sequential, Size = 8)]
10+
public struct UShort4(ushort x, ushort y, ushort z, ushort w)
11+
{
12+
public ushort X = x, Y = y, Z = z, W = w;
13+
}

0 commit comments

Comments
 (0)