Skip to content

feat: Direct Composition zero-copy path via ASurfaceControl + HWC overlay#1

Merged
Vower2993 merged 21 commits into
mainfrom
direct-composition-port
Jun 27, 2026
Merged

feat: Direct Composition zero-copy path via ASurfaceControl + HWC overlay#1
Vower2993 merged 21 commits into
mainfrom
direct-composition-port

Conversation

@Vower2993

Copy link
Copy Markdown
Owner

Port of PR WinNative-Emu#380 to the new VulkanRenderer architecture with soft-boot hardening and battery/CPU optimizations. See commit message for full details.

Xnick417x and others added 21 commits June 18, 2026 17:25
Fix glass control opacity slider dimming

Remap the GameHub brightness curve, anchored at the 40% default: dims
further below it and stays responsive above instead of saturating past
~70%. Non-glass style unaffected.
* feat: Fx effects per shortcut

Persist screen-effect settings on the active shortcut instead of the container; fall back to the container value when a shortcut has none, so existing setups still apply on first launch then diverge per shortcut.

* feat: Fx effects fully per shortcut, drop container

Read and write screen-effect settings only on the active shortcut, falling back to app preferences when no shortcut is launched; the container no longer stores or supplies these settings.
Default BCn emulation to auto and restrict compute decompression to non-Adreno GPUs
* Added Ajay Prefix Pro v1.8 Downloader

* Update Ajay Prefix Pro downloader version in JSON
* Add component installer to container cards

New blue container button on each container card opens a Compose sheet listing installable system-library components from a remote catalog, and installs the no-installer ones into the selected wineprefix: download, tar.xz/zst extract, copy DLLs to system32/syswow64, DLL overrides and registry keys. Components needing an in-Wine installer or CAB extraction are gated for now.

* Add CAB extraction; move duplicate into container menu

Component installer extracts CAB archives via the system image's cabextract (cab_extract and get_from_cab, with glob source support), so cab-based library components install fully into the selected container. Container card: duplicate action moved into the 3-dot menu and the card layout restored.

* Run installers, regsvr32 and font registration for components

Component installer now handles install_exe/install_msi by staging the installer into the container and running it in a Wine boot session (gated dependency mode that captures the exit code and auto-closes), register_dll via a batched regsvr32 session, and register_font/replace_font/install_fonts via registry and file edits. Normal container launches are unaffected (gated by an intent flag).

* Track installs per container; fix mono/cjkfonts/gecko

Persist installed components per container (marker under the container dir) so the list shows them installed and won't double-install; carries with duplicated containers. Fixes: uninstall step is a no-op, zip archives extract, and a session that closes without a guest-termination signal now completes instead of hanging (idempotent completion + onDestroy fallback; MSI installs default to /passive).

* Serialize component installs so a second tap queues

* Finish queued component install only after its session window tears down

* Cap container tile width on wide screens; order component sections

* Clean up installer staging dir after component install

* Cancel component install and free the lock when the sheet is closed
Second selectable wrapper in container and shortcut settings. Selecting it
overlays its wrapper bundle and uses the guest usr/lib adrenotools hooks;
switching back to the default wrapper restores the stock wrapper and layers.
* Fix concurrency, crash and protocol bugs from source audit

- shmat uninitialized return + getenv null-safety (android_sysvshm, alsa plugin)
- packColor channel packing (drawable)
- X11 DestroySubwindows missing break; XI2 SelectEvents num_masks==0; writeString8 byte padding; ClientSocket write error propagation
- thread safety: connectedClients SparseArray, SysVSharedMemory.delete, fakeinput maps, steam bridge lazy init, SessionKeepAlive fields
- Vulkan null-fence guard and present-mode lock
- ALSA write bounds check + drain(); framesPerBuffer visibility
- tar hard-link symlink-ancestor check; curl_global_init once-init
- GuestProgramLauncher PID generation guard + dependency-check reset
- ProcessHelper Process.pid()/StringBuilder; PulseAudio patch hardening; steam log gating behind WNB_LOG

* Remove unused dead-code shims

