Skip to content

Commit ad186e5

Browse files
Resolve WebGPU native assets across deployment layouts
Resolve the DirectX Shader Compiler through the host's native-library search and recover its location from the loaded module, replacing the application-directory probe that failed under the NuGet runtimes layout. Anchor fallback probing to the library's own image so Native AOT shared libraries hosted by foreign processes resolve wgpu_native and DXC, and register the Silk.NET window platforms explicitly so trimming cannot remove platform discovery.
1 parent da11e33 commit ad186e5

4 files changed

Lines changed: 244 additions & 3 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Runtime.InteropServices;
5+
using FilePath = System.IO.Path;
6+
7+
namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native;
8+
9+
/// <summary>
10+
/// Locates this library's own loaded module on disk.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// Under Native AOT this code is compiled into a shared library or executable image, and when
15+
/// that image is a plugin loaded by a foreign host, <see cref="AppContext.BaseDirectory"/>
16+
/// points at the host process rather than the image. Anchoring native-asset probing to the
17+
/// image itself keeps resolution correct there. Under JIT hosting managed code does not live
18+
/// inside a loadable image, so resolution reports failure and callers fall back to standard
19+
/// probing.
20+
/// </para>
21+
/// <para>
22+
/// The runtime provides no managed API for recovering a loaded image's path; the mechanism
23+
/// here follows <see href="https://github.com/dotnet/runtime/issues/112290"/> and the
24+
/// address-anchored approach demonstrated in
25+
/// <see href="https://github.com/dotnet/runtime/discussions/122457"/>.
26+
/// </para>
27+
/// </remarks>
28+
internal static unsafe partial class NativeModuleLocator
29+
{
30+
/// <summary>
31+
/// <c>GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT</c> from <c>libloaderapi.h</c>. The
32+
/// returned handle does not take a reference on the module, so it must not be freed.
33+
/// </summary>
34+
private const uint GetModuleHandleExFlagUnchangedRefCount = 0x00000002;
35+
36+
/// <summary>
37+
/// <c>GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS</c> from <c>libloaderapi.h</c>. The address
38+
/// argument is any address inside the target module rather than a module name.
39+
/// </summary>
40+
private const uint GetModuleHandleExFlagFromAddress = 0x00000004;
41+
42+
/// <summary>
43+
/// Attempts to resolve the directory of the image containing this library's code.
44+
/// </summary>
45+
/// <param name="directory">Receives the image directory.</param>
46+
/// <returns><see langword="true"/> when the image path could be resolved.</returns>
47+
public static bool TryGetModuleDirectory(out string directory)
48+
{
49+
directory = string.Empty;
50+
delegate* managed<void> anchor = &Anchor;
51+
52+
string modulePath;
53+
if (OperatingSystem.IsWindows())
54+
{
55+
if (GetModuleHandleExW(GetModuleHandleExFlagFromAddress | GetModuleHandleExFlagUnchangedRefCount, (nint)anchor, out nint module) == 0)
56+
{
57+
return false;
58+
}
59+
60+
modulePath = GetModuleFilePath(module);
61+
}
62+
else
63+
{
64+
// dladdr reports the image containing an address. Resolving it through the process
65+
// exports avoids binding to a platform-specific libdl name.
66+
if (!NativeLibrary.TryGetExport(NativeLibrary.GetMainProgramHandle(), "dladdr", out nint export))
67+
{
68+
return false;
69+
}
70+
71+
delegate* unmanaged<void*, DlInfo*, int> dladdr = (delegate* unmanaged<void*, DlInfo*, int>)export;
72+
DlInfo info;
73+
if (dladdr(anchor, &info) == 0 || info.FileName is null)
74+
{
75+
return false;
76+
}
77+
78+
modulePath = Marshal.PtrToStringUTF8((nint)info.FileName) ?? string.Empty;
79+
}
80+
81+
if (modulePath.Length == 0)
82+
{
83+
return false;
84+
}
85+
86+
string? parent = FilePath.GetDirectoryName(modulePath);
87+
if (parent is null || parent.Length == 0)
88+
{
89+
return false;
90+
}
91+
92+
directory = parent;
93+
return true;
94+
}
95+
96+
/// <summary>
97+
/// Reads the file path of a loaded native module.
98+
/// </summary>
99+
/// <param name="module">The loaded module handle.</param>
100+
/// <returns>The module's file path, or an empty string when it cannot be read.</returns>
101+
public static string GetModuleFilePath(nint module)
102+
{
103+
Span<char> buffer = stackalloc char[260];
104+
fixed (char* bufferPointer = buffer)
105+
{
106+
uint length = GetModuleFileNameW(module, bufferPointer, (uint)buffer.Length);
107+
if (length > 0 && length < buffer.Length)
108+
{
109+
return new string(buffer[..(int)length]);
110+
}
111+
}
112+
113+
// Cold fallback for paths beyond MAX_PATH up to the documented Windows maximum.
114+
char[] largeBuffer = new char[short.MaxValue];
115+
fixed (char* largePointer = largeBuffer)
116+
{
117+
uint length = GetModuleFileNameW(module, largePointer, (uint)largeBuffer.Length);
118+
return length > 0 && length < largeBuffer.Length ? new string(largeBuffer, 0, (int)length) : string.Empty;
119+
}
120+
}
121+
122+
/// <summary>
123+
/// Provides an address that lives inside this library's compiled image.
124+
/// </summary>
125+
private static void Anchor()
126+
{
127+
}
128+
129+
/// <summary>
130+
/// Reads the file path of a loaded module into the supplied buffer.
131+
/// </summary>
132+
/// <param name="module">The loaded module handle.</param>
133+
/// <param name="filename">The buffer receiving the path.</param>
134+
/// <param name="size">The buffer length in characters.</param>
135+
/// <returns>The number of characters written, or zero on failure.</returns>
136+
[LibraryImport("kernel32", EntryPoint = "GetModuleFileNameW", SetLastError = true)]
137+
private static partial uint GetModuleFileNameW(nint module, char* filename, uint size);
138+
139+
/// <summary>
140+
/// Retrieves a module handle for the image containing the supplied address.
141+
/// </summary>
142+
/// <param name="flags">The handle retrieval flags.</param>
143+
/// <param name="address">An address inside the target image.</param>
144+
/// <param name="module">Receives the module handle.</param>
145+
/// <returns>A non-zero value on success.</returns>
146+
[LibraryImport("kernel32", EntryPoint = "GetModuleHandleExW", SetLastError = true)]
147+
private static partial int GetModuleHandleExW(uint flags, nint address, out nint module);
148+
149+
/// <summary>
150+
/// Image information reported by <c>dladdr</c>.
151+
/// </summary>
152+
[StructLayout(LayoutKind.Sequential)]
153+
private struct DlInfo
154+
{
155+
/// <summary>
156+
/// The path of the image containing the queried address.
157+
/// </summary>
158+
public byte* FileName;
159+
160+
/// <summary>
161+
/// The base address of the image.
162+
/// </summary>
163+
public void* BaseAddress;
164+
165+
/// <summary>
166+
/// The name of the nearest symbol.
167+
/// </summary>
168+
public byte* SymbolName;
169+
170+
/// <summary>
171+
/// The address of the nearest symbol.
172+
/// </summary>
173+
public void* SymbolAddress;
174+
}
175+
}

