From 8196818e9fc1bd7f2b7da40a7472cae58a84d29e Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Wed, 30 Sep 2026 23:00:55 +0800 Subject: [PATCH] [Test] (extmem_probe): the T4 leg asks whether the render server can draw into an image allocated outside Android - and the device answers twice: this driver has no dma-buf import, Android-side allocation works end to end, and the offer must carry the plane layout rather than one linear plane. --- tools/spikes/extmem_probe/README.md | 122 ++ tools/spikes/extmem_probe/extmem_probe.cpp | 1416 +++++++++++++++++- tools/spikes/extmem_probe/peer/t4_gbm_peer.c | 1192 +++++++++++++++ 3 files changed, 2724 insertions(+), 6 deletions(-) create mode 100644 tools/spikes/extmem_probe/peer/t4_gbm_peer.c diff --git a/tools/spikes/extmem_probe/README.md b/tools/spikes/extmem_probe/README.md index b8a751af..8bb7c558 100644 --- a/tools/spikes/extmem_probe/README.md +++ b/tools/spikes/extmem_probe/README.md @@ -181,6 +181,128 @@ and the client reads both fills through the still-held pointer; after the last round it unlocks and relocks once more (`after unlock+relock`), which separates "GPU writes never landed" from "the held mapping kept stale lines". +## T4 — an image allocated outside Android + +Can the render server draw into an image that was allocated **outside** Android — +a GBM buffer minted in a Linux container on the same kernel? That is the +`{Owner=Platform, Storage=OfferedImage}` cell an Anland-style Wayland host needs, +and nothing in MobileGL imports a dma-buf today. It is a per-device fact, not a +code fact. + +### What the device answered + +Redmi 23117RK66C, Adreno 750, Android 16, Droidspaces Fedora 44 container, +2026-09-30. The answer has two halves and they point in opposite directions. + +| row | verdict | what it means | +|---|---|---| +| `T4-image-import` | `UNSUPPORTED` | This driver does not advertise `EGL_EXT_image_dma_buf_import`, so a container-minted dma-buf has no spelling as an EGLImage here. A capability answer, not a failure — and the reason the second row exists. | +| `T4-ahb-image` | `FAIL`, and the failure is the finding | **Android-side allocation works end to end.** `AHardwareBuffer_allocate` → `eglGetNativeClientBufferANDROID` → `eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID)` → `glEGLImageTargetRenderbufferStorageOES` (FBO `GL_FRAMEBUFFER_COMPLETE`) → clear, `glFinish`, and a waited `glFenceSync` — all clean — and `glEGLImageTargetTexture2DOES` reads **the same bytes back on both pixels**. The container then imports the same handle's fd with `gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER, …)` and **that** is where it breaks: the bytes it maps are not the bytes the GPU wrote, and its own writes do not arrive either. | + +The reason is visible in the offer: the AHB's handle carries **two** descriptors, +while the import claims `DRM_FORMAT_MOD_LINEAR` over plane 0 alone. A multi-plane, +possibly compressed allocation imported as one linear plane is not a view of that +buffer, so a CPU mapping of it agrees with nothing — and no userspace API hands the +importer the modifier it should have used, because `AHardwareBuffer` does not expose +one. (`AHardwareBuffer_Desc::stride`, for its part, is in **pixels** while every +consumer on the wire wants bytes; that trap cost a device run and is now a named +constant beside the fourcc.) + +### What this decides + +The cell is reachable **only** when the image is allocated where the server can use +it — on the Android side — **and** its layout travels with the offer. That is a +requirement on the host that mints the scanout buffers, not a MobileGL feature: +Anland-style hosts that allocate in the container cannot be served by this driver, +and a host that publishes plane count, strides, offsets and modifier alongside the +fd can be. Note also that the container never needs a CPU view of the image in the +product: the compositor hands the buffer to its GL, which is MobileGL, and the +server renders into it. The CPU mapping in this leg is a *verification device* — +which is exactly why `FAIL` here means "a linear view is invalid", not "the driver +cannot render into an AHardwareBuffer". + +This leg's topology differs from every other one: the peer is **not** a re-exec +child. It is a glibc process inside the container, because only that side can +allocate on the device Anland hands the container. So the probe **listens** +(`--only-t4[=@endpoint]`, default `@mgl-t4`) and the peer **dials** it. The +endpoint is an abstract socket name because a Droidspaces container runs +`net_mode=host`: the abstract namespace is common to both sides, while a +filesystem pathname would have to exist in the other side's root. + +Both rows run on the one connection `--only-t4` opens, so the peer never waits for +a frame by name and a skipped row is not an error. + +* **`T4-image-import`** — the container-minted direction. This side has no GBM + device, so it waits for an fd, asks the driver whether the announced + `(fourcc, modifier)` pair is one it even lists (`eglQueryDmaBufFormatsEXT` / `eglQueryDmaBufModifiersEXT`; a pair + the driver does not list is a capability answer, an import failure on a pair it + *does* list is a driver bug, and the two are never reported the same way), + imports the buffer as a texture **and** as a renderbuffer, reads two pixels on + each path — `(0,0)` and the far corner, so a wrong stride or offset cannot + pass as a working import — and then clears each path to a colour. +* **`T4-ahb-image`** — the Android-minted direction, and the one the measurement + says the architecture must use. This side allocates an `AHardwareBuffer` + (`R8G8B8A8_UNORM`, `GPU_SAMPLED_IMAGE|GPU_COLOR_OUTPUT|CPU_READ_OFTEN|CPU_WRITE_OFTEN`), + imports **the same buffer** into GL as an EGLImage, clears it through the + renderbuffer path behind a reported fence, hands its handle to the peer over a + socketpair (`AHardwareBuffer_sendHandleToUnixSocket`), and then reads the peer's + answer back through the **texture** path at `(0,0)` and at the far corner. One + decisive leg, `ahb[D]`: it needs the peer to have seen this side's clear colour + at both pixels *and* this side to have read the peer's pattern at both. +* **`peer/t4_gbm_peer.c`** — serves both rows. For the first it allocates with + `gbm_bo_create(…, GBM_FORMAT_ARGB8888, + GBM_BO_USE_RENDERING|GBM_BO_USE_LINEAR)`, fills the whole mapping with + `--seed=RRGGBBAA` in ARGB8888 memory order (B,G,R,A; a *solid* fill, so a + vertical flip cannot turn a working import into a reported failure), sends the + fd over `SCM_RIGHTS`, and after each of the probe's two writes takes a **fresh** + read mapping and answers with the bytes it actually saw. A driver that refuses + the allocation has answered the question: the refusal is printed verbatim and + the peer exits non-zero, with no silent fallback to another format. For the + second it takes the descriptors off the socketpair, imports the first with + `gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER, …)` and reports **how many** arrived — + the plane count is itself evidence — then compares both pixels against the + announced colour before writing a pattern of its own. + +The peer **must run inside the container**: `/dev/dri/renderD128` there is the +Anland-backed `msm_drm` GBM device, and the Android side has no GBM device at all. + +```sh +S=; DS="/data/local/Droidspaces/bin/droidspaces --name=Fedora" + +# peer: into the container's own rootfs (adb push lands on the Android side), +# then built there with the container's own glibc gcc -- the documented line is +# gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm +base64 -w0 tools/spikes/extmem_probe/peer/t4_gbm_peer.c > /tmp/t4_gbm_peer.c.b64 +adb -s $S shell "$DS run sh -c 'base64 -d > /root/t4_gbm_peer.c'" < /tmp/t4_gbm_peer.c.b64 +adb -s $S shell "$DS run sh -c 'cd /root && gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm'" + +# probe: build and start it FIRST — it is the side that listens +ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 ./build_android.sh /tmp/extmem-build +adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/extmem_probe +adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe --only-t4" \ + | tee out-t4-$S.txt + +# then, with the probe listening, dial it from the container +adb -s $S shell "$DS run /root/t4_gbm_peer --endpoint=@mgl-t4" +``` + +Peer options: `--endpoint=@mgl-t4`, `--width=64`, `--height=64`, +`--seed=RRGGBBAA`, `--timeout=60`. The row is `T4-image-import`, with two +decisive legs `tex[D]` and `rb[D]` under the usual rule: `OK` needs each path to +have read the peer's seed bytes **and** to have had its own write observed by the +peer. The peer exits 0 only on that same two-way condition, and its last line +carries every raw value (stride, format, modifier, the fd's kernel name, the +exporter, both pixels of both paths) — read those, not the exit code. + +Two things there will look like failures and are not. `gbm_bo_unmap()` of a +**WRITE** mapping segfaults on this container's mixed-version Mesa (distro +`libgbm`/`dri_gbm` 26.1.5 plus an unowned self-built +`libgallium-26.2.0-devel.so`), so the peer deliberately leaves its fill mapping +mapped instead. And the peer's fd name plus exporter (`/dmabuf:`, +`exp_name: system`) is the evidence that this is the same *kind* of object the +compositor scans out: kwin's own scanout allocations come from that same system +dma-heap. + ## Route mode (P11 B2 step 1) Does a client's AHB cross the *real* B1 routes into the server app? A spike APK built with diff --git a/tools/spikes/extmem_probe/extmem_probe.cpp b/tools/spikes/extmem_probe/extmem_probe.cpp index eacb1673..a69315e9 100644 --- a/tools/spikes/extmem_probe/extmem_probe.cpp +++ b/tools/spikes/extmem_probe/extmem_probe.cpp @@ -4050,6 +4050,1393 @@ static void runT3Parent(VkCtx& c, uint64_t size) { close(memfd); } +// --------------------------------------------------------------------------- +// T4: the image the peer allocated OUTSIDE Android +// +// The {Owner=Platform, Storage=OfferedImage} cell of an Anland-style Wayland host: a +// glibc peer in a Droidspaces container creates the GBM buffer on the device the +// container owns and offers it here as a dma-buf, and this side -- the one holding the +// live EGL/GLES context -- has to be able to READ it and to DRAW into it. +// +// The topology is the reverse of every other leg on purpose. The peer is not a re-exec +// of /proc/self/exe: nothing here needs a second driver instance (the only reason the +// other legs exec is that driver threads and device state do not survive fork), and the +// peer has to be glibc to create a GBM buffer at all. So this side LISTENS on an +// abstract unix socket and the peer dials it, and because a Droidspaces container runs +// with net_mode=host, one network namespace holds both ends. The peer is therefore not +// this process's child: there is nothing to reapChild(), and the frozen protocol has no +// MSG_BYE for T4, so the socket close is the whole teardown. +// +// Two failures must never be reported the same way: +// capability (fourcc, modifier) is not in the driver's own eglQueryDmaBufFormatsEXT / +// eglQueryDmaBufModifiersEXT answer. Refusing a pair the driver never +// advertised is the driver being right, and no import attempt can change it. +// driver bug the pair IS advertised and the import still fails. +// The capability question is asked first and its answer travels in the row. +// +// Both paths are read at (0,0) AND at the far corner: a wrong stride or offset shifts the +// corner texel, which a one-pixel read cannot see. +// +// T4 is two rows over that one connection, and they answer different halves: +// T4-image-import the container's dma-buf, imported here. Gated on +// EGL_EXT_image_dma_buf_import: a driver without it gets a capability +// answer recorded before the ladder is entered, never an import +// failure dressed up as a driver bug. +// T4-ahb-image an AHardwareBuffer minted on THIS side and imported in the container, +// which is the direction that still answers on such a driver. +// +// GL hands back channels (R,G,B,A) where the peer's seed is memory bytes, and the fourcc the +// peer allocates -- GBM's ARGB8888 -- is B,G,R,A in memory. A working import therefore reads +// back the seed's bytes in CHANNEL order rather than verbatim, so the row carries both +// readings: the verbatim one the protocol specifies, and the channel-order one beside it, so +// the byte-order convention can never be mistaken for a broken import (and vice versa). +// +// The host build compiles this section, but its runT4 is a one-line SKIP row: the only way +// this side can be dialed is rtListen(), which lives inside the AHardwareBuffer guard with +// the rest of the T0/route block, and a host build has neither a listener nor a container +// peer. T4 itself needs no AHardwareBuffer -- PROBE_HAVE_AHB is simply the macro that +// carries rtListen() -- so the guard is spelled out here with the stub beside it rather +// than left to look like an Android-only leg. +// --------------------------------------------------------------------------- + +static const char* kT4Row = "T4-image-import"; + +#if PROBE_HAVE_AHB + +// The peer spells the same literal, so neither side depends on the other's byte order. +static const uint32_t kT4Magic = 0x5434474DU; + +enum : uint32_t { + MSG_T4_OFFER = 50, + MSG_T4_READ = 51, + MSG_T4_WROTE = 52, + MSG_T4_ACK = 53, + MSG_T4_RESULT = 54, + // The Android-minted half of T4, appended so that no existing tag moves. The + // AHardwareBuffer handle travels beside the offer exactly the way the T0 legs + // send theirs -- AHardwareBuffer_sendHandleToUnixSocket into one end of a + // socketpair, and that end's peer over SCM_RIGHTS with the message. + MSG_T4_AHB_OFFER = 55, + MSG_T4_AHB_ACK = 56, +}; + +// Byte for byte the peer's struct in peer/t4_gbm_peer.c: every field is fixed width, so the +// two compilers cannot disagree about padding even though only one of them is bionic. +struct T4Layout { + uint32_t magic, version, width, height, fourcc, stride, offset, reserved; + uint64_t modifier; // UINT64_MAX = the peer has none, so rung 0 has nothing to name + uint8_t seed[4]; // what the peer filled the image with, in the buffer's memory order + char peer[128]; + char note[128]; +}; + +struct T4Read { + int32_t texOk, rbOk, rung, pad; + uint8_t texWord[4], texCorner[4], rbWord[4], rbCorner[4]; + char importNote[160]; +}; + +// path 0 = renderbuffer, 1 = texture. `color` is the byte sequence the peer should find in +// the buffer, so it is in memory order and not in GL channel order. +struct T4Wrote { + int32_t path; + uint8_t color[4]; +}; + +struct T4Ack { + int32_t ok; + uint8_t observed[4]; + char note[128]; +}; + +// MSG_T4_AHB_OFFER: the buffer THIS side minted, described so the container can +// import it without guessing any part of the layout. The fourcc is the DRM name +// whose memory order is the order AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM names -- +// DRM_FORMAT_ABGR8888, fourcc_code('A','B','2','4') = 0x34324241, whose +// little-endian [31:0] A:B:G:R layout puts R,G,B,A in bytes 0..3, the same pairing +// gralloc itself uses for HAL_PIXEL_FORMAT_RGBA_8888 -- so the bytes the peer +// compares are the bytes glClearColor() produced, and no channel-renaming step +// sits between the two sides where a convention could pass for a working image. +// The stride is the stride AHardwareBuffer_describe() reported, never width*4. +struct T4AhbOffer { + uint32_t magic, version, width, height, stride, fourcc; + uint8_t seed[4]; // what this side cleared into the buffer, in memory order + char note[128]; +}; + +// MSG_T4_AHB_ACK: what the container's GBM import made of the fd, what both pixels +// of its mapping held BEFORE it wrote anything, and the pattern it then wrote over +// the whole mapping. `matched` is the peer's own comparison -- bit0 for (0,0), +// bit1 for the far corner -- and this side recomputes it from the two pixel fields +// instead of trusting it, so a disagreement between the bits and the bytes is +// visible in the row. +struct T4AhbAck { + int32_t imported; // the peer's gbm_bo_import returned a bo + int32_t matched; // 1 = (0,0) held the announced seed, 2 = the far corner did + uint32_t stride; // the stride the peer's own mapping reports + uint8_t observed[4]; + uint8_t observedCorner[4]; + uint8_t pattern[4]; // zero when the peer could not write anything + char note[128]; +}; + +// The dma-buf import entry points stay local to T4 rather than joining GlCtx: T1-gles asks +// the same question through GL_EXT_memory_object, and widening GlCtx would move that leg +// for no reason. +struct T4Egl { + PFNEGLCREATEIMAGEKHRPROC pCreateImage = nullptr; + PFNEGLDESTROYIMAGEKHRPROC pDestroyImage = nullptr; + PFNEGLQUERYDMABUFFORMATSEXTPROC pFormats = nullptr; + PFNEGLQUERYDMABUFMODIFIERSEXTPROC pModifiers = nullptr; + PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture = nullptr; + PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC pTargetRb = nullptr; +}; + +// The two-pixel read only means anything byte-exact: a wrong stride or offset shows up as +// differing bytes, which a decimal rendering would hide. +static std::string t4Hex4(const uint8_t* b) { + return fmt("%02x%02x%02x%02x", b[0], b[1], b[2], b[3]); +} + +// glClientWaitSync answers in its own vocabulary, which glErrStr does not cover. +static std::string t4WaitStr(GLenum w) { + switch (w) { + case GL_ALREADY_SIGNALED: return "GL_ALREADY_SIGNALED"; + case GL_CONDITION_SATISFIED: return "GL_CONDITION_SATISFIED"; + case GL_TIMEOUT_EXPIRED: return "GL_TIMEOUT_EXPIRED"; + case GL_WAIT_FAILED: return "GL_WAIT_FAILED"; + default: return fmt("GL(0x%04x)", (unsigned)w); + } +} +// The driver's own answer to "do you take this (fourcc, modifier) pair", asked before any +// import so that a pair the driver never advertised can never be reported as an import +// failure. `asked` says whether the answer means anything at all. +struct T4Cap { + bool asked = false; + bool formatListed = false; + bool listed = false; + int formats = 0; + int modifiers = 0; + std::string note; +}; + +static T4Cap t4Capability(EGLDisplay dpy, const T4Egl& e, uint32_t fourcc, uint64_t modifier) { + T4Cap cap; + if (!e.pFormats || !e.pModifiers) { + cap.note = "EGL_EXT_image_dma_buf_import_modifiers absent, or its two entry points are: the driver " + "publishes no (fourcc, modifier) list, so an import failure cannot be attributed either way"; + return cap; + } + EGLint total = 0; + if (!e.pFormats(dpy, 0, nullptr, &total) || total <= 0) { + cap.note = fmt("eglQueryDmaBufFormatsEXT(list) -> 0x%04x num=%d", (unsigned)eglGetError(), (int)total); + return cap; + } + std::vector formats((size_t)total); + EGLint filled = 0; + if (!e.pFormats(dpy, total, formats.data(), &filled)) { + cap.note = fmt("eglQueryDmaBufFormatsEXT(%d) -> 0x%04x", (int)total, (unsigned)eglGetError()); + return cap; + } + cap.asked = true; + cap.formats = (int)filled; + for (EGLint i = 0; i < filled; ++i) { + if ((uint32_t)formats[(size_t)i] == fourcc) cap.formatListed = true; + } + if (!cap.formatListed) { + cap.note = fmt("fourcc=0x%08x is not among the %d formats the driver lists: this is a capability answer, " + "not an import failure", + fourcc, (int)filled); + return cap; + } + EGLint nmods = 0; + if (!e.pModifiers(dpy, (EGLint)fourcc, 0, nullptr, nullptr, &nmods)) { + cap.note = fmt("fourcc=0x%08x is listed, eglQueryDmaBufModifiersEXT(list) -> 0x%04x", fourcc, + (unsigned)eglGetError()); + return cap; + } + cap.modifiers = (int)nmods; + if (modifier == UINT64_MAX) { + // Nothing to look up: the implicit modifier is the only one rung 1 can name, and it + // is legal for every format the driver lists. + cap.listed = true; + cap.note = fmt("fourcc=0x%08x is listed; the driver lists %d explicit modifier(s) but the peer offered " + "none, so rung 1 is the only rung that can be spelled", + fourcc, (int)nmods); + return cap; + } + if (nmods <= 0) { + cap.note = fmt("fourcc=0x%08x is listed with no explicit modifiers and the peer offered modifier=0x%016llx: " + "the driver never advertised that pair", + fourcc, (unsigned long long)modifier); + return cap; + } + std::vector mods((size_t)nmods); + EGLint modFilled = 0; + if (!e.pModifiers(dpy, (EGLint)fourcc, nmods, mods.data(), nullptr, &modFilled)) { + cap.note = fmt("eglQueryDmaBufModifiersEXT(fourcc=0x%08x, %d) -> 0x%04x", fourcc, (int)nmods, + (unsigned)eglGetError()); + return cap; + } + for (EGLint i = 0; i < modFilled; ++i) { + if ((uint64_t)mods[(size_t)i] == modifier) cap.listed = true; + } + cap.note = cap.listed + ? fmt("fourcc=0x%08x modifier=0x%016llx is in the driver's own list (%d formats, %d modifiers " + "for this one), so any import failure below is the driver refusing its own pair", + fourcc, (unsigned long long)modifier, (int)filled, (int)modFilled) + : fmt("fourcc=0x%08x is listed but modifier=0x%016llx is not among its %d modifiers", fourcc, + (unsigned long long)modifier, (int)modFilled); + return cap; +} + +// The ladder EGL_EXT_image_dma_buf_import{,_modifiers} defines. Rung 0 names the modifier +// through EGL_DMA_BUF_PLANE0_MODIFIER_*_EXT, rung 1 is the original four-integer form. A +// peer with no modifier has no rung-0 spelling at all, and the ladder records that as +// "skipped" rather than as a rejection, so "rung 1 accepted" cannot be misread as the +// driver having refused rung 0. +struct T4Import { + EGLImageKHR image = EGL_NO_IMAGE_KHR; + int rung = -1; + std::string accepted = "none"; + std::string ladder; + std::string fail; +}; + +static T4Import t4ImportImage(EGLDisplay dpy, const T4Egl& e, int fd, const T4Layout& lay) { + T4Import imp; + for (int rung = 0; rung < 2; ++rung) { + if (rung == 0 && lay.modifier == UINT64_MAX) { + imp.ladder += "rung0=skipped(peer offered no modifier) "; + continue; + } + EGLint attrs[24]; + int n = 0; + attrs[n++] = EGL_WIDTH; + attrs[n++] = (EGLint)lay.width; + attrs[n++] = EGL_HEIGHT; + attrs[n++] = (EGLint)lay.height; + attrs[n++] = EGL_LINUX_DRM_FOURCC_EXT; + attrs[n++] = (EGLint)lay.fourcc; + attrs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT; + attrs[n++] = fd; + attrs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; + attrs[n++] = (EGLint)lay.offset; + attrs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; + attrs[n++] = (EGLint)lay.stride; + if (rung == 0) { + attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; + attrs[n++] = (EGLint)(lay.modifier & 0xFFFFFFFFu); + attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; + attrs[n++] = (EGLint)(lay.modifier >> 32); + } + attrs[n++] = EGL_NONE; + EGLImageKHR img = e.pCreateImage(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs); + // read the error either way: it is this call's, and one left in the queue would be + // blamed on the next one + const EGLint err = eglGetError(); + if (img != EGL_NO_IMAGE_KHR) { + imp.image = img; + imp.rung = rung; + imp.accepted = rung == 0 ? "rung0-modifier" : "rung1-legacy"; + imp.ladder += fmt("rung%d=accepted ", rung); + return imp; + } + imp.ladder += fmt("rung%d=0x%04x ", rung, (unsigned)err); + imp.fail = fmt("eglCreateImageKHR(rung%d) -> 0x%04x", rung, (unsigned)err); + } + return imp; +} +// The memory order of the 32-bit layouts the peer can plausibly hand over. A DRM fourcc is +// a name, not a layout rule -- 'AR24' is [31:0] A:R:G:B, so a little-endian buffer holds +// B,G,R,A -- and a derived-then-wrong order would silently invert the peer's byte compare, +// so the layouts are written out instead of computed. +struct T4MemOrder { + uint32_t fourcc; + const char* mem; // channels in memory order, byte 0 first +}; + +static const T4MemOrder kT4MemOrders[] = { + {0x34325241u, "BGRA"}, // 'AR24' DRM_FORMAT_ARGB8888 + {0x34324241u, "RGBA"}, // 'AB24' DRM_FORMAT_ABGR8888 + {0x34324152u, "ABGR"}, // 'RA24' DRM_FORMAT_RGBA8888 + {0x34324142u, "ARGB"}, // 'BA24' DRM_FORMAT_BGRA8888 + {0x34325258u, "BGRX"}, // 'XR24' DRM_FORMAT_XRGB8888 + {0x34324258u, "RGBX"}, // 'XB24' DRM_FORMAT_XBGR8888 + {0x34325852u, "XBGR"}, // 'RX24' DRM_FORMAT_RGBX8888 + {0x34325842u, "XRGB"}, // 'BX24' DRM_FORMAT_BGRX8888 +}; + +struct T4ColorPlan { + bool pinned = false; + GLfloat clear[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + std::string note; +}; + +static T4ColorPlan t4PlanColor(uint32_t fourcc, const uint8_t want[4]) { + T4ColorPlan plan; + const char* mem = nullptr; + for (const T4MemOrder& m : kT4MemOrders) { + if (m.fourcc == fourcc) mem = m.mem; + } + if (!mem) { + // GL channel order is all this side can honestly claim here, and the row says the + // compare the peer is about to make was not pinned by the format. + for (int i = 0; i < 4; ++i) plan.clear[i] = want[i] / 255.0f; + plan.note = fmt("fourcc=0x%08x is not one of the 32-bit layouts whose memory order this side knows, so " + "the clear goes out in GL channel order and the peer's byte compare is not pinned by " + "the format", + fourcc); + return plan; + } + plan.pinned = true; + for (int i = 0; i < 4; ++i) { + switch (mem[i]) { + case 'R': plan.clear[0] = want[i] / 255.0f; break; + case 'G': plan.clear[1] = want[i] / 255.0f; break; + case 'B': plan.clear[2] = want[i] / 255.0f; break; + case 'A': plan.clear[3] = want[i] / 255.0f; break; + default: break; // 'X' is written as 1.0, which want[3] = 0xFF matches + } + } + plan.note = fmt("fourcc=0x%08x holds %s in memory", fourcc, mem); + return plan; +} + +// GL hands back channels while the peer's seed is memory bytes; when the fourcc's memory +// order is known the seed can be re-read in channel order, and the row reports that +// comparison next to the verbatim one so a byte-order convention can never pass for a +// broken import. Returns false, leaving `out` alone, when the order is not known. +static bool t4SeedAsChannels(uint32_t fourcc, const uint8_t seed[4], uint8_t out[4]) { + const char* mem = nullptr; + for (const T4MemOrder& m : kT4MemOrders) { + if (m.fourcc == fourcc) mem = m.mem; + } + if (!mem) return false; + const char* channels = "RGBA"; + for (int ch = 0; ch < 4; ++ch) { + out[ch] = 0xFF; // an 'X' byte carries nothing; the driver writes 1.0 there + for (int i = 0; i < 4; ++i) { + if (mem[i] == channels[ch]) out[ch] = seed[i]; + } + } + return true; +} + +// The 1x1 sampling program the texture read goes through. The leg's own FBO holds the +// image, so the draw has to land somewhere else, and the probe's surface is a 1x1 pbuffer -- +// exactly the target this needs. +static const char* kT4ReadVs = + "#version 300 es\n" + "void main() {\n" + " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" + " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" + "}\n"; + +// highp on the texel centre: (w - 0.5) / w is not representable in mediump for a real image, +// and a rounded coordinate would sample the neighbouring texel -- exactly the mistake the +// corner read exists to catch. +static const char* kT4ReadFs = + "#version 300 es\n" + "precision highp float;\n" + "precision highp sampler2D;\n" + "uniform sampler2D uTex;\n" + "uniform vec2 uTexel;\n" + "out vec4 oColor;\n" + "void main() { oColor = texture(uTex, uTexel); }\n"; +static GLuint t4BuildReadProgram(std::string* fail) { + auto compile = [&](GLenum kind, const char* src) -> GLuint { + GLuint sh = glCreateShader(kind); + glShaderSource(sh, 1, &src, nullptr); + glCompileShader(sh); + GLint ok = 0; + glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); + if (!ok) { + char log[512] = {0}; + glGetShaderInfoLog(sh, sizeof(log) - 1, nullptr, log); + *fail = std::string("shader compile failed: ") + log; + glDeleteShader(sh); + return 0; + } + return sh; + }; + GLuint vs = compile(GL_VERTEX_SHADER, kT4ReadVs); + if (!vs) return 0; + GLuint fs = compile(GL_FRAGMENT_SHADER, kT4ReadFs); + if (!fs) { + glDeleteShader(vs); + return 0; + } + GLuint prog = glCreateProgram(); + glAttachShader(prog, vs); + glAttachShader(prog, fs); + glLinkProgram(prog); + glDeleteShader(vs); + glDeleteShader(fs); + GLint ok = 0; + glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) { + char log[512] = {0}; + glGetProgramInfoLog(prog, sizeof(log) - 1, nullptr, log); + *fail = std::string("link failed: ") + log; + glDeleteProgram(prog); + return 0; + } + return prog; +} + +static bool t4ReadTexel(GLuint prog, GLint locTex, GLint locTexel, GLuint tex, uint32_t w, uint32_t h, uint32_t x, + uint32_t y, uint8_t out[4], std::string* fail) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, 1, 1); + glUseProgram(prog); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + glUniform1i(locTex, 0); + glUniform2f(locTexel, ((float)x + 0.5f) / (float)w, ((float)y + 0.5f) / (float)h); + glDrain(); + glDrawArrays(GL_TRIANGLES, 0, 3); + const GLenum drawErr = glDrain(); + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out); + const GLenum readErr = glDrain(); + if (drawErr != GL_NO_ERROR || readErr != GL_NO_ERROR) { + *fail = fmt("1x1 sampling draw -> %s, glReadPixels -> %s", glErrStr(drawErr).c_str(), + glErrStr(readErr).c_str()); + return false; + } + return true; +} + +static bool t4ReadRb(GLuint fbo, uint32_t x, uint32_t y, uint8_t out[4], std::string* fail) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glDrain(); + glReadPixels((GLint)x, (GLint)y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out); + const GLenum e = glDrain(); + if (e != GL_NO_ERROR) { + *fail = "glReadPixels off the image FBO -> " + glErrStr(e); + return false; + } + return true; +} + +// Clears one path to `color`, then glFinish *and* an explicit fence whose wait result is what +// comes back: the peer is a different process on a different driver stack, so "the drawing +// landed" needs evidence this side can show, and glFinish alone only says the commands were +// accepted. +static std::string t4WritePath(GLuint fbo, uint32_t w, uint32_t h, const GLfloat color[4]) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glViewport(0, 0, (GLsizei)w, (GLsizei)h); + glClearColor(color[0], color[1], color[2], color[3]); + glDrain(); + glClear(GL_COLOR_BUFFER_BIT); + const GLenum clearErr = glDrain(); + glFinish(); + const GLenum finishErr = glDrain(); + const GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + const GLenum fenceErr = glDrain(); + std::string wait = "no sync object"; + if (sync) { + // bounded: an unsignalled fence has to read as GL_TIMEOUT_EXPIRED, never as a hang + wait = t4WaitStr(glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull)); + glDeleteSync(sync); + glDrain(); + } + return fmt("clear=%s finish=%s fence=%s wait=%s", glErrStr(clearErr).c_str(), glErrStr(finishErr).c_str(), + glErrStr(fenceErr).c_str(), wait.c_str()); +} + +// Per-path state: the import attempt, the two reads, and the peer's answer to the clear. +struct T4PathRun { + const char* name = "?"; + bool targetOk = false; // glEGLImageTarget*OES took the image + std::string targetFail; + bool fboOk = false; // and the image is a complete COLOR_ATTACHMENT0 + std::string fboFail; + bool readDone = false; + uint8_t word[4] = {0, 0, 0, 0}; + uint8_t corner[4] = {0, 0, 0, 0}; + std::string readFail; + bool wrote = false; + bool ackGot = false; + int32_t ackOk = 0; + uint8_t observed[4] = {0, 0, 0, 0}; + std::string ackNote; + std::string writeFail; +}; + +struct T4GL { + GLuint tex = 0; + GLuint rb = 0; + GLuint fboTex = 0; + GLuint fboRb = 0; + GLuint prog = 0; + GLint locTex = -1; + GLint locTexel = -1; +}; + +// Waits for `want`, stepping over -- and naming -- any other frame. The frozen protocol +// gives MSG_T4_RESULT no payload contract, so a frame carrying it is skipped rather than +// parsed, which keeps the stream aligned for the ack that follows. Skipping assumes the +// unexpected frame is no larger than the one being awaited, which is the only size the +// protocol defines. +static bool t4RecvTag(int sock, uint32_t want, void* payload, size_t maxLen, size_t* gotOut, int* fdOut, + std::string* skipped, std::string* fail) { + for (int i = 0; i < 8; ++i) { + uint32_t tag = 0; + size_t got = 0; + int fd = -1; + if (!recvMsg(sock, &tag, payload, maxLen, &got, &fd)) { + *fail = fmt("recvMsg -> errno=%d(%s)", errno, strerror(errno)); + if (fd >= 0) close(fd); + return false; + } + if (tag == want) { + if (gotOut) { + *gotOut = got; + } + if (fdOut) { + *fdOut = fd; + } else if (fd >= 0) { + close(fd); + } + return true; + } + if (fd >= 0) close(fd); + if (skipped) { + if (!skipped->empty()) *skipped += ","; + *skipped += fmt("tag%u(len=%zu)", tag, got); + } + } + *fail = "too many unexpected frames before the expected one"; + return false; +} + +// The Android-minted row, defined at the end of this section: runT4 records the +// container-minted direction first and this one second, and the second is the row +// the first cannot stand in for. +static void runT4AhbRow(GlCtx& g, const T4Egl& e, int sock, const T4Layout& lay); + +static void runT4(GlCtx& g, bool glOk, const char* endpoint) { + // Capability gates come before the listen: with no context, or without the extensions + // and entry points the import is spelled in, there is nothing this side could import, + // and saying so here beats leaving the peer to discover it through its own timeout. + if (!glOk) { + record(kT4Row, "SKIP", "no headless GLES context: nothing on this side could import the peer's dma-buf"); + return; + } + T4Egl e; + e.pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); + e.pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); + e.pFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT"); + e.pModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT"); + e.pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); + e.pTargetRb = + (PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC)eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"); + + if (!g.hasEgl("EGL_KHR_image_base") || !e.pCreateImage || !e.pDestroyImage) { + record(kT4Row, "UNSUPPORTED", + fmt("EGL_KHR_image_base=%d and of that extension eglCreateImageKHR=%d eglDestroyImageKHR=%d", + (int)g.hasEgl("EGL_KHR_image_base"), (int)(e.pCreateImage != nullptr), + (int)(e.pDestroyImage != nullptr))); + return; + } + if (!g.hasGl("GL_OES_EGL_image") || (!e.pTargetTexture && !e.pTargetRb)) { + record(kT4Row, "UNSUPPORTED", + fmt("GL_OES_EGL_image=%d and of that extension glEGLImageTargetTexture2DOES=%d " + "glEGLImageTargetRenderbufferStorageOES=%d", + (int)g.hasGl("GL_OES_EGL_image"), (int)(e.pTargetTexture != nullptr), + (int)(e.pTargetRb != nullptr))); + return; + } + + int ls = rtListen(endpoint); + if (ls < 0) { + record(kT4Row, "FAIL", fmt("could not listen on %s for the container peer: errno=%d(%s)", endpoint, errno, + strerror(errno))); + return; + } + setRecvTimeout(ls, 60); + pr("T4: listening on %s for the Droidspaces peer (abstract socket; net_mode=host keeps both ends in one " + "namespace)", + endpoint); + int sock = accept(ls, nullptr, nullptr); + close(ls); + if (sock < 0) { + record(kT4Row, "FAIL", fmt("no peer dialed %s within the accept timeout: errno=%d(%s)", endpoint, errno, + strerror(errno))); + return; + } + setRecvTimeout(sock, 60); + + // ---- 1. the peer's offer ------------------------------------------------- + T4Layout lay{}; + uint32_t tag = 0; + size_t got = 0; + int fd = -1; + if (!recvMsg(sock, &tag, &lay, sizeof(lay), &got, &fd) || tag != MSG_T4_OFFER || got != sizeof(lay)) { + record(kT4Row, "FAIL", fmt("no T4 offer: tag=%u len=%zu errno=%d(%s)", tag, got, errno, strerror(errno))); + if (fd >= 0) close(fd); + close(sock); + return; + } + lay.peer[sizeof(lay.peer) - 1] = 0; + lay.note[sizeof(lay.note) - 1] = 0; + const std::string fdDesc = describeFd(fd); + const std::string modifierStr = + lay.modifier == UINT64_MAX ? std::string("none") : fmt("0x%016llx", (unsigned long long)lay.modifier); + pr("T4 peer [%s] %s", lay.peer, lay.note); + pr("T4 offer: v%u %ux%u fourcc=0x%08x stride=%u offset=%u modifier=%s seed=%s fd[%s]", lay.version, lay.width, + lay.height, lay.fourcc, lay.stride, lay.offset, modifierStr.c_str(), t4Hex4(lay.seed).c_str(), + fdDesc.c_str()); + + if (lay.magic != kT4Magic) { + record(kT4Row, "FAIL", fmt("offer magic=0x%08x, expected 0x%08x: the two sides do not speak the same " + "protocol | peer [%s] %s", + lay.magic, kT4Magic, lay.peer, lay.note)); + if (fd >= 0) close(fd); + close(sock); + return; + } + if (fd < 0) { + record(kT4Row, "FAIL", fmt("the offer carried no fd over SCM_RIGHTS, so the peer could not create the GBM " + "buffer | %ux%u fourcc=0x%08x | peer [%s] %s", + lay.width, lay.height, lay.fourcc, lay.peer, lay.note)); + close(sock); + return; + } + if (lay.width == 0 || lay.height == 0 || lay.stride == 0) { + record(kT4Row, "FAIL", fmt("degenerate layout %ux%u stride=%u: nothing can be imported from it | fd[%s]", + lay.width, lay.height, lay.stride, fdDesc.c_str())); + close(fd); + close(sock); + return; + } + + // ---- the capability gate: EGL_EXT_image_dma_buf_import ------------------- + // Every rung of the EGL_LINUX_DMA_BUF_EXT ladder is defined by that extension, + // so a driver that does not advertise it does not take a container's dma-buf at + // all. Walking the ladder anyway would report a capability answer as an import + // failure -- the two things this row exists to keep apart -- so the row is + // recorded here, naming the extension, and the ladder is not entered. The peer + // stays connected: the Android-minted row below is the half of T4 that can still + // be measured on such a driver, and it needs this same connection. + if (!g.hasEgl("EGL_EXT_image_dma_buf_import")) { + pr("T4: EGL_EXT_image_dma_buf_import is not advertised: the EGL_LINUX_DMA_BUF_EXT ladder is not walked"); + record(kT4Row, "UNSUPPORTED", + fmt("EGL_EXT_image_dma_buf_import is not advertised (EGL_KHR_image_base=%d, GL_OES_EGL_image=%d), so " + "EGL_LINUX_DMA_BUF_EXT has no spelling here and no rung of that ladder can be accepted or " + "rejected: the container's %ux%u fourcc=0x%08x stride=%u offset=%u modifier=%s fd[%s] and peer " + "[%s] %s are recorded as an offer this driver cannot take up", + (int)g.hasEgl("EGL_KHR_image_base"), (int)g.hasGl("GL_OES_EGL_image"), lay.width, lay.height, + lay.fourcc, lay.stride, lay.offset, modifierStr.c_str(), fdDesc.c_str(), lay.peer, lay.note)); + runT4AhbRow(g, e, sock, lay); + close(fd); + close(sock); + return; + } + + // ---- 2. the driver's own question, before any import --------------------- + const T4Cap cap = t4Capability(g.dpy, e, lay.fourcc, lay.modifier); + pr("T4 capability: asked=%d formatListed=%d pairListed=%d formats=%d modifiers=%d | %s", (int)cap.asked, + (int)cap.formatListed, (int)cap.listed, cap.formats, cap.modifiers, cap.note.c_str()); + + // ---- 3. the import ladder ------------------------------------------------ + T4Import imp = t4ImportImage(g.dpy, e, fd, lay); + pr("T4 import: accepted=%s | ladder: %s| %s", imp.accepted.c_str(), imp.ladder.c_str(), + imp.fail.empty() ? "-" : imp.fail.c_str()); + + // ---- 4. read both paths, at two pixels each ------------------------------ + T4GL gl; + T4PathRun tex, rb; + tex.name = "tex"; + rb.name = "rb"; + uint8_t seedCh[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + const bool seedChKnown = t4SeedAsChannels(lay.fourcc, lay.seed, seedCh); + + if (imp.image != EGL_NO_IMAGE_KHR) { + if (e.pTargetTexture) { + glGenFramebuffers(1, &gl.fboTex); + glGenTextures(1, &gl.tex); + glBindTexture(GL_TEXTURE_2D, gl.tex); + // NEAREST and no mip chain: the read has to land on the texel asked for and not + // on an average of its neighbours + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glDrain(); + e.pTargetTexture(GL_TEXTURE_2D, (GLeglImageOES)imp.image); + const GLenum texErr = glDrain(); + if (texErr != GL_NO_ERROR) { + tex.targetFail = "glEGLImageTargetTexture2DOES -> " + glErrStr(texErr); + } else { + tex.targetOk = true; + glBindFramebuffer(GL_FRAMEBUFFER, gl.fboTex); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl.tex, 0); + const GLenum st = glCheckFramebufferStatus(GL_FRAMEBUFFER); + const GLenum attachErr = glDrain(); + if (st != GL_FRAMEBUFFER_COMPLETE || attachErr != GL_NO_ERROR) { + tex.fboFail = fmt("the image texture attached but the FBO is 0x%04x (%s)", (unsigned)st, + glErrStr(attachErr).c_str()); + } else { + tex.fboOk = true; + } + } + } else { + tex.targetFail = "glEGLImageTargetTexture2DOES is absent, so the texture path was not exercised"; + } + + if (e.pTargetRb) { + glGenFramebuffers(1, &gl.fboRb); + glGenRenderbuffers(1, &gl.rb); + glBindRenderbuffer(GL_RENDERBUFFER, gl.rb); + glDrain(); + e.pTargetRb(GL_RENDERBUFFER, (GLeglImageOES)imp.image); + const GLenum rbErr = glDrain(); + if (rbErr != GL_NO_ERROR) { + rb.targetFail = "glEGLImageTargetRenderbufferStorageOES -> " + glErrStr(rbErr); + } else { + rb.targetOk = true; + glBindFramebuffer(GL_FRAMEBUFFER, gl.fboRb); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, gl.rb); + const GLenum st = glCheckFramebufferStatus(GL_FRAMEBUFFER); + const GLenum attachErr = glDrain(); + if (st != GL_FRAMEBUFFER_COMPLETE || attachErr != GL_NO_ERROR) { + rb.fboFail = fmt("the image renderbuffer attached but the FBO is 0x%04x (%s)", (unsigned)st, + glErrStr(attachErr).c_str()); + } else { + rb.fboOk = true; + } + } + } else { + rb.targetFail = + "glEGLImageTargetRenderbufferStorageOES is absent, so the renderbuffer path was not exercised"; + } + } else { + tex.targetFail = fmt("no EGLImage to attach: %s (ladder: %s)", + imp.fail.empty() ? "the import ladder was not entered" : imp.fail.c_str(), + imp.ladder.c_str()); + rb.targetFail = tex.targetFail; + } + + const uint32_t farX = lay.width - 1, farY = lay.height - 1; + if (tex.targetOk) { + std::string progFail; + gl.prog = t4BuildReadProgram(&progFail); + if (!gl.prog) { + tex.readFail = progFail; + } else { + gl.locTex = glGetUniformLocation(gl.prog, "uTex"); + gl.locTexel = glGetUniformLocation(gl.prog, "uTexel"); + std::string failA, failB; + const bool okA = t4ReadTexel(gl.prog, gl.locTex, gl.locTexel, gl.tex, lay.width, lay.height, 0, 0, + tex.word, &failA); + const bool okB = t4ReadTexel(gl.prog, gl.locTex, gl.locTexel, gl.tex, lay.width, lay.height, farX, farY, + tex.corner, &failB); + tex.readDone = okA && okB; + if (!okA) tex.readFail = "pixel(0,0): " + failA; + if (!okB) tex.readFail += (tex.readFail.empty() ? "" : "; ") + std::string("pixel(w-1,h-1): ") + failB; + } + } else { + tex.readFail = "not attempted: " + tex.targetFail; + } + + if (rb.targetOk && rb.fboOk) { + std::string failA, failB; + const bool okA = t4ReadRb(gl.fboRb, 0, 0, rb.word, &failA); + const bool okB = t4ReadRb(gl.fboRb, farX, farY, rb.corner, &failB); + rb.readDone = okA && okB; + if (!okA) rb.readFail = "pixel(0,0): " + failA; + if (!okB) rb.readFail += (rb.readFail.empty() ? "" : "; ") + std::string("pixel(w-1,h-1): ") + failB; + } else if (rb.targetOk) { + rb.readFail = "not attempted: " + rb.fboFail; + } else { + rb.readFail = "not attempted: " + rb.targetFail; + } + + // The protocol's read comparison is verbatim and the verdict keeps it that way. GL hands + // back channels while the peer's seed is memory bytes, so when the fourcc's memory order + // is known the same comparison is also made against the seed re-read in channel order: + // the two disagreeing is a fact about byte order, not about the import, and the row shows + // both rather than letting the verbatim test alone imply a broken driver. + const bool texReadOk = tex.targetOk && tex.readDone && !memcmp(tex.word, lay.seed, 4) && + !memcmp(tex.corner, lay.seed, 4); + const bool rbReadOk = rb.targetOk && rb.readDone && !memcmp(rb.word, lay.seed, 4) && + !memcmp(rb.corner, lay.seed, 4); + const bool texOrderOk = seedChKnown && !memcmp(tex.word, seedCh, 4) && !memcmp(tex.corner, seedCh, 4); + const bool rbOrderOk = seedChKnown && !memcmp(rb.word, seedCh, 4) && !memcmp(rb.corner, seedCh, 4); + + // ---- 5. what the two reads produced, back to the peer -------------------- + T4Read rep{}; + rep.texOk = texReadOk ? 1 : 0; + rep.rbOk = rbReadOk ? 1 : 0; + rep.rung = imp.rung; + memcpy(rep.texWord, tex.word, 4); + memcpy(rep.texCorner, tex.corner, 4); + memcpy(rep.rbWord, rb.word, 4); + memcpy(rep.rbCorner, rb.corner, 4); + snprintf(rep.importNote, sizeof(rep.importNote), "%s | ladder: %s| %s", imp.accepted.c_str(), imp.ladder.c_str(), + cap.note.c_str()); + const bool readSent = sendMsg(sock, MSG_T4_READ, &rep, sizeof(rep), -1); + if (!readSent) pr("T4: MSG_T4_READ did not go out: errno=%d(%s)", errno, strerror(errno)); + + // ---- 6/7. draw into each path, ack by ack -------------------------------- + // Both palettes end in 0xFF so an ignored 'X' channel and a real 'A' agree, and the two + // clears differ in every byte so a swapped path cannot pass unnoticed. + const uint8_t rbWant[4] = {0x11, 0x22, 0x33, 0xFF}; + const uint8_t texWant[4] = {0x44, 0x55, 0x66, 0xFF}; + const T4ColorPlan rbPlan = t4PlanColor(lay.fourcc, rbWant); + const T4ColorPlan texPlan = t4PlanColor(lay.fourcc, texWant); + std::string rbFence = "not attempted", texFence = "not attempted"; + std::string skipped; + + if (rb.fboOk) { + rbFence = t4WritePath(gl.fboRb, lay.width, lay.height, rbPlan.clear); + T4Wrote w{}; + w.path = 0; + memcpy(w.color, rbWant, 4); + if (!sendMsg(sock, MSG_T4_WROTE, &w, sizeof(w), -1)) { + rb.writeFail = fmt("sendMsg(MSG_T4_WROTE path=0) errno=%d(%s)", errno, strerror(errno)); + } else { + rb.wrote = true; + T4Ack ack{}; + std::string fail; + if (!t4RecvTag(sock, MSG_T4_ACK, &ack, sizeof(ack), nullptr, nullptr, &skipped, &fail)) { + rb.writeFail = "no ack for the renderbuffer write: " + fail; + } else { + ack.note[sizeof(ack.note) - 1] = 0; + rb.ackGot = true; + rb.ackOk = ack.ok; + memcpy(rb.observed, ack.observed, 4); + rb.ackNote = ack.note; + if (!ack.ok) { + rb.writeFail = fmt("peer ack ok=0 observed=%s want=%s note=[%s]", t4Hex4(ack.observed).c_str(), + t4Hex4(rbWant).c_str(), ack.note); + } + } + } + } else { + const std::string whyNot = rb.targetOk ? rb.fboFail : rb.targetFail; + rb.writeFail = "no clear was attempted: " + whyNot; + } + + if (tex.fboOk) { + texFence = t4WritePath(gl.fboTex, lay.width, lay.height, texPlan.clear); + T4Wrote w{}; + w.path = 1; + memcpy(w.color, texWant, 4); + if (!sendMsg(sock, MSG_T4_WROTE, &w, sizeof(w), -1)) { + tex.writeFail = fmt("sendMsg(MSG_T4_WROTE path=1) errno=%d(%s)", errno, strerror(errno)); + } else { + tex.wrote = true; + T4Ack ack{}; + std::string fail; + if (!t4RecvTag(sock, MSG_T4_ACK, &ack, sizeof(ack), nullptr, nullptr, &skipped, &fail)) { + tex.writeFail = "no ack for the texture write: " + fail; + } else { + ack.note[sizeof(ack.note) - 1] = 0; + tex.ackGot = true; + tex.ackOk = ack.ok; + memcpy(tex.observed, ack.observed, 4); + tex.ackNote = ack.note; + if (!ack.ok) { + tex.writeFail = fmt("peer ack ok=0 observed=%s want=%s note=[%s]", t4Hex4(ack.observed).c_str(), + t4Hex4(texWant).c_str(), ack.note); + } + } + } + } else { + const std::string whyNot = tex.targetOk ? tex.fboFail : tex.targetFail; + tex.writeFail = "no clear was attempted: " + whyNot; + } + pr("T4 clear: renderbuffer %s ack=%d | texture %s ack=%d", rbFence.c_str(), rb.ackGot ? rb.ackOk : -1, + texFence.c_str(), tex.ackGot ? tex.ackOk : -1); + + // ---- 8. the peer's last word --------------------------------------------- + // MSG_T4_RESULT has no payload contract in the frozen protocol, so it is taken raw: the + // length and the leading bytes go into the row and nothing is parsed out of it. + uint8_t lastWord[256]; + memset(lastWord, 0, sizeof(lastWord)); + std::string resultNote; + { + setRecvTimeout(sock, 2); // bounded: a peer that says nothing must not hold the row up + uint32_t rtag = 0; + size_t rgot = 0; + if (recvMsg(sock, &rtag, lastWord, sizeof(lastWord), &rgot, nullptr)) { + if (rtag == MSG_T4_RESULT) { + std::string hex; + for (size_t i = 0; i < rgot && i < 16; ++i) hex += fmt("%02x", lastWord[i]); + resultNote = fmt("MSG_T4_RESULT len=%zu bytes=%s", rgot, hex.c_str()); + } else { + resultNote = fmt("the peer's last frame was tag=%u len=%zu, not MSG_T4_RESULT", rtag, rgot); + } + } else { + resultNote = fmt("no MSG_T4_RESULT frame (the peer closed, timed out, or sent more than %zu bytes): " + "errno=%d(%s)", + sizeof(lastWord), errno, strerror(errno)); + } + } + + // ---- the verdict --------------------------------------------------------- + std::vector legs; + { + Leg l; + l.name = tex.name; + l.decisive = true; + l.attempted = tex.targetOk; + l.readOk = texReadOk; + l.writeOk = tex.ackGot && tex.ackOk != 0; + if (!tex.targetOk) + l.fail = tex.targetFail; + else if (!tex.readDone) + l.fail = "texture read: " + tex.readFail; + else if (!l.readOk) + l.fail = fmt("texture read is not the peer's seed verbatim: word=%s corner=%s seed(memory)=%s " + "seed(channels)=%s channelMatch=%d -- GL hands back channels while the peer fills " + "memory bytes, so a fourcc whose memory order is not RGBA differs here on a working " + "import", + t4Hex4(tex.word).c_str(), t4Hex4(tex.corner).c_str(), t4Hex4(lay.seed).c_str(), + seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", (int)texOrderOk); + else if (!l.writeOk) + l.fail = tex.writeFail; + legs.push_back(l); + } + { + Leg l; + l.name = rb.name; + l.decisive = true; + l.attempted = rb.targetOk; + l.readOk = rbReadOk; + l.writeOk = rb.ackGot && rb.ackOk != 0; + if (!rb.targetOk) + l.fail = rb.targetFail; + else if (!rb.readDone) + l.fail = "renderbuffer read: " + rb.readFail; + else if (!l.readOk) + l.fail = fmt("renderbuffer read is not the peer's seed verbatim: word=%s corner=%s seed(memory)=%s " + "seed(channels)=%s channelMatch=%d -- GL hands back channels while the peer fills " + "memory bytes, so a fourcc whose memory order is not RGBA differs here on a working " + "import", + t4Hex4(rb.word).c_str(), t4Hex4(rb.corner).c_str(), t4Hex4(lay.seed).c_str(), + seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", (int)rbOrderOk); + else if (!l.writeOk) + l.fail = rb.writeFail; + legs.push_back(l); + } + std::string why; + const char* status = legVerdict(legs, &why); + + // every failure string already names its step, so an empty one leaves no marker + auto join = [](const std::string& a, const std::string& b) { + if (a.empty()) return b; + if (b.empty()) return a; + return a + "; " + b; + }; + + std::string detail = fmt( + "%s | %ux%u fourcc=0x%08x stride=%u offset=%u modifier=%s seed=%s | capability[asked=%d format=%d pair=%d " + "formats=%d modifiers=%d: %s] | import[accepted=%s ladder: %s| %s] | fd[%s]", + legTrace(legs).c_str(), lay.width, lay.height, lay.fourcc, lay.stride, lay.offset, modifierStr.c_str(), + t4Hex4(lay.seed).c_str(), (int)cap.asked, (int)cap.formatListed, (int)cap.listed, cap.formats, cap.modifiers, + cap.note.c_str(), imp.accepted.c_str(), imp.ladder.c_str(), imp.fail.empty() ? "-" : imp.fail.c_str(), + fdDesc.c_str()); + detail += fmt(" | tex[target=%d fbo=%d word=%s corner=%s read=%d orderMapped=%d(%s) fail=[%s]]", + (int)tex.targetOk, (int)tex.fboOk, t4Hex4(tex.word).c_str(), t4Hex4(tex.corner).c_str(), + (int)tex.readDone, (int)texOrderOk, seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", + join(tex.targetFail, join(tex.readFail, tex.writeFail)).c_str()); + detail += fmt(" rb[target=%d fbo=%d word=%s corner=%s read=%d orderMapped=%d(%s) fail=[%s]]", + (int)rb.targetOk, (int)rb.fboOk, t4Hex4(rb.word).c_str(), t4Hex4(rb.corner).c_str(), + (int)rb.readDone, (int)rbOrderOk, seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", + join(rb.targetFail, join(rb.readFail, rb.writeFail)).c_str()); + detail += fmt(" | clear[rb fence=%s want=%s ack=%d observed=%s note=[%s]] [tex fence=%s want=%s ack=%d " + "observed=%s note=[%s]] skipped=[%s]", + rbFence.c_str(), t4Hex4(rbWant).c_str(), rb.ackGot ? rb.ackOk : -1, t4Hex4(rb.observed).c_str(), + rb.ackNote.c_str(), texFence.c_str(), t4Hex4(texWant).c_str(), tex.ackGot ? tex.ackOk : -1, + t4Hex4(tex.observed).c_str(), tex.ackNote.c_str(), skipped.c_str()); + detail += fmt(" | colourOrder[rb pinned=%d %s] [tex pinned=%d %s] | readSent=%d | %s | peer [%s] %s | %s", + (int)rbPlan.pinned, rbPlan.note.c_str(), (int)texPlan.pinned, texPlan.note.c_str(), + (int)readSent, resultNote.c_str(), lay.peer, lay.note, why.c_str()); + record(kT4Row, status, detail); + + // The other half of T4, over the connection that is still open: this side mints + // the image and the container imports it. See runT4AhbRow for what it measures. + runT4AhbRow(g, e, sock, lay); + + // The EGLImage outlives every GL object that wraps it, and the fd is the peer's only + // handle on the buffer: tear down in that order. + if (gl.prog) glDeleteProgram(gl.prog); + if (gl.fboTex) glDeleteFramebuffers(1, &gl.fboTex); + if (gl.fboRb) glDeleteFramebuffers(1, &gl.fboRb); + if (gl.tex) glDeleteTextures(1, &gl.tex); + if (gl.rb) glDeleteRenderbuffers(1, &gl.rb); + if (imp.image != EGL_NO_IMAGE_KHR) e.pDestroyImage(g.dpy, imp.image); + close(fd); + close(sock); +} + +// --------------------------------------------------------------------------- +// T4-ahb-image: the same image, minted on the Android side +// +// T4-image-import above asks whether an image allocated in the container can be +// imported HERE. This row asks the other half of the same question, and it is the +// half that survives on a driver with no EGL_EXT_image_dma_buf_import. An app +// cannot adopt the container's fd as an AHardwareBuffer (createFromHandle is a +// SystemApi, not NDK), so the image is born on this side and travels the other way: +// the buffer is allocated in this process, handed over as an fd over a socketpair -- +// the mechanism T0 hands its BLOB across -- and imported in the container as a GBM bo. +// +// Both directions are measured, and neither is measured by this side alone: +// read the peer must find, at (0,0) AND at the far corner of its own mapping, +// the four bytes this side cleared into the image. +// write the peer fills its mapping with a pattern of its own and this side reads +// it back through the GPU -- glEGLImageTargetTexture2DOES plus the same 1x1 +// sampling program the row above uses, not a second CPU mapping of a buffer +// this process already owns, which would prove nothing about the peer. +// A refusal anywhere is recorded where it happened with its EGL or GL error named, +// and can never come out as OK. +// --------------------------------------------------------------------------- + +static const char* kT4AhbRow = "T4-ahb-image"; + +// The DRM fourcc this row offers, and why it is that one: an AHardwareBuffer does +// not carry a fourcc, so the two sides must agree on memory order in words. +// AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM is R,G,B,A in memory, and the DRM name with +// that same little-endian layout is DRM_FORMAT_ABGR8888, fourcc_code('A','B','2','4') +// = 0x34324241, whose [31:0] A:B:G:R puts R in byte 0 -- the pairing gralloc itself +// uses for HAL_PIXEL_FORMAT_RGBA_8888. Choosing it is what makes the comparison +// below mean something: t4PlanColor() derives the clear colour from the same table +// entry the peer's bytes are compared through, so a channel mistake cannot cancel +// itself out between the two sides. +static const uint32_t kT4AhbFourcc = 0x34324241u; // DRM_FORMAT_ABGR8888, memory order R,G,B,A + +// AHardwareBuffer_Desc::stride IS IN PIXELS -- the platform's own unit -- while every +// consumer on the wire wants the row stride in BYTES: gbm_bo_import is told it, and the +// peer's mapping and both of its pixel offsets follow from it. Announcing the pixel +// count as if it were bytes cost a device run (the peer imported a 64-byte row for a +// 256-byte-row image), so the conversion lives here, next to the fourcc that fixes the +// bytes per pixel: DRM_FORMAT_ABGR8888 is four bytes per pixel, and the AHB format this +// row allocates (AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) is the same memory order. +static const uint32_t kT4AhbBytesPerPixel = 4u; + +// The colour this side clears into the image before offering it: four bytes in that +// memory order, all four distinct, so a permuted or repeated byte shows up as a +// different sequence instead of hiding inside the colour itself. +static const uint8_t kT4AhbSeed[4] = {0x2A, 0x5B, 0x8C, 0xFF}; + +static void runT4AhbRow(GlCtx& g, const T4Egl& e, int sock, const T4Layout& lay) { + // The row is sized by the leg's only size knob: the peer dialled with --width/ + // --height (64x64 by default) and the same two numbers size this allocation, so a + // single option moves both sides and they cannot silently disagree about the + // layout. The stride, by contrast, is never assumed to be width*4: it is what + // AHardwareBuffer_describe() reports in PIXELS, converted to the BYTES every consumer + // on the wire wants, and it is what the peer is told. + const uint32_t w = lay.width, h = lay.height; + const uint32_t farX = w - 1, farY = h - 1; + const T4ColorPlan plan = t4PlanColor(kT4AhbFourcc, kT4AhbSeed); + + // ---- the entry points this row is spelled in ----------------------------- + if (!g.hasEgl("EGL_ANDROID_get_native_client_buffer") || !g.pGetNativeClientBuffer) { + record(kT4AhbRow, "UNSUPPORTED", + fmt("EGL_ANDROID_get_native_client_buffer=%d and of that extension eglGetNativeClientBufferANDROID=%d: " + "an AHardwareBuffer cannot become an EGLClientBuffer here, so there is no image to offer", + (int)g.hasEgl("EGL_ANDROID_get_native_client_buffer"), + (int)(g.pGetNativeClientBuffer != nullptr))); + return; + } + if (!e.pTargetRb || !e.pTargetTexture) { + record(kT4AhbRow, "UNSUPPORTED", + fmt("GL_OES_EGL_image=%d but glEGLImageTargetRenderbufferStorageOES=%d glEGLImageTargetTexture2DOES=%d: " + "this row clears through a renderbuffer and reads back through the texture path, and one of the " + "two is absent", + (int)g.hasGl("GL_OES_EGL_image"), (int)(e.pTargetRb != nullptr), + (int)(e.pTargetTexture != nullptr))); + return; + } + + // ---- 1. the buffer, allocated here, described verbatim ------------------- + AHardwareBuffer_Desc desc{}; + desc.width = w; + desc.height = h; + desc.layers = 1; + desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | + AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN; + AHardwareBuffer* ahb = nullptr; + const int allocRc = AHardwareBuffer_allocate(&desc, &ahb); + if (allocRc != 0 || !ahb) { + record(kT4AhbRow, "UNSUPPORTED", + fmt("AHardwareBuffer_allocate(width=%u height=%u layers=1 format=AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM " + "usage=0x%llx) rc=%d errno=%d(%s): this side cannot mint the image at all, so the " + "Android-minted direction has nothing to measure", + w, h, (unsigned long long)desc.usage, allocRc, errno, strerror(errno))); + return; + } + AHardwareBuffer_Desc back{}; + AHardwareBuffer_describe(ahb, &back); + pr("T4-ahb: AHardwareBuffer_allocate rc=%d, AHardwareBuffer_describe -> width=%u height=%u layers=%u " + "format=0x%x usage=0x%llx stride=%u rfu0=%u rfu1=%llu", + allocRc, back.width, back.height, back.layers, (unsigned)back.format, (unsigned long long)back.usage, + back.stride, back.rfu0, (unsigned long long)back.rfu1); + if (back.width == 0 || back.height == 0 || back.stride == 0) { + record(kT4AhbRow, "FAIL", + fmt("AHardwareBuffer_describe returned a degenerate layout: %ux%u layers=%u stride=%u", back.width, + back.height, back.layers, back.stride)); + AHardwareBuffer_release(ahb); + return; + } + + // ---- 2. import it HERE: client buffer -> EGLImage -> renderbuffer -------- + EGLClientBuffer cb = g.pGetNativeClientBuffer(ahb); + const EGLint cbErr = eglGetError(); + pr("T4-ahb: eglGetNativeClientBufferANDROID(ahb=%p) -> %p, eglGetError=0x%04x", (void*)ahb, (void*)cb, + (unsigned)cbErr); + if (!cb) { + record(kT4AhbRow, "FAIL", + fmt("eglGetNativeClientBufferANDROID(ahb) returned NULL with eglGetError=0x%04x, for an " + "AHardwareBuffer this same driver allocated one call earlier (%ux%u stride=%u)", + (unsigned)cbErr, back.width, back.height, back.stride)); + AHardwareBuffer_release(ahb); + return; + } + const EGLint imgAttrs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; + EGLImageKHR img = e.pCreateImage(g.dpy, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, cb, imgAttrs); + const EGLint imgErr = eglGetError(); + pr("T4-ahb: eglCreateImageKHR(EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, clientBuffer=%p) -> %p, " + "eglGetError=0x%04x", + (void*)cb, (void*)img, (unsigned)imgErr); + if (img == EGL_NO_IMAGE_KHR) { + record(kT4AhbRow, "FAIL", + fmt("eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) -> EGL_NO_IMAGE_KHR with eglGetError=0x%04x: the " + "driver took the client buffer (%p) and then refused the image", + (unsigned)imgErr, (void*)cb)); + AHardwareBuffer_release(ahb); + return; + } + + GLuint rb = 0, fboRb = 0, tex = 0, fboTex = 0, prog = 0; + std::string stepFail; + glGenRenderbuffers(1, &rb); + glBindRenderbuffer(GL_RENDERBUFFER, rb); + glDrain(); + e.pTargetRb(GL_RENDERBUFFER, (GLeglImageOES)img); + const GLenum rbErr = glDrain(); + pr("T4-ahb: glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, image) -> %s", glErrStr(rbErr).c_str()); + if (rbErr != GL_NO_ERROR) stepFail = "glEGLImageTargetRenderbufferStorageOES -> " + glErrStr(rbErr); + glGenFramebuffers(1, &fboRb); + glBindFramebuffer(GL_FRAMEBUFFER, fboRb); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb); + const GLenum fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + const GLenum attachErr = glDrain(); + pr("T4-ahb: image renderbuffer FBO completeness=0x%04x (%s), glGetError=%s", (unsigned)fboStatus, + fboStatus == GL_FRAMEBUFFER_COMPLETE ? "GL_FRAMEBUFFER_COMPLETE" : "not complete", + glErrStr(attachErr).c_str()); + if (stepFail.empty() && (fboStatus != GL_FRAMEBUFFER_COMPLETE || attachErr != GL_NO_ERROR)) { + stepFail = fmt("the image renderbuffer attached but the FBO is 0x%04x (%s)", (unsigned)fboStatus, + glErrStr(attachErr).c_str()); + } + if (!stepFail.empty()) { + record(kT4AhbRow, "FAIL", + fmt("%s | AHB[%ux%u layers=%u format=0x%x usage=0x%llx stride=%u] | fourcc=0x%08x seed=%s | image=%p " + "egl=0x%04x clientBuffer=%p egl=0x%04x | nothing was drawn and nothing was offered", + stepFail.c_str(), back.width, back.height, back.layers, (unsigned)back.format, + (unsigned long long)back.usage, back.stride, kT4AhbFourcc, t4Hex4(kT4AhbSeed).c_str(), + (void*)img, (unsigned)imgErr, (void*)cb, (unsigned)cbErr)); + glDeleteFramebuffers(1, &fboRb); + glDeleteRenderbuffers(1, &rb); + e.pDestroyImage(g.dpy, img); + AHardwareBuffer_release(ahb); + return; + } + + // The clear is what the peer's two pixels are compared against, so it goes + // through the same clear + glFinish + fence path the row above uses, and that + // fence's wait result is reported verbatim. + const std::string clearFence = t4WritePath(fboRb, w, h, plan.clear); + pr("T4-ahb: clear %s into the renderbuffer FBO: %s | %s", t4Hex4(kT4AhbSeed).c_str(), clearFence.c_str(), + plan.note.c_str()); + + // ---- 3. hand it over: socketpair + SCM_RIGHTS, exactly as T0 does ------- + T4AhbOffer off{}; + off.magic = kT4Magic; + off.version = 1; + off.width = back.width; + off.height = back.height; + off.stride = back.stride * kT4AhbBytesPerPixel; // describe() reports pixels; the wire wants bytes + off.fourcc = kT4AhbFourcc; + memcpy(off.seed, kT4AhbSeed, 4); + snprintf(off.note, sizeof(off.note), "clear=[%s] fence=[%s] modifier=DRM_FORMAT_MOD_LINEAR assumed", + t4Hex4(kT4AhbSeed).c_str(), clearFence.c_str()); + off.note[sizeof(off.note) - 1] = 0; + int sp[2] = {-1, -1}; + int sendRc = -1; + std::string spDesc = "no socketpair"; + std::string sendFail; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp) != 0) { + sendRc = -errno; + sendFail = fmt("socketpair -> errno=%d(%s)", errno, strerror(errno)); + } else { + spDesc = describeFd(sp[1]); + sendRc = AHardwareBuffer_sendHandleToUnixSocket(ahb, sp[0]); + if (sendRc != 0) { + sendFail = fmt("AHardwareBuffer_sendHandleToUnixSocket(ahb, socketpair[0]) -> %d errno=%d(%s)", sendRc, + errno, strerror(errno)); + } + close(sp[0]); + if (sendRc == 0 && !sendMsg(sock, MSG_T4_AHB_OFFER, &off, sizeof(off), sp[1])) { + sendRc = -errno; + sendFail = fmt("sendMsg(MSG_T4_AHB_OFFER, fd=socketpair[1]) -> errno=%d(%s)", errno, strerror(errno)); + } + close(sp[1]); + } + const bool offered = sendRc == 0; + pr("T4-ahb: MSG_T4_AHB_OFFER %ux%u stride=%u B (%u px x %u B/px) fourcc=0x%08x seed=%s note=[%s] " + "handle-send=%d fd[%s]", + off.width, off.height, off.stride, back.stride, kT4AhbBytesPerPixel, off.fourcc, + t4Hex4(off.seed).c_str(), off.note, sendRc, spDesc.c_str()); + + // ---- 4. the peer's answer ------------------------------------------------ + T4AhbAck ack{}; + size_t ackGot = 0; + std::string ackFail; + std::string ackSkipped; + bool ackRecv = false; + if (offered) { + // The dma-buf row's last step shortens the socket timeout to 2s to bound its + // wait for a frame the peer never sends; this wait is a real exchange and + // takes the leg's own budget back. + setRecvTimeout(sock, 60); + ackRecv = t4RecvTag(sock, MSG_T4_AHB_ACK, &ack, sizeof(ack), &ackGot, nullptr, &ackSkipped, &ackFail); + } else { + ackFail = sendFail; + } + if (ackRecv) { + ack.note[sizeof(ack.note) - 1] = 0; + pr("T4-ahb: MSG_T4_AHB_ACK imported=%d matched=0x%x stride=%u observed=%s observedCorner=%s pattern=%s " + "note=[%s] skipped=[%s]", + (int)ack.imported, (int)ack.matched, ack.stride, t4Hex4(ack.observed).c_str(), + t4Hex4(ack.observedCorner).c_str(), t4Hex4(ack.pattern).c_str(), ack.note, ackSkipped.c_str()); + } else if (offered) { + pr("T4-ahb: no MSG_T4_AHB_ACK: %s", ackFail.c_str()); + } + + // ---- 5. this side's own read: the texture path, at both pixels ----------- + // Taken AFTER the peer's ack, so what comes back is its answer and not the + // colour this side cleared a moment ago. + std::string readFail; + bool readDone = false; + uint8_t texWord[4] = {0, 0, 0, 0}, texCorner[4] = {0, 0, 0, 0}; + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glDrain(); + e.pTargetTexture(GL_TEXTURE_2D, (GLeglImageOES)img); + const GLenum texErr = glDrain(); + pr("T4-ahb: glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image) -> %s", glErrStr(texErr).c_str()); + if (texErr != GL_NO_ERROR) { + readFail = "glEGLImageTargetTexture2DOES -> " + glErrStr(texErr); + } else { + glGenFramebuffers(1, &fboTex); + glBindFramebuffer(GL_FRAMEBUFFER, fboTex); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + const GLenum texFboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + const GLenum texAttachErr = glDrain(); + if (texFboStatus != GL_FRAMEBUFFER_COMPLETE || texAttachErr != GL_NO_ERROR) { + readFail = fmt("the image texture attached but the FBO is 0x%04x (%s)", (unsigned)texFboStatus, + glErrStr(texAttachErr).c_str()); + } + } + if (readFail.empty()) { + std::string progFail; + prog = t4BuildReadProgram(&progFail); + if (!prog) { + readFail = progFail; + } else { + const GLint locTex = glGetUniformLocation(prog, "uTex"); + const GLint locTexel = glGetUniformLocation(prog, "uTexel"); + std::string failA, failB; + const bool okA = t4ReadTexel(prog, locTex, locTexel, tex, w, h, 0, 0, texWord, &failA); + const bool okB = t4ReadTexel(prog, locTex, locTexel, tex, w, h, farX, farY, texCorner, &failB); + readDone = okA && okB; + if (!okA) readFail = "pixel(0,0): " + failA; + if (!okB) readFail += (readFail.empty() ? "" : "; ") + std::string("pixel(w-1,h-1): ") + failB; + const std::string rf = readFail.empty() ? std::string("-") : readFail; + pr("T4-ahb: texture-path read word=%s corner=%s readDone=%d fail=[%s]", t4Hex4(texWord).c_str(), + t4Hex4(texCorner).c_str(), (int)readDone, rf.c_str()); + } + } + + // ---- 6. the row's verdict: one decisive leg, both halves named ----------- + Leg l; + l.name = "ahb"; + l.decisive = true; + l.attempted = offered; + l.readOk = ackRecv && ack.imported && (ack.matched & 3) == 3 && !memcmp(ack.observed, kT4AhbSeed, 4) && + !memcmp(ack.observedCorner, kT4AhbSeed, 4); + l.writeOk = ackRecv && readDone && !memcmp(texWord, ack.pattern, 4) && !memcmp(texCorner, ack.pattern, 4); + if (!offered) { + l.fail = "the offer never went out: " + sendFail; + } else if (!ackRecv) { + l.fail = "no MSG_T4_AHB_ACK: " + ackFail; + } else if (!ack.imported) { + l.fail = fmt("the container's gbm_bo_import refused the fd: peer note=[%s]", ack.note); + } else if (!l.readOk) { + l.fail = fmt("the peer did not see the clear colour at both pixels: matched=0x%x observed=%s corner=%s " + "want=%s", + (unsigned)ack.matched, t4Hex4(ack.observed).c_str(), t4Hex4(ack.observedCorner).c_str(), + t4Hex4(kT4AhbSeed).c_str()); + } else if (!readDone) { + l.fail = "texture-path read of the same image: " + readFail; + } else if (!l.writeOk) { + l.fail = fmt("texture-path read is not the peer's pattern: word=%s corner=%s pattern=%s", + t4Hex4(texWord).c_str(), t4Hex4(texCorner).c_str(), t4Hex4(ack.pattern).c_str()); + } + std::vector legs{l}; + std::string why; + const char* status = legVerdict(legs, &why); + + // Every raw value goes into the row, in the shape the other legs use: the leg + // trace first, then what each step actually saw, then the failure. + std::string detail = fmt( + "%s | AHB[%ux%u layers=%u format=0x%x usage=0x%llx stride=%u rfu0=%u rfu1=%llu] | fourcc=0x%08x [%s] | " + "seed=%s | egl[clientBuffer=%p err=0x%04x image=%p err=0x%04x] | rbFbo[status=0x%04x glErr=%s] | " + "clear[%s] | offer[sent=%d handleSend=%d fd[%s] note=[%s]] | ack[len=%zu got=%d imported=%d matched=0x%x " + "stride=%u observed=%s observedCorner=%s pattern=%s note=[%s] skipped=[%s]] | texRead[done=%d word=%s " + "corner=%s fail=[%s]] | %s | peer [%s] %s", + legTrace(legs).c_str(), back.width, back.height, back.layers, (unsigned)back.format, + (unsigned long long)back.usage, back.stride, back.rfu0, (unsigned long long)back.rfu1, kT4AhbFourcc, + plan.note.c_str(), t4Hex4(kT4AhbSeed).c_str(), (void*)cb, (unsigned)cbErr, (void*)img, (unsigned)imgErr, + (unsigned)fboStatus, glErrStr(attachErr).c_str(), clearFence.c_str(), (int)offered, sendRc, spDesc.c_str(), + off.note, ackGot, (int)ackRecv, ackRecv ? (int)ack.imported : -1, ackRecv ? (int)ack.matched : 0, + ack.stride, t4Hex4(ack.observed).c_str(), t4Hex4(ack.observedCorner).c_str(), t4Hex4(ack.pattern).c_str(), + ackRecv ? ack.note : ackFail.c_str(), ackSkipped.c_str(), (int)readDone, t4Hex4(texWord).c_str(), + t4Hex4(texCorner).c_str(), readFail.empty() ? "-" : readFail.c_str(), why.c_str(), lay.peer, lay.note); + record(kT4AhbRow, status, detail); + + // The EGLImage outlives every GL object that wraps it, and the AHardwareBuffer is + // what the image was created from, so the teardown order is the one the row above + // already uses. + if (prog) glDeleteProgram(prog); + if (fboTex) glDeleteFramebuffers(1, &fboTex); + if (fboRb) glDeleteFramebuffers(1, &fboRb); + if (tex) glDeleteTextures(1, &tex); + if (rb) glDeleteRenderbuffers(1, &rb); + e.pDestroyImage(g.dpy, img); + AHardwareBuffer_release(ahb); +} + +#else // !PROBE_HAVE_AHB + +static void runT4(GlCtx&, bool, const char*) { + record(kT4Row, "SKIP", + "host build has no listener: rtListen() is compiled with the AHardwareBuffer block, so the container " + "peer has nothing to dial"); +} + +#endif // PROBE_HAVE_AHB + // --------------------------------------------------------------------------- // main // --------------------------------------------------------------------------- @@ -4087,8 +5474,12 @@ int main(int argc, char** argv) { uint64_t size = kDefaultSize; bool routeClientMode = false; std::string routeEndpoint; + std::string t4Endpoint = "@mgl-t4"; const char* childRoute = nullptr; bool doT1 = true, doT0 = true, doT3 = true, doGles = true; + // T4 waits for a peer that only exists when it is asked for, so it is off unless + // --only-t4 turns it on. + bool doT4 = false; bool regionsAtEnd = false; uint32_t sustainedRounds = 0; std::string markerPath; @@ -4129,23 +5520,34 @@ int main(int argc, char** argv) { } else if (!strcmp(a, "--sustained-lock")) { sustainedRounds = 8; } else if (!strcmp(a, "--only-t1")) { - doT0 = doT3 = false; + doT0 = doT3 = doT4 = false; } else if (!strcmp(a, "--only-t0")) { - doT1 = doT3 = doGles = false; + doT1 = doT3 = doGles = doT4 = false; } else if (!strcmp(a, "--only-sustained")) { - doT1 = doT0 = doT3 = doGles = false; + doT1 = doT0 = doT3 = doGles = doT4 = false; if (!sustainedRounds) sustainedRounds = 8; } else if (!strcmp(a, "--only-t3")) { - doT1 = doT0 = doGles = false; + doT1 = doT0 = doGles = doT4 = false; } else if (!strcmp(a, "--only-gles")) { - doT1 = doT0 = doT3 = false; + doT1 = doT0 = doT3 = doT4 = false; + } else if (!strcmp(a, "--only-t4")) { + doT1 = doT0 = doT3 = doGles = false; + doT4 = true; + } else if (!strncmp(a, "--only-t4=", 10)) { + doT1 = doT0 = doT3 = doGles = false; + doT4 = true; + t4Endpoint = a + 10; } else if (!strcmp(a, "--no-gles")) { doGles = false; } else if (!strcmp(a, "--help")) { printf("usage: extmem_probe [markerPath] [--size=BYTES] [--regions-at-end] [--sustained-lock[=ROUNDS]]\n" " extmem_probe --route-client[=@endpoint] [--size=BYTES] [--sustained-lock=ROUNDS] [--regions-at-end]\n" " extmem_probe --serve (packaged as libMobileGLServer.so)\n" - " [--only-t0|--only-t1|--only-t3|--only-gles|--only-sustained] [--no-gles]\n" + " [--only-t0|--only-t1|--only-t3|--only-gles|--only-sustained|--only-t4[=@endpoint]] [--no-gles]\n" + " --only-t4[=@endpoint]: listen on @mgl-t4 (default) for the droidspaces container peer;\n" + " one connection carries both T4 rows: T4-image-import (its dma-buf\n" + " into this side) and T4-ahb-image (this side's AHardwareBuffer into\n" + " the container, sent as an fd over SCM_RIGHTS)\n" " markerPath: exec-hook mode; options are also read from .args\n"); return 0; } @@ -4210,6 +5612,8 @@ int main(int argc, char** argv) { runT3Parent(c, size); runT3ClientAllocParent(c, size); } + pr("=== phase T4: an image allocated outside Android (dma-buf from the container peer) ==="); + if (doT4) runT4(g, glOk, t4Endpoint.c_str()); glCtxDestroy(g); vkCtxDestroy(c); diff --git a/tools/spikes/extmem_probe/peer/t4_gbm_peer.c b/tools/spikes/extmem_probe/peer/t4_gbm_peer.c new file mode 100644 index 00000000..51adbc8d --- /dev/null +++ b/tools/spikes/extmem_probe/peer/t4_gbm_peer.c @@ -0,0 +1,1192 @@ +// t4_gbm_peer -- T4's outside-Android end: an image allocated in the container. +// +// T4 asks whether the render server can draw into an image that was allocated +// OUTSIDE Android -- the {Owner=Platform, Storage=OfferedImage} cell an +// Anland-style Wayland host needs. Only one side of that question can be asked +// from the probe's own process: the probe runs in the Android app domain and +// has no GBM device, while the Anland-backed GBM device (/dev/dri/renderD128, +// msm_drm) belongs to the Droidspaces container. So the container side +// allocates, fills, and later re-reads, and the probe imports. That is why the +// probe LISTENS on an abstract unix socket and this program dials it -- the +// reverse of every other leg's topology. +// +// Not part of the Android build, not in any CMake target, no library beyond +// libc and libgbm. Build it INSIDE the container, because that is where the +// GBM device is: +// +// gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm +// +// The wire format is mirrored byte for byte from the T4 section of +// extmem_probe.cpp. Nothing here is defensive about the layout: a peer that +// invented its own struct would prove nothing about the probe's import. +// +// Two properties of this container shape the code: +// * gbm_bo_unmap() segfaults for every WRITE mapping here (distro libgbm / +// dri_gbm 26.1.5 mixed with an unowned self-built libgallium-26.2.0-devel), +// while READ mappings unmap cleanly. The fill mapping is therefore left +// mapped on purpose: this process is about to exit, and a leaked mapping +// cannot be mistaken for a driver answer the way a SIGSEGV could. +// * Every check takes its OWN fresh read mapping, because a mapping taken +// before the probe's GL write would answer a question about this CPU's +// cache lines rather than about the buffer the probe drew into. +// +// T4 has two rows and this peer serves both, in whichever order the probe asks: +// * T4-image-import -- the image is allocated HERE (GBM) and imported there. +// The probe asks that row only when its driver advertises +// EGL_EXT_image_dma_buf_import; when it does not, the row is recorded as a +// capability answer and this side never sees MSG_T4_READ or MSG_T4_WROTE at +// all. No frame may therefore be waited for by name, which is why the +// receive loop at the end dispatches on the tag instead. +// * T4-ahb-image -- the image is allocated on the ANDROID side and imported +// HERE, because an app cannot adopt this side's fd as an AHardwareBuffer +// (AHardwareBuffer_createFromHandle is a SystemApi, not part of the NDK): +// the buffer has to be born on the side that can name it, while this side +// only needs an fd it can gbm_bo_import. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// DRM_FORMAT_MOD_INVALID lives in drm_fourcc.h, which is not part of libgbm's +// interface. This build must not need libdrm's headers for one constant; the +// value below is the kernel UAPI value. +#define T4_DRM_FORMAT_MOD_INVALID 0x00ffffffffffffffULL + +// DRM_FORMAT_MOD_LINEAR, same source, same reason. The Android-minted row is +// imported with GBM_BO_IMPORT_FD_MODIFIER because that is the import form that +// names a modifier at all, and a buffer an app allocated with CPU read and write +// access is the linear one. +#define T4_DRM_FORMAT_MOD_LINEAR 0ULL + +// --------------------------------------------------------------------------- +// logging +// --------------------------------------------------------------------------- + +// Every line is flushed as it is written: a crash later in the run (unmap of a +// WRITE mapping is one on this stack) must not be able to take the raw +// observations with it. +static void pr(const char* f, ...) __attribute__((format(printf, 1, 2))); +static void pr(const char* f, ...) { + va_list ap; + va_start(ap, f); + fputs("[t4-peer] ", stdout); + vfprintf(stdout, f, ap); + va_end(ap); + fputc('\n', stdout); + fflush(stdout); +} + +static const char* ynStr(int v) { return v < 0 ? "n/a" : v ? "yes" : "no"; } + +// A colour is always four raw bytes in ARGB8888 memory order, so that is how it +// is printed; --seed=RRGGBBAA is the readable spelling of the same four bytes. +static void hex4(const uint8_t b[4], char out[16]) { + snprintf(out, 16, "%02X,%02X,%02X,%02X", b[0], b[1], b[2], b[3]); +} + +// --------------------------------------------------------------------------- +// the T4 wire format, mirrored from extmem_probe.cpp +// --------------------------------------------------------------------------- + +enum MsgTag { + MSG_T4_OFFER = 50, // peer -> probe, one dma-buf fd riding in SCM_RIGHTS + MSG_T4_READ = 51, // probe -> peer: what the probe read back through its imports + MSG_T4_WROTE = 52, // probe -> peer: what the probe just cleared, and which path + MSG_T4_ACK = 53, // peer -> probe: what the peer then saw in its own mapping + MSG_T4_RESULT = 54, // declared by the leg; this sequence never sends it to the peer + MSG_T4_AHB_OFFER = 55, // probe -> peer: an AHardwareBuffer minted on the Android side + MSG_T4_AHB_ACK = 56, // peer -> probe: what importing it saw, and what was written back + MSG_BYE = 99, // the probe's own teardown tag, tolerated below +}; + +struct MsgHeader { + uint32_t tag; + uint32_t len; +}; + +// magic = 0x5434474D; on a little-endian machine the four bytes read 'M','G','4','T'. +#define T4_MAGIC 0x5434474Du +// The description the probe prints back at the user. modifier carries the +// protocol's own sentinel: UINT64_MAX means "this peer has no modifier to +// declare", which is what DRM_FORMAT_MOD_INVALID is translated to on the way out. +struct T4Layout { + uint32_t magic, version, width, height, fourcc, stride, offset, reserved; + uint64_t modifier; + uint8_t seed[4]; + char peer[128]; + char note[128]; +}; + +// The probe's two read paths, two pixels each: (0,0) and the far corner. A +// wrong stride or offset cannot pass a corner check, which is the whole reason +// the corner is in the protocol. +struct T4Read { + int32_t texOk, rbOk, rung, pad; + uint8_t texWord[4], texCorner[4], rbWord[4], rbCorner[4]; + char importNote[160]; +}; + +struct T4Wrote { + int32_t path; // 0 = renderbuffer, 1 = texture + uint8_t color[4]; +}; + +struct T4Ack { + int32_t ok; + uint8_t observed[4]; + char note[128]; +}; + +// MSG_T4_AHB_OFFER: the Android side's AHardwareBuffer, described so this side +// can import it without guessing any part of the layout. The fourcc is the DRM +// name whose memory order is the order AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM +// names -- DRM_FORMAT_ABGR8888, fourcc_code('A','B','2','4') = 0x34324241, +// whose little-endian [31:0] A:B:G:R layout puts R,G,B,A in bytes 0..3 -- so the +// bytes compared below are the bytes the probe's glClearColor produced, with no +// channel-renaming step in between where a convention could hide a mistake. +// The stride is the probe's own reported stride, never width*4. +struct T4AhbOffer { + uint32_t magic, version, width, height, stride, fourcc; + uint8_t seed[4]; // what the probe cleared into the buffer, in memory order + char note[128]; +}; + +// MSG_T4_AHB_ACK: what gbm_bo_import made of the fd, what both pixels held +// BEFORE anything was written, and the pattern then written over the whole +// mapping. `matched` is this side's own comparison -- bit0 for (0,0), bit1 for +// the far corner -- and the probe recomputes it from the two pixel fields, so a +// disagreement between the bits and the bytes is visible rather than trusted. +struct T4AhbAck { + int32_t imported; // gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER) returned a bo + int32_t matched; // 1 = (0,0) held the announced seed, 2 = the far corner did + uint32_t stride; // the stride this side's own mapping reports + uint8_t observed[4]; + uint8_t observedCorner[4]; + uint8_t pattern[4]; // zero when nothing could be written + char note[128]; +}; + +// Every probe -> peer payload, so that one recvMsg can take whatever arrives and +// the tag decides how it is read (see the receive loop in main). +union T4Payload { + struct T4Read read; + struct T4Wrote wrote; + struct T4AhbOffer ahb; +}; + +// --------------------------------------------------------------------------- +// message plumbing (same shape as the probe's sendMsg/recvMsg) +// --------------------------------------------------------------------------- + +// errno as it stood AT the failing call: the caller prints it, and any work in +// between would otherwise clobber it. +static int gMsgErrno = 0; + +static bool writeAll(int fd, const void* p, size_t n) { + const uint8_t* b = (const uint8_t*)p; + while (n) { + ssize_t w = write(fd, b, n); + if (w <= 0) { + if (w < 0 && errno == EINTR) continue; + return false; + } + b += w; + n -= (size_t)w; + } + return true; +} + +static bool readAll(int fd, void* p, size_t n) { + uint8_t* b = (uint8_t*)p; + while (n) { + ssize_t r = read(fd, b, n); + if (r <= 0) { + if (r < 0 && errno == EINTR) continue; + return false; + } + b += r; + n -= (size_t)r; + } + return true; +} + +// header + payload go out in one sendmsg so the fd lands with the header byte +static bool sendMsg(int sock, uint32_t tag, const void* payload, size_t len, int fdToPass) { + struct MsgHeader h = {tag, (uint32_t)len}; + struct iovec iov[2]; + iov[0].iov_base = &h; + iov[0].iov_len = sizeof(h); + iov[1].iov_base = (void*)payload; + iov[1].iov_len = len; + + char cbuf[CMSG_SPACE(sizeof(int))]; + memset(cbuf, 0, sizeof(cbuf)); + + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = iov; + msg.msg_iovlen = len ? 2 : 1; + if (fdToPass >= 0) { + msg.msg_control = cbuf; + msg.msg_controllen = sizeof(cbuf); + struct cmsghdr* cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SCM_RIGHTS; + cm->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cm), &fdToPass, sizeof(int)); + } + ssize_t s; + do { + s = sendmsg(sock, &msg, 0); + } while (s < 0 && errno == EINTR); + if (s < 0) { + gMsgErrno = errno; + return false; + } + size_t total = sizeof(h) + len; + if ((size_t)s == total) return true; + // partial: finish the tail with plain writes (control data already delivered) + size_t done = (size_t)s; + if (done < sizeof(h)) { + gMsgErrno = EIO; + return false; + } + if (!writeAll(sock, (const uint8_t*)payload + (done - sizeof(h)), total - done)) { + gMsgErrno = errno; + return false; + } + return true; +} + +static bool recvMsg(int sock, uint32_t* tag, void* payload, size_t maxLen, size_t* outLen, int* fdOut) { + if (fdOut) *fdOut = -1; + struct MsgHeader h; + memset(&h, 0, sizeof(h)); + struct iovec iov; + iov.iov_base = &h; + iov.iov_len = sizeof(h); + + char cbuf[CMSG_SPACE(sizeof(int))]; + memset(cbuf, 0, sizeof(cbuf)); + + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cbuf; + msg.msg_controllen = sizeof(cbuf); + + ssize_t r; + do { + r = recvmsg(sock, &msg, MSG_WAITALL); + } while (r < 0 && errno == EINTR); + if (r != (ssize_t)sizeof(h)) { + gMsgErrno = r < 0 ? errno : 0; + return false; + } + + for (struct cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) { + int got = -1; + memcpy(&got, CMSG_DATA(cm), sizeof(int)); + if (fdOut) { + *fdOut = got; + } else if (got >= 0) { + close(got); + } + } + } + *tag = h.tag; + if (outLen) *outLen = h.len; + if (h.len > maxLen) { + gMsgErrno = EMSGSIZE; + return false; + } + if (h.len && !readAll(sock, payload, h.len)) { + gMsgErrno = errno; + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// peer state: built up as the run proceeds and printed raw at the end +// --------------------------------------------------------------------------- + +static const char* gEndpoint = "@mgl-t4"; +static uint32_t gWidth = 64, gHeight = 64; +static uint32_t gSeedWord = 0x1A2B3C4Du; // RRGGBBAA +static int gTimeout = 60; +static uint8_t gSeed[4]; + +static struct gbm_device* gDev = NULL; +static struct gbm_bo* gBo = NULL; +static uint32_t gStride = 0; +static uint32_t gFourcc = GBM_FORMAT_ARGB8888; +static uint64_t gModifierRaw = 0, gModifierTx = 0; +static int gBoFd = -1; +static char gBoFdName[512] = ""; +static unsigned gBoHandle = 0; + +// Each description string is built once, where its value is learned, so the +// final line never has to guess whether a zero means "linear" or "not reached". +static char gDevDesc[160] = "device="; +static char gBoDesc[256] = "bo="; +static char gFdDesc[1024] = "fd="; +static char gFillDesc[224] = "fill="; + +static int gHaveRead = 0; +static struct T4Read gRead; +static int gHaveWrote[2] = {0, 0}; +static uint8_t gWrote[2][4]; +static uint8_t gObserved[2][4]; +static uint8_t gObservedCorner[2][4]; +static int gMatched[2] = {-1, -1}; +static int gMatchedCorner[2] = {-1, -1}; + +// The Android-minted row's own state. `gHaveAhbOffer` is also what tells the +// verdict that this row was asked at all: the probe skips it when the Android +// side cannot even mint the buffer, and a row that was never asked is not a +// failure. +static int gHaveAhbOffer = 0; +static int gAhbImported = 0; +static int gAhbFilled = 0; +static uint32_t gAhbStride = 0; +static uint8_t gAhbSeed[4]; +static uint8_t gAhbPattern[4]; +static uint8_t gAhbObserved[4]; +static uint8_t gAhbObservedCorner[4]; +static char gAhbNote[160] = ""; +static char gAhbFdDesc[768] = "fd="; +static char gAhbBoDesc[200] = "bo="; +static char gAhbFillDesc[240] = "fill="; + +// The four bytes this side writes over the probe's clear, in the buffer's own +// memory order. No two of them are equal, so a repeated-byte fill cannot pass +// for it; the ack announces it and the probe compares against THAT, never against +// a constant of its own, so the two sides cannot agree on the wrong number. +static const uint8_t T4_AHB_PATTERN[4] = {0xC7, 0x39, 0x04, 0xE1}; + +// --------------------------------------------------------------------------- +// GBM side +// --------------------------------------------------------------------------- + +// The fd's own kernel name and its exporter are the evidence that this is the +// same KIND of buffer the compositor scans out: kwin holds fds named /dmabuf: +// with exp_name system (its own dma-heap allocations) next to /dmabuf: +// SurfaceView[com.anland...] with exp_name qcom,system (Android buffers it +// imported). A peer buffer from a different heap would be a different answer to +// T4 even if the import itself succeeded, so both halves are printed verbatim. +// Fills the caller's two buffers and prints both lines verbatim. Shared with the +// Android-minted row: an fd that arrived from the other direction carries the +// other side's exporter, and that is exactly as much evidence as this one. +static void fdEvidence(int fd, char* nameOut, size_t nameLen, char* exporterOut, size_t exporterLen) { + char path[64]; + snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); + char link[500]; + ssize_t n = readlink(path, link, sizeof(link) - 1); + if (n < 0) { + int e = errno; + snprintf(nameOut, nameLen, "", e, strerror(e)); + pr("readlink(%s) failed errno=%d (%s)", path, e, strerror(e)); + } else { + link[n] = 0; + snprintf(nameOut, nameLen, "%s", link); + } + pr("readlink(/proc/self/fd/%d)=\"%s\"", fd, nameOut); + + char info[64]; + snprintf(info, sizeof(info), "/proc/self/fdinfo/%d", fd); + snprintf(exporterOut, exporterLen, ""); + FILE* f = fopen(info, "r"); + if (!f) { + int e = errno; + snprintf(exporterOut, exporterLen, "", e, strerror(e)); + pr("open(%s) failed errno=%d (%s)", info, e, strerror(e)); + return; + } + char line[256]; + int found = 0; + while (fgets(line, sizeof(line), f)) { + if (!strncmp(line, "exp_name:", 9)) { + size_t l = strlen(line); + while (l && (line[l - 1] == '\n' || line[l - 1] == '\r')) line[--l] = 0; + snprintf(exporterOut, exporterLen, "%s", line + 9); + found = 1; + break; + } + } + fclose(f); + if (!found) snprintf(exporterOut, exporterLen, ""); + pr("%s exp_name:%s", info, exporterOut); +} + +static void printFdEvidence(int fd) { + char expName[300]; + fdEvidence(fd, gBoFdName, sizeof(gBoFdName), expName, sizeof(expName)); + snprintf(gFdDesc, sizeof(gFdDesc), "fd=%d fdname=\"%s\" exporter=\"%s\"", fd, gBoFdName, expName); +} + +// ARGB8888 in memory order is the byte triple B,G,R,A, and --seed=RRGGBBAA is +// read in exactly that order. The fill is SOLID, so a vertical flip cannot turn +// a working import into a reported failure; the whole mapping (stride * height +// bytes, row padding included) gets the same four bytes repeated. +static bool fillWithSeed(void) { + uint32_t mapStride = 0; + void* mapData = NULL; + void* map = gbm_bo_map(gBo, 0, 0, gWidth, gHeight, GBM_BO_TRANSFER_WRITE, &mapStride, &mapData); + if (!map || map == MAP_FAILED) { + int e = errno; + pr("gbm_bo_map(WRITE) failed errno=%d (%s)", e, strerror(e)); + return false; + } + uint8_t* base = (uint8_t*)(mapData ? mapData : map); + gSeed[0] = (uint8_t)((gSeedWord >> 8) & 0xFFu); // B + gSeed[1] = (uint8_t)((gSeedWord >> 16) & 0xFFu); // G + gSeed[2] = (uint8_t)((gSeedWord >> 24) & 0xFFu); // R + gSeed[3] = (uint8_t)(gSeedWord & 0xFFu); // A + uint32_t s = mapStride ? mapStride : gStride; + uint64_t n = (uint64_t)s * gHeight; + for (uint64_t i = 0; i < n; ++i) base[i] = gSeed[i & 3u]; + + char seedS[16]; + hex4(gSeed, seedS); + pr("gbm_bo_map(WRITE) = %p mapping stride=%u (bo stride=%u), filled %llu bytes with seed 0x%08X mem=[%s]", + (void*)base, s, gStride, (unsigned long long)n, gSeedWord, seedS); + snprintf(gFillDesc, sizeof(gFillDesc), "seed=0x%08X seedMem=[%s] filled=%llu mapStride=%u", + gSeedWord, seedS, (unsigned long long)n, s); + // Deliberately not unmapped: see the header. The mapping stays valid until + // the process exits, which is the next thing that happens to it. + return true; +} + +// --------------------------------------------------------------------------- +// one check: fresh mapping, both pixels, raw bytes on stdout before the unmap +// --------------------------------------------------------------------------- + +// Fills the ack and returns 1 only when the announced colour was observed at +// BOTH pixels. On a mismatch it also scans the mapping for the announced colour +// and for our own seed, which separates "the probe's write never landed here" +// from "it landed somewhere else" -- the same distinction the T1 leg reports as +// payloadAt=. +static int checkPath(int path, const uint8_t color[4], struct T4Ack* ack, int* cornerMatchOut, + uint8_t cornerOut[4]) { + const char* name = path == 0 ? "renderbuffer" : path == 1 ? "texture" : "unknown"; + char aS[16], fS[16], cS[16]; + hex4(color, aS); + *cornerMatchOut = -1; + memset(cornerOut, 0, 4); + ack->ok = 0; + memset(ack->observed, 0, 4); + ack->note[0] = 0; + + uint32_t mapStride = 0; + void* mapData = NULL; + void* map = gbm_bo_map(gBo, 0, 0, gWidth, gHeight, GBM_BO_TRANSFER_READ, &mapStride, &mapData); + if (!map || map == MAP_FAILED) { + int e = errno; + pr("MSG_T4_WROTE path=%d (%s) announced=[%s]: gbm_bo_map(READ) failed errno=%d (%s)", + path, name, aS, e, strerror(e)); + snprintf(ack->note, sizeof(ack->note), "gbm_bo_map(READ) failed errno=%d (%s)", e, strerror(e)); + return 0; + } + const uint8_t* base = (const uint8_t*)(mapData ? mapData : map); + uint32_t s = mapStride ? mapStride : gStride; + size_t cornerOff = (size_t)(gHeight - 1) * s + (size_t)(gWidth - 1) * 4; + size_t n = (size_t)s * gHeight; + uint8_t first[4], corner[4]; + memcpy(first, base, 4); + memcpy(corner, base + cornerOff, 4); + hex4(first, fS); + hex4(corner, cS); + + int mFirst = memcmp(first, color, 4) == 0; + int mCorner = memcmp(corner, color, 4) == 0; + pr("MSG_T4_WROTE path=%d (%s) announced=[%s] -> fresh mapping stride=%u base=%p: " + "px(0,0)=[%s] match=%s px(%u,%u)@%zu=[%s] match=%s", + path, name, aS, s, (const void*)base, fS, ynStr(mFirst), gWidth - 1, gHeight - 1, cornerOff, cS, + ynStr(mCorner)); + + long annAt = -1, seedAt = -1; + if (!(mFirst && mCorner)) { + for (size_t i = 0; i + 4 <= n; ++i) { + if (annAt < 0 && memcmp(base + i, color, 4) == 0) annAt = (long)i; + if (seedAt < 0 && memcmp(base + i, gSeed, 4) == 0) seedAt = (long)i; + if (annAt >= 0 && seedAt >= 0) break; + } + pr(" mismatch scan over %zu mapped bytes: announcedColorAt=%ld seedAt=%ld", n, annAt, seedAt); + } + + ack->ok = (mFirst && mCorner) ? 1 : 0; + // ack.observed carries the FIRST pixel, which is the four bytes the probe + // compares against the colour it announced. The corner travels in the note + // and on stdout: a stride or offset mistake shows up there and nowhere else. + memcpy(ack->observed, first, 4); + memcpy(cornerOut, corner, 4); + *cornerMatchOut = mCorner; + snprintf(ack->note, sizeof(ack->note), "%s stride=%u px00=[%s] corner=[%s]%s", name, s, fS, cS, + (mFirst && mCorner) ? "" : (annAt >= 0 ? " announced-seen-elsewhere" : " announced-absent")); + // The observation is already on stdout: a crash in unmap must not be able to + // hide the bytes this check produced. + gbm_bo_unmap(gBo, mapData ? mapData : map); + return mFirst && mCorner; +} + +// --------------------------------------------------------------------------- +// the Android-minted row: an AHardwareBuffer's dma-buf, imported and answered +// --------------------------------------------------------------------------- + +// The buffer does not arrive as a frame of this protocol: the probe writes its +// Android handle into one end of a socketpair and sends the OTHER end here in +// SCM_RIGHTS beside MSG_T4_AHB_OFFER, so what is on that fd is Android's own +// handle encoding -- of no use in a glibc process, and libandroid is not linkable +// from one anyway. The kernel carries the part this side can use: the buffer's +// plane fd travels as SCM_RIGHTS control data, so the fd is taken from whichever +// recvmsg carries it. The bytes are read -- a message carrying only control data +// would otherwise look like EOF -- and reported, never interpreted. +static int recvAhbFd(int fd) { + for (int attempt = 0; attempt < 4; ++attempt) { + char data[512]; + char cbuf[CMSG_SPACE(sizeof(int) * (GBM_MAX_PLANES + 4))]; + struct iovec iov; + iov.iov_base = data; + iov.iov_len = sizeof(data); + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cbuf; + msg.msg_controllen = sizeof(cbuf); + ssize_t r; + do { + r = recvmsg(fd, &msg, 0); + } while (r < 0 && errno == EINTR); + if (r < 0) { + int e = errno; + pr("recvmsg(ahb socket) failed errno=%d (%s)", e, strerror(e)); + return -1; + } + int got[GBM_MAX_PLANES + 4]; + int nfds = 0; + for (struct cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { + if (cm->cmsg_level != SOL_SOCKET || cm->cmsg_type != SCM_RIGHTS) continue; + size_t bytes = cm->cmsg_len > CMSG_LEN(0) ? cm->cmsg_len - CMSG_LEN(0) : 0; + int count = (int)(bytes / sizeof(int)); + int room = (int)(sizeof(got) / sizeof(got[0])) - nfds; + if (count > room) count = room; + if (count > 0) { + memcpy(got + nfds, CMSG_DATA(cm), (size_t)count * sizeof(int)); + nfds += count; + } + } + pr("ahb socket: recvmsg #%d -> %zd byte(s) of Android handle encoding, %d fd(s) in SCM_RIGHTS%s", + attempt + 1, r, nfds, + (msg.msg_flags & MSG_CTRUNC) ? " (MSG_CTRUNC: the control buffer was too small)" : ""); + if (nfds > 0) { + for (int i = 1; i < nfds; ++i) close(got[i]); + if (nfds > 1) + pr("ahb socket: %d fds arrived; fd[0]=%d is used and the other %d are closed", nfds, got[0], + nfds - 1); + return got[0]; + } + if (r == 0) break; // the sender is gone and no fd was ever attached + } + pr("ahb socket: no SCM_RIGHTS fd arrived on the socketpair"); + return -1; +} + +// Sends the ack and records what it said, so the FINAL line reports the same +// numbers that went over the wire rather than a second guess at them. +static void sendAhbAck(int sock, struct T4AhbAck* ack) { + char patS[16], obsS[16], corS[16]; + hex4(ack->pattern, patS); + hex4(ack->observed, obsS); + hex4(ack->observedCorner, corS); + ack->note[sizeof(ack->note) - 1] = 0; + snprintf(gAhbNote, sizeof(gAhbNote), "%s", ack->note); + gAhbImported = ack->imported; + gAhbStride = ack->stride; + memcpy(gAhbObserved, ack->observed, 4); + memcpy(gAhbObservedCorner, ack->observedCorner, 4); + memcpy(gAhbPattern, ack->pattern, 4); + pr("MSG_T4_AHB_ACK sent imported=%d matched=0x%x stride=%u observed=[%s] observedCorner=[%s] " + "pattern=[%s] note=\"%s\"", + (int)ack->imported, (int)ack->matched, ack->stride, obsS, corS, patS, ack->note); + if (!sendMsg(sock, MSG_T4_AHB_ACK, ack, sizeof(*ack), -1)) + pr("sendMsg(MSG_T4_AHB_ACK) failed errno=%d (%s)", gMsgErrno, strerror(gMsgErrno)); +} + +// One MSG_T4_AHB_OFFER: import the fd, read both pixels before writing anything, +// then fill the mapping with this side's pattern. `fd` is the socketpair end that +// arrived in SCM_RIGHTS and `off` is the payload that travelled beside it; the +// caller has already checked the payload's tag and length. +static void serveAhbOffer(int sock, int fd, const struct T4AhbOffer* off) { + struct T4AhbAck ack; + memset(&ack, 0, sizeof(ack)); + memcpy(ack.pattern, T4_AHB_PATTERN, 4); + + char seedS[16], patS[16]; + hex4(off->seed, seedS); + hex4(ack.pattern, patS); + pr("MSG_T4_AHB_OFFER w=%u h=%u stride=%u fourcc=0x%08X seed=[%s] fd=%d note=\"%s\"", off->width, + off->height, off->stride, (unsigned)off->fourcc, seedS, fd, off->note); + + gHaveAhbOffer = 1; + memcpy(gAhbSeed, off->seed, 4); + memcpy(gAhbPattern, ack.pattern, 4); + + if (fd < 0) { + snprintf(ack.note, sizeof(ack.note), "the offer carried no socketpair fd in SCM_RIGHTS"); + pr("MSG_T4_AHB_OFFER: %s", ack.note); + sendAhbAck(sock, &ack); + return; + } + const int ahbFd = recvAhbFd(fd); + if (ahbFd < 0) { + snprintf(ack.note, sizeof(ack.note), "no dma-buf fd arrived on the socketpair"); + sendAhbAck(sock, &ack); + return; + } + char ahbName[400], ahbExport[300]; + fdEvidence(ahbFd, ahbName, sizeof(ahbName), ahbExport, sizeof(ahbExport)); + snprintf(gAhbFdDesc, sizeof(gAhbFdDesc), "fd=%d fdname=\"%s\" exporter=\"%s\"", ahbFd, ahbName, ahbExport); + + if (off->width == 0 || off->height == 0 || off->stride == 0) { + snprintf(ack.note, sizeof(ack.note), "degenerate offer %ux%u stride=%u", off->width, off->height, + off->stride); + pr("MSG_T4_AHB_OFFER: %s", ack.note); + close(ahbFd); + sendAhbAck(sock, &ack); + return; + } + + struct gbm_import_fd_modifier_data imp; + memset(&imp, 0, sizeof(imp)); + imp.width = off->width; + imp.height = off->height; + imp.format = off->fourcc; + imp.num_fds = 1; + imp.fds[0] = ahbFd; + imp.strides[0] = (int)off->stride; + imp.offsets[0] = 0; + imp.modifier = T4_DRM_FORMAT_MOD_LINEAR; + struct gbm_bo* bo = + gbm_bo_import(gDev, GBM_BO_IMPORT_FD_MODIFIER, &imp, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR); + if (!bo) { + int e = errno; + pr("gbm_bo_import(FD_MODIFIER, %ux%u format=0x%08X stride=%d offset=0 modifier=0x%016" PRIx64 + " fd=%d) refused errno=%d (%s)", + off->width, off->height, (unsigned)off->fourcc, imp.strides[0], (uint64_t)imp.modifier, ahbFd, e, + strerror(e)); + snprintf(ack.note, sizeof(ack.note), "gbm_bo_import refused errno=%d (%s)", e, strerror(e)); + close(ahbFd); + sendAhbAck(sock, &ack); + return; + } + ack.imported = 1; + pr("gbm_bo_import = OK bo=%u %ux%u stride=%u format=0x%08X modifier=0x%016" PRIx64 " planes=%u", + gbm_bo_get_handle(bo).u32, gbm_bo_get_width(bo), gbm_bo_get_height(bo), gbm_bo_get_stride(bo), + (unsigned)gbm_bo_get_format(bo), (uint64_t)gbm_bo_get_modifier(bo), gbm_bo_get_plane_count(bo)); + snprintf(gAhbBoDesc, sizeof(gAhbBoDesc), + "bo=%u w=%u h=%u stride=%u fourcc=0x%08X modifier=0x%016" PRIx64 " planes=%u", + gbm_bo_get_handle(bo).u32, gbm_bo_get_width(bo), gbm_bo_get_height(bo), gbm_bo_get_stride(bo), + (unsigned)gbm_bo_get_format(bo), (uint64_t)gbm_bo_get_modifier(bo), gbm_bo_get_plane_count(bo)); + // The import holds its own reference on the buffer, so this process's copy of + // the fd has done its job. + close(ahbFd); + + // ---- read direction: a fresh READ mapping, both pixels, before any write --- + uint32_t mapStride = 0; + void* mapData = NULL; + void* map = gbm_bo_map(bo, 0, 0, off->width, off->height, GBM_BO_TRANSFER_READ, &mapStride, &mapData); + if (!map || map == MAP_FAILED) { + int e = errno; + pr("gbm_bo_map(imported, READ) failed errno=%d (%s)", e, strerror(e)); + snprintf(ack.note, sizeof(ack.note), "gbm_bo_map(READ) failed errno=%d (%s)", e, strerror(e)); + gbm_bo_destroy(bo); // no mapping is live here, so this is the clean case + sendAhbAck(sock, &ack); + return; + } + const uint8_t* base = (const uint8_t*)(mapData ? mapData : map); + const uint32_t s = mapStride ? mapStride : off->stride; + ack.stride = s; + const size_t cornerOff = (size_t)(off->height - 1) * s + (size_t)(off->width - 1) * 4; + uint8_t first[4], corner[4]; + memcpy(first, base, 4); + memcpy(corner, base + cornerOff, 4); + memcpy(ack.observed, first, 4); + memcpy(ack.observedCorner, corner, 4); + const int mFirst = memcmp(first, off->seed, 4) == 0; + const int mCorner = memcmp(corner, off->seed, 4) == 0; + ack.matched = (mFirst ? 1 : 0) | (mCorner ? 2 : 0); + char fS[16], cS[16]; + hex4(first, fS); + hex4(corner, cS); + pr("AHB import: mapStride=%u (bo stride=%u) px(0,0)=[%s] match=%s px(%u,%u)@%zu=[%s] match=%s " + "announced seed=[%s]", + s, gbm_bo_get_stride(bo), fS, ynStr(mFirst), off->width - 1, off->height - 1, cornerOff, cS, ynStr(mCorner), + seedS); + if (!(mFirst && mCorner)) { + // The same distinction checkPath() reports: the announced colour either + // landed somewhere else in this mapping or never arrived at all. + const size_t n = (size_t)s * off->height; + long annAt = -1; + for (size_t i = 0; i + 4 <= n; ++i) { + if (memcmp(base + i, off->seed, 4) == 0) { + annAt = (long)i; + break; + } + } + pr(" mismatch scan over %zu mapped bytes: announcedSeedAt=%ld", n, annAt); + } + // A READ mapping unmaps cleanly on this stack (only WRITE unmaps crash), and + // the fill below needs a mapping of its own anyway. + gbm_bo_unmap(bo, mapData ? mapData : map); + + // ---- fill direction: one WRITE mapping, filled and left mapped ------------ + uint32_t fillStride = 0; + void* fillData = NULL; + void* fill = gbm_bo_map(bo, 0, 0, off->width, off->height, GBM_BO_TRANSFER_WRITE, &fillStride, &fillData); + if (!fill || fill == MAP_FAILED) { + int e = errno; + pr("gbm_bo_map(imported, WRITE) failed errno=%d (%s) -- nothing was written, so nothing is " + "announced", + e, strerror(e)); + memset(ack.pattern, 0, 4); + snprintf(ack.note, sizeof(ack.note), "gbm_bo_map(WRITE) failed errno=%d (%s): pattern not written", e, + strerror(e)); + gbm_bo_destroy(bo); + sendAhbAck(sock, &ack); + return; + } + uint8_t* fillBase = (uint8_t*)(fillData ? fillData : fill); + const uint32_t fs = fillStride ? fillStride : s; + const uint64_t fillBytes = (uint64_t)fs * off->height; + for (uint64_t i = 0; i < fillBytes; ++i) fillBase[i] = ack.pattern[i & 3u]; + gAhbFilled = 1; + pr("gbm_bo_map(imported, WRITE) = %p mapping stride=%u (bo stride=%u), wrote %llu bytes of pattern [%s]", + (void*)fillBase, fs, gbm_bo_get_stride(bo), (unsigned long long)fillBytes, patS); + snprintf(gAhbFillDesc, sizeof(gAhbFillDesc), "pattern=[%s] filled=%llu mapStride=%u", patS, + (unsigned long long)fillBytes, fs); + snprintf(ack.note, sizeof(ack.note), + "imported via gbm_bo_import(FD_MODIFIER) stride=%u px00=[%s] corner=[%s] pattern=[%s]", s, fS, cS, + patS); + // The WRITE mapping is deliberately NOT unmapped, and this bo is deliberately + // not destroyed either (destroy tears the mapping down): on this stack unmapping + // a write mapping segfaults, and this process is about to exit. Whether the + // fill actually reached the buffer is not this side's claim to make -- the probe + // reads it back through the GPU, and that read is the evidence. + sendAhbAck(sock, &ack); +} + +// --------------------------------------------------------------------------- +// endpoint +// --------------------------------------------------------------------------- + +// The endpoint is an abstract name by default and has to stay one: a Droidspaces +// container runs net_mode=host, so the abstract namespace is common to both +// sides, while a filesystem path would have to exist in the other side's root. +static int connectEndpoint(const char* ep) { + int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (s < 0) { + int e = errno; + pr("socket(AF_UNIX) failed errno=%d (%s)", e, strerror(e)); + return -1; + } + struct sockaddr_un a; + memset(&a, 0, sizeof(a)); + a.sun_family = AF_UNIX; + socklen_t len; + if (ep[0] == '@') { + strncpy(a.sun_path + 1, ep + 1, sizeof(a.sun_path) - 2); + len = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + strlen(ep + 1)); + } else { + strncpy(a.sun_path, ep, sizeof(a.sun_path) - 1); + len = (socklen_t)sizeof(a); + } + if (connect(s, (struct sockaddr*)&a, len) != 0) { + int e = errno; + pr("connect(%s) failed errno=%d (%s)", ep, e, strerror(e)); + close(s); + return -1; + } + struct timeval tv; + tv.tv_sec = gTimeout; + tv.tv_usec = 0; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + return s; +} + +// --------------------------------------------------------------------------- +// the final line: every raw observation, nothing summarised away +// --------------------------------------------------------------------------- + +static int finish(int rc, const char* why) { + char texW[16] = "-", texC[16] = "-", rbW[16] = "-", rbC[16] = "-"; + char rbA[16] = "-", rbO[16] = "-", rbK[16] = "-"; + char txA[16] = "-", txO[16] = "-", txK[16] = "-"; + char ahbSeedS[16] = "-", ahbObsS[16] = "-", ahbCorS[16] = "-", ahbPatS[16] = "-"; + if (gHaveRead) { + hex4(gRead.texWord, texW); + hex4(gRead.texCorner, texC); + hex4(gRead.rbWord, rbW); + hex4(gRead.rbCorner, rbC); + } + if (gHaveWrote[0]) { + hex4(gWrote[0], rbA); + hex4(gObserved[0], rbO); + hex4(gObservedCorner[0], rbK); + } + if (gHaveWrote[1]) { + hex4(gWrote[1], txA); + hex4(gObserved[1], txO); + hex4(gObservedCorner[1], txK); + } + if (gHaveAhbOffer) { + hex4(gAhbSeed, ahbSeedS); + hex4(gAhbObserved, ahbObsS); + hex4(gAhbObservedCorner, ahbCorS); + hex4(gAhbPattern, ahbPatS); + } + // "Both directions held": the probe saw our seed bytes at both pixels of both + // paths, and we saw the probe's announced colour at both pixels of both + // paths. Either half alone is a one-way mapping and is not T4's answer. + int readDir = gHaveRead && gRead.texOk && gRead.rbOk && + memcmp(gRead.texWord, gSeed, 4) == 0 && memcmp(gRead.texCorner, gSeed, 4) == 0 && + memcmp(gRead.rbWord, gSeed, 4) == 0 && memcmp(gRead.rbCorner, gSeed, 4) == 0; + int writeDir = gHaveWrote[0] && gHaveWrote[1] && gMatched[0] && gMatched[1]; + // The Android-minted row is not two paths of one leg: what this side can see of + // it is the import, the two pixels of the imported mapping, and its own fill. + // The other half of it -- the probe reading that fill back through the GPU -- is + // the probe's leg, and no number here can stand in for it. + int ahbMatched = gHaveAhbOffer && memcmp(gAhbObserved, gAhbSeed, 4) == 0 && + memcmp(gAhbObservedCorner, gAhbSeed, 4) == 0; + int ahbDir = gAhbImported && gAhbFilled && ahbMatched; + // A row the probe never asked is not a failure here: with no + // EGL_EXT_image_dma_buf_import on the Android side the probe records + // T4-image-import as a capability answer and never sends MSG_T4_READ or + // MSG_T4_WROTE, so only the rows that were actually exercised count. + int dbufRan = gHaveRead || gHaveWrote[0] || gHaveWrote[1]; + int pass = (!dbufRan || (readDir && writeDir)) && (!gHaveAhbOffer || ahbDir); + + pr("FINAL endpoint=%s %s %s %s %s " + "texOk=%d rbOk=%d rung=%d texWord=[%s] texCorner=[%s] rbWord=[%s] rbCorner=[%s] importNote=\"%s\" " + "rb_announced=[%s] rb_observed=[%s] rb_corner=[%s] rb_match=%s rb_corner_match=%s " + "tex_announced=[%s] tex_observed=[%s] tex_corner=[%s] tex_match=%s tex_corner_match=%s " + "readDirection=%s writeDirection=%s " + "ahbOffer=%s ahbImported=%d ahbStride=%u ahbSeed=[%s] ahbObserved=[%s] ahbObservedCorner=[%s] " + "ahbPattern=[%s] ahbMatched=%s ahbDirection=%s ahbFd=%s ahbBo=%s ahbFill=%s ahbNote=\"%s\" " + "verdict=%s why=\"%s\"", + gEndpoint, gDevDesc, gBoDesc, gFdDesc, gFillDesc, + gHaveRead ? (int)gRead.texOk : -1, gHaveRead ? (int)gRead.rbOk : -1, gHaveRead ? (int)gRead.rung : -1, + texW, texC, rbW, rbC, gHaveRead ? gRead.importNote : "-", + rbA, rbO, rbK, ynStr(gMatched[0]), ynStr(gMatchedCorner[0]), + txA, txO, txK, ynStr(gMatched[1]), ynStr(gMatchedCorner[1]), + readDir ? "held" : (gHaveRead ? "broken" : "notrun"), + writeDir ? "held" : (gHaveWrote[0] || gHaveWrote[1] ? "broken" : "notrun"), + gHaveAhbOffer ? "yes" : "no", gHaveAhbOffer ? gAhbImported : -1, gAhbStride, ahbSeedS, ahbObsS, ahbCorS, + ahbPatS, ynStr(ahbMatched), ahbDir ? "held" : (gHaveAhbOffer ? "broken" : "notrun"), gAhbFdDesc, + gAhbBoDesc, gAhbFillDesc, gAhbNote, + pass ? "PASS" : "FAIL", why && why[0] ? why : "-"); + return pass ? 0 : rc; +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +static void usage(void) { + printf("usage: t4_gbm_peer [--endpoint=@mgl-t4] [--width=64] [--height=64] " + "[--seed=RRGGBBAA] [--timeout=60]\n" + " Allocates a GBM buffer in the container, fills it with the seed, offers its\n" + " dma-buf fd to extmem_probe --only-t4 over an abstract unix socket, and checks\n" + " what the probe drew into it. It also answers the probe's own AHardwareBuffer\n" + " (T4-ahb-image): imports its fd as a GBM bo, compares both pixels against the\n" + " colour the probe announced, and fills its mapping with a pattern of its own.\n" + " --width/--height size the GBM buffer and keep the probe's AHardwareBuffer at\n" + " the same size. Exit 0 only when every row the probe asked for held.\n"); +} + +int main(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + const char* a = argv[i]; + if (!strncmp(a, "--endpoint=", 11)) { + gEndpoint = a + 11; + } else if (!strncmp(a, "--width=", 8)) { + gWidth = (uint32_t)strtoul(a + 8, NULL, 0); + } else if (!strncmp(a, "--height=", 9)) { + gHeight = (uint32_t)strtoul(a + 9, NULL, 0); + } else if (!strncmp(a, "--seed=", 7)) { + gSeedWord = (uint32_t)strtoul(a + 7, NULL, 16); + } else if (!strncmp(a, "--timeout=", 10)) { + gTimeout = (int)strtol(a + 10, NULL, 0); + } else if (!strcmp(a, "--help")) { + usage(); + return 0; + } else { + pr("unknown option \"%s\"", a); + usage(); + return 2; + } + } + if (!gWidth || !gHeight || gWidth > 16384 || gHeight > 16384) { + pr("--width/--height must be 1..16384 (got %u x %u)", gWidth, gHeight); + return 2; + } + if (gTimeout < 1) { + pr("--timeout must be at least 1 second (got %d)", gTimeout); + return 2; + } + + // A probe that dies mid-exchange must not take this process down with it; the + // send path reports EPIPE instead. + signal(SIGPIPE, SIG_IGN); + + const char* devPath = "/dev/dri/renderD128"; + pr("t4_gbm_peer: endpoint=%s %ux%u seed=0x%08X timeout=%ds uid=%d pid=%d", gEndpoint, gWidth, + gHeight, gSeedWord, gTimeout, (int)getuid(), (int)getpid()); + + int devFd = open(devPath, O_RDWR | O_CLOEXEC); + if (devFd < 0) { + int e = errno; + pr("open(%s) failed errno=%d (%s) -- the peer must run inside the container: that is where " + "the Anland-backed GBM device lives", + devPath, e, strerror(e)); + return 3; + } + gDev = gbm_create_device(devFd); + if (!gDev) { + int e = errno; + pr("gbm_create_device(%s) failed fd=%d errno=%d (%s)", devPath, devFd, e, strerror(e)); + return 3; + } + snprintf(gDevDesc, sizeof(gDevDesc), "device=%s backend=\"%s\" devFd=%d", devPath, + gbm_device_get_backend_name(gDev), devFd); + pr("gbm_create_device = OK %s", gDevDesc); + + // The driver's own verdict on this format and these flags IS the result. + // There is no fallback to another format: an ARGB8888 RENDERING|LINEAR + // refusal is an answer about the device, and a silent substitution would + // destroy it. + const uint32_t flags = GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR; + gBo = gbm_bo_create(gDev, gWidth, gHeight, GBM_FORMAT_ARGB8888, flags); + if (!gBo) { + int e = errno; + pr("gbm_bo_create(%ux%u, GBM_FORMAT_ARGB8888=0x%08X, flags=0x%X " + "GBM_BO_USE_RENDERING|GBM_BO_USE_LINEAR) refused: errno=%d (%s)", + gWidth, gHeight, (unsigned)GBM_FORMAT_ARGB8888, flags, e, strerror(e)); + return 4; + } + + gStride = gbm_bo_get_stride(gBo); + gFourcc = gbm_bo_get_format(gBo); + gModifierRaw = gbm_bo_get_modifier(gBo); + gBoHandle = gbm_bo_get_handle(gBo).u32; + // The protocol carries UINT64_MAX for "no modifier"; DRM_FORMAT_MOD_INVALID + // is the kernel's way of saying exactly that, so it is translated on the way + // out. The raw value is printed either way. + gModifierTx = (gModifierRaw == T4_DRM_FORMAT_MOD_INVALID) ? UINT64_MAX : gModifierRaw; + pr("gbm_bo_create = OK handle=%u width=%u height=%u planes=%u", gBoHandle, gbm_bo_get_width(gBo), + gbm_bo_get_height(gBo), gbm_bo_get_plane_count(gBo)); + pr("gbm_bo_get_stride = %u", gStride); + pr("gbm_bo_get_format = 0x%08X (DRM_FORMAT_ARGB8888 = 'AR24')", (unsigned)gFourcc); + pr("gbm_bo_get_modifier = 0x%016" PRIx64 " (DRM_FORMAT_MOD_LINEAR = 0; sent as 0x%016" PRIx64 ")", + (uint64_t)gModifierRaw, (uint64_t)gModifierTx); + snprintf(gBoDesc, sizeof(gBoDesc), + "bo=%u w=%u h=%u planes=%u stride=%u fourcc=0x%08X modifier_raw=0x%016" PRIx64 + " modifier_tx=0x%016" PRIx64, + gBoHandle, gbm_bo_get_width(gBo), gbm_bo_get_height(gBo), gbm_bo_get_plane_count(gBo), + gStride, (unsigned)gFourcc, (uint64_t)gModifierRaw, (uint64_t)gModifierTx); + + if (!fillWithSeed()) return 5; + + gBoFd = gbm_bo_get_fd(gBo); + if (gBoFd < 0) { + int e = errno; + pr("gbm_bo_get_fd failed errno=%d (%s) -- an unexportable buffer is not a shareable one", e, + strerror(e)); + return 5; + } + printFdEvidence(gBoFd); + + int sock = connectEndpoint(gEndpoint); + if (sock < 0) return 6; + pr("connected to %s (SOCK_STREAM, %ds recv timeout)", gEndpoint, gTimeout); + + struct T4Layout lay; + memset(&lay, 0, sizeof(lay)); + lay.magic = T4_MAGIC; + lay.version = 1; // the only version this leg has ever had; the probe prints it + lay.width = gWidth; + lay.height = gHeight; + lay.fourcc = gFourcc; + lay.stride = gStride; + lay.offset = 0; // a fresh single-plane bo is mapped from its base + lay.modifier = gModifierTx; + memcpy(lay.seed, gSeed, 4); + snprintf(lay.peer, sizeof(lay.peer), "t4_gbm_peer pid=%d backend=%s bo=%u %ux%u stride=%u", + (int)getpid(), gbm_device_get_backend_name(gDev), gBoHandle, gWidth, gHeight, gStride); + // The note stays numeric on purpose: the exported name and the exporter are + // printed on stdout in full, and a name longer than this field would be + // silently cut here, which is worse than not carrying it at all. + snprintf(lay.note, sizeof(lay.note), "fill=0x%08X stride=%u fourcc=0x%08X", gSeedWord, gStride, + (unsigned)gFourcc); + lay.peer[sizeof(lay.peer) - 1] = 0; + lay.note[sizeof(lay.note) - 1] = 0; + + char seedS[16]; + hex4(gSeed, seedS); + if (!sendMsg(sock, MSG_T4_OFFER, &lay, sizeof(lay), gBoFd)) { + pr("sendMsg(MSG_T4_OFFER) failed errno=%d (%s)", gMsgErrno, strerror(gMsgErrno)); + close(sock); + return 6; + } + pr("MSG_T4_OFFER sent size=%zu magic=0x%08X version=%u w=%u h=%u fourcc=0x%08X stride=%u " + "offset=%u modifier=0x%016" PRIx64 " seed=0x%08X mem=[%s] fd=%d fdname=\"%s\"", + sizeof(lay), lay.magic, lay.version, lay.width, lay.height, (unsigned)lay.fourcc, lay.stride, + lay.offset, (uint64_t)lay.modifier, gSeedWord, seedS, gBoFd, gBoFdName); + pr("MSG_T4_OFFER peer=\"%s\"", lay.peer); + pr("MSG_T4_OFFER note=\"%s\"", lay.note); + + // The probe drives the exchange and this side answers whatever it sends. Either + // row can be skipped by the probe -- with no EGL_EXT_image_dma_buf_import on the + // Android side the dma-buf row is recorded as a capability answer and neither + // MSG_T4_READ nor MSG_T4_WROTE is ever sent -- so no tag may be waited for by + // name. The probe closing the socket after its own verdicts is the normal end of + // the exchange, not an error. + const char* why = "-"; + uint32_t tag = 0; + size_t got = 0; + union T4Payload payload; + memset(&gRead, 0, sizeof(gRead)); + for (;;) { + int fd = -1; + memset(&payload, 0, sizeof(payload)); + if (!recvMsg(sock, &tag, &payload, sizeof(payload), &got, &fd)) { + if (gMsgErrno != 0) { + why = "recvMsg failed while waiting for the probe"; + pr("recvMsg failed errno=%d (%s)", gMsgErrno, strerror(gMsgErrno)); + } else if (!gHaveRead && !gHaveWrote[0] && !gHaveWrote[1] && !gHaveAhbOffer) { + why = "no MSG_T4_READ, MSG_T4_WROTE or MSG_T4_AHB_OFFER from the probe"; + pr("%s: the probe closed the socket first", why); + } else { + pr("the probe closed the socket: end of the exchange"); + } + break; + } + if (tag == MSG_T4_RESULT || tag == MSG_BYE) { + pr("probe closed the exchange with tag=%u", tag); + if (!(gHaveWrote[0] && gHaveWrote[1]) && !gHaveAhbOffer) why = "probe closed the exchange early"; + break; + } + if (tag != MSG_T4_AHB_OFFER && fd >= 0) { + pr("tag=%u arrived with fd=%d, which this protocol does not define: closed", tag, fd); + close(fd); + fd = -1; + } + if (tag == MSG_T4_READ) { + if (got != sizeof(payload.read)) { + pr("expected MSG_T4_READ(%zu), got tag=%u len=%zu", sizeof(payload.read), tag, got); + why = "unexpected length for MSG_T4_READ"; + break; + } + gRead = payload.read; + gHaveRead = 1; + gRead.importNote[sizeof(gRead.importNote) - 1] = 0; + char tw[16], tc[16], rw[16], rc[16]; + hex4(gRead.texWord, tw); + hex4(gRead.texCorner, tc); + hex4(gRead.rbWord, rw); + hex4(gRead.rbCorner, rc); + pr("MSG_T4_READ texOk=%d rbOk=%d rung=%d texWord=[%s] texCorner=[%s] rbWord=[%s] rbCorner=[%s] " + "importNote=\"%s\"", + (int)gRead.texOk, (int)gRead.rbOk, (int)gRead.rung, tw, tc, rw, rc, gRead.importNote); + pr("MSG_T4_READ vs our seed 0x%08X mem=[%s]: texWord=%s texCorner=%s rbWord=%s rbCorner=%s", + gSeedWord, seedS, memcmp(gRead.texWord, gSeed, 4) == 0 ? "match" : "MISMATCH", + memcmp(gRead.texCorner, gSeed, 4) == 0 ? "match" : "MISMATCH", + memcmp(gRead.rbWord, gSeed, 4) == 0 ? "match" : "MISMATCH", + memcmp(gRead.rbCorner, gSeed, 4) == 0 ? "match" : "MISMATCH"); + + continue; + } + if (tag == MSG_T4_WROTE) { + // The probe writes twice, once per path, and waits for each ACK. Its + // verdict is local (its step 8), so nothing here answers with anything + // but the bytes this side actually saw. + struct T4Wrote w = payload.wrote; + if (got != sizeof(w)) { + pr("expected MSG_T4_WROTE(%zu), got tag=%u len=%zu", sizeof(w), tag, got); + why = "unexpected length for MSG_T4_WROTE"; + break; + } + if (w.path < 0 || w.path > 1) { + pr("MSG_T4_WROTE path=%d is neither 0 (renderbuffer) nor 1 (texture); acking ok=0", w.path); + struct T4Ack bad; + memset(&bad, 0, sizeof(bad)); + snprintf(bad.note, sizeof(bad.note), "unknown path %d", (int)w.path); + if (!sendMsg(sock, MSG_T4_ACK, &bad, sizeof(bad), -1)) + pr("sendMsg(MSG_T4_ACK) failed errno=%d (%s)", gMsgErrno, strerror(gMsgErrno)); + continue; + } + int path = (int)w.path; + memcpy(gWrote[path], w.color, 4); + gHaveWrote[path] = 1; + struct T4Ack ack; + memset(&ack, 0, sizeof(ack)); + // The ack carries the first pixel, because that is the four bytes the + // probe compares; the corner is kept here and travels in the note and on + // stdout, where a stride or offset mistake shows up. + int cornerMatch = -1; + int matched = checkPath(path, w.color, &ack, &cornerMatch, gObservedCorner[path]); + gMatched[path] = matched; + gMatchedCorner[path] = cornerMatch; + memcpy(gObserved[path], ack.observed, 4); + if (!sendMsg(sock, MSG_T4_ACK, &ack, sizeof(ack), -1)) { + pr("sendMsg(MSG_T4_ACK) failed errno=%d (%s)", gMsgErrno, strerror(gMsgErrno)); + why = "could not send MSG_T4_ACK"; + break; + } + char oS[16]; + hex4(ack.observed, oS); + pr("MSG_T4_ACK sent path=%d ok=%d observed=[%s] note=\"%s\"", path, (int)ack.ok, oS, ack.note); + continue; + } + if (tag == MSG_T4_AHB_OFFER) { + // The AHB offer is the other direction of the same question: an image + // the Android side minted, which arrives as a socketpair end in + // SCM_RIGHTS plus this payload. + if (got != sizeof(payload.ahb)) { + pr("expected MSG_T4_AHB_OFFER(%zu), got tag=%u len=%zu", sizeof(payload.ahb), tag, got); + if (fd >= 0) close(fd); + why = "unexpected length for MSG_T4_AHB_OFFER"; + break; + } + payload.ahb.note[sizeof(payload.ahb.note) - 1] = 0; + serveAhbOffer(sock, fd, &payload.ahb); + if (fd >= 0) close(fd); + continue; + } + pr("unexpected tag=%u len=%zu: the exchange stops here", tag, got); + why = "unexpected tag from the probe"; + break; + } + + close(sock); + return finish(8, why); +}