#include "HDRDisplay.h"

#include "PCH.h"

#include "Buffer.h"
#include "Effects11.h"
#include "Globals.h"
#include "I18n/I18n.h"
#include "LinearLighting.h"
#include "Menu.h"
#include "ShaderCache.h"
#include "State.h"
#include "Upscaling.h"
#include "Util.h"
#include <algorithm>
#include <dxgi1_4.h>
#include <dxgi1_6.h>
#include <imgui.h>

#define I18N_KEY_PREFIX "feature.hdr_display."

// Win11 24H2 display config types. Compat_ prefix avoids collision with SDK enum members.
typedef enum
{
	Compat_DISPLAYCONFIG_ADVANCED_COLOR_MODE_SDR = 0,
	Compat_DISPLAYCONFIG_ADVANCED_COLOR_MODE_WCG = 1,
	Compat_DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR = 2
} Compat_DISPLAYCONFIG_ADVANCED_COLOR_MODE;

typedef struct Compat_DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2
{
	DISPLAYCONFIG_DEVICE_INFO_HEADER header;
	union
	{
		struct
		{
			UINT32 advancedColorSupported: 1;
			UINT32 advancedColorActive: 1;
			UINT32 reserved1: 1;
			UINT32 advancedColorLimitedByPolicy: 1;
			UINT32 highDynamicRangeSupported: 1;
			UINT32 highDynamicRangeUserEnabled: 1;
			UINT32 wideColorSupported: 1;
			UINT32 wideColorUserEnabled: 1;
			UINT32 reserved: 24;
		};
		UINT32 value;
	};
	DISPLAYCONFIG_COLOR_ENCODING colorEncoding;
	UINT32 bitsPerColorChannel;
	Compat_DISPLAYCONFIG_ADVANCED_COLOR_MODE activeColorMode;
} Compat_DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2;

static constexpr DISPLAYCONFIG_DEVICE_INFO_TYPE kDisplayConfigGetAdvancedColorInfo2 =
	static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(15);

// HDR display detection
// Credits: Luma Framework by Filippo Tarpini (MIT License)
// https://github.com/Filoppi/Luma-Framework/blob/f1fbc2a36f2d24fd551721ce90f26821a8e754c1/Source/Core/utils/display.hpp
namespace
{
	// Returns the GDI device name for the swap chain's output via GetContainingOutput.
	// Returns false if the output cannot be determined (e.g. Streamline wraps the swap chain).
	bool GetSwapChainOutputDeviceName(IDXGISwapChain* swapChain, WCHAR (&outDeviceName)[32])
	{
		winrt::com_ptr<IDXGIOutput> output;
		if (FAILED(swapChain->GetContainingOutput(output.put())))
			return false;
		DXGI_OUTPUT_DESC desc{};
		if (FAILED(output->GetDesc(&desc)))
			return false;
		wcsncpy_s(outDeviceName, desc.DeviceName, _TRUNCATE);
		// DeviceName is ASCII (e.g. "\\.\DISPLAY1") — safe to log as narrow string
		char narrowName[32]{};
		WideCharToMultiByte(CP_UTF8, 0, desc.DeviceName, -1, narrowName, sizeof(narrowName), nullptr, nullptr);
		logger::debug("[HDR] Swap chain output device: {}", narrowName);
		return true;
	}

	bool GetDisplayConfigPathInfo(IDXGISwapChain* swapChain, DISPLAYCONFIG_PATH_INFO& outPathInfo)
	{
		WCHAR deviceName[32]{};
		if (!GetSwapChainOutputDeviceName(swapChain, deviceName))
			return false;

		uint32_t pathCount, modeCount;
		if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount) != ERROR_SUCCESS)
			return false;

		std::vector<DISPLAYCONFIG_PATH_INFO> paths(pathCount);
		std::vector<DISPLAYCONFIG_MODE_INFO> modes(modeCount);
		if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, paths.data(), &modeCount, modes.data(), nullptr) != ERROR_SUCCESS)
			return false;

		for (auto& pathInfo : paths) {
			// DISPLAYCONFIG_SOURCE_IN_USE skips inactive sources (disconnected displays)
			if (!(pathInfo.flags & DISPLAYCONFIG_PATH_ACTIVE) ||
				!(pathInfo.sourceInfo.statusFlags & DISPLAYCONFIG_SOURCE_IN_USE))
				continue;

			// QDC_ONLY_ACTIVE_PATHS excludes virtual-mode paths; no index selection needed
			DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName{};
			sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
			sourceName.header.size = sizeof(sourceName);
			sourceName.header.adapterId = pathInfo.sourceInfo.adapterId;
			sourceName.header.id = pathInfo.sourceInfo.id;
			if (DisplayConfigGetDeviceInfo(&sourceName.header) == ERROR_SUCCESS) {
				if (wcscmp(sourceName.viewGdiDeviceName, deviceName) == 0) {
					outPathInfo = pathInfo;
					return true;
				}
			}
		}
		return false;
	}

	bool IsHDRSupportedAndEnabled(IDXGISwapChain* swapChain, bool& supported, bool& enabled)
	{
		supported = false;
		enabled = false;

		DISPLAYCONFIG_PATH_INFO pathInfo{};
		if (!GetDisplayConfigPathInfo(swapChain, pathInfo)) {
			// GetContainingOutput fails under frame-gen wrappers. Fall back to enumerating
			// the device adapter's outputs by HMONITOR. Only detects active HDR, not capable.
			HWND outputWindow = nullptr;
			DXGI_SWAP_CHAIN_DESC scDescFull{};
			if (SUCCEEDED(swapChain->GetDesc(&scDescFull)))
				outputWindow = scDescFull.OutputWindow;

			HMONITOR hMonitor = outputWindow ? MonitorFromWindow(outputWindow, MONITOR_DEFAULTTONEAREST) : nullptr;
			if (!hMonitor) {
				logger::warn("[HDR] HDR detection failed - cannot determine monitor from swap chain OutputWindow");
				return false;
			}

			// Enumerate outputs on the device's own adapter; avoids touching other GPUs.
			winrt::com_ptr<IDXGIDevice> dxgiDevice;
			winrt::com_ptr<IDXGIAdapter> adapter;
			if (globals::d3d::device &&
				SUCCEEDED(globals::d3d::device->QueryInterface(IID_PPV_ARGS(dxgiDevice.put()))) &&
				SUCCEEDED(dxgiDevice->GetAdapter(adapter.put()))) {
				for (UINT oi = 0;; ++oi) {
					winrt::com_ptr<IDXGIOutput> out;
					HRESULT hr = adapter->EnumOutputs(oi, out.put());
					if (hr == DXGI_ERROR_NOT_FOUND)
						break;
					if (FAILED(hr)) {
						logger::debug("[HDR] EnumOutputs failed: hr=0x{:08X}", static_cast<unsigned>(hr));
						break;
					}
					DXGI_OUTPUT_DESC outDesc{};
					if (FAILED(out->GetDesc(&outDesc)) || outDesc.Monitor != hMonitor)
						continue;
					winrt::com_ptr<IDXGIOutput6> out6;
					if (SUCCEEDED(out->QueryInterface(IID_PPV_ARGS(out6.put())))) {
						DXGI_OUTPUT_DESC1 desc1{};
						if (SUCCEEDED(out6->GetDesc1(&desc1))) {
							enabled = desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
							supported = enabled;
							logger::debug("[HDR] DXGI fallback detection: colorSpace={}", static_cast<int>(desc1.ColorSpace));
							return true;
						}
					}
				}
			}
			logger::warn("[HDR] HDR detection failed - cannot determine monitor (Streamline/frame-gen?)");
			return false;
		}

		// Try Windows 11 24H2+ API first - directly reports HDR hardware capability
		// Credits: renodx by clshortfuse (MIT License)
		// https://github.com/clshortfuse/renodx/blob/01f3739685ba8f850d82fb1a11a5ec1104e6b1b8/src/games/hollowknight-silksong/addon.cpp#L545
		Compat_DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 colorInfo2{};
		colorInfo2.header.type = kDisplayConfigGetAdvancedColorInfo2;
		colorInfo2.header.size = sizeof(colorInfo2);
		colorInfo2.header.adapterId = pathInfo.targetInfo.adapterId;
		colorInfo2.header.id = pathInfo.targetInfo.id;
		if (DisplayConfigGetDeviceInfo(&colorInfo2.header) == ERROR_SUCCESS) {
			supported = colorInfo2.highDynamicRangeSupported != 0;
			enabled = colorInfo2.activeColorMode == Compat_DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR;
			UINT32 hdrSupported = colorInfo2.highDynamicRangeSupported;
			UINT32 activeMode = static_cast<UINT32>(colorInfo2.activeColorMode);
			logger::debug("[HDR] Win11 24H2 detection: highDynamicRangeSupported={}, activeColorMode={}", hdrSupported, activeMode);
			return true;
		}

		// Fallback for older Windows versions
		DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO colorInfo{};
		colorInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
		colorInfo.header.size = sizeof(colorInfo);
		colorInfo.header.adapterId = pathInfo.targetInfo.adapterId;
		colorInfo.header.id = pathInfo.targetInfo.id;
		if (DisplayConfigGetDeviceInfo(&colorInfo.header) == ERROR_SUCCESS) {
			supported = colorInfo.advancedColorSupported != 0;

			DXGI_COLOR_SPACE_TYPE swapChainOutputColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
			{
				winrt::com_ptr<IDXGIOutput> containingOutput;
				if (SUCCEEDED(swapChain->GetContainingOutput(containingOutput.put()))) {
					winrt::com_ptr<IDXGIOutput6> out6;
					if (SUCCEEDED(containingOutput->QueryInterface(IID_PPV_ARGS(out6.put())))) {
						DXGI_OUTPUT_DESC1 desc1{};
						if (SUCCEEDED(out6->GetDesc1(&desc1)))
							swapChainOutputColorSpace = desc1.ColorSpace;
					}
				}
			}

			enabled = (colorInfo.advancedColorEnabled != 0) && (swapChainOutputColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);
			UINT32 advancedSupported = colorInfo.advancedColorSupported;
			UINT32 advancedEnabled = colorInfo.advancedColorEnabled;
			int colorSpaceInt = static_cast<int>(swapChainOutputColorSpace);
			logger::debug("[HDR] Legacy detection: advancedColorSupported={}, advancedColorEnabled={}, swapChainColorSpace={}, enabled={}",
				advancedSupported, advancedEnabled, colorSpaceInt, enabled);
			return true;
		}

		return false;
	}

	// Hook structs for the HDR pipeline - installed in PostPostLoad when Upscaling is not loaded.
	// When Upscaling IS loaded, it installs equivalent hooks covering the same addresses.
	struct HDR_Main_PostProcessing
	{
		static void thunk(RE::ImageSpaceManager* a_this, uint32_t a3, RE::RENDER_TARGET a_target, void* a_4, bool a_5)
		{
			auto* hdr = &globals::features::hdrDisplay;
			hdr->RedirectFramebuffer();
			func(a_this, a3, a_target, a_4, a_5);
			hdr->RestoreFramebuffer();
		}
		static inline REL::Relocation<decltype(thunk)> func;
	};

	struct HDR_MenuManagerDrawInterfaceStartHook
	{
		static void thunk(int64_t a1)
		{
			globals::features::hdrDisplay.SetUIBuffer();
			func(a1);
		}
		static inline REL::Relocation<decltype(thunk)> func;
	};
}

