Monorepo for Aesthetic.Computer aesthetic.computer
at main 975 lines 35 kB view raw
1#include "drm-display.h" 2#include "font.h" 3#include <stdio.h> 4#include <stdlib.h> 5#include <string.h> 6#include <fcntl.h> 7#include <unistd.h> 8#include <sys/ioctl.h> 9#include <sys/mman.h> 10#include <sys/select.h> 11#include <linux/fb.h> 12 13// ============================================================ 14// SDL3 GPU-accelerated display — loaded via dlopen (no link-time dep) 15// Falls back to DRM/fbdev if SDL3 libs are missing or broken. 16// ============================================================ 17 18#include <dlfcn.h> 19#include <signal.h> 20#include <sys/wait.h> 21 22extern void ac_log(const char *fmt, ...); 23 24// SDL3 function pointers (resolved via dlsym at runtime) 25static void *sdl_lib_handle = NULL; 26typedef int (*pfn_SDL_Init)(unsigned int); 27typedef void (*pfn_SDL_Quit)(void); 28typedef const char *(*pfn_SDL_GetError)(void); 29typedef unsigned int (*pfn_SDL_GetPrimaryDisplay)(void); 30typedef const void *(*pfn_SDL_GetDesktopDisplayMode)(unsigned int); 31typedef void *(*pfn_SDL_CreateWindow)(const char *, int, int, unsigned int); 32typedef void (*pfn_SDL_DestroyWindow)(void *); 33typedef void *(*pfn_SDL_CreateRenderer)(void *, const char *); 34typedef void (*pfn_SDL_DestroyRenderer)(void *); 35typedef int (*pfn_SDL_RenderClear)(void *); 36typedef int (*pfn_SDL_RenderPresent)(void *); 37typedef int (*pfn_SDL_RenderTexture)(void *, void *, const void *, const void *); 38typedef void *(*pfn_SDL_CreateTexture)(void *, unsigned int, int, int, int); 39typedef void (*pfn_SDL_DestroyTexture)(void *); 40typedef int (*pfn_SDL_UpdateTexture)(void *, const void *, const void *, int); 41typedef int (*pfn_SDL_SetRenderVSync)(void *, int); 42typedef int (*pfn_SDL_SetTextureScaleMode)(void *, int); 43typedef void (*pfn_SDL_HideCursor)(void); 44typedef const char *(*pfn_SDL_GetRendererName)(void *); 45 46static struct { 47 pfn_SDL_Init Init; 48 pfn_SDL_Quit Quit; 49 pfn_SDL_GetError GetError; 50 pfn_SDL_GetPrimaryDisplay GetPrimaryDisplay; 51 pfn_SDL_GetDesktopDisplayMode GetDesktopDisplayMode; 52 pfn_SDL_CreateWindow CreateWindow; 53 pfn_SDL_DestroyWindow DestroyWindow; 54 pfn_SDL_CreateRenderer CreateRenderer; 55 pfn_SDL_DestroyRenderer DestroyRenderer; 56 pfn_SDL_RenderClear RenderClear; 57 pfn_SDL_RenderPresent RenderPresent; 58 pfn_SDL_RenderTexture RenderTexture; 59 pfn_SDL_CreateTexture CreateTexture; 60 pfn_SDL_DestroyTexture DestroyTexture; 61 pfn_SDL_UpdateTexture UpdateTexture; 62 pfn_SDL_SetRenderVSync SetRenderVSync; 63 pfn_SDL_SetTextureScaleMode SetTextureScaleMode; 64 pfn_SDL_HideCursor HideCursor; 65 pfn_SDL_GetRendererName GetRendererName; 66} sdl = {0}; 67 68static int sdl_load(void) { 69 if (sdl_lib_handle) return 1; 70 setenv("LIBGL_DRIVERS_PATH", "/lib64/dri", 0); 71 setenv("GBM_DRIVERS_PATH", "/lib64/dri", 0); 72 setenv("MESA_LOADER_DRIVER_OVERRIDE", "iris", 0); 73 sdl_lib_handle = dlopen("libSDL3.so.0", RTLD_LAZY); 74 if (!sdl_lib_handle) { 75 ac_log("[sdl3] dlopen failed: %s\n", dlerror()); 76 return 0; 77 } 78 #define LOAD(name) sdl.name = (pfn_SDL_##name)dlsym(sdl_lib_handle, "SDL_" #name) 79 LOAD(Init); LOAD(Quit); LOAD(GetError); 80 LOAD(GetPrimaryDisplay); LOAD(GetDesktopDisplayMode); 81 LOAD(CreateWindow); LOAD(DestroyWindow); 82 LOAD(CreateRenderer); LOAD(DestroyRenderer); 83 LOAD(RenderClear); LOAD(RenderPresent); LOAD(RenderTexture); 84 LOAD(CreateTexture); LOAD(DestroyTexture); LOAD(UpdateTexture); 85 LOAD(SetRenderVSync); LOAD(SetTextureScaleMode); 86 LOAD(HideCursor); LOAD(GetRendererName); 87 #undef LOAD 88 if (!sdl.Init || !sdl.CreateWindow || !sdl.CreateRenderer) { 89 ac_log("[sdl3] Missing required symbols\n"); 90 dlclose(sdl_lib_handle); 91 sdl_lib_handle = NULL; 92 return 0; 93 } 94 return 1; 95} 96 97// Signal-based crash recovery for SDL3 init (no fork needed) 98#include <setjmp.h> 99static sigjmp_buf sdl_crash_jmp; 100static volatile int sdl_crash_sig = 0; 101 102static void sdl_crash_handler(int sig) { 103 sdl_crash_sig = sig; 104 siglongjmp(sdl_crash_jmp, 1); 105} 106 107static ACDisplay *sdl_init(void) { 108 // Check if SDL was disabled by init after a previous crash 109 const char *no_sdl = getenv("AC_NO_SDL"); 110 if (no_sdl && no_sdl[0] == '1') { 111 ac_log("[sdl3] Disabled via AC_NO_SDL (previous crash) — using DRM\n"); 112 return NULL; 113 } 114 115 // Set Mesa env vars before any dlopen 116 setenv("LIBGL_DRIVERS_PATH", "/lib64/dri", 0); 117 setenv("GBM_DRIVERS_PATH", "/lib64/dri", 0); 118 setenv("MESA_LOADER_DRIVER_OVERRIDE", "iris", 0); 119 setenv("SDL_VIDEO_DRIVER", "kmsdrm", 0); 120 121 // Install crash handler — catches SIGSEGV/SIGBUS from Mesa DRI loading 122 struct sigaction sa = {0}, old_segv = {0}, old_bus = {0}, old_abrt = {0}; 123 sa.sa_handler = sdl_crash_handler; 124 sa.sa_flags = 0; 125 sigemptyset(&sa.sa_mask); 126 sigaction(SIGSEGV, &sa, &old_segv); 127 sigaction(SIGBUS, &sa, &old_bus); 128 sigaction(SIGABRT, &sa, &old_abrt); 129 130 if (sigsetjmp(sdl_crash_jmp, 1) != 0) { 131 // Crashed during SDL init — restore handlers and fall back 132 sigaction(SIGSEGV, &old_segv, NULL); 133 sigaction(SIGBUS, &old_bus, NULL); 134 sigaction(SIGABRT, &old_abrt, NULL); 135 ac_log("[sdl3] Crashed (signal %d) during init — falling back to DRM\n", sdl_crash_sig); 136 if (sdl_lib_handle) { dlclose(sdl_lib_handle); sdl_lib_handle = NULL; } 137 memset(&sdl, 0, sizeof(sdl)); 138 return NULL; 139 } 140 141 // Try loading SDL3 (dlopen may trigger Mesa DRI load → potential crash) 142 if (!sdl_load()) { 143 sigaction(SIGSEGV, &old_segv, NULL); 144 sigaction(SIGBUS, &old_bus, NULL); 145 sigaction(SIGABRT, &old_abrt, NULL); 146 return NULL; 147 } 148 149 if (!sdl.Init(0x20)) { // SDL_INIT_VIDEO 150 ac_log("[sdl3] SDL_Init failed: %s\n", sdl.GetError ? sdl.GetError() : "?"); 151 sigaction(SIGSEGV, &old_segv, NULL); 152 sigaction(SIGBUS, &old_bus, NULL); 153 sigaction(SIGABRT, &old_abrt, NULL); 154 return NULL; 155 } 156 // Past the danger zone — restore handlers now 157 sigaction(SIGSEGV, &old_segv, NULL); 158 sigaction(SIGBUS, &old_bus, NULL); 159 sigaction(SIGABRT, &old_abrt, NULL); 160 161 unsigned int primary = sdl.GetPrimaryDisplay(); 162 if (!primary) { 163 ac_log("[sdl3] No primary display: %s\n", sdl.GetError()); 164 sdl.Quit(); 165 return NULL; 166 } 167 // SDL_DisplayMode: { int displayID, int format, int w, int h, float refresh, ... } 168 typedef struct { unsigned int id; unsigned int fmt; int w, h; float refresh; int pad; void *d; } SDLMode; 169 const SDLMode *dm = (const SDLMode *)sdl.GetDesktopDisplayMode(primary); 170 if (!dm) { 171 ac_log("[sdl3] GetDesktopDisplayMode failed: %s\n", sdl.GetError()); 172 sdl.Quit(); 173 return NULL; 174 } 175 176 void *win = sdl.CreateWindow("ac-native", dm->w, dm->h, 0x1); // SDL_WINDOW_FULLSCREEN 177 if (!win) { 178 ac_log("[sdl3] CreateWindow failed: %s\n", sdl.GetError()); 179 sdl.Quit(); 180 return NULL; 181 } 182 183 if (sdl.HideCursor) sdl.HideCursor(); 184 185 void *ren = sdl.CreateRenderer(win, NULL); 186 if (!ren) { 187 ac_log("[sdl3] CreateRenderer failed: %s\n", sdl.GetError()); 188 sdl.DestroyWindow(win); 189 sdl.Quit(); 190 return NULL; 191 } 192 193 if (sdl.SetRenderVSync) sdl.SetRenderVSync(ren, 1); 194 195 const char *ren_name = sdl.GetRendererName ? sdl.GetRendererName(ren) : "unknown"; 196 ac_log("[sdl3] Renderer: %s\n", ren_name ? ren_name : "unknown"); 197 198 ACDisplay *d = calloc(1, sizeof(ACDisplay)); 199 d->fd = -1; 200 d->is_sdl = 1; 201 d->width = dm->w; 202 d->height = dm->h; 203 d->sdl_window = win; 204 d->sdl_renderer = ren; 205 d->sdl_texture = NULL; 206 d->sdl_tex_w = 0; 207 d->sdl_tex_h = 0; 208 snprintf(d->sdl_renderer_name, sizeof(d->sdl_renderer_name), "%s", 209 ren_name ? ren_name : "unknown"); 210 211 // Restore signal handlers — SDL init survived 212 sigaction(SIGSEGV, &old_segv, NULL); 213 sigaction(SIGBUS, &old_bus, NULL); 214 sigaction(SIGABRT, &old_abrt, NULL); 215 216 ac_log("[sdl3] Ready (%dx%d)\n", d->width, d->height); 217 return d; 218} 219 220// ============================================================ 221// fbdev fallback — uses /dev/fb0 (EFI framebuffer via efifb) 222// ============================================================ 223 224static ACDisplay *fbdev_init(void) { 225 // Try opening fb0 with retries (may take time to appear in devtmpfs) 226 int fd = -1; 227 for (int attempt = 0; attempt < 100; attempt++) { 228 fd = open("/dev/fb0", O_RDWR); 229 if (fd >= 0) break; 230 usleep(20000); // 20ms 231 } 232 if (fd < 0) { 233 fprintf(stderr, "[fbdev] Cannot open /dev/fb0 after retries\n"); 234 // List what's in /dev for debugging 235 return NULL; 236 } 237 238 struct fb_var_screeninfo vinfo; 239 struct fb_fix_screeninfo finfo; 240 if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) < 0 || 241 ioctl(fd, FBIOGET_FSCREENINFO, &finfo) < 0) { 242 fprintf(stderr, "[fbdev] Cannot get screen info\n"); 243 close(fd); 244 return NULL; 245 } 246 247 fprintf(stderr, "[fbdev] Display: %dx%d, %d bpp, stride %d\n", 248 vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, finfo.line_length); 249 fprintf(stderr, "[fbdev] RGBA: %d/%d/%d/%d offset %d/%d/%d/%d\n", 250 vinfo.red.length, vinfo.green.length, vinfo.blue.length, vinfo.transp.length, 251 vinfo.red.offset, vinfo.green.offset, vinfo.blue.offset, vinfo.transp.offset); 252 253 if (vinfo.bits_per_pixel != 32) { 254 // Try setting 32bpp 255 vinfo.bits_per_pixel = 32; 256 vinfo.red.offset = 16; vinfo.red.length = 8; 257 vinfo.green.offset = 8; vinfo.green.length = 8; 258 vinfo.blue.offset = 0; vinfo.blue.length = 8; 259 vinfo.transp.offset = 24; vinfo.transp.length = 8; 260 if (ioctl(fd, FBIOPUT_VSCREENINFO, &vinfo) < 0) { 261 fprintf(stderr, "[fbdev] Cannot set 32bpp, using native %dbpp\n", 262 vinfo.bits_per_pixel); 263 // Re-read after failed set 264 ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); 265 ioctl(fd, FBIOGET_FSCREENINFO, &finfo); 266 if (vinfo.bits_per_pixel != 32) { 267 fprintf(stderr, "[fbdev] Unsupported bpp: %d\n", vinfo.bits_per_pixel); 268 close(fd); 269 return NULL; 270 } 271 } 272 ioctl(fd, FBIOGET_FSCREENINFO, &finfo); 273 fprintf(stderr, "[fbdev] Set 32bpp: stride now %d\n", finfo.line_length); 274 } 275 276 uint32_t map_size = finfo.line_length * vinfo.yres; 277 uint32_t *map = mmap(0, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 278 if (map == MAP_FAILED) { 279 perror("[fbdev] mmap failed"); 280 close(fd); 281 return NULL; 282 } 283 284 // Clear to black 285 memset(map, 0, map_size); 286 287 // Blank the console cursor / disable console 288 int tty_fd = open("/dev/tty0", O_RDWR); 289 if (tty_fd >= 0) { 290 // KDSETMODE KD_GRAPHICS = 1 291 ioctl(tty_fd, 0x4B3A, 1); 292 close(tty_fd); 293 } 294 295 ACDisplay *d = calloc(1, sizeof(ACDisplay)); 296 d->fd = fd; 297 d->is_fbdev = 1; 298 d->width = (int)vinfo.xres; 299 d->height = (int)vinfo.yres; 300 d->fbdev_map = map; 301 d->fbdev_size = map_size; 302 d->fbdev_stride = (int)(finfo.line_length / sizeof(uint32_t)); 303 304 // Detect if framebuffer is BGR (blue at high offset, red at low) 305 // Our internal format is ARGB: A<<24 | R<<16 | G<<8 | B 306 // EFI framebuffers are often XBGR: X<<24 | B<<16 | G<<8 | R 307 if (vinfo.blue.offset > vinfo.red.offset) { 308 d->fbdev_swap_rb = 1; 309 fprintf(stderr, "[fbdev] BGR format detected — will swap R/B\n"); 310 } 311 312 // Use buffers[0].map as the software back buffer (we copy to fbdev_map on flip) 313 uint32_t buf_size = (uint32_t)(d->width * d->height * 4); 314 d->buffers[0].map = malloc(buf_size); 315 d->buffers[0].size = buf_size; 316 d->buffers[0].pitch = (uint32_t)(d->width * 4); 317 memset(d->buffers[0].map, 0, buf_size); 318 319 fprintf(stderr, "[fbdev] Ready (%dx%d, swap_rb=%d)\n", 320 d->width, d->height, d->fbdev_swap_rb); 321 return d; 322} 323 324// ============================================================ 325// DRM display 326// ============================================================ 327 328static int try_open_drm(void) { 329 const char *paths[] = { 330 "/dev/dri/card0", 331 "/dev/dri/card1", 332 NULL 333 }; 334 for (int i = 0; paths[i]; i++) { 335 int fd = open(paths[i], O_RDWR | O_CLOEXEC); 336 if (fd >= 0) { 337 if (drmSetMaster(fd) == 0) { 338 fprintf(stderr, "[drm] Opened %s\n", paths[i]); 339 return fd; 340 } 341 fprintf(stderr, "[drm] Opened %s (no master)\n", paths[i]); 342 return fd; 343 } 344 } 345 return -1; 346} 347 348static int create_dumb_buffer(ACDisplay *d, int idx) { 349 struct drm_mode_create_dumb create = { 350 .width = (uint32_t)d->width, 351 .height = (uint32_t)d->height, 352 .bpp = 32, 353 }; 354 355 if (drmIoctl(d->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0) { 356 perror("[drm] Create dumb buffer failed"); 357 return -1; 358 } 359 360 d->buffers[idx].handle = create.handle; 361 d->buffers[idx].pitch = create.pitch; 362 d->buffers[idx].size = create.size; 363 364 if (drmModeAddFB(d->fd, (uint32_t)d->width, (uint32_t)d->height, 24, 32, 365 create.pitch, create.handle, &d->buffers[idx].fb_id) < 0) { 366 perror("[drm] AddFB failed"); 367 return -1; 368 } 369 370 struct drm_mode_map_dumb map = { .handle = create.handle }; 371 if (drmIoctl(d->fd, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) { 372 perror("[drm] Map dumb buffer failed"); 373 return -1; 374 } 375 376 d->buffers[idx].map = mmap(0, create.size, PROT_READ | PROT_WRITE, 377 MAP_SHARED, d->fd, map.offset); 378 if (d->buffers[idx].map == MAP_FAILED) { 379 perror("[drm] mmap failed"); 380 return -1; 381 } 382 383 memset(d->buffers[idx].map, 0, create.size); 384 return 0; 385} 386 387ACDisplay *drm_init(void) { 388 extern void ac_log(const char *fmt, ...); 389 ac_log("[drm] drm_init() start\n"); 390 ACDisplay *sdl = sdl_init(); 391 if (sdl) return sdl; 392 ac_log("[drm] SDL3 failed, falling back to DRM dumb buffers\n"); 393 394 ACDisplay *d = calloc(1, sizeof(ACDisplay)); 395 if (!d) { ac_log("[drm] calloc failed\n"); return NULL; } 396 397 ac_log("[drm] try_open_drm...\n"); 398 d->fd = try_open_drm(); 399 ac_log("[drm] fd=%d\n", d->fd); 400 if (d->fd < 0) { 401 ac_log("[drm] No DRM device, trying fbdev\n"); 402 free(d); 403 return fbdev_init(); 404 } 405 406 uint64_t has_dumb; 407 if (drmGetCap(d->fd, DRM_CAP_DUMB_BUFFER, &has_dumb) < 0 || !has_dumb) { 408 fprintf(stderr, "[drm] No dumb buffer support, trying fbdev...\n"); 409 close(d->fd); 410 free(d); 411 return fbdev_init(); 412 } 413 414 drmModeRes *res = drmModeGetResources(d->fd); 415 if (!res) { 416 fprintf(stderr, "[drm] No resources, trying fbdev...\n"); 417 close(d->fd); 418 free(d); 419 return fbdev_init(); 420 } 421 422 // Prefer internal panel (eDP/LVDS) over external (HDMI/DP) to avoid slow EDID probes 423 drmModeConnector *conn = NULL; 424 // First pass: look for internal panel 425 for (int i = 0; i < res->count_connectors; i++) { 426 drmModeConnector *c = drmModeGetConnector(d->fd, res->connectors[i]); 427 if (!c) continue; 428 if (c->connection == DRM_MODE_CONNECTED && c->count_modes > 0 && 429 (c->connector_type == DRM_MODE_CONNECTOR_eDP || 430 c->connector_type == DRM_MODE_CONNECTOR_LVDS || 431 c->connector_type == DRM_MODE_CONNECTOR_DSI)) { 432 conn = c; 433 fprintf(stderr, "[drm] Using internal panel (type %d)\n", c->connector_type); 434 break; 435 } 436 drmModeFreeConnector(c); 437 } 438 // Second pass: any connected display 439 if (!conn) { 440 for (int i = 0; i < res->count_connectors; i++) { 441 conn = drmModeGetConnector(d->fd, res->connectors[i]); 442 if (conn && conn->connection == DRM_MODE_CONNECTED && conn->count_modes > 0) 443 break; 444 if (conn) drmModeFreeConnector(conn); 445 conn = NULL; 446 } 447 } 448 449 if (!conn) { 450 fprintf(stderr, "[drm] No connected display, trying fbdev...\n"); 451 drmModeFreeResources(res); 452 close(d->fd); 453 free(d); 454 return fbdev_init(); 455 } 456 457 d->connector_id = conn->connector_id; 458 d->mode = conn->modes[0]; 459 d->width = d->mode.hdisplay; 460 d->height = d->mode.vdisplay; 461 fprintf(stderr, "[drm] Display: %dx%d @ %dHz\n", 462 d->width, d->height, d->mode.vrefresh); 463 464 drmModeEncoder *enc = NULL; 465 if (conn->encoder_id) { 466 enc = drmModeGetEncoder(d->fd, conn->encoder_id); 467 } 468 if (!enc) { 469 for (int i = 0; i < conn->count_encoders; i++) { 470 enc = drmModeGetEncoder(d->fd, conn->encoders[i]); 471 if (enc) break; 472 } 473 } 474 if (!enc) { 475 fprintf(stderr, "[drm] No encoder, trying fbdev...\n"); 476 drmModeFreeConnector(conn); 477 drmModeFreeResources(res); 478 close(d->fd); 479 free(d); 480 return fbdev_init(); 481 } 482 483 d->crtc_id = enc->crtc_id; 484 if (!d->crtc_id) { 485 for (int i = 0; i < res->count_crtcs; i++) { 486 if (enc->possible_crtcs & (1u << i)) { 487 d->crtc_id = res->crtcs[i]; 488 break; 489 } 490 } 491 } 492 493 d->saved_crtc = drmModeGetCrtc(d->fd, d->crtc_id); 494 495 drmModeFreeEncoder(enc); 496 drmModeFreeConnector(conn); 497 drmModeFreeResources(res); 498 499 if (create_dumb_buffer(d, 0) < 0 || create_dumb_buffer(d, 1) < 0) { 500 drm_destroy(d); 501 return fbdev_init(); 502 } 503 504 d->front = 0; 505 506 if (drmModeSetCrtc(d->fd, d->crtc_id, d->buffers[0].fb_id, 0, 0, 507 &d->connector_id, 1, &d->mode) < 0) { 508 perror("[drm] SetCrtc failed, trying fbdev..."); 509 drm_destroy(d); 510 return fbdev_init(); 511 } 512 513 fprintf(stderr, "[drm] Ready\n"); 514 return d; 515} 516 517void drm_flip(ACDisplay *d) { 518 if (d->is_fbdev) { 519 uint32_t *src = d->buffers[0].map; 520 uint32_t *dst = d->fbdev_map; 521 int src_stride = d->width; 522 int dst_stride = d->fbdev_stride; 523 if (d->fbdev_swap_rb) { 524 // Convert ARGB → ABGR (swap R and B channels) 525 for (int y = 0; y < d->height; y++) { 526 uint32_t *s = src + y * src_stride; 527 uint32_t *dd = dst + y * dst_stride; 528 for (int x = 0; x < d->width; x++) { 529 uint32_t p = s[x]; 530 dd[x] = (p & 0xFF00FF00u) | // keep A and G 531 ((p & 0x00FF0000u) >> 16) | // R → B 532 ((p & 0x000000FFu) << 16); // B → R 533 } 534 } 535 } else { 536 for (int y = 0; y < d->height; y++) { 537 memcpy(dst + y * dst_stride, src + y * src_stride, (size_t)(d->width * 4)); 538 } 539 } 540 return; 541 } 542 int back = 1 - d->front; 543 // Page flip synchronized to vblank (prevents tearing) 544 if (drmModePageFlip(d->fd, d->crtc_id, d->buffers[back].fb_id, 545 DRM_MODE_PAGE_FLIP_EVENT, d) == 0) { 546 // Wait for the flip event (blocks until vblank) 547 fd_set fds; 548 FD_ZERO(&fds); 549 FD_SET(d->fd, &fds); 550 struct timeval tv = { .tv_sec = 0, .tv_usec = 50000 }; // 50ms timeout 551 if (select(d->fd + 1, &fds, NULL, NULL, &tv) > 0) { 552 drmEventContext ev = { 553 .version = 2, 554 .page_flip_handler = NULL, // we just need to consume the event 555 }; 556 drmHandleEvent(d->fd, &ev); 557 } 558 } else { 559 // Fallback to SetCrtc if page flip not supported 560 drmModeSetCrtc(d->fd, d->crtc_id, d->buffers[back].fb_id, 0, 0, 561 &d->connector_id, 1, &d->mode); 562 } 563 d->front = back; 564} 565 566uint32_t *drm_back_buffer(ACDisplay *d) { 567 if (d->is_fbdev) { 568 return d->buffers[0].map; 569 } 570 return d->buffers[1 - d->front].map; 571} 572 573int drm_back_stride(ACDisplay *d) { 574 if (d->is_fbdev) { 575 return d->width; 576 } 577 return (int)(d->buffers[1 - d->front].pitch / sizeof(uint32_t)); 578} 579 580uint32_t *drm_front_buffer(ACDisplay *d) { 581 if (d->is_fbdev) return d->fbdev_map; 582 return d->buffers[d->front].map; 583} 584 585int drm_front_stride(ACDisplay *d) { 586 if (d->is_fbdev) return d->fbdev_stride; 587 return (int)(d->buffers[d->front].pitch / sizeof(uint32_t)); 588} 589 590void display_present(ACDisplay *d, ACFramebuffer *screen, int scale) { 591 if (!d || !screen) return; 592 593 if (d->is_sdl && sdl.CreateTexture) { 594 // Create/recreate texture if framebuffer size changed 595 if (!d->sdl_texture || d->sdl_tex_w != screen->width || d->sdl_tex_h != screen->height) { 596 if (d->sdl_texture && sdl.DestroyTexture) sdl.DestroyTexture(d->sdl_texture); 597 d->sdl_texture = sdl.CreateTexture(d->sdl_renderer, 598 0x16362004, 1, // SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING 599 screen->width, screen->height); 600 if (d->sdl_texture && sdl.SetTextureScaleMode) { 601 sdl.SetTextureScaleMode(d->sdl_texture, 0); // SDL_SCALEMODE_NEAREST 602 } 603 d->sdl_tex_w = screen->width; 604 d->sdl_tex_h = screen->height; 605 fprintf(stderr, "[sdl3] Created texture %dx%d\n", screen->width, screen->height); 606 } 607 if (sdl.UpdateTexture) 608 sdl.UpdateTexture(d->sdl_texture, NULL, 609 screen->pixels, screen->stride * (int)sizeof(uint32_t)); 610 if (sdl.RenderClear) sdl.RenderClear(d->sdl_renderer); 611 if (sdl.RenderTexture) sdl.RenderTexture(d->sdl_renderer, d->sdl_texture, NULL, NULL); 612 if (sdl.RenderPresent) sdl.RenderPresent(d->sdl_renderer); 613 return; 614 } 615 (void)scale; 616 // CPU fallback: scale to back buffer and flip 617 fb_copy_scaled(screen, drm_back_buffer(d), 618 d->width, d->height, drm_back_stride(d), scale); 619 drm_flip(d); 620} 621 622// ============================================================ 623// Secondary HDMI display — solid color fill 624// ============================================================ 625 626ACSecondaryDisplay *drm_init_secondary(ACDisplay *primary) { 627 if (!primary || primary->is_fbdev || primary->fd < 0) return NULL; 628 if (primary->is_sdl) return NULL; 629 630 drmModeRes *res = drmModeGetResources(primary->fd); 631 if (!res) return NULL; 632 633 drmModeConnector *conn = NULL; 634 for (int i = 0; i < res->count_connectors; i++) { 635 drmModeConnector *c = drmModeGetConnector(primary->fd, res->connectors[i]); 636 if (!c) continue; 637 if (c->connector_id == primary->connector_id) { drmModeFreeConnector(c); continue; } 638 if (c->connection == DRM_MODE_CONNECTED && c->count_modes > 0 && 639 (c->connector_type == DRM_MODE_CONNECTOR_HDMIA || 640 c->connector_type == DRM_MODE_CONNECTOR_HDMIB || 641 c->connector_type == DRM_MODE_CONNECTOR_DisplayPort)) { 642 conn = c; 643 break; 644 } 645 drmModeFreeConnector(c); 646 } 647 drmModeFreeResources(res); 648 649 if (!conn) { 650 fprintf(stderr, "[drm-secondary] No HDMI/DP display found\n"); 651 return NULL; 652 } 653 654 ACSecondaryDisplay *s = calloc(1, sizeof(ACSecondaryDisplay)); 655 s->fd = primary->fd; 656 s->connector_id = conn->connector_id; 657 s->mode = conn->modes[0]; 658 s->width = s->mode.hdisplay; 659 s->height = s->mode.vdisplay; 660 fprintf(stderr, "[drm-secondary] HDMI: %dx%d @ %dHz\n", 661 s->width, s->height, s->mode.vrefresh); 662 663 drmModeEncoder *enc = NULL; 664 if (conn->encoder_id) enc = drmModeGetEncoder(primary->fd, conn->encoder_id); 665 if (!enc) { 666 for (int i = 0; i < conn->count_encoders; i++) { 667 enc = drmModeGetEncoder(primary->fd, conn->encoders[i]); 668 if (enc && enc->crtc_id != primary->crtc_id) break; 669 if (enc) { drmModeFreeEncoder(enc); enc = NULL; } 670 } 671 } 672 drmModeFreeConnector(conn); 673 674 if (!enc) { fprintf(stderr, "[drm-secondary] No encoder\n"); free(s); return NULL; } 675 676 s->crtc_id = enc->crtc_id; 677 if (!s->crtc_id) { 678 drmModeRes *r2 = drmModeGetResources(primary->fd); 679 if (r2) { 680 for (int i = 0; i < r2->count_crtcs; i++) { 681 if (r2->crtcs[i] != primary->crtc_id && (enc->possible_crtcs & (1 << i))) { 682 s->crtc_id = r2->crtcs[i]; 683 break; 684 } 685 } 686 drmModeFreeResources(r2); 687 } 688 } 689 drmModeFreeEncoder(enc); 690 691 if (!s->crtc_id) { fprintf(stderr, "[drm-secondary] No free CRTC\n"); free(s); return NULL; } 692 693 s->saved_crtc = drmModeGetCrtc(primary->fd, s->crtc_id); 694 695 // Allocate two dumb buffers for double-buffering 696 for (int b = 0; b < 2; b++) { 697 struct drm_mode_create_dumb create = { .width = s->width, .height = s->height, .bpp = 32 }; 698 if (drmIoctl(s->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0) { 699 fprintf(stderr, "[drm-secondary] Create dumb buffer %d failed\n", b); free(s); return NULL; 700 } 701 s->bufs[b].handle = create.handle; 702 s->bufs[b].pitch = create.pitch; 703 s->bufs[b].size = create.size; 704 705 if (drmModeAddFB(s->fd, s->width, s->height, 24, 32, 706 s->bufs[b].pitch, s->bufs[b].handle, &s->bufs[b].fb_id) < 0) { 707 fprintf(stderr, "[drm-secondary] AddFB %d failed\n", b); 708 struct drm_mode_destroy_dumb destroy = { .handle = s->bufs[b].handle }; 709 drmIoctl(s->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); 710 free(s); return NULL; 711 } 712 713 struct drm_mode_map_dumb map_req = { .handle = s->bufs[b].handle }; 714 if (drmIoctl(s->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req) < 0) { 715 fprintf(stderr, "[drm-secondary] Map %d failed\n", b); 716 drmModeRmFB(s->fd, s->bufs[b].fb_id); 717 struct drm_mode_destroy_dumb destroy = { .handle = s->bufs[b].handle }; 718 drmIoctl(s->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); 719 free(s); return NULL; 720 } 721 s->bufs[b].map = mmap(0, s->bufs[b].size, PROT_READ | PROT_WRITE, 722 MAP_SHARED, s->fd, map_req.offset); 723 if (s->bufs[b].map == MAP_FAILED) { 724 fprintf(stderr, "[drm-secondary] mmap %d failed\n", b); 725 drmModeRmFB(s->fd, s->bufs[b].fb_id); 726 struct drm_mode_destroy_dumb destroy = { .handle = s->bufs[b].handle }; 727 drmIoctl(s->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); 728 free(s); return NULL; 729 } 730 // Clear to black 731 memset(s->bufs[b].map, 0, s->bufs[b].size); 732 } 733 734 s->buf_front = 0; 735 if (drmModeSetCrtc(s->fd, s->crtc_id, s->bufs[0].fb_id, 0, 0, 736 &s->connector_id, 1, &s->mode) < 0) { 737 fprintf(stderr, "[drm-secondary] SetCrtc failed\n"); 738 for (int b = 0; b < 2; b++) { 739 munmap(s->bufs[b].map, s->bufs[b].size); 740 drmModeRmFB(s->fd, s->bufs[b].fb_id); 741 struct drm_mode_destroy_dumb destroy = { .handle = s->bufs[b].handle }; 742 drmIoctl(s->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); 743 } 744 free(s); return NULL; 745 } 746 747 // Small internal render target (1/8 native res for fast CPU rendering) 748 int sw = (s->width + 7) / 8; 749 int sh = (s->height + 7) / 8; 750 s->small_fb = fb_create(sw, sh); 751 if (!s->small_fb) { 752 fprintf(stderr, "[drm-secondary] fb_create small failed\n"); 753 // non-fatal — will fall back to full-res if NULL 754 } 755 756 s->active = 1; 757 fprintf(stderr, "[drm-secondary] HDMI output active %dx%d (small %dx%d)\n", 758 s->width, s->height, sw, sh); 759 return s; 760} 761 762int drm_secondary_is_connected(ACDisplay *primary) { 763 if (!primary || primary->is_fbdev || primary->fd < 0) return 0; 764 drmModeRes *res = drmModeGetResources(primary->fd); 765 if (!res) return 0; 766 int found = 0; 767 for (int i = 0; i < res->count_connectors && !found; i++) { 768 drmModeConnector *c = drmModeGetConnector(primary->fd, res->connectors[i]); 769 if (!c) continue; 770 if (c->connector_id != primary->connector_id && 771 c->connection == DRM_MODE_CONNECTED && c->count_modes > 0 && 772 (c->connector_type == DRM_MODE_CONNECTOR_HDMIA || 773 c->connector_type == DRM_MODE_CONNECTOR_HDMIB || 774 c->connector_type == DRM_MODE_CONNECTOR_DisplayPort)) 775 found = 1; 776 drmModeFreeConnector(c); 777 } 778 drmModeFreeResources(res); 779 return found; 780} 781 782void drm_secondary_present_waveform(ACSecondaryDisplay *s, ACGraph *g, 783 float *waveform, int wf_size, int wf_pos) { 784 if (!s || !s->active || !g) return; 785 786 // Use small_fb as the render target (1/8 native res — fast) 787 ACFramebuffer *render_fb = s->small_fb; 788 if (!render_fb) return; // nothing to render into 789 790 int rw = render_fb->width; 791 int rh = render_fb->height; 792 793 // Save graph's original target and switch to small buffer 794 ACFramebuffer *orig_target = g->fb; 795 graph_page(g, render_fb); 796 797 // Dark background 798 graph_wipe(g, (ACColor){8, 8, 16, 255}); 799 800 // Draw waveform — quantized bars, two-tone top/bottom 801 if (waveform) { 802 // Use 64 bars across the width for a clean spectrum look 803 int N = 64; 804 int bar_w = rw / N; 805 if (bar_w < 1) bar_w = 1; 806 807 for (int i = 0; i < N; i++) { 808 int idx = (wf_pos + wf_size - N + i) % wf_size; 809 float sample = waveform[idx]; 810 float amp = sample < 0 ? -sample : sample; // abs 811 int bar_h = (int)(amp * rh * 0.92f); 812 if (bar_h < 1) bar_h = 1; 813 if (bar_h > rh) bar_h = rh; 814 815 int x = i * bar_w; 816 int gap = bar_w > 2 ? 1 : 0; // 1px gap between bars if wide enough 817 818 // Top region (empty space above bar) — slightly lighter than bg 819 graph_ink(g, (ACColor){16, 24, 40, 255}); 820 graph_box(g, x, 0, bar_w - gap, rh - bar_h, 1); 821 822 // Bottom bar fill — bright blue/cyan 823 graph_ink(g, (ACColor){60, 160, 240, 255}); 824 graph_box(g, x, rh - bar_h, bar_w - gap, bar_h, 1); 825 826 // Top 2px of each bar: bright accent 827 graph_ink(g, (ACColor){160, 220, 255, 255}); 828 graph_box(g, x, rh - bar_h, bar_w - gap, 2, 1); 829 } 830 } 831 832 // Resolution text (shows native res) at small scale 833 char res_str[32]; 834 snprintf(res_str, sizeof(res_str), "%dx%d", s->width, s->height); 835 graph_ink(g, (ACColor){160, 160, 180, 255}); 836 font_draw(g, res_str, 2, 2, 1); 837 838 // Restore original render target 839 graph_page(g, orig_target); 840 841 // Scale small_fb up to HDMI back buffer 842 int back = 1 - s->buf_front; 843 int dst_stride = (int)(s->bufs[back].pitch / sizeof(uint32_t)); 844 fb_copy_scaled(render_fb, s->bufs[back].map, s->width, s->height, dst_stride, 8); 845 846 // Async page flip 847 int ret = drmModePageFlip(s->fd, s->crtc_id, s->bufs[back].fb_id, 848 DRM_MODE_PAGE_FLIP_ASYNC, NULL); 849 if (ret != 0) { 850 drmModeSetCrtc(s->fd, s->crtc_id, s->bufs[back].fb_id, 0, 0, 851 &s->connector_id, 1, &s->mode); 852 } 853 s->buf_front = back; 854} 855 856void drm_secondary_fill(ACSecondaryDisplay *s, uint8_t r, uint8_t g, uint8_t b) { 857 if (!s || !s->active || !s->bufs[0].map) return; 858 uint32_t pixel = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; 859 int back = 1 - s->buf_front; 860 uint32_t *p = s->bufs[back].map; 861 int total = s->width * s->height; 862 for (int i = 0; i < total; i++) p[i] = pixel; 863 drmModeSetCrtc(s->fd, s->crtc_id, s->bufs[back].fb_id, 0, 0, 864 &s->connector_id, 1, &s->mode); 865 s->buf_front = back; 866} 867 868void drm_secondary_destroy(ACSecondaryDisplay *s) { 869 if (!s) return; 870 if (s->saved_crtc) { 871 drmModeSetCrtc(s->fd, s->saved_crtc->crtc_id, s->saved_crtc->buffer_id, 872 s->saved_crtc->x, s->saved_crtc->y, 873 &s->connector_id, 1, &s->saved_crtc->mode); 874 drmModeFreeCrtc(s->saved_crtc); 875 } 876 if (s->small_fb) fb_destroy(s->small_fb); 877 for (int b = 0; b < 2; b++) { 878 if (s->bufs[b].map && s->bufs[b].map != MAP_FAILED) 879 munmap(s->bufs[b].map, s->bufs[b].size); 880 if (s->bufs[b].fb_id) drmModeRmFB(s->fd, s->bufs[b].fb_id); 881 if (s->bufs[b].handle) { 882 struct drm_mode_destroy_dumb destroy = { .handle = s->bufs[b].handle }; 883 drmIoctl(s->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); 884 } 885 } 886 free(s); 887} 888 889const char *drm_display_driver(ACDisplay *d) { 890 if (!d) return "none"; 891 if (d->is_sdl) { 892 static char buf[48]; 893 snprintf(buf, sizeof(buf), "sdl3:%s", d->sdl_renderer_name); 894 return buf; 895 } 896 if (d->is_fbdev) return "fbdev"; 897 return "drm"; 898} 899 900void drm_destroy(ACDisplay *d) { 901 if (!d) return; 902 903 if (d->is_sdl) { 904 if (d->sdl_texture && sdl.DestroyTexture) sdl.DestroyTexture(d->sdl_texture); 905 if (d->sdl_renderer && sdl.DestroyRenderer) sdl.DestroyRenderer(d->sdl_renderer); 906 if (d->sdl_window && sdl.DestroyWindow) sdl.DestroyWindow(d->sdl_window); 907 if (sdl.Quit) sdl.Quit(); 908 free(d); 909 return; 910 } 911 912 if (d->is_fbdev) { 913 // Restore console text mode 914 int tty_fd = open("/dev/tty0", O_RDWR); 915 if (tty_fd >= 0) { 916 ioctl(tty_fd, 0x4B3A, 0); // KD_TEXT 917 close(tty_fd); 918 } 919 if (d->fbdev_map && d->fbdev_map != MAP_FAILED) 920 munmap(d->fbdev_map, d->fbdev_size); 921 if (d->buffers[0].map) 922 free(d->buffers[0].map); 923 if (d->fd >= 0) close(d->fd); 924 free(d); 925 return; 926 } 927 928 if (d->saved_crtc) { 929 drmModeSetCrtc(d->fd, d->saved_crtc->crtc_id, d->saved_crtc->buffer_id, 930 d->saved_crtc->x, d->saved_crtc->y, 931 &d->connector_id, 1, &d->saved_crtc->mode); 932 drmModeFreeCrtc(d->saved_crtc); 933 } 934 935 for (int i = 0; i < 2; i++) { 936 if (d->buffers[i].map && d->buffers[i].map != MAP_FAILED) 937 munmap(d->buffers[i].map, d->buffers[i].size); 938 if (d->buffers[i].fb_id) 939 drmModeRmFB(d->fd, d->buffers[i].fb_id); 940 if (d->buffers[i].handle) { 941 struct drm_mode_destroy_dumb destroy = { .handle = d->buffers[i].handle }; 942 drmIoctl(d->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); 943 } 944 } 945 946 if (d->fd >= 0) close(d->fd); 947 free(d); 948} 949 950// Release DRM master so another process (cage) can take the display 951int drm_release_master(void *display) { 952 ACDisplay *d = (ACDisplay *)display; 953 if (!d || d->is_fbdev || d->fd < 0) return -1; 954 int rc = drmDropMaster(d->fd); 955 // Also close the fd so cage can open /dev/dri/card0 exclusively 956 close(d->fd); 957 d->fd = -1; 958 return rc; 959} 960 961// Reclaim DRM master after browser exits — reopen the device 962int drm_acquire_master(void *display) { 963 ACDisplay *d = (ACDisplay *)display; 964 if (!d || d->is_fbdev) return -1; 965 if (d->fd < 0) { 966 // Reopen the DRM device 967 const char *paths[] = {"/dev/dri/card0", "/dev/dri/card1", NULL}; 968 for (int i = 0; paths[i]; i++) { 969 d->fd = open(paths[i], O_RDWR | O_CLOEXEC); 970 if (d->fd >= 0) break; 971 } 972 if (d->fd < 0) return -1; 973 } 974 return drmSetMaster(d->fd); 975}