Compare commits

...
Author SHA1 Message Date
xXJsonDeruloXx 1c80bcb070 fix: show the saved Steam Deck mode on the Steam page before Performance has been opened 2026-09-29 19:54:29 -04:00
xXJsonDeruloXx 6565e93e8e perf: skip xalia and gamescope realtime queues by default, add a realtime toggle, default the mic off 2026-09-29 19:54:29 -04:00
xXJsonDeruloXx 72dedcb772 perf: cache pinned downloads, skip unchanged proot rebuilds, and wait out a Docker Desktop restart in the local build 2026-09-29 19:54:07 -04:00
Kurt Himebauch 25079ab819 Merge pull request #84 from maxjivi05/fix/performance-misc 2026-09-29 19:41:38 -04:00
maxjivi05 736b7bed5e perf(gamescope): ask X for the pointer only while a cursor is drawn
The gamepad-cursor port queried the X pointer on every vblank, a
blocking round trip to Xwayland on the paint thread. With no cursor
image the position comes from wlserver as upstream; while the cursor is
hidden for inactivity X is asked every 50 ms. The realtime-queue comment
now says it is a no-op on KGSL Turnip.
2026-09-29 18:44:32 -04:00
maxjivi05 a3f4c911e5 perf: poll without starting processes, and stop rewriting unchanged Steam files
The session's watchers slept with sleep(1) and read files with cat, an
exec each under proot; they now wait in bash itself. steam-compat
writes its tool, the adopted wrappers and config.vdf only when they
change, the games watcher probes ownership when its matches change or
every five minutes instead of every 30 s, and Proton no longer inherits
the client's steamrtarm64 library path.
2026-09-29 18:44:32 -04:00
maxjivi05 caa7682c6f perf: default to Turnip sysmem, compact Zink descriptors and the Performance + TSO FEX preset
WinNative's defaults for every Linux session. Choices already saved by
a user are kept.
2026-09-29 18:44:32 -04:00
maxjivi05 e217cbe66d perf: pace buffer releases to the screen, read the requested refresh, drop sustained mode and ADPF
A replaced buffer went straight back to a game with no frame limit, so a
MAILBOX/IMMEDIATE swapchain drew frames that were never shown; it now
comes back once per refresh as in WinNative. gamescope's -r and the
compositor read the rate of the mode the activity asked for rather than
the one the panel was still in, and the zero-copy layer votes it.
Sustained performance mode, which caps clocks on most power HALs, and
the ADPF session, which reported frame intervals as work, are gone.
2026-09-29 18:44:32 -04:00
maxjivi05 4914b2cc69 perf: stop proot's fake_id0 and ioctl traps from stopping every stat, sendmsg and GPU ioctl
fake_id0 (loaded by -i for Xwayland's set*id) now traces only the set*id
family and the xattr fixups when the ids given are proot's own, and the
seccomp filter traces ioctl only for the termios and FICLONE requests
proot rewrites. The faccessat2 and statx exit stops are dropped on
kernels that have the calls.
2026-09-29 18:44:32 -04:00
Kurt Himebauch 6ecd6fa1d0 Merge pull request #81 from Droid-Deck/fix/proot-kompat-utsname-only
perf: stop proot's kompat from trapping hot syscalls, and stop respawning a crashing mangoapp
2026-09-29 15:38:52 -04:00
xXJsonDeruloXx 0f4e83eefd fix: stop gamescope respawning a mangoapp that keeps dying at start 2026-09-29 15:08:28 -04:00
xXJsonDeruloXx d62525f5af perf: stop proot's kompat from trapping futex/epoll/fcntl and stripping the vDSO when only the hostname changes 2026-09-29 14:39:53 -04:00
Kurt Himebauch f4d2c37fbd Merge pull request #78 from Droid-Deck/fix/ci-publish-retention
Fix DroidDeck-CI main build publishing
2026-09-29 10:48:38 -04:00
xXJsonDeruloXx ffd9f92d7b ci: keep main DroidDeck-CI releases published 2026-09-29 10:46:50 -04:00
Kurt Himebauch 213d110766 Merge pull request #55 from Droid-Deck/fix/steamdeck-performance-overlay
Deck mode: working Steam performance overlay (MangoHud)
2026-09-29 09:41:51 -04:00
xXJsonDeruloXx 1a8c91b49b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/MainActivity.kt
2026-09-29 09:41:11 -04:00
Kurt Himebauch 9644a4cc7f fix: use Android's 300 ms double-tap window for Back so the single-press action comes sooner (#73) 2026-09-29 02:10:51 -04:00
Kurt Himebauch 24c250fdb4 fix: send wl_keyboard modifiers so Shift and Caps Lock produce capitals (#71) 2026-09-29 02:10:01 -04:00
Kurt Himebauch 9d891d4db1 Merge branch 'main' into fix/steamdeck-performance-overlay 2026-09-28 16:36:33 -04:00
xXJSONDeruloXx e797bc5f01 mangoapp: stat sources for more Snapdragon generations
KGSL's files and thermal zone names differ between Snapdragon generations.
Each value now takes the first source that exists, in the order GameNative
and the Winlator forks read them: GPU load from gpu_busy_percentage or
devfreq/gpu_load, GPU clock from devfreq/cur_freq or gpuclk, GPU temperature
from KGSL's temp, devfreq/temp or a zone whose sensor name has "gpu". CPU
zones are any whose name has "cpu" and not "gpu" (cpuss-0, cpu0-silver-usr,
apc1-cpu0-usr, ...), matched without regard to case.
2026-09-28 12:28:45 -04:00
xXJSONDeruloXx 0cc3fe6bca mangoapp: GPU memory from KGSL
mangoapp's VRAM figure on a Qualcomm driver comes only from the gpu_mem_total
trace event. The tracefs stand-in's event list now declares that event as
Android kernels do, and reading the stand-in's events answers with one
record carrying the total KGSL reports in /sys/class/kgsl/kgsl/page_alloc,
which every Adreno under Android has and the app can read.
2026-09-28 12:28:45 -04:00
xXJSONDeruloXx ab8542e4e5 mangoapp: show Adreno GPU and CPU stats
Valve's mangoapp reads an Adreno GPU's load, clock and temperatures from the
paths of its own hardware, and the CPU temperature from thermal zones named as
mainline kernels name them. Every Adreno under Android keeps the GPU values in
KGSL's sysfs, readable by the app, and Android kernels name the CPU sensors
cpuss-N / cpu-N-M; the session binds each where mangoapp looks. Thermal zones
are found by sensor name, since their numbering differs per device, and
anything that cannot be found is left alone.
2026-09-28 12:19:01 -04:00
xXJSONDeruloXx c2bcf20172 mangoapp: run the performance overlay under Android
Three things kept Valve's mangoapp from drawing the overlay in a session:

- GLFW 3.4 takes XDG_SESSION_TYPE=wayland with WAYLAND_DISPLAY set as
  Wayland-only, and mangoapp needs gamescope's X server; the wrapper sets
  XDG_SESSION_TYPE=x11.
- On a Qualcomm display driver it opens a tracefs instance for the GPU memory
  total and aborts when that fails, which it always does for an Android app.
  The session preload now places the instance in an empty directory of its own
  when the real one cannot be made.
- gamescope sends it each frame's timings through a System V message queue,
  and Android kernels have no System V IPC. sysv.c now emulates the queue
  operations gamescope and mangoapp use, beside its semaphores and shared
  memory.
2026-09-28 12:19:01 -04:00
xXJSONDeruloXx 2341659cea Deck mode: enable Steam's performance overlay controls
The Quick Access menu's performance overlay toggle and level slider stayed
greyed out: the client exposes SteamClient.System.Perf only with -steamos3,
which SteamOS and Armada pass alongside -steamdeck. Deck mode now launches the
client with both. The session already puts package/beta back to the forced
branch before every start, which keeps -steamos3's own branch switch from
turning into an update loop.

The Deck mode switch moves from the Performance page to the Steam page's
Client settings, next to the client branch it selects by default.
2026-09-28 12:19:01 -04:00
xXJSONDeruloXx b2eaf380dc Blend translucent Wayland surfaces
The compositor composed every surface with a blit, which replaces what is under it. gamescope's
Wayland backend hands its overlay and notification planes to the host as full-size translucent
subsurfaces, so a Steam toast, the Quick Access menu or Deck mode's performance overlay blacked
out what was beneath it.

Surfaces whose buffer has alpha and that are not declared opaque now go through an alpha pass
(premultiplied source-over); everything else, and any image the driver cannot sample, keeps the
blit. wl_surface.set_opaque_region is honoured, and only for programs that declare opaque regions
at all, so Wine windows with a meaningless alpha channel are drawn as before. On the SurfaceControl
path a translucent plane is never promoted to the game layer, and the overlay layer is marked
translucent when its plane is.

From WinNative (maxjivi05, feature/wayland-gamescope 69218fc; GPL-3.0), with the SurfaceControl
layer handling added.
2026-09-28 12:18:50 -04:00
41 changed files with 1665 additions and 290 deletions

No files matched your search

+22 -6
View File
@@ -22,8 +22,10 @@ permissions:
pull-requests: write
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# Replace older builds only for the same PR. Each push/dispatch gets its own group so one main
# build cannot cancel another before it signs and publishes.
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PRELOAD_CACHE_VERSION: v2
@@ -489,10 +491,24 @@ jobs:
gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true
gh release create "$tag" -R "$CI_REPO" --target main --prerelease \
--title "main ${GITHUB_SHA::7}" --notes "$notes" signed/*
# Only the newest KEEP main builds stay; PR releases (pr-N) are left alone.
gh release list -R "$CI_REPO" -L 200 --json tagName,createdAt \
--jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.createdAt) | reverse | .['"$KEEP"':][] | .tagName' |
while IFS= read -r old; do gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag; done
# Keep the newest main builds by publication time. GitHub App releases can share createdAt.
gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \
--jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' |
while IFS= read -r old; do
# Never remove this run's release, even if the API returns an unexpected ordering.
if [ "$old" = "$tag" ]; then
continue
fi
# Another main publish may have removed the same old release concurrently.
if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then
if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then
echo "::error::Could not delete old release $old"
exit 1
fi
fi
done
published=$(gh release view "$tag" -R "$CI_REPO" --json tagName,assets)
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<< "$published" >/dev/null
url="$GITHUB_SERVER_URL/$CI_REPO/releases/tag/$tag"
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
echo "release=$url" >> "$GITHUB_OUTPUT"
+1
View File
@@ -12,6 +12,7 @@ captures/
# copies cannot drift.
app/src/main/assets/linuxfs/
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/.proot-inputs
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
# Kotlin compiler session files (build-tool state, never source).
@@ -19,6 +19,7 @@ add_library(bannerwayland SHARED
src/wl_toplevel_icon.c
src/wl_color_mgmt.c
src/hdr_compose.c
src/blend_pass.c
generated/color-management-v1-protocol.c
generated/xdg-shell-protocol.c
generated/linux-dmabuf-v1-protocol.c
@@ -0,0 +1,9 @@
#version 450
// Wayland buffers carry premultiplied alpha; the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
layout(binding = 0) uniform sampler2D Surface;
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(Surface, uv);
}
@@ -0,0 +1,10 @@
#version 450
// One surface as a quad: dst = the target rectangle in NDC, src = the buffer rectangle in UV.
layout(push_constant) uniform PC { vec4 dst; vec4 src; } pc;
layout(location = 0) out vec2 uv;
void main() {
vec2 t = vec2(gl_VertexIndex & 1, gl_VertexIndex >> 1);
gl_Position = vec4(mix(pc.dst.xy, pc.dst.zw, t), 0.0, 1.0);
uv = mix(pc.src.xy, pc.src.zw, t);
}
@@ -0,0 +1,22 @@
// 1115.1.0
#pragma once
const uint32_t blend_frag_code[] = {
0x07230203,0x00010000,0x0008000b,0x00000014,0x00000000,0x00020011,0x00000001,0x0006000b,
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000009,0x00000011,0x00030010,
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
0x00000000,0x00050005,0x00000009,0x4374756f,0x726f6c6f,0x00000000,0x00040005,0x0000000d,
0x66727553,0x00656361,0x00030005,0x00000011,0x00007675,0x00040047,0x00000009,0x0000001e,
0x00000000,0x00040047,0x0000000d,0x00000021,0x00000000,0x00040047,0x0000000d,0x00000022,
0x00000000,0x00040047,0x00000011,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,
0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,
0x00000004,0x00040020,0x00000008,0x00000003,0x00000007,0x0004003b,0x00000008,0x00000009,
0x00000003,0x00090019,0x0000000a,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,
0x00000001,0x00000000,0x0003001b,0x0000000b,0x0000000a,0x00040020,0x0000000c,0x00000000,
0x0000000b,0x0004003b,0x0000000c,0x0000000d,0x00000000,0x00040017,0x0000000f,0x00000006,
0x00000002,0x00040020,0x00000010,0x00000001,0x0000000f,0x0004003b,0x00000010,0x00000011,
0x00000001,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,
0x0004003d,0x0000000b,0x0000000e,0x0000000d,0x0004003d,0x0000000f,0x00000012,0x00000011,
0x00050057,0x00000007,0x00000013,0x0000000e,0x00000012,0x0003003e,0x00000009,0x00000013,
0x000100fd,0x00010038
};
@@ -0,0 +1,315 @@
/* Alpha composition: one surface drawn source-over onto the scene - see blend_pass.h. Compositor thread. */
#define _POSIX_C_SOURCE 200809L
#include "blend_pass.h"
#include "vk_loader.h"
#include "vk_present.h" /* banner_log, vkp_gpu_name */
#include <string.h>
/* SPIR-V, pre-compiled (glslangValidator -V blend.vert --vn blend_vert_code, the same for blend.frag)
* and committed next to the GLSL: the NDK build compiles no shaders. */
#include "blend_vert.h"
#include "blend_frag.h"
struct pc_blend { float dst[4]; float src[4]; };
static VkDevice g_dev;
static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */
static VkSampler g_sampler;
static VkDescriptorSetLayout g_dsl;
static VkPipelineLayout g_pl;
static VkDescriptorPool g_dpool;
static VkShaderModule g_vert, g_frag;
static unsigned g_frame = 1;
/* One render pass + pipeline per target format: the screen swapchain's, the scene image's, the HDR
* composition's. */
#define NFMT 4
static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT];
#define NSRC 32
static struct { VkImage img; VkImageView view; VkDescriptorSet ds; unsigned frame; } g_src[NSRC];
#define NDST 12
static struct { VkImage img; VkFormat fmt; int w, h; VkImageView view; VkFramebuffer fb; unsigned frame; } g_dst[NDST];
void blendp_bind_device(VkDevice dev) { g_dev = dev; }
void blendp_begin_frame(void) { g_frame++; }
static void src_release(int i) {
if (g_src[i].ds) g_vk.FreeDescriptorSets(g_dev, g_dpool, 1, &g_src[i].ds);
if (g_src[i].view) g_vk.DestroyImageView(g_dev, g_src[i].view, NULL);
memset(&g_src[i], 0, sizeof(g_src[i]));
}
static void dst_release(int i) {
if (g_dst[i].fb) g_vk.DestroyFramebuffer(g_dev, g_dst[i].fb, NULL);
if (g_dst[i].view) g_vk.DestroyImageView(g_dev, g_dst[i].view, NULL);
memset(&g_dst[i], 0, sizeof(g_dst[i]));
}
void blendp_forget_image(VkImage img) {
if (!g_dev || !img) return;
for (int i = 0; i < NSRC; i++) if (g_src[i].img == img) src_release(i);
for (int i = 0; i < NDST; i++) if (g_dst[i].img == img) dst_release(i);
}
static VkShaderModule shader(const uint32_t *code, size_t size) {
VkShaderModule m = VK_NULL_HANDLE;
VkShaderModuleCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = size, .pCode = code};
if (g_vk.CreateShaderModule(g_dev, &ci, NULL, &m) != VK_SUCCESS) return VK_NULL_HANDLE;
return m;
}
/* The target keeps what the blits put there (LOAD) and stays in TRANSFER_DST_OPTIMAL for the blits
* that follow. The dependencies order the pass between them. */
static VkRenderPass make_rp(VkFormat fmt) {
VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.initialLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL};
VkAttachmentReference ref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription sub = {.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, .colorAttachmentCount = 1,
.pColorAttachments = &ref};
VkSubpassDependency deps[2] = {
{.srcSubpass = VK_SUBPASS_EXTERNAL, .dstSubpass = 0,
.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT},
{.srcSubpass = 0, .dstSubpass = VK_SUBPASS_EXTERNAL,
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT |
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT}};
VkRenderPassCreateInfo rci = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, .attachmentCount = 1,
.pAttachments = &att, .subpassCount = 1, .pSubpasses = &sub,
.dependencyCount = 2, .pDependencies = deps};
VkRenderPass rp = VK_NULL_HANDLE;
if (g_vk.CreateRenderPass(g_dev, &rci, NULL, &rp) != VK_SUCCESS) return VK_NULL_HANDLE;
return rp;
}
static VkPipeline make_pipe(VkRenderPass rp) {
VkPipelineShaderStageCreateInfo st[2] = {
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = g_vert, .pName = "main"},
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = g_frag, .pName = "main"}};
VkPipelineVertexInputStateCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo ia = {.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP};
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo ds = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = 2, .pDynamicStates = dyn};
VkPipelineViewportStateCreateInfo vp = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.viewportCount = 1, .scissorCount = 1};
VkPipelineRasterizationStateCreateInfo rs = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.polygonMode = VK_POLYGON_MODE_FILL, .cullMode = VK_CULL_MODE_NONE,
.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, .lineWidth = 1.0f};
VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT};
/* The target's alpha is left alone: the screen is opaque whatever is drawn onto it. */
VkPipelineColorBlendAttachmentState ba = {
.blendEnable = VK_TRUE, .srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, .colorBlendOp = VK_BLEND_OP_ADD,
.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
.alphaBlendOp = VK_BLEND_OP_ADD,
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT};
VkPipelineColorBlendStateCreateInfo cb = {.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.attachmentCount = 1, .pAttachments = &ba};
VkGraphicsPipelineCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .stageCount = 2,
.pStages = st, .pVertexInputState = &vi, .pInputAssemblyState = &ia,
.pViewportState = &vp, .pRasterizationState = &rs, .pMultisampleState = &ms,
.pColorBlendState = &cb, .pDynamicState = &ds, .layout = g_pl,
.renderPass = rp, .subpass = 0};
VkPipeline p = VK_NULL_HANDLE;
if (g_vk.CreateGraphicsPipelines(g_dev, VK_NULL_HANDLE, 1, &pi, NULL, &p) != VK_SUCCESS) return VK_NULL_HANDLE;
return p;
}
static void objects_destroy(void) {
if (g_vert) g_vk.DestroyShaderModule(g_dev, g_vert, NULL);
if (g_frag) g_vk.DestroyShaderModule(g_dev, g_frag, NULL);
if (g_dpool) g_vk.DestroyDescriptorPool(g_dev, g_dpool, NULL);
if (g_pl) g_vk.DestroyPipelineLayout(g_dev, g_pl, NULL);
if (g_dsl) g_vk.DestroyDescriptorSetLayout(g_dev, g_dsl, NULL);
if (g_sampler) g_vk.DestroySampler(g_dev, g_sampler, NULL);
g_vert = g_frag = VK_NULL_HANDLE; g_dpool = VK_NULL_HANDLE; g_pl = VK_NULL_HANDLE;
g_dsl = VK_NULL_HANDLE; g_sampler = VK_NULL_HANDLE;
}
static int objects_ensure(void) {
if (g_ready) return g_ready == 1 ? 0 : -1;
g_ready = -1;
if (!g_dev || !g_vk.CreateGraphicsPipelines) goto fail;
VkSamplerCreateInfo sci = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE};
if (g_vk.CreateSampler(g_dev, &sci, NULL, &g_sampler) != VK_SUCCESS) goto fail;
VkDescriptorSetLayoutBinding b = {.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT};
VkDescriptorSetLayoutCreateInfo dli = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.bindingCount = 1, .pBindings = &b};
if (g_vk.CreateDescriptorSetLayout(g_dev, &dli, NULL, &g_dsl) != VK_SUCCESS) goto fail;
VkPushConstantRange pcr = {.stageFlags = VK_SHADER_STAGE_VERTEX_BIT, .offset = 0, .size = sizeof(struct pc_blend)};
VkPipelineLayoutCreateInfo pli = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
.pSetLayouts = &g_dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pcr};
if (g_vk.CreatePipelineLayout(g_dev, &pli, NULL, &g_pl) != VK_SUCCESS) goto fail;
VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NSRC};
VkDescriptorPoolCreateInfo dpi = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, .maxSets = NSRC,
.poolSizeCount = 1, .pPoolSizes = &ps};
if (g_vk.CreateDescriptorPool(g_dev, &dpi, NULL, &g_dpool) != VK_SUCCESS) goto fail;
if (!(g_vert = shader(blend_vert_code, sizeof(blend_vert_code)))) goto fail;
if (!(g_frag = shader(blend_frag_code, sizeof(blend_frag_code)))) goto fail;
g_ready = 1;
banner_log("gpu", "alpha composition ready on %s: translucent surfaces are blended over what is under them",
vkp_gpu_name());
return 0;
fail:
objects_destroy();
banner_log("error", "gpu: the alpha composition pass could not be built on this driver; translucent surfaces "
"are drawn opaque this session");
return -1;
}
static int out_slot(VkFormat fmt) {
int i;
for (i = 0; i < NFMT && g_out[i].pipe; i++) if (g_out[i].fmt == fmt) return i;
if (i == NFMT) return -1;
VkRenderPass rp = make_rp(fmt);
VkPipeline pipe = rp ? make_pipe(rp) : VK_NULL_HANDLE;
if (!pipe) {
if (rp) g_vk.DestroyRenderPass(g_dev, rp, NULL);
return -1;
}
g_out[i].fmt = fmt; g_out[i].rp = rp; g_out[i].pipe = pipe;
return i;
}
static VkImageView make_view(VkImage img, VkFormat fmt) {
VkImageView v = VK_NULL_HANDLE;
VkImageViewCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = img,
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = fmt,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (g_vk.CreateImageView(g_dev, &ci, NULL, &v) != VK_SUCCESS) return VK_NULL_HANDLE;
return v;
}
/* The slot to recycle: a free one, else the one unused for longest - never one this command buffer
* already refers to. -1 = every slot is in this frame. */
#define OLDEST(arr, n, out) do { \
unsigned age_ = 0; \
(out) = -1; \
for (int k_ = 0; k_ < (n); k_++) { \
if (!(arr)[k_].img) { (out) = k_; break; } \
if ((arr)[k_].frame != g_frame && g_frame - (arr)[k_].frame > age_) { age_ = g_frame - (arr)[k_].frame; (out) = k_; } \
} \
} while (0)
static VkDescriptorSet src_ensure(VkImage img, VkFormat fmt, VkImageLayout layout) {
int i;
for (i = 0; i < NSRC; i++)
if (g_src[i].img == img) { g_src[i].frame = g_frame; return g_src[i].ds; }
OLDEST(g_src, NSRC, i);
if (i < 0) return VK_NULL_HANDLE;
src_release(i);
if (!(g_src[i].view = make_view(img, fmt))) return VK_NULL_HANDLE;
VkDescriptorSetAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
.descriptorPool = g_dpool, .descriptorSetCount = 1, .pSetLayouts = &g_dsl};
if (g_vk.AllocateDescriptorSets(g_dev, &ai, &g_src[i].ds) != VK_SUCCESS) {
g_src[i].ds = VK_NULL_HANDLE;
src_release(i);
return VK_NULL_HANDLE;
}
VkDescriptorImageInfo ii = {.sampler = g_sampler, .imageView = g_src[i].view, .imageLayout = layout};
VkWriteDescriptorSet wr = {.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = g_src[i].ds, .dstBinding = 0,
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.pImageInfo = &ii};
g_vk.UpdateDescriptorSets(g_dev, 1, &wr, 0, NULL);
g_src[i].img = img; g_src[i].frame = g_frame;
return g_src[i].ds;
}
static VkFramebuffer dst_ensure(VkImage img, VkFormat fmt, int w, int h, VkRenderPass rp) {
int i;
for (i = 0; i < NDST; i++) {
if (g_dst[i].img != img) continue;
if (g_dst[i].fmt == fmt && g_dst[i].w == w && g_dst[i].h == h) { g_dst[i].frame = g_frame; return g_dst[i].fb; }
if (g_dst[i].frame == g_frame) return VK_NULL_HANDLE;
break;
}
if (i == NDST) OLDEST(g_dst, NDST, i);
if (i < 0) return VK_NULL_HANDLE;
dst_release(i);
if (!(g_dst[i].view = make_view(img, fmt))) return VK_NULL_HANDLE;
VkFramebufferCreateInfo fi = {.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .renderPass = rp,
.attachmentCount = 1, .pAttachments = &g_dst[i].view, .width = (uint32_t)w,
.height = (uint32_t)h, .layers = 1};
if (g_vk.CreateFramebuffer(g_dev, &fi, NULL, &g_dst[i].fb) != VK_SUCCESS) {
g_dst[i].fb = VK_NULL_HANDLE;
dst_release(i);
return VK_NULL_HANDLE;
}
g_dst[i].img = img; g_dst[i].fmt = fmt; g_dst[i].w = w; g_dst[i].h = h; g_dst[i].frame = g_frame;
return g_dst[i].fb;
}
static void src_layout(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImageLayout to) {
int sampling = to == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkImageMemoryBarrier b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = from, .newLayout = to,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1},
.srcAccessMask = sampling ? VK_ACCESS_TRANSFER_READ_BIT : VK_ACCESS_SHADER_READ_BIT,
.dstAccessMask = sampling ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT};
g_vk.CmdPipelineBarrier(cmd, sampling ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
sampling ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, NULL, 0, NULL, 1, &b);
}
int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general,
VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit) {
if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0 || objects_ensure() != 0) return -1;
int slot = out_slot(dst_fmt);
if (slot < 0) return -1;
VkImageLayout sampled = src_general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorSet ds = src_ensure(src, src_fmt, sampled);
if (!ds) return -1;
VkFramebuffer fb = dst_ensure(dst, dst_fmt, dst_w, dst_h, g_out[slot].rp);
if (!fb) return -1;
const VkOffset3D *d = blit->dstOffsets, *s = blit->srcOffsets;
int x0 = d[0].x < d[1].x ? d[0].x : d[1].x, x1 = d[0].x < d[1].x ? d[1].x : d[0].x;
int y0 = d[0].y < d[1].y ? d[0].y : d[1].y, y1 = d[0].y < d[1].y ? d[1].y : d[0].y;
if (x0 < 0) x0 = 0;
if (y0 < 0) y0 = 0;
if (x1 > dst_w) x1 = dst_w;
if (y1 > dst_h) y1 = dst_h;
if (x1 <= x0 || y1 <= y0) return 0;
struct pc_blend pc = {
.dst = {2.0f * d[0].x / dst_w - 1.0f, 2.0f * d[0].y / dst_h - 1.0f,
2.0f * d[1].x / dst_w - 1.0f, 2.0f * d[1].y / dst_h - 1.0f},
.src = {(float)s[0].x / src_w, (float)s[0].y / src_h, (float)s[1].x / src_w, (float)s[1].y / src_h}};
if (!src_general) src_layout(cmd, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sampled);
VkRenderPassBeginInfo rpi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = g_out[slot].rp,
.framebuffer = fb,
.renderArea = {{x0, y0}, {(uint32_t)(x1 - x0), (uint32_t)(y1 - y0)}}};
VkViewport vp = {0, 0, (float)dst_w, (float)dst_h, 0, 1};
g_vk.CmdBeginRenderPass(cmd, &rpi, VK_SUBPASS_CONTENTS_INLINE);
g_vk.CmdSetViewport(cmd, 0, 1, &vp);
g_vk.CmdSetScissor(cmd, 0, 1, &rpi.renderArea);
g_vk.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_out[slot].pipe);
g_vk.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_pl, 0, 1, &ds, 0, NULL);
g_vk.CmdPushConstants(cmd, g_pl, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc);
g_vk.CmdDraw(cmd, 4, 1, 0, 0);
g_vk.CmdEndRenderPass(cmd);
if (!src_general) src_layout(cmd, src, sampled, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
return 0;
}
@@ -0,0 +1,28 @@
#ifndef BLEND_PASS_H
#define BLEND_PASS_H
/*
* Alpha composition. The scene is composed with blits, which replace what is under them; a surface
* with translucent pixels (gamescope's overlay and notification planes over a game, a toolkit's
* shadowed popup) is drawn through this pass instead, source-over with premultiplied alpha.
*
* Compositor thread. Objects are built on the first use; if the driver refuses them the pass is
* unavailable for the session (logged once) and the caller keeps blitting.
*/
#define VK_USE_PLATFORM_ANDROID_KHR
#include <vulkan/vulkan.h>
/* Called from dev_init (device up). */
void blendp_bind_device(VkDevice dev);
/* A source or target image is about to be destroyed: drop what is cached for it. Frames are fenced,
* so nothing submitted still uses it. */
void blendp_forget_image(VkImage img);
/* A new command buffer is being recorded: what the previous one used may be recycled again. */
void blendp_begin_frame(void);
/* Record one surface over `dst`. `dst` is in TRANSFER_DST_OPTIMAL before and after. `src` was created
* with SAMPLED usage and is in GENERAL (src_general) or TRANSFER_SRC_OPTIMAL, and is left there.
* `blit` is the draw as the blit path maps it. 0 = recorded, -1 = unavailable (nothing recorded). */
int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general,
VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit);
#endif
@@ -0,0 +1,55 @@
// 1115.1.0
#pragma once
const uint32_t blend_vert_code[] = {
0x07230203,0x00010000,0x0008000b,0x0000003a,0x00000000,0x00020011,0x00000001,0x0006000b,
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
0x0008000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000c,0x0000001b,0x00000031,
0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,0x00000000,0x00030005,
0x00000009,0x00000074,0x00060005,0x0000000c,0x565f6c67,0x65747265,0x646e4978,0x00007865,
0x00060005,0x00000019,0x505f6c67,0x65567265,0x78657472,0x00000000,0x00060006,0x00000019,
0x00000000,0x505f6c67,0x7469736f,0x006e6f69,0x00070006,0x00000019,0x00000001,0x505f6c67,
0x746e696f,0x657a6953,0x00000000,0x00070006,0x00000019,0x00000002,0x435f6c67,0x4470696c,
0x61747369,0x0065636e,0x00070006,0x00000019,0x00000003,0x435f6c67,0x446c6c75,0x61747369,
0x0065636e,0x00030005,0x0000001b,0x00000000,0x00030005,0x0000001d,0x00004350,0x00040006,
0x0000001d,0x00000000,0x00747364,0x00040006,0x0000001d,0x00000001,0x00637273,0x00030005,
0x0000001f,0x00006370,0x00030005,0x00000031,0x00007675,0x00040047,0x0000000c,0x0000000b,
0x0000002a,0x00030047,0x00000019,0x00000002,0x00050048,0x00000019,0x00000000,0x0000000b,
0x00000000,0x00050048,0x00000019,0x00000001,0x0000000b,0x00000001,0x00050048,0x00000019,
0x00000002,0x0000000b,0x00000003,0x00050048,0x00000019,0x00000003,0x0000000b,0x00000004,
0x00030047,0x0000001d,0x00000002,0x00050048,0x0000001d,0x00000000,0x00000023,0x00000000,
0x00050048,0x0000001d,0x00000001,0x00000023,0x00000010,0x00040047,0x00000031,0x0000001e,
0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,
0x00000020,0x00040017,0x00000007,0x00000006,0x00000002,0x00040020,0x00000008,0x00000007,
0x00000007,0x00040015,0x0000000a,0x00000020,0x00000001,0x00040020,0x0000000b,0x00000001,
0x0000000a,0x0004003b,0x0000000b,0x0000000c,0x00000001,0x0004002b,0x0000000a,0x0000000e,
0x00000001,0x00040017,0x00000015,0x00000006,0x00000004,0x00040015,0x00000016,0x00000020,
0x00000000,0x0004002b,0x00000016,0x00000017,0x00000001,0x0004001c,0x00000018,0x00000006,
0x00000017,0x0006001e,0x00000019,0x00000015,0x00000006,0x00000018,0x00000018,0x00040020,
0x0000001a,0x00000003,0x00000019,0x0004003b,0x0000001a,0x0000001b,0x00000003,0x0004002b,
0x0000000a,0x0000001c,0x00000000,0x0004001e,0x0000001d,0x00000015,0x00000015,0x00040020,
0x0000001e,0x00000009,0x0000001d,0x0004003b,0x0000001e,0x0000001f,0x00000009,0x00040020,
0x00000020,0x00000009,0x00000015,0x0004002b,0x00000006,0x00000029,0x00000000,0x0004002b,
0x00000006,0x0000002a,0x3f800000,0x00040020,0x0000002e,0x00000003,0x00000015,0x00040020,
0x00000030,0x00000003,0x00000007,0x0004003b,0x00000030,0x00000031,0x00000003,0x00050036,
0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008,
0x00000009,0x00000007,0x0004003d,0x0000000a,0x0000000d,0x0000000c,0x000500c7,0x0000000a,
0x0000000f,0x0000000d,0x0000000e,0x0004006f,0x00000006,0x00000010,0x0000000f,0x0004003d,
0x0000000a,0x00000011,0x0000000c,0x000500c3,0x0000000a,0x00000012,0x00000011,0x0000000e,
0x0004006f,0x00000006,0x00000013,0x00000012,0x00050050,0x00000007,0x00000014,0x00000010,
0x00000013,0x0003003e,0x00000009,0x00000014,0x00050041,0x00000020,0x00000021,0x0000001f,
0x0000001c,0x0004003d,0x00000015,0x00000022,0x00000021,0x0007004f,0x00000007,0x00000023,
0x00000022,0x00000022,0x00000000,0x00000001,0x00050041,0x00000020,0x00000024,0x0000001f,
0x0000001c,0x0004003d,0x00000015,0x00000025,0x00000024,0x0007004f,0x00000007,0x00000026,
0x00000025,0x00000025,0x00000002,0x00000003,0x0004003d,0x00000007,0x00000027,0x00000009,
0x0008000c,0x00000007,0x00000028,0x00000001,0x0000002e,0x00000023,0x00000026,0x00000027,
0x00050051,0x00000006,0x0000002b,0x00000028,0x00000000,0x00050051,0x00000006,0x0000002c,
0x00000028,0x00000001,0x00070050,0x00000015,0x0000002d,0x0000002b,0x0000002c,0x00000029,
0x0000002a,0x00050041,0x0000002e,0x0000002f,0x0000001b,0x0000001c,0x0003003e,0x0000002f,
0x0000002d,0x00050041,0x00000020,0x00000032,0x0000001f,0x0000000e,0x0004003d,0x00000015,
0x00000033,0x00000032,0x0007004f,0x00000007,0x00000034,0x00000033,0x00000033,0x00000000,
0x00000001,0x00050041,0x00000020,0x00000035,0x0000001f,0x0000000e,0x0004003d,0x00000015,
0x00000036,0x00000035,0x0007004f,0x00000007,0x00000037,0x00000036,0x00000036,0x00000002,
0x00000003,0x0004003d,0x00000007,0x00000038,0x00000009,0x0008000c,0x00000007,0x00000039,
0x00000001,0x0000002e,0x00000034,0x00000037,0x00000038,0x0003003e,0x00000031,0x00000039,
0x000100fd,0x00010038
};
+107 -18
View File
@@ -238,6 +238,8 @@ struct surface {
struct dmabuf_buffer *dmabuf_buf; /* its imported image, kept until replaced or the surface goes */
struct wl_listener dmabuf_destroy;
int buf_w, buf_h, has_content;
int buf_alpha;
int opaque[4], pending_opaque[4], pending_opaque_set; /* x, y, w, h; w = 0: none */
int src_set, dst_set;
float src[4];
int dst[2];
@@ -433,9 +435,16 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
* delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier
* limit had spaced out, and the game's first frames after that waited on empty slots. */
static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) {
int limit = g_fps_limit;
if (limit <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
int64_t interval = 1000000000LL / limit, now = now_ns();
const int limit = g_fps_limit;
/* Without a limit of its own a game is paced to the screen. A swapchain that does not wait for
* frame callbacks (MAILBOX, IMMEDIATE) is held back by nothing else here, so handing a replaced
* buffer straight back let it draw as fast as the GPU allowed and everything above the refresh
* rate was thrown away - 178 frames a second for 91 on screen, measured (WinNative, maxjivi05).
* One release per refresh is the back-pressure X11 gives, and the screen shows the same frames
* either way. */
const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns;
if (interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
int64_t now = now_ns();
int64_t at = s->next_release_ns + interval;
int64_t latest = now + interval * (int64_t)(s->releases_pending + 1);
if (at < now) at = now;
@@ -466,6 +475,12 @@ static struct seat_pointer g_ptrs[MAX_PTRS];
static int g_nptrs;
static struct seat_keyboard g_kbs[MAX_PTRS];
static int g_nkbs;
/* Modifier state in the keymap's real-modifier bits (Shift=0x1, Lock=0x2, Control=0x4, Mod1=0x8,
* Mod4=0x40). A client of the wayland backend - gamescope, labwc - takes modifiers only from
* wl_keyboard.modifiers and ignores what a Shift key event does to its xkb state, so without this
* every capital arrived lowercase. */
static uint32_t g_mods_depressed, g_mods_locked;
static uint32_t g_mod_keys_held; /* one bit per modifier key below, so a repeat press is harmless */
static struct seat_touch g_touches[MAX_PTRS];
static int g_ntouches;
@@ -716,6 +731,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
wl_buffer_send_release(buffer);
s->buf_w = w;
s->buf_h = h;
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
s->has_content = s->shm_img != NULL;
g_stat_shm++;
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
@@ -767,6 +783,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
}
s->buf_w = b->width;
s->buf_h = b->height;
s->buf_alpha = (b->format & 0xff) == 'A';
s->has_content = b->img != NULL;
g_stat_dmabuf++;
const int announced_now = !s->announced_vulkan;
@@ -938,8 +955,27 @@ static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t c
wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy);
wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback));
}
/* The bounding box serves pointer confinement. The exact flag also tells opaque-region handling
* whether the bounding box itself is the whole region. */
struct region { int set, exact; int x, y, w, h; };
/* A wl_surface's opaque region is pending until its next commit. An exact region that covers
* the surface lets the blit path keep handling an otherwise alpha-capable buffer. */
static void surface_set_opaque(struct wl_client *c, struct wl_resource *r,
struct wl_resource *region) {}
struct wl_resource *region) {
struct surface *s = wl_resource_get_user_data(r);
struct region *rg = region ? wl_resource_get_user_data(region) : NULL;
memset(s->pending_opaque, 0, sizeof(s->pending_opaque));
s->pending_opaque_set = 1;
if (rg && rg->set && rg->exact) {
s->pending_opaque[0] = rg->x;
s->pending_opaque[1] = rg->y;
s->pending_opaque[2] = rg->w;
s->pending_opaque[3] = rg->h;
}
struct client_info *ci = client_info_of(c);
if (ci) ci->declares_opaque = 1;
}
static void surface_set_input(struct wl_client *c, struct wl_resource *r,
struct wl_resource *region) {}
@@ -1030,6 +1066,10 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
memcpy(s->dst, s->pending_dst, sizeof(s->dst));
s->pending_dst_set = 0;
}
if (s->pending_opaque_set) {
memcpy(s->opaque, s->pending_opaque, sizeof(s->opaque));
s->pending_opaque_set = 0;
}
if (s->pending_attach) {
struct wl_resource *buffer = s->pending_buffer;
@@ -1168,24 +1208,29 @@ static void surface_resource_destroy(struct wl_resource *r) {
/* ------------------------------------------------------------------ wl_region */
/* A region is kept as the bounding box of its rectangles: enough for pointer confinement,
* where winewayland sends one rectangle (the ClipCursor area). Subtractions are ignored. */
struct region { int set; int x, y, w, h; };
static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void region_add(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {
struct region *rg = wl_resource_get_user_data(r);
if (!rg || w <= 0 || h <= 0) return;
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = 1; return; }
int x2 = rg->x + rg->w > x + w ? rg->x + rg->w : x + w;
int y2 = rg->y + rg->h > y + h ? rg->y + rg->h : y + h;
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = rg->exact = 1; return; }
long long x2 = (long long)rg->x + rg->w, y2 = (long long)rg->y + rg->h;
long long nx2 = (long long)x + w, ny2 = (long long)y + h;
int inside = x >= rg->x && y >= rg->y && nx2 <= x2 && ny2 <= y2;
int around = x <= rg->x && y <= rg->y && nx2 >= x2 && ny2 >= y2;
if (!inside && !around) rg->exact = 0;
if (nx2 > x2) x2 = nx2;
if (ny2 > y2) y2 = ny2;
if (x < rg->x) rg->x = x;
if (y < rg->y) rg->y = y;
rg->w = x2 - rg->x; rg->h = y2 - rg->y;
rg->w = (int)(x2 - rg->x > INT32_MAX ? INT32_MAX : x2 - rg->x);
rg->h = (int)(y2 - rg->y > INT32_MAX ? INT32_MAX : y2 - rg->y);
}
static void region_subtract(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
int32_t x, int32_t y, int32_t w, int32_t h) {
struct region *rg = wl_resource_get_user_data(r);
if (rg && w > 0 && h > 0) rg->exact = 0;
}
static void region_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); }
static const struct wl_region_interface region_impl = {
.destroy = region_destroy,
@@ -1659,6 +1704,17 @@ static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, i
g_hdr_unimported_below = dl->n;
}
/* A client that describes opaque regions can intentionally leave part of an alpha buffer see-through.
* Gamescope uses this for its full-size Steam notification and overlay planes. */
static int surface_translucent(const struct surface *s, int w, int h) {
if (!s->buf_alpha) return 0;
const struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
if (!ci || !ci->declares_opaque) return 0;
const int *o = s->opaque;
return !(o[2] > 0 && o[0] <= 0 && o[1] <= 0 &&
(long long)o[0] + o[2] >= w && (long long)o[1] + o[3] >= h);
}
static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy) {
struct vkp_image *img = surface_image(s);
float sx = 0, sy = 0, sw = (float)s->buf_w, sh = (float)s->buf_h;
@@ -1678,7 +1734,8 @@ static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy)
dl->d = d;
dl->cap = cap;
}
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh};
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh,
surface_translucent(s, dw, dh)};
s->drawn = 1;
}
@@ -1749,6 +1806,7 @@ static int on_frame_timer(void *data) {
static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) {
for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) {
const struct vkp_draw *d = &dl->d[i];
if (d->blend) continue; /* the translucent plane must not replace the opaque game layer */
if (!vkp_image_is_dmabuf(d->img)) continue;
if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue;
if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) ||
@@ -2087,7 +2145,7 @@ static void render_scene(void) {
* its own layer, cropped and placed by the display. */
int go[8], ov = 0;
if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go))
ov = sc_layer_present_overlay(dl.d[li + 1].img, go) == 0 ? 1 : -1;
ov = sc_layer_present_overlay(dl.d[li + 1].img, go, dl.d[li + 1].blend) == 0 ? 1 : -1;
if (ov <= 0) sc_layer_hide_overlay();
if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */
sc_layer_hide();
@@ -2370,8 +2428,8 @@ static void keyboard_focus(struct wl_resource *target) {
if (sk->focus == target) continue;
sk->focus = target;
wl_keyboard_send_enter(sk->kb, wl_display_next_serial(g_display), target, &keys);
/* Baseline modifiers = none; Shift/Ctrl arrive as their own key events. */
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display), 0, 0, 0, 0);
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display),
g_mods_depressed, 0, g_mods_locked, 0);
}
wl_array_release(&keys);
banner_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */
@@ -3068,7 +3126,34 @@ static void scroll_event(int steps) {
wl_display_flush_clients(g_display);
}
/* Folds a key into the modifier state; returns whether the state changed. */
static int update_modifiers(uint32_t evdev, int pressed) {
static const struct { uint32_t evdev, mask; } mod_keys[] = {
{ 42, 0x1 }, { 54, 0x1 }, /* Shift L/R */
{ 29, 0x4 }, { 97, 0x4 }, /* Control L/R */
{ 56, 0x8 }, { 100, 0x8 }, /* Alt L/R, both Mod1 in this keymap */
{ 125, 0x40 }, { 126, 0x40 }, /* Super L/R */
};
uint32_t depressed = g_mods_depressed, locked = g_mods_locked;
if (evdev == 58) { /* Caps Lock toggles on press */
if (pressed) locked ^= 0x2;
} else {
int i, n = (int)(sizeof(mod_keys) / sizeof(mod_keys[0]));
for (i = 0; i < n && mod_keys[i].evdev != evdev; i++) {}
if (i == n) return 0;
if (pressed) g_mod_keys_held |= 1u << i; else g_mod_keys_held &= ~(1u << i);
depressed = 0;
for (int j = 0; j < n; j++) if (g_mod_keys_held & (1u << j)) depressed |= mod_keys[j].mask;
}
if (depressed == g_mods_depressed && locked == g_mods_locked) return 0;
g_mods_depressed = depressed;
g_mods_locked = locked;
return 1;
}
static void key_event(uint32_t evdev, int pressed) {
/* Tracked even with nothing to deliver to, so a Shift released unseen is not left held. */
int mods_changed = update_modifiers(evdev, pressed);
/* Keys go to the program window the user last clicked (else the topmost non-shell window),
* never to Wine's desktop surface: winewayland hands each key to the hwnd of the surface that
* holds keyboard focus, and a key handed to explorer's desktop hwnd is queued to explorer's
@@ -3088,9 +3173,13 @@ static void key_event(uint32_t evdev, int pressed) {
struct seat_keyboard *sk;
if (!keyboard_for(client)) return;
keyboard_focus(target->resource);
for_each_keyboard_of(client, sk)
for_each_keyboard_of(client, sk) {
wl_keyboard_send_key(sk->kb, wl_display_next_serial(g_display), now_ms(), evdev,
pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
if (mods_changed)
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display),
g_mods_depressed, 0, g_mods_locked, 0);
}
wl_display_flush_clients(g_display);
}
@@ -60,6 +60,9 @@ struct client_info {
/* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's
* Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */
unsigned asked_feedback : 1, shm_gl_said : 1;
/* Clients that describe opaque regions can use alpha outside those regions. Wine windows
* never describe them, and their unused alpha channel must not make them translucent. */
unsigned declares_opaque : 1;
unsigned dmabuf_buffers, shm_frames;
struct client_info *next;
};
+10 -9
View File
@@ -85,6 +85,7 @@ static struct {
#define ASC_VISIBILITY_HIDE 0
#define ASC_VISIBILITY_SHOW 1
#define ASC_TRANSPARENCY_TRANSLUCENT 1
#define ASC_TRANSPARENCY_OPAQUE 2
static int load_api(void) {
@@ -766,7 +767,8 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) {
/* The transaction that puts pool slot `idx` of layer `l` on screen at `r`, as `color` (NULL = no
* description). 0 = applied. */
static int present_slot(struct layer *l, int idx, const int r[8], const struct banner_color *color) {
static int present_slot(struct layer *l, int idx, const int r[8], const struct banner_color *color,
int translucent) {
struct slot *s = &l->slots[idx];
ASurfaceTransaction *tx = api.txCreate();
if (!tx) return -1;
@@ -774,9 +776,8 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct b
if (old) api.setZOrder(tx, l->sc, l->z);
/* The blit was waited for on the CPU, so no acquire fence is needed (-1). */
api.setBuffer(tx, l->sc, s->ahb, -1);
/* The compositor composes with blits, which overwrite: nothing is ever alpha-blended on the
* copy path either, so every layer is opaque and the picture matches it pixel for pixel. */
api.setBufferTransparency(tx, l->sc, ASC_TRANSPARENCY_OPAQUE);
api.setBufferTransparency(tx, l->sc,
translucent ? ASC_TRANSPARENCY_TRANSLUCENT : ASC_TRANSPARENCY_OPAQUE);
apply_geometry(tx, l, r);
apply_frame_rate(tx, l);
apply_colour(tx, l, color);
@@ -883,7 +884,7 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru
int idx = take_free_slot(l, sw, sh, AHB_RGBA8, &wait_fd);
if (idx < 0) { log_drop(l); return 0; }
if (vkp_blit_image(src, l->slots[idx].img, wait_fd) != 0) return -1;
if (present_slot(l, idx, r, color) != 0) return -1;
if (present_slot(l, idx, r, color, 0) != 0) return -1;
if (color && color->dataspace) banner_color_frame_shown(color, BANNER_HDR_LAYER_COPY, AHB_RGBA8);
if (!l->first_logged) {
l->first_logged = 1;
@@ -908,7 +909,7 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
int idx = take_free_slot(l, rw, rh, AHB_RGBA8, &wait_fd);
if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; }
if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1;
if (present_slot(l, idx, r, NULL) != 0) return -1; /* the effects chain's result is 8-bit sRGB */
if (present_slot(l, idx, r, NULL, 0) != 0) return -1; /* the effects chain's result is 8-bit sRGB */
if (!l->first_logged) {
l->first_logged = 1;
banner_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); "
@@ -950,7 +951,7 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
}
if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; }
if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1;
if (present_slot(l, idx, r, tm ? NULL : color) != 0) return -1; /* tone-mapped = plain sRGB: no tag */
if (present_slot(l, idx, r, tm ? NULL : color, 0) != 0) return -1; /* tone-mapped = plain sRGB: no tag */
if (color && color->dataspace)
banner_color_frame_shown(color, tm ? BANNER_HDR_TONEMAPPED : BANNER_HDR_COMPOSED, tm ? AHB_RGBA8 : g_hdr_pool_fmt);
static int said = -1;
@@ -1031,7 +1032,7 @@ int sc_layer_overlay_affordable(void) {
return ok;
}
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]) {
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent) {
struct layer *l = layer_of(SC_LAYER_OVERLAY);
if (!src || !geo) return -1;
/* The caller decides with sc_layer_overlay_affordable() BEFORE it commits the game to a layer;
@@ -1046,7 +1047,7 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]) {
/* The window is copied into the layer buffer 1:1; `geo` crops it and places it, so the layer
* is exactly the window's rectangle on screen and nothing else is blended anywhere. */
if (vkp_blit_image(src, l->slots[idx].img, wait_fd) != 0) return -1;
if (present_slot(l, idx, geo, NULL) != 0) return -1;
if (present_slot(l, idx, geo, NULL, translucent) != 0) return -1;
if (!l->first_logged) {
l->first_logged = 1;
log_layer_count();
+2 -1
View File
@@ -150,8 +150,9 @@ int sc_layer_overlay_affordable(void);
/* OVERLAY layer: show `src` (one window's imported frame) above the game layer, at the placement
* `geo` = {src x0,y0,x1,y1 in image pixels, dst x0,y0,x1,y1 in output pixels} from vkp_map_draw.
* `translucent` preserves a client's alpha over the game for Steam notifications and overlays.
* 0 = shown or dropped, -1 = unavailable (the caller must fall back to the copy path). */
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]);
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent);
/* The scene is not a single fullscreen window this frame: hide every layer that is up. */
void sc_layer_hide(void);
+65 -23
View File
@@ -7,6 +7,7 @@
#include "effects_chain.h"
#include "framegen_bridge.h"
#include "hdr_compose.h"
#include "blend_pass.h"
#include "banner_color.h"
#include <stdio.h>
#include <stdlib.h>
@@ -32,6 +33,8 @@ struct vkp_image {
void *map; /* shm images: persistently mapped linear memory */
VkDeviceSize offset, row_pitch;
int in_general; /* shm images: moved from PREINITIALIZED to GENERAL */
VkFormat fmt;
int sampled; /* created with SAMPLED usage: the alpha pass can read it */
};
static ANativeWindow *g_window; /* the window frames go to; compositor thread only */
@@ -230,6 +233,7 @@ static void destroy_swapchain(void) {
if (g_dev_state != 1) return;
g_vk.DeviceWaitIdle(g_dev);
reset_sync();
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
g_swapchain = VK_NULL_HANDLE;
g_swap_md_identity = 0; /* a new swapchain gets the metadata again */
@@ -525,6 +529,7 @@ static int dev_init(void) {
vkp_effects_bind_device(g_dev, g_pd, &g_memprops);
hdrc_bind_device(g_dev, &g_memprops);
blendp_bind_device(g_dev);
g_dev_state = 1;
reset_sync();
init_black_image();
@@ -873,9 +878,8 @@ const char *vkp_modifier_name(uint64_t modifier) {
return other;
}
/* Can the driver create the image vkp_image_import_dmabuf() creates for this format+modifier
* (2D, DRM_FORMAT_MODIFIER tiling, TRANSFER_SRC, dma-buf memory) - and import it? */
static int modifier_importable(VkFormat fmt, uint64_t modifier) {
/* Can the driver create and import this format/modifier with the requested image usage? */
static int modifier_importable(VkFormat fmt, uint64_t modifier, VkImageUsageFlags usage) {
if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; /* can't ask; the create will tell */
VkPhysicalDeviceExternalImageFormatInfo ext = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO,
@@ -886,7 +890,7 @@ static int modifier_importable(VkFormat fmt, uint64_t modifier) {
VkPhysicalDeviceImageFormatInfo2 info = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .pNext = &mod,
.format = fmt, .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT};
.usage = usage};
VkExternalImageFormatProperties extp = {.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES};
VkImageFormatProperties2 props = {.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, .pNext = &extp};
if (g_vk.GetPhysicalDeviceImageFormatProperties2(g_pd, &info, &props) != VK_SUCCESS) return 0;
@@ -914,7 +918,7 @@ int vkp_dmabuf_modifiers(uint32_t drm_format, uint64_t *out, int max) {
if (m != VKP_MOD_LINEAR && m != VKP_MOD_QCOM_COMPRESSED) why = "unknown layout";
else if (props[i].drmFormatModifierPlaneCount != 1) why = "not single-plane";
else if (!(props[i].drmFormatModifierTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT)) why = "no blit source";
else if (!modifier_importable(fmt, m)) why = "not importable as a dma-buf";
else if (!modifier_importable(fmt, m, VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) why = "not importable as a dma-buf";
if (why) {
LOGI("dmabuf: %c%c%c%c %s reported by the driver but not advertised: %s",
drm_format & 0xff, (drm_format >> 8) & 0xff, (drm_format >> 16) & 0xff,
@@ -942,6 +946,10 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
struct vkp_image *img = calloc(1, sizeof(*img));
if (!img) return NULL;
img->w = w; img->h = h; img->dmabuf = 1; img->blit_dst = as_blit_dst ? 1 : 0;
img->fmt = drm_to_vk(drm_format);
const VkImageUsageFlags sampled_usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
img->sampled = !as_blit_dst && g_vk.GetPhysicalDeviceImageFormatProperties2 &&
modifier_importable(img->fmt, modifier, sampled_usage);
VkSubresourceLayout plane = {.offset = offset, .rowPitch = stride};
VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = {
@@ -952,10 +960,11 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT};
VkImageCreateInfo ici = {
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = &extImg,
.imageType = VK_IMAGE_TYPE_2D, .format = drm_to_vk(drm_format), .extent = {w, h, 1},
.imageType = VK_IMAGE_TYPE_2D, .format = img->fmt, .extent = {w, h, 1},
.mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
.usage = as_blit_dst ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.usage = as_blit_dst ? VK_IMAGE_USAGE_TRANSFER_DST_BIT
: img->sampled ? sampled_usage : VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
VkResult cr = g_vk.CreateImage(g_dev, &ici, NULL, &img->image);
if (cr != VK_SUCCESS) {
@@ -1008,18 +1017,35 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
return img;
}
/* Linear wl_shm images can use the blend pass only if the driver samples them with a linear filter. */
static int shm_sampled(void) {
static int known;
if (!known) {
const VkFormatFeatureFlags need = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
VkFormatProperties2 fp = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2};
if (g_vk.GetPhysicalDeviceFormatProperties2)
g_vk.GetPhysicalDeviceFormatProperties2(g_pd, VK_FORMAT_B8G8R8A8_UNORM, &fp);
known = (fp.formatProperties.linearTilingFeatures & need) == need ? 1 : -1;
}
return known == 1;
}
struct vkp_image *vkp_image_create_shm(int w, int h) {
if (w <= 0 || h <= 0 || dev_init() != 0) return NULL;
struct vkp_image *img = calloc(1, sizeof(*img));
if (!img) return NULL;
img->w = w; img->h = h;
img->fmt = VK_FORMAT_B8G8R8A8_UNORM;
img->sampled = shm_sampled();
VkImageCreateInfo ici = {
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = VK_FORMAT_B8G8R8A8_UNORM, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
.format = img->fmt, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | (img->sampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0),
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED};
if (g_vk.CreateImage(g_dev, &ici, NULL, &img->image) != VK_SUCCESS) { free(img); return NULL; }
@@ -1062,6 +1088,7 @@ int vkp_image_height(const struct vkp_image *img) { return img ? img->h : 0; }
void vkp_image_destroy(struct vkp_image *img) {
if (!img) return;
if (g_dev) {
blendp_forget_image(img->image);
if (img->map) g_vk.UnmapMemory(g_dev, img->mem);
if (img->image) g_vk.DestroyImage(g_dev, img->image, NULL);
if (img->mem) g_vk.FreeMemory(g_dev, img->mem, NULL);
@@ -1111,8 +1138,21 @@ static int draw_to_scene_blit(const struct vkp_draw *d, int scene_w, int scene_h
return map_draw(d, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, (float)scene_w, (float)scene_h, blit);
}
/* A translucent draw needs source-over blending; every other draw keeps the existing blit path. */
static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkImageBlit *blit, VkImage target,
VkFormat target_fmt, int target_w, int target_h, VkFilter filter) {
const struct vkp_image *im = d->img;
if (d->blend && im->sampled &&
blendp_draw(cmd, im->image, im->fmt, im->w, im->h, !im->dmabuf, target, target_fmt, target_w, target_h,
blit) == 0)
return;
g_vk.CmdBlitImage(cmd, im->image, im->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, filter);
}
static void destroy_scene_image(void) {
if (g_scene.img) hdrc_forget_image(g_scene.img); /* the HDR pass may have drawn into it */
if (g_scene.img) blendp_forget_image(g_scene.img);
if (g_scene.img) g_vk.DestroyImage(g_dev, g_scene.img, NULL);
if (g_scene.mem) g_vk.FreeMemory(g_dev, g_scene.mem, NULL);
memset(&g_scene, 0, sizeof(g_scene));
@@ -1150,6 +1190,7 @@ static int ensure_scene_image(int w, int h) {
static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImageUsageFlags usage, const char *what) {
if (s->img && s->w == w && s->h == h && s->fmt == fmt) return 0;
if (s->img) hdrc_forget_image(s->img);
if (s->img) blendp_forget_image(s->img);
if (s->img) g_vk.DestroyImage(g_dev, s->img, NULL);
if (s->mem) g_vk.FreeMemory(g_dev, s->mem, NULL);
memset(s, 0, sizeof(*s));
@@ -1190,7 +1231,8 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
int scene_w, int scene_h, VkImage out, VkFormat out_fmt, enum hdrc_mode mode) {
if (!hf || !hf->is_hdr || n <= 0) return -1;
if (ensure_img(&g_mixed, scene_w, scene_h, HDR_FMT,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
"HDR composition (mixed)") != 0)
return -1;
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
@@ -1214,9 +1256,7 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
VkImageBlit blit;
if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue;
/* A blit converts UNORM to UNORM by value: 8-bit sRGB and 10-bit PQ both land unchanged. */
g_vk.CmdBlitImage(cmd, draws[i].img->image,
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
g_mixed.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
record_draw(cmd, &draws[i], &blit, g_mixed.img, HDR_FMT, scene_w, scene_h, VK_FILTER_LINEAR);
if (hf->is_hdr[i] && lowest_hdr < 0) lowest_hdr = i;
}
static int truncated_said;
@@ -1249,6 +1289,7 @@ static void device_lost(const char *where) {
vkp_framegen_device_lost();
banner_log("error", "GPU device lost (VK_ERROR_DEVICE_LOST in %s): the compositor has stopped presenting; "
"restart the session", where);
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
g_swapchain = VK_NULL_HANDLE;
if (g_surface) g_vk.DestroySurfaceKHR(g_inst, g_surface, NULL);
@@ -1547,6 +1588,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
g_vk.BeginCommandBuffer(cmd, &bi);
blendp_begin_frame();
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
/* Source images: take dmabufs from the client's queue family, move shm images to
@@ -1589,8 +1631,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
free(bars);
/* Clear the composition target to black (the letterbox on the direct path, the desktop's
* background on the pass) before the draws land on it. A draw that covers the whole scene
* overwrites every pixel of its rectangle (blits never blend), so then only what lies outside it
* background on the pass) before the draws land on it. An opaque draw that covers the whole scene
* overwrites every pixel of its rectangle, so then only what lies outside it
* needs black: the letterbox bars on the direct path, nothing at all on the pass (the scene image
* is exactly the scene). Anything else - no such draw, the HDR composition - clears the whole
* target as before. */
@@ -1600,7 +1642,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
if (!(hf && pass)) {
for (int i = n - 1; i >= 0 && !covered; i--) {
const struct vkp_draw *d = &draws[i];
if (!d->img || d->dx > 0 || d->dy > 0 || d->dx + d->dw < scene_w || d->dy + d->dh < scene_h) continue;
if (!d->img || d->blend || d->dx > 0 || d->dy > 0 || d->dx + d->dw < scene_w ||
d->dy + d->dh < scene_h) continue;
covered = pass ? draw_to_scene_blit(d, scene_w, scene_h, &cover_blit) : draw_to_blit(d, &cover_blit);
}
}
@@ -1633,10 +1676,9 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
VkImageBlit blit;
if (!draws[i].img) continue;
if (pass ? !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit) : !draw_to_blit(&draws[i], &blit)) continue;
g_vk.CmdBlitImage(cmd, draws[i].img->image,
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit,
pass ? VK_FILTER_LINEAR : blit_filter);
record_draw(cmd, &draws[i], &blit, target, pass ? scene_fmt : g_swap_fmt,
pass ? scene_w : (int)g_extent.width, pass ? scene_h : (int)g_extent.height,
pass ? VK_FILTER_LINEAR : blit_filter);
drawn++;
}
@@ -2073,6 +2115,7 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
g_vk.BeginCommandBuffer(cmd, &bi);
blendp_begin_frame();
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
/* Same acquisition as vkp_render's compositor pass: the sources come from the client's queue
@@ -2132,9 +2175,8 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
for (int i = 0; i < n; i++) {
VkImageBlit blit;
if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue;
g_vk.CmdBlitImage(cmd, draws[i].img->image,
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
g_scene.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
record_draw(cmd, &draws[i], &blit, g_scene.img, SCENE_FMT, scene_w, scene_h,
VK_FILTER_LINEAR);
}
if (vkp_effects_active()) vkp_effects_set_formats(VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM);
}
@@ -79,6 +79,7 @@ struct vkp_draw {
struct vkp_image *img;
float sx, sy, sw, sh;
int dx, dy, dw, dh;
int blend; /* translucent: composed over what is under it (blend_pass.h) */
};
// The GPU the renderer runs on ("Adreno (TM) 750"), empty before the device is up.
@@ -126,7 +126,8 @@ class MainActivity : ComponentActivity() {
private var gameCores by mutableStateOf<Set<Int>>(emptySet())
private var tuSysmem by mutableStateOf(false)
private var zinkLazy by mutableStateOf(false)
private var noXalia by mutableStateOf(false)
private var noXalia by mutableStateOf(true)
private var gamescopeRealtime by mutableStateOf(false)
private var prootNoSeccomp by mutableStateOf(false)
private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME)
private var phantomWarning by mutableStateOf<String?>(null)
@@ -503,8 +504,9 @@ class MainActivity : ComponentActivity() {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
// The microphone is on by default; ask once, with the storage prompt, so voice chat works
// without a trip to the settings. A refusal is not asked again - the toggle asks when used.
// The microphone is off by default. Someone who turned it on before the permission was
// granted is asked once here, with the storage prompt; a refusal is not asked again - the
// toggle asks when used.
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
wanted.add(Manifest.permission.RECORD_AUDIO)
@@ -805,6 +807,7 @@ class MainActivity : ComponentActivity() {
storageOptions = storageOptions,
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
addedGamesDirs = if (mode == SessionService.MODE_STEAM) addedGamesDirs else null,
addedGames = if (mode == SessionService.MODE_STEAM) addedGames else emptyList(),
@@ -856,6 +859,11 @@ class MainActivity : ComponentActivity() {
},
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
onSteamChannel = { id -> SessionPrefs.setSteamChannel(this, id); steamChannel = id },
onSteamDeckMode = { on ->
SessionPrefs.setSteamDeckMode(this, on)
steamDeckMode = on
steamChannel = SessionPrefs.steamChannel(this)
},
onRunSteamAtStartup = { on ->
SessionPrefs.setRunSteamAtStartup(this, on)
runSteamAtStartup = on
@@ -886,15 +894,16 @@ class MainActivity : ComponentActivity() {
PerformancePage(
cores = CpuCores.all.map { c -> CoreRow(c, "cpu$c" + (CpuCores.maxGhz(c)?.let { String.format(java.util.Locale.US, " · %.1f GHz", it) } ?: "")) },
clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores,
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, steamDeckMode = steamDeckMode, noXalia = noXalia,
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia,
gamescopeRealtime = gamescopeRealtime,
prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning,
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on },
onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on },
onGlThread = { on -> SessionPrefs.setGlThread(this, on); glThread = on },
onNoGlError = { on -> SessionPrefs.setNoGlError(this, on); noGlError = on },
onSteamDeckMode = { on -> SessionPrefs.setSteamDeckMode(this, on); steamDeckMode = on },
onNoXalia = { on -> SessionPrefs.setNoXalia(this, on); noXalia = on },
onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on },
onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on },
onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) },
onClientCore = { core, on ->
@@ -939,6 +948,7 @@ class MainActivity : ComponentActivity() {
customResolution = SessionPrefs.customResolution(this, mode)
fexPreset = SessionPrefs.fexPreset(this)
steamChannel = SessionPrefs.steamChannel(this)
steamDeckMode = SessionPrefs.steamDeckMode(this)
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
refreshAddedGames()
@@ -988,6 +998,7 @@ class MainActivity : ComponentActivity() {
tuSysmem = SessionPrefs.tuSysmem(this)
zinkLazy = SessionPrefs.zinkLazy(this)
noXalia = SessionPrefs.noXalia(this)
gamescopeRealtime = SessionPrefs.gamescopeRealtime(this)
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
guestHostname = SessionPrefs.guestHostname(this)
refreshPhantomStatus()
@@ -47,7 +47,6 @@ import com.droiddeck.launcher.input.SecondScreenMode
import com.droiddeck.launcher.input.TouchpadGestures
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.session.LoadingState
import com.droiddeck.launcher.session.PerfHints
import com.droiddeck.launcher.session.PerfHud
import com.droiddeck.launcher.session.PerfMode
import com.droiddeck.launcher.session.SessionPrefs
@@ -203,8 +202,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
SessionEvents.record("agent.start_requested", mapOf("mode" to SessionState.mode))
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// Game-tier power policy for the whole session: a sustained clock floor, the panel's
// fastest mode, and the OS told it is in gameplay. Logged so a slow device says why.
// Game-tier power policy for the whole session: the panel's fastest mode, and the OS told
// it is in gameplay. Logged so a slow device says why.
Log.i(TAG, "perf: " + PerfMode.apply(this))
goFullscreen()
// The device's volume keys change the stream the session plays on (the relay and
@@ -460,7 +459,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
performBackAction(double = false)
}
pendingBackAction = pending
uiHandler.postDelayed(pending, BACK_DOUBLE_PRESS_TIMEOUT_MS)
// Android's own double-tap window (300 ms): the single action waits it out, so it is felt.
uiHandler.postDelayed(pending, android.view.ViewConfiguration.getDoubleTapTimeout().toLong())
}
private fun performBackAction(double: Boolean) {
@@ -760,9 +760,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
applicationInfo.nativeLibraryDir, size.first, size.second, refreshHz(),
)
if (!SessionState.running) SessionEvents.record("compositor.started")
// ADPF: the compositor thread's frame intervals go to the power HAL against the panel's
// period, so a long frame raises CPU clocks now rather than after the load averages up.
PerfHints.arm(this, refreshHz())
// The service owns everything below the compositor. It is started whenever no session is
// running - NOT only when the compositor was just started: the compositor lives for the
// whole process, so the second Play after a session ended used to re-attach the Surface,
@@ -848,9 +845,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
return Pair(width and 1.inv(), height and 1.inv())
}
/**
* The rate of the mode PerfMode asked for, not the one the panel is still in: the switch lands
* some frames after onCreate, and read too early it gave gamescope `-r 60` on a 120 Hz panel -
* the client and every game then paced themselves to 60. (WinNative's requestedPanelRefreshRate.)
*/
private fun refreshHz(): Float {
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
val hz = display?.refreshRate ?: 60f
val wanted = window.attributes.preferredDisplayModeId
val hz = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
?: display?.refreshRate ?: 60f
return if (hz > 1f) hz else 60f
}
@@ -1598,7 +1602,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
companion object {
private const val TAG = "SessionActivity"
private const val UNBUFFERED_SOURCES = InputDevice.SOURCE_CLASS_JOYSTICK or InputDevice.SOURCE_CLASS_TRACKBALL or InputDevice.SOURCE_CLASS_POSITION
private const val BACK_DOUBLE_PRESS_TIMEOUT_MS = 500L
private const val CURSOR_PAD_HOLD_MS = 1200L
private const val DRAWER_HAT_THRESHOLD = 0.5f
private const val DRAWER_STICK_ENTER_THRESHOLD = 0.55f
@@ -146,6 +146,7 @@ object DeviceReport {
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" })
k("Skip xalia", SessionPrefs.noXalia(context))
k("gamescope realtime", SessionPrefs.gamescopeRealtime(context))
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
k("Guest host name", SessionPrefs.guestHostname(context))
k("DirectAudio for games", SessionPrefs.directAudio(context))
@@ -193,6 +193,8 @@ public final class LinuxRuntime {
}
}
bindGpuNode(context, cmd);
bindAdrenoStats(cmd);
bindCpuTemps(cmd, root);
if (extraBinds != null) {
for (String spec : extraBinds) bind(cmd, spec);
}
@@ -244,6 +246,82 @@ public final class LinuxRuntime {
bind(cmd, KGSL_DEVICE + ":/dev/dri/" + node);
}
/**
* Valve's mangoapp (Deck mode's performance overlay) reads an Adreno GPU's load, clock and
* temperatures from where they are on Valve's own hardware; every Adreno under Android keeps
* them in KGSL's sysfs, readable by the app, though which files a kernel has differs between
* Snapdragon generations - so each value takes the first source that exists, the order other
* Android PC emulators (GameNative, Winlator forks) read them in. A value found nowhere is left
* alone.
*/
private static void bindAdrenoStats(List<String> cmd) {
String kgsl = "/sys/class/kgsl/kgsl-3d0/";
String gpuTemp = firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu"));
String[][] stats = {
{firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"), "/sys/kernel/debug/dri/0/perf_now"},
{firstReadable(kgsl + "devfreq/cur_freq", kgsl + "gpuclk"),
"/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"},
{gpuTemp, "/sys/class/thermal/thermal_zone28/temp"},
{gpuTemp, "/sys/class/thermal/thermal_zone26/temp"},
{thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"},
};
for (String[] stat : stats) {
if (stat[0] != null) bind(cmd, stat[0] + ":" + stat[1]);
}
}
/**
* mangoapp's CPU temperature is the mean of the thermal zones named cpuN-thermal or
* cpuN-top-thermal, as mainline kernels name them; Android kernels name the same sensors
* cpuss-0, cpu-1-0, cpu0-silver-usr, apc1-cpu0-usr and the like, so those zones are shown
* under the mainline name.
*/
private static void bindCpuTemps(List<String> cmd, File root) {
File name = new File(root, "etc/bannerlator/cpu-thermal-type");
try {
if (!name.isFile()) Files.write(name.toPath(), "cpu0-thermal\n".getBytes(StandardCharsets.US_ASCII));
} catch (IOException e) {
return;
}
for (java.util.Map.Entry<String, File> zone : thermalZones().entrySet()) {
String type = zone.getKey();
if (type.contains("cpu") && !type.contains("gpu") && !type.matches("cpu\\d-(top-)?thermal")) {
bind(cmd, name.getPath() + ":" + new File(zone.getValue(), "type").getPath());
}
}
}
private static String firstReadable(String... paths) {
for (String path : paths) {
if (path != null && new File(path).canRead()) return path;
}
return null;
}
/** The temp file of the thermal zone for a sensor: the one named so, else the first whose name has it. */
private static String thermalZone(String sensor) {
java.util.Map<String, File> zones = thermalZones();
File zone = zones.get(sensor);
for (java.util.Map.Entry<String, File> e : new java.util.TreeMap<>(zones).entrySet()) {
if (zone == null && e.getKey().contains(sensor)) zone = e.getValue();
}
return zone != null ? new File(zone, "temp").getPath() : null;
}
/** The device's thermal zones by their sensor's name, in lower case. */
private static java.util.Map<String, File> thermalZones() {
java.util.Map<String, File> byType = new java.util.HashMap<>();
File[] zones = new File("/sys/class/thermal").listFiles((dir, n) -> n.startsWith("thermal_zone"));
if (zones == null) return byType;
for (File zone : zones) {
try (java.io.BufferedReader r = new java.io.BufferedReader(new java.io.FileReader(new File(zone, "type")))) {
byType.putIfAbsent(String.valueOf(r.readLine()).trim().toLowerCase(java.util.Locale.ROOT), zone);
} catch (IOException ignored) {
}
}
return byType;
}
private static void bind(List<String> cmd, String spec) {
cmd.add("-b");
cmd.add(spec);
@@ -1,87 +0,0 @@
package com.droiddeck.launcher.session
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PerformanceHintManager
import android.util.Log
import androidx.annotation.RequiresApi
import com.droiddeck.launcher.wayland.WaylandCompositor
import java.util.concurrent.Executors
/**
* ADPF (Android 12+): a PerformanceHintManager session over the compositor thread, told the
* panel's frame period as its target and each presented frame's interval as the actual. When a
* frame runs long the power HAL raises CPU clocks for the next one at once, instead of the
* governor waiting for the load average to climb - the difference between a menu that stutters
* on the first scroll and one that does not.
*
* The session may only carry this process's threads, so the guest (gamescope, the Steam client
* under FEX) cannot join it; they are covered by the app-wide game category and sustained mode.
* Below API 31 nothing here runs; on a power HAL without hint sessions createHintSession returns
* null and the log says so.
*/
object PerfHints {
private const val TAG = "PerfHints"
@Volatile private var session: PerformanceHintManager.Session? = null
@Volatile private var target = 0L
/** Compositor thread only. */
private var last = 0L
private val main = Handler(Looper.getMainLooper())
/** Reports leave the compositor thread: each one is a binder call to the power HAL. */
private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "adpf-report").apply { isDaemon = true } }
/** Opens the session once the compositor thread exists (it is started just before this). */
fun arm(context: Context, refreshHz: Float) {
if (Build.VERSION.SDK_INT < 31) { Log.i(TAG, "adpf: needs Android 12; this is API ${Build.VERSION.SDK_INT}"); return }
val wanted = (1_000_000_000.0 / refreshHz.coerceAtLeast(30f)).toLong()
val s = session
if (s != null) {
if (wanted != target) { target = wanted; try { s.updateTargetWorkDuration(wanted) } catch (t: Throwable) { Log.w(TAG, "adpf: retarget", t) } }
return
}
target = wanted
val app = context.applicationContext
var tries = 0
main.post(object : Runnable {
override fun run() {
if (session != null) return
val tid = try { WaylandCompositor.nativeCompositorTid() } catch (t: Throwable) { 0 }
if (tid == 0) {
if (++tries < 30) main.postDelayed(this, 100) else Log.w(TAG, "adpf: the compositor thread never reported its tid")
return
}
open(app, tid)
}
})
}
@RequiresApi(31)
private fun open(app: Context, tid: Int) {
val pm = app.getSystemService(PerformanceHintManager::class.java)
if (pm == null) { Log.i(TAG, "adpf: no PerformanceHintManager on this device"); return }
val s = try { pm.createHintSession(intArrayOf(tid), target) } catch (t: Throwable) { Log.w(TAG, "adpf: createHintSession", t); null }
if (s == null) { Log.i(TAG, "adpf: unsupported here (the power HAL has no hint sessions)"); return }
last = 0L
session = s
Log.i(TAG, "adpf: session on compositor thread $tid · target ${target / 1000} µs · HAL wants updates every ${pm.preferredUpdateRateNanos / 1000} µs")
}
/** Compositor thread, once per presented game frame; the interval since the last is the actual. */
@JvmStatic
fun onFrame() {
val s = session ?: return
val now = System.nanoTime()
val prev = last
last = now
if (prev == 0L) return
val dt = now - prev
// Under half a millisecond is a double present; over half a second is a pause, not a frame.
if (dt < 500_000L || dt > 500_000_000L) return
worker.execute {
try { s.reportActualWorkDuration(dt) } catch (t: Throwable) { Log.w(TAG, "adpf: report failed, session dropped", t); session = null }
}
}
}
@@ -4,7 +4,6 @@ import android.app.Activity
import android.app.GameManager
import android.app.GameState
import android.os.Build
import android.os.PowerManager
import android.util.Log
/**
@@ -13,8 +12,10 @@ import android.util.Log
* The manifest already makes the app a game (appCategory + game_mode_config); this is the
* per-window half, applied when the session activity is created:
*
* - sustained performance mode (Android 7+): a clock floor that does not throttle away after
* two minutes, which suits an hour in Big Picture better than a boost that fades;
* - NOT sustained performance mode: on Pixel and Qualcomm power HALs it caps the CPU and GPU at
* a level the device can hold indefinitely rather than holding a floor, which cost games and
* Big Picture their peak clocks. WinNative never asks for it; the thermal budget is left to
* the game;
* - the panel's fastest mode at its current size (Android 6+): a 120 Hz phone is switched to
* 120 Hz for the menu, not only when a game votes for it;
* - GameManager's game state (Android 13+): "in gameplay", so an OEM framework that releases
@@ -27,15 +28,7 @@ object PerfMode {
private const val TAG = "PerfMode"
fun apply(a: Activity): String {
val parts = ArrayList<String>(3)
// Sustained performance mode.
parts += if (Build.VERSION.SDK_INT >= 24) {
val pm = a.getSystemService(PowerManager::class.java)
if (pm?.isSustainedPerformanceModeSupported == true) {
try { a.window.setSustainedPerformanceMode(true); "sustained mode on" } catch (t: Throwable) { Log.w(TAG, "sustained mode", t); "sustained mode refused" }
} else "sustained mode unsupported"
} else "sustained mode needs Android 7"
val parts = ArrayList<String>(2)
// The fastest display mode at the panel's current size.
parts += try {
@@ -143,8 +143,8 @@ object SessionPrefs {
prefs(context).edit().putBoolean("directAudio", on).apply()
}
/** The microphone for voice chat, on unless turned off; used only once RECORD_AUDIO is granted. */
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", true)
/** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false)
/** Whether the app has already asked for the microphone once at start-up. */
fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false)
@@ -188,14 +188,28 @@ object SessionPrefs {
* xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation.
* Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls
* reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured -
* it storms, and the session dies seconds after Big Picture appears.
* it storms, and the session dies seconds after Big Picture appears. On by default: where it
* does run, it sits beside every game under FEX for nothing a controller-first session needs
* (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2).
*/
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", false)
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true)
fun setNoXalia(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("noXalia", on).apply()
}
/**
* Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1,
* which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in
* Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device
* whose GPU is waiting on the CPU.
*/
fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false)
fun setGamescopeRealtime(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("gamescopeRealtime", on).apply()
}
/**
* Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1).
*
@@ -227,8 +241,13 @@ object SessionPrefs {
return trimmed.takeIf { Regex("[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?").matches(it) }
}
/** Turnip's sysmem rendering (TU_DEBUG=sysmem) for the runtime's driver: bypasses GMEM tiling. */
fun tuSysmem(context: Context): Boolean = prefs(context).getBoolean("tuSysmem", false)
/**
* Turnip's sysmem rendering (TU_DEBUG=sysmem) for the runtime's driver: bypasses GMEM tiling.
* On by default, as WinNative runs every Linux session: Chromium -> ANGLE -> Zink draws the
* client's interface as many small render passes, each paying GMEM's load/store and binning,
* and WinNative's A/B on an Adreno 840 put a game ahead with it too (Palworld 42.8 against 41.3).
*/
fun tuSysmem(context: Context): Boolean = prefs(context).getBoolean("tuSysmem", true)
fun setTuSysmem(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("tuSysmem", on).apply()
@@ -248,11 +267,14 @@ object SessionPrefs {
fun noGlError(context: Context): Boolean = prefs(context).getBoolean("noGlError", true)
fun setNoGlError(context: Context, on: Boolean) { prefs(context).edit().putBoolean("noGlError", on).apply() }
/** `steam -steamdeck -steamos3`: the client as SteamOS runs it. Expects Deck hardware; untested. */
/** Runs the SteamOS gamepad client with its Quick Access performance controls. */
fun steamDeckMode(context: Context): Boolean = prefs(context).getBoolean("steamDeckMode", false)
fun setSteamDeckMode(context: Context, on: Boolean) { prefs(context).edit().putBoolean("steamDeckMode", on).apply() }
/** Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) for the client's GL-on-Vulkan UI. On by default. */
/**
* Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) with its compact set layout
* (ZINK_DEBUG=compact) for the client's GL-on-Vulkan UI, as WinNative runs it. On by default.
*/
fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true)
fun setZinkLazy(context: Context, on: Boolean) {
@@ -309,8 +331,15 @@ object SessionPrefs {
fun resolutionChosen(context: Context, mode: String): Boolean =
prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null
/** The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults. */
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "") ?: ""
/**
* The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults.
* Performance + TSO unless chosen, WinNative's default: FEX's own defaults keep half-barrier TSO
* and full-precision x87, which cost every x86 game time; a game that needs them can still be
* given another preset.
*/
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", DEFAULT_FEX_PRESET) ?: DEFAULT_FEX_PRESET
private const val DEFAULT_FEX_PRESET = "PERFORMANCE_TSO"
fun setFexPreset(context: Context, id: String) {
prefs(context).edit().putString("fexPreset", id).apply()
@@ -513,7 +513,11 @@ class SessionService : Service() {
// Zink renders the client's UI (Chromium -> ANGLE -> Zink -> Turnip). Lazy descriptors is
// the mode Zink recommends where the driver has no descriptor buffer, and what Ludashi ships
// by default for its Zink path; a switch here because on one Fold the menus run at 14 fps.
if (SessionPrefs.zinkLazy(this)) guest.add("ZINK_DESCRIPTORS=lazy")
// compact packs Zink's descriptor sets into fewer, so a draw binds less (WinNative's default).
if (SessionPrefs.zinkLazy(this)) {
guest.add("ZINK_DESCRIPTORS=lazy")
guest.add("ZINK_DEBUG=compact")
}
// The rest of the client-interface switches (SessionPrefs): GL marshalled off the calling
// thread, no GL error checks, and the client run as SteamOS runs it (the script reads
// BL_STEAMDECK; it is the one that builds the command line).
@@ -526,9 +530,12 @@ class SessionService : Service() {
SessionPrefs.writeForceFullscreenFlag(this)
}
// Proton's own gate for its xalia helper (its `proton` script reads this, and sets
// XALIA_SUPPORTED_ONLY itself otherwise). Off by default: xalia is Valve's, and on a device
// whose seccomp answers its syscalls normally there is no reason to take it away.
// XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game
// a slice of a core for gamepad navigation the session already has.
if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0")
// gamescope's realtime Vulkan queues (the session script turns this into
// GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on.
guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0"))
// Anything else, for a device that cannot be reached with a debugger: Downloads/droiddeck-env
// holds KEY=VALUE lines that go into the session's environment as written, after ours, so a
// line here wins. Zink and Turnip tunables (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's,
@@ -68,6 +68,8 @@ class ModeSettings(
val forceFullscreen: Boolean? = null,
/** Steam only: the client branch forced on the command line. */
val steamChannel: String? = null,
/** Steam only: enable the SteamOS client interface and its performance controls. */
val steamDeckMode: Boolean = false,
/** Steam only: start a Steam session when DroidDeck opens. */
val runSteamAtStartup: Boolean = false,
/** Steam only: the user's chosen Games folders; null outside Steam. */
@@ -123,6 +125,7 @@ class ModeSettingsActions(
val onFexPreset: (String) -> Unit = {},
val onForceFullscreen: (Boolean) -> Unit = {},
val onSteamChannel: (String) -> Unit = {},
val onSteamDeckMode: (Boolean) -> Unit = {},
val onRunSteamAtStartup: (Boolean) -> Unit = {},
val onPickAddedGamesDir: () -> Unit = {},
val onForgetAddedGamesDir: (path: String) -> Unit = {},
@@ -338,6 +341,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
}
}
if (steam && s.steamChannel != null) SettingsGroup("Client") {
ToggleRow(
host, "steamdeck", "Steam Deck mode",
"Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.",
s.steamDeckMode, onChange = a.onSteamDeckMode,
)
ChoiceRow(
host, "channel", "Client branch", "The Steam client build the session forces. Applies at the next session start; the client may update itself once.",
listOf("publicbeta" to "Public beta", "steamdeck_publicbeta" to "Steam Deck public beta"), s.steamChannel,
@@ -36,8 +36,8 @@ fun PerformancePage(
zinkLazy: Boolean,
glThread: Boolean,
noGlError: Boolean,
steamDeckMode: Boolean,
noXalia: Boolean,
gamescopeRealtime: Boolean,
prootNoSeccomp: Boolean,
guestHostname: String,
phantomWarning: String?,
@@ -46,8 +46,8 @@ fun PerformancePage(
onZinkLazy: (Boolean) -> Unit,
onGlThread: (Boolean) -> Unit,
onNoGlError: (Boolean) -> Unit,
onSteamDeckMode: (Boolean) -> Unit,
onNoXalia: (Boolean) -> Unit,
onGamescopeRealtime: (Boolean) -> Unit,
onProotNoSeccomp: (Boolean) -> Unit,
onGuestHostname: (String) -> Unit,
onClientCore: (Int, Boolean) -> Unit,
@@ -88,7 +88,7 @@ fun PerformancePage(
)
ToggleRow(
host, "zink", "Zink: lazy descriptors",
"Recommended for drivers without descriptor buffers.",
"Lazy, compact descriptors. Recommended for drivers without descriptor buffers.",
zinkLazy, onChange = onZinkLazy,
)
ToggleRow(
@@ -97,20 +97,20 @@ fun PerformancePage(
noGlError, onChange = onNoGlError,
)
ToggleRow(
host, "deck", "Steam Deck mode",
"Runs the client with -droiddeck. SteamOS helpers and battery info are provided.",
steamDeckMode, onChange = onSteamDeckMode,
host, "gsrealtime", "gamescope: realtime GPU queue",
"Gives the compositor's GPU work priority over the game's. Can smooth frame pacing, but can cost games GPU time.",
gamescopeRealtime, onChange = onGamescopeRealtime,
)
}
SettingsGroup("Session fixes") {
ToggleRow(
host, "sysmem", "Turnip: sysmem rendering",
"Required on Adreno 710/720/722. May fix corruption on other Adreno GPUs, but can reduce performance.",
"On by default: faster for the Steam interface and most games. Required on Adreno 710/720/722.",
tuSysmem, onChange = onTuSysmem,
)
ToggleRow(
host, "xalia", "Skip Steam's xalia helper",
"Disables Proton's gamepad navigation helper. Try if sessions crash at startup.",
"Disables Proton's gamepad navigation helper, which costs every game CPU time. Turn off only if a game needs it.",
noXalia, onChange = onNoXalia,
)
ToggleRow(
@@ -41,6 +41,10 @@ object CompositorHost {
}
WaylandCompositor.nativeSetOutputSize(outputWidth, outputHeight)
WaylandCompositor.nativeSetOutputRefreshRate(refreshHz)
// The cadence the zero-copy layer votes for. Left at 0 the system infers it from the rate
// already being achieved, and on a phone whose vendor picks clocks from that a session
// that has been slowed reads as one that wants to be. (WinNative, WaylandSession.)
WaylandCompositor.nativeSetLayerFrameRate(refreshHz)
WaylandCompositor.nativeStartWithSurface(
surface, xdgRuntimeDir, driverPath, libraryName, nativeLibDir,
)
@@ -91,7 +91,6 @@ public final class WaylandCompositor {
long smoothed = frameIntervalNanos;
frameIntervalNanos = smoothed == 0 ? interval : (smoothed * 3 + interval) / 4;
}
com.droiddeck.launcher.session.PerfHints.onFrame();
GameListener l = gameListener;
if (l != null) l.onGameFrame();
}
@@ -218,7 +217,7 @@ public final class WaylandCompositor {
* once per tick, so games run unthrottled and the screen shows their latest frame. */
public static native void nativeVsync(long frameTimeNanos);
/** The compositor thread's Linux tid, or 0 before it has started (see PerfHints). */
/** The compositor thread's Linux tid, or 0 before it has started. */
public static native int nativeCompositorTid();
/** Shortcut launches: don't draw explorer's windows (desktop, taskbar, Start menu), matching the
@@ -49,6 +49,15 @@ class GameEnvironmentStoreTest {
assertFalse(JSONObject(guest.readText()).getJSONObject("shared").has("FEX_SMCCHECKS"))
}
@Test fun unchosenFexPresetPublishesPerformanceTso() {
GameEnvironmentStore.save(context, GameEnvironment.Config())
val shared = JSONObject(guest.readText()).getJSONObject("shared")
assertEquals("1", shared.getString("FEX_TSOENABLED"))
assertEquals("0", shared.getString("FEX_HALFBARRIERTSOENABLED"))
assertEquals("1", shared.getString("FEX_X87REDUCEDPRECISION"))
assertEquals("1", shared.getString("FEX_MULTIBLOCK"))
}
@Test fun invalidDataCannotReplaceSavedConfiguration() {
val original = GameEnvironment.Config(shared = mapOf("CUSTOM" to "ok"))
GameEnvironmentStore.save(context, original)
+57 -16
View File
@@ -89,7 +89,17 @@ linuxfs_replaced=1
rm -rf -- "${linuxfs_dir}"
mkdir -p "${linuxfs_dir}"
if ! docker image inspect "${image_name}" >/dev/null 2>&1; then
# Docker Desktop's VM restarts now and then, and until its engine has loaded its image store it
# answers "No such image" for images it has. The rebuild that follows hangs on the registry
# (the base image's credentials go through docker-credential-desktop), so give a restarting daemon
# up to a minute before deciding the image is really missing.
image_present=0
for _ in $(seq 1 30); do
if inspect_error=$(docker image inspect "${image_name}" 2>&1 >/dev/null); then image_present=1; break; fi
sleep 2
done
if [[ "${image_present}" = 0 ]]; then
echo "Build image ${image_name} not found (${inspect_error:-no error}); building it." >&2
docker build --platform linux/amd64 -t "${image_name}" \
-f "${repo_root}/tools/local-cross.Dockerfile" "${repo_root}"
fi
@@ -164,20 +174,39 @@ if [[ -z "${github_repo}" ]]; then
github_repo=${github_repo%.git}
fi
# Pinned downloads are kept between builds, named by their checksum, so a rebuild fetches nothing
# it already has. A cached file is checked again before use; a bad one is fetched anew.
cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"}
mkdir -p "${cache_dir}"
# cached <sha256> <name> <command...>: prints the cached path, running the command (which writes
# to "$out") only when the cache has no file with that checksum.
cached() {
local sha=$1 name=$2 out
shift 2
out="${cache_dir}/${sha}-${name}"
if [[ -f "${out}" ]] && printf '%s %s\n' "${sha}" "${out}" | shasum -a 256 -c - >/dev/null 2>&1; then
echo "${out}"
return 0
fi
rm -f "${out}.part"
out="${out}.part" "$@" >&2
printf '%s %s\n' "${sha}" "${out}.part" | shasum -a 256 -c - >&2
mv "${out}.part" "${out}"
echo "${out}"
}
if [[ -f "${repo_root}/tools/gamescope/release.env" ]]; then
. "${repo_root}/tools/gamescope/release.env"
gamescope_archive="${staging_dir}/gamescope.tzst"
gh release download "${GAMESCOPE_TAG}" -R "${github_repo}" -p gamescope.tzst -O "${gamescope_archive}"
printf '%s %s\n' "${GAMESCOPE_SHA256}" "${gamescope_archive}" | shasum -a 256 -c -
gamescope_archive=$(cached "${GAMESCOPE_SHA256}" gamescope.tzst \
bash -c 'gh release download "$0" -R "$1" -p gamescope.tzst -O "$out"' "${GAMESCOPE_TAG}" "${github_repo}")
zstd -dc "${gamescope_archive}" | tar -xf - -C "${linuxfs_dir}"
test -f "${linuxfs_dir}/usr/local/bin/gamescope"
fi
if [[ -f "${repo_root}/tools/wlroots/release.env" ]]; then
. "${repo_root}/tools/wlroots/release.env"
wlroots_archive="${staging_dir}/wlroots.tzst"
gh release download "${WLROOTS_TAG}" -R "${github_repo}" -p wlroots.tzst -O "${wlroots_archive}"
printf '%s %s\n' "${WLROOTS_SHA256}" "${wlroots_archive}" | shasum -a 256 -c -
wlroots_archive=$(cached "${WLROOTS_SHA256}" wlroots.tzst \
bash -c 'gh release download "$0" -R "$1" -p wlroots.tzst -O "$out"' "${WLROOTS_TAG}" "${github_repo}")
zstd -dc "${wlroots_archive}" | tar -xf - -C "${linuxfs_dir}"
test -f "${linuxfs_dir}/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
fi
@@ -187,9 +216,8 @@ mango_pkgs="${staging_dir}/mango-pkgs"
mkdir -p "${mango_dir}" "${mango_pkgs}"
while read -r package_sha256 package_url; do
[[ -n "${package_url}" ]] || continue
package_archive="${staging_dir}/$(basename "${package_url}")"
curl -fsSL --retry 3 -o "${package_archive}" "${package_url}"
printf '%s %s\n' "${package_sha256}" "${package_archive}" | shasum -a 256 -c -
package_archive=$(cached "${package_sha256}" "$(basename "${package_url}")" \
bash -c 'curl -fsSL --retry 3 -o "$out" "$0"' "${package_url}")
zstd -dc "${package_archive}" | tar -xf - -C "${mango_pkgs}"
done < <(grep -v '^#' "${repo_root}/tools/mangoapp/packages.txt")
install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp"
@@ -201,11 +229,14 @@ install -m644 "${repo_root}/tools/mangoapp/mangoapp" "${linuxfs_dir}/usr/local/b
pa_source=${DROIDDECK_PA13_SOURCE_DIR:-"${staging_dir}/pulseaudio-13.0"}
if [[ -z "${DROIDDECK_PA13_SOURCE_DIR:-}" ]]; then
curl -fsSL -o "${staging_dir}/pulseaudio-13.0.tar.gz" \
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
pa_tarball="${cache_dir}/pulseaudio-13.0.tar.gz"
if [[ ! -s "${pa_tarball}" ]] || ! tar -tzf "${pa_tarball}" >/dev/null 2>&1; then
curl -fsSL -o "${pa_tarball}.part" \
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mv "${pa_tarball}.part" "${pa_tarball}"
fi
mkdir -p "${pa_source}"
tar -xzf "${staging_dir}/pulseaudio-13.0.tar.gz" \
-C "${pa_source}" --strip-components=1
tar -xzf "${pa_tarball}" -C "${pa_source}" --strip-components=1
fi
if [[ ! -f "${pa_source}/src/pulse/version.h.in" ]]; then
echo "PulseAudio 13.0 source not found at ${pa_source}; set DROIDDECK_PA13_SOURCE_DIR." >&2
@@ -214,7 +245,17 @@ fi
sink_output="${staging_dir}/sink-out"
"${repo_root}/tools/aaudio-sink/build.sh" "${pa_source}" "${sink_output}"
"${repo_root}/tools/proot/build.sh" "${repo_root}/app/src/main/jniLibs/arm64-v8a"
# proot is rebuilt only when its sources (source.env, the patches, the build script) changed since
# the libraries in jniLibs were built.
proot_out="${repo_root}/app/src/main/jniLibs/arm64-v8a"
proot_inputs=$(cd "${repo_root}/tools/proot" && find . -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
if [[ -f "${proot_out}/libproot.so" && -f "${proot_out}/libproot-loader.so" \
&& "$(cat "${proot_out}/.proot-inputs" 2>/dev/null)" = "${proot_inputs}" ]]; then
echo "proot: sources unchanged, keeping ${proot_out}/libproot.so"
else
"${repo_root}/tools/proot/build.sh" "${proot_out}"
echo "${proot_inputs}" > "${proot_out}/.proot-inputs"
fi
cp -p "${bundle_asset}" "${bundle_backup}"
bundle_dir="${staging_dir}/pulseaudio-bundle"
@@ -229,7 +270,7 @@ install -m755 "${sink_output}/module-aaudio-sink.so" \
"${bundle_dir}/modules/arm64/module-aaudio-sink.so"
install -m755 "${sink_output}/module-directaudio-sink.so" \
"${bundle_dir}/modules/arm64/module-directaudio-sink.so"
tar -cf - -C "${bundle_dir}" . | zstd -19 -c > "${staging_dir}/pulseaudio.tzst"
tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tzst"
bundle_replaced=1
mv "${staging_dir}/pulseaudio.tzst" "${bundle_asset}"
+5 -2
View File
@@ -14,8 +14,11 @@ library list, before anything is published.
- `0020-color-p3-red-is-wide-gamut.patch` - Armada, verbatim.
- `0100-realtime-queue-and-gamepad-cursor.patch` - this app, two of Armada's ported by hand onto
3.16.29: realtime-priority Vulkan queues on request (`GAMESCOPE_FORCE_VULKAN_REALTIME=1`)
without CAP_SYS_NICE, which proot can never have; and the gamepad-driven cursor sprite following
the X pointer that XTest moves (it sat frozen).
without CAP_SYS_NICE, which proot can never have (a no-op on KGSL Turnip, which has a single
submit-queue priority); and the gamepad-driven cursor sprite following the X pointer that XTest
moves (it sat frozen). The X pointer is asked for only while a cursor image is drawn - every
vblank while shown, every 50 ms while hidden for inactivity - since each ask is a blocking round
trip to Xwayland on the paint thread; with no image, wlserver's position is used as upstream does.
- `0110-wayland-backend-touch.patch` - this app: the nested Wayland backend bound only the host's
pointer and keyboard, so a finger on the phone's screen never reached Steam. It now binds
`wl_touch` too and hands each finger to wlserver's touch path (`wlserver_touchdown` / `motion` /
@@ -7,8 +7,8 @@ index 54e4efc..024af02 100644
float queuePriorities = 1.0f;
+ // Without CAP_SYS_NICE gamescope never asks for realtime-priority queues. Under proot on
+ // Android there is no capability to have, but the driver may still grant the priority (Turnip
+ // does on Adreno), and the existing fallback below covers a driver that refuses. Opt in with
+ // Android there is no capability to have; the fallback below covers a driver that refuses. KGSL
+ // Turnip accepts but ignores it (one submit-queue priority, contexts created alike). Opt in with
+ // GAMESCOPE_FORCE_VULKAN_REALTIME=1. (Armada's --force-vulkan-realtime, as an environment switch.)
+ const char *pszForceRealtime = getenv( "GAMESCOPE_FORCE_VULKAN_REALTIME" );
+ const bool useRealtimeVulkanQueue = gamescope::Process::HasCapSysNice() || ( pszForceRealtime && atoi( pszForceRealtime ) != 0 );
@@ -52,27 +52,10 @@ index 54e4efc..024af02 100644
fprintf(stderr, "vkCreateDevice failed with a high-priority queue (compute). Falling back to regular priority (all).\n");
queueCreateInfos[0].pNext = nullptr;
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
index 18b799e..16f7846 100644
index 10990b8..08afbc4 100644
--- a/src/steamcompmgr.cpp
+++ b/src/steamcompmgr.cpp
@@ -2020,23 +2020,41 @@ MouseCursor::MouseCursor(xwayland_ctx_t *ctx)
void MouseCursor::UpdatePosition()
{
+ // The gamepad-driven cursor is moved through XTest, which moves the X pointer and nothing
+ // else: wlserver's copy of the position never changes, so the sprite sat frozen while the
+ // pointer moved. Ask X where the pointer is. (Armada 0001, on 3.16.29.)
+ Window root_ret, child_ret;
+ int root_x = m_x, root_y = m_y, win_x = 0, win_y = 0;
+ unsigned int mask;
+ XQueryPointer( m_ctx->dpy, DefaultRootWindow( m_ctx->dpy ),
+ &root_ret, &child_ret,
+ &root_x, &root_y,
+ &win_x, &win_y,
+ &mask );
+
wlserver_lock();
struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint();
@@ -2025,18 +2025,53 @@ void MouseCursor::UpdatePosition()
if ( pConstraint && pConstraint->current.cursor_hint.enabled )
{
m_bConstrained = true;
@@ -84,23 +67,61 @@ index 18b799e..16f7846 100644
}
- else
+ m_bConstrained = false;
+ wlserver_unlock();
+ if ( root_x != m_x || root_y != m_y )
+ // Nothing to draw (the gamepad UI, most games): the position only feeds focus and zoom, and
+ // wlserver's copy is what stock gamescope uses for those. No round trip to X.
+ if ( m_imageEmpty )
{
- m_bConstrained = false;
-
- m_x = wlserver.mouse_surface_cursorx;
- m_y = wlserver.mouse_surface_cursory;
m_x = wlserver.mouse_surface_cursorx;
m_y = wlserver.mouse_surface_cursory;
+ wlserver_unlock();
+ return;
}
wlserver_unlock();
+
+ // The gamepad-driven cursor is moved through XTest, which moves the X pointer and nothing
+ // else: wlserver's copy of the position never changes, so the sprite sat frozen while the
+ // pointer moved. Ask X where the pointer is. (Armada 0001, on 3.16.29.) That is a blocking
+ // round trip to Xwayland on the paint thread, which waits while Xwayland is busy drawing the
+ // client's interface: once per vblank only while the sprite is shown, and every 50 ms while
+ // it is hidden for inactivity - enough to bring it back within a few frames of a move.
+ if ( wlserver.bCursorHidden )
+ {
+ const uint64_t now = get_time_in_nanos();
+ if ( now - m_ulLastPointerQuery < 50'000'000ul )
+ return;
+ m_ulLastPointerQuery = now;
+ }
+ Window root_ret, child_ret;
+ int root_x = m_x, root_y = m_y, win_x = 0, win_y = 0;
+ unsigned int mask;
+ XQueryPointer( m_ctx->dpy, DefaultRootWindow( m_ctx->dpy ),
+ &root_ret, &child_ret,
+ &root_x, &root_y,
+ &win_x, &win_y,
+ &mask );
+
+ if ( root_x != m_x || root_y != m_y )
+ {
+ m_x = root_x;
+ m_y = root_y;
+ if ( !m_imageEmpty )
+ {
+ wlserver.ulLastMovedCursorTime = get_time_in_nanos();
+ wlserver.bCursorHidden = !wlserver.bCursorHasImage;
+ hasRepaintNonBasePlane = true;
+ }
}
- wlserver_unlock();
+ wlserver.ulLastMovedCursorTime = get_time_in_nanos();
+ wlserver.bCursorHidden = !wlserver.bCursorHasImage;
+ hasRepaintNonBasePlane = true;
+ }
}
void MouseCursor::checkSuspension()
diff --git a/src/steamcompmgr.hpp b/src/steamcompmgr.hpp
index c428784..bd62fd1 100644
--- a/src/steamcompmgr.hpp
+++ b/src/steamcompmgr.hpp
@@ -109,6 +109,7 @@ private:
int m_x = 0, m_y = 0;
bool m_bConstrained = false;
+ uint64_t m_ulLastPointerQuery = 0;
int m_hotspotX = 0, m_hotspotY = 0;
gamescope::OwningRc<CVulkanTexture> m_texture;
@@ -73,10 +73,21 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
fi
unset bl_icd_lib
fi
# The app's gamescope build (tools/gamescope) takes realtime-priority Vulkan queues on this
# The app's gamescope build (tools/gamescope) asks for realtime-priority Vulkan queues on this
# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority
# where the driver refuses, and the runtime's own build ignores it.
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-1}
# where the driver refuses, and the runtime's own build ignores it. On KGSL Turnip (Mesa 26.2) it
# changes nothing yet: the driver reports one submit-queue priority and creates every KGSL context
# with the same flags, so no priority reaches the GPU. Kept for a driver that maps it, and off
# unless the app's Performance page turns it on (BL_GAMESCOPE_REALTIME=1).
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
# A sleep that starts no process. Under proot every exec is a traced execve plus the loader's
# opens and maps, and the watchers below poll for the whole session: `sleep 1` in a loop was 60
# execs a minute each. read -t on a pipe nothing ever writes to waits in bash itself.
nap() {
[ -n "${bl_nap_fd:-}" ] || exec {bl_nap_fd}<> <(:)
read -r -t "$1" -u "$bl_nap_fd" _ 2>/dev/null || :
}
export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
@@ -266,7 +277,7 @@ case $mode in
for _p in $(pgrep -f 'steamwebhelper|steamrtarm64/steam|gamescope' 2>/dev/null); do
taskset -cp "$BL_CLIENT_CPUS" "$_p" >/dev/null 2>&1 || true
done
sleep 5
nap 5
done
) &
fi
@@ -374,8 +385,9 @@ case $mode in
[ -z "${BL_DESKTOP:-}" ] && (
last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}
while :; do
sleep 1
want=$(cat "$HOME/.droiddeck-fill" 2>/dev/null) || continue
nap 1
want=
{ read -r want || [ -n "$want" ]; } < "$HOME/.droiddeck-fill" 2>/dev/null || continue
case $want in 0|1) ;; *) continue ;; esac
[ "$want" = "$last" ] && continue
if xprop -root -f GAMESCOPE_FORCE_WINDOWS_FULLSCREEN 32c -set GAMESCOPE_FORCE_WINDOWS_FULLSCREEN "$want" 2>/dev/null; then
@@ -387,7 +399,7 @@ case $mode in
(
beat=0
for _ in $(seq 1 720); do
sleep 5
nap 5
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
beat=$((beat + 1))
if [ "$((beat % 3))" -eq 0 ]; then
@@ -501,20 +513,19 @@ BUSCONF
# desktop's launchers ask for; gamescope shows one window at a time and the game takes over.
_bl_ui=(-gamepadui)
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
# Deck mode (BL_STEAMDECK=1, the Performance page): -steamdeck is the flag that puts the Quick
# Access Menu, the performance overlay and the Deck settings pages on screen. Only -steamdeck,
# never -steamos3: that one makes the client manage itself as SteamOS does - it calls
# steamos-select-branch and opts the install into the steamdeck_stable client branch, writes
# that to package/beta, and from then on the branch on disk loses to the -clientbeta below on
# every launch; the updater finds "installed version 0", marks an update pending and exits 42
# to apply it, which never happens, so the client restarts for ever - under a running game,
# taking Steam Input with it each time. (Bannerlator 38996ea1, on device.) The SteamOS helpers
# the client calls in this mode are staged as no-op stubs under /usr/bin.
# Deck mode (BL_STEAMDECK=1, the Steam page's Client settings): -steamdeck is the flag that puts
# the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay
# controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes
# the client manage itself as SteamOS does - it calls steamos-select-branch and writes
# steamdeck_stable to package/beta, and a branch on disk that loses to the -clientbeta below
# is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The
# loop below puts package/beta back before every start. The SteamOS helpers the client calls
# in this mode are staged as no-op stubs under /usr/bin.
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
_bl_ui+=(-steamdeck)
_bl_ui+=(-steamdeck -steamos3)
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
echo "== steam: Deck mode on (-steamdeck)"
echo "== steam: Deck mode on (-steamdeck -steamos3)"
fi
# What a killed session leaves behind. Android ends sessions without warning (backgrounding, the
# low-memory killer), and the client then finds .crash - opening its update UI on the spinner -
@@ -559,7 +570,7 @@ BUSCONF
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
exit 0
fi
sleep 0.5
nap 0.5
done
) &
stop_watcher=$!
@@ -146,6 +146,26 @@ bl_game_cpus() {
# game never gets OpenGL; left at its own size in a window, it draws small in a corner. So these
# titles are told to stay windowed at the session's own size - unless the user's launch options
# already say something about the video mode, which are left as they are.
# The session puts the client's own libraries (steamrtarm64, its panorama/) ahead of everything for
# the client, and a game inherits that: Wine then resolved gnutls, SDL, FFmpeg and the rest against
# the client's private copies instead of the rootfs it was built for. Inside Steam's runtime
# container that path never reaches Proton either. (WinNative's winnative-proton-launch.)
BL_LIBPATH_SETUP = """
bl_libpath() {
[ -n "${LD_LIBRARY_PATH:-}" ] || return 0
local kept= dir old_ifs=$IFS
IFS=:
for dir in $LD_LIBRARY_PATH; do
case $dir in
"" | */steamrtarm64 | */steamrtarm64/*) ;;
*) kept=${kept:+$kept:}$dir ;;
esac
done
IFS=$old_ifs
if [ -n "$kept" ]; then export LD_LIBRARY_PATH="$kept"; else unset LD_LIBRARY_PATH; fi
}
"""
BL_IDTECH3_SETUP = """
bl_idtech3() {
BL_EXTRA_ARGS=()
@@ -233,6 +253,7 @@ for name in %s; do
bl_directaudio "$depot"
bl_game_cpus
bl_idtech3 "$@"
bl_libpath
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$depot/proton" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
fi
done
@@ -244,7 +265,7 @@ exit 1
# something else.
LAUNCHER_SH = LAUNCHER_SH.replace(
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
+ BL_COMPONENTS_SETUP, 1)
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of
# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot
@@ -280,6 +301,7 @@ bl_components "$here"
bl_directaudio "$here"
bl_game_cpus
bl_idtech3 "$@"
bl_libpath
# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be
# started from its own tree.
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$here/%s" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
@@ -369,6 +391,28 @@ def serialize(tokens):
return "\n".join(out) + "\n"
def write_changed(path, text, mode=None):
"""Writes `text` to `path` only when it differs, through a rename so a reader never sees half a
file. The session runs this script every 15 s for as long as the client does; rewriting files
that have not changed cost a python start's worth of writes each time, and config.vdf is the
client's own, which it may be writing at that moment. Returns whether anything was written."""
try:
with open(path, encoding="utf-8", errors="replace") as f:
same = f.read() == text
except OSError:
same = False
if not same:
tmp = path + ".bannerlator"
with open(tmp, "w", encoding="utf-8") as f:
f.write(text)
if mode is not None:
os.chmod(tmp, mode)
os.replace(tmp, path)
elif mode is not None and (os.stat(path).st_mode & 0o7777) != mode:
os.chmod(path, mode)
return not same
def first_key(path, section):
"""The single key naming a block inside `section`, or None. Used to keep a third-party tool's
own internal name, so a mapping the user already made against it keeps working."""
@@ -442,14 +486,10 @@ def adopt_extras(tools):
body = (EXTRA_WRAPPER_SH % entry_point).replace(
'here=$(dirname "$0")',
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
+ BL_COMPONENTS_SETUP, 1)
with open(wrapper, "w", encoding="utf-8") as f:
f.write(body)
os.chmod(wrapper, 0o755)
with open(manifest, "w", encoding="utf-8") as f:
f.write(EXTRA_MANIFEST_VDF)
with open(compat_vdf, "w", encoding="utf-8") as f:
f.write(EXTRA_COMPAT_TOOL_VDF % (name, entry))
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
write_changed(wrapper, body, 0o755)
write_changed(manifest, EXTRA_MANIFEST_VDF)
write_changed(compat_vdf, EXTRA_COMPAT_TOOL_VDF % (name, entry))
except OSError as e:
print("bannerlator-steam-compat: could not adopt %s (%s)" % (entry, e))
continue
@@ -494,7 +534,8 @@ def register_default(config_path, apps=(), name=None, protect=()):
# Tool names the user may deliberately have chosen (see adopt_extras); never repointed.
keep = {n.lower() for n in protect}
with open(config_path, encoding="utf-8", errors="replace") as f:
tokens = tokenize(f.read())
original = f.read()
tokens = tokenize(original)
steam = find_block(tokens, ["InstallConfigStore", "Software", "Valve", "Steam"])
if steam is None:
return
@@ -521,10 +562,10 @@ def register_default(config_path, apps=(), name=None, protect=()):
continue
tokens[mapping + 1:mapping + 1] = [
'"%s"' % app, "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"250"', "}"]
tmp = config_path + ".bannerlator"
with open(tmp, "w", encoding="utf-8") as f:
f.write(serialize(tokens))
os.replace(tmp, config_path)
# Nothing to change is the usual case on the session's beat: the client's own file is left alone.
text = serialize(tokens)
if text != serialize(tokenize(original)):
write_changed(config_path, text)
def find_source(steam_root):
@@ -542,18 +583,14 @@ def build_tool(tool_dir):
"""Our own manifests and the launcher. The depot is found at launch time, not here."""
if os.path.islink(tool_dir):
os.unlink(tool_dir)
elif os.path.isdir(tool_dir):
# Earlier versions mirrored the depot in here as symlinks; start clean.
elif os.path.isdir(tool_dir) and set(os.listdir(tool_dir)) - set(OWN_FILES):
# Earlier versions mirrored the depot in here as symlinks; start clean. A directory holding
# only our own files is kept, and each file rewritten only when it changed.
shutil.rmtree(tool_dir)
os.makedirs(tool_dir)
with open(os.path.join(tool_dir, "toolmanifest.vdf"), "w", encoding="utf-8") as f:
f.write(TOOL_MANIFEST_VDF)
with open(os.path.join(tool_dir, "compatibilitytool.vdf"), "w", encoding="utf-8") as f:
f.write(COMPAT_TOOL_VDF % TOOL)
launcher = os.path.join(tool_dir, LAUNCHER)
with open(launcher, "w", encoding="utf-8") as f:
f.write(LAUNCHER_SH)
os.chmod(launcher, 0o755)
os.makedirs(tool_dir, exist_ok=True)
write_changed(os.path.join(tool_dir, "toolmanifest.vdf"), TOOL_MANIFEST_VDF)
write_changed(os.path.join(tool_dir, "compatibilitytool.vdf"), COMPAT_TOOL_VDF % TOOL)
write_changed(os.path.join(tool_dir, LAUNCHER), LAUNCHER_SH, 0o755)
def main():
@@ -135,6 +135,10 @@ def watch(steam_root, listing):
candidates, sources = {}, {}
next_lookup = 0
last_status = None
# The probe is a fresh python that loads steamclient.so, seconds of CPU under proot. Ownership
# only moves with a purchase or a new match, so it runs when the matches or their sources
# change, after a failed probe, and otherwise every five minutes - not on every 30 s beat.
probed, next_probe = None, 0
while True:
now = time.monotonic()
for game in games:
@@ -150,13 +154,16 @@ def watch(steam_root, listing):
if now >= next_lookup:
next_lookup = now + 300
status = "waiting for a matching Steam title"
if candidates:
key = (dict(candidates), dict(sources))
if candidates and (key != probed or now >= next_probe):
probed, next_probe = key, now + 300
try:
result = subprocess.run([sys.executable, __file__, "probe", steam_root,
*map(str, sorted(set(candidates.values())))],
capture_output=True, text=True, timeout=15)
if result.returncode != 0:
status = "ownership probe failed: " + (result.stderr.strip()[-300:] or str(result.returncode))
probed = None
else:
line = next(line for line in result.stdout.splitlines() if line.startswith("DROIDDECK_OWNERSHIP="))
snapshot = json.loads(line.removeprefix("DROIDDECK_OWNERSHIP="))
@@ -168,6 +175,9 @@ def watch(steam_root, listing):
status = "matched %d/%d games; %d owned" % (len(candidates), len(games), sum(snapshot["owned"].values()))
except (OSError, ValueError, KeyError, StopIteration, subprocess.TimeoutExpired) as error:
status = "ownership check unavailable: " + str(error)
probed = None
elif candidates:
status = last_status
if status != last_status:
print("bannerlator-steam-games: " + status, flush=True)
last_status = status
+124 -1
View File
@@ -7,7 +7,8 @@
* segment is a plain file that shmat maps. Objects are found by the inode of their file, which is
* what the id is. semop applies its operations one by one (no all-or-nothing across a set) and
* SEM_UNDO is accepted but not performed - enough for the client, which uses single-counter sets.
* Message queues stay unsupported. */
* A message queue is a table of slots in its file: gamescope hands mangoapp (Deck mode's performance
* overlay) each frame's timings through one. */
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
@@ -15,6 +16,7 @@
#include <limits.h>
#include <linux/futex.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdint.h>
@@ -23,6 +25,7 @@
#include <string.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/stat.h>
@@ -396,6 +399,126 @@ int shmctl(int shmid, int cmd, struct shmid_ds *buf) {
}
}
/* ------------------------------------------------------------------ message queues */
/* Only what gamescope and mangoapp use: msgtyp 0 or > 0, IPC_NOWAIT and IPC_RMID. */
#define MSG_MAGIC 0x574e4d51u /* "WNMQ" */
#define MSG_SLOTS 32
struct msg_queue {
uint32_t magic;
_Atomic int32_t lock; /* 0 free, else the holder's pid */
_Atomic int32_t changed; /* bumped on every send and receive; waiters sleep on it */
uint64_t next_seq;
struct { uint64_t seq; long mtype; size_t len; char data[2048]; } slot[MSG_SLOTS]; /* seq 0: free */
};
static struct msg_queue *msg_map(int id) {
int fd = open_by_id("msg", id);
if (fd < 0) return NULL;
struct msg_queue *q = mmap(NULL, sizeof(*q), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (q == MAP_FAILED) return NULL;
if (q->magic == MSG_MAGIC) return q;
munmap(q, sizeof(*q));
errno = EINVAL;
return NULL;
}
/* A process killed while holding the lock (Android ends sessions without warning) loses it. */
static void msg_lock(struct msg_queue *q) {
for (int32_t owner = 0; !atomic_compare_exchange_strong(&q->lock, &owner, getpid()); owner = 0) {
if (kill(owner, 0) != 0 && errno == ESRCH) atomic_compare_exchange_strong(&q->lock, &owner, 0);
else futex(&q->lock, FUTEX_WAIT, owner, &(struct timespec){0, 20000000});
}
}
static void msg_unlock(struct msg_queue *q, int changed) {
atomic_store(&q->lock, 0);
futex(&q->lock, FUTEX_WAKE, 1, NULL);
if (changed) {
atomic_fetch_add(&q->changed, 1);
futex(&q->changed, FUTEX_WAKE, INT_MAX, NULL);
}
}
int msgget(key_t key, int msgflg) {
int created;
int fd = open_object("msg", key, msgflg, &created);
if (fd < 0) return -1;
uint32_t magic = MSG_MAGIC;
if (created && (ftruncate(fd, sizeof(struct msg_queue)) != 0 || pwrite(fd, &magic, sizeof(magic), 0) != sizeof(magic))) {
close(fd);
return -1;
}
int id = object_id(fd);
close(fd);
return id;
}
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg) {
struct msg_queue *q = msg_map(msqid);
if (!q) return -1;
if (msgsz > sizeof(q->slot[0].data) || *(const long *)msgp < 1) { munmap(q, sizeof(*q)); errno = EINVAL; return -1; }
for (;;) {
msg_lock(q);
int32_t seen = atomic_load(&q->changed);
for (int i = 0; i < MSG_SLOTS; i++) {
if (q->slot[i].seq) continue;
q->slot[i].mtype = *(const long *)msgp;
q->slot[i].len = msgsz;
memcpy(q->slot[i].data, (const long *)msgp + 1, msgsz);
q->slot[i].seq = ++q->next_seq;
msg_unlock(q, 1);
munmap(q, sizeof(*q));
return 0;
}
msg_unlock(q, 0);
if ((msgflg & IPC_NOWAIT) || (futex(&q->changed, FUTEX_WAIT, seen, NULL) != 0 && errno == EINTR)) {
munmap(q, sizeof(*q));
if (msgflg & IPC_NOWAIT) errno = EAGAIN;
return -1;
}
}
}
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) {
if (msgtyp < 0) { errno = EINVAL; return -1; }
struct msg_queue *q = msg_map(msqid);
if (!q) return -1;
for (;;) {
msg_lock(q);
int32_t seen = atomic_load(&q->changed);
int best = -1;
for (int i = 0; i < MSG_SLOTS; i++)
if (q->slot[i].seq && (!msgtyp || q->slot[i].mtype == msgtyp) && (best < 0 || q->slot[i].seq < q->slot[best].seq))
best = i;
if (best >= 0) {
size_t n = q->slot[best].len < msgsz ? q->slot[best].len : msgsz;
*(long *)msgp = q->slot[best].mtype;
memcpy((long *)msgp + 1, q->slot[best].data, n);
q->slot[best].seq = 0;
msg_unlock(q, 1);
munmap(q, sizeof(*q));
return (ssize_t)n;
}
msg_unlock(q, 0);
if ((msgflg & IPC_NOWAIT) || (futex(&q->changed, FUTEX_WAIT, seen, NULL) != 0 && errno == EINTR)) {
munmap(q, sizeof(*q));
if (msgflg & IPC_NOWAIT) errno = ENOMSG;
return -1;
}
}
}
int msgctl(int msqid, int cmd, struct msqid_ds *buf) {
if (cmd != IPC_RMID) { errno = EINVAL; return -1; }
pthread_mutex_lock(&g_lock);
int rc = unlink_by_id("msg", msqid);
pthread_mutex_unlock(&g_lock);
return rc;
}
/*
* fork() carries over only the calling thread, so a lock another thread was holding at that
* instant stays held in the child by a thread that is not there to release it. Taking it before
+101
View File
@@ -0,0 +1,101 @@
/*
* A trace instance for mangoapp where the kernel's tracefs is closed to the app.
*
* Valve's mangoapp (Deck mode's performance overlay) reads the GPU memory total on a Qualcomm
* display driver (msm_dpu / msm_drm) from the gpu_mem_total event, through a tracefs instance of
* its own - and aborts when tracefs_instance_create() fails, taking the whole overlay with it.
* Android mounts tracefs for root and the readtracefs group only, so it always fails here.
*
* The real calls run first. Only when they fail is the instance placed in an empty directory of
* the session's own, and reading its events answers with one gpu_mem_total record carrying the
* total KGSL - the GPU driver of every Adreno under Android - reports to the app.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#define STANDIN_DIR "/tmp/droiddeck-tracefs"
#define KGSL_TOTAL "/sys/class/kgsl/kgsl/page_alloc"
struct tracefs_instance;
struct tep_handle;
struct tep_event;
/* The head of libtraceevent 1.8's struct tep_record (the version tools/mangoapp/packages.txt pins). */
struct tep_record {
unsigned long long ts, offset;
long long missed_events;
int record_size, size;
void *data;
int cpu, ref_count, locked;
void *priv;
};
/* The event as Android kernels declare it (trace/events/gpu_mem.h). */
static const char gpu_mem_total_format[] =
"name: gpu_mem_total\n"
"ID: 1\n"
"format:\n"
"\tfield:unsigned short common_type;\toffset:0;\tsize:2;\tsigned:0;\n"
"\tfield:unsigned char common_flags;\toffset:2;\tsize:1;\tsigned:0;\n"
"\tfield:unsigned char common_preempt_count;\toffset:3;\tsize:1;\tsigned:0;\n"
"\tfield:int common_pid;\toffset:4;\tsize:4;\tsigned:1;\n"
"\n"
"\tfield:uint32_t gpu_id;\toffset:8;\tsize:4;\tsigned:0;\n"
"\tfield:uint32_t pid;\toffset:12;\tsize:4;\tsigned:0;\n"
"\tfield:uint64_t size;\toffset:16;\tsize:8;\tsigned:0;\n"
"\n"
"print fmt: \"gpu_id=%u pid=%u size=%llu\", REC->gpu_id, REC->pid, REC->size\n";
static struct tracefs_instance *standin;
struct tracefs_instance *tracefs_instance_create(const char *name) {
struct tracefs_instance *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_instance_create");
struct tracefs_instance *instance = real ? real(name) : NULL;
if (instance || !name) return instance;
struct tracefs_instance *(*alloc)(const char *, const char *) = dlsym(RTLD_NEXT, "tracefs_instance_alloc");
if (!alloc) return NULL;
char path[PATH_MAX];
mkdir(STANDIN_DIR, 0700);
mkdir(STANDIN_DIR "/instances", 0700);
snprintf(path, sizeof(path), STANDIN_DIR "/instances/%s", name);
mkdir(path, 0700);
return standin = alloc(STANDIN_DIR, name);
}
struct tep_handle *tracefs_local_events(const char *tracing_dir) {
if (!tracing_dir || strncmp(tracing_dir, STANDIN_DIR "/", sizeof(STANDIN_DIR))) {
struct tep_handle *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_local_events");
return real ? real(tracing_dir) : NULL;
}
struct tep_handle *(*tep_alloc)(void) = dlsym(RTLD_DEFAULT, "tep_alloc");
int (*parse)(struct tep_handle *, const char *, unsigned long, const char *) = dlsym(RTLD_DEFAULT, "tep_parse_event");
struct tep_handle *tep = tep_alloc ? tep_alloc() : NULL;
if (tep && parse) parse(tep, gpu_mem_total_format, sizeof(gpu_mem_total_format) - 1, "gpu_mem");
return tep;
}
int tracefs_iterate_raw_events(struct tep_handle *tep, struct tracefs_instance *instance, void *cpus, int cpu_size,
int (*callback)(struct tep_event *, struct tep_record *, int, void *), void *context) {
if (!standin || instance != standin) {
int (*real)(struct tep_handle *, struct tracefs_instance *, void *, int,
int (*)(struct tep_event *, struct tep_record *, int, void *), void *) =
dlsym(RTLD_NEXT, "tracefs_iterate_raw_events");
return real ? real(tep, instance, cpus, cpu_size, callback, context) : -1;
}
struct tep_event *(*find)(struct tep_handle *, int) = dlsym(RTLD_DEFAULT, "tep_find_event");
struct tep_event *event = find && tep ? find(tep, 1) : NULL;
unsigned long long total;
FILE *f = fopen(KGSL_TOTAL, "r");
int have = f && fscanf(f, "%llu", &total) == 1;
if (f) fclose(f);
if (!event || !have || !callback) return -1;
struct { uint16_t type; uint8_t flags, preempt; int32_t common_pid; uint32_t gpu_id, pid; uint64_t size; } data = {
.type = 1, .size = total};
struct tep_record record = {.record_size = sizeof(data), .size = sizeof(data), .data = &data};
return callback(event, &record, 0, context);
}
+17
View File
@@ -1,4 +1,21 @@
#!/bin/sh
# gamescope --mangoapp runs `mangoapp` from PATH; this puts Valve's build beside the libraries the
# runtime lacks (fmt 10, spdlog 1.13, glfw, libtracefs, libtraceevent), staged by the app.
# mangoapp reads its window through GLFW's X11 API. GLFW 3.4 takes XDG_SESSION_TYPE=wayland with a
# WAYLAND_DISPLAY as Wayland-only, and unset, libwayland falls back to wayland-0 - DroidDeck's own
# compositor, which shares XDG_RUNTIME_DIR - so both fail; x11 makes GLFW use gamescope's X server.
export XDG_SESSION_TYPE=x11
# gamescope's reaper restarts mangoapp the moment it exits, with no backoff. One that dies at start
# (a stale session preload without the System V queue, a driver it crashes on) was started about
# once a second for the whole session, each start a full exec under proot - the Steam menu itself
# lagged. More than 5 starts in 60 s leaves the overlay off: this waits instead of exiting, so the
# reaper has nothing to restart until the session ends.
starts=/tmp/droiddeck-mangoapp-starts
now=$(date +%s)
recent=$(awk -v now="$now" '$1 > now - 60' "$starts" 2>/dev/null)
printf '%s\n%s\n' "$recent" "$now" | sed '/^$/d' > "$starts"
if [ "$(wc -l < "$starts")" -gt 5 ]; then
echo "mangoapp: started $(wc -l < "$starts") times in 60 s; the performance overlay stays off for this session" >&2
exec sleep infinity
fi
LD_LIBRARY_PATH=/usr/local/lib/mangoapp${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} exec /usr/local/lib/mangoapp/mangoapp "$@"
+26
View File
@@ -44,3 +44,29 @@ Ported from WinNative (`main`, 53836ca9, "Fix/performance and vac"):
- `0009-tracee-relatives-sweep.patch` - tracees count their children, so a terminating thread
with no children or ptracees no longer walks every tracee; the per-stop memory collector is
emptied instead of freed and reallocated.
Added by DroidDeck:
- `0011-kompat-utsname-only.patch` - `--kernel-release` (the guest's `DroidDeck` hostname) loads
kompat, whose filter traps `futex`, `fcntl`, `epoll_pwait`, `pselect6`, `pipe2`, `eventfd2`,
`socket` and more, and which strips `AT_SYSINFO_EHDR` on every `execve`, so glibc runs without
the vDSO. When the virtual release is not older than the real kernel and the hwcap is left
alone, every one of those handlers is a no-op: kompat now traces only `uname`, `sethostname`
and `setdomainname` and leaves the auxv as the kernel wrote it. On an SD 8 Gen 2 guest this
took a futex ping-pong from 467 to 97 us, `epoll_pwait` from 60 to 0.8 us and `fcntl` from
40-107 to 0.4 us, and `clock_gettime` back to the vDSO.
- `0012-fake_id0-identity-only.patch` - `-i uid:gid` (for Xwayland's setgid/setuid before it runs
xkbcomp) loads fake_id0, whose filter traps every `fstat`/`newfstatat`/`stat` (entry and exit),
every `sendmsg` (all Wayland, X11, Chromium and PulseAudio traffic), `socket`, `getsockopt`, the
`get*id` family and the chown/chmod family. When the ids given are the ones proot really has and
are not 0, every one of those handlers is a no-op; fake_id0 now traces only the `set*id` family
and the xattr permission fixups, and leaves set-user-ID bits alone on `execve` (Android mounts the
app's data `nosuid`, so the kernel would not honour them either), which keeps the ids fixed. On an
x86_64 host build under the same options: `fstat` 41.7 -> 1.3 us, `sendmsg` 18.0 -> 2.5 us.
- `0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch` - every `ioctl` stopped on entry and
exit, which is every GPU submit and wait. The filter now traces only the requests enter.c and
exit.c act on (`TCSETSF`, the four termios2 requests, `FICLONE`) and allows the rest at once, and
the ioctl block is emitted first (after WinNative 53836ca9, which traces only the termios2 ones).
The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and
the `statx` one (emulation on kernels before 4.11) are dropped when the running kernel is newer.
Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop).
@@ -0,0 +1,64 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: DroidDeck <[email protected]>
Date: Tue, 29 Sep 2026 00:00:00 +0000
Subject: [PATCH] kompat: only trace the utsname calls when no older kernel is
emulated
---
src/extension/kompat/kompat.c | 29 +++++++++++++++++++++++++++--
1 file changed, 27 insertions(+), 2 deletions(-)
diff --git a/src/extension/kompat/kompat.c b/src/extension/kompat/kompat.c
index 5c39942..4cb7dec 100644
--- a/src/extension/kompat/kompat.c
+++ b/src/extension/kompat/kompat.c
@@ -985,6 +985,29 @@ static FilteredSysnum filtered_sysnums[] = {
FILTERED_SYSNUM_END,
};
+/* When the virtual kernel is not older than the actual one and the
+ * hwcap is left alone, the only thing left to emulate is the utsname
+ * itself: every other handler above returns without a change. */
+static FilteredSysnum utsname_only_sysnums[] = {
+ { PR_setdomainname, FILTER_SYSEXIT },
+ { PR_sethostname, FILTER_SYSEXIT },
+ { PR_uname, FILTER_SYSEXIT },
+ FILTERED_SYSNUM_END,
+};
+
+/**
+ * Return whether @config only renames the kernel identity (nodename,
+ * domainname, version string) without emulating an older kernel or
+ * another hwcap, in which case futex, fcntl, epoll_pwait, ... don't
+ * have to stop in the tracer and the vDSO can be left in place.
+ */
+static bool utsname_only(const Config *config)
+{
+ return config->actual_release != 0
+ && config->virtual_release >= config->actual_release
+ && config->hwcap == (word_t) -1;
+}
+
/**
* Handler for this @extension. It is triggered each time an @event
* occured. See ExtensionEvent for the meaning of @data1 and @data2.
@@ -1007,7 +1030,9 @@ int kompat_callback(Extension *extension, ExtensionEvent event,
if (status < 0)
return -1;
- extension->filtered_sysnums = filtered_sysnums;
+ extension->filtered_sysnums = utsname_only(config)
+ ? utsname_only_sysnums
+ : filtered_sysnums;
return 0;
}
@@ -1038,7 +1063,7 @@ int kompat_callback(Extension *extension, ExtensionEvent event,
/* Note: this can be done only before PRoot pushes the
* load script into tracee's stack. */
- if ((int) result >= 0 && sysnum == PR_execve)
+ if ((int) result >= 0 && sysnum == PR_execve && !utsname_only(config))
adjust_elf_auxv(tracee, config);
return 0;
}
@@ -0,0 +1,103 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: DroidDeck <[email protected]>
Date: Tue, 29 Sep 2026 00:00:00 +0000
Subject: [PATCH] fake_id0: only trace the set*id and xattr calls when the ids
are PRoot's own
---
src/extension/fake_id0/config.h | 5 ++++
src/extension/fake_id0/fake_id0.c | 44 ++++++++++++++++++++++++++++++-
2 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/src/extension/fake_id0/config.h b/src/extension/fake_id0/config.h
index 2e78783..5227ced 100644
--- a/src/extension/fake_id0/config.h
+++ b/src/extension/fake_id0/config.h
@@ -2,6 +2,7 @@
#define FAKE_ID0_CONFIG_H
#include <sys/types.h> /* uid_t, gid_t */
+#include <stdbool.h> /* bool, */
typedef struct {
uid_t ruid;
@@ -15,6 +16,10 @@ typedef struct {
gid_t fsgid;
mode_t umask;
+
+ /* The emulated ids are the ones PRoot really has: see
+ * identity_sysnums in fake_id0.c. */
+ bool identity;
} Config;
#endif /* FAKE_ID0_CONFIG_H */
diff --git a/src/extension/fake_id0/fake_id0.c b/src/extension/fake_id0/fake_id0.c
index 9477f1c..fe08372 100644
--- a/src/extension/fake_id0/fake_id0.c
+++ b/src/extension/fake_id0/fake_id0.c
@@ -352,6 +352,40 @@ static FilteredSysnum filtered_sysnums[] = {
FILTERED_SYSNUM_END,
};
+/* When the emulated ids are the ones PRoot really has, and the tracee
+ * is not root, every handler for the syscalls above but these is a
+ * no-op: stat and get*id report the real ids already, SCM_CREDENTIALS
+ * and SO_PEERCRED carry them, chown swaps an id for itself, and the
+ * permission and chroot fixups only apply to euid 0. The set*id
+ * family is still emulated (Android's app seccomp policy traps it) and
+ * so are the xattr permission fixups, which apply to any euid. An
+ * unprivileged set*id can only take an id the tracee already holds,
+ * and set-user-ID bits are not honoured in this mode (Android mounts
+ * the app's data nosuid, so the kernel would not honour them either),
+ * so the ids never change and the list stays sufficient. */
+static FilteredSysnum identity_sysnums[] = {
+ { PR_fsetxattr, FILTER_SYSEXIT },
+ { PR_lsetxattr, FILTER_SYSEXIT },
+ { PR_setfsgid, FILTER_SYSEXIT },
+ { PR_setfsgid32, FILTER_SYSEXIT },
+ { PR_setfsuid, FILTER_SYSEXIT },
+ { PR_setfsuid32, FILTER_SYSEXIT },
+ { PR_setgid, FILTER_SYSEXIT },
+ { PR_setgid32, FILTER_SYSEXIT },
+ { PR_setregid, FILTER_SYSEXIT },
+ { PR_setregid32, FILTER_SYSEXIT },
+ { PR_setresgid, FILTER_SYSEXIT },
+ { PR_setresgid32, FILTER_SYSEXIT },
+ { PR_setresuid, FILTER_SYSEXIT },
+ { PR_setresuid32, FILTER_SYSEXIT },
+ { PR_setreuid, FILTER_SYSEXIT },
+ { PR_setreuid32, FILTER_SYSEXIT },
+ { PR_setuid, FILTER_SYSEXIT },
+ { PR_setuid32, FILTER_SYSEXIT },
+ { PR_setxattr, FILTER_SYSEXIT },
+ FILTERED_SYSNUM_END,
+};
+
/**
* Restore the @node->mode for the given @node->path.
*
@@ -1051,6 +1085,11 @@ static int handle_sysexit_start(Tracee *tracee, Config *config) {
if ((int) result < 0 || tracee->status < 0 || sysnum != PR_execve)
return 0;
+ /* The auxv already carries the real ids, and set-user-ID bits
+ * are not honoured in this mode: see identity_sysnums. */
+ if (config->identity)
+ return 0;
+
/* This has to be done before PRoot pushes the load
* script into tracee's stack. */
if (!tracee->skip_proot_loader)
@@ -1119,7 +1158,10 @@ int fake_id0_callback(Extension *extension, ExtensionEvent event, intptr_t data1
/* Set the umask to the typical linux value. */
config->umask = 022;
- extension->filtered_sysnums = filtered_sysnums;
+ config->identity = (uid != 0 && uid == (int) getuid() && gid == (int) getgid());
+ extension->filtered_sysnums = config->identity
+ ? identity_sysnums
+ : filtered_sysnums;
return 0;
}
@@ -0,0 +1,169 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: DroidDeck <[email protected]>
Date: Tue, 29 Sep 2026 00:00:00 +0000
Subject: [PATCH] seccomp: trace ioctl by request, drop exit stops the kernel
does not need
---
src/syscall/seccomp.c | 98 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 97 insertions(+), 1 deletion(-)
diff --git a/src/syscall/seccomp.c b/src/syscall/seccomp.c
index 0b381bb..1116ab2 100644
--- a/src/syscall/seccomp.c
+++ b/src/syscall/seccomp.c
@@ -39,6 +39,11 @@
#include <stdint.h> /* uint*_t, UINT*_MAX, */
#include <stdbool.h>
#include <sys/resource.h>
+#include <sys/utsname.h> /* uname(2), */
+#include <stdio.h> /* sscanf(3), */
+#include <asm/ioctls.h> /* TCGETS2, TCSETS*2, */
+#include <asm/termbits.h> /* struct termios2, */
+#include <linux/fs.h> /* FICLONE, */
#include <assert.h> /* assert(3), */
#include "syscall/seccomp.h"
@@ -166,6 +171,46 @@ static int add_trace_prlimit_stack(struct sock_fprog *program, word_t syscall, i
return add_statements(program, LENGTH_TRACE_PRLIMIT_STACK, statements);
}
+/**
+ * Append to @program->filter the statements that trace @syscall
+ * (ioctl) only for the requests enter.c and exit.c act on - the
+ * termios requests Android's policy denies an app on a pty, rewritten
+ * to ones it allows, and FICLONE, whose EACCES is turned into
+ * EOPNOTSUPP - and allow any other request at once. Every GPU submit
+ * and wait is an ioctl, so they no longer stop in the tracer, and the
+ * block is emitted first so they are also spared the rest of the list.
+ * (After WinNative, maxjivi05, 53836ca9.)
+ */
+static int add_trace_ioctl_requests(struct sock_fprog *program, word_t syscall, int flag)
+{
+ const size_t request_offset = offsetof(struct seccomp_data, args[1]);
+
+ #define LENGTH_TRACE_IOCTL 10
+ struct sock_filter statements[LENGTH_TRACE_IOCTL] = {
+ /* Not this syscall: skip the block, the accumulator
+ * still holds the syscall number. */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, syscall, 0, 9),
+
+ /* Load the request's low word into the accumulator. */
+ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, request_offset),
+
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSF, 5, 0),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCGETS2, 4, 0),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETS2, 3, 0),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSW2, 2, 0),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSF2, 1, 0),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (uint32_t) FICLONE, 0, 1),
+
+ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE + flag),
+ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
+ };
+
+ DEBUG_FILTER("FILTER: trace if syscall == %ld and a termios2/TCSETSF/FICLONE request\n",
+ syscall);
+
+ return add_statements(program, LENGTH_TRACE_IOCTL, statements);
+}
+
static size_t trace_length(Sysnum sysnum)
{
switch (sysnum) {
@@ -174,6 +219,8 @@ static size_t trace_length(Sysnum sysnum)
return LENGTH_TRACE_SYSCALL_IF_ARG;
case PR_prlimit64:
return LENGTH_TRACE_PRLIMIT_STACK;
+ case PR_ioctl:
+ return LENGTH_TRACE_IOCTL;
default:
return LENGTH_TRACE_SYSCALL;
}
@@ -191,6 +238,8 @@ static int add_trace(struct sock_fprog *program, Sysnum sysnum, word_t syscall,
return add_trace_syscall_if_arg(program, syscall, 0, RLIMIT_STACK, flag);
case PR_prlimit64:
return add_trace_prlimit_stack(program, syscall, flag);
+ case PR_ioctl:
+ return add_trace_ioctl_requests(program, syscall, flag);
default:
return add_trace_syscall(program, syscall, flag);
}
@@ -329,7 +378,7 @@ static int set_seccomp_filters(const FilteredSysnum *sysnums)
struct sock_fprog program = { .len = 0, .filter = NULL };
size_t section_length;
- size_t i, j, k;
+ size_t i, j, k, pass;
int status;
status = new_program_filter(&program);
@@ -355,8 +404,14 @@ static int set_seccomp_filters(const FilteredSysnum *sysnums)
if (status < 0)
goto end;
+ /* Two passes: ioctl first, it is by far the most frequent
+ * syscall and almost every one of them is allowed. */
+ for (pass = 0; pass < 2; pass++)
for (j = 0; j < seccomp_archs[i].nb_abis; j++) {
for (k = 0; sysnums[k].value != PR_void; k++) {
+ if ((sysnums[k].value == PR_ioctl) != (pass == 0))
+ continue;
+
/* Get the architecture specific syscall number. */
syscall = detranslate_sysnum(seccomp_archs[i].abis[j], sysnums[k].value);
if (syscall == SYSCALL_AVOIDER)
@@ -572,12 +627,53 @@ int enable_syscall_filtering(const Tracee *tracee)
return 0;
}
+/**
+ * Drop the exit stops PRoot only needs on kernels older than the one it
+ * runs on. The faccessat2 exit stop restores the arguments for glibc's
+ * fallback to faccessat on ENOSYS (kernels before 5.8, termux 5ba8b95);
+ * the statx one emulates the call when it fails, which only a kernel
+ * before 4.11 does for want of it - a statx Android's policy traps is
+ * emulated from SIGSYS instead. Both stops re-read and re-translate
+ * the path, and are paid on every call.
+ */
+static void drop_unneeded_exit_stops(void)
+{
+ static bool done = false;
+ struct utsname utsname;
+ int major, minor;
+ size_t i;
+
+ if (done)
+ return;
+ done = true;
+
+ if (uname(&utsname) < 0 || sscanf(utsname.release, "%d.%d", &major, &minor) != 2)
+ return;
+
+ for (i = 0; proot_sysnums[i].value != PR_void; i++) {
+ switch (proot_sysnums[i].value) {
+ case PR_faccessat2:
+ if (major > 5 || (major == 5 && minor >= 8))
+ proot_sysnums[i].flags &= ~FILTER_SYSEXIT;
+ break;
+ case PR_statx:
+ if (major > 4 || (major == 4 && minor >= 11))
+ proot_sysnums[i].flags &= ~FILTER_SYSEXIT;
+ break;
+ default:
+ break;
+ }
+ }
+}
+
static int collect_filtered_sysnums(TALLOC_CTX *context, const Tracee *tracee,
FilteredSysnum **filtered_sysnums)
{
Extension *extension;
int status;
+ drop_unneeded_exit_stops();
+
/* Add the sysnums required by PRoot to the list of filtered
* sysnums. TODO: only if path translation is required. */
status = merge_filtered_sysnums(context, filtered_sysnums, proot_sysnums);