Files
sharpemu/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
T
Mike Saito caf859cc52 Fix guest shutdown when VideoOut window is closed (#184)
Propagate Silk window close to runtime teardown so audio and CPU workers stop instead of continuing after the presentation window is dismissed.
2026-07-15 00:52:01 +03:00

7286 lines
282 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Silk.NET.Core;
using Silk.NET.Core.Native;
using Silk.NET.Maths;
using SharpEmu.Libs.Agc;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Windowing;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using VkBuffer = Silk.NET.Vulkan.Buffer;
using VkSemaphore = Silk.NET.Vulkan.Semaphore;
namespace SharpEmu.Libs.VideoOut;
internal enum GuestDrawKind
{
None,
FullscreenBarycentric,
}
internal sealed record VulkanGuestDrawTexture(
ulong Address,
uint Width,
uint Height,
uint Format,
uint NumberType,
byte[] RgbaPixels,
bool IsFallback,
bool IsStorage,
uint MipLevels = 1,
uint MipLevel = 0,
uint Pitch = 0,
uint TileMode = 0,
uint DstSelect = 0xFAC,
VulkanGuestSampler Sampler = default);
internal readonly record struct VulkanGuestSampler(
uint Word0,
uint Word1,
uint Word2,
uint Word3);
internal sealed record VulkanGuestMemoryBuffer(
ulong BaseAddress,
byte[] Data);
internal sealed record VulkanGuestVertexBuffer(
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data);
internal sealed record VulkanGuestIndexBuffer(
byte[] Data,
bool Is32Bit);
internal readonly record struct VulkanGuestRect(
int X,
int Y,
uint Width,
uint Height);
internal readonly record struct VulkanGuestViewport(
float X,
float Y,
float Width,
float Height,
float MinDepth,
float MaxDepth);
internal readonly record struct VulkanGuestBlendState(
bool Enable,
uint ColorSrcFactor,
uint ColorDstFactor,
uint ColorFunc,
uint AlphaSrcFactor,
uint AlphaDstFactor,
uint AlphaFunc,
bool SeparateAlphaBlend,
uint WriteMask)
{
public static VulkanGuestBlendState Default { get; } = new(
Enable: false,
ColorSrcFactor: 1,
ColorDstFactor: 0,
ColorFunc: 0,
AlphaSrcFactor: 1,
AlphaDstFactor: 0,
AlphaFunc: 0,
SeparateAlphaBlend: false,
WriteMask: 0xFu);
}
internal sealed record VulkanGuestRenderState(
VulkanGuestBlendState Blend,
VulkanGuestRect? Scissor,
VulkanGuestViewport? Viewport)
{
public static VulkanGuestRenderState Default { get; } = new(
VulkanGuestBlendState.Default,
Scissor: null,
Viewport: null);
}
internal sealed record VulkanGuestRenderTarget(
ulong Address,
uint Width,
uint Height,
uint Format,
uint NumberType,
uint MipLevels = 1);
internal sealed record VulkanTranslatedGuestDraw(
byte[] VertexSpirv,
byte[] PixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> Textures,
IReadOnlyList<VulkanGuestMemoryBuffer> GlobalMemoryBuffers,
IReadOnlyList<VulkanGuestVertexBuffer> VertexBuffers,
uint AttributeCount,
uint VertexCount,
uint InstanceCount,
uint PrimitiveType,
VulkanGuestIndexBuffer? IndexBuffer,
VulkanGuestRenderState RenderState);
internal sealed record VulkanOffscreenGuestDraw(
VulkanTranslatedGuestDraw Draw,
VulkanGuestRenderTarget Target,
bool PublishTarget);
internal sealed record VulkanComputeGuestDispatch(
ulong ShaderAddress,
byte[] ComputeSpirv,
IReadOnlyList<VulkanGuestDrawTexture> Textures,
IReadOnlyList<VulkanGuestMemoryBuffer> GlobalMemoryBuffers,
uint GroupCountX,
uint GroupCountY,
uint GroupCountZ);
internal static unsafe class VulkanVideoPresenter
{
private const uint DefaultWindowWidth = 1280;
private const uint DefaultWindowHeight = 720;
private const int MaxPendingGuestWork = 16;
private const int MaxGuestWorkPerRender = 16;
private const uint GuestPrimitiveRectList = 0x11;
private const uint GuestFormatR32Uint = 0x10004;
private const uint GuestFormatR32Sint = 0x20004;
private const uint GuestFormatR32Sfloat = 0x30004;
private const uint GuestFormatR16G16Uint = 0x10005;
private const uint GuestFormatR16G16Sint = 0x20005;
private const uint GuestFormatR16G16Sfloat = 0x30005;
private const uint GuestFormatR8G8B8A8Uint = 0x1000A;
private const uint GuestFormatR8G8B8A8Sint = 0x2000A;
private const uint GuestFormatR16G16B16A16Uint = 0x1000C;
private const uint GuestFormatR16G16B16A16Sint = 0x2000C;
private static readonly object _gate = new();
private static readonly Queue<object> _pendingGuestWork = new();
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
private static readonly Dictionary<ulong, uint> _gpuGuestImages = new();
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
_tracedGuestImageSubmissions = [];
private static Thread? _thread;
private static Presentation? _latestPresentation;
private static byte[]? _copyFragmentSpirv;
private static uint _windowWidth;
private static uint _windowHeight;
private static bool _closed;
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
private const uint NvidiaVendorId = 0x10DE;
private static bool _splashHidden;
private static long _enqueuedGuestWorkSequence;
private static long _completedGuestWorkSequence;
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
{
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
return string.Equals(mode, "1", StringComparison.Ordinal) ||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
}
public static void EnsureStarted(uint width, uint height)
{
if (width == 0 || height == 0)
{
return;
}
lock (_gate)
{
if (_closed || _thread is not null)
{
return;
}
}
var hasSplash = PngSplashLoader.TryLoad(
out var splashPixels,
out var splashWidth,
out var splashHeight);
lock (_gate)
{
if (_closed || _thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_latestPresentation ??= _splashHidden
? new Presentation(
CreateBlackFrame(width, height),
width,
height,
1,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false)
: hasSplash
? new Presentation(
splashPixels,
splashWidth,
splashHeight,
1,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: true)
: new Presentation(
null,
width,
height,
0,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
}
public static void HideSplashScreen()
{
lock (_gate)
{
_splashHidden = true;
if (_closed || _latestPresentation is not { IsSplash: true } latest)
{
return;
}
var sequence = latest.Sequence + 1;
_latestPresentation = new Presentation(
CreateBlackFrame(latest.Width, latest.Height),
latest.Width,
latest.Height,
sequence,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
System.Threading.Monitor.PulseAll(_gate);
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
}
}
public static void Submit(byte[] bgraFrame, uint width, uint height)
{
if (bgraFrame.Length != checked((int)(width * height * 4)))
{
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
}
lock (_gate)
{
if (_closed)
{
return;
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(
bgraFrame,
width,
height,
sequence,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
System.Threading.Monitor.PulseAll(_gate);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
}
public static void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height)
{
if (drawKind == GuestDrawKind.None || width == 0 || height == 0)
{
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
}
lock (_gate)
{
if (_closed ||
_latestPresentation is { Pixels: null } latest &&
latest.DrawKind == drawKind &&
latest.Width == width &&
latest.Height == height)
{
return;
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(
null,
width,
height,
sequence,
drawKind,
TranslatedDraw: null,
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
System.Threading.Monitor.PulseAll(_gate);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
}
public static void SubmitTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
uint width,
uint height,
uint attributeCount,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 || width == 0 || height == 0)
{
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
}
lock (_gate)
{
if (_closed)
{
return;
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(
null,
width,
height,
sequence,
GuestDrawKind.None,
new VulkanTranslatedGuestDraw(
vertexSpirv ?? [],
pixelSpirv,
textures.ToArray(),
globalMemoryBuffers.ToArray(),
vertexBuffers?.ToArray() ?? [],
attributeCount,
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
renderState ?? VulkanGuestRenderState.Default),
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
System.Threading.Monitor.PulseAll(_gate);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
}
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
VulkanGuestRenderTarget target,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 ||
target.Address == 0 ||
target.Width == 0 ||
target.Height == 0)
{
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
}
lock (_gate)
{
if (_closed)
{
return;
}
var guestTextureFormat = GetGuestTextureFormat(
target.Format,
target.NumberType);
if (guestTextureFormat != 0)
{
_availableGuestImages[target.Address] = guestTextureFormat;
}
EnqueueGuestWorkLocked(
new VulkanOffscreenGuestDraw(
new VulkanTranslatedGuestDraw(
vertexSpirv ?? [],
pixelSpirv,
textures.ToArray(),
globalMemoryBuffers.ToArray(),
vertexBuffers?.ToArray() ?? [],
attributeCount,
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
renderState ?? VulkanGuestRenderState.Default),
target,
PublishTarget: true));
}
}
public static void SubmitStorageTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
uint width,
uint height)
{
if (pixelSpirv.Length == 0 ||
width == 0 ||
height == 0 ||
textures.All(texture => !texture.IsStorage))
{
return;
}
lock (_gate)
{
if (_closed)
{
return;
}
EnqueueGuestWorkLocked(
new VulkanOffscreenGuestDraw(
new VulkanTranslatedGuestDraw(
[],
pixelSpirv,
textures.ToArray(),
globalMemoryBuffers.ToArray(),
[],
attributeCount,
3,
1,
4,
null,
VulkanGuestRenderState.Default),
new VulkanGuestRenderTarget(
Address: 0,
width,
height,
Format: 12,
NumberType: 7),
PublishTarget: false));
}
}
public static void SubmitComputeDispatch(
ulong shaderAddress,
byte[] computeSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ)
{
if (computeSpirv.Length == 0 ||
groupCountX == 0 ||
groupCountY == 0 ||
groupCountZ == 0 ||
textures.All(texture => !texture.IsStorage))
{
return;
}
lock (_gate)
{
if (_closed)
{
return;
}
EnqueueGuestWorkLocked(
new VulkanComputeGuestDispatch(
shaderAddress,
computeSpirv,
textures.ToArray(),
globalMemoryBuffers.ToArray(),
groupCountX,
groupCountY,
groupCountZ));
}
}
public static bool TrySubmitGuestImage(
ulong address,
uint width,
uint height,
uint pitchInPixel)
{
var traceSubmission = false;
lock (_gate)
{
// VideoOut registration does not imply a rendered Vulkan image.
var known = _gpuGuestImages.ContainsKey(address);
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
$"{width}x{height} known={known}");
}
if (_closed)
{
return false;
}
// The caller (VideoOutExports.SubmitFlip) reports the flip as successful either
// way, so an unregistered address means the frame is dropped silently; warn once
// per address so that shows up in the log.
if (!known)
{
if (_tracedGuestImageSubmissions.Add((address, width, height)))
{
Console.Error.WriteLine(
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " +
$"{width}x{height} - flip target has no completed GPU render output");
}
return false;
}
traceSubmission =
_tracedGuestImageSubmissions.Add((address, width, height));
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(
null,
width,
height,
sequence,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: 0,
IsSplash: false,
GuestImageAddress: address);
System.Threading.Monitor.PulseAll(_gate);
if (_thread is not null)
{
return true;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
if (traceSubmission)
{
var effectivePitch = pitchInPixel == 0 ? width : pitchInPixel;
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_guest_image addr=0x{address:X16} " +
$"size={width}x{height} pitch={effectivePitch}");
}
return true;
}
// Display buffers registered through sceVideoOutRegisterBuffers are valid flip targets
// even when no AGC render-target write to them was ever observed.
internal static void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
{
if (address == 0 || guestFormat == 0)
{
return;
}
lock (_gate)
{
_availableGuestImages[address] = guestFormat;
}
}
internal static bool IsGpuGuestImageAvailable(
ulong address,
uint format,
uint numberType)
{
var guestFormat = GetGuestTextureFormat(format, numberType);
if (address == 0 || guestFormat == 0)
{
return false;
}
lock (_gate)
{
return _gpuGuestImages.TryGetValue(address, out var availableFormat) &&
availableFormat == guestFormat;
}
}
public static bool TrySubmitGuestImageBlit(
ulong sourceAddress,
uint sourceWidth,
uint sourceHeight,
uint sourceFormat,
ulong destinationAddress,
uint destinationWidth,
uint destinationHeight,
uint destinationFormat)
{
if (sourceAddress == 0 ||
destinationAddress == 0 ||
sourceWidth == 0 ||
sourceHeight == 0 ||
destinationWidth == 0 ||
destinationHeight == 0 ||
!TryGetCopyFragmentShader(out var fragmentSpirv))
{
return false;
}
lock (_gate)
{
if (_closed ||
!_availableGuestImages.ContainsKey(sourceAddress) ||
GetGuestTextureFormat(destinationFormat, 0) == 0)
{
return false;
}
}
SubmitOffscreenTranslatedDraw(
fragmentSpirv,
[
new VulkanGuestDrawTexture(
sourceAddress,
sourceWidth,
sourceHeight,
sourceFormat,
NumberType: 0,
[],
IsFallback: false,
IsStorage: false),
],
[],
attributeCount: 1,
new VulkanGuestRenderTarget(
destinationAddress,
destinationWidth,
destinationHeight,
destinationFormat,
NumberType: 0));
return true;
}
private static bool TryGetCopyFragmentShader(out byte[] spirv)
{
lock (_gate)
{
if (_copyFragmentSpirv is not null)
{
spirv = _copyFragmentSpirv;
return true;
}
}
spirv = SpirvFixedShaders.CreateCopyFragment();
lock (_gate)
{
_copyFragmentSpirv ??= spirv;
spirv = _copyFragmentSpirv;
}
return true;
}
private static uint GetGuestTextureFormat(uint format, uint numberType) =>
(format, numberType) switch
{
(9, _) => 9,
(4, 4) => GuestFormatR32Uint,
(4, 5) => GuestFormatR32Sint,
(4, 7) => GuestFormatR32Sfloat,
(5, 4) => GuestFormatR16G16Uint,
(5, 5) => GuestFormatR16G16Sint,
(5, 7) => GuestFormatR16G16Sfloat,
(10, 4) => GuestFormatR8G8B8A8Uint,
(10, 5) => GuestFormatR8G8B8A8Sint,
(10, _) => 56,
(12, 4) => GuestFormatR16G16B16A16Uint,
(12, 5) => GuestFormatR16G16B16A16Sint,
(12, 7) => 71,
(_, 0) when IsKnownGuestTextureFormat(format) => format,
_ => 0,
};
private static bool IsKnownGuestTextureFormat(uint format) =>
format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71;
private static byte[] CreateBlackFrame(uint width, uint height)
{
if (width == 0 || height == 0 || width > 8192 || height > 8192)
{
width = 1;
height = 1;
}
var pixels = GC.AllocateUninitializedArray<byte>(checked((int)(width * height * 4)));
pixels.AsSpan().Clear();
for (var offset = 3; offset < pixels.Length; offset += 4)
{
pixels[offset] = 0xFF;
}
return pixels;
}
private static void Run()
{
uint width;
uint height;
lock (_gate)
{
width = _windowWidth == 0 ? _latestPresentation?.Width ?? 1280 : _windowWidth;
height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
}
try
{
using var presenter = new Presenter(width, height);
presenter.Run();
}
catch (Exception exception)
{
Console.Error.WriteLine($"[LOADER][ERROR] Vulkan VideoOut presenter failed: {exception}");
}
finally
{
lock (_gate)
{
_closed = true;
_thread = null;
System.Threading.Monitor.PulseAll(_gate);
}
}
}
private static bool TryTakePresentation(long presentedSequence, out Presentation presentation)
{
lock (_gate)
{
if (_latestPresentation is not { } latest ||
latest.Sequence == presentedSequence ||
latest.RequiredGuestWorkSequence > _completedGuestWorkSequence)
{
presentation = default;
return false;
}
presentation = latest;
return true;
}
}
private static void EnqueueGuestWorkLocked(object work)
{
while (!_closed &&
_thread is not null &&
_pendingGuestWork.Count >= MaxPendingGuestWork)
{
System.Threading.Monitor.Wait(_gate);
}
if (_closed)
{
return;
}
_pendingGuestWork.Enqueue(work);
_enqueuedGuestWorkSequence++;
System.Threading.Monitor.PulseAll(_gate);
}
private static bool TryTakeGuestWork(out object work)
{
lock (_gate)
{
return _pendingGuestWork.TryDequeue(out work!);
}
}
private static void CompleteGuestWork()
{
lock (_gate)
{
_completedGuestWorkSequence++;
System.Threading.Monitor.PulseAll(_gate);
}
}
private readonly record struct Presentation(
byte[]? Pixels,
uint Width,
uint Height,
long Sequence,
GuestDrawKind DrawKind,
VulkanTranslatedGuestDraw? TranslatedDraw,
long RequiredGuestWorkSequence,
bool IsSplash,
ulong GuestImageAddress = 0);
private sealed class Presenter : IDisposable
{
private const string FullscreenBarycentricVertexSpirv =
"AwIjBwAAAQALAAgAMgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ACAAAAAAABAAAAG1haW4AAAAADQAAABoAAAApAAAAAwADAAIAAADCAQAABQAEAAQAAABtYWluAAAAAAUABgALAAAAZ2xfUGVyVmVydGV4AAAAAAYABgALAAAAAAAAAGdsX1Bvc2l0aW9uAAYABwALAAAAAQAAAGdsX1BvaW50U2l6ZQAAAAAGAAcACwAAAAIAAABnbF9DbGlwRGlzdGFuY2UABgAHAAsAAAADAAAAZ2xfQ3VsbERpc3RhbmNlAAUAAwANAAAAAAAAAAUABgAaAAAAZ2xfVmVydGV4SW5kZXgAAAUABQAdAAAAaW5kZXhhYmxlAAAABQAFACkAAABiYXJ5Y2VudHJpYwAFAAUALwAAAGluZGV4YWJsZQAAAEcAAwALAAAAAgAAAEgABQALAAAAAAAAAAsAAAAAAAAASAAFAAsAAAABAAAACwAAAAEAAABIAAUACwAAAAIAAAALAAAAAwAAAEgABQALAAAAAwAAAAsAAAAEAAAARwAEABoAAAALAAAAKgAAAEcABAApAAAAHgAAAAAAAAATAAIAAgAAACEAAwADAAAAAgAAABYAAwAGAAAAIAAAABcABAAHAAAABgAAAAQAAAAVAAQACAAAACAAAAAAAAAAKwAEAAgAAAAJAAAAAQAAABwABAAKAAAABgAAAAkAAAAeAAYACwAAAAcAAAAGAAAACgAAAAoAAAAgAAQADAAAAAMAAAALAAAAOwAEAAwAAAANAAAAAwAAABUABAAOAAAAIAAAAAEAAAArAAQADgAAAA8AAAAAAAAAFwAEABAAAAAGAAAAAgAAACsABAAIAAAAEQAAAAMAAAAcAAQAEgAAABAAAAARAAAAKwAEAAYAAAATAAAAAACAvywABQAQAAAAFAAAABMAAAATAAAAKwAEAAYAAAAVAAAAAABAQCwABQAQAAAAFgAAABUAAAATAAAALAAFABAAAAAXAAAAEwAAABUAAAAsAAYAEgAAABgAAAAUAAAAFgAAABcAAAAgAAQAGQAAAAEAAAAOAAAAOwAEABkAAAAaAAAAAQAAACAABAAcAAAABwAAABIAAAAgAAQAHgAAAAcAAAAQAAAAKwAEAAYAAAAhAAAAAAAAACsABAAGAAAAIgAAAAAAgD8gAAQAJgAAAAMAAAAHAAAAIAAEACgAAAADAAAAEAAAADsABAAoAAAAKQAAAAMAAAAsAAUAEAAAACoAAAAiAAAAIQAAACwABQAQAAAAKwAAACEAAAAiAAAALAAFABAAAAAsAAAAIQAAACEAAAAsAAYAEgAAAC0AAAAqAAAAKwAAACwAAAA2AAUAAgAAAAQAAAAAAAAAAwAAAPgAAgAFAAAAOwAEABwAAAAdAAAABwAAADsABAAcAAAALwAAAAcAAAA9AAQADgAAABsAAAAaAAAAPgADAB0AAAAYAAAAQQAFAB4AAAAfAAAAHQAAABsAAAA9AAQAEAAAACAAAAAfAAAAUQAFAAYAAAAjAAAAIAAAAAAAAABRAAUABgAAACQAAAAgAAAAAQAAAFAABwAHAAAAJQAAACMAAAAkAAAAIQAAACIAAABBAAUAJgAAACcAAAANAAAADwAAAD4AAwAnAAAAJQAAAD0ABAAOAAAALgAAABoAAAA+AAMALwAAAC0AAABBAAUAHgAAADAAAAAvAAAALgAAAD0ABAAQAAAAMQAAADAAAAA+AAMAKQAAADEAAAD9AAEAOAABAA==";
private const string FullscreenBarycentricFragmentSpirv =
"AwIjBwAAAQALAAgAEgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ABwAEAAAABAAAAG1haW4AAAAACQAAAAwAAAAQAAMABAAAAAcAAAADAAMAAgAAAMIBAAAFAAQABAAAAG1haW4AAAAABQAFAAkAAABvdXRDb2xvcgAAAAAFAAUADAAAAGJhcnljZW50cmljAEcABAAJAAAAHgAAAAAAAABHAAQADAAAAB4AAAAAAAAAEwACAAIAAAAhAAMAAwAAAAIAAAAWAAMABgAAACAAAAAXAAQABwAAAAYAAAAEAAAAIAAEAAgAAAADAAAABwAAADsABAAIAAAACQAAAAMAAAAXAAQACgAAAAYAAAACAAAAIAAEAAsAAAABAAAACgAAADsABAALAAAADAAAAAEAAAArAAQABgAAAA4AAAAAAAAANgAFAAIAAAAEAAAAAAAAAAMAAAD4AAIABQAAAD0ABAAKAAAADQAAAAwAAABRAAUABgAAAA8AAAANAAAAAAAAAFEABQAGAAAAEAAAAA0AAAABAAAAUAAHAAcAAAARAAAADwAAABAAAAAOAAAADgAAAD4AAwAJAAAAEQAAAP0AAQA4AAEA";
private readonly IWindow _window;
private const int MaxInFlightGuestSubmissions = 8;
private const double PerformanceHudSampleSeconds = 0.5;
private const uint ThreadQueryLimitedInformation = 0x0800;
private static readonly bool _performanceHudEnabled =
!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PERF_HUD"),
"0",
StringComparison.Ordinal);
private Vk _vk = null!;
private KhrSurface _surfaceApi = null!;
private KhrSwapchain _swapchainApi = null!;
private delegate* unmanaged<Device, DebugUtilsObjectNameInfoEXT*, Result> _setDebugUtilsObjectName;
private delegate* unmanaged<CommandBuffer, DebugUtilsLabelEXT*, void> _cmdBeginDebugUtilsLabel;
private delegate* unmanaged<CommandBuffer, void> _cmdEndDebugUtilsLabel;
private Instance _instance;
private SurfaceKHR _surface;
private DebugUtilsMessengerEXT _debugMessenger;
private ExtDebugUtils? _debugUtils;
private PhysicalDevice _physicalDevice;
private Device _device;
private PipelineCache _pipelineCache;
private string? _pipelineCachePath;
private Queue _queue;
private uint _queueFamilyIndex;
private SwapchainKHR _swapchain;
private Image[] _swapchainImages = [];
private ImageView[] _swapchainImageViews = [];
private Framebuffer[] _framebuffers = [];
private bool[] _imageInitialized = [];
private Format _swapchainFormat;
private Extent2D _extent;
private RenderPass _renderPass;
private PipelineLayout _pipelineLayout;
private Pipeline _barycentricPipeline;
private CommandPool _commandPool;
private CommandBuffer _commandBuffer;
private CommandBuffer _presentationCommandBuffer;
private VkSemaphore _imageAvailable;
private VkSemaphore _renderFinished;
private Fence _presentationFence;
private bool _presentationInFlight;
private TranslatedDrawResources? _pendingPresentationResources;
private VkBuffer _stagingBuffer;
private DeviceMemory _stagingMemory;
private ulong _stagingSize;
private long _presentedSequence;
private long _performanceHudLastTimestamp;
private TimeSpan _performanceHudLastProcessCpu;
private long _performanceHudPresentedFrames;
private long _performanceHudLastPresentedFrames;
private long _performanceHudLastReadCount;
private long _performanceHudLastReadBytes;
private long _performanceHudLastReadHits;
private long _performanceHudLastReadPvmBytes;
private long _performanceHudLastReadLibcBytes;
private readonly Dictionary<int, TimeSpan> _performanceHudThreadCpu = [];
private readonly Dictionary<int, string> _performanceHudThreadNames = [];
private bool _vulkanReady;
private bool _firstFramePresented;
private bool _firstGuestDrawPresented;
private bool _splashPresented;
private bool _swapchainRecreateDeferred;
private bool _tracedPresentedSwapchain;
private bool _swapchainReadbackPending;
private bool _deviceLost;
private bool _deviceLostLogged;
private int _directPresentationCount;
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
private static readonly bool _dumpTextures = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
"1",
StringComparison.Ordinal);
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new();
private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new();
private readonly HashSet<ulong> _tracedGuestImageContents = new();
private readonly Dictionary<ulong, int> _tracedGuestWriteCounts = new();
private int _tracedVertexBufferCount;
private readonly Dictionary<byte[], Pipeline> _computePipelines =
new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<GraphicsPipelineKey, Pipeline> _graphicsPipelines = new();
private readonly Dictionary<VulkanGuestSampler, Sampler> _samplers = new();
private readonly Dictionary<byte[], string> _shaderDigests =
new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<DescriptorLayoutKey, DescriptorLayoutBundle>
_descriptorLayouts = new();
private readonly Dictionary<HostBufferPoolKey, Stack<HostBufferAllocation>>
_hostBufferPool = new();
private readonly Dictionary<ulong, HostBufferAllocation> _hostBufferAllocations = new();
private readonly Queue<PendingGuestSubmission> _pendingGuestSubmissions = new();
private readonly record struct GraphicsPipelineKey(
string VertexShader,
string FragmentShader,
Format RenderTargetFormat,
PrimitiveTopology Topology,
VulkanGuestBlendState Blend,
string ResourceLayout,
string VertexLayout);
private readonly record struct HostBufferPoolKey(
BufferUsageFlags Usage,
ulong Capacity);
private readonly record struct DescriptorLayoutKey(
ShaderStageFlags Stages,
string Resources);
private sealed record DescriptorLayoutBundle(
DescriptorSetLayout DescriptorSetLayout,
PipelineLayout PipelineLayout);
private sealed record HostBufferAllocation(
VkBuffer Buffer,
DeviceMemory Memory,
HostBufferPoolKey Key);
private sealed class TranslatedDrawResources
{
public string DebugName = "SharpEmu translated";
public PipelineLayout PipelineLayout;
public Pipeline Pipeline;
public bool PipelineCached;
public bool DescriptorLayoutCached;
public DescriptorSetLayout DescriptorSetLayout;
public DescriptorPool DescriptorPool;
public DescriptorSet DescriptorSet;
public TextureResource[] Textures = [];
public GlobalBufferResource[] GlobalMemoryBuffers = [];
public VertexBufferResource[] VertexBuffers = [];
public VkBuffer IndexBuffer;
public DeviceMemory IndexMemory;
public bool Index32Bit;
public uint VertexCount = 3;
public uint InstanceCount = 1;
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
public VulkanGuestBlendState Blend = VulkanGuestBlendState.Default;
public VulkanGuestRect? Scissor;
public VulkanGuestViewport? Viewport;
}
private sealed class TextureResource
{
public ulong Address;
public VkBuffer StagingBuffer;
public DeviceMemory StagingMemory;
public Image Image;
public DeviceMemory ImageMemory;
public ImageView View;
public uint Width;
public uint Height;
public uint RowLength;
public uint DstSelect;
public bool NeedsUpload;
public bool OwnsStorage;
public bool IsStorage;
public VulkanGuestSampler SamplerState;
public Sampler Sampler;
public GuestImageResource? GuestImage;
public ulong CpuContentFingerprint;
public bool UpdatesCpuContent;
}
private sealed class GlobalBufferResource
{
public VkBuffer Buffer;
public DeviceMemory Memory;
public ulong Size;
}
private sealed class VertexBufferResource
{
public VkBuffer Buffer;
public DeviceMemory Memory;
public ulong Size;
public uint Location;
public uint ComponentCount;
public uint DataFormat;
public uint NumberFormat;
public uint Stride;
public uint OffsetBytes;
}
private sealed class GuestImageResource
{
public ulong Address;
public uint Width;
public uint Height;
public uint MipLevels;
public Format Format;
public Image Image;
public DeviceMemory Memory;
public ImageView View;
public ImageView[] MipViews = [];
public Dictionary<(Format Format, uint MipLevel, uint LevelCount, uint DstSelect), ImageView> FormatViews { get; } = new();
public RenderPass RenderPass;
public Framebuffer Framebuffer;
public bool Initialized;
public bool InitialUploadPending;
public bool IsCpuBacked;
public ulong CpuContentFingerprint;
}
private sealed record PendingGuestSubmission(
Fence Fence,
CommandBuffer CommandBuffer,
TranslatedDrawResources Resources,
IReadOnlyList<GuestImageResource> TraceImages,
string DebugName);
public Presenter(uint width, uint height)
{
var options = WindowOptions.DefaultVulkan;
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
options.Title = VideoOutExports.GetWindowTitle();
options.WindowBorder = WindowBorder.Fixed;
// FIFO already provides the presentation clock. Throttling Silk's render loop
// as well can miss alternating vblanks and collapse delivery to 30 FPS or less.
options.VSync = false;
options.FramesPerSecond = 0;
options.UpdatesPerSecond = 0;
_window = Window.Create(options);
_window.Load += Initialize;
_window.Render += Render;
_window.Closing += OnWindowClosing;
}
private void OnWindowClosing()
{
VideoOutExports.NotifyPresentationWindowClosed();
DisposeVulkan();
}
public void Run() => _window.Run();
public void Dispose()
{
DisposeVulkan();
try
{
_window.Dispose();
}
catch (InvalidOperationException exception)
when (exception.Message.Contains("render loop", StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan VideoOut window dispose skipped during render loop: {exception.Message}");
}
}
private void Initialize()
{
if (PngSplashLoader.TryLoadIcon(out var iconPixels, out var iconWidth, out var iconHeight))
{
var icon = new RawImage((int)iconWidth, (int)iconHeight, iconPixels);
_window.SetWindowIcon(ref icon);
}
WaitForRenderDocAttachIfRequested();
_vk = Vk.GetApi();
CreateInstance();
CreateSurface();
SelectPhysicalDevice();
CreateDevice();
CreatePipelineCache();
CreateSwapchain();
CreateCommandResources();
CreateGuestDrawResources();
_vulkanReady = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}");
}
private static void WaitForRenderDocAttachIfRequested()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_RENDERDOC_WAIT");
if (string.IsNullOrWhiteSpace(value))
{
return;
}
if (string.Equals(value, "enter", StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Waiting for RenderDoc attach before Vulkan init. pid={Environment.ProcessId}. Press Enter to continue.");
_ = Console.ReadLine();
return;
}
var seconds = 15;
if (int.TryParse(value, out var parsedSeconds))
{
seconds = Math.Clamp(parsedSeconds, 1, 300);
}
Console.Error.WriteLine(
$"[LOADER][INFO] Waiting {seconds}s for RenderDoc attach before Vulkan init. pid={Environment.ProcessId}");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
}
private bool IsInstanceExtensionAvailable(string extensionName)
{
uint extensionCount = 0;
if (_vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, null) != Result.Success ||
extensionCount == 0)
{
return false;
}
var properties = new ExtensionProperties[extensionCount];
fixed (ExtensionProperties* propertyPointer = properties)
{
if (_vk.EnumerateInstanceExtensionProperties(
(byte*)null,
&extensionCount,
propertyPointer) != Result.Success)
{
return false;
}
var expected = Encoding.UTF8.GetBytes(extensionName);
for (var index = 0; index < extensionCount; index++)
{
if (Utf8NullTerminatedEquals(propertyPointer[index].ExtensionName, expected))
{
return true;
}
}
}
return false;
}
private static bool Utf8NullTerminatedEquals(byte* actual, ReadOnlySpan<byte> expected)
{
for (var index = 0; index < expected.Length; index++)
{
if (actual[index] != expected[index])
{
return false;
}
}
return actual[expected.Length] == 0;
}
private void LoadDebugUtilsCommands()
{
var setObjectName = _vk.GetDeviceProcAddr(_device, "vkSetDebugUtilsObjectNameEXT");
var beginLabel = _vk.GetDeviceProcAddr(_device, "vkCmdBeginDebugUtilsLabelEXT");
var endLabel = _vk.GetDeviceProcAddr(_device, "vkCmdEndDebugUtilsLabelEXT");
_setDebugUtilsObjectName =
(delegate* unmanaged<Device, DebugUtilsObjectNameInfoEXT*, Result>)
setObjectName.Handle;
_cmdBeginDebugUtilsLabel =
(delegate* unmanaged<CommandBuffer, DebugUtilsLabelEXT*, void>)
beginLabel.Handle;
_cmdEndDebugUtilsLabel =
(delegate* unmanaged<CommandBuffer, void>)
endLabel.Handle;
if (_setDebugUtilsObjectName is not null)
{
Console.Error.WriteLine("[LOADER][INFO] Vulkan debug labels enabled.");
}
}
private void SetDebugName(ObjectType objectType, ulong objectHandle, string name)
{
if (_setDebugUtilsObjectName is null ||
_device.Handle == 0 ||
objectHandle == 0)
{
return;
}
var bytes = NullTerminatedUtf8(name);
fixed (byte* namePointer = bytes)
{
var info = new DebugUtilsObjectNameInfoEXT
{
SType = StructureType.DebugUtilsObjectNameInfoExt,
ObjectType = objectType,
ObjectHandle = objectHandle,
PObjectName = namePointer,
};
_ = _setDebugUtilsObjectName(_device, &info);
}
}
private void BeginDebugLabel(CommandBuffer commandBuffer, string name)
{
if (_cmdBeginDebugUtilsLabel is null ||
commandBuffer.Handle == 0)
{
return;
}
var bytes = NullTerminatedUtf8(name);
fixed (byte* namePointer = bytes)
{
var label = new DebugUtilsLabelEXT
{
SType = StructureType.DebugUtilsLabelExt,
PLabelName = namePointer,
};
label.Color[0] = 0.20f;
label.Color[1] = 0.60f;
label.Color[2] = 1.00f;
label.Color[3] = 1.00f;
_cmdBeginDebugUtilsLabel(commandBuffer, &label);
}
}
private void EndDebugLabel(CommandBuffer commandBuffer)
{
if (_cmdEndDebugUtilsLabel is not null &&
commandBuffer.Handle != 0)
{
_cmdEndDebugUtilsLabel(commandBuffer);
}
}
private static byte[] NullTerminatedUtf8(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
Array.Resize(ref bytes, bytes.Length + 1);
return bytes;
}
private static string BuildComputeDebugName(VulkanComputeGuestDispatch dispatch)
{
var storage = dispatch.Textures.FirstOrDefault(texture => texture.IsStorage && texture.Address != 0);
return storage is null
? $"SharpEmu compute cs=0x{dispatch.ShaderAddress:X16} " +
$"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}"
: $"SharpEmu compute cs=0x{dispatch.ShaderAddress:X16} " +
$"storage=0x{storage.Address:X16} " +
$"{storage.Width}x{storage.Height} fmt{storage.Format} " +
$"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}";
}
private static string GuestImageDebugName(VulkanGuestRenderTarget target, Format format) =>
$"SharpEmu guest 0x{target.Address:X16} {target.Width}x{target.Height} " +
$"fmt{target.Format}/{format}";
private static string TextureDebugName(VulkanGuestDrawTexture texture, Format format) =>
$"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " +
$"fmt{texture.Format}/{format}";
private void CreateInstance()
{
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
byte* validationLayerName = null;
try
{
var applicationInfo = new ApplicationInfo
{
SType = StructureType.ApplicationInfo,
PApplicationName = applicationName,
ApplicationVersion = Vk.MakeVersion(0, 0, 1),
PEngineName = applicationName,
EngineVersion = Vk.MakeVersion(0, 0, 1),
ApiVersion = Vk.Version12,
};
var extensions = _window.VkSurface!.GetRequiredExtensions(out var extensionCount);
byte* debugUtilsExtension = null;
var enabledExtensionCount = (int)extensionCount;
var enabledExtensions = stackalloc byte*[(int)extensionCount + 1];
for (var index = 0; index < (int)extensionCount; index++)
{
enabledExtensions[index] = extensions[index];
}
if (IsInstanceExtensionAvailable(DebugUtilsExtensionName))
{
debugUtilsExtension = (byte*)SilkMarshal.StringToPtr(DebugUtilsExtensionName);
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
}
if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
{
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
}
else if (enableValidation)
{
Console.Error.WriteLine("[LOADER][WARN] SHARPEMU_VK_VALIDATION=1 but VK_LAYER_KHRONOS_validation not found (Vulkan SDK installed?).");
}
var layers = stackalloc byte*[1];
if (validationLayerName is not null)
{
layers[0] = validationLayerName;
}
var createInfo = new InstanceCreateInfo
{
SType = StructureType.InstanceCreateInfo,
PApplicationInfo = &applicationInfo,
EnabledExtensionCount = (uint)enabledExtensionCount,
PpEnabledExtensionNames = enabledExtensions,
EnabledLayerCount = validationLayerName is not null ? 1u : 0u,
PpEnabledLayerNames = validationLayerName is not null ? layers : null,
};
try
{
Check(_vk.CreateInstance(&createInfo, null, out _instance), "vkCreateInstance");
if (!_vk.TryGetInstanceExtension(_instance, out _surfaceApi))
{
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
}
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
{
_debugUtils = debugUtils;
RegisterDebugMessenger(debugUtils);
Console.Error.WriteLine("[LOADER][INFO] Vulkan Validation Layers active (SHARPEMU_VK_VALIDATION=1).");
}
}
finally
{
if (debugUtilsExtension is not null)
{
SilkMarshal.Free((nint)debugUtilsExtension);
}
}
}
finally
{
SilkMarshal.Free((nint)applicationName);
if (validationLayerName is not null)
{
SilkMarshal.Free((nint)validationLayerName);
}
}
}
private bool IsInstanceLayerAvailable(string layerName)
{
uint layerCount = 0;
if (_vk.EnumerateInstanceLayerProperties(&layerCount, null) != Result.Success || layerCount == 0)
{
return false;
}
var properties = new LayerProperties[layerCount];
fixed (LayerProperties* propertyPointer = properties)
{
if (_vk.EnumerateInstanceLayerProperties(&layerCount, propertyPointer) != Result.Success)
{
return false;
}
var expected = Encoding.UTF8.GetBytes(layerName);
for (var index = 0; index < layerCount; index++)
{
if (Utf8NullTerminatedEquals(propertyPointer[index].LayerName, expected))
{
return true;
}
}
}
return false;
}
private void RegisterDebugMessenger(ExtDebugUtils debugUtils)
{
var messengerInfo = new DebugUtilsMessengerCreateInfoEXT
{
SType = StructureType.DebugUtilsMessengerCreateInfoExt,
MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt
| DebugUtilsMessageSeverityFlagsEXT.WarningBitExt,
MessageType = DebugUtilsMessageTypeFlagsEXT.ValidationBitExt
| DebugUtilsMessageTypeFlagsEXT.PerformanceBitExt
| DebugUtilsMessageTypeFlagsEXT.GeneralBitExt,
PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(DebugCallback),
};
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
"vkCreateDebugUtilsMessengerEXT");
}
private static unsafe uint DebugCallback(
DebugUtilsMessageSeverityFlagsEXT severity,
DebugUtilsMessageTypeFlagsEXT type,
DebugUtilsMessengerCallbackDataEXT* callbackData,
void* userData)
{
var message = SilkMarshal.PtrToString((nint)callbackData->PMessage);
var prefix = severity switch
{
DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt => "[VULKAN][ERROR]",
DebugUtilsMessageSeverityFlagsEXT.WarningBitExt => "[VULKAN][WARN]",
_ => "[VULKAN][INFO]",
};
Console.Error.WriteLine($"{prefix} {message}");
return Vk.False;
}
private void CreateSurface()
{
var instanceHandle = new VkHandle(_instance.Handle);
var surfaceHandle = _window.VkSurface!.Create<AllocationCallbacks>(instanceHandle, null);
_surface = new SurfaceKHR(surfaceHandle.Handle);
}
private void SelectPhysicalDevice()
{
uint deviceCount = 0;
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, null), "vkEnumeratePhysicalDevices");
if (deviceCount == 0)
{
throw new InvalidOperationException("No Vulkan physical device was found.");
}
var devices = new PhysicalDevice[deviceCount];
fixed (PhysicalDevice* devicePointer = devices)
{
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
}
// Hybrid laptops enumerate the iGPU first, and AMD's integrated driver
// segfaults compiling some translated shaders, so rank rather than take
// the first hit. SHARPEMU_VK_DEVICE=<substring> pins an adapter by name.
var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE");
var bestScore = int.MinValue;
var found = false;
foreach (var device in devices)
{
uint queueCount = 0;
_vk.GetPhysicalDeviceQueueFamilyProperties(device, &queueCount, null);
var queues = new QueueFamilyProperties[queueCount];
fixed (QueueFamilyProperties* queuePointer = queues)
{
_vk.GetPhysicalDeviceQueueFamilyProperties(device, &queueCount, queuePointer);
}
for (uint index = 0; index < queueCount; index++)
{
var supportsGraphics = (queues[index].QueueFlags & QueueFlags.GraphicsBit) != 0;
_surfaceApi.GetPhysicalDeviceSurfaceSupport(device, index, _surface, out var supportsPresent);
if (!supportsGraphics || !supportsPresent)
{
continue;
}
_vk.GetPhysicalDeviceProperties(device, out var properties);
var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
var score = ScorePhysicalDevice(properties, name, deviceOverride);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}");
if (score > bestScore)
{
bestScore = score;
_physicalDevice = device;
_queueFamilyIndex = index;
found = true;
}
break;
}
}
if (!found)
{
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
}
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
VideoOutExports.SetSelectedGpuName(selectedName);
_window.Title = VideoOutExports.GetWindowTitle();
}
private static int ScorePhysicalDevice(
PhysicalDeviceProperties properties,
string name,
string? deviceOverride)
{
if (!string.IsNullOrWhiteSpace(deviceOverride))
{
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
}
var score = properties.DeviceType switch
{
PhysicalDeviceType.DiscreteGpu => 300,
PhysicalDeviceType.VirtualGpu => 100,
PhysicalDeviceType.Cpu => 50,
// Last resort: only picked when nothing else can present.
PhysicalDeviceType.IntegratedGpu => -100,
_ => 10,
};
if (properties.VendorID == NvidiaVendorId)
{
score += 500;
}
return score;
}
private void CreateDevice()
{
var priority = 1.0f;
var queueInfo = new DeviceQueueCreateInfo
{
SType = StructureType.DeviceQueueCreateInfo,
QueueFamilyIndex = _queueFamilyIndex,
QueueCount = 1,
PQueuePriorities = &priority,
};
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
var enabledFeatures = new PhysicalDeviceFeatures
{
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64,
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
ShaderStorageImageWriteWithoutFormat = supportedFeatures.ShaderStorageImageWriteWithoutFormat,
RobustBufferAccess = supportedFeatures.RobustBufferAccess,
};
if (!supportedFeatures.RobustBufferAccess)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support robustBufferAccess " +
"translated shaders performing out-of-bounds buffer access may cause device loss.");
}
if (!supportedFeatures.ShaderInt64)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderInt64 " +
"translated shaders using 64-bit integers will fail.");
}
if (!supportedFeatures.VertexPipelineStoresAndAtomics || !supportedFeatures.FragmentStoresAndAtomics)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support vertexPipelineStoresAndAtomics/fragmentStoresAndAtomics " +
"translated shaders using storage buffers in vertex/fragment stages may fail.");
}
if (!supportedFeatures.ShaderImageGatherExtended)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderImageGatherExtended " +
"translated shaders using image gather with offsets/LOD/bias will fail.");
}
if (!supportedFeatures.ShaderStorageImageReadWithoutFormat ||
!supportedFeatures.ShaderStorageImageWriteWithoutFormat)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderStorageImage(Read|Write)WithoutFormat " +
"translated shaders using unformatted storage image load/store will fail.");
}
var maintenance8Features = new PhysicalDeviceMaintenance8FeaturesKHR
{
SType = StructureType.PhysicalDeviceMaintenance8FeaturesKhr,
};
var robustness2Features = new PhysicalDeviceRobustness2FeaturesEXT
{
SType = StructureType.PhysicalDeviceRobustness2FeaturesExt,
PNext = &maintenance8Features,
};
var featuresQuery = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &robustness2Features,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
var supportsMaintenance8 = maintenance8Features.Maintenance8;
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
var supportsNullDescriptor = robustness2Features.NullDescriptor;
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
if (!supportsMaintenance8)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support VK_KHR_maintenance8 " +
"translated shaders using a dynamic texel offset on non-gather image samples will fail.");
}
if (!supportsRobustImageAccess2)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support VK_EXT_robustness2 robustImageAccess2 " +
"translated shaders performing out-of-bounds image access may cause device loss.");
}
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
var maintenance8Extension = (byte*)SilkMarshal.StringToPtr("VK_KHR_maintenance8");
var robustness2Extension = (byte*)SilkMarshal.StringToPtr("VK_EXT_robustness2");
try
{
var extensions = stackalloc byte*[3];
var extensionCount = 0u;
extensions[extensionCount++] = swapchainExtension;
if (supportsMaintenance8)
{
extensions[extensionCount++] = maintenance8Extension;
}
if (supportsRobustness2)
{
extensions[extensionCount++] = robustness2Extension;
}
maintenance8Features.Maintenance8 = supportsMaintenance8;
maintenance8Features.PNext = null;
robustness2Features.RobustBufferAccess2 =
supportsRobustImageAccess2 && supportedFeatures.RobustBufferAccess;
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
robustness2Features.NullDescriptor = supportsNullDescriptor;
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
var features2 = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = supportsRobustness2
? &robustness2Features
: (supportsMaintenance8 ? &maintenance8Features : null),
Features = enabledFeatures,
};
var createInfo = new DeviceCreateInfo
{
SType = StructureType.DeviceCreateInfo,
PNext = &features2,
QueueCreateInfoCount = 1,
PQueueCreateInfos = &queueInfo,
EnabledExtensionCount = extensionCount,
PpEnabledExtensionNames = extensions,
};
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
}
finally
{
SilkMarshal.Free((nint)swapchainExtension);
SilkMarshal.Free((nint)maintenance8Extension);
SilkMarshal.Free((nint)robustness2Extension);
}
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
LoadDebugUtilsCommands();
if (!_vk.TryGetDeviceExtension(_instance, _device, out _swapchainApi))
{
throw new InvalidOperationException("VK_KHR_swapchain is unavailable.");
}
}
private void CreatePipelineCache()
{
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
var directory = Path.Combine(AppContext.BaseDirectory, "pipeline-cache");
_pipelineCachePath = Path.Combine(
directory,
$"vulkan-{properties.VendorID:X4}-{properties.DeviceID:X4}-{properties.DriverVersion:X8}.bin");
byte[] initialData = [];
try
{
if (File.Exists(_pipelineCachePath))
{
initialData = File.ReadAllBytes(_pipelineCachePath);
}
}
catch (IOException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache could not be read: {exception.Message}");
}
fixed (byte* initialDataPointer = initialData)
{
var createInfo = new PipelineCacheCreateInfo
{
SType = StructureType.PipelineCacheCreateInfo,
InitialDataSize = (nuint)initialData.Length,
PInitialData = initialData.Length == 0 ? null : initialDataPointer,
};
var result = _vk.CreatePipelineCache(
_device,
&createInfo,
null,
out _pipelineCache);
if (result != Result.Success && initialData.Length != 0)
{
createInfo.InitialDataSize = 0;
createInfo.PInitialData = null;
Check(
_vk.CreatePipelineCache(
_device,
&createInfo,
null,
out _pipelineCache),
"vkCreatePipelineCache(empty)");
initialData = [];
}
else
{
Check(result, "vkCreatePipelineCache");
}
}
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache: " +
$"{(initialData.Length == 0 ? "new" : $"loaded {initialData.Length} bytes")}");
}
private void SavePipelineCache()
{
if (_pipelineCache.Handle == 0 || string.IsNullOrWhiteSpace(_pipelineCachePath))
{
return;
}
try
{
nuint size = 0;
Check(
_vk.GetPipelineCacheData(_device, _pipelineCache, &size, null),
"vkGetPipelineCacheData(size)");
if (size == 0 || size > int.MaxValue)
{
return;
}
var data = new byte[(int)size];
fixed (byte* dataPointer = data)
{
Check(
_vk.GetPipelineCacheData(
_device,
_pipelineCache,
&size,
dataPointer),
"vkGetPipelineCacheData");
}
var directory = Path.GetDirectoryName(_pipelineCachePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllBytes(_pipelineCachePath, data.AsSpan(0, (int)size).ToArray());
}
catch (Exception exception) when (
exception is IOException or UnauthorizedAccessException or InvalidOperationException)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache could not be saved: {exception.Message}");
}
}
private void CreateSwapchain()
{
Check(
_surfaceApi.GetPhysicalDeviceSurfaceCapabilities(_physicalDevice, _surface, out var capabilities),
"vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
uint formatCount = 0;
Check(
_surfaceApi.GetPhysicalDeviceSurfaceFormats(_physicalDevice, _surface, &formatCount, null),
"vkGetPhysicalDeviceSurfaceFormatsKHR");
var formats = new SurfaceFormatKHR[formatCount];
fixed (SurfaceFormatKHR* formatPointer = formats)
{
Check(
_surfaceApi.GetPhysicalDeviceSurfaceFormats(_physicalDevice, _surface, &formatCount, formatPointer),
"vkGetPhysicalDeviceSurfaceFormatsKHR");
}
var surfaceFormat = ChooseSurfaceFormat(formats);
_swapchainFormat = surfaceFormat.Format;
_extent = ChooseExtent(capabilities);
uint presentModeCount = 0;
Check(
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
_physicalDevice,
_surface,
&presentModeCount,
null),
"vkGetPhysicalDeviceSurfacePresentModesKHR");
var presentModes = new PresentModeKHR[presentModeCount];
fixed (PresentModeKHR* presentModePointer = presentModes)
{
Check(
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
_physicalDevice,
_surface,
&presentModeCount,
presentModePointer),
"vkGetPhysicalDeviceSurfacePresentModesKHR");
}
var presentMode = presentModes.Contains(PresentModeKHR.MailboxKhr)
? PresentModeKHR.MailboxKhr
: PresentModeKHR.FifoKhr;
Console.Error.WriteLine($"[LOADER][INFO] Vulkan present mode: {presentMode}");
var imageCount = capabilities.MinImageCount + 1;
if (capabilities.MaxImageCount != 0)
{
imageCount = Math.Min(imageCount, capabilities.MaxImageCount);
}
var compositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha);
var createInfo = new SwapchainCreateInfoKHR
{
SType = StructureType.SwapchainCreateInfoKhr,
Surface = _surface,
MinImageCount = imageCount,
ImageFormat = surfaceFormat.Format,
ImageColorSpace = surfaceFormat.ColorSpace,
ImageExtent = _extent,
ImageArrayLayers = 1,
ImageUsage =
ImageUsageFlags.TransferDstBit |
ImageUsageFlags.TransferSrcBit |
ImageUsageFlags.ColorAttachmentBit,
ImageSharingMode = SharingMode.Exclusive,
PreTransform = capabilities.CurrentTransform,
CompositeAlpha = compositeAlpha,
PresentMode = presentMode,
Clipped = true,
};
Check(_swapchainApi.CreateSwapchain(_device, &createInfo, null, out _swapchain), "vkCreateSwapchainKHR");
uint swapchainImageCount = 0;
Check(
_swapchainApi.GetSwapchainImages(_device, _swapchain, &swapchainImageCount, null),
"vkGetSwapchainImagesKHR");
_swapchainImages = new Image[swapchainImageCount];
fixed (Image* imagePointer = _swapchainImages)
{
Check(
_swapchainApi.GetSwapchainImages(_device, _swapchain, &swapchainImageCount, imagePointer),
"vkGetSwapchainImagesKHR");
}
_imageInitialized = new bool[swapchainImageCount];
}
private void CreateCommandResources()
{
var poolInfo = new CommandPoolCreateInfo
{
SType = StructureType.CommandPoolCreateInfo,
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
QueueFamilyIndex = _queueFamilyIndex,
};
Check(_vk.CreateCommandPool(_device, &poolInfo, null, out _commandPool), "vkCreateCommandPool");
var allocateInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
Level = CommandBufferLevel.Primary,
CommandBufferCount = 1,
};
Check(_vk.AllocateCommandBuffers(_device, &allocateInfo, out _commandBuffer), "vkAllocateCommandBuffers");
_presentationCommandBuffer = _commandBuffer;
var semaphoreInfo = new SemaphoreCreateInfo
{
SType = StructureType.SemaphoreCreateInfo,
};
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _imageAvailable), "vkCreateSemaphore");
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinished), "vkCreateSemaphore");
var fenceInfo = new FenceCreateInfo
{
SType = StructureType.FenceCreateInfo,
};
Check(
_vk.CreateFence(_device, &fenceInfo, null, out _presentationFence),
"vkCreateFence(presentation)");
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
}
private void CompletePendingPresentation(bool wait)
{
if (!_presentationInFlight)
{
return;
}
var fence = _presentationFence;
var result = wait
? _vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue)
: _vk.GetFenceStatus(_device, fence);
if (!wait && result == Result.NotReady)
{
return;
}
Check(result, "vkWaitForFences(presentation)");
_presentationInFlight = false;
if (_pendingPresentationResources is not null)
{
MarkSampledImagesInitialized(_pendingPresentationResources);
MarkStorageImagesInitialized(_pendingPresentationResources);
DestroyTranslatedDrawResources(_pendingPresentationResources);
_pendingPresentationResources = null;
}
}
private CommandBuffer AllocateGuestCommandBuffer()
{
var allocateInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
Level = CommandBufferLevel.Primary,
CommandBufferCount = 1,
};
CommandBuffer commandBuffer;
Check(
_vk.AllocateCommandBuffers(
_device,
&allocateInfo,
out commandBuffer),
"vkAllocateCommandBuffers(guest)");
return commandBuffer;
}
private void SubmitGuestCommandBuffer(
CommandBuffer commandBuffer,
TranslatedDrawResources resources,
IReadOnlyList<GuestImageResource> traceImages)
{
var fenceInfo = new FenceCreateInfo
{
SType = StructureType.FenceCreateInfo,
};
Fence fence;
Check(
_vk.CreateFence(_device, &fenceInfo, null, out fence),
"vkCreateFence(guest)");
try
{
var submitInfo = new SubmitInfo
{
SType = StructureType.SubmitInfo,
CommandBufferCount = 1,
PCommandBuffers = &commandBuffer,
};
Check(
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
"vkQueueSubmit(guest)");
}
catch
{
_vk.DestroyFence(_device, fence, null);
throw;
}
_pendingGuestSubmissions.Enqueue(
new PendingGuestSubmission(
fence,
commandBuffer,
resources,
traceImages,
resources.DebugName));
}
private void SubmitGuestCommandBufferAndWait(CommandBuffer commandBuffer)
{
var fenceInfo = new FenceCreateInfo
{
SType = StructureType.FenceCreateInfo,
};
Fence fence;
Check(
_vk.CreateFence(_device, &fenceInfo, null, out fence),
"vkCreateFence(guest chunk)");
try
{
var submitInfo = new SubmitInfo
{
SType = StructureType.SubmitInfo,
CommandBufferCount = 1,
PCommandBuffers = &commandBuffer,
};
Check(
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
"vkQueueSubmit(guest chunk)");
Check(
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
"vkWaitForFences(guest chunk)");
}
finally
{
_vk.DestroyFence(_device, fence, null);
}
_vk.FreeCommandBuffers(_device, _commandPool, 1, &commandBuffer);
}
private void EnsureGuestSubmissionCapacity()
{
CollectCompletedGuestSubmissions(waitForOldest: false);
if (_pendingGuestSubmissions.Count >= MaxInFlightGuestSubmissions)
{
CollectCompletedGuestSubmissions(waitForOldest: true);
}
}
private void CollectCompletedGuestSubmissions(bool waitForOldest)
{
if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest))
{
var fence = oldest.Fence;
var result = _vk.WaitForFences(
_device,
1,
&fence,
true,
ulong.MaxValue);
Check(result, $"vkWaitForFences(guest: {oldest.DebugName})");
}
while (_pendingGuestSubmissions.TryPeek(out var submission))
{
var status = _vk.GetFenceStatus(_device, submission.Fence);
if (status == Result.NotReady)
{
break;
}
Check(status, $"vkGetFenceStatus(guest: {submission.DebugName})");
_pendingGuestSubmissions.Dequeue();
foreach (var image in submission.TraceImages)
{
TraceGuestImageContents(image);
}
DestroyTranslatedDrawResources(submission.Resources);
var commandBuffer = submission.CommandBuffer;
_vk.FreeCommandBuffers(
_device,
_commandPool,
1,
&commandBuffer);
_vk.DestroyFence(_device, submission.Fence, null);
}
}
private IReadOnlyList<GuestImageResource> GetTraceImages(
TranslatedDrawResources resources,
GuestImageResource? renderTarget = null)
{
var images = new HashSet<GuestImageResource>();
if (renderTarget is not null &&
ShouldTraceGuestImageContents(renderTarget))
{
images.Add(renderTarget);
}
foreach (var texture in resources.Textures)
{
if (texture.IsStorage &&
texture.GuestImage is { } image &&
ShouldTraceGuestImageContents(image))
{
images.Add(image);
}
}
return images.ToArray();
}
private void CreateGuestDrawResources()
{
var colorAttachment = new AttachmentDescription
{
Format = _swapchainFormat,
Samples = SampleCountFlags.Count1Bit,
LoadOp = AttachmentLoadOp.Clear,
StoreOp = AttachmentStoreOp.Store,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare,
InitialLayout = ImageLayout.Undefined,
FinalLayout = ImageLayout.PresentSrcKhr,
};
var colorReference = new AttachmentReference
{
Attachment = 0,
Layout = ImageLayout.ColorAttachmentOptimal,
};
var subpass = new SubpassDescription
{
PipelineBindPoint = PipelineBindPoint.Graphics,
ColorAttachmentCount = 1,
PColorAttachments = &colorReference,
};
var dependency = new SubpassDependency
{
SrcSubpass = Vk.SubpassExternal,
DstSubpass = 0,
SrcStageMask = PipelineStageFlags.ColorAttachmentOutputBit,
DstStageMask = PipelineStageFlags.ColorAttachmentOutputBit,
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
};
var renderPassInfo = new RenderPassCreateInfo
{
SType = StructureType.RenderPassCreateInfo,
AttachmentCount = 1,
PAttachments = &colorAttachment,
SubpassCount = 1,
PSubpasses = &subpass,
DependencyCount = 1,
PDependencies = &dependency,
};
Check(_vk.CreateRenderPass(_device, &renderPassInfo, null, out _renderPass), "vkCreateRenderPass");
_swapchainImageViews = new ImageView[_swapchainImages.Length];
_framebuffers = new Framebuffer[_swapchainImages.Length];
for (var index = 0; index < _swapchainImages.Length; index++)
{
var viewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = _swapchainImages[index],
ViewType = ImageViewType.Type2D,
Format = _swapchainFormat,
Components = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity),
SubresourceRange = ColorSubresourceRange(),
};
Check(
_vk.CreateImageView(_device, &viewInfo, null, out _swapchainImageViews[index]),
"vkCreateImageView");
var imageView = _swapchainImageViews[index];
var framebufferInfo = new FramebufferCreateInfo
{
SType = StructureType.FramebufferCreateInfo,
RenderPass = _renderPass,
AttachmentCount = 1,
PAttachments = &imageView,
Width = _extent.Width,
Height = _extent.Height,
Layers = 1,
};
Check(
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out _framebuffers[index]),
"vkCreateFramebuffer");
}
var layoutInfo = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
};
Check(
_vk.CreatePipelineLayout(_device, &layoutInfo, null, out _pipelineLayout),
"vkCreatePipelineLayout");
CreateBarycentricPipeline();
}
private void CreateBarycentricPipeline()
{
var vertexBytes = Convert.FromBase64String(FullscreenBarycentricVertexSpirv);
var fragmentBytes = Convert.FromBase64String(FullscreenBarycentricFragmentSpirv);
var vertexModule = CreateShaderModule(vertexBytes);
var fragmentModule = CreateShaderModule(fragmentBytes);
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
try
{
var shaderStages = stackalloc PipelineShaderStageCreateInfo[2];
shaderStages[0] = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.VertexBit,
Module = vertexModule,
PName = entryPoint,
};
shaderStages[1] = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.FragmentBit,
Module = fragmentModule,
PName = entryPoint,
};
var vertexInput = new PipelineVertexInputStateCreateInfo
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
};
var inputAssembly = new PipelineInputAssemblyStateCreateInfo
{
SType = StructureType.PipelineInputAssemblyStateCreateInfo,
Topology = PrimitiveTopology.TriangleList,
};
var viewport = new Viewport(0, 0, _extent.Width, _extent.Height, 0, 1);
var scissor = new Rect2D(new Offset2D(0, 0), _extent);
var viewportState = new PipelineViewportStateCreateInfo
{
SType = StructureType.PipelineViewportStateCreateInfo,
ViewportCount = 1,
PViewports = &viewport,
ScissorCount = 1,
PScissors = &scissor,
};
var rasterization = new PipelineRasterizationStateCreateInfo
{
SType = StructureType.PipelineRasterizationStateCreateInfo,
PolygonMode = PolygonMode.Fill,
CullMode = CullModeFlags.None,
FrontFace = FrontFace.CounterClockwise,
LineWidth = 1,
};
var multisample = new PipelineMultisampleStateCreateInfo
{
SType = StructureType.PipelineMultisampleStateCreateInfo,
RasterizationSamples = SampleCountFlags.Count1Bit,
};
var colorBlendAttachment = new PipelineColorBlendAttachmentState
{
ColorWriteMask =
ColorComponentFlags.RBit |
ColorComponentFlags.GBit |
ColorComponentFlags.BBit |
ColorComponentFlags.ABit,
};
var colorBlend = new PipelineColorBlendStateCreateInfo
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
AttachmentCount = 1,
PAttachments = &colorBlendAttachment,
};
var pipelineInfo = new GraphicsPipelineCreateInfo
{
SType = StructureType.GraphicsPipelineCreateInfo,
StageCount = 2,
PStages = shaderStages,
PVertexInputState = &vertexInput,
PInputAssemblyState = &inputAssembly,
PViewportState = &viewportState,
PRasterizationState = &rasterization,
PMultisampleState = &multisample,
PColorBlendState = &colorBlend,
Layout = _pipelineLayout,
RenderPass = _renderPass,
Subpass = 0,
};
Check(
_vk.CreateGraphicsPipelines(
_device,
_pipelineCache,
1,
&pipelineInfo,
null,
out _barycentricPipeline),
"vkCreateGraphicsPipelines");
}
finally
{
SilkMarshal.Free((nint)entryPoint);
_vk.DestroyShaderModule(_device, fragmentModule, null);
_vk.DestroyShaderModule(_device, vertexModule, null);
}
}
private ShaderModule CreateShaderModule(byte[] code)
{
fixed (byte* codePointer = code)
{
var createInfo = new ShaderModuleCreateInfo
{
SType = StructureType.ShaderModuleCreateInfo,
CodeSize = (nuint)code.Length,
PCode = (uint*)codePointer,
};
Check(
_vk.CreateShaderModule(_device, &createInfo, null, out var module),
"vkCreateShaderModule");
return module;
}
}
private TranslatedDrawResources CreateTranslatedDrawResources(
VulkanTranslatedGuestDraw draw,
RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent)
{
var vertexSpirv = draw.VertexSpirv;
if (vertexSpirv.Length == 0 &&
!TryCompileFullscreenVertexShader(
draw.AttributeCount,
out vertexSpirv,
out var vertexError))
{
throw new InvalidOperationException($"translated vertex shader failed: {vertexError}");
}
var resources = new TranslatedDrawResources
{
DebugName = "SharpEmu draw",
Textures = new TextureResource[draw.Textures.Count],
GlobalMemoryBuffers =
new GlobalBufferResource[draw.GlobalMemoryBuffers.Count],
VertexBuffers = new VertexBufferResource[draw.VertexBuffers.Count],
VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer),
InstanceCount = Math.Max(draw.InstanceCount, 1),
Topology = GetPrimitiveTopology(draw.PrimitiveType),
Blend = draw.RenderState.Blend,
Scissor = draw.RenderState.Scissor,
Viewport = draw.RenderState.Viewport,
};
try
{
foreach (var texture in draw.Textures)
{
if (texture.IsStorage)
{
_ = ResolveStorageGuestImage(texture);
}
}
for (var index = 0; index < draw.Textures.Count; index++)
{
resources.Textures[index] = ResolveTextureResource(draw.Textures[index]);
}
for (var index = 0; index < draw.GlobalMemoryBuffers.Count; index++)
{
resources.GlobalMemoryBuffers[index] =
CreateGlobalBufferResource(draw.GlobalMemoryBuffers[index]);
}
for (var index = 0; index < draw.VertexBuffers.Count; index++)
{
resources.VertexBuffers[index] =
CreateVertexBufferResource(draw.VertexBuffers[index]);
}
if (draw.IndexBuffer is { Data.Length: > 0 } indexBuffer)
{
resources.IndexBuffer = CreateHostBuffer(
indexBuffer.Data,
BufferUsageFlags.IndexBufferBit,
out resources.IndexMemory);
resources.Index32Bit = indexBuffer.Is32Bit;
}
CreateTranslatedDescriptorResources(
resources,
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit);
CreateTranslatedPipeline(
resources,
vertexSpirv,
draw.PixelSpirv,
renderPass,
renderTargetFormat,
extent);
return resources;
}
catch
{
DestroyTranslatedDrawResources(resources);
throw;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TranslatedDrawResources CreateComputeDispatchResources(
VulkanComputeGuestDispatch dispatch)
{
var traceResources = dispatch.Textures.Count >= 8;
if (traceResources)
{
TraceVulkanShader(
$"vk.compute_resources begin groups={dispatch.GroupCountX}x" +
$"{dispatch.GroupCountY}x{dispatch.GroupCountZ} textures={dispatch.Textures.Count}");
}
var resources = new TranslatedDrawResources
{
DebugName = BuildComputeDebugName(dispatch),
Textures = new TextureResource[dispatch.Textures.Count],
GlobalMemoryBuffers =
new GlobalBufferResource[dispatch.GlobalMemoryBuffers.Count],
};
try
{
for (var index = 0; index < dispatch.Textures.Count; index++)
{
var texture = dispatch.Textures[index];
if (texture.IsStorage)
{
if (traceResources)
{
TraceVulkanShader(
$"vk.compute_resources storage[{index}] begin " +
$"addr=0x{texture.Address:X16} fmt={texture.Format} " +
$"size={texture.Width}x{texture.Height} " +
$"mips={texture.MipLevels} level={texture.MipLevel}");
}
_ = ResolveStorageImageResource(texture);
if (traceResources)
{
TraceVulkanShader($"vk.compute_resources storage[{index}] ready");
}
}
}
if (traceResources)
{
TraceVulkanShader("vk.compute_resources resolve begin");
}
for (var index = 0; index < dispatch.Textures.Count; index++)
{
resources.Textures[index] =
ResolveTextureResource(dispatch.Textures[index]);
}
if (traceResources)
{
TraceVulkanShader("vk.compute_resources resolve ready");
}
for (var index = 0; index < dispatch.GlobalMemoryBuffers.Count; index++)
{
resources.GlobalMemoryBuffers[index] =
CreateGlobalBufferResource(dispatch.GlobalMemoryBuffers[index]);
}
if (traceResources)
{
TraceVulkanShader("vk.compute_resources descriptors begin");
}
CreateTranslatedDescriptorResources(resources, ShaderStageFlags.ComputeBit);
if (traceResources)
{
TraceVulkanShader("vk.compute_resources descriptors ready");
}
if (traceResources)
{
TraceVulkanShader(
$"vk.compute_resources pipeline begin " +
$"cs=0x{dispatch.ShaderAddress:X16} " +
$"spirv={dispatch.ComputeSpirv.Length} " +
$"textures={resources.Textures.Length} " +
$"globals={resources.GlobalMemoryBuffers.Length}");
}
CreateComputePipeline(resources, dispatch.ComputeSpirv);
if (traceResources)
{
TraceVulkanShader("vk.compute_resources pipeline ready");
}
return resources;
}
catch
{
DestroyTranslatedDrawResources(resources);
throw;
}
}
private static bool TryCompileFullscreenVertexShader(
uint attributeCount,
out byte[] spirv,
out string error)
{
spirv = [];
error = string.Empty;
if (attributeCount > 32)
{
error = $"too many interpolated attributes: {attributeCount}";
return false;
}
spirv = SpirvFixedShaders.CreateFullscreenVertex(attributeCount);
return true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void CreateTranslatedDescriptorResources(
TranslatedDrawResources resources,
ShaderStageFlags stageFlags)
{
var textureCount = resources.Textures.Length;
var sampledImageCount = resources.Textures.Count(texture => !texture.IsStorage);
var storageImageCount = textureCount - sampledImageCount;
var globalBufferCount = resources.GlobalMemoryBuffers.Length;
var bindingCount = textureCount + (globalBufferCount == 0 ? 0 : 1);
var layout = GetOrCreateDescriptorLayout(resources, stageFlags, bindingCount);
resources.DescriptorSetLayout = layout.DescriptorSetLayout;
resources.PipelineLayout = layout.PipelineLayout;
resources.DescriptorLayoutCached = true;
if (bindingCount == 0)
{
return;
}
var setLayout = layout.DescriptorSetLayout;
var poolSizes = new DescriptorPoolSize[
(sampledImageCount == 0 ? 0 : 1) +
(storageImageCount == 0 ? 0 : 1) +
(globalBufferCount == 0 ? 0 : 1)];
var poolSizeIndex = 0;
if (sampledImageCount != 0)
{
poolSizes[poolSizeIndex++] = new DescriptorPoolSize
{
Type = DescriptorType.CombinedImageSampler,
DescriptorCount = (uint)sampledImageCount,
};
}
if (storageImageCount != 0)
{
poolSizes[poolSizeIndex++] = new DescriptorPoolSize
{
Type = DescriptorType.StorageImage,
DescriptorCount = (uint)storageImageCount,
};
}
if (globalBufferCount != 0)
{
poolSizes[poolSizeIndex] = new DescriptorPoolSize
{
Type = DescriptorType.StorageBuffer,
DescriptorCount = (uint)globalBufferCount,
};
}
fixed (DescriptorPoolSize* poolSizePointer = poolSizes)
{
var poolInfo = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
MaxSets = 1,
PoolSizeCount = (uint)poolSizes.Length,
PPoolSizes = poolSizePointer,
};
DescriptorPool descriptorPool;
Check(
_vk.CreateDescriptorPool(
_device,
&poolInfo,
null,
out descriptorPool),
"vkCreateDescriptorPool");
resources.DescriptorPool = descriptorPool;
}
var allocateInfo = new DescriptorSetAllocateInfo
{
SType = StructureType.DescriptorSetAllocateInfo,
DescriptorPool = resources.DescriptorPool,
DescriptorSetCount = 1,
PSetLayouts = &setLayout,
};
DescriptorSet descriptorSet;
Check(
_vk.AllocateDescriptorSets(_device, &allocateInfo, out descriptorSet),
"vkAllocateDescriptorSets");
resources.DescriptorSet = descriptorSet;
var imageInfos = new DescriptorImageInfo[textureCount];
var bufferInfos = new DescriptorBufferInfo[globalBufferCount];
var writes = new WriteDescriptorSet[bindingCount];
fixed (DescriptorImageInfo* imageInfoPointer = imageInfos)
fixed (DescriptorBufferInfo* bufferInfoPointer = bufferInfos)
fixed (WriteDescriptorSet* writePointer = writes)
{
var writeIndex = 0;
if (globalBufferCount != 0)
{
for (var index = 0; index < globalBufferCount; index++)
{
bufferInfoPointer[index] = new DescriptorBufferInfo
{
Buffer = resources.GlobalMemoryBuffers[index].Buffer,
Offset = 0,
Range = resources.GlobalMemoryBuffers[index].Size,
};
}
writePointer[writeIndex++] = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = resources.DescriptorSet,
DstBinding = 0,
DescriptorCount = (uint)globalBufferCount,
DescriptorType = DescriptorType.StorageBuffer,
PBufferInfo = bufferInfoPointer,
};
}
for (var index = 0; index < textureCount; index++)
{
var isStorage = resources.Textures[index].IsStorage;
if (!isStorage &&
resources.Textures[index].Sampler.Handle == 0)
{
resources.Textures[index].Sampler =
CreateSampler(resources.Textures[index].SamplerState);
}
imageInfoPointer[index] = new DescriptorImageInfo
{
Sampler = isStorage ? default : resources.Textures[index].Sampler,
ImageView = resources.Textures[index].View,
ImageLayout = isStorage ||
resources.Textures[index].GuestImage is { } guestImage &&
resources.Textures.Any(
texture =>
texture.IsStorage &&
texture.GuestImage == guestImage)
? ImageLayout.General
: ImageLayout.ShaderReadOnlyOptimal,
};
writePointer[writeIndex++] = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = resources.DescriptorSet,
DstBinding = (uint)(index + 1),
DescriptorCount = 1,
DescriptorType = isStorage
? DescriptorType.StorageImage
: DescriptorType.CombinedImageSampler,
PImageInfo = &imageInfoPointer[index],
};
}
_vk.UpdateDescriptorSets(
_device,
(uint)bindingCount,
writePointer,
0,
null);
}
}
private void CreateTranslatedPipeline(
TranslatedDrawResources resources,
byte[] vertexSpirv,
byte[] fragmentSpirv,
RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent)
{
var pipelineKey = new GraphicsPipelineKey(
GetShaderDigest(vertexSpirv),
GetShaderDigest(fragmentSpirv),
renderTargetFormat,
resources.Topology,
resources.Blend,
GetResourceLayoutKey(resources),
GetVertexLayoutKey(resources));
if (_graphicsPipelines.TryGetValue(pipelineKey, out var cachedPipeline))
{
resources.Pipeline = cachedPipeline;
resources.PipelineCached = true;
return;
}
var vertexModule = CreateShaderModule(vertexSpirv);
var fragmentModule = CreateShaderModule(fragmentSpirv);
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
try
{
var shaderStages = stackalloc PipelineShaderStageCreateInfo[2];
shaderStages[0] = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.VertexBit,
Module = vertexModule,
PName = entryPoint,
};
shaderStages[1] = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.FragmentBit,
Module = fragmentModule,
PName = entryPoint,
};
var vertexBindingDescriptions =
new VertexInputBindingDescription[resources.VertexBuffers.Length];
var vertexAttributeDescriptions =
new VertexInputAttributeDescription[resources.VertexBuffers.Length];
for (var index = 0; index < resources.VertexBuffers.Length; index++)
{
var vertexBuffer = resources.VertexBuffers[index];
vertexBindingDescriptions[index] = new VertexInputBindingDescription
{
Binding = (uint)index,
Stride = vertexBuffer.Stride == 0
? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float)
: vertexBuffer.Stride,
InputRate = VertexInputRate.Vertex,
};
vertexAttributeDescriptions[index] = new VertexInputAttributeDescription
{
Location = vertexBuffer.Location,
Binding = (uint)index,
Format = ToVkVertexFormat(
vertexBuffer.DataFormat,
vertexBuffer.NumberFormat,
vertexBuffer.ComponentCount),
Offset = 0,
};
}
fixed (VertexInputBindingDescription* vertexBindingPointerBase = vertexBindingDescriptions)
fixed (VertexInputAttributeDescription* vertexAttributePointerBase = vertexAttributeDescriptions)
{
var vertexInput = new PipelineVertexInputStateCreateInfo
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
VertexBindingDescriptionCount = (uint)vertexBindingDescriptions.Length,
PVertexBindingDescriptions = vertexBindingDescriptions.Length == 0
? null
: vertexBindingPointerBase,
VertexAttributeDescriptionCount = (uint)vertexAttributeDescriptions.Length,
PVertexAttributeDescriptions = vertexAttributeDescriptions.Length == 0
? null
: vertexAttributePointerBase,
};
var inputAssembly = new PipelineInputAssemblyStateCreateInfo
{
SType = StructureType.PipelineInputAssemblyStateCreateInfo,
Topology = resources.Topology,
};
var viewport = new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
var scissor = new Rect2D(new Offset2D(0, 0), extent);
var viewportState = new PipelineViewportStateCreateInfo
{
SType = StructureType.PipelineViewportStateCreateInfo,
ViewportCount = 1,
PViewports = &viewport,
ScissorCount = 1,
PScissors = &scissor,
};
var rasterization = new PipelineRasterizationStateCreateInfo
{
SType = StructureType.PipelineRasterizationStateCreateInfo,
PolygonMode = PolygonMode.Fill,
CullMode = CullModeFlags.None,
FrontFace = FrontFace.CounterClockwise,
LineWidth = 1,
};
var multisample = new PipelineMultisampleStateCreateInfo
{
SType = StructureType.PipelineMultisampleStateCreateInfo,
RasterizationSamples = SampleCountFlags.Count1Bit,
};
var colorBlendAttachment = new PipelineColorBlendAttachmentState
{
BlendEnable = resources.Blend.Enable,
SrcColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorSrcFactor),
DstColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorDstFactor),
ColorBlendOp = ToVkBlendOp(resources.Blend.ColorFunc),
SrcAlphaBlendFactor = resources.Blend.SeparateAlphaBlend
? ToVkBlendFactor(resources.Blend.AlphaSrcFactor)
: ToVkBlendFactor(resources.Blend.ColorSrcFactor),
DstAlphaBlendFactor = resources.Blend.SeparateAlphaBlend
? ToVkBlendFactor(resources.Blend.AlphaDstFactor)
: ToVkBlendFactor(resources.Blend.ColorDstFactor),
AlphaBlendOp = resources.Blend.SeparateAlphaBlend
? ToVkBlendOp(resources.Blend.AlphaFunc)
: ToVkBlendOp(resources.Blend.ColorFunc),
ColorWriteMask =
ToVkColorWriteMask(resources.Blend.WriteMask),
};
var colorBlend = new PipelineColorBlendStateCreateInfo
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
AttachmentCount = 1,
PAttachments = &colorBlendAttachment,
};
var dynamicStateValues = stackalloc DynamicState[2];
dynamicStateValues[0] = DynamicState.Viewport;
dynamicStateValues[1] = DynamicState.Scissor;
var dynamicState = new PipelineDynamicStateCreateInfo
{
SType = StructureType.PipelineDynamicStateCreateInfo,
DynamicStateCount = 2,
PDynamicStates = dynamicStateValues,
};
var pipelineInfo = new GraphicsPipelineCreateInfo
{
SType = StructureType.GraphicsPipelineCreateInfo,
StageCount = 2,
PStages = shaderStages,
PVertexInputState = &vertexInput,
PInputAssemblyState = &inputAssembly,
PViewportState = &viewportState,
PRasterizationState = &rasterization,
PMultisampleState = &multisample,
PColorBlendState = &colorBlend,
PDynamicState = &dynamicState,
Layout = resources.PipelineLayout,
RenderPass = renderPass,
Subpass = 0,
};
Pipeline pipeline;
Check(
_vk.CreateGraphicsPipelines(
_device,
_pipelineCache,
1,
&pipelineInfo,
null,
out pipeline),
"vkCreateGraphicsPipelines(translated)");
resources.Pipeline = pipeline;
resources.PipelineCached = true;
_graphicsPipelines.Add(pipelineKey, pipeline);
SetDebugName(
ObjectType.Pipeline,
pipeline.Handle,
$"SharpEmu graphics ps={fragmentSpirv.Length}b attrs={resources.Textures.Length}");
}
}
finally
{
SilkMarshal.Free((nint)entryPoint);
_vk.DestroyShaderModule(_device, fragmentModule, null);
_vk.DestroyShaderModule(_device, vertexModule, null);
}
}
private DescriptorLayoutBundle GetOrCreateDescriptorLayout(
TranslatedDrawResources resources,
ShaderStageFlags stageFlags,
int bindingCount)
{
var key = new DescriptorLayoutKey(stageFlags, GetResourceLayoutKey(resources));
if (_descriptorLayouts.TryGetValue(key, out var cached))
{
return cached;
}
DescriptorSetLayout descriptorSetLayout = default;
if (bindingCount != 0)
{
var bindings = new DescriptorSetLayoutBinding[bindingCount];
var bindingOffset = 0;
if (resources.GlobalMemoryBuffers.Length != 0)
{
bindings[bindingOffset++] = new DescriptorSetLayoutBinding
{
Binding = 0,
DescriptorType = DescriptorType.StorageBuffer,
DescriptorCount = (uint)resources.GlobalMemoryBuffers.Length,
StageFlags = stageFlags,
};
}
for (var index = 0; index < resources.Textures.Length; index++)
{
bindings[bindingOffset + index] = new DescriptorSetLayoutBinding
{
Binding = (uint)(index + 1),
DescriptorType = resources.Textures[index].IsStorage
? DescriptorType.StorageImage
: DescriptorType.CombinedImageSampler,
DescriptorCount = 1,
StageFlags = stageFlags,
};
}
fixed (DescriptorSetLayoutBinding* bindingPointer = bindings)
{
var descriptorInfo = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
BindingCount = (uint)bindings.Length,
PBindings = bindingPointer,
};
Check(
_vk.CreateDescriptorSetLayout(
_device,
&descriptorInfo,
null,
out descriptorSetLayout),
"vkCreateDescriptorSetLayout");
}
}
var pipelineInfo = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
};
if (descriptorSetLayout.Handle != 0)
{
pipelineInfo.SetLayoutCount = 1;
pipelineInfo.PSetLayouts = &descriptorSetLayout;
}
PipelineLayout pipelineLayout;
Check(
_vk.CreatePipelineLayout(
_device,
&pipelineInfo,
null,
out pipelineLayout),
"vkCreatePipelineLayout");
var created = new DescriptorLayoutBundle(descriptorSetLayout, pipelineLayout);
_descriptorLayouts.Add(key, created);
return created;
}
private string GetShaderDigest(byte[] spirv)
{
if (_shaderDigests.TryGetValue(spirv, out var digest))
{
return digest;
}
digest = Convert.ToHexString(SHA256.HashData(spirv));
_shaderDigests.Add(spirv, digest);
return digest;
}
private static string GetResourceLayoutKey(TranslatedDrawResources resources)
{
var key = new StringBuilder();
key.Append(resources.GlobalMemoryBuffers.Length).Append(':');
foreach (var texture in resources.Textures)
{
key.Append(texture.IsStorage ? 'S' : 'T');
}
return key.ToString();
}
private static string GetVertexLayoutKey(TranslatedDrawResources resources)
{
var key = new StringBuilder();
foreach (var buffer in resources.VertexBuffers)
{
key.Append(buffer.Location).Append(',')
.Append(buffer.ComponentCount).Append(',')
.Append(buffer.DataFormat).Append(',')
.Append(buffer.NumberFormat).Append(',')
.Append(buffer.Stride == 0
? Math.Max(buffer.ComponentCount, 1) * sizeof(float)
: buffer.Stride)
.Append(';');
}
return key.ToString();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void CreateComputePipeline(
TranslatedDrawResources resources,
byte[] computeSpirv)
{
if (_computePipelines.TryGetValue(computeSpirv, out var cachedPipeline))
{
resources.Pipeline = cachedPipeline;
resources.PipelineCached = true;
return;
}
var computeModule = CreateShaderModule(computeSpirv);
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
try
{
var stage = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.ComputeBit,
Module = computeModule,
PName = entryPoint,
};
var pipelineInfo = new ComputePipelineCreateInfo
{
SType = StructureType.ComputePipelineCreateInfo,
Flags = PipelineCreateFlags.CreateDispatchBaseBit,
Stage = stage,
Layout = resources.PipelineLayout,
};
Pipeline pipeline;
Check(
_vk.CreateComputePipelines(
_device,
_pipelineCache,
1,
&pipelineInfo,
null,
out pipeline),
"vkCreateComputePipelines(translated)");
resources.Pipeline = pipeline;
resources.PipelineCached = true;
SetDebugName(
ObjectType.Pipeline,
pipeline.Handle,
$"SharpEmu compute cs={computeSpirv.Length}b");
_computePipelines.Add(computeSpirv, pipeline);
}
finally
{
SilkMarshal.Free((nint)entryPoint);
_vk.DestroyShaderModule(_device, computeModule, null);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TextureResource ResolveTextureResource(VulkanGuestDrawTexture texture)
{
if (texture.IsStorage)
{
return ResolveStorageImageResource(texture);
}
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
if (texture.Address != 0 &&
_guestImages.TryGetValue(texture.Address, out var guestImage) &&
IsCompatibleGuestImageAlias(texture, guestImage) &&
IsCompatibleViewFormat(guestImage.Format, vkFormat) &&
TryGetOrCreateGuestImageView(
guestImage,
vkFormat,
mipLevel: 0,
levelCount: guestImage.MipLevels,
dstSelect: texture.DstSelect,
out var view))
{
if (ShouldTraceVulkanResources() &&
_tracedTextureCacheHits.Add(
(texture.Address, texture.Width, texture.Height, vkFormat)))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.texture_cache_hit addr=0x{texture.Address:X16} " +
$"size={texture.Width}x{texture.Height} " +
$"image_format={guestImage.Format} view_format={vkFormat}");
}
if (guestImage.Width != texture.Width ||
guestImage.Height != texture.Height)
{
TraceVulkanShader(
$"vk.texture_cache_alias addr=0x{texture.Address:X16} " +
$"texture={texture.Width}x{texture.Height} " +
$"image={guestImage.Width}x{guestImage.Height} " +
$"tile={texture.TileMode} format={vkFormat}");
}
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
{
return refresh;
}
return new TextureResource
{
Address = texture.Address,
Image = guestImage.Image,
View = view,
Width = guestImage.Width,
Height = guestImage.Height,
RowLength = guestImage.Width,
DstSelect = texture.DstSelect,
SamplerState = texture.Sampler,
GuestImage = guestImage,
};
}
return CreateTextureResource(texture);
}
private bool TryCreateCpuTextureRefreshResource(
VulkanGuestDrawTexture texture,
GuestImageResource guestImage,
ImageView view,
out TextureResource resource)
{
resource = default!;
if (!guestImage.IsCpuBacked ||
guestImage.Width != texture.Width ||
guestImage.Height != texture.Height ||
guestImage.MipLevels != 1 ||
texture.RgbaPixels.Length == 0)
{
return false;
}
var rowLength = texture.TileMode == 0
? Math.Max(texture.Pitch, texture.Width)
: texture.Width;
var expectedSize = GetTextureByteCount(texture.Format, rowLength, texture.Height);
if (expectedSize == 0 || expectedSize > int.MaxValue)
{
return false;
}
var pixels = texture.RgbaPixels.Length == (int)expectedSize
? texture.RgbaPixels
: CreateFallbackTexturePixels(texture.Format, rowLength, texture.Height, expectedSize);
var fingerprint = ComputeTextureContentFingerprint(pixels);
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
guestImage.CpuContentFingerprint == fingerprint)
{
return false;
}
var uploadPixels = texture.Format == 13
? ExpandRgb32Pixels(pixels)
: pixels;
var debugName = TextureDebugName(texture, guestImage.Format);
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
uploadPixels,
$"{debugName} refresh staging");
TraceVulkanShader(
$"vk.texture_refresh addr=0x{texture.Address:X16} " +
$"size={texture.Width}x{texture.Height} bytes={uploadPixels.Length}");
resource = new TextureResource
{
Address = texture.Address,
StagingBuffer = stagingBuffer,
StagingMemory = stagingMemory,
Image = guestImage.Image,
View = view,
Width = guestImage.Width,
Height = guestImage.Height,
RowLength = rowLength,
DstSelect = texture.DstSelect,
NeedsUpload = true,
SamplerState = texture.Sampler,
GuestImage = guestImage,
CpuContentFingerprint = fingerprint,
UpdatesCpuContent = true,
};
return true;
}
private static bool IsCompatibleGuestImageAlias(
VulkanGuestDrawTexture texture,
GuestImageResource guestImage)
{
if (guestImage.Width == texture.Width &&
guestImage.Height == texture.Height)
{
return true;
}
if (texture.TileMode == 0 ||
texture.Width == 0 ||
texture.Height == 0)
{
return false;
}
return texture.Width <= guestImage.Width &&
texture.Height <= guestImage.Height;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture)
{
if (texture.Address == 0)
{
return CreateStorageScratchResource(texture);
}
var guestImage = ResolveStorageGuestImage(texture);
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
var view = GetOrCreateGuestImageView(
guestImage,
vkFormat,
texture.MipLevel,
levelCount: 1);
var resource = new TextureResource
{
Address = texture.Address,
Image = guestImage.Image,
View = view,
Width = guestImage.Width,
Height = guestImage.Height,
RowLength = guestImage.Width,
DstSelect = texture.DstSelect,
IsStorage = true,
SamplerState = texture.Sampler,
GuestImage = guestImage,
};
if (!guestImage.Initialized &&
!guestImage.InitialUploadPending &&
texture.MipLevel == 0)
{
var expectedSize = GetTextureByteCount(
texture.Format,
texture.Width,
texture.Height);
if ((ulong)texture.RgbaPixels.Length == expectedSize &&
texture.RgbaPixels.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
{
resource.StagingBuffer = CreateBuffer(
expectedSize,
BufferUsageFlags.TransferSrcBit,
MemoryPropertyFlags.HostVisibleBit |
MemoryPropertyFlags.HostCoherentBit,
out resource.StagingMemory);
void* mapped;
Check(
_vk.MapMemory(
_device,
resource.StagingMemory,
0,
expectedSize,
0,
&mapped),
"vkMapMemory(storage texture)");
fixed (byte* source = texture.RgbaPixels)
{
System.Buffer.MemoryCopy(
source,
mapped,
texture.RgbaPixels.Length,
texture.RgbaPixels.Length);
}
_vk.UnmapMemory(_device, resource.StagingMemory);
resource.NeedsUpload = true;
guestImage.InitialUploadPending = true;
TraceVulkanShader(
$"vk.storage_upload addr=0x{texture.Address:X16} " +
$"size={texture.Width}x{texture.Height} bytes={expectedSize}");
}
}
return resource;
}
private TextureResource CreateStorageScratchResource(VulkanGuestDrawTexture texture)
{
var width = Math.Max(texture.Width, 1);
var height = Math.Max(texture.Height, 1);
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
var imageInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.Type2D,
Format = vkFormat,
Extent = new Extent3D(width, height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.Count1Bit,
Tiling = ImageTiling.Optimal,
Usage =
ImageUsageFlags.SampledBit |
ImageUsageFlags.StorageBit |
ImageUsageFlags.TransferSrcBit |
ImageUsageFlags.TransferDstBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
};
Check(
_vk.CreateImage(_device, &imageInfo, null, out var image),
"vkCreateImage(storage scratch)");
_vk.GetImageMemoryRequirements(_device, image, out var requirements);
var allocationInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = requirements.Size,
MemoryTypeIndex = FindMemoryType(
requirements.MemoryTypeBits,
MemoryPropertyFlags.DeviceLocalBit),
};
Check(
_vk.AllocateMemory(_device, &allocationInfo, null, out var memory),
"vkAllocateMemory(storage scratch)");
Check(
_vk.BindImageMemory(_device, image, memory, 0),
"vkBindImageMemory(storage scratch)");
var viewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
ViewType = ImageViewType.Type2D,
Format = vkFormat,
Components = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity),
SubresourceRange = ColorSubresourceRange(),
};
Check(
_vk.CreateImageView(_device, &viewInfo, null, out var view),
"vkCreateImageView(storage scratch)");
SetDebugName(ObjectType.Image, image.Handle, $"SharpEmu scratch storage {width}x{height} {vkFormat}");
SetDebugName(ObjectType.ImageView, view.Handle, $"SharpEmu scratch storage {width}x{height} {vkFormat} view");
var guestImage = new GuestImageResource
{
Address = 0,
Width = width,
Height = height,
MipLevels = 1,
Format = vkFormat,
Image = image,
Memory = memory,
View = view,
};
return new TextureResource
{
Address = 0,
Image = image,
ImageMemory = memory,
View = view,
Width = width,
Height = height,
RowLength = width,
DstSelect = texture.DstSelect,
OwnsStorage = true,
IsStorage = true,
SamplerState = texture.Sampler,
GuestImage = guestImage,
};
}
private GuestImageResource ResolveStorageGuestImage(VulkanGuestDrawTexture texture)
{
if (texture.Address == 0)
{
throw new InvalidOperationException("Storage image has no guest address.");
}
var format = GetTextureFormat(texture.Format, texture.NumberType);
var guestImage = GetOrCreateGuestImage(
new VulkanGuestRenderTarget(
texture.Address,
texture.Width,
texture.Height,
texture.Format,
texture.NumberType,
texture.MipLevels),
format);
if (texture.MipLevel >= guestImage.MipLevels)
{
throw new InvalidOperationException(
$"Storage mip {texture.MipLevel} exceeds image mip count {guestImage.MipLevels}.");
}
return guestImage;
}
private TextureResource CreateTextureResource(VulkanGuestDrawTexture texture)
{
var width = Math.Max(texture.Width, 1);
var height = Math.Max(texture.Height, 1);
var rowLength = texture.TileMode == 0
? Math.Max(texture.Pitch, width)
: width;
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
var expectedSize = GetTextureByteCount(texture.Format, rowLength, height);
if (_tracedTextureUploads.Add((texture.Address, width, height, vkFormat)))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " +
$"fmt={texture.Format} num={texture.NumberType} vk={vkFormat} " +
$"size={width}x{height} row={rowLength} tile={texture.TileMode} " +
$"dst=0x{texture.DstSelect:X3} " +
$"bytes={texture.RgbaPixels.Length} expected={expectedSize}");
}
var pixels = texture.RgbaPixels.Length == (int)expectedSize
? texture.RgbaPixels
: CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize);
DumpTextureUpload(texture, pixels, rowLength, width, height);
var uploadPixels = texture.Format == 13
? ExpandRgb32Pixels(pixels)
: pixels;
var contentFingerprint = ComputeTextureContentFingerprint(pixels);
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
uploadPixels,
$"{TextureDebugName(texture, vkFormat)} staging");
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
var imageInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
Flags = supportsAttachmentUsage
? ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit
: 0,
ImageType = ImageType.Type2D,
Format = vkFormat,
Extent = new Extent3D(width, height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.Count1Bit,
Tiling = ImageTiling.Optimal,
Usage = supportsAttachmentUsage
? ImageUsageFlags.TransferDstBit |
ImageUsageFlags.SampledBit |
ImageUsageFlags.ColorAttachmentBit |
ImageUsageFlags.StorageBit |
ImageUsageFlags.TransferSrcBit
: ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
};
Check(_vk.CreateImage(_device, &imageInfo, null, out var image), "vkCreateImage(texture)");
_vk.GetImageMemoryRequirements(_device, image, out var imageRequirements);
var memoryInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = imageRequirements.Size,
MemoryTypeIndex = FindMemoryType(
imageRequirements.MemoryTypeBits,
MemoryPropertyFlags.DeviceLocalBit),
};
Check(_vk.AllocateMemory(_device, &memoryInfo, null, out var imageMemory), "vkAllocateMemory(texture)");
Check(_vk.BindImageMemory(_device, image, imageMemory, 0), "vkBindImageMemory(texture)");
var viewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
ViewType = ImageViewType.Type2D,
Format = vkFormat,
Components = ToVkComponentMapping(texture.DstSelect),
SubresourceRange = ColorSubresourceRange(),
};
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
var debugName = TextureDebugName(texture, vkFormat);
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
var resource = new TextureResource
{
Address = texture.Address,
StagingBuffer = stagingBuffer,
StagingMemory = stagingMemory,
Image = image,
ImageMemory = imageMemory,
View = view,
Width = width,
Height = height,
RowLength = rowLength,
DstSelect = texture.DstSelect,
NeedsUpload = true,
OwnsStorage = true,
SamplerState = texture.Sampler,
CpuContentFingerprint = contentFingerprint,
UpdatesCpuContent = texture.Address != 0,
};
if (texture.Address != 0 &&
!_guestImages.ContainsKey(texture.Address))
{
var guestImage = new GuestImageResource
{
Address = texture.Address,
Width = width,
Height = height,
MipLevels = 1,
Format = vkFormat,
Image = image,
Memory = imageMemory,
View = view,
InitialUploadPending = true,
IsCpuBacked = true,
CpuContentFingerprint = contentFingerprint,
};
_guestImages.Add(texture.Address, guestImage);
resource.OwnsStorage = false;
resource.GuestImage = guestImage;
lock (_gate)
{
var guestFormat = VulkanVideoPresenter.GetGuestTextureFormat(
texture.Format,
texture.NumberType);
if (guestFormat != 0)
{
_availableGuestImages[texture.Address] = guestFormat;
_gpuGuestImages.Remove(texture.Address);
}
}
}
return resource;
}
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(
byte[] pixels,
string debugName)
{
var size = (ulong)pixels.Length;
var buffer = CreateBuffer(
size,
BufferUsageFlags.TransferSrcBit,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out var memory);
void* mapped;
Check(_vk.MapMemory(_device, memory, 0, size, 0, &mapped), "vkMapMemory(texture)");
fixed (byte* source = pixels)
{
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
}
_vk.UnmapMemory(_device, memory);
SetDebugName(ObjectType.Buffer, buffer.Handle, debugName);
return (buffer, memory);
}
private static ulong ComputeTextureContentFingerprint(ReadOnlySpan<byte> pixels)
{
const ulong offsetBasis = 14695981039346656037;
const ulong prime = 1099511628211;
var h0 = offsetBasis ^ (ulong)pixels.Length;
var h1 = offsetBasis;
var h2 = offsetBasis;
var h3 = offsetBasis;
var words = MemoryMarshal.Cast<byte, ulong>(pixels);
var index = 0;
var blockEnd = words.Length - (words.Length & 3);
for (; index < blockEnd; index += 4)
{
h0 = (h0 ^ words[index]) * prime;
h1 = (h1 ^ words[index + 1]) * prime;
h2 = (h2 ^ words[index + 2]) * prime;
h3 = (h3 ^ words[index + 3]) * prime;
}
for (; index < words.Length; index++)
{
h0 = (h0 ^ words[index]) * prime;
}
var hash = h0;
hash = (hash ^ h1) * prime;
hash = (hash ^ h2) * prime;
hash = (hash ^ h3) * prime;
foreach (var value in pixels[(words.Length * sizeof(ulong))..])
{
hash = (hash ^ value) * prime;
}
return hash;
}
private void DumpTextureUpload(
VulkanGuestDrawTexture texture,
byte[] pixels,
uint rowLength,
uint width,
uint height)
{
if (!_dumpTextures ||
texture.IsFallback ||
texture.IsStorage ||
GetTextureBytesPerPixel(texture.Format) != 4 ||
width == 0 ||
height == 0 ||
!_dumpedTextures.Add((texture.Address, width, height, texture.Format)))
{
return;
}
var rowBytes = checked((int)rowLength * 4);
var visibleRowBytes = checked((int)width * 4);
if (pixels.Length < checked(rowBytes * (int)height))
{
return;
}
var directory = Path.Combine(AppContext.BaseDirectory, "texture-dumps");
Directory.CreateDirectory(directory);
var path = Path.Combine(
directory,
$"tex-{texture.Address:X16}-{width}x{height}-fmt{texture.Format}-row{rowLength}.bmp");
WriteRgbaBmp(path, pixels, rowBytes, visibleRowBytes, (int)width, (int)height);
}
private static void WriteRgbaBmp(
string path,
byte[] rgba,
int sourceRowBytes,
int visibleRowBytes,
int width,
int height)
{
const int fileHeaderSize = 14;
const int infoHeaderSize = 40;
const int bytesPerPixel = 4;
var pixelBytes = checked(width * height * bytesPerPixel);
var fileSize = fileHeaderSize + infoHeaderSize + pixelBytes;
var output = new byte[fileSize];
output[0] = (byte)'B';
output[1] = (byte)'M';
WriteUInt32(output, 2, (uint)fileSize);
WriteUInt32(output, 10, fileHeaderSize + infoHeaderSize);
WriteUInt32(output, 14, infoHeaderSize);
WriteInt32(output, 18, width);
WriteInt32(output, 22, -height);
WriteUInt16(output, 26, 1);
WriteUInt16(output, 28, 32);
WriteUInt32(output, 34, (uint)pixelBytes);
var destinationOffset = fileHeaderSize + infoHeaderSize;
for (var y = 0; y < height; y++)
{
var sourceOffset = y * sourceRowBytes;
for (var x = 0; x < visibleRowBytes; x += bytesPerPixel)
{
var destination = destinationOffset + y * visibleRowBytes + x;
output[destination + 0] = rgba[sourceOffset + x + 2];
output[destination + 1] = rgba[sourceOffset + x + 1];
output[destination + 2] = rgba[sourceOffset + x + 0];
output[destination + 3] = rgba[sourceOffset + x + 3];
}
}
File.WriteAllBytes(path, output);
}
private static void WriteUInt16(byte[] output, int offset, ushort value)
{
output[offset + 0] = (byte)value;
output[offset + 1] = (byte)(value >> 8);
}
private static void WriteUInt32(byte[] output, int offset, uint value)
{
output[offset + 0] = (byte)value;
output[offset + 1] = (byte)(value >> 8);
output[offset + 2] = (byte)(value >> 16);
output[offset + 3] = (byte)(value >> 24);
}
private static void WriteInt32(byte[] output, int offset, int value) =>
WriteUInt32(output, offset, unchecked((uint)value));
private Sampler CreateSampler(VulkanGuestSampler sampler)
{
if (_samplers.TryGetValue(sampler, out var cachedSampler))
{
return cachedSampler;
}
var minLod = DecodeSamplerMipFilter(sampler) == 0
? 0f
: DecodeSamplerMinLod(sampler);
var maxLod = DecodeSamplerMipFilter(sampler) == 0
? 0f
: DecodeSamplerMaxLod(sampler);
var samplerInfo = new SamplerCreateInfo
{
SType = StructureType.SamplerCreateInfo,
MagFilter = ToVkFilter(DecodeSamplerMagFilter(sampler)),
MinFilter = ToVkFilter(DecodeSamplerMinFilter(sampler)),
MipmapMode = ToVkMipFilter(DecodeSamplerMipFilter(sampler)),
AddressModeU = ToVkSamplerAddressMode(DecodeSamplerClampX(sampler)),
AddressModeV = ToVkSamplerAddressMode(DecodeSamplerClampY(sampler)),
AddressModeW = ToVkSamplerAddressMode(DecodeSamplerClampZ(sampler)),
MipLodBias = DecodeSamplerLodBias(sampler),
CompareEnable = DecodeSamplerDepthCompare(sampler) != 0,
CompareOp = ToVkCompareOp(DecodeSamplerDepthCompare(sampler)),
MinLod = minLod,
MaxLod = Math.Max(minLod, maxLod),
BorderColor = ToVkBorderColor(DecodeSamplerBorderColor(sampler)),
};
Sampler vkSampler;
Check(
_vk.CreateSampler(_device, &samplerInfo, null, out vkSampler),
"vkCreateSampler(texture)");
_samplers.Add(sampler, vkSampler);
return vkSampler;
}
private static ComponentMapping ToVkComponentMapping(uint dstSelect)
{
if (dstSelect == 0)
{
dstSelect = 0xFAC;
}
return new ComponentMapping(
ToVkComponentSwizzle(dstSelect & 0x7),
ToVkComponentSwizzle((dstSelect >> 3) & 0x7),
ToVkComponentSwizzle((dstSelect >> 6) & 0x7),
ToVkComponentSwizzle((dstSelect >> 9) & 0x7));
}
private static ComponentSwizzle ToVkComponentSwizzle(uint selector) =>
selector switch
{
0 => ComponentSwizzle.Zero,
1 => ComponentSwizzle.One,
4 => ComponentSwizzle.R,
5 => ComponentSwizzle.G,
6 => ComponentSwizzle.B,
7 => ComponentSwizzle.A,
_ => ComponentSwizzle.Identity,
};
private static byte[] ExpandRgb32Pixels(byte[] pixels)
{
var texelCount = pixels.Length / 12;
var expanded = new byte[checked(texelCount * 16)];
for (var texel = 0; texel < texelCount; texel++)
{
System.Buffer.BlockCopy(pixels, texel * 12, expanded, texel * 16, 12);
expanded[texel * 16 + 14] = 0x80;
expanded[texel * 16 + 15] = 0x3F;
}
return expanded;
}
private GlobalBufferResource CreateGlobalBufferResource(
VulkanGuestMemoryBuffer guestBuffer)
{
var buffer = CreateHostBuffer(
guestBuffer.Data,
BufferUsageFlags.StorageBufferBit,
out var memory);
var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint));
if (ShouldTraceVulkanResources() &&
_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length)))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
$"bytes={guestBuffer.Data.Length}");
}
SetDebugName(
ObjectType.Buffer,
buffer.Handle,
$"SharpEmu global 0x{guestBuffer.BaseAddress:X16} {guestBuffer.Data.Length}b");
return new GlobalBufferResource
{
Buffer = buffer,
Memory = memory,
Size = size,
};
}
private VertexBufferResource CreateVertexBufferResource(
VulkanGuestVertexBuffer guestBuffer)
{
var buffer = CreateHostBuffer(
guestBuffer.Data,
BufferUsageFlags.VertexBufferBit,
out var memory);
var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint));
SetDebugName(
ObjectType.Buffer,
buffer.Handle,
$"SharpEmu vertex loc{guestBuffer.Location} " +
$"0x{guestBuffer.BaseAddress:X16} {guestBuffer.Data.Length}b");
if (_tracedVertexBufferCount++ < 64)
{
TraceVulkanShader(
$"vk.vertex_buffer loc={guestBuffer.Location} " +
$"base=0x{guestBuffer.BaseAddress:X16} stride={guestBuffer.Stride} " +
$"offset={guestBuffer.OffsetBytes} comps={guestBuffer.ComponentCount} " +
$"fmt={guestBuffer.DataFormat}/num={guestBuffer.NumberFormat} " +
$"bytes={guestBuffer.Data.Length}");
}
return new VertexBufferResource
{
Buffer = buffer,
Memory = memory,
Size = size,
Location = guestBuffer.Location,
ComponentCount = guestBuffer.ComponentCount,
DataFormat = guestBuffer.DataFormat,
NumberFormat = guestBuffer.NumberFormat,
Stride = guestBuffer.Stride,
OffsetBytes = guestBuffer.OffsetBytes,
};
}
private VkBuffer CreateHostBuffer(
ReadOnlySpan<byte> data,
BufferUsageFlags usage,
out DeviceMemory memory)
{
var size = (ulong)Math.Max(data.Length, sizeof(uint));
var capacity = BitOperations.RoundUpToPowerOf2(size);
var key = new HostBufferPoolKey(usage, capacity);
if (!_hostBufferPool.TryGetValue(key, out var available))
{
available = new Stack<HostBufferAllocation>();
_hostBufferPool.Add(key, available);
}
HostBufferAllocation allocation;
if (available.TryPop(out var pooled))
{
allocation = pooled;
}
else
{
var buffer = CreateBuffer(
capacity,
usage,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out var allocatedMemory);
allocation = new HostBufferAllocation(buffer, allocatedMemory, key);
_hostBufferAllocations.Add(buffer.Handle, allocation);
}
memory = allocation.Memory;
void* mapped;
Check(_vk.MapMemory(_device, memory, 0, size, 0, &mapped), "vkMapMemory(host)");
try
{
fixed (byte* source = data)
{
System.Buffer.MemoryCopy(
source,
mapped,
checked((long)size),
data.Length);
}
}
finally
{
_vk.UnmapMemory(_device, memory);
}
return allocation.Buffer;
}
private void RecycleHostBuffer(VkBuffer buffer, DeviceMemory memory)
{
if (buffer.Handle == 0)
{
return;
}
if (_hostBufferAllocations.TryGetValue(buffer.Handle, out var allocation) &&
allocation.Memory.Handle == memory.Handle)
{
_hostBufferPool[allocation.Key].Push(allocation);
return;
}
_vk.DestroyBuffer(_device, buffer, null);
if (memory.Handle != 0)
{
_vk.FreeMemory(_device, memory, null);
}
}
private static PrimitiveTopology GetPrimitiveTopology(uint primitiveType) =>
primitiveType switch
{
1 => PrimitiveTopology.PointList,
2 => PrimitiveTopology.LineList,
3 => PrimitiveTopology.LineStrip,
5 => PrimitiveTopology.TriangleFan,
6 => PrimitiveTopology.TriangleStrip,
GuestPrimitiveRectList => PrimitiveTopology.TriangleStrip,
_ => PrimitiveTopology.TriangleList,
};
private static Format ToVkVertexFormat(
uint dataFormat,
uint numberFormat,
uint componentCount) =>
(dataFormat, numberFormat) switch
{
(1, 0) => Format.R8Unorm,
(1, 1) => Format.R8SNorm,
(1, 4) => Format.R8Uint,
(1, 5) => Format.R8Sint,
(1, 9) => Format.R8Srgb,
(2, 0) => Format.R16Unorm,
(2, 1) => Format.R16SNorm,
(2, 4) => Format.R16Uint,
(2, 5) => Format.R16Sint,
(2, 7) => Format.R16Sfloat,
(3, 0) => Format.R8G8Unorm,
(3, 1) => Format.R8G8SNorm,
(3, 4) => Format.R8G8Uint,
(3, 5) => Format.R8G8Sint,
(3, 9) => Format.R8G8Srgb,
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 0) => Format.R16G16Unorm,
(5, 1) => Format.R16G16SNorm,
(5, 2) => Format.R16G16Uscaled,
(5, 3) => Format.R16G16Sscaled,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(6, 7) => Format.B10G11R11UfloatPack32,
(7, 7) => Format.B10G11R11UfloatPack32,
(8, 0) => Format.A2B10G10R10UnormPack32,
(8, 1) => Format.A2B10G10R10SNormPack32,
(8, 2) => Format.A2B10G10R10UscaledPack32,
(8, 3) => Format.A2B10G10R10SscaledPack32,
(8, 4) => Format.A2B10G10R10UintPack32,
(8, 5) => Format.A2B10G10R10SintPack32,
(9, 0) => Format.A2R10G10B10UnormPack32,
(9, 1) => Format.A2R10G10B10SNormPack32,
(9, 2) => Format.A2R10G10B10UscaledPack32,
(9, 3) => Format.A2R10G10B10SscaledPack32,
(9, 4) => Format.A2R10G10B10UintPack32,
(9, 5) => Format.A2R10G10B10SintPack32,
(10, 0) => Format.R8G8B8A8Unorm,
(10, 1) => Format.R8G8B8A8SNorm,
(10, 2) => Format.R8G8B8A8Uscaled,
(10, 3) => Format.R8G8B8A8Sscaled,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, 9) => Format.R8G8B8A8Srgb,
(11, 4) => Format.R32G32Uint,
(11, 5) => Format.R32G32Sint,
(11, 7) => Format.R32G32Sfloat,
(12, 0) => Format.R16G16B16A16Unorm,
(12, 1) => Format.R16G16B16A16SNorm,
(12, 2) => Format.R16G16B16A16Uscaled,
(12, 3) => Format.R16G16B16A16Sscaled,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 6) => Format.R16G16B16A16SNorm,
(12, 7) => Format.R16G16B16A16Sfloat,
(13, 4) => Format.R32G32B32Uint,
(13, 5) => Format.R32G32B32Sint,
(13, 7) => Format.R32G32B32Sfloat,
(14, 4) => Format.R32G32B32A32Uint,
(14, 5) => Format.R32G32B32A32Sint,
(14, 7) => Format.R32G32B32A32Sfloat,
(16, 0) => Format.B5G6R5UnormPack16,
(17, 0) => Format.R5G5B5A1UnormPack16,
(19, 0) => Format.R4G4B4A4UnormPack16,
(34, 7) => Format.E5B9G9R9UfloatPack32,
_ => ToVkFloatVertexFormat(componentCount),
};
private static Format ToVkFloatVertexFormat(uint componentCount) =>
componentCount switch
{
1 => Format.R32Sfloat,
2 => Format.R32G32Sfloat,
3 => Format.R32G32B32Sfloat,
4 => Format.R32G32B32A32Sfloat,
_ => Format.R32Sfloat,
};
private static ulong GetVertexBindingOffset(VertexBufferResource vertexBuffer)
{
if (vertexBuffer.OffsetBytes < vertexBuffer.Size)
{
return vertexBuffer.OffsetBytes;
}
TraceVulkanShader(
$"vk.vertex_offset_oob loc={vertexBuffer.Location} " +
$"offset={vertexBuffer.OffsetBytes} size={vertexBuffer.Size}");
return 0;
}
private static uint GetDrawVertexCount(
uint primitiveType,
uint vertexCount,
VulkanGuestIndexBuffer? indexBuffer)
{
if (primitiveType == GuestPrimitiveRectList && indexBuffer is null)
{
return 4;
}
return vertexCount;
}
private static BlendFactor ToVkBlendFactor(uint factor) =>
factor switch
{
0 => BlendFactor.Zero,
1 => BlendFactor.One,
2 => BlendFactor.SrcColor,
3 => BlendFactor.OneMinusSrcColor,
4 => BlendFactor.SrcAlpha,
5 => BlendFactor.OneMinusSrcAlpha,
6 => BlendFactor.DstAlpha,
7 => BlendFactor.OneMinusDstAlpha,
8 => BlendFactor.DstColor,
9 => BlendFactor.OneMinusDstColor,
10 => BlendFactor.SrcAlphaSaturate,
13 => BlendFactor.ConstantColor,
14 => BlendFactor.OneMinusConstantColor,
15 => BlendFactor.Src1Color,
16 => BlendFactor.OneMinusSrc1Color,
17 => BlendFactor.Src1Alpha,
18 => BlendFactor.OneMinusSrc1Alpha,
19 => BlendFactor.ConstantAlpha,
20 => BlendFactor.OneMinusConstantAlpha,
_ => BlendFactor.One,
};
private static BlendOp ToVkBlendOp(uint function) =>
function switch
{
0 => BlendOp.Add,
1 => BlendOp.Subtract,
2 => BlendOp.Min,
3 => BlendOp.Max,
4 => BlendOp.ReverseSubtract,
_ => BlendOp.Add,
};
private static uint DecodeSamplerClampX(VulkanGuestSampler sampler) =>
sampler.Word0 & 0x7u;
private static uint DecodeSamplerClampY(VulkanGuestSampler sampler) =>
(sampler.Word0 >> 3) & 0x7u;
private static uint DecodeSamplerClampZ(VulkanGuestSampler sampler) =>
(sampler.Word0 >> 6) & 0x7u;
private static uint DecodeSamplerDepthCompare(VulkanGuestSampler sampler) =>
(sampler.Word0 >> 12) & 0x7u;
private static float DecodeSamplerMinLod(VulkanGuestSampler sampler) =>
(sampler.Word1 & 0xFFFu) / 256.0f;
private static float DecodeSamplerMaxLod(VulkanGuestSampler sampler) =>
((sampler.Word1 >> 12) & 0xFFFu) / 256.0f;
private static float DecodeSamplerLodBias(VulkanGuestSampler sampler)
{
var raw = sampler.Word2 & 0x3FFFu;
var signed = (short)((raw ^ 0x2000u) - 0x2000u);
return signed / 256.0f;
}
private static uint DecodeSamplerMagFilter(VulkanGuestSampler sampler) =>
(sampler.Word2 >> 20) & 0x3u;
private static uint DecodeSamplerMinFilter(VulkanGuestSampler sampler) =>
(sampler.Word2 >> 22) & 0x3u;
private static uint DecodeSamplerMipFilter(VulkanGuestSampler sampler) =>
(sampler.Word2 >> 26) & 0x3u;
private static uint DecodeSamplerBorderColor(VulkanGuestSampler sampler) =>
(sampler.Word3 >> 30) & 0x3u;
private static SamplerAddressMode ToVkSamplerAddressMode(uint mode) =>
mode switch
{
0 => SamplerAddressMode.Repeat,
1 => SamplerAddressMode.MirroredRepeat,
2 => SamplerAddressMode.ClampToEdge,
3 or 5 or 7 => SamplerAddressMode.MirrorClampToEdge,
4 or 6 => SamplerAddressMode.ClampToBorder,
_ => SamplerAddressMode.ClampToEdge,
};
private static Filter ToVkFilter(uint filter) =>
filter is 1 or 3 ? Filter.Linear : Filter.Nearest;
private static SamplerMipmapMode ToVkMipFilter(uint filter) =>
filter == 2 ? SamplerMipmapMode.Linear : SamplerMipmapMode.Nearest;
private static CompareOp ToVkCompareOp(uint compare) =>
compare switch
{
1 => CompareOp.Less,
2 => CompareOp.Equal,
3 => CompareOp.LessOrEqual,
4 => CompareOp.Greater,
5 => CompareOp.NotEqual,
6 => CompareOp.GreaterOrEqual,
7 => CompareOp.Always,
_ => CompareOp.Never,
};
private static BorderColor ToVkBorderColor(uint color) =>
color switch
{
1 => BorderColor.FloatTransparentBlack,
2 => BorderColor.FloatOpaqueWhite,
_ => BorderColor.FloatOpaqueBlack,
};
private static ColorComponentFlags ToVkColorWriteMask(uint mask)
{
var flags = default(ColorComponentFlags);
if ((mask & 1u) != 0)
{
flags |= ColorComponentFlags.RBit;
}
if ((mask & 2u) != 0)
{
flags |= ColorComponentFlags.GBit;
}
if ((mask & 4u) != 0)
{
flags |= ColorComponentFlags.BBit;
}
if ((mask & 8u) != 0)
{
flags |= ColorComponentFlags.ABit;
}
return flags;
}
private static VulkanGuestRect ClampScissor(VulkanGuestRect? scissor, Extent2D extent)
{
if (scissor is not { } rect)
{
return new VulkanGuestRect(0, 0, extent.Width, extent.Height);
}
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
var top = Math.Clamp(rect.Y, 0, checked((int)extent.Height));
var right = Math.Clamp(
rect.X + checked((int)rect.Width),
left,
checked((int)extent.Width));
var bottom = Math.Clamp(
rect.Y + checked((int)rect.Height),
top,
checked((int)extent.Height));
return new VulkanGuestRect(
left,
top,
checked((uint)(right - left)),
checked((uint)(bottom - top)));
}
private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent)
{
if (viewport is not { } rect)
{
return new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
}
var maxX = (float)extent.Width;
var maxY = (float)extent.Height;
var left = Math.Clamp(rect.X, 0f, maxX);
var right = Math.Clamp(rect.X + rect.Width, left, maxX);
var yOrigin = Math.Clamp(rect.Y, 0f, maxY);
var yEnd = Math.Clamp(rect.Y + rect.Height, 0f, maxY);
var minDepth = Math.Clamp(rect.MinDepth, 0f, 1f);
var maxDepth = Math.Clamp(rect.MaxDepth, minDepth, 1f);
return new Viewport(
left,
yOrigin,
right - left,
yEnd - yOrigin,
minDepth,
maxDepth);
}
private static byte[] CreateFallbackTexturePixels(uint format, uint width, uint height, ulong expectedSize)
{
if (format is 9 or 10 or 56 or 62 or 64)
{
return CreateBlackFrame(width, height);
}
return new byte[checked((int)expectedSize)];
}
private static ulong GetTextureBytesPerPixel(uint format) =>
format switch
{
1 => 1UL,
2 => 2UL,
3 => 2UL,
4 => 4UL,
5 => 4UL,
6 => 4UL,
7 => 4UL,
9 => 4UL,
10 => 4UL,
11 => 8UL,
12 => 8UL,
13 => 12UL,
14 => 16UL,
20 => 4UL,
22 => 8UL,
29 => 4UL,
36 => 1UL,
49 => 1UL,
56 => 4UL,
62 => 4UL,
64 => 4UL,
71 => 8UL,
_ => 4UL,
};
private static ulong GetTextureByteCount(uint format, uint width, uint height)
{
var blockBytes = format switch
{
169 or 170 => 8UL,
171 or 172 or 173 or 174 or 175 or 176 or
177 or 178 or 179 or 180 or 181 or 182 => 16UL,
_ => 0UL,
};
return blockBytes == 0
? checked((ulong)width * height * GetTextureBytesPerPixel(format))
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
}
private static Format GetTextureFormat(uint format, uint numberType) =>
(format, numberType) switch
{
(9, _) => Format.A2R10G10B10UnormPack32,
(GuestFormatR32Uint, _) => Format.R32Uint,
(GuestFormatR32Sint, _) => Format.R32Sint,
(GuestFormatR32Sfloat, _) => Format.R32Sfloat,
(GuestFormatR16G16Uint, _) => Format.R16G16Uint,
(GuestFormatR16G16Sint, _) => Format.R16G16Sint,
(GuestFormatR16G16Sfloat, _) => Format.R16G16Sfloat,
(GuestFormatR8G8B8A8Uint, _) => Format.R8G8B8A8Uint,
(GuestFormatR8G8B8A8Sint, _) => Format.R8G8B8A8Sint,
(GuestFormatR16G16B16A16Uint, _) => Format.R16G16B16A16Uint,
(GuestFormatR16G16B16A16Sint, _) => Format.R16G16B16A16Sint,
(1, 0) => Format.R8Unorm,
(2, 7) => Format.R16Sfloat,
(3, 0) => Format.R8G8Unorm,
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 0) => Format.R16G16Unorm,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(6, 7) => Format.B10G11R11UfloatPack32,
(10, 0) => Format.R8G8B8A8Unorm,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(11, 4) => Format.R32G32Uint,
(11, 5) => Format.R32G32Sint,
(11, 7) => Format.R32G32Sfloat,
(12, 0) => Format.R16G16B16A16Unorm,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 7) => Format.R16G16B16A16Sfloat,
(13, 4) => Format.R32G32B32A32Uint,
(13, 5) => Format.R32G32B32A32Sint,
(13, _) => Format.R32G32B32A32Sfloat,
(14, 4) => Format.R32G32B32A32Uint,
(14, 5) => Format.R32G32B32A32Sint,
(14, 7) => Format.R32G32B32A32Sfloat,
(20, _) => Format.R32Uint,
(4, _) => Format.R32Sfloat,
(5, _) => Format.R16G16Sfloat,
(7, _) => Format.B10G11R11UfloatPack32,
(14, _) => Format.R32G32B32A32Sfloat,
(22, _) => Format.R16G16B16A16Sfloat,
(29, _) => Format.R32Sfloat,
(36, _) => Format.R8Unorm,
(49, _) => Format.R8Uint,
(56, _) => Format.R8G8B8A8Unorm,
(62, _) => Format.R8G8B8A8Unorm,
(64, _) => Format.R8G8B8A8Unorm,
(71, _) => Format.R16G16B16A16Sfloat,
(75, _) => Format.R32G32Sfloat,
(169, _) => Format.BC1RgbaUnormBlock,
(170, _) => Format.BC1RgbaSrgbBlock,
(181, _) => Format.BC7UnormBlock,
(182, _) => Format.BC7SrgbBlock,
_ => Format.R8G8B8A8Unorm,
};
private static Format GetRenderTargetFormat(uint format, uint numberType) =>
(format, numberType) switch
{
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(9, _) => Format.A2R10G10B10UnormPack32,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, _) => Format.R8G8B8A8Unorm,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 7) => Format.R16G16B16A16Sfloat,
(_, 0) => GetTextureFormat(format, numberType),
_ => Format.Undefined,
};
private static bool IsBlockCompressedFormat(Format format) =>
format is Format.BC1RgbaUnormBlock or
Format.BC1RgbaSrgbBlock or
Format.BC7UnormBlock or
Format.BC7SrgbBlock;
private VkBuffer CreateBuffer(
ulong size,
BufferUsageFlags usage,
MemoryPropertyFlags memoryFlags,
out DeviceMemory memory)
{
var bufferInfo = new BufferCreateInfo
{
SType = StructureType.BufferCreateInfo,
Size = size,
Usage = usage,
SharingMode = SharingMode.Exclusive,
};
Check(_vk.CreateBuffer(_device, &bufferInfo, null, out var buffer), "vkCreateBuffer");
_vk.GetBufferMemoryRequirements(_device, buffer, out var requirements);
var memoryInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = requirements.Size,
MemoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, memoryFlags),
};
Check(_vk.AllocateMemory(_device, &memoryInfo, null, out memory), "vkAllocateMemory");
Check(_vk.BindBufferMemory(_device, buffer, memory, 0), "vkBindBufferMemory");
return buffer;
}
private void CreateStagingBuffer(ulong size)
{
_stagingBuffer = CreateBuffer(
size,
BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out _stagingMemory);
_stagingSize = size;
}
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags)
{
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out var properties);
var memoryTypes = &properties.MemoryTypes.Element0;
for (uint index = 0; index < properties.MemoryTypeCount; index++)
{
if ((typeBits & (1u << (int)index)) != 0 &&
(memoryTypes[index].PropertyFlags & requiredFlags) == requiredFlags)
{
return index;
}
}
throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found.");
}
private const uint MaxComputeZSlicesPerSubmission = 8;
private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work)
{
if (_deviceLost)
{
return;
}
if (AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress))
{
TraceVulkanShader(
$"vk.compute_skip cs=0x{work.ShaderAddress:X16} " +
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
$"textures={work.Textures.Count}");
return;
}
TranslatedDrawResources? resources = null;
CommandBuffer commandBuffer = default;
var submitted = false;
try
{
EnsureGuestSubmissionCapacity();
resources = CreateComputeDispatchResources(work);
var batchCount = Math.Max(
1u,
(uint)Math.Ceiling(work.GroupCountZ / (double)MaxComputeZSlicesPerSubmission));
for (var batchIndex = 0u; batchIndex < batchCount; batchIndex++)
{
var zStart = batchIndex * MaxComputeZSlicesPerSubmission;
var zCount = Math.Min(MaxComputeZSlicesPerSubmission, work.GroupCountZ - zStart);
var isFirstBatch = batchIndex == 0;
var isLastBatch = batchIndex == batchCount - 1;
commandBuffer = AllocateGuestCommandBuffer();
_commandBuffer = commandBuffer;
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
};
Check(
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
"vkBeginCommandBuffer(compute)");
BeginDebugLabel(_commandBuffer, resources.DebugName);
if (isFirstBatch)
{
RecordTextureUploads(resources, PipelineStageFlags.ComputeShaderBit);
RecordStorageImagesForWrite(resources, PipelineStageFlags.ComputeShaderBit);
}
_vk.CmdBindPipeline(
_commandBuffer,
PipelineBindPoint.Compute,
resources.Pipeline);
if (resources.DescriptorSet.Handle != 0)
{
var descriptorSet = resources.DescriptorSet;
_vk.CmdBindDescriptorSets(
_commandBuffer,
PipelineBindPoint.Compute,
resources.PipelineLayout,
0,
1,
&descriptorSet,
0,
null);
}
RecordChunkedComputeDispatch(_commandBuffer, work, zStart, zCount);
if (isLastBatch)
{
RecordStorageImagesForRead(resources, PipelineStageFlags.ComputeShaderBit);
}
EndDebugLabel(_commandBuffer);
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(compute)");
TraceVulkanShader(
$"vk.compute_submit cs=0x{work.ShaderAddress:X16} " +
$"batch={batchIndex}/{batchCount} z={zStart}..{zStart + zCount}");
if (isLastBatch)
{
SubmitGuestCommandBuffer(
commandBuffer,
resources,
GetTraceImages(resources));
submitted = true;
}
else
{
SubmitGuestCommandBufferAndWait(commandBuffer);
commandBuffer = default;
}
}
MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false);
TraceVulkanShader(
$"vk.compute_dispatch groups={work.GroupCountX}x" +
$"{work.GroupCountY}x{work.GroupCountZ} " +
$"textures={work.Textures.Count} cs=0x{work.ShaderAddress:X16} " +
$"batches={batchCount}");
}
catch (Exception exception)
{
if (TryMarkDeviceLost(exception))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][ERROR] Vulkan compute dispatch failed " +
$"cs=0x{work.ShaderAddress:X16}: {exception.Message}");
}
finally
{
_commandBuffer = _presentationCommandBuffer;
if (!submitted && commandBuffer.Handle != 0)
{
_vk.FreeCommandBuffers(
_device,
_commandPool,
1,
&commandBuffer);
}
if (!submitted && resources is not null)
{
DestroyTranslatedDrawResources(resources);
}
}
}
private void RecordChunkedComputeDispatch(
CommandBuffer commandBuffer,
VulkanComputeGuestDispatch work,
uint zStart,
uint zCount)
{
const uint maxWorkgroupsPerCommand = 4096;
var yChunk = Math.Max(
1u,
Math.Min(
work.GroupCountY,
maxWorkgroupsPerCommand / Math.Max(work.GroupCountX, 1u)));
var commandCount = 0u;
for (var z = zStart; z < zStart + zCount; z++)
{
for (var y = 0u; y < work.GroupCountY; y += yChunk)
{
var countY = Math.Min(yChunk, work.GroupCountY - y);
_vk.CmdDispatchBase(
commandBuffer,
0,
y,
z,
work.GroupCountX,
countY,
1);
commandCount++;
}
}
if (commandCount > 1)
{
TraceVulkanShader(
$"vk.compute_chunked cs=0x{work.ShaderAddress:X16} " +
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
$"z_range={zStart}..{zStart + zCount} commands={commandCount} y_chunk={yChunk}");
}
}
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
{
if (_deviceLost)
{
return;
}
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
if (format == Format.Undefined)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped unsupported render target " +
$"addr=0x{work.Target.Address:X16} format={work.Target.Format} " +
$"number={work.Target.NumberType}");
return;
}
if (work.Draw.Textures.Any(texture =>
texture.Address == work.Target.Address &&
texture.Address != 0))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped render-target feedback loop " +
$"addr=0x{work.Target.Address:X16}");
return;
}
var target = GetOrCreateGuestImage(work.Target, format);
TranslatedDrawResources? resources = null;
CommandBuffer commandBuffer = default;
var submitted = false;
try
{
EnsureGuestSubmissionCapacity();
var extent = new Extent2D(target.Width, target.Height);
resources = CreateTranslatedDrawResources(
work.Draw,
target.RenderPass,
target.Format,
extent);
resources.DebugName =
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
$"{work.Target.Width}x{work.Target.Height} fmt{work.Target.Format}";
commandBuffer = AllocateGuestCommandBuffer();
_commandBuffer = commandBuffer;
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
};
Check(
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
"vkBeginCommandBuffer(offscreen)");
BeginDebugLabel(_commandBuffer, resources.DebugName);
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
var toColorAttachment = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
OldLayout = targetHasPriorContents
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.ColorAttachmentOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = target.Image,
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
targetHasPriorContents
? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.ColorAttachmentOutputBit,
0,
0,
null,
0,
null,
1,
&toColorAttachment);
RecordTranslatedGraphicsPass(
resources,
target.RenderPass,
target.Framebuffer,
extent);
RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit);
var toShaderRead = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.ColorAttachmentWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.ColorAttachmentOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = target.Image,
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.ColorAttachmentOutputBit,
PipelineStageFlags.FragmentShaderBit,
0,
0,
null,
0,
null,
1,
&toShaderRead);
EndDebugLabel(_commandBuffer);
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)");
SubmitGuestCommandBuffer(
commandBuffer,
resources,
GetTraceImages(resources, target));
submitted = true;
target.Initialized = true;
MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false);
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
work.Target.Format,
work.Target.NumberType);
if (work.PublishTarget && guestTextureFormat != 0)
{
lock (_gate)
{
_availableGuestImages[target.Address] = guestTextureFormat;
_gpuGuestImages[target.Address] = guestTextureFormat;
}
}
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
{
var writeCount = _tracedGuestWriteCounts.TryGetValue(
target.Address,
out var previousCount)
? previousCount + 1
: 1;
_tracedGuestWriteCounts[target.Address] = writeCount;
if (writeCount <= 3)
{
_commandBuffer = _presentationCommandBuffer;
Check(
_vk.QueueWaitIdle(_queue),
"vkQueueWaitIdle(guest write trace)");
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.guest_write_sample " +
$"addr=0x{target.Address:X16} write={writeCount} " +
$"ps_bytes={work.Draw.PixelSpirv.Length}");
TraceGuestImageContents(target);
}
}
TraceVulkanShader(
$"vk.offscreen_draw addr=0x{target.Address:X16} " +
$"size={target.Width}x{target.Height} format={target.Format} " +
$"textures={work.Draw.Textures.Count}");
}
catch (Exception exception)
{
if (TryMarkDeviceLost(exception))
{
return;
}
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
!failedTarget.Initialized)
{
lock (_gate)
{
_availableGuestImages.Remove(work.Target.Address);
_gpuGuestImages.Remove(work.Target.Address);
}
}
Console.Error.WriteLine(
$"[LOADER][ERROR] Vulkan offscreen draw failed " +
$"addr=0x{work.Target.Address:X16}: {exception.Message}");
}
finally
{
_commandBuffer = _presentationCommandBuffer;
if (!submitted && commandBuffer.Handle != 0)
{
_vk.FreeCommandBuffers(
_device,
_commandPool,
1,
&commandBuffer);
}
if (!submitted && resources is not null)
{
DestroyTranslatedDrawResources(resources);
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private GuestImageResource GetOrCreateGuestImage(
VulkanGuestRenderTarget target,
Format format)
{
var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels);
if (_guestImages.TryGetValue(target.Address, out var existing))
{
if (existing.Width == target.Width &&
existing.Height == target.Height &&
existing.MipLevels == mipLevels &&
existing.Format == format)
{
existing.IsCpuBacked = false;
existing.CpuContentFingerprint = 0;
if (existing.RenderPass.Handle == 0)
{
var attachmentView = existing.MipViews.Length > 0
? existing.MipViews[0]
: existing.View;
var (promotedRenderPass, promotedFramebuffer) = CreateRenderPassAndFramebuffer(
existing.Format,
attachmentView,
existing.Width,
existing.Height);
existing.RenderPass = promotedRenderPass;
existing.Framebuffer = promotedFramebuffer;
var promotedName = GuestImageDebugName(target, format);
SetDebugName(ObjectType.RenderPass, promotedRenderPass.Handle, $"{promotedName} renderpass");
SetDebugName(ObjectType.Framebuffer, promotedFramebuffer.Handle, $"{promotedName} framebuffer");
}
return existing;
}
DestroyGuestImage(existing);
_guestImages.Remove(target.Address);
lock (_gate)
{
_availableGuestImages.Remove(target.Address);
_gpuGuestImages.Remove(target.Address);
}
}
var imageInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
Flags =
ImageCreateFlags.CreateMutableFormatBit |
ImageCreateFlags.CreateExtendedUsageBit,
ImageType = ImageType.Type2D,
Format = format,
Extent = new Extent3D(target.Width, target.Height, 1),
MipLevels = mipLevels,
ArrayLayers = 1,
Samples = SampleCountFlags.Count1Bit,
Tiling = ImageTiling.Optimal,
Usage =
ImageUsageFlags.ColorAttachmentBit |
ImageUsageFlags.SampledBit |
ImageUsageFlags.StorageBit |
ImageUsageFlags.TransferSrcBit |
ImageUsageFlags.TransferDstBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
};
Check(_vk.CreateImage(_device, &imageInfo, null, out var image), "vkCreateImage(offscreen)");
_vk.GetImageMemoryRequirements(_device, image, out var requirements);
var allocationInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = requirements.Size,
MemoryTypeIndex = FindMemoryType(
requirements.MemoryTypeBits,
MemoryPropertyFlags.DeviceLocalBit),
};
Check(
_vk.AllocateMemory(_device, &allocationInfo, null, out var memory),
"vkAllocateMemory(offscreen)");
Check(_vk.BindImageMemory(_device, image, memory, 0), "vkBindImageMemory(offscreen)");
var viewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
ViewType = ImageViewType.Type2D,
Format = format,
Components = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity),
SubresourceRange = ColorSubresourceRange(0, mipLevels),
};
Check(
_vk.CreateImageView(_device, &viewInfo, null, out var view),
"vkCreateImageView(offscreen)");
var mipViews = new ImageView[mipLevels];
for (uint mipLevel = 0; mipLevel < mipLevels; mipLevel++)
{
viewInfo.SubresourceRange = ColorSubresourceRange(mipLevel, 1);
ImageView mipView;
Check(
_vk.CreateImageView(
_device,
&viewInfo,
null,
out mipView),
"vkCreateImageView(offscreen mip)");
mipViews[mipLevel] = mipView;
}
var (renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
format,
mipViews[0],
target.Width,
target.Height);
var resource = new GuestImageResource
{
Address = target.Address,
Width = target.Width,
Height = target.Height,
MipLevels = mipLevels,
Format = format,
Image = image,
Memory = memory,
View = view,
MipViews = mipViews,
RenderPass = renderPass,
Framebuffer = framebuffer,
};
var debugName = GuestImageDebugName(target, format);
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
for (var mipLevel = 0; mipLevel < mipViews.Length; mipLevel++)
{
SetDebugName(
ObjectType.ImageView,
mipViews[mipLevel].Handle,
$"{debugName} mip{mipLevel}");
}
SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass");
SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer");
_guestImages.Add(target.Address, resource);
return resource;
}
private (RenderPass RenderPass, Framebuffer Framebuffer) CreateRenderPassAndFramebuffer(
Format format,
ImageView attachmentView,
uint width,
uint height)
{
var colorAttachment = new AttachmentDescription
{
Format = format,
Samples = SampleCountFlags.Count1Bit,
LoadOp = AttachmentLoadOp.Load,
StoreOp = AttachmentStoreOp.Store,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare,
InitialLayout = ImageLayout.ColorAttachmentOptimal,
FinalLayout = ImageLayout.ColorAttachmentOptimal,
};
var colorReference = new AttachmentReference
{
Attachment = 0,
Layout = ImageLayout.ColorAttachmentOptimal,
};
var subpass = new SubpassDescription
{
PipelineBindPoint = PipelineBindPoint.Graphics,
ColorAttachmentCount = 1,
PColorAttachments = &colorReference,
};
var renderPassInfo = new RenderPassCreateInfo
{
SType = StructureType.RenderPassCreateInfo,
AttachmentCount = 1,
PAttachments = &colorAttachment,
SubpassCount = 1,
PSubpasses = &subpass,
};
Check(
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
"vkCreateRenderPass(offscreen)");
var attachment = attachmentView;
var framebufferInfo = new FramebufferCreateInfo
{
SType = StructureType.FramebufferCreateInfo,
RenderPass = renderPass,
AttachmentCount = 1,
PAttachments = &attachment,
Width = width,
Height = height,
Layers = 1,
};
Check(
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out var framebuffer),
"vkCreateFramebuffer(offscreen)");
return (renderPass, framebuffer);
}
private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels)
{
var largestDimension = Math.Max(width, height);
uint maximumMipLevels = 1;
while (largestDimension > 1)
{
largestDimension >>= 1;
maximumMipLevels++;
}
return Math.Min(Math.Max(requestedMipLevels, 1u), maximumMipLevels);
}
private void DestroyGuestImage(GuestImageResource resource)
{
foreach (var view in resource.FormatViews.Values)
{
if (view.Handle != 0)
{
_vk.DestroyImageView(_device, view, null);
}
}
resource.FormatViews.Clear();
if (resource.Framebuffer.Handle != 0)
{
_vk.DestroyFramebuffer(_device, resource.Framebuffer, null);
}
if (resource.RenderPass.Handle != 0)
{
_vk.DestroyRenderPass(_device, resource.RenderPass, null);
}
if (resource.View.Handle != 0)
{
_vk.DestroyImageView(_device, resource.View, null);
}
foreach (var mipView in resource.MipViews)
{
if (mipView.Handle != 0)
{
_vk.DestroyImageView(_device, mipView, null);
}
}
if (resource.Image.Handle != 0)
{
_vk.DestroyImage(_device, resource.Image, null);
}
if (resource.Memory.Handle != 0)
{
_vk.FreeMemory(_device, resource.Memory, null);
}
}
private static uint GetGuestTextureFormat(Format format) =>
format switch
{
Format.A2R10G10B10UnormPack32 => 9,
Format.R8G8B8A8Unorm => 56,
Format.R16G16Unorm => 5,
Format.R16G16B16A16Unorm => 12,
Format.R32Uint => GuestFormatR32Uint,
Format.R32Sint => GuestFormatR32Sint,
Format.R32Sfloat => GuestFormatR32Sfloat,
Format.R16G16Uint => GuestFormatR16G16Uint,
Format.R16G16Sint => GuestFormatR16G16Sint,
Format.R16G16Sfloat => GuestFormatR16G16Sfloat,
Format.R8G8B8A8Uint => GuestFormatR8G8B8A8Uint,
Format.R8G8B8A8Sint => GuestFormatR8G8B8A8Sint,
Format.R16G16B16A16Uint => GuestFormatR16G16B16A16Uint,
Format.R16G16B16A16Sint => GuestFormatR16G16B16A16Sint,
Format.R16G16B16A16Sfloat => 71,
_ => 0,
};
private bool TryGetOrCreateGuestImageView(
GuestImageResource resource,
Format format,
uint mipLevel,
uint levelCount,
uint dstSelect,
out ImageView view)
{
try
{
view = GetOrCreateGuestImageView(resource, format, mipLevel, levelCount, dstSelect);
return true;
}
catch (Exception exception)
{
view = default;
TraceVulkanShader(
$"vk.texture_alias_view_failed addr=0x{resource.Address:X16} " +
$"image_format={resource.Format} view_format={format}: {exception.Message}");
return false;
}
}
private ImageView GetOrCreateGuestImageView(
GuestImageResource resource,
Format format,
uint mipLevel,
uint levelCount,
uint dstSelect = 0xFAC)
{
if (mipLevel >= resource.MipLevels)
{
throw new InvalidOperationException(
$"View mip {mipLevel} exceeds image mip count {resource.MipLevels}.");
}
levelCount = Math.Max(levelCount, 1);
levelCount = Math.Min(levelCount, resource.MipLevels - mipLevel);
if (format == resource.Format && dstSelect == 0xFAC)
{
if (mipLevel == 0 && levelCount == resource.MipLevels)
{
return resource.View;
}
if (levelCount == 1)
{
return resource.MipViews[mipLevel];
}
}
if (!IsCompatibleViewFormat(resource.Format, format))
{
throw new InvalidOperationException(
$"Incompatible image view format {format} for image {resource.Format}.");
}
var key = (format, mipLevel, levelCount, dstSelect);
if (resource.FormatViews.TryGetValue(key, out var existing))
{
return existing;
}
var viewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = resource.Image,
ViewType = ImageViewType.Type2D,
Format = format,
Components = ToVkComponentMapping(dstSelect),
SubresourceRange = ColorSubresourceRange(mipLevel, levelCount),
};
ImageView view;
Check(
_vk.CreateImageView(_device, &viewInfo, null, out view),
"vkCreateImageView(guest alias)");
resource.FormatViews.Add(key, view);
SetDebugName(
ObjectType.ImageView,
view.Handle,
$"SharpEmu guest 0x{resource.Address:X16} alias {format} mip{mipLevel}+{levelCount}");
TraceVulkanShader(
$"vk.texture_alias_view addr=0x{resource.Address:X16} " +
$"image_format={resource.Format} view_format={format} " +
$"mip={mipLevel} levels={levelCount} dst=0x{dstSelect:X3}");
return view;
}
private static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat)
{
if (imageFormat == viewFormat)
{
return true;
}
var imageClass = GetFormatCompatibilityClass(imageFormat);
return imageClass != 0 && imageClass == GetFormatCompatibilityClass(viewFormat);
}
private static uint GetFormatCompatibilityClass(Format format) =>
format switch
{
Format.R8Unorm or
Format.R8Uint or
Format.R8Sint => 8,
Format.R16Sfloat => 16,
Format.R32Uint or
Format.R32Sint or
Format.R32Sfloat or
Format.R16G16Unorm or
Format.R16G16Uint or
Format.R16G16Sint or
Format.R16G16Sfloat or
Format.R8G8B8A8Unorm or
Format.R8G8B8A8Uint or
Format.R8G8B8A8Sint or
Format.A2R10G10B10UnormPack32 or
Format.B10G11R11UfloatPack32 => 32,
Format.R32G32Uint or
Format.R32G32Sint or
Format.R32G32Sfloat or
Format.R16G16B16A16Unorm or
Format.R16G16B16A16Uint or
Format.R16G16B16A16Sint or
Format.R16G16B16A16Sfloat => 64,
Format.R32G32B32Sfloat => 96,
Format.R32G32B32A32Uint or
Format.R32G32B32A32Sint or
Format.R32G32B32A32Sfloat => 128,
_ => 0,
};
private void UpdatePerformanceHud()
{
if (!_performanceHudEnabled || !OperatingSystem.IsWindows())
{
return;
}
var now = Stopwatch.GetTimestamp();
if (_performanceHudLastTimestamp != 0 &&
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds <
PerformanceHudSampleSeconds)
{
return;
}
try
{
using var process = Process.GetCurrentProcess();
var processCpu = process.TotalProcessorTime;
var currentThreadCpu = new Dictionary<int, TimeSpan>();
var currentThreadIds = new HashSet<int>();
var hottestThreadId = 0;
var hottestThreadCpuSeconds = 0.0;
foreach (ProcessThread thread in process.Threads)
{
using (thread)
{
try
{
var threadId = thread.Id;
var cpu = thread.TotalProcessorTime;
currentThreadIds.Add(threadId);
currentThreadCpu[threadId] = cpu;
if (_performanceHudThreadCpu.TryGetValue(threadId, out var previousCpu))
{
var deltaSeconds = Math.Max(0.0, (cpu - previousCpu).TotalSeconds);
if (deltaSeconds > hottestThreadCpuSeconds)
{
hottestThreadCpuSeconds = deltaSeconds;
hottestThreadId = threadId;
}
}
}
catch (InvalidOperationException)
{
}
}
}
if (_performanceHudLastTimestamp != 0)
{
var elapsedSeconds = Math.Max(
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds,
0.001);
var processCpuPercent = Math.Max(
0.0,
(processCpu - _performanceHudLastProcessCpu).TotalSeconds /
elapsedSeconds /
Math.Max(Environment.ProcessorCount, 1) *
100.0);
var hottestThreadPercent = hottestThreadCpuSeconds / elapsedSeconds * 100.0;
var presentedFrames = _performanceHudPresentedFrames;
var fps = (presentedFrames - _performanceHudLastPresentedFrames) / elapsedSeconds;
var hotName = hottestThreadId == 0
? "idle"
: GetPerformanceThreadName(hottestThreadId);
long guestBacklog;
int queuedGuestWork;
lock (_gate)
{
guestBacklog = Math.Max(
0,
_enqueuedGuestWorkSequence - _completedGuestWorkSequence);
queuedGuestWork = _pendingGuestWork.Count;
}
var gpuInFlight = _pendingGuestSubmissions.Count +
(_presentationInFlight ? 1 : 0);
var readCount = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
var readBytes = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
var readHits = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
var readPvmBytes = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
var readLibcBytes = Interlocked.Read(
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
var readsPerSecond =
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
var readMbPerSecond =
(readBytes - _performanceHudLastReadBytes) /
elapsedSeconds /
(1024.0 * 1024.0);
var readHitsPerSecond =
(readHits - _performanceHudLastReadHits) / elapsedSeconds;
var readPvmMbPerSecond =
(readPvmBytes - _performanceHudLastReadPvmBytes) /
elapsedSeconds /
(1024.0 * 1024.0);
var readLibcMbPerSecond =
(readLibcBytes - _performanceHudLastReadLibcBytes) /
elapsedSeconds /
(1024.0 * 1024.0);
_performanceHudLastReadCount = readCount;
_performanceHudLastReadBytes = readBytes;
_performanceHudLastReadHits = readHits;
_performanceHudLastReadPvmBytes = readPvmBytes;
_performanceHudLastReadLibcBytes = readLibcBytes;
_window.Title =
$"FPS {fps:0.0} CPU {processCpuPercent:0}% | " +
$"HOT {hotName}#{hottestThreadId} {hottestThreadPercent:0}% | " +
$"WORK {guestBacklog} (q{queuedGuestWork}/gpu{gpuInFlight}) | " +
$"RD {readsPerSecond:0}/s {readMbPerSecond:0}MB/s h{readHitsPerSecond:0}/s " +
$"P{readPvmMbPerSecond:0} L{readLibcMbPerSecond:0} | " +
VideoOutExports.GetWindowTitle();
_performanceHudLastPresentedFrames = presentedFrames;
}
_performanceHudThreadCpu.Clear();
foreach (var (threadId, cpu) in currentThreadCpu)
{
_performanceHudThreadCpu[threadId] = cpu;
}
foreach (var staleThreadId in _performanceHudThreadNames.Keys
.Where(threadId => !currentThreadIds.Contains(threadId))
.ToArray())
{
_performanceHudThreadNames.Remove(staleThreadId);
}
_performanceHudLastProcessCpu = processCpu;
_performanceHudLastTimestamp = now;
}
catch (Exception exception) when (
exception is InvalidOperationException or System.ComponentModel.Win32Exception)
{
_performanceHudLastTimestamp = now;
}
}
private string GetPerformanceThreadName(int threadId)
{
if (_performanceHudThreadNames.TryGetValue(threadId, out var cached))
{
return cached;
}
var name = "tid";
var handle = OpenThread(ThreadQueryLimitedInformation, false, (uint)threadId);
if (handle != 0)
{
try
{
if (GetThreadDescription(handle, out var description) >= 0 && description != 0)
{
try
{
var described = Marshal.PtrToStringUni(description);
if (!string.IsNullOrWhiteSpace(described))
{
name = described.Length <= 28 ? described : described[..28];
}
}
finally
{
LocalFree(description);
}
}
}
finally
{
CloseHandle(handle);
}
}
_performanceHudThreadNames[threadId] = name;
return name;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);
[DllImport("kernel32.dll")]
private static extern int GetThreadDescription(nint thread, out nint description);
[DllImport("kernel32.dll")]
private static extern nint LocalFree(nint memory);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
private void WaitForRenderWork()
{
var gpuWorkInFlight = _pendingGuestSubmissions.Count > 0 || _presentationInFlight;
lock (_gate)
{
if (_closed ||
_pendingGuestWork.Count > 0 ||
(_latestPresentation is { } latest &&
latest.Sequence != _presentedSequence &&
latest.RequiredGuestWorkSequence <= _completedGuestWorkSequence))
{
return;
}
System.Threading.Monitor.Wait(_gate, gpuWorkInFlight ? 1 : 8);
}
}
private void Render(double _)
{
if (!_vulkanReady)
{
return;
}
WaitForRenderWork();
UpdatePerformanceHud();
_commandBuffer = _presentationCommandBuffer;
if (!_deviceLost)
{
CollectCompletedGuestSubmissions(waitForOldest: false);
}
var completedWork = 0;
while (completedWork < MaxGuestWorkPerRender &&
TryTakeGuestWork(out var work))
{
try
{
switch (work)
{
case VulkanOffscreenGuestDraw offscreenDraw:
ExecuteOffscreenDraw(offscreenDraw);
break;
case VulkanComputeGuestDispatch computeDispatch:
ExecuteComputeDispatch(computeDispatch);
break;
}
}
finally
{
CompleteGuestWork();
}
completedWork++;
}
if (!TryTakePresentation(_presentedSequence, out var presentation))
{
return;
}
if (presentation.Pixels is null &&
presentation.DrawKind != GuestDrawKind.FullscreenBarycentric &&
presentation.TranslatedDraw is null &&
presentation.GuestImageAddress == 0)
{
return;
}
CompletePendingPresentation(wait: true);
byte[]? pixels = null;
if (presentation.Pixels is { } sourcePixels)
{
pixels = presentation.Width == _extent.Width && presentation.Height == _extent.Height
? sourcePixels
: ScaleBgra(
sourcePixels,
presentation.Width,
presentation.Height,
_extent.Width,
_extent.Height);
if ((ulong)pixels.Length > _stagingSize)
{
return;
}
}
TranslatedDrawResources? translatedResources = null;
GuestImageResource? presentedGuestImage = null;
if (presentation.GuestImageAddress != 0 &&
(!_guestImages.TryGetValue(
presentation.GuestImageAddress,
out presentedGuestImage) ||
!presentedGuestImage.Initialized))
{
return;
}
if (presentedGuestImage is not null)
{
_directPresentationCount++;
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
_directPresentationCount is 1 or 30 or 120)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " +
$"addr=0x{presentedGuestImage.Address:X16}");
TraceGuestImageContents(presentedGuestImage);
}
}
if (presentation.TranslatedDraw is { } translatedDraw)
{
try
{
translatedResources = CreateTranslatedDrawResources(
translatedDraw,
_renderPass,
_swapchainFormat,
_extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.translated_draw kind={presentation.DrawKind} " +
$"textures={translatedResources.Textures.Length}");
foreach (var boundTexture in translatedResources.Textures)
{
if (boundTexture.GuestImage is { } guestImage &&
_tracedGuestImageContents.Add(guestImage.Address))
{
TraceGuestImageContents(guestImage);
}
}
}
}
catch (Exception exception)
{
_presentedSequence = presentation.Sequence;
Console.Error.WriteLine(
$"[LOADER][ERROR] Vulkan VideoOut translated draw setup failed: {exception.Message}");
return;
}
}
uint imageIndex;
var acquireResult = _swapchainApi.AcquireNextImage(
_device,
_swapchain,
ulong.MaxValue,
_imageAvailable,
default,
&imageIndex);
if (acquireResult == Result.ErrorOutOfDateKhr)
{
RecreateSwapchainResources("vkAcquireNextImageKHR", acquireResult);
if (translatedResources is not null)
{
DestroyTranslatedDrawResources(translatedResources);
}
return;
}
CheckSwapchainResult(acquireResult, "vkAcquireNextImageKHR");
var recreateAfterPresent = acquireResult == Result.SuboptimalKhr;
if (pixels is not null)
{
void* mapped;
Check(
_vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped),
"vkMapMemory");
fixed (byte* source = pixels)
{
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
}
_vk.UnmapMemory(_device, _stagingMemory);
}
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
};
Check(_vk.BeginCommandBuffer(_commandBuffer, &beginInfo), "vkBeginCommandBuffer");
PipelineStageFlags waitStage;
if (pixels is not null)
{
RecordUpload(imageIndex);
waitStage = PipelineStageFlags.TransferBit;
}
else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric)
{
var clearValue = default(ClearValue);
var renderPassInfo = new RenderPassBeginInfo
{
SType = StructureType.RenderPassBeginInfo,
RenderPass = _renderPass,
Framebuffer = _framebuffers[imageIndex],
RenderArea = new Rect2D(new Offset2D(0, 0), _extent),
ClearValueCount = 1,
PClearValues = &clearValue,
};
_vk.CmdBeginRenderPass(
_commandBuffer,
&renderPassInfo,
SubpassContents.Inline);
_vk.CmdBindPipeline(
_commandBuffer,
PipelineBindPoint.Graphics,
_barycentricPipeline);
_vk.CmdDraw(_commandBuffer, 3, 1, 0, 0);
_vk.CmdEndRenderPass(_commandBuffer);
waitStage = PipelineStageFlags.ColorAttachmentOutputBit;
}
else if (presentedGuestImage is not null)
{
RecordGuestImageBlit(imageIndex, presentedGuestImage);
waitStage = PipelineStageFlags.TransferBit;
}
else if (translatedResources is not null)
{
RecordTranslatedDraw(imageIndex, translatedResources);
waitStage = PipelineStageFlags.AllCommandsBit;
}
else
{
throw new InvalidOperationException(
$"Unsupported translated guest draw: {presentation.DrawKind}.");
}
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer");
var imageAvailable = _imageAvailable;
var commandBuffer = _commandBuffer;
var renderFinished = _renderFinished;
var submitInfo = new SubmitInfo
{
SType = StructureType.SubmitInfo,
WaitSemaphoreCount = 1,
PWaitSemaphores = &imageAvailable,
PWaitDstStageMask = &waitStage,
CommandBufferCount = 1,
PCommandBuffers = &commandBuffer,
SignalSemaphoreCount = 1,
PSignalSemaphores = &renderFinished,
};
var presentationFence = _presentationFence;
Check(
_vk.ResetFences(_device, 1, &presentationFence),
"vkResetFences(presentation)");
Check(
_vk.QueueSubmit(_queue, 1, &submitInfo, presentationFence),
"vkQueueSubmit");
_presentationInFlight = true;
_pendingPresentationResources = translatedResources;
translatedResources = null;
var swapchain = _swapchain;
var presentInfo = new PresentInfoKHR
{
SType = StructureType.PresentInfoKhr,
WaitSemaphoreCount = 1,
PWaitSemaphores = &renderFinished,
SwapchainCount = 1,
PSwapchains = &swapchain,
PImageIndices = &imageIndex,
};
var presentResult = _swapchainApi.QueuePresent(_queue, &presentInfo);
if (presentResult == Result.ErrorOutOfDateKhr)
{
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false);
if (translatedResources is not null)
{
DestroyTranslatedDrawResources(translatedResources);
}
RecreateSwapchainResources("vkQueuePresentKHR", presentResult);
return;
}
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
VideoOutExports.ReportPresentedFrame();
_performanceHudPresentedFrames++;
if (_swapchainReadbackPending)
{
CompletePendingPresentation(wait: true);
TraceSwapchainReadback();
}
CollectCompletedGuestSubmissions(waitForOldest: false);
_imageInitialized[imageIndex] = true;
_presentedSequence = presentation.Sequence;
if (presentation.IsSplash && !_splashPresented)
{
_splashPresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented splash: " +
$"{presentation.Width}x{presentation.Height}");
}
else if (!presentation.IsSplash && !_firstFramePresented)
{
_firstFramePresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented first frame: " +
$"{presentation.Width}x{presentation.Height}");
}
if (pixels is null && !_firstGuestDrawPresented)
{
_firstGuestDrawPresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented guest frame: " +
(presentedGuestImage is not null
? $"image=0x{presentedGuestImage.Address:X16} " +
$"{presentedGuestImage.Width}x{presentedGuestImage.Height}"
: presentation.TranslatedDraw is null
? $"{presentation.DrawKind}"
: $"shader textures={presentation.TranslatedDraw.Textures.Count}"));
}
if (recreateAfterPresent)
{
RecreateSwapchainResources("present suboptimal", Result.SuboptimalKhr);
}
}
private void TraceGuestImageContents(GuestImageResource image)
{
var bytesPerPixel = GetReadbackBytesPerPixel(image.Format);
if (bytesPerPixel == 0)
{
TraceVulkanShader(
$"vk.guest_image addr=0x{image.Address:X16} " +
$"format={image.Format} readback=unsupported");
return;
}
var byteCount = checked((ulong)image.Width * image.Height * bytesPerPixel);
var buffer = CreateBuffer(
byteCount,
BufferUsageFlags.TransferDstBit,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out var memory);
try
{
Check(
_vk.ResetCommandBuffer(_commandBuffer, 0),
"vkResetCommandBuffer(guest readback)");
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
};
Check(
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
"vkBeginCommandBuffer(guest readback)");
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.ShaderReadBit,
DstAccessMask = AccessFlags.TransferReadBit,
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
NewLayout = ImageLayout.TransferSrcOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image.Image,
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.FragmentShaderBit |
PipelineStageFlags.ComputeShaderBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
var region = new BufferImageCopy
{
ImageSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ColorBit,
LayerCount = 1,
},
ImageExtent = new Extent3D(image.Width, image.Height, 1),
};
_vk.CmdCopyImageToBuffer(
_commandBuffer,
image.Image,
ImageLayout.TransferSrcOptimal,
buffer,
1,
&region);
var toShaderRead = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferReadBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.TransferSrcOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image.Image,
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
PipelineStageFlags.FragmentShaderBit |
PipelineStageFlags.ComputeShaderBit,
0,
0,
null,
0,
null,
1,
&toShaderRead);
Check(
_vk.EndCommandBuffer(_commandBuffer),
"vkEndCommandBuffer(guest readback)");
var commandBuffer = _commandBuffer;
var submitInfo = new SubmitInfo
{
SType = StructureType.SubmitInfo,
CommandBufferCount = 1,
PCommandBuffers = &commandBuffer,
};
Check(
_vk.QueueSubmit(_queue, 1, &submitInfo, default),
"vkQueueSubmit(guest readback)");
Check(
_vk.QueueWaitIdle(_queue),
"vkQueueWaitIdle(guest readback)");
void* mapped;
Check(
_vk.MapMemory(_device, memory, 0, byteCount, 0, &mapped),
"vkMapMemory(guest readback)");
try
{
var bytes = new ReadOnlySpan<byte>(mapped, checked((int)byteCount));
var nonzeroBytes = 0L;
ulong hash = 14695981039346656037UL;
foreach (var value in bytes)
{
nonzeroBytes += value == 0 ? 0 : 1;
hash = (hash ^ value) * 1099511628211UL;
}
var nonblackPixels = CountNonblackPixels(
bytes,
image.Format,
bytesPerPixel);
var centerOffset = checked(
((int)(image.Height / 2) * (int)image.Width +
(int)(image.Width / 2)) *
(int)bytesPerPixel);
var center = Convert.ToHexString(
bytes.Slice(centerOffset, (int)bytesPerPixel));
TraceVulkanShader(
$"vk.guest_image addr=0x{image.Address:X16} " +
$"size={image.Width}x{image.Height} format={image.Format} " +
$"nonzero_bytes={nonzeroBytes}/{byteCount} " +
$"nonblack_pixels={nonblackPixels}/{(ulong)image.Width * image.Height} " +
$"center={center} hash=0x{hash:X16}");
DumpGuestImageBytes(image, bytes);
}
finally
{
_vk.UnmapMemory(_device, memory);
}
}
finally
{
_vk.DestroyBuffer(_device, buffer, null);
_vk.FreeMemory(_device, memory, null);
}
}
private static void DumpGuestImageBytes(
GuestImageResource image,
ReadOnlySpan<byte> bytes)
{
var directory =
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_DUMP_DIR");
if (string.IsNullOrWhiteSpace(directory))
{
return;
}
Directory.CreateDirectory(directory);
var path = Path.Combine(
directory,
$"0x{image.Address:X16}-{image.Width}x{image.Height}-{image.Format}.rgba");
File.WriteAllBytes(path, bytes.ToArray());
}
private static uint GetReadbackBytesPerPixel(Format format) =>
format switch
{
Format.R8Unorm or
Format.R8Uint or
Format.R8Sint => 1,
Format.R32Uint or
Format.R32Sint or
Format.R32Sfloat or
Format.R16G16Uint or
Format.R16G16Sint or
Format.R16G16Sfloat or
Format.R8G8B8A8Uint or
Format.R8G8B8A8Sint or
Format.R8G8B8A8Unorm or
Format.A2R10G10B10UnormPack32 => 4,
Format.R16G16B16A16Uint or
Format.R16G16B16A16Sint or
Format.R16G16B16A16Sfloat => 8,
Format.R32G32Uint or
Format.R32G32Sint or
Format.R32G32Sfloat => 8,
Format.R32G32B32A32Uint or
Format.R32G32B32A32Sint or
Format.R32G32B32A32Sfloat => 16,
_ => 0,
};
private static long CountNonblackPixels(
ReadOnlySpan<byte> bytes,
Format format,
uint bytesPerPixel)
{
var count = 0L;
for (var offset = 0; offset < bytes.Length; offset += (int)bytesPerPixel)
{
var pixel = bytes.Slice(offset, (int)bytesPerPixel);
var hasColor = format switch
{
Format.A2R10G10B10UnormPack32 =>
(BitConverter.ToUInt32(pixel) & 0x3FFFFFFFu) != 0,
Format.R8G8B8A8Uint or
Format.R8G8B8A8Sint or
Format.R8G8B8A8Unorm =>
pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0,
Format.R16G16B16A16Uint or
Format.R16G16B16A16Sint or
Format.R16G16B16A16Sfloat =>
pixel[..6].IndexOfAnyExcept((byte)0) >= 0,
_ => pixel.IndexOfAnyExcept((byte)0) >= 0,
};
count += hasColor ? 1 : 0;
}
return count;
}
private void RecordTranslatedDraw(uint imageIndex, TranslatedDrawResources resources)
{
BeginDebugLabel(_commandBuffer, "SharpEmu swapchain draw");
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
RecordTranslatedGraphicsPass(
resources,
_renderPass,
_framebuffers[imageIndex],
_extent);
RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit);
EndDebugLabel(_commandBuffer);
}
private void RecordTextureUploads(
TranslatedDrawResources resources,
PipelineStageFlags shaderStage)
{
foreach (var texture in resources.Textures)
{
if (!texture.NeedsUpload)
{
continue;
}
var hasPriorContents = texture.GuestImage is { } guestImage &&
(guestImage.Initialized || guestImage.InitialUploadPending);
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = hasPriorContents
? AccessFlags.ShaderReadBit
: 0,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = hasPriorContents
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = texture.Image,
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
hasPriorContents
? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
var copyRegion = new BufferImageCopy
{
BufferRowLength = texture.RowLength > texture.Width
? texture.RowLength
: 0,
ImageSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ColorBit,
LayerCount = 1,
},
ImageExtent = new Extent3D(texture.Width, texture.Height, 1),
};
_vk.CmdCopyBufferToImage(
_commandBuffer,
texture.StagingBuffer,
texture.Image,
ImageLayout.TransferDstOptimal,
1,
&copyRegion);
var toShaderRead = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = texture.Image,
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
shaderStage,
0,
0,
null,
0,
null,
1,
&toShaderRead);
}
}
private void RecordStorageImagesForWrite(
TranslatedDrawResources resources,
PipelineStageFlags shaderStage)
{
var transitioned = new HashSet<GuestImageResource>();
foreach (var texture in resources.Textures)
{
if (!texture.IsStorage ||
texture.GuestImage is not { } guestImage ||
!transitioned.Add(guestImage))
{
continue;
}
var barrier = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask =
guestImage.Initialized || guestImage.InitialUploadPending
? AccessFlags.ShaderReadBit
: 0,
DstAccessMask =
AccessFlags.ShaderReadBit |
AccessFlags.ShaderWriteBit,
OldLayout =
guestImage.Initialized || guestImage.InitialUploadPending
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.General,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = guestImage.Image,
SubresourceRange = ColorSubresourceRange(0, guestImage.MipLevels),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
guestImage.Initialized || guestImage.InitialUploadPending
? shaderStage
: PipelineStageFlags.TopOfPipeBit,
shaderStage,
0,
0,
null,
0,
null,
1,
&barrier);
}
}
private void RecordStorageImagesForRead(
TranslatedDrawResources resources,
PipelineStageFlags shaderStage)
{
var transitioned = new HashSet<GuestImageResource>();
foreach (var texture in resources.Textures)
{
if (!texture.IsStorage ||
texture.GuestImage is not { } guestImage ||
!transitioned.Add(guestImage))
{
continue;
}
var barrier = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask =
AccessFlags.ShaderReadBit |
AccessFlags.ShaderWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.General,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = guestImage.Image,
SubresourceRange = ColorSubresourceRange(0, guestImage.MipLevels),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
shaderStage,
shaderStage,
0,
0,
null,
0,
null,
1,
&barrier);
}
}
private void MarkStorageImagesInitialized(
TranslatedDrawResources resources,
bool traceContents = true)
{
List<GuestImageResource>? traceImages = null;
lock (_gate)
{
foreach (var texture in resources.Textures)
{
if (!texture.IsStorage ||
texture.Address == 0 ||
texture.GuestImage is not { } guestImage)
{
continue;
}
guestImage.Initialized = true;
guestImage.InitialUploadPending = false;
var format = GetGuestTextureFormat(guestImage.Format);
if (format != 0)
{
_availableGuestImages[texture.Address] = format;
_gpuGuestImages[texture.Address] = format;
}
if (traceContents &&
ShouldTraceGuestImageContents(guestImage))
{
traceImages ??= [];
traceImages.Add(guestImage);
}
}
}
if (traceImages is null)
{
return;
}
foreach (var image in traceImages)
{
TraceGuestImageContents(image);
}
}
private static void MarkSampledImagesInitialized(
TranslatedDrawResources resources)
{
lock (_gate)
{
foreach (var texture in resources.Textures)
{
if (!texture.NeedsUpload ||
texture.IsStorage ||
texture.Address == 0 ||
texture.GuestImage is not { } guestImage)
{
continue;
}
guestImage.Initialized = true;
guestImage.InitialUploadPending = false;
if (texture.UpdatesCpuContent)
{
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
}
}
}
}
private bool ShouldTraceGuestImageContents(GuestImageResource image)
{
if (image.Address == 0)
{
return false;
}
var addressMatched = ShouldTraceGuestImageAddressForDiagnostics(image.Address);
var broadTrace =
ShouldTraceGuestImageContentsForDiagnostics() &&
image.Width >= 1280 &&
image.Height >= 720;
return (addressMatched || broadTrace) &&
_tracedGuestImageContents.Add(image.Address);
}
private static bool ShouldTraceGuestImageContentsForDiagnostics() =>
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES"),
"1",
StringComparison.Ordinal);
private static bool ShouldTraceGuestImageAddressForDiagnostics(ulong address)
{
return AddressListContains(
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS",
address);
}
private static bool ShouldTraceGuestImageWriteForDiagnostics(ulong address)
{
return AddressListContains(
"SHARPEMU_TRACE_GUEST_WRITES",
address);
}
private static bool AddressListContains(
string environmentVariable,
ulong address)
{
var addresses = Environment.GetEnvironmentVariable(environmentVariable);
if (string.IsNullOrWhiteSpace(addresses))
{
return false;
}
foreach (var token in addresses.Split(
[',', ';', ' ', '\t'],
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (token == "*")
{
return true;
}
var span = token.AsSpan();
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
span = span[2..];
}
if (ulong.TryParse(
span,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture,
out var parsed) &&
parsed == address)
{
return true;
}
}
return false;
}
private static bool ShouldTraceVulkanResources() =>
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
"1",
StringComparison.Ordinal);
private void RecordTranslatedGraphicsPass(
TranslatedDrawResources resources,
RenderPass renderPass,
Framebuffer framebuffer,
Extent2D extent)
{
var clearValue = default(ClearValue);
var renderPassInfo = new RenderPassBeginInfo
{
SType = StructureType.RenderPassBeginInfo,
RenderPass = renderPass,
Framebuffer = framebuffer,
RenderArea = new Rect2D(new Offset2D(0, 0), extent),
ClearValueCount = 1,
PClearValues = &clearValue,
};
_vk.CmdBeginRenderPass(
_commandBuffer,
&renderPassInfo,
SubpassContents.Inline);
_vk.CmdBindPipeline(
_commandBuffer,
PipelineBindPoint.Graphics,
resources.Pipeline);
if (resources.DescriptorSet.Handle != 0)
{
var descriptorSet = resources.DescriptorSet;
_vk.CmdBindDescriptorSets(
_commandBuffer,
PipelineBindPoint.Graphics,
resources.PipelineLayout,
0,
1,
&descriptorSet,
0,
null);
}
var drawScissor = ClampScissor(resources.Scissor, extent);
if (drawScissor.Width == 0 || drawScissor.Height == 0)
{
_vk.CmdEndRenderPass(_commandBuffer);
return;
}
var drawViewport = ClampViewport(resources.Viewport, extent);
_vk.CmdSetViewport(_commandBuffer, 0, 1, &drawViewport);
if (resources.VertexBuffers.Length != 0)
{
var buffers = stackalloc VkBuffer[resources.VertexBuffers.Length];
var offsets = stackalloc ulong[resources.VertexBuffers.Length];
for (var index = 0; index < resources.VertexBuffers.Length; index++)
{
buffers[index] = resources.VertexBuffers[index].Buffer;
offsets[index] = GetVertexBindingOffset(resources.VertexBuffers[index]);
}
_vk.CmdBindVertexBuffers(
_commandBuffer,
0,
(uint)resources.VertexBuffers.Length,
buffers,
offsets);
}
var scissor = new Rect2D(
new Offset2D(drawScissor.X, drawScissor.Y),
new Extent2D(drawScissor.Width, drawScissor.Height));
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
if (resources.IndexBuffer.Handle != 0)
{
_vk.CmdBindIndexBuffer(
_commandBuffer,
resources.IndexBuffer,
0,
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
_vk.CmdDrawIndexed(
_commandBuffer,
resources.VertexCount,
resources.InstanceCount,
0,
0,
0);
}
else
{
_vk.CmdDraw(
_commandBuffer,
resources.VertexCount,
resources.InstanceCount,
0,
0);
}
_vk.CmdEndRenderPass(_commandBuffer);
}
private void DestroyTranslatedDrawResources(TranslatedDrawResources resources)
{
foreach (var texture in resources.Textures)
{
if (texture is null)
{
continue;
}
if (texture.OwnsStorage && texture.View.Handle != 0)
{
_vk.DestroyImageView(_device, texture.View, null);
}
if (texture.OwnsStorage && texture.Image.Handle != 0)
{
_vk.DestroyImage(_device, texture.Image, null);
}
if (texture.OwnsStorage && texture.ImageMemory.Handle != 0)
{
_vk.FreeMemory(_device, texture.ImageMemory, null);
}
if (texture.StagingBuffer.Handle != 0)
{
_vk.DestroyBuffer(_device, texture.StagingBuffer, null);
}
if (texture.StagingMemory.Handle != 0)
{
_vk.FreeMemory(_device, texture.StagingMemory, null);
}
if (texture.NeedsUpload &&
texture.GuestImage is { Initialized: false } guestImage)
{
guestImage.InitialUploadPending = false;
}
}
foreach (var globalBuffer in resources.GlobalMemoryBuffers)
{
if (globalBuffer is null)
{
continue;
}
RecycleHostBuffer(globalBuffer.Buffer, globalBuffer.Memory);
}
foreach (var vertexBuffer in resources.VertexBuffers)
{
if (vertexBuffer is null)
{
continue;
}
RecycleHostBuffer(vertexBuffer.Buffer, vertexBuffer.Memory);
}
RecycleHostBuffer(resources.IndexBuffer, resources.IndexMemory);
if (!resources.PipelineCached && resources.Pipeline.Handle != 0)
{
_vk.DestroyPipeline(_device, resources.Pipeline, null);
}
if (resources.DescriptorPool.Handle != 0)
{
_vk.DestroyDescriptorPool(_device, resources.DescriptorPool, null);
}
if (!resources.DescriptorLayoutCached &&
resources.PipelineLayout.Handle != 0)
{
_vk.DestroyPipelineLayout(_device, resources.PipelineLayout, null);
}
if (!resources.DescriptorLayoutCached &&
resources.DescriptorSetLayout.Handle != 0)
{
_vk.DestroyDescriptorSetLayout(_device, resources.DescriptorSetLayout, null);
}
}
private void RecordUpload(uint imageIndex)
{
var oldLayout = _imageInitialized[imageIndex]
? ImageLayout.PresentSrcKhr
: ImageLayout.Undefined;
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = _imageInitialized[imageIndex] ? AccessFlags.MemoryReadBit : 0,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = oldLayout,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
_imageInitialized[imageIndex]
? PipelineStageFlags.BottomOfPipeBit
: PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
var copyRegion = new BufferImageCopy
{
ImageSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ColorBit,
LayerCount = 1,
},
ImageExtent = new Extent3D(_extent.Width, _extent.Height, 1),
};
_vk.CmdCopyBufferToImage(
_commandBuffer,
_stagingBuffer,
_swapchainImages[imageIndex],
ImageLayout.TransferDstOptimal,
1,
&copyRegion);
var toPresent = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.MemoryReadBit,
OldLayout = ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.PresentSrcKhr,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
PipelineStageFlags.BottomOfPipeBit,
0,
0,
null,
0,
null,
1,
&toPresent);
}
private void RecordGuestImageBlit(
uint imageIndex,
GuestImageResource source)
{
var traceDestination =
ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_tracedPresentedSwapchain;
_tracedPresentedSwapchain |= traceDestination;
BeginDebugLabel(
_commandBuffer,
$"SharpEmu present image 0x{source.Address:X16}");
var sourceToTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.ShaderReadBit,
DstAccessMask = AccessFlags.TransferReadBit,
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
NewLayout = ImageLayout.TransferSrcOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = source.Image,
SubresourceRange = ColorSubresourceRange(),
};
var destinationToTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = _imageInitialized[imageIndex]
? AccessFlags.MemoryReadBit
: 0,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = _imageInitialized[imageIndex]
? ImageLayout.PresentSrcKhr
: ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
var barriers = stackalloc ImageMemoryBarrier[2];
barriers[0] = sourceToTransfer;
barriers[1] = destinationToTransfer;
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.AllCommandsBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
2,
barriers);
var sourceOffsets = new ImageBlit.SrcOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(
checked((int)source.Width),
checked((int)source.Height),
1),
};
var destinationOffsets = new ImageBlit.DstOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(
checked((int)_extent.Width),
checked((int)_extent.Height),
1),
};
var region = new ImageBlit
{
SrcSubresource = new ImageSubresourceLayers(
ImageAspectFlags.ColorBit,
0,
0,
1),
SrcOffsets = sourceOffsets,
DstSubresource = new ImageSubresourceLayers(
ImageAspectFlags.ColorBit,
0,
0,
1),
DstOffsets = destinationOffsets,
};
_vk.CmdBlitImage(
_commandBuffer,
source.Image,
ImageLayout.TransferSrcOptimal,
_swapchainImages[imageIndex],
ImageLayout.TransferDstOptimal,
1,
&region,
Filter.Nearest);
if (traceDestination)
{
var destinationToReadback = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.TransferReadBit,
OldLayout = ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.TransferSrcOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&destinationToReadback);
var copyRegion = new BufferImageCopy
{
ImageSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ColorBit,
LayerCount = 1,
},
ImageExtent = new Extent3D(_extent.Width, _extent.Height, 1),
};
_vk.CmdCopyImageToBuffer(
_commandBuffer,
_swapchainImages[imageIndex],
ImageLayout.TransferSrcOptimal,
_stagingBuffer,
1,
&copyRegion);
_swapchainReadbackPending = true;
}
var sourceToShaderRead = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferReadBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.TransferSrcOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = source.Image,
SubresourceRange = ColorSubresourceRange(),
};
var destinationToPresent = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = traceDestination
? AccessFlags.TransferReadBit
: AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.MemoryReadBit,
OldLayout = traceDestination
? ImageLayout.TransferSrcOptimal
: ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.PresentSrcKhr,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
barriers[0] = sourceToShaderRead;
barriers[1] = destinationToPresent;
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
PipelineStageFlags.AllCommandsBit,
0,
0,
null,
0,
null,
2,
barriers);
EndDebugLabel(_commandBuffer);
}
private void TraceSwapchainReadback()
{
_swapchainReadbackPending = false;
var byteCount = checked((ulong)_extent.Width * _extent.Height * 4);
void* mapped;
Check(
_vk.MapMemory(_device, _stagingMemory, 0, byteCount, 0, &mapped),
"vkMapMemory(swapchain readback)");
try
{
var bytes = new ReadOnlySpan<byte>(mapped, checked((int)byteCount));
var nonzeroBytes = 0L;
var nonblackPixels = 0L;
ulong hash = 14695981039346656037UL;
for (var offset = 0; offset < bytes.Length; offset += 4)
{
var b0 = bytes[offset];
var b1 = bytes[offset + 1];
var b2 = bytes[offset + 2];
var b3 = bytes[offset + 3];
nonzeroBytes += b0 == 0 ? 0 : 1;
nonzeroBytes += b1 == 0 ? 0 : 1;
nonzeroBytes += b2 == 0 ? 0 : 1;
nonzeroBytes += b3 == 0 ? 0 : 1;
nonblackPixels += b0 != 0 || b1 != 0 || b2 != 0 ? 1 : 0;
hash = (hash ^ b0) * 1099511628211UL;
hash = (hash ^ b1) * 1099511628211UL;
hash = (hash ^ b2) * 1099511628211UL;
hash = (hash ^ b3) * 1099511628211UL;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.swapchain_image size={_extent.Width}x{_extent.Height} " +
$"format={_swapchainFormat} nonzero_bytes={nonzeroBytes}/{byteCount} " +
$"nonblack_pixels={nonblackPixels}/{(ulong)_extent.Width * _extent.Height} " +
$"hash=0x{hash:X16}");
}
finally
{
_vk.UnmapMemory(_device, _stagingMemory);
}
}
private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities)
{
if (capabilities.CurrentExtent.Width != uint.MaxValue)
{
var fallbackWidth = _extent.Width != 0
? _extent.Width
: DefaultWindowWidth;
var fallbackHeight = _extent.Height != 0
? _extent.Height
: DefaultWindowHeight;
return new Extent2D(
ClampSurfaceExtent(
capabilities.CurrentExtent.Width,
fallbackWidth,
capabilities.MinImageExtent.Width,
capabilities.MaxImageExtent.Width),
ClampSurfaceExtent(
capabilities.CurrentExtent.Height,
fallbackHeight,
capabilities.MinImageExtent.Height,
capabilities.MaxImageExtent.Height));
}
var size = _window.FramebufferSize;
return new Extent2D(
ClampSurfaceExtent(
(uint)Math.Max(size.X, 1),
DefaultWindowWidth,
capabilities.MinImageExtent.Width,
capabilities.MaxImageExtent.Width),
ClampSurfaceExtent(
(uint)Math.Max(size.Y, 1),
DefaultWindowHeight,
capabilities.MinImageExtent.Height,
capabilities.MaxImageExtent.Height));
}
private static uint ClampSurfaceExtent(
uint value,
uint fallback,
uint minimum,
uint maximum)
{
value = value <= 1 && fallback > 1 ? fallback : value;
minimum = Math.Max(minimum, 1u);
maximum = Math.Max(maximum, minimum);
return Math.Clamp(value, minimum, maximum);
}
private static SurfaceFormatKHR ChooseSurfaceFormat(IReadOnlyList<SurfaceFormatKHR> formats)
{
foreach (var format in formats)
{
if (format.Format is Format.B8G8R8A8Srgb or Format.B8G8R8A8Unorm &&
format.ColorSpace == ColorSpaceKHR.SpaceSrgbNonlinearKhr)
{
return format;
}
}
return formats.Count > 0
? formats[0]
: throw new InvalidOperationException("The Vulkan surface exposes no pixel formats.");
}
private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKHR supported)
{
foreach (var candidate in new[]
{
CompositeAlphaFlagsKHR.OpaqueBitKhr,
CompositeAlphaFlagsKHR.PreMultipliedBitKhr,
CompositeAlphaFlagsKHR.PostMultipliedBitKhr,
CompositeAlphaFlagsKHR.InheritBitKhr,
})
{
if ((supported & candidate) != 0)
{
return candidate;
}
}
throw new InvalidOperationException("The Vulkan surface exposes no composite alpha mode.");
}
private static ImageSubresourceRange ColorSubresourceRange(
uint baseMipLevel = 0,
uint levelCount = 1) =>
new()
{
AspectMask = ImageAspectFlags.ColorBit,
BaseMipLevel = baseMipLevel,
LevelCount = levelCount,
LayerCount = 1,
};
private static byte[] ScaleBgra(byte[] source, uint sourceWidth, uint sourceHeight, uint width, uint height)
{
var destination = new byte[checked((int)(width * height * 4))];
for (uint y = 0; y < height; y++)
{
var sourceY = (uint)(((ulong)y * sourceHeight) / height);
for (uint x = 0; x < width; x++)
{
var sourceX = (uint)(((ulong)x * sourceWidth) / width);
var sourceOffset = checked((int)(((ulong)sourceY * sourceWidth + sourceX) * 4));
var destinationOffset = checked((int)(((ulong)y * width + x) * 4));
source.AsSpan(sourceOffset, 4).CopyTo(destination.AsSpan(destinationOffset, 4));
}
}
return destination;
}
private void DisposeVulkan()
{
if (!_vulkanReady)
{
return;
}
if (_debugUtils is not null && _debugMessenger.Handle != 0)
{
_debugUtils.DestroyDebugUtilsMessenger(_instance, _debugMessenger, null);
}
_vulkanReady = false;
_vk.DeviceWaitIdle(_device);
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false);
SavePipelineCache();
foreach (var pipeline in _computePipelines.Values)
{
_vk.DestroyPipeline(_device, pipeline, null);
}
_computePipelines.Clear();
foreach (var pipeline in _graphicsPipelines.Values)
{
_vk.DestroyPipeline(_device, pipeline, null);
}
_graphicsPipelines.Clear();
if (_pipelineCache.Handle != 0)
{
_vk.DestroyPipelineCache(_device, _pipelineCache, null);
_pipelineCache = default;
}
foreach (var layout in _descriptorLayouts.Values)
{
_vk.DestroyPipelineLayout(_device, layout.PipelineLayout, null);
if (layout.DescriptorSetLayout.Handle != 0)
{
_vk.DestroyDescriptorSetLayout(
_device,
layout.DescriptorSetLayout,
null);
}
}
_descriptorLayouts.Clear();
foreach (var sampler in _samplers.Values)
{
_vk.DestroySampler(_device, sampler, null);
}
_samplers.Clear();
_shaderDigests.Clear();
foreach (var allocation in _hostBufferAllocations.Values)
{
_vk.DestroyBuffer(_device, allocation.Buffer, null);
_vk.FreeMemory(_device, allocation.Memory, null);
}
_hostBufferAllocations.Clear();
_hostBufferPool.Clear();
foreach (var guestImage in _guestImages.Values)
{
DestroyGuestImage(guestImage);
}
_guestImages.Clear();
lock (_gate)
{
_availableGuestImages.Clear();
_gpuGuestImages.Clear();
}
DestroySwapchainResources();
if (_device.Handle != 0)
{
_vk.DestroyDevice(_device, null);
_device = default;
}
if (_surface.Handle != 0)
{
_surfaceApi.DestroySurface(_instance, _surface, null);
_surface = default;
}
if (_instance.Handle != 0)
{
_vk.DestroyInstance(_instance, null);
_instance = default;
}
}
private void RecreateSwapchainResources(string operation, Result result)
{
if (_device.Handle == 0)
{
return;
}
Check(
_surfaceApi.GetPhysicalDeviceSurfaceCapabilities(
_physicalDevice,
_surface,
out var capabilities),
"vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
var framebufferSize = _window.FramebufferSize;
var hasFixedExtent = capabilities.CurrentExtent.Width != uint.MaxValue;
var surfaceWidth = hasFixedExtent
? capabilities.CurrentExtent.Width
: (uint)Math.Max(framebufferSize.X, 0);
var surfaceHeight = hasFixedExtent
? capabilities.CurrentExtent.Height
: (uint)Math.Max(framebufferSize.Y, 0);
if (surfaceWidth <= 1 || surfaceHeight <= 1)
{
if (!_swapchainRecreateDeferred)
{
_swapchainRecreateDeferred = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut deferred swapchain recreation: " +
$"surface={surfaceWidth}x{surfaceHeight}");
}
return;
}
_swapchainRecreateDeferred = false;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}");
_vk.DeviceWaitIdle(_device);
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false);
DestroySwapchainResources();
CreateSwapchain();
CreateCommandResources();
CreateGuestDrawResources();
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut recreated swapchain: " +
$"{_extent.Width}x{_extent.Height}, format={_swapchainFormat}");
}
private void DestroySwapchainResources()
{
if (_stagingBuffer.Handle != 0)
{
_vk.DestroyBuffer(_device, _stagingBuffer, null);
_stagingBuffer = default;
}
if (_stagingMemory.Handle != 0)
{
_vk.FreeMemory(_device, _stagingMemory, null);
_stagingMemory = default;
_stagingSize = 0;
}
if (_imageAvailable.Handle != 0)
{
_vk.DestroySemaphore(_device, _imageAvailable, null);
_imageAvailable = default;
}
if (_renderFinished.Handle != 0)
{
_vk.DestroySemaphore(_device, _renderFinished, null);
_renderFinished = default;
}
if (_presentationFence.Handle != 0)
{
_vk.DestroyFence(_device, _presentationFence, null);
_presentationFence = default;
}
if (_barycentricPipeline.Handle != 0)
{
_vk.DestroyPipeline(_device, _barycentricPipeline, null);
_barycentricPipeline = default;
}
if (_pipelineLayout.Handle != 0)
{
_vk.DestroyPipelineLayout(_device, _pipelineLayout, null);
_pipelineLayout = default;
}
foreach (var framebuffer in _framebuffers)
{
if (framebuffer.Handle != 0)
{
_vk.DestroyFramebuffer(_device, framebuffer, null);
}
}
if (_renderPass.Handle != 0)
{
_vk.DestroyRenderPass(_device, _renderPass, null);
_renderPass = default;
}
foreach (var imageView in _swapchainImageViews)
{
if (imageView.Handle != 0)
{
_vk.DestroyImageView(_device, imageView, null);
}
}
if (_commandPool.Handle != 0)
{
_vk.DestroyCommandPool(_device, _commandPool, null);
_commandPool = default;
_commandBuffer = default;
_presentationCommandBuffer = default;
}
if (_swapchain.Handle != 0)
{
_swapchainApi.DestroySwapchain(_device, _swapchain, null);
_swapchain = default;
}
_swapchainImages = [];
_swapchainImageViews = [];
_framebuffers = [];
_imageInitialized = [];
}
private static void CheckSwapchainResult(Result result, string operation)
{
if (result is Result.Success or Result.SuboptimalKhr)
{
return;
}
throw new InvalidOperationException($"{operation} failed with {result}.");
}
private static void Check(Result result, string operation)
{
if (result != Result.Success)
{
throw new InvalidOperationException($"{operation} failed with {result}.");
}
}
private bool TryMarkDeviceLost(Exception exception)
{
if (!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
{
return false;
}
_deviceLost = true;
if (!_deviceLostLogged)
{
_deviceLostLogged = true;
Console.Error.WriteLine(
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
exception.Message);
}
return true;
}
private static void TraceVulkanShader(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
}
}
}