This commit is contained in:
Luxferre
2026-08-14 13:49:01 +03:00
commit 563db09bf1
3 changed files with 272 additions and 0 deletions
+25
View File
@@ -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
+95
View File
@@ -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 <get|set|up|down> [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`).
+152
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/types.h>
#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 <get|set|up|down> [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;
}