ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
59.55k stars 10.15k forks source link

How is the state of SRGB colorspace with Vulkan? #6611

Open carlosdr02 opened 1 year ago

carlosdr02 commented 1 year ago

So because ImGui outputs its stuff in linear colorspace, if you use an SRGB framebuffer, you have gamma correction being applied twice, that's why the ImGui windows look really bright. So from a long time I've been using UNORM framebuffer to avoid that, however, because SRGB makes the driver apply gamma correction automatically so it may do it in an implementation defined way, which may lead to better performance. So if you want to use SRGB framebuffers with ImGui, you basically have to modify the shader that ImGui uses and apply the inverse of gamma correction.

My question is, is there a way to change the way ImGui deals with colors to be able to use SRGB framebuffers. Matter of fact, why isn't that the default behavior? Is there a plan to add it in the future?

I know this topic has been brought up before, but this is messing with me just by the fact that I don't like to have to modify the source code of my submodules because then I have to fork it or add a local copy of ImGui into my project, which I rather not do. I prefer to keep it as a submodule.

adenine-dev commented 11 months ago

also running into this issue, any known workarounds for this aside from editing the dear imgui source?

Cyphall commented 10 months ago

A workaround is to create two image views from your framebuffer image, one with a linear format and the other with an sRGB format, and to render/sample from the right one depending on the situation

Eiyeron commented 3 months ago

Hello. If you, like me, was wondering had to be done in order to provide the backend a proper image view of the swapchain window, here's a few notes on how I made it work with (hopefully) no complains from the validation layer. Note that this has only been tested on one setup yet I suppose and hope that adapting it to other output setups won't take too long.

Instead of using an intermediate framebuffer that would be blit on the presented image, I instead opted for having different image views over the same images, the swapchain's. This is not the solution, it's just one I that just sparked my curiosity.

In order to use multiple image views on the swapchain images, you need to:

The first extension will allow image views on swapchain images to be created with other formats than the image's and the second one is needed to inform vkCreateImageView which formats will be used with that view. The second extension is actually needed by the first one.

Then, during the swapchain creation, you need to add VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR on VkSwapchainCreateInfoKHR::flags and pNext-chain a VkImageFormatListCreateInfo with at least all the formats that will be used with the swapchain. Here, I hardcoded it with {VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM} but please adapt that with your formats.

Once that's done, you should be able to create your linear image views, their framebuffers and give those to ImGui's render passes or dynamic rendering process to have the proper color space on both the main window and the other viewports.

Here's an excerpt of my own engine. I started with vkGuide's legacy tutorial so I use VkBootstrap here to let it handle the swapchain creation, format selection and all the bells and whistles but I hope you'll still be able to figure what was done under the hood.

// As said earlier, this is required by the mutable bit.
VkImageFormatListCreateInfo formatListCreateInfo = {};
// As I'm writing this code, I haven't figured out how to work *with* vkb to select the matching linear format yet so this is hardcoded. You might need to change that depending on your own setup.
VkFormat usageFormats[2] = {VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM};
formatListCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
formatListCreateInfo.pViewFormats = usageFormats;
// Here too.
formatListCreateInfo.viewFormatCount = 2;

vkb::SwapchainBuilder swapchainBuilder(physicalDevice, device, renderSurface);

auto buildResult = swapchainBuilder.use_default_format_selection()
                                    // Adds the flags. Part of the magic happen here.
                                    .set_create_flags(VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR)
                                    // This will attach formatListCreateInfo to the internal VkSwapchainCreateInfoKHR
                                    .add_pNext(&formatListCreateInfo)
                                    // <irrelevant code removed, those weren't the settings you were looking for>
                                    .build();
// <irrelevant code removed, this was mostly about swapchain creation error checking>
// vkb's wrapper over VkSwapchain and a few other infos related to it.
swapchain = *buildResult;
// The "normal" swapchain image that will be used with my engine's framebuffers
// It's mostly a loop that creates as many image views as needed.
// The same is done with the linear views a few lines down.
imageViews = swapchain.get_image_views().value();

VkImageViewUsageCreateInfo usageFlags = {};
usageFlags.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO;
// I actually use the stored `usage_flag` from the swapchain object returned by vkb,
// but this harcoded flag should work for ImGui's needs. Adapt that to your needs.
usageFlags.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

VkImageViewCreateInfo linearCreateInfo = {};
linearCreateInfo.pNext = &usageFlags;
// Again, this is hardcoded and you might need to either adapt or write smarter code.
linearCreateInfo.format = VK_FORMAT_B8G8R8A8_UNORM;
// <irrelevant code removed, the rest of the fill of VkImageViewCreateInfo is left as an exercise to the reader>

// The linear-format image views that will be used for ImGui only.
linearImageViews.resize(imageViews.size());
for (auto i = 0u; i < imageViews.size(); ++i)
{
    linearCreateInfo.image = images[i];
    vkCreateImageView(device, &linearCreateInfo, nullptr, &linearImageViews[i]);
}

The rest should be quite straightforward: create ImGui's renderpass with new framebuffers created with the alternative set of image views and you should be ready to go. Don't forget destroy those framebuffers and image views if the present becomes out of date (resize or other windowevents). At this point, I got the exact result I hoped for.

Again, this is only one solution. You could also use an intermediate buffer to render into if you have the VRAM to spare or hack the built-in SPV blobs that are stored in the backend to compensate for the color space mismatch (as I did before integrating the docking branch). I don't recommand doing the latter, it doesn't play nice with the docking branch (you'd have the proper colors on the main window but the other viewports will be too dark to be readable).

Sorry in advance if the code isn't totally clear or if there were some mistakes that crept up while I was touching up that copy-pasta. I hope this note shall help future people wanting to try the Vulkan adventure.

Have a nice day.

Addendum: Because I had a doubt the instant I clicked on Comment, I had to look the GPUOpen's extension support matrix and it looks like the feature is not widely spread. I don't know if it's because support was added in recent driver versions or if there's a general lack of hardware support, but given that I tried that on my 1050Ti, I forgot to take a moment to ponder if using this extension was worth the lack of backwards compatibility. That will probably be one factor to weight on while considering this solution.