bool HDRDisplay::isHDRMonitor = false;
bool HDRDisplay::isHDRCapableMonitor = false;
bool HDRDisplay::wasExclusiveFullscreen = false;

bool HDRDisplay::DetectHDR()
{
	if (!globals::d3d::swapChain) {
		isHDRMonitor = false;
		isHDRCapableMonitor = false;
		return false;
	}

	bool hdrSupported = false;
	bool hdrEnabled = false;

	IsHDRSupportedAndEnabled(globals::d3d::swapChain, hdrSupported, hdrEnabled);

	isHDRMonitor = hdrEnabled;
	isHDRCapableMonitor = hdrSupported;
	logger::info("[HDR] HDR display detection: supported={}, enabled={}", hdrSupported, hdrEnabled);
	return hdrEnabled;
}

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
	HDRDisplay::Settings,
	enableHDR,
	hdrPaperWhite,
	hdrPeakNits,
	hdrUIBrightness,
	dontShowHDRWarning,
	hdrAutoDetected);

void HDRDisplay::DrawSettings()
{
	auto hdrWarningPopupTitle = std::format("{}##HDRDisplay", T(TKEY("warning_popup_title"), "HDR Warning"));

	if (isHDRMonitor) {
		Util::Text::Success(T(TKEY("display_detected"), "HDR Display Detected"));
	} else if (isHDRCapableMonitor) {
		Util::Text::Warning(T(TKEY("capable_display_windows_hdr_off"), "HDR Capable Display (Windows HDR is off)"));
		if (auto _tt = Util::HoverTooltipWrapper()) {
			ImGui::TextUnformatted(T(TKEY("capable_display_windows_hdr_off_tooltip_0"), "Your monitor supports HDR, but Windows HDR is currently disabled."));
			ImGui::TextUnformatted(T(TKEY("capable_display_windows_hdr_off_tooltip_1"), "Enable HDR in Windows Display Settings to allow auto-detection."));
		}
	} else {
		Util::Text::Warning(T(TKEY("sdr_display_not_detected"), "SDR Display (HDR not detected)"));
	}

	const bool isExclusiveFullscreen = globals::features::upscaling.loaded ? !globals::features::upscaling.isWindowed : wasExclusiveFullscreen;

	if (isExclusiveFullscreen) {
		ImGui::Spacing();
		Util::Text::WrappedWarning(T(TKEY("exclusive_fullscreen_warning"), "WARNING: Exclusive Fullscreen detected."));
		Util::Text::WrappedWarning(T(TKEY("exclusive_fullscreen_warning_detail"), "HDR is not compatible with Exclusive Fullscreen and may not work correctly. Switch to Borderless Windowed mode for proper HDR support."));
		ImGui::Spacing();
	}

	ImGui::Spacing();

	bool oldEnableHDR;
	bool currentEnableHDR;
	{
		std::lock_guard<std::mutex> lock(settingsMutex);
		oldEnableHDR = settings.enableHDR;
		currentEnableHDR = settings.enableHDR;
	}

	// Disable the checkbox only when no HDR monitor is detected AND HDR is not already on
	// (allow disabling HDR even on SDR if it was enabled from saved settings).
	if (!isHDRMonitor && !currentEnableHDR) {
		ImGui::BeginDisabled();
	}

	if (ImGui::Checkbox(T(TKEY("enable_hdr"), "Enable HDR"), &currentEnableHDR)) {
		{
			std::lock_guard<std::mutex> lock(settingsMutex);
			settings.enableHDR = currentEnableHDR;
			if (settings.enableHDR && !oldEnableHDR) {
				logger::info("HDR: enableHDR changed to: true");
				UpdateHDRData();
				UpdateSwapChainColorSpace();
			} else if (!settings.enableHDR && oldEnableHDR) {
				logger::info("HDR: enableHDR changed to: false");
				UpdateHDRData();
				UpdateSwapChainColorSpace();
			}
		}
	}

	if (!isHDRMonitor && !oldEnableHDR) {
		ImGui::EndDisabled();
	}

	if (auto _tt = Util::HoverTooltipWrapper()) {
		if (isHDRMonitor) {
			ImGui::TextUnformatted(T(TKEY("enable_hdr_tooltip"), "Enable HDR output. Matches vanilla visuals with extended dynamic range."));
		} else if (isHDRCapableMonitor) {
			ImGui::TextUnformatted(T(TKEY("enable_hdr_tooltip_windows_off"), "Monitor supports HDR but Windows HDR is off. Enable HDR in Windows Display Settings, then restart the game."));
		} else {
			ImGui::TextUnformatted(T(TKEY("enable_hdr_tooltip_not_detected"), "HDR display not detected. Use Advanced button to override."));
		}
	}

	// Advanced override button — shown when HDR is neither active nor auto-detected
	if (!isHDRMonitor && !oldEnableHDR) {
		ImGui::SameLine();
		if (ImGui::Button(T(TKEY("advanced"), "Advanced"))) {
			bool dontShowWarning;
			{
				std::lock_guard<std::mutex> lock(settingsMutex);
				dontShowWarning = settings.dontShowHDRWarning;
			}
			if (!dontShowWarning) {
				pendingHDREnable = true;
				showHDRWarningPopup = true;
				ImGui::OpenPopup(hdrWarningPopupTitle.c_str());
			} else {
				// User previously dismissed warnings, enable directly
				{
					std::lock_guard<std::mutex> lock(settingsMutex);
					settings.enableHDR = true;
					logger::info("HDR: enableHDR changed to: true (advanced override, warning suppressed)");
					UpdateHDRData();
					UpdateSwapChainColorSpace();
				}
			}
		}
		if (auto _tt = Util::HoverTooltipWrapper()) {
			if (isHDRCapableMonitor) {
				ImGui::TextUnformatted(T(TKEY("advanced_tooltip_enable_windows_hdr"), "Enable Windows HDR instead of forcing it here."));
			} else {
				ImGui::TextUnformatted(T(TKEY("advanced_tooltip_force_enable"), "Force enable HDR even without detection (not recommended)."));
			}
		}
	}

	{
		std::lock_guard<std::mutex> lock(settingsMutex);
		if (!isHDRMonitor && settings.enableHDR) {
			ImGui::Spacing();
			Util::Text::WrappedWarning(T(TKEY("enabled_without_detected_display"), "HDR is enabled but no HDR display was detected."));
		}
	}

	if (auto popup = Util::CenteredPopupModal(hdrWarningPopupTitle.c_str(), &showHDRWarningPopup, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
		// Prevent background dimming by pushing lower modal dimming
		ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1.0f);

		Util::Text::Warning(T(TKEY("force_enable_hdr_warning"), "WARNING: Force Enable HDR"));
		ImGui::Separator();
		ImGui::Spacing();
		Util::Text::WrappedWarning(T(TKEY("force_enable_hdr_detected_warning"), "HDR was not detected on your monitor."));
		Util::Text::WrappedWarning(T(TKEY("force_enable_hdr_sdr_warning"), "The game will look VERY WRONG on an SDR (standard) display."));
		ImGui::Spacing();
		ImGui::TextWrapped("%s", T(TKEY("force_enable_hdr_confirm"), "Only proceed if you have an HDR-capable display that was not detected correctly."));
		ImGui::Spacing();
		ImGui::Separator();
		ImGui::Spacing();

		const auto buttonWidthForLabel = [](const char* label) {
			return ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f;
		};
		const char* forceEnableLabel = T(TKEY("force_enable_hdr"), "Force Enable HDR");
		const char* cancelLabel = T(TKEY("cancel"), "Cancel");
		const float buttonWidth = std::max({
			ThemeManager::Constants::POPUP_BUTTON_WIDTH * Util::GetUIScale(),
			buttonWidthForLabel(forceEnableLabel),
			buttonWidthForLabel(cancelLabel)
		});

		if (ImGui::Button(forceEnableLabel, ImVec2(buttonWidth, 0))) {
			{
				std::lock_guard<std::mutex> lock(settingsMutex);
				settings.enableHDR = true;
				logger::info("HDR: enableHDR changed to: true (forced override)");
				UpdateHDRData();
				UpdateSwapChainColorSpace();
			}
			showHDRWarningPopup = false;
			pendingHDREnable = false;
			ImGui::CloseCurrentPopup();
		}
		ImGui::SameLine();
		if (ImGui::Button(cancelLabel, ImVec2(buttonWidth, 0))) {
			{
				std::lock_guard<std::mutex> lock(settingsMutex);
				settings.enableHDR = false;
			}
			showHDRWarningPopup = false;
			pendingHDREnable = false;
			ImGui::CloseCurrentPopup();
		}

		ImGui::Spacing();
		ImGui::Separator();
		ImGui::Spacing();

		bool dontShowWarning;
		{
			std::lock_guard<std::mutex> lock(settingsMutex);
			dontShowWarning = settings.dontShowHDRWarning;
		}
		ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, ImGui::GetStyle().FramePadding.y * 0.5f));
		ImGui::SetWindowFontScale(0.9f);
		if (ImGui::Checkbox(T(TKEY("dont_show_again"), "Don't show me this again"), &dontShowWarning)) {
			std::lock_guard<std::mutex> lock(settingsMutex);
			settings.dontShowHDRWarning = dontShowWarning;
		}
		ImGui::SetWindowFontScale(1.0f);
		ImGui::PopStyleVar();

		ImGui::PopStyleVar();
	}

	bool isHDREnabled;
	{
		std::lock_guard<std::mutex> lock(settingsMutex);
		isHDREnabled = settings.enableHDR;
	}

	if (isHDREnabled) {
		ImGui::Spacing();

		uint oldPaperWhite;
		uint currentPaperWhite;
		uint oldPeakNits;
		uint currentPeakNits;
		{
			std::lock_guard<std::mutex> lock(settingsMutex);
			oldPaperWhite = settings.hdrPaperWhite;
			currentPaperWhite = settings.hdrPaperWhite;
			oldPeakNits = settings.hdrPeakNits;
			currentPeakNits = settings.hdrPeakNits;
		}

		ImGui::SliderInt(T(TKEY("paper_white_nits"), "Paper White (nits)"), reinterpret_cast<int*>(&currentPaperWhite), 80, 500);
		{
			std::lock_guard<std::mutex> lock(settingsMutex);
			if (currentPaperWhite >= settings.hdrPeakNits) {
				currentPaperWhite = settings.hdrPeakNits - 1;
			}
			settings.hdrPaperWhite = currentPaperWhite;
			if (oldPaperWhite != settings.hdrPaperWhite) {
				UpdateHDRData();
			}
		}
		if (auto _tt = Util::HoverTooltipWrapper()) {
			ImGui::TextUnformatted(T(TKEY("paper_white_tooltip_0"), "How bright SDR white appears on your HDR display."));
			ImGui::TextUnformatted(T(TKEY("paper_white_tooltip_1"), "203 nits is the ITU BT.2408 reference. Increase for a brighter image."));
		}

		ImGui::SliderInt(T(TKEY("peak_brightness_nits"), "Peak Brightness (nits)"), reinterpret_cast<int*>(&currentPeakNits), 400, 10000);
		{
			std::lock_guard<std::mutex> lock(settingsMutex);
			if (currentPeakNits <= settings.hdrPaperWhite) {
				currentPeakNits = settings.hdrPaperWhite + 1;
			}
			settings.hdrPeakNits = currentPeakNits;
			if (oldPeakNits != settings.hdrPeakNits) {
				UpdateHDRData();
			}
		}
		if (auto _tt = Util::HoverTooltipWrapper()) {
			ImGui::TextUnformatted(T(TKEY("peak_brightness_tooltip_0"), "Maximum brightness your display can produce."));
			ImGui::TextUnformatted(T(TKEY("peak_brightness_tooltip_1"), "Set to match your display's actual peak brightness."));
		}

		ImGui::TextDisabled(T(TKEY("display_reports_max_nits"), "Display reports: %.0f nits max"), cachedDisplayMaxLuminance);
		if (auto _tt = Util::HoverTooltipWrapper()) {
			ImGui::TextUnformatted(T(TKEY("display_reports_max_nits_tooltip_0"), "Reported by OS/driver (DXGI MaxLuminance), not a direct meter reading."));
			ImGui::TextUnformatted(T(TKEY("display_reports_max_nits_tooltip_1"), "It may be EDID metadata and can differ from real highlight peak output."));
			ImGui::TextUnformatted(T(TKEY("display_reports_max_nits_tooltip_2"), "Treat this as a starting point and tune Peak Brightness as needed."));
		}
	}

	// UI brightness slider - only shown when HDR is enabled
	ImGui::Spacing();
	{
		std::lock_guard<std::mutex> lock(settingsMutex);
		if (settings.enableHDR) {
			float oldUIBrightness = settings.hdrUIBrightness;
			float currentUIBrightness = settings.hdrUIBrightness;

			ImGui::SliderFloat(T(TKEY("ui_brightness_multiplier"), "UI Brightness Multiplier"), &currentUIBrightness, 0.5f, 5.0f, "%.2fx");
			if (oldUIBrightness != currentUIBrightness) {
				settings.hdrUIBrightness = currentUIBrightness;
				UpdateHDRData();
			}
			if (auto _tt = Util::HoverTooltipWrapper()) {
				ImGui::TextUnformatted(T(TKEY("ui_brightness_multiplier_tooltip_0"), "UI brightness = Paper White x this multiplier in HDR mode."));
				ImGui::TextUnformatted(T(TKEY("ui_brightness_multiplier_tooltip_1"), "1.00x = UI renders at Paper White brightness. Higher values make UI brighter relative to scene content."));
				ImGui::TextUnformatted(T(TKEY("ui_brightness_multiplier_tooltip_2"), "Note: Main menu and loading screens always render at Paper White brightness."));
			}
		}
	}
}

