阅读视图

发现新文章,点击刷新页面。
🔲 ☆

在鸿蒙电脑上的虚拟机内启动 Linux

在鸿蒙电脑上的虚拟机内启动 Linux

背景

最近在研究鸿蒙电脑,群友 @Fearyncess 摸索出了,如何在鸿蒙电脑上的虚拟机内启动 Linux,而不是 Windows。在此做个复现并记录。

方法

目前鸿蒙的应用市场上有两家虚拟机,我用 Oseasy 虚拟机,但是理论上铠大师也是可以的。(P.S. @driver1998 反馈:“铠大师测试也能启动,但键盘左右方向键的处理有点问题,虚拟机内收不到按键松开的信号,EFI 和 Linux 里 面都是这样。目前建议用 OSEasy。”)

首先需要在 U 盘上,把一个 UEFI arm64 的 Linux 安装盘写进去。我用的是 Ventoy + Debian Installer,理论上直接写例如 Debian 发行版的安装 ISO 也是可以的。

然后把 U 盘插到鸿蒙电脑上,打开 Windows 虚拟机,直通到虚拟机里面,保证虚拟机里面可以看到 U 盘。

接着,进入 Windows 磁盘管理,缩小 Windows 的 NTFS 分区,留出空间。注意 Windows 启动的时候会自动 growpart,所以装 Debian 前,不要回到 Windows。装好以后,可以继续用 Windows。

接着,重启 Windows,同时按住 Escape,进入 OVMF 的界面,然后选择 Boot Manager,从 U 盘启动,然后就进入 Ventoy 的界面了。(注:根据 @quiccat 群友提醒,在 Windows 内,通过设置->系统->恢复->高级启动->UEFI 固件设置也可以进入 OVMF 的设置界面)

剩下的就是正常的 Linux 安装过程了,分区的时候,注意保留 Windows 已有的 NTFS,可以和 Windows 用同一个 ESP 分区。网络的话,配置静态 IP 是 172.16.100.2,默认网关是 172.16.100.1 即可。重启以后,在 grub 界面,修改 linux 配置,在 cmdline 一栏添加 modprobe.blacklist=vmwgfx,这样就能启动了。内核版本是 Debian Bookworm 的 6.1。

各内核版本启动情况:

  • 5.10 from debian:正常
  • 6.1 from debian:正常
  • 6.2/6.3/6.4 from kernel.ubuntu.com: echo simpledrm > /etc/modules-load.d/simpledrm.conf 后正常,否则系统可以启动但是图形界面起不来
  • 6.5 from kernel.ubuntu.com:起不来,需要强制关机
  • 6.12 from debian:起不来,需要强制关机

经过 @Fearyncess 的二分,找到了导致问题的 commit

最终效果:

主要的遗憾是分辨率:屏幕两侧有黑边,并且由于宽度不同,触摸屏的位置映射会偏中间。

附录

Geekbench 6 测试结果:

如果没有 blacklist 的话,vmwgfx 驱动的报错:

vmwgfx 0000:00:04.0: [drm] FIFO at 0x0000000020000000 size is 2048 kiB vmwgfx 0000:00:04.0: [drm] VRAM at 0x0000000010000000 size is 262144 kiB vmwgfx 0000:00:04.0: [drm] *ERROR* Unsupported SVGA ID 0xffffffff on chipset 0x405 vmwgfx: probe of 0000:00:04.0 failed with error -38 

blacklist vmwgfx 后用的是 efifb:

[ 0.465898] pci 0000:00:04.0: BAR 1: assigned to efifb [ 1.197638] efifb: probing for efifb [ 1.197705] efifb: framebuffer at 0x10000000, using 7500k, total 7500k [ 1.197708] efifb: mode is 1600x1200x32, linelength=6400, pages=1 [ 1.197711] efifb: scrolling: redraw [ 1.197712] efifb: Truecolor: size=8:8:8:8, shift=24:16:8:0 

虚拟机的 IP 地址,从宿主机也可以直接访问,通过 WVMBr 访问,目测是直接 Tap 接出来,然后建了个 Bridge,外加 NAT,只是没有 DHCP。

融合开发引擎

2026/04/01 更新:《融合开发引擎》App 在应用市场的应用尝鲜上架,可以获得一个 Linux 环境,Linux 6.6.0 内核的 openeuler。使用可见网络上的视频 鸿蒙电脑官方欧拉虚拟机上线。想用 Debian 的话,也可以按照 HarmonyOS 6 Linux 容器替换成 debian trixie 换成 Debian。

🔲 ☆

终端模拟器的文字绘制

终端模拟器的文字绘制

背景

最近在造鸿蒙电脑上的终端模拟器 Termony,一开始用 ArkTS 的 Text + Span 空间来绘制终端,后来发现这样性能和可定制性比较差,就选择了自己用 OpenGL 实现,顺带学习了一下终端模拟器的文字绘制是什么样的一个过程。

读取字形

文本绘制,首先就要从字体文件中读取字形,提取出 Bitmap 来,然后把 Bitmap 绘制到该去的地方。为了提取这些信息,首先用 FreeType 库,它可以解析字体文件,然后计算出给定大小的给定字符的 Bitmap。但是,这个 Bitmap 它只记录字体非空白的部分(准确的说,是 Bounding Box),如下图的 width * height 部分:

