sharpemu/tests/SharpEmu.ShaderCompiler.Met.../MetalNative.cs

282 lines
10 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.ShaderCompiler.Metal.Tests;
/// <summary>
/// Minimal Metal.framework access via objc_msgSend — just enough to compile MSL source
/// with the OS runtime compiler and dispatch a single-thread compute kernel. Kept
/// dependency-free on purpose; object lifetimes lean on process teardown, which is fine
/// for a test host.
/// </summary>
internal static partial class MetalNative
{
private const string ObjCLibrary = "/usr/lib/libobjc.A.dylib";
private const string MetalFramework = "/System/Library/Frameworks/Metal.framework/Metal";
[StructLayout(LayoutKind.Sequential)]
private struct MtlSize
{
public nuint Width;
public nuint Height;
public nuint Depth;
}
[LibraryImport(MetalFramework)]
private static partial nint MTLCreateSystemDefaultDevice();
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
private static partial nint objc_getClass(string name);
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
private static partial nint sel_registerName(string name);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial nint Send(nint receiver, nint selector);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial nint Send(nint receiver, nint selector, nint argument);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial nint Send(nint receiver, nint selector, nint argument0, nint argument1, ref nint error);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial nint Send(nint receiver, nint selector, nint argument, ref nint error);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial nint SendBuffer(nint receiver, nint selector, nint bytes, nuint length, nuint options);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial void SendVoid(nint receiver, nint selector);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial void SendVoid(nint receiver, nint selector, nint argument);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial void SendVoidBool(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool argument);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial void SendSetBuffer(nint receiver, nint selector, nint buffer, nuint offset, nuint index);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial void SendDispatch(nint receiver, nint selector, MtlSize threadgroups, MtlSize threadsPerThreadgroup);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial void SendUseResource(nint receiver, nint selector, nint resource, nuint usage);
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
private static partial ulong SendULong(nint receiver, nint selector);
// MTLResourceUsageRead | MTLResourceUsageWrite for buffers reached through an argument buffer.
private const nuint ReadWriteUsage = 3;
private static readonly Lazy<nint> Device = new(() =>
OperatingSystem.IsMacOS() ? MTLCreateSystemDefaultDevice() : 0);
public static bool IsAvailable => Device.Value != 0;
private static nint Selector(string name) => sel_registerName(name);
private static nint NsString(string value)
{
var utf8 = Marshal.StringToCoTaskMemUTF8(value);
try
{
return Send(objc_getClass("NSString"), Selector("stringWithUTF8String:"), utf8);
}
finally
{
Marshal.FreeCoTaskMem(utf8);
}
}
private static string DescribeError(nint error)
{
if (error == 0)
{
return "unknown error";
}
var description = Send(error, Selector("localizedDescription"));
var utf8 = Send(description, Selector("UTF8String"));
return Marshal.PtrToStringUTF8(utf8) ?? "unknown error";
}
/// <summary>Compiles MSL source with the OS runtime compiler.</summary>
public static bool TryCompileLibrary(string source, out nint library, out string error)
{
library = 0;
error = string.Empty;
// Metal defaults to fast-math; GCN float semantics do not survive it, so the
// harness compiles the way a real Metal backend must: fast-math off.
var options = Send(Send(objc_getClass("MTLCompileOptions"), Selector("alloc")), Selector("init"));
SendVoidBool(options, Selector("setFastMathEnabled:"), false);
nint nsError = 0;
library = Send(
Device.Value,
Selector("newLibraryWithSource:options:error:"),
NsString(source),
options,
ref nsError);
if (library == 0)
{
error = DescribeError(nsError);
return false;
}
return true;
}
/// <summary>Runs a kernel compiled through a compile request: the argument buffer
/// is filled directly from the layout with the data buffer's address and length
/// (every buffer field points at the one data buffer), push data at slot 1.</summary>
public static bool TryExecuteWithArgumentBuffer(
nint library,
string entryPoint,
byte[] bufferContents,
byte[] pushData,
Gen5MslArgumentLayout layout,
uint threadsPerThreadgroup,
out byte[] result,
out string error)
{
result = [];
var buffer = SharedBuffer(bufferContents);
if (buffer == 0)
{
error = "failed to create the data buffer";
return false;
}
var address = SendULong(buffer, Selector("gpuAddress"));
var arguments = new byte[Math.Max(layout.ByteSize, 8)];
foreach (var field in layout.Fields)
{
for (var element = 0; element < field.Count; element++)
{
switch (field.FieldKind)
{
case MslArgumentFieldKind.BufferPointers:
BitConverter.TryWriteBytes(arguments.AsSpan((int)field.ByteOffset + (element * 8)), address);
break;
case MslArgumentFieldKind.BufferByteCounts:
BitConverter.TryWriteBytes(arguments.AsSpan((int)field.ByteOffset + (element * 4)), (uint)bufferContents.Length);
break;
}
}
}
var argumentBuffer = SharedBuffer(arguments);
var pushBuffer = SharedBuffer(pushData.Length == 0 ? new byte[4] : pushData);
if (argumentBuffer == 0 || pushBuffer == 0)
{
error = "failed to create the argument or push buffer";
return false;
}
return RunKernel(
library,
entryPoint,
[(argumentBuffer, (nuint)Gen5MslArgumentLayout.ResourcesBufferIndex), (pushBuffer, (nuint)Gen5MslArgumentLayout.PushDataBufferIndex)],
[buffer],
threadsPerThreadgroup,
buffer,
bufferContents.Length,
out result,
out error);
}
// options 0 = MTLResourceStorageModeShared: CPU-visible for readback.
private static nint SharedBuffer(byte[] contents)
{
unsafe
{
fixed (byte* bytes = contents)
{
return SendBuffer(
Device.Value,
Selector("newBufferWithBytes:length:options:"),
(nint)bytes,
(nuint)contents.Length,
0);
}
}
}
private static bool RunKernel(
nint library,
string entryPoint,
IReadOnlyList<(nint Buffer, nuint Index)> bindings,
IReadOnlyList<nint> residentBuffers,
uint threadsPerThreadgroup,
nint resultBuffer,
int resultLength,
out byte[] result,
out string error)
{
result = [];
error = string.Empty;
var function = Send(library, Selector("newFunctionWithName:"), NsString(entryPoint));
if (function == 0)
{
error = $"entry point '{entryPoint}' not found in the compiled library";
return false;
}
nint nsError = 0;
var pipeline = Send(
Device.Value,
Selector("newComputePipelineStateWithFunction:error:"),
function,
ref nsError);
if (pipeline == 0)
{
error = $"pipeline creation failed: {DescribeError(nsError)}";
return false;
}
var queue = Send(Device.Value, Selector("newCommandQueue"));
if (queue == 0)
{
error = "failed to create the command queue";
return false;
}
var commandBuffer = Send(queue, Selector("commandBuffer"));
var encoder = Send(commandBuffer, Selector("computeCommandEncoder"));
SendVoid(encoder, Selector("setComputePipelineState:"), pipeline);
foreach (var (buffer, index) in bindings)
{
SendSetBuffer(encoder, Selector("setBuffer:offset:atIndex:"), buffer, 0, index);
}
foreach (var resident in residentBuffers)
{
SendUseResource(encoder, Selector("useResource:usage:"), resident, ReadWriteUsage);
}
var oneGroup = new MtlSize { Width = 1, Height = 1, Depth = 1 };
var threads = new MtlSize { Width = threadsPerThreadgroup, Height = 1, Depth = 1 };
SendDispatch(encoder, Selector("dispatchThreadgroups:threadsPerThreadgroup:"), oneGroup, threads);
SendVoid(encoder, Selector("endEncoding"));
SendVoid(commandBuffer, Selector("commit"));
SendVoid(commandBuffer, Selector("waitUntilCompleted"));
var contentsPointer = Send(resultBuffer, Selector("contents"));
if (contentsPointer == 0)
{
error = "buffer contents unavailable after execution";
return false;
}
result = new byte[resultLength];
Marshal.Copy(contentsPointer, result, 0, result.Length);
return true;
}
}