#undef I18N_KEY_PREFIX

void HDRDisplay::SaveSettings(json& o_json)
{
	std::lock_guard<std::mutex> lock(settingsMutex);
	o_json = settings;
}

void HDRDisplay::LoadSettings(json& o_json)
{
	std::lock_guard<std::mutex> lock(settingsMutex);

	bool oldEnableHDR = settings.enableHDR;

	settings = o_json;

	// Defer auto-detection to SetupResources where the swap chain is available.
	// DetectHDR() needs globals::d3d::swapChain which isn't valid during early plugin init.
	// hdrAutoDetected starts false in defaults and is only set true after auto-detect
	// completes in SetupResources, so this correctly triggers on first launch even
	// when the default config was auto-generated with enableHDR: false.
	if (!settings.hdrAutoDetected) {
		pendingAutoDetect = true;
		logger::info("[HDR] Auto-detection not yet run - deferring to SetupResources");
	}

	if (settings.enableHDR != oldEnableHDR) {
		UpdateHDRData();
		UpdateSwapChainColorSpace();
	}
}

void HDRDisplay::RestoreDefaultSettings()
{
	bool hdrMonitor = DetectHDR();
	settings.enableHDR = hdrMonitor;
	settings.hdrPaperWhite = 203;
	settings.hdrPeakNits = 1000;
	settings.hdrUIBrightness = 1.0f;
	settings.dontShowHDRWarning = false;
}

void HDRDisplay::DataLoaded()
{
	// Use Skyrim's built-in ini setting to upgrade all HDR render targets to 16-bit float format.
	auto setting = RE::GetINISetting("bUse64bitsHDRRenderTarget:Display");
	if (setting) {
		setting->data.b = true;
		logger::info("[HDR Display] Enabled bUse64bitsHDRRenderTarget - all required render targets will use R16G16B16A16_FLOAT");
	} else {
		logger::warn("[HDR Display] bUse64bitsHDRRenderTarget ini setting not found");
	}
}

void HDRDisplay::PostPostLoad()
{
	// When Upscaling is loaded it installs equivalent hooks for these same addresses in its own
	// PostPostLoad. Only install here when Upscaling is absent.
	if (!globals::features::upscaling.loaded) {
		logger::info("[HDR Display] Installing HDR pipeline hooks (Upscaling not loaded)");
		stl::detour_thunk<HDR_MenuManagerDrawInterfaceStartHook>(REL::RelocationID(79947, 82084));
		stl::write_thunk_call<HDR_Main_PostProcessing>(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7));
	}
}

void HDRDisplay::SetupResources()
{
	if (hdrTexture || outputTexture || uiTexture || hdrDataCB) {
		DestroyResources();
	}

	DetectHDR();

	if (pendingAutoDetect) {
		pendingAutoDetect = false;
		std::lock_guard<std::mutex> lock(settingsMutex);
		settings.enableHDR = isHDRMonitor;
		settings.hdrAutoDetected = true;
		logger::info("[HDR] Auto-configured HDR based on display: {}", isHDRMonitor ? "enabled" : "disabled");
	}

	cachedDisplayMaxLuminance = GetDisplayMaxLuminance();

	// Set up swap chain color space BEFORE querying format and creating textures
	// This ensures outputTexture matches the actual swap chain format for CopyResource
	UpdateSwapChainColorSpace();

	auto renderer = globals::game::renderer;
	auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN];

	D3D11_TEXTURE2D_DESC texDesc{};
	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};

	main.texture->GetDesc(&texDesc);
	main.SRV->GetDesc(&srvDesc);
	main.UAV->GetDesc(&uavDesc);

	// Get the actual swap chain format for output texture
	DXGI_FORMAT swapChainFormat = DXGI_FORMAT_R10G10B10A2_UNORM;  // HDR format
	DXGI_SWAP_CHAIN_DESC scDesc;
	if (SUCCEEDED(globals::d3d::swapChain->GetDesc(&scDesc))) {
		swapChainFormat = scDesc.BufferDesc.Format;
		logger::info("[HDR] Swap chain format: {} ({})", (int)swapChainFormat,
			swapChainFormat == DXGI_FORMAT_R10G10B10A2_UNORM  ? "R10G10B10A2_UNORM (HDR10)" :
			swapChainFormat == DXGI_FORMAT_R16G16B16A16_FLOAT ? "R16G16B16A16_FLOAT (scRGB)" :
			swapChainFormat == DXGI_FORMAT_R8G8B8A8_UNORM     ? "R8G8B8A8_UNORM (SDR 8-bit)" :
			swapChainFormat == DXGI_FORMAT_B8G8R8A8_UNORM     ? "B8G8R8A8_UNORM (SDR 8-bit)" :
																"other");
	}

	texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
	srvDesc.Format = texDesc.Format;
	uavDesc.Format = texDesc.Format;

	hdrTexture = new Texture2D(texDesc, "HDR::HdrTexture");
	hdrTexture->CreateSRV(srvDesc);
	hdrTexture->CreateUAV(uavDesc);

	// RTV so ISHDR can render directly into this float texture
	D3D11_RENDER_TARGET_VIEW_DESC hdrRtvDesc{};
	hdrRtvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
	hdrRtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
	hdrRtvDesc.Texture2D.MipSlice = 0;
	hdrTexture->CreateRTV(hdrRtvDesc);

	// Output texture must match the swap chain format: ApplyHDR does
	// CopyResource(backBuffer, outputTexture) and CopyResource requires identical formats.
	texDesc.Format = swapChainFormat;
	srvDesc.Format = texDesc.Format;
	uavDesc.Format = texDesc.Format;

	outputTexture = new Texture2D(texDesc, "HDR::OutputTexture");
	outputTexture->CreateSRV(srvDesc);
	outputTexture->CreateUAV(uavDesc);

	// UI texture for separate UI rendering
	// Use R8G8B8A8_UNORM (8-bit SDR) - vanilla UI is SDR and 8-bit precision
	// naturally truncates near-black ghost bar artifacts to zero
	D3D11_TEXTURE2D_DESC uiTexDesc = texDesc;
	uiTexDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	uiTexDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS;

	D3D11_SHADER_RESOURCE_VIEW_DESC uiSrvDesc = srvDesc;
	uiSrvDesc.Format = uiTexDesc.Format;

	D3D11_UNORDERED_ACCESS_VIEW_DESC uiUavDesc = uavDesc;
	uiUavDesc.Format = uiTexDesc.Format;

	uiTexture = new Texture2D(uiTexDesc, "HDR::UiTexture");
	uiTexture->CreateSRV(uiSrvDesc);
	uiTexture->CreateUAV(uiUavDesc);

	D3D11_RENDER_TARGET_VIEW_DESC rtvDesc{};
	rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
	rtvDesc.Texture2D.MipSlice = 0;
	uiTexture->CreateRTV(rtvDesc);

	hdrDataCB = new ConstantBuffer(ConstantBufferDesc<HDRDataCB>(), "HDR::DataCB");

	UpdateHDRData();

	GetHDROutputCS();
	GetUIBrightnessCS();

	UpgradeLDRRenderTargets();
}

void HDRDisplay::BeginUIRendering()
{
	// Skip if D3D12 frame gen is active - it has its own UI buffer handling
	if (globals::features::upscaling.d3d12SwapChainActive)
		return;

	if (renderingUI)
		return;

	if (!uiTexture || !uiTexture->rtv)
		return;

	auto context = globals::d3d::context;

	if (savedRTV) {
		savedRTV->Release();
		savedRTV = nullptr;
	}
	if (savedDSV) {
		savedDSV->Release();
		savedDSV = nullptr;
	}

	context->OMGetRenderTargets(1, &savedRTV, &savedDSV);

	// Do NOT clear - vanilla UI has already rendered to uiTexture via SetUIBuffer()
	// Just ensure ImGui also renders to the same texture
	ID3D11RenderTargetView* rtv = uiTexture->rtv.get();
	context->OMSetRenderTargets(1, &rtv, nullptr);

	renderingUI = true;
}

void HDRDisplay::EndUIRendering()
{
	if (globals::features::upscaling.d3d12SwapChainActive)
		return;

	if (!renderingUI)
		return;

	auto context = globals::d3d::context;

	context->OMSetRenderTargets(1, &savedRTV, savedDSV);

	if (savedRTV) {
		savedRTV->Release();
		savedRTV = nullptr;
	}
	if (savedDSV) {
		savedDSV->Release();
		savedDSV = nullptr;
	}

	renderingUI = false;
}

