From ef853fe85887b2cb9af6a4309a2482afbed0f4f1 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 30 Sep 2026 10:35:55 +0800 Subject: [PATCH] Brightness (Linux): adds a native impl --- src/detection/brightness/brightness.h | 48 +++ src/detection/brightness/brightness_bsd.c | 298 +++++++++++++++--- src/detection/brightness/brightness_linux.c | 318 +++++++++++++++++++- 3 files changed, 614 insertions(+), 50 deletions(-) diff --git a/src/detection/brightness/brightness.h b/src/detection/brightness/brightness.h index abb94787e..fe7f082ca 100644 --- a/src/detection/brightness/brightness.h +++ b/src/detection/brightness/brightness.h @@ -11,6 +11,7 @@ typedef struct FFBrightnessResult { enum { FF_DDC_EDID_ADDR = 0x50u, + FF_DDC_EDID_WRITE_ADDR = FF_DDC_EDID_ADDR << 1, FF_DDC_CI_ADDR = 0x37u, FF_DDC_CI_WRITE_ADDR = FF_DDC_CI_ADDR << 1, FF_DDC_CI_READ_ADDR = FF_DDC_CI_WRITE_ADDR | 1, @@ -19,8 +20,55 @@ enum { FF_DDC_CI_SET_VCP = 0x03u, FF_DDC_CI_COMMAND_PACKET = 0x80u, FF_DDC_CI_LUMINANCE_OPCODE = 0x10u, + FF_DDC_CI_VCP_RESPONSE_LENGTH = 8u, + FF_DDC_CI_VCP_RESPONSE_PACKET_SIZE = 11u, + FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE = 12u, + FF_DDC_CI_RESPONSE_CHECKSUM_ADDR = 0x50u, }; #define FF_BRIGHTNESS_DDCCI_SLEEP_SKIP ((uint32_t) -1) #define FF_DDC_CI_MAKE_HEADER(len) (FF_DDC_CI_COMMAND_PACKET | ((len) & 0x7F)) +static inline uint8_t ffDdcCiChecksum(uint8_t address, const uint8_t* data, size_t length) { + uint8_t checksum = address; + for (size_t i = 0; i < length; ++i) { + checksum ^= data[i]; + } + return checksum; +} + +static inline void ffDdcCiBuildGetVcpRequest(uint8_t request[5]) { + request[0] = FF_DDC_CI_VCP_COMMAND; + request[1] = FF_DDC_CI_MAKE_HEADER(2); + request[2] = FF_DDC_CI_GET_VCP; + request[3] = FF_DDC_CI_LUMINANCE_OPCODE; + request[4] = ffDdcCiChecksum(FF_DDC_CI_WRITE_ADDR, request, 4); +} + +static inline bool ffDdcCiValidateGetVcpResponse(const uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) { + if (response[0] != FF_DDC_CI_WRITE_ADDR + || response[1] != FF_DDC_CI_MAKE_HEADER(FF_DDC_CI_VCP_RESPONSE_LENGTH) + || response[2] != 0x02u + || response[3] != 0x00u + || response[4] != FF_DDC_CI_LUMINANCE_OPCODE + || response[5] != 0x00u) { + return false; + } + + return ffDdcCiChecksum(FF_DDC_CI_RESPONSE_CHECKSUM_ADDR, response, FF_DDC_CI_VCP_RESPONSE_PACKET_SIZE - 1u) == response[FF_DDC_CI_VCP_RESPONSE_PACKET_SIZE - 1u]; +} + +#define FF_I2C_DEVICE_CACHE_MAX 32 + +typedef struct FFBrightnessDdcCache { + int devices[FF_I2C_DEVICE_CACHE_MAX]; + size_t count; +} FFBrightnessDdcCache; + +static inline FFstrbuf ffBrightnessDdcCachePath(void) { + FFstrbuf path = ffStrbufCreateCopy(&instance.state.platform.cacheDir); + ffStrbufEnsureEndsWithC(&path, '/'); + ffStrbufAppendS(&path, "fastfetch/brightness/i2c_cache.txt"); + return path; +} + const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result); // list of FFBrightnessResult diff --git a/src/detection/brightness/brightness_bsd.c b/src/detection/brightness/brightness_bsd.c index bf09098b2..d2a25fd04 100644 --- a/src/detection/brightness/brightness_bsd.c +++ b/src/detection/brightness/brightness_bsd.c @@ -4,7 +4,11 @@ #include "common/kmod.h" #include "common/debug.h" #include "common/time.h" +#include "common/edidHelper.h" +#include +#include +#include #include #include #include @@ -12,65 +16,261 @@ #if __has_include() #include -const char* detectWithDdcci([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { - // FIXME: doesn't work for me - for (char i = '0'; i <= '9'; ++i) { - char path[] = "/dev/iic0"; - path[ARRAY_SIZE(path) - 2] = i; + // Try to load cache from file + static bool loadCacheFromFile(FFBrightnessDdcCache* cache) { + cache->count = 0; + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + bool valid = false; + FF_DEBUG("Brightness DDC/CI: loading I2C cache from %s", cacheDir.chars); + if (ffReadFileBuffer(cacheDir.chars, &cacheContent)) { + // Parse cache: first number is count, rest are device numbers + char* ptr = cacheContent.chars; + char* end = ptr + cacheContent.length; - FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); - if (fd < 0) { - int err = errno; - if (err == ENOENT) { - FF_DEBUG("No more I2C devices"); - break; - } else if (i == '0' && !ffKmodLoaded("iicbus")) { - FF_DEBUG("iicbus module is not loaded"); - return "No I2C devices found and iicbus module is not loaded"; + // Read count + char* next; + long count = strtol(ptr, &next, 10); + if (next != ptr && count > 0 && count <= FF_I2C_DEVICE_CACHE_MAX) { + ptr = next; + + // Read device numbers and reject truncated or trailing data. + size_t parsedCount = 0; + for (; parsedCount < (size_t) count && ptr < end; ++parsedCount) { + long dev = strtol(ptr, &next, 10); + if (next == ptr || dev < 0 || dev >= 100) { + break; + } + cache->devices[parsedCount] = (int) dev; + ptr = next; + } + + while (ptr < end && isspace((unsigned char) *ptr)) { + ++ptr; + } + + if (parsedCount == (size_t) count && ptr == end) { + cache->count = parsedCount; + valid = true; + for (size_t i = 0; i < cache->count; ++i) { + char path[32]; + snprintf(path, sizeof(path), "/dev/iic%d", cache->devices[i]); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: cached bus %d is unavailable (errno=%d: %s)", cache->devices[i], error, strerror(error)); + valid = false; + break; + } + } + } } - FF_DEBUG("open(/dev/iic%c) failed: %s", i, strerror(errno)); - continue; } - uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 }; - i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3]; + if (!valid) { + FF_DEBUG("Brightness DDC/CI: I2C cache is missing or invalid"); + cache->count = 0; + return false; + } - int ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_WRITE_ADDR, .flags = IIC_M_WR, .len = ARRAY_SIZE(i2cIn), .buf = i2cIn }, .nmsgs = 1 }); - if (ret < 0) { - FF_DEBUG("First ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno)); - continue; + FF_DEBUG("Brightness DDC/CI: loaded %zu bus(es) from cache", cache->count); + return true; + } + + // Save cache to file + static void saveCacheToFile(const FFBrightnessDdcCache* cache) { + if (cache->count == 0) { + return; + } + + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateF("%zu", cache->count); + for (size_t i = 0; i < cache->count; i++) { + ffStrbufAppendF(&cacheContent, " %d", cache->devices[i]); + } + + if (ffWriteFileBuffer(cacheDir.chars, &cacheContent)) { + FF_DEBUG("Brightness DDC/CI: saved %zu bus(es) to %s", cache->count, cacheDir.chars); + } else { + FF_DEBUG("Brightness DDC/CI: failed to save cache to %s", cacheDir.chars); + } + } + + static bool readDdcCiResponse(int fd, FFBrightnessOptions* options, uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) { + uint8_t request[5]; + ffDdcCiBuildGetVcpRequest(request); + + struct iic_msg msg = { + .slave = FF_DDC_CI_WRITE_ADDR, + .flags = IIC_M_WR, + .len = sizeof(request), + .buf = request + }; + + struct iic_rdwr_data data = { + .msgs = &msg, + .nmsgs = 1 + }; + + int ioctlResult = ioctl(fd, I2CRDWR, &data); + if (ioctlResult < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: write request failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error)); + return false; } ffTimeSleep(options->ddcciSleep); - uint8_t i2cOut[12] = {}; - ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_READ_ADDR, // LSB will be overridden by kernel to set read bit - .flags = IIC_M_RD, - .len = ARRAY_SIZE(i2cOut), - .buf = i2cOut }, - .nmsgs = 1 }); - if (ret < 0) { - FF_DEBUG("Second ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno)); - continue; - } - if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) { - FF_DEBUG("i2c out validation failed: i2cOut[2] = 0x%02x, i2cOut[3] = 0x%02x", i2cOut[2], i2cOut[3]); - continue; + msg.slave = FF_DDC_CI_WRITE_ADDR; + msg.flags = IIC_M_RD; + msg.len = FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE; + msg.buf = response; + ioctlResult = ioctl(fd, I2CRDWR, &data); + if (ioctlResult < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: read response failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error)); + return false; } - uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9]; - uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7]; + if (!ffDdcCiValidateGetVcpResponse(response)) { + FF_DEBUG("Brightness DDC/CI: invalid response (addr=0x%02x, header=0x%02x, type=0x%02x, opcode=0x%02x, checksum=0x%02x)", + (unsigned) response[0], (unsigned) response[1], (unsigned) response[2], (unsigned) response[4], (unsigned) response[10]); + return false; + } - FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); - brightness->max = max; - brightness->min = 0; - brightness->current = current; - ffStrbufInitS(&brightness->name, path + strlen("/dev/")); - brightness->builtin = false; + FF_DEBUG("Brightness DDC/CI: valid response (fd=%d, current=%u, max=%u)", fd, + ((unsigned) response[8] << 8u) | (unsigned) response[9], + ((unsigned) response[6] << 8u) | (unsigned) response[7]); + return true; } - return nullptr; -} + static bool scanI2cDevices(FFBrightnessOptions* options, FFBrightnessDdcCache* cache) { + FF_DEBUG("Brightness DDC/CI: scanning FreeBSD I2C buses"); + cache->count = 0; + + for (int i = 0; i < 100 && cache->count < FF_I2C_DEVICE_CACHE_MAX; i++) { + char path[32]; + snprintf(path, sizeof(path), "/dev/iic%d", i); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + continue; + } + + FF_DEBUG("Brightness DDC/CI: probing %s", path); + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + cache->devices[cache->count++] = i; + FF_DEBUG("Brightness DDC/CI: found compatible display on %s", path); + } + + FF_DEBUG("Brightness DDC/CI: scan finished with %zu compatible bus(es)", cache->count); + return cache->count > 0; + } + + static bool readEdid(int fd, uint8_t* edidData) { + uint8_t offset = 0; + + struct iic_msg msgs[2]; + msgs[0].slave = FF_DDC_EDID_WRITE_ADDR; + msgs[0].flags = IIC_M_WR; + msgs[0].len = 1; + msgs[0].buf = &offset; + + msgs[1].slave = FF_DDC_EDID_WRITE_ADDR; + msgs[1].flags = IIC_M_RD; + msgs[1].len = 128; + msgs[1].buf = edidData; + + struct iic_rdwr_data data = { + .msgs = msgs, + .nmsgs = 2 + }; + + return ioctl(fd, I2CRDWR, &data) >= 0; + } + + static bool detectCachedDevices(FFBrightnessOptions* options, FFlist* result, FFBrightnessDdcCache* cache) { + const size_t cachedCount = cache->count; + const uint32_t resultLength = result->length; + size_t validCount = 0; + + FF_DEBUG("Brightness DDC/CI: validating %zu cached bus(es)", cachedCount); + for (size_t i = 0; i < cachedCount; ++i) { + const int device = cache->devices[i]; + char path[32]; + snprintf(path, sizeof(path), "/dev/iic%d", device); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: failed to open cached bus %d (errno=%d: %s)", device, error, strerror(error)); + continue; + } + + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + uint32_t current = ((uint32_t) response[8] << 8u) + (uint32_t) response[9]; + uint32_t max = ((uint32_t) response[6] << 8u) + (uint32_t) response[7]; + if (max == 0 || current > max) { + FF_DEBUG("Brightness DDC/CI: ignoring bus %d with invalid brightness range %u/%u", device, current, max); + continue; + } + + cache->devices[validCount++] = device; + + // Try to read EDID for monitor name + uint8_t edidData[128]; + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(32); + if (readEdid(fd, edidData)) { + ffEdidGetName(edidData, &name); + } + if (name.length == 0) { + ffStrbufSetF(&name, "I2C-%d", device); + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + brightness->builtin = false; + ffStrbufInitMove(&brightness->name, &name); + FF_DEBUG("Brightness DDC/CI: added display '%s' from bus %d (%u/%u)", brightness->name.chars, device, current, max); + } + + cache->count = validCount; + FF_DEBUG("Brightness DDC/CI: cache validation kept %zu bus(es), result count is %u", validCount, result->length); + return result->length > resultLength; + } + + const char* detectWithDdcci(FFBrightnessOptions* options, FFlist* result) { + FFBrightnessDdcCache cache = {0}; + FF_DEBUG("Brightness DDC/CI: detection started (result count=%u)", result->length); + if (!loadCacheFromFile(&cache)) { + if (!scanI2cDevices(options, &cache)) { + FF_DEBUG("Brightness DDC/CI: no compatible I2C device found during initial scan"); + return "No I2C devices found"; + } + } + + if (!detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: cached detection found no usable display; rescanning"); + if (!scanI2cDevices(options, &cache) || !detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: rescan found no compatible display"); + return "No DDC/CI compatible displays found"; + } + } + + saveCacheToFile(&cache); + FF_DEBUG("Brightness DDC/CI: detection finished with %u result(s)", result->length); + return nullptr; + } #else @@ -138,10 +338,18 @@ const char* detectWithBacklight([[maybe_unused]] FFBrightnessOptions* options, [ #endif const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { + FF_DEBUG("Brightness: detection started (result count=%u)", result->length); detectWithBacklight(options, result); + FF_DEBUG("Brightness: backlight detection finished (result count=%u)", result->length); if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length == 0) { + FF_DEBUG("Brightness: trying DDC/CI because no backlight result was found"); detectWithDdcci(options, result); + } else if (options->ddcciSleep == FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) { + FF_DEBUG("Brightness: skipping DDC/CI because it is disabled"); + } else { + FF_DEBUG("Brightness: skipping DDC/CI because backlight detection already found %u result(s)", result->length); } + FF_DEBUG("Brightness: detection finished (result count=%u)", result->length); return nullptr; } diff --git a/src/detection/brightness/brightness_linux.c b/src/detection/brightness/brightness_linux.c index 515eb1959..1c7d6e888 100644 --- a/src/detection/brightness/brightness_linux.c +++ b/src/detection/brightness/brightness_linux.c @@ -2,9 +2,297 @@ #include "common/io.h" #include "common/edidHelper.h" #include "common/strutil.h" +#include "common/debug.h" +#include "common/time.h" +#include #include +#include #include +#include + +#if defined(__linux__) + #include + #include + #include + #include + #include + #include + + // Try to load cache from file + static bool loadCacheFromFile(FFBrightnessDdcCache* cache) { + cache->count = 0; + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + bool valid = false; + FF_DEBUG("Brightness DDC/CI: loading I2C cache from %s", cacheDir.chars); + if (ffReadFileBuffer(cacheDir.chars, &cacheContent)) { + // Parse cache: first number is count, rest are device numbers + char* ptr = cacheContent.chars; + char* end = ptr + cacheContent.length; + + // Read count + char* next; + long count = strtol(ptr, &next, 10); + if (next != ptr && count > 0 && count <= FF_I2C_DEVICE_CACHE_MAX) { + ptr = next; + + // Read device numbers and reject truncated or trailing data. + size_t parsedCount = 0; + for (; parsedCount < (size_t) count && ptr < end; ++parsedCount) { + long dev = strtol(ptr, &next, 10); + if (next == ptr || dev < 0 || dev >= 100) { + break; + } + cache->devices[parsedCount] = (int) dev; + ptr = next; + } + + while (ptr < end && isspace((unsigned char) *ptr)) { + ++ptr; + } + + if (parsedCount == (size_t) count && ptr == end) { + cache->count = parsedCount; + valid = true; + for (size_t i = 0; i < cache->count; ++i) { + char path[32]; + snprintf(path, sizeof(path), "/dev/i2c-%d", cache->devices[i]); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: cached bus %d is unavailable (errno=%d: %s)", cache->devices[i], error, strerror(error)); + valid = false; + break; + } + } + } + } + } + + if (!valid) { + FF_DEBUG("Brightness DDC/CI: I2C cache is missing or invalid"); + cache->count = 0; + return false; + } + + FF_DEBUG("Brightness DDC/CI: loaded %zu bus(es) from cache", cache->count); + return true; + } + + // Save cache to file + static void saveCacheToFile(const FFBrightnessDdcCache* cache) { + if (cache->count == 0) { + return; + } + + FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateF("%zu", cache->count); + for (size_t i = 0; i < cache->count; i++) { + ffStrbufAppendF(&cacheContent, " %d", cache->devices[i]); + } + + if (ffWriteFileBuffer(cacheDir.chars, &cacheContent)) { + FF_DEBUG("Brightness DDC/CI: saved %zu bus(es) to %s", cache->count, cacheDir.chars); + } else { + FF_DEBUG("Brightness DDC/CI: failed to save cache to %s", cacheDir.chars); + } + } + + static bool readDdcCiResponse(int fd, FFBrightnessOptions* options, uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) { + uint8_t request[5]; + ffDdcCiBuildGetVcpRequest(request); + + // i2c_msg.addr is a 7-bit address; the 8-bit write/read addresses are + // used only for the DDC/CI checksum. + struct i2c_msg msg = { + .addr = FF_DDC_CI_ADDR, + .flags = 0, + .len = sizeof(request), + .buf = request + }; + + struct i2c_rdwr_ioctl_data data = { + .msgs = &msg, + .nmsgs = 1 + }; + + int ioctlResult = ioctl(fd, I2C_RDWR, &data); + if (ioctlResult != 1) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: write request failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error)); + return false; + } + + ffTimeSleep(options->ddcciSleep); + + msg.flags = I2C_M_RD; + msg.len = FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE; + msg.buf = response; + ioctlResult = ioctl(fd, I2C_RDWR, &data); + if (ioctlResult != 1) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: read response failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error)); + return false; + } + + if (!ffDdcCiValidateGetVcpResponse(response)) { + FF_DEBUG("Brightness DDC/CI: invalid response (addr=0x%02x, header=0x%02x, type=0x%02x, opcode=0x%02x, checksum=0x%02x)", + (unsigned) response[0], (unsigned) response[1], (unsigned) response[2], (unsigned) response[4], (unsigned) response[10]); + return false; + } + + FF_DEBUG("Brightness DDC/CI: valid response (fd=%d, current=%u, max=%u)", fd, + ((unsigned) response[8] << 8u) | (unsigned) response[9], + ((unsigned) response[6] << 8u) | (unsigned) response[7]); + return true; + } + + static bool scanI2cDevices(FFBrightnessOptions* options, FFBrightnessDdcCache* cache) { + FF_DEBUG("Brightness DDC/CI: scanning Linux I2C buses"); + cache->count = 0; + + for (int i = 0; i < 100 && cache->count < FF_I2C_DEVICE_CACHE_MAX; i++) { + char path[32]; + snprintf(path, sizeof(path), "/dev/i2c-%d", i); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + continue; + } + + FF_DEBUG("Brightness DDC/CI: probing %s", path); + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + cache->devices[cache->count++] = i; + FF_DEBUG("Brightness DDC/CI: found compatible display on %s", path); + } + + FF_DEBUG("Brightness DDC/CI: scan finished with %zu compatible bus(es)", cache->count); + return cache->count > 0; + } + + static bool readEdid(int fd, uint8_t* edidData) { + uint8_t offset = 0; + + struct i2c_msg msgs[2]; + msgs[0].addr = FF_DDC_EDID_ADDR; + msgs[0].flags = 0; + msgs[0].len = 1; + msgs[0].buf = &offset; + + msgs[1].addr = FF_DDC_EDID_ADDR; + msgs[1].flags = I2C_M_RD; + msgs[1].len = 128; + msgs[1].buf = edidData; + + struct i2c_rdwr_ioctl_data data; + data.msgs = msgs; + data.nmsgs = 2; + + return ioctl(fd, I2C_RDWR, &data) == 2; + } + + static bool detectCachedDevices(FFBrightnessOptions* options, FFlist* result, FFBrightnessDdcCache* cache) { + const size_t cachedCount = cache->count; + const uint32_t resultLength = result->length; + size_t validCount = 0; + + FF_DEBUG("Brightness DDC/CI: validating %zu cached bus(es)", cachedCount); + for (size_t i = 0; i < cachedCount; ++i) { + const int device = cache->devices[i]; + char path[32]; + snprintf(path, sizeof(path), "/dev/i2c-%d", device); + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int error = errno; + FF_DEBUG("Brightness DDC/CI: failed to open cached bus %d (errno=%d: %s)", device, error, strerror(error)); + continue; + } + + uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]; + if (!readDdcCiResponse(fd, options, response)) { + continue; + } + + uint32_t current = ((uint32_t) response[8] << 8u) + (uint32_t) response[9]; + uint32_t max = ((uint32_t) response[6] << 8u) + (uint32_t) response[7]; + if (max == 0 || current > max) { + FF_DEBUG("Brightness DDC/CI: ignoring bus %d with invalid brightness range %u/%u", device, current, max); + continue; + } + + cache->devices[validCount++] = device; + + // Try to read EDID for monitor name + uint8_t edidData[128]; + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(32); + if (readEdid(fd, edidData)) { + ffEdidGetName(edidData, &name); + } + if (name.length == 0) { + ffStrbufSetF(&name, "I2C-%d", device); + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + brightness->builtin = false; + ffStrbufInitMove(&brightness->name, &name); + FF_DEBUG("Brightness DDC/CI: added display '%s' from bus %d (%u/%u)", brightness->name.chars, device, current, max); + } + + cache->count = validCount; + FF_DEBUG("Brightness DDC/CI: cache validation kept %zu bus(es), result count is %u", validCount, result->length); + return result->length > resultLength; + } + + static const char* detectWithDdcciNative(FFBrightnessOptions* options, FFlist* result, uint32_t expectedResultLength) { +#ifdef FF_HAVE_DDCUTIL + const uint32_t resultLength = result->length; +#else + (void) expectedResultLength; +#endif + FFBrightnessDdcCache cache = {0}; + FF_DEBUG("Brightness DDC/CI: native detection started (result count=%u, expected=%u)", result->length, expectedResultLength); + if (!loadCacheFromFile(&cache)) { + if (!scanI2cDevices(options, &cache)) { + FF_DEBUG("Brightness DDC/CI: no compatible I2C device found during initial scan"); + return "No I2C devices found"; + } + } + + if (!detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: cached detection found no usable display; rescanning"); + // A cached bus can remain present after its monitor is unplugged or + // its number is reused. Rebuild the cache when no valid reply arrives. + if (!scanI2cDevices(options, &cache) || !detectCachedDevices(options, result, &cache)) { + FF_DEBUG("Brightness DDC/CI: rescan found no compatible display"); + return "No DDC/CI compatible displays found"; + } + } + + #ifdef FF_HAVE_DDCUTIL + if (result->length < expectedResultLength) { + FF_DEBUG("Brightness DDC/CI: native result is incomplete (%u/%u); rolling back for ddcutil fallback", result->length, expectedResultLength); + FFBrightnessResult discarded; + while (result->length > resultLength && FF_LIST_POP(*result, &discarded)) { + ffStrbufDestroy(&discarded.name); + } + return "Native DDC/CI found only a subset of displays"; + } + #endif + + saveCacheToFile(&cache); + FF_DEBUG("Brightness DDC/CI: native detection finished with %u result(s)", result->length); + return nullptr; + } +#endif static const char* detectWithBacklight(FFlist* result) { // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-backlight @@ -75,8 +363,9 @@ static const char* detectWithBacklight(FFlist* result) { return nullptr; } +#include "detection/displayserver/displayserver.h" + #ifdef FF_HAVE_DDCUTIL - #include "detection/displayserver/displayserver.h" #include "common/library.h" #include "common/mallocHelper.h" @@ -162,16 +451,35 @@ static const char* detectWithDdcci([[maybe_unused]] FFBrightnessOptions* options #endif const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) { + FF_DEBUG("Brightness: detection started (result count=%u)", result->length); detectWithBacklight(result); + FF_DEBUG("Brightness: backlight detection finished (result count=%u)", result->length); -#ifdef FF_HAVE_DDCUTIL if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) { const FFDisplayServerResult* displayServer = ffConnectDisplayServer(); if (result->length < displayServer->displays.length) { - detectWithDdcci(options, result); - } - } + FF_DEBUG("Brightness: trying native DDC/CI (result count=%u, display count=%u)", result->length, displayServer->displays.length); +#if defined(__linux__) + // Try native I2C first + const char* nativeError = detectWithDdcciNative(options, result, displayServer->displays.length); + if (nativeError != nullptr) { +#ifdef FF_HAVE_DDCUTIL + FF_DEBUG("Native I2C DDC/CI failed: %s, falling back to ddcutil", nativeError); + detectWithDdcci(options, result); +#else + FF_DEBUG("Native I2C DDC/CI failed: %s; ddcutil fallback is unavailable", nativeError); #endif + } +#elif defined(FF_HAVE_DDCUTIL) + detectWithDdcci(options, result); +#endif + } else { + FF_DEBUG("Brightness: skipping DDC/CI because result count=%u meets display count=%u", result->length, displayServer->displays.length); + } + } else { + FF_DEBUG("Brightness: skipping DDC/CI because it is disabled"); + } + FF_DEBUG("Brightness: detection finished (result count=%u)", result->length); return nullptr; }