mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Docs] (Anland): Chrome decodes video in hardware through the YUV shared images, with probes to check it
This commit is contained in:
@@ -126,9 +126,9 @@ timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/
|
||||
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
|
||||
```
|
||||
Chrome: `bash scripts/chrome-launch.sh <tag> [flags]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`; `CHROME_GPU=in-process` puts the GPU back in the browser, which cannot recover from a device loss).
|
||||
Video: the container has a VA-API driver on the render node the GPU process gets (`/usr/lib/dri/msm_drm_drv_video.so`, V4L2 decode into its own NV12 dma-bufs); MobileGL cannot import those (only its own RGBA shared images; no dma-buf import in the phone's Vulkan, no `GL_OES_EGL_image_external`), and Chrome's GPU process aborts (SIGTRAP, "GPU process exited unexpectedly: exit_code=5") on the first decoded frame - window flicker + stalled video in a loop. `mobilegl-startup.sh chrome` therefore passes `--disable-accelerated-video-decode`. Diagnose a video with a CDP `Media.enable` dump (`kVideoDecoderName`); remote debugging needs a non-default `--user-data-dir`, and a fresh profile also needs `--password-store=basic` (else page loads wait forever on KWallet).
|
||||
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labelled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's colour hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf.
|
||||
Device loss: `setprop debug.mobilegl.inject_device_lost_pid <pid>` (as root) loses the server session of the client whose pid - as it sees itself in the container - is `<pid>`, once per value, on any backend and whether or not it presents; `debug.mobilegl.inject_device_lost_at N` instead loses whichever session makes the N-th device check (Espryt presents and read-backs only). `bash scripts/chrome-recover.sh <in-process|process> [rounds]` checks that Chrome repaints after such a loss without a restart (battery-guarded, stops below 40%). `bash scripts/kwin-recover.sh [rounds]` loses KWin's own session (default twice) and checks that KWin restarts compositing on a fresh session - same KWin, plasmashell and client processes, the screen still animating (glmark2 on Wayland, glxgears on X11, `CHROME=1` adds Chrome) - which needs the anland backend + kwin.patch reset handling deployed (`kwin-stage.sh build`).
|
||||
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
|
||||
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs), `yuvprobe.c` (YUV dma-buf sampling, `gcc -O1 /root/yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm -lm`), `cdpmedia.py` (which decoder a Chrome page uses); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
|
||||
GPU hangs: a shader that never ends stops the WHOLE GPU (every process, MobileGL or not) until the kernel resets it - ~2 s, or ~10 s ("Preemption Fault" in dmesg) when a higher-priority context waits; nothing in user space shortens one stall. What the server does is end the guilty session so it cannot hang the GPU again (Magma: event-bracketed submissions + `mgl-gpu-watch`, budget `MOBILEGL_GPU_HANG_BUDGET_MS` / `debug.mobilegl.gpu_hang_budget_ms`, default 2000; Espryt: the driver's GUILTY reset status, which it reports after the ~2 s reset only). Repro: `scripts/probes/hangclient.c` (Wayland + GL 3.3 core; `-H 60 -N 1 -E 60` hangs every 60 frames, `-H -1` is a frame-gap observer; build with wayland-scanner's xdg-shell files, `-lwayland-client -lwayland-egl -lEGL -lOpenGL -lm`), and `gpuprobe.c` (NDK, plain adb shell; `gpuprobe 30 high` is a high-priority GLES context outside MobileGL - it both measures the device-wide stall and forces the 10 s preemption path).
|
||||
|
||||
A result counts only if ALL hold:
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
# cdpmedia.py [port] [seconds] - ask a Chrome started with --remote-debugging-port=<port> (and a
|
||||
# non-default --user-data-dir) which video decoder its first page's players use, and evaluate
|
||||
# `window.__mglStats` / the element `#t` text of the page. Standard library only (no websocket
|
||||
# module in the container): a minimal RFC 6455 client.
|
||||
# python3 cdpmedia.py 9333 6
|
||||
# Prints kVideoDecoderName / kIsPlatformVideoDecoder / kVideoTracks properties and media errors as
|
||||
# they arrive, then the page's stats text.
|
||||
import base64, json, os, socket, struct, sys, time, urllib.request
|
||||
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9333
|
||||
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0
|
||||
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
|
||||
page = next(t for t in targets if t.get("type") == "page")
|
||||
url = page["webSocketDebuggerUrl"]
|
||||
path = url.split(f"127.0.0.1:{port}", 1)[1]
|
||||
sock = socket.create_connection(("127.0.0.1", port))
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
|
||||
response = b""
|
||||
while b"\r\n\r\n" not in response:
|
||||
response += sock.recv(4096)
|
||||
buffer = response.split(b"\r\n\r\n", 1)[1]
|
||||
|
||||
|
||||
def send(message):
|
||||
data = json.dumps(message).encode()
|
||||
header = bytes([0x81])
|
||||
mask = os.urandom(4)
|
||||
if len(data) < 126:
|
||||
header += bytes([0x80 | len(data)])
|
||||
elif len(data) < 65536:
|
||||
header += bytes([0x80 | 126]) + struct.pack(">H", len(data))
|
||||
else:
|
||||
header += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
|
||||
sock.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||||
|
||||
|
||||
def recv_exact(n):
|
||||
global buffer
|
||||
while len(buffer) < n:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
raise EOFError
|
||||
buffer += chunk
|
||||
out, buffer = buffer[:n], buffer[n:]
|
||||
return out
|
||||
|
||||
|
||||
def recv():
|
||||
first, second = recv_exact(2)
|
||||
length = second & 0x7F
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", recv_exact(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", recv_exact(8))[0]
|
||||
payload = recv_exact(length)
|
||||
return json.loads(payload) if first & 0x0F == 1 else None
|
||||
|
||||
|
||||
send({"id": 1, "method": "Media.enable"})
|
||||
# Players created before the domain was enabled report nothing: reload so the page makes new ones.
|
||||
if os.environ.get("CDP_NAVIGATE"):
|
||||
send({"id": 3, "method": "Page.navigate", "params": {"url": os.environ["CDP_NAVIGATE"]}})
|
||||
elif os.environ.get("CDP_RELOAD", "1") == "1":
|
||||
send({"id": 3, "method": "Page.reload"})
|
||||
deadline = time.time() + seconds
|
||||
sock.settimeout(0.5)
|
||||
seen = set()
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
message = recv()
|
||||
except socket.timeout:
|
||||
continue
|
||||
if not message or "method" not in message:
|
||||
continue
|
||||
if message["method"] == "Media.playerPropertiesChanged":
|
||||
for prop in message["params"]["properties"]:
|
||||
if prop["name"] in ("kVideoDecoderName", "kIsPlatformVideoDecoder", "kVideoTracks", "kResolution"):
|
||||
line = f"{prop['name']} = {prop['value']}"
|
||||
if line not in seen:
|
||||
seen.add(line)
|
||||
print(line)
|
||||
elif message["method"] in ("Media.playerErrorsRaised",):
|
||||
print("media error:", json.dumps(message["params"])[:300])
|
||||
send({"id": 2, "method": "Runtime.evaluate",
|
||||
"params": {"expression": os.environ.get("CDP_EXPR", "(document.getElementById('t')||{}).textContent"), "returnByValue": True}})
|
||||
sock.settimeout(3)
|
||||
while True:
|
||||
message = recv()
|
||||
if message and message.get("id") == 2:
|
||||
print("page:", message.get("result", {}).get("result", {}).get("value"))
|
||||
break
|
||||
@@ -0,0 +1,410 @@
|
||||
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
|
||||
//
|
||||
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
|
||||
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
|
||||
//
|
||||
// What it does, in an ES 3.0 context on a pbuffer:
|
||||
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
|
||||
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
|
||||
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
|
||||
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
|
||||
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
|
||||
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
|
||||
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
|
||||
// again: the new order must show (the server refreshes a foreign buffer at each frame).
|
||||
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
|
||||
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
|
||||
// check is that every call succeeds).
|
||||
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
|
||||
#define _GNU_SOURCE
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <fcntl.h>
|
||||
#include <gbm.h>
|
||||
#include <linux/dma-buf.h>
|
||||
#include <linux/dma-heap.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef DRM_FORMAT_NV12
|
||||
#define DRM_FORMAT_NV12 0x3231564E
|
||||
#endif
|
||||
#ifndef DRM_FORMAT_P010
|
||||
#define DRM_FORMAT_P010 0x30313050
|
||||
#endif
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL: " __VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
|
||||
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
|
||||
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
|
||||
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
|
||||
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
|
||||
static PFNEGLCREATESYNCKHRPROC pCreateSync;
|
||||
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
|
||||
static EGLDisplay g_display;
|
||||
|
||||
enum { W = 256, H = 128, BARS = 4 };
|
||||
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
|
||||
|
||||
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
|
||||
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
|
||||
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
|
||||
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
|
||||
const double y = kr * r + kg * g + kb * b;
|
||||
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
|
||||
const double scale = (double)(1 << (bits - 8));
|
||||
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
|
||||
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
|
||||
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
|
||||
}
|
||||
|
||||
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
|
||||
// samples with the 10 significant bits high.
|
||||
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
|
||||
const int bits = bytes == 1 ? 8 : 10;
|
||||
for (int y = 0; y < H; ++y) {
|
||||
for (int x = 0; x < W; ++x) {
|
||||
int bar = x * BARS / W;
|
||||
if (reverse) bar = BARS - 1 - bar;
|
||||
int code[3];
|
||||
Encode709(kBars[bar], bits, code);
|
||||
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
|
||||
if (bytes == 1) py[0] = (uint8_t)code[0];
|
||||
else *(uint16_t*)py = (uint16_t)(code[0] << 6);
|
||||
if ((x & 1) == 0 && (y & 1) == 0) {
|
||||
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
|
||||
if (bytes == 1) {
|
||||
pc[0] = (uint8_t)code[1];
|
||||
pc[1] = (uint8_t)code[2];
|
||||
} else {
|
||||
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
|
||||
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint Compile(GLenum type, const char* source) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, NULL);
|
||||
glCompileShader(shader);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
|
||||
printf("shader: %s\n", log);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
static GLuint g_program, g_fbo, g_rgba;
|
||||
|
||||
static void SetUpDraw(void) {
|
||||
const char* vs = "#version 300 es\n"
|
||||
"out vec2 uv;\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"
|
||||
" uv = p;\n"
|
||||
"}\n";
|
||||
const char* fs = "#version 300 es\n"
|
||||
"#extension GL_OES_EGL_image_external_essl3 : require\n"
|
||||
"precision mediump float;\n"
|
||||
"uniform samplerExternalOES tex;\n"
|
||||
"in vec2 uv;\n"
|
||||
"out vec4 color;\n"
|
||||
"void main() { color = texture(tex, uv); }\n";
|
||||
g_program = glCreateProgram();
|
||||
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
|
||||
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
|
||||
glLinkProgram(g_program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
|
||||
CHECK(linked, "the samplerExternalOES program did not link");
|
||||
GLint type = 0, size = 0;
|
||||
char name[64];
|
||||
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
|
||||
printf("uniform 0: %s type 0x%04x\n", name, type);
|
||||
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
|
||||
glGenTextures(1, &g_rgba);
|
||||
glBindTexture(GL_TEXTURE_2D, g_rgba);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
|
||||
glGenFramebuffers(1, &g_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
|
||||
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
|
||||
GLuint vao;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
}
|
||||
|
||||
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
|
||||
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
|
||||
glViewport(0, 0, W, H);
|
||||
glUseProgram(g_program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
static unsigned char pixels[W * H * 4];
|
||||
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
GLenum error = glGetError();
|
||||
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
|
||||
if (tolerance < 0) {
|
||||
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
|
||||
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
|
||||
pixels[(H / 2 * W + W / 2) * 4 + 3]);
|
||||
return;
|
||||
}
|
||||
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
|
||||
// so any row does; take two to catch a broken chroma row pitch.
|
||||
const int rows[2] = {H / 4, (3 * H) / 4};
|
||||
for (int r = 0; r < 2; ++r) {
|
||||
for (int bar = 0; bar < BARS; ++bar) {
|
||||
const int x = bar * (W / BARS) + W / (2 * BARS);
|
||||
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
|
||||
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
|
||||
int worst = 0;
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
int d = abs((int)p[c] - (int)want[c]);
|
||||
if (d > worst) worst = d;
|
||||
}
|
||||
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
|
||||
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
|
||||
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int AllocateDmaHeap(size_t size) {
|
||||
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
|
||||
if (heap < 0) {
|
||||
perror("open /dev/dma_heap/system");
|
||||
return -1;
|
||||
}
|
||||
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
|
||||
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
|
||||
perror("DMA_HEAP_IOCTL_ALLOC");
|
||||
close(heap);
|
||||
return -1;
|
||||
}
|
||||
close(heap);
|
||||
return (int)data.fd;
|
||||
}
|
||||
|
||||
static void Sync(int fd, int start) {
|
||||
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
|
||||
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
|
||||
}
|
||||
|
||||
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
|
||||
EGLint attribs[64];
|
||||
int n = 0;
|
||||
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
|
||||
A(EGL_WIDTH, W);
|
||||
A(EGL_HEIGHT, H);
|
||||
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
|
||||
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
|
||||
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
|
||||
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
|
||||
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
|
||||
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
|
||||
if (modifier) {
|
||||
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
|
||||
}
|
||||
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
|
||||
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
|
||||
attribs[n] = EGL_NONE;
|
||||
#undef A
|
||||
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
|
||||
return image;
|
||||
}
|
||||
|
||||
static GLuint ExternalTexture(EGLImageKHR image) {
|
||||
GLuint texture;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
|
||||
GLenum error = glGetError();
|
||||
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
|
||||
GLint bound = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
|
||||
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A frame boundary as an offscreen producer makes one: a native fence.
|
||||
static void EndFrame(void) {
|
||||
EGLint attribs[] = {EGL_NONE};
|
||||
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
|
||||
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
|
||||
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
|
||||
}
|
||||
|
||||
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
|
||||
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
|
||||
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
|
||||
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
|
||||
const uint32_t offsetC = pitchY * (H + 16);
|
||||
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
|
||||
int fd = AllocateDmaHeap(size);
|
||||
if (fd < 0) {
|
||||
CHECK(0, "%s: no dma-buf to test with", what);
|
||||
return;
|
||||
}
|
||||
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
|
||||
if (map == MAP_FAILED) return;
|
||||
Sync(fd, 1);
|
||||
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
|
||||
Sync(fd, 0);
|
||||
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
|
||||
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
|
||||
if (image == EGL_NO_IMAGE_KHR) return;
|
||||
GLuint texture = ExternalTexture(image);
|
||||
DrawAndCheck(texture, what, 0, tolerance);
|
||||
EndFrame();
|
||||
// New content, same buffer: shows after the frame boundary.
|
||||
Sync(fd, 1);
|
||||
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
|
||||
Sync(fd, 0);
|
||||
EndFrame();
|
||||
DrawAndCheck(texture, what, 1, tolerance);
|
||||
glDeleteTextures(1, &texture);
|
||||
pDestroyImage(g_display, image);
|
||||
munmap(map, size);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void GbmTest(void) {
|
||||
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
|
||||
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
|
||||
struct gbm_device* gbm = gbm_create_device(node);
|
||||
if (gbm == NULL) {
|
||||
CHECK(0, "gbm_create_device failed");
|
||||
return;
|
||||
}
|
||||
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
|
||||
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
|
||||
printf("gbm NV12 supported: %d, P010: %d\n", supported,
|
||||
gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
|
||||
CHECK(supported == 1, "gbm does not support NV12");
|
||||
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
|
||||
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
|
||||
if (bo != NULL) {
|
||||
const int planes = gbm_bo_get_plane_count(bo);
|
||||
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
|
||||
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
|
||||
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
|
||||
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
|
||||
int fd = gbm_bo_get_fd(bo);
|
||||
EGLImageKHR image =
|
||||
Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
|
||||
gbm_bo_get_stride_for_plane(bo, 1), 0);
|
||||
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
|
||||
if (image != EGL_NO_IMAGE_KHR) {
|
||||
GLuint texture = ExternalTexture(image);
|
||||
DrawAndCheck(texture, "gbm NV12", 0, -1);
|
||||
glDeleteTextures(1, &texture);
|
||||
pDestroyImage(g_display, image);
|
||||
}
|
||||
close(fd);
|
||||
gbm_bo_destroy(bo);
|
||||
}
|
||||
gbm_device_destroy(gbm);
|
||||
close(node);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
EGLint major, minor;
|
||||
if (!eglInitialize(g_display, &major, &minor)) {
|
||||
printf("FAIL: eglInitialize\n");
|
||||
return 2;
|
||||
}
|
||||
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
|
||||
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
|
||||
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
|
||||
eglBindAPI(EGL_OPENGL_ES_API);
|
||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
|
||||
EGLConfig config;
|
||||
EGLint count = 0;
|
||||
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
|
||||
if (count < 1) {
|
||||
printf("FAIL: no config\n");
|
||||
return 2;
|
||||
}
|
||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
|
||||
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
|
||||
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
||||
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
|
||||
printf("FAIL: no ES 3 context\n");
|
||||
return 2;
|
||||
}
|
||||
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
|
||||
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
|
||||
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
|
||||
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
|
||||
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
|
||||
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
|
||||
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
|
||||
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
|
||||
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
|
||||
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
|
||||
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
|
||||
|
||||
EGLint formats[32], formatCount = 0;
|
||||
pQueryFormats(g_display, 32, formats, &formatCount);
|
||||
int nv12 = 0;
|
||||
for (int i = 0; i < formatCount; ++i) {
|
||||
printf("dma-buf format %.4s", (const char*)&formats[i]);
|
||||
EGLuint64KHR modifiers[4];
|
||||
EGLBoolean externalOnly[4];
|
||||
EGLint modifierCount = 0;
|
||||
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
|
||||
for (int m = 0; m < modifierCount; ++m)
|
||||
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
|
||||
printf("\n");
|
||||
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
|
||||
}
|
||||
CHECK(nv12, "NV12 is not a listed dma-buf format");
|
||||
|
||||
SetUpDraw();
|
||||
ForeignTest(DRM_FORMAT_NV12, 1, 12);
|
||||
ForeignTest(DRM_FORMAT_P010, 2, 16);
|
||||
GbmTest();
|
||||
|
||||
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
|
||||
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglTerminate(g_display);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ The repository is organized into following top-level modules:
|
||||
2. **MG_Impl** — front-end implementations of Graphics APIs that interact with `MG_State` and `MG_Backend`: OpenGL, plus the window-system APIs EGL, GLX, WGL, CGL and NSOpenGL.
|
||||
3. **MG_Backend** — per-backend translation layer that maps front-end Graphics APIs' semantics and state into concrete backend API calls (e.g. OpenGL ES, Vulkan).
|
||||
4. **MG_Pipe** and **MG_Remote** — the command stream between front-end and backend, and the client/server transport that lets the backend run in a separate process (see `docs/Disaggregated`).
|
||||
5. **MG_Gbm** — a GBM backend whose buffers are images shared with the MobileGL server, so Wayland clients and compositors can exchange them as dma-bufs.
|
||||
5. **MG_Gbm** — a GBM backend whose buffers are images shared with the MobileGL server, so Wayland clients and compositors can exchange them as dma-bufs (RGBA, and NV12/P010 YUV where the server's platform allocates them). YUV dma-bufs - the server's own or a video decoder's - import through `EGL_EXT_image_dma_buf_import` and are sampled through `GL_OES_EGL_image_external(_essl3)`: the backend converts the image into the texture's RGBA level at its first use in each frame (zero-copy for a server image; a foreign buffer, which the platform cannot import, is first copied into one by the CPU).
|
||||
6. **MG_Util** and other utility modules.
|
||||
|
||||
## Third-party components
|
||||
@@ -173,6 +173,7 @@ MobileGL supports runtime configuration via environment variables.
|
||||
| `MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_COHERENT_AS_FLUSH` | Treat persistent `GL_MAP_FLUSH_EXPLICIT_BIT` maps as coherent (app-compat for engines like Flywheel that never flush them). | `0`, `1` | `0` |
|
||||
| `MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION` | Always emulate depth/stencil `glReadPixels`/`glGetTexImage` by shader sampling on Espryt, instead of using the driver's own depth/stencil readback where it has one. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_YUV_DRIVER_CONVERSION` | Espryt: convert YUV images with the driver's external-sampler conversion (its own colour space) instead of `GL_EXT_YUV_target` with the import's `EGL_YUV_COLOR_SPACE_HINT_EXT` / `EGL_SAMPLE_RANGE_HINT_EXT`. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_SERVER_FRAME_STATS` | Log one `P65ServerFrame` timing line per presented frame on the server (apply vs. wall time, socket read rate). On Android also `debug.mobilegl.server_frame_stats`. | `0`, `1` | `0` |
|
||||
| `VK_ICD_FILENAMES` | Select the Vulkan ICD used by the Vulkan loader. | Path to an ICD JSON file | Loader default |
|
||||
|
||||
|
||||
@@ -124,6 +124,8 @@ sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
|
||||
|
||||
Chrome:`bash scripts/chrome-launch.sh <tag> [参数]`(日志 `/tmp/mgl-chrome-<tag>.client.log`,截图 `ch-<tag>.png`)。探针:`scripts/probes/texfmt.c`(各纹理格式是否保住 alpha)、`dmahash.c`(对某进程的 dma-buf 取样哈希);放进容器 `/root` 编,例如 `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`,用 `mgrun offscreen` 跑(之前的确切编译参数未验证)。
|
||||
|
||||
视频:Chrome 走硬解(VaapiVideoDecoder)。容器的 VA-API 驱动(`msm_drm_drv_video.so`,V4L2 解码)自己分配 surface 并导出 NV12 dma-buf(单 fd,R8 + GR88 两层),Chrome 以 NV12/P010 EGLImage 绑到 `GL_TEXTURE_EXTERNAL_OES`。这种**外来** YUV dma-buf 平台导入不了,MobileGL 走明确标注的 CPU 拷贝兜底:server 映射它、每帧首次使用时拷进自己的 YUV AHB(`AHardwareBuffer_lockPlanes`),再在 GPU 上转换进纹理的 RGBA 层(Espryt:`GL_EXT_YUV_target` + 导入时的色彩 hint,`MOBILEGL_YUV_DRIVER_CONVERSION=1` 改用驱动的 external sampler;Magma:AHB external format 上的 `VkSamplerYcbcrConversion`)。server 自己分配的 NV12/P010(GBM)零拷贝。`MOBILEGL_CHROME_VIDEO_DECODE=software` 恢复 `--disable-accelerated-video-decode`。检查:容器里 `python3 scripts/probes/cdpmedia.py <port> <秒>`(CDP `Media.enable`,`kVideoDecoderName`),`scripts/probes/yuvprobe.c` 像素校验外来 NV12/P010 导入、逐帧刷新和 GBM NV12。KWin 自己的 NV12 路径(拆成 R8 + GR88 两个导入)不支持,KWin 不通告 YUV 格式。
|
||||
|
||||
结论成立要同时满足:
|
||||
|
||||
- client 日志是 `presented through linux-dmabuf shared images`,不是 `wl_shm`;出现 `could not be presented through a linux-dmabuf shared image` 说明退回了;
|
||||
|
||||
Reference in New Issue
Block a user