void HDRDisplay::RedirectFramebuffer()
{
	if (!settings.enableHDR || !hdrTexture || !hdrTexture->rtv)
		return;

	if (!GetHDROutputCS())
		return;

	if (framebufferRedirected)
		return;

	auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];

	savedFramebufferTexture = fb.texture;
	savedFramebufferSRV = fb.SRV;
	savedFramebufferRTV = fb.RTV;

	// Redirect to hdrTexture (R16G16B16A16_FLOAT) so ISHDR can write values >1.0
	fb.texture = reinterpret_cast<ID3D11Texture2D*>(hdrTexture->resource.get());
	fb.SRV = hdrTexture->srv.get();
	fb.RTV = hdrTexture->rtv.get();

	framebufferRedirected = true;
}

void HDRDisplay::RestoreFramebuffer()
{
	if (!framebufferRedirected)
		return;

	auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];

	fb.texture = savedFramebufferTexture;
	fb.SRV = savedFramebufferSRV;
	fb.RTV = savedFramebufferRTV;

	savedFramebufferTexture = nullptr;
	savedFramebufferSRV = nullptr;
	savedFramebufferRTV = nullptr;
	framebufferRedirected = false;
}

bool HDRDisplay::IsFGCompositingThisFrame() const
{
	return globals::features::upscaling.ShouldUseFrameGenerationThisFrame();
}

HDRDisplay::D3D12UIBufferMode HDRDisplay::GetD3D12UIBufferMode()
{
	D3D12UIBufferMode mode;
	if (!globals::features::upscaling.d3d12SwapChainActive)
		return mode;

	const bool hdrReady = loaded && hdrDataCB && outputTexture;
	const bool hdrShaderAvailable = hdrReady && GetHDROutputCS() != nullptr;

	mode.useUIBuffer = hdrShaderAvailable || IsFGCompositingThisFrame();
	mode.useFallbackCopy = hdrReady && !hdrShaderAvailable;
	return mode;
}

bool HDRDisplay::ShouldUseD3D12UIBuffer()
{
	return GetD3D12UIBufferMode().useUIBuffer;
}

void HDRDisplay::SetUIBuffer()
{
	auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];

	// D3D12 swap chain path: route UI to uiBufferWrapped only when a compositor
	// (ApplyHDR or FFX FG UI composition) will read it; otherwise render UI
	// directly into the wrapped back buffer so it survives Present when both
	// compositors are skipped (HDR unloaded + FG off/paused). If HDR is loaded
	// but the shader is missing, keep UI in kFRAMEBUFFER so the ApplyHDR
	// fallback copy carries it to the wrapped back buffer.
	if (globals::features::upscaling.d3d12SwapChainActive) {
		auto& upscaling = globals::features::upscaling;
		if (!upscaling.dx12SwapChain.swapChainBufferWrapped || !upscaling.dx12SwapChain.swapChainBufferWrapped->rtv)
			return;

		const auto uiBufferMode = GetD3D12UIBufferMode();

		if (uiBufferMode.useUIBuffer && (!upscaling.dx12SwapChain.uiBufferWrapped || !upscaling.dx12SwapChain.uiBufferWrapped->rtv))
			return;

		ID3D11RenderTargetView* targetRTV = uiBufferMode.useUIBuffer ?
		                                        upscaling.dx12SwapChain.uiBufferWrapped->rtv :
		                                    uiBufferMode.useFallbackCopy ? fb.RTV :
		                                                                   upscaling.dx12SwapChain.swapChainBufferWrapped->rtv;

		if (uiBufferMode.useUIBuffer) {
			float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
			globals::d3d::context->ClearRenderTargetView(targetRTV, clearColor);
		}

		fb.RTV = targetRTV;
		globals::d3d::context->OMSetRenderTargets(1, &fb.RTV, nullptr);
		return;
	}

	// SDR mode: vanilla UI composites directly to kFRAMEBUFFER, no redirect needed
	if (!settings.enableHDR)
		return;

	// Don't redirect if the HDR compute shader isn't available - vanilla UI path works without it
	if (!GetHDROutputCS())
		return;

	if (!uiTexture || !uiTexture->rtv || !hdrDataCB || !outputTexture)
		return;

	if (!savedFramebufferRTV) {
		savedFramebufferRTV = fb.RTV;
	}

	float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
	globals::d3d::context->ClearRenderTargetView(uiTexture->rtv.get(), clearColor);

	fb.RTV = uiTexture->rtv.get();
	globals::d3d::context->OMSetRenderTargets(1, &fb.RTV, nullptr);
}

bool HDRDisplay::UsesDeferredPresentComposite() const
{
	return loaded && settings.enableHDR &&
	       !globals::features::upscaling.d3d12SwapChainActive && uiTexture && uiTexture->rtv && hdrOutputCS;
}

void HDRDisplay::SyncFramebufferUIRedirect()
{
	if (!uiTexture || !uiTexture->rtv)
		return;

	auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];
	fb.RTV = uiTexture->rtv.get();
	globals::d3d::context->OMSetRenderTargets(1, &fb.RTV, nullptr);
}

namespace
{
	struct PresentSuppressionScope
	{
		PresentSuppressionScope() { globals::features::hdrDisplay.SetPresentSuppressed(true); }
		~PresentSuppressionScope() { globals::features::hdrDisplay.SetPresentSuppressed(false); }
	};

	struct SwapChainPresentBottom
	{
		static HRESULT WINAPI thunk(IDXGISwapChain* This, UINT SyncInterval, UINT Flags)
		{
			if (globals::features::hdrDisplay.IsPresentSuppressed())
				return S_OK;

			return func(This, SyncInterval, Flags);
		}
		static inline REL::Relocation<decltype(thunk)> func;
	};

	// Vtable slot 35 — patches alpha blend to [One, InvSrcAlpha, Add] when UI is composited from a
	// separate buffer. Mods using [InvSrcAlpha, Zero] (e.g. IED) write alpha*(1-alpha) instead
	// of alpha, causing ~94% scene bleed through opaque windows in the HDROutputCS composite.
	struct ID3D11DeviceContext_OMSetBlendState
	{
		static void WINAPI thunk(ID3D11DeviceContext* This, ID3D11BlendState* pBlendState, const FLOAT BlendFactor[4], UINT SampleMask)
		{
			if (pBlendState) {
				auto& hdr = globals::features::hdrDisplay;
				const bool d3d11HdrCapture = hdr.loaded && hdr.settings.enableHDR && hdr.uiTexture;
				const bool fgCapture = globals::features::upscaling.d3d12SwapChainActive;
				if (d3d11HdrCapture || fgCapture)
					pBlendState = hdr.GetPatchedAlphaBlendState(pBlendState);
			}
			func(This, pBlendState, BlendFactor, SampleMask);
		}
		static inline REL::Relocation<decltype(thunk)> func;
	};
}

void HDRDisplay::InstallSwapChainPresentHooks(IDXGISwapChain* swapChain)
{
	stl::detour_vfunc<8, SwapChainPresentBottom>(swapChain);
	stl::detour_vfunc<35, ID3D11DeviceContext_OMSetBlendState>(globals::d3d::context);
}

ID3D11BlendState* HDRDisplay::GetPatchedAlphaBlendState(ID3D11BlendState* original)
{
	auto it = patchedBlendStateCache.find(original);
	if (it != patchedBlendStateCache.end())
		return it->second ? it->second.get() : original;

	D3D11_BLEND_DESC desc{};
	original->GetDesc(&desc);

	const int slotCount = desc.IndependentBlendEnable ? 8 : 1;
	bool needsPatch = false;
	for (int i = 0; i < slotCount; i++) {
		const auto& rt = desc.RenderTarget[i];
		if (rt.BlendEnable &&
			(rt.SrcBlendAlpha != D3D11_BLEND_ONE ||
				rt.DestBlendAlpha != D3D11_BLEND_INV_SRC_ALPHA ||
				rt.BlendOpAlpha != D3D11_BLEND_OP_ADD)) {
			needsPatch = true;
			break;
		}
	}

	if (!needsPatch) {
		patchedBlendStateCache[original] = nullptr;
		return original;
	}

	for (int i = 0; i < slotCount; i++) {
		auto& rt = desc.RenderTarget[i];
		if (rt.BlendEnable) {
			rt.SrcBlendAlpha = D3D11_BLEND_ONE;
			rt.DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
			rt.BlendOpAlpha = D3D11_BLEND_OP_ADD;
		}
	}
	if (!desc.IndependentBlendEnable) {
		for (int i = 1; i < 8; i++)
			desc.RenderTarget[i] = desc.RenderTarget[0];
	}

	winrt::com_ptr<ID3D11BlendState> patched;
	if (FAILED(globals::d3d::device->CreateBlendState(&desc, patched.put()))) {
		patchedBlendStateCache[original] = nullptr;
		return original;
	}

	auto* raw = patched.get();
	patchedBlendStateCache[original] = std::move(patched);
	return raw;
}

