[VideoOut] Add Bink2 support via FFMPEG bridge (#527)

* [VideoOut] Add Bink2 support via FFMPEG bridge

* [CMake] update commit

* [CMake] update commit
This commit is contained in:
Berk
2026-07-22 21:41:41 +03:00
committed by GitHub
parent d3600c9255
commit f704586a8d
22 changed files with 3087 additions and 310 deletions
@@ -2230,7 +2230,6 @@ internal static unsafe class VulkanVideoPresenter
if (IsGuestWorkCompletedLocked(pending.RequiredGuestWorkSequence))
{
presentation = _pendingGuestImagePresentations.Dequeue();
TryReplaceWithBinkFrame(ref presentation);
return true;
}
@@ -2263,29 +2262,10 @@ internal static unsafe class VulkanVideoPresenter
}
presentation = latest;
TryReplaceWithBinkFrame(ref presentation);
return true;
}
}
private static void TryReplaceWithBinkFrame(ref Presentation presentation)
{
if (!Bink2MovieBridge.TryDecodeNextFrame(out var pixels, out var width, out var height))
{
return;
}
presentation = new Presentation(
pixels,
width,
height,
presentation.Sequence,
GuestDrawKind.None,
TranslatedDraw: null,
presentation.RequiredGuestWorkSequence,
IsSplash: false);
}
private static readonly HashSet<long> _tracedGuestImagePresentRejections = new();
private static bool HasPendingGuestPresentation(long presentedSequence)
@@ -2835,6 +2815,40 @@ internal static unsafe class VulkanVideoPresenter
private VkBuffer _stagingBuffer;
private DeviceMemory _stagingMemory;
private ulong _stagingSize;
private VkBuffer[] _frameUploadBuffers = [];
private DeviceMemory[] _frameUploadMemory = [];
private nint[] _frameUploadMapped = [];
private Image _hostMovieImage;
private DeviceMemory _hostMovieImageMemory;
private ImageView _hostMovieImageView;
private uint _hostMovieImageWidth;
private uint _hostMovieImageHeight;
private Format _hostMovieImageFormat;
private uint _hostMovieImageDstSelect;
private bool _hostMovieImageInitialized;
private Image _hostMovieChromaImage;
private DeviceMemory _hostMovieChromaImageMemory;
private ImageView _hostMovieChromaImageView;
private uint _hostMovieChromaImageWidth;
private uint _hostMovieChromaImageHeight;
private uint _hostMovieChromaImageDstSelect;
private bool _hostMovieChromaImageInitialized;
private byte[]? _hostMovieFramePixels;
private byte[]? _hostMovieLumaPixels;
private byte[]? _hostMovieChromaPixels;
private uint _hostMovieFrameWidth;
private uint _hostMovieFrameHeight;
private long _hostMovieFrameSerial;
private long _hostMovieConvertedFrameSerial = -1;
private long _hostMovieLumaUploadedFrameSerial = -1;
private long _hostMovieChromaUploadedFrameSerial = -1;
private string? _hostMovieFramePath;
private ulong _hostMovieLumaTextureAddress;
private ulong _hostMovieChromaTextureAddress;
private uint _hostMovieLumaDstSelect;
private uint _hostMovieChromaDstSelect;
private readonly HashSet<string> _tracedHostMovieTextureBindings =
new(StringComparer.Ordinal);
// Perf overlay: CPU-rasterized panel copied through per-slot staging
// buffers into one image, then blitted onto the swapchain.
private Image _overlayImage;
@@ -3027,6 +3041,9 @@ internal static unsafe class VulkanVideoPresenter
public bool OwnsStorage;
public bool IsStorage;
public bool Cached;
public bool IsHostMovie;
public int HostMoviePlane = -1;
public long HostMovieFrameSerial;
public ulong CpuContentFingerprint;
public bool UpdatesCpuContent;
public GuestSampler SamplerState;
@@ -4328,6 +4345,7 @@ internal static unsafe class VulkanVideoPresenter
var surfaceFormat = ChooseSurfaceFormat(formats);
_swapchainFormat = surfaceFormat.Format;
_extent = ChooseExtent(capabilities);
Bink2MovieBridge.SetPresentationSize(_extent.Width, _extent.Height);
var presentMode = ChoosePresentMode();
var imageCount = capabilities.MinImageCount + 1;
if (capabilities.MaxImageCount != 0)
@@ -4474,6 +4492,7 @@ internal static unsafe class VulkanVideoPresenter
_presentationCommandBuffer = _commandBuffer;
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
CreateFrameUploadBuffers(_stagingSize);
CreateOverlayResources();
}
@@ -6076,10 +6095,15 @@ internal static unsafe class VulkanVideoPresenter
}
}
var hostMovieTextures = FindHostMovieTextureBindings(draw.Textures);
for (var index = 0; index < draw.Textures.Count; index++)
{
var texture = draw.Textures[index];
var resolved = ResolveTextureResource(texture);
var resolved = index == hostMovieTextures.Luma
? CreateHostMovieTextureResource(texture, plane: 0)
: index == hostMovieTextures.Chroma
? CreateHostMovieTextureResource(texture, plane: 1)
: ResolveTextureResource(texture);
var feedbackTarget = !texture.IsStorage
? feedbackTargets?.FirstOrDefault(target =>
ReferenceEquals(resolved.GuestImage, target))
@@ -6934,6 +6958,343 @@ internal static unsafe class VulkanVideoPresenter
}
}
private void PumpHostMovieFrame()
{
if (!Bink2MovieBridge.TryDecodeNextFrame(
advanceClock: _hostMovieLumaTextureAddress != 0 &&
_hostMovieChromaTextureAddress != 0,
out var pixels,
out var width,
out var height,
out var advanced,
out var frameSerial,
out var hostPath))
{
// Keep the last decoded image until a replacement arrives.
// Movie sessions are queued asynchronously; clearing here
// exposes the guest decoder's neutral surfaces between the
// final frame of one movie and the first frame of the next.
return;
}
if (!string.Equals(
_hostMovieFramePath,
hostPath,
StringComparison.OrdinalIgnoreCase))
{
_hostMovieFramePath = hostPath;
_hostMovieLumaTextureAddress = 0;
_hostMovieChromaTextureAddress = 0;
_hostMovieLumaDstSelect = 0;
_hostMovieChromaDstSelect = 0;
_hostMovieConvertedFrameSerial = -1;
_hostMovieLumaUploadedFrameSerial = -1;
_hostMovieChromaUploadedFrameSerial = -1;
}
if (!advanced && _hostMovieFramePixels is not null)
{
return;
}
_hostMovieFramePixels = pixels;
_hostMovieFrameWidth = width;
_hostMovieFrameHeight = height;
_hostMovieFrameSerial = frameSerial;
}
private readonly record struct HostMovieTextureBindings(int Luma, int Chroma)
{
public static HostMovieTextureBindings None { get; } = new(-1, -1);
}
private HostMovieTextureBindings FindHostMovieTextureBindings(
IReadOnlyList<GuestDrawTexture> textures)
{
if (_hostMovieFramePixels is null ||
_hostMovieFrameWidth == 0 ||
_hostMovieFrameHeight == 0)
{
return HostMovieTextureBindings.None;
}
if (_hostMovieLumaTextureAddress != 0 &&
_hostMovieChromaTextureAddress != 0)
{
var lumaIndex = -1;
var chromaIndex = -1;
for (var index = 0; index < textures.Count; index++)
{
if (textures[index].Address == _hostMovieLumaTextureAddress)
{
lumaIndex = index;
}
else if (textures[index].Address == _hostMovieChromaTextureAddress)
{
chromaIndex = index;
}
}
if (lumaIndex >= 0 && chromaIndex >= 0)
{
return RememberHostMovieTextureMappings(
textures,
lumaIndex,
chromaIndex);
}
// Bluepoint alternates decoder output between multiple Y/UV
// surface pairs. Fall through and discover the active pair
// instead of sampling the stale guest surface on every other
// movie draw.
}
var bestLumaIndex = -1;
var bestChromaIndex = -1;
ulong bestArea = 0;
for (var lumaIndex = 0; lumaIndex < textures.Count; lumaIndex++)
{
var luma = textures[lumaIndex];
if (!IsHostMovieLumaCandidate(luma))
{
continue;
}
for (var chromaIndex = 0; chromaIndex < textures.Count; chromaIndex++)
{
var chroma = textures[chromaIndex];
if (!IsHostMovieChromaCandidate(luma, chroma))
{
continue;
}
var area = (ulong)luma.Width * luma.Height;
if (bestLumaIndex < 0 || area > bestArea)
{
bestLumaIndex = lumaIndex;
bestChromaIndex = chromaIndex;
bestArea = area;
}
}
}
if (bestLumaIndex < 0 || bestChromaIndex < 0)
{
return HostMovieTextureBindings.None;
}
var lumaTexture = textures[bestLumaIndex];
var chromaTexture = textures[bestChromaIndex];
_hostMovieLumaTextureAddress = lumaTexture.Address;
_hostMovieChromaTextureAddress = chromaTexture.Address;
_hostMovieLumaDstSelect = lumaTexture.DstSelect;
_hostMovieChromaDstSelect = chromaTexture.DstSelect;
var traceKey =
$"{_hostMovieFramePath}|{lumaTexture.Address:X16}|{chromaTexture.Address:X16}";
if (_tracedHostMovieTextureBindings.Add(traceKey))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Bink2 YUV textures bound: " +
$"{Path.GetFileName(_hostMovieFramePath)} " +
$"y={bestLumaIndex}:0x{lumaTexture.Address:X16}:" +
$"{lumaTexture.Width}x{lumaTexture.Height}:dst=0x{lumaTexture.DstSelect:X} " +
$"uv={bestChromaIndex}:0x{chromaTexture.Address:X16}:" +
$"{chromaTexture.Width}x{chromaTexture.Height}:dst=0x{chromaTexture.DstSelect:X} " +
$"host={_hostMovieFrameWidth}x{_hostMovieFrameHeight}.");
}
return new HostMovieTextureBindings(bestLumaIndex, bestChromaIndex);
}
private HostMovieTextureBindings RememberHostMovieTextureMappings(
IReadOnlyList<GuestDrawTexture> textures,
int lumaIndex,
int chromaIndex)
{
_hostMovieLumaDstSelect = textures[lumaIndex].DstSelect;
_hostMovieChromaDstSelect = textures[chromaIndex].DstSelect;
return new HostMovieTextureBindings(lumaIndex, chromaIndex);
}
private bool IsHostMovieLumaCandidate(GuestDrawTexture texture)
{
if (texture.Address == 0 ||
texture.IsStorage ||
texture.IsFallback ||
texture.ArrayedView ||
texture.ArrayLayers > 1 ||
texture.Format != 1 ||
texture.NumberType != 0 ||
texture.Width < 1280 ||
texture.Height < 720)
{
return false;
}
var guestAspect = (ulong)texture.Width * _hostMovieFrameHeight;
var hostAspect = (ulong)texture.Height * _hostMovieFrameWidth;
var difference = guestAspect > hostAspect
? guestAspect - hostAspect
: hostAspect - guestAspect;
return difference * 100 <= Math.Max(guestAspect, hostAspect) * 2;
}
private static bool IsHostMovieChromaCandidate(
GuestDrawTexture luma,
GuestDrawTexture chroma)
{
return chroma.Address != 0 &&
chroma.Address != luma.Address &&
!chroma.IsStorage &&
!chroma.IsFallback &&
!chroma.ArrayedView &&
chroma.ArrayLayers <= 1 &&
chroma.Format == 3 &&
chroma.NumberType == 0 &&
luma.Width == chroma.Width * 2 &&
luma.Height == chroma.Height * 2;
}
private TextureResource CreateHostMovieTextureResource(
GuestDrawTexture texture,
int plane)
{
EnsureHostMovieYuvFrame();
var isLuma = plane == 0;
var pixels = isLuma ? _hostMovieLumaPixels! : _hostMovieChromaPixels!;
var width = isLuma
? _hostMovieFrameWidth
: (_hostMovieFrameWidth + 1) / 2;
var height = isLuma
? _hostMovieFrameHeight
: (_hostMovieFrameHeight + 1) / 2;
EnsureHostMovieImages(
_hostMovieFrameWidth,
_hostMovieFrameHeight,
_hostMovieLumaDstSelect,
(_hostMovieFrameWidth + 1) / 2,
(_hostMovieFrameHeight + 1) / 2,
_hostMovieChromaDstSelect);
var uploadedFrameSerial = isLuma
? _hostMovieLumaUploadedFrameSerial
: _hostMovieChromaUploadedFrameSerial;
var needsUpload = uploadedFrameSerial != _hostMovieFrameSerial;
VkBuffer stagingBuffer = default;
DeviceMemory stagingMemory = default;
if (needsUpload)
{
(stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
pixels,
$"Bink2 frame {_hostMovieFrameSerial} plane {plane} staging");
}
return new TextureResource
{
Address = texture.Address,
StagingBuffer = stagingBuffer,
StagingMemory = stagingMemory,
Image = isLuma ? _hostMovieImage : _hostMovieChromaImage,
View = isLuma ? _hostMovieImageView : _hostMovieChromaImageView,
Width = width,
Height = height,
RowLength = width,
DstSelect = texture.DstSelect,
NeedsUpload = needsUpload,
IsHostMovie = true,
HostMoviePlane = plane,
HostMovieFrameSerial = _hostMovieFrameSerial,
SamplerState = texture.Sampler,
};
}
private void EnsureHostMovieYuvFrame()
{
if (_hostMovieConvertedFrameSerial == _hostMovieFrameSerial)
{
return;
}
var bgra = _hostMovieFramePixels ??
throw new InvalidOperationException("Host movie frame is unavailable.");
var width = checked((int)_hostMovieFrameWidth);
var height = checked((int)_hostMovieFrameHeight);
var chromaWidth = (width + 1) / 2;
var chromaHeight = (height + 1) / 2;
if (_hostMovieLumaPixels?.Length != width * height)
{
_hostMovieLumaPixels = GC.AllocateUninitializedArray<byte>(width * height);
}
if (_hostMovieChromaPixels?.Length != chromaWidth * chromaHeight * 2)
{
_hostMovieChromaPixels =
GC.AllocateUninitializedArray<byte>(chromaWidth * chromaHeight * 2);
}
ConvertBgraToYuv420(
bgra,
width,
height,
_hostMovieLumaPixels,
_hostMovieChromaPixels);
_hostMovieConvertedFrameSerial = _hostMovieFrameSerial;
}
internal static void ConvertBgraToYuv420(
ReadOnlySpan<byte> bgra,
int width,
int height,
Span<byte> luma,
Span<byte> chroma)
{
var chromaWidth = (width + 1) / 2;
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var source = (y * width + x) * 4;
var b = bgra[source];
var g = bgra[source + 1];
var r = bgra[source + 2];
luma[y * width + x] = ClampByte(
(54 * r + 183 * g + 19 * b + 128) >> 8);
}
}
for (var y = 0; y < height; y += 2)
{
for (var x = 0; x < width; x += 2)
{
var red = 0;
var green = 0;
var blue = 0;
var samples = 0;
for (var sampleY = y; sampleY < Math.Min(y + 2, height); sampleY++)
{
for (var sampleX = x; sampleX < Math.Min(x + 2, width); sampleX++)
{
var source = (sampleY * width + sampleX) * 4;
blue += bgra[source];
green += bgra[source + 1];
red += bgra[source + 2];
samples++;
}
}
red /= samples;
green /= samples;
blue /= samples;
var destination = ((y / 2) * chromaWidth + x / 2) * 2;
chroma[destination] = ClampByte(
((128 * red - 116 * green - 12 * blue + 128) >> 8) + 128);
chroma[destination + 1] = ClampByte(
((-29 * red - 99 * green + 128 * blue + 128) >> 8) + 128);
}
}
}
private static byte ClampByte(int value) => (byte)Math.Clamp(value, 0, 255);
[MethodImpl(MethodImplOptions.NoInlining)]
private TextureResource ResolveTextureResource(GuestDrawTexture texture)
{
@@ -9539,6 +9900,26 @@ internal static unsafe class VulkanVideoPresenter
_stagingSize = size;
}
private void CreateFrameUploadBuffers(ulong size)
{
_frameUploadBuffers = new VkBuffer[MaxFramesInFlight];
_frameUploadMemory = new DeviceMemory[MaxFramesInFlight];
_frameUploadMapped = new nint[MaxFramesInFlight];
for (var slot = 0; slot < MaxFramesInFlight; slot++)
{
_frameUploadBuffers[slot] = CreateBuffer(
size,
BufferUsageFlags.TransferSrcBit,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out _frameUploadMemory[slot]);
void* mapped;
Check(
_vk.MapMemory(_device, _frameUploadMemory[slot], 0, size, 0, &mapped),
"vkMapMemory(frame upload)");
_frameUploadMapped[slot] = (nint)mapped;
}
}
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags)
{
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out var properties);
@@ -9590,6 +9971,8 @@ internal static unsafe class VulkanVideoPresenter
return;
}
PumpHostMovieFrame();
if (_skipAllCompute ||
AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress) ||
(_skipTallComputeZ > 0 && work.GroupCountZ >= _skipTallComputeZ))
@@ -12476,10 +12859,12 @@ internal static unsafe class VulkanVideoPresenter
EvictDirtyCachedTextures();
var completedWork = 0;
HashSet<string>? deferredOrderedQueues = null;
var renderWorkDeadline = _renderWorkBudgetTicks > 0
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
var workBudgetTicks = _renderWorkBudgetTicks;
var renderWorkDeadline = workBudgetTicks > 0
? System.Diagnostics.Stopwatch.GetTimestamp() + workBudgetTicks
: long.MaxValue;
while (completedWork < _maxGuestWorkPerRender)
var workLimit = _maxGuestWorkPerRender;
while (completedWork < workLimit)
{
// Never block the macOS main thread waiting for in-flight GPU
// work to drain. If submission is at capacity (a slow-compute
@@ -12536,6 +12921,14 @@ internal static unsafe class VulkanVideoPresenter
}
try
{
// A host-decoded movie only overrides which image gets
// presented (see the presentation selection below); the
// guest's own command stream keeps draining normally.
// Silently discarding these instead of executing them
// leaves guest-visible completion state (labels, buffers,
// job results) permanently unwritten, which desyncs the
// engine's own job system and previously crashed it
// shortly after the movie finished.
switch (work)
{
case VulkanOffscreenGuestDraw offscreenDraw:
@@ -12613,7 +13006,8 @@ internal static unsafe class VulkanVideoPresenter
FlushBatchedGuestCommands();
CollectAbandonedGuestImageVersions();
if (!TryTakePresentation(_presentedSequence, out var presentation))
Presentation presentation;
if (!TryTakePresentation(_presentedSequence, out presentation))
{
if (_pendingHostSplashReplay is { } splash)
{
@@ -12637,7 +13031,6 @@ internal static unsafe class VulkanVideoPresenter
return;
}
}
if (_hostSurface is not null)
{
if (presentation.IsSplash && presentation.Pixels is not null)
@@ -12690,6 +13083,7 @@ internal static unsafe class VulkanVideoPresenter
{
return;
}
}
TranslatedDrawResources? translatedResources = null;
@@ -12817,19 +13211,11 @@ internal static unsafe class VulkanVideoPresenter
if (pixels is not null)
{
// The staging buffer is shared across frame slots; a CPU
// pixel upload (splash / host frames) degrades to serial
// presentation rather than corrupting an in-flight copy.
WaitAllFrameSlots();
void* mapped;
Check(
_vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped),
"vkMapMemory");
var mapped = (void*)_frameUploadMapped[frameSlot];
fixed (byte* source = pixels)
{
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
}
_vk.UnmapMemory(_device, _stagingMemory);
}
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
@@ -12843,7 +13229,7 @@ internal static unsafe class VulkanVideoPresenter
PipelineStageFlags waitStage;
if (pixels is not null)
{
RecordUpload(imageIndex);
RecordUpload(imageIndex, frameSlot);
waitStage = PipelineStageFlags.TransferBit;
}
else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric)
@@ -13365,11 +13751,22 @@ internal static unsafe class VulkanVideoPresenter
continue;
}
var hostMovieImageInitialized = texture.HostMoviePlane switch
{
0 => _hostMovieImageInitialized,
1 => _hostMovieChromaImageInitialized,
_ => false,
};
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = texture.IsHostMovie && hostMovieImageInitialized
? AccessFlags.ShaderReadBit
: 0,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = ImageLayout.Undefined,
OldLayout = texture.IsHostMovie && hostMovieImageInitialized
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
@@ -13378,7 +13775,9 @@ internal static unsafe class VulkanVideoPresenter
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TopOfPipeBit,
texture.IsHostMovie && hostMovieImageInitialized
? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.TransferBit,
0,
0,
@@ -13438,6 +13837,19 @@ internal static unsafe class VulkanVideoPresenter
// reuse the image without restaging it.
texture.NeedsUpload = false;
}
if (texture.IsHostMovie)
{
if (texture.HostMoviePlane == 0)
{
_hostMovieImageInitialized = true;
_hostMovieLumaUploadedFrameSerial = texture.HostMovieFrameSerial;
}
else if (texture.HostMoviePlane == 1)
{
_hostMovieChromaImageInitialized = true;
_hostMovieChromaUploadedFrameSerial = texture.HostMovieFrameSerial;
}
}
}
}
@@ -14751,7 +15163,176 @@ internal static unsafe class VulkanVideoPresenter
}
}
private void RecordUpload(uint imageIndex)
private void EnsureHostMovieImages(
uint lumaWidth,
uint lumaHeight,
uint lumaDstSelect,
uint chromaWidth,
uint chromaHeight,
uint chromaDstSelect)
{
if (_hostMovieImage.Handle != 0 &&
_hostMovieImageWidth == lumaWidth &&
_hostMovieImageHeight == lumaHeight &&
_hostMovieImageFormat == Format.R8Unorm &&
_hostMovieImageDstSelect == lumaDstSelect &&
_hostMovieChromaImage.Handle != 0 &&
_hostMovieChromaImageWidth == chromaWidth &&
_hostMovieChromaImageHeight == chromaHeight &&
_hostMovieChromaImageDstSelect == chromaDstSelect)
{
return;
}
if (_hostMovieImage.Handle != 0 || _hostMovieChromaImage.Handle != 0)
{
FlushBatchedGuestCommands();
WaitForAllGuestSubmissions();
DrainFrameSlots();
DestroyHostMovieImage();
}
CreateHostMoviePlaneImage(
lumaWidth,
lumaHeight,
Format.R8Unorm,
lumaDstSelect,
"luma",
out _hostMovieImage,
out _hostMovieImageMemory,
out _hostMovieImageView);
CreateHostMoviePlaneImage(
chromaWidth,
chromaHeight,
Format.R8G8Unorm,
chromaDstSelect,
"chroma",
out _hostMovieChromaImage,
out _hostMovieChromaImageMemory,
out _hostMovieChromaImageView);
_hostMovieImageWidth = lumaWidth;
_hostMovieImageHeight = lumaHeight;
_hostMovieImageFormat = Format.R8Unorm;
_hostMovieImageDstSelect = lumaDstSelect;
_hostMovieChromaImageWidth = chromaWidth;
_hostMovieChromaImageHeight = chromaHeight;
_hostMovieChromaImageDstSelect = chromaDstSelect;
_hostMovieImageInitialized = false;
_hostMovieChromaImageInitialized = false;
_hostMovieLumaUploadedFrameSerial = -1;
_hostMovieChromaUploadedFrameSerial = -1;
}
private void CreateHostMoviePlaneImage(
uint width,
uint height,
Format format,
uint dstSelect,
string planeName,
out Image image,
out DeviceMemory memory,
out ImageView view)
{
var imageInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.Type2D,
Format = format,
Extent = new Extent3D(width, height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.Count1Bit,
Tiling = ImageTiling.Optimal,
Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
};
Check(
_vk.CreateImage(_device, &imageInfo, null, out image),
$"vkCreateImage(host movie {planeName})");
_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 memory),
$"vkAllocateMemory(host movie {planeName})");
Check(
_vk.BindImageMemory(_device, image, memory, 0),
$"vkBindImageMemory(host movie {planeName})");
var viewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
ViewType = ImageViewType.Type2D,
Format = format,
Components = ToVkComponentMapping(dstSelect),
SubresourceRange = ColorSubresourceRange(),
};
Check(
_vk.CreateImageView(
_device,
&viewInfo,
null,
out view),
$"vkCreateImageView(host movie {planeName})");
SetDebugName(ObjectType.Image, image.Handle, $"SharpEmu Bink2 {planeName} image");
SetDebugName(ObjectType.ImageView, view.Handle, $"SharpEmu Bink2 {planeName} view");
}
private void DestroyHostMovieImage()
{
if (_hostMovieImageView.Handle != 0)
{
_vk.DestroyImageView(_device, _hostMovieImageView, null);
_hostMovieImageView = default;
}
if (_hostMovieImage.Handle != 0)
{
_vk.DestroyImage(_device, _hostMovieImage, null);
_hostMovieImage = default;
}
if (_hostMovieImageMemory.Handle != 0)
{
_vk.FreeMemory(_device, _hostMovieImageMemory, null);
_hostMovieImageMemory = default;
}
if (_hostMovieChromaImageView.Handle != 0)
{
_vk.DestroyImageView(_device, _hostMovieChromaImageView, null);
_hostMovieChromaImageView = default;
}
if (_hostMovieChromaImage.Handle != 0)
{
_vk.DestroyImage(_device, _hostMovieChromaImage, null);
_hostMovieChromaImage = default;
}
if (_hostMovieChromaImageMemory.Handle != 0)
{
_vk.FreeMemory(_device, _hostMovieChromaImageMemory, null);
_hostMovieChromaImageMemory = default;
}
_hostMovieImageWidth = 0;
_hostMovieImageHeight = 0;
_hostMovieImageFormat = Format.Undefined;
_hostMovieChromaImageWidth = 0;
_hostMovieChromaImageHeight = 0;
_hostMovieImageInitialized = false;
_hostMovieChromaImageInitialized = false;
_hostMovieLumaUploadedFrameSerial = -1;
_hostMovieChromaUploadedFrameSerial = -1;
}
private void RecordUpload(uint imageIndex, int frameSlot)
{
var oldLayout = _imageInitialized[imageIndex]
? ImageLayout.PresentSrcKhr
@@ -14793,7 +15374,7 @@ internal static unsafe class VulkanVideoPresenter
};
_vk.CmdCopyBufferToImage(
_commandBuffer,
_stagingBuffer,
_frameUploadBuffers[frameSlot],
_swapchainImages[imageIndex],
ImageLayout.TransferDstOptimal,
1,
@@ -15661,7 +16242,26 @@ internal static unsafe class VulkanVideoPresenter
private void DestroySwapchainResources()
{
DestroyHostMovieImage();
DestroyPresentEncodeImage();
for (var slot = 0; slot < _frameUploadBuffers.Length; slot++)
{
if (_frameUploadMapped.Length > slot && _frameUploadMapped[slot] != 0)
{
_vk.UnmapMemory(_device, _frameUploadMemory[slot]);
}
if (_frameUploadBuffers[slot].Handle != 0)
{
_vk.DestroyBuffer(_device, _frameUploadBuffers[slot], null);
}
if (_frameUploadMemory[slot].Handle != 0)
{
_vk.FreeMemory(_device, _frameUploadMemory[slot], null);
}
}
_frameUploadBuffers = [];
_frameUploadMemory = [];
_frameUploadMapped = [];
if (_stagingBuffer.Handle != 0)
{
_vk.DestroyBuffer(_device, _stagingBuffer, null);