From a07f9752d30e3c43fe643a9e63e584ad5a62c070 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 5 Nov 2026 20:07:59 +0800 Subject: [PATCH] =?UTF-8?q?SDK:=20=E5=86=85=E7=BD=AE=20mipsel=20o32=20?= =?UTF-8?q?=E6=97=B6=E9=92=9F/=E7=9D=A1=E7=9C=A0=20ABI=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=88meow=5Fui=5Ftime.c=EF=BC=89+=20=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zig 0.17 内置 musl 在 mipsel 上头声明 64 位 time_t、实现走 time32 系统调用 (__ILP32__ 守卫),导致 meow_ui_time_ms() 毫秒位恒 0、高 32 位随机, usleep(16000) 立即返回(帧节拍/轮询变忙等空转)。主仓已验证的修复逐字节 搬入公开仓:取时按内核 o32 ABI(8 字节 {int32 sec; int32 nsec})再升 64 位, 同 TU 强定义 __clock_gettime/64 与睡眠入口,链接期接管 libc。 - meow_ui_time.c / CLOCK_ABI.md:cp 搬运,md5 与主仓一致(含原注释与依据链) - meow_ui_gfx.c:删掉 now_ms/meow_ui_time_ms(避免与 meow_ui_time.o 重复符号), 6 处内部调用改外部 meow_ui_time_ms; 不再使用 - meow_ui_internal.h:补 meow_time_ms_from_parts 声明 - build_sdk.sh:OBJS 首位加 meow_ui_time,头注释写明归档须排在 -lc 之前 - test_meow_time.c:适配公开仓构建方式,加负 sec 回绕两例,输出断言数(15) - sdk/README.md:公开说明本次修复、链接顺序要求与已知限制(2038 前有效) - tuku/test/run_tests.sh、run_webtoon_tests.sh:$SDK 源文件清单补 meow_ui_time.c (宿主集成测试直接编 SDK 源,否则链接期 undefined symbol) 验证:sdk/build_sdk.sh 0 warning;宿主 test_meow_time == PASS == checks=15; tuku 两套测试全 PASS(run_tests 462+106+325 断言、webtoon 240 断言,0 failed); 探针/ picbrowser 反汇编确认 __clock_gettime 取自本 TU(8 字节 time32 + 升位), 无 libc VDSO 前导;picbrowser 仍为 MIPS32r2 静态 ELF。 --- sdk/CLOCK_ABI.md | 187 +++++++++++++++++++++++++++++++++ sdk/README.md | 59 +++++++++++ sdk/build_sdk.sh | 9 +- sdk/meow_ui_gfx.c | 25 ++--- sdk/meow_ui_internal.h | 4 + sdk/meow_ui_time.c | 147 ++++++++++++++++++++++++++ sdk/test_meow_time.c | 111 +++++++++++++++++++ tuku/test/run_tests.sh | 10 +- tuku/test/run_webtoon_tests.sh | 2 +- 9 files changed, 531 insertions(+), 23 deletions(-) create mode 100644 sdk/CLOCK_ABI.md create mode 100644 sdk/README.md create mode 100644 sdk/meow_ui_time.c create mode 100644 sdk/test_meow_time.c diff --git a/sdk/CLOCK_ABI.md b/sdk/CLOCK_ABI.md new file mode 100644 index 0000000..f44cc64 --- /dev/null +++ b/sdk/CLOCK_ABI.md @@ -0,0 +1,187 @@ +# SDK 时钟 ABI 说明(meow_ui_time_ms 真机故障的根因与修复) + +面向对象:维护 SDK / 工具链 / 重链 App 的人。**结论先行**:这不是 SDK 代码写错, +是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾;修复落在 SDK 链上 +(`patch_v2/sdk/meow_ui_time.c` + 链接期强定义),改后需重编 `meow_ui.a` 并重链四 App。 + +## 1. 现象(真机) + +M10 抓到的日志首列(`meow_ui_time_ms()`,64 位)可拆成: + +``` +6607363222717682072 → lo=1135000 hi=1538396632 +8754846870717682072 → lo=1135000 hi=2038396632 +18138485921460348192 → lo=1140000 hi=4223195352 +``` + +- 低 32 位 = 开机秒数 × 1000,**恒为 1000 的倍数 → 毫秒位丢失**; +- 高 32 位每次调用随机、无单调性。 + +同进程另一份取时(旧容器编的 core)正常 → 问题只在「zig 0.17 编的那部分」。 + +## 2. 根因链(三段证据) + +### 2.1 zig 头文件:`time_t` 是 64 位,`struct timespec` 16 字节 + +`/libc/include/mips-linux-musl/bits/alltypes.h`: + +```c +#define _REDIR_TIME64 1 +#define __USE_TIME_BITS64 1 +typedef _Int64 time_t; /* 无条件 64 位 */ +struct timespec { time_t tv_sec; ...; long tv_nsec; ... }; /* tv_sec@0(8B)、tv_nsec@8 */ +``` + +`include/generic-musl/time.h` 因此在 `_REDIR_TIME64` 下把 `clock_gettime` 重定向到 +`__clock_gettime64`。用该链编译的 C 代码一律按「16 字节、tv_sec 8 字节」取数 +(`patch_v2/sdk/meow_ui.a(meow_ui_gfx.o)` 的旧 `meow_ui_time_ms` 反汇编:`lw 0x10/0x14` +取 tv_sec、`lw 0x18` 取 tv_nsec,正是这个布局)。 + +### 2.2 zig 的 clang 给 mipsel 预定义了 `__ILP32__` + +``` +$ echo | zig cc --target=mipsel-linux-musleabi -dM -E - | grep __ILP32__ +#define __ILP32__ 1 +``` + +(arm-linux-musleabi 同样为 1;x86-linux-musl / aarch64 为 0。容器里的 gcc 对 mips o32 +**不定义** `__ILP32__`。) + +### 2.3 musl 的 time64 回退分支被这句 `!__ILP32__` 整体编译掉 + +`/libc/musl/src/time/clock_gettime.c`: + +```c +#if defined(SYS_clock_gettime64) && !defined(__ILP32__) + ... __syscall(SYS_clock_gettime64, clk, ts); /* 4403 */ + ... 失败(ENOSYS) 再 __syscall(SYS_clock_gettime, clk, ts32); /* 4263 + 宽度转换 */ +#else + r = __syscall(SYS_clock_gettime, clk, ts); /* 直接把 8 字节 time32 写进调用者缓冲 */ +#endif +``` + +这句 `!__ILP32__` 本意只排除 x32(`__ILP32__` 是 x32 的标记),但 clang 把它当成 +「32 位数据模型」的通名。于是 zig 的 mipsel libc 走进 `#else`: +`__clock_gettime64` 实际执行 **SYS_clock_gettime(4263)**,只写 8 字节 +`{ int32 tv_sec; int32 tv_nsec; }`。 + +**证据(反汇编,当前 zig 0.17 静态链接的探针)**: + +``` +0002576c <__clock_gettime64>: + 257c8: addiu $2,$zero,0x10a7 ; 4263 = SYS_clock_gettime(time32) + 257cc: syscall ; $5 直接是调用者的 16 字节 struct 指针,无 4403 尝试、无转换 +``` + +同一符号在**旧容器产物** `patch_v2/ncm_app/ncm`(已入库,gcc 构建)里是: + +``` +115748: addiu $2,$zero,0x1133 ; 4403 = SYS_clock_gettime64 +11574c: syscall +... +115774: addiu $2,$zero,0x10a7 ; ENOSYS 后回退 4263,写入本地 ts32 +... + ; 再把 ts32 升成 64 位写回调用者(tv_sec=tv_sec、tv_nsec=tv_nsec) +``` + +两者对比即根因:**头文件 ABI(64 位 time_t)与 libc 实现 ABI(32 位 time32)不一致**。 + +### 2.4 数值解释(为什么「低 32 = 秒×1000、高 32 随机」) + +libc 把 `{sec, nsec}` 当两个 int32 写进 16 字节缓冲的前 8 字节;SDK 按 64 位读: + +``` +tv_sec = sec | (nsec << 32) // 低字正确、高字=纳秒 +tv_nsec = 缓冲偏移 8 的未初始化值 // 毫秒项≈0 +result = tv_sec*1000 + tv_nsec/1e6 + = sec*1000 + (nsec*1000 mod 2^32)<<32 + 垃圾/1e6 +``` + +- 低 32 位只剩 `sec*1000`(nsec 项左移 32 位不落到低字)→ 恒为 1000 的倍数; +- 高 32 位 = `nsec*1000 mod 2^32` → 每次随机、不单调; +- `tv_nsec` 来自越界读 → 通常是小值/0(真机上恰好 0,所以没有额外尾数)。 + +## 3. 修复(`patch_v2/sdk/meow_ui_time.c`) + +1. **取数走内核 o32 ABI**:`meow_ui_time_ms()` 用 + `syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &{int32 sec; int32 nsec;})`, + 再 `meow_time_ms_from_parts(sec, nsec)` 升成 64 位毫秒。完全不依赖 libc 头声明的 + `struct timespec` 宽度 —— 即使将来 zig 修好(或换回容器链),内核 time32 入口始终存在。 +2. **链接期修 libc 入口**:同一 TU 强定义 `__clock_gettime` / `__clock_gettime64` + (musl 的 `time()`、`gettimeofday()`、`__timedwait_cp()`(pthread 定时等待)内部都调 + `__clock_gettime`)。应用链接顺序是 `... meow_ui.a ... -lc`,归档先解析符号,libc 里那份 + 坏实现(及其 VDSO 前导)不会被拉入 —— 用 `nm` 验证链接产物里没有 `cgt_init/__vdsosym`, + 且 `__time64`/`__timedwait_cp` 的 `jal` 指向本 TU 的实现。 +3. **睡眠入口一并修正(同源缺陷)**:`__nanosleep_time64` / `__clock_nanosleep(_time64)` + 先按 time32 转换再发 `SYS_nanosleep`/`SYS_clock_nanosleep`,`rem` 再扩回 64 位。 + 修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 → + SDK 里 `usleep(16000)`(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。 +4. **去掉四处重复的 `now_ms()`**:`meow_ui_gfx.c` / `meow_ui_sys.c` / `meow_ui_input.c` / + `meow_cachegov.c` 统一调 `meow_ui_time_ms()`(cachegov 保留宿主测试注入桩)。 +5. 构建脚本 `build_sdk.sh` 的 OBJS 增加 `meow_ui_time`;宿主单测 `test_meow_time.c` + 固定换算边界与「单调且前进」;探针 `tools/clock_abi_probe.c` 用于宿主/真机一眼验收。 + +非 mips 目标(宿主单测、aarch64 宿主)走普通 `clock_gettime`,行为不变。 + +## 4. 验证 + +宿主: + +``` +cd patch_v2/sdk +gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/meow_ui_time.o +gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/meow_ui_time.o -o /tmp/test_meow_time +/tmp/test_meow_time # == PASS ==(换算边界 + 单调前进) +``` + +交叉: + +``` +bash patch_v2/sdk/build_sdk_host.sh --verify # 成员清单/架构自检 + 0 warning +llvm-objdump -d meow_ui.a(meow_ui_time.o) # meow_ui_time_ms: syscall 4263, lw 0x18 取 nsec、sra 扩展 tv_sec +``` + +链接期接管(用真实归档链一个探针,检查符号来源): + +``` +# 不应出现 libc 的 VDSO 前导(cgt_init/__vdsosym)→ libc 的取时实现未被拉入 +strings -a clock_abi_probe_sdk | grep -c __vdso # 期望 0 +llvm-nm clock_abi_probe_sdk | grep -E 'clock_gettime|nanosleep' +# __time64 / __timedwait_cp 的 jal 应指向本 TU 的 __clock_gettime;睡眠走 SYS_nanosleep(4166)/clock_nanosleep(4265) +``` + +探针 `tools/clock_abi_probe.c`(宿主 `gcc` 或 mipsel 交叉;加 `-DMEOW_PROBE_WITH_SDK` +并链 `meow_ui.a` 可同时验收 `meow_ui_time_ms`):把 `struct timespec` 预填 0xAA 后取时, +若偏移 8..11 仍是 0xAA 即说明 libc 只写了 8 字节(ABI 不一致),退出码 2。 + +真机(塔台批准后):日志首列应连续、出现非整千差;`meow_ui_time_ms()` 两次取样差 = 真实毫秒。 +本次设备已离线,真机确认**待办**。 + +## 5. 影响面 + +- **SDK 内**:`meow_ui_time_ms()` 及其上层 —— 动画 tween(`meow_ui_anim.c`)、滚动/fling + (`meow_ui_scroll.c`/`meow_select.c`)、IME 冷却(`meow_ui_ime.c`)、状态栏动画 + (`meow_ui_gfx.c`)、通知 TTL(`meow_ui_notify.c`)、输入层 tick/启动冷却 + (`meow_ui_input.c`)、cachegov 回退窗口(`meow_cachegov.c`)、日志首列(`meow_ui_log.c`)。 +- **App 侧**:`on_tick` 入参 `now`、`meow_anim_*`、以及任何自行调用 `meow_ui_time_ms()` 的 + App(hello/reader/files/ncm…)。 +- **产物**:`meow_ui.a` 变更 → 需**重编并重链四 App**(ncm/reader/files/hello); + `time()`/`gettimeofday()` 的修正也在同一份归档里生效(链接期覆盖)。 +- 真机写入红线未触碰;本单只改 SDK 源与构建脚本。 + +## 6. 同源残余(未在本单修复,需单独安排) + +同一句 `!__ILP32__` 还打穿了以下路径(睡眠族已在上文一并修掉,余下这些症状不同、 +修复面更大): + +| 路径 | 症状 | 说明 | +| --- | --- | --- | +| `futex` 相对超时 | 亚秒 timed wait 立即超时 | `__timedwait_cp` 的 `__futex4_cp` 同款 `!__ILP32__` 守卫;`pthread_cond_timedwait` 的「now」已由本单修正,但相对超时仍按 8 字节读(>1s 的整秒等待基本可用,亚秒等待失效) | +| `timerfd_settime` | 到期时间错乱 | `struct itimerspec`(32 字节)当 16 字节读;当前 SDK 未用 timerfd | +| `clock_settime`/`clock_adjtime`/`pselect`/`ppoll`/`sigtimedwait`/`semtimedop` 等 | 未验证 | 同族守卫;SDK 目前未直接使用 | + +建议:向 zig 上游报 musl `__ILP32__` 守卫问题(clang 对 32 位目标统一定义 `__ILP32__`, +而该守卫本意只排除 x32);根治应换用容器旧链(gcc,不定义 `__ILP32__`)编 SDK,或 +downgrade/修补 zig 内置 musl。**不要**指望 `-D_TIME_BITS=32`:zig 的 +`alltypes.h` 是无条件 `#define __USE_TIME_BITS64 1`,命令行改不动;而只改用户侧头文件会让 +用户结构(8 字节)与 libc 内部(16 字节,pthread 等)再次错配。 diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..a4d97f0 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,59 @@ +# sdk/ — meow_ui 应用框架(源码 + 构建) + +本目录是 `meow_ui.a` 的**自包含**源码树:公共头在 `include/`(伞头 `meow_ui.h`), +构建脚本 `build_sdk.sh`,宿主逻辑测试 `test_*.c`。上手指南见仓库根 +[README.md](../README.md) 与 [docs/APP_DEV_GUIDE.md](../docs/APP_DEV_GUIDE.md)。 + +## 构建 + +```sh +cd sdk +./build_sdk.sh # 产出 sdk/meow_ui.a(mipsel 静态库) +``` + +交叉编译器默认 `$HOME/Project/R3ProII/.tools/zig-mipsel-cc`,可用 `MEOW_CC` 覆盖。 + +## 时钟:mipsel o32 时钟/睡眠 ABI 修复(已内置) + +**公开 SDK 现已内置 mipsel o32 时钟/睡眠 ABI 修复(见 [CLOCK_ABI.md](CLOCK_ABI.md))**, +源码在 `meow_ui_time.c`。 + +- 背景:zig 内置 musl 在 mipsel 上「头文件声明 64 位 `time_t`、实现却走 time32 系统调用」, + 导致 `meow_ui_time_ms()` 毫秒位恒 0、高 32 位随机;`usleep()` 亚秒等待立即返回(忙等空转)。 +- 修复:`meow_ui_time_ms()` 直接按**内核 o32 ABI**(8 字节 `{int32 sec; int32 nsec;}`)取时, + 再自行升成 64 位毫秒;同一 TU 强定义 `__clock_gettime` / `__clock_gettime64` / + `__clock_nanosleep` / `__clock_nanosleep_time64` / `__nanosleep_time64`,在**链接期接管** + libc 里那份 ABI 不一致的实现。 +- **链接顺序要求**:应用必须把 `meow_ui.a` 放在 `-lc` **之前**(`cc ... meow_ui.a ... -lc`), + 强定义才能生效。仓库内 `examples/hello/build_hello.sh` 与 `tuku/build_tuku.sh` 均如此; + 自定义构建脚本请照抄这个顺序,否则取时/睡眠会静默回到坏实现。 +- 非 mipsel 目标(宿主单测、aarch64 等)走普通 `clock_gettime`,行为不变。 + +### 已知限制 + +- **2038 前有效**:内核侧仍是 time32(`tv_sec` 为 int32),秒数超出 `INT32_MAX` 即溢出; + 本修复只保证「头/实现的宽度一致」,不把内核升到 time64。 +- 同源缺陷**未修**:futex 相对超时(`pthread_cond_timedwait` 的亚秒等待)、`timerfd_settime`、 + `clock_settime`/`clock_adjtime`、`pselect`/`ppoll` 等仍受同一句 `!__ILP32__` 影响; + 清单与影响见 [CLOCK_ABI.md](CLOCK_ABI.md) §6。SDK 自身不直接使用这些路径。 +- 真机验收:本修复的证据是反汇编 + 宿主单测(本机无 qemu-mipsel),设备侧仍需实测确认。 + +## 宿主测试 + +无硬件即可跑(`gcc` 直接编译,不链 `meow_ui.a`): + +```sh +cd sdk +# 时钟换算与单调性 +gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o +gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time +./test_meow_time # == PASS == / checks=N + +# 伞头与其余模块测试(示例) +gcc -I. -Iinclude test_umbrella.c <各 sdk 源 .c> test_host_stub.c -lpthread -o test_umbrella +./test_umbrella +``` + +注意:宿主编译时 `meow_ui_time.c` 走 `MEOW_TIME32_KERNEL_ABI 0` 分支(普通 `clock_gettime`), +因此**宿主测试不能证明 mipsel o32 路径正确** —— 那条路径的正确性靠 `CLOCK_ABI.md §4` 的 +反汇编证据(`nm` 符号来源 + `syscall 4263/4265` 的 8 字节转换)。 diff --git a/sdk/build_sdk.sh b/sdk/build_sdk.sh index 2b0bf4c..d03749f 100755 --- a/sdk/build_sdk.sh +++ b/sdk/build_sdk.sh @@ -6,6 +6,11 @@ # # 交叉编译器:默认 MeowOS 开发机路径 $HOME/Project/R3ProII/.tools/zig-mipsel-cc; # 可用环境变量 MEOW_CC 覆盖(或用你自己装好的 zig 交叉 cc,--target=mipsel-linux-musleabi soft-float)。 +# +# 时钟(meow_ui_time):mipsel 上 zig 内置 musl 的 time64 ABI 自相矛盾(头声明 64 位 time_t、 +# 实现走 time32 取数),该 TU 按内核 o32 ABI 取时并**强定义** __clock_gettime*/睡眠入口,靠链接期 +# 接管 libc 那份坏实现。因此应用链接顺序必须是 `... meow_ui.a ... -lc`(归档在 libc 之前, +# 现有 build_tuku.sh 即如此);根因与证据链见 sdk/CLOCK_ABI.md。 set -e cd "$(dirname "$0")" CC="${MEOW_CC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}" @@ -16,7 +21,9 @@ rm -f meow_ui.a *.o # meow_font.c = 预渲染位图字体运行时(自包含:只含 meow_font.h + 标准头;最小 zlib ABI 自声明, # 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。新模块(box/anim/scroll/str + cachegov/lazygrid/ # select/net_ipc)随主仓增量并入。 -OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc" +# meow_ui_time.c 必须**编进归档**且排在前面:它提供 meow_ui_time_ms(gfx 已不再实现)与 +# __clock_gettime* 强定义,归档成员随 meow_ui_time_ms 的引用被拉入,从而在 -lc 之前解析。 +OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc" CFLAGS="-Os -static -Iinclude -I." for f in $OBJS; do echo "== cc $f ==" diff --git a/sdk/meow_ui_gfx.c b/sdk/meow_ui_gfx.c index a73bf87..8418b0d 100644 --- a/sdk/meow_ui_gfx.c +++ b/sdk/meow_ui_gfx.c @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -172,15 +171,9 @@ static void refresh_governor(unsigned long long cost_ms, int period) } /* ---- 工具 ---- */ -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(); } - +/* 时钟统一由 meow_ui_time.c 提供(meow_ui_time_ms,声明见 include/meow_ui_draw.h): + * mipsel 上 zig musl 的 time64 ABI 自相矛盾,取时必须走内核 o32 ABI 再升位, + * 故本 TU 不再保留取时实现(详见 sdk/CLOCK_ABI.md)。 */ static uint16_t rgb565(uint32_t c) { return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)); @@ -895,7 +888,7 @@ 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 now = meow_ui_time_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; } @@ -932,7 +925,7 @@ void meow_ui_statusbar_set_collapsed(int on) 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(); + g_sbar_collapse_start_ms = meow_ui_time_ms(); dirty_full(); /* 顶栏显隐/上收变化 → 全帧重画 */ meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合,agent-146) */ } @@ -1012,7 +1005,7 @@ int meow_ui_overlay_suppressed_active(void) 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(); + unsigned long long now = meow_ui_time_ms(); if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res; s_last_chk = now; { @@ -1066,7 +1059,7 @@ static void present(void) } /* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现; * 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */ - unsigned long long now = now_ms(); + unsigned long long now = meow_ui_time_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; @@ -1096,7 +1089,7 @@ static void present(void) } if (g_fb_mem) { ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */ - unsigned long long tbg = now_ms(); + unsigned long long tbg = meow_ui_time_ms(); /* 「同一个状态栏」闭环:App 进程不渲染状态栏(meow_ui_sys_render_enabled()==0),也绝不向 * 真实 fb 写顶条 inset 区(顶条由 launcher 系统进程叠加绘制)。否则 App 首帧全量拷贝 * (meow_ui_init 置 dirty_full)会把 launcher 刚叠画的顶条冲成未初始化数据——真机 @@ -1133,7 +1126,7 @@ static void present(void) } } } - refresh_governor(now_ms() - tbg, period); + refresh_governor(meow_ui_time_ms() - tbg, period); } g_last_present_ms = now; g_frames_presented++; diff --git a/sdk/meow_ui_internal.h b/sdk/meow_ui_internal.h index 83813b9..df64e0d 100644 --- a/sdk/meow_ui_internal.h +++ b/sdk/meow_ui_internal.h @@ -284,4 +284,8 @@ int meow_ime_handle_event(const meow_input_ev *e); /* 当前 IME 是否拼音模式(供 meow_ui_input.c 决定是否喂长按轮询)。 */ int meow_ime_mode_pinyin(void); +/* ---- 时钟(meow_ui_time.c)---- */ +/* 秒/纳秒 → 单调毫秒的唯一换算点(meow_ui_time_ms 与宿主单测共用)。 */ +uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec); + #endif /* MEOW_UI_INTERNAL_H */ diff --git a/sdk/meow_ui_time.c b/sdk/meow_ui_time.c new file mode 100644 index 0000000..dd72a1d --- /dev/null +++ b/sdk/meow_ui_time.c @@ -0,0 +1,147 @@ +/* meow_ui_time.c — SDK 单调时钟(meow_ui_time_ms)+ mipsel o32 时间 ABI 兜底。 + * + * 根因(M11;真机日志 + 反汇编证据链见 sdk/CLOCK_ABI.md): + * zig 0.17 的 clang 前端给 32 位 mips 目标预定义了 __ILP32__;zig 自带 musl 的 + * time64 回退分支统一写作 `#if defined(SYS_x_time64) && !defined(__ILP32__)` + * (该 `!__ILP32__` 本意只排除 x32),于是 mipsel 的 libc 把「time64 入口」编译成了 + * 老 32 位实现:__clock_gettime64 直接做 SYS_clock_gettime(4263),只写 8 字节 + * `{ int32 tv_sec; int32 tv_nsec; }`。而 zig 头文件又无条件把 time_t 声明成 64 位 + * (struct timespec 16 字节、tv_sec 8 字节),两边 ABI 不一致: + * tv_sec 低字 = 真 sec,高字 = 真 nsec → 64 位结果高 32 位随机、非单调; + * tv_nsec(结构偏移 8)根本没被写 → 毫秒位恒 0(真机实测全是 1000 的倍数)。 + * 容器旧链(gcc)不定义 __ILP32__,time64 回退正确——对照 patch_v2/ncm_app/ncm: + * 同一函数里先 SYS_clock_gettime64(4403)、ENOSYS 后走 4263 并做宽度转换。 + * + * 修复:不依赖 libc 头声明的 struct timespec 宽度,直接按**内核 o32 ABI** + * (8 字节 time32:int32 sec + int32 nsec)取时,再自行升成 64 位毫秒; + * 同时在本 TU 给出 __clock_gettime / __clock_gettime64 的强定义。应用链接顺序是 + * `... meow_ui.a ... -lc`,归档先参与解析,因此链接期整体替换掉 libc 里那份 + * ABI 不一致的实现:time()、gettimeofday()、pthread 定时等待的内部取时 + * (musl 的 time()/__timedwait_cp 都调 __clock_gettime)随之修正。 + * 上层时间位宽仍是 64 位,只有内核取数按 o32 time32;2038 前有效。 + * + * 同一句 `!__ILP32__` 还打穿了睡眠:__nanosleep_time64/__clock_nanosleep 走 `#else` + * 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 → + * `usleep(16000)` 立即返回(SDK 里帧节拍/轮询会变忙等空转)。这里一并按 time32 + * 转换后重发,恢复亚秒睡眠。 + * + * 残余(未修):futex 相对超时(pthread 定时等待的剩余时间按 8 字节读)、timerfd_settime、 + * clock_settime/adjtime、pselect/ppoll 超时——同为该守卫的受害者,见 CLOCK_ABI.md §6。 + */ +#include +#include +#include + +#include "meow_ui.h" + +#if defined(__mips__) && !defined(__mips64) && (__SIZEOF_LONG__ == 4) +#define MEOW_TIME32_KERNEL_ABI 1 +#include +#include +#else +#define MEOW_TIME32_KERNEL_ABI 0 +#endif + +uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec) +{ + return (uint64_t)sec * 1000u + (uint64_t)nsec / 1000000u; +} + +uint64_t meow_ui_time_ms(void) +{ +#if MEOW_TIME32_KERNEL_ABI + struct meow_time32 { int32_t tv_sec; int32_t tv_nsec; } t; + t.tv_sec = 0; + t.tv_nsec = 0; + if (syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &t) != 0) return 0; + return meow_time_ms_from_parts(t.tv_sec, t.tv_nsec); +#else + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0; + return meow_time_ms_from_parts((int64_t)ts.tv_sec, (int64_t)ts.tv_nsec); +#endif +} + +#if MEOW_TIME32_KERNEL_ABI +/* 内核 o32 time32 布局(与 libc 头的 time_t 宽度无关)。 */ +struct meow_time32 { + int32_t tv_sec; + int32_t tv_nsec; +}; + +static int32_t time32_clamp(int64_t v) +{ + if (v > INT32_MAX) return INT32_MAX; + if (v < 0) return 0; + return (int32_t)v; +} + +/* ---- libc 取时入口的强定义 ---- + * 返回约定与 musl 一致:0 成功 / -1 失败(errno)。 + * 说明: 在 _REDIR_TIME64 下把标识符 clock_gettime 的引用改名为 + * __clock_gettime64,故本 TU 不定义 clock_gettime 这个名字;musl 里它只是 + * __clock_gettime 的 weak alias,同一份语义已由下面的强定义接管。 */ +int __clock_gettime(clockid_t clk, struct timespec *ts); +int __clock_gettime64(clockid_t clk, struct timespec *ts); + +int __clock_gettime(clockid_t clk, struct timespec *ts) +{ + struct meow_time32 t; + if (!ts) { errno = EFAULT; return -1; } + t.tv_sec = 0; + t.tv_nsec = 0; + if (syscall(SYS_clock_gettime32, clk, &t) != 0) return -1; + ts->tv_sec = t.tv_sec; + ts->tv_nsec = t.tv_nsec; + return 0; +} + +int __clock_gettime64(clockid_t clk, struct timespec *ts) +{ + return __clock_gettime(clk, ts); +} + +/* ---- 睡眠入口(同源缺陷:亚秒 usleep 变空转)---- + * __clock_nanosleep 按 musl 内部约定返回 0 或正 errno;__nanosleep_time64 是 + * nanosleep/usleep 的重定向目标,按公共约定返回 0/-1(errno)。 */ +int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem); +int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem); +int __nanosleep_time64(const struct timespec *req, struct timespec *rem); + +int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem) +{ + struct meow_time32 rq, rm; + long r; + + if (clk == CLOCK_THREAD_CPUTIME_ID) return EINVAL; + if (!req) return EFAULT; + if (req->tv_nsec < 0 || req->tv_nsec >= 1000000000L) return EINVAL; + rq.tv_sec = time32_clamp(req->tv_sec); + rq.tv_nsec = (int32_t)req->tv_nsec; + rm.tv_sec = 0; + rm.tv_nsec = 0; + + if (clk == CLOCK_REALTIME && !flags) + r = syscall(SYS_nanosleep, &rq, rem ? &rm : 0); + else + r = syscall(SYS_clock_nanosleep_time32, clk, flags, &rq, rem ? &rm : 0); + if (r != 0) return errno; + if (rem) { + rem->tv_sec = rm.tv_sec; + rem->tv_nsec = rm.tv_nsec; + } + return 0; +} + +int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem) +{ + return __clock_nanosleep(clk, flags, req, rem); +} + +int __nanosleep_time64(const struct timespec *req, struct timespec *rem) +{ + int r = __clock_nanosleep(CLOCK_REALTIME, 0, req, rem); + if (r) { errno = r; return -1; } + return 0; +} +#endif diff --git a/sdk/test_meow_time.c b/sdk/test_meow_time.c new file mode 100644 index 0000000..a27967c --- /dev/null +++ b/sdk/test_meow_time.c @@ -0,0 +1,111 @@ +/* test_meow_time.c — 宿主单测:SDK 时钟换算(meow_ui_time.c / meow_time_ms_from_parts)。 + * + * 背景(M11/M21):mipsel 真机上 meow_ui_time_ms() 曾返回「低 32 位=秒×1000、高 32 位随机」, + * 根因是 zig musl 头(time_t 64 位)与 libc 实现(o32 time32 取数)ABI 不一致,见 + * sdk/CLOCK_ABI.md。修复后取数统一走 meow_time_ms_from_parts(sec, nsec),本测试把 + * 换算边界(毫秒进位/截断/大秒数/负值回绕)与「真时钟单调且前进」固定在宿主上。 + * + * 注意:宿主(非 mips)编译时 meow_ui_time.c 走 MEOW_TIME32_KERNEL_ABI=0 分支,即普通 + * clock_gettime —— 本测试**不能**证明 mipsel o32 路径正确(那条路径只靠反汇编证据)。 + * + * 编译(在 sdk/ 目录下): + * gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o + * gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time + * 运行:./test_meow_time (exit 0=PASS / 1=FAIL) + */ +#include +#include +#include + +#include "meow_ui.h" +#include "meow_ui_internal.h" + +static int g_fail = 0; +static unsigned long g_checks = 0; + +static void eq_ms(const char *name, int64_t sec, int64_t nsec, uint64_t want) +{ + uint64_t got = meow_time_ms_from_parts(sec, nsec); + g_checks++; + if (got != want) { + printf("FAIL %s: sec=%lld nsec=%lld got=%llu want=%llu\n", + name, (long long)sec, (long long)nsec, + (unsigned long long)got, (unsigned long long)want); + g_fail = 1; + } else { + printf("ok %s = %llu\n", name, (unsigned long long)got); + } +} + +int main(void) +{ + /* ---- 1) 换算边界:毫秒 = sec*1000 + nsec/1e6(整除截断) ---- */ + eq_ms("零", 0, 0, 0); + eq_ms("(sec,0) 整秒", 1, 0, 1000); + eq_ms("999999ns 截断", 0, 999999, 0); + eq_ms("1ms 进位", 0, 1000000, 1); + eq_ms("1ns 截断", 0, 1, 0); + eq_ms("开机 1135.008s", 1135, 8000000, 1135008); + eq_ms("跨秒 1.999999999s", 1, 999999999, 1999); + eq_ms("开机 1204.999999999s", 1204, 999999999, 1204999); + eq_ms("接近 2038", 2147483647, 999999999, 2147483647999ULL); + + /* ---- 2) 负值:from_parts 只做无符号升位((uint64_t)sec*1000 按二补回绕),不钳制。 + * 真正把内核 time32 的负 sec 钳成 0 的是 meow_ui_time.c 的 time32_clamp(),它只在 + * mipsel 分支编译、宿主不可达;这里把 from_parts 的回绕语义钉住,防止有人悄悄把它 + * 理解成「负值 = 0」而改掉换算。 ---- */ + eq_ms("负 sec 回绕(-1s)", -1, 0, 0xFFFFFFFFFFFFFC18ULL); + eq_ms("负 sec+1ms 回绕", -1, 1000000, 0xFFFFFFFFFFFFFC19ULL); + + /* ---- 3) 真时钟:非零、单调不减、量级合理(不上溢成「高 32 随机」) ---- */ + uint64_t prev = meow_ui_time_ms(); + g_checks++; + if (prev == 0) { + printf("FAIL 时钟返回 0(clock_gettime 失败?)\n"); + g_fail = 1; + } + g_checks++; + if (prev >= (1ULL << 40)) { + printf("FAIL 时钟量级异常(>=2^40 ms):%llu\n", (unsigned long long)prev); + g_fail = 1; + } + /* 有界采样(避免时钟停走时测试挂死):要求单调不减且确实前进 */ + int advanced = 0; + for (int i = 0; i < 200000 && !advanced; i++) { + uint64_t now = meow_ui_time_ms(); + if (now < prev) { + printf("FAIL 单调倒退:%llu -> %llu\n", + (unsigned long long)prev, (unsigned long long)now); + g_fail = 1; + break; + } + advanced = now > prev; + prev = now; + } + g_checks++; + if (!advanced) { + printf("FAIL 时钟未前进(200000 次采样内无变化)\n"); + g_fail = 1; + } else { + printf("ok 时钟前进且单调\n"); + } + + /* ---- 4) 动画常用差值:两次取样的差 = 毫秒(不能是 0 恒等/整千) ---- */ + uint64_t t0 = meow_ui_time_ms(); + int waited = 0; + for (int i = 0; i < 200000 && !waited; i++) + waited = meow_ui_time_ms() != t0; + uint64_t t1 = meow_ui_time_ms(); + g_checks++; + if (!waited || t1 <= t0 || t1 - t0 > 5000) { + printf("FAIL 取样差值异常:%llu -> %llu\n", + (unsigned long long)t0, (unsigned long long)t1); + g_fail = 1; + } else { + printf("ok 取样差值 %llu ms\n", (unsigned long long)(t1 - t0)); + } + + printf(g_fail ? "== FAIL ==\n" : "== PASS ==\n"); + printf("checks=%lu\n", g_checks); + return g_fail; +} diff --git a/tuku/test/run_tests.sh b/tuku/test/run_tests.sh index 2c35f8a..0f8417e 100755 --- a/tuku/test/run_tests.sh +++ b/tuku/test/run_tests.sh @@ -63,7 +63,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \ test/test_settings_lock_host.c ui/settings/settings.c ui/lock/lock.c \ logic/pin.c logic/reader.c \ - "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ + "$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ "$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \ "$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \ @@ -81,7 +81,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then test/test_browse_ui_host.c ui/library/library.c logic/library.c \ logic/reader.c logic/reader/page_source.c logic/reader/archive_source.c \ logic/reader/page_cache.c logic/zip/zip.c \ - "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ + "$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ "$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \ "$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \ @@ -99,7 +99,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then logic/library.c logic/reader.c logic/reader/page_source.c \ logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \ logic/reader/nav.c \ - "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ + "$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ "$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \ "$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \ @@ -116,7 +116,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then logic/library.c logic/reader.c logic/reader/page_source.c \ logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \ logic/reader/nav.c \ - "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ + "$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ "$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \ "$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \ @@ -133,7 +133,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then logic/library.c logic/reader.c logic/reader/page_source.c \ logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \ logic/reader/nav.c \ - "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ + "$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ "$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \ "$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \ diff --git a/tuku/test/run_webtoon_tests.sh b/tuku/test/run_webtoon_tests.sh index 81f5d9d..bc945bb 100755 --- a/tuku/test/run_webtoon_tests.sh +++ b/tuku/test/run_webtoon_tests.sh @@ -34,7 +34,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then logic/library.c logic/reader.c logic/reader/page_source.c \ logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \ logic/reader/nav.c \ - "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ + "$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \ "$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \ "$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \