从这份 lsblk 输出可以确认:

- nvme0n1p1 1G → **EFI 系统分区**(`/boot/efi`)
- nvme0n1p2 2G → /boot 分区
- nvme0n1p3 1.8T → 物理卷,其下逻辑卷 ubuntu--vg-ubuntu--lv 才是 真正的根(/)

下面给出**完全离线**的挂载 + GRUB 重装命令,直接复制即可。

---

### 1. 启动 LiveCD → 打开终端
“Try Ubuntu” 进入桌面后 `Ctrl+Alt+T`。

---

### 2. 激活 LVM 并挂载各分区
sudo vgchange -ay          # 激活卷组(否则逻辑卷不可见)
sudo mkdir -p /mnt/ubuntu
sudo mount /dev/mapper/ubuntu--vg-ubuntu--lv /mnt/ubuntu   # 挂载根(LVM)
sudo mount /dev/nvme0n1p2 /mnt/ubuntu/boot                 # 挂载 /boot
sudo mount /dev/nvme0n1p1 /mnt/ubuntu/boot/efi              # 挂载 EFI
# 绑定虚拟文件系统
for i in dev proc sys; do sudo mount --bind /$i /mnt/ubuntu/$i; done

---

### 3. 进入原系统环境
sudo chroot /mnt/ubuntu /bin/bash

---

### 4. 重装 GRUB(UEFI 模式,离线)
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu --removable
update-grub

---

### 5. 退出并重启
exit
sudo reboot
拔掉 LiveCD,GRUB 菜单应已恢复,可正常进入 Ubuntu。
因为 soluna 支持写 app 应用,而我最近正好打算为我父母和妻子编写一个抖音直播录制工具 app,想要试试。不过这有个问题,就是录制部分需要使用 ffmpeg 来进行,而 ltask 虽然可以 N:M 调度 lua 虚拟机,但是如果像 ffmpeg 这种阻塞调用,就会导致线程阻塞了。
当然,解决方法也是有的,比如引入事件循环,将 fork 的进程挂载到事件循环上,比如使用 bee.lua 就是个不错的选择。不过这意味着需要修改 soluna 的源码,在虚拟机注入 bee.lua。但我又不太想修改源码。首先这个功能对于 soluna 本身没有什么必要,因此我倾向于不到上游提需求;其次,我也不想修改源码,方便后续同步上游最新更新。
这时候我突然理解了 C 语言的一个优点了。
ltask 预留了一个 -DLTASK_EXTERNAL_OPENLIBS 选项,soluna 本身的库就是通过 -DLTASK_EXTERNAL_OPENLIBS=soluna_openlibs 在编译时注入符号的。
这意味着,我只需要把 soluna 设置为我的子仓库依赖,然后创建一个例如 soluna_app.c 的文件。在这个文件里面我创建一个 soluna_app_openlibs 函数,它内部除了调用 soluna_openlibs 还注入 bee.lua。最后在编译时,我把定义改成 -DLTASK_EXTERNAL_OPENLIBS=soluna_app_openlibs 从而就可以实现不更改源码,增量编译增加功能
yoga 在排版中的具体作用是什么呢?举个简单的例子,就拿 soluna/test/layout.lua 来举例:
我们创建如下一个 hud 排版, 它是一个简单的左右结构。其中左边我们定义了宽度为 400, 右边则是填满剩下的宽度。两者都填充满高度。另外右边是上下结构,其中第一个 node 占了70%的宽度,第二个占了30%的宽度。

local hud = [[
id : screen
padding : 10
direction : row
gap : 10
left :
 width : 400
 background : 0x40000000
right :
 flex : 1
 gap : 10
 node :
  flex : 0.7
  background : 0x40ffffff
 node :
  flex : 0.3
  background : 0x40ffffff
]]

此时单单使用这个 hud 我们还做不了什么。
紧接着我们在脚本入参中,可以获得启动时窗口的宽高, 我们可以将其设置到 screen 上:

local dom = layout.load(datalist.parse_list(hud))
local screen = dom.screen

local function calc_hub()
 screen.width = args.width
 screen.height = args.height
 return layout.calc(dom)
end

上面这段代码做了三件事: 加载布局到 yoga 中, 然后为 screen 设置宽高, 接着调用 calc 计算布局。
最后,假设宽是800,高是600,那么 yoga 会根据 left 的宽,计算出 right 的宽度为 400, 两者的高度都是 600; 而在 right 中第一个 node 的高 420, 第二个则是 180.
最后我们将计算结果使用 matquad 材质构建出实体:

