mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-01 15:39:47 +08:00
Sdl backend (#670)
* [audio] added sdl audio backend and in-tree atrac9 decoder * [input] replaced per-platform pad readers with sdl gamepad input * [video] added sdl window and host display plumbing * [gui] added host display options and per-game render settings * [bink] synced host movie playback to the guest audio clock * [cpu] hooked windows write faults into guest image tracking * [perf] added guest and render profiling, reserved host cpu lanes * [kernel] fixed stale pthread mutex handle alias * [host] wired the sdl session, save-data paths and project references * [audio] hoisted ajm trace stackalloc out of its loop * [video] Add guest image sync setting * [build] Strip native symbols * reuse
This commit is contained in:
@@ -237,7 +237,21 @@ internal interface IGuestGpuBackend
|
||||
|
||||
void SubmitGuestImageFill(ulong address, uint fillValue);
|
||||
|
||||
void SubmitGuestImageWrite(ulong address, byte[] pixels);
|
||||
/// <summary>
|
||||
/// Uploads guest-authored pixels into a live guest image. <paramref name="rowOffset"/>
|
||||
/// is the first image row the buffer covers, so a caller that knows only part
|
||||
/// of the surface changed can send that band instead of the whole thing; the
|
||||
/// untouched rows on the host already hold the same bytes.
|
||||
/// </summary>
|
||||
void SubmitGuestImageWrite(ulong address, byte[] pixels, uint rowOffset = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a non-zero <c>rowOffset</c> is honoured. Backends that cannot
|
||||
/// upload a sub-range must report false so callers keep sending the whole
|
||||
/// surface: a dropped band would leave the host copy stale, which is worse
|
||||
/// than an oversized upload.
|
||||
/// </summary>
|
||||
bool SupportsPartialImageWrite => false;
|
||||
|
||||
/// <summary>
|
||||
/// Asks the presenter to refresh CPU-dirty guest images on its render/present
|
||||
|
||||
@@ -398,7 +398,7 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
|
||||
public void SubmitGuestImageFill(ulong address, uint fillValue) =>
|
||||
MetalVideoPresenter.SubmitGuestImageFill(address, fillValue);
|
||||
|
||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels) =>
|
||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels, uint rowOffset = 0) =>
|
||||
MetalVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
||||
|
||||
public void RequestCpuWrittenGuestImageSync(ulong scopeAddress = 0, ulong scopeByteCount = ulong.MaxValue) =>
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Metal;
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard state sampled from the Metal presenter's window, feeding the POSIX
|
||||
/// host input seam so pad emulation works like the Vulkan presenter's
|
||||
/// HostWindowInput. Key events arrive on the AppKit main thread as macOS
|
||||
/// virtual key codes; pad reads happen on guest threads, so state is guarded.
|
||||
/// Window gamepads are not surfaced by AppKit — controller support would go
|
||||
/// through GameController.framework and is out of scope here.
|
||||
/// </summary>
|
||||
internal static class MetalHostInput
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static readonly HashSet<ushort> Pressed = new();
|
||||
private static volatile bool _connected;
|
||||
|
||||
/// <summary>Registers this window's keyboard as the host input source.</summary>
|
||||
public static void Attach()
|
||||
{
|
||||
_connected = true;
|
||||
PosixHostInput.SetSource(new MetalWindowInputSource());
|
||||
Console.Error.WriteLine("[LOADER][INFO] Window keyboard input attached for pad emulation.");
|
||||
}
|
||||
|
||||
// Debug automation: SHARPEMU_METAL_AUTOKEY="12:0x24,15:0x24" presses the
|
||||
// macOS key code at each elapsed-seconds mark for a few frames, letting
|
||||
// headless test runs navigate menus without a human at the keyboard.
|
||||
private static readonly List<(double At, ushort Key, bool[] State)> _autoKeys = ParseAutoKeys();
|
||||
private static readonly System.Diagnostics.Stopwatch _autoKeyClock =
|
||||
System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
private static List<(double, ushort, bool[])> ParseAutoKeys()
|
||||
{
|
||||
var keys = new List<(double, ushort, bool[])>();
|
||||
var spec = Environment.GetEnvironmentVariable("SHARPEMU_METAL_AUTOKEY");
|
||||
if (string.IsNullOrWhiteSpace(spec))
|
||||
{
|
||||
return keys;
|
||||
}
|
||||
|
||||
foreach (var entry in spec.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var parts = entry.Split(':');
|
||||
if (parts.Length == 2 &&
|
||||
double.TryParse(parts[0], out var at) &&
|
||||
TryParseKeyCode(parts[1], out var key))
|
||||
{
|
||||
keys.Add((at, key, new bool[2]));
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static bool TryParseKeyCode(string text, out ushort key)
|
||||
{
|
||||
return text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? ushort.TryParse(text[2..], System.Globalization.NumberStyles.HexNumber, null, out key)
|
||||
: ushort.TryParse(text, out key);
|
||||
}
|
||||
|
||||
/// <summary>Called once per render frame; fires and releases scripted keys.</summary>
|
||||
public static void PumpAutoKeys()
|
||||
{
|
||||
if (_autoKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var elapsed = _autoKeyClock.Elapsed.TotalSeconds;
|
||||
foreach (var (at, key, state) in _autoKeys)
|
||||
{
|
||||
if (!state[0] && elapsed >= at)
|
||||
{
|
||||
state[0] = true;
|
||||
KeyDown(key, isRepeat: false);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Metal autokey press 0x{key:X} at {elapsed:F1}s");
|
||||
}
|
||||
else if (state[0] && !state[1] && elapsed >= at + 0.2)
|
||||
{
|
||||
state[1] = true;
|
||||
KeyUp(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void KeyDown(ushort keyCode, bool isRepeat)
|
||||
{
|
||||
// kVK_F1: parity with the Vulkan window's perf-overlay toggle.
|
||||
if (keyCode == 0x7A && !isRepeat)
|
||||
{
|
||||
VideoOut.PerfOverlay.Toggle();
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
Pressed.Add(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void KeyUp(ushort keyCode)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
Pressed.Remove(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsKeyCodeDown(ushort keyCode)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Pressed.Contains(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MetalWindowInputSource : IPosixWindowInputSource
|
||||
{
|
||||
public bool HasKeyboardFocus => _connected;
|
||||
|
||||
public bool IsKeyDown(int virtualKey) =>
|
||||
TryMapVirtualKey(virtualKey, out var keyCode) && IsKeyCodeDown(keyCode);
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination) => 0;
|
||||
|
||||
public string? DescribeConnectedGamepad() => null;
|
||||
}
|
||||
|
||||
/// <summary>Windows virtual-key semantics (the seam's contract) to macOS
|
||||
/// kVK virtual key codes, covering the keys pad emulation polls.</summary>
|
||||
private static bool TryMapVirtualKey(int vk, out ushort keyCode)
|
||||
{
|
||||
keyCode = vk switch
|
||||
{
|
||||
0x08 => 0x33, // Backspace -> kVK_Delete
|
||||
0x09 => 0x30, // Tab
|
||||
0x0D => 0x24, // Enter -> kVK_Return
|
||||
0x1B => 0x35, // Escape
|
||||
0x20 => 0x31, // Space
|
||||
0x25 => 0x7B, // Left
|
||||
0x26 => 0x7E, // Up
|
||||
0x27 => 0x7C, // Right
|
||||
0x28 => 0x7D, // Down
|
||||
// Letters: macOS ANSI key codes are layout-position based and
|
||||
// non-contiguous, so map each polled letter explicitly.
|
||||
0x41 => 0x00, // A
|
||||
0x42 => 0x0B, // B
|
||||
0x43 => 0x08, // C
|
||||
0x44 => 0x02, // D
|
||||
0x45 => 0x0E, // E
|
||||
0x46 => 0x03, // F
|
||||
0x47 => 0x05, // G
|
||||
0x48 => 0x04, // H
|
||||
0x49 => 0x22, // I
|
||||
0x4A => 0x26, // J
|
||||
0x4B => 0x28, // K
|
||||
0x4C => 0x25, // L
|
||||
0x4D => 0x2E, // M
|
||||
0x4E => 0x2D, // N
|
||||
0x4F => 0x1F, // O
|
||||
0x50 => 0x23, // P
|
||||
0x51 => 0x0C, // Q
|
||||
0x52 => 0x0F, // R
|
||||
0x53 => 0x01, // S
|
||||
0x54 => 0x11, // T
|
||||
0x55 => 0x20, // U
|
||||
0x56 => 0x09, // V
|
||||
0x57 => 0x0D, // W
|
||||
0x58 => 0x07, // X
|
||||
0x59 => 0x10, // Y
|
||||
0x5A => 0x06, // Z
|
||||
_ => ushort.MaxValue,
|
||||
};
|
||||
return keyCode != ushort.MaxValue;
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,6 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Metal;
|
||||
|
||||
// Core Graphics / Metal ABI structs passed by value through objc_msgSend. Struct
|
||||
// *returns* are deliberately never used: on x86-64 (this process runs under Rosetta
|
||||
// on Apple silicon) large struct returns switch to objc_msgSend_stret, and avoiding
|
||||
// them entirely keeps one calling convention everywhere.
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CGRect
|
||||
{
|
||||
public double X;
|
||||
public double Y;
|
||||
public double Width;
|
||||
public double Height;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CGSize
|
||||
{
|
||||
@@ -93,31 +80,21 @@ internal struct MtlViewport
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Objective-C runtime access for the Metal presenter: AppKit, QuartzCore, and Metal
|
||||
/// Objective-C runtime access for the Metal presenter: QuartzCore and Metal
|
||||
/// through objc_msgSend, with one LibraryImport overload per distinct native
|
||||
/// signature. Dependency-free by design — this plus the OS frameworks is the entire
|
||||
/// Metal path, which is what keeps it NativeAOT-clean.
|
||||
/// </summary>
|
||||
internal static partial class MetalNative
|
||||
{
|
||||
private const string CoreFoundation =
|
||||
"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
|
||||
|
||||
[LibraryImport(CoreFoundation)]
|
||||
public static partial nint CFRunLoopGetMain();
|
||||
|
||||
[LibraryImport(CoreFoundation)]
|
||||
public static partial void CFRunLoopStop(nint runLoop);
|
||||
|
||||
private const string ObjCLibrary = "/usr/lib/libobjc.A.dylib";
|
||||
private const string MetalFramework = "/System/Library/Frameworks/Metal.framework/Metal";
|
||||
private const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit";
|
||||
private const string QuartzCoreFramework = "/System/Library/Frameworks/QuartzCore.framework/QuartzCore";
|
||||
|
||||
private static bool _frameworksLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the AppKit and QuartzCore classes visible to objc_getClass; Metal is
|
||||
/// Makes QuartzCore classes visible to objc_getClass; Metal is
|
||||
/// pulled in by its own LibraryImport. Call once before any Class() lookup.
|
||||
/// </summary>
|
||||
public static void EnsureFrameworksLoaded()
|
||||
@@ -127,7 +104,6 @@ internal static partial class MetalNative
|
||||
return;
|
||||
}
|
||||
|
||||
NativeLibrary.Load(AppKitFramework);
|
||||
NativeLibrary.Load(QuartzCoreFramework);
|
||||
_frameworksLoaded = true;
|
||||
}
|
||||
@@ -141,16 +117,6 @@ internal static partial class MetalNative
|
||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
||||
private static partial nint sel_registerName(string name);
|
||||
|
||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
||||
public static partial nint objc_allocateClassPair(nint superclass, string name, nuint extraBytes);
|
||||
|
||||
[LibraryImport(ObjCLibrary)]
|
||||
public static partial void objc_registerClassPair(nint cls);
|
||||
|
||||
[LibraryImport(ObjCLibrary, StringMarshalling = StringMarshalling.Utf8)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
public static partial bool class_addMethod(nint cls, nint name, nint imp, string types);
|
||||
|
||||
[LibraryImport(ObjCLibrary)]
|
||||
public static partial nint objc_autoreleasePoolPush();
|
||||
|
||||
@@ -169,14 +135,6 @@ internal static partial class MetalNative
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint Send(nint receiver, nint selector, nint argument);
|
||||
|
||||
|
||||
/// <summary>objc_msgSend for a CGRect-returning selector (e.g. -bounds).
|
||||
/// A 32-byte struct is returned via the x86-64 stret ABI — a hidden
|
||||
/// pointer to caller storage passed ahead of self/_cmd — so this must not
|
||||
/// be folded into the plain objc_msgSend overloads.</summary>
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend_stret")]
|
||||
public static partial void SendStretRect(out CGRect result, nint receiver, nint selector);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint Send(nint receiver, nint selector, nint argument, ref nint error);
|
||||
|
||||
@@ -209,17 +167,6 @@ internal static partial class MetalNative
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial void SendVoid(nint receiver, nint selector, nint argument0, nint argument1);
|
||||
|
||||
/// <summary>performSelectorOnMainThread:withObject:waitUntilDone: — the SEL
|
||||
/// to perform is itself an argument, followed by the object and the wait
|
||||
/// flag.</summary>
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial void SendVoidPerformSelector(
|
||||
nint receiver,
|
||||
nint selector,
|
||||
nint performedSelector,
|
||||
nint argument,
|
||||
[MarshalAs(UnmanagedType.I1)] bool waitUntilDone);
|
||||
|
||||
/// <summary>setSwizzle: on MTLTextureDescriptor. Four one-byte
|
||||
/// MTLTextureSwizzle values, passed packed like the framework expects.</summary>
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
@@ -237,9 +184,6 @@ internal static partial class MetalNative
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial void SendVoidSize(nint receiver, nint selector, CGSize size);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial void SendVoidRect(nint receiver, nint selector, CGRect rect);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial void SendVoidClearColor(nint receiver, nint selector, MtlClearColor color);
|
||||
|
||||
@@ -328,37 +272,6 @@ internal static partial class MetalNative
|
||||
nuint indexBufferOffset,
|
||||
nuint instanceCount);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint SendTimer(
|
||||
nint receiver,
|
||||
nint selector,
|
||||
double interval,
|
||||
nint target,
|
||||
nint timerSelector,
|
||||
nint userInfo,
|
||||
[MarshalAs(UnmanagedType.I1)] bool repeats);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint SendInitFrame(nint receiver, nint selector, CGRect frame);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint SendInitWindow(
|
||||
nint receiver,
|
||||
nint selector,
|
||||
CGRect contentRect,
|
||||
nuint styleMask,
|
||||
nuint backing,
|
||||
[MarshalAs(UnmanagedType.I1)] bool defer);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint SendNextEvent(
|
||||
nint receiver,
|
||||
nint selector,
|
||||
ulong eventMask,
|
||||
nint untilDate,
|
||||
nint inMode,
|
||||
[MarshalAs(UnmanagedType.I1)] bool dequeue);
|
||||
|
||||
[LibraryImport(ObjCLibrary, EntryPoint = "objc_msgSend")]
|
||||
public static partial nint SendTextureDescriptor(
|
||||
nint receiver,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
@@ -11,28 +9,12 @@ using SharpEmu.ShaderCompiler.Metal;
|
||||
namespace SharpEmu.Libs.Gpu.Metal;
|
||||
|
||||
/// <summary>
|
||||
/// The Metal presenter: an AppKit window hosting a CAMetalLayer. A CADisplayLink
|
||||
/// requested from the content view drives <see cref="RenderFrame"/> in sync with the
|
||||
/// display refresh, on the main run loop — the loop whose Core Animation observer
|
||||
/// commits presented drawables to the window server, so it must be a real running
|
||||
/// run loop (a hand-pumped event drain never fires that observer and the window
|
||||
/// stays black). Everything AppKit runs on the process main thread via
|
||||
/// <see cref="HostMainThread"/> (AppKit traps off-main), which the CLI parks for us.
|
||||
/// The Metal presenter uses SDL for its host window, events and input, then renders
|
||||
/// into the CAMetalLayer owned by SDL's Metal view. Windowing and input therefore
|
||||
/// share the Vulkan path while all Metal command encoding remains native.
|
||||
/// </summary>
|
||||
internal static partial class MetalVideoPresenter
|
||||
{
|
||||
private const uint DefaultWindowWidth = 1280;
|
||||
private const uint DefaultWindowHeight = 720;
|
||||
|
||||
// NSWindow style: Titled | Closable | Miniaturizable | Resizable. Resizable
|
||||
// both lets the user drag the window edges and turns the green zoom button
|
||||
// into the full-screen toggle (paired with the collection behavior below).
|
||||
private const nuint WindowStyleMask = 1 | 2 | 4 | 8;
|
||||
|
||||
// NSWindowCollectionBehaviorFullScreenPrimary: opt this window into native
|
||||
// full-screen, so the green button enters full-screen rather than zooming.
|
||||
private const nuint FullScreenPrimaryBehavior = 1 << 7;
|
||||
private const nuint BackingStoreBuffered = 2;
|
||||
private const nuint PixelFormatBgra8Unorm = (nuint)MtlPixelFormat.Bgra8Unorm;
|
||||
private const nuint LoadActionLoad = 1;
|
||||
private const nuint LoadActionClear = 2;
|
||||
@@ -69,17 +51,14 @@ internal static partial class MetalVideoPresenter
|
||||
private static readonly byte[] _overlayPixels =
|
||||
new byte[PerfOverlay.PanelWidth * PerfOverlay.PanelHeight * 4];
|
||||
|
||||
// Presenter objects and per-frame present state, shared between window setup
|
||||
// and the display-link RenderFrame callback (both on the main thread).
|
||||
// Presenter objects and per-frame present state, all used on the SDL host thread.
|
||||
private static nint _device;
|
||||
private static nint _commandQueue;
|
||||
private static nint _metalLayer;
|
||||
private static nint _presentPipeline;
|
||||
private static nint _presentSampler;
|
||||
private static nint _window;
|
||||
private static nint _application;
|
||||
private static nint _renderTimer;
|
||||
private static nint _renderTimerTarget;
|
||||
private static SdlHostWindow? _hostWindow;
|
||||
private static HostVideoOptions _videoOptions = HostVideoOptions.Default;
|
||||
private static double _drawableWidth;
|
||||
private static double _drawableHeight;
|
||||
private static nint _frameTexture;
|
||||
@@ -91,10 +70,23 @@ internal static partial class MetalVideoPresenter
|
||||
private static nint _ownedVersionTexture;
|
||||
private static ulong _presentGuestAddress;
|
||||
private static long _presentedSequence = -1;
|
||||
private static bool _userClosed;
|
||||
private static uint _windowWidth;
|
||||
private static uint _windowHeight;
|
||||
|
||||
public static bool TryConfigureVideo(HostVideoOptions options)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_thread is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_videoOptions = options.Normalize();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void EnsureStarted(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
@@ -183,6 +175,7 @@ internal static partial class MetalVideoPresenter
|
||||
public static void RequestClose()
|
||||
{
|
||||
Volatile.Write(ref _closeRequested, true);
|
||||
Volatile.Read(ref _hostWindow)?.Close();
|
||||
}
|
||||
|
||||
private static void StartPresenterLocked()
|
||||
@@ -247,33 +240,23 @@ internal static partial class MetalVideoPresenter
|
||||
VideoOutExports.SetSelectedGpuName(deviceName);
|
||||
}
|
||||
|
||||
// Fixed window like the Vulkan presenter: guest frames letterbox into
|
||||
// it. Sizing the window from the guest's display mode (4K) exceeds the
|
||||
// screen — macOS clamps the window while the layer keeps the requested
|
||||
// geometry, leaving the visible region showing nothing but clear.
|
||||
const uint width = DefaultWindowWidth;
|
||||
const uint height = DefaultWindowHeight;
|
||||
HostVideoOptions videoOptions;
|
||||
lock (_gate)
|
||||
{
|
||||
videoOptions = _videoOptions;
|
||||
}
|
||||
|
||||
using var hostWindow = new SdlHostWindow(
|
||||
VideoOutExports.GetWindowTitle(),
|
||||
videoOptions,
|
||||
SdlGraphicsApi.Metal,
|
||||
ToggleMetalPerformanceHud);
|
||||
Volatile.Write(ref _hostWindow, hostWindow);
|
||||
var setupPool = MetalNative.objc_autoreleasePoolPush();
|
||||
try
|
||||
{
|
||||
_application = MetalNative.Send(
|
||||
MetalNative.Class("NSApplication"), MetalNative.Selector("sharedApplication"));
|
||||
// NSApplicationActivationPolicyRegular: dock icon + key window like any app.
|
||||
MetalNative.Send(_application, MetalNative.Selector("setActivationPolicy:"), 0);
|
||||
MetalNative.SendVoid(_application, MetalNative.Selector("finishLaunching"));
|
||||
|
||||
_window = CreateWindow(width, height);
|
||||
|
||||
// Swap in the key-capturing view before the metal layer attaches so
|
||||
// the layer lands on the input-aware content view.
|
||||
var keyView = MetalNative.SendInitFrame(
|
||||
MetalNative.Send(CreateKeyViewClass(), MetalNative.Selector("alloc")),
|
||||
MetalNative.Selector("initWithFrame:"),
|
||||
new CGRect { X = 0, Y = 0, Width = width, Height = height });
|
||||
MetalNative.SendVoid(_window, MetalNative.Selector("setContentView:"), keyView);
|
||||
|
||||
_metalLayer = CreateLayer(_device, _window, out _drawableWidth, out _drawableHeight);
|
||||
_metalLayer = hostWindow.CreateMetalLayer();
|
||||
ConfigureMetalLayer(_device, _metalLayer);
|
||||
_commandQueue = MetalNative.Send(_device, MetalNative.Selector("newCommandQueue"));
|
||||
if (!TryCreatePresentPipeline(_device, out _presentPipeline, out var pipelineError))
|
||||
{
|
||||
@@ -282,31 +265,7 @@ internal static partial class MetalVideoPresenter
|
||||
}
|
||||
|
||||
_presentSampler = CreateLinearSampler(_device);
|
||||
|
||||
MetalNative.SendVoid(_window, MetalNative.Selector("makeKeyAndOrderFront:"), 0);
|
||||
MetalNative.SendVoidBool(
|
||||
_application, MetalNative.Selector("activateIgnoringOtherApps:"), true);
|
||||
MetalNative.SendVoid(_window, MetalNative.Selector("makeFirstResponder:"), keyView);
|
||||
MetalHostInput.Attach();
|
||||
|
||||
// A repeating NSTimer on this (main) run loop fires onFrame: at the
|
||||
// display rate. CADisplayLink (NSView.displayLinkWithTarget:selector:)
|
||||
// is the natural choice but its callback never fires under the x86-64
|
||||
// Rosetta process this emulator runs as — proven in isolation against
|
||||
// a bare AppKit harness, where a timer fires and composites and the
|
||||
// display link does not. nextDrawable still throttles presentation to
|
||||
// the display, so the timer only needs to keep up, not pace precisely.
|
||||
_renderTimerTarget = CreateRenderTimerTarget();
|
||||
_renderTimer = MetalNative.Send(
|
||||
MetalNative.SendTimer(
|
||||
MetalNative.Class("NSTimer"),
|
||||
MetalNative.Selector("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"),
|
||||
1.0 / 60.0,
|
||||
_renderTimerTarget,
|
||||
MetalNative.Selector("onFrame:"),
|
||||
0,
|
||||
repeats: true),
|
||||
MetalNative.Selector("retain"));
|
||||
SyncDrawableSizeToLayer();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -314,25 +273,20 @@ internal static partial class MetalVideoPresenter
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("[LOADER][INFO] Metal VideoOut presenter started.");
|
||||
|
||||
// [NSApp run] runs the main run loop (its Core Animation observer commits
|
||||
// presented drawables to the window server) AND fully activates the app,
|
||||
// which a bare CFRunLoopRun does not — the CADisplayLink is only serviced
|
||||
// once the app is running, and NSApp dispatches window events itself.
|
||||
// Returns once RenderFrame stops it.
|
||||
MetalNative.SendVoid(_application, MetalNative.Selector("run"));
|
||||
|
||||
var closePool = MetalNative.objc_autoreleasePoolPush();
|
||||
try
|
||||
{
|
||||
MetalNative.SendVoid(_window, MetalNative.Selector("close"));
|
||||
hostWindow.Run(
|
||||
static () => { },
|
||||
static _ => RenderFrameSafely(),
|
||||
static () => { });
|
||||
}
|
||||
finally
|
||||
{
|
||||
MetalNative.objc_autoreleasePoolPop(closePool);
|
||||
Volatile.Write(ref _hostWindow, null);
|
||||
_metalLayer = 0;
|
||||
}
|
||||
|
||||
if (_userClosed)
|
||||
if (hostWindow.ClosedByUser)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Metal VideoOut window closed; requesting emulator shutdown.");
|
||||
@@ -340,115 +294,8 @@ internal static partial class MetalVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An NSView subclass that records key events for pad emulation. Overriding
|
||||
/// keyDown:/keyUp: (instead of an event monitor) needs no ObjC blocks, and
|
||||
/// swallowing the events also silences the system alert beep AppKit plays
|
||||
/// for unhandled keys. Registered once per process.
|
||||
/// </summary>
|
||||
private static unsafe nint CreateKeyViewClass()
|
||||
{
|
||||
var cls = MetalNative.objc_allocateClassPair(
|
||||
MetalNative.Class("NSView"), "SharpEmuMetalView", 0);
|
||||
if (cls == 0)
|
||||
{
|
||||
return MetalNative.Class("SharpEmuMetalView");
|
||||
}
|
||||
|
||||
var keyDown = (nint)(delegate* unmanaged[Cdecl]<nint, nint, nint, void>)&OnKeyDown;
|
||||
MetalNative.class_addMethod(cls, MetalNative.Selector("keyDown:"), keyDown, "v@:@");
|
||||
var keyUp = (nint)(delegate* unmanaged[Cdecl]<nint, nint, nint, void>)&OnKeyUp;
|
||||
MetalNative.class_addMethod(cls, MetalNative.Selector("keyUp:"), keyUp, "v@:@");
|
||||
// Command-modified keys never reach keyDown: — AppKit routes them through
|
||||
// performKeyEquivalent:, so Cmd+F1 (Metal Performance HUD) hooks in here.
|
||||
var keyEquivalent = (nint)(delegate* unmanaged[Cdecl]<nint, nint, nint, byte>)&OnPerformKeyEquivalent;
|
||||
MetalNative.class_addMethod(
|
||||
cls, MetalNative.Selector("performKeyEquivalent:"), keyEquivalent, "c@:@");
|
||||
// First responder status is what routes key events to this view.
|
||||
var accepts = (nint)(delegate* unmanaged[Cdecl]<nint, nint, byte>)&AcceptsFirstResponder;
|
||||
MetalNative.class_addMethod(
|
||||
cls, MetalNative.Selector("acceptsFirstResponder"), accepts, "c@:");
|
||||
MetalNative.objc_registerClassPair(cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static void OnKeyDown(nint self, nint cmd, nint nsEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var keyCode = (ushort)(MetalNative.Send(nsEvent, MetalNative.Selector("keyCode")) & 0xFFFF);
|
||||
var isRepeat = MetalNative.SendBool(nsEvent, MetalNative.Selector("isARepeat"));
|
||||
|
||||
// Function keys can arrive here even with Command held (AppKit only
|
||||
// reroutes some chords through the key-equivalent path), so catch
|
||||
// Cmd+F1 in both places — and keep it away from MetalHostInput so it
|
||||
// never toggles the plain-F1 perf overlay.
|
||||
var modifiers = (ulong)MetalNative.Send(nsEvent, MetalNative.Selector("modifierFlags"));
|
||||
if (keyCode == KeyCodeF1 && (modifiers & NsEventModifierFlagCommand) != 0)
|
||||
{
|
||||
if (!isRepeat)
|
||||
{
|
||||
ToggleMetalPerformanceHud();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MetalHostInput.KeyDown(keyCode, isRepeat);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] Metal key-down handler failed: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static void OnKeyUp(nint self, nint cmd, nint nsEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var keyCode = (ushort)(MetalNative.Send(nsEvent, MetalNative.Selector("keyCode")) & 0xFFFF);
|
||||
MetalHostInput.KeyUp(keyCode);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] Metal key-up handler failed: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static byte AcceptsFirstResponder(nint self, nint cmd) => 1;
|
||||
|
||||
private const ushort KeyCodeF1 = 0x7A;
|
||||
private const ulong NsEventModifierFlagCommand = 1UL << 20;
|
||||
private static bool _metalHudVisible;
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static byte OnPerformKeyEquivalent(nint self, nint cmd, nint nsEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var keyCode = (ushort)(MetalNative.Send(nsEvent, MetalNative.Selector("keyCode")) & 0xFFFF);
|
||||
var modifiers = (ulong)MetalNative.Send(nsEvent, MetalNative.Selector("modifierFlags"));
|
||||
if (keyCode == KeyCodeF1 && (modifiers & NsEventModifierFlagCommand) != 0)
|
||||
{
|
||||
if (!MetalNative.SendBool(nsEvent, MetalNative.Selector("isARepeat")))
|
||||
{
|
||||
ToggleMetalPerformanceHud();
|
||||
}
|
||||
|
||||
return 1; // handled: no system beep, no further routing
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] Metal key-equivalent handler failed: {exception.Message}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cmd+F1: Apple's Metal Performance HUD on the CAMetalLayer (plain F1 keeps
|
||||
/// the built-in CPU-rasterized perf overlay). Configured per Apple's
|
||||
@@ -456,7 +303,7 @@ internal static partial class MetalVideoPresenter
|
||||
/// mode=default|disabled and logging=default, plus any MTL_HUD_* environment
|
||||
/// keys directly in the dictionary — all three HUD flags (enabled, per-frame
|
||||
/// logging, shader-compile logging) ride in one property set. Runs on the
|
||||
/// AppKit main thread (the key-equivalent path), same thread as the render loop.
|
||||
/// SDL host thread, the same thread as the render loop.
|
||||
/// </summary>
|
||||
private static void ToggleMetalPerformanceHud()
|
||||
{
|
||||
@@ -508,33 +355,13 @@ internal static partial class MetalVideoPresenter
|
||||
$"[LOADER][INFO] Metal Performance HUD {(_metalHudVisible ? "shown" : "hidden")} (Cmd+F1).");
|
||||
}
|
||||
|
||||
private static unsafe nint CreateRenderTimerTarget()
|
||||
/// <summary>The SDL host loop drains guest work continuously. The method
|
||||
/// remains as the enqueue-side hook used by guest image synchronization.</summary>
|
||||
internal static void ScheduleGuestWorkDrain()
|
||||
{
|
||||
// A minimal NSObject subclass whose onFrame: is our unmanaged callback —
|
||||
// the dependency-free way to hand a target/selector to NSTimer without a
|
||||
// binding library. Registered once per process.
|
||||
var cls = MetalNative.objc_allocateClassPair(
|
||||
MetalNative.Class("NSObject"), "SharpEmuRenderTimerTarget", 0);
|
||||
if (cls != 0)
|
||||
{
|
||||
var imp = (nint)(delegate* unmanaged[Cdecl]<nint, nint, nint, void>)&OnRenderTimer;
|
||||
// "v@:@": void return, self, _cmd, one object argument (the timer).
|
||||
MetalNative.class_addMethod(cls, MetalNative.Selector("onFrame:"), imp, "v@:@");
|
||||
var wakeImp = (nint)(delegate* unmanaged[Cdecl]<nint, nint, nint, void>)&OnGuestWorkWake;
|
||||
MetalNative.class_addMethod(cls, MetalNative.Selector("onGuestWork:"), wakeImp, "v@:@");
|
||||
MetalNative.objc_registerClassPair(cls);
|
||||
}
|
||||
else
|
||||
{
|
||||
cls = MetalNative.Class("SharpEmuRenderTimerTarget");
|
||||
}
|
||||
|
||||
return MetalNative.Send(
|
||||
MetalNative.Send(cls, MetalNative.Selector("alloc")), MetalNative.Selector("init"));
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static void OnRenderTimer(nint self, nint cmd, nint timer)
|
||||
private static void RenderFrameSafely()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -546,78 +373,13 @@ internal static partial class MetalVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Set while an onGuestWork: wake is scheduled on the main run
|
||||
/// loop; coalesces enqueue-side wake requests to one in-flight message.</summary>
|
||||
private static int _guestWorkWakeScheduled;
|
||||
|
||||
/// <summary>Wakes the main run loop to drain guest work now instead of at
|
||||
/// the next render tick. Guest submit→wait round-trips (release-mem labels,
|
||||
/// CPU-visible write-backs) otherwise cost a full frame interval each —
|
||||
/// games that chain several per frame crawl at a fraction of the display
|
||||
/// rate. Safe from any thread; no-op until the presenter starts.</summary>
|
||||
internal static void ScheduleGuestWorkDrain()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _guestWorkWakeScheduled, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = _renderTimerTarget;
|
||||
if (target == 0)
|
||||
{
|
||||
Volatile.Write(ref _guestWorkWakeScheduled, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
MetalNative.SendVoidPerformSelector(
|
||||
target,
|
||||
MetalNative.Selector("performSelectorOnMainThread:withObject:waitUntilDone:"),
|
||||
MetalNative.Selector("onGuestWork:"),
|
||||
0,
|
||||
waitUntilDone: false);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static void OnGuestWorkWake(nint self, nint cmd, nint argument)
|
||||
{
|
||||
Volatile.Write(ref _guestWorkWakeScheduled, 0);
|
||||
if (_device == 0 || _commandQueue == 0 || Volatile.Read(ref _closeRequested))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pool = MetalNative.objc_autoreleasePoolPush();
|
||||
try
|
||||
{
|
||||
DrainGuestWork(_device, _commandQueue);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Metal guest work wake failed: {exception}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
MetalNative.objc_autoreleasePoolPop(pool);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderFrame()
|
||||
{
|
||||
MetalHostInput.PumpAutoKeys();
|
||||
var pool = MetalNative.objc_autoreleasePoolPush();
|
||||
try
|
||||
{
|
||||
// NSApp.run dispatches window events itself, so there is no manual
|
||||
// event drain here.
|
||||
var visible = MetalNative.SendBool(_window, MetalNative.Selector("isVisible"));
|
||||
if (Volatile.Read(ref _closeRequested) || !visible)
|
||||
if (Volatile.Read(ref _closeRequested))
|
||||
{
|
||||
_userClosed = !visible && !Volatile.Read(ref _closeRequested);
|
||||
MetalNative.SendVoid(_renderTimer, MetalNative.Selector("invalidate"));
|
||||
// Stop both the AppKit loop and the underlying CFRunLoop so
|
||||
// [NSApp run] returns.
|
||||
MetalNative.SendVoid(_application, MetalNative.Selector("stop:"), 0);
|
||||
MetalNative.CFRunLoopStop(MetalNative.CFRunLoopGetMain());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -715,8 +477,7 @@ internal static partial class MetalVideoPresenter
|
||||
if (!string.Equals(title, _lastWindowTitle, StringComparison.Ordinal))
|
||||
{
|
||||
_lastWindowTitle = title;
|
||||
MetalNative.SendVoid(
|
||||
_window, MetalNative.Selector("setTitle:"), MetalNative.NsString(title));
|
||||
Volatile.Read(ref _hostWindow)?.SetTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,8 +486,20 @@ internal static partial class MetalVideoPresenter
|
||||
// for a drawable, or nextDrawable keeps handing back the original
|
||||
// resolution and Core Animation stretches it (blurry, mis-scaled
|
||||
// overlay). No-op when the size is unchanged, i.e. almost every tick.
|
||||
var hostWindow = Volatile.Read(ref _hostWindow);
|
||||
if (hostWindow?.ConsumeSurfaceRestore() == true)
|
||||
{
|
||||
_drawableWidth = 0;
|
||||
_drawableHeight = 0;
|
||||
}
|
||||
|
||||
SyncDrawableSizeToLayer();
|
||||
|
||||
if (hostWindow?.IsMinimized == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var drawable = MetalNative.Send(_metalLayer, MetalNative.Selector("nextDrawable"));
|
||||
if (drawable == 0)
|
||||
{
|
||||
@@ -846,43 +619,33 @@ internal static partial class MetalVideoPresenter
|
||||
3);
|
||||
}
|
||||
|
||||
private static nint CreateWindow(uint width, uint height)
|
||||
private static void ConfigureMetalLayer(nint device, nint layer)
|
||||
{
|
||||
var window = MetalNative.SendInitWindow(
|
||||
MetalNative.Send(MetalNative.Class("NSWindow"), MetalNative.Selector("alloc")),
|
||||
MetalNative.Selector("initWithContentRect:styleMask:backing:defer:"),
|
||||
new CGRect { X = 0, Y = 0, Width = width, Height = height },
|
||||
WindowStyleMask,
|
||||
BackingStoreBuffered,
|
||||
defer: false);
|
||||
// The presenter owns the handle; AppKit must not free it on user close.
|
||||
MetalNative.SendVoidBool(window, MetalNative.Selector("setReleasedWhenClosed:"), false);
|
||||
MetalNative.Send(
|
||||
window, MetalNative.Selector("setCollectionBehavior:"), (nint)FullScreenPrimaryBehavior);
|
||||
MetalNative.SendVoid(
|
||||
window,
|
||||
MetalNative.Selector("setTitle:"),
|
||||
MetalNative.NsString(VideoOutExports.GetWindowTitle()));
|
||||
MetalNative.SendVoid(window, MetalNative.Selector("center"));
|
||||
// makeKeyAndOrderFront happens after the metal layer is attached.
|
||||
return window;
|
||||
MetalNative.SendVoid(layer, MetalNative.Selector("setDevice:"), device);
|
||||
MetalNative.Send(layer, MetalNative.Selector("setPixelFormat:"), (nint)PixelFormatBgra8Unorm);
|
||||
MetalNative.SendVoidBool(layer, MetalNative.Selector("setOpaque:"), true);
|
||||
MetalNative.SendVoidBool(layer, MetalNative.Selector("setFramebufferOnly:"), true);
|
||||
|
||||
var setDisplaySync = MetalNative.Selector("setDisplaySyncEnabled:");
|
||||
if (MetalNative.SendBool(layer, MetalNative.Selector("respondsToSelector:"), setDisplaySync))
|
||||
{
|
||||
MetalNative.SendVoidBool(layer, setDisplaySync, _videoOptions.VSync);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Keeps the CAMetalLayer's drawable size (pixels) matched to its
|
||||
/// current bounds (points) × scale as the window resizes or moves between
|
||||
/// displays. CAMetalLayer never updates drawableSize on its own, even as a
|
||||
/// view's backing layer, so the render loop drives it.</summary>
|
||||
/// <summary>Keeps the SDL-owned CAMetalLayer drawable in host pixels as the
|
||||
/// window resizes or moves between displays with different scale factors.</summary>
|
||||
private static void SyncDrawableSizeToLayer()
|
||||
{
|
||||
MetalNative.SendStretRect(out var bounds, _metalLayer, MetalNative.Selector("bounds"));
|
||||
var scale = MetalNative.SendDouble(_metalLayer, MetalNative.Selector("contentsScale"));
|
||||
if (scale <= 0)
|
||||
var hostWindow = Volatile.Read(ref _hostWindow);
|
||||
if (hostWindow is null || _metalLayer == 0)
|
||||
{
|
||||
scale = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
var width = Math.Max(1, Math.Round(bounds.Width * scale));
|
||||
var height = Math.Max(1, Math.Round(bounds.Height * scale));
|
||||
var pixelSize = hostWindow.PixelSize;
|
||||
var width = (double)pixelSize.Width;
|
||||
var height = (double)pixelSize.Height;
|
||||
if (width == _drawableWidth && height == _drawableHeight)
|
||||
{
|
||||
return;
|
||||
@@ -896,57 +659,6 @@ internal static partial class MetalVideoPresenter
|
||||
new CGSize { Width = width, Height = height });
|
||||
}
|
||||
|
||||
private static nint CreateLayer(nint device, nint window, out double drawableWidth, out double drawableHeight)
|
||||
{
|
||||
var contentView = MetalNative.Send(window, MetalNative.Selector("contentView"));
|
||||
var scale = MetalNative.SendDouble(window, MetalNative.Selector("backingScaleFactor"));
|
||||
if (scale <= 0)
|
||||
{
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
const uint width = DefaultWindowWidth;
|
||||
const uint height = DefaultWindowHeight;
|
||||
drawableWidth = width * scale;
|
||||
drawableHeight = height * scale;
|
||||
|
||||
var layer = MetalNative.Send(
|
||||
MetalNative.Send(MetalNative.Class("CAMetalLayer"), MetalNative.Selector("alloc")),
|
||||
MetalNative.Selector("init"));
|
||||
MetalNative.SendVoid(layer, MetalNative.Selector("setDevice:"), device);
|
||||
MetalNative.Send(layer, MetalNative.Selector("setPixelFormat:"), (nint)PixelFormatBgra8Unorm);
|
||||
// A Core Animation layer composites with its alpha channel by default, so
|
||||
// a presented frame whose guest alpha is zero would show through as the
|
||||
// window background (black). The presenter output is a finished opaque
|
||||
// frame; mark the layer opaque so alpha never reaches the compositor.
|
||||
MetalNative.SendVoidBool(layer, MetalNative.Selector("setOpaque:"), true);
|
||||
MetalNative.SendVoidBool(layer, MetalNative.Selector("setFramebufferOnly:"), true);
|
||||
MetalNative.SendVoidDouble(layer, MetalNative.Selector("setContentsScale:"), scale);
|
||||
MetalNative.SendVoidSize(
|
||||
layer,
|
||||
MetalNative.Selector("setDrawableSize:"),
|
||||
new CGSize { Width = drawableWidth, Height = drawableHeight });
|
||||
|
||||
// A manually created layer defaults to a zero-size frame, and a hosted
|
||||
// layer's geometry is the caller's job: without this the presenter
|
||||
// happily presents every drawable into a layer with no on-screen
|
||||
// extent — a permanently black window.
|
||||
MetalNative.SendVoidRect(
|
||||
layer,
|
||||
MetalNative.Selector("setFrame:"),
|
||||
new CGRect { X = 0, Y = 0, Width = width, Height = height });
|
||||
|
||||
// wantsLayer FIRST, then the layer: that makes the metal layer the
|
||||
// view's AppKit-managed BACKING layer (geometry and window-server
|
||||
// commits handled by AppKit) — the SDL/GLFW pattern. The reverse order
|
||||
// creates a layer-hosting view whose tree the app must commit itself,
|
||||
// which never composites under a manually pumped run loop.
|
||||
MetalNative.SendVoidBool(contentView, MetalNative.Selector("setWantsLayer:"), true);
|
||||
MetalNative.SendVoid(contentView, MetalNative.Selector("setLayer:"), layer);
|
||||
MetalNative.SendVoid(MetalNative.Class("CATransaction"), MetalNative.Selector("flush"));
|
||||
return layer;
|
||||
}
|
||||
|
||||
private static bool TryCreatePresentPipeline(nint device, out nint pipeline, out string error)
|
||||
{
|
||||
pipeline = 0;
|
||||
@@ -1105,10 +817,24 @@ internal static partial class MetalVideoPresenter
|
||||
{
|
||||
MetalNative.SendVoid(encoder, MetalNative.Selector("setRenderPipelineState:"), pipeline);
|
||||
|
||||
// Aspect-fit letterbox: scale the frame into the drawable via the viewport.
|
||||
var scale = Math.Min(drawableWidth / frameWidth, drawableHeight / frameHeight);
|
||||
var viewportWidth = frameWidth * scale;
|
||||
var viewportHeight = frameHeight * scale;
|
||||
var scaleX = drawableWidth / frameWidth;
|
||||
var scaleY = drawableHeight / frameHeight;
|
||||
var viewportWidth = drawableWidth;
|
||||
var viewportHeight = drawableHeight;
|
||||
if (_videoOptions.ScalingMode != HostScalingMode.Stretch)
|
||||
{
|
||||
var scale = _videoOptions.ScalingMode == HostScalingMode.Cover
|
||||
? Math.Max(scaleX, scaleY)
|
||||
: Math.Min(scaleX, scaleY);
|
||||
if (_videoOptions.ScalingMode == HostScalingMode.Integer && scale >= 1)
|
||||
{
|
||||
scale = Math.Floor(scale);
|
||||
}
|
||||
|
||||
viewportWidth = frameWidth * scale;
|
||||
viewportHeight = frameHeight * scale;
|
||||
}
|
||||
|
||||
MetalNative.SendVoidViewport(
|
||||
encoder,
|
||||
MetalNative.Selector("setViewport:"),
|
||||
|
||||
@@ -378,8 +378,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||
public void SubmitGuestImageFill(ulong address, uint fillValue) =>
|
||||
VulkanVideoPresenter.SubmitGuestImageFill(address, fillValue);
|
||||
|
||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels) =>
|
||||
VulkanVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
||||
public void SubmitGuestImageWrite(ulong address, byte[] pixels, uint rowOffset = 0) =>
|
||||
VulkanVideoPresenter.SubmitGuestImageWrite(address, pixels, rowOffset);
|
||||
|
||||
public bool SupportsPartialImageWrite => true;
|
||||
|
||||
public void RequestCpuWrittenGuestImageSync(ulong scopeAddress = 0, ulong scopeByteCount = ulong.MaxValue) =>
|
||||
VulkanVideoPresenter.RequestCpuWrittenGuestImageSync(scopeAddress, scopeByteCount);
|
||||
|
||||
Reference in New Issue
Block a user