HRESULT HDRDisplay::PresentToSwapChain(IDXGISwapChain* swapChain, UINT syncInterval, UINT flags)
{
	return SwapChainPresentBottom::func(swapChain, syncInterval, flags);
}

void HDRDisplay::DrawImGuiForPresent(bool frameGenActive, bool hdrReady)
{
	if (frameGenActive) {
		auto& data = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];
		globals::d3d::context->OMSetRenderTargets(1, &data.RTV, nullptr);
	} else if (hdrReady && uiTexture && uiTexture->rtv && uiTexture->resource) {
		ID3D11RenderTargetView* uiRTV = uiTexture->rtv.get();
		D3D11_TEXTURE2D_DESC texDesc{};
		uiTexture->resource->GetDesc(&texDesc);

		if (texDesc.Width > 0) {
			globals::d3d::context->OMSetRenderTargets(1, &uiRTV, nullptr);

			D3D11_VIEWPORT uiViewport{};
			uiViewport.Width = static_cast<float>(texDesc.Width);
			uiViewport.Height = static_cast<float>(texDesc.Height);
			uiViewport.MinDepth = 0.0f;
			uiViewport.MaxDepth = 1.0f;
			globals::d3d::context->RSSetViewports(1, &uiViewport);
		}
	} else {
		auto& data = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];
		globals::d3d::context->OMSetRenderTargets(1, &data.RTV, nullptr);
	}
}

void HDRDisplay::RunHDRBeforePresentChain(bool hdrReady)
{
	if (!hdrReady)
		return;

	ID3D11RenderTargetView* nullRTV = nullptr;
	globals::d3d::context->OMSetRenderTargets(1, &nullRTV, nullptr);
	ApplyHDR();
}

HRESULT HDRDisplay::RunPresentChainWithHDR(
	IDXGISwapChain* swapChain,
	UINT syncInterval,
	UINT flags,
	bool hdrReady,
	bool frameGenActive,
	const std::function<HRESULT(IDXGISwapChain*, UINT, UINT)>& presentChain)
{
	if (UsesDeferredPresentComposite()) {
		SyncFramebufferUIRedirect();
		{
			PresentSuppressionScope suppress;
			const HRESULT suppressedResult = presentChain(swapChain, syncInterval, flags);
			if (FAILED(suppressedResult))
				logger::warn("Suppressed presentChain returned {:08X} (expected S_OK)", static_cast<unsigned>(suppressedResult));
		}

		ID3D11RenderTargetView* nullRTV = nullptr;
		globals::d3d::context->OMSetRenderTargets(1, &nullRTV, nullptr);
		ApplyHDR();
		const HRESULT retval = PresentToSwapChain(swapChain, syncInterval, flags);
		ClearUIBuffer();
		return retval;
	}

	RunHDRBeforePresentChain(hdrReady);

	if (hdrReady) {
		if (!frameGenActive) {
			ClearUIBuffer();
		}
		auto& data = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];
		globals::d3d::context->OMSetRenderTargets(1, &data.RTV, nullptr);
	}

	return presentChain(swapChain, syncInterval, flags);
}

HRESULT HDRDisplay::HandleSwapChainPresent(
	IDXGISwapChain* swapChain,
	UINT syncInterval,
	UINT flags,
	const std::function<HRESULT(IDXGISwapChain*, UINT, UINT)>& presentChain)
{
	const bool frameGenActive = globals::features::upscaling.d3d12SwapChainActive;
	const bool hdrReady = loaded && hdrDataCB && outputTexture && (settings.enableHDR || frameGenActive);

	D3D11_VIEWPORT savedViewport{};
	UINT viewportCount = 1;
	globals::d3d::context->RSGetViewports(&viewportCount, &savedViewport);

	DrawImGuiForPresent(frameGenActive, hdrReady);
	globals::menu->DrawOverlay();
	globals::d3d::context->RSSetViewports(1, &savedViewport);

	return RunPresentChainWithHDR(swapChain, syncInterval, flags, hdrReady, frameGenActive, presentChain);
}

void HDRDisplay::ClearUIBuffer()
{
	if (globals::features::upscaling.d3d12SwapChainActive)
		return;

	if (!uiTexture || !uiTexture->rtv)
		return;

	float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
	globals::d3d::context->ClearRenderTargetView(uiTexture->rtv.get(), clearColor);

	if (savedFramebufferRTV) {
		auto& data = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER];
		data.RTV = savedFramebufferRTV;
		savedFramebufferRTV = nullptr;
	}
}

void HDRDisplay::ApplyHDR()
{
	ZoneScoped;
	TracyD3D11Zone(globals::state->tracyCtx, "HDR Processing");

	std::lock_guard<std::mutex> lock(settingsMutex);

	if (!hdrDataCB || !hdrTexture || !outputTexture)
		return;

	auto& upscaling = globals::features::upscaling;

	auto context = globals::d3d::context;
	auto state = globals::state;
	auto renderer = globals::game::renderer;

	UpdateHDRData();

	state->BeginPerfEvent("HDR Processing");

	{
		// When HDR is enabled, ISHDR wrote to hdrTexture (float16, values >1.0 preserved).
		// When SDR, ISHDR wrote to kFRAMEBUFFER (UNORM, tonemapped 0-1).
		auto& framebufferRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER];

		// Scene SRV selection:
		// - HDR: hdrTexture has float16 scene values >1.0 preserved from ISHDR.
		// - SDR: kFRAMEBUFFER has the tonemapped 0-1 ISHDR output.
		ID3D11ShaderResourceView* sceneSRV =
			(settings.enableHDR && hdrTexture && hdrTexture->srv) ? hdrTexture->srv.get() :
																	framebufferRT.SRV;

		// Choose the correct UI buffer based on which path is active.
		ID3D11ShaderResourceView* uiSRV = nullptr;
		if (upscaling.d3d12SwapChainActive && upscaling.dx12SwapChain.uiBufferWrapped) {
			uiSRV = upscaling.dx12SwapChain.uiBufferWrapped->srv;
		} else if (uiTexture && uiTexture->srv) {
			uiSRV = uiTexture->srv.get();
		}

		if (!GetHDROutputCS()) {
			// Fallback: HDR shader files not present - copy kFRAMEBUFFER directly to output
			if (upscaling.d3d12SwapChainActive) {
				// SetUIBuffer keeps non-FG fallback UI in kFRAMEBUFFER; FG keeps using
				// uiBufferWrapped for FidelityFX UI composition.
				context->CopyResource(upscaling.dx12SwapChain.swapChainBufferWrapped->resource11, framebufferRT.texture);
			} else {
				ID3D11Texture2D* backBuffer = nullptr;
				HRESULT hr = globals::d3d::swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
				if (SUCCEEDED(hr) && backBuffer) {
					context->CopyResource(backBuffer, framebufferRT.texture);
					backBuffer->Release();
				}
			}

			state->EndPerfEvent();
			return;
		}

		DispatchHDROutput(sceneSRV, uiSRV, outputTexture->uav.get());
	}

	if (upscaling.d3d12SwapChainActive) {
		if (upscaling.dx12SwapChain.swapChainBufferWrapped &&
			upscaling.dx12SwapChain.swapChainBufferWrapped->resource11 &&
			outputTexture && outputTexture->resource) {
			context->CopyResource(upscaling.dx12SwapChain.swapChainBufferWrapped->resource11, outputTexture->resource.get());
		}
	} else {
		ID3D11Texture2D* backBuffer = nullptr;
		HRESULT hr = globals::d3d::swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
		if (SUCCEEDED(hr) && backBuffer) {
			context->CopyResource(backBuffer, outputTexture->resource.get());
			backBuffer->Release();
		}
	}

	state->EndPerfEvent();
}

