Skip to content

KWin high-CPU loop after Anland consumer resize/reconnect when dmabuf import fails #77

Description

@zheksoon

I'm playing with Anland on Debian 13 under Droidspaces (rootfs image from Gold) and found constant very high CPU usage of KWin process even without active apps/effects. I debugged it with Codex, and it generated the following issue text. I know it might be not very ethical to post pure AI-generated issue, but this is the only way for me - I don't understand much of Wayland/Anland implementation details, so hope this makes sense. Split screen is not required to reproduce it - it reproduces even with full-screen immersive mode Anland app. Thanks!

Summary

KWin can enter a persistent high-CPU state after the Android Anland consumer changes surface size, disconnects, and reconnects. The trigger is commonly an Android split-screen/task transition while a foreground client such as Konsole is visible.

After the Android surface stabilizes, KWin continues consuming a substantial fraction of one CPU indefinitely. The failure is accompanied by:

kwin_backend_anland: consumer disconnected, entering fallback
kwin_scene_opengl: Error creating EGLImageKHR:  "EGL_BAD_ALLOC"
kwin_backend_anland: failed to import dmabuf 0 as texture

The immediate recovery bug appears to be in AnlandBackend::onReconnectTimer(): it ignores the false return from AnlandEglLayer::importBuffers(), then stops fallback handling, installs notifiers, uninhibits the RenderLoop, and requests a repaint even though importBuffers() has released all textures and left m_bufCount == 0.

This ignored return value was previously identified in issue #50 and PR #51. PR #51 was closed without being merged, and the current main branch still ignores the return value. This report concerns persistent CPU consumption rather than the SIGBUS symptom from #50.

Environment

  • Device: Samsung SM-T875 (gts7l / gts7lxxx)
  • Android: Android 16, LineageOS 23 build
  • Kernel: Linux 4.19.325 custom/perf build
  • Container runtime: Droidspaces
  • Container OS: Debian 13 (trixie), arm64
  • Desktop: KDE Plasma Wayland
  • KWin: 6.3.6, patched Anland build
  • Mesa: 25.0.7-2+deb13u1, using the Android/KGSL graphics stack
  • Android consumer package: com.anland.consumer
  • Consumer-reported version: versionName=5.19-694d235, versionCode=350
  • Anland source commit corresponding to 694d235: tag 5.20
  • Installed KWin package build time: 2026-09-07T01:39:49Z
  • Installed libkwin.so.6 is byte-for-byte identical to the current Debian 13 asset from Goldzxcbug/droidspaces-package release anland-kde-packages

Relevant environment:

ANLAND=1
ANLAND_SOCKET=/run/display.sock
ANLAND_DRM_DEVICE=/dev/dri/renderD128
ANLAND_SKIP_IMPLICIT_SYNC_WAIT=1
ANLAND_DISABLE_AUDIO=1

The issue therefore persists with the Anland audio engine disabled and is distinct from the audio-socket high-CPU problem fixed by PR #61.

SELinux is permissive in this configuration. AVC messages are logged but are not enforcing denials.

Reproduction

  1. Start a clean KDE Plasma Wayland session through Anland.
  2. Open Konsole and leave it visible in the Anland display.
  3. Put the Droidspaces activity and com.anland.consumer/.SecondaryActivity into Android split-screen.
  4. Cause task/focus transitions between the two Android activities, or otherwise cause the consumer SurfaceView to change size.
  5. Observe the consumer stop its render thread, allocate/collect four new dmabufs, and reconnect.
  6. Observe one or more KWin dmabuf import failures with EGL_BAD_ALLOC.
  7. Stop interacting and leave both Android activities stable.
  8. Observe that KWin continues consuming a substantial fraction of one CPU indefinitely.

Observed Android surface-size sequence during one reproduction:

10:10:08  surfaceChanged: 1270x1282
10:10:09  surfaceChanged: 1270x1544
10:10:11  surfaceChanged: 1270x1282
10:10:19  surfaceChanged: 1270x1544

Each size change caused a sequence similar to:

surfaceChanged: 1270x1544
render thread stopped
configured: socket=/data/local/tmp/anland-...sock
render thread started
reconnecting...
collecting 4 dma-bufs via dequeue/queue
connected
exit fallback triggered

KWin remained hot after the final surfaceChanged event and after the Android consumer stopped reporting surface churn.

Observed behavior

In one reproduction, four one-second top -H samples showed approximately:

KWin main thread:       50-53% CPU
KWin compositor thread: 10-12% CPU
other render thread:      2-3% CPU

In a later reproduction, the KWin main thread used approximately 37-42% CPU and its render-queue thread used approximately 6-7% CPU. The Android consumer itself used only approximately 3-7% CPU.

During a later quiet 12-second sample of the already-bad state:

  • KWin accumulated approximately 2.49 CPU-seconds.
  • No new consumer-disconnect log entry appeared.
  • KWin's open-FD count remained constant at 181.
  • No new Android surface transition was required to sustain the CPU use.

