mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-02 03:52:43 +08:00
[perf] optimize CLI ANSI output and refresh previews
This commit is contained in:
@@ -99,7 +99,7 @@ crossdesk-cli 123456789 --renderer ansi --fps 8
|
||||
|
||||
媒体回调仅同步复制借用的 NV12 数据,校验尺寸、容量、native 描述符与 stride;不保留借用指针。上限单帧 16,777,216 像素、每边 8192、偶数尺寸。最多一个待转换帧,替换前先完成校验与复制,无效帧不会挤掉已有画面;播放器另保留最近转换的一帧用于静止桌面的本地放大/重绘,并在接收新帧时回收上一帧缓冲区供后续复制使用。只丢弃解码后图像。显示器切换/关闭提升 generation,旧帧失效。
|
||||
|
||||
转换与输出在拥有会话的事件循环,媒体回调独立。输出至多一个完整消息(2MiB),按令牌预算分块非阻塞写入,突发额度上限仍为 1MiB。本地 Kitty 使用 8MiB/s;存在 `SSH_CONNECTION`、`SSH_CLIENT` 或 `SSH_TTY` 时,Kitty 保持 1MiB/s,ANSI 和 Pi 桥接也保持 1MiB/s。不能开始新画面时保留最新解码帧。ANSI 差分基准只在整帧输出完成后提交。Pi 桥接另要求消费帧 ACK,最多一帧等待 ACK,输入映射只在对应 ACK 后切换到新网格。状态栏优先在完整画面输出之间更新,避免大图持续刷新时状态饥饿;可写事件唤醒短写重试,仍遵守字节预算。控制事件至多 64 条,协议输入 64KiB/嵌套 32 层,输入粘贴缓冲 32KiB,文本单次最多 16KiB。
|
||||
转换与输出在拥有会话的事件循环,媒体回调独立。输出至多一个完整消息(2MiB),按令牌预算分块非阻塞写入,突发额度上限仍为 1MiB。本地交互 ANSI 和 Kitty 使用 8MiB/s;存在 `SSH_CONNECTION`、`SSH_CLIENT` 或 `SSH_TTY` 时,两者保持 1MiB/s,Pi 桥接始终保持 1MiB/s。不能开始新画面时保留最新解码帧。ANSI 差分按终端实际使用的颜色与字形比较,忽略量化后相同的颜色和空格不可见的前景色;同一行可输出多个变化片段,并按光标定位、颜色切换和字符的实际字节成本合并短间隔。差分基准只在整帧输出完成后提交。Pi 桥接另要求消费帧 ACK,最多一帧等待 ACK,输入映射只在对应 ACK 后切换到新网格。状态栏优先在完整画面输出之间更新,避免大图持续刷新时状态饥饿;可写事件唤醒短写重试,仍遵守字节预算。控制事件至多 64 条,协议输入 64KiB/嵌套 32 层,输入粘贴缓冲 32KiB,文本单次最多 16KiB。
|
||||
|
||||
测量像素渲染帧率时使用 release 构建;debug 的 `-O0` 面积采样开销明显更高。共享内存中的像素不经过终端字节预算,但仍受转换和终端消费速度限制;8MiB/s 是内联路径的输出上限,不是帧率保证。解码、像素转换、压缩、终端消费速度和帧大小都会影响结果。退出时,未完成画面最多继续写入 2s;取消图像传输及恢复屏幕的短写最多另重试 500ms。POSIX 立即恢复 termios 并清理输入,不等待输出排空;如果终端始终不消费输出,屏幕清理只能尽力发送。
|
||||
|
||||
|
||||
@@ -122,6 +122,16 @@
|
||||
- SSH 环境保持原有 1MiB/s 字节预算且不探测共享内存;慢速输出降远端请求但保留本地目标。ANSI 默认 30/显式 15 和 bridge 默认 8/帧 ACK 通过。所有 Kitty 场景继续验证不显示未确认帧、最多两个终端图像、共享对象释放及退出后终端模式恢复。
|
||||
- 以上数字是合成源和伪终端的完整输出接收率,不包含真实视频解码、网络或 Ghostty GPU 呈现。全部额外测试代码、数据、构建脚本和产物位于工程外系统临时目录;本次未新增工程内测试,尚未对新版进行 Ghostty 实窗验收。
|
||||
|
||||
## ANSI 多片段差分与本地吞吐(2026-10-31)
|
||||
|
||||
- 本地交互 ANSI 的持续输出预算由 1MiB/s 提高到 8MiB/s;SSH 与 bridge 仍为 1MiB/s。保留 2MiB 单消息、1MiB 突发、非阻塞短写、最新帧替换和完整输出后提交差分基准的约束。
|
||||
- 同一行的分散修改按多个片段输出,短间隔按实际编码字节成本合并,比较包含下一单元的颜色设置。差分忽略色板量化后相同的颜色、空格前景色以及 ASCII 模式不显示的字形/背景字段;未修改采样、象限拟合及 Kitty 传输。
|
||||
- macOS arm64 CLI debug/release 构建及两种产物的 `doctor --json` 通过,完成后工程配置恢复为桌面与 CLI 同时启用的 debug。工程外 ASan/UBSan 检查通过:全部四象限字形、8/24/256 色、ASCII、首尾单元、长短间隔、色板等价、多种尺寸及偏移变化、600 帧连续随机更新与整帧重绘的屏幕一致性;140 组转换逐格一致,bridge 行输出及 Kitty 数据保持一致。
|
||||
- `-O2`、240×100 合成网格:10 行各两处分散修改由 90,058 降到 931 字节;隔列修改由 919,260 降到 562,601 字节;量化后颜色相同的整屏变化由 72,716 降到 0 字节。高色彩变化整帧仍为 922,929 字节,2×3 光标修改仍为 262 字节。多片段选择会增加部分交错修改场景的编码计算,以减少终端输出量。
|
||||
- 使用生产 CLI/渲染器的 `-O3` 构建、1920×1080 连续变化合成 NV12、240×100 字符视口、固定目标 30 FPS 的本地 PTY 对比:跳过启动阶段后的完整帧接收速率约 1.64 → 6.42 FPS,约 3.9 倍;平均每帧约 640 KB。分别设置 `SSH_TTY`、`SSH_CONNECTION`、`SSH_CLIENT` 时预算均保持 1MiB/s;设置 `TMUX` 的本地 ANSI 路径使用 8MiB/s。
|
||||
- 暂停 PTY 读取 1.3s,并在暂停期间发送退出命令,恢复读取后完整帧计数与提交计数一致,约 1.17s 内退出,备用屏幕、termios 和非阻塞标志恢复。没有连接真实远端,也未验证真实终端的 GPU 绘制、SSH 或 tmux 实际显示;上述帧率为合成源的完整输出接收率。
|
||||
- 额外检查源码、脚本、数据和产物均位于工程外系统临时目录,未新增或修改工程内测试文件/用例。
|
||||
|
||||
## 实测边界
|
||||
|
||||
本地检查不涵盖 Linux / Windows 完整构建、跨平台远程控制、Kitty/SSH 实际终端显示、真实 Pi TUI 或长时间资源占用。五个平台 CI 配置与 npm 包名均不代表已发布或已验收。
|
||||
|
||||
+4
-3
@@ -350,12 +350,13 @@ int View(SessionOptions options, const std::string &device,
|
||||
if (renderer == "kitty" && !kitty)
|
||||
throw std::runtime_error(
|
||||
"Kitty probe failed or multiplexer active; use --renderer ansi");
|
||||
// Local pixel frames need more throughput than character output. Keep the
|
||||
// conservative budget for SSH and the existing one-message/burst limits.
|
||||
// Both pixel frames and large true-colour character grids need local
|
||||
// throughput. Keep SSH conservative and retain the one-message/burst limits
|
||||
// and nonblocking writes so slow terminals still apply backpressure.
|
||||
const bool local_terminal = Env("SSH_CONNECTION").empty() &&
|
||||
Env("SSH_CLIENT").empty() &&
|
||||
Env("SSH_TTY").empty();
|
||||
if (kitty && local_terminal)
|
||||
if (local_terminal)
|
||||
output.SetByteRate(8 * 1024 * 1024);
|
||||
SharedImage shared_image;
|
||||
bool shared_enabled = false;
|
||||
|
||||
@@ -44,14 +44,14 @@ On **2026-10-25**, 16 images were rendered from the actual Slint components at `
|
||||
|
||||
## CLI 图示 / CLI captures
|
||||
|
||||
- **2026-10-31 04:56(UTC+8)重新采集**,使用在 macOS arm64 上从 `22796368` 构建的 release CLI,连接同一用户授权的被控设备,分别采集 ANSI 与 Kitty 输出。两次连接均保持只读;图示展示远端播放动画时的不同画面。
|
||||
- 使用工程外的 POSIX 伪终端采集器,视口为 **128 列 × 40 行**,每格 **10 × 20 像素**,输出 PNG 为 **1280 × 800**。顶栏与底部快捷键直接来自 CLI 输出;图示不包含连接密码或真实设备 ID。
|
||||
- ANSI 图由实际转义序列恢复字符网格及前景/背景色,四象限字符按对应的色块渲染。Kitty 图使用本地共享内存传输:采集器通过 `mmap` 复制完整 RGB 帧后返回确认,按协议 placement 的行列尺寸显示。采集时只读取状态栏所需的字符,保存图片时才做最终缩放和 PNG 渲染,减少采集器对输出的阻塞。
|
||||
- 截图中的 Kitty `FPS 27`、ANSI `FPS 7` 是当时统计区间的实际输出值。会话日志确认 Kitty 使用 `shared_memory`,未发生回退;静止画面下 ANSI 的差分为空时不产生新的输出计数,可显示 `FPS 0`。本轮同时缩小了视口并优化采集器,不能据此单独量化窗口大小的性能收益。
|
||||
- 这些图片是**实际 CLI 输出的渲染图**。Ghostty 和 macOS「终端」窗口的自动化访问被工具拒绝,未取得原生窗口截图,也未据此完成这两款应用的实窗验收。字体栅格、图像缩放和显示时序由采集器处理,不能用图中 FPS 推断 Ghostty 或其他终端的实际显示性能。
|
||||
- 构建配置、采集脚本、原始输出与中间产物均存放在工程外临时目录;工程内只保存最终图示及文档。示例命令使用占位 ID,凭据通过隐藏提示输入。
|
||||
- **2026-10-31 05:17–05:18(UTC+8)重新采集**,使用在 macOS arm64 上从 `1b716f36` 及当前工作区 ANSI 优化重新构建的 release CLI,连接同一用户授权的被控设备。两次连接均保持只读,展示远端播放动画时的不同画面。
|
||||
- 沿用工程外的 POSIX 伪终端采集器,视口为 **128 列 × 40 行**,每格 **10 × 20 像素**,输出 PNG 为 **1280 × 800**。顶栏与底部快捷键直接来自 CLI 输出;图示不包含连接密码或真实设备 ID。
|
||||
- ANSI 图由实际转义序列恢复字符网格及前景/背景色,四象限字符按对应色块渲染;采集器等待完整 ANSI 帧结束后保存,支持本轮新增的行内多片段差分。Kitty 图使用本地共享内存传输:采集器通过 `mmap` 复制完整 RGB 帧后返回确认,按协议 placement 的行列尺寸显示。采集时只读取状态栏所需的字符,保存图片时才做最终缩放和 PNG 渲染。
|
||||
- 图中的 ANSI `FPS 26`、Kitty `FPS 24` 是对应统计区间的实际完整输出次数。会话日志确认本地输出预算均为 8MiB/s,Kitty 使用 `shared_memory`,未发生回退;两次连接正常退出。源分辨率与画面内容不同,不能用这两张图直接比较后端性能或量化本次优化收益。静止画面下 ANSI 差分为空时不产生新的输出计数,可显示 `FPS 0`。
|
||||
- 这些图片是**实际 CLI 输出的渲染图**,本轮沿用 PTY 采集,没有取得 Ghostty 或 macOS「终端」的原生窗口截图。字体栅格、图像缩放和显示时序由采集器处理,不能用图中 FPS 推断终端的实际显示性能。
|
||||
- 采集脚本、原始输出、构建日志、源码/二进制哈希和中间产物均存放在工程外临时目录;工程内只更新最终图示及来源说明。示例命令使用占位 ID,凭据通过隐藏提示输入;构建后已恢复原来的 debug 配置。
|
||||
|
||||
Refreshed at **04:56 (UTC+8) on 2026-10-31** with a macOS arm64 release CLI built from `22796368`, using real read-only connections to the same authorized host during animation playback. An external POSIX PTY collector reports 128 × 40 cells at 10 × 20 pixels and renders the actual CLI output to PNG. The ANSI image reconstructs character cells and colors. The Kitty collector copies each complete shared-memory RGB frame through `mmap` before acknowledging it, then applies placement dimensions when saving the image. Status polling reads only the top row, and final image scaling and PNG rendering happen only when saving. The captured Kitty `FPS 27` and ANSI `FPS 7` are interval output counts; an unchanged ANSI view can show `FPS 0`. Both viewport size and collector processing changed, so this is not an isolated window-size benchmark. These output renderings do not validate native Ghostty or macOS Terminal rendering or GPU performance. Passwords and real device IDs are omitted. Capture scripts, raw output, build configuration, and intermediate files stay outside the repository.
|
||||
Refreshed at **05:17–05:18 (UTC+8) on 2026-10-31** with a freshly rebuilt macOS arm64 release CLI from `1b716f36` plus the working-tree ANSI optimizations, using real read-only connections to the same authorized host during animation playback. The external POSIX PTY collector retains 128 × 40 cells at 10 × 20 pixels and renders the actual CLI output to 1280 × 800 PNGs. The ANSI collector reconstructs character cells and colors, supports multiple changed spans per row, and waits for a complete frame before saving. The Kitty collector copies each complete shared-memory RGB frame through `mmap` before acknowledging it and applies placement dimensions when saving. The captured ANSI `FPS 26` and Kitty `FPS 24` are interval counts of complete outputs. Both sessions used an 8MiB/s local output budget and exited normally; Kitty used shared memory with no fallback. Source resolutions and scene content differ, so these images are not a direct backend comparison or an isolated optimization benchmark. An unchanged ANSI view can show `FPS 0`. These output renderings do not validate native Ghostty or macOS Terminal rendering or GPU performance. Passwords and real device IDs are omitted. Scripts, raw output, build logs, source/binary hashes, and intermediate files stay outside the repository; the original debug configuration was restored.
|
||||
|
||||
重新采集时,使用 release CLI,在支持相应协议的终端或 PTY 采集器中分别运行 `crossdesk-cli <设备ID> --renderer ansi --quality high` 和 `crossdesk-cli <设备ID> --renderer kitty --quality high`,通过隐藏提示输入密码,等待顶栏显示 `Displaying` 且统计稳定后捕获完整视口,再按 `q` 退出。若使用 PTY 采集器,须正确处理终端尺寸回复、ANSI 光标与颜色、Kitty 探测、分块上传、共享内存读取与确认,以及 placement 缩放,并在图注中明确采集方式。更新图片前检查敏感信息、画面比例和状态栏完整性,并同步更新本节的日期、版本与验证范围。
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 830 KiB After Width: | Height: | Size: 821 KiB |
@@ -430,6 +430,23 @@ static void AppendCells(std::string &out, const Grid &g, int y, int begin,
|
||||
AppendUtf8(out, g.viewport.utf8 ? c.glyph : 0x20);
|
||||
}
|
||||
}
|
||||
static bool SameTerminalCell(const Cell &a, const Cell &b, const Viewport &v) {
|
||||
if (a == b)
|
||||
return true;
|
||||
if (!v.utf8)
|
||||
return TerminalColor(a.top, v.colors) == TerminalColor(b.top, v.colors);
|
||||
return a.glyph == b.glyph &&
|
||||
TerminalColor(a.bottom, v.colors) == TerminalColor(b.bottom, v.colors) &&
|
||||
(a.glyph == 0x20 ||
|
||||
TerminalColor(a.top, v.colors) == TerminalColor(b.top, v.colors));
|
||||
}
|
||||
static void AppendCursor(std::string &out, int row, int column) {
|
||||
out += "\033[";
|
||||
AppendNumber(out, row + 1);
|
||||
out += ';';
|
||||
AppendNumber(out, column + 1);
|
||||
out += 'H';
|
||||
}
|
||||
std::vector<std::string> Lines(const Grid &g) {
|
||||
std::vector<std::string> lines;
|
||||
lines.reserve(g.viewport.rows);
|
||||
@@ -445,6 +462,7 @@ std::vector<std::string> Lines(const Grid &g) {
|
||||
}
|
||||
std::string Difference(const Grid &g, const std::optional<Grid> &previous) {
|
||||
std::string out;
|
||||
std::string skipped, joined;
|
||||
AnsiColors colors;
|
||||
const bool same = previous &&
|
||||
previous->viewport.columns == g.viewport.columns &&
|
||||
@@ -454,24 +472,54 @@ std::string Difference(const Grid &g, const std::optional<Grid> &previous) {
|
||||
previous->viewport.utf8 == g.viewport.utf8 &&
|
||||
previous->cells.size() == g.cells.size();
|
||||
for (int y = 0; y < g.viewport.rows; ++y) {
|
||||
const auto offset = size_t(y) * g.viewport.columns;
|
||||
const auto unchanged = [&](int x) {
|
||||
return SameTerminalCell(g.cells[offset + x], previous->cells[offset + x],
|
||||
g.viewport);
|
||||
};
|
||||
int begin = 0, end = g.viewport.columns;
|
||||
if (same) {
|
||||
const auto offset = size_t(y) * g.viewport.columns;
|
||||
while (begin < end &&
|
||||
g.cells[offset + begin] == previous->cells[offset + begin])
|
||||
while (begin < end && unchanged(begin))
|
||||
++begin;
|
||||
while (end > begin &&
|
||||
g.cells[offset + end - 1] == previous->cells[offset + end - 1])
|
||||
while (end > begin && unchanged(end - 1))
|
||||
--end;
|
||||
}
|
||||
if (begin == end)
|
||||
continue;
|
||||
out += "\033[";
|
||||
AppendNumber(out, g.viewport.row_offset + y + 1);
|
||||
out += ';';
|
||||
AppendNumber(out, begin + 1);
|
||||
out += 'H';
|
||||
AppendCells(out, g, y, begin, end, colors);
|
||||
AppendCursor(out, g.viewport.row_offset + y, begin);
|
||||
if (!same) {
|
||||
AppendCells(out, g, y, begin, end, colors);
|
||||
continue;
|
||||
}
|
||||
AppendCells(out, g, y, begin, begin + 1, colors);
|
||||
for (int x = begin + 1; x < end;) {
|
||||
const int start = x;
|
||||
if (!unchanged(x)) {
|
||||
while (++x < end && !unchanged(x)) {}
|
||||
AppendCells(out, g, y, start, x, colors);
|
||||
continue;
|
||||
}
|
||||
while (++x < end && unchanged(x)) {}
|
||||
// end already excludes the unchanged suffix, so x is a changed cell.
|
||||
// Compare actual bytes, including the next cell's SGR: crossing a short
|
||||
// gap can be cheaper than moving the cursor and restoring its colours.
|
||||
skipped.clear();
|
||||
auto skip_colors = colors;
|
||||
AppendCursor(skipped, g.viewport.row_offset + y, x);
|
||||
AppendCells(skipped, g, y, x, x + 1, skip_colors);
|
||||
joined.clear();
|
||||
auto join_colors = colors;
|
||||
for (int gap = start; gap <= x && joined.size() <= skipped.size(); ++gap)
|
||||
AppendCells(joined, g, y, gap, gap + 1, join_colors);
|
||||
if (joined.size() <= skipped.size()) {
|
||||
out += joined;
|
||||
colors = join_colors;
|
||||
} else {
|
||||
out += skipped;
|
||||
colors = skip_colors;
|
||||
}
|
||||
++x;
|
||||
}
|
||||
}
|
||||
if (!out.empty())
|
||||
out += "\033[0m";
|
||||
|
||||
Reference in New Issue
Block a user