Compare commits

...
Author SHA1 Message Date
xXJsonDeruloXx 1799fc6621 docs: note the heavy-game launches that never reached a scene 2026-09-29 19:00:34 -04:00
xXJsonDeruloXx b711a96863 docs: write up the performance findings, and give the local build's image check a minute to outlast a Docker Desktop restart 2026-09-29 18:14:09 -04:00
xXJsonDeruloXx f3c923b472 perf: leave Turnip sysmem off by default again; it gained nothing on an Adreno 740 and only the 710-720 drivers need it 2026-09-29 18:04:41 -04:00
xXJsonDeruloXx 1e804562b1 perf: WinNative's GPU and FEX defaults, opt-in sustained mode, the panel's top refresh rate, and fork-free session polling 2026-09-29 16:22:51 -04:00
xXJsonDeruloXx e275fbd788 perf: cache pinned downloads, skip unchanged proot rebuilds and log why the build image is rebuilt in the local build 2026-09-29 16:22:51 -04:00
xXJsonDeruloXx c3bd298ecf fix: show the saved Steam Deck mode on the Steam page before Performance has been opened 2026-09-29 16:05:25 -04:00
xXJsonDeruloXx 807f5adcba perf: skip xalia and gamescope realtime queues by default, add a realtime toggle, default the mic off 2026-09-29 15:54:03 -04:00
xXJsonDeruloXx d6bc4da759 fix: retry the local build's image check instead of rebuilding on a busy docker daemon 2026-09-29 15:54:03 -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
34 changed files with 1382 additions and 123 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
};
+97 -15
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];
@@ -466,6 +468,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 +724,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 +776,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 +948,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 +1059,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 +1201,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 +1697,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 +1727,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 +1799,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 +2138,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 +2421,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 +3119,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 +3166,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,9 @@ 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 sustainedPerformance 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 +505,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 +808,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 +860,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 +895,17 @@ 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, sustainedPerformance = sustainedPerformance,
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 },
onSustainedPerformance = { on -> SessionPrefs.setSustainedPerformance(this, on); sustainedPerformance = on },
onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on },
onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) },
onClientCore = { core, on ->
@@ -939,6 +950,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 +1000,8 @@ class MainActivity : ComponentActivity() {
tuSysmem = SessionPrefs.tuSysmem(this)
zinkLazy = SessionPrefs.zinkLazy(this)
noXalia = SessionPrefs.noXalia(this)
gamescopeRealtime = SessionPrefs.gamescopeRealtime(this)
sustainedPerformance = SessionPrefs.sustainedPerformance(this)
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
guestHostname = SessionPrefs.guestHostname(this)
refreshPhantomStatus()
@@ -460,7 +460,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) {
@@ -848,9 +849,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
return Pair(width and 1.inv(), height and 1.inv())
}
/**
* The rate the panel runs the session at: its fastest mode at the current size, which PerfMode
* asks for. The display's current rate is read too early for that - a phone still at 60 Hz
* when the surface arrives handed gamescope -r 60 and capped every game at 60.
*/
private fun refreshHz(): Float {
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
val hz = display?.refreshRate ?: 60f
val cur = display?.mode
val best = if (cur == null) null else display.supportedModes
.filter { it.physicalWidth == cur.physicalWidth && it.physicalHeight == cur.physicalHeight }
.maxOfOrNull { it.refreshRate }
val hz = maxOf(best ?: 0f, display?.refreshRate ?: 0f)
return if (hz > 1f) hz else 60f
}
@@ -1598,7 +1608,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,8 @@ 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("Sustained performance mode", SessionPrefs.sustainedPerformance(context))
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
k("Guest host name", SessionPrefs.guestHostname(context))
k("DirectAudio for games", SessionPrefs.directAudio(context))
@@ -27,8 +27,9 @@ object GameEnvironment {
fun defaults(preset: String): Map<String, String?> = linkedMapOf<String, String?>(
"MESA_SHADER_CACHE_DISABLE" to "false",
"VKD3D_FEATURE_LEVEL" to "12_2",
"VKD3D_SHADER_MODEL" to "6_9",
// The feature level is left to vkd3d-proton, which reports what the driver can do; forcing
// 12_2 sent games down DX12 Ultimate paths Turnip only emulates. 6_6 is WinNative's model.
"VKD3D_SHADER_MODEL" to "6_6",
).apply {
FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) }
}
@@ -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);
@@ -13,8 +13,8 @@ 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;
* - sustained performance mode (Android 7+), only when the Performance page asks for it: clocks
* the SoC can hold for hours, which on many devices caps the peak a session otherwise gets;
* - 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
@@ -30,7 +30,7 @@ object PerfMode {
val parts = ArrayList<String>(3)
// Sustained performance mode.
parts += if (Build.VERSION.SDK_INT >= 24) {
parts += if (!SessionPrefs.sustainedPerformance(a)) "sustained mode off" else 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" }
@@ -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,39 @@ 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.
*/
/**
* Whether the session window asks for Android's sustained performance mode. Off by default: it
* holds the SoC at clocks it can keep for hours, which on many devices is a cap well under
* the peak a game session gets without it (WinNative never asks for it).
*/
fun sustainedPerformance(context: Context): Boolean = prefs(context).getBoolean("sustainedPerformance", false)
fun setSustainedPerformance(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("sustainedPerformance", on).apply()
}
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).
*
@@ -248,7 +273,7 @@ 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() }
@@ -309,8 +334,12 @@ 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 own
* defaults. PERFORMANCE_TSO unless chosen: TSO only on the main path, x87 reduced precision
* and multiblock, as WinNative ships it.
*/
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "PERFORMANCE_TSO") ?: "PERFORMANCE_TSO"
fun setFexPreset(context: Context, id: String) {
prefs(context).edit().putString("fexPreset", id).apply()
@@ -240,10 +240,12 @@ class SessionService : Service() {
val override = File(Environment.getExternalStorageDirectory(), TU_DEBUG_SWITCH)
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim() }
if (!override.isNullOrEmpty()) return override
if (SessionPrefs.tuSysmem(this)) return "sysmem"
if (linuxDriverId.isEmpty()) return null
val name = LinuxVulkanDriverManager(this).getDriverName(linuxDriverId).lowercase()
return if (name.contains("710-720") || name.contains("710_720")) "sysmem" else null
// noconform: Turnip keeps the Vulkan features it has not passed conformance for (WinNative's
// default beside sysmem), which DXVK and vkd3d-proton reach for.
val sysmem = SessionPrefs.tuSysmem(this) || linuxDriverId.isNotEmpty() &&
LinuxVulkanDriverManager(this).getDriverName(linuxDriverId).lowercase()
.let { it.contains("710-720") || it.contains("710_720") }
return if (sysmem) "noconform,sysmem" else "noconform"
}
// ── The session ─────────────────────────────────────────────────────────────────────────
@@ -519,6 +521,8 @@ class SessionService : Service() {
// BL_STEAMDECK; it is the one that builds the command line).
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
// Zink packs its descriptor sets and shader interfaces tighter (WinNative's default).
guest.add("ZINK_DEBUG=compact")
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
if (SessionState.mode == MODE_STEAM) {
@@ -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,9 @@ fun PerformancePage(
zinkLazy: Boolean,
glThread: Boolean,
noGlError: Boolean,
steamDeckMode: Boolean,
noXalia: Boolean,
gamescopeRealtime: Boolean,
sustainedPerformance: Boolean,
prootNoSeccomp: Boolean,
guestHostname: String,
phantomWarning: String?,
@@ -46,8 +47,9 @@ fun PerformancePage(
onZinkLazy: (Boolean) -> Unit,
onGlThread: (Boolean) -> Unit,
onNoGlError: (Boolean) -> Unit,
onSteamDeckMode: (Boolean) -> Unit,
onNoXalia: (Boolean) -> Unit,
onGamescopeRealtime: (Boolean) -> Unit,
onSustainedPerformance: (Boolean) -> Unit,
onProotNoSeccomp: (Boolean) -> Unit,
onGuestHostname: (String) -> Unit,
onClientCore: (Int, Boolean) -> Unit,
@@ -97,9 +99,9 @@ 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") {
@@ -110,9 +112,14 @@ fun PerformancePage(
)
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(
host, "sustained", "Android sustained performance mode",
"Holds clocks the device can keep for hours. Steadier on hot devices, but usually a lower peak.",
sustainedPerformance, onChange = onSustainedPerformance,
)
ToggleRow(
host, "seccomp", "Run proot without seccomp",
"May fix missing syscall errors on some kernels, but can reduce performance.",
+129
View File
@@ -0,0 +1,129 @@
# DroidDeck - Performance findings
Why DroidDeck 0.2.0 ran slower than WinNative and Bannerlator for many people, what was found, what was
changed, what was measured, and what is still open. Written 2026-09-29 from the Discord reports, the GitHub
issues, and a day of measurement on an AYN Thor (SD 8 Gen 2, Adreno 740). Companion to
[progress-log.md](progress-log.md).
## What people reported
- **Discord (#general, 2026-09-26 to 09-29).** The same game at ~90-100 fps in WinNative and ~30-40 in
DroidDeck 0.1.7, ~25 in 0.2.0 (Adreno 732); "15 fps in a 2D Unity game" on an 8 Elite while Bannerlator's
Steam client is fine; "20 fps / 7 fps in the Steam client, all renders on CPU" on an Adreno 8xx; Bannerlator
locked at 120 fps where DroidDeck is not; "less performant, more compatible" as the usual summary.
- **[#65](https://github.com/Droid-Deck/DroidDeck/issues/65)** - the best report: Silksong at 85-90 fps on
0.1.5 and 37-50 on 0.2.0 on a Fold8, GPU 82% busy vs 16%, proot 3-5x busier, the game's main thread waiting
on `futex` and the wineserver pipe. History table: 90 (0.1.5), 103 (0.1.6), 58/110/48 (0.1.7), 47-50 (0.2.0).
- **[#64](https://github.com/Droid-Deck/DroidDeck/issues/64)** - Samsung's SSRM clamps CPU/GPU to ~60% in a
session (Tab S8 Ultra). A platform limit, separate from everything below; no sanctioned way around it found.
## Cause 1: proot's kompat extension traced every hot syscall (fixed, PR #81)
`--kernel-release` (added 2026-09-25 to give the guest the `DroidDeck` hostname, `53ce764`) loads proot's
kompat extension. Its filter list includes `futex`, `epoll_pwait`, `fcntl`, `pselect6`, `pipe2`, `eventfd2`,
`socket`, so each of those from every guest thread stopped in the single tracer, and it drops
`AT_SYSINFO_EHDR` on every `execve`, so glibc ran without the vDSO. With the real kernel release passed, every
one of those handlers is a no-op. Patch `tools/proot/patches/0011-kompat-utsname-only.patch` traces only
`uname`/`sethostname`/`setdomainname` in that case.
Guest micro-benchmarks (same rootfs, proot with vs without the patch):
| | old + `--kernel-release` | patched | no flag at all |
|---|---|---|---|
| futex ping-pong | 465 us | 102 us | 96 us |
| `epoll_pwait(0)` | 60 us | 0.87 us | 0.8 us |
| `fcntl(F_GETFL)` | 40-107 us | 0.39 us | 0.38 us |
| `clock_gettime` | 0.34 us | 0.19 us | 0.11-0.19 us |
A-B-A in Once Upon a KATAMARI (standing still, only `libproot.so` swapped): +2-5% fps, proot CPU ~56% ->
~35%. Small there because Katamari is limited by translated game code; sync-heavy games should gain more.
## Cause 2: a crash-looping mangoapp in Deck mode (fixed, PR #81)
`gamescopereaper --respawn -- mangoapp` restarts the overlay the moment it exits, with no backoff. On a build
whose session preload lacked the System V message queue emulation, mangoapp died right after creating its
swapchain and was restarted 132 times in ~2.5 minutes; each restart is a full exec under proot and the Steam
menu itself lagged. Local builds that skip rebuilding `libblsession.so` hit this; CI builds do not. The wrapper
(`tools/mangoapp/mangoapp`) now parks after more than 5 starts in 60 s.
## What Bannerlator and WinNative do differently
Read from their sources and a device that has Bannerlator installed:
- **Same proot patches** (0001-0009 from WinNative); the Steam client mode uses the same Valve ARM64 Proton, so
**the same server-side Wine synchronisation** (`wineserver: using server-side synchronization` in every
session log here). No structural gap remains once kompat is out.
- **Defaults differ.** Bannerlator ships Deck mode off (it notes Steam Input takes the pad and breaks games),
never passes `-steamos3`, does not set gamescope's realtime queue, runs xalia. WinNative defaults to
`TU_DEBUG=noconform,sysmem`, `ZINK_DEBUG=compact`, FEX `PERFORMANCE_TSO`, vkd3d shader model 6_6, and never
turns on sustained performance mode.
## Changes on `perf/steam-client-parity`
- xalia skipped by default (it ran ~10% of a core beside every game under FEX).
- gamescope realtime Vulkan queues: new toggle (Performance -> Client interface), **off** by default.
- Microphone off by default; no permission prompt at start-up.
- FEX default preset `PERFORMANCE_TSO`; `TU_DEBUG=noconform` always (plus `sysmem` for the 710-720 drivers or
when chosen); `ZINK_DEBUG=compact`; vkd3d shader model 6_6, feature level left to vkd3d-proton.
- Sustained performance mode is opt-in (Performance page); PerfMode logs "sustained mode off".
- The session gets the panel's fastest mode's refresh rate instead of the rate at the moment the surface arrived.
- Session polling without forks: the fill-screen and stop watchers wait on a FIFO read timeout and read files
with builtins; the redistributable seeder and the compat registrar (Python) start only when a prefix or the
Steam config actually changed, not every 5-15 s.
- The Steam page shows the saved Deck mode before the Performance page has been opened (it showed "off" while
the session ran Deck mode with mangoapp).
- `tools/build_local.sh`: caches its pinned downloads, skips an unchanged proot build, and says why it rebuilds
the Docker image; a warm build is ~30 s (it used to hang on a registry fetch when the daemon briefly reported
the image missing).
- Turnip **sysmem** was made a default and then reverted: it gained nothing on an Adreno 740.
## How it was measured
- **fps:** `BannerWayland [stats]` lines in logcat (`adb logcat -s BannerWayland:I`), every 10 s. No `su`, no
process on the device. The "screen" number counts presents and runs ~1.5x the game's own overlay counter in
Alan Wake (60 vs 38-40): compare like with like, and read the overlay for the game's real rate.
- **Scene:** Alan Wake's American Nightmare, first gameplay spot ("Secure the primary pipes"), standing still.
Reached without touching the screen: `~/.bl-autolaunch` is ignored once Steam is up, so the game is started with
`steam steam://rungameid/<id>` from a second proot invocation (the client forwards it), and A is pressed with
`sendevent` on the device's own gamepad node (`/dev/input/event12`) - the app only forwards events from a real
controller device, so `adb shell input` presses are dropped.
- **Thermals:** the SoC reaches ~92-95 C in any run, so each run starts only after the CPU zones are under 55 C.
A one-shot comparison made hot was misleading (the new build read 52-54 fps then, 58-61 cooled).
Alan Wake, cooled starts, fan at max, 60 s each (screen counter, six 10 s samples):
| Build | screen fps | game overlay | proot CPU | wineserver CPU |
|---|---|---|---|---|
| `main` (kompat fix, old defaults) | 56.9-58.6 (avg 57.7) | 39 | ~39% | ~30% |
| new defaults | 58.6-61.5 (avg 59.6) | 38-39 | ~27% | ~34% |
| new defaults, sysmem off | 59.3-62.9 (avg 61.1) | 38 | - | - |
Read: the defaults are worth at most a few percent here and inside the run-to-run noise (about +-1.5 fps). The
game's own frame rate stayed at 38-40 with CPU ~65% and GPU ~56-60%, so this scene is bound by something none
of these settings touch.
## Open leads, biggest first
1. **Wine synchronisation.** Proton 11 dropped esync; fsync needs `futex_waitv`, which Android's app sandbox
blocks even on a 6.12 kernel; ntsync is not in Android kernels. Every lock and event is a pipe round trip to
wineserver (30-34% CPU in Alan Wake, 18% of the main thread blocked in `read()` in #65). Winlator-family apps
run esync, which needs only `eventfd` and `poll`. Restoring esync in the ARM64 Proton is the largest
likely win and the largest piece of work; emulating `futex_waitv` in proot puts every wait back through the
tracer and is probably a dead end.
2. **The game's main thread under FEX.** Alan Wake's process uses 139-178% CPU with neither the CPU nor the GPU
saturated overall. FEX settings beyond the presets (SMC checks, multiblock, TSO variants) are worth a sweep
on a game whose main thread is the limit.
3. **#65's other 0.1.5 -> 0.2.0 differences, untested:** the fake `power_supply` tree, `BL_HDR`/`DXVK_HDR`, the
output width (1280 -> the panel's shape), `FEX_TSOENABLED` (1 -> 0), the tracer binary. Their Silksong setup
is sync-heavy and is the best test of the kompat fix.
4. **gamescope** calls `XQueryPointer` on every vblank in the Steam UI (patch `0100`); query only when a
gamepad cursor move is pending. Needs a new pinned gamescope release.
5. **Heavy games on a fresh install.** Metal Gear Solid V: Ground Zeroes sat on Steam's launch splash for 5+ minutes
with wineserver at ~50% CPU and the GPU at 12% (no window ever appeared); Bulletstorm took ~4.5 minutes to reach
its first logo, the game process at 45-105% CPU and the GPU at 9%, and the session ended a minute later. Neither
reached a scene worth measuring. Both are first launches (redistributable install scripts, shader work), and a
wineserver spinning at 50% while a game shows nothing is itself the server-side synchronisation cost of lead 1.
6. **Reports not yet reproduced:** software rendering on Adreno 8xx ("all renders on CPU"), Deck mode using
most of the RAM, Samsung SSRM clamping (#64), two DroidDeck packages both holding a session.
7. **Smaller:** proot tracer pinned to a big core (Max's note: no core wake-up lag), `steamrtarm64` on Proton's
`LD_LIBRARY_PATH`, the always-on wake and Wi-Fi locks, ADPF hints fed frame intervals instead of work time.
+1
View File
@@ -4,6 +4,7 @@ Running engineering log for the DroidDeck app (`com.droiddeck.launcher`; called
`com.steamdeck.launcher`, until 2026-09-23 - entries below that date keep the old name). Newest state first, then
the timeline, then lessons and backlog. Companion to the README (what the app *does*) and to
`docs/releases/` (what each version said) - this is *how it got here and where it stands*.
Performance work has its own write-up: [performance.md](performance.md).
---
+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}"
@@ -75,8 +75,9 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
fi
# The app's gamescope build (tools/gamescope) takes 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. Off unless the app's
# Performance page turns it on (BL_GAMESCOPE_REALTIME=1).
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
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}
@@ -371,11 +372,18 @@ case $mode in
# ~/.droiddeck-fill, and each change goes to gamescope as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on
# the root window, which it watches. What gamescope was started with is where this begins.
# Not on the desktop (BL_DESKTOP): no gamescope there, and the loop would outlive the client.
# The loops below poll for the whole session, and every sleep, cat or script they start is a
# fork and exec under proot. They wait with a read timeout on a FIFO nothing writes to and
# read files with builtins, so an idle beat costs no process at all.
_bl_fifo=$(mktemp -u "${TMPDIR:-/tmp}/bl-nap.XXXXXX") && mkfifo "$_bl_fifo" \
&& exec {_bl_nap}<>"$_bl_fifo" && rm -f "$_bl_fifo"
nap() { if [ -n "${_bl_nap:-}" ]; then read -r -t "$1" -u "$_bl_nap" _ || true; else sleep "$1"; fi; }
[ -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 <"$HOME/.droiddeck-fill" 2>/dev/null || [ -n "$want" ] || 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
@@ -384,14 +392,37 @@ case $mode in
fi
done
) &
# Each beat only starts a script when there is something for it: the seeder when a prefix has
# a registry it has not marked, the registrar (Python, the heavier start) when a library, the
# tools directory or the client's config changed since its last run.
(
compat_stamp=$HOME/.cache/droiddeck-compat-stamp
needs_seed() {
local r
for r in "$steam_root"/steamapps/compatdata/*/pfx/system.reg \
/mnt/bannerlator-sd/steamapps/compatdata/*/pfx/system.reg; do
[ -f "$r" ] && [ ! -e "${r%/system.reg}/.bannerlator-redists" ] && return 0
done
return 1
}
needs_compat() {
local d
[ -e "$compat_stamp" ] || return 0
for d in "$steam_root/steamapps" "$steam_root/compatibilitytools.d" "$steam_root/config" \
/mnt/bannerlator-sd/steamapps; do
[[ -e $d && $d -nt $compat_stamp ]] && return 0
done
return 1
}
beat=0
for _ in $(seq 1 720); do
sleep 5
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
while [ "$beat" -lt 720 ]; do
nap 5
needs_seed && { bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true; }
beat=$((beat + 1))
if [ "$((beat % 3))" -eq 0 ]; then
if [ "$((beat % 3))" -eq 0 ] && needs_compat; then
: >"$compat_stamp"
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
: >"$compat_stamp"
fi
done
) &
@@ -501,20 +532,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 +589,7 @@ BUSCONF
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
exit 0
fi
sleep 0.5
nap 0.5
done
) &
stop_watcher=$!
+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 "$@"
+11
View File
@@ -44,3 +44,14 @@ 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.
@@ -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;
}