// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Runtime.InteropServices; namespace SharpEmu.ShaderCompiler.Metal.Tests; /// /// 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. /// 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); private static readonly Lazy 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"; } /// Compiles MSL source with the OS runtime compiler. 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; } /// /// Runs one thread of a compute kernel with the guest data buffer and the /// SharpEmuUniforms constant buffer bound at the caller-supplied indices /// (per the Gen5MslTranslator contract the uniforms index equals the /// global-buffer count), then returns the data buffer contents. /// public static bool TryExecuteSingleThread( nint library, string entryPoint, byte[] bufferContents, byte[] uniformsContents, nuint dataIndex, nuint uniformsIndex, out byte[] result, out string error) => TryExecuteThreadgroup( library, entryPoint, bufferContents, uniformsContents, dataIndex, uniformsIndex, threadsPerThreadgroup: 1, out result, out error); /// Runs the kernel as a single threadgroup of /// threads, so wave64 fixtures can /// exercise both 32-wide simdgroups of one guest wave under a threadgroup /// barrier. public static bool TryExecuteThreadgroup( nint library, string entryPoint, byte[] bufferContents, byte[] uniformsContents, nuint dataIndex, nuint uniformsIndex, uint threadsPerThreadgroup, 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")); nint buffer; unsafe { fixed (byte* contents = bufferContents) { // options 0 = MTLResourceStorageModeShared: CPU-visible for readback. buffer = SendBuffer( Device.Value, Selector("newBufferWithBytes:length:options:"), (nint)contents, (nuint)bufferContents.Length, 0); } } nint uniforms; unsafe { fixed (byte* contents = uniformsContents) { uniforms = SendBuffer( Device.Value, Selector("newBufferWithBytes:length:options:"), (nint)contents, (nuint)uniformsContents.Length, 0); } } if (queue == 0 || buffer == 0 || uniforms == 0) { error = "failed to create command queue or buffer"; return false; } var commandBuffer = Send(queue, Selector("commandBuffer")); var encoder = Send(commandBuffer, Selector("computeCommandEncoder")); SendVoid(encoder, Selector("setComputePipelineState:"), pipeline); SendSetBuffer(encoder, Selector("setBuffer:offset:atIndex:"), buffer, 0, dataIndex); SendSetBuffer(encoder, Selector("setBuffer:offset:atIndex:"), uniforms, 0, uniformsIndex); 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(buffer, Selector("contents")); if (contentsPointer == 0) { error = "buffer contents unavailable after execution"; return false; } result = new byte[bufferContents.Length]; Marshal.Copy(contentsPointer, result, 0, result.Length); return true; } }