This indicates that Android surface churn triggers the failure, but ongoing surface churn, reconnect attempts, log output, or FD leakage is not required to sustain it.

Representative KWin log interval:

[64221.740585] kwin_backend_anland: consumer disconnected, entering fallback
[64222.026730] kwin_scene_opengl: Error creating EGLImageKHR:  "EGL_BAD_ALLOC"
[64222.026730] kwin_backend_anland: failed to import dmabuf 0 as texture
[64223.530533] kwin_backend_anland: consumer disconnected, entering fallback
[64225.279672] kwin_backend_anland: consumer disconnected, entering fallback
[64225.504983] kwin_scene_opengl: Error creating EGLImageKHR:  "EGL_BAD_ALLOC"
[64225.504983] kwin_backend_anland: failed to import dmabuf 0 as texture
[64226.507533] kwin_backend_anland: consumer disconnected, entering fallback
[64227.759646] kwin_backend_anland: consumer disconnected, entering fallback
[64228.007948] kwin_scene_opengl: Error creating EGLImageKHR:  "EGL_BAD_ALLOC"
[64228.007948] kwin_backend_anland: failed to import dmabuf 0 as texture
[64236.017817] kwin_backend_anland: consumer disconnected, entering fallback

The exact CPU percentage varies between samples, but it remains far above the approximately 1-2% idle behavior seen in a clean session.

Expected behavior

If a replacement dmabuf set cannot be imported, KWin should remain in fallback with its RenderLoop inhibited. It should release/reject that complete consumer resource generation and retry later using a fresh, complete resource generation.

KWin should resume rendering only after all dmabufs required for the new generation have been imported successfully.

Source-level cause

At commit 6e8f1c6f8a53918484dc0c117b4d60ed85bb383a, the Debian 13 backend does this in AnlandBackend::onReconnectTimer():

if (try_exit_fallback(m_display) != 0) {
    return;
}

qCInfo(KWIN_ANLAND) << "consumer reconnected";
m_inFallback = false;
m_consumerReady = false;
m_reconnectTimer->stop();

AnlandEglLayer *layer = m_outputs[0]->eglLayer();
if (layer) {
    layer->importBuffers(get_buf_count(m_display));
}
setupNotifiers();
anland_audio_set_fd(get_audio_fd(m_display));
push_resources_request(m_display, SERVICE_TYPE_CAMERA, nullptr);
m_outputs[0]->resumeRendering();
if (layer) {
    layer->addRepaint(infiniteRegion());
}

Source:

https://github.com/superturtlee/anland/blob/6e8f1c6f8a53918484dc0c117b4d60ed85bb383a/producers/kde/anland_backend_debian13_v5/src/backends/anland/anland_backend.cpp#L492-L531

However, AnlandEglLayer::importBuffers() explicitly returns false when importDmaBufAsTexture() fails. Before returning, it calls releaseBuffers(), which clears the FBOs/textures and sets m_bufCount = 0:

std::shared_ptr<GLTexture> texture = m_backend->importDmaBufAsTexture(attrs);
if (!texture) {
    qCWarning(KWIN_ANLAND) << "failed to import dmabuf" << i << "as texture";
    releaseBuffers();
    return false;
}

The caller discards this result and resumes rendering with no imported buffers. doBeginFrame() subsequently returns std::nullopt whenever m_bufCount == 0.

This produces a split state:

  • display_ctx::fallback == false, because try_exit_fallback() succeeded at receiving FDs and dmabuf metadata;
  • AnlandBackend::m_inFallback == false;
  • the reconnect timer is stopped;
  • the KWin render loop is uninhibited;
  • AnlandEglLayer::m_bufCount == 0 and there is no valid render target.

That state is consistent with rendering/compositing work continuing after the failed import and with CPU consumption persisting after the Android consumer stabilizes.

Why PR #51 should not be applied unchanged

PR #51 correctly noticed that importBuffers() was ignored and proposed:

if (!layer->importBuffers(get_buf_count(m_display))) {
    qCWarning(KWIN_ANLAND) << "buffer import failed on reconnect, re-entering fallback";
    enterFallback();
    return;
}

However, AnlandBackend::enterFallback() only changes the C++ backend state. It cannot call the private display_producer.c enter_fallback() function. Therefore, after the proposed call:

AnlandBackend::m_inFallback == true
display_ctx::fallback == false

On the next timer event, try_exit_fallback() sees display_ctx::fallback == false and returns success immediately without acquiring a new consumer generation. The backend can then retry importing the same rejected resources. At the 200 ms timer interval, that risks becoming a repeated import/error loop rather than a correct recovery.

PR #51:

#51

Existing related issue:

#50

Proposed fix

Treat reconnect plus dmabuf import as one transaction. The backend should not publish the connected state or resume rendering until the import has committed successfully.