steam_glxshim.c (and its unreferenced prebuilt .so), alsa_client.c, and evshim.c are never compiled-and-loaded or called at runtime; drop the sources, their CMake targets, and the now-dead aaudio link.

* Fix ProcessHelper PID retrieval to compile against Android SDK

java.lang.Process.pid() is not resolvable in the AGP javac step; invoke the public pid() method reflectively (still avoids the Android 14+ blocked private pid field).

* Revert fakeinput and ProcessHelper PID changes that broke container boot

The #5 fakeinput mutex rewrite hung winedevice on the /dev/input path (wine never booted, black screen), and the WinNative-Emu#31 Process.pid() reflection returned -1 at runtime. Both restored to upstream behavior; splitCommand cleanup kept.
i18n: add 32 missing session drawer Fx/HUD/task strings to all locales

Translate untranslated session_drawer_*, session_task_*, and session_gyroscope_orientation_mode keys into da, de, es, fr, hi, it, ko, pl, pt-rBR, ro, ru, uk, zh-rCN, zh-rTW. All locales now match the default string set.
* Keep setup wizard nav row mounted so component list doesn't shift on download

* Queue setup wizard component installs so selections download sequentially
* Add download buttons to logs viewer

Add Download all and per-log download that save the logs zip / single
file to Downloads/WinNative/logs. Reorder log card actions to
delete, share, download.

* Save logs to WinNative/logs and show save toast over logs dialog

Write downloaded logs to /storage/emulated/0/WinNative/logs and anchor
the saved-path toast to the logs dialog window so it shows in front.

* Mark downloaded logs with green-fill button and remember them

Per-row download button stays green and fills with an illuminated glass
tint once its log is downloaded. The downloaded state persists across
opening the logs dialog (keyed per log), and is pruned on delete.

* Give Components delete button a red bordered box

Add a DangerRed border and subtle fill to the Components remove button
so it matches the other component actions and the logs delete button.
* Restyle library top bar and add-game buttons to blue accent

Top-bar settings, search and filter buttons and the add-game FAB now use a
transparent fill with an accent-blue outline and accent-blue icons. The FAB's
off-center elevation shadow is replaced with a centered radial glow.

* Use accent-blue tap ripple on library top-bar and add buttons

The settings, search, filter and add-game buttons now show an accent-blue
tap ripple instead of the default grey, and the add-game glow radius is tuned.

* Color the signed-out library person icon accent blue

The placeholder person icon on the sign-in empty state now uses the accent
blue instead of grey.

* Add in-app file manager opened from a library top-bar button

Adds a folder button to the library top bar that opens a file manager built on
the directory picker: browse files and folders across C:/Z:/D:/internal storage
roots, copy/cut/paste, rename, delete, new folder, plus run an executable in a
chosen container and create a shortcut from one. A long-press opens a small
anchored action menu on the item.

* Add drive roots to game picker and tidy file manager footer

The custom-game exe/folder pickers now list the C:/Z:/D:/internal drive roots
in the storage roots dropdown, and the exe picker defaults to the Downloads
drive. The file manager's Paste and New Folder buttons use the accent colour.

* Run file manager copy/move off the UI thread with a progress dialog

Paste now streams on a background coroutine and shows a cancelable progress overlay, fixing the freeze/ANR when moving large files. Paste, New Folder and Close are outline-only with a blue tap ripple.

* Make picker footer buttons outline-only and localize file manager strings

All picker buttons (Cancel, Close, OK, Create, Rename, Delete) are now transparent with a colored outline and a tap ripple, no resting fill. Every file manager label, menu item and toast moved to string resources and translated into all locales.

* Color the downloads Clear button blue to match Pause All
* Drivers: align asset card fill color, remove grey border

* Drivers/Components: compact download + installed pills, restore asset card border

* Settings: recolor screen backgrounds to darker navy

* Container/shortcut settings: recolor dialog background to darker navy

* Containers: default desktop wallpaper, Original option, in-app image picker

* Containers: localize desktop background type options

* Drivers: page through GitHub releases up to 200
Updated Ajay Prefix Pro 1.8 Downloader