local function draw_hud()
 for _, obj in ipairs(draw_list) do
  args.batch:add(matquad.quad(obj.w, obj.h, obj.background), obj.x, obj.y)
 end
end

并使用 batch:add 提交绘制命令,告诉框架,我要创建一个四边形(quad), 它宽w高h, 使用背景色 background, 绘制时的坐标是 x 和 y。
最后在 frame 中调用该函数,也就是每一帧都绘制。
当窗口发生变化时,我们又更新宽高,然后重新计算 node 布局信息,这样就可以在下一帧及时更新整体布局。
batch 是 soluna 在调用我们编写的 main.lua 时传入的参数。我们绘制时只需调用 batch:add, 这意味着,我们可以在任意地方调用,并不是说一定要在同一个地方调用。这有助于低耦合,高内聚。当我们需要绘制多个模块的 ui 时,每个 ui 可以独立传入 batch, 在内部实现排版和绘制。一般来说最佳实践是每个模块都具有一个 draw 函数和 update 函数。这样遍历多个模块时,聚合的函数只需要先 update 再 draw, 而不需要关注具体内部实现,各个模块也是独立实现的。 deepfuture 就是如此做。
在绘制 ui 时,我们可以有很多个 node,而不仅仅是一个 node。例如,bg 是一个 node, hud 也是一个 node。每个 node 都需要通过调用 calc 方法来更新结构。因此当整个界面的不同部位更新频率不同时,我们可以将其分开成多个 node,单独 calc 其变更,避免每帧都需要对整个 ui 重新计算,减轻压力。另外一个 node 中的 id 都是唯一的,而在多个 node 中 id 是可以重复的。这个 id 只是 yoga 用来计算使用,或者我们可以通过 id 来获取和设置 yoga 中的布局信息。这是我一开始误解的一点。我使用 soluna 编写一个 todo app 时,将整个页面都描绘成一个固定的 node ,结果在更新、计算时,遇到了动态添加的困难。
当我静下心来仔细阅读 deepfuture 代码时,发现了这一点。
soluna 使用 facebook/yoga 作为排版工具。这个工具是 react native 的基础设施,仅仅是负责排版,不负责渲染。在 soluna 中,我们可以用 datalist 描述排版,然后将其载入到 yoga 中,生成一个 node, 接着根据 node 结构来生成渲染指令。这里的渲染指令是指传入 batch:add 的参数。因为实际渲染都是通过 batch 来实现的。关于 batch:add 函数的 api 的函数签名描述如下:

- batch:add(obj [, x [, y]])
- obj 可以是: 正整数 sprite id;材质模块返回的 userdata(例如 matquad.quad、mattext.block(…) 的结果);或已经打包好的二进制流。附带坐标则先平移后提交。

可以说是很简单了,把材质和坐标放进去就可以绘制。
但是正因为如此简单,如果我们要直接使用它来绘制整个画面,将会较为复杂。这就是为什么我们需要一个排版工具。这个排版工具实际上就是通过一系列抽象,使得我们在设计某些具体细节时,不需要关注全局。最后通过一次 calc 计算,替我们计算某个节点的全局坐标,这样我们就可以较为轻松的绘制。当然,如果有别的排版工具,也完全可以不使用 yoga 进行排版,这不是一个必要条件。
最近开始尝试学习如何使用 soluna,但是我太过急躁了,只是弄清了一点 BATCH (批渲染) API 的用法,就急不可耐地试图编写一个 TODO APP, 而且还是让 AI 写的。其结果就是代码质量失控,我完全不知道 AI 写了什么,任凭其忽悠。于是我变得急躁,失去耐心,失去谦虚。最后搞砸了一切,从头再来
最近给 soluna 提交了一些 pr
接下来还可以添加一个 icon 特性,然后 deepfuture 可以添加设置界面
内置还要有一个 config 服务,这样插件可以使用该服务创建和读取配置。例如刮削,需要读取 tmdb api key ,没有时,需要页面展示配置指引
同理, 刮削服务 scrap.lua 也可以被 download 通知启动刮削
然后再创建一个 download 服务,调用 rss.lua 提供的 notify 接口注册通知。 rss 有更新时就会通知到 download 服务进行下载。
service 内置一个 plugin.lua ,专门启动插件 service。例如,我们有一个 service 叫作 rss.lua ,在插件页面点击下载后,会携带插件代码,ltask.call plugin service 请求创建 rss service,定期检查 rss ,并注册一个路由用来展示 rss 功能页面。
Back to Top