/* * meow_ui_gfx.c — meow_ui 渲染后端(B1.0+B1.1)。 * * 独立实现:自建 fb 上下文(自 mmap /dev/fb0、自建 back_buf、自实现脏区+present), * 不复用 launcher 的全局 g_dirty/fb(launcher 与 app 是不同进程)。机制参考 * MEOW_APP_SDK.md §1/§2 与 launcher 的 M2.16 无闪烁地基,代码全部独立书写。 * * 抽象:所有绘制目标 = 离屏 back_buf(g_fb);meow_ui_flush() 时单次原子 * vsync+blit 到 fb0(真实设备),或写出 PPM 截图(宿主/qemu 离屏,MEOW_UI_DUMP)。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "meow_ui_sys.h" #include "sb_icons.h" #include "../meow_launcherd/meow_font.h" #include #include #include #include #include #include #include #include #include /* 本设备实测 ioctl 号(勿与通用头交换) */ #define FBIOGET_VSCREENINFO 0x4600 #define FBIOGET_FSCREENINFO 0x4602 #define FBIO_WAITFORVSYNC 0x4620 #define FB_DEV "/dev/fb0" #define MEOW_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.png" #define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.idx" #define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png" #define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx" /* 默认字库(卡 MeowOS/fonts/default/regular,M2.6 字重结构)与固件内置之外,launcher 会把 * 「当前生效字体」经环境变量下发给 app(见 meow_launcherd/app_launcher.c `launch_app` / font_theme.c `meow_font_effective_paths`): * MEOW_FONT_PNG / MEOW_FONT_IDX = 绝对路径;MEOW_FONT_APP = 或 "default"。 * SDK load_font 优先读 env(匹配系统 font.file),无则回退卡默认 → 固件。 */ #define MUI_DEFAULT_W 480 #define MUI_DEFAULT_H 720 /* §10.5 叠加让位标记:与 launcher meow_launcher_internal.h NOTIF_ACTIVE 同路径。 * 通知中心叠加会话期存在;宿主/单测可经 MEOW_NOTIF_ACTIVE_FILE 覆写路径(进程内只解析一次)。 */ #define MEOW_NOTIF_ACTIVE_DEFAULT "/tmp/meow_notif_active" #define MEOW_OVERLAY_CHK_MS 200 /* 存在性检查节流:别每帧 stat */ /* 脏矩形上限(超出合并为全屏) */ #define MUI_DIRTY_MAX 12 struct mu_rect { int x, y, w, h; }; /* ---- fb 上下文 ---- */ static unsigned char *g_back_buf = NULL; /* 离屏(所有绘制目标) */ static unsigned char *g_fb_mem = NULL; /* 真实 mmap 帧缓冲(present 落点;NULL=离屏) */ static unsigned char *g_fb = NULL; /* 绘制指针 = g_back_buf */ static int g_fb_fd = -1; static int g_xres = MUI_DEFAULT_W, g_yres = MUI_DEFAULT_H; static int g_bpp = 16; static int g_line = 0; /* 每行字节数 */ static int g_init_ok = 0; static int g_mfont_ready = 0; static int g_dirty[/*MUI_DIRTY_MAX*/12][4]; /* [x,y,w,h] */ static int g_ndirty = 0; static int g_dirty_full = 1; /* ---- 绘制期裁剪(clip rect,§3;默认全屏)---- * 栈式嵌套:push 与当前 clip 求交后压栈,pop 恢复。写像素原语(put_px / put_px_blend / * fill_rect / fill_rect_alpha / round / circle / ring / blit_icon / blit_rgba / text) * 统一过 g_clip;顶层(g_clip_n==0)= 全屏(g_xres×g_yres),故 IME/模态的全屏 scrim * 直绘不受裁剪。 */ #define MUI_CLIP_MAX 16 static int g_clip_stack[MUI_CLIP_MAX][4]; /* [x0,y0,x1,y1](闭开区间) */ static int g_clip_n = 0; static void clip_cur(int out[4]) { /* 当前 clip 边界(无 clip = 全屏) */ if (g_clip_n > 0) { out[0] = g_clip_stack[g_clip_n - 1][0]; out[1] = g_clip_stack[g_clip_n - 1][1]; out[2] = g_clip_stack[g_clip_n - 1][2]; out[3] = g_clip_stack[g_clip_n - 1][3]; } else { out[0] = 0; out[1] = 0; out[2] = g_xres; out[3] = g_yres; } } /* 逐像素 clip 判定:须 inline —— MIPS -Os 若抽成函数调用,每一像素(ring/circle/圆角/ * alpha/blit/5x7)都付一次 call+prologue,软渲上被显著放大(宿主 x86 -Os 会内联故测不出)。 * 强制内联后为 4 次整数比较,与旧「越界检查」同价;顶层(g_clip_n==0)= 全屏零开销直通。 */ static inline int clip_contains(int x, int y) { if (g_clip_n == 0) return x >= 0 && x < g_xres && y >= 0 && y < g_yres; return x >= g_clip_stack[g_clip_n - 1][0] && x < g_clip_stack[g_clip_n - 1][2] && y >= g_clip_stack[g_clip_n - 1][1] && y < g_clip_stack[g_clip_n - 1][3]; } void meow_gfx_clip_push(int x, int y, int w, int h) { if (!g_init_ok || w <= 0 || h <= 0) return; int c[4]; clip_cur(c); int ix0 = x > c[0] ? x : c[0]; int iy0 = y > c[1] ? y : c[1]; int ix1 = (x + w) < c[2] ? (x + w) : c[2]; int iy1 = (y + h) < c[3] ? (y + h) : c[3]; if (ix1 < ix0) ix1 = ix0; if (iy1 < iy0) iy1 = iy0; if (g_clip_n >= MUI_CLIP_MAX) return; /* 栈满:忽略本次 push(保持当前 clip) */ g_clip_stack[g_clip_n][0] = ix0; g_clip_stack[g_clip_n][1] = iy0; g_clip_stack[g_clip_n][2] = ix1; g_clip_stack[g_clip_n][3] = iy1; g_clip_n++; } void meow_gfx_clip_pop(void) { if (g_clip_n > 0) g_clip_n--; } static char *g_dump_path = NULL; /* ---- 状态栏「上收出屏只留提示线」(collapsed,基座)---- * g_sbar_collapsed = 目标态(1=上收);g_sbar_collapse_h = 当前实际上收偏移 * (0..MUI_SBAR_H:0=完全展开,MUI_SBAR_H=内容全部上移出屏、仅留提示线)。 * 基座让 meow_ui_statusbar_set_collapsed(on) 立即生效(true→只画提示线/内容上移); * 过渡(每帧插值)留待后续代理或经 meow_ui_statusbar_animate(ms) 启用的简易线性逼近。 * 「收合=只留提示线」占高 = MUI_SBAR_COLLAPSE_H(=MEOW_SBAR_HOTZONE_FS=4,meow_ui_sys.h), * 与 inset(meow_ui_statusbar_height())/顶部下滑热区/收合渲染同口径——replaced MUI_SBAR_HINT_H。 */ static int g_sbar_collapsed = 0; /* 目标:1=上收 */ static int g_sbar_collapse_h = 0; /* 当前上收偏移(0..MUI_SBAR_H) */ static int g_sbar_collapse_anim_ms = 0; /* 过渡时长(0=瞬切);meow_ui_statusbar_animate 设置 */ static int g_sbar_collapse_from_h = 0; /* 本次过渡起点偏移 */ static unsigned long long g_sbar_collapse_start_ms = 0; /* 本次过渡起始(monotonic ms) */ /* ---- 字体档位(MEOW_FONT_TIER,基座存档 + 公共 get/set)---- * launcher 按 app.json "fonts":<档> 注入 MEOW_FONT_TIER=<0|1|2>;SDK 读入并以 * meow_font_set_tier/tier 暴露。按档装载大字库(GBK/Full)由 meow_font_reload_tier() * 在 meow_font.c 实现;meow_font_set_tier() 在档位变化时经 env/settings 路由到该钩子真重载。 */ static int g_font_tier = MEOW_FONT_TIER_COMMON; /* ---- 帧率调度(A-1/A-2:从 launcher M2.16 迁入,刷新率调度归口 SDK)---- * meow_ui_flush()/present() 的 fps 档位与自适应降档 governor。launcher 不再自带 * 一套帧率门控;launcher 与 app 统一走这里 → 全局观感/刷新率一致。 * 设置接口:meow_ui_set_refresh_rate()/meow_ui_set_refresh_always_max()。 */ static int g_refresh_rate_cfg = 60; /* 配置目标档 60/30/15(上限) */ static int g_eff_rate = 60; /* 当前有效帧率(governor 可降档) */ static int g_refresh_always_max = 0; /* 1=恒 max 不降档 */ static unsigned long long g_last_present_ms = 0; static unsigned long long g_frames_presented = 0; static unsigned long long g_present_cost_sum = 0; static int g_present_cost_n = 0; static int g_gov_low = 0; /* 每档上限(refresh_always_max=1 时恒 max;0 时 governor 可在 [15,30,60] 内降/升档)。 */ static int rate_bump(int r) { return (r == 15) ? 30 : (r == 30) ? 60 : 60; } static int rate_drop(int r) { return (r == 60) ? 30 : (r == 30) ? 15 : 15; } /* 自适应降档 governor:统计每帧 present 耗时;连续 12 帧平均超预算 → 降一档; * 连续 4 批(≈48 帧)平均远低于预算 → 升一档。refresh_always_max=1 时恒 max 不降。 */ static void refresh_governor(unsigned long long cost_ms, int period) { if (g_refresh_always_max || g_eff_rate <= 0) return; g_present_cost_sum += cost_ms; g_present_cost_n++; if (g_present_cost_n < 12) return; unsigned long long avg = g_present_cost_sum / (unsigned int)g_present_cost_n; if (avg > (unsigned long long)period) { if (g_eff_rate > 15) g_eff_rate = rate_drop(g_eff_rate); g_gov_low = 0; } else if (avg * 2 < (unsigned long long)period) { if (++g_gov_low >= 4) { int nr = rate_bump(g_eff_rate); if (nr <= g_refresh_rate_cfg) g_eff_rate = nr; g_gov_low = 0; } } else g_gov_low = 0; g_present_cost_sum = 0; g_present_cost_n = 0; } /* ---- 工具 ---- */ static uint64_t now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; } uint64_t meow_ui_time_ms(void) { return now_ms(); } static uint16_t rgb565(uint32_t c) { return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)); } static uint8_t ex5(uint16_t v) { return (uint8_t)(((v) << 3) | ((v) >> 2)); } /* 5→8 */ static uint8_t ex6(uint16_t v) { return (uint8_t)(((v) << 2) | ((v) >> 4)); } /* 6→8 */ static int rect_hit(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by; } /* ================= 脏区追踪 ================= */ static void dirty_full(void) { g_dirty_full = 1; g_ndirty = 0; } static void dirty_reset(void) { g_ndirty = 0; g_dirty_full = 0; } static void dirty_add(int x, int y, int w, int h) { if (g_dirty_full) return; if (w <= 0 || h <= 0) return; if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x >= g_xres || y >= g_yres) return; if (x + w > g_xres) w = g_xres - x; if (y + h > g_yres) h = g_yres - y; if (w <= 0 || h <= 0) return; /* 绘制期裁剪:脏区也不越 clip(顶层 clip=全屏 → 无操作)。 */ if (g_clip_n > 0) { int *c = g_clip_stack[g_clip_n - 1]; int x1 = x + w, y1 = y + h; if (x < c[0]) x = c[0]; if (y < c[1]) y = c[1]; if (x1 > c[2]) x1 = c[2]; if (y1 > c[3]) y1 = c[3]; if (x1 <= x || y1 <= y) return; w = x1 - x; h = y1 - y; } for (int i = 0; i < g_ndirty; i++) { if (rect_hit(g_dirty[i][0], g_dirty[i][1], g_dirty[i][2], g_dirty[i][3], x, y, w, h)) { int nx1 = g_dirty[i][0] < x ? g_dirty[i][0] : x; int ny1 = g_dirty[i][1] < y ? g_dirty[i][1] : y; int nx2 = (g_dirty[i][0] + g_dirty[i][2]) > (x + w) ? (g_dirty[i][0] + g_dirty[i][2]) : (x + w); int ny2 = (g_dirty[i][1] + g_dirty[i][3]) > (y + h) ? (g_dirty[i][1] + g_dirty[i][3]) : (y + h); g_dirty[i][0] = nx1; g_dirty[i][1] = ny1; g_dirty[i][2] = nx2 - nx1; g_dirty[i][3] = ny2 - ny1; return; } } if (g_ndirty >= MUI_DIRTY_MAX) { dirty_full(); return; } g_dirty[g_ndirty][0] = x; g_dirty[g_ndirty][1] = y; g_dirty[g_ndirty][2] = w; g_dirty[g_ndirty][3] = h; g_ndirty++; } void meow_gfx_mark_dirty(int x, int y, int w, int h) { dirty_add(x, y, w, h); } void meow_gfx_mark_full(void) { dirty_full(); } void meow_gfx_dirty_reset(void) { dirty_reset(); } /* flush 挂起(IME/模态阻塞循环用):挂起期间 flush/flush_region 只记脏不上屏, * 解除后由调用方一次 flush 原子上屏——避免「App on_tick 重绘先上屏、覆盖层随后再上屏」 * 同一区域一帧两刷的频闪。 */ static int g_flush_hold = 0; void meow_gfx_flush_hold(int on) { g_flush_hold = on ? 1 : 0; } /* back_buf 每行字节数 / 位深(M2.71-bug3:模态/IME 整帧快照恢复用)。 */ int meow_ui_back_buf_pitch(void) { return g_line; } int meow_ui_back_buf_bpp(void) { return g_bpp; } /* launcher(系统组件)与 SDK 共用同一 back_buf:launcher 的 fb 绘制指针指向它, * 从而 launcher 与 app 统一渲染栈(同一离屏 + 同一 present)。 */ unsigned char *meow_ui_back_buf(void) { return g_back_buf; } /* ================= MD3 图标 blit(写进本 SDK back_buf) ================= * 复用 sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)——状态栏右侧图标簇 * (meow_ui_sys_statusbar_render)与 TopAppBar 位图动作图标(W_TOPAPPBAR)共用同源位图。 * idx = sb_icons[] 索引(状态栏 SB_I_*、动作 MEOW_ICON_*,见 meow_ui_internal.h) * x = 图标槽左缘(槽宽 SB_ICON_SIZE=32) * ink_h = 目标 ink 高(>0 等比缩放;0 = 保持位图自然 ink 尺寸——WiFi 强弱档用) * fg = 24-bit RGB 前景(主题 on_surface / on_surface_variant,日/夜由主题表提供) * cy = ink 垂直中心线(状态栏=28px 栏约 14;TopAppBar=栏中点) * 先求位图 ink 边界(阈值 32,忽略近透明抗锯齿),按 ink 在槽内水平居中、垂直中心对齐 cy; * 像素按 alpha 软边混合进 back_buf(自 g_fb/g_line/g_xres/g_yres,与 launcher sb_blit_icon 同源算法)。 */ /* 像素核心:只采样混合进 back_buf,不标脏区。供状态栏路径(blit 后另标顶栏脏区)与 * 控件路径(meow_gfx_blit_icon_ink,控件 invalidation 独立管理脏区)共用,避免状态栏的 * 顶栏脏区假设污染控件绘制(topbar 不在屏幕最顶行时标错区域)。 */ static void icon_blit_px(int idx, int x, int ink_h, uint32_t fg, int cy) { const unsigned char *src = sb_icons[idx].data; uint16_t col = rgb565(fg); /* 1) ink 边界(含空图兜底) */ int minx = SB_ICON_SIZE, miny = SB_ICON_SIZE, maxx = -1, maxy = -1; for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) { if (src[i] >= 32) { int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE; if (xx < minx) minx = xx; if (xx > maxx) maxx = xx; if (yy < miny) miny = yy; if (yy > maxy) maxy = yy; } } if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; } int ink_w = maxx - minx + 1, ink_h_n = maxy - miny + 1; int t_h = (ink_h > 0) ? ink_h : ink_h_n; int t_w = ink_w * t_h / ink_h_n; if (t_w < 1) t_w = 1; if (t_w > SB_ICON_SIZE * 2) t_w = SB_ICON_SIZE * 2; /* 防御:等比极端上限 */ /* 2) 目标框左上:水平居中于 32px 槽;垂直中心 = cy */ int x0 = x + (SB_ICON_SIZE - t_w) / 2; int y0 = cy - t_h / 2; /* 3) 最近邻采样映射:目标 (dx,dy) ← 源 ink */ for (int dy = 0; dy < t_h; dy++) { int ty = y0 + dy; if (ty < 0 || ty >= g_yres) continue; int sy = miny + (dy * ink_h_n) / t_h; uint16_t *row = (uint16_t *)(g_fb + (size_t)ty * g_line); for (int dx = 0; dx < t_w; dx++) { int tx = x0 + dx; if (tx < 0 || tx >= g_xres) continue; if (!clip_contains(tx, ty)) continue; int sx = minx + (dx * ink_w) / t_w; int a = src[sy * SB_ICON_SIZE + sx]; if (a < 8) continue; /* 透明像素跳过 */ uint16_t bg = row[tx]; if (a >= 240) { row[tx] = col; continue; } int a4 = a >> 4; /* 0..15 */ int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4; int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4; int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4; row[tx] = (uint16_t)((r << 11) | (g << 5) | b); } } } /* 状态栏路径:blit 像素 + 把整条顶栏标脏(present 只重画顶条即可)。 */ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) { (void)y; if (!g_init_ok || !g_fb) return; if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return; icon_blit_px(idx, x, ink_h, fg, cy); dirty_add(x, 0, SB_ICON_SIZE, MUI_SBAR_H); } /* 控件路径(TopAppBar 动作图标):只写像素,不标「顶栏」脏区——控件矩形由 * meow_widget_draw 的 clip + invalidation(setter 标脏)独立管理,避免把状态栏的 * 顶栏脏区假设带进控件绘制(topbar 不位于屏幕最顶行时标错区域、触发误重画)。 */ void meow_gfx_blit_icon_ink(int idx, int x, int ink_h, uint32_t fg, int cy) { if (!g_init_ok || !g_fb) return; if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return; icon_blit_px(idx, x, ink_h, fg, cy); } /* ================= 像素写入 ================= */ static void px_write_rgb565(int x, int y, uint16_t c16) { *(uint16_t *)(g_fb + (size_t)y * g_line + (size_t)x * 2) = c16; } static uint16_t px_read_rgb565(int x, int y) { return *(uint16_t *)(g_fb + (size_t)y * g_line + (size_t)x * 2); } static void put_px(int x, int y, uint32_t color) { if (!clip_contains(x, y)) return; if (g_bpp == 16) { px_write_rgb565(x, y, rgb565(color)); } else { unsigned char *p = g_fb + (size_t)y * g_line + (size_t)x * 4; *(uint32_t *)p = (color << 8) | 0xFF; } } static void put_px_blend(int x, int y, uint32_t color, int alpha) { if (!clip_contains(x, y)) return; uint16_t fg = rgb565(color); if (g_bpp == 16) { uint16_t bg = px_read_rgb565(x, y); int ia = 255 - alpha; unsigned r = ((((fg >> 11) & 0x1F) * alpha + ((bg >> 11) & 0x1F) * ia) / 255); unsigned gg = ((((fg >> 5) & 0x3F) * alpha + ((bg >> 5) & 0x3F) * ia) / 255); unsigned b = (((fg & 0x1F) * alpha + (bg & 0x1F) * ia) / 255); px_write_rgb565(x, y, (uint16_t)((r << 11) | (gg << 5) | b)); } else { unsigned char *p = g_fb + (size_t)y * g_line + (size_t)x * 4; uint32_t bg = (*(uint32_t *)p) >> 8; int ia = 255 - alpha; unsigned r = ((((color >> 16) & 0xFF) * alpha + ((bg >> 16) & 0xFF) * ia) / 255); unsigned gg = ((((color >> 8) & 0xFF) * alpha + ((bg >> 8) & 0xFF) * ia) / 255); unsigned b = (((color & 0xFF) * alpha + (bg & 0xFF) * ia) / 255); *(uint32_t *)p = (r << 24) | (gg << 16) | (b << 8) | 0xFF; } } /* ================= 基本绘制 ================= */ void meow_ui_clear(uint32_t color) { if (!g_init_ok) return; if (g_clip_n > 0) { /* 绘制期裁剪:清当前 clip 区(顶层 clip=全屏 → 走快速整刷) */ meow_ui_fill_rect(0, 0, g_xres, g_yres, color); return; } /* 快速整刷(16bit 用半字填充,可 memset 当色值由 32bit 拼接掩码) */ if (g_bpp == 16) { uint16_t c16 = rgb565(color); uint32_t c32 = (uint32_t)c16 | ((uint32_t)c16 << 16); for (int y = 0; y < g_yres; y++) { uint32_t *row = (uint32_t *)(g_fb + (size_t)y * g_line); int n = g_xres / 2; for (int i = 0; i < n; i++) row[i] = c32; if (g_xres & 1) px_write_rgb565(g_xres - 1, y, c16); } } else { for (int y = 0; y < g_yres; y++) for (int x = 0; x < g_xres; x++) put_px(x, y, color); } dirty_full(); } void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color) { if (!g_init_ok) return; if (w <= 0 || h <= 0) return; if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x >= g_xres || y >= g_yres) return; if (x + w > g_xres) w = g_xres - x; if (y + h > g_yres) h = g_yres - y; if (w <= 0 || h <= 0) return; if (g_clip_n > 0) { /* 绘制期裁剪:与当前 clip 求交(顶层 clip=全屏 → 无操作) */ int *c = g_clip_stack[g_clip_n - 1]; int x1 = x + w, y1 = y + h; if (x < c[0]) x = c[0]; if (y < c[1]) y = c[1]; if (x1 > c[2]) x1 = c[2]; if (y1 > c[3]) y1 = c[3]; if (x1 <= x || y1 <= y) return; w = x1 - x; h = y1 - y; } if (g_bpp == 16) { uint16_t c16 = rgb565(color); for (int ry = y; ry < y + h; ry++) { uint16_t *rp = (uint16_t *)(g_fb + (size_t)ry * g_line + (size_t)x * 2); for (int rx = 0; rx < w; rx++) rp[rx] = c16; } } else { for (int ry = y; ry < y + h; ry++) for (int rx = x; rx < x + w; rx++) put_px(rx, ry, color); } dirty_add(x, y, w, h); } void meow_ui_put_px(int x, int y, uint32_t color) { if (!g_init_ok) return; put_px(x, y, color); dirty_add(x, y, 1, 1); } void meow_ui_draw_rect(int x, int y, int w, int h, uint32_t color) { if (w <= 0 || h <= 0) return; meow_ui_fill_rect(x, y, w, 1, color); meow_ui_fill_rect(x, y + h - 1, w, 1, color); meow_ui_fill_rect(x, y, 1, h, color); meow_ui_fill_rect(x + w - 1, y, 1, h, color); } /* 圆角矩形区域判定(含内缩) */ static int rr_inside(int px, int py, int x, int y, int w, int h, int r) { if (px < x || px >= x + w || py < y || py >= y + h) return 0; if (r <= 0) return 1; if (w < 2 * r || h < 2 * r) return 1; /* 半径过大退化为矩形 */ int cx1 = x + r, cy1 = y + r; int cx2 = x + w - 1 - r, cy2 = y + h - 1 - r; if (px < cx1 && py < cy1) return ((px - cx1) * (px - cx1) + (py - cy1) * (py - cy1)) <= r * r; if (px > cx2 && py < cy1) return ((px - cx2) * (px - cx2) + (py - cy1) * (py - cy1)) <= r * r; if (px < cx1 && py > cy2) return ((px - cx1) * (px - cx1) + (py - cy2) * (py - cy2)) <= r * r; if (px > cx2 && py > cy2) return ((px - cx2) * (px - cx2) + (py - cy2) * (py - cy2)) <= r * r; return 1; } static int rr_border(int px, int py, int x, int y, int w, int h, int r) { if (!rr_inside(px, py, x, y, w, h, r)) return 0; int ir = r - 1; if (ir < 0) ir = 0; if (w > 2 && h > 2 && rr_inside(px, py, x + 1, y + 1, w - 2, h - 2, ir)) return 0; return 1; } void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color) { if (!g_init_ok) return; int x1 = x, y1 = y, x2 = x + w, y2 = y + h; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; for (int py = y1; py < y2; py++) for (int px = x1; px < x2; px++) if (rr_inside(px, py, x, y, w, h, r)) put_px(px, py, color); dirty_add(x1, y1, x2 - x1, y2 - y1); } void meow_ui_draw_rect_round(int x, int y, int w, int h, int r, uint32_t color) { if (!g_init_ok) return; int x1 = x, y1 = y, x2 = x + w, y2 = y + h; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; for (int py = y1; py < y2; py++) for (int px = x1; px < x2; px++) if (rr_border(px, py, x, y, w, h, r)) put_px(px, py, color); dirty_add(x1, y1, x2 - x1, y2 - y1); } /* 圆角矩形半透明填充(M2.71-bug4):按圆角裁剪后逐像素混合 α。供「禁用变暗」等状态层用, * 避免在圆角外的方形区域也叠色(旧 fill_rect_alpha 会在圆角外露出灰方块)。 */ void meow_gfx_fill_round_alpha(int x, int y, int w, int h, int r, uint32_t color, int alpha) { if (!g_init_ok) return; if (alpha <= 0) return; if (alpha >= 255) { meow_gfx_fill_round(x, y, w, h, r, color); return; } if (w <= 0 || h <= 0) return; int x1 = x, y1 = y, x2 = x + w, y2 = y + h; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; for (int py = y1; py < y2; py++) for (int px = x1; px < x2; px++) if (rr_inside(px, py, x, y, w, h, r)) put_px_blend(px, py, color, alpha); dirty_add(x1, y1, x2 - x1, y2 - y1); } /* ================= §5.8.6 batch2:圆形/线段/圆弧环(软渲,无 -lm) ================= * 无 GPU 合成、构建只链 libz+pthread(无 -lm),故不用 math.h:COS 用象限对称查表、 * 圆用 bbox 距离测试、弧带用「沿弧采样成粗线(圆帽)」近似。 */ static const int m_cos90[91] = { 1024,1024,1023,1023,1022,1020,1018,1016, 1014,1011,1008,1005,1002,998,994,989, 984,979,974,968,962,956,949,943, 935,928,920,912,904,896,887,878, 868,859,849,839,828,818,807,796, 784,773,761,749,737,724,711,698, 685,672,658,644,630,616,602,587, 573,558,543,527,512,496,481,465, 449,433,416,400,384,367,350,333, 316,299,282,265,248,230,213,195, 178,160,143,125,107,89,71,54, 36,18,0, }; static int m_cos_deg(int deg) /* 任意角度(可负)→ cos(deg)*1024 */ { int a = deg % 360; if (a < 0) a += 360; if (a <= 90) return m_cos90[a]; if (a <= 180) return -m_cos90[180 - a]; if (a <= 270) return -m_cos90[a - 180]; return m_cos90[360 - a]; } static int m_sin_deg(int deg) { return m_cos_deg(90 - deg); } void meow_gfx_fill_circle(int cx, int cy, int r, uint32_t color) { if (!g_init_ok || r <= 0) return; int x1 = cx - r, y1 = cy - r, x2 = cx + r + 1, y2 = cy + r + 1; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; if (x2 <= x1 || y2 <= y1) return; int r2 = r * r; for (int py = y1; py < y2; py++) { int dy = py - cy; for (int px = x1; px < x2; px++) { int dx = px - cx; if (dx * dx + dy * dy <= r2) put_px(px, py, color); } } dirty_add(x1, y1, x2 - x1, y2 - y1); } void meow_gfx_line(int x0, int y0, int x1, int y1, int th, uint32_t color) { if (!g_init_ok) return; int dx = x1 - x0, dy = y1 - y0; int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; int steps = (adx > ady ? adx : ady) + 1; if (steps <= 1) { meow_gfx_fill_circle(x0, y0, th / 2 + 1, color); return; } int cap = th / 2; if (cap < 1) cap = 1; for (int i = 0; i <= steps; i++) { int x = x0 + dx * i / steps; int y = y0 + dy * i / steps; if (cap <= 1) put_px(x, y, color); else meow_gfx_fill_circle(x, y, cap, color); } } void meow_gfx_ring_arc(int cx, int cy, int r, int th, int start_deg, int sweep_deg, uint32_t color) { if (!g_init_ok || r <= 0) return; int sw = sweep_deg % 360; if (sw < 0) sw += 360; /* 整环(|sweep|>=360):bbox 距离带(无角度判定,最快)。注意 sweep=360 → sw=0, * 若靠 sw>=360 判整环会落下走弧分支只画一个点(顺带让 Radio 外圈/圆形进度轨道环渲染成点)。 */ if (sweep_deg >= 360 || sweep_deg <= -360) { int r1 = r - th / 2, r2 = r + th / 2; if (r1 < 0) r1 = 0; int x1 = cx - r2, y1 = cy - r2, x2 = cx + r2 + 1, y2 = cy + r2 + 1; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; if (x2 <= x1 || y2 <= y1) return; for (int py = y1; py < y2; py++) for (int px = x1; px < x2; px++) { int dx = px - cx, dy = py - cy; int d2 = dx * dx + dy * dy; if (d2 >= r1 * r1 && d2 <= r2 * r2) put_px(px, py, color); } dirty_add(x1, y1, x2 - x1, y2 - y1); return; } if (sw == 0) return; /* 零弧长:不画(sweep=0;整环已在上面处理) */ /* 弧带:沿弧采样相邻点、粗线连接(圆帽)近似。方向:deg 右(0)→上(90)→左(180)→下(270),顺时针。 */ int n = sw / 2 + 1; if (n < 2) n = 2; int px = cx + (m_cos_deg(start_deg) * r) / 1024; int py = cy - (m_sin_deg(start_deg) * r) / 1024; /* 屏幕 y 向下 → sin 取负 */ for (int i = 1; i <= n; i++) { int deg = start_deg + sw * i / n; int nx = cx + (m_cos_deg(deg) * r) / 1024; int ny = cy - (m_sin_deg(deg) * r) / 1024; meow_gfx_line(px, py, nx, ny, th, color); px = nx; py = ny; } } void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha) { if (!g_init_ok) return; if (alpha <= 0) return; if (alpha >= 255) { meow_ui_fill_rect(x, y, w, h, color); return; } if (w <= 0 || h <= 0) return; if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x >= g_xres || y >= g_yres) return; if (x + w > g_xres) w = g_xres - x; if (y + h > g_yres) h = g_yres - y; if (w <= 0 || h <= 0) return; if (g_clip_n > 0) { /* 绘制期裁剪:与当前 clip 求交(顶层 clip=全屏 → 无操作) */ int *c = g_clip_stack[g_clip_n - 1]; int x1 = x + w, y1 = y + h; if (x < c[0]) x = c[0]; if (y < c[1]) y = c[1]; if (x1 > c[2]) x1 = c[2]; if (y1 > c[3]) y1 = c[3]; if (x1 <= x || y1 <= y) return; w = x1 - x; h = y1 - y; } for (int ry = y; ry < y + h; ry++) for (int rx = x; rx < x + w; rx++) put_px_blend(rx, ry, color, alpha); dirty_add(x, y, w, h); } /* ================= 左滑返回视觉条(GESTURE_BACK §2.3) ================= */ #define MUI_HINT_H 5 static int g_hint_last = 0; /* 上次画过的条长(清条时只清到它,避免残留) */ /* 顶部条区「快照恢复」:画条前把 [0..宽)×[hy..hy+MUI_HINT_H) 拍进小缓冲区;回缩/清空时 * 回填快照还原「真实底色」而非 th->bg —— 全屏页面底=主题 accent,若填 th->bg(黑) 会在 * accent 底上留下黑色残条(直到下次区域刷新才消失)。快照只在首现条时拍一次(后续 grow * 不再覆盖快照),每行/全长布局全保留;缓冲为一次性分配(宽×H 行,480×5×2B≈4.8KB)。 */ static unsigned char *g_hint_snap = NULL; /* 快照(g_line×MUI_HINT_H 字节;NULL=未拍/分配失败) */ static int g_hint_snap_y = 0; /* 快照行的起始 y(本次手势时的 hy) */ /* 拍下「顶部条区」当前 back_buf 内容(只在 g_hint_last==0 的首现条时调用)。 */ static void hint_snapshot(void) { int hy = meow_ui_statusbar_height(); if (hy < 0) hy = 0; g_hint_snap_y = hy; if (hy + MUI_HINT_H > g_yres) return; /* 越界:放弃(回缩走 bg 兜底) */ size_t len = (size_t)MUI_HINT_H * (size_t)g_line; if (!g_hint_snap) { g_hint_snap = malloc(len); if (!g_hint_snap) return; } memcpy(g_hint_snap, g_fb + (size_t)hy * g_line, len); } /* 从快照回填列区间 [x0,x1) 的 MUI_HINT_H 行(还原条下真实底色)。x0/x1 已夹到屏宽。 */ static void hint_restore(int x0, int x1) { if (x1 <= x0) return; if (x0 < 0) x0 = 0; if (x1 > g_xres) x1 = g_xres; if (!g_hint_snap) { /* 无快照(分配失败):兜底填背景(旧行为) */ const meow_theme *th = meow_theme_get(); meow_ui_fill_rect(x0, g_hint_snap_y, x1 - x0, MUI_HINT_H, th->bg); return; } int bpp = g_bpp / 8; for (int r = 0; r < MUI_HINT_H; r++) { size_t row = (size_t)(g_hint_snap_y + r) * g_line; memcpy(g_fb + row + (size_t)x0 * bpp, g_hint_snap + (size_t)r * g_line + (size_t)x0 * bpp, (size_t)(x1 - x0) * bpp); } meow_gfx_mark_dirty(x0, g_hint_snap_y, x1 - x0, MUI_HINT_H); /* 标脏:调用方 flush_region 上屏 */ } void meow_ui_swipe_hint(int x) { if (!g_init_ok) return; const meow_theme *th = meow_theme_get(); /* 进度条画在状态栏下沿(z 在状态栏之下,不被全局状态栏覆盖)。 * 有状态栏(非全屏)→ y = 状态栏高度;收合(collapsed)→ y=提示线下缘 * (MUI_SBAR_COLLAPSE_H,与提示线不重叠);无状态栏(全屏/隐藏)→ y=0。 */ int hy = meow_ui_statusbar_height(); /* 单一实现(与非全屏 Hello 完全一致,全屏仅 hy 不同):grow 画 0..x、shrunk 先把 * 超出部分还原为快照、x<=0 整条还原。x<0 视为清空;条长上限=屏宽。条色仍取 th->fg。 */ if (x > 0) { if (x > g_xres) x = g_xres; if (g_hint_last == 0) hint_snapshot(); /* 首现条:拍下未污染区,供此后回填 */ /* 往回滑(新长度 < 上次)时,把「上次画到旧长度、本次超出的 [x,old)」还原为快照 * 真实底色(全屏 accent),而非填 th->bg —— 否则左滑回缩在 accent 底上漏黑。 */ if (g_hint_last > x) hint_restore(x, g_hint_last); meow_ui_fill_rect(0, hy, x, MUI_HINT_H, th->fg); g_hint_last = x; } else if (g_hint_last > 0) { /* 手指回原点/抬手未达标/抬手达标:整条还原为快照真实底色(全屏 accent 上无黑残) */ hint_restore(0, g_hint_last); g_hint_last = 0; } /* 不发 flush:由调用方 meow_ui_flush_region(0,0,宽,6) 只刷顶部条 */ } /* ================= 5x7 回退点阵字(与 launcher/hello 一致的表,独立携带) ================= */ static const unsigned char mfont5x7[][8] = { {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00},{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00}, {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00},{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00}, {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00},{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00}, {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00},{0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00}, {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00},{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00}, {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},{0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00}, {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00},{0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00},{0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00}, {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00},{0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00},{0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00}, {0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00},{0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00}, {0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00},{0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00}, {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},{0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00}, {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00},{0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00}, {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00},{0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00}, {0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},{0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00}, {0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00},{0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00}, {0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00},{0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, {0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00},{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, }; #define FONT5X7_N (sizeof(mfont5x7)/sizeof(mfont5x7[0])) static int glyph_index(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'Z') return 10 + (c - 'A'); if (c >= 'a' && c <= 'z') return 10 + (c - 'a'); if (c == ' ') return 36; if (c == '.') return 37; if (c == '!') return 38; if (c == '-' || c == '_') return 39; return 36; } static void draw_glyph5x7(int x, int y, char c, int scale, uint32_t color) { const unsigned char *g = mfont5x7[glyph_index(c)]; for (int row = 0; row < 7; row++) { unsigned char bits = g[row]; for (int col = 0; col < 5; col++) { if (bits & (0x10 >> col)) { for (int dy = 0; dy < scale; dy++) for (int dx = 0; dx < scale; dx++) put_px(x + col * scale + dx, y + row * scale + dy, color); } } } } static void draw_text5x7(int x, int y, const char *s, int scale, uint32_t color) { while (*s) { draw_glyph5x7(x, y, *s, scale, color); x += 6 * scale; s++; } } /* ================= 文本(字库优先,回退点阵) ================= */ static int fallback_scale(int size) { return (size >= 16) ? 2 : 1; } static void text_draw(int x, int y, const char *s, int size, uint32_t color) { if (!g_init_ok || !s) return; if (g_mfont_ready && g_bpp == 16) { int w = meow_font_width_text(s, size); int hh = meow_font_line_height(size); if (g_clip_n > 0) { int *c = g_clip_stack[g_clip_n - 1]; if (x + w <= c[0] || x >= c[2] || y + hh <= c[1] || y >= c[3]) return; /* 与 clip 无交 */ if (!(x >= c[0] && x + w <= c[2] && y >= c[1] && y + hh <= c[3])) { /* 部分相交:用「偏移 fb 子矩形 + 临时 fb 几何」做裁剪(meow_font 只按全屏剪)。 * 把 fb 基址指向 clip 左上角、fb_w/h 设为 clip 尺寸、原点平移到 clip 坐标, * 这样 meow_font 的逐字形裁剪天然落在 clip 内;画完恢复全屏几何。 */ uint16_t *fb = (uint16_t *)(g_fb + (size_t)c[1] * g_line + (size_t)c[0] * 2); meow_font_set_fb((unsigned)(c[2] - c[0]), (unsigned)(c[3] - c[1]), (unsigned)g_line); meow_font_drawtext(fb, x - c[0], y - c[1], s, rgb565(color), size); meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); dirty_add(x, y, w, hh); return; } } meow_font_drawtext((uint16_t *)g_fb, x, y, s, rgb565(color), size); dirty_add(x, y, w, hh); } else { int sc = fallback_scale(size); draw_text5x7(x, y, s, sc, color); /* 走 put_px,自动过 clip */ dirty_add(x, y, (int)strlen(s) * 6 * sc, 7 * sc); } } void meow_ui_text(int x, int y, const char *utf8, int size, uint32_t color) { text_draw(x, y, utf8, size, color); } void meow_ui_text_right(int right_x, int y, const char *utf8, int size, uint32_t color) { text_draw(right_x - meow_ui_text_width(utf8, size), y, utf8, size, color); } void meow_ui_text_center(int cx, int y, const char *utf8, int size, uint32_t color) { text_draw(cx - meow_ui_text_width(utf8, size) / 2, y, utf8, size, color); } int meow_ui_text_width(const char *utf8, int size) { if (!utf8) return 0; if (g_mfont_ready) return meow_font_width_text(utf8, size); return (int)strlen(utf8) * 6 * fallback_scale(size); } int meow_ui_text_height(int size) { if (g_mfont_ready) return meow_font_line_height(size); return 7 * fallback_scale(size); } /* ================= 矢量字(FreeType)渲染薄封装(§10.1) ================= * meow_ft_text 在 meow_ui_ft.c(独立 .o,App 显式调 meow_ft_* 才拉取整条 FreeType 依赖), * 它经本处两个薄封装写进 back_buf:blend 一个灰度像素(过 clip)、标一块脏区。 * 这两函数在 gfx.o 里(任何 App 本就拉取 gfx.o),ft.o 引用它们不引入新的未拉入成员, * 从而保证「不调 meow_ft_* 的 App 零增量」(freetype .o 不会被拉入)。 */ void meow_gfx_ft_blend_px(int x, int y, uint32_t color, int alpha) { put_px_blend(x, y, color, alpha); } void meow_gfx_ft_dirty(int x, int y, int w, int h) { dirty_add(x, y, w, h); } /* ================= RGBA blit(图片层用) ================= */ void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha) { if (!g_init_ok || !rgba || w <= 0 || h <= 0 || alpha <= 0) return; int x1 = x, y1 = y, x2 = x + w, y2 = y + h; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; for (int py = y1; py < y2; py++) { int sy = py - y; for (int px = x1; px < x2; px++) { int sx = px - x; const uint8_t *p = rgba + ((size_t)sy * w + sx) * 4; int a = (alpha * p[3]) / 255; if (a <= 0) continue; uint32_t c = ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]; if (a >= 255) put_px(px, py, c); else put_px_blend(px, py, c, a); } } dirty_add(x1, y1, x2 - x1, y2 - y1); } /* ================= present / 上屏 ================= */ static void dump_ppm(void) { if (!g_dump_path || !g_back_buf) return; FILE *f = fopen(g_dump_path, "wb"); if (!f) return; fprintf(f, "P6\n%d %d\n255\n", g_xres, g_yres); for (int y = 0; y < g_yres; y++) { for (int x = 0; x < g_xres; x++) { unsigned char r, g, b; if (g_bpp == 16) { uint16_t px = *(uint16_t *)(g_back_buf + (size_t)y * g_line + (size_t)x * 2); r = ex5((px >> 11) & 0x1F); g = ex6((px >> 5) & 0x3F); b = ex5(px & 0x1F); } else { uint32_t px = *(uint32_t *)(g_back_buf + (size_t)y * g_line + (size_t)x * 4); r = (px >> 16) & 0xFF; g = (px >> 8) & 0xFF; b = px & 0xFF; } fputc(r, f); fputc(g, f); fputc(b, f); } } fclose(f); } /* ================= 状态栏「上收出屏只留提示线」(collapsed,基座) ================= */ /* 每帧推进上收/恢复:向目标偏移(collapsed→MUI_SBAR_H、否则→0)线性逼近; * 无过渡时长(g_sbar_collapse_anim_ms<=0)= 一步到位(基座默认)。动画插值骨架在此, * 后续代理可换缓动/每帧步进。 */ static void statusbar_collapse_step(void) { int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0; if (g_sbar_collapse_h == tgt) return; if (g_sbar_collapse_anim_ms <= 0) { g_sbar_collapse_h = tgt; return; } unsigned long long now = now_ms(); unsigned long long el = now - g_sbar_collapse_start_ms; int span = g_sbar_collapse_anim_ms; if ((int)el >= span) { g_sbar_collapse_h = tgt; return; } int delta = tgt - g_sbar_collapse_from_h; g_sbar_collapse_h = g_sbar_collapse_from_h + (int)((long long)delta * (long long)el / (long long)span); } /* 上收中是否需要逐帧重画(动画未达目标位置时置位,强制每帧推进)。 */ static int statusbar_collapse_in_transition(void) { int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0; return g_sbar_collapse_h != tgt; } /* 上收状态栏:内容整体上移出屏(不画时钟/电量),仅在顶部画一条「提示线」。 * 与 meow_ui_swipe_hint(左滑返回进度条,5px,动态)不同:此线是「状态栏可下拉还原」的 * 常驻提示(主题强调色下边缘)。收合只占/清「提示线」高度区域(MUI_SBAR_COLLAPSE_H=4), * 不再清整 MUI_SBAR_H(28) 行 —— app 内容按 inset=提示线高可上移到提示线下缘,无 28px 空条。 */ static void statusbar_collapsed_render(int h) { (void)h; const meow_theme *th = meow_theme_get(); int w = meow_ui_width(); /* h 越大内容越贴顶;基座:折叠态清「提示线」区域底 + 顶部提示线(提示线下缘=主题 accent) */ meow_ui_fill_rect(0, 0, w, MUI_SBAR_COLLAPSE_H, th->bg); meow_ui_fill_rect(0, MUI_SBAR_COLLAPSE_H - 1, w, 1, th->accent); } /* ---- 公共 API(基座实现;动画插值骨架见 statusbar_collapse_step)---- */ void meow_ui_statusbar_set_collapsed(int on) { int tgt = on ? 1 : 0; if (tgt == g_sbar_collapsed) return; g_sbar_collapsed = tgt; g_sbar_collapse_from_h = g_sbar_collapse_h; g_sbar_collapse_start_ms = now_ms(); dirty_full(); /* 顶栏显隐/上收变化 → 全帧重画 */ meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合,agent-146) */ } int meow_ui_statusbar_collapsed(void) { return g_sbar_collapsed; } /* 当前上收进度(0..MUI_SBAR_H)。供 launcher 按「可见条高 = 28-offset」标脏切齐(agent-146)。 */ int meow_ui_statusbar_collapse_offset(void) { return g_sbar_collapse_h; } /* 上收/恢复过渡是否仍在推进(current offset != target)。供 launcher 过渡期高频刷(agent-146)。 */ int meow_ui_statusbar_animating(void) { int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0; return g_sbar_collapse_h != tgt; } void meow_ui_statusbar_animate(int ms) { g_sbar_collapse_anim_ms = (ms > 0) ? ms : 0; /* 0=瞬切(基座默认) */ } int meow_ui_statusbar_hotzone(void) { /* 通知中心触发(顶部下滑)热区:与渲染分离(agent-146 定案:收合 ≠ 禁用顶部热区)。 * 状态栏「激活」(非全屏隐藏,含收合)时热区恒 = 整条状态栏高 MUI_SBAR_H(28)——即使收合 * 也只上收渲染,顶部下滑仍可打开通知中心(渲染与热区事件分离)。 * 仅「全屏隐藏」(immersive,inset0)时缩小到 MEOW_SBAR_HOTZONE_FS(保留一处下拉入口)。 */ if (meow_ui_sys_statusbar_active()) return MUI_SBAR_H; return MEOW_SBAR_HOTZONE_FS; } /* 顶部状态栏条是否被本次脏区触及(含整帧):触及或数据已变(force)才需要重画顶条。 */ static int statusbar_touched(void) { if (g_dirty_full) return 1; for (int i = 0; i < g_ndirty; i++) if (g_dirty[i][1] < MUI_SBAR_H) return 1; /* 脏矩形与顶条(前 MUI_SBAR_H 行)相交 */ return 0; } /* ---- §10.5 叠加让位自抑(present 层)---- * /tmp/meow_notif_active 存在 = 通知中心叠加会话进行中(launcher 正在 fb 上渲染面板)。 * App 进程(非系统 UI 渲染者)此时不上屏;200ms 节流缓存存在性(别每帧 stat)。 * 计数 g_overlay_suppressed 供宿主单测断言(meow_gfx_overlay_suppressed_count)。 */ static unsigned long long g_overlay_suppressed = 0; /* §后台化保活-残帧:记录「上一次 present 是否处于叠加自抑态」,用于在抑制→不抑制的恢复边沿 * 强制整帧重画(盖掉 launcher 在后台化期间画进 buffer0 的桌面/主界面帧)。 */ static int g_was_overlay_suppressed = 0; static int overlay_present_suppressed(void) { if (meow_ui_sys_render_enabled()) return 0; /* launcher(系统 UI)渲染面板,照常上屏 */ static char s_path[108]; static int s_path_init = 0; static unsigned long long s_last_chk = 0; static int s_last_res = 0; if (!s_path_init) { const char *e = getenv("MEOW_NOTIF_ACTIVE_FILE"); snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : MEOW_NOTIF_ACTIVE_DEFAULT); s_path_init = 1; } unsigned long long now = now_ms(); if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res; s_last_chk = now; s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0; return s_last_res; } unsigned long long meow_gfx_overlay_suppressed_count(void) { return g_overlay_suppressed; } static void present(void) { if (!g_init_ok || !g_back_buf) return; if (!(g_dirty_full || g_ndirty > 0)) return; /* §10.5 叠加让位自抑(仅 App 进程):App 前台播放中唤起通知中心时 launcher 不 SIGSTOP * (保 PCM 供流,ncm 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf, * 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层。系统 UI 进程(launcher 渲染面板) * 不抑制。抑制期脏区保留累积(超上限自然升级整帧),标记消失后下一次 present 一次上屏。 */ if (overlay_present_suppressed()) { g_overlay_suppressed++; g_was_overlay_suppressed = 1; return; } /* §后台化保活-残帧:抑制→不抑制的**恢复边沿**强制整帧重画。App 被 launcher 转后台保活 * (/tmp/meow_notif_active 存在)期间 present 被自抑,back_buf 仍保留 App 自己的完整帧; * 但 launcher 已把桌面/主界面整帧画进 buffer0 盖在 App 残帧上。恢复前台时若只 present * 抑制期累积的小脏区(时钟/状态栏),launcher 的桌面帧会在 App 未重画区域透出(真机 * 「桌面叠在 ncm UI 下面/旧播放界面层叠」。标记由有→无即「回到前台」,强制 dirty_full * 让下一次 present 把 App 完整 back_buf 一次上屏,盖掉桌面帧。仅真实 App 进程(launcher * 系统 UI 恒不抑制,本边沿不触发)。 */ if (g_was_overlay_suppressed) { g_was_overlay_suppressed = 0; dirty_full(); } /* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现; * 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */ unsigned long long now = now_ms(); int period = (g_eff_rate > 0) ? (1000 / g_eff_rate) : 16; if (!g_refresh_always_max) { if (g_last_present_ms && now - g_last_present_ms < (unsigned long long)period) return; } /* §6.1 ②:App 跨 Activity 全局控件(SDK 托管骨架)。在 active activity 内容之上、 * 全局状态栏之下绘制——跨 Activity 常驻,仅在有帧需上屏(已过帧率门控)时调用。 * render_cb 内的 meow_ui_* 绘制会自行标脏,随下方 dirty 列表一并上屏。 */ meow_ui_global_render(); /* 全局状态栏(A-0)+ 上收(collapsed)基座:上屏前把状态栏画进 back_buf 顶部行。 * 展开态=整条(MUI_SBAR_H 行);收合态只占/清「提示线」区域(MUI_SBAR_COLLAPSE_H 行), * app 内容(inset=提示线高)可上移到提示线下缘,无 28px 空条。全屏/隐藏时 active()==0 * 不画(沉浸),app 内容可占满整屏。触发条件:数据已变(1s 轻刷/显隐切换) / * 顶条被 app 内容触及 / 上收动画未到位(逐帧推进)。展开态仍走 * meow_ui_sys_statusbar_render()(meow_ui_sys.c 不改,留给后续代理); * 上收折叠态(collapse_h>=MUI_SBAR_H)由基座只画提示线(内容上移出屏)。 */ if (meow_ui_sys_render_enabled() && meow_ui_sys_statusbar_active() && (meow_ui_sys_statusbar_force() || statusbar_touched() || statusbar_collapse_in_transition())) { statusbar_collapse_step(); if (g_sbar_collapse_h >= MUI_SBAR_H) statusbar_collapsed_render(g_sbar_collapse_h); else /* 收合过渡/展开态:内容上移 g_sbar_collapse_h 像素并裁顶(yoff=0=整条)。 * 仅画可见条高 [0, 28-offset) 行,其余行=App 内容(launcher 按 statusbar_height 标脏 * 切齐),实现「平滑上移出屏」而不残留、不黑框。 */ meow_ui_sys_statusbar_render_off(g_sbar_collapse_h); } if (g_fb_mem) { ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */ unsigned long long tbg = now_ms(); /* 「同一个状态栏」闭环:App 进程不渲染状态栏(meow_ui_sys_render_enabled()==0),也绝不向 * 真实 fb 写顶条 inset 区(顶条由 launcher 系统进程叠加绘制)。否则 App 首帧全量拷贝 * (meow_ui_init 置 dirty_full)会把 launcher 刚叠画的顶条冲成未初始化数据——真机 * 「进入 App 顶条黑 1 秒再出现」。sb_clip = 本进程当前状态栏 inset(展开=28 / 收合=4 / * 隐藏=0):全量拷贝只拷 [sb, yres) 行;逐 dirty 矩形把上沿截断到 y>=sb,整块落在顶条区则跳过。 * 仅 App 进程(非系统 UI)截断;launcher(系统 UI)sb_clip=0,行为不变。 */ int sb_clip = 0; if (!meow_ui_sys_render_enabled()) sb_clip = meow_ui_statusbar_height(); if (sb_clip < 0) sb_clip = 0; if (sb_clip > g_yres) sb_clip = g_yres; if (g_dirty_full) { if (sb_clip > 0) { memcpy(g_fb_mem + (size_t)sb_clip * g_line, g_back_buf + (size_t)sb_clip * g_line, (size_t)g_line * (size_t)(g_yres - sb_clip)); } else { memcpy(g_fb_mem, g_back_buf, (size_t)g_line * (size_t)g_yres); } } else { int bppb = g_bpp / 8; for (int i = 0; i < g_ndirty; i++) { int rw = g_dirty[i][2], ry = g_dirty[i][1], rh = g_dirty[i][3]; if (sb_clip > 0) { if (ry + rh <= sb_clip) continue; /* 整块在顶条 inset 区:App 不写 */ if (ry < sb_clip) { /* 上沿与顶条区相交:截断到 sb_clip */ rh -= (sb_clip - ry); ry = sb_clip; if (rh <= 0 || rw <= 0) continue; } } for (int yy = 0; yy < rh; yy++) { size_t off = (size_t)(ry + yy) * g_line + (size_t)g_dirty[i][0] * bppb; memcpy(g_fb_mem + off, g_back_buf + off, (size_t)rw * bppb); } } } refresh_governor(now_ms() - tbg, period); } g_last_present_ms = now; g_frames_presented++; if (g_dump_path) dump_ppm(); dirty_reset(); } void meow_ui_flush(void) { if (!g_flush_hold) present(); } void meow_ui_flush_region(int x, int y, int w, int h) { dirty_add(x, y, w, h); if (!g_flush_hold) present(); } /* ---- 帧率调度设置接口(launcher 与 app 共用;归口 PERF_SCHEDULER 显示刷新率子域)---- */ /* 设目标档位(60/30/15):同时把当前有效档对齐到目标(与 launcher settings_load 语义一致)。 */ void meow_ui_set_refresh_rate(int r) { if (r != 60 && r != 30 && r != 15) return; g_refresh_rate_cfg = r; g_eff_rate = r; } /* 1=恒 max 不降档(性能优先);0=允许 governor 在 [15,30,60] 内自适应降/升档。 */ void meow_ui_set_refresh_always_max(int on) { g_refresh_always_max = on ? 1 : 0; } /* 当前有效帧率(governor 可能已降档;验证/诊断用)。 */ int meow_ui_effective_rate(void) { return g_eff_rate; } /* 已呈现帧计数(验证 present 频率用)。 */ unsigned long long meow_ui_frames_presented(void) { return g_frames_presented; } int meow_gfx_is_device(void) { return g_fb_mem != NULL; } int meow_gfx_mfont_ready(void) { return g_mfont_ready; } /* ---- 字体档位(公共 API,基座定义 + font 域真重载钩子)---- * meow_font_tier()/meow_font_set_tier() 在 meow_ui_gfx.c 定义(符号唯一),font 代理勿在 * meow_font.c 重复定义;大字库按档装载由 meow_font.c meow_font_reload_tier() 实现,并在 * meow_font_set_tier() 处唤起(见下)。 */ int meow_font_tier(void) { return g_font_tier; } int meow_font_set_tier(int tier) { int old = g_font_tier; if (tier < MEOW_FONT_TIER_COMMON) tier = MEOW_FONT_TIER_COMMON; if (tier > MEOW_FONT_TIER_FULL) tier = MEOW_FONT_TIER_FULL; if (tier == g_font_tier) return g_font_tier; g_font_tier = tier; /* 真重载(font 域钩子,M2.5x):按档装载大字库。launcher 已按档解析并注入 * MEOW_FONT_PNG/IDX(tier>0 → GBK/Full 大字库绝对路径);重读 env 重载。 * 无 env/失败 → 回退卡默认 MeowOS/fonts/default → 固件。 * 大字库 atlas 大(GBK≈5-15MB+),meow_font_reload_tier 先 unload 旧再 load 新,防双驻留。 */ const char *ep = getenv("MEOW_FONT_PNG"); const char *ei = getenv("MEOW_FONT_IDX"); if (ep && ep[0] && ei && ei[0] && meow_font_reload_tier(tier, ep, ei) == 0) return g_font_tier; if (meow_font_reload_tier(tier, MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) return g_font_tier; if (meow_font_reload_tier(tier, MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) return g_font_tier; g_font_tier = old; /* 全部失败 → 回退旧档(字库可能已不可用,5x7 点阵兜底,系统不死) */ return g_font_tier; } /* ================= 初始化/销毁 ================= */ /* 字库加载优先级(M2.x): * 1) 环境变量 MEOW_FONT_PNG/MEOW_FONT_IDX(launcher 注入的「当前生效字体」= 系统 font.file) * —— app 未自带字库(§5.7 load_handle 未实现)时跟随系统 font.file; * 2) 卡默认 MeowOS/fonts/default/regular/font.png(M2.6 字重结构;不再硬编码 MeowAPPs/fonts); * 3) 固件 /usr/share/meowos/font/font.png。 * 全失败 → g_mfont_ready=0,text_draw 回退 5x7 点阵(系统不死)。 */ static int load_font(void) { /* 字体档位(MEOW_FONT_TIER env,由 launcher 按 app.json "fonts" 注入): * 0=默认4K常用 1=大字库GBK 2=Full。launcher 已按档解析大字库路径并经 MEOW_FONT_PNG/IDX * 注入(见 meow_font_effective_paths_tiered),故下方 meow_font_load(ep,ei) 即装载 * 对应档位字库(tier>0 → GBK/Full 大字库);此处 read env 存档对齐公共 get/set。 */ const char *etr = getenv("MEOW_FONT_TIER"); if (etr && etr[0]) { g_font_tier = atoi(etr); if (g_font_tier < MEOW_FONT_TIER_COMMON) g_font_tier = MEOW_FONT_TIER_COMMON; if (g_font_tier > MEOW_FONT_TIER_FULL) g_font_tier = MEOW_FONT_TIER_FULL; } const char *ep = getenv("MEOW_FONT_PNG"); const char *ei = getenv("MEOW_FONT_IDX"); if (ep && ep[0] && ei && ei[0] && meow_font_load(ep, ei) == 0) return 1; if (meow_font_load(MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) return 1; if (meow_font_load(MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) return 1; return 0; } /* M2.5x 多字体:读 launcher 注入的环境变量,构建 6 字符类 → 字库的映射(同 family 去重、 * ≤4 套上限由 meow_font_config_multifont 统一处理)。launcher 注入(见 meow_launcherd/app_launcher.c * `launch_app`):MEOW_MULTI_FONT=0/1;MEOW_FONT_PNG/IDX=全局(保留);每类 * MEOW_FONT_CLS__PNG/IDX(命中则覆盖全局;未注入 = 该类跟随全局)。 * 仅当全局字库已装载(g_mfont_ready)才配置;关/无 env → 单字体,零额外开销。 */ static void meow_gfx_load_multifont(void) { static const char *cls_env[MEOW_FCLS_COUNT][2] = { { "MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_IDX" }, { "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_CJK_SYM_IDX" }, { "MEOW_FONT_CLS_NUM_PNG", "MEOW_FONT_CLS_NUM_IDX" }, { "MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_IDX" }, { "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_LATIN_SYM_IDX" }, { "MEOW_FONT_CLS_OTHER_PNG", "MEOW_FONT_CLS_OTHER_IDX" }, }; const char *emf = getenv("MEOW_MULTI_FONT"); int on = emf && emf[0] == '1'; const char *gp = getenv("MEOW_FONT_PNG"); const char *gi = getenv("MEOW_FONT_IDX"); meow_font_class_cfg cfg[MEOW_FCLS_COUNT]; memset(cfg, 0, sizeof cfg); for (int c = 0; c < MEOW_FCLS_COUNT; c++) { const char *p = getenv(cls_env[c][0]); const char *idx = getenv(cls_env[c][1]); if (p && p[0] && idx && idx[0]) { cfg[c].png = p; cfg[c].idx = idx; } } int distinct = 0, truncated = 0; if (meow_font_config_multifont(on, gp, gi, cfg, &distinct, &truncated) != 0) meow_log(MEOW_LOG_WARN, "multi_font: config failed, single-font fallback"); else if (truncated) meow_log(MEOW_LOG_WARN, "multi_font: >%d distinct fonts -> capped (more memory!)", distinct); } int meow_ui_init(const meow_ui_opts *opts) { if (g_init_ok) return 0; meow_log_install_exit_handler(); /* M2.78:SIGTERM/SIGINT 兜底写 exit: 日志 */ meow_ui_install_sigusr1_guard(); /* M2.87:SIGUSR1 无害护栏(防泄漏 SIGUSR1 杀 App) */ int fw = 0, fh = 0; if (opts) { fw = opts->width; fh = opts->height; } int fd = open(FB_DEV, O_RDWR); if (fd >= 0) { unsigned char vinfo[160], finfo[68]; memset(vinfo, 0, sizeof vinfo); memset(finfo, 0, sizeof finfo); if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) == 0 && ioctl(fd, FBIOGET_FSCREENINFO, finfo) == 0) { g_xres = *(uint32_t *)(vinfo + 0); g_yres = *(uint32_t *)(vinfo + 4); g_bpp = *(uint32_t *)(vinfo + 24); g_line = *(uint32_t *)(finfo + 44); if (!g_line) g_line = g_xres * (g_bpp / 8); if (g_bpp != 16 && g_bpp != 32) g_bpp = 16; size_t maplen = (size_t)g_line * (size_t)g_yres; void *m = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (m != MAP_FAILED) { g_fb_mem = m; g_fb_fd = fd; } else { close(fd); fd = -1; g_fb_mem = NULL; } } else { close(fd); fd = -1; g_fb_mem = NULL; } } /* host/qemu 无 fb:使用缺省/覆写几何,走离屏(present 只 dump) */ if (g_fb_mem == NULL) { if (g_xres <= 0 || g_xres > 4096) g_xres = MUI_DEFAULT_W; if (g_yres <= 0 || g_yres > 4096) g_yres = MUI_DEFAULT_H; if (g_bpp != 16 && g_bpp != 32) g_bpp = 16; g_line = g_xres * (g_bpp / 8); if (fd >= 0) { close(fd); fd = -1; } } if (fw > 0 && fw <= 4096) g_xres = fw; if (fh > 0 && fh <= 4096) g_yres = fh; g_line = g_xres * (g_bpp / 8); g_back_buf = malloc((size_t)g_line * (size_t)g_yres); if (!g_back_buf) { if (g_fb_mem) munmap(g_fb_mem, 0); if (fd >= 0) close(fd); return -1; } g_fb = g_back_buf; meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); g_mfont_ready = load_font(); if (g_mfont_ready) meow_gfx_load_multifont(); /* M2.5x 多字体(读 env;关/无 env = 单字体) */ const char *dp = getenv("MEOW_UI_DUMP"); if (dp && *dp) g_dump_path = strdup(dp); g_init_ok = 1; dirty_full(); meow_ui_sys_init(); /* A-0:系统状态读取器首刷(电量/WiFi/蓝牙/音量),状态栏就绪 */ /* 全屏预置(基座):launcher 按 app.json "fullscreen" 注入 MEOW_FULLSCREEN=1,SDK 据此 * 预先设全屏(隐藏整条状态栏/沉浸,statusbar_active()==0、statusbar_height()==0)。 * 未注入/非"1"=非全屏(默认)。放在 sys_init 后使首帧即为全屏态。 */ const char *efs = getenv("MEOW_FULLSCREEN"); if (efs && efs[0] == '1') meow_ui_set_fullscreen(1); return 0; } void meow_ui_shutdown(void) { if (!g_init_ok) return; meow_font_unload(); if (g_fb_mem) munmap(g_fb_mem, (size_t)g_line * (size_t)g_yres); if (g_fb_fd >= 0) close(g_fb_fd); if (g_back_buf) free(g_back_buf); if (g_dump_path) { free(g_dump_path); g_dump_path = NULL; } if (g_hint_snap) { free(g_hint_snap); g_hint_snap = NULL; } g_back_buf = NULL; g_fb = NULL; g_fb_mem = NULL; g_fb_fd = -1; g_init_ok = 0; g_mfont_ready = 0; dirty_reset(); } int meow_ui_width(void) { return g_xres; } int meow_ui_height(void) { return g_yres; }