One possible design:

  1. Add a public producer-library function, for example:

    void reject_consumer_resources(display_ctx *ctx);

    Its implementation should invoke the existing producer-side fallback transition so that it:

    • sets display_ctx::fallback = true;
    • runs pre_release_cb before borrowed FDs are closed;
    • releases all consumer dmabufs, data/fence/audio/event FDs, pending fence and shm mapping;
    • invokes the registered fallback callback.
  2. Keep AnlandBackend::m_inFallback == true, keep the reconnect timer running, and keep the RenderLoop inhibited while importing a candidate generation.

  3. Change onReconnectTimer() conceptually to:

    if (try_exit_fallback(m_display) != 0) {
        return;
    }
    
    AnlandEglLayer *layer = m_outputs[0]->eglLayer();
    if (!layer || !layer->importBuffers(get_buf_count(m_display))) {
        qCWarning(KWIN_ANLAND) << "failed to import consumer buffer generation";
        reject_consumer_resources(m_display);
        // Remain inhibited and let the existing timer retry later.
        return;
    }
    
    // Commit the transition only after every import succeeded.
    m_inFallback = false;
    m_consumerReady = false;
    m_reconnectTimer->stop();
    setupNotifiers();
    anland_audio_set_fd(get_audio_fd(m_display));
    push_resources_request(m_display, SERVICE_TYPE_CAMERA, nullptr);
    m_outputs[0]->resumeRendering();
    layer->addRepaint(infiniteRegion());
    qCInfo(KWIN_ANLAND) << "consumer reconnected";

The precise callback/state ordering may need adjustment to avoid duplicate cleanup, but both the C producer state and C++ backend state must remain synchronized.

An import failure should preferably use a slower or bounded backoff than the normal 200 ms discovery interval so persistent driver/import failure cannot itself cause a 5 Hz resource-recreation loop. Recovery should not be permanently capped after an arbitrary number of attempts; it should remain able to recover when a genuinely valid consumer generation appears.

Additional error-handling problems found

trigger_refresh() ignores sendmsg() failure

display_producer.c::trigger_refresh() calls:

sendmsg(ctx->fence_fd, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);

but ignores its return value and always returns 0. A broken fence channel can therefore be reported to the KWin layer as a successfully handed-off frame. The producer should detect a failed/short send, close the pending fence, enter fallback, and return an error which notifyFramePresented() handles without waiting for a buffer-ready event that will never arrive.

Source:

https://github.com/superturtlee/anland/blob/6e8f1c6f8a53918484dc0c117b4d60ed85bb383a/producers/kde/anland_backend_debian13_v5/src/backends/anland/display_producer.c#L259-L299

Some receive failures do not enter fallback

For example, poll_input_event() returns -1 when recv_all() fails, but does not always call enter_fallback(). Similar handling exists in extended-data/FD receive paths.

All EOF, short-read, unrecoverable receive, POLLHUP, and POLLERR paths should move the producer and backend into fallback and remove/disable the corresponding QSocketNotifier. Otherwise, a notifier associated with a permanently readable EOF/error condition may repeatedly activate the main Qt event loop.

Suggested regression tests

  1. Inject an invalid dmabuf FD or force importDmaBufAsTexture() to return null during reconnect.
  2. Verify that:
    • the KWin RenderLoop remains inhibited;
    • no repaint is scheduled against a zero-buffer layer;
    • both fallback flags remain true;
    • all FDs from the rejected generation are closed;
    • CPU returns close to idle;
    • the next attempt obtains a new resource generation rather than reusing the failed one.
  3. Restore valid dmabufs and verify that KWin automatically reconnects and resumes rendering.
  4. Close the data and fence sockets independently and verify that each failure produces exactly one fallback transition without an event-loop spin.
  5. Repeat rapid Android surface changes between 1270x1282 and 1270x1544, then leave the surface stable and verify that KWin remains near idle.

Diagnostics and safety notes

  • A live debugger was not used for the final reproduction because a previous debugger attach destabilized the compositor session.
  • strace/perf attachment is restricted in this container, so the precise hot instruction could not be sampled safely.
  • Earlier stack observations reached KWin compositing and Mesa/KGSL reset/status paths, but the futex/fence activity should currently be treated as a consequence or secondary hypothesis, not as the proven root cause.
  • The ignored import failure and invalid state transition are independently demonstrable from current source and must be corrected even if a separate Mesa/KGSL defect is responsible for the initial EGL_BAD_ALLOC.

Workarounds

  • Avoid split-screen/task/status-bar transitions that recreate the Android consumer surface.
  • If KWin becomes hot, restarting the Android consumer may recover it if the next dmabuf import succeeds.
  • If consumer-only restart does not recover it, restarting the Plasma session resets the KWin backend state, but is disruptive.
  • ANLAND_SKIP_IMPLICIT_SYNC_WAIT=1, disabling audio, disabling effects, or applying a CPU quota does not correct the invalid reconnect state.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions