diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9a92c3aed2d..8c7fd791cd3 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -406,7 +406,8 @@ qt_internal_extend_target(Gui CONDITION WIN32 platform/windows/qwindowsguieventdispatcher.cpp platform/windows/qwindowsguieventdispatcher_p.h platform/windows/qwindowsmimeconverter.h platform/windows/qwindowsmimeconverter.cpp platform/windows/qwindowsnativeinterface.cpp - rhi/qrhid3d11.cpp rhi/qrhid3d11_p.h rhi/qrhid3dhelpers_p.h + rhi/qrhid3d11.cpp rhi/qrhid3d11_p.h + rhi/qrhid3dhelpers.cpp rhi/qrhid3dhelpers_p.h rhi/vs_test_p.h rhi/qrhid3d12.cpp rhi/qrhid3d12_p.h rhi/cs_mipmap_p.h diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index 255209a9b06..67265ebf464 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -7361,11 +7361,12 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar \brief Describes the high dynamic range related information of the swapchain's associated output. - To perform tonemapping, one often needs to know the maximum luminance of - the display the swapchain's window is associated with. While this is often - made user-configurable, it can be highly useful to set defaults based on - the values reported by the display itself, thus providing a decent starting - point. + To perform HDR-compatible tonemapping, where the target range is not [0,1], + one often needs to know the maximum luminance of the display the + swapchain's window is associated with. While this is often made + user-configurable (think brightness, gamma and similar settings in games), + it can be highly useful to set defaults based on the values reported by the + display itself, thus providing a decent starting point. There are some problems however: the information is exposed in different forms on different platforms, whereas with cross-platform graphics APIs @@ -7373,11 +7374,6 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar information is not in the scope of the API (and may rather be retrievable via other platform-specific means, if any). - The struct returned from QRhiSwapChain::hdrInfo() contains either some - hard-coded defaults, indicated by the \c isHardCodedDefaults field, or real - values received from an API such as DXGI (IDXGIOutput6) or Cocoa - (NSScreen). The default is 1000 nits for maximum luminance. - With Metal on macOS/iOS, there is no luminance values exposed in the platform APIs. Instead, the maximum color component value, that would be 1.0 in a non-HDR setup, is provided. The \c limitsType field indicates what @@ -7386,8 +7382,21 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar fit. With an API like Vulkan, where there is no way to get such information, the - values are always the built-in defaults and \c isHardCodedDefaults is - always true. + values are always the built-in defaults. + + Therefore, the struct returned from QRhiSwapChain::hdrInfo() contains + either some hard-coded defaults or real values received from an API such as + DXGI (IDXGIOutput6) or Cocoa (NSScreen). When no platform queries are + available (or needs using platform facilities out of scope for QRhi), the + hard-coded defaults are a maximum luminance of 1000 nits and an SDR white + level of 200. + + The struct also exposes the presumed luminance behavior of the platform and + its compositor, to indicate what a color component value of 1.0 is treated + as in a HDR color buffer. In some cases it will be necessary to perform + color correction of non-HDR content composited with HDR content. To enable + this, the SDR white level is queried from the system on some platforms + (Windows) and exposed here. \note This is a RHI API with limited compatibility guarantees, see \l QRhi for details. @@ -7406,16 +7415,20 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar */ /*! - \variable QRhiSwapChainHdrInfo::isHardCodedDefaults + \enum QRhiSwapChainHdrInfo::LuminanceBehavior - Set to true when the data in the QRhiSwapChainHdrInfo consists entirely of - the hard-coded default values, for example because there is no way to query - the relevant information with a given graphics API or platform. (or because - querying it can be achieved only by means, e.g. platform APIs in some other - area, that are out of scope for the QRhi layer of the Qt graphics stack to - handle) + \value SceneReferred Indicates that the color value of 1.0 is interpreted + as 80 nits. This is the behavior of HDR-enabled windows with the Windows + compositor. See + \l{https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range}{this + page} for more information on HDR on Windows. - \sa QRhiSwapChain::hdrInfo() + \value DisplayReferred Indicates that the color value of 1.0 is interpreted + as the value of the SDR white. (which can be e.g. 200 nits, but will vary + depending on screen brightness) This is the behavior of HDR-enabled windows + on Apple platforms. See + \l{https://developer.apple.com/documentation/metal/hdr_content/displaying_hdr_content_in_a_metal_layer}{this + page} for more information on Apple's EDR system. */ /*! @@ -7445,7 +7458,27 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar } luminanceInNits; \endcode - Whereas for macOS/iOS, the current maximum and potential maximum color + On Windows the minimum and maximum luminance depends on the screen + brightness. While not relevant for desktops, on laptops the screen + brightness may change at any time. Increasing brightness implies decreased + maximum luminance. In addition, the results may also be dependent on the + HDR Content Brightness set in Windows Settings' System/Display/HDR view, + if there is such a setting. + + Note however that the changes made to the laptop screen's brightness or in + the system settings while the application is running are not necessarily + reflected in the returned values, meaning calling hdrInfo() again may still + return the same luminance range as before for the rest of the process' + lifetime. The exact behavior is up to DXGI and Qt has no control over it. + + \note The Windows compositor works in scene-referred mode for HDR content. + A color component value of 1.0 corresponds to a luminance of 80 nits. When + rendering non-HDR content (e.g. 2D UI elements), the correction of the + white level is often necessary. (e.g., outputting the fragment color (1, 1, + 1) will likely lead to showing a shade of white that is too dim on-screen) + See \l sdrWhiteLevel. + + For macOS/iOS, the current maximum and potential maximum color component values are provided: \code @@ -7455,14 +7488,62 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar } colorComponentValue; \endcode + The value may depend on the screen brightness, which on laptops means that + the result may change in the next call to hdrInfo() if the brightness was + changed in the meantime. The maximum screen brightness implies a maximum + color value of 1.0. + + \note Apple's EDR is display-referred. 1.0 corresponds to a luminance level + of SDR white (e.g. 200 nits), the value of which varies based on the screen + brightness and possibly other settings. The exact luminance value for that, + or the maximum luminance of the display, are not exposed to the + applications. + + \note It has been observed that the color component values are not set to + the correct larger-than-1 value right away on startup on some macOS + systems, but the values tend to change during or after the first frame. + \sa QRhiSwapChain::hdrInfo() */ +/*! + \variable QRhiSwapChainHdrInfo::luminanceBehavior + + Describes the platform's presumed behavior with regards to color values. + + \sa sdrWhiteLevel + */ + +/*! + \variable QRhiSwapChainHdrInfo::sdrWhiteLevel + + On Windows this is the dynamic SDR white level in nits. The value is + dependent on the screen brightness (on laptops), and the SDR or HDR Content + Brightness settings in the Windows settings' System/Display/HDR view. + + To perform white level correction for non-HDR (SDR) content, such as 2D UI + elemenents, multiply the final color with sdrWhiteLevel / 80.0 whenever + \l luminanceBehavior is SceneReferred. (assuming Windows and a linear + extended sRGB (scRGB) color space) + + On other platforms the value is always a pre-defined value, 200. This may + not match the system's actual SDR white level, but the value of this + variable is not relevant in practice when the \l luminanceBehavior is + DisplayReferred, because then the color component value of 1.0 refers to + the SDR white by default. + + \sa luminanceBehavior +*/ + /*! \return the HDR information for the associated display. - The returned struct is always the default one if createOrResize() has not - been successfully called yet. + Do not assume that this is a cheap operation. Depending on the platform, + this function makes various platform queries which may have a performance + impact. + + \note Can be called before createOrResize() as long as the window is + \l{setWindow()}{set}. \note What happens when moving a window with an initialized swapchain between displays (HDR to HDR with different characteristics, HDR to SDR, @@ -7477,10 +7558,11 @@ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer tar QRhiSwapChainHdrInfo QRhiSwapChain::hdrInfo() { QRhiSwapChainHdrInfo info; - info.isHardCodedDefaults = true; info.limitsType = QRhiSwapChainHdrInfo::LuminanceInNits; info.limits.luminanceInNits.minLuminance = 0.0f; info.limits.luminanceInNits.maxLuminance = 1000.0f; + info.luminanceBehavior = QRhiSwapChainHdrInfo::SceneReferred; + info.sdrWhiteLevel = 200.0f; return info; } @@ -7488,7 +7570,7 @@ QRhiSwapChainHdrInfo QRhiSwapChain::hdrInfo() QDebug operator<<(QDebug dbg, const QRhiSwapChainHdrInfo &info) { QDebugStateSaver saver(dbg); - dbg.nospace() << "QRhiSwapChainHdrInfo(" << (info.isHardCodedDefaults ? "with hard-coded defaults" : "queried from system"); + dbg.nospace() << "QRhiSwapChainHdrInfo("; switch (info.limitsType) { case QRhiSwapChainHdrInfo::LuminanceInNits: dbg.nospace() << " minLuminance=" << info.limits.luminanceInNits.minLuminance @@ -7499,6 +7581,14 @@ QDebug operator<<(QDebug dbg, const QRhiSwapChainHdrInfo &info) dbg.nospace() << " maxPotentialColorComponentValue=" << info.limits.colorComponentValue.maxPotentialColorComponentValue; break; } + switch (info.luminanceBehavior) { + case QRhiSwapChainHdrInfo::SceneReferred: + dbg.nospace() << " scene-referred, SDR white level=" << info.sdrWhiteLevel; + break; + case QRhiSwapChainHdrInfo::DisplayReferred: + dbg.nospace() << " display-referred"; + break; + } dbg.nospace() << ')'; return dbg; } diff --git a/src/gui/rhi/qrhi.h b/src/gui/rhi/qrhi.h index 2d5ee7346ed..2a4c16b8817 100644 --- a/src/gui/rhi/qrhi.h +++ b/src/gui/rhi/qrhi.h @@ -1480,11 +1480,16 @@ Q_DECLARE_TYPEINFO(QRhiGraphicsPipeline::TargetBlend, Q_RELOCATABLE_TYPE); struct QRhiSwapChainHdrInfo { - bool isHardCodedDefaults; enum LimitsType { LuminanceInNits, ColorComponentValue }; + + enum LuminanceBehavior { + SceneReferred, + DisplayReferred + }; + LimitsType limitsType; union { struct { @@ -1496,6 +1501,8 @@ struct QRhiSwapChainHdrInfo float maxPotentialColorComponentValue; } colorComponentValue; } limits; + LuminanceBehavior luminanceBehavior; + float sdrWhiteLevel; }; Q_DECLARE_TYPEINFO(QRhiSwapChainHdrInfo, Q_RELOCATABLE_TYPE); diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp index ce6104d3c4d..8e6dd24dc55 100644 --- a/src/gui/rhi/qrhid3d11.cpp +++ b/src/gui/rhi/qrhid3d11.cpp @@ -4863,44 +4863,6 @@ QSize QD3D11SwapChain::surfacePixelSize() return m_window->size() * m_window->devicePixelRatio(); } -static bool output6ForWindow(QWindow *w, IDXGIAdapter1 *adapter, IDXGIOutput6 **result) -{ - bool ok = false; - QRect wr = w->geometry(); - wr = QRect(wr.topLeft() * w->devicePixelRatio(), wr.size() * w->devicePixelRatio()); - const QPoint center = wr.center(); - IDXGIOutput *currentOutput = nullptr; - IDXGIOutput *output = nullptr; - for (UINT i = 0; adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND; ++i) { - DXGI_OUTPUT_DESC desc; - output->GetDesc(&desc); - const RECT r = desc.DesktopCoordinates; - const QRect dr(QPoint(r.left, r.top), QPoint(r.right - 1, r.bottom - 1)); - if (dr.contains(center)) { - currentOutput = output; - break; - } else { - output->Release(); - } - } - if (currentOutput) { - ok = SUCCEEDED(currentOutput->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast(result))); - currentOutput->Release(); - } - return ok; -} - -static bool outputDesc1ForWindow(QWindow *w, IDXGIAdapter1 *adapter, DXGI_OUTPUT_DESC1 *result) -{ - bool ok = false; - IDXGIOutput6 *out6 = nullptr; - if (output6ForWindow(w, adapter, &out6)) { - ok = SUCCEEDED(out6->GetDesc1(result)); - out6->Release(); - } - return ok; -} - bool QD3D11SwapChain::isFormatSupported(Format f) { if (f == SDR) @@ -4913,7 +4875,7 @@ bool QD3D11SwapChain::isFormatSupported(Format f) QRHI_RES_RHI(QRhiD3D11); DXGI_OUTPUT_DESC1 desc1; - if (outputDesc1ForWindow(m_window, rhiD->activeAdapter, &desc1)) { + if (QRhiD3D::outputDesc1ForWindow(m_window, rhiD->activeAdapter, &desc1)) { if (desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) return f == QRhiSwapChain::HDRExtendedSrgbLinear || f == QRhiSwapChain::HDR10; } @@ -4924,14 +4886,16 @@ bool QD3D11SwapChain::isFormatSupported(Format f) QRhiSwapChainHdrInfo QD3D11SwapChain::hdrInfo() { QRhiSwapChainHdrInfo info = QRhiSwapChain::hdrInfo(); + // Must use m_window, not window, given this may be called before createOrResize(). if (m_window) { QRHI_RES_RHI(QRhiD3D11); DXGI_OUTPUT_DESC1 hdrOutputDesc; - if (outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc)) { - info.isHardCodedDefaults = false; + if (QRhiD3D::outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc)) { info.limitsType = QRhiSwapChainHdrInfo::LuminanceInNits; info.limits.luminanceInNits.minLuminance = hdrOutputDesc.MinLuminance; info.limits.luminanceInNits.maxLuminance = hdrOutputDesc.MaxLuminance; + info.luminanceBehavior = QRhiSwapChainHdrInfo::SceneReferred; // 1.0 = 80 nits + info.sdrWhiteLevel = QRhiD3D::sdrWhiteLevelInNits(hdrOutputDesc); } } return info; @@ -5058,7 +5022,7 @@ bool QD3D11SwapChain::createOrResize() DXGI_COLOR_SPACE_TYPE hdrColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; // SDR DXGI_OUTPUT_DESC1 hdrOutputDesc; - if (outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc) && m_format != SDR) { + if (QRhiD3D::outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc) && m_format != SDR) { // https://docs.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range if (hdrOutputDesc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) { switch (m_format) { diff --git a/src/gui/rhi/qrhid3d12.cpp b/src/gui/rhi/qrhid3d12.cpp index 0b5d7e38751..90dbc53acbe 100644 --- a/src/gui/rhi/qrhid3d12.cpp +++ b/src/gui/rhi/qrhid3d12.cpp @@ -6066,44 +6066,6 @@ QSize QD3D12SwapChain::surfacePixelSize() return m_window->size() * m_window->devicePixelRatio(); } -static bool output6ForWindow(QWindow *w, IDXGIAdapter1 *adapter, IDXGIOutput6 **result) -{ - bool ok = false; - QRect wr = w->geometry(); - wr = QRect(wr.topLeft() * w->devicePixelRatio(), wr.size() * w->devicePixelRatio()); - const QPoint center = wr.center(); - IDXGIOutput *currentOutput = nullptr; - IDXGIOutput *output = nullptr; - for (UINT i = 0; adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND; ++i) { - DXGI_OUTPUT_DESC desc; - output->GetDesc(&desc); - const RECT r = desc.DesktopCoordinates; - const QRect dr(QPoint(r.left, r.top), QPoint(r.right - 1, r.bottom - 1)); - if (dr.contains(center)) { - currentOutput = output; - break; - } else { - output->Release(); - } - } - if (currentOutput) { - ok = SUCCEEDED(currentOutput->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast(result))); - currentOutput->Release(); - } - return ok; -} - -static bool outputDesc1ForWindow(QWindow *w, IDXGIAdapter1 *adapter, DXGI_OUTPUT_DESC1 *result) -{ - bool ok = false; - IDXGIOutput6 *out6 = nullptr; - if (output6ForWindow(w, adapter, &out6)) { - ok = SUCCEEDED(out6->GetDesc1(result)); - out6->Release(); - } - return ok; -} - bool QD3D12SwapChain::isFormatSupported(Format f) { if (f == SDR) @@ -6116,7 +6078,7 @@ bool QD3D12SwapChain::isFormatSupported(Format f) QRHI_RES_RHI(QRhiD3D12); DXGI_OUTPUT_DESC1 desc1; - if (outputDesc1ForWindow(m_window, rhiD->activeAdapter, &desc1)) { + if (QRhiD3D::outputDesc1ForWindow(m_window, rhiD->activeAdapter, &desc1)) { if (desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) return f == QRhiSwapChain::HDRExtendedSrgbLinear || f == QRhiSwapChain::HDR10; } @@ -6127,14 +6089,16 @@ bool QD3D12SwapChain::isFormatSupported(Format f) QRhiSwapChainHdrInfo QD3D12SwapChain::hdrInfo() { QRhiSwapChainHdrInfo info = QRhiSwapChain::hdrInfo(); + // Must use m_window, not window, given this may be called before createOrResize(). if (m_window) { QRHI_RES_RHI(QRhiD3D12); DXGI_OUTPUT_DESC1 hdrOutputDesc; - if (outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc)) { - info.isHardCodedDefaults = false; + if (QRhiD3D::outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc)) { info.limitsType = QRhiSwapChainHdrInfo::LuminanceInNits; info.limits.luminanceInNits.minLuminance = hdrOutputDesc.MinLuminance; info.limits.luminanceInNits.maxLuminance = hdrOutputDesc.MaxLuminance; + info.luminanceBehavior = QRhiSwapChainHdrInfo::SceneReferred; // 1.0 = 80 nits + info.sdrWhiteLevel = QRhiD3D::sdrWhiteLevelInNits(hdrOutputDesc); } } return info; @@ -6177,7 +6141,7 @@ void QD3D12SwapChain::chooseFormats() hdrColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; // SDR DXGI_OUTPUT_DESC1 hdrOutputDesc; QRHI_RES_RHI(QRhiD3D12); - if (outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc) && m_format != SDR) { + if (QRhiD3D::outputDesc1ForWindow(m_window, rhiD->activeAdapter, &hdrOutputDesc) && m_format != SDR) { // https://docs.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range if (hdrOutputDesc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) { switch (m_format) { diff --git a/src/gui/rhi/qrhid3dhelpers.cpp b/src/gui/rhi/qrhid3dhelpers.cpp new file mode 100644 index 00000000000..3ee5ae58328 --- /dev/null +++ b/src/gui/rhi/qrhid3dhelpers.cpp @@ -0,0 +1,160 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qrhid3dhelpers_p.h" + +QT_BEGIN_NAMESPACE + +namespace QRhiD3D { + +bool output6ForWindow(QWindow *w, IDXGIAdapter1 *adapter, IDXGIOutput6 **result) +{ + bool ok = false; + QRect wr = w->geometry(); + wr = QRect(wr.topLeft() * w->devicePixelRatio(), wr.size() * w->devicePixelRatio()); + const QPoint center = wr.center(); + IDXGIOutput *currentOutput = nullptr; + IDXGIOutput *output = nullptr; + for (UINT i = 0; adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND; ++i) { + DXGI_OUTPUT_DESC desc; + output->GetDesc(&desc); + const RECT r = desc.DesktopCoordinates; + const QRect dr(QPoint(r.left, r.top), QPoint(r.right - 1, r.bottom - 1)); + if (dr.contains(center)) { + currentOutput = output; + break; + } else { + output->Release(); + } + } + if (currentOutput) { + ok = SUCCEEDED(currentOutput->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast(result))); + currentOutput->Release(); + } + return ok; +} + +bool outputDesc1ForWindow(QWindow *w, IDXGIAdapter1 *adapter, DXGI_OUTPUT_DESC1 *result) +{ + bool ok = false; + IDXGIOutput6 *out6 = nullptr; + if (output6ForWindow(w, adapter, &out6)) { + ok = SUCCEEDED(out6->GetDesc1(result)); + out6->Release(); + } + return ok; +} + +float sdrWhiteLevelInNits(const DXGI_OUTPUT_DESC1 &outputDesc) +{ + QVector pathInfos; + uint32_t pathInfoCount, modeInfoCount; + LONG result; + do { + if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathInfoCount, &modeInfoCount) == ERROR_SUCCESS) { + pathInfos.resize(pathInfoCount); + QVector modeInfos(modeInfoCount); + result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathInfoCount, pathInfos.data(), &modeInfoCount, modeInfos.data(), nullptr); + } else { + return 200.0f; + } + } while (result == ERROR_INSUFFICIENT_BUFFER); + + MONITORINFOEX monitorInfo = {}; + monitorInfo.cbSize = sizeof(monitorInfo); + GetMonitorInfo(outputDesc.Monitor, &monitorInfo); + + for (const DISPLAYCONFIG_PATH_INFO &info : pathInfos) { + DISPLAYCONFIG_SOURCE_DEVICE_NAME deviceName = {}; + deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + deviceName.header.size = sizeof(deviceName); + deviceName.header.adapterId = info.sourceInfo.adapterId; + deviceName.header.id = info.sourceInfo.id; + if (DisplayConfigGetDeviceInfo(&deviceName.header) == ERROR_SUCCESS) { + if (!wcscmp(monitorInfo.szDevice, deviceName.viewGdiDeviceName)) { + DISPLAYCONFIG_SDR_WHITE_LEVEL whiteLevel = {}; + whiteLevel.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL; + whiteLevel.header.size = sizeof(DISPLAYCONFIG_SDR_WHITE_LEVEL); + whiteLevel.header.adapterId = info.targetInfo.adapterId; + whiteLevel.header.id = info.targetInfo.id; + if (DisplayConfigGetDeviceInfo(&whiteLevel.header) == ERROR_SUCCESS) + return whiteLevel.SDRWhiteLevel * 80 / 1000.0f; + } + } + } + + return 200.0f; +} + +pD3DCompile resolveD3DCompile() +{ + for (const wchar_t *libraryName : {L"D3DCompiler_47", L"D3DCompiler_43"}) { + QSystemLibrary library(libraryName); + if (library.load()) { + if (auto symbol = library.resolve("D3DCompile")) + return reinterpret_cast(symbol); + } else { + qWarning("Failed to load D3DCompiler_47/43.dll"); + } + } + return nullptr; +} + +IDCompositionDevice *createDirectCompositionDevice() +{ + QSystemLibrary dcomplib(QStringLiteral("dcomp")); + typedef HRESULT (__stdcall *DCompositionCreateDeviceFuncPtr)( + _In_opt_ IDXGIDevice *dxgiDevice, + _In_ REFIID iid, + _Outptr_ void **dcompositionDevice); + DCompositionCreateDeviceFuncPtr func = reinterpret_cast( + dcomplib.resolve("DCompositionCreateDevice")); + if (!func) { + qWarning("Unable to resolve DCompositionCreateDevice, perhaps dcomp.dll is missing?"); + return nullptr; + } + IDCompositionDevice *device = nullptr; + HRESULT hr = func(nullptr, __uuidof(IDCompositionDevice), reinterpret_cast(&device)); + if (FAILED(hr)) { + qWarning("Failed to create Direct Composition device: %s", + qPrintable(QSystemError::windowsComString(hr))); + return nullptr; + } + return device; +} + +#ifdef QRHI_D3D12_HAS_DXC +std::pair createDxcCompiler() +{ + QSystemLibrary dxclib(QStringLiteral("dxcompiler")); + // this will not be in the system library location, hence onlySystemDirectory==false + if (!dxclib.load(false)) { + qWarning("Failed to load dxcompiler.dll"); + return {}; + } + DxcCreateInstanceProc func = reinterpret_cast(dxclib.resolve("DxcCreateInstance")); + if (!func) { + qWarning("Unable to resolve DxcCreateInstance"); + return {}; + } + IDxcCompiler *compiler = nullptr; + HRESULT hr = func(CLSID_DxcCompiler, __uuidof(IDxcCompiler), reinterpret_cast(&compiler)); + if (FAILED(hr)) { + qWarning("Failed to create dxc compiler instance: %s", + qPrintable(QSystemError::windowsComString(hr))); + return {}; + } + IDxcLibrary *library = nullptr; + hr = func(CLSID_DxcLibrary, __uuidof(IDxcLibrary), reinterpret_cast(&library)); + if (FAILED(hr)) { + qWarning("Failed to create dxc library instance: %s", + qPrintable(QSystemError::windowsComString(hr))); + return {}; + } + return { compiler, library }; +} +#endif + +} // namespace + +QT_END_NAMESPACE diff --git a/src/gui/rhi/qrhid3dhelpers_p.h b/src/gui/rhi/qrhid3dhelpers_p.h index 07fcddb7f75..f4cf6deaed6 100644 --- a/src/gui/rhi/qrhid3dhelpers_p.h +++ b/src/gui/rhi/qrhid3dhelpers_p.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -30,73 +31,16 @@ QT_BEGIN_NAMESPACE namespace QRhiD3D { -inline pD3DCompile resolveD3DCompile() -{ - for (const wchar_t *libraryName : {L"D3DCompiler_47", L"D3DCompiler_43"}) { - QSystemLibrary library(libraryName); - if (library.load()) { - if (auto symbol = library.resolve("D3DCompile")) - return reinterpret_cast(symbol); - } else { - qWarning("Failed to load D3DCompiler_47/43.dll"); - } - } - return nullptr; -} +bool output6ForWindow(QWindow *w, IDXGIAdapter1 *adapter, IDXGIOutput6 **result); +bool outputDesc1ForWindow(QWindow *w, IDXGIAdapter1 *adapter, DXGI_OUTPUT_DESC1 *result); +float sdrWhiteLevelInNits(const DXGI_OUTPUT_DESC1 &outputDesc); -inline IDCompositionDevice *createDirectCompositionDevice() -{ - QSystemLibrary dcomplib(QStringLiteral("dcomp")); - typedef HRESULT (__stdcall *DCompositionCreateDeviceFuncPtr)( - _In_opt_ IDXGIDevice *dxgiDevice, - _In_ REFIID iid, - _Outptr_ void **dcompositionDevice); - DCompositionCreateDeviceFuncPtr func = reinterpret_cast( - dcomplib.resolve("DCompositionCreateDevice")); - if (!func) { - qWarning("Unable to resolve DCompositionCreateDevice, perhaps dcomp.dll is missing?"); - return nullptr; - } - IDCompositionDevice *device = nullptr; - HRESULT hr = func(nullptr, __uuidof(IDCompositionDevice), reinterpret_cast(&device)); - if (FAILED(hr)) { - qWarning("Failed to create Direct Composition device: %s", - qPrintable(QSystemError::windowsComString(hr))); - return nullptr; - } - return device; -} +pD3DCompile resolveD3DCompile(); + +IDCompositionDevice *createDirectCompositionDevice(); #ifdef QRHI_D3D12_HAS_DXC -inline std::pair createDxcCompiler() -{ - QSystemLibrary dxclib(QStringLiteral("dxcompiler")); - // this will not be in the system library location, hence onlySystemDirectory==false - if (!dxclib.load(false)) { - qWarning("Failed to load dxcompiler.dll"); - return {}; - } - DxcCreateInstanceProc func = reinterpret_cast(dxclib.resolve("DxcCreateInstance")); - if (!func) { - qWarning("Unable to resolve DxcCreateInstance"); - return {}; - } - IDxcCompiler *compiler = nullptr; - HRESULT hr = func(CLSID_DxcCompiler, __uuidof(IDxcCompiler), reinterpret_cast(&compiler)); - if (FAILED(hr)) { - qWarning("Failed to create dxc compiler instance: %s", - qPrintable(QSystemError::windowsComString(hr))); - return {}; - } - IDxcLibrary *library = nullptr; - hr = func(CLSID_DxcLibrary, __uuidof(IDxcLibrary), reinterpret_cast(&library)); - if (FAILED(hr)) { - qWarning("Failed to create dxc library instance: %s", - qPrintable(QSystemError::windowsComString(hr))); - return {}; - } - return { compiler, library }; -} +std::pair createDxcCompiler(); #endif } // namespace diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index cafb7ff7cb7..95a954fab5b 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -6402,7 +6402,9 @@ QRhiSwapChainHdrInfo QMetalSwapChain::hdrInfo() QRhiSwapChainHdrInfo info; info.limitsType = QRhiSwapChainHdrInfo::ColorComponentValue; info.limits.colorComponentValue.maxColorComponentValue = 1; - info.isHardCodedDefaults = true; + info.limits.colorComponentValue.maxPotentialColorComponentValue = 1; + info.luminanceBehavior = QRhiSwapChainHdrInfo::DisplayReferred; // 1.0 = SDR white + info.sdrWhiteLevel = 200; // typical value, but dummy (don't know the real one); won't matter due to being display-referred if (m_window) { // Must use m_window, not window, given this may be called before createOrResize(). @@ -6411,14 +6413,12 @@ QRhiSwapChainHdrInfo QMetalSwapChain::hdrInfo() NSScreen *screen = view.window.screen; info.limits.colorComponentValue.maxColorComponentValue = screen.maximumExtendedDynamicRangeColorComponentValue; info.limits.colorComponentValue.maxPotentialColorComponentValue = screen.maximumPotentialExtendedDynamicRangeColorComponentValue; - info.isHardCodedDefaults = false; #else if (@available(iOS 16.0, *)) { UIView *view = reinterpret_cast(m_window->winId()); UIScreen *screen = view.window.windowScene.screen; info.limits.colorComponentValue.maxColorComponentValue = view.window.windowScene.screen.currentEDRHeadroom; info.limits.colorComponentValue.maxPotentialColorComponentValue = screen.potentialEDRHeadroom; - info.isHardCodedDefaults = false; } #endif } diff --git a/tests/manual/rhi/hdr/CMakeLists.txt b/tests/manual/rhi/hdr/CMakeLists.txt new file mode 100644 index 00000000000..cf6e7662a4b --- /dev/null +++ b/tests/manual/rhi/hdr/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_internal_add_manual_test(hdr + GUI + SOURCES + hdr.cpp + LIBRARIES + Qt::Gui + Qt::GuiPrivate +) + +qt_internal_add_resource(hdr "hdr" + PREFIX + "/" + FILES + "hdrtexture.vert.qsb" + "hdrtexture.frag.qsb" +) + +set(imgui_base ../shared/imgui) +set(imgui_target hdr) +include(${imgui_base}/imgui.cmakeinc) diff --git a/tests/manual/rhi/hdr/buildshaders.bat b/tests/manual/rhi/hdr/buildshaders.bat new file mode 100644 index 00000000000..7710c85f838 --- /dev/null +++ b/tests/manual/rhi/hdr/buildshaders.bat @@ -0,0 +1,2 @@ +qsb --qt6 hdrtexture.vert -o hdrtexture.vert.qsb +qsb --qt6 hdrtexture.frag -o hdrtexture.frag.qsb diff --git a/tests/manual/rhi/hdr/hdr.cpp b/tests/manual/rhi/hdr/hdr.cpp new file mode 100644 index 00000000000..ab51b7a8e8c --- /dev/null +++ b/tests/manual/rhi/hdr/hdr.cpp @@ -0,0 +1,448 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +// Test application for HDR with scRGB. +// Launch with the argument "scrgb" or "sdr", perhaps side-by-side even. + +#define EXAMPLEFW_PREINIT +#define EXAMPLEFW_IMGUI +#include "../shared/examplefw.h" + +#include "../shared/cube.h" + +QByteArray loadHdr(const QString &fn, QSize *size) +{ + QFile f(fn); + if (!f.open(QIODevice::ReadOnly)) { + qWarning("Failed to open %s", qPrintable(fn)); + return QByteArray(); + } + + char sig[256]; + f.read(sig, 11); + if (strncmp(sig, "#?RADIANCE\n", 11)) + return QByteArray(); + + QByteArray buf = f.readAll(); + const char *p = buf.constData(); + const char *pEnd = p + buf.size(); + + // Process lines until the empty one. + QByteArray line; + while (p < pEnd) { + char c = *p++; + if (c == '\n') { + if (line.isEmpty()) + break; + if (line.startsWith(QByteArrayLiteral("FORMAT="))) { + const QByteArray format = line.mid(7).trimmed(); + if (format != QByteArrayLiteral("32-bit_rle_rgbe")) { + qWarning("HDR format '%s' is not supported", format.constData()); + return QByteArray(); + } + } + line.clear(); + } else { + line.append(c); + } + } + if (p == pEnd) { + qWarning("Malformed HDR image data at property strings"); + return QByteArray(); + } + + // Get the resolution string. + while (p < pEnd) { + char c = *p++; + if (c == '\n') + break; + line.append(c); + } + if (p == pEnd) { + qWarning("Malformed HDR image data at resolution string"); + return QByteArray(); + } + + int w = 0, h = 0; + // We only care about the standard orientation. + if (!sscanf(line.constData(), "-Y %d +X %d", &h, &w)) { + qWarning("Unsupported HDR resolution string '%s'", line.constData()); + return QByteArray(); + } + if (w <= 0 || h <= 0) { + qWarning("Invalid HDR resolution"); + return QByteArray(); + } + + // output is RGBA32F + const int blockSize = 4 * sizeof(float); + QByteArray data; + data.resize(w * h * blockSize); + + typedef unsigned char RGBE[4]; + RGBE *scanline = new RGBE[w]; + + for (int y = 0; y < h; ++y) { + if (pEnd - p < 4) { + qWarning("Unexpected end of HDR data"); + delete[] scanline; + return QByteArray(); + } + + scanline[0][0] = *p++; + scanline[0][1] = *p++; + scanline[0][2] = *p++; + scanline[0][3] = *p++; + + if (scanline[0][0] == 2 && scanline[0][1] == 2 && scanline[0][2] < 128) { + // new rle, the first pixel was a dummy + for (int channel = 0; channel < 4; ++channel) { + for (int x = 0; x < w && p < pEnd; ) { + unsigned char c = *p++; + if (c > 128) { // run + if (p < pEnd) { + int repCount = c & 127; + c = *p++; + while (repCount--) + scanline[x++][channel] = c; + } + } else { // not a run + while (c-- && p < pEnd) + scanline[x++][channel] = *p++; + } + } + } + } else { + // old rle + scanline[0][0] = 2; + int bitshift = 0; + int x = 1; + while (x < w && pEnd - p >= 4) { + scanline[x][0] = *p++; + scanline[x][1] = *p++; + scanline[x][2] = *p++; + scanline[x][3] = *p++; + + if (scanline[x][0] == 1 && scanline[x][1] == 1 && scanline[x][2] == 1) { // run + int repCount = scanline[x][3] << bitshift; + while (repCount--) { + memcpy(scanline[x], scanline[x - 1], 4); + ++x; + } + bitshift += 8; + } else { // not a run + ++x; + bitshift = 0; + } + } + } + + // adjust for -Y orientation + float *fp = reinterpret_cast(data.data() + (h - 1 - y) * blockSize * w); + for (int x = 0; x < w; ++x) { + float d = qPow(2.0f, float(scanline[x][3]) - 128.0f); + float r = scanline[x][0] / 256.0f * d; + float g = scanline[x][1] / 256.0f * d; + float b = scanline[x][2] / 256.0f * d; + float a = 1.0f; + *fp++ = r; + *fp++ = g; + *fp++ = b; + *fp++ = a; + } + } + + delete[] scanline; + + *size = QSize(w, h); + + return data; +} + +struct { + QMatrix4x4 winProj; + QList releasePool; + QRhiResourceUpdateBatch *initialUpdates = nullptr; + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ubuf = nullptr; + QRhiTexture *tex = nullptr; + QRhiSampler *sampler = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + QRhiGraphicsPipeline *ps = nullptr; + bool showDemoWindow = true; + QVector3D rotation; + + bool usingHDRWindow; + bool adjustSDR = false; + float SDRWhiteLevelInNits = 200.0f; + bool tonemapHDR = false; + float tonemapInMax = 2.5f; + float tonemapOutMax = 0.0f; + + QString imageFile; +} d; + +void preInit() +{ + QStringList args = QCoreApplication::arguments(); + if (args.contains("scrgb")) { + d.usingHDRWindow = true; + swapchainFormat = QRhiSwapChain::HDRExtendedSrgbLinear; + } else if (args.contains("sdr")) { + d.usingHDRWindow = false; + swapchainFormat = QRhiSwapChain::SDR; + } else { + qFatal("Missing command line argument, specify scrgb or sdr"); + } + + if (args.contains("file")) { + d.imageFile = args[args.indexOf("file") + 1]; + qDebug("Using HDR image file %s", qPrintable(d.imageFile)); + } else { + qFatal("Missing command line argument, specify 'file' followed by a .hdr file. " + "Download for example the original .exr from https://viewer.openhdr.org/i/5fcb9a595812624a99d24c62/linear " + "and use ImageMagick's 'convert' to convert from .exr to .hdr"); + } +} + +void Window::customInit() +{ + if (!m_r->isTextureFormatSupported(QRhiTexture::RGBA32F)) + qWarning("RGBA32F texture format is not supported"); + + d.initialUpdates = m_r->nextResourceUpdateBatch(); + + d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube)); + d.vbuf->create(); + d.releasePool << d.vbuf; + + d.initialUpdates->uploadStaticBuffer(d.vbuf, cube); + + d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4 * 4); + d.ubuf->create(); + d.releasePool << d.ubuf; + + qint32 flip = 1; + d.initialUpdates->updateDynamicBuffer(d.ubuf, 64, 4, &flip); + + qint32 doLinearToSRGBInShader = !d.usingHDRWindow; + d.initialUpdates->updateDynamicBuffer(d.ubuf, 68, 4, &doLinearToSRGBInShader); + + QSize size; + QByteArray floatData = loadHdr(d.imageFile, &size); + + d.tex = m_r->newTexture(QRhiTexture::RGBA32F, size); + d.releasePool << d.tex; + d.tex->create(); + QRhiTextureUploadDescription desc({ 0, 0, { floatData.constData(), quint32(floatData.size()) } }); + d.initialUpdates->uploadTexture(d.tex, desc); + + d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, + QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge); + d.releasePool << d.sampler; + d.sampler->create(); + + d.srb = m_r->newShaderResourceBindings(); + d.releasePool << d.srb; + d.srb->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler) + }); + d.srb->create(); + + d.ps = m_r->newGraphicsPipeline(); + d.releasePool << d.ps; + d.ps->setCullMode(QRhiGraphicsPipeline::Back); + const QRhiShaderStage stages[] = { + { QRhiShaderStage::Vertex, getShader(QLatin1String(":/hdrtexture.vert.qsb")) }, + { QRhiShaderStage::Fragment, getShader(QLatin1String(":/hdrtexture.frag.qsb")) } + }; + d.ps->setShaderStages(stages, stages + 2); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 3 * sizeof(float) }, + { 2 * sizeof(float) } + }); + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, + { 1, 1, QRhiVertexInputAttribute::Float2, 0 } + }); + d.ps->setVertexInputLayout(inputLayout); + d.ps->setShaderResourceBindings(d.srb); + d.ps->setRenderPassDescriptor(m_rp); + d.ps->create(); +} + +void Window::customRelease() +{ + qDeleteAll(d.releasePool); + d.releasePool.clear(); +} + +void Window::customRender() +{ + if (d.tonemapOutMax == 0.0f) { + QRhiSwapChainHdrInfo info = m_sc->hdrInfo(); + switch (info.limitsType) { + case QRhiSwapChainHdrInfo::LuminanceInNits: + d.tonemapOutMax = info.limits.luminanceInNits.maxLuminance / 80.0f; + break; + case QRhiSwapChainHdrInfo::ColorComponentValue: + // because on macOS it changes dynamically when starting up, so retry in next frame if it's still just 1.0 + if (info.limits.colorComponentValue.maxColorComponentValue > 1.0f) + d.tonemapOutMax = info.limits.colorComponentValue.maxColorComponentValue; + break; + } + } + + QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); + QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch(); + if (d.initialUpdates) { + u->merge(d.initialUpdates); + d.initialUpdates->release(); + d.initialUpdates = nullptr; + } + + QMatrix4x4 mvp = m_proj; + mvp.rotate(d.rotation.x(), 1, 0, 0); + mvp.rotate(d.rotation.y(), 0, 1, 0); + mvp.rotate(d.rotation.z(), 0, 0, 1); + u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData()); + + if (d.usingHDRWindow && d.tonemapHDR) { + u->updateDynamicBuffer(d.ubuf, 72, 4, &d.tonemapInMax); + u->updateDynamicBuffer(d.ubuf, 76, 4, &d.tonemapOutMax); + } else { + float zero[2] = {}; + u->updateDynamicBuffer(d.ubuf, 72, 8, zero); + } + + QColor clearColor = Qt::green; // sRGB + if (d.usingHDRWindow && d.adjustSDR) { + float sdrMultiplier = d.SDRWhiteLevelInNits / 80.0f; // scRGB 1.0 = 80 nits (and linear gamma) + clearColor = QColor::fromRgbF(clearColor.redF() * sdrMultiplier, + clearColor.greenF() * sdrMultiplier, + clearColor.blueF() * sdrMultiplier, + 1.0f); + } + + const QSize outputSizeInPixels = m_sc->currentPixelSize(); + cb->beginPass(m_sc->currentFrameRenderTarget(), clearColor, { 1.0f, 0 }, u); + + cb->setGraphicsPipeline(d.ps); + cb->setViewport(QRhiViewport(0, 0, outputSizeInPixels.width(), outputSizeInPixels.height())); + cb->setShaderResources(); + const QRhiCommandBuffer::VertexInput vbufBindings[] = { + { d.vbuf, 0 }, + { d.vbuf, quint32(36 * 3 * sizeof(float)) } + }; + cb->setVertexInput(0, 2, vbufBindings); + cb->draw(36); + + m_imguiRenderer->render(); + + cb->endPass(); +} + +static void addTip(const char *s) +{ + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(300); + ImGui::TextUnformatted(s); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +void Window::customGui() +{ + ImGui::SetNextWindowBgAlpha(1.0f); + ImGui::SetNextWindowPos(ImVec2(10, 420), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(800, 300), ImGuiCond_FirstUseEver); + ImGui::Begin("HDR test"); + + if (d.usingHDRWindow) { + ImGui::Text("The window is now scRGB (extended *linear* sRGB) + FP16 color buffer,\n" + "the ImGui UI and the green background are considered SDR content,\n" + "the cube is using a HDR texture."); + ImGui::Checkbox("Adjust SDR content", &d.adjustSDR); + addTip("Multiplies fragment colors for non-HDR content with sdr_white_level / 80. " + "Not relevant with macOS (due to EDR being display-referred)."); + if (d.adjustSDR) { + ImGui::SliderFloat("SDR white level (nits)", &d.SDRWhiteLevelInNits, 0.0f, 1000.0f); + imguiHDRMultiplier = d.SDRWhiteLevelInNits / 80.0f; // scRGB 1.0 = 80 nits (and linear gamma) + } else { + imguiHDRMultiplier = 1.0f; // 0 would mean linear to sRGB; don't want that with HDR + } + ImGui::Separator(); + ImGui::Checkbox("Tonemap HDR content", &d.tonemapHDR); + addTip("Perform some most basic Reinhard tonemapping (changed to suit HDR) on the 3D content (the cube). " + "Display max luminance is set to the max color component value (macOS) or max luminance in nits / 80 (Windows) by default."); + if (d.tonemapHDR) { + ImGui::SliderFloat("Content max luminance\n(color component value)", &d.tonemapInMax, 0.0f, 20.0f); + ImGui::SliderFloat("Display max luminance\n(color component value)", &d.tonemapOutMax, 0.0f, 20.0f); + } + } else { + ImGui::Text("The window is standard dynamic range (no HDR, so non-linear sRGB effectively).\n" + "Here we just do linear -> sRGB for everything (UI, textured cube)\n" + "at the end of the pipeline, while the Qt::green background is already sRGB."); + } + + ImGui::End(); + + ImGui::SetNextWindowPos(ImVec2(850, 560), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(420, 140), ImGuiCond_FirstUseEver); + ImGui::Begin("Misc"); + float *rot = reinterpret_cast(&d.rotation); + ImGui::SliderFloat("Rotation X", &rot[0], 0.0f, 360.0f); + ImGui::SliderFloat("Rotation Y", &rot[1], 0.0f, 360.0f); + ImGui::SliderFloat("Rotation Z", &rot[2], 0.0f, 360.0f); + ImGui::End(); + + if (d.usingHDRWindow) { + ImGui::SetNextWindowPos(ImVec2(850, 10), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(420, 180), ImGuiCond_FirstUseEver); + ImGui::Begin("Actual platform info"); + QRhiSwapChainHdrInfo info = m_sc->hdrInfo(); + switch (info.limitsType) { + case QRhiSwapChainHdrInfo::LuminanceInNits: + ImGui::Text("Platform provides luminance in nits"); + addTip("Windows/D3D: On laptops this will be screen brightness dependent. Increasing brightness implies the max luminance decreases. " + "It also seems to be affected by HDR Content Brightness in the Settings, if there is one (e.g. on laptops; not to be confused with SDR Content Brightess). " + "(note that the DXGI query does not seem to return changed values if there are runtime changes unless restarting the app)."); + ImGui::Text("Min luminance: %.4f\nMax luminance: %.4f", + info.limits.luminanceInNits.minLuminance, + info.limits.luminanceInNits.maxLuminance); + break; + case QRhiSwapChainHdrInfo::ColorComponentValue: + ImGui::Text("Platform provides color component values"); + addTip("macOS/Metal: On laptops this will be screen brightness dependent. Increasing brightness decreases the max color value. " + "Max brightness may bring it down to 1.0."); + ImGui::Text("maxColorComponentValue: %.4f\nmaxPotentialColorComponentValue: %.4f", + info.limits.colorComponentValue.maxColorComponentValue, + info.limits.colorComponentValue.maxPotentialColorComponentValue); + break; + } + ImGui::Separator(); + switch (info.luminanceBehavior) { + case QRhiSwapChainHdrInfo::SceneReferred: + ImGui::Text("Luminance behavior is scene-referred"); + break; + case QRhiSwapChainHdrInfo::DisplayReferred: + ImGui::Text("Luminance behavior is display-referred"); + break; + } + addTip("Windows (DWM) HDR is scene-referred: 1.0 = 80 nits.\n\n" + "Apple EDR is display-referred: the value of 1.0 refers to whatever the system's current SDR white level is (100, 200, ... nits depending on the brightness)."); + if (info.luminanceBehavior == QRhiSwapChainHdrInfo::SceneReferred) { + ImGui::Text("SDR white level: %.4f nits", info.sdrWhiteLevel); + addTip("On Windows this is queried from DISPLAYCONFIG_SDR_WHITE_LEVEL. " + "Affected by the slider in the Windows Settings (System/Display/HDR/[S|H]DR Content Brightness). " + "With max screen brightness (laptops) the value will likely be the same as the max luminance."); + } + ImGui::End(); + } +} diff --git a/tests/manual/rhi/hdr/hdrtexture.frag b/tests/manual/rhi/hdr/hdrtexture.frag new file mode 100644 index 00000000000..9d5e12005a9 --- /dev/null +++ b/tests/manual/rhi/hdr/hdrtexture.frag @@ -0,0 +1,44 @@ +#version 440 + +layout(location = 0) in vec2 v_texcoord; + +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 mvp; + int flip; + int sdr; + float tonemapInMax; + float tonemapOutMax; +} ubuf; + +layout(binding = 1) uniform sampler2D tex; + +vec3 linearToSRGB(vec3 color) +{ + vec3 S1 = sqrt(color); + vec3 S2 = sqrt(S1); + vec3 S3 = sqrt(S2); + return 0.585122381 * S1 + 0.783140355 * S2 - 0.368262736 * S3; +} + +// https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range +vec3 simpleReinhardTonemapper(vec3 c, float inMax, float outMax) +{ + c /= vec3(inMax); + c.r = c.r / (1 + c.r); + c.g = c.g / (1 + c.g); + c.b = c.b / (1 + c.b); + c *= vec3(outMax); + return c; +} + +void main() +{ + vec4 c = texture(tex, v_texcoord); + if (ubuf.tonemapInMax != 0) + c.rgb = simpleReinhardTonemapper(c.rgb, ubuf.tonemapInMax, ubuf.tonemapOutMax); + else if (ubuf.sdr != 0) + c.rgb = linearToSRGB(c.rgb); + fragColor = vec4(c.rgb * c.a, c.a); +} diff --git a/tests/manual/rhi/hdr/hdrtexture.frag.qsb b/tests/manual/rhi/hdr/hdrtexture.frag.qsb new file mode 100644 index 00000000000..441b660f0b9 Binary files /dev/null and b/tests/manual/rhi/hdr/hdrtexture.frag.qsb differ diff --git a/tests/manual/rhi/hdr/hdrtexture.vert b/tests/manual/rhi/hdr/hdrtexture.vert new file mode 100644 index 00000000000..336d5f8cfba --- /dev/null +++ b/tests/manual/rhi/hdr/hdrtexture.vert @@ -0,0 +1,22 @@ +#version 440 + +layout(location = 0) in vec4 position; +layout(location = 1) in vec2 texcoord; + +layout(location = 0) out vec2 v_texcoord; + +layout(std140, binding = 0) uniform buf { + mat4 mvp; + int flip; + int sdr; + float tonemapInMaxNits; + float tonemapOutMaxNits; +}; + +void main() +{ + v_texcoord = vec2(texcoord.x, texcoord.y); + if (flip != 0) + v_texcoord.y = 1.0 - v_texcoord.y; + gl_Position = mvp * position; +} diff --git a/tests/manual/rhi/hdr/hdrtexture.vert.qsb b/tests/manual/rhi/hdr/hdrtexture.vert.qsb new file mode 100644 index 00000000000..bcfb0da2730 Binary files /dev/null and b/tests/manual/rhi/hdr/hdrtexture.vert.qsb differ diff --git a/tests/manual/rhi/rhi.pro b/tests/manual/rhi/rhi.pro deleted file mode 100644 index c688c6aa176..00000000000 --- a/tests/manual/rhi/rhi.pro +++ /dev/null @@ -1,40 +0,0 @@ -TEMPLATE = subdirs - -SUBDIRS += \ - hellominimalcrossgfxtriangle \ - compressedtexture_bc1 \ - compressedtexture_bc1_subupload \ - texuploads \ - msaatexture \ - msaarenderbuffer \ - cubemap \ - cubemap_scissor \ - cubemap_render \ - multiwindow \ - multiwindow_threaded \ - triquadcube \ - offscreen \ - floattexture \ - float16texture_with_compute \ - mrt \ - shadowmap \ - computebuffer \ - computeimage \ - instancing \ - noninstanced \ - tex3d \ - texturearray \ - polygonmode \ - tessellation \ - geometryshader \ - stenciloutline \ - stereo \ - tex1d \ - displacement \ - imguirenderer \ - multiview - -qtConfig(widgets) { - SUBDIRS += \ - qrhiprof -} diff --git a/tests/manual/rhi/shared/examplefw.h b/tests/manual/rhi/shared/examplefw.h index 2bab0a9e681..d0fa903859e 100644 --- a/tests/manual/rhi/shared/examplefw.h +++ b/tests/manual/rhi/shared/examplefw.h @@ -80,6 +80,8 @@ QRhi::BeginFrameFlags beginFrameFlags; QRhi::EndFrameFlags endFrameFlags; bool transparentBackground = false; bool debugLayer = true; +QRhiSwapChain::Format swapchainFormat = QRhiSwapChain::SDR; +float imguiHDRMultiplier = 0.0f; class Window : public QWindow { @@ -280,6 +282,10 @@ void Window::init() m_sc->setDepthStencil(m_ds); m_sc->setSampleCount(sampleCount); m_sc->setFlags(scFlags); + if (!m_sc->isFormatSupported(swapchainFormat)) + qWarning("Swapchain reports that requested format %d is not supported", int(swapchainFormat)); + else + m_sc->setFormat(swapchainFormat); m_rp = m_sc->newCompatibleRenderPassDescriptor(); m_sc->setRenderPassDescriptor(m_rp); @@ -398,7 +404,7 @@ void Window::render() QMatrix4x4 guiMvp = m_r->clipSpaceCorrMatrix(); guiMvp.ortho(0, outputSizeInPixels.width() / dpr, outputSizeInPixels.height() / dpr, 0, 1, -1); - m_imguiRenderer->prepare(m_r, rt, cb, guiMvp, 1.0f); + m_imguiRenderer->prepare(m_r, rt, cb, guiMvp, 1.0f, imguiHDRMultiplier); #endif customRender(); diff --git a/tests/manual/rhi/shared/imgui/imgui.frag b/tests/manual/rhi/shared/imgui/imgui.frag index 51bd615deb3..6169dd639f0 100644 --- a/tests/manual/rhi/shared/imgui/imgui.frag +++ b/tests/manual/rhi/shared/imgui/imgui.frag @@ -8,14 +8,30 @@ layout(location = 0) out vec4 fragColor; layout(std140, binding = 0) uniform buf { mat4 mvp; float opacity; + // Windows HDR: set to SDR_white_level_in_nits / 80 + // macOS/iOS EDR: set to 1.0 + // No HDR: set to 0.0, will do linear to sRGB at the end then. + float hdrWhiteLevelMult; }; layout(binding = 1) uniform sampler2D tex; +vec3 linearToSRGB(vec3 color) +{ + vec3 S1 = sqrt(color); + vec3 S2 = sqrt(S1); + vec3 S3 = sqrt(S2); + return 0.585122381 * S1 + 0.783140355 * S2 - 0.368262736 * S3; +} + void main() { vec4 c = v_color * texture(tex, v_texcoord); c.a *= opacity; - c.rgb *= c.a; + if (hdrWhiteLevelMult > 0.0) + c.rgb *= hdrWhiteLevelMult; + else + c.rgb = linearToSRGB(c.rgb); + c.rgb *= c.a; // premultiplied alpha fragColor = c; } diff --git a/tests/manual/rhi/shared/imgui/imgui.frag.qsb b/tests/manual/rhi/shared/imgui/imgui.frag.qsb index 2abf236f56e..09b1e446978 100644 Binary files a/tests/manual/rhi/shared/imgui/imgui.frag.qsb and b/tests/manual/rhi/shared/imgui/imgui.frag.qsb differ diff --git a/tests/manual/rhi/shared/imgui/imgui.vert b/tests/manual/rhi/shared/imgui/imgui.vert index bb24a22c13c..45510ea0fe6 100644 --- a/tests/manual/rhi/shared/imgui/imgui.vert +++ b/tests/manual/rhi/shared/imgui/imgui.vert @@ -10,6 +10,7 @@ layout(location = 1) out vec4 v_color; layout(std140, binding = 0) uniform buf { mat4 mvp; float opacity; + float sdrMult; }; void main() diff --git a/tests/manual/rhi/shared/imgui/imgui.vert.qsb b/tests/manual/rhi/shared/imgui/imgui.vert.qsb index 0f2300f676c..3ee5a7b7163 100644 Binary files a/tests/manual/rhi/shared/imgui/imgui.vert.qsb and b/tests/manual/rhi/shared/imgui/imgui.vert.qsb differ diff --git a/tests/manual/rhi/shared/imgui/qrhiimgui.cpp b/tests/manual/rhi/shared/imgui/qrhiimgui.cpp index d4330238b91..b5f39b020fe 100644 --- a/tests/manual/rhi/shared/imgui/qrhiimgui.cpp +++ b/tests/manual/rhi/shared/imgui/qrhiimgui.cpp @@ -48,7 +48,12 @@ void QRhiImguiRenderer::releaseResources() m_rhi = nullptr; } -void QRhiImguiRenderer::prepare(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb, const QMatrix4x4 &mvp, float opacity) +void QRhiImguiRenderer::prepare(QRhi *rhi, + QRhiRenderTarget *rt, + QRhiCommandBuffer *cb, + const QMatrix4x4 &mvp, + float opacity, + float hdrWhiteLevelMultiplierOrZeroForSDRsRGB) { if (!m_rhi) { m_rhi = rhi; @@ -89,7 +94,7 @@ void QRhiImguiRenderer::prepare(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuff } if (!m_ubuf) { - m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4)); + m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4 + 4)); m_ubuf->setName(QByteArrayLiteral("imgui uniform buffer")); if (!m_ubuf->create()) return; @@ -201,6 +206,7 @@ void QRhiImguiRenderer::prepare(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuff u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData()); u->updateDynamicBuffer(m_ubuf.get(), 64, 4, &opacity); + u->updateDynamicBuffer(m_ubuf.get(), 68, 4, &hdrWhiteLevelMultiplierOrZeroForSDRsRGB); for (int i = 0; i < texturesNeedUpdate.count(); ++i) { Texture &t(m_textures[texturesNeedUpdate[i]]); diff --git a/tests/manual/rhi/shared/imgui/qrhiimgui_p.h b/tests/manual/rhi/shared/imgui/qrhiimgui_p.h index 31782144bf9..5605578c6c7 100644 --- a/tests/manual/rhi/shared/imgui/qrhiimgui_p.h +++ b/tests/manual/rhi/shared/imgui/qrhiimgui_p.h @@ -45,7 +45,12 @@ public: StaticRenderData sf; FrameRenderData f; - void prepare(QRhi *rhi, QRhiRenderTarget *rt, QRhiCommandBuffer *cb, const QMatrix4x4 &mvp, float opacity); + void prepare(QRhi *rhi, + QRhiRenderTarget *rt, + QRhiCommandBuffer *cb, + const QMatrix4x4 &mvp, + float opacity = 1.0f, + float hdrWhiteLevelMultiplierOrZeroForSDRsRGB = 0.0f); void render(); void releaseResources();