src/ImageSharp.Drawing.WebGPU/Native/WebGPUNative.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ static WebGPUNative()
2929
// This prevents an older same-named dependency from satisfying our generated v29 ABI.
3030
LibraryHandle = NativeLibrary.Load(libraryPath);
3131
}
32+
else if (NativeModuleLocator.TryGetModuleDirectory(out string moduleDirectory))
33+
{
34+
// A Native AOT shared library hosted by a foreign process probes relative to that
35+
// host, so also look beside this library's own image.
36+
string anchoredPath = System.IO.Path.Combine(moduleDirectory, fileName);
37+
if (File.Exists(anchoredPath))
38+
{
39+
LibraryHandle = NativeLibrary.Load(anchoredPath);
40+
}
41+
}
3242

3343
NativeLibrary.SetDllImportResolver(typeof(WebGPUNative).Assembly, ResolveLibrary);
3444
}

src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -608,9 +608,54 @@ private static void EnsureInitialized()
608608
/// <returns><see langword="true"/> when both files are available, or on non-Windows platforms.</returns>
609609
private static bool TryGetDxcPaths(out string dxcPath, out string dxilPath)
610610
{
611-
dxcPath = FilePath.Combine(AppContext.BaseDirectory, "dxcompiler.dll");
612-
dxilPath = FilePath.Combine(AppContext.BaseDirectory, "dxil.dll");
613-
return !OperatingSystem.IsWindows() || (File.Exists(dxcPath) && File.Exists(dxilPath));
611+
dxcPath = string.Empty;
612+
dxilPath = string.Empty;
613+
if (!OperatingSystem.IsWindows())
614+
{
615+
return true;
616+
}
617+
618+
// wgpu receives the compiler as a file location rather than binding it as an import,
619+
// so resolve it through the host's native-library search. That search already honors
620+
// every deployment layout: flat outputs, the NuGet runtimes layout, and
621+
// self-contained publishes.
622+
if (!NativeLibrary.TryLoad("dxcompiler.dll", typeof(WebGPURuntime).Assembly, null, out IntPtr module))
623+
{
624+
// A Native AOT shared library hosted by a foreign process probes relative to that
625+
// host, so fall back to the directory of this library's own image.
626+
if (NativeModuleLocator.TryGetModuleDirectory(out string moduleDirectory))
627+
{
628+
dxcPath = FilePath.Combine(moduleDirectory, "dxcompiler.dll");
629+
dxilPath = FilePath.Combine(moduleDirectory, "dxil.dll");
630+
if (File.Exists(dxcPath) && File.Exists(dxilPath))
631+
{
632+
return true;
633+
}
634+
}
635+
636+
dxcPath = string.Empty;
637+
dxilPath = string.Empty;
638+
return false;
639+
}
640+
641+
try
642+
{
643+
dxcPath = NativeModuleLocator.GetModuleFilePath(module);
644+
}
645+
finally
646+
{
647+
NativeLibrary.Free(module);
648+
}
649+
650+
if (dxcPath.Length == 0)
651+
{
652+
return false;
653+
}
654+
655+
// The compiler loads dxil.dll from its own directory to sign shaders, so both files
656+
// ship side by side in every layout.
657+
dxilPath = FilePath.Combine(FilePath.GetDirectoryName(dxcPath)!, "dxil.dll");
658+
return File.Exists(dxilPath);
614659
}
615660

616661
/// <summary>

src/ImageSharp.Drawing.WebGPU/WebGPUWindow.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Diagnostics.CodeAnalysis;
55
using Silk.NET.Maths;
66
using Silk.NET.Windowing;
7+
using Silk.NET.Windowing.Glfw;
8+
using Silk.NET.Windowing.Sdl;
79
using NativeWindowBorder = Silk.NET.Windowing.WindowBorder;
810
using NativeWindowState = Silk.NET.Windowing.WindowState;
911

@@ -26,6 +28,15 @@ public sealed class WebGPUWindow : IDisposable
2628
private readonly WebGPUSurfaceResources resources;
2729
private bool isDisposed;
2830

31+
static WebGPUWindow()
32+
{
33+
// Silk.NET discovers window platforms by scanning assemblies with reflection, which
34+
// trimming removes under Native AOT. Registering the shipped platforms explicitly keeps
35+
// window creation working in every deployment model.
36+
GlfwWindowing.RegisterPlatform();
37+
SdlWindowing.RegisterPlatform();
38+
}
39+
2940
/// <summary>
3041
/// Initializes a new instance of the <see cref="WebGPUWindow"/> class.
3142
/// </summary>

0 commit comments

Comments
 (0)