- Added colourful gui
- Increased default font size
- Updated "About Ajay Prefix Pro" and can be read in container dark mode
…tive-Emu#576)

Remove elevation shadow that bled through the transparent circle fill of the settings, search, file-picker and filter buttons, showing as an off-center polygon over background art.
ALSA output now defaults to PERFORMANCE_MODE_NONE at 40ms latency instead of LOW_LATENCY at 16ms. LOW_LATENCY capped the AudioTrack buffer capacity, so underrun recovery could not grow it, causing crackle/dropouts on bursty streams. An unset performance-mode env var now resolves to NONE; low_latency stays an explicit opt-in. PulseAudio default latency raised from 40ms to 144ms.
Fix our native Io extraction to extract 8sharded pieces at a time to speed imagef installation up.
Batch headers and reg files to speed container creation up.
Ditch xz_embbeded for liblzma to speed up layer extraction.
Discard the Unneeded recursive font intailizing on boot to speed boot process up.
…rlay

Port PR WinNative-Emu#380 (WinNative-Emu#380) from the old GLES GLRenderer
architecture to the new native VulkanRenderer (PR WinNative-Emu#343). Achieves true
zero-copy display by routing fullscreen game frames directly to
SurfaceFlinger via a child ASurfaceControl layer, bypassing the
VulkanRenderer's GPU compositing blit. HWC promotes the SC layer to a
DPU overlay plane — zero GPU compositing cost, zero buffer copy.

=== SOFT-BOOT HARDENING (vs original PR WinNative-Emu#380) ===

The original PR WinNative-Emu#380 caused soft boots (device reboots) on several device
families. Research report: /home/z/my-project/download/pr380-research-report.md

Fixes applied:
1. Smoke-test buffer REMOVED. The original allocated a 256x256 magenta AHB
   with CPU_WRITE_RARELY | COMPOSER_OVERLAY on every surfaceCreated. On
   Adreno 6xx qdgralloc / MediaTek / older Exynos, the CPU_WRITE +
   COMPOSER_OVERLAY combo triggers a kernel panic → soft boot. Real game
   frames prove the path works; the proof-of-life is not needed.

2. Device-family blocklist added (SurfaceCompositor.isBlocklisted):
   - Xiaomi + Android 14+ (HyperOS 2.0+) — BLOCKED. Flutter disabled SC
     entirely on these (flutter/flutter#160025).
   - Samsung OneUI 4.1+ (Android 12+) — warned but allowed (less
     reproducible).
   The block is conservative: when in doubt, block.

3. dstX/dstY validation in nativePushBuffer. Negative destination
   coordinates were silently passed to ASurfaceTransaction, which crashes
   SurfaceFlinger on some OEM ROMs.

4. Wait-for-in-flight on release(). The native side tracks in-flight
   ASurfaceTransaction_apply calls and waits (up to 500ms) for them to
   complete before ASurfaceControl_release. Prevents the Xiaomi/HyperOS
   crash where releasing a SC while a transaction is in-flight kills SF.

5. Fence FD leak prevention. Every error path in nativePushBuffer closes
   the acquire_fence_fd (the framework only takes ownership on the success
   path of setBuffer).

=== BATTERY / CPU OPTIMIZATIONS ===

1. Cache check before JNI. The Java side caches (ahbPtr, dstW, dstH) and
   only calls nativePushBuffer when something changed. DRI3 allocates a
   fresh GPUImage per Present, so AHB-pointer identity is a sufficient
   dirty check. No transaction is created for unchanged frames — this is
   the primary CPU/battery win.

2. Self-detach on failure. After DC_FAIL_LIMIT (8) consecutive pushBuffer
   failures, the renderer nulls directCompositionTarget so subsequent
   frames don't keep paying the JNI cost for a permanent failure.

3. Magnifier guard. When the magnifier overlay is active, the SC layer is
   hidden immediately (not after the next frame) so the GL-rendered
   overlay is visible.

4. Always-render Vulkan composition (defence in depth). The VulkanRenderer
   still composites every frame underneath the SC layer. If the SC path
   fails for any reason, the GL output is still visible. This also
   prevents the stale-frame reveal on direct→fallback transition.

=== ARCHITECTURE ===

Data flow:
  1. DXVK/Wine renders normally via X11 (no Vulkan layer interception).
  2. X server's Drawable receives the AHardwareBuffer via DRI3
     PIXMAP_FROM_BUFFERS.
  3. VulkanRenderer.buildAndSubmitFrame() composites the scene normally,
     then calls maybePushDirectComposition(directCandidate).
  4. The hook extracts the AHardwareBuffer from the candidate's
     scanoutSource (a GPUImage) via getHardwareBufferPtr().
  5. Calls DirectCompositionLayer.pushBuffer(ahbPtr, 0, 0, w, h, fenceFd).
  6. JNI → surface_compositor.c → ASurfaceTransaction_setBuffer + geometry
     + colour/brightness + apply().
  7. SurfaceFlinger + HWC promote the SC layer to a DPU overlay plane —
     zero GPU compositing, zero buffer copy.

The SC layer at z=1 covers the VulkanRenderer's output at z=0. HWC decides
overlay promotion based on layer properties (fullscreen, opaque, RGBA_8888).
Phase 4 brightness fix (setBufferDataSpace=SRGB, setBufferTransparency=OPAQUE,
setExtendedRangeBrightness=1.0,1.0) neutralises the Snapdragon DPU's
SDR-on-HDR brightness boost.

Per-container toggle (Container.EXTRA_DIRECT_COMPOSITION, default off).
When disabled, zero behavior change vs. pre-DC.

=== FILES ===

New:
  app/src/main/cpp/winlator/surface_compositor.c (550 lines)
    JNI wrappers around ASurfaceControl/ASurfaceTransaction. dlopen/dlsym
    so the lib loads on minSdk 26. In-flight tracking + wait-for-complete
    on release. dstX/dstY validation. Smoke test removed.
  app/src/main/runtime/display/composition/SurfaceCompositor.java
    Static isAvailable() probe with device-family blocklist.
  app/src/main/runtime/display/composition/DirectCompositionLayer.java
    Synchronized ASurfaceControl wrapper. attach/pushBuffer/hide/release.

Modified:
  app/src/main/cpp/CMakeLists.txt — add surface_compositor.c to winlator lib
  app/src/main/runtime/display/renderer/VulkanRenderer.java (+203 lines)
    Per-frame hook (maybePushDirectComposition), hide logic, cache, failure
    counter, setDirectCompositionTarget. Tracks directCandidate in
    buildAndSubmitFrame.
  app/src/main/runtime/display/renderer/GPUImage.java (+20 lines)
    getHardwareBufferPtr() public accessor.
  app/src/main/runtime/display/xserver/Drawable.java (+49 lines)
    acquireFenceFd (AtomicInteger) with takeAcquireFenceFd/setAcquireFenceFd.
  app/src/main/runtime/display/XServerDisplayActivity.java (+131 lines)
    installDirectCompositionLifecycle, releaseDirectCompositionLayer.
    SurfaceHolder.Callback for attach/release. Cleanup in onDestroy.
  app/src/main/runtime/container/Container.java (+37 lines)
    EXTRA_DIRECT_COMPOSITION toggle + accessors.
  app/src/main/feature/library/GameSettings.kt (+11 lines)
    directComposition state + SettingCheckbox.
  app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt (+3 lines)
    Load/save the toggle.
  app/src/main/res/values/strings.xml (+2 strings)
    session_display_direct_composition + summary.

=== VERIFICATION ===

- C syntax + object compile: PASS (NDK r27 clang, aarch64-linux-android26)
- JNI symbols exported: 5/5 (nativeIsAvailable, nativeCreateFromWindow,
  nativeDetachAndRelease, nativeHide, nativePushBuffer)
- Smoke-test symbols absent: PASS
- javac syntax check: PASS (all errors are missing-external-dependency,
  zero syntax/semantic errors in new code)
- bash -n on scripts: PASS
- 3-stage audit: PASS (fix verified, no regressions, secondary fixes confirmed)

Reference: WinNative-Emu#380
Research: /home/z/my-project/download/pr380-research-report.md
…rect Composition

Four fixes based on user feedback from the first test build:

1. SHORTCUT PERSISTENCE (the main bug)
   The Direct Composition toggle in shortcut settings was not persistent —
   every time the user re-entered shortcut settings, it was turned off. Root
   cause: ShortcutSettingsComposeDialog.kt had no load/save/reload logic for
   the directComposition setting (it only handled containers, not shortcuts).
   Fixed by adding the getShortcutSetting/saveOverride/reload pattern that
   fullscreenStretched already uses. Shortcut now overrides container, and
   the toggle persists across dialog open/close.

2. ACTIVITY READS SHORTCUT OR CONTAINER
   installDirectCompositionLifecycle in XServerDisplayActivity only checked
   container.isDirectCompositionEnabled(). Now matches the swapRB pattern:
   shortcut.getExtra(EXTRA_DIRECT_COMPOSITION, container fallback). If the
   shortcut overrides the container setting, the shortcut's value wins.

3. HUD INDICATOR
   Added a ' + DC' (green) suffix to the FrameRating renderer label when
   Direct Composition is active. The VulkanRenderer fires a
   DirectCompositionStateListener callback when dcLayerActive transitions
   true/false; XServerDisplayActivity registers a listener that calls
   frameRating.setDirectCompositionActive(). The user can now see at a
   glance whether zero-copy is active (when the FPS monitor is enabled).

4. DIAGNOSTIC FILE LOGGING
   The user's shared logs (wine_*.txt, fexcore_*.txt) only capture Wine/FEX
   stderr — they do NOT contain Android logcat. So the SurfaceCompositor /
   XServerDisplayActivity / VulkanRenderer DC logging was invisible in the
   user's logs. Fixed by adding SurfaceCompositor.initDiagnosticFile() +
   logEvent() + closeDiagnosticFile() which writes timestamped lines to
   direct-composition.log in the app's logs directory. This file is
   auto-included when the user shares logs (LogManager shares all *.log /
   *.txt files). Every DC lifecycle event is now captured: init, availability
   check, attach, first frame pushed, push failures, self-detach, release.

Files changed:
  - ShortcutSettingsComposeDialog.kt: +16 lines (load + save + reload)
  - XServerDisplayActivity.java: +62 lines (shortcut read, HUD listener
    wiring, diagnostic file init/log/close, log calls in lifecycle)
  - SurfaceCompositor.java: +90 lines (initDiagnosticFile, logEvent,
    closeDiagnosticFile)
  - VulkanRenderer.java: +38 lines (DirectCompositionStateListener,
    notifyDirectCompositionStateListener, log calls on state transitions)
  - FrameRating.java: +32 lines (directCompositionActive field,
    setDirectCompositionActive method, ' + DC' green suffix in
    updateRendererText)

Build verified: C compile clean, javac zero syntax errors, all DC
identifiers use fully-qualified names (no missing imports). 3-stage audit
passed.
…or why DC skips frames

ROOT CAUSE ANALYSIS (from direct-composition.log):

The log showed:
  [18:47:45] DirectCompositionLayer ATTACHED — SC layer created, waiting for first frame
  [18:51:06] releaseDirectCompositionLayer: detaching + releasing SC layer

3 minutes 21 seconds of gameplay with ZERO 'DC ACTIVE — first frame pushed'
log lines. The SC layer attached but never received a frame.

ROOT CAUSE: maybePushDirectComposition checked
  if (!directCandidate.isDirectScanout()) return false;
but directScanout=true is set on the PIXMAP drawable (in DRI3Extension.java
line 326), NOT on the WINDOW drawable. The window drawable (which is what
buildAndSubmitFrame tracks as directCandidate) never has directScanout=true.
So every frame was silently rejected at this gate — the SC layer was attached
but never fed.

FIX:
Removed the isDirectScanout() check entirely. The real signal that a
candidate qualifies for Direct Composition is that its scanoutSource's
texture is a GPUImage with a valid AHardwareBuffer pointer — which is
exactly what the subsequent checks (tex instanceof GPUImage, ahbPtr != 0)
already verify. The isDirectScanout() check was redundant AND wrong
(checked the wrong drawable).

DIAGNOSTIC LOGGING (so the next log tells us exactly what's happening):
Added throttled logging that fires only when the skip REASON CHANGES (not
per-frame, to avoid spam):

1. In buildAndSubmitFrame: logs when directCandidate transitions
   null<->present, with window count and screen dimensions. This tells
   us whether ANY window ever qualifies as a fullscreen candidate.

2. In maybePushDirectComposition: logs the specific skip reason when it
   changes:
   - 'no-texture' — scanoutSource has no texture
   - 'texture-not-gpuimage(Texture)' — texture is a plain Texture, not a
     GPUImage (means DRI3 AHB path isn't being used for this window)
   - 'gpuimage-ahb-null' — GPUImage exists but AHB pointer is 0 (allocation
     failed or buffer destroyed)
   - 'ok' — candidate qualifies (no log line for this state)

Also logs on successful first push:
   'DC ACTIVE — first frame pushed to SurfaceControl (ahb=0x... WxH
    drawable=WxH)'

And on every pushBuffer failure:
   'DC pushBuffer FAILED (#N) — ahb=0x...'

These are in addition to the existing 'DC DISABLED — N consecutive
failures' log.

The next direct-composition.log will tell us EXACTLY which gate is
blocking frames (or confirm that frames are now flowing).

Files changed: VulkanRenderer.java (+65/-7 lines)
… optimization)

User confirmed commit 1 (d781226) works: DC is active, game displays
correctly, no soft boot, 'Vulkan + DC' shows in HUD. Now adding the
CPU/battery optimizations identified in the research report (section 4).

OPTIMIZATION: vsync-paced frame submission

Before: nativePushBuffer called apply() fire-and-forget every frame. The
render thread queued transactions as fast as it could produce frames,
which SurfaceFlinger had to backlog-process. This wasted CPU on both
sides (render thread spinning, SF draining a queue) and battery (no
alignment with display vsync).

After: each transaction registers an OnComplete callback (API 29+) via
ASurfaceTransaction_setOnComplete. The callback fires on SF's binder
thread when the buffer is 'observable on display'. The render thread
calls nativeWaitForPreviousFrame(20ms) BEFORE the next pushBuffer,
blocking until the previous frame is truly done. This paces the render
thread to the display's vsync rate — we never queue more than one
transaction ahead of SF.

IMPLEMENTATION:

surface_compositor.c (+122 lines):
  - Added ASurfaceTransaction_OnComplete / OnCommit callback typedefs
  - Resolved setOnComplete + setOnCommit symbols via dlsym
  - g_has_on_complete flag (true on API 29+, which is all supported devices)
  - on_transaction_complete() callback: calls inflight_decrement() on SF's
    thread when the transaction completes
  - nativePushBuffer: when g_has_on_complete, registers the callback before
    apply() and does NOT decrement synchronously (the callback does it).
    Falls back to fire-and-forget (sync decrement) if setOnComplete is
    missing.
  - nativeWaitForPreviousFrame(timeout_ms): blocks the render thread on
    g_inflight_cv until inflight_count drops to 0 (previous frame done).
    20ms timeout — proceeds on timeout to avoid freezing the render thread.

DirectCompositionLayer.java (+30 lines):
  - waitForPreviousFrame(timeoutMs) public method + nativeWaitForPreviousFrame
    declaration

VulkanRenderer.java (+18 lines):
  - In maybePushDirectComposition, call dcTarget.waitForPreviousFrame(20L)
    BEFORE pushBuffer, but only when dcLastPushedAhb != 0 (first frame has
    nothing to wait for). The wait happens INSIDE the renderLock so the
    X-server worker can't swap the scanoutSource mid-wait.

The result: the render thread now sleeps until SF signals completion,
instead of busy-looping apply() calls. This reduces CPU usage on the
render thread and aligns frame submission with the display's vsync.

Build verified: C compile clean, 6 JNI symbols exported (including the
new nativeWaitForPreviousFrame), javac zero syntax errors.
@Vower2993 Vower2993 merged commit 1341429 into main Jun 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants