chromium-fourier r2 + firefox-fourier 150.0.1 + KWIN_PIVOT.md
build and publish packages / distcc-avahi-aarch64 (push) Successful in 46s
build and publish packages / lmcp-any (push) Successful in 9s
build and publish packages / lmcp-debian (push) Successful in 4s
build and publish packages / claude-his-any (push) Successful in 7s
build and publish packages / ffmpeg-v4l2-request-aarch64 (push) Successful in 12m8s
build and publish packages / claude-his-debian (push) Successful in 5s

chromium-fourier:
- patch 3/3 nv12-external-oes-on-modifier-external-only.patch — adds
  NativePixmapEGLBinding::ModifierRequiresExternalOES helper, extends
  OzoneImageGLTexturesHolder::GetBinding to honor EGL external_only
  flag for NV12 dmabufs on panfrost / panthor. Validated on ohm
  (RK3566 hantro mainline 6.19.10): bbb_1080p30_h264.mp4 plays at
  34.7 % combined CPU vs ~131 % pre-patch baseline (~3.8x).
- PKGBUILD pkgrel 1->2, source array + sha256sums + prepare() hook for
  patch 4, patch numbering 1/2,2/2 -> 1/3,2/3,3/3.
- NEXT.md appended with 2026-04-28 section: patch 4 design, validation
  log, KWin GL_ALPHA bug pinpoint (preexisting since 2026-03-06,
  affects every wayland video client; unrelated to chromium-fourier),
  device-renumbering note (/dev/video1 = encoder post-reboot).
- KWIN_PIVOT.md: 4-phase plan to identify and patch KWin's
  glTexImage2D(internalFormat=GL_ALPHA) site, ohm-only test plan,
  scope discipline.
- patches/ now tracked (compiler-rt-adjust-paths, enable-v4l2,
  wayland-allow-direct-egl-gles2, nv12-external-oes); the dead-end
  chromeos-pipeline-bypass.patch removed.

firefox-fourier:
- 4 patches (gfxinfo v4l2 stateless fourccs, libwrapper hwdevice ctx,
  ffmpegvideo v4l2-request route, prefs v4l2-request default).
- PKGBUILD bumped to firefox 150.0.1, Arch toolchain glue patches
  layered in, mozconfig with --without-wasm-sandboxed-libraries for
  ALARM, package() launcher fix (rm -f symlink before cat > to avoid
  ENOENT through the dangling /usr/local symlink mach install drops).
- 150.0.1-1-aarch64.pkg.tar.zst built on boltzmann (95 MB), pending
  fresnel power-on for V4L2 stateless validation on RK3399.
This commit is contained in:
2026-04-28 12:02:18 +00:00
parent 7bb2fbeca9
commit 8756ce38be
15 changed files with 1711 additions and 60 deletions
+181
View File
@@ -0,0 +1,181 @@
# KWin pivot — fix the `glTexImage2D(GL_ALPHA)` stall
## What we know
KWin 6.6.4-1 on Arch Linux ARM (Plasma 6.6.4-1, mesa 26.0.5-1, libdrm
2.4.131-1) on ohm (PineTab2 / RK3566 / panfrost) silently corrupts its
GL command queue mid-frame whenever a wayland client posts a video
buffer. The journal carries a rolling stream of:
```
kwin_wayland: 0x4: GL_INVALID_VALUE in glTexImage2D(internalFormat=GL_ALPHA)
kwin_wayland: 0x4: GL_INVALID_OPERATION in glTexSubImage2D(invalid texture level 0) × N
```
`GL_ALPHA` is not a valid `internalFormat` for `glTexImage2D` under
**OpenGL ES 3.x** (it was the GLES1.x single-channel alpha format;
GLES3 deprecates it for sized formats — `GL_R8`, `GL_LUMINANCE8_ALPHA8`,
etc.). Once the texture allocation fails, the `glTexSubImage2D` calls
that should populate it all error at level 0. KWin keeps retrying the
same broken upload every frame, never recovers, and the present-callback
path that depends on that texture stops acking client frames. Every
wayland video client deadlocks on the missing ack.
First occurrence in this box's journal: **2026-03-06** — the bug
predates any chromium-fourier work by roughly seven weeks.
## Triangulation already in hand
| Client | Outcome |
|---|---|
| chromium-fourier 149-r2 (with patch 3/3) | plays ~3 s @ 34.7 % CPU then renderer/GPU park in `futex_do_wait` |
| chromium-fourier 149-r2 (without patch 3/3) | plays ~10 s (slower path delays surfacing) then identical deadlock |
| VLC | `cannot convert decoder/filter output to any format supported by the output``could not initialize video chain` |
| mpv `--vo=null --hwdec=v4l2request` | `Could not create device.` (mpv-side bug, separate, unrelated) |
| ffmpeg `-hwaccel v4l2request -i bbb -f null -` | plays through clean at 36 fps; hardware path is healthy |
Decode path is healthy on this hardware. The wall is exclusively the
compositor's GL backend.
## Constraint: ohm is the only test box on hand
ampere (RK3588 / panthor) is in the boxes-from-Shenzhen pile, currently
DOWN. fresnel (RK3399 / Pinebook Pro) is offline. boltzmann (Rock 5
ITX+ build host) doesn't run KWin. We do every step on ohm; we accept
the wifi flakiness and the occasional reboot.
## Phase 1 — Reproduce outside chrome and bound the trigger (1 evening)
Goal: a deterministic, headless-or-near-headless reproduction that
doesn't require launching a 800-MB browser.
1. **Smallest-possible client.** Build a 50-line C wayland client that
creates a `wp_linux_dmabuf_v1` buffer, pumps frames at 30 fps, and
exits when KWin first errors. Use `weston-simple-dmabuf-egl` from
the `weston` package as a starting template — already does exactly
this but without our specific format/modifier matrix.
2. **Vary the format/modifier matrix.** Run the smallest-possible
client with each of: NV12 + LINEAR, NV12 + AFBC, NV12 + AFRC,
AR24 + LINEAR, XR24 + LINEAR. We already know NV12 paths trigger;
confirming AR24/XR24 do *not* trigger localizes the bug to KWin's
YUV import path (vs a generic dmabuf import bug).
3. **Vary the buffer dimensions.** Some KWin texture-cache paths
allocate fixed-size internal scratch textures; non-power-of-two,
non-multiple-of-16, or specifically odd-aspect cases sometimes
trigger paths that healthy aspect ratios skip. Test 1920×1080,
1280×720, 854×480, 640×360 and a deliberately weird 1366×768.
4. **Vary KWin scene type.** Switch
`kwin_wayland --scene-type=opengl` vs `--scene-type=opengl-es`
(current default on this hardware). If the bug only fires under
GLES, that's a strong signal — the offending site is in a
GLES-only fallback.
By the end of Phase 1 we should have a one-line `weston-simple-dmabuf-egl
-format=NV12 -modifier=…` that triggers the GL_ALPHA error within
seconds, plus a yes/no answer to "does AR24 also trigger".
## Phase 2 — Identify the call site (12 evenings)
The crime scene is somewhere in `kwin/src/scene/*` or
`kwin/src/effects/*`. Suspects, ranked:
- **`SurfaceItemWayland::createPixmapTexture``GLTexture::create`
with `GL_ALPHA`.** This is the most likely path: KWin allocates a
fallback per-plane texture when the dmabuf import path can't take
the buffer whole. NV12 has a Y plane (single-channel) and a CbCr
plane (two-channel); historically the Y plane has been allocated as
`GL_ALPHA` in software fallbacks. If the EGL dmabuf import returned
`EGL_BAD_ATTRIBUTE` for `external_only` modifiers and KWin fell
through to per-plane, this is exactly where it would land.
- **`BlurEffect::initBlurTexture` / `BackgroundContrastEffect::*`.**
Single-channel noise textures for blur dither. Less likely (these
fire on every frame regardless of video clients) but listed for
completeness.
- **Window-decoration text glyph cache.** Qt's QGLTexture historically
requested `GL_ALPHA` for monochrome glyph atlases. Plasma 6 should
have moved to `GL_RED` long ago, but a stale code path in a
third-party theme or systray icon could still hit it.
- **Cursor texture upload via `wl_shm_pool` + ARGB8888.** KWin's
cursor scene sometimes uploads via glTexImage2D — but the format
there is `GL_RGBA`, not `GL_ALPHA`. Probably not the suspect.
Tooling to identify *which*:
1. **`apitrace trace --api egl kwin_wayland …`** then
`apitrace dump trace.trace | grep -B5 GL_ALPHA`. Apitrace gives
us the C++ call stack at the offending site if KWin was built with
debug symbols.
2. **`MESA_GL_DEBUG=context KWIN_GL_DEBUG=1 kwin_wayland --replace`**
plus `glDebugMessageCallback` already installed in KWin's
`OpenGLBackend` will print the source/type/severity for each
`GL_INVALID_VALUE`. Whether the file/line in the message includes
the user-space caller depends on Mesa's debug-extension support;
on panfrost it usually does include the GL function name and an
ID, but not the C++ source — that is what apitrace adds.
3. **Build kwin from source** (`extra/kwin` PKGBUILD on Arch ARM,
patch in `-DDEBUG=ON`, `-DCMAKE_BUILD_TYPE=Debug`) so the call
stacks resolve to file:line.
## Phase 3 — Write the patch (½ evening once Phase 2 is done)
If the offender is a `GL_ALPHA` allocation in a GLES3 context, the
fix is mechanical:
```diff
- glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0,
- GL_ALPHA, GL_UNSIGNED_BYTE, data);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0,
+ GL_RED, GL_UNSIGNED_BYTE, data);
```
…and adjust the consuming shader's swizzle:
```diff
- gl_FragColor = vec4(texture2D(s, uv).a, …);
+ gl_FragColor = vec4(texture2D(s, uv).r, …);
```
If the offender is a per-plane fallback in the dmabuf import path
(suspect #1 above), the patch is larger because the right fix is to
*not fall through to the broken path* — handle the `external_only`
case by binding `GL_TEXTURE_EXTERNAL_OES` instead. That mirrors the
chromium-fourier patch 3/3 done at the chromium layer; symmetry says
KWin should do the same in its `glTexImage` consumer.
## Phase 4 — Ship and upstream (1 evening)
1. **Local Arch package** as `kwin-fourier` under
`marfrit-packages/arch/kwin-fourier/`, sibling to chromium-fourier
and firefox-fourier. PKGBUILD inherits from `extra/kwin`, drops
in our patch, bumps `pkgrel`. Same `provides=kwin conflicts=kwin`
pattern.
2. **Validate on ohm** by running the chromium-fourier 149-r2 build +
the bbb sample for a minute uninterrupted. Success = no GL_ALPHA
in the journal, no stall, smooth playback at the 34.7 % CPU
number from the chromium validation.
3. **Upstream** via:
- File a `kwin` bug on bugs.kde.org with: apitrace fragment, our
hardware (Mali-G52 panfrost on RK3566 mainline), exact mesa
version, repro steps via `weston-simple-dmabuf-egl` if Phase 1
produced one.
- Push an MR to invent.kde.org/plasma/kwin against `master`.
4. **Document** the fix in `chromium-fourier/docs/dmabuf-zero-copy.md`
so the next person who lands on the same wall finds the breadcrumb
trail.
## What success looks like
`chromium-fourier-149-r2` on ohm under KWin Wayland plays
`bbb_1080p30_h264.mp4` end-to-end at the 34.7 % CPU figure already
recorded by the architectural validation, with zero `GL_INVALID_VALUE`
in the journal during playback. That number is the goal of the entire
chromium-fourier campaign for RK3566 — it is currently blocked on a
bug that has nothing to do with chromium.
## Scope discipline
We do not turn this into "audit the entire KWin GLES backend." If
Phase 2 surfaces additional latent GL_INVALID_* errors that don't
matter for video playback, we note them in the bug report and move
on. The pivot is explicitly "remove this single wall so the
chromium-fourier patch series can ship a working stack on RK3566."
+278
View File
@@ -148,3 +148,281 @@ to install chromium's bundled clang (x86_64 host, arm64 sysroot), then
The boltzmann `chromium-builder` LXD container is preserved as fallback
but no longer the active build host. If cross-compile pans out, that
container can be torn down.
## First runtime validation on ohm — 2026-04-26 22:26 UTC
Cross-compile produced a working aarch64 binary (chrome 647 MB ELF +
chrome_crashpad_handler 4.3 MB + .pak + locales). Tarball
`chromium-fourier-147.tar.gz` (226 MB) transferred CT 220 → hertz → ohm.
Launched in mfritsche's KWin Wayland session (tty2, panfrost render
node) playing `bbb_1080p30_h264.mp4` from file:// with
`LIBVA_DRIVER_NAME=v4l2_request`,
`LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video0`,
`--use-gl=egl --ozone-platform=wayland
--enable-features=VaapiVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxGL
--disable-features=UseChromeOSDirectVideoDecoder
--autoplay-policy=no-user-gesture-required`.
**Result: V4L2 path NOT engaged.** Chrome 147 routes the H.264 stream
through `MojoVideoDecoderService``media/filters/ffmpeg_video_decoder.cc`
(software FFmpeg). Renderer pegs at ~92 % CPU, `/dev/video0` is never
opened (`fuser` returns empty), no `V4L2VideoDecoder` /
`VaapiVideoDecoder` log lines appear at `--v=1
--vmodule="*/vaapi/*=2,*/v4l2/*=2,*video_decoder*=2,*media/gpu/*=2"`.
Compositor also fell back to software (`Switching to software
compositing.` even though panfrost render node was picked) — secondary
issue, separate from the codec wall.
**Conclusion**: 7Ji-style gn args (`use_v4l2_codec=true
use_v4lplugin=true use_linux_v4l2_only=true`) alone are insufficient
on chromium 147. The V4L2VideoDecoder factory is still gated behind
`BUILDFLAG(IS_CHROMEOS)``media/mojo/services/gpu_mojo_media_client_*.cc`
and `media/gpu/gpu_video_decode_accelerator_factory.cc` only register
the V4L2 path on ChromeOS targets.
## Validation pass 2 — 2026-04-26 22:38 UTC — V4L2VDA proven engaged
Two distinct issues were diagnosed and the codec one was fully resolved
without source surgery beyond a 2-line patch:
### Issue 1 — runtime master gate
`media::kAcceleratedVideoDecodeLinux` (user-visible feature name
"AcceleratedVideoDecoder") is hard-coded in
`media/base/media_switches.cc:750` to `FEATURE_ENABLED_BY_DEFAULT` only
when `BUILDFLAG(USE_VAAPI)` is set. On a USE_V4L2_CODEC-only build it
defaults DISABLED, the linux gpu_mojo_media_client returns
`VideoDecoderType::kUnknown`, and chrome silently falls back to
`media/filters/ffmpeg_video_decoder.cc`.
**Fix**: 2-line patch (now `patches/enable-v4l2-decoder-default.patch`):
```
-#if BUILDFLAG(USE_VAAPI)
+#if BUILDFLAG(USE_VAAPI) || BUILDFLAG(USE_V4L2_CODEC)
```
The placeholder `chromeos-pipeline-bypass.patch` was deleted; PKGBUILD
now references the real patch. **Verified to apply cleanly on the CT 220
tree** (chromium 149 main).
### Issue 2 — bundled GL libs missing from tarball
The first runtime tarball shipped only `chrome` + `.pak` + locales +
`chrome_crashpad_handler`. It omitted `libEGL.so` / `libGLESv2.so`
(ANGLE) plus `libvk_swiftshader.so` and `libvulkan.so.1`. Without these,
the GPU process logs `gl::init::InitializeStaticGLBindingsOneOff failed`
and chrome falls into "Switching to software compositing." mode — which
*also* gates the V4L2 path off because the gpu_mojo_media_client never
gets a chance to dispatch.
Additionally, `--use-gl=egl` is rejected ("Requested GL implementation
gl=egl-gles2,angle=none not found in allowed implementations:
[(gl=egl-angle,angle=opengl|opengles|vulkan)]"): the build only allows
ANGLE-mediated paths. Right launcher invocation:
`--use-gl=angle --use-angle=gles`.
**Fix**: package the four libs alongside `chrome` and update the
launcher flag set. Both will be encoded in the next iteration of the
PKGBUILD's `package()` and a `chromium-fourier` launcher script.
### What we observed once both fixes were in place
With patch + bundled libs + `--enable-features=AcceleratedVideoDecoder`
+ `--use-gl=angle --use-angle=gles`, chrome on RK3566 hantro logs:
```
[gpu]: V4L2VideoDecoder()
[gpu]: Open(): No devices supporting H264 for type: 0 <- type=0 is single-planar; chrome retries multi-planar
[gpu]: InitializeBackend(): Using a stateless API for profile: h264 main and fourcc: S264
[gpu]: SetupInputFormat(): Input (OUTPUT queue) Fourcc: S264
[gpu]: AllocateInputBuffers(): Requesting: 17 OUTPUT buffers of type V4L2_MEMORY_MMAP
[gpu]: SetExtCtrlsInit(): Setting EXT_CTRLS for H264
[gpu]: SetupOutputFormat(): Output (CAPTURE queue) candidate: NV12
[gpu]: ContinueChangeResolution(): Requesting: 6 CAPTURE buffers of type V4L2_MEMORY_MMAP
[renderer]: OnDecoderSelected<video>: V4L2VideoDecoder
MediaEvent: "Selected V4L2VideoDecoder for video decoding,
config: codec: h264, profile: h264 main, [...]
coded size: [1920,1080], visible rect: [0,0,1920,1080]"
```
`fuser /dev/video1 /dev/media0` shows `chrome` (gpu pid) holding both
fds. The hantro stateless decoder is engaged. **First end-to-end
chromium-fourier V4L2 hardware decode validation: PASS** for H.264
1080p Big Buck Bunny on PineTab2.
Caveat: the render-side CPU was still ~85% during playback. Subsequent
investigation traced this to a different root cause than initially
guessed (see Pass 3 below).
## Validation pass 3 — 2026-04-26 22:50 UTC — zero-copy diagnosis
The 85 % CPU is **not** caused by software compositing or dmabuf v5
negotiation. The dmabuf-v5 warning ("Binding to zwp_linux_dmabuf_v1
version 4 but version 5 is available") is benign — chrome happily binds
to its supported max (v4). The `WaylandZwpLinuxDmabuf::OnTrancheFlags`
NOTIMPLEMENTED is also benign — KWin sends it, chrome ignores it, but
the substantive feedback (formats + modifiers) lands via
`OnTrancheFormats` / `OnTrancheTargetDevice` regardless.
Real cause: `gpu_feature_info.supports_nv12_gl_native_pixmap` ends up
**false** on this build. With it false, V4L2-decoded NV12 frames go
through the NV12-to-AR24 VPP conversion path (see
`media/mojo/services/gpu_mojo_media_client_linux.cc`
`GetPreferredRenderableFourccs` — without NV12 native pixmap support,
only `Fourcc::AR24` is added to the renderable set, forcing the VPP).
That's where the 85 % is spent.
Why is `supports_nv12_gl_native_pixmap` false?
`GLOzoneEGLWayland::CanImportNativePixmap` (in
`ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc`) requires the
chrome GL display to expose `EGL_EXT_image_dma_buf_import`. With
`--use-gl=angle --use-angle=gles`, chrome's GL display sits behind
ANGLE's EGL, and ANGLE's GLES backend on Linux does not propagate
`EGL_EXT_image_dma_buf_import` from the underlying mesa EGL up to its
clients. Verified directly: `EGL_PLATFORM=surfaceless eglinfo` on ohm
shows panfrost native EGL exposes both
`EGL_EXT_image_dma_buf_import` and `EGL_EXT_image_dma_buf_import_modifiers`
the capability is there at the panfrost layer, ANGLE just hides it.
We tried `--use-gl=egl` (direct EGL/GLES2, bypass ANGLE) but were
rejected with "Requested GL implementation (gl=egl-gles2,angle=none) not
found in allowed implementations". `WaylandSurfaceFactory::GetAllowedGLImplementations()`
in chromium 149 advertises only ANGLE-mediated impls; the
`kGLImplementationEGLGLES2` slot is missing from the list. The
`CreateViewGLSurface` dispatcher does still handle that impl — only the
*advertisement* was tightened.
### Patch 2/2 — `wayland-allow-direct-egl-gles2.patch`
3-line diff in `ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc`:
```
+ impls.emplace_back(gl::GLImplementationParts(gl::kGLImplementationEGLGLES2));
impls.emplace_back(gl::ANGLEImplementation::kOpenGL);
```
Re-allows the direct EGL/GLES2 path, ahead of the ANGLE entries so
chrome picks it by default. Verified to apply cleanly on the CT 220
tree; staged via `patch -p1` mid-rebuild (ninja's mtime-based rebuild
will pick up the change automatically).
### Outstanding for next pass (revised)
1. Rebuild lands → repackage with all four GL libs +
`chrome_crashpad_handler` + chrome → ship to ohm.
2. Validate via `chrome --use-gl=egl --ozone-platform=wayland`
`--enable-features=AcceleratedVideoDecoder` (no ANGLE shim) and
confirm `chrome://gpu` reports `Native GpuMemoryBuffers: true` and
`supports_nv12_gl_native_pixmap=true`. Target CPU during 1080p30 H.264
playback: under 30 % combined renderer + gpu.
3. If (2) passes, declare V1 of chromium-fourier shippable on ohm.
4. Add a `chromium-fourier` launcher shim under `/usr/bin/` that
defaults to `--use-gl=egl --ozone-platform=wayland`.
5. Sort the chromium 147 vs 149 confusion — the fetch went to ToT on
main rather than the 147 release branch. Either pin the branch or
accept that we're tracking ToT (probably preferable for V4L2 fixes
that are still in flight upstream).
6. Replicate end-to-end on RK3588 (ampere CoolPi or boltzmann Rock 5
ITX+) once the mainline VDPU381 driver is stable on those — those
boxes use **panthor** for Mali-G610 (Valhall), not panfrost; the
patches should be backend-agnostic but the validation is per-box.
### State of the build host (post pass 3)
- CT 220 `/build/chromium/src` patched with both
`enable-v4l2-decoder-default.patch` and
`wayland-allow-direct-egl-gles2.patch` (applied directly with
`patch -p1` mid-rebuild; ninja picks up the mtime change).
- `chromium-rebuild.service` running as a transient unit, output in
`/tmp/chromium-rebuild.log`. Most of the 93k ninja steps are cache
hits; only the patched files + their downstream objects need
recompiling.
- Tarball still on CT 220 at `/build/chromium-fourier-147.tar.gz`
(misleadingly named: it's actually 149.0.7812.0 from the main fetch,
not the 147 release tarball — separate cleanup for next pass) and on
hertz at `/tmp/chromium-fourier-147.tar.gz`. **Will be replaced by
the post-rebuild tarball once it lands.**
---
## 2026-04-28 — Patch 4 lands, KWin owns the residual stall
### Patch 4 — `nv12-external-oes-on-modifier-external-only.patch`
On panfrost / panthor, every NV12 modifier (LINEAR + AFBC ×2 + AFRC) is
flagged `external_only` in `eglQueryDmaBufModifiersEXT`. Chromium's
`OzoneImageGLTexturesHolder::GetBinding` only picked
`GL_TEXTURE_EXTERNAL_OES` when the SharedImageFormat carried
`PrefersExternalSampler` — which is set for the generic Linux multi-plane
case but **not** for V4L2 producers that arrive via the standard ozone
pixmap path. The frame then took the `GL_TEXTURE_2D` branch, ANGLE's
`validationES.cpp:4894` rejected the YUV EGLImage on a non-EXTERNAL_OES
target, the import failed, and the renderer fell back to the
NV12→AR24 software conversion (~131 % CPU baseline).
Patch closes the gap: also pick `EXTERNAL_OES` when the EGL driver
advertises the pixmap's modifier as `external_only` (cached per
`(fourcc, modifier)` tuple via a function-local
`base::flat_map`+`base::Lock`, so the EGL round-trip stays off the
per-frame hot path). Adds a single static helper
`NativePixmapEGLBinding::ModifierRequiresExternalOES`. ~+90 lines, zero
deletions, no shader changes (Skia Ganesh already handles
`GL_TEXTURE_EXTERNAL_OES` natively via `GrGLTextureInfo.fTarget`).
### Validation on ohm (RK3566 PineTab2 / hantro mainline 6.19.10)
- `bbb_1080p30_h264.mp4` plays clean, no garble, no decoder error
- Steady-state **34.7 % combined CPU** during 1080p30 H.264 (browser 12 +
GPU 9 + net 6 + render 6 + audio 1) vs v3 baseline ~131 % — **~3.8×
reduction**. Risk-1 (ANGLE+EXTERNAL_OES sampling regression on Skia
Ganesh / panfrost) **cleared**.
- `V4L2VideoDecoder()` constructor + `Using a stateless API for profile:
h264 main and fourcc: S264` confirmed in log; 6 CAPTURE buffers
V4L2_MEMORY_MMAP, NV12 output. 19 live dmabuf fds in GPU process
during steady playback — healthy V4L2 rotation + compositor depth, not
a leak.
### KWin 6.6.4 GL_ALPHA bug — separate, preexisting, blocks long playback
Across BOTH v3 (no patch 4) and v4 (with patch 4) chromium-fourier
builds, mid-playback the renderer + GPU process both park in
`futex_do_wait`, `<video>` element keeps its ⏸ icon, currentTime
advances on the audio clock, and audio outputs static (last ALSA buffer
recycled) then silence. No D-state, no v4l2/vb2/dma_fence wchan, no
error in `chrome-v[34].log`.
`journalctl` for `kwin_wayland`:
```
GL_INVALID_VALUE in glTexImage2D(internalFormat=GL_ALPHA)
GL_INVALID_OPERATION in glTexSubImage2D(invalid texture level 0) × N
```
First occurrence on this box: **2026-03-06**. KWin is requesting an
internal format that doesn't exist in modern GLES (`GL_ALPHA` is GLES1.x
legacy, not valid for `glTexImage2D` with GLES3 contexts). The
allocation fails, then every `glTexSubImage2D` to that texture errors at
level 0; KWin keeps retrying the same broken upload every frame, never
recovers. The frame-callback ack to wayland clients stalls → chrome's
renderer parks waiting for the present-feedback that never lands.
Patch 4 looks "guilty" only because of timing: with NV12 zero-copy, the
renderer is fast enough to actually post a v4l2-backed dmabuf within the
window where KWin's broken path runs; v3 was slow enough (NV12→AR24
software conversion) that the bug surfaced 510× later. **Triangulation:**
chrome v4 stalls + chrome v3 stalls + VLC `cannot convert decoder/filter
output` + mpv `could not initialize video chain` — every wayland video
client hits it; ffmpeg `-hwaccel v4l2request -f null` plays through
clean (decode path is healthy, the wall is the compositor's GL backend).
### Decoder-stack sanity post-reboot (2026-04-28 ~13:30)
After a reboot the V4L2 device numbering shuffled:
- `/dev/video0` = `rockchip,rk3568-vpu-dec` (hantro DEC, was video1)
- `/dev/video1` = `rockchip,rk3568-vepu-enc` (hantro ENC)
- `/dev/video2` = `rockchip-rga`
- `/dev/media0` = controller for DEC, `/dev/media1` = controller for ENC
Anything that hardcoded `/dev/video1` for decode now talks to the
encoder. Chrome and ffmpeg both handle this transparently (they enumerate
via media-ctl); mpv's `--hwdec=v4l2request` returns `Could not create
device` post-shuffle — separate mpv-side bug, not ours.
### Outstanding (revised, supersedes earlier list)
1. **Patch 4 lands publicly:** bump PKGBUILD `source=` and `prepare()`,
commit + tag a `chromium-fourier-149-r4` release on
`git@github.com:marfrit/chromium-fourier`.
2. **KWin pivot** — see `KWIN_PIVOT.md` (separate doc) for the plan to
identify and patch the `glTexImage2D(GL_ALPHA)` site, since ohm is the
only board on hand and every wayland video client is affected.
3. **Replication on ampere** (RK3588, panthor + rkvdec2 + hantro
multiplanar) — needs ampere woken; currently DOWN.
4. **firefox-fourier 150 build** — `firefox-fourier-150.0.1-1-aarch64.pkg.tar.zst`
is built (95 MB on workstation:/tmp/, sha256 acbf1870…), pending
fresnel power-on for V4L2 stateless validation on RK3399.
+137 -38
View File
@@ -4,19 +4,20 @@
# (RK3566 hantro / RK3588 VDPU381) on **mainline** kernel + Wayland +
# panfrost / panthor — no vendor MPP, no Mali blob, no panfork, no
# 5.10 BSP kernel. Fills the niche that 7Ji's chromium-mpp explicitly
# does not (it forces BSP + X11 + vendor stack); see
# /home/mfritsche/src/marfrit-packages/arch/chromium-fourier/STUDY.md
# for the full rationale.
# does not (it forces BSP + X11 + vendor stack); see STUDY.md and
# NEXT.md alongside this PKGBUILD for the full rationale and the
# validation log on PineTab2 (RK3566).
#
# Build host: chromium-builder LXD container on boltzmann (8 cores,
# 28 GB RAM cap, 824 GB NVMe). 6-10 h initial build.
# Multi-arch: builds natively on x86_64 and aarch64. The x86_64 path
# is primarily a development / CI host; the runtime target audience is
# aarch64. The two patches are architecture-independent.
pkgname=chromium-fourier
pkgver=147.0.7727.116
pkgrel=1
pkgrel=2
epoch=1
pkgdesc='Chromium with V4L2VDA HW video decode unlocked for mainline Linux Wayland on Rockchip'
arch=('aarch64')
arch=('aarch64' 'x86_64')
url='https://www.chromium.org/Home'
license=('BSD-3-Clause')
depends=(
@@ -68,45 +69,84 @@ optdepends=(
provides=(chromium)
conflicts=(chromium)
options=('!lto' '!strip')
# NB: the chromium tarball is fetched lazily by build.sh on the build
# host (5.7 GB compressed, doesn't belong in this git repo). The
# PKGBUILD assumes /build/chromium/src is already populated.
# Canonical chromium release tarball (5.7 GB compressed). Versions track
# the chromium release schedule (see https://chromiumdash.appspot.com).
# When bumping pkgver the patches may need their hunk line numbers
# refreshed against the new tree — they are written against
# media/base/media_switches.cc and ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc
# which both move around between minor releases.
source=(
'patches/chromeos-pipeline-bypass.patch'
"https://commondatastorage.googleapis.com/chromium-browser-official/chromium-${pkgver}.tar.xz"
'patches/enable-v4l2-decoder-default.patch'
'patches/wayland-allow-direct-egl-gles2.patch'
'patches/nv12-external-oes-on-modifier-external-only.patch'
)
sha256sums=(
'SKIP'
'SKIP'
'SKIP'
'SKIP'
)
# NB: this PKGBUILD is currently development-shaped — it operates on a
# pre-extracted chromium tree at /build/chromium/src rather than letting
# makepkg fetch + unpack the 5.7 GB tarball into ${srcdir}. Once the
# patches stabilise we'll switch to the canonical pattern with
# source=(${url}/chromium-${pkgver}.tar.xz ...) so the standard makepkg
# flow + CI fermi-style pipeline work end-to-end.
prepare() {
cd /build/chromium/src
cd "${srcdir}/chromium-${pkgver}"
# Fourier-local: bypass the chromeos pipeline so VaapiVideoDecoder /
# V4L2VDA is reachable on Linux non-ChromeOS. The 7Ji-style gn args
# (use_v4l2_codec / use_v4lplugin / use_linux_v4l2_only) may be
# sufficient on their own; this patch is the fallback if they aren't.
patch -Np1 -i "${srcdir}/patches/chromeos-pipeline-bypass.patch" || true
# Fourier patch 1/2: flip kAcceleratedVideoDecodeLinux's default to
# enabled when USE_V4L2_CODEC is the build's HW decode path. Without
# this the runtime master gate stays off on USE_V4L2_CODEC-only builds
# and chrome silently falls back to ffmpeg software decode. See the
# patch header for the validation log on RK3566 hantro.
patch -Np1 -i "${srcdir}/patches/enable-v4l2-decoder-default.patch"
# Fourier patch 2/3: re-allow the direct EGL/GLES2 path in the Wayland
# ozone surface factory so panfrost's EGL_EXT_image_dma_buf_import
# surfaces to chrome's GL display, lighting up the NV12 zero-copy
# native-pixmap pipeline. The launcher defaults to ANGLE (DCHECK in
# gl_context_egl.cc:241 fires on direct EGL with non-official builds);
# this patch keeps the direct path available for users who flip
# is_official_build=true and want the lower-CPU pipeline.
patch -Np1 -i "${srcdir}/patches/wayland-allow-direct-egl-gles2.patch"
# Fourier patch 3/3: pick GL_TEXTURE_EXTERNAL_OES for NV12 dmabufs
# whose DRM modifier is advertised external_only by the EGL driver.
# On panfrost / panthor every NV12 modifier (LINEAR + AFBC + AFRC) is
# external_only; chromium's default OzoneImageGLTexturesHolder picked
# GL_TEXTURE_2D and ANGLE then rejected the YUV EGLImage on a
# non-EXTERNAL_OES target, forcing the NV12->AR24 software conversion
# fallback. This closes that gap and enables the actual zero-copy
# path. Validated on ohm (RK3566 hantro): 1080p30 H.264 drops from
# ~131% combined CPU to ~34.7% (~3.8x). See patches/0004 for context.
patch -Np1 -i "${srcdir}/patches/nv12-external-oes-on-modifier-external-only.patch"
# Use system node, system java
rm -f third_party/node/linux/node-linux-arm64/bin/node
mkdir -p third_party/node/linux/node-linux-arm64/bin
ln -sf /usr/bin/node third_party/node/linux/node-linux-arm64/bin/
case "$CARCH" in
aarch64) _node_dir=node-linux-arm64 ;;
x86_64) _node_dir=node-linux-x64 ;;
esac
rm -f "third_party/node/linux/${_node_dir}/bin/node"
mkdir -p "third_party/node/linux/${_node_dir}/bin"
ln -sf /usr/bin/node "third_party/node/linux/${_node_dir}/bin/"
ln -sf /usr/bin/java third_party/jdk/current/bin/ 2>/dev/null || true
}
build() {
cd /build/chromium/src
cd "${srcdir}/chromium-${pkgver}"
case "$CARCH" in
aarch64) _target_cpu="arm64" ;;
x86_64) _target_cpu="x64" ;;
esac
local _flags=(
'is_official_build=true'
"target_cpu=\"${_target_cpu}\""
'is_official_build=false'
'is_debug=false'
# dcheck_always_on defaults to !is_official_build (true here);
# explicitly off so the direct EGL/GLES2 path doesn't FATAL on
# gl_context_egl.cc:241's DCHECK(!global_texture_share_group_).
'dcheck_always_on=false'
'symbol_level=0'
'is_cfi=false'
'treat_warnings_as_errors=false'
@@ -123,7 +163,7 @@ build() {
'use_v4l2_codec=true'
'use_v4lplugin=true'
'use_linux_v4l2_only=true'
'use_vaapi=true'
'use_vaapi=false'
# Codec branding for proprietary codec support (H.264 etc.)
'ffmpeg_branding="Chrome"'
@@ -136,20 +176,38 @@ build() {
)
gn gen out/Default --args="${_flags[*]}"
autoninja -C out/Default chrome chromedriver chrome_sandbox
ninja -C out/Default chrome chrome_crashpad_handler
}
package() {
cd /build/chromium/src
cd "${srcdir}/chromium-${pkgver}"
install -Dm755 out/Default/chrome "${pkgdir}/usr/lib/chromium/chromium"
install -Dm4755 out/Default/chrome_sandbox "${pkgdir}/usr/lib/chromium/chrome-sandbox"
install -Dm755 out/Default/chromedriver "${pkgdir}/usr/bin/chromedriver"
install -Dm755 out/Default/chrome_crashpad_handler \
"${pkgdir}/usr/lib/chromium/chrome_crashpad_handler"
[ -f out/Default/chrome_sandbox ] && install -Dm4755 out/Default/chrome_sandbox \
"${pkgdir}/usr/lib/chromium/chrome-sandbox"
[ -f out/Default/chromedriver ] && install -Dm755 out/Default/chromedriver \
"${pkgdir}/usr/bin/chromedriver"
# Bundled GL/Vulkan runtime — chrome dlopens these from its own dir,
# not /usr/lib/. Without them GL init fails and chrome falls back to
# software compositing.
for so in libEGL.so libGLESv2.so libvk_swiftshader.so libvulkan.so.1; do
[ -f "out/Default/$so" ] && install -Dm755 "out/Default/$so" \
"${pkgdir}/usr/lib/chromium/$so"
done
# ANGLE and SwiftShader ICD config files
for icd in out/Default/*_icd.json; do
[ -f "$icd" ] && install -Dm644 "$icd" \
"${pkgdir}/usr/lib/chromium/$(basename "$icd")"
done
# Resources / locales / pak files
for f in chrome_100_percent.pak chrome_200_percent.pak resources.pak \
v8_context_snapshot.bin icudtl.dat headless_lib_data.pak \
headless_lib_strings.pak; do
v8_context_snapshot.bin snapshot_blob.bin icudtl.dat \
headless_lib_data.pak headless_lib_strings.pak \
headless_command_resources.pak; do
[ -f "out/Default/$f" ] && install -Dm644 "out/Default/$f" \
"${pkgdir}/usr/lib/chromium/$f"
done
@@ -160,7 +218,48 @@ package() {
cp -r out/Default/locales/* "${pkgdir}/usr/lib/chromium/locales/"
fi
# Top-level launcher
# Launcher shim — defaults to ANGLE→GLES on Wayland with Vulkan
# disabled. Vulkan is off by default because:
# - panvk on RK3566 (Mali-G52 Bifrost) returns
# VK_ERROR_INCOMPATIBLE_DRIVER on chromium's probe and breaks
# V4L2 dispatch downstream (chrome falls back to FFmpeg software);
# - panthor on RK3588 (Mali-G610 Valhall) is more functional but
# not yet validated end-to-end against this build.
#
# User overrides for development on other Rockchips:
# --enable-features=Vulkan enable Vulkan (panthor / others)
# --use-vulkan=native|swiftshader pick the Vulkan backend
# --disable-features=Vulkan explicit re-disable
# Any of those on the command line short-circuits the launcher's
# default disable, so the user's intent always wins.
install -dm755 "${pkgdir}/usr/bin"
ln -s /usr/lib/chromium/chromium "${pkgdir}/usr/bin/chromium"
cat > "${pkgdir}/usr/bin/chromium" <<'LAUNCHER'
#!/bin/bash
# chromium-fourier launcher — V4L2 HW decode + Wayland + ANGLE
# Vulkan disabled by default; pass --enable-features=Vulkan or
# --use-vulkan=native to opt in (e.g. RK3588 panthor work).
USER_HANDLES_VULKAN=0
for arg in "$@"; do
case "$arg" in
--use-vulkan*|--enable-features=*Vulkan*|--disable-features=*Vulkan*|--use-angle=vulkan*)
USER_HANDLES_VULKAN=1
break
;;
esac
done
vulkan_default=()
if [ "$USER_HANDLES_VULKAN" = 0 ]; then
vulkan_default=(--disable-features=Vulkan)
fi
exec /usr/lib/chromium/chromium \
--ozone-platform=wayland \
--use-gl=angle --use-angle=gles \
--enable-features=AcceleratedVideoDecoder \
"${vulkan_default[@]}" \
"$@"
LAUNCHER
chmod 0755 "${pkgdir}/usr/bin/chromium"
}
@@ -1,22 +0,0 @@
From: Markus Fritsche <mfritsche@reauktion.de>
Subject: media/gpu: skip chromeos VideoDecoderPipeline on non-ChromeOS Linux
Placeholder. The patch will be developed against the actual chromium
147.0.7727.116 source tree on chromium-builder@boltzmann once the
tarball is extracted and we can read the exact code paths in:
- media/gpu/chromeos/video_decoder_pipeline.cc (PickDecoderOutputFormat,
Initialize, ImageProcessor setup)
- media/gpu/vaapi/vaapi_video_decoder.cc (ApplyResolutionChangeWithScreenSizes,
line ~1219 where "failed Initialize()ing the frame pool" fires)
The 7Ji-style gn args (`use_v4l2_codec=true use_v4lplugin=true
use_linux_v4l2_only=true`) MAY be sufficient by themselves to route
decode through the legacy V4L2VDA path entirely, bypassing the
chromeos pipeline without source patches. First build will tell us;
this file is the placeholder for the patch we'll need if it isn't.
# Empty no-op patch. patch -p1 < this won't change anything.
diff --git a/PLACEHOLDER b/PLACEHOLDER
new file mode 100644
index 0000000..e69de29
@@ -0,0 +1,38 @@
From: 7Ji <7Ji@example.com> (originally), adapted for chromium-fourier
Subject: Adjust compiler-rt library path layout for system clang on Arch
Linux ARM, where compiler-rt installs to lib/clang/N/lib/linux/ with
-aarch64 filename suffix instead of chromium's expected
lib/clang/N/lib/aarch64-unknown-linux-gnu/ layout.
diff --git a/build/config/clang/BUILD.gn b/build/config/clang/BUILD.gn
index d4de2e0cca0..57359c32121 100644
--- a/build/config/clang/BUILD.gn
+++ b/build/config/clang/BUILD.gn
@@ -130,12 +130,15 @@ template("clang_lib") {
} else if (is_linux || is_chromeos) {
if (current_cpu == "x64") {
_dir = "x86_64-unknown-linux-gnu"
+ _suffix = "-x86_64"
} else if (current_cpu == "x86") {
_dir = "i386-unknown-linux-gnu"
+ _suffix = "-i386"
} else if (current_cpu == "arm") {
_dir = "armv7-unknown-linux-gnueabihf"
} else if (current_cpu == "arm64") {
_dir = "aarch64-unknown-linux-gnu"
+ _suffix = "-aarch64"
} else {
assert(false) # Unhandled cpu type
}
@@ -166,6 +169,11 @@ template("clang_lib") {
assert(false) # Unhandled target platform
}
+ # Bit of a hack to make this find builtins from compiler-rt >= 16
+ if (is_linux || is_chromeos) {
+ _dir = "linux"
+ }
+
_clang_lib_dir = "$clang_base_path/lib/clang/$clang_version/lib"
_lib_file = "${_prefix}clang_rt.${_libname}${_suffix}.${_ext}"
libs = [ "$_clang_lib_dir/$_dir/$_lib_file" ]
@@ -0,0 +1,55 @@
From: Markus Fritsche <mfritsche@reauktion.de>
Subject: media: default kAcceleratedVideoDecodeLinux to enabled when
USE_V4L2_CODEC is the build's hardware decode path
Date: 2026-04-26
Background
----------
chromium-fourier targets mainline-Linux Wayland on Rockchip (RK3566 hantro,
RK3588 VDPU381) where the only HW video decode path is V4L2 stateless
(via the in-tree media/gpu/v4l2 stack). The build is configured with
use_vaapi = false
use_v4l2_codec = true
use_v4lplugin = true
use_linux_v4l2_only = true
Without this patch, GPU-process V4L2 decode is compiled in but stays
runtime-disabled by default. The runtime master gate
`media::kAcceleratedVideoDecodeLinux` (the user-visible feature name is
"AcceleratedVideoDecoder") is currently flipped to ENABLED_BY_DEFAULT only
when `BUILDFLAG(USE_VAAPI)` is set. On a USE_V4L2_CODEC-only build the
feature stays DISABLED_BY_DEFAULT, the linux gpu_mojo_media_client returns
`VideoDecoderType::kUnknown`, and `<video>` falls all the way back to
`media/filters/ffmpeg_video_decoder.cc` (software).
We confirmed this by hand on the PineTab2 (RK3566 hantro): with
`--enable-features=AcceleratedVideoDecoder` chrome correctly selects
`V4L2VideoDecoder` for h264 main, opens /dev/video1 + /dev/media0,
allocates 17 OUTPUT + 6 CAPTURE NV12 buffers, and runs SetExtCtrlsInit for
H264. Without the runtime flag, none of that happens.
Fix
---
Treat `USE_V4L2_CODEC` symmetrically with `USE_VAAPI` for the runtime
default of the master gate. A user can still disable it via
`--disable-features=AcceleratedVideoDecoder`.
This does NOT touch the `kAcceleratedVideoDecodeLinuxGL` companion gate
(already ENABLED_BY_DEFAULT) or any of the per-decoder selection logic in
`media/mojo/services/gpu_mojo_media_client_linux.cc` -- that file already
dispatches to the V4L2 decoder when `USE_V4L2_CODEC && !USE_VAAPI`, gated
behind the master flag we are flipping here.
diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc
--- a/media/base/media_switches.cc
+++ b/media/base/media_switches.cc
@@ -749,7 +749,7 @@ BASE_FEATURE(kUnifiedAutoplay, base::FEATURE_ENABLED_BY_DEFAULT);
// on chromeos, but needs an experiment on linux.
BASE_FEATURE(kAcceleratedVideoDecodeLinux,
"AcceleratedVideoDecoder",
-#if BUILDFLAG(USE_VAAPI)
+#if BUILDFLAG(USE_VAAPI) || BUILDFLAG(USE_V4L2_CODEC)
base::FEATURE_ENABLED_BY_DEFAULT);
#else
base::FEATURE_DISABLED_BY_DEFAULT);
@@ -0,0 +1,187 @@
From: Markus Fritsche <mfritsche@reauktion.de>
Subject: [PATCH] gpu/ozone: pick GL_TEXTURE_EXTERNAL_OES for NV12 dmabufs whose
DRM modifier is advertised external_only by the EGL driver
Date: 2026-04-28
Background
----------
On mainline-Linux Mali GPUs (mesa panfrost / panthor on Bifrost / Valhall)
every NV12 modifier exposed by `eglQueryDmaBufModifiersEXT` is flagged
`external_only` — DRM_FORMAT_MOD_LINEAR + ARM AFBC × 2 + ARM AFRC. Mesa's
behavior is spec-correct: GLES sampling of multi-plane formats is
defined only via `samplerExternalOES`, never `sampler2D`. The chromium
NV12 import path at
`gpu/command_buffer/service/shared_image/ozone_image_gl_textures_holder.cc`
already chooses `GL_TEXTURE_EXTERNAL_OES` when the SharedImageFormat is
flagged `PrefersExternalSampler` — but that flag is only set for the
generic "multi-plane on Linux" case in
`media/gpu/chromeos/mailbox_video_frame_converter.cc`. Frames that
arrive with an `external_only`-flagged modifier from a producer that
didn't set the flag (V4L2 hantro NV12 with AFBC/AFRC capture format on
RK3588's rkvdec2, future NativePixmap producers, etc.) hit the
`GL_TEXTURE_2D` path; ANGLE's `validationES.cpp:4894` then rejects YUV
EGLImages on non-EXTERNAL_OES targets, and the import fails.
This patch closes the gap: the texture-target choice in
`OzoneImageGLTexturesHolder::GetBinding` now consults the EGL driver's
`external_only` annotation for the pixmap's actual modifier in addition
to `format.PrefersExternalSampler()`. If either says "external sampler
required", the target switches to `GL_TEXTURE_EXTERNAL_OES`. Skia
Ganesh handles `GL_TEXTURE_EXTERNAL_OES` natively via
`GrGLTextureInfo.fTarget`, so no shader changes are required. Same
infrastructure chromium already uses for Android camera / decoder
dmabufs, retargeted at the Linux ozone layer.
Result is cached per `(fourcc, modifier)` tuple via a function-local
static `base::flat_map`, so the EGL query is not on the per-frame hot
path — once per unique format+modifier combination, after which the
runtime cost is a hash lookup behind a base::Lock.
Bug crbug.com/1498703 is the closest existing tracker; framing this
upstream as "make Linux NV12 import path consistent with the
ChromeOS PrefersExternalSampler default" is the right angle.
diff --git a/gpu/command_buffer/service/shared_image/ozone_image_gl_textures_holder.cc b/gpu/command_buffer/service/shared_image/ozone_image_gl_textures_holder.cc
index 525bdcb0dc..43b0723326 100644
--- a/gpu/command_buffer/service/shared_image/ozone_image_gl_textures_holder.cc
+++ b/gpu/command_buffer/service/shared_image/ozone_image_gl_textures_holder.cc
@@ -16,6 +16,7 @@
#include "ui/gl/gl_bindings.h"
#include "ui/gl/scoped_binders.h"
#include "ui/ozone/public/gl_ozone.h"
+#include "ui/ozone/common/native_pixmap_egl_binding.h"
#include "ui/ozone/public/native_pixmap_gl_binding.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/ozone/public/surface_factory_ozone.h"
@@ -82,7 +83,14 @@ std::unique_ptr<ui::NativePixmapGLBinding> GetBinding(
// being multiplanar (if using per-plane sampling of a multiplanar texture,
// the buffer format passed in here must be the single-planar format of the
// plane).
- if (format.PrefersExternalSampler()) {
+ // chromium-fourier: also pick GL_TEXTURE_EXTERNAL_OES whenever the
+ // pixmap's DRM modifier is advertised external_only by the EGL
+ // driver. Mesa panfrost / panthor mark every NV12 modifier
+ // external_only — the PrefersExternalSampler flag alone misses
+ // the AFBC / AFRC tiled paths.
+ if (format.PrefersExternalSampler() ||
+ ui::NativePixmapEGLBinding::ModifierRequiresExternalOES(
+ pixmap.get(), plane_format)) {
target = GL_TEXTURE_EXTERNAL_OES;
} else {
target = GL_TEXTURE_2D;
diff --git a/ui/ozone/common/native_pixmap_egl_binding.cc b/ui/ozone/common/native_pixmap_egl_binding.cc
index 31877f4459..6855c1093e 100644
--- a/ui/ozone/common/native_pixmap_egl_binding.cc
+++ b/ui/ozone/common/native_pixmap_egl_binding.cc
@@ -6,10 +6,13 @@
#include <array>
+#include "base/containers/flat_map.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
+#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
+#include "base/synchronization/lock.h"
#include "ui/gfx/linux/drm_util_linux.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_surface_egl.h"
@@ -56,6 +59,75 @@ bool NativePixmapEGLBinding::IsSharedImageFormatSupported(
viz::SharedImageFormat format) {
return GetFourCCFormatFromSharedImageFormat(format) != DRM_FORMAT_INVALID;
}
+// static
+bool NativePixmapEGLBinding::ModifierRequiresExternalOES(
+ const gfx::NativePixmap* pixmap,
+ viz::SharedImageFormat format) {
+ // chromium-fourier: query the EGL driver for the (fourcc, modifier)
+ // tuple's external_only flag. Cache results — eglQueryDmaBufModifiersEXT
+ // is a synchronous round-trip into the driver and we want it off the
+ // per-frame hot path. The cache lives for the lifetime of the GPU
+ // process (modifier tables don't change after EGL init).
+ if (!pixmap) {
+ return false;
+ }
+ const uint64_t modifier = pixmap->GetFormatModifier();
+ if (modifier == gfx::NativePixmapHandle::kNoModifier) {
+ // Implicit linear — same answer the driver would give for the
+ // matching LINEAR entry, but cheaper not to query.
+ return false;
+ }
+ const uint32_t fourcc = GetFourCCFormatFromSharedImageFormat(format);
+ if (fourcc == DRM_FORMAT_INVALID) {
+ return false;
+ }
+
+ using Key = std::pair<uint32_t, uint64_t>;
+ static base::NoDestructor<base::Lock> cache_lock;
+ static base::NoDestructor<base::flat_map<Key, bool>> cache;
+ {
+ base::AutoLock lock(*cache_lock);
+ auto it = cache->find({fourcc, modifier});
+ if (it != cache->end()) {
+ return it->second;
+ }
+ }
+
+ bool external_only = false;
+ do {
+ auto* display = gl::GLSurfaceEGL::GetGLDisplayEGL();
+ if (!display || !display->ext->b_EGL_EXT_image_dma_buf_import_modifiers) {
+ break;
+ }
+ EGLDisplay egl_display = display->GetDisplay();
+ EGLint num_modifiers = 0;
+ if (!eglQueryDmaBufModifiersEXT(egl_display, fourcc, 0, nullptr, nullptr,
+ &num_modifiers) ||
+ num_modifiers <= 0) {
+ break;
+ }
+ std::vector<EGLuint64KHR> modifiers(num_modifiers);
+ std::vector<EGLBoolean> ext_only(num_modifiers);
+ if (!eglQueryDmaBufModifiersEXT(egl_display, fourcc, num_modifiers,
+ modifiers.data(), ext_only.data(),
+ &num_modifiers)) {
+ break;
+ }
+ for (EGLint i = 0; i < num_modifiers; ++i) {
+ if (modifiers[i] == modifier) {
+ external_only = (ext_only[i] == EGL_TRUE);
+ break;
+ }
+ }
+ } while (0);
+
+ {
+ base::AutoLock lock(*cache_lock);
+ cache->insert_or_assign({fourcc, modifier}, external_only);
+ }
+ return external_only;
+}
+
// static
std::unique_ptr<NativePixmapGLBinding> NativePixmapEGLBinding::Create(
diff --git a/ui/ozone/common/native_pixmap_egl_binding.h b/ui/ozone/common/native_pixmap_egl_binding.h
index 61fb0de77f..ad3ac9ced5 100644
--- a/ui/ozone/common/native_pixmap_egl_binding.h
+++ b/ui/ozone/common/native_pixmap_egl_binding.h
@@ -27,6 +27,17 @@ class NativePixmapEGLBinding : public NativePixmapGLBinding {
static bool IsSharedImageFormatSupported(viz::SharedImageFormat format);
+ // chromium-fourier: returns true when |pixmap|'s DRM format modifier
+ // is advertised by the EGL driver as `external_only` for the given
+ // SharedImage format. Used at SharedImage creation time to override
+ // the default GL_TEXTURE_2D target to GL_TEXTURE_EXTERNAL_OES so that
+ // mesa panfrost / panthor NV12 dmabufs (always external_only) import
+ // cleanly via glEGLImageTargetTexture2DOES + samplerExternalOES.
+ // Result is cached per (fourcc, modifier) tuple — the underlying
+ // eglQueryDmaBufModifiersEXT call is not on the per-frame hot path.
+ static bool ModifierRequiresExternalOES(const gfx::NativePixmap* pixmap,
+ viz::SharedImageFormat format);
+
// Create an EGLImage from a given NativePixmap and plane and bind
// |texture_id| to |target| followed by binding the image to |target|. The
// color space is for the external sampler: When we sample the YUV buffer as
@@ -0,0 +1,57 @@
From: Markus Fritsche <mfritsche@reauktion.de>
Subject: ozone/wayland: re-allow direct EGL/GLES2 path (no ANGLE shim)
Date: 2026-04-26
Background
----------
On Wayland-only ozone builds the surface factory currently advertises only
ANGLE-mediated GL implementations (`kOpenGL`, `kOpenGLES`, `kSwiftShader`,
`kVulkan`). Anything driving `--use-gl=egl` (the historical
`kGLImplementationEGLGLES2`) is rejected at startup with
Requested GL implementation (gl=egl-gles2,angle=none) not found in
allowed implementations: [(gl=egl-angle,angle=opengl|opengles|...)]
The downstream switch already handles `kGLImplementationEGLGLES2` in
`GetGLOzone`, so the dispatcher is wired -- it's the *advertisement* that
got tightened.
The cost of that tightening on RK3566 hantro / panfrost is real: with
ANGLE in the path, chrome's display-side EGL is ANGLE's own EGL. ANGLE's
GLES backend on Linux does not propagate
`EGL_EXT_image_dma_buf_import` through to the chrome GL display, so
`gpu_feature_info.supports_nv12_gl_native_pixmap` ends up false. That in
turn forces the V4L2-decoded NV12 frames through the
NV12-to-AR24 VPP conversion path before they hit the compositor, costing
~85 % CPU at 1080p30 even though the hantro VPU is doing the actual
decode for free.
Allowing the direct EGL/GLES2 path back means chrome's EGL is panfrost's
EGL (via mesa), which exposes the dmabuf-import extensions natively, and
the zero-copy NV12 native pixmap path lights up.
Fix
---
Add `kGLImplementationEGLGLES2` to the head of the allowed list; ANGLE
remains the default fallback and is still selected when the user passes
`--use-gl=angle ...`. The position is deliberate: on a USE_V4L2_CODEC
hardware-decode build the user almost always wants the dmabuf-capable
direct path; ANGLE is still there for browsers that need its conformance
fixups.
This does not affect non-Wayland ozone backends.
diff --git a/ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc b/ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc
--- a/ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc
+++ b/ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc
@@ -223,6 +223,10 @@ std::vector<gl::GLImplementationParts>
WaylandSurfaceFactory::GetAllowedGLImplementations() {
std::vector<gl::GLImplementationParts> impls;
if (egl_implementation_) {
+ // chromium-fourier: keep the direct EGL/GLES2 path available so
+ // panfrost's EGL_EXT_image_dma_buf_import surfaces to chrome's GL
+ // display layer. See patch header for rationale.
+ impls.emplace_back(gl::GLImplementationParts(gl::kGLImplementationEGLGLES2));
impls.emplace_back(gl::ANGLEImplementation::kOpenGL);
impls.emplace_back(gl::ANGLEImplementation::kOpenGLES);
impls.emplace_back(gl::ANGLEImplementation::kSwiftShader);