From 01766b4d4d3757816768909620596b256f0a1957 Mon Sep 17 00:00:00 2001 From: Xander Frangos <33106561+xanderfrangos@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:57:07 -0400 Subject: [PATCH] Preliminary HDR support https://github.com/xanderfrangos/twinkle-tray/issues/97 --- package-lock.json | 19 +++ package.json | 1 + src/Monitors.js | 58 +++++++ src/components/MonitorInfo.jsx | 18 +++ src/components/SettingsWindow.jsx | 1 + src/electron.js | 21 ++- src/modules/windows-hdr/.gitignore | 5 + src/modules/windows-hdr/binding.gyp | 18 +++ src/modules/windows-hdr/index.js | 6 + src/modules/windows-hdr/package.json | 25 +++ src/modules/windows-hdr/windows-hdr.cc | 212 +++++++++++++++++++++++++ src/settings-preload.js | 5 + 12 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 src/modules/windows-hdr/.gitignore create mode 100644 src/modules/windows-hdr/binding.gyp create mode 100644 src/modules/windows-hdr/index.js create mode 100644 src/modules/windows-hdr/package.json create mode 100644 src/modules/windows-hdr/windows-hdr.cc diff --git a/package-lock.json b/package-lock.json index 22b69e1d..051b611e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "suncalc": "^1.9.0", "win32-displayconfig": "^0.1.0", "windows-accent-colors": "^1.0.1", + "windows-hdr": "file:src/modules/windows-hdr", "wmi-bridge": "file:src/modules/wmi-bridge", "wmi-client": "^0.5.0" }, @@ -20090,6 +20091,10 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, + "node_modules/windows-hdr": { + "resolved": "src/modules/windows-hdr", + "link": true + }, "node_modules/wmi-bridge": { "resolved": "src/modules/wmi-bridge", "link": true @@ -20493,6 +20498,20 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, + "src/modules/windows-hdr": { + "version": "1.0.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "1.5.0", + "node-addon-api": "^7.0.0" + } + }, + "src/modules/windows-hdr/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, "src/modules/wmi-bridge": { "version": "1.0.0", "hasInstallScript": true, diff --git a/package.json b/package.json index f79558cc..d8f249f7 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "suncalc": "^1.9.0", "win32-displayconfig": "^0.1.0", "windows-accent-colors": "^1.0.1", + "windows-hdr": "file:src/modules/windows-hdr", "wmi-bridge": "file:src/modules/wmi-bridge", "wmi-client": "^0.5.0" }, diff --git a/src/Monitors.js b/src/Monitors.js index 8cdb74fe..50cd8ccb 100644 --- a/src/Monitors.js +++ b/src/Monitors.js @@ -4,6 +4,7 @@ console.log = (...args) => { args.unshift(tag); oLog(...args) } console.log("Monitors.js starting. If you see this again, something bad happened!") const w32disp = require("win32-displayconfig"); const wmibridge = require("wmi-bridge"); +const hdr = require("windows-hdr"); const { exec } = require('child_process'); require("os").setPriority(0, require("os").constants.priority.PRIORITY_BELOW_NORMAL) @@ -37,6 +38,8 @@ process.on('message', async (data) => { }) } else if (data.type === "brightness") { setBrightness(data.brightness, data.id) + } else if (data.type === "sdr") { + setSDRBrightness(data.brightness, data.id) } else if (data.type === "settings") { settings = data.settings @@ -187,6 +190,17 @@ refreshMonitors = async (fullRefresh = false, ddcciType = "default", alwaysSendU } } + // HDR + if (settings.enableHDR) { + try { + startTime = process.hrtime.bigint() + monitorsHDR = await getHDRDisplays(monitors); + console.log(`getHDRDisplays() Total: ${(startTime - process.hrtime.bigint()) / BigInt(-1000000)}ms`) + } catch (e) { + console.log("\x1b[41m" + "getHDRDisplays() failed!" + "\x1b[0m", e) + } + } + // Hide internal if (settings?.hideClosedLid) { const wmiMonitor = Object.values(monitors).find(mon => mon.type === "wmi") @@ -363,6 +377,17 @@ getAllMonitors = async (ddcciMethod = "default") => { console.log("getBrightnessWMI() skipped due to previous failure.") } + // HDR + if (settings.enableHDR) { + try { + startTime = process.hrtime.bigint() + monitorsHDR = await getHDRDisplays(foundMonitors); + console.log(`getHDRDisplays() Total: ${(startTime - process.hrtime.bigint()) / BigInt(-1000000)}ms`) + } catch (e) { + console.log("\x1b[41m" + "getHDRDisplays() failed!" + "\x1b[0m", e) + } + } + // Hide internal if (settings?.hideClosedLid) { const wmiMonitor = Object.values(foundMonitors).find(mon => mon.type === "wmi") @@ -447,6 +472,29 @@ setStudioDisplayBrightness = async (serial, brightness) => { } } +getHDRDisplays = async (monitors) => { + try { + const displays = hdr.getDisplays() + for(const display of displays) { + const hwid = display.path.split("#") + updateDisplay(monitors, hwid[2], { + name: display.name, + key: hwid[2], + id: display.path, + hwid, + sdrNits: display.nits, + sdrLevel: parseInt((display.nits - 80) / 4), + hdr: "supported" + }); + displays[hwid[2]] = display + } + } catch(e) { + console.log("\x1b[41m" + "getHDRDisplays(): failed to access displays" + "\x1b[0m", e) + } + + return monitors +} + let wmiFailed = false getMonitorsWMI = () => { return new Promise(async (resolve, reject) => { @@ -825,6 +873,16 @@ updateDisplay = (monitors, hwid2, info = {}) => { return true } +function setSDRBrightness(brightness, id) { + if(!settings.enableHDR) return false; + try { + console.log("sdr", brightness, id) + hdr.setSDRBrightness(id, (brightness * 0.01 * 400) + 80) + } catch(e) { + console.log(`Couldn't update SDR brightness! [${id}]`, e); + } +} + function setBrightness(brightness, id) { try { if (id) { diff --git a/src/components/MonitorInfo.jsx b/src/components/MonitorInfo.jsx index dd604eaa..b33d61c3 100644 --- a/src/components/MonitorInfo.jsx +++ b/src/components/MonitorInfo.jsx @@ -7,6 +7,7 @@ export default function MonitorInfo(props) { const [contrast, setContrast] = useState(monitor?.features?.["0x12"] ? monitor?.features?.["0x12"][0] : 50) const [volume, setVolume] = useState(monitor?.features?.["0x62"] ? monitor?.features?.["0x62"][0] : 50) const [powerState, setPowerState] = useState(monitor?.features?.["0xD6"] ? monitor?.features?.["0xD6"][0] : 50) + const [sdr, setSDR] = useState(monitor.sdrLevel >= 0 ? monitor.sdrLevel : 50) const [manualVCP, setManualVCP] = useState("") const [manualValue, setManualValue] = useState("") @@ -75,6 +76,14 @@ export default function MonitorInfo(props) { ) + // SDR test + extraHTML.push( +
+
SDR
+ { setSDR(val); setSDRBrightness(monitor.id, val) }} scrolling={false} /> +
+ ) + return (

@@ -101,6 +110,15 @@ function setVCP(monitor, code, value) { })) } +function setSDRBrightness(monitor, value) { + window.dispatchEvent(new CustomEvent("set-sdr-brightness", { + detail: { + monitor, + value + } + })) +} + function getDebugMonitorType(type) { if (type == "none") { return (<>None ) diff --git a/src/components/SettingsWindow.jsx b/src/components/SettingsWindow.jsx index 184fbdbc..44ef9dee 100644 --- a/src/components/SettingsWindow.jsx +++ b/src/components/SettingsWindow.jsx @@ -1431,6 +1431,7 @@ export default class SettingsWindow extends PureComponent { + diff --git a/src/electron.js b/src/electron.js index 874477a5..e5dbc3f8 100644 --- a/src/electron.js +++ b/src/electron.js @@ -465,6 +465,7 @@ const defaultSettings = { disableWMIC: false, disableWMI: false, disableWin32: false, + enableHDR: false, autoDisabledWMI: false, useWin32Event: true, useElectronEvents: true, @@ -1875,7 +1876,12 @@ let ignoreBrightnessEventTimeout = false function updateBrightness(index, newLevel, useCap = true, vcpValue = "brightness", clearTransition = true) { try { let level = newLevel - let vcp = (vcpValue === "brightness" ? "brightness" : `0x${parseInt(vcpValue).toString(16)}`) + let vcp = "brightness" + switch(vcpValue) { + case "brightness": vcp = "brightness"; break; + case "sdr": vcp = "sdr"; break; + default: vcp = `0x${parseInt(vcpValue).toString(16)}`; + } let monitor = false if (typeof index == "string" && index * 1 != index) { @@ -1909,9 +1915,15 @@ function updateBrightness(index, newLevel, useCap = true, vcpValue = "brightness return false } - const normalized = normalizeBrightness(level, false, (useCap ? monitor.min : 0), (useCap ? monitor.max : 100)) + const normalized = normalizeBrightness(level, false, 0, 100) - if (monitor.type == "ddcci") { + if (vcp === "sdr") { + monitorsThread.send({ + type: "sdr", + brightness: normalized, + id: monitor.id + }) + } else if (monitor.type == "ddcci") { if (vcp === "brightness") { monitor.brightness = level monitor.brightnessRaw = normalized @@ -2295,6 +2307,9 @@ ipcMain.on('sleep-display', (e, hwid) => turnOffDisplayDDC(hwid, true)) ipcMain.on('set-vcp', (e, values) => { updateBrightnessThrottle(values.monitor, values.value, false, true, values.code) }) +ipcMain.on('set-sdr-brightness', (e, values) => { + updateBrightnessThrottle(values.monitor, values.value, false, true, "sdr") +}) ipcMain.on('get-window-history', () => sendToAllWindows('window-history', windowHistory)) diff --git a/src/modules/windows-hdr/.gitignore b/src/modules/windows-hdr/.gitignore new file mode 100644 index 00000000..86ff847a --- /dev/null +++ b/src/modules/windows-hdr/.gitignore @@ -0,0 +1,5 @@ +node_modules +*.log* +build +.vscode/ipch +.history \ No newline at end of file diff --git a/src/modules/windows-hdr/binding.gyp b/src/modules/windows-hdr/binding.gyp new file mode 100644 index 00000000..811eeb6e --- /dev/null +++ b/src/modules/windows-hdr/binding.gyp @@ -0,0 +1,18 @@ +{ + "targets": [ + { + "target_name": "windows-hdr", + "cflags!": [ "-fno-exceptions" ], + "cflags_cc!": [ "-fno-exceptions" ], + "conditions": [ + ["OS=='win'", { + "sources": [ "windows-hdr.cc" ] + }], + ], + "include_dirs": [ + " +#include +#include +#include +#include + +enum DISPLAYCONFIG_DEVICE_INFO_TYPE_INTERNAL { + DISPLAYCONFIG_DEVICE_INFO_SET_SDR_WHITE_LEVEL = 0xFFFFFFEE, +}; + +typedef struct _DISPLAYCONFIG_SET_SDR_WHITE_LEVEL { + DISPLAYCONFIG_DEVICE_INFO_HEADER header; + unsigned int SDRWhiteLevel; + unsigned char finalValue; +} _DISPLAYCONFIG_SET_SDR_WHITE_LEVEL; + +LONG pathSetSdrWhite(DISPLAYCONFIG_PATH_INFO path, int nits) { + _DISPLAYCONFIG_SET_SDR_WHITE_LEVEL sdrWhiteParams = {}; + sdrWhiteParams.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE) DISPLAYCONFIG_DEVICE_INFO_SET_SDR_WHITE_LEVEL; + sdrWhiteParams.header.size = sizeof(sdrWhiteParams); + sdrWhiteParams.header.adapterId = path.targetInfo.adapterId; + sdrWhiteParams.header.id = path.targetInfo.id; + + sdrWhiteParams.SDRWhiteLevel = nits * 1000 / 80; + sdrWhiteParams.finalValue = 1; + + return DisplayConfigSetDeviceInfo(&sdrWhiteParams.header); +} + +struct Display { + std::string name; + std::string path; + int nits; + boolean hdrSupported; + boolean hdrEnabled; + DISPLAYCONFIG_PATH_INFO target; + int bits; +}; + +std::string wcharToString(const wchar_t* wstr, boolean hasNullTerminator) { + int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr) - (hasNullTerminator ? 1 : 0); + std::string str(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &str[0], size_needed, nullptr, nullptr); + return str; +} + +std::map getDisplays() { + std::map newDisplays; + DISPLAYCONFIG_PATH_INFO *paths = 0; + DISPLAYCONFIG_MODE_INFO *modes = 0; + UINT32 pathCount, modeCount; + { + UINT32 flags = QDC_ONLY_ACTIVE_PATHS; + LONG result = ERROR_SUCCESS; + + do { + if (paths) { + free(paths); + } + if (modes) { + free(modes); + } + + result = GetDisplayConfigBufferSizes(flags, &pathCount, &modeCount); + + if (result != ERROR_SUCCESS) { + fprintf(stderr, "Error on GetDisplayConfigBufferSizes\n"); + return newDisplays; + } + + paths = (DISPLAYCONFIG_PATH_INFO *) malloc(pathCount * sizeof(paths[0])); + modes = (DISPLAYCONFIG_MODE_INFO *) malloc(modeCount * sizeof(modes[0])); + + result = QueryDisplayConfig(flags, &pathCount, paths, &modeCount, modes, 0); + if (result != ERROR_SUCCESS && result != ERROR_INSUFFICIENT_BUFFER) { + fprintf(stderr, "Error on QueryDisplayConfig\n"); + return newDisplays; + } + } while (result == ERROR_INSUFFICIENT_BUFFER); + } + + for (int i = 0; i < pathCount; i++) { + DISPLAYCONFIG_PATH_INFO path = paths[i]; + + DISPLAYCONFIG_TARGET_DEVICE_NAME targetName = {}; + targetName.header.adapterId = path.targetInfo.adapterId; + targetName.header.id = path.targetInfo.id; + targetName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + targetName.header.size = sizeof(targetName); + LONG result = DisplayConfigGetDeviceInfo(&targetName.header); + + if (result != ERROR_SUCCESS) { + fprintf(stderr, "Error on DisplayConfigGetDeviceInfo for target name\n"); + return newDisplays; + } + + DISPLAYCONFIG_SDR_WHITE_LEVEL displayInfo = {}; + displayInfo.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE) DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL; + displayInfo.header.size = sizeof(displayInfo); + displayInfo.header.adapterId = path.targetInfo.adapterId; + displayInfo.header.id = path.targetInfo.id; + + result = DisplayConfigGetDeviceInfo(&displayInfo.header); + + if (result != ERROR_SUCCESS) { + fprintf(stderr, "Error on DisplayConfigGetDeviceInfo for SDR white level\n"); + return newDisplays; + } + + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO hdrInfo = {}; + hdrInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO; + hdrInfo.header.size = sizeof(DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO); + hdrInfo.header.adapterId = path.targetInfo.adapterId; + hdrInfo.header.id = path.targetInfo.id; + + result = DisplayConfigGetDeviceInfo(&hdrInfo.header); + + int nits = (int) displayInfo.SDRWhiteLevel * 80 / 1000; + std::string monitorDevicePath = wcharToString(targetName.monitorDevicePath, false); + + Display newDisplay; + newDisplay.name = wcharToString(targetName.monitorFriendlyDeviceName, true); + newDisplay.path = monitorDevicePath.substr(0, monitorDevicePath.find("#{")); + newDisplay.nits = nits; + newDisplay.hdrSupported = hdrInfo.advancedColorSupported; + newDisplay.hdrEnabled = hdrInfo.advancedColorEnabled; + newDisplay.bits = hdrInfo.bitsPerColorChannel; + newDisplay.target = path; + + newDisplays.insert({ newDisplay.name, newDisplay }); + } + + return newDisplays; +} + +boolean setSDRBrightness(DISPLAYCONFIG_PATH_INFO target, int desiredNits) { + int nits = desiredNits; + + if (nits < 80) { + nits = 80; + } + + if (nits > 480) { + nits = 480; + } + + if (nits % 4 != 0) { + nits += 4 - (nits % 4); + } + + LONG result = pathSetSdrWhite(target, nits); + + if (result != ERROR_SUCCESS) { + fprintf(stderr, "Error on DisplayConfigSetDeviceInfo for SDR white level\n"); + return false; + } + + return true; +} + + +Napi::Array nodeGetDisplays(const Napi::CallbackInfo& info) { + std::map displays = getDisplays(); + + Napi::Env env = info.Env(); + Napi::Array out = Napi::Array::New(env); + + int i = 0; + for (auto& display : displays) { + Napi::Object displayObj = Napi::Object::New(env); + displayObj.Set(Napi::String::New(env, "name"), Napi::String::New(env, display.second.name)); + displayObj.Set(Napi::String::New(env, "path"), Napi::String::New(env, display.second.path)); + displayObj.Set(Napi::String::New(env, "nits"), Napi::Number::New(env, display.second.nits)); + displayObj.Set(Napi::String::New(env, "hdrSupported"), Napi::Boolean::New(env, display.second.hdrSupported)); + displayObj.Set(Napi::String::New(env, "hdrEnabled"), Napi::Boolean::New(env, display.second.hdrEnabled)); + displayObj.Set(Napi::String::New(env, "bits"), Napi::Number::New(env, display.second.bits)); + out.Set(i++, displayObj); + } + + return out; +} + +Napi::Boolean nodeSetSDRBrightness(const Napi::CallbackInfo& info) { + if(info.Length() != 2) { + fprintf(stderr, "Invalid number of parameters.\n"); + return Napi::Boolean::New(info.Env(), false); + } + Napi::String path = info[0].As(); + Napi::Number nits = info[1].As(); + + std::map displays = getDisplays(); + + boolean result = false; + for (auto& display : displays) { + if(display.second.path == (std::string)path) { + result = setSDRBrightness(display.second.target, nits); + break; + } + } + + return Napi::Boolean::New(info.Env(), result); +} + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, nodeGetDisplays)); + exports.Set(Napi::String::New(env, "setSDRBrightness"), Napi::Function::New(env, nodeSetSDRBrightness)); + return exports; +} + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) \ No newline at end of file diff --git a/src/settings-preload.js b/src/settings-preload.js index a0cc804a..c4e77e77 100644 --- a/src/settings-preload.js +++ b/src/settings-preload.js @@ -232,6 +232,11 @@ window.addEventListener("setVCP", e => { ipc.send("set-vcp", { monitor, code, value }) }) +window.addEventListener("set-sdr-brightness", e => { + const { monitor, value } = e.detail + ipc.send("set-sdr-brightness", { monitor, value }) +}) + const SunCalc = require('suncalc') function getSunCalcTimes(lat, long) { const localTimes = SunCalc.getTimes(new Date(), lat, long)