void HDRDisplay::DispatchHDROutput(ID3D11ShaderResourceView* sceneSRV, ID3D11ShaderResourceView* uiSRV, ID3D11UnorderedAccessView* uav)
{
	auto context = globals::d3d::context;
	auto computeShader = GetHDROutputCS();
	if (!computeShader || !uav)
		return;

	ID3D11ShaderResourceView* views[2] = { sceneSRV, uiSRV };
	context->CSSetShaderResources(0, ARRAYSIZE(views), views);

	ID3D11UnorderedAccessView* uavs[1] = { uav };
	context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr);

	ID3D11Buffer* cbs[1] = { hdrDataCB->CB() };
	context->CSSetConstantBuffers(0, ARRAYSIZE(cbs), cbs);

	context->CSSetShader(computeShader, nullptr, 0);

	auto dispatchCount = Util::GetScreenDispatchCount(false);
	globals::profiler->BeginPass("HDRDisplay::HDROutput");
	context->Dispatch(dispatchCount.x, dispatchCount.y, 1);
	globals::profiler->EndPass();

	views[0] = nullptr;
	views[1] = nullptr;
	context->CSSetShaderResources(0, ARRAYSIZE(views), views);

	uavs[0] = nullptr;
	context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr);

	cbs[0] = nullptr;
	context->CSSetConstantBuffers(0, ARRAYSIZE(cbs), cbs);

	context->CSSetShader(nullptr, nullptr, 0);
}

void HDRDisplay::SnapshotCleanScene()
{
	if (!hdrTexture || !hdrTexture->resource)
		return;

	const D3D11_TEXTURE2D_DESC& sceneDesc = hdrTexture->desc;

	// (Re)create on first use or when the scene texture changes.
	if (cleanSceneCapture) {
		const D3D11_TEXTURE2D_DESC& capDesc = cleanSceneCapture->desc;
		if (capDesc.Width != sceneDesc.Width || capDesc.Height != sceneDesc.Height || capDesc.Format != sceneDesc.Format) {
			cleanSceneCapture->srv = nullptr;
			cleanSceneCapture->resource = nullptr;
			delete cleanSceneCapture;
			cleanSceneCapture = nullptr;
		}
	}

	if (!cleanSceneCapture) {
		D3D11_TEXTURE2D_DESC capDesc = sceneDesc;
		capDesc.MipLevels = 1;
		capDesc.ArraySize = 1;
		capDesc.SampleDesc.Count = 1;
		capDesc.SampleDesc.Quality = 0;
		capDesc.Usage = D3D11_USAGE_DEFAULT;
		capDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
		capDesc.CPUAccessFlags = 0;
		capDesc.MiscFlags = 0;

		cleanSceneCapture = new Texture2D(capDesc, "HDR::CleanSceneCapture");

		D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{};
		srvDesc.Format = capDesc.Format;
		srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
		srvDesc.Texture2D.MipLevels = 1;
		cleanSceneCapture->CreateSRV(srvDesc);
	}

	globals::d3d::context->CopyResource(cleanSceneCapture->resource.get(), hdrTexture->resource.get());
	cleanSceneCaptureFrame = globals::state->frameCount;
}

bool HDRDisplay::IsCleanSceneCaptureFresh() const
{
	return cleanSceneCapture && cleanSceneCapture->srv && cleanSceneCaptureFrame == globals::state->frameCount;
}

ID3D11Texture2D* HDRDisplay::ComposeCleanCapture(ID3D11ShaderResourceView* sceneSRV, bool sdrPreview)
{
	std::lock_guard<std::mutex> lock(settingsMutex);

	if (!settings.enableHDR || !sceneSRV || !hdrDataCB || !outputTexture || !outputTexture->uav || !outputTexture->resource)
		return nullptr;

	if (!GetHDROutputCS())
		return nullptr;

	// Null UI SRV (t1 samples as 0) leaves the scene alone: no UI, menu, or blur.
	HDRDataCB data = BuildHDRData();
	data.previewSDR = sdrPreview ? 1.f : 0.f;
	hdrDataCB->Update(data);

	DispatchHDROutput(sceneSRV, nullptr, outputTexture->uav.get());

	// Restore the canonical CB for the display composite this frame.
	UpdateHDRData();

	return outputTexture->resource.get();
}

void HDRDisplay::DestroyResources()
{
	if (hdrTexture) {
		hdrTexture->srv = nullptr;
		hdrTexture->uav = nullptr;
		hdrTexture->rtv = nullptr;
		hdrTexture->resource = nullptr;
		delete hdrTexture;
		hdrTexture = nullptr;
	}

	if (outputTexture) {
		outputTexture->srv = nullptr;
		outputTexture->uav = nullptr;
		outputTexture->resource = nullptr;
		delete outputTexture;
		outputTexture = nullptr;
	}

	if (uiTexture) {
		uiTexture->srv = nullptr;
		uiTexture->uav = nullptr;
		uiTexture->rtv = nullptr;
		uiTexture->resource = nullptr;
		delete uiTexture;
		uiTexture = nullptr;
	}

	if (cleanSceneCapture) {
		cleanSceneCapture->srv = nullptr;
		cleanSceneCapture->resource = nullptr;
		delete cleanSceneCapture;
		cleanSceneCapture = nullptr;
		cleanSceneCaptureFrame = UINT32_MAX;
	}

	if (hdrDataCB) {
		delete hdrDataCB;
		hdrDataCB = nullptr;
	}

	RestoreLDRRenderTargets();
}

void HDRDisplay::UpgradeLDRRenderTargets()
{
	auto renderer = globals::game::renderer;
	auto device = globals::d3d::device;

	static const RE::RENDER_TARGETS::RENDER_TARGET ldrTargets[] = {
		RE::RENDER_TARGETS::kLDR_DOWNSAMPLE0,
		RE::RENDER_TARGETS::kLDR_BLURSWAP,
		RE::RENDER_TARGETS::kBLURFULL_BUFFER,
		RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY,
		RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY2,
		RE::RENDER_TARGETS::kTEMPORAL_AA_UI_ACCUMULATION_1,
		RE::RENDER_TARGETS::kTEMPORAL_AA_UI_ACCUMULATION_2,
	};

	for (auto targetId : ldrTargets) {
		auto& rt = renderer->GetRuntimeData().renderTargets[targetId];
		if (!rt.texture)
			continue;

		D3D11_TEXTURE2D_DESC origDesc{};
		rt.texture->GetDesc(&origDesc);

		if (origDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT)
			continue;

		SavedRenderTarget saved;
		saved.texture = rt.texture;
		saved.RTV = rt.RTV;
		saved.SRV = rt.SRV;
		saved.UAV = rt.UAV;

		D3D11_TEXTURE2D_DESC newDesc = origDesc;
		newDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;

		ID3D11Texture2D* newTexture = nullptr;
		if (FAILED(device->CreateTexture2D(&newDesc, nullptr, &newTexture)))
			continue;

		ID3D11RenderTargetView* newRTV = nullptr;
		D3D11_RENDER_TARGET_VIEW_DESC rtvDesc{};
		rtvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
		rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
		rtvDesc.Texture2D.MipSlice = 0;
		if (FAILED(device->CreateRenderTargetView(newTexture, &rtvDesc, &newRTV))) {
			newTexture->Release();
			continue;
		}

		ID3D11ShaderResourceView* newSRV = nullptr;
		D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{};
		srvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
		srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
		srvDesc.Texture2D.MostDetailedMip = 0;
		srvDesc.Texture2D.MipLevels = 1;
		if (FAILED(device->CreateShaderResourceView(newTexture, &srvDesc, &newSRV))) {
			newRTV->Release();
			newTexture->Release();
			continue;
		}

		ID3D11UnorderedAccessView* newUAV = nullptr;
		if (rt.UAV) {
			D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc{};
			rt.UAV->GetDesc(&uavDesc);
			uavDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;

			if (FAILED(device->CreateUnorderedAccessView(newTexture, &uavDesc, &newUAV))) {
				newSRV->Release();
				newRTV->Release();
				newTexture->Release();
				continue;
			}
		}

		rt.texture = newTexture;
		rt.RTV = newRTV;
		rt.SRV = newSRV;
		rt.UAV = newUAV;

		savedLDRTargets.push_back({ targetId, saved });
		logger::info("[HDR] Upgraded render target {} to R16G16B16A16_FLOAT (was format {})", static_cast<int>(targetId), static_cast<int>(origDesc.Format));
	}
}

void HDRDisplay::RestoreLDRRenderTargets()
{
	auto renderer = globals::game::renderer;

	for (auto& [targetId, saved] : savedLDRTargets) {
		auto& rt = renderer->GetRuntimeData().renderTargets[targetId];

		if (rt.texture)
			rt.texture->Release();
		if (rt.RTV)
			rt.RTV->Release();
		if (rt.SRV)
			rt.SRV->Release();
		if (rt.UAV)
			rt.UAV->Release();

		rt.texture = saved.texture;
		rt.RTV = saved.RTV;
		rt.SRV = saved.SRV;
		rt.UAV = saved.UAV;
	}
	savedLDRTargets.clear();
}

