Skip to content

Commit

Permalink
[2.X] Add a helper for custom IO in Assimp (#2199)
Browse files Browse the repository at this point in the history
* Add a helper for custom IO in Assimp

* Add license for model

* Move global using to csproj

* dedupe

* fix comp error
  • Loading branch information
Perksey committed Jul 1, 2024
1 parent 9b12fbe commit 8ac952e
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 0 deletions.
15 changes: 15 additions & 0 deletions Silk.NET.sln
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.OpenXR.Extensions.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.OpenXR.Extensions.ANDROIDSYS", "src\OpenXR\Extensions\Silk.NET.OpenXR.Extensions.ANDROIDSYS\Silk.NET.OpenXR.Extensions.ANDROIDSYS.csproj", "{01B6FFA0-5B37-44EA-ABDF-7BABD05874C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Assimp.Tests", "src\Assimp\Silk.NET.Assimp.Tests\Silk.NET.Assimp.Tests.csproj", "{12D0A556-7DDF-4902-8911-1DA3F6331149}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -3757,6 +3759,18 @@ Global
{01B6FFA0-5B37-44EA-ABDF-7BABD05874C5}.Release|x64.Build.0 = Release|Any CPU
{01B6FFA0-5B37-44EA-ABDF-7BABD05874C5}.Release|x86.ActiveCfg = Release|Any CPU
{01B6FFA0-5B37-44EA-ABDF-7BABD05874C5}.Release|x86.Build.0 = Release|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Debug|x64.ActiveCfg = Debug|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Debug|x64.Build.0 = Debug|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Debug|x86.ActiveCfg = Debug|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Debug|x86.Build.0 = Debug|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Release|Any CPU.Build.0 = Release|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Release|x64.ActiveCfg = Release|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Release|x64.Build.0 = Release|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Release|x86.ActiveCfg = Release|Any CPU
{12D0A556-7DDF-4902-8911-1DA3F6331149}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -4057,6 +4071,7 @@ Global
{B70533BB-FB84-4BC3-888C-88E5F40FD22D} = {90471225-AC23-424E-B62E-F6EC4C6ECAC0}
{25ABCA5E-4FF6-43ED-9A5E-443E1373EC5C} = {90471225-AC23-424E-B62E-F6EC4C6ECAC0}
{01B6FFA0-5B37-44EA-ABDF-7BABD05874C5} = {90471225-AC23-424E-B62E-F6EC4C6ECAC0}
{12D0A556-7DDF-4902-8911-1DA3F6331149} = {6EADA376-E83F-40B7-9539-71DD17AEF7A4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F5273D7F-3334-48DF-94E3-41AE6816CD4D}
Expand Down
1 change: 1 addition & 0 deletions src/Assimp/Silk.NET.Assimp.Tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!Model.zip
54 changes: 54 additions & 0 deletions src/Assimp/Silk.NET.Assimp.Tests/CustomFileIOTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Reflection;
using Xunit.Abstractions;

namespace Silk.NET.Assimp.Tests;

public unsafe class CustomFileIOTests
{
private readonly ITestOutputHelper _out;

public CustomFileIOTests(ITestOutputHelper @out)
{
_out = @out;
}

[Fact]
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
public void ImportTest()
{
using var modelFile = Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(CustomFileIOTests), "Model.zip");
Assert.NotEqual(modelFile, null);
using var modelArch = new ZipArchive(modelFile!, ZipArchiveMode.Read);
using var assimp = Assimp.GetApi();
using var io = new CustomFileIO
(
(file, access, mode) =>
{
_out.WriteLine($"Opening file: {file} - {access}/{mode}");
Assert.Equal((int) (access & FileAccess.Write), 0);
Assert.Equal(mode, FileMode.Open);
using var compressed = modelArch.GetEntry(file.ToString())?.Open();
if (compressed is null)
{
return null;
}
var ms = new MemoryStream();
compressed.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return ms;
}
);
var scene = assimp.ImportFileEx("M1 Garand.obj", (uint) PostProcessSteps.Triangulate, ref io.FileIO);
if (scene == null || scene->MFlags == Assimp.SceneFlagsIncomplete || scene->MRootNode == null)
{
_out.WriteLine($"Assimp returned error: {assimp.GetErrorStringS()}");
}

Assert.NotEqual((nint)scene, 0);
Assert.NotEqual(scene->MFlags, (uint)Assimp.SceneFlagsIncomplete);
Assert.NotEqual((nint)scene->MRootNode, 0);
}
}
1 change: 1 addition & 0 deletions src/Assimp/Silk.NET.Assimp.Tests/Model.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Licensed under the CC-BY 4.0 license from https://sketchfab.com/3d-models/m1-garandfree-highpolly-game-asset-army-weapon-5c2b4d83f36f4963aabd2750ed5a30e7
Binary file added src/Assimp/Silk.NET.Assimp.Tests/Model.zip
Binary file not shown.
39 changes: 39 additions & 0 deletions src/Assimp/Silk.NET.Assimp.Tests/Silk.NET.Assimp.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Silk.NET.Assimp\Silk.NET.Assimp.csproj" />
</ItemGroup>

<ItemGroup>
<None Remove="Model.zip" />
<EmbeddedResource Include="Model.zip" />
<Content Include="../../Native/Silk.NET.Assimp.Native/runtimes/$(RuntimeIdentifier)/native/*" CopyToPublishDirectory="PreserveNewest" CopyToOutputDirectory="PreserveNewest" />
<Using Include="Xunit" />
</ItemGroup>

</Project>
180 changes: 180 additions & 0 deletions src/Assimp/Silk.NET.Assimp/CustomFileIO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Silk.NET.Core.Native;

namespace Silk.NET.Assimp;

#nullable enable

#if NET5_0_OR_GREATER
/// <summary>
/// A delegate for use with <see cref="OpenFileCustomIOCallback"/> to open a <see cref="Stream"/> representing the
/// requested file, or <c>null</c> if this was not possible.
/// </summary>
/// <param name="file">The file to open.</param>
/// <param name="access">The access mode.</param>
/// <param name="mode">The file open mode.</param>
/// <returns>The opened file, or null if not successful.</returns>
public delegate Stream? OpenFileCustomIOCallback(ReadOnlySpan<char> file, FileAccess access, FileMode mode);

/// <summary>
/// An adapter for using a <see cref="Stream"/> through Assimp's <see cref="FileIO"/> interface.
/// </summary>
public sealed unsafe class CustomFileIO : IDisposable
{
private OpenFileCustomIOCallback _streamFactory;
private FileIO _fileIO;

public CustomFileIO(OpenFileCustomIOCallback streamFactory)
{
var self = GCHandle.Alloc(this);
if (!self.IsAllocated)
{
throw new InvalidOperationException("Failed to allocate GCHandle for CustomFileIO");
}

_streamFactory = streamFactory;
_fileIO = new FileIO
{
OpenProc = (delegate* unmanaged[Cdecl]<FileIO*, byte*, byte*, File*>) &OpenFile,
CloseProc = (delegate* unmanaged[Cdecl]<FileIO*, File*, void>) &CloseFile,
UserData = (byte*) GCHandle.ToIntPtr(self)
};
}

/// <summary>
/// Gets a reference to the <see cref="FileIO"/> to pass to native.
/// </summary>
public ref FileIO FileIO => ref _fileIO;

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static File* OpenFile(FileIO* mFileSystem, byte* pFile, byte* pMode)
{
if (GCHandle.FromIntPtr((nint) mFileSystem->UserData).Target is not CustomFileIO self)
{
throw new InvalidOperationException("Invalid UserData for FileIO.");
}

// We can use SilkMarshal.PtrToString for the string marshalling here, but given this is a short-lived
// allocation that is likely to be small, we can survive without it.
var len = 0;
while (pFile[len] != 0)
{
len++;
}

var fileSpan = new Span<byte>(pFile, len);
var charLen = Encoding.UTF8.GetCharCount(fileSpan);
var chars = charLen > 256 ? new char[charLen] : stackalloc char[charLen];
Encoding.UTF8.GetChars(fileSpan, chars);

len = 0;
while (pMode[len] != 0)
{
len++;
}

var modeSpan = new Span<byte>(pMode, len);
var plus = modeSpan.Contains((byte) '+');
var write = modeSpan.Contains((byte) 'w') || modeSpan.Contains((byte) 'W');
var read = modeSpan.Contains((byte) 'r') || modeSpan.Contains((byte) 'R');
var append = modeSpan.Contains((byte) 'a') || modeSpan.Contains((byte) 'A');
var access = plus ? FileAccess.ReadWrite :
write || append ? FileAccess.Write :
read ? FileAccess.Read : throw new ArgumentException
("Invalid mode, must have 'r', 'w', 'a', or '+' set.", nameof(pMode));
var mode = append && !plus ? FileMode.Append :
read || append ? FileMode.Open :
write ? FileMode.Create : throw new ArgumentException("Invalid mode", nameof(pMode));

var stream = self._streamFactory(chars, access, mode);
if (stream is null)
{
return null;
}

if (append && plus)
{
stream.Seek(0, SeekOrigin.End);
}

var gch = GCHandle.Alloc(stream);
if (!gch.IsAllocated)
{
throw new InvalidOperationException("Failed to allocate GCHandle.");
}

var file = (File*)SilkMarshal.Allocate(sizeof(File));
*file = new File
{
ReadProc = (delegate* unmanaged[Cdecl]<File*, byte*, nuint, nuint, nuint>)&ReadFile,
WriteProc = (delegate* unmanaged[Cdecl]<File*, byte*, nuint, nuint, nuint>)&WriteFile,
FileSizeProc = (delegate* unmanaged[Cdecl]<File*, nuint>)&FileSize,
TellProc = (delegate* unmanaged[Cdecl]<File*, nuint>)&FileTell,
FlushProc = (delegate* unmanaged[Cdecl]<File*, void>)&FileFlush,
SeekProc = (delegate* unmanaged[Cdecl]<File*, nuint, Origin, Return>)&FileSeek,
UserData = (byte*)GCHandle.ToIntPtr(gch)
};

return file;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void CloseFile(FileIO* mFileSystem, File* pFile)
{
var sHandle = GCHandle.FromIntPtr((nint) pFile->UserData);
if (sHandle.Target is not Stream s)
{
throw new InvalidOperationException("Invalid UserData for FileIO.");
}

s.Dispose();
sHandle.Free();
SilkMarshal.Free((nint) pFile);
}

public static Stream GetStream(File* file) =>
GCHandle.FromIntPtr((nint) file->UserData).Target is not Stream s
? throw new InvalidOperationException("Invalid UserData for File.")
: s;

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static nuint ReadFile(File* file, byte* buffer, nuint size, nuint count) =>
((nuint) GetStream(file).Read(new Span<byte>(buffer, (int) (size * count)))) / size;

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static nuint WriteFile(File* file, byte* buffer, nuint size, nuint count)
{
GetStream(file).Write(new Span<byte>(buffer, (int) (size * count)));
return count;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static nuint FileSize(File* file) =>
(nuint) GetStream(file).Length;

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static nuint FileTell(File* file) =>
(nuint) GetStream(file).Position;

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void FileFlush(File* file)
=> GetStream(file).Flush();

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static Return FileSeek(File* file, nuint offset, Origin origin)
{
GetStream(file).Seek((long) offset, (SeekOrigin) origin);
return Return.Success;
}

/// <inheritdoc />
public void Dispose() => GCHandle.FromIntPtr((nint) _fileIO.UserData).Free();
}
#endif

0 comments on commit 8ac952e

Please sign in to comment.