从这份
- nvme0n1p1 1G → **EFI 系统分区**(`/boot/efi`)
- nvme0n1p2 2G → /boot 分区
- nvme0n1p3 1.8T → 物理卷,其下逻辑卷
下面给出**完全离线**的挂载 + GRUB 重装命令,直接复制即可。
---
### 1. 启动 LiveCD → 打开终端
“Try Ubuntu” 进入桌面后 `Ctrl+Alt+T`。
---
### 2. 激活 LVM 并挂载各分区
---
### 3. 进入原系统环境
---
### 4. 重装 GRUB(UEFI 模式,离线)
---
### 5. 退出并重启
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 从而就可以实现不更改源码,增量编译增加功能
当然,解决方法也是有的,比如引入事件循环,将 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 在排版中的具体作用是什么呢?举个简单的例子,就拿
我们创建如下一个 hud 排版, 它是一个简单的左右结构。其中左边我们定义了宽度为 400, 右边则是填满剩下的宽度。两者都填充满高度。另外右边是上下结构,其中第一个 node 占了70%的宽度,第二个占了30%的宽度。
此时单单使用这个 hud 我们还做不了什么。
紧接着我们在脚本入参中,可以获得启动时窗口的宽高, 我们可以将其设置到 screen 上:
上面这段代码做了三件事: 加载布局到 yoga 中, 然后为 screen 设置宽高, 接着调用 calc 计算布局。
最后,假设宽是800,高是600,那么 yoga 会根据 left 的宽,计算出 right 的宽度为 400, 两者的高度都是 600; 而在 right 中第一个 node 的高 420, 第二个则是 180.
最后我们将计算结果使用 matquad 材质构建出实体:
并使用
最后在 frame 中调用该函数,也就是每一帧都绘制。
当窗口发生变化时,我们又更新宽高,然后重新计算 node 布局信息,这样就可以在下一帧及时更新整体布局。
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 代码时,发现了这一点。
当我静下心来仔细阅读 deepfuture 代码时,发现了这一点。
soluna 使用 facebook/yoga 作为排版工具。这个工具是 react native 的基础设施,仅仅是负责排版,不负责渲染。在 soluna 中,我们可以用 datalist 描述排版,然后将其载入到 yoga 中,生成一个 node, 接着根据 node 结构来生成渲染指令。这里的渲染指令是指传入 batch:add 的参数。因为实际渲染都是通过 batch 来实现的。关于 batch:add 函数的 api 的函数签名描述如下:
可以说是很简单了,把材质和坐标放进去就可以绘制。
但是正因为如此简单,如果我们要直接使用它来绘制整个画面,将会较为复杂。这就是为什么我们需要一个排版工具。这个排版工具实际上就是通过一系列抽象,使得我们在设计某些具体细节时,不需要关注全局。最后通过一次 calc 计算,替我们计算某个节点的全局坐标,这样我们就可以较为轻松的绘制。当然,如果有别的排版工具,也完全可以不使用 yoga 进行排版,这不是一个必要条件。
- batch:add(obj [, x [, y]])
- obj 可以是: 正整数 sprite id;材质模块返回的 userdata(例如 matquad.quad、mattext.block(…) 的结果);或已经打包好的二进制流。附带坐标则先平移后提交。
可以说是很简单了,把材质和坐标放进去就可以绘制。
但是正因为如此简单,如果我们要直接使用它来绘制整个画面,将会较为复杂。这就是为什么我们需要一个排版工具。这个排版工具实际上就是通过一系列抽象,使得我们在设计某些具体细节时,不需要关注全局。最后通过一次 calc 计算,替我们计算某个节点的全局坐标,这样我们就可以较为轻松的绘制。当然,如果有别的排版工具,也完全可以不使用 yoga 进行排版,这不是一个必要条件。
最近开始尝试学习如何使用 soluna,但是我太过急躁了,只是弄清了一点 BATCH (批渲染) API 的用法,就急不可耐地试图编写一个 TODO APP, 而且还是让 AI 写的。其结果就是代码质量失控,我完全不知道 AI 写了什么,任凭其忽悠。于是我变得急躁,失去耐心,失去谦虚。最后搞砸了一切,从头再来