void HDRDisplay::ClearShaderCache()
{
	if (hdrOutputCS) {
		hdrOutputCS->Release();
		hdrOutputCS = nullptr;
	}
	if (uiBrightnessCS) {
		uiBrightnessCS->Release();
		uiBrightnessCS = nullptr;
	}
}

ID3D11ComputeShader* HDRDisplay::GetHDROutputCS()
{
	if (!hdrOutputCS) {
		std::vector<std::pair<const char*, const char*>> defines;
		hdrOutputCS = static_cast<ID3D11ComputeShader*>(Util::CompileShader(L"Data\\Shaders\\HDRDisplay\\HDROutputCS.hlsl", defines, "cs_5_0"));
		if (!hdrOutputCS) {
			logger::error("HDR: Failed to compile HDROutputCS.hlsl");
		}
	}
	return hdrOutputCS;
}

ID3D11ComputeShader* HDRDisplay::GetUIBrightnessCS()
{
	if (!uiBrightnessCS) {
		std::vector<std::pair<const char*, const char*>> defines;
		uiBrightnessCS = static_cast<ID3D11ComputeShader*>(Util::CompileShader(L"Data\\Shaders\\HDRDisplay\\UIBrightnessCS.hlsl", defines, "cs_5_0"));
		if (!uiBrightnessCS) {
			logger::error("HDR: Failed to compile UIBrightnessCS.hlsl");
		}
	}
	return uiBrightnessCS;
}

void HDRDisplay::ScaleUIBrightnessForFG()
{
	ZoneScoped;
	TracyD3D11Zone(globals::state->tracyCtx, "UI Brightness Scale");

	auto& upscaling = globals::features::upscaling;
	// FG merges PQ UI from this pass; paused UI stays gamma for HDROutput.
	if (!IsFGCompositingThisFrame())
		return;

	if (!settings.enableHDR)
		return;

	if (!hdrDataCB || !upscaling.dx12SwapChain.uiBufferWrapped || !upscaling.dx12SwapChain.uiBufferWrapped->uav)
		return;

	auto context = globals::d3d::context;
	auto state = globals::state;

	state->BeginPerfEvent("UI Brightness Scale");

	UpdateHDRData();

	auto dispatchCount = Util::GetScreenDispatchCount(false);

	ID3D11UnorderedAccessView* uavs[1] = { upscaling.dx12SwapChain.uiBufferWrapped->uav };
	context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr);

	ID3D11Buffer* cbs[1] = { hdrDataCB->CB() };
	context->CSSetConstantBuffers(0, 1, cbs);

	auto computeShader = GetUIBrightnessCS();
	if (computeShader) {
		context->CSSetShader(computeShader, nullptr, 0);
		globals::profiler->BeginPass("HDRDisplay::UIBrightness");
		context->Dispatch(dispatchCount.x, dispatchCount.y, 1);
		globals::profiler->EndPass();
	}

	uavs[0] = nullptr;
	context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr);
	cbs[0] = nullptr;
	context->CSSetConstantBuffers(0, 1, cbs);
	context->CSSetShader(nullptr, nullptr, 0);

	state->EndPerfEvent();
}

float HDRDisplay::GetDisplayMaxLuminance() const
{
	float maxLuminance = 1000.0f;

	winrt::com_ptr<IDXGIOutput> output;
	if (globals::d3d::swapChain && SUCCEEDED(globals::d3d::swapChain->GetContainingOutput(output.put()))) {
		winrt::com_ptr<IDXGIOutput6> output6;
		if (SUCCEEDED(output->QueryInterface(IID_PPV_ARGS(output6.put())))) {
			DXGI_OUTPUT_DESC1 desc1;
			if (SUCCEEDED(output6->GetDesc1(&desc1))) {
				maxLuminance = desc1.MaxLuminance;
				if (maxLuminance < 80.0f) {
					maxLuminance = 1000.0f;  // Fallback if display reports invalid value
				}
			}
		}
	}
	return maxLuminance;
}

float4 HDRDisplay::GetSharedDataHDR() const
{
	if (!loaded)
		return { 0.0f, 0.0f, 0.0f, 0.0f };

	auto* state = globals::state;
	const bool isMainOrLoading = state->isMainMenuOpen || state->isLoadingMenuOpen;
	auto* ui = globals::game::ui;
	const bool inMenuOrPause =
		ui && (ui->GameIsPaused() || state->isMainMenuOpen || state->isLoadingMenuOpen || state->isMapMenuOpen);

	float menuSceneEncoding = kHdrMenuSceneGameplay;
	if (isMainOrLoading) {
		menuSceneEncoding = kHdrMenuSceneMainOrLoading;
	} else if (inMenuOrPause) {
		menuSceneEncoding = kHdrMenuScenePauseOrMap;
	}

	return {
		settings.enableHDR ? 1.0f : 0.0f,
		static_cast<float>(settings.hdrPaperWhite),
		static_cast<float>(settings.hdrPeakNits),
		menuSceneEncoding
	};
}

HDRDisplay::HDRDataCB HDRDisplay::BuildHDRData() const
{
	bool isMainOrLoadingMenu = globals::state->isMainMenuOpen || globals::state->isLoadingMenuOpen;
	auto* ui = globals::game::ui;
	bool skipUIComposite = IsFGCompositingThisFrame();

	// Linear Lighting keeps the pipeline linear throughout.
	// Without it, ISHDR gamma-encodes its output even in HDR mode.
	bool isSceneLinear = globals::features::linearLighting.settings.enableLinearLighting;

	// Use user-specified peak brightness for highlights compression
	float effectivePeakNits = static_cast<float>(settings.hdrPeakNits);

	HDRDataCB data{};
	data.enableHDR = settings.enableHDR ? 1.f : 0.f;
	data.paperWhite = static_cast<float>(settings.hdrPaperWhite);
	data.peakNits = effectivePeakNits;
	data.skipUIComposite = skipUIComposite ? 1.f : 0.f;
	data.uiBrightness = settings.hdrUIBrightness;
	data.isSceneLinear = isSceneLinear ? 1.f : 0.f;
	data.pad0 = isMainOrLoadingMenu ? 1.f : 0.f;
	// TweenMenu = pause UI. ScaleUIBrightnessForFG skips while GameIsPaused(), so HDROutputCS applies the same mid-alpha boost when compositing gamma UI.
	data.fgTweenMenuMidAlphaBoost = (ui && ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)) ? 1.f : 0.f;
	data.previewSDR = 0.f;
	data.applyAutoHDR = globals::features::effects11.ReplacedTonemapperThisFrame() ? 1.f : 0.f;
	return data;
}

void HDRDisplay::UpdateHDRData() const
{
	if (!hdrDataCB)
		return;

	hdrDataCB->Update(BuildHDRData());
}

void HDRDisplay::UpdateSwapChainColorSpace() const
{
	auto& upscaling = globals::features::upscaling;

	// For Frame Gen, update the D3D12 swap chain color space
	if (upscaling.d3d12SwapChainActive) {
		upscaling.dx12SwapChain.SetColorSpace(settings.enableHDR);
		// HDR metadata is not set - some monitors have issues with HDR10 static metadata.
		// DX12SwapChain handles color space only; metadata control is centralized here.
		if (upscaling.dx12SwapChain.swapChain) {
			upscaling.dx12SwapChain.swapChain->SetHDRMetaData(DXGI_HDR_METADATA_TYPE_NONE, 0, nullptr);
		}
		return;
	}

	IDXGISwapChain4* swapChain4 = nullptr;

	if (globals::d3d::swapChain) {
		globals::d3d::swapChain->QueryInterface(IID_PPV_ARGS(&swapChain4));
	}

	if (!swapChain4)
		return;

	if (settings.enableHDR) {
		HRESULT hr = swapChain4->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);
		if (SUCCEEDED(hr)) {
			logger::info("[HDR] Set swap chain color space to HDR10 (PQ/BT.2020)");
			// Do NOT set HDR10 static metadata - it can cause issues on some monitors
			// The HGiG approach is to handle highlights compression in the shader instead.
			swapChain4->SetHDRMetaData(DXGI_HDR_METADATA_TYPE_NONE, 0, nullptr);
		} else {
			logger::warn("[HDR] Failed to set HDR10 color space");
		}
	} else {
		HRESULT hr = swapChain4->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709);
		if (SUCCEEDED(hr)) {
			logger::info("[HDR] Set swap chain color space to SDR (sRGB)");
			swapChain4->SetHDRMetaData(DXGI_HDR_METADATA_TYPE_NONE, 0, nullptr);
		} else {
			logger::warn("[HDR] Failed to set SDR color space");
		}
	}

	swapChain4->Release();
}
