Monorepo for Aesthetic.Computer
aesthetic.computer
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}