commit accf4246997194128d54f3d4161a10d48dbe8457 Author: cosmonaut Date: Mon Jan 17 20:39:23 2022 -0800 initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..676182a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +moonlibs/**/* filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ca70f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +.vscode/ +.vs/ +Properties/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d51e2a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/MoonWorks"] + path = lib/MoonWorks + url = https://gitea.moonside.games/MoonsideGames/MoonWorks.git diff --git a/MoonWorksComputeSpriteBatch.csproj b/MoonWorksComputeSpriteBatch.csproj new file mode 100644 index 0000000..715009e --- /dev/null +++ b/MoonWorksComputeSpriteBatch.csproj @@ -0,0 +1,38 @@ + + + + Exe + net5.0 + true + + + + $(DefaultItemExcludes);lib\**\* + + + + + + + + + Always + + + + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + diff --git a/MoonWorksComputeSpriteBatch.sln b/MoonWorksComputeSpriteBatch.sln new file mode 100644 index 0000000..ea06904 --- /dev/null +++ b/MoonWorksComputeSpriteBatch.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoonWorksComputeSpriteBatch", "MoonWorksComputeSpriteBatch.csproj", "{1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Debug|x64.Build.0 = Debug|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Debug|x86.Build.0 = Debug|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Release|Any CPU.Build.0 = Release|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Release|x64.ActiveCfg = Release|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Release|x64.Build.0 = Release|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Release|x86.ActiveCfg = Release|Any CPU + {1AAA8AA3-3376-4369-A9CB-1F10A51DDE07}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/content/sprite.frag.spv b/content/sprite.frag.spv new file mode 100644 index 0000000..89181f8 Binary files /dev/null and b/content/sprite.frag.spv differ diff --git a/content/sprite.vert.spv b/content/sprite.vert.spv new file mode 100644 index 0000000..3804160 Binary files /dev/null and b/content/sprite.vert.spv differ diff --git a/content/spritebatch.comp.spv b/content/spritebatch.comp.spv new file mode 100644 index 0000000..fc2529a Binary files /dev/null and b/content/spritebatch.comp.spv differ diff --git a/lib/MoonWorks b/lib/MoonWorks new file mode 160000 index 0000000..8022cd1 --- /dev/null +++ b/lib/MoonWorks @@ -0,0 +1 @@ +Subproject commit 8022cd101124163306329913c918e379fd57aebf diff --git a/moonlibs/windows/FAudio.dll b/moonlibs/windows/FAudio.dll new file mode 100644 index 0000000..3214a4f --- /dev/null +++ b/moonlibs/windows/FAudio.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c842f655c68b0fd2c5ee7a8c21b497c135b61a730c20113758fa1f7ce379cf1 +size 307648 diff --git a/moonlibs/windows/Refresh.dll b/moonlibs/windows/Refresh.dll new file mode 100644 index 0000000..2202a38 --- /dev/null +++ b/moonlibs/windows/Refresh.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e283c69d0930dc39ff684524d8fb36e2c96d9e602dea33e6da84731b56ca72e +size 340992 diff --git a/moonlibs/windows/SDL2.dll b/moonlibs/windows/SDL2.dll new file mode 100644 index 0000000..84a14d4 --- /dev/null +++ b/moonlibs/windows/SDL2.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f65b50d693484d5d5a2bb8df1cf520628dd744e99e9a937bb936839b990943a0 +size 1561088 diff --git a/shaders/sprite.frag b/shaders/sprite.frag new file mode 100644 index 0000000..bad4382 --- /dev/null +++ b/shaders/sprite.frag @@ -0,0 +1,11 @@ +#version 450 + +layout(set = 1, binding = 0) uniform sampler2D uniformTexture; + +layout(location = 0) in vec2 fragCoord; +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = texture(uniformTexture, fragCoord); +} diff --git a/shaders/sprite.vert b/shaders/sprite.vert new file mode 100644 index 0000000..ebdfbeb --- /dev/null +++ b/shaders/sprite.vert @@ -0,0 +1,17 @@ +#version 450 + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec2 inTexCoord; + +layout(location = 0) out vec2 fragCoord; + +layout(set = 2, binding = 0) uniform UBO +{ + mat4 viewProjection; +} ubo; + +void main() +{ + gl_Position = ubo.viewProjection * vec4(inPosition, 1.0); + fragCoord = inTexCoord; +} diff --git a/shaders/spritebatch.comp b/shaders/spritebatch.comp new file mode 100644 index 0000000..6a17c04 --- /dev/null +++ b/shaders/spritebatch.comp @@ -0,0 +1,44 @@ +#version 450 + +struct Vertex +{ + vec3 position; + vec2 texcoord; +}; + +// Binding 0: vertices +layout(set = 0, binding = 0) buffer Vertices +{ + Vertex vertices[ ]; +}; + +// Binding 1: transform matrices +layout(set = 0, binding = 1) buffer Transforms +{ + mat4 transforms[ ]; +}; + +layout(set = 2, binding = 0) uniform UBO +{ + uint vertexCount; +} ubo; + +layout (local_size_x = 256) in; + +void main() +{ + // Current buffer index + uint index = gl_GlobalInvocationID.x; + + // Don't write past particle count + if (index >= ubo.vertexCount) + { + return; + } + + uint transformIndex = index / 4; + mat4 transform = transforms[transformIndex]; + vec4 position = vec4(vertices[index].position, 1.0); + + vertices[index].position = vec3(transform * position); +} diff --git a/src/CameraUniforms.cs b/src/CameraUniforms.cs new file mode 100644 index 0000000..162472d --- /dev/null +++ b/src/CameraUniforms.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; +using MoonWorks.Math; + +namespace MoonWorksComputeSpriteBatch +{ + [StructLayout(LayoutKind.Sequential)] + public struct CameraUniforms + { + public Matrix4x4 viewProjectionMatrix; + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..07e4aa6 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,21 @@ +using MoonWorks.Window; + +namespace MoonWorksComputeSpriteBatch +{ + class Program + { + static void Main(string[] args) + { + WindowCreateInfo windowCreateInfo = new WindowCreateInfo + { + WindowTitle = "Compute Sprite Batch", + WindowWidth = 1280, + WindowHeight = 720, + ScreenMode = ScreenMode.Windowed + }; + + TestGame game = new TestGame(windowCreateInfo, MoonWorks.Graphics.PresentMode.FIFO, 60, true); + game.Run(); + } + } +} diff --git a/src/Sprite.cs b/src/Sprite.cs new file mode 100644 index 0000000..f0d77e2 --- /dev/null +++ b/src/Sprite.cs @@ -0,0 +1,18 @@ +using MoonWorks.Graphics; + +namespace MoonWorksComputeSpriteBatch +{ + public struct Sprite + { + public Rect Texcoord { get; } + public uint Width { get; } + public uint Height { get; } + + public Sprite(Rect texcoord, uint width, uint height) + { + Texcoord = texcoord; + Width = width; + Height = height; + } + } +} diff --git a/src/SpriteBatch.cs b/src/SpriteBatch.cs new file mode 100644 index 0000000..1d6ba59 --- /dev/null +++ b/src/SpriteBatch.cs @@ -0,0 +1,173 @@ +using System.IO; +using System.Runtime.InteropServices; +using MoonWorks.Graphics; +using MoonWorks.Math; + +namespace MoonWorksComputeSpriteBatch +{ + [StructLayout(LayoutKind.Sequential)] + public struct SpriteBatchUniforms + { + public uint VertexCount { get; set; } + } + + public class SpriteBatch + { + private const int MAX_SPRITES = 16384; + private const int MAX_VERTICES = MAX_SPRITES * 4; + private const int MAX_INDICES = MAX_SPRITES * 6; + private const int MAX_MATRICES = MAX_SPRITES; + + private static ComputePipeline ComputePipeline = null; + + private Buffer VertexBuffer { get; } + private Buffer IndexBuffer { get; } + private Buffer TransformBuffer { get; } + + private readonly VertexPositionTexcoord[] Vertices; + private static readonly short[] Indices = GenerateIndexArray(); + private readonly Matrix4x4[] Transforms; + + private Texture CurrentTexture { get; set; } + private Sampler CurrentSampler { get; set; } + private uint VertexCount { get; set; } + private uint TransformCount { get; set; } + + public SpriteBatch(GraphicsDevice graphicsDevice) + { + if (ComputePipeline == null) + { + var computeShaderModule = new ShaderModule(graphicsDevice, Path.Combine(System.Environment.CurrentDirectory, "Content", "spritebatch.comp.spv")); + + var computeShaderState = new ShaderStageState + { + ShaderModule = computeShaderModule, + EntryPointName = "main", + UniformBufferSize = (uint) Marshal.SizeOf() + }; + + ComputePipeline = new ComputePipeline(graphicsDevice, computeShaderState, 2, 0); + } + + Vertices = new VertexPositionTexcoord[MAX_VERTICES]; + VertexBuffer = new Buffer( + graphicsDevice, + BufferUsageFlags.Vertex | BufferUsageFlags.Compute, + (uint)(MAX_VERTICES * Marshal.SizeOf()) + ); + + IndexBuffer = new Buffer( + graphicsDevice, + BufferUsageFlags.Index | BufferUsageFlags.Compute, + MAX_INDICES * sizeof(short) + ); + + Transforms = new Matrix4x4[MAX_MATRICES]; + TransformBuffer = new Buffer( + graphicsDevice, + BufferUsageFlags.Compute, + (uint)(MAX_MATRICES * Marshal.SizeOf()) + ); + + var commandBuffer = graphicsDevice.AcquireCommandBuffer(); + commandBuffer.SetBufferData(IndexBuffer, Indices); + graphicsDevice.Submit(commandBuffer); + } + + public void Start(Texture texture, Sampler sampler) + { + TransformCount = 0; + VertexCount = 0; + CurrentTexture = texture; + CurrentSampler = sampler; + } + + public void Add(Sprite sprite, Matrix4x4 transform) + { + Vertices[VertexCount].position.X = 0; + Vertices[VertexCount].position.Y = 0; + Vertices[VertexCount].texcoord.X = sprite.Texcoord.X; + Vertices[VertexCount].texcoord.Y = sprite.Texcoord.Y; + + Vertices[VertexCount + 1].position.X = sprite.Width; + Vertices[VertexCount + 1].position.Y = 0; + Vertices[VertexCount + 1].texcoord.X = sprite.Texcoord.X + sprite.Texcoord.W; + Vertices[VertexCount + 1].texcoord.Y = sprite.Texcoord.Y; + + Vertices[VertexCount + 2].position.X = 0; + Vertices[VertexCount + 2].position.Y = sprite.Height; + Vertices[VertexCount + 2].texcoord.X = sprite.Texcoord.X; + Vertices[VertexCount + 2].texcoord.Y = sprite.Texcoord.Y + sprite.Texcoord.H; + + Vertices[VertexCount + 3].position.X = sprite.Width; + Vertices[VertexCount + 3].position.Y = sprite.Height; + Vertices[VertexCount + 3].texcoord.X = sprite.Texcoord.X + sprite.Texcoord.W; + Vertices[VertexCount + 3].texcoord.Y = sprite.Texcoord.Y + sprite.Texcoord.H; + + VertexCount += 4; + + Transforms[TransformCount] = transform; + TransformCount += 1; + } + + public void Flush( + CommandBuffer commandBuffer, + RenderPass renderPass, + Framebuffer framebuffer, + Rect renderArea, + GraphicsPipeline graphicsPipeline, + CameraUniforms cameraUniforms + ) { + if (VertexCount == 0) + { + return; + } + + commandBuffer.SetBufferData(VertexBuffer, Vertices, 0, 0, VertexCount); + commandBuffer.SetBufferData(TransformBuffer, Transforms, 0, 0, TransformCount); + + commandBuffer.BindComputePipeline(ComputePipeline); + commandBuffer.BindComputeBuffers(VertexBuffer, TransformBuffer); + var offset = commandBuffer.PushComputeShaderUniforms(new SpriteBatchUniforms + { + VertexCount = VertexCount + }); + commandBuffer.DispatchCompute(VertexCount / 256, 1, 1, offset); + + commandBuffer.BeginRenderPass(renderPass, framebuffer, renderArea, Vector4.Zero); + commandBuffer.BindGraphicsPipeline(graphicsPipeline); + commandBuffer.BindVertexBuffers(VertexBuffer); + commandBuffer.BindIndexBuffer(IndexBuffer, IndexElementSize.Sixteen); + commandBuffer.BindFragmentSamplers(new TextureSamplerBinding { Texture = CurrentTexture, Sampler = CurrentSampler }); + offset = commandBuffer.PushVertexShaderUniforms(cameraUniforms); + + commandBuffer.DrawIndexedPrimitives( + 0, + 0, + VertexCount / 2, + offset, + 0 + ); + + commandBuffer.EndRenderPass(); + + VertexCount = 0; + TransformCount = 0; + } + + private static short[] GenerateIndexArray() + { + var result = new short[MAX_INDICES]; + for (int i = 0, j = 0; i < MAX_INDICES; i += 6, j += 4) + { + result[i] = (short)j; + result[i + 1] = (short)(j + 1); + result[i + 2] = (short)(j + 2); + result[i + 3] = (short)(j + 2); + result[i + 4] = (short)(j + 1); + result[i + 5] = (short)(j + 3); + } + return result; + } + } +} diff --git a/src/TestGame.cs b/src/TestGame.cs new file mode 100644 index 0000000..f73a041 --- /dev/null +++ b/src/TestGame.cs @@ -0,0 +1,237 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using MoonWorks; +using MoonWorks.Graphics; +using MoonWorks.Math; +using MoonWorks.Window; + +namespace MoonWorksComputeSpriteBatch +{ + public class TestGame : Game + { + private RenderPass mainRenderPass; + private RenderTarget mainColorTarget; + private Framebuffer mainFramebuffer; + private Rect mainRenderArea; + + private GraphicsPipeline spritePipeline; + + private SpriteBatch spriteBatch; + + private Texture whitePixel; + private Sampler sampler; + + private const int SPRITECOUNT = 8092; + private Vector3[] positions = new Vector3[SPRITECOUNT]; + + private uint windowWidth; + private uint windowHeight; + + private Random random = new Random(); + + public TestGame(WindowCreateInfo windowCreateInfo, PresentMode presentMode, int targetTimestep = 60, bool debugMode = false) : base(windowCreateInfo, presentMode, targetTimestep, debugMode) + { + windowWidth = windowCreateInfo.WindowWidth; + windowHeight = windowCreateInfo.WindowHeight; + + var vertexShaderModule = new ShaderModule(GraphicsDevice, Path.Combine(Environment.CurrentDirectory, "Content", "sprite.vert.spv")); + var fragmentShaderModule = new ShaderModule(GraphicsDevice, Path.Combine(Environment.CurrentDirectory, "Content", "sprite.frag.spv")); + + ColorTargetDescription colorTargetDescription = new ColorTargetDescription + { + Format = TextureFormat.R8G8B8A8, + MultisampleCount = SampleCount.One, + LoadOp = LoadOp.Clear, + StoreOp = StoreOp.Store + }; + + mainRenderPass = new RenderPass(GraphicsDevice, colorTargetDescription); + + mainColorTarget = RenderTarget.CreateBackedRenderTarget( + GraphicsDevice, + windowWidth, + windowHeight, + TextureFormat.R8G8B8A8, + false + ); + + mainFramebuffer = new Framebuffer( + GraphicsDevice, + windowWidth, + windowHeight, + mainRenderPass, + null, + mainColorTarget + ); + + mainRenderArea = new Rect + { + X = 0, + Y = 0, + W = (int) windowWidth, + H = (int) windowHeight + }; + + /* Pipeline */ + + ColorTargetBlendState[] colorTargetBlendStates = new ColorTargetBlendState[1] + { + ColorTargetBlendState.None + }; + + ColorBlendState colorBlendState = new ColorBlendState + { + LogicOpEnable = false, + LogicOp = LogicOp.NoOp, + BlendConstants = new BlendConstants(), + ColorTargetBlendStates = colorTargetBlendStates + }; + + DepthStencilState depthStencilState = DepthStencilState.Disable; + + ShaderStageState vertexShaderState = new ShaderStageState + { + ShaderModule = vertexShaderModule, + EntryPointName = "main", + UniformBufferSize = (uint) Marshal.SizeOf() + }; + + ShaderStageState fragmentShaderState = new ShaderStageState + { + ShaderModule = fragmentShaderModule, + EntryPointName = "main", + UniformBufferSize = 0 + }; + + MultisampleState multisampleState = MultisampleState.None; + + GraphicsPipelineLayoutInfo pipelineLayoutInfo = new GraphicsPipelineLayoutInfo + { + VertexSamplerBindingCount = 0, + FragmentSamplerBindingCount = 1 + }; + + RasterizerState rasterizerState = RasterizerState.CCW_CullNone; + + var vertexBindings = new VertexBinding[1] + { + new VertexBinding + { + Binding = 0, + InputRate = VertexInputRate.Vertex, + Stride = (uint) Marshal.SizeOf() + } + }; + + var vertexAttributes = new VertexAttribute[2] + { + new VertexAttribute + { + Binding = 0, + Location = 0, + Format = VertexElementFormat.Vector3, + Offset = 0 + }, + new VertexAttribute + { + Binding = 0, + Location = 1, + Format = VertexElementFormat.Vector2, + Offset = (uint) Marshal.OffsetOf("texcoord") + } + }; + + VertexInputState vertexInputState = new VertexInputState + { + VertexBindings = vertexBindings, + VertexAttributes = vertexAttributes + }; + + var viewports = new Viewport[1] + { + new Viewport + { + X = 0, + Y = 0, + W = windowWidth, + H = windowHeight, + MinDepth = 0, + MaxDepth = 1 + } + }; + + var scissors = new Rect[1] + { + new Rect + { + X = 0, + Y = 0, + W = (int) windowWidth, + H = (int) windowHeight + } + }; + + ViewportState viewportState = new ViewportState + { + Viewports = viewports, + Scissors = scissors + }; + + var graphicsPipelineCreateInfo = new GraphicsPipelineCreateInfo + { + ColorBlendState = colorBlendState, + DepthStencilState = depthStencilState, + VertexShaderState = vertexShaderState, + FragmentShaderState = fragmentShaderState, + MultisampleState = multisampleState, + PipelineLayoutInfo = pipelineLayoutInfo, + RasterizerState = rasterizerState, + PrimitiveType = PrimitiveType.TriangleList, + VertexInputState = vertexInputState, + ViewportState = viewportState, + RenderPass = mainRenderPass + }; + + spritePipeline = new GraphicsPipeline(GraphicsDevice, graphicsPipelineCreateInfo); + + spriteBatch = new SpriteBatch(GraphicsDevice); + + whitePixel = Texture.CreateTexture2D(GraphicsDevice, 1, 1, TextureFormat.R8G8B8A8, TextureUsageFlags.Sampler); + + var commandBuffer = GraphicsDevice.AcquireCommandBuffer(); + commandBuffer.SetTextureData(whitePixel, new Color[] { Color.White }); + GraphicsDevice.Submit(commandBuffer); + + sampler = new Sampler(GraphicsDevice, SamplerCreateInfo.PointWrap); + } + + protected override void Update(TimeSpan dt) + { + for (var i = 0; i < SPRITECOUNT; i += 1) + { + positions[i].X = (float) (random.NextDouble() * windowWidth) - 64; + positions[i].Y = (float) (random.NextDouble() * windowHeight) - 64; + } + } + + protected override void Draw(TimeSpan dt, double alpha) + { + var commandBuffer = GraphicsDevice.AcquireCommandBuffer(); + + var viewProjection = Matrix4x4.CreateLookAt(new Vector3(windowWidth / 2, windowHeight / 2, 1), new Vector3(windowWidth / 2, windowHeight / 2, 0), Vector3.Up) * Matrix4x4.CreateOrthographic(windowWidth, windowHeight, 0.1f, 1000); + spriteBatch.Start(whitePixel, sampler); + + for (var i = 0; i < SPRITECOUNT; i += 1) + { + var transform = Matrix4x4.CreateTranslation(positions[i]); + spriteBatch.Add(new Sprite(new Rect { X = 0, Y = 0, W = 0, H = 0 }, 128, 128), transform); + } + + spriteBatch.Flush(commandBuffer, mainRenderPass, mainFramebuffer, mainRenderArea, spritePipeline, new CameraUniforms { viewProjectionMatrix = viewProjection }); + + commandBuffer.QueuePresent(mainColorTarget.TextureSlice, Filter.Nearest); + GraphicsDevice.Submit(commandBuffer); + } + } +} diff --git a/src/VertexPositionTexcoord.cs b/src/VertexPositionTexcoord.cs new file mode 100644 index 0000000..9e95c45 --- /dev/null +++ b/src/VertexPositionTexcoord.cs @@ -0,0 +1,15 @@ +using MoonWorks.Math; +using System.Runtime.InteropServices; + +namespace MoonWorksComputeSpriteBatch +{ + // SPIR-V requires vectors to not cross 16-byte boundaries + [StructLayout(LayoutKind.Explicit, Size=32)] + public struct VertexPositionTexcoord + { + [FieldOffset(0)] + public Vector3 position; + [FieldOffset(16)] + public Vector2 texcoord; + } +}