diff --git a/Aquarium.Engine.slnx b/Aquarium.Engine.slnx index 2eb83ff..5335fd0 100644 --- a/Aquarium.Engine.slnx +++ b/Aquarium.Engine.slnx @@ -1,5 +1,7 @@ + + diff --git a/README.md b/README.md index 651fc91..e8606e4 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,17 @@ compiles changed shaders in the running process, then swaps the D3D shader objects only after compilation succeeds. A bad shader edit leaves the previous working shaders bound and writes the compiler failure to stderr. +Live gameplay/runtime code is split into `Aquarium.Engine.Live` behind +`Aquarium.Engine.Contracts`. The host loads that live DLL through a collectible +assembly load context, while `dev-watch.ps1` builds live-only changes into +`artifacts\dev-reload\live-slots` and updates `live-current.txt`. The running +host sees the pointer change, loads and starts the new runtime first, then +disposes the old one only after the replacement is valid. A bad live DLL leaves +the previous runtime alive and reports the reload failure to stderr. Successful +reloads rehydrate through CultCache without restarting the window or D3D device. +Host, renderer, contract, project, and script changes still rebuild and restart +the apphost. + Runtime live state is a typed CultCache document, not a loose sidecar file. The first document is `epiphany.aquarium.live_state`, currently banking camera target, yaw, pitch, distance, time, and save generation every few frames so a diff --git a/scripts/dev-reload.ps1 b/scripts/dev-reload.ps1 index 8f13ed4..b5c6f38 100644 --- a/scripts/dev-reload.ps1 +++ b/scripts/dev-reload.ps1 @@ -11,10 +11,12 @@ Set-StrictMode -Version Latest $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $projectPath = Join-Path $repoRoot "src\Aquarium.Engine\Aquarium.Engine.csproj" +$liveProjectPath = Join-Path $repoRoot "src\Aquarium.Engine.Live\Aquarium.Engine.Live.csproj" $devRoot = Join-Path $repoRoot "artifacts\dev-reload" $slotRoot = Join-Path $devRoot "slots" $statePath = Join-Path $devRoot "state.clixml" $buildStatePath = Join-Path $devRoot "last-build.clixml" +$liveReloadPointerPath = Join-Path $devRoot "live-current.txt" $cultCachePath = Join-Path $devRoot "cultcache\aquarium-client.msgpack" $shaderSourcePath = Join-Path $repoRoot "src\Aquarium.Engine\Render\Shaders\Aquarium.hlsl" $stdoutLogPath = Join-Path $devRoot "latest.out.log" @@ -138,6 +140,13 @@ if ($LASTEXITCODE -ne 0) { throw "dotnet build failed with exit code $LASTEXITCODE." } +dotnet build $liveProjectPath ` + -c Debug ` + -o $slotPath +if ($LASTEXITCODE -ne 0) { + throw "live runtime build failed with exit code $LASTEXITCODE." +} + if ($BuildOnly) { @{ slot = $slotPath @@ -153,11 +162,23 @@ if ($BuildOnly) { Stop-PreviousOwnedProcess $exePath = Join-Path $slotPath "Aquarium.Engine.exe" +$liveAssemblyPath = Join-Path $slotPath "Aquarium.Engine.Live.dll" if (-not (Test-Path $exePath)) { throw "Expected apphost was not produced: $exePath" } -$arguments = @("--cache", $cultCachePath, "--shader-source", $shaderSourcePath) +if (-not (Test-Path $liveAssemblyPath)) { + throw "Expected live runtime assembly was not produced: $liveAssemblyPath" +} + +Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8 + +$arguments = @( + "--cache", $cultCachePath, + "--shader-source", $shaderSourcePath, + "--live-assembly", $liveAssemblyPath, + "--live-reload-pointer", $liveReloadPointerPath +) if ($Headless) { $arguments += "--headless" } diff --git a/scripts/dev-watch.ps1 b/scripts/dev-watch.ps1 index 6f53e31..ddbfcb6 100644 --- a/scripts/dev-watch.ps1 +++ b/scripts/dev-watch.ps1 @@ -12,9 +12,13 @@ Set-StrictMode -Version Latest $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $reloadScript = Join-Path $PSScriptRoot "dev-reload.ps1" +$liveProjectPath = Join-Path $repoRoot "src\Aquarium.Engine.Live\Aquarium.Engine.Live.csproj" $watchLogPath = Join-Path $repoRoot "artifacts\dev-reload\watch.log" +$liveSlotRoot = Join-Path $repoRoot "artifacts\dev-reload\live-slots" +$liveReloadPointerPath = Join-Path $repoRoot "artifacts\dev-reload\live-current.txt" New-Item -ItemType Directory -Force -Path (Split-Path $watchLogPath) | Out-Null +New-Item -ItemType Directory -Force -Path $liveSlotRoot | Out-Null function Write-WatchLog([string]$message) { $line = "$(Get-Date -Format o) $message" @@ -22,7 +26,7 @@ function Write-WatchLog([string]$message) { Add-Content -Path $watchLogPath -Value $line } -function Get-WatchedFiles { +function Get-SourceFiles { $roots = @( (Join-Path $repoRoot "src"), (Join-Path $repoRoot "Aquarium.Engine.slnx"), @@ -49,8 +53,20 @@ function Get-WatchedFiles { } } -function Get-SourceFingerprint { - $fingerprintInput = Get-WatchedFiles | +function Get-LiveFiles { + Get-SourceFiles | Where-Object { + $_.FullName -like (Join-Path $repoRoot "src\Aquarium.Engine.Live\*") + } +} + +function Get-RestartFiles { + Get-SourceFiles | Where-Object { + $_.FullName -notlike (Join-Path $repoRoot "src\Aquarium.Engine.Live\*") + } +} + +function Get-Fingerprint($files) { + $fingerprintInput = $files | Sort-Object FullName | ForEach-Object { "$($_.FullName)|$($_.Length)|$($_.LastWriteTimeUtc.Ticks)" @@ -68,15 +84,39 @@ function Get-SourceFingerprint { } } -function Wait-StableFingerprint { - $first = Get-SourceFingerprint - Start-Sleep -Milliseconds $DebounceMilliseconds - $second = Get-SourceFingerprint +function Get-SourceFingerprint { + Get-Fingerprint (Get-SourceFiles) +} - while ($first -ne $second) { +function Get-LiveFingerprint { + Get-Fingerprint (Get-LiveFiles) +} + +function Get-RestartFingerprint { + Get-Fingerprint (Get-RestartFiles) +} + +function Wait-StableFingerprints { + $first = @{ + all = Get-SourceFingerprint + live = Get-LiveFingerprint + restart = Get-RestartFingerprint + } + Start-Sleep -Milliseconds $DebounceMilliseconds + $second = @{ + all = Get-SourceFingerprint + live = Get-LiveFingerprint + restart = Get-RestartFingerprint + } + + while ($first.all -ne $second.all) { $first = $second Start-Sleep -Milliseconds $DebounceMilliseconds - $second = Get-SourceFingerprint + $second = @{ + all = Get-SourceFingerprint + live = Get-LiveFingerprint + restart = Get-RestartFingerprint + } } return $second @@ -111,17 +151,39 @@ function Invoke-Reload { } } -$lastGoodFingerprint = Wait-StableFingerprint +function Invoke-LiveReload { + param([string]$reason) + + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $liveSlot = Join-Path $liveSlotRoot "$timestamp-$([guid]::NewGuid().ToString("N").Substring(0, 8))" + New-Item -ItemType Directory -Force -Path $liveSlot | Out-Null + + Write-WatchLog "Live reload requested: $reason" + dotnet build $liveProjectPath -c Debug -o $liveSlot + if ($LASTEXITCODE -ne 0) { + throw "live runtime build failed with exit code $LASTEXITCODE." + } + + $liveAssemblyPath = Join-Path $liveSlot "Aquarium.Engine.Live.dll" + if (-not (Test-Path $liveAssemblyPath)) { + throw "Expected live runtime assembly was not produced: $liveAssemblyPath" + } + + Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8 + Write-WatchLog "Live reload pointer updated: $liveAssemblyPath" +} + +$lastGoodFingerprint = Wait-StableFingerprints $lastFailedFingerprint = $null if (-not $NoInitialLaunch) { try { Invoke-Reload "initial launch" - $lastGoodFingerprint = Wait-StableFingerprint + $lastGoodFingerprint = Wait-StableFingerprints $lastFailedFingerprint = $null } catch { - $lastFailedFingerprint = $lastGoodFingerprint + $lastFailedFingerprint = $lastGoodFingerprint.all Write-WatchLog "Initial launch failed; keeping any previous good process alive. $($_.Exception.Message)" } } @@ -130,9 +192,9 @@ Write-WatchLog "Watching Aquarium sources. Press Ctrl+C to stop." do { Start-Sleep -Milliseconds $IntervalMilliseconds - $currentFingerprint = Wait-StableFingerprint + $currentFingerprint = Wait-StableFingerprints - if ($currentFingerprint -eq $lastGoodFingerprint -or $currentFingerprint -eq $lastFailedFingerprint) { + if ($currentFingerprint.all -eq $lastGoodFingerprint.all -or $currentFingerprint.all -eq $lastFailedFingerprint) { if ($Once) { break } @@ -141,12 +203,18 @@ do { } try { - Invoke-Reload "source change" + if ($currentFingerprint.restart -eq $lastGoodFingerprint.restart -and $currentFingerprint.live -ne $lastGoodFingerprint.live) { + Invoke-LiveReload "live source change" + } + else { + Invoke-Reload "host source change" + } + $lastGoodFingerprint = $currentFingerprint $lastFailedFingerprint = $null } catch { - $lastFailedFingerprint = $currentFingerprint + $lastFailedFingerprint = $currentFingerprint.all Write-WatchLog "Reload failed; previous good process stays alive. $($_.Exception.Message)" } diff --git a/src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj b/src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj new file mode 100644 index 0000000..9ed914b --- /dev/null +++ b/src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/src/Aquarium.Engine.Contracts/AquariumFrame.cs b/src/Aquarium.Engine.Contracts/AquariumFrame.cs new file mode 100644 index 0000000..159cbff --- /dev/null +++ b/src/Aquarium.Engine.Contracts/AquariumFrame.cs @@ -0,0 +1,6 @@ +using System.Numerics; + +namespace Aquarium.Engine; + +public readonly record struct AquariumFrame(GridFrame Grid, Vector3 CameraPosition, float TimeSeconds); + diff --git a/src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs b/src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs new file mode 100644 index 0000000..80fe2b0 --- /dev/null +++ b/src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs @@ -0,0 +1,22 @@ +using Aquarium.Engine.Input; + +namespace Aquarium.Engine; + +public interface IAquariumRuntime : IDisposable +{ + AquariumRuntimeOptions Options { get; } + + AquariumFrame Frame { get; } + + void Start(); + + void Update(float deltaSeconds, InputState input); +} + +public interface IAquariumRuntimeFactory +{ + IAquariumRuntime Create(AquariumRuntimeOptions options); +} + +public readonly record struct AquariumRuntimeOptions(bool Headless, string? CultCachePath); + diff --git a/src/Aquarium.Engine/Input/InputState.cs b/src/Aquarium.Engine.Contracts/Input/InputState.cs similarity index 100% rename from src/Aquarium.Engine/Input/InputState.cs rename to src/Aquarium.Engine.Contracts/Input/InputState.cs diff --git a/src/Aquarium.Engine/World/GridFrame.cs b/src/Aquarium.Engine.Contracts/World/GridFrame.cs similarity index 52% rename from src/Aquarium.Engine/World/GridFrame.cs rename to src/Aquarium.Engine.Contracts/World/GridFrame.cs index c2d6bcf..87da8f9 100644 --- a/src/Aquarium.Engine/World/GridFrame.cs +++ b/src/Aquarium.Engine.Contracts/World/GridFrame.cs @@ -4,8 +4,8 @@ namespace Aquarium.Engine; public readonly record struct GridFrame(Vector2 Center, float Radius) { - public static GridFrame FromCamera(OrbitCameraRig camera) + public static GridFrame FromCamera(Vector2 target, float distance) { - return new GridFrame(camera.Target, camera.Distance); + return new GridFrame(target, distance); } } diff --git a/src/Aquarium.Engine.Live/Aquarium.Engine.Live.csproj b/src/Aquarium.Engine.Live/Aquarium.Engine.Live.csproj new file mode 100644 index 0000000..068b095 --- /dev/null +++ b/src/Aquarium.Engine.Live/Aquarium.Engine.Live.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + diff --git a/src/Aquarium.Engine/AquariumRuntime.cs b/src/Aquarium.Engine.Live/AquariumRuntime.cs similarity index 85% rename from src/Aquarium.Engine/AquariumRuntime.cs rename to src/Aquarium.Engine.Live/AquariumRuntime.cs index 1ba55ee..046553d 100644 --- a/src/Aquarium.Engine/AquariumRuntime.cs +++ b/src/Aquarium.Engine.Live/AquariumRuntime.cs @@ -4,7 +4,7 @@ using Aquarium.Engine.State; namespace Aquarium.Engine; -public sealed class AquariumRuntime : IDisposable +public sealed class AquariumRuntime : IAquariumRuntime { private const float StateSaveIntervalSeconds = 0.20f; @@ -26,7 +26,7 @@ public sealed class AquariumRuntime : IDisposable pitchRadians: liveState.CameraPitchRadians, distance: liveState.CameraDistance); timeSeconds = liveState.TimeSeconds; - gridFrame = GridFrame.FromCamera(cameraRig); + gridFrame = GridFrame.FromCamera(cameraRig.Target, cameraRig.Distance); } public AquariumRuntimeOptions Options { get; } @@ -46,7 +46,7 @@ public sealed class AquariumRuntime : IDisposable { timeSeconds += Math.Max(deltaSeconds, 0.0f); cameraRig.ApplyInput(input, deltaSeconds); - gridFrame = GridFrame.FromCamera(cameraRig); + gridFrame = GridFrame.FromCamera(cameraRig.Target, cameraRig.Distance); secondsSinceStateSave += Math.Max(deltaSeconds, 0.0f); if (secondsSinceStateSave >= StateSaveIntervalSeconds) { @@ -75,6 +75,10 @@ public sealed class AquariumRuntime : IDisposable } } -public readonly record struct AquariumRuntimeOptions(bool Headless, string? CultCachePath); - -public readonly record struct AquariumFrame(GridFrame Grid, System.Numerics.Vector3 CameraPosition, float TimeSeconds); +public sealed class AquariumRuntimeFactory : IAquariumRuntimeFactory +{ + public IAquariumRuntime Create(AquariumRuntimeOptions options) + { + return new AquariumRuntime(options); + } +} diff --git a/src/Aquarium.Engine/State/AquariumCultStateStore.cs b/src/Aquarium.Engine.Live/State/AquariumCultStateStore.cs similarity index 100% rename from src/Aquarium.Engine/State/AquariumCultStateStore.cs rename to src/Aquarium.Engine.Live/State/AquariumCultStateStore.cs diff --git a/src/Aquarium.Engine/State/AquariumLiveState.cs b/src/Aquarium.Engine.Live/State/AquariumLiveState.cs similarity index 100% rename from src/Aquarium.Engine/State/AquariumLiveState.cs rename to src/Aquarium.Engine.Live/State/AquariumLiveState.cs diff --git a/src/Aquarium.Engine/World/Angle.cs b/src/Aquarium.Engine.Live/World/Angle.cs similarity index 100% rename from src/Aquarium.Engine/World/Angle.cs rename to src/Aquarium.Engine.Live/World/Angle.cs diff --git a/src/Aquarium.Engine/World/OrbitCameraRig.cs b/src/Aquarium.Engine.Live/World/OrbitCameraRig.cs similarity index 100% rename from src/Aquarium.Engine/World/OrbitCameraRig.cs rename to src/Aquarium.Engine.Live/World/OrbitCameraRig.cs diff --git a/src/Aquarium.Engine/Aquarium.Engine.csproj b/src/Aquarium.Engine/Aquarium.Engine.csproj index b91dfac..e9b5dda 100644 --- a/src/Aquarium.Engine/Aquarium.Engine.csproj +++ b/src/Aquarium.Engine/Aquarium.Engine.csproj @@ -9,6 +9,7 @@ + @@ -17,8 +18,6 @@ - - diff --git a/src/Aquarium.Engine/AquariumHost.cs b/src/Aquarium.Engine/AquariumHost.cs index 9045125..8eaa99d 100644 --- a/src/Aquarium.Engine/AquariumHost.cs +++ b/src/Aquarium.Engine/AquariumHost.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Aquarium.Engine.Input; +using Aquarium.Engine.Live; using Aquarium.Engine.Platform; using Aquarium.Engine.Render; @@ -9,7 +10,9 @@ public static class AquariumHost { public static int Run(string[] args) { - using var runtime = new AquariumRuntime(new AquariumRuntimeOptions(ParseHeadless(args), ParseCachePath(args))); + var runtimeOptions = new AquariumRuntimeOptions(ParseHeadless(args), ParseCachePath(args)); + using var runtimeLoader = new LiveRuntimeLoader(runtimeOptions, ParseLiveAssemblyPath(args), ParseLiveReloadPointerPath(args)); + var runtime = runtimeLoader.Load(); var input = new InputState(); var width = runtime.Options.Headless ? 640 : 1280; var height = runtime.Options.Headless ? 360 : 720; @@ -18,8 +21,6 @@ public static class AquariumHost window.PaintSplash(); using var renderer = new D3D11Renderer(window.Handle, window.ClientWidth, window.ClientHeight, ParseShaderPath(args)); - runtime.Start(); - var frameClock = Stopwatch.StartNew(); var lastFrame = frameClock.Elapsed; var frames = 0; @@ -36,8 +37,8 @@ public static class AquariumHost var deltaSeconds = (float)(now - lastFrame).TotalSeconds; lastFrame = now; - runtime.Update(deltaSeconds, input); - renderer.Render(runtime.Frame); + runtimeLoader.Update(deltaSeconds, input); + renderer.Render(runtimeLoader.Runtime.Frame); if (runtime.Options.Headless && ++frames >= 2) { @@ -80,4 +81,32 @@ public static class AquariumHost return Environment.GetEnvironmentVariable("AQUARIUM_SHADER_SOURCE"); } + + private static string? ParseLiveAssemblyPath(IReadOnlyCollection args) + { + var values = args.ToArray(); + for (var index = 0; index < values.Length - 1; index++) + { + if (string.Equals(values[index], "--live-assembly", StringComparison.OrdinalIgnoreCase)) + { + return values[index + 1]; + } + } + + return Environment.GetEnvironmentVariable("AQUARIUM_LIVE_ASSEMBLY"); + } + + private static string? ParseLiveReloadPointerPath(IReadOnlyCollection args) + { + var values = args.ToArray(); + for (var index = 0; index < values.Length - 1; index++) + { + if (string.Equals(values[index], "--live-reload-pointer", StringComparison.OrdinalIgnoreCase)) + { + return values[index + 1]; + } + } + + return Environment.GetEnvironmentVariable("AQUARIUM_LIVE_RELOAD_POINTER"); + } } diff --git a/src/Aquarium.Engine/Live/LiveRuntimeLoader.cs b/src/Aquarium.Engine/Live/LiveRuntimeLoader.cs new file mode 100644 index 0000000..88121b1 --- /dev/null +++ b/src/Aquarium.Engine/Live/LiveRuntimeLoader.cs @@ -0,0 +1,157 @@ +using System.Reflection; +using System.Runtime.Loader; +using System.Diagnostics; + +namespace Aquarium.Engine.Live; + +public sealed class LiveRuntimeLoader : IDisposable +{ + private const string LiveAssemblyName = "Aquarium.Engine.Live.dll"; + private const string FactoryTypeName = "Aquarium.Engine.AquariumRuntimeFactory"; + + private readonly string liveAssemblyPath; + private readonly string? liveReloadPointerPath; + private readonly Stopwatch reloadClock = Stopwatch.StartNew(); + private readonly AquariumRuntimeOptions options; + private ReloadedRuntime? current; + private TimeSpan lastReloadCheck; + private string? currentAssemblyPath; + + public LiveRuntimeLoader(AquariumRuntimeOptions options, string? liveAssemblyPath = null, string? liveReloadPointerPath = null) + { + this.liveAssemblyPath = liveAssemblyPath ?? Path.Combine(AppContext.BaseDirectory, LiveAssemblyName); + this.liveReloadPointerPath = liveReloadPointerPath; + this.options = options; + } + + public IAquariumRuntime Runtime + { + get + { + if (current == null) + { + Reload(liveAssemblyPath, options); + } + + return current!.Runtime; + } + } + + public IAquariumRuntime Load() + { + Reload(liveAssemblyPath, options); + return Runtime; + } + + public void Reload(string assemblyPath, AquariumRuntimeOptions options) + { + var previous = current; + var loadContext = new LiveAssemblyLoadContext(assemblyPath); + IAquariumRuntime? nextRuntime = null; + + try + { + var assembly = loadContext.LoadFromAssemblyPath(assemblyPath); + var factoryType = assembly.GetType(FactoryTypeName, throwOnError: true) + ?? throw new InvalidOperationException($"Live runtime factory was not found: {FactoryTypeName}"); + var factory = (IAquariumRuntimeFactory?)Activator.CreateInstance(factoryType) + ?? throw new InvalidOperationException($"Live runtime factory could not be created: {FactoryTypeName}"); + + nextRuntime = factory.Create(options); + nextRuntime.Start(); + } + catch + { + nextRuntime?.Dispose(); + loadContext.Unload(); + throw; + } + + current = new ReloadedRuntime(loadContext, nextRuntime); + currentAssemblyPath = assemblyPath; + previous?.Runtime.Dispose(); + previous?.Unload(); + } + + public void Update(float deltaSeconds, Input.InputState input) + { + TryReloadFromPointer(); + Runtime.Update(deltaSeconds, input); + } + + private void TryReloadFromPointer() + { + if (string.IsNullOrWhiteSpace(liveReloadPointerPath) || reloadClock.Elapsed - lastReloadCheck < TimeSpan.FromMilliseconds(500)) + { + return; + } + + lastReloadCheck = reloadClock.Elapsed; + string candidatePath; + try + { + candidatePath = File.ReadAllText(liveReloadPointerPath).Trim(); + } + catch + { + return; + } + + if (string.IsNullOrWhiteSpace(candidatePath) + || string.Equals(candidatePath, currentAssemblyPath, StringComparison.OrdinalIgnoreCase) + || !File.Exists(candidatePath)) + { + return; + } + + try + { + Reload(candidatePath, options); + Console.WriteLine($"Live runtime reload applied: {candidatePath}"); + } + catch (Exception error) + { + Console.Error.WriteLine($"Live runtime reload failed; keeping previous runtime. {error.Message}"); + } + } + + public void Dispose() + { + var runtime = current; + current = null; + runtime?.Runtime.Dispose(); + runtime?.Unload(); + } + + private sealed class ReloadedRuntime(LiveAssemblyLoadContext loadContext, IAquariumRuntime runtime) + { + public IAquariumRuntime Runtime { get; } = runtime; + + public void Unload() + { + loadContext.Unload(); + } + } + + private sealed class LiveAssemblyLoadContext(string assemblyPath) : AssemblyLoadContext(isCollectible: true) + { + private readonly AssemblyDependencyResolver resolver = new(assemblyPath); + + protected override Assembly? Load(AssemblyName assemblyName) + { + if (assemblyName.Name == typeof(IAquariumRuntime).Assembly.GetName().Name) + { + return null; + } + + var path = resolver.ResolveAssemblyToPath(assemblyName); + return path == null ? null : LoadFromAssemblyPath(path); + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var path = resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return path == null ? IntPtr.Zero : LoadUnmanagedDllFromPath(path); + } + } +}