fix: imageCubeArray via core features + SRGB swapchain + LUT color conversion

Three fixes:

1. imageCubeArray: enabled via VkPhysicalDeviceFeatures (core 1.0 feature)
   instead of VkPhysicalDeviceImageCubeArrayFeatures (wrong sType in pNext
   chain was interpreted as VkExternalMemoryImageCreateInfoNV)

2. Swapchain format: reverted to B8G8R8A8_SRGB (was UNORM which driver
   didn't support → fell back to A2B10G10R10 10-bit format=58 → wrong
   pixel layout for capture)

3. Video colors: SRGB→linear conversion via 256-entry LUT in
   ReadCapturedBuffer. SRGB swapchain gives gamma-compressed bytes,
   FFmpeg expects linear. LUT converts each B/G/R channel using
   standard SRGB formula: s<=0.04045 ? s/12.92 : ((s+0.055)/1.055)^2.4

Also: video captures before ImGui (no UI in recording)
This commit is contained in:
emil28092005
2026-06-19 09:17:35 +03:00
parent d6939e9efa
commit 9531b1e9d5
3 changed files with 36 additions and 13 deletions
Binary file not shown.
+10 -12
View File
@@ -305,21 +305,19 @@ internal sealed unsafe class VulkanContext : IDisposable
synchronization2 = VkBool32.True, synchronization2 = VkBool32.True,
}; };
var cubeArrayFeatures = new VkPhysicalDeviceImageCubeArrayFeatures
{
sType = VkStructureType.PhysicalDeviceImageCubeArrayFeatures,
pNext = (nint)(&sync2Features),
imageCubeArray = VkBool32.True,
};
var renderingFeatures = new VkPhysicalDeviceDynamicRenderingFeatures var renderingFeatures = new VkPhysicalDeviceDynamicRenderingFeatures
{ {
sType = VkStructureType.PhysicalDeviceDynamicRenderingFeatures, sType = VkStructureType.PhysicalDeviceDynamicRenderingFeatures,
pNext = (nint)(&cubeArrayFeatures), pNext = (nint)(&sync2Features),
dynamicRendering = VkBool32.True, dynamicRendering = VkBool32.True,
}; };
var features = new VkPhysicalDeviceFeatures
{
imageCubeArray = VkBool32.True,
};
var deviceInfo = new VkDeviceCreateInfo var deviceInfo = new VkDeviceCreateInfo
{ {
sType = VkStructureType.DeviceCreateInfo, sType = VkStructureType.DeviceCreateInfo,
@@ -327,7 +325,7 @@ internal sealed unsafe class VulkanContext : IDisposable
queueCreateInfoCount = 1, queueCreateInfoCount = 1,
enabledExtensionCount = (uint)extNames.Length, enabledExtensionCount = (uint)extNames.Length,
ppEnabledExtensionNames = extPtrs, ppEnabledExtensionNames = extPtrs,
pEnabledFeatures = null, pEnabledFeatures = &features,
pNext = (nint)(&renderingFeatures), pNext = (nint)(&renderingFeatures),
}; };
@@ -361,12 +359,12 @@ internal sealed unsafe class VulkanContext : IDisposable
var formats = stackalloc VkSurfaceFormatKHR[(int)formatCount]; var formats = stackalloc VkSurfaceFormatKHR[(int)formatCount];
Vk.vkGetPhysicalDeviceSurfaceFormatsKHR(PhysicalDevice, Surface, &formatCount, formats); Vk.vkGetPhysicalDeviceSurfaceFormatsKHR(PhysicalDevice, Surface, &formatCount, formats);
SurfaceFormat = VkFormat.B8G8R8A8Unorm; SurfaceFormat = VkFormat.B8G8R8A8Srgb;
SurfaceColorSpace = VkColorSpaceKHR.SrgbNonlinearKHR; SurfaceColorSpace = VkColorSpaceKHR.SrgbNonlinearKHR;
for (uint i = 0; i < formatCount; i++) for (uint i = 0; i < formatCount; i++)
{ {
if (formats[(int)i].format == VkFormat.B8G8R8A8Unorm && if (formats[(int)i].format == VkFormat.B8G8R8A8Srgb &&
formats[(int)i].colorSpace == VkColorSpaceKHR.SrgbNonlinearKHR) formats[(int)i].colorSpace == VkColorSpaceKHR.SrgbNonlinearKHR)
{ {
SurfaceFormat = formats[(int)i].format; SurfaceFormat = formats[(int)i].format;
@@ -375,7 +373,7 @@ internal sealed unsafe class VulkanContext : IDisposable
} }
} }
if (SurfaceFormat == VkFormat.B8G8R8A8Unorm) if (SurfaceFormat == VkFormat.B8G8R8A8Srgb)
{ {
SurfaceFormat = formats[0].format; SurfaceFormat = formats[0].format;
SurfaceColorSpace = formats[0].colorSpace; SurfaceColorSpace = formats[0].colorSpace;
+26 -1
View File
@@ -149,10 +149,35 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
Marshal.Copy((nint)pData, result, 0, (int)_screenshotBufferSize); Marshal.Copy((nint)pData, result, 0, (int)_screenshotBufferSize);
Vk.vkUnmapMemory(_ctx.Device, _screenshotMemory); Vk.vkUnmapMemory(_ctx.Device, _screenshotMemory);
// Data is BGRA from swapchain — no conversion needed, FFmpeg handles bgra // Swapchain is B8G8R8A8_SRGB — raw bytes are SRGB-encoded
// Convert SRGB→linear for FFmpeg (which expects linear bgra)
for (int i = 0; i < result.Length; i += 4)
{
result[i] = SrgbToLinear(result[i]); // B
result[i + 1] = SrgbToLinear(result[i + 1]); // G
result[i + 2] = SrgbToLinear(result[i + 2]); // R
// result[i + 3] = A (keep as-is)
}
return result; return result;
} }
private static readonly byte[] SrgbLut = BuildSrgbLut();
private static byte[] BuildSrgbLut()
{
var lut = new byte[256];
for (int i = 0; i < 256; i++)
{
float s = i / 255.0f;
float l = s <= 0.04045f ? s / 12.92f : MathF.Pow((s + 0.055f) / 1.055f, 2.4f);
lut[i] = (byte)Math.Clamp(l * 255.0f, 0, 255);
}
return lut;
}
private static byte SrgbToLinear(byte srgb) => SrgbLut[srgb];
public void BeginImGuiFrame() public void BeginImGuiFrame()
{ {
_imGui?.NewFrame(); _imGui?.NewFrame();