diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index a446f2b7..7f65f533 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -14481,13 +14481,25 @@ namespace MobileGL::MG_Backend::DirectGLES { if (const auto* entry = FindSessionSurfaceLocked(session, drawSurface)) windowSurface = entry->Window != static_cast(0); } + EGLBoolean mglSwapped = EGL_FALSE; if (g_EGLFuncs.eglSwapBuffersWithDamageEXT != nullptr) { const MG_Util::Damage::Region& damage = MG_Backend::CurrentPresentDamage(); EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; const EGLint count = static_cast(MG_Util::Damage::NativeSwapRects(windowSurface, damage, rects)); - g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, count > 0 ? rects : nullptr, count); - } else { - g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); + mglSwapped = g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, count > 0 ? rects : nullptr, count); + // A DRIVER THAT NAMES THE ENTRY POINT AND THEN REFUSES THE SWAP. Some vendor EGLs + // export eglSwapBuffersWithDamageEXT and answer EGL_FALSE on a window surface without + // setting an error (measured on a 4.14 vendor kernel with an Adreno driver; the plain + // entry point presents the same surface happily). Keeping the frame on that answer + // queues nothing at all: the compositor composites into a window whose buffer queue + // never sees a buffer. Fall back to the plain call instead of dropping the frame. + if (mglSwapped == EGL_FALSE && g_EGLFuncs.eglSwapBuffers != nullptr) { + MGLOG_W_ONCE("DirectGLES: this driver's eglSwapBuffersWithDamageEXT refused a window " + "surface; presenting through eglSwapBuffers for the rest of the process"); + mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); + } + } else if (g_EGLFuncs.eglSwapBuffers != nullptr) { + mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); } NoteDrawSurfaceMoved(); // a window surface takes a new size at its swap if (drawSurface != EGL_NO_SURFACE) { diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 7c6a5c65..02496113 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -450,7 +450,7 @@ namespace MobileGL::MG_Remote::Server { MGLOG_W_ONCE("MG_Remote server: a dma-buf import was refused: the client is this process"); return false; } - SI::ImageRef image = SI::Identify(fd, why); + SI::ImageRef image = SI::Identify(fd, op.Width, op.Height, op.Format, why); // YUV: the planes and colour hints ride in Damage (MGPImportPlaneWord). A buffer this // server did not allocate is taken too, through the labelled CPU-copy fallback // (SharedImageRegistry.h, Image::ForeignSource); RGBA ones never are. diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp index 73043dd5..500a4ffb 100644 --- a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #if defined(__ANDROID__) @@ -48,6 +49,9 @@ namespace MobileGL::MG_Remote::Server::SharedImages { std::map, std::weak_ptr> byIdentity; // Foreign YUV images, by the identity of the buffer they copy from. std::map, std::weak_ptr> foreignByIdentity; + // Images an ambiguous-identity import has already named, so the next one takes the + // next image the server exported under that identity instead of naming this one twice. + std::set claimedByIdentity; // Set once two live images were seen with one identity: the kernel shares an inode // between dma-bufs, so a descriptor can no longer say which image it is. Bool identityAmbiguous = false; @@ -362,6 +366,7 @@ namespace MobileGL::MG_Remote::Server::SharedImages { // Only this image's own entries: the weak pointer is already expired, but a newer // image may have taken the identity over (the kernel recycles inodes). if (auto it = reg.byId.find(Id); it != reg.byId.end() && it->second.expired()) reg.byId.erase(it); + reg.claimedByIdentity.erase(Id); const auto key = std::make_pair(IdentityDev, IdentityIno); if (auto it = reg.byIdentity.find(key); it != reg.byIdentity.end() && it->second.expired()) reg.byIdentity.erase(it); @@ -444,7 +449,8 @@ namespace MobileGL::MG_Remote::Server::SharedImages { if (!reg.identityAmbiguous) { MGLOG_E("MG_Remote server: two live shared images have one dma-buf identity (dev %llu ino %llu); " "this kernel does not keep dma-buf inodes unique, so a returned descriptor cannot name its " - "image and every shared-image import is refused from here on", + "image. Imports degrade to naming it by extent and format over the images exported under " + "that identity, in allocation order (SharedImages::Identify)", static_cast(key.first), static_cast(key.second)); } reg.identityAmbiguous = true; @@ -586,9 +592,9 @@ namespace MobileGL::MG_Remote::Server::SharedImages { return image.YuvHints; } - ImageRef Identify(int fd, std::string& why) { + ImageRef Identify(int fd, Uint32 width, Uint32 height, Uint32 fourcc, std::string& why) { #if defined(_WIN32) - (void)fd; + (void)fd, (void)width, (void)height, (void)fourcc; why = "shared images need POSIX descriptors"; return nullptr; #else @@ -599,9 +605,30 @@ namespace MobileGL::MG_Remote::Server::SharedImages { } Registry& reg = Reg(); const std::lock_guard lock(reg.mutex); + // A KERNEL THAT DOES NOT NAME ITS BUFFERS. Every descriptor of every image reports one + // identity here, so the descriptor cannot say which image it is; the import's own extent + // and format do, over the images exported under that identity, oldest first and each + // claimed once - the order the producer allocated and hands them over in. A foreign image + // is never a candidate: it is the server's copy of a client buffer, never handed out. if (reg.identityAmbiguous) { - why = "dma-buf identities are not unique on this kernel"; - return nullptr; + ImageRef chosen; + for (const auto& [id, weak] : reg.byId) { + const ImageRef image = weak.lock(); + if (image == nullptr || image->Foreign != nullptr || image->IdentityDev != dev || + image->IdentityIno != ino) + continue; + if (image->Width != width || image->Height != height || image->Fourcc != fourcc) continue; + if (reg.claimedByIdentity.count(image->Id) != 0) continue; + if (chosen == nullptr || image->Id < chosen->Id) chosen = image; + } + if (chosen == nullptr) { + why = Format("no unclaimed live image of %llux%llu fourcc 0x%08llx was exported under the only " + "identity this kernel reports for descriptors", + width, height, fourcc); + return nullptr; + } + reg.claimedByIdentity.insert(chosen->Id); + return chosen; } const auto it = reg.byIdentity.find(std::make_pair(dev, ino)); ImageRef image = it != reg.byIdentity.end() ? it->second.lock() : nullptr; diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.h b/MobileGL/MG_Remote/Server/SharedImageRegistry.h index 0ad895a3..21099e42 100644 --- a/MobileGL/MG_Remote/Server/SharedImageRegistry.h +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.h @@ -138,9 +138,19 @@ namespace MobileGL::MG_Remote::Server::SharedImages { // A new image of `width` x `height` `fourcc`, registered. Null with `why` on failure. ImageRef Allocate(Uint32 width, Uint32 height, Uint32 fourcc, std::string& why); - // The live image `fd` was exported from, or null (`why` says which test failed): a descriptor - // this server never exported, or one whose identity the kernel does not keep unique. - ImageRef Identify(int fd, std::string& why); + // The live image the descriptor was exported from, or null (why says which test failed): a + // descriptor this server never exported, or one no live image matches. + // + // WHICH IDENTITY A DESCRIPTOR CARRIES IS THE KERNEL'S TO SAY. (st_dev, st_ino) is the + // honest answer, and the only one on kernels that give every dma-buf its own inode. Some + // do not: there, every descriptor reports one identity (measured on a 4.14 vendor kernel: + // dev 12, ino 10658, st_size 0, and /proc/self/fdinfo carries nothing else either), so the + // descriptor cannot name its image. The import then names it by what it DOES carry - the + // extent and the format it declares - and the images the server exported under that + // identity are taken in allocation order, each claimable once, which is the order the + // producer hands them over. A kernel whose descriptors are ambiguous degrades to that, + // and says so once. + ImageRef Identify(int fd, Uint32 width, Uint32 height, Uint32 fourcc, std::string& why); // A YUV import of a dma-buf this server did not export (Image::ForeignSource): the live // foreign image already made from the same buffer with the same shape, or a new one, which diff --git a/MobileGL/MG_Test/Wire/SharedImageTest.cpp b/MobileGL/MG_Test/Wire/SharedImageTest.cpp index e865ded9..de4c3b8e 100644 --- a/MobileGL/MG_Test/Wire/SharedImageTest.cpp +++ b/MobileGL/MG_Test/Wire/SharedImageTest.cpp @@ -59,7 +59,7 @@ TEST(SharedImage, AnExportedDescriptorIdentifiesItsImageAfterCrossingASocket) { EXPECT_EQ(image->Modifier, SI::kModifierInvalid); const int peer = ThroughSocket(image->Fd); ASSERT_GE(peer, 0); - SI::ImageRef found = SI::Identify(peer, why); + SI::ImageRef found = SI::Identify(peer, 64, 32, SI::kFourccAbgr8888, why); ::close(peer); ASSERT_NE(found, nullptr) << why; EXPECT_EQ(found->Id, image->Id); @@ -73,7 +73,7 @@ TEST(SharedImage, AForeignDescriptorIsNotIdentified) { ASSERT_NE(image, nullptr) << why; const int foreign = ::memfd_create("not-a-shared-image", MFD_CLOEXEC); ASSERT_GE(foreign, 0); - EXPECT_EQ(SI::Identify(foreign, why), nullptr); + EXPECT_EQ(SI::Identify(foreign, 16, 16, SI::kFourccXbgr8888, why), nullptr); EXPECT_FALSE(why.empty()); ::close(foreign); } @@ -88,7 +88,7 @@ TEST(SharedImage, AnImageDiesWithItsLastReferenceAndIsNoLongerIdentified) { image.reset(); EXPECT_EQ(SI::LiveCount(), live - 1); EXPECT_EQ(SI::Find(id), nullptr); - EXPECT_EQ(SI::Identify(kept, why), nullptr); + EXPECT_EQ(SI::Identify(kept, 8, 8, SI::kFourccAbgr8888, why), nullptr); ::close(kept); } @@ -153,7 +153,7 @@ TEST(SharedImage, TheInboxKeepsSharedImageAndT0DescriptorsApart) { int fd = -1; ASSERT_EQ(inbox.TakeSharedImageFd(5, 1000, &fd, why), Server::AdoptInbox::Outcome::Taken) << why; - SI::ImageRef found = SI::Identify(fd, why); + SI::ImageRef found = SI::Identify(fd, 16, 16, SI::kFourccAbgr8888, why); ::close(fd); ASSERT_NE(found, nullptr) << why; EXPECT_EQ(found->Id, image->Id); @@ -373,7 +373,7 @@ TEST(SharedImageYuv, AnNv12ImageHasTwoPlanesInOneDescriptor) { EXPECT_GE(image->Plane1Offset, image->Stride * 32u); // Its own descriptor names it, like any image this server exported. const int peer = ThroughSocket(image->Fd); - EXPECT_EQ(SI::Identify(peer, why), image); + EXPECT_EQ(SI::Identify(peer, 64, 32, SI::kFourccNv12, why), image); ::close(peer); } @@ -384,7 +384,7 @@ TEST(SharedImageYuv, AForeignBufferIsCopiedIntoAnImageOfItsOwn) { ForeignFrame frame = MakeForeignNv12(48, 30, 0); ASSERT_GE(frame.Fd, 0); // Not one of the server's: Identify refuses it, the foreign import takes it. - EXPECT_EQ(SI::Identify(frame.Fd, why), nullptr); + EXPECT_EQ(SI::Identify(frame.Fd, 48, 30, SI::kFourccNv12, why), nullptr); const int peer = ThroughSocket(frame.Fd); SI::ImageRef image = SI::ImportForeignYuv(peer, 48, 30, SI::kFourccNv12, frame.Layout, why); ASSERT_NE(image, nullptr) << why; diff --git a/docs/Disaggregated/notes/anland/changes-for-d1100-mali.md b/docs/Disaggregated/notes/anland/changes-for-d1100-mali.md new file mode 100644 index 00000000..86cf0238 --- /dev/null +++ b/docs/Disaggregated/notes/anland/changes-for-d1100-mali.md @@ -0,0 +1,174 @@ +# 为了在天玑 1100 + Mali(内核 4.14)上跑起来,改了什么、为什么 + +> 本文只记**改动与动机**。参考机(Lenovo Y700 / Adreno 750 / 内核 6.1 / KernelSU / 有 systemd 的 +> Arch 容器)的完整操作手册见 [`runbook-plasma.md`](runbook-plasma.md);本文的改动都在分支 +> `feat/disaggregated-fix-d1100-mali` 上,两个在 MobileGL 里(本仓库),两个在 anland 的 +> consumer app 里。 +> +> 判据:改之前 `screencap` 是 37 KB 的纯黑;改之后是 1.4 MB 的完整 Plasma 桌面 +> (壁纸 + 面板 + 时钟),client 日志的验收项从 `presented through wl_shm` 变成 +> `presented through linux-dmabuf shared images`。 + +## 这台机器与参考机的差异(每条改动都从这里来) + +| 项 | 参考机 | 本机 | +|---|---|---| +| GPU / 驱动 | Adreno 750 | **Mali**(`/dev/mali0`) | +| 内核 | 6.1 | **4.14.186**(cgroup v1) | +| 容器 | Arch + **systemd** | Fedora 44,**无 systemd**(`custom_init=ds-init.sh`) | +| 容器里的编译器 | 有 | **没有**,且无 DNS → 全部主机交叉编译 | +| `/dev/dri` | 有 `renderD*` | 只有 `card0` | + +--- + +# MobileGL 仓库(本分支) + +## 改动 1:dma-buf 身份认不出来时,用导入自己的 extent + format 认领 + +**文件**:`MobileGL/MG_Remote/Server/SharedImageRegistry.{h,cpp}`、`MobileGL/MG_Remote/Server/PipeApplier.cpp` + +**改了什么**:`Identify(int fd, …)` 增加 `width/height/fourcc` 三个参数(导入记录本来就有它们)。 +内核身份 `(st_dev, st_ino)` 仍然优先;只有当注册表已经发现身份**不唯一**时,才退化为:在该身份 +下已导出的活镜像里,挑 **extent 与 fourcc 都相同、且尚未被认领** 的那个,按 `Id` 从小到大 +(= 分配顺序 = 生产者交给合成器的顺序),认领一次。 + +**为什么必须改**:零拷贝的整条链是"server 分配 AHardwareBuffer → 导出 dma-buf fd → client 把它做成 +`wl_buffer` → 合成器 `eglCreateImageKHR` 导回 → server 认出是自己那块"。认的依据只有 +`(st_dev, st_ino)`。本机内核给**所有** dma-buf descriptor 发同一个身份(实测三个不同的 buffer): + +``` +bo0: fd=15 dev=12 ino=10658 size=0 fdinfo[pos: 0 flags: 02000000 mnt_id: 12 ] +bo1: fd=17 dev=12 ino=10658 size=0 … +bo2: fd=19 dev=12 ino=10658 size=0 … +``` + +`fdinfo` 里没有 name、没有别的唯一量——用户态拿不到任何能区分 buffer 的东西。注册表检测到 +"两块活镜像同一个身份"后**按设计 fail-closed**: + +``` +[ERROR] two live shared images have one dma-buf identity (dev 12 ino 10658); this kernel does not + keep dma-buf inodes unique … +[WARN] a dma-buf import was refused: dma-buf identities are not unique on this kernel +``` + +于是**每一个** linux-dmabuf 窗口都被拒:client 全退回 `wl_shm`(`8 presented through wl_shm`), +合成器也没法采样它上不了屏的东西。fail-closed 本身是对的(猜错会把别人的像素当自己的), +缺的只是**内核给不出身份时还有别的可用依据**——导入声明的尺寸和格式,加上 server 自己的分配顺序。 + +**验证**:自包含探针 `eglprobe4.c`(主机 clang+lld 编,容器里以 Gold 跑)——`gbm_bo_create` +取一块 server shared image 的 fd,再走合成器的导入路径:修前 +`eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) -> (nil)`,修后 `-> 0x1`;会话里 +refusal 归零、client 日志变成 `presented through linux-dmabuf shared images`。 + +## 改动 2:damage 变体被驱动拒绝时,回落普通 `eglSwapBuffers` + +**文件**:`MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp` 的 `Present()` + +**改了什么**:`eglSwapBuffersWithDamageEXT` 返回 EGL_FALSE 时,用 `eglSwapBuffers` 再试一次, +并 `MGLOG_W_ONCE` 说明一次(没有该扩展的驱动照旧走普通入口)。 + +**为什么必须改**:server 的 `Present()` 原本优先用 WithDamage 变体(注释说"驱动提供哪个拼写就用 +哪个")。本机的 Mali/4.14 vendor EGL **暴露了这个入口,却对窗口面返回 EGL_FALSE 且不设 EGL 错误**: + +``` +server window: 1080x2194 fmt=0x1 (pointer=0xb4000070d902ad00) ← ANativeWindow 是活的、尺寸对 +swap entry points: withDamage=0x71a0887040 plain=0x71a087c62c +withDamage → swap=0 err=0x3000 ← 同一块 surface、同一线程 +plain → swap=1 err=0x3000 +``` + +排除了绑定问题(`session.Draw == eglGetCurrentSurface(EGL_DRAW)`、context 也 current),也不是窗口 +问题(`ANativeWindow_getWidth/Height/Format` 都正常)——就是驱动拒。信任那个指针的后果是 +**一帧都不入队**:SurfaceView 的 BLAST 层 `frame events` 全 0、`geomLayerBounds` 0×0,屏幕上只有 +`Background for SurfaceView` 那块纯黑占位;合成器又能采样但因为上不了屏,client 也跟着退回 +`wl_shm`。这条是"屏幕全黑"的最后一环。 + +**验证**:修后 `frame events` 出现真实时间戳,`screencap` 从 37 KB 变 1.4 MB 的完整桌面。 + +--- + +# anland 仓库(consumer app) + +这两条不在本仓库里,但缺任何一条桌面都上不了屏。 + +## 改动 3:`:mobilegl` worker 设置 `MOBILEGL_IPC_SURFACE=server` + +**文件**:`consumers/anland_v5/android_consumer/app/src/main/java/com/anland/consumer/MobileGLWorker.java` + +**改了什么**:在 `Os.setenv("MOBILEGL_IPC_ROLE", "server", …)` 旁边加 +`Os.setenv("MOBILEGL_IPC_SURFACE", "server", true)`。 + +**为什么必须改**:`Config.h` 里 `IpcTable::Surface` 的默认值是 `IpcSurface::Offscreen`。 +client 侧(容器里的 `mobilegl-startup.sh`/`start-mobilegl-session.sh`)按 `server` 去要 +server 的窗口,而 **server 自己的进程没有任何 `MOBILEGL_IPC_SURFACE`**,于是它把 app 递过来的 +窗口 attach 了、却**从不创建 server-owned 窗口面、也从不往上面 swap**。日志上表现为只有一句 +`a ServerOwned window surface is served on its window`,没有真正的 +`surface=window … owner=server`。补上之后这一行立刻出现。 + +## 改动 4:几何回调不再把 Surface 塌成 0×0 + +**文件**:`…/MainActivity.java`(`MobileGLConnection` 的几何回调) + +**改了什么**:回调收到 `(0,0)`("不指定尺寸")时,只有在视图**确实已有尺寸**时才 +`holder.setSizeFromLayout()`;否则什么都不做,保留现有 surface。 + +**为什么必须改**:这个回调可能在视图第一次布局之前被调用,此时 `setSizeFromLayout()` 会把 +`SurfaceHolder` 定成 0×0;0×0 的 surface 上任何生产者都无法 queue,之后每一帧 +`eglSwapBuffers` 都会失败——正是"BLAST 层 `geomLayerBounds` 0×0 且永远没有 buffer"的成因。 + +--- + +# 环境侧(不是代码,但缺了就起不来) + +1. **`libatomic.so.1`**:主机交叉编出来的 KWin 需要它,而这个 Fedora 运行时镜像没有 + (`kwin_wayland: error while loading shared libraries: libatomic.so.1`)。从 sysroot 把 + `libatomic.so.1.2.0` + 软链补进 rootfs(容器停着时直接写 + `/data/local/Droidspaces/fedora-kde/usr/lib64/`)。`ldd` 其余 Qt6/KF6 依赖都齐。 +2. **无 systemd 的会话**:容器里 systemd 259 因"内核 4.14 + cgroup v1"直接拒绝启动,会话由 + `ds-init.sh` 自己拉起。等价物是把 `mobilegl-startup.sh plasma` 的那套 env 写进 + `/opt/mobilegl/bin/start-mobilegl-session.sh`(PATH 前置 `/opt/mobilegl/kwin/bin` 让 + `kwin_wayland_wrapper` 起我们的 KWin),再让 `ds-init.sh` 的最后一句 exec 它。**同时必须去掉 + `LIBGL_ALWAYS_SOFTWARE=1`/`GALLIUM_DRIVER=llvmpipe`**:MobileGL 对"要求软件渲染的进程"主动 + 让位,留着它整个会话就还是 llvmpipe。 +3. **GBM 后端的路径**:Fedora 的 loader 搜 `/usr/lib64/gbm`(上游脚本写的是 Arch 的 + `/usr/lib/gbm`);`libGLX_mobilegl.so.0` 同理放 `/usr/lib64`。 +4. **`/dev/dri` 只有 `card0`**:`MOBILEGL_GBM_NODE` 退到 `/dev/null` —— 那个节点只当身份、 + 后端从不对它 ioctl,这是 `mobilegl-startup.sh` 自己的回退路径,不需要改设备节点权限。 +5. **KWin 的构建**:pristine `kwin-6.7.5` + `Arch_v5/kwin.patch` + `anland_backend_Arch_v5` + + 顶层 `libdisplay_producer/`(后者的 `protocol.h`/`socket_utils.c` 是指向 `common/` 的符号链接, + **拷贝时要 -L 解引用**;也**不要**把 `common/` 整个拷进 backend 目录,会让 `kde_clang_format` + 生成重名 target)。6.7.5 上只有 `src/opengl/eglcontext.cpp` 的 Hunk #2 打不上,按补丁原意手工把 + no-arg `makeCurrent()` 里的 `EGL_NO_SURFACE, EGL_NO_SURFACE` 换成 + `m_display->defaultSurface()`。 + +--- + +# 已知但未处理 + +- daemon 的 producer/consumer 每 ~5 s 抖一次(`producer connected/disconnected`、 + `consumer re-deposited 5 fds`,kwin 侧 `consumer disconnected, entering fallback`)。MobileGL + 模式下合成不受影响(窗口与默认 framebuffer 属于 KWin),桌面照常上屏,成因未查。 +- 桌面静止时两次 `screencap` 的 md5 相同(时钟只到分钟、无动画);判断"活着"要看 server 的 + `P65ServerFrame` 计数或 SurfaceView 的 frame events。 +- **"桌面全黑"里有一半是锁屏**:会话跑一会儿后 kscreenlocker 会把屏幕锁上,看到的黑屏其实是 + greeter(它自己也打 `Renderer Name: …`,所以日志里会误以为是桌面的渲染器)。判断方法: + `pgrep -a -f kscreenlocker_greet`。修法:写 `~/.config/kscreenlockerrc` + (`[Daemon] Autolock=false`/`LockOnStart=false`)后重启会话;**杀 greeter 没用**,kwin 会用 + `--immediateLock` 再起一个,D-Bus 的 `SetActive(false)` 也解不开。这一条是"看不到完整桌面" + 的直接原因,与渲染无关。 +- **Magma(DirectVulkan)已复验:桌面能上屏**(`Renderer Name: Magma`、`8 presented through + linux-dmabuf`、SurfaceView frame events 是真实时间戳、`screencap` 1.05 MB 的桌面 + 面板 + 光标)。 + 上面三条代码改动都与后端无关,两个后端都撞到同一个坑,也都因此被修好。 +- **Magma 少画字(未修,已缩小到一条线索)**:同一场景下所有 `Text` 都不画(键盘按键是空白白块、 + 没有 q/w/e…/"American English",面板时钟也没有),而矩形/图标/壁纸都正常。已用探针**逐条排除**: + `eglprobe6/8`(ES2 的 `GL_ALPHA` 与 ES3 的 `GL_R8`/`GL_RED` 采样 `.a`/`.r` 均正确)、 + `eglprobe7`(`UNPACK_ALIGNMENT=1` 的窄行上传正确)、`eglprobe9`(图集模式:大纹理 + + `glTexSubImage2D` 偏移 + `GL_UNPACK_ROW_LENGTH`,值全对、未触碰区域仍为 0)、 + `eglprobe10`(`fwidth` 与距离场 `smoothstep` 都正常)。 + 剩下的一手证据在容器日志里:**Magma 下 Qt 的 RHI 上下文建立失败**—— + `QRhiGles2: Failed to make context current. Expect bad things to happen.` 与 + `QWaylandGLContext::makeCurrent: eglError: 0x3002`(EGL_NOT_INITIALIZED),Espryt 下没有这两条。 + 下一步应从 `DirectVulkan` 的 `MakeEGLCurrent`/`OnClientContextBound`(`m_contextBindings` + 按上下文找 surface 的那条路,注释里明确写着"Qt 每个窗口一个线程与上下文")查起。 + 顺带记一个可查项:Magma 的 `GL_EXTENSIONS` **不含 `OES_standard_derivatives`**,而 `fwidth` + 实际可用。