From 563db09bf14de28e4a2e9b7000d7ca424c03309f Mon Sep 17 00:00:00 2001 From: Luxferre Date: Fri, 14 Aug 2026 13:49:01 +0300 Subject: [PATCH] init --- Makefile | 25 +++++++++ README.md | 95 +++++++++++++++++++++++++++++++++ nvbright.c | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 nvbright.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..09cde61 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +INSTALL ?= install + +TARGET = nvbright +SRC = nvbright.c + +.PHONY: all clean install uninstall + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SRC) + +install: $(TARGET) + $(INSTALL) -d $(DESTDIR)$(BINDIR) + $(INSTALL) -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET) + +uninstall: + rm -f $(DESTDIR)$(BINDIR)/$(TARGET) + +clean: + rm -f $(TARGET) *.o diff --git a/README.md b/README.md new file mode 100644 index 0000000..4618944 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# NVbright: directly control NVIDIA-based LCD backlight brightness on Apple MacBook Air A1370 + +NVbright is a lightweight utility for controlling LCD backlight brightness on NVIDIA-equipped Apple MacBooks running FreeBSD under EFI boot. + +## About + +On older NVIDIA-based Apple MacBooks (such as the MacBook Air A1370 with GeForce 320M, MCP89, or GT218 chipsets), LCD backlight level is modulated via a Pulse Width Modulation (PWM) signal generated by the GPU display engine (`PDISPLAY`). + +Under macOS or BIOS compatibility mode, firmware dynamically alters backlight brightness through System Management Interrupts (SMI) triggered by legacy I/O port writes. However, when booting FreeBSD natively in EFI mode, this legacy SMI path is unavailable, leaving the screen backlight stuck or uncontrollable via standard ACPI methods. + +`nvbright` addresses this by directly interacting with the GPU MMIO register BAR: + +1. Identifies the NVIDIA VGA controller's physical MMIO BAR0 address dynamically using `/usr/sbin/pciconf`. +2. Maps the register space into memory via `/dev/mem` using `mmap(2)`. +3. Detects the active Serial Output Router (SOR) with an initialized PWM divisor (`PWM_DIV`). +4. Reads and computes current duty cycle or writes new values latching the display engine registers (`PWM_CTL`). + +The register mapping and control logic mirror the Linux `nouveau` driver implementation for NV50 and NVA3 chip architectures: + +- `NV50_PDISP_SOR_PWM_DIV(i) = 0x0061c080 + i * 0x800` +- `NV50_PDISP_SOR_PWM_CTL(i) = 0x0061c084 + i * 0x800` +- `CTL_NEW (0x80000000)`: Latch/apply updated brightness value +- `CTL_UNK (0x40000000)`: Required control flag on NVA3-class chipsets + +## Build + +`nvbright` is written in standard C and relies only on standard POSIX and FreeBSD system headers (`sys/mman.h`, `sys/types.h`, `fcntl.h`, `unistd.h`). No external dependencies or third-party libraries are required. + +Compile using `make`: + +```sh +make +``` + +Or compile manually using `clang` / `cc`: + +```sh +cc -O2 -Wall -Wextra -o nvbright nvbright.c +``` + +To install the binary system-wide to `/usr/local/bin` (respects `PREFIX` and `DESTDIR`): + +```sh +sudo make install +``` + +## Usage + +Because `nvbright` requires direct access to `/dev/mem` to map GPU MMIO registers and executes `/usr/sbin/pciconf`, it must be run as root (e.g., with `sudo` or `doas`). + +### Command Syntax + +```sh +nvbright [value 0-100] +``` + +### Commands + +- **`get`**: Queries the hardware and prints the current brightness level (percentage from 0 to 100), active SOR index, and PWM divisor. + ```sh + sudo nvbright get + ``` + *Example Output:* + ```text + Brightness: 65 / 100 (SOR1, pwm_div=1024) + ``` + +- **`set <0-100>`**: Sets the display backlight directly to the specified percentage value. + ```sh + sudo nvbright set 75 + ``` + *Example Output:* + ```text + Set brightness: 75 / 100 + ``` + +- **`up`**: Increases brightness by 10% (clamped at 100%). + ```sh + sudo nvbright up + ``` + +- **`down`**: Decreases brightness by 10% (clamped at 0%). + ```sh + sudo nvbright down + ``` + +### Desktop and Window Manager Integration + +You can bind `nvbright up` and `nvbright down` commands to the brightness keys (`XF86MonBrightnessUp` and `XF86MonBrightnessDown`) in window manager configurations (such as Sway, i3, or Openbox) or configure `sudoers` / `doas.conf` rules to allow execution without password prompts. + +## Credits + +Created by Luxferre in 2026, released into the public domain with no warranties. + +Hardware register specifications and control flow inspired by the Linux `nouveau` DRM backlight driver (`nv50` / `nva3`). diff --git a/nvbright.c b/nvbright.c new file mode 100644 index 0000000..dacc071 --- /dev/null +++ b/nvbright.c @@ -0,0 +1,152 @@ +/* + * nvbright - NVIDIA GPU backlight control for Apple MacBooks (FreeBSD) + * + * On NVIDIA-based Apple hardware (e.g. MacBook Air A1370 / GeForce 320M, + * MCP89 / GT218) the LCD backlight is driven by a PWM signal generated by + * the GPU's display engine (PDISPLAY). The firmware normally reprograms it + * through an SMI triggered by legacy I/O ports, but that path is dead under + * FreeBSD (EFI boot), so we program the GPU MMIO PWM registers directly. + * This mirrors the Linux nouveau nv50 / nva3 backlight driver: + * + * NV50_PDISP_SOR_PWM_DIV(i) = 0x0061c080 + i * 0x800 + * NV50_PDISP_SOR_PWM_CTL(i) = 0x0061c084 + i * 0x800 + * CTL_NEW = 0x80000000 (latch / apply new value) + * CTL_UNK = 0x40000000 (required on nva3-class chips) + * + * The backlight SOR is located at runtime (the one with a programmed + * PWM_DIV), and the GPU's MMIO register BAR is mapped via /dev/mem. + * Requires root to run. + * + * Created by Luxferre in 2026, released into the public domain. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PWM_DIV(i) (0x0061c080UL + (i) * 0x800UL) +#define PWM_CTL(i) (0x0061c084UL + (i) * 0x800UL) +#define CTL_NEW 0x80000000u +#define CTL_UNK 0x40000000u +#define MAX_SOR 8 +#define STEP 10 +#define FIXED_DIV 1025u /* used only if no SOR has a programmed PWM_DIV */ + +static int g_memfd = -1; +static volatile uint32_t *g_bar; +static int g_sor = -1; +static uint32_t g_div; + +static void die(const char *msg) { + fprintf(stderr, "nvbright: %s: %s\n", msg, strerror(errno)); + exit(1); +} + +static uint32_t rd(uint32_t off) { return g_bar[off / 4]; } +static void wr(uint32_t off, uint32_t v) { g_bar[off / 4] = v; } + +/* Locate the NVIDIA VGA device's MMIO register BAR (BAR0) via pciconf. */ +static void locate_bar(uint64_t *base, uint64_t *size) { + FILE *fp = popen("/usr/sbin/pciconf -l -b", "r"); + if(!fp) die("popen /usr/sbin/pciconf"); + char line[1024]; + int in_nv = 0; + *base = 0; *size = 0; + while(fgets(line, sizeof(line), fp)) { + if(line[0] != ' ' && line[0] != '\t') { + in_nv = (strstr(line, "vendor=0x10de") && + (strstr(line, "class=0x030000") || strstr(line, "vgapci"))); + continue; + } + if(!in_nv) continue; + if(strstr(line, "bar") && strstr(line, "[10]")) { + char *b = strstr(line, "base"); + char *s = strstr(line, "size"); + unsigned long long bb = 0, sz = 0; + if(b) sscanf(b, "base 0x%llx", &bb); + if(s) sscanf(s, "size %llu", &sz); + *base = (uint64_t)bb; + *size = (uint64_t)sz; + break; + } + } + pclose(fp); + if(!*base) die("could not locate NVIDIA GPU MMIO BAR (is this an nvidia vga?)"); +} + +/* Pick the SOR whose PWM the firmware already programmed (the backlight). */ +static void detect_sor(void) { + for(int i = 0; i < MAX_SOR; i++) { + uint32_t div = rd(PWM_DIV(i)); + if(div != 0) { g_sor = i; g_div = div; return; } + } + g_sor = 0; /* fallback: nv50-style fixed period on SOR0 */ + g_div = 0; +} + +static int bl_get(void) { + uint32_t ctl = rd(PWM_CTL(g_sor)); + uint32_t val = ctl & 0x00ffffffu; + uint32_t div = g_div ? g_div : FIXED_DIV; + long lvl = ((long)val * 100 + (long)(div / 2)) / (long)div; + if(lvl < 0) lvl = 0; + if(lvl > 100) lvl = 100; + return (int)lvl; +} + +static void bl_set(int level) { + if(level < 0) level = 0; + if(level > 100) level = 100; + uint32_t div = g_div ? g_div : FIXED_DIV; + uint32_t val = (uint32_t)(((uint64_t)level * div) / 100); + wr(PWM_DIV(g_sor), div); + wr(PWM_CTL(g_sor), val | CTL_NEW | CTL_UNK); +} + +int main(int argc, char **argv) { + if(argc < 2) { + fprintf(stderr, "Usage: %s [value 0-100]\n", argv[0]); + fprintf(stderr, " get print current brightness (0-100)\n"); + fprintf(stderr, " set <0-100> set brightness\n"); + fprintf(stderr, " up / down step brightness by %d\n", STEP); + return 1; + } + + uint64_t base = 0, size = 0; + locate_bar(&base, &size); + if(size == 0) size = 0x1000000UL; + + g_memfd = open("/dev/mem", O_RDWR); + if(g_memfd < 0) die("open /dev/mem (must run as root)"); + void *m = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, g_memfd, (off_t)base); + if(m == MAP_FAILED) die("mmap /dev/mem"); + g_bar = (volatile uint32_t *)m; + + detect_sor(); + + if(strcmp(argv[1], "get") == 0) { + printf("Brightness: %d / 100 (SOR%d, pwm_div=%u)\n", + bl_get(), g_sor, g_div ? g_div : FIXED_DIV); + } else if(strcmp(argv[1], "set") == 0 && argc >= 3) { + bl_set(atoi(argv[2])); + printf("Set brightness: %d / 100\n", bl_get()); + } else if(strcmp(argv[1], "up") == 0) { + int v = bl_get() + STEP; if(v > 100) v = 100; + bl_set(v); + printf("Brightness up: %d / 100\n", bl_get()); + } else if(strcmp(argv[1], "down") == 0) { + int v = bl_get() - STEP; if(v < 0) v = 0; + bl_set(v); + printf("Brightness down: %d / 100\n", bl_get()); + } else { + fprintf(stderr, "Unknown command: %s\n", argv[1]); + return 1; + } + return 0; +}