(图源:Managing Glyphs - FreeType Tutorial II

其中 x 轴,应该是同一行的字体对齐的,这样才会看到有高有低的字符出现在同一行,而不是全部上对齐或者下对齐。得到的 Bitmap 是行优先的,也就是说:

  • 图中左上角,坐标 (xMin, yMax) 对应 Bitmap 数组的下标是 0
  • 图中右上角,坐标 (xMax, yMax) 对应 Bitmap 数组的下标是 width-1
  • 图中左下角,坐标 (xMin, yMin) 对应 Bitmap 数组的下标是 width*(height-1)
  • 图中右下角,坐标 (xMax, yMax) 对应 Bitmap 数组的下标是 width*(height-1)+width-1

得到这个 Bitmap 后,如果我们不用 OpenGL,而是直接生成 PNG,那就直接进行一次 copy 甚至 blend 就可以把文字绘制上去了。但是,我们要用 OpenGL 的 shader,就需要把 bitmap 放到 texture 里面。由于目前我们用的就是单色的字体,所以它对应只有一个 channel 的 texture。

OpenGL 的 texture,里面也是保存的 bitmap,但它的坐标系统的命名方式不太一样:它的水平向右方向是 U 轴,竖直向上方向是 V 轴,然后它的 bitmap 保存个数也是行优先,但是从 (0, 0) 坐标开始保存像素,然后 U 和 V 的范围都是 0 到 1。

所以,如果我们创建一个 width*height 的单通道 texture,直接把上面的 bitmap 拷贝到 texture 内部,实际的效果大概是:

 V  ^  |  C D  |  |  A------B--->U 

上图中几个点的坐标以及对应的 bitmap 数组的下标:

  • A 点:U = 0,V = 0,对应 bitmap 数组下标 0
  • B 点:U = 1,V = 0,对应 bitmap 数组下标 width-1
  • C 点:U = 0,V = 1,对应 bitmap 数组下标 width*(height-1)
  • D 点:U = 1,V = 1,对应 bitmap 数组下标 width*(height-1)+width-1

所以在向 OpenGL 的 texture 保存 bitmap 的时候,相当于做了一个上下翻转,不过这没有关系,后续在指定三角形顶点的 U V 坐标的时候,保证对应关系即可。

逐个字符绘制

有了这个基础以后,就可以实现逐个字符绘制:提前把所有要用到的字符,从字体提取出对应的 Bitmap,每个字符对应到一个 Texture。然后要绘制文字的时候,逐个字符,用对应的 Texture,在想要绘制的位置上,绘制一个字符。为了实现这个目的,写一个简单的 Shader:

// vertex shader #version 320 es  in vec4 vertex; // xy is position, zw is its texture coordinates out vec2 texCoors; // output texture coordinates void main() {  gl_Position.xy = vertex.xy;  gl_Position.z = 0.0; // we don't care about depth now  gl_Position.w = 1.0; // (x, y, z, w) corresponds to (x/w, y/w, z/w), so we set w = 1.0  texCoords = vertex.zw; } // fragment shader #version 320 es precision lowp float; in vec2 texCoords; out vec4 color; uniform sampler2D text; void main() {  float alpha = texture(text, texCoords).r;  color = vec4(1.0, 1.0, 1.0, alpha); } 

在这里,我们给每个顶点设置四个属性,包在一个 vec4 中:

  • xy:记录了这个顶点的坐标,x 和 y 范围都是 -1 到 1
  • zw:记录了这个顶点的 texture 坐标 u 和 v,范围都是 0 到 1

vertex shader 只是简单地把这些信息传递到顶点的坐标和 fragment shader。fragment shader 做的事情是:

  • 根据当前点经过插值出来的 u v 坐标,在 texture 中进行采样
  • 由于这个 texture 只有单通道,所以它的第一个 channel 也就是 texture(text, texCoords).r 就代表了这个字体在这个位置的 alpha 值
  • 然后把 alpha 值输出:(1.0, 1.0, 1.0, alpha),即带有 alpha 的白色

在绘制文字之前,先绘制好背景色,然后通过设置 blending function:

glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  

它使得 blending 采用如下的公式:

final = src * src.alpha + dest * (1 - src.alpha); 

这里 dest 就是绘制文本前的颜色,src 就是 fragment shader 输出的颜色,也就是 (1.0, 1.0, 1.0, alpha)。代入公式,就知道最终的结果是:

final.r = 1 * alpha + dest.r * (1 - alpha); final.g = 1 * alpha + dest.g * (1 - alpha); final.b = 1 * alpha + dest.b * (1 - alpha); 

也就是以 alpha 为不透明度,把白色和背景颜色进行了一次 blend。

如果要设置字体颜色,只需要修改一下 fragment shader:

#version 320 es precision lowp float; in vec2 texCoords; out vec4 color; uniform sampler2D text; uniform vec3 textColor; void main() {  float alpha = texture(text, texCoords).r;  color = vec4(textColor, alpha); } 

此时 src 等于 (textColor.r, textColor.g, textColor.b, alpha),经过融合后的结果为:

final.r = textColor.r * alpha + dest.r * (1 - alpha); final.g = textColor.g * alpha + dest.g * (1 - alpha); final.b = textColor.b * alpha + dest.b * (1 - alpha); 

即最终颜色,等于字体颜色和原来背景颜色,基于 bitmap 的 alpha 值的融合。

解决了颜色,接下来考虑如何设置顶点的信息。前面提到,得到的 bitmap 是一个矩形,而 OpenGL 绘图的基本元素是三角形,因此我们需要拆分成两个三角形来绘图,假如说要绘制一个矩形,它个四个顶点如下:

3-4 | | 1-2 

如果确定左下角 3 这个顶点的坐标是 (xpos, ypos),然后矩形的宽度是 w,高度是 h,考虑到 OpenGL 的坐标系也是向右 X 正方向,向上 Y 正方向,那么这四个顶点的坐标:

  • 顶点 1:(xpos, ypos)
  • 顶点 2:(xpos + w, ypos)
  • 顶点 3:(xpos, ypos + h)
  • 顶点 4:(xpos + w, ypos + h)

接下来考虑这些顶点对应的 uv 坐标。首先,我们知道这些顶点对应的 bitmap 的下标在哪里;然后我们又知道这些 bitmap 的下标对应的 uv 坐标,那就每个顶点找一次对应关系:

  • 顶点 1:(xpos, ypos),下标是 width*(height-1),uv 坐标是 (0, 1)
  • 顶点 2:(xpos + w, ypos),下标是 width*(height-1)+width-1,uv 坐标是 (1, 1)
  • 顶点 3:(xpos, ypos + h),下标是 0,uv 坐标是 (0, 0)
  • 顶点 4:(xpos + w, ypos + h),下标是 width-1,uv 坐标是 (1, 0)

为了绘制这个矩形,绘制两个三角形,分别是 3->1->2 和 3->2->4,一共六个顶点的 (x, y, u, v) 信息就是:

  • 3: (xpos , ypos + h, 0, 0)
  • 1: (xpos , ypos , 0, 1)
  • 2: (xpos + w, ypos , 1, 1)
  • 3: (xpos , ypos + h, 0, 0)
  • 2: (xpos + w, ypos , 1, 1)
  • 4: (xpos + w, ypos + h, 1, 0)

把这些数传递给 vertex shader,就可以画出来这个字符了。

最后还有一个小细节:上述的 xpos 和 ypos 说的是矩形左下角的坐标,但是我们画图的时候,实际上期望的是把字符都画到同一条线上。也就是说,我们指定 origin 的 xy 坐标,然后根据每个字符的 bearingX 和 bearingY 来算出它的矩形的左下角的坐标 xpos 和 ypos:

  • xpos = originX + bearingX
  • ypos = originY + bearingY - height

至此就实现了逐个字符绘制需要的所有内容。这也是 Text Rendering - Learn OpenGL 这篇文章所讲的内容。

Texture Atlas

上面这种逐字符绘制的方法比较简单,但是也有硬伤,比如每次绘制字符,都需要切换 texture,更新 buffer,再进行一次 glDrawArrays 进行绘制,效率比较低。所以一个想法是,把这些 bitmap 拼接起来,合成一个大的 texture,然后把每个字符在这个大的 texture 内的 uv 坐标保存下来。这样,可以一次性把所有字符的所有三角形都传递给 OpenGL,一次绘制完成,不涉及到 texture 的切换。这样效率会高很多。

具体到代码上,也就是分成两步:

  • bitmap 的拼接,这一步比较灵活,理想情况下是构造一个比较紧密的排布,但也可以留一些空间,直接对齐到最大宽度/高度的整数倍网格上,然后进行 uv 坐标的计算
  • 剩下的,就是在计算顶点信息的时候,用计算好的 uv 坐标,其中 left/right 对应 bitmap 左右两侧的 u 坐标,top/bottom 对应 bitmap 上下两侧的 v 坐标(注意 top 比 bottom 小,因为竖直方向是反的):
    • 3: (xpos , ypos + h, left , top )
    • 1: (xpos , ypos , left , bottom)
    • 2: (xpos + w, ypos , right, bottom)
    • 3: (xpos , ypos + h, left , top )
    • 2: (xpos + w, ypos , right, bottom)
    • 4: (xpos + w, ypos + h, right, top )

此外,在前面的 shader 代码里,字体颜色用的是 uniform,也就是每次调用只能用同一种颜色。修改的方法,就是把它也变成顶点的属性,从 vertex shader 直接传给 fragment shader,替代 uniform 变量。不过由于 vec4 已经放不下更多的维度了,所以需要另外开一个 attribute:

// vertex shader #version 320 es  in vec4 vertex; // xy is position, zw is its texture coordinates in vec3 textColor; out vec2 texCoors; // output texture coordinates out vec3 fragTextColor; // send to fragment shader void main() {  gl_Position.xy = vertex.xy;  gl_Position.z = 0.0; // we don't care about depth now  gl_Position.w = 1.0; // (x, y, z, w) corresponds to (x/w, y/w, z/w), so we set w = 1.0  texCoords = vertex.zw;  fragTextColor = textColor; }  // fragment shader #version 320 es precision lowp float; in vec2 texCoords; in vec3 fragTextColor; out vec4 color; uniform sampler2D text; void main() {  float alpha = texture(text, texCoords).r;  color = vec4(fragTextColor, alpha); } 

背景和光标绘制

接下来回到终端模拟器,它除了绘制字符,还需要绘制背景颜色和光标。前面在绘制字符的时候,只把 bounding box 绘制了出来,那么剩下的空白部分是没有绘制的。但是终端里,每一个位置的背景颜色都可能不同,所以还需要给每个位置绘制对应的背景颜色。这里有两种做法:

第一种做法是,把前面每个字符的 bitmap 扩展到终端里一个固定的位置的大小,这样每次绘制的矩形,就是完整的一个位置的区域,这个时候再去绘制背景颜色,就比较容易了:修改 vertex shader 和 fragment shader,在内部进行一次 blend:color = vec4(fragTextColor.rgb * alpha + fragBackgroundColor.rgb * (1.0 - alpha), 1.0),相当于是丢掉了 OpenGL 的 blend function,自己完成了前景和后景的绘制。

但这个方法有个问题:并非所有的字符的 bitmap 都可以放到一个固定大小的矩形里的。有一些特殊字符,要么长的太高,要么在很下面的位置。后续可能还有更复杂的需求,比如 CJK 和 Emoji,那么字符的宽度又不一样了。所以这个时候导出了第二种做法:

  • 第一轮,先绘制出终端每个位置的背景颜色
  • 第二轮,再绘制出每个位置的字符,和背景进行融合

这时候 shader 没法自己做 blend,所以这考虑怎么用 blend function 来实现这个 blend 的计算。首先,要考虑我们最终需要的结果是:

final.r = textColor.r * alpha + dest.r * (1 - alpha); final.g = textColor.g * alpha + dest.g * (1 - alpha); final.b = textColor.b * alpha + dest.b * (1 - alpha); final.a = 1.0; 

由于是 OpenGL 做的 blending,我们需要用 OpenGL 自带的 blending mode 来实现上述公式。OpenGL 可以指定 RGB 的 source 和 dest 的 blending 方式,比如:

  • GL_ONE:乘以 1 的系数
  • GL_ONE_MINUS_SRC_ALPHA:乘以 (1 - source.a) 的系数

根据这个,就可以想到,设置 source = vec4(textColor.rgb * alpha, alpha),设置 source 采用 GL_ONE 方式,dest 采用 GL_ONE_MINUS_SRC_ALPHA 模式,那么 OpenGL 负责剩下的 blending 工作 final = source * 1 + dest * (1 - source.a)(要求 dest.a = 1.0):

final.r = source.r * 1.0 + dest.r * (1 - source.a) = textColor.r * alpha + dest.r * (1 - alpha); final.g = source.g * 1.0 + dest.g * (1 - source.a) = textColor.g * alpha + dest.g * (1 - alpha); final.b = source.b * 1.0 + dest.b * (1 - source.a) = textColor.b * alpha + dest.b * (1 - alpha); final.a = source.a * 1.0 + dest.a * (1 - source.a) = alpha + 1.0 * (1 - alpha) = 1.0; 

正好实现了想要的计算公式。这个方法来自于 Text Rendering - WebRender。有了这个推导后,就可以分两轮,完成终端里前后景的绘制了。

目前 Termony 用的就是这种实现方法:

  • 首先把不同字重的各种字符的 bitmap 拼在一起,放在一个 texture 内部
  • 使用两阶段绘制,第一阶段

注:如果在 source 使用 GL_SRC_ALPHA,设置 source = vec4(textColor.rgb, alpha),这样 final.r = source.r * source.a + dest.r * (1 - source.a) = textColor.r * alpha + dest.r * (1 - alpha),结果是上面是一样的,不过这个时候 final 的 alpha 值等于 source.a * source.a + dest.a * (1 - source.a) 是 alpha 和 dest.a 经过 blend 以后的结果,不再是 1.0,如果不用它就无所谓。上面这种 vec4(textColor.rgb * alpha, alpha) 的计算方法,叫做 premultiplied alpha,也就是预先把 alpha 乘到颜色项里,可以方便后续的计算。

在鸿蒙上使用 OpenGL 渲染

最后再简单列举一下,在鸿蒙上用 OpenGL 渲染都需要哪些事情:

首先,在 ArkTS 中,插入一个 XComponent,然后在 XComponentController 的回调函数中,通知 native api:

import testNapi from 'libentry.so';  class MyXComponentController extends XComponentController {  onSurfaceCreated(surfaceId: string): void {  hilog.info(DOMAIN, 'testTag', 'onSurfaceCreated surfaceId: %{public}s', surfaceId);  testNapi.createSurface(BigInt(surfaceId));  }   onSurfaceChanged(surfaceId: string, rect: SurfaceRect): void {  hilog.info(DOMAIN, 'testTag', 'onSurfaceChanged surfaceId: %{public}s rect: %{public}s', surfaceId, JSON.stringify(rect));  testNapi.resizeSurface(BigInt(surfaceId), rect.surfaceWidth, rect.surfaceHeight);  }   onSurfaceDestroyed(surfaceId: string): void {  hilog.info(DOMAIN, 'testTag', 'onSurfaceDestroyed surfaceId: %{public}s', surfaceId);  testNapi.destroySurface(BigInt(surfaceId))  } }  @Component struct Index {  xComponentController: XComponentController = new MyXComponentController();   build() {  // ...  XComponent({  type: XComponentType.SURFACE,  controller: this.xComponentController  })  } } 

native 部分需要实现至少两个函数:createSurface 和 resizeSurface。其中主要的工作在 CreateSurface 中完成,ResizeSurface 会在窗口大小变化的时候被调用。

CreateSurface 要做的事情:

读取 surface id:

size_t argc = 1; napi_value args[1] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);  int64_t surface_id = 0; bool lossless = true; napi_status res = napi_get_value_bigint_int64(env, args[0], &surface_id, &lossless); assert(res == napi_ok); 

创建 OHNativeWindow:

OHNativeWindow *native_window; OH_NativeWindow_CreateNativeWindowFromSurfaceId(surface_id, &native_window); assert(native_window); 

创建 EGLDisplay:

EGLNativeWindowType egl_window = (EGLNativeWindowType)native_window; EGLDisplay egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); assert(egl_display != EGL_NO_DISPLAY); 

初始化 EGL:

EGLint major_version; EGLint minor_version; EGLBoolean egl_res = eglInitialize(egl_display, &major_version, &minor_version); assert(egl_res == EGL_TRUE); 

选择 EGL 配置:

const EGLint attrib[] = {EGL_SURFACE_TYPE,  EGL_WINDOW_BIT,  EGL_RENDERABLE_TYPE,  EGL_OPENGL_ES2_BIT,  EGL_RED_SIZE,  8,  EGL_GREEN_SIZE,  8,  EGL_BLUE_SIZE,  8,  EGL_ALPHA_SIZE,  8,  EGL_DEPTH_SIZE,  24,  EGL_STENCIL_SIZE,  8,  EGL_SAMPLE_BUFFERS,  1,  EGL_SAMPLES,  4, // Request 4 samples for multisampling  EGL_NONE};  const EGLint max_config_size = 1; EGLint num_configs; EGLConfig egl_config; egl_res = eglChooseConfig(egl_display, attrib, &egl_config, max_config_size, &num_configs); assert(egl_res == EGL_TRUE); 

创建 EGLSurface:

EGLSurface egl_surface = eglCreateWindowSurface(egl_display, egl_config, egl_window, NULL); 

创建 EGLContext:

EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; EGLContext egl_context = eglCreateContext(egl_display, egl_config, EGL_NO_CONTEXT, context_attributes); 

在当前线程启用 EGL:

eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); 

在这之后就可以用 OpenGL 的各种函数了。OpenGL 绘制完成以后,更新到窗口上:

eglSwapBuffers(egl_display, egl_surface); 

在 ResizeSurface 中,主要是更新 glViewport,让它按照新的 surface 大小来绘制。

参考

🔲 ☆

鸿蒙电脑 MateBook Pro 开箱体验

鸿蒙电脑 MateBook Pro 开箱体验

购买

2025.6.6 号正式开卖,当华为线上商城显示没货的时候,果断去线下门店买了一台回来。购买的是 32GB 内存,1TB SSD 存储,加柔光屏的版本,型号 HAD-W32,原价 9999,国补后 7999。

开箱

由于用了国补,需要当面激活,就在店里直接激活了,所以没有体验到鸿蒙系统的扫码激活功能,有点可惜。激活前的第一次开机需要插电,直接按电源键是没有反应的。激活过程也很简单,联网,创建用户,登录华为账号,输入指纹,就可以了。包装盒里还有 140W 单口 Type-C 电源适配器,体积挺小的。此外附赠了一条 Type-C to Type-C 的线,还有一个 Type-C 有线耳机,外加一个 Type-A 母口加 Type-C 公口的线,可以用来接 Type-A 公口的外设。此外还有快速指南和一个超纤抛光布。店家还贴心地提供了一个虚拟机的安装教程。

外形上,就是 MateBook X Pro 加了一个 HarmonyOS 的标识,上手很轻,不愧是不到一公斤的笔记本,对于习惯用 MacBookAir 轻薄本的我来说,是很大的一个亮点。不像 MacBookAir,这台鸿蒙电脑有风扇,有点小小的不习惯,但还算安静。

规格如下:

  • 970g 重量
  • 14.2 寸显示器
  • 3120x2080 分辨率,120 Hz 刷新率
  • 1.8mm 键程键盘
  • 70 Wh 电池

系统体验

预装的版本是 HarmonyOS 5.0.1.305,有更新 5.0.1.310 SP9(SP9C00E301R9P2patch02,内核 1.9.5 2025-05-26),首先更新了一下系统。这是我的第一台支持触屏的笔记本,所以用起来还有点新奇。这个柔光屏用起来触感不错,和之前买的柔光屏 MatePad 的触感类似。

底部状态栏的颜色会随着情况变化,比如在桌面的时候,默认壁纸是黑色的,状态栏也就是黑色的。如果打开了设置,设置是白色的,状态栏也就是白色的。之后可以多测试一下它具体的变色逻辑。

系统里预装了 WPS Office,迅雷,亿图,中望 CAD,剪映,好压,抖音等,面向的客户群体很显然了。虽然预装,但都可以卸载。

内置了控制手机屏幕的功能,有略微的不跟手,但由于电脑本身也是触屏,所以体验还是和手机很接近的。下方是经典的三个按钮。这个协同,可以电脑和手机同时操作,还是挺好的,不会说电脑控制了手机,手机就不能用的情况。手机界面左上角会提示协同中。键鼠共享功能不错,可以把手机当屏幕,然后用电脑的键盘和触摸板控制,外接的鼠标也可以。

触摸板手势方面,可以在设置里修改,比如菜单弹出改成双指点按或轻点。触摸板的手感比苹果还是有一定的差距,但是屏幕触摸弥补了这个问题。没有找到三指拖拽的手势,它用的是类似 Windows 的轻点两次,第二次不抬起的做法。

屏幕分辨率 2080 x 3120,14.2 英寸。

2025-06-17 推送了 5.0.1.310 SP9,SP9C00E301R9P2patch05,内核 1.9.5 2025-05-26。

2025-06-24 推送了 5.0.1.315 SP12,SP12C00E302R9P3patch01,内核 1.9.5 2025-06-20。

2025-06-30 推送了 5.0.1.315 SP17,SP17C00E302R9P3,内核 1.9.5 2025-06-24。

应用体验

目前(2025 年 6 月 6 日)应用商城有这些软件:

  • Bilibili
  • 飞书
  • 钉钉
  • 腾讯会议
  • QQ(在应用尝鲜内)
  • CodeArts IDE(在应用尝鲜内,需要开发者模式)

暂时还没有微信,可以通过操控手机来发微信,但是在消息栏里按回车是换行,没找到发送按钮对应的电脑按键,需要手动操。但是居然有企业微信。

UPDATE: 2025-06-26 微信正式上架。

UPDATE: 2025-06-13 收到了微信的测试短信,可以体验了,版本是 4.0.1.30。2025-06-14 推送了 4.0.1.31 测试版本。2025-06-19 推送了 4.0.1.32 版本。

支持应用接续,在手机上播放的 B 站视频,可以在电脑上接续继续看。

期待一个功能,当电脑上出现需要扫的二维码的时候,可以通过协同功能,不用操作手机,让手机直接扫电脑的屏幕。不过反过来,如果电脑上有需要输入手机短信验证码的场景,就已经很方便了。

试了一下腾讯会议,声音,视频,共享屏幕都是正常工作的。但是共享的屏幕只有笔记本自己的屏幕,还不能选取共享哪个屏幕的内容,也不能选取共享哪个窗口。

开发者模式

开发者模式的打开方式和手机上一样,在设置里狂点软件版本。自带了一个 Terminal App,会提示你如何打开开发者模式。

打开以后就可以访问终端了。shell 是用的 toybox。df 如下:

$ df -h Filesystem Size Used Avail Use% Mounted on tmpfs 16G 52K 16G 1% / tmpfs 16G 0 16G 0% /storage/hmdfs /dev/block/dm-4 5.7M 5.7M 0 100% /cust /dev/block/dm-6 3.1G 3.1G 0 100% /preload /dev/block/dm-0 3.0G 3.0G 0 100% /system/variant /dev/block/dm-5 8.0K 8.0K 0 100% /version /dev/block/platform/b0000000.hi_pcie/by-name/userdata 928G 59G 869G 7% /data/service/hnp tmpfs 16G 0 16G 0% /module_update /dev/block/dm-2 9.3G 8.1G 1.1G 88% /sys_prod devfs 15G 104M 15G 1% /dev /data/service/el2/100/hmdfs/non_account 928G 59G 869G 7% /mnt/hmdfs/100/non_account /dev/block/loop0 114M 112M 0 100% /module_update/ArkWebCore tmpfs 1.0G 608K 0.9G 1% /dev/shm 

查看 /proc/cpuinfo。四个 0xd42(2.0 GHz),八个 0xd43(2.0 GHz),八个 0xd03(2.3 GHz),共 20 个逻辑核。从 part id 来看,0xd03 和 0xd42 对应麒麟 9010 的大核和中核,但 0xd43 是新的 part id。

使用 https://github.com/jiegec/SPECCPU2017Harmony 性能测试:

  • X90 P-Core 2.3 GHz 0xd03 Full: INT 4.87 FP 7.42
  • X90 E-Core 2.0 GHz 0xd43 Full: INT 4.28 FP 6.52
  • X90 LPE-Core 2.0 GHz 0xd42 Full: INT 3.25 FP TODO
  • 9010 P-Core 2.3 GHz 0xd03 Best: INT 4.18 FP 6.22
  • 9010 P-Core 2.3 GHz 0xd03 Full: INT 3.96 FP 5.86
  • 9010 E-Core 2.2 GHz 0xd42 Full: INT 3.21 FP 4.72

详细数据: https://github.com/jiegec/SPECCPU2017Harmony/tree/master/results。Best 代表每一项单独跑,散热条件好,Full 代表顺着跑一遍,散热条件差。由于编译器和编译选项不同,不能和在其他平台上跑的 SPEC CPU 2017 成绩直接对比,仅供参考。

大概性能排序:X90 P-Core > X90 E-Core > 9010 P-Core > X90 LPE-Core > 9010 E-Core > 9010 LPE-Core。

即使是同样的 2.3 GHz 0xd03 的核,X90 比 9010 快上 20%:可能是散热问题,或者缓存大小和内存带宽的问题,或许连微架构都是不一样的,这些都需要后续进一步测试。而 X90 的中核也比 9010 的大核要快。

CodeArts IDE

试了一下从应用商城安装的 CodeArts IDE,显示支持 Java 和 Python 开发,UI 上有点像 JetBrains,但应该是基于 VSCode 做的二次开发。实际测了一下,用它创建 Python 项目后,可以在 CodeArts IDE 的命令行里用 Python3:

$ pwd /storage/Users/currentUser/IDEProjects/pythonProject $ python3 main.py Hello World! $ which python3 /storage/Users/currentUser/IDEProjects/pythonProject/venv/bin/python3o $ /data/app/bin/python --version Python 3.12.5 

这里的 /storage/Users/currentUser/ 就是 HOME 目录,对应文件管理器的个人目录。

看了看 /data/app/bin 目录,下面有 git,python,unzip, vi,rg,java(bisheng jdk 8/17),ssh,electron(用来跑 LSP!)等等。

试了试 pip,也是工作的:

(venv) $ python3 -m pip install requests (venv) $ python3 Python 3.12.5 (main, Aug 28 2024, 01:18:17) [Clang 15.0.4 (llvm-project 81cdec3cd117b1e6e3a9f1ebc4695d790c978463)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import requests >>> requests.get("https://github.com").status_code 200 >>>  

需要 native 编译的库,比如 numpy 还不行,会提示找不到 make。

终端里的 ssh 是可以用的,实测 ssh 到远程的 linux 上是没问题的。

终端里的括号补全有一些问题,等待修复。CodeArts IDE 的 Python 单步调试功能也是工作的。

似乎没有安装 Remote 开发的插件,也没有安装插件的菜单。

既然可以跑 shell,意味着可以 execve 了,意味着可以做 termux 的类似物了。期待鸿蒙 5 上早日有 Termux 用,直接跑 Linux 发行版。实际测了一下,Popen 确实是工作的。

UPDATE: 开了个坑:https://github.com/jiegec/Termony,目前已经能跑很多命令了,包括在鸿蒙电脑上编译 C/C++ 代码。

试了一下 HOME 目录,发现它里面不能有可执行的文件,所以可能还是得打包到一个 App 里面,通过 /data/app/bin 类似的路径来访问。

在 CodeArts IDE 里,可以访问 /data/storage/el1/bundle 目录,里面有一个 pc_entry.hap 文件,可以通过 cat /data/storage/el1/bundle/pc_entry.hap | ssh hostname "cat - > pc_entry.hap" 拷贝到其他机器上。这个文件有 1.9GB,可以看到在 /data/app 下面的各种文件,其实是来自于这个 pc_entry.haphnp/arm64-v8a 下面的一系列文件,例如 git.hnp 就是一个 zip 压缩包,里面就是 /data/app/git.org/git_1.2 目录的内容,这个东西叫做 应用包内 Native 包(.hnp)。这些文件在 module.json 里声明,对应 hnpPackages 标签

{  "module": {  "hnpPackages": [  {  "package": "electron.hnp",  "type": "private"  },  {  "package": "huaweicloud-smartassist-java-ls.hnp",  "type": "private"  },  {  "package": "bishengjdk8.hnp",  "type": "private"  },  {  "package": "rg.hnp",  "type": "private"  },  {  "package": "unzip.hnp",  "type": "private"  },  {  "package": "git.hnp",  "type": "private"  },  {  "package": "bishengjdk17.hnp",  "type": "private"  },  {  "package": "python.hnp",  "type": "private"  }  ],  "name": "pc_entry",  "packageName": "pc_entry"  } } 

解压 git.hnp 后,里面的文件会被复制到 /data/app/git.org/git_1.2 目录下,然后有一个 hnp.json 指定了在 /data/app/bin 创建哪些文件的软连接,比如:

{  "install": {  "links": [  {  "source": "bin/expr",  "target": "expr"  },  {  "source": "bin/git",  "target": "git"  }  ]  },  "name": "git",  "type": "hnp-config",  "version": "1.2" } 

在 HarmonyOS SDK 里,有一个 hnpcli,可以用来生成 .hnp 文件。

除此之外,就是 VSCode 加各种插件了。

鸿蒙电脑上,可以访问各个 App 的内部目录了,无论是自带的文件浏览器,还是通过 DevEco Studio。这给调试带来了很多便利。

UPDATE: 2025-06-21 推送了 1.0.3 版本。实测在 shell 里面输入括号,不会出现括号补全跑到错误的位置的问题了。

UPDATE: 2025-12-01 尝试 CodeArts 1.0.9 版本,可以创建 C++ 项目了,编译没问题并且有自签名的提示,但是执行编译出来的程序还是会报错,估计还是自签名的机制还有问题。

UPDATE: 2025-12-11 经 @w12101111 群友提醒,在设置->隐私和安全->高级->勾选运行外部来源的扩展程序之后,就可以在 CodeArts IDE 里运行和调试编译出来的 C++ 程序了。至此,鸿蒙电脑自己运行自己编译的 ELF 已经是可行的了。

DevEco Studio

2025-12-01 拿到了 DevEco Studio 的内测资格(直接网上申请即可),测试了一下 DevEco Studio 6.0.5.220 鸿蒙预览版。目前能够构建 hap,并且安装在鸿蒙电脑上,通过 USB 连接鸿蒙手机也可以正常安装。

虚拟机

目前应用商城有两家虚拟机:Oseasy 和铠大师。两者都是提示安装 ARM64 版本的 Windows,尝试了一下给它一个 Debian 的安装 ISO,它不认。用的 unattended install,不需要进行什么操作。Oseasy 和铠大师的虚拟机不能同时开,但是可以一边安装完,再去安装另一边的 Windows。

试了试在虚拟机里装 WSL,说没有硬件虚拟化,大概是没有打开嵌套虚拟化的功能。

在 6 核 Oseasy 虚拟机里运行 ARM64 Geekbench 6:Single-Core 1436, Multi-Core 5296。Oseasy 8 核:Single-Core 1462, Multi-Core 7043。算上剩下的 12 个逻辑核,考虑虚拟化的开销,多核分数达到网传的 11640 分,感觉是可能的。

Oseasy 虚拟机只允许开到 8 个核心,实测下来,会优先调度到 0xD03 的八个逻辑核中其中四个逻辑核(不同时用一个物理核的两个逻辑核),之后再调度到 0xD43 的八个逻辑核中的四个逻辑核(也不同时用同一个物理核的两个逻辑核)。在 Oseasy 虚拟机里看到的 CPU 信息是 Cortex-A53,没有正确暴露外面的处理器信息,从 cpuinfo 来看,也没有暴露 SVE。

UPDATE: 能跑 Linux 了,见 在鸿蒙电脑上的虚拟机内启动 Linux

融合开发引擎

2026/04/01 更新:《融合开发引擎》App 在应用市场的应用尝鲜上架,可以获得一个 Linux 环境,Linux 6.6.0 内核的 openeuler。使用可见网络上的视频 鸿蒙电脑官方欧拉虚拟机上线。想用 Debian 的话,也可以按照 HarmonyOS 6 Linux 容器替换成 debian trixie 换成 Debian。

外设

把 Type-C Hub 接到 MateBook Pro 上,显示器,键盘鼠标都正常工作了。

侧载

打开开发者模式后,在设置里,可以打开 USB 调试:把电脑右边的 USB Type-C 接到另一台电脑上,就可以用 hdc 连接了。

然后给自己的项目加上 2in1 的 device type:

diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 index 38bdcc9..ad6fd45 100644 --- a/entry/build-profile.json5 +++ b/entry/build-profile.json5 @@ -30,7 +30,13 @@  ],  "targets": [  { - "name": "default" + "name": "default", + "config": { + "deviceType": [ + "default", + "2in1" + ] + }  },  {  "name": "ohosTest", diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index 7b8532f..76c009c 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -5,7 +5,8 @@  "description": "$string:module_desc",  "mainElement": "EntryAbility",  "deviceTypes": [ - "default" + "default", + "2in1"  ],  "requestPermissions": [  { 

就可以在鸿蒙电脑上跑了。我编写的两个鸿蒙上的应用:https://github.com/jiegec/SPECCPU2017Harmonyhttps://github.com/jiegec/NetworkToolsHarmony 都能正常在 MateBook Pro 上运行。

测试的过程中,发现用 hdc 传文件到电脑比传手机更快:Pura 70 Pro+ 是 24 MB/s,MateBook Pro 是 31 MB/s。

开源的鸿蒙应用也可以编译 + 运行:

目前还没找到怎么让鸿蒙电脑自己调试自己。

卓易通

2025-12-15:在应用市场的应用尝鲜里看到了卓易通,目前只能全屏打开 Android 应用。试了一下 Duolinguo,是左右分屏的显示方式,有点类似双折叠手机,左右各一个竖屏。

移植问题

  • ioctl(fd, TCSETS) 会失败,ioctl(fd, TCSETSW) 则成功
  • libc 缺少一些函数,比如 getspnam,有一些函数不可用,例如 getpwuid
  • openssl 的 hwcap 检测有问题,可能会导致 sigill
  • 无法访问 /proc/stat

Termony

目前通过 https://github.com/jiegec/Termony 运行了一些 benchmark:

$ vkpeak 0 device = Maleoon 916  fp32-scalar = 718.54 GFLOPS fp32-vec4 = 1038.34 GFLOPS  fp16-scalar = 1083.84 GFLOPS fp16-vec4 = 1791.44 GFLOPS fp16-matrix = 0.00 GFLOPS  fp64-scalar = 0.00 GFLOPS fp64-vec4 = 0.00 GFLOPS  int32-scalar = 303.34 GIOPS int32-vec4 = 316.56 GIOPS  int16-scalar = 709.12 GIOPS int16-vec4 = 830.55 GIOPS  int8-dotprod = 0.00 GIOPS int8-matrix = 0.00 GIOPS  bf16-dotprod = 0.00 GFLOPS bf16-matrix = 0.00 GFLOPS 

未完待续

❌