mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-22 23:24:34 -04:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c80bcb070 | ||
|
|
6565e93e8e | ||
|
|
72dedcb772 | ||
|
|
25079ab819 | ||
|
|
736b7bed5e | ||
|
|
a3f4c911e5 | ||
|
|
caa7682c6f | ||
|
|
e217cbe66d | ||
|
|
4914b2cc69 | ||
|
|
6ecd6fa1d0 | ||
|
|
0f4e83eefd | ||
|
|
d62525f5af | ||
|
|
f4d2c37fbd | ||
|
|
ffd9f92d7b | ||
|
|
213d110766 | ||
|
|
1a8c91b49b | ||
|
|
9644a4cc7f | ||
|
|
24c250fdb4 | ||
|
|
9d891d4db1 | ||
|
|
e797bc5f01 | ||
|
|
0cc3fe6bca | ||
|
|
ab8542e4e5 | ||
|
|
c2bcf20172 | ||
|
|
2341659cea | ||
|
|
b2eaf380dc |
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -238,6 +238,8 @@ struct surface {
|
||||
struct dmabuf_buffer *dmabuf_buf; /* its imported image, kept until replaced or the surface goes */
|
||||
struct wl_listener dmabuf_destroy;
|
||||
int buf_w, buf_h, has_content;
|
||||
int buf_alpha;
|
||||
int opaque[4], pending_opaque[4], pending_opaque_set; /* x, y, w, h; w = 0: none */
|
||||
int src_set, dst_set;
|
||||
float src[4];
|
||||
int dst[2];
|
||||
@@ -433,9 +435,16 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
|
||||
* delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier
|
||||
* limit had spaced out, and the game's first frames after that waited on empty slots. */
|
||||
static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) {
|
||||
int limit = g_fps_limit;
|
||||
if (limit <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
int64_t interval = 1000000000LL / limit, now = now_ns();
|
||||
const int limit = g_fps_limit;
|
||||
/* Without a limit of its own a game is paced to the screen. A swapchain that does not wait for
|
||||
* frame callbacks (MAILBOX, IMMEDIATE) is held back by nothing else here, so handing a replaced
|
||||
* buffer straight back let it draw as fast as the GPU allowed and everything above the refresh
|
||||
* rate was thrown away - 178 frames a second for 91 on screen, measured (WinNative, maxjivi05).
|
||||
* One release per refresh is the back-pressure X11 gives, and the screen shows the same frames
|
||||
* either way. */
|
||||
const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns;
|
||||
if (interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
int64_t now = now_ns();
|
||||
int64_t at = s->next_release_ns + interval;
|
||||
int64_t latest = now + interval * (int64_t)(s->releases_pending + 1);
|
||||
if (at < now) at = now;
|
||||
@@ -466,6 +475,12 @@ static struct seat_pointer g_ptrs[MAX_PTRS];
|
||||
static int g_nptrs;
|
||||
static struct seat_keyboard g_kbs[MAX_PTRS];
|
||||
static int g_nkbs;
|
||||
/* Modifier state in the keymap's real-modifier bits (Shift=0x1, Lock=0x2, Control=0x4, Mod1=0x8,
|
||||
* Mod4=0x40). A client of the wayland backend - gamescope, labwc - takes modifiers only from
|
||||
* wl_keyboard.modifiers and ignores what a Shift key event does to its xkb state, so without this
|
||||
* every capital arrived lowercase. */
|
||||
static uint32_t g_mods_depressed, g_mods_locked;
|
||||
static uint32_t g_mod_keys_held; /* one bit per modifier key below, so a repeat press is harmless */
|
||||
static struct seat_touch g_touches[MAX_PTRS];
|
||||
static int g_ntouches;
|
||||
|
||||
@@ -716,6 +731,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
|
||||
wl_buffer_send_release(buffer);
|
||||
s->buf_w = w;
|
||||
s->buf_h = h;
|
||||
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
|
||||
s->has_content = s->shm_img != NULL;
|
||||
g_stat_shm++;
|
||||
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
|
||||
@@ -767,6 +783,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
}
|
||||
s->buf_w = b->width;
|
||||
s->buf_h = b->height;
|
||||
s->buf_alpha = (b->format & 0xff) == 'A';
|
||||
s->has_content = b->img != NULL;
|
||||
g_stat_dmabuf++;
|
||||
const int announced_now = !s->announced_vulkan;
|
||||
@@ -938,8 +955,27 @@ static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t c
|
||||
wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy);
|
||||
wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback));
|
||||
}
|
||||
/* The bounding box serves pointer confinement. The exact flag also tells opaque-region handling
|
||||
* whether the bounding box itself is the whole region. */
|
||||
struct region { int set, exact; int x, y, w, h; };
|
||||
|
||||
/* A wl_surface's opaque region is pending until its next commit. An exact region that covers
|
||||
* the surface lets the blit path keep handling an otherwise alpha-capable buffer. */
|
||||
static void surface_set_opaque(struct wl_client *c, struct wl_resource *r,
|
||||
struct wl_resource *region) {}
|
||||
struct wl_resource *region) {
|
||||
struct surface *s = wl_resource_get_user_data(r);
|
||||
struct region *rg = region ? wl_resource_get_user_data(region) : NULL;
|
||||
memset(s->pending_opaque, 0, sizeof(s->pending_opaque));
|
||||
s->pending_opaque_set = 1;
|
||||
if (rg && rg->set && rg->exact) {
|
||||
s->pending_opaque[0] = rg->x;
|
||||
s->pending_opaque[1] = rg->y;
|
||||
s->pending_opaque[2] = rg->w;
|
||||
s->pending_opaque[3] = rg->h;
|
||||
}
|
||||
struct client_info *ci = client_info_of(c);
|
||||
if (ci) ci->declares_opaque = 1;
|
||||
}
|
||||
static void surface_set_input(struct wl_client *c, struct wl_resource *r,
|
||||
struct wl_resource *region) {}
|
||||
|
||||
@@ -1030,6 +1066,10 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
memcpy(s->dst, s->pending_dst, sizeof(s->dst));
|
||||
s->pending_dst_set = 0;
|
||||
}
|
||||
if (s->pending_opaque_set) {
|
||||
memcpy(s->opaque, s->pending_opaque, sizeof(s->opaque));
|
||||
s->pending_opaque_set = 0;
|
||||
}
|
||||
|
||||
if (s->pending_attach) {
|
||||
struct wl_resource *buffer = s->pending_buffer;
|
||||
@@ -1168,24 +1208,29 @@ static void surface_resource_destroy(struct wl_resource *r) {
|
||||
|
||||
/* ------------------------------------------------------------------ wl_region */
|
||||
|
||||
/* A region is kept as the bounding box of its rectangles: enough for pointer confinement,
|
||||
* where winewayland sends one rectangle (the ClipCursor area). Subtractions are ignored. */
|
||||
struct region { int set; int x, y, w, h; };
|
||||
|
||||
static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
static void region_add(struct wl_client *c, struct wl_resource *r,
|
||||
int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
struct region *rg = wl_resource_get_user_data(r);
|
||||
if (!rg || w <= 0 || h <= 0) return;
|
||||
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = 1; return; }
|
||||
int x2 = rg->x + rg->w > x + w ? rg->x + rg->w : x + w;
|
||||
int y2 = rg->y + rg->h > y + h ? rg->y + rg->h : y + h;
|
||||
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = rg->exact = 1; return; }
|
||||
long long x2 = (long long)rg->x + rg->w, y2 = (long long)rg->y + rg->h;
|
||||
long long nx2 = (long long)x + w, ny2 = (long long)y + h;
|
||||
int inside = x >= rg->x && y >= rg->y && nx2 <= x2 && ny2 <= y2;
|
||||
int around = x <= rg->x && y <= rg->y && nx2 >= x2 && ny2 >= y2;
|
||||
if (!inside && !around) rg->exact = 0;
|
||||
if (nx2 > x2) x2 = nx2;
|
||||
if (ny2 > y2) y2 = ny2;
|
||||
if (x < rg->x) rg->x = x;
|
||||
if (y < rg->y) rg->y = y;
|
||||
rg->w = x2 - rg->x; rg->h = y2 - rg->y;
|
||||
rg->w = (int)(x2 - rg->x > INT32_MAX ? INT32_MAX : x2 - rg->x);
|
||||
rg->h = (int)(y2 - rg->y > INT32_MAX ? INT32_MAX : y2 - rg->y);
|
||||
}
|
||||
static void region_subtract(struct wl_client *c, struct wl_resource *r,
|
||||
int32_t x, int32_t y, int32_t w, int32_t h) {}
|
||||
int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
struct region *rg = wl_resource_get_user_data(r);
|
||||
if (rg && w > 0 && h > 0) rg->exact = 0;
|
||||
}
|
||||
static void region_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); }
|
||||
static const struct wl_region_interface region_impl = {
|
||||
.destroy = region_destroy,
|
||||
@@ -1659,6 +1704,17 @@ static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, i
|
||||
g_hdr_unimported_below = dl->n;
|
||||
}
|
||||
|
||||
/* A client that describes opaque regions can intentionally leave part of an alpha buffer see-through.
|
||||
* Gamescope uses this for its full-size Steam notification and overlay planes. */
|
||||
static int surface_translucent(const struct surface *s, int w, int h) {
|
||||
if (!s->buf_alpha) return 0;
|
||||
const struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
|
||||
if (!ci || !ci->declares_opaque) return 0;
|
||||
const int *o = s->opaque;
|
||||
return !(o[2] > 0 && o[0] <= 0 && o[1] <= 0 &&
|
||||
(long long)o[0] + o[2] >= w && (long long)o[1] + o[3] >= h);
|
||||
}
|
||||
|
||||
static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy) {
|
||||
struct vkp_image *img = surface_image(s);
|
||||
float sx = 0, sy = 0, sw = (float)s->buf_w, sh = (float)s->buf_h;
|
||||
@@ -1678,7 +1734,8 @@ static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy)
|
||||
dl->d = d;
|
||||
dl->cap = cap;
|
||||
}
|
||||
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh};
|
||||
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh,
|
||||
surface_translucent(s, dw, dh)};
|
||||
s->drawn = 1;
|
||||
}
|
||||
|
||||
@@ -1749,6 +1806,7 @@ static int on_frame_timer(void *data) {
|
||||
static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) {
|
||||
for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) {
|
||||
const struct vkp_draw *d = &dl->d[i];
|
||||
if (d->blend) continue; /* the translucent plane must not replace the opaque game layer */
|
||||
if (!vkp_image_is_dmabuf(d->img)) continue;
|
||||
if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue;
|
||||
if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) ||
|
||||
@@ -2087,7 +2145,7 @@ static void render_scene(void) {
|
||||
* its own layer, cropped and placed by the display. */
|
||||
int go[8], ov = 0;
|
||||
if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go))
|
||||
ov = sc_layer_present_overlay(dl.d[li + 1].img, go) == 0 ? 1 : -1;
|
||||
ov = sc_layer_present_overlay(dl.d[li + 1].img, go, dl.d[li + 1].blend) == 0 ? 1 : -1;
|
||||
if (ov <= 0) sc_layer_hide_overlay();
|
||||
if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */
|
||||
sc_layer_hide();
|
||||
@@ -2370,8 +2428,8 @@ static void keyboard_focus(struct wl_resource *target) {
|
||||
if (sk->focus == target) continue;
|
||||
sk->focus = target;
|
||||
wl_keyboard_send_enter(sk->kb, wl_display_next_serial(g_display), target, &keys);
|
||||
/* Baseline modifiers = none; Shift/Ctrl arrive as their own key events. */
|
||||
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display), 0, 0, 0, 0);
|
||||
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display),
|
||||
g_mods_depressed, 0, g_mods_locked, 0);
|
||||
}
|
||||
wl_array_release(&keys);
|
||||
banner_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */
|
||||
@@ -3068,7 +3126,34 @@ static void scroll_event(int steps) {
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
|
||||
/* Folds a key into the modifier state; returns whether the state changed. */
|
||||
static int update_modifiers(uint32_t evdev, int pressed) {
|
||||
static const struct { uint32_t evdev, mask; } mod_keys[] = {
|
||||
{ 42, 0x1 }, { 54, 0x1 }, /* Shift L/R */
|
||||
{ 29, 0x4 }, { 97, 0x4 }, /* Control L/R */
|
||||
{ 56, 0x8 }, { 100, 0x8 }, /* Alt L/R, both Mod1 in this keymap */
|
||||
{ 125, 0x40 }, { 126, 0x40 }, /* Super L/R */
|
||||
};
|
||||
uint32_t depressed = g_mods_depressed, locked = g_mods_locked;
|
||||
if (evdev == 58) { /* Caps Lock toggles on press */
|
||||
if (pressed) locked ^= 0x2;
|
||||
} else {
|
||||
int i, n = (int)(sizeof(mod_keys) / sizeof(mod_keys[0]));
|
||||
for (i = 0; i < n && mod_keys[i].evdev != evdev; i++) {}
|
||||
if (i == n) return 0;
|
||||
if (pressed) g_mod_keys_held |= 1u << i; else g_mod_keys_held &= ~(1u << i);
|
||||
depressed = 0;
|
||||
for (int j = 0; j < n; j++) if (g_mod_keys_held & (1u << j)) depressed |= mod_keys[j].mask;
|
||||
}
|
||||
if (depressed == g_mods_depressed && locked == g_mods_locked) return 0;
|
||||
g_mods_depressed = depressed;
|
||||
g_mods_locked = locked;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void key_event(uint32_t evdev, int pressed) {
|
||||
/* Tracked even with nothing to deliver to, so a Shift released unseen is not left held. */
|
||||
int mods_changed = update_modifiers(evdev, pressed);
|
||||
/* Keys go to the program window the user last clicked (else the topmost non-shell window),
|
||||
* never to Wine's desktop surface: winewayland hands each key to the hwnd of the surface that
|
||||
* holds keyboard focus, and a key handed to explorer's desktop hwnd is queued to explorer's
|
||||
@@ -3088,9 +3173,13 @@ static void key_event(uint32_t evdev, int pressed) {
|
||||
struct seat_keyboard *sk;
|
||||
if (!keyboard_for(client)) return;
|
||||
keyboard_focus(target->resource);
|
||||
for_each_keyboard_of(client, sk)
|
||||
for_each_keyboard_of(client, sk) {
|
||||
wl_keyboard_send_key(sk->kb, wl_display_next_serial(g_display), now_ms(), evdev,
|
||||
pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
|
||||
if (mods_changed)
|
||||
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display),
|
||||
g_mods_depressed, 0, g_mods_locked, 0);
|
||||
}
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ struct client_info {
|
||||
/* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's
|
||||
* Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */
|
||||
unsigned asked_feedback : 1, shm_gl_said : 1;
|
||||
/* Clients that describe opaque regions can use alpha outside those regions. Wine windows
|
||||
* never describe them, and their unused alpha channel must not make them translucent. */
|
||||
unsigned declares_opaque : 1;
|
||||
unsigned dmabuf_buffers, shm_frames;
|
||||
struct client_info *next;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "effects_chain.h"
|
||||
#include "framegen_bridge.h"
|
||||
#include "hdr_compose.h"
|
||||
#include "blend_pass.h"
|
||||
#include "banner_color.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -32,6 +33,8 @@ struct vkp_image {
|
||||
void *map; /* shm images: persistently mapped linear memory */
|
||||
VkDeviceSize offset, row_pitch;
|
||||
int in_general; /* shm images: moved from PREINITIALIZED to GENERAL */
|
||||
VkFormat fmt;
|
||||
int sampled; /* created with SAMPLED usage: the alpha pass can read it */
|
||||
};
|
||||
|
||||
static ANativeWindow *g_window; /* the window frames go to; compositor thread only */
|
||||
@@ -230,6 +233,7 @@ static void destroy_swapchain(void) {
|
||||
if (g_dev_state != 1) return;
|
||||
g_vk.DeviceWaitIdle(g_dev);
|
||||
reset_sync();
|
||||
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
|
||||
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
|
||||
g_swapchain = VK_NULL_HANDLE;
|
||||
g_swap_md_identity = 0; /* a new swapchain gets the metadata again */
|
||||
@@ -525,6 +529,7 @@ static int dev_init(void) {
|
||||
|
||||
vkp_effects_bind_device(g_dev, g_pd, &g_memprops);
|
||||
hdrc_bind_device(g_dev, &g_memprops);
|
||||
blendp_bind_device(g_dev);
|
||||
g_dev_state = 1;
|
||||
reset_sync();
|
||||
init_black_image();
|
||||
@@ -873,9 +878,8 @@ const char *vkp_modifier_name(uint64_t modifier) {
|
||||
return other;
|
||||
}
|
||||
|
||||
/* Can the driver create the image vkp_image_import_dmabuf() creates for this format+modifier
|
||||
* (2D, DRM_FORMAT_MODIFIER tiling, TRANSFER_SRC, dma-buf memory) - and import it? */
|
||||
static int modifier_importable(VkFormat fmt, uint64_t modifier) {
|
||||
/* Can the driver create and import this format/modifier with the requested image usage? */
|
||||
static int modifier_importable(VkFormat fmt, uint64_t modifier, VkImageUsageFlags usage) {
|
||||
if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; /* can't ask; the create will tell */
|
||||
VkPhysicalDeviceExternalImageFormatInfo ext = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO,
|
||||
@@ -886,7 +890,7 @@ static int modifier_importable(VkFormat fmt, uint64_t modifier) {
|
||||
VkPhysicalDeviceImageFormatInfo2 info = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .pNext = &mod,
|
||||
.format = fmt, .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT};
|
||||
.usage = usage};
|
||||
VkExternalImageFormatProperties extp = {.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES};
|
||||
VkImageFormatProperties2 props = {.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, .pNext = &extp};
|
||||
if (g_vk.GetPhysicalDeviceImageFormatProperties2(g_pd, &info, &props) != VK_SUCCESS) return 0;
|
||||
@@ -914,7 +918,7 @@ int vkp_dmabuf_modifiers(uint32_t drm_format, uint64_t *out, int max) {
|
||||
if (m != VKP_MOD_LINEAR && m != VKP_MOD_QCOM_COMPRESSED) why = "unknown layout";
|
||||
else if (props[i].drmFormatModifierPlaneCount != 1) why = "not single-plane";
|
||||
else if (!(props[i].drmFormatModifierTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT)) why = "no blit source";
|
||||
else if (!modifier_importable(fmt, m)) why = "not importable as a dma-buf";
|
||||
else if (!modifier_importable(fmt, m, VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) why = "not importable as a dma-buf";
|
||||
if (why) {
|
||||
LOGI("dmabuf: %c%c%c%c %s reported by the driver but not advertised: %s",
|
||||
drm_format & 0xff, (drm_format >> 8) & 0xff, (drm_format >> 16) & 0xff,
|
||||
@@ -942,6 +946,10 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
struct vkp_image *img = calloc(1, sizeof(*img));
|
||||
if (!img) return NULL;
|
||||
img->w = w; img->h = h; img->dmabuf = 1; img->blit_dst = as_blit_dst ? 1 : 0;
|
||||
img->fmt = drm_to_vk(drm_format);
|
||||
const VkImageUsageFlags sampled_usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
img->sampled = !as_blit_dst && g_vk.GetPhysicalDeviceImageFormatProperties2 &&
|
||||
modifier_importable(img->fmt, modifier, sampled_usage);
|
||||
|
||||
VkSubresourceLayout plane = {.offset = offset, .rowPitch = stride};
|
||||
VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = {
|
||||
@@ -952,10 +960,11 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT};
|
||||
VkImageCreateInfo ici = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = &extImg,
|
||||
.imageType = VK_IMAGE_TYPE_2D, .format = drm_to_vk(drm_format), .extent = {w, h, 1},
|
||||
.imageType = VK_IMAGE_TYPE_2D, .format = img->fmt, .extent = {w, h, 1},
|
||||
.mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
|
||||
.usage = as_blit_dst ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.usage = as_blit_dst ? VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
: img->sampled ? sampled_usage : VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
VkResult cr = g_vk.CreateImage(g_dev, &ici, NULL, &img->image);
|
||||
if (cr != VK_SUCCESS) {
|
||||
@@ -1008,18 +1017,35 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
return img;
|
||||
}
|
||||
|
||||
/* Linear wl_shm images can use the blend pass only if the driver samples them with a linear filter. */
|
||||
static int shm_sampled(void) {
|
||||
static int known;
|
||||
if (!known) {
|
||||
const VkFormatFeatureFlags need = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
|
||||
VkFormatProperties2 fp = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2};
|
||||
if (g_vk.GetPhysicalDeviceFormatProperties2)
|
||||
g_vk.GetPhysicalDeviceFormatProperties2(g_pd, VK_FORMAT_B8G8R8A8_UNORM, &fp);
|
||||
known = (fp.formatProperties.linearTilingFeatures & need) == need ? 1 : -1;
|
||||
}
|
||||
return known == 1;
|
||||
}
|
||||
|
||||
struct vkp_image *vkp_image_create_shm(int w, int h) {
|
||||
if (w <= 0 || h <= 0 || dev_init() != 0) return NULL;
|
||||
|
||||
struct vkp_image *img = calloc(1, sizeof(*img));
|
||||
if (!img) return NULL;
|
||||
img->w = w; img->h = h;
|
||||
img->fmt = VK_FORMAT_B8G8R8A8_UNORM;
|
||||
img->sampled = shm_sampled();
|
||||
|
||||
VkImageCreateInfo ici = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = VK_FORMAT_B8G8R8A8_UNORM, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
|
||||
.format = img->fmt, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | (img->sampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0),
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED};
|
||||
if (g_vk.CreateImage(g_dev, &ici, NULL, &img->image) != VK_SUCCESS) { free(img); return NULL; }
|
||||
|
||||
@@ -1062,6 +1088,7 @@ int vkp_image_height(const struct vkp_image *img) { return img ? img->h : 0; }
|
||||
void vkp_image_destroy(struct vkp_image *img) {
|
||||
if (!img) return;
|
||||
if (g_dev) {
|
||||
blendp_forget_image(img->image);
|
||||
if (img->map) g_vk.UnmapMemory(g_dev, img->mem);
|
||||
if (img->image) g_vk.DestroyImage(g_dev, img->image, NULL);
|
||||
if (img->mem) g_vk.FreeMemory(g_dev, img->mem, NULL);
|
||||
@@ -1111,8 +1138,21 @@ static int draw_to_scene_blit(const struct vkp_draw *d, int scene_w, int scene_h
|
||||
return map_draw(d, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, (float)scene_w, (float)scene_h, blit);
|
||||
}
|
||||
|
||||
/* A translucent draw needs source-over blending; every other draw keeps the existing blit path. */
|
||||
static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkImageBlit *blit, VkImage target,
|
||||
VkFormat target_fmt, int target_w, int target_h, VkFilter filter) {
|
||||
const struct vkp_image *im = d->img;
|
||||
if (d->blend && im->sampled &&
|
||||
blendp_draw(cmd, im->image, im->fmt, im->w, im->h, !im->dmabuf, target, target_fmt, target_w, target_h,
|
||||
blit) == 0)
|
||||
return;
|
||||
g_vk.CmdBlitImage(cmd, im->image, im->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, filter);
|
||||
}
|
||||
|
||||
static void destroy_scene_image(void) {
|
||||
if (g_scene.img) hdrc_forget_image(g_scene.img); /* the HDR pass may have drawn into it */
|
||||
if (g_scene.img) blendp_forget_image(g_scene.img);
|
||||
if (g_scene.img) g_vk.DestroyImage(g_dev, g_scene.img, NULL);
|
||||
if (g_scene.mem) g_vk.FreeMemory(g_dev, g_scene.mem, NULL);
|
||||
memset(&g_scene, 0, sizeof(g_scene));
|
||||
@@ -1150,6 +1190,7 @@ static int ensure_scene_image(int w, int h) {
|
||||
static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImageUsageFlags usage, const char *what) {
|
||||
if (s->img && s->w == w && s->h == h && s->fmt == fmt) return 0;
|
||||
if (s->img) hdrc_forget_image(s->img);
|
||||
if (s->img) blendp_forget_image(s->img);
|
||||
if (s->img) g_vk.DestroyImage(g_dev, s->img, NULL);
|
||||
if (s->mem) g_vk.FreeMemory(g_dev, s->mem, NULL);
|
||||
memset(s, 0, sizeof(*s));
|
||||
@@ -1190,7 +1231,8 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
|
||||
int scene_w, int scene_h, VkImage out, VkFormat out_fmt, enum hdrc_mode mode) {
|
||||
if (!hf || !hf->is_hdr || n <= 0) return -1;
|
||||
if (ensure_img(&g_mixed, scene_w, scene_h, HDR_FMT,
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
|
||||
"HDR composition (mixed)") != 0)
|
||||
return -1;
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
@@ -1214,9 +1256,7 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
|
||||
VkImageBlit blit;
|
||||
if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue;
|
||||
/* A blit converts UNORM to UNORM by value: 8-bit sRGB and 10-bit PQ both land unchanged. */
|
||||
g_vk.CmdBlitImage(cmd, draws[i].img->image,
|
||||
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
g_mixed.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
|
||||
record_draw(cmd, &draws[i], &blit, g_mixed.img, HDR_FMT, scene_w, scene_h, VK_FILTER_LINEAR);
|
||||
if (hf->is_hdr[i] && lowest_hdr < 0) lowest_hdr = i;
|
||||
}
|
||||
static int truncated_said;
|
||||
@@ -1249,6 +1289,7 @@ static void device_lost(const char *where) {
|
||||
vkp_framegen_device_lost();
|
||||
banner_log("error", "GPU device lost (VK_ERROR_DEVICE_LOST in %s): the compositor has stopped presenting; "
|
||||
"restart the session", where);
|
||||
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
|
||||
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
|
||||
g_swapchain = VK_NULL_HANDLE;
|
||||
if (g_surface) g_vk.DestroySurfaceKHR(g_inst, g_surface, NULL);
|
||||
@@ -1547,6 +1588,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(cmd, &bi);
|
||||
blendp_begin_frame();
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
|
||||
/* Source images: take dmabufs from the client's queue family, move shm images to
|
||||
@@ -1589,8 +1631,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
free(bars);
|
||||
|
||||
/* Clear the composition target to black (the letterbox on the direct path, the desktop's
|
||||
* background on the pass) before the draws land on it. A draw that covers the whole scene
|
||||
* overwrites every pixel of its rectangle (blits never blend), so then only what lies outside it
|
||||
* background on the pass) before the draws land on it. An opaque draw that covers the whole scene
|
||||
* overwrites every pixel of its rectangle, so then only what lies outside it
|
||||
* needs black: the letterbox bars on the direct path, nothing at all on the pass (the scene image
|
||||
* is exactly the scene). Anything else - no such draw, the HDR composition - clears the whole
|
||||
* target as before. */
|
||||
@@ -1600,7 +1642,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
if (!(hf && pass)) {
|
||||
for (int i = n - 1; i >= 0 && !covered; i--) {
|
||||
const struct vkp_draw *d = &draws[i];
|
||||
if (!d->img || d->dx > 0 || d->dy > 0 || d->dx + d->dw < scene_w || d->dy + d->dh < scene_h) continue;
|
||||
if (!d->img || d->blend || d->dx > 0 || d->dy > 0 || d->dx + d->dw < scene_w ||
|
||||
d->dy + d->dh < scene_h) continue;
|
||||
covered = pass ? draw_to_scene_blit(d, scene_w, scene_h, &cover_blit) : draw_to_blit(d, &cover_blit);
|
||||
}
|
||||
}
|
||||
@@ -1633,10 +1676,9 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
VkImageBlit blit;
|
||||
if (!draws[i].img) continue;
|
||||
if (pass ? !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit) : !draw_to_blit(&draws[i], &blit)) continue;
|
||||
g_vk.CmdBlitImage(cmd, draws[i].img->image,
|
||||
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit,
|
||||
pass ? VK_FILTER_LINEAR : blit_filter);
|
||||
record_draw(cmd, &draws[i], &blit, target, pass ? scene_fmt : g_swap_fmt,
|
||||
pass ? scene_w : (int)g_extent.width, pass ? scene_h : (int)g_extent.height,
|
||||
pass ? VK_FILTER_LINEAR : blit_filter);
|
||||
drawn++;
|
||||
}
|
||||
|
||||
@@ -2073,6 +2115,7 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(cmd, &bi);
|
||||
blendp_begin_frame();
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
|
||||
/* Same acquisition as vkp_render's compositor pass: the sources come from the client's queue
|
||||
@@ -2132,9 +2175,8 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
for (int i = 0; i < n; i++) {
|
||||
VkImageBlit blit;
|
||||
if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue;
|
||||
g_vk.CmdBlitImage(cmd, draws[i].img->image,
|
||||
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
g_scene.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
|
||||
record_draw(cmd, &draws[i], &blit, g_scene.img, SCENE_FMT, scene_w, scene_h,
|
||||
VK_FILTER_LINEAR);
|
||||
}
|
||||
if (vkp_effects_active()) vkp_effects_set_formats(VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ struct vkp_draw {
|
||||
struct vkp_image *img;
|
||||
float sx, sy, sw, sh;
|
||||
int dx, dy, dw, dh;
|
||||
int blend; /* translucent: composed over what is under it (blend_pass.h) */
|
||||
};
|
||||
|
||||
// The GPU the renderer runs on ("Adreno (TM) 750"), empty before the device is up.
|
||||
|
||||
@@ -126,7 +126,8 @@ class MainActivity : ComponentActivity() {
|
||||
private var gameCores by mutableStateOf<Set<Int>>(emptySet())
|
||||
private var tuSysmem by mutableStateOf(false)
|
||||
private var zinkLazy by mutableStateOf(false)
|
||||
private var noXalia by mutableStateOf(false)
|
||||
private var noXalia by mutableStateOf(true)
|
||||
private var gamescopeRealtime by mutableStateOf(false)
|
||||
private var prootNoSeccomp by mutableStateOf(false)
|
||||
private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME)
|
||||
private var phantomWarning by mutableStateOf<String?>(null)
|
||||
@@ -503,8 +504,9 @@ class MainActivity : ComponentActivity() {
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
// The microphone is on by default; ask once, with the storage prompt, so voice chat works
|
||||
// without a trip to the settings. A refusal is not asked again - the toggle asks when used.
|
||||
// The microphone is off by default. Someone who turned it on before the permission was
|
||||
// granted is asked once here, with the storage prompt; a refusal is not asked again - the
|
||||
// toggle asks when used.
|
||||
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
|
||||
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.RECORD_AUDIO)
|
||||
@@ -805,6 +807,7 @@ class MainActivity : ComponentActivity() {
|
||||
storageOptions = storageOptions,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
|
||||
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
|
||||
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
|
||||
addedGamesDirs = if (mode == SessionService.MODE_STEAM) addedGamesDirs else null,
|
||||
addedGames = if (mode == SessionService.MODE_STEAM) addedGames else emptyList(),
|
||||
@@ -856,6 +859,11 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
onSteamChannel = { id -> SessionPrefs.setSteamChannel(this, id); steamChannel = id },
|
||||
onSteamDeckMode = { on ->
|
||||
SessionPrefs.setSteamDeckMode(this, on)
|
||||
steamDeckMode = on
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
},
|
||||
onRunSteamAtStartup = { on ->
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
@@ -886,15 +894,16 @@ class MainActivity : ComponentActivity() {
|
||||
PerformancePage(
|
||||
cores = CpuCores.all.map { c -> CoreRow(c, "cpu$c" + (CpuCores.maxGhz(c)?.let { String.format(java.util.Locale.US, " · %.1f GHz", it) } ?: "")) },
|
||||
clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores,
|
||||
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, steamDeckMode = steamDeckMode, noXalia = noXalia,
|
||||
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia,
|
||||
gamescopeRealtime = gamescopeRealtime,
|
||||
prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning,
|
||||
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
|
||||
onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on },
|
||||
onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on },
|
||||
onGlThread = { on -> SessionPrefs.setGlThread(this, on); glThread = on },
|
||||
onNoGlError = { on -> SessionPrefs.setNoGlError(this, on); noGlError = on },
|
||||
onSteamDeckMode = { on -> SessionPrefs.setSteamDeckMode(this, on); steamDeckMode = on },
|
||||
onNoXalia = { on -> SessionPrefs.setNoXalia(this, on); noXalia = on },
|
||||
onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on },
|
||||
onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on },
|
||||
onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) },
|
||||
onClientCore = { core, on ->
|
||||
@@ -939,6 +948,7 @@ class MainActivity : ComponentActivity() {
|
||||
customResolution = SessionPrefs.customResolution(this, mode)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
steamDeckMode = SessionPrefs.steamDeckMode(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
|
||||
refreshAddedGames()
|
||||
@@ -988,6 +998,7 @@ class MainActivity : ComponentActivity() {
|
||||
tuSysmem = SessionPrefs.tuSysmem(this)
|
||||
zinkLazy = SessionPrefs.zinkLazy(this)
|
||||
noXalia = SessionPrefs.noXalia(this)
|
||||
gamescopeRealtime = SessionPrefs.gamescopeRealtime(this)
|
||||
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
|
||||
guestHostname = SessionPrefs.guestHostname(this)
|
||||
refreshPhantomStatus()
|
||||
|
||||
@@ -47,7 +47,6 @@ import com.droiddeck.launcher.input.SecondScreenMode
|
||||
import com.droiddeck.launcher.input.TouchpadGestures
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.LoadingState
|
||||
import com.droiddeck.launcher.session.PerfHints
|
||||
import com.droiddeck.launcher.session.PerfHud
|
||||
import com.droiddeck.launcher.session.PerfMode
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
@@ -203,8 +202,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionEvents.record("agent.start_requested", mapOf("mode" to SessionState.mode))
|
||||
}
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
// Game-tier power policy for the whole session: a sustained clock floor, the panel's
|
||||
// fastest mode, and the OS told it is in gameplay. Logged so a slow device says why.
|
||||
// Game-tier power policy for the whole session: the panel's fastest mode, and the OS told
|
||||
// it is in gameplay. Logged so a slow device says why.
|
||||
Log.i(TAG, "perf: " + PerfMode.apply(this))
|
||||
goFullscreen()
|
||||
// The device's volume keys change the stream the session plays on (the relay and
|
||||
@@ -460,7 +459,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
performBackAction(double = false)
|
||||
}
|
||||
pendingBackAction = pending
|
||||
uiHandler.postDelayed(pending, BACK_DOUBLE_PRESS_TIMEOUT_MS)
|
||||
// Android's own double-tap window (300 ms): the single action waits it out, so it is felt.
|
||||
uiHandler.postDelayed(pending, android.view.ViewConfiguration.getDoubleTapTimeout().toLong())
|
||||
}
|
||||
|
||||
private fun performBackAction(double: Boolean) {
|
||||
@@ -760,9 +760,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
applicationInfo.nativeLibraryDir, size.first, size.second, refreshHz(),
|
||||
)
|
||||
if (!SessionState.running) SessionEvents.record("compositor.started")
|
||||
// ADPF: the compositor thread's frame intervals go to the power HAL against the panel's
|
||||
// period, so a long frame raises CPU clocks now rather than after the load averages up.
|
||||
PerfHints.arm(this, refreshHz())
|
||||
// The service owns everything below the compositor. It is started whenever no session is
|
||||
// running - NOT only when the compositor was just started: the compositor lives for the
|
||||
// whole process, so the second Play after a session ended used to re-attach the Surface,
|
||||
@@ -848,9 +845,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return Pair(width and 1.inv(), height and 1.inv())
|
||||
}
|
||||
|
||||
/**
|
||||
* The rate of the mode PerfMode asked for, not the one the panel is still in: the switch lands
|
||||
* some frames after onCreate, and read too early it gave gamescope `-r 60` on a 120 Hz panel -
|
||||
* the client and every game then paced themselves to 60. (WinNative's requestedPanelRefreshRate.)
|
||||
*/
|
||||
private fun refreshHz(): Float {
|
||||
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
|
||||
val hz = display?.refreshRate ?: 60f
|
||||
val wanted = window.attributes.preferredDisplayModeId
|
||||
val hz = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
|
||||
?: display?.refreshRate ?: 60f
|
||||
return if (hz > 1f) hz else 60f
|
||||
}
|
||||
|
||||
@@ -1598,7 +1602,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
companion object {
|
||||
private const val TAG = "SessionActivity"
|
||||
private const val UNBUFFERED_SOURCES = InputDevice.SOURCE_CLASS_JOYSTICK or InputDevice.SOURCE_CLASS_TRACKBALL or InputDevice.SOURCE_CLASS_POSITION
|
||||
private const val BACK_DOUBLE_PRESS_TIMEOUT_MS = 500L
|
||||
private const val CURSOR_PAD_HOLD_MS = 1200L
|
||||
private const val DRAWER_HAT_THRESHOLD = 0.5f
|
||||
private const val DRAWER_STICK_ENTER_THRESHOLD = 0.55f
|
||||
|
||||
@@ -146,6 +146,7 @@ object DeviceReport {
|
||||
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
|
||||
k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" })
|
||||
k("Skip xalia", SessionPrefs.noXalia(context))
|
||||
k("gamescope realtime", SessionPrefs.gamescopeRealtime(context))
|
||||
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
|
||||
k("Guest host name", SessionPrefs.guestHostname(context))
|
||||
k("DirectAudio for games", SessionPrefs.directAudio(context))
|
||||
|
||||
@@ -193,6 +193,8 @@ public final class LinuxRuntime {
|
||||
}
|
||||
}
|
||||
bindGpuNode(context, cmd);
|
||||
bindAdrenoStats(cmd);
|
||||
bindCpuTemps(cmd, root);
|
||||
if (extraBinds != null) {
|
||||
for (String spec : extraBinds) bind(cmd, spec);
|
||||
}
|
||||
@@ -244,6 +246,82 @@ public final class LinuxRuntime {
|
||||
bind(cmd, KGSL_DEVICE + ":/dev/dri/" + node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valve's mangoapp (Deck mode's performance overlay) reads an Adreno GPU's load, clock and
|
||||
* temperatures from where they are on Valve's own hardware; every Adreno under Android keeps
|
||||
* them in KGSL's sysfs, readable by the app, though which files a kernel has differs between
|
||||
* Snapdragon generations - so each value takes the first source that exists, the order other
|
||||
* Android PC emulators (GameNative, Winlator forks) read them in. A value found nowhere is left
|
||||
* alone.
|
||||
*/
|
||||
private static void bindAdrenoStats(List<String> cmd) {
|
||||
String kgsl = "/sys/class/kgsl/kgsl-3d0/";
|
||||
String gpuTemp = firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu"));
|
||||
String[][] stats = {
|
||||
{firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"), "/sys/kernel/debug/dri/0/perf_now"},
|
||||
{firstReadable(kgsl + "devfreq/cur_freq", kgsl + "gpuclk"),
|
||||
"/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"},
|
||||
{gpuTemp, "/sys/class/thermal/thermal_zone28/temp"},
|
||||
{gpuTemp, "/sys/class/thermal/thermal_zone26/temp"},
|
||||
{thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"},
|
||||
};
|
||||
for (String[] stat : stats) {
|
||||
if (stat[0] != null) bind(cmd, stat[0] + ":" + stat[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mangoapp's CPU temperature is the mean of the thermal zones named cpuN-thermal or
|
||||
* cpuN-top-thermal, as mainline kernels name them; Android kernels name the same sensors
|
||||
* cpuss-0, cpu-1-0, cpu0-silver-usr, apc1-cpu0-usr and the like, so those zones are shown
|
||||
* under the mainline name.
|
||||
*/
|
||||
private static void bindCpuTemps(List<String> cmd, File root) {
|
||||
File name = new File(root, "etc/bannerlator/cpu-thermal-type");
|
||||
try {
|
||||
if (!name.isFile()) Files.write(name.toPath(), "cpu0-thermal\n".getBytes(StandardCharsets.US_ASCII));
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
for (java.util.Map.Entry<String, File> zone : thermalZones().entrySet()) {
|
||||
String type = zone.getKey();
|
||||
if (type.contains("cpu") && !type.contains("gpu") && !type.matches("cpu\\d-(top-)?thermal")) {
|
||||
bind(cmd, name.getPath() + ":" + new File(zone.getValue(), "type").getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstReadable(String... paths) {
|
||||
for (String path : paths) {
|
||||
if (path != null && new File(path).canRead()) return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The temp file of the thermal zone for a sensor: the one named so, else the first whose name has it. */
|
||||
private static String thermalZone(String sensor) {
|
||||
java.util.Map<String, File> zones = thermalZones();
|
||||
File zone = zones.get(sensor);
|
||||
for (java.util.Map.Entry<String, File> e : new java.util.TreeMap<>(zones).entrySet()) {
|
||||
if (zone == null && e.getKey().contains(sensor)) zone = e.getValue();
|
||||
}
|
||||
return zone != null ? new File(zone, "temp").getPath() : null;
|
||||
}
|
||||
|
||||
/** The device's thermal zones by their sensor's name, in lower case. */
|
||||
private static java.util.Map<String, File> thermalZones() {
|
||||
java.util.Map<String, File> byType = new java.util.HashMap<>();
|
||||
File[] zones = new File("/sys/class/thermal").listFiles((dir, n) -> n.startsWith("thermal_zone"));
|
||||
if (zones == null) return byType;
|
||||
for (File zone : zones) {
|
||||
try (java.io.BufferedReader r = new java.io.BufferedReader(new java.io.FileReader(new File(zone, "type")))) {
|
||||
byType.putIfAbsent(String.valueOf(r.readLine()).trim().toLowerCase(java.util.Locale.ROOT), zone);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
return byType;
|
||||
}
|
||||
|
||||
private static void bind(List<String> cmd, String spec) {
|
||||
cmd.add("-b");
|
||||
cmd.add(spec);
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.PerformanceHintManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* ADPF (Android 12+): a PerformanceHintManager session over the compositor thread, told the
|
||||
* panel's frame period as its target and each presented frame's interval as the actual. When a
|
||||
* frame runs long the power HAL raises CPU clocks for the next one at once, instead of the
|
||||
* governor waiting for the load average to climb - the difference between a menu that stutters
|
||||
* on the first scroll and one that does not.
|
||||
*
|
||||
* The session may only carry this process's threads, so the guest (gamescope, the Steam client
|
||||
* under FEX) cannot join it; they are covered by the app-wide game category and sustained mode.
|
||||
* Below API 31 nothing here runs; on a power HAL without hint sessions createHintSession returns
|
||||
* null and the log says so.
|
||||
*/
|
||||
object PerfHints {
|
||||
private const val TAG = "PerfHints"
|
||||
|
||||
@Volatile private var session: PerformanceHintManager.Session? = null
|
||||
@Volatile private var target = 0L
|
||||
/** Compositor thread only. */
|
||||
private var last = 0L
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
/** Reports leave the compositor thread: each one is a binder call to the power HAL. */
|
||||
private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "adpf-report").apply { isDaemon = true } }
|
||||
|
||||
/** Opens the session once the compositor thread exists (it is started just before this). */
|
||||
fun arm(context: Context, refreshHz: Float) {
|
||||
if (Build.VERSION.SDK_INT < 31) { Log.i(TAG, "adpf: needs Android 12; this is API ${Build.VERSION.SDK_INT}"); return }
|
||||
val wanted = (1_000_000_000.0 / refreshHz.coerceAtLeast(30f)).toLong()
|
||||
val s = session
|
||||
if (s != null) {
|
||||
if (wanted != target) { target = wanted; try { s.updateTargetWorkDuration(wanted) } catch (t: Throwable) { Log.w(TAG, "adpf: retarget", t) } }
|
||||
return
|
||||
}
|
||||
target = wanted
|
||||
val app = context.applicationContext
|
||||
var tries = 0
|
||||
main.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (session != null) return
|
||||
val tid = try { WaylandCompositor.nativeCompositorTid() } catch (t: Throwable) { 0 }
|
||||
if (tid == 0) {
|
||||
if (++tries < 30) main.postDelayed(this, 100) else Log.w(TAG, "adpf: the compositor thread never reported its tid")
|
||||
return
|
||||
}
|
||||
open(app, tid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private fun open(app: Context, tid: Int) {
|
||||
val pm = app.getSystemService(PerformanceHintManager::class.java)
|
||||
if (pm == null) { Log.i(TAG, "adpf: no PerformanceHintManager on this device"); return }
|
||||
val s = try { pm.createHintSession(intArrayOf(tid), target) } catch (t: Throwable) { Log.w(TAG, "adpf: createHintSession", t); null }
|
||||
if (s == null) { Log.i(TAG, "adpf: unsupported here (the power HAL has no hint sessions)"); return }
|
||||
last = 0L
|
||||
session = s
|
||||
Log.i(TAG, "adpf: session on compositor thread $tid · target ${target / 1000} µs · HAL wants updates every ${pm.preferredUpdateRateNanos / 1000} µs")
|
||||
}
|
||||
|
||||
/** Compositor thread, once per presented game frame; the interval since the last is the actual. */
|
||||
@JvmStatic
|
||||
fun onFrame() {
|
||||
val s = session ?: return
|
||||
val now = System.nanoTime()
|
||||
val prev = last
|
||||
last = now
|
||||
if (prev == 0L) return
|
||||
val dt = now - prev
|
||||
// Under half a millisecond is a double present; over half a second is a pause, not a frame.
|
||||
if (dt < 500_000L || dt > 500_000_000L) return
|
||||
worker.execute {
|
||||
try { s.reportActualWorkDuration(dt) } catch (t: Throwable) { Log.w(TAG, "adpf: report failed, session dropped", t); session = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.app.Activity
|
||||
import android.app.GameManager
|
||||
import android.app.GameState
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
@@ -13,8 +12,10 @@ import android.util.Log
|
||||
* The manifest already makes the app a game (appCategory + game_mode_config); this is the
|
||||
* per-window half, applied when the session activity is created:
|
||||
*
|
||||
* - sustained performance mode (Android 7+): a clock floor that does not throttle away after
|
||||
* two minutes, which suits an hour in Big Picture better than a boost that fades;
|
||||
* - NOT sustained performance mode: on Pixel and Qualcomm power HALs it caps the CPU and GPU at
|
||||
* a level the device can hold indefinitely rather than holding a floor, which cost games and
|
||||
* Big Picture their peak clocks. WinNative never asks for it; the thermal budget is left to
|
||||
* the game;
|
||||
* - the panel's fastest mode at its current size (Android 6+): a 120 Hz phone is switched to
|
||||
* 120 Hz for the menu, not only when a game votes for it;
|
||||
* - GameManager's game state (Android 13+): "in gameplay", so an OEM framework that releases
|
||||
@@ -27,15 +28,7 @@ object PerfMode {
|
||||
private const val TAG = "PerfMode"
|
||||
|
||||
fun apply(a: Activity): String {
|
||||
val parts = ArrayList<String>(3)
|
||||
|
||||
// Sustained performance mode.
|
||||
parts += if (Build.VERSION.SDK_INT >= 24) {
|
||||
val pm = a.getSystemService(PowerManager::class.java)
|
||||
if (pm?.isSustainedPerformanceModeSupported == true) {
|
||||
try { a.window.setSustainedPerformanceMode(true); "sustained mode on" } catch (t: Throwable) { Log.w(TAG, "sustained mode", t); "sustained mode refused" }
|
||||
} else "sustained mode unsupported"
|
||||
} else "sustained mode needs Android 7"
|
||||
val parts = ArrayList<String>(2)
|
||||
|
||||
// The fastest display mode at the panel's current size.
|
||||
parts += try {
|
||||
|
||||
@@ -143,8 +143,8 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("directAudio", on).apply()
|
||||
}
|
||||
|
||||
/** The microphone for voice chat, on unless turned off; used only once RECORD_AUDIO is granted. */
|
||||
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", true)
|
||||
/** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */
|
||||
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false)
|
||||
|
||||
/** Whether the app has already asked for the microphone once at start-up. */
|
||||
fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false)
|
||||
@@ -188,14 +188,28 @@ object SessionPrefs {
|
||||
* xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation.
|
||||
* Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls
|
||||
* reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured -
|
||||
* it storms, and the session dies seconds after Big Picture appears.
|
||||
* it storms, and the session dies seconds after Big Picture appears. On by default: where it
|
||||
* does run, it sits beside every game under FEX for nothing a controller-first session needs
|
||||
* (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2).
|
||||
*/
|
||||
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", false)
|
||||
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true)
|
||||
|
||||
fun setNoXalia(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("noXalia", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1,
|
||||
* which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in
|
||||
* Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device
|
||||
* whose GPU is waiting on the CPU.
|
||||
*/
|
||||
fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false)
|
||||
|
||||
fun setGamescopeRealtime(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("gamescopeRealtime", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1).
|
||||
*
|
||||
@@ -227,8 +241,13 @@ object SessionPrefs {
|
||||
return trimmed.takeIf { Regex("[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?").matches(it) }
|
||||
}
|
||||
|
||||
/** Turnip's sysmem rendering (TU_DEBUG=sysmem) for the runtime's driver: bypasses GMEM tiling. */
|
||||
fun tuSysmem(context: Context): Boolean = prefs(context).getBoolean("tuSysmem", false)
|
||||
/**
|
||||
* Turnip's sysmem rendering (TU_DEBUG=sysmem) for the runtime's driver: bypasses GMEM tiling.
|
||||
* On by default, as WinNative runs every Linux session: Chromium -> ANGLE -> Zink draws the
|
||||
* client's interface as many small render passes, each paying GMEM's load/store and binning,
|
||||
* and WinNative's A/B on an Adreno 840 put a game ahead with it too (Palworld 42.8 against 41.3).
|
||||
*/
|
||||
fun tuSysmem(context: Context): Boolean = prefs(context).getBoolean("tuSysmem", true)
|
||||
|
||||
fun setTuSysmem(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("tuSysmem", on).apply()
|
||||
@@ -248,11 +267,14 @@ object SessionPrefs {
|
||||
fun noGlError(context: Context): Boolean = prefs(context).getBoolean("noGlError", true)
|
||||
fun setNoGlError(context: Context, on: Boolean) { prefs(context).edit().putBoolean("noGlError", on).apply() }
|
||||
|
||||
/** `steam -steamdeck -steamos3`: the client as SteamOS runs it. Expects Deck hardware; untested. */
|
||||
/** Runs the SteamOS gamepad client with its Quick Access performance controls. */
|
||||
fun steamDeckMode(context: Context): Boolean = prefs(context).getBoolean("steamDeckMode", false)
|
||||
fun setSteamDeckMode(context: Context, on: Boolean) { prefs(context).edit().putBoolean("steamDeckMode", on).apply() }
|
||||
|
||||
/** Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) for the client's GL-on-Vulkan UI. On by default. */
|
||||
/**
|
||||
* Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) with its compact set layout
|
||||
* (ZINK_DEBUG=compact) for the client's GL-on-Vulkan UI, as WinNative runs it. On by default.
|
||||
*/
|
||||
fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true)
|
||||
|
||||
fun setZinkLazy(context: Context, on: Boolean) {
|
||||
@@ -309,8 +331,15 @@ object SessionPrefs {
|
||||
fun resolutionChosen(context: Context, mode: String): Boolean =
|
||||
prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null
|
||||
|
||||
/** The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults. */
|
||||
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "") ?: ""
|
||||
/**
|
||||
* The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults.
|
||||
* Performance + TSO unless chosen, WinNative's default: FEX's own defaults keep half-barrier TSO
|
||||
* and full-precision x87, which cost every x86 game time; a game that needs them can still be
|
||||
* given another preset.
|
||||
*/
|
||||
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", DEFAULT_FEX_PRESET) ?: DEFAULT_FEX_PRESET
|
||||
|
||||
private const val DEFAULT_FEX_PRESET = "PERFORMANCE_TSO"
|
||||
|
||||
fun setFexPreset(context: Context, id: String) {
|
||||
prefs(context).edit().putString("fexPreset", id).apply()
|
||||
|
||||
@@ -513,7 +513,11 @@ class SessionService : Service() {
|
||||
// Zink renders the client's UI (Chromium -> ANGLE -> Zink -> Turnip). Lazy descriptors is
|
||||
// the mode Zink recommends where the driver has no descriptor buffer, and what Ludashi ships
|
||||
// by default for its Zink path; a switch here because on one Fold the menus run at 14 fps.
|
||||
if (SessionPrefs.zinkLazy(this)) guest.add("ZINK_DESCRIPTORS=lazy")
|
||||
// compact packs Zink's descriptor sets into fewer, so a draw binds less (WinNative's default).
|
||||
if (SessionPrefs.zinkLazy(this)) {
|
||||
guest.add("ZINK_DESCRIPTORS=lazy")
|
||||
guest.add("ZINK_DEBUG=compact")
|
||||
}
|
||||
// The rest of the client-interface switches (SessionPrefs): GL marshalled off the calling
|
||||
// thread, no GL error checks, and the client run as SteamOS runs it (the script reads
|
||||
// BL_STEAMDECK; it is the one that builds the command line).
|
||||
@@ -526,9 +530,12 @@ class SessionService : Service() {
|
||||
SessionPrefs.writeForceFullscreenFlag(this)
|
||||
}
|
||||
// Proton's own gate for its xalia helper (its `proton` script reads this, and sets
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Off by default: xalia is Valve's, and on a device
|
||||
// whose seccomp answers its syscalls normally there is no reason to take it away.
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game
|
||||
// a slice of a core for gamepad navigation the session already has.
|
||||
if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0")
|
||||
// gamescope's realtime Vulkan queues (the session script turns this into
|
||||
// GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on.
|
||||
guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0"))
|
||||
// Anything else, for a device that cannot be reached with a debugger: Downloads/droiddeck-env
|
||||
// holds KEY=VALUE lines that go into the session's environment as written, after ours, so a
|
||||
// line here wins. Zink and Turnip tunables (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's,
|
||||
|
||||
@@ -68,6 +68,8 @@ class ModeSettings(
|
||||
val forceFullscreen: Boolean? = null,
|
||||
/** Steam only: the client branch forced on the command line. */
|
||||
val steamChannel: String? = null,
|
||||
/** Steam only: enable the SteamOS client interface and its performance controls. */
|
||||
val steamDeckMode: Boolean = false,
|
||||
/** Steam only: start a Steam session when DroidDeck opens. */
|
||||
val runSteamAtStartup: Boolean = false,
|
||||
/** Steam only: the user's chosen Games folders; null outside Steam. */
|
||||
@@ -123,6 +125,7 @@ class ModeSettingsActions(
|
||||
val onFexPreset: (String) -> Unit = {},
|
||||
val onForceFullscreen: (Boolean) -> Unit = {},
|
||||
val onSteamChannel: (String) -> Unit = {},
|
||||
val onSteamDeckMode: (Boolean) -> Unit = {},
|
||||
val onRunSteamAtStartup: (Boolean) -> Unit = {},
|
||||
val onPickAddedGamesDir: () -> Unit = {},
|
||||
val onForgetAddedGamesDir: (path: String) -> Unit = {},
|
||||
@@ -338,6 +341,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
}
|
||||
if (steam && s.steamChannel != null) SettingsGroup("Client") {
|
||||
ToggleRow(
|
||||
host, "steamdeck", "Steam Deck mode",
|
||||
"Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.",
|
||||
s.steamDeckMode, onChange = a.onSteamDeckMode,
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "channel", "Client branch", "The Steam client build the session forces. Applies at the next session start; the client may update itself once.",
|
||||
listOf("publicbeta" to "Public beta", "steamdeck_publicbeta" to "Steam Deck public beta"), s.steamChannel,
|
||||
|
||||
@@ -36,8 +36,8 @@ fun PerformancePage(
|
||||
zinkLazy: Boolean,
|
||||
glThread: Boolean,
|
||||
noGlError: Boolean,
|
||||
steamDeckMode: Boolean,
|
||||
noXalia: Boolean,
|
||||
gamescopeRealtime: Boolean,
|
||||
prootNoSeccomp: Boolean,
|
||||
guestHostname: String,
|
||||
phantomWarning: String?,
|
||||
@@ -46,8 +46,8 @@ fun PerformancePage(
|
||||
onZinkLazy: (Boolean) -> Unit,
|
||||
onGlThread: (Boolean) -> Unit,
|
||||
onNoGlError: (Boolean) -> Unit,
|
||||
onSteamDeckMode: (Boolean) -> Unit,
|
||||
onNoXalia: (Boolean) -> Unit,
|
||||
onGamescopeRealtime: (Boolean) -> Unit,
|
||||
onProotNoSeccomp: (Boolean) -> Unit,
|
||||
onGuestHostname: (String) -> Unit,
|
||||
onClientCore: (Int, Boolean) -> Unit,
|
||||
@@ -88,7 +88,7 @@ fun PerformancePage(
|
||||
)
|
||||
ToggleRow(
|
||||
host, "zink", "Zink: lazy descriptors",
|
||||
"Recommended for drivers without descriptor buffers.",
|
||||
"Lazy, compact descriptors. Recommended for drivers without descriptor buffers.",
|
||||
zinkLazy, onChange = onZinkLazy,
|
||||
)
|
||||
ToggleRow(
|
||||
@@ -97,20 +97,20 @@ fun PerformancePage(
|
||||
noGlError, onChange = onNoGlError,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "deck", "Steam Deck mode",
|
||||
"Runs the client with -droiddeck. SteamOS helpers and battery info are provided.",
|
||||
steamDeckMode, onChange = onSteamDeckMode,
|
||||
host, "gsrealtime", "gamescope: realtime GPU queue",
|
||||
"Gives the compositor's GPU work priority over the game's. Can smooth frame pacing, but can cost games GPU time.",
|
||||
gamescopeRealtime, onChange = onGamescopeRealtime,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Session fixes") {
|
||||
ToggleRow(
|
||||
host, "sysmem", "Turnip: sysmem rendering",
|
||||
"Required on Adreno 710/720/722. May fix corruption on other Adreno GPUs, but can reduce performance.",
|
||||
"On by default: faster for the Steam interface and most games. Required on Adreno 710/720/722.",
|
||||
tuSysmem, onChange = onTuSysmem,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "xalia", "Skip Steam's xalia helper",
|
||||
"Disables Proton's gamepad navigation helper. Try if sessions crash at startup.",
|
||||
"Disables Proton's gamepad navigation helper, which costs every game CPU time. Turn off only if a game needs it.",
|
||||
noXalia, onChange = onNoXalia,
|
||||
)
|
||||
ToggleRow(
|
||||
|
||||
@@ -41,6 +41,10 @@ object CompositorHost {
|
||||
}
|
||||
WaylandCompositor.nativeSetOutputSize(outputWidth, outputHeight)
|
||||
WaylandCompositor.nativeSetOutputRefreshRate(refreshHz)
|
||||
// The cadence the zero-copy layer votes for. Left at 0 the system infers it from the rate
|
||||
// already being achieved, and on a phone whose vendor picks clocks from that a session
|
||||
// that has been slowed reads as one that wants to be. (WinNative, WaylandSession.)
|
||||
WaylandCompositor.nativeSetLayerFrameRate(refreshHz)
|
||||
WaylandCompositor.nativeStartWithSurface(
|
||||
surface, xdgRuntimeDir, driverPath, libraryName, nativeLibDir,
|
||||
)
|
||||
|
||||
@@ -91,7 +91,6 @@ public final class WaylandCompositor {
|
||||
long smoothed = frameIntervalNanos;
|
||||
frameIntervalNanos = smoothed == 0 ? interval : (smoothed * 3 + interval) / 4;
|
||||
}
|
||||
com.droiddeck.launcher.session.PerfHints.onFrame();
|
||||
GameListener l = gameListener;
|
||||
if (l != null) l.onGameFrame();
|
||||
}
|
||||
@@ -218,7 +217,7 @@ public final class WaylandCompositor {
|
||||
* once per tick, so games run unthrottled and the screen shows their latest frame. */
|
||||
public static native void nativeVsync(long frameTimeNanos);
|
||||
|
||||
/** The compositor thread's Linux tid, or 0 before it has started (see PerfHints). */
|
||||
/** The compositor thread's Linux tid, or 0 before it has started. */
|
||||
public static native int nativeCompositorTid();
|
||||
|
||||
/** Shortcut launches: don't draw explorer's windows (desktop, taskbar, Start menu), matching the
|
||||
|
||||
@@ -49,6 +49,15 @@ class GameEnvironmentStoreTest {
|
||||
assertFalse(JSONObject(guest.readText()).getJSONObject("shared").has("FEX_SMCCHECKS"))
|
||||
}
|
||||
|
||||
@Test fun unchosenFexPresetPublishesPerformanceTso() {
|
||||
GameEnvironmentStore.save(context, GameEnvironment.Config())
|
||||
val shared = JSONObject(guest.readText()).getJSONObject("shared")
|
||||
assertEquals("1", shared.getString("FEX_TSOENABLED"))
|
||||
assertEquals("0", shared.getString("FEX_HALFBARRIERTSOENABLED"))
|
||||
assertEquals("1", shared.getString("FEX_X87REDUCEDPRECISION"))
|
||||
assertEquals("1", shared.getString("FEX_MULTIBLOCK"))
|
||||
}
|
||||
|
||||
@Test fun invalidDataCannotReplaceSavedConfiguration() {
|
||||
val original = GameEnvironment.Config(shared = mapOf("CUSTOM" to "ok"))
|
||||
GameEnvironmentStore.save(context, original)
|
||||
|
||||
+57
-16
@@ -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}"
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ library list, before anything is published.
|
||||
- `0020-color-p3-red-is-wide-gamut.patch` - Armada, verbatim.
|
||||
- `0100-realtime-queue-and-gamepad-cursor.patch` - this app, two of Armada's ported by hand onto
|
||||
3.16.29: realtime-priority Vulkan queues on request (`GAMESCOPE_FORCE_VULKAN_REALTIME=1`)
|
||||
without CAP_SYS_NICE, which proot can never have; and the gamepad-driven cursor sprite following
|
||||
the X pointer that XTest moves (it sat frozen).
|
||||
without CAP_SYS_NICE, which proot can never have (a no-op on KGSL Turnip, which has a single
|
||||
submit-queue priority); and the gamepad-driven cursor sprite following the X pointer that XTest
|
||||
moves (it sat frozen). The X pointer is asked for only while a cursor image is drawn - every
|
||||
vblank while shown, every 50 ms while hidden for inactivity - since each ask is a blocking round
|
||||
trip to Xwayland on the paint thread; with no image, wlserver's position is used as upstream does.
|
||||
- `0110-wayland-backend-touch.patch` - this app: the nested Wayland backend bound only the host's
|
||||
pointer and keyboard, so a finger on the phone's screen never reached Steam. It now binds
|
||||
`wl_touch` too and hands each finger to wlserver's touch path (`wlserver_touchdown` / `motion` /
|
||||
|
||||
@@ -7,8 +7,8 @@ index 54e4efc..024af02 100644
|
||||
|
||||
float queuePriorities = 1.0f;
|
||||
+ // Without CAP_SYS_NICE gamescope never asks for realtime-priority queues. Under proot on
|
||||
+ // Android there is no capability to have, but the driver may still grant the priority (Turnip
|
||||
+ // does on Adreno), and the existing fallback below covers a driver that refuses. Opt in with
|
||||
+ // Android there is no capability to have; the fallback below covers a driver that refuses. KGSL
|
||||
+ // Turnip accepts but ignores it (one submit-queue priority, contexts created alike). Opt in with
|
||||
+ // GAMESCOPE_FORCE_VULKAN_REALTIME=1. (Armada's --force-vulkan-realtime, as an environment switch.)
|
||||
+ const char *pszForceRealtime = getenv( "GAMESCOPE_FORCE_VULKAN_REALTIME" );
|
||||
+ const bool useRealtimeVulkanQueue = gamescope::Process::HasCapSysNice() || ( pszForceRealtime && atoi( pszForceRealtime ) != 0 );
|
||||
@@ -52,27 +52,10 @@ index 54e4efc..024af02 100644
|
||||
fprintf(stderr, "vkCreateDevice failed with a high-priority queue (compute). Falling back to regular priority (all).\n");
|
||||
queueCreateInfos[0].pNext = nullptr;
|
||||
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
|
||||
index 18b799e..16f7846 100644
|
||||
index 10990b8..08afbc4 100644
|
||||
--- a/src/steamcompmgr.cpp
|
||||
+++ b/src/steamcompmgr.cpp
|
||||
@@ -2020,23 +2020,41 @@ MouseCursor::MouseCursor(xwayland_ctx_t *ctx)
|
||||
|
||||
void MouseCursor::UpdatePosition()
|
||||
{
|
||||
+ // The gamepad-driven cursor is moved through XTest, which moves the X pointer and nothing
|
||||
+ // else: wlserver's copy of the position never changes, so the sprite sat frozen while the
|
||||
+ // pointer moved. Ask X where the pointer is. (Armada 0001, on 3.16.29.)
|
||||
+ Window root_ret, child_ret;
|
||||
+ int root_x = m_x, root_y = m_y, win_x = 0, win_y = 0;
|
||||
+ unsigned int mask;
|
||||
+ XQueryPointer( m_ctx->dpy, DefaultRootWindow( m_ctx->dpy ),
|
||||
+ &root_ret, &child_ret,
|
||||
+ &root_x, &root_y,
|
||||
+ &win_x, &win_y,
|
||||
+ &mask );
|
||||
+
|
||||
wlserver_lock();
|
||||
struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint();
|
||||
@@ -2025,18 +2025,53 @@ void MouseCursor::UpdatePosition()
|
||||
if ( pConstraint && pConstraint->current.cursor_hint.enabled )
|
||||
{
|
||||
m_bConstrained = true;
|
||||
@@ -84,23 +67,61 @@ index 18b799e..16f7846 100644
|
||||
}
|
||||
- else
|
||||
+ m_bConstrained = false;
|
||||
+ wlserver_unlock();
|
||||
+ if ( root_x != m_x || root_y != m_y )
|
||||
+ // Nothing to draw (the gamepad UI, most games): the position only feeds focus and zoom, and
|
||||
+ // wlserver's copy is what stock gamescope uses for those. No round trip to X.
|
||||
+ if ( m_imageEmpty )
|
||||
{
|
||||
- m_bConstrained = false;
|
||||
-
|
||||
- m_x = wlserver.mouse_surface_cursorx;
|
||||
- m_y = wlserver.mouse_surface_cursory;
|
||||
m_x = wlserver.mouse_surface_cursorx;
|
||||
m_y = wlserver.mouse_surface_cursory;
|
||||
+ wlserver_unlock();
|
||||
+ return;
|
||||
}
|
||||
wlserver_unlock();
|
||||
+
|
||||
+ // The gamepad-driven cursor is moved through XTest, which moves the X pointer and nothing
|
||||
+ // else: wlserver's copy of the position never changes, so the sprite sat frozen while the
|
||||
+ // pointer moved. Ask X where the pointer is. (Armada 0001, on 3.16.29.) That is a blocking
|
||||
+ // round trip to Xwayland on the paint thread, which waits while Xwayland is busy drawing the
|
||||
+ // client's interface: once per vblank only while the sprite is shown, and every 50 ms while
|
||||
+ // it is hidden for inactivity - enough to bring it back within a few frames of a move.
|
||||
+ if ( wlserver.bCursorHidden )
|
||||
+ {
|
||||
+ const uint64_t now = get_time_in_nanos();
|
||||
+ if ( now - m_ulLastPointerQuery < 50'000'000ul )
|
||||
+ return;
|
||||
+ m_ulLastPointerQuery = now;
|
||||
+ }
|
||||
+ Window root_ret, child_ret;
|
||||
+ int root_x = m_x, root_y = m_y, win_x = 0, win_y = 0;
|
||||
+ unsigned int mask;
|
||||
+ XQueryPointer( m_ctx->dpy, DefaultRootWindow( m_ctx->dpy ),
|
||||
+ &root_ret, &child_ret,
|
||||
+ &root_x, &root_y,
|
||||
+ &win_x, &win_y,
|
||||
+ &mask );
|
||||
+
|
||||
+ if ( root_x != m_x || root_y != m_y )
|
||||
+ {
|
||||
+ m_x = root_x;
|
||||
+ m_y = root_y;
|
||||
+ if ( !m_imageEmpty )
|
||||
+ {
|
||||
+ wlserver.ulLastMovedCursorTime = get_time_in_nanos();
|
||||
+ wlserver.bCursorHidden = !wlserver.bCursorHasImage;
|
||||
+ hasRepaintNonBasePlane = true;
|
||||
+ }
|
||||
}
|
||||
- wlserver_unlock();
|
||||
+ wlserver.ulLastMovedCursorTime = get_time_in_nanos();
|
||||
+ wlserver.bCursorHidden = !wlserver.bCursorHasImage;
|
||||
+ hasRepaintNonBasePlane = true;
|
||||
+ }
|
||||
}
|
||||
|
||||
void MouseCursor::checkSuspension()
|
||||
diff --git a/src/steamcompmgr.hpp b/src/steamcompmgr.hpp
|
||||
index c428784..bd62fd1 100644
|
||||
--- a/src/steamcompmgr.hpp
|
||||
+++ b/src/steamcompmgr.hpp
|
||||
@@ -109,6 +109,7 @@ private:
|
||||
|
||||
int m_x = 0, m_y = 0;
|
||||
bool m_bConstrained = false;
|
||||
+ uint64_t m_ulLastPointerQuery = 0;
|
||||
int m_hotspotX = 0, m_hotspotY = 0;
|
||||
|
||||
gamescope::OwningRc<CVulkanTexture> m_texture;
|
||||
@@ -73,10 +73,21 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
|
||||
fi
|
||||
unset bl_icd_lib
|
||||
fi
|
||||
# The app's gamescope build (tools/gamescope) takes realtime-priority Vulkan queues on this
|
||||
# The app's gamescope build (tools/gamescope) asks for realtime-priority Vulkan queues on this
|
||||
# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority
|
||||
# where the driver refuses, and the runtime's own build ignores it.
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-1}
|
||||
# where the driver refuses, and the runtime's own build ignores it. On KGSL Turnip (Mesa 26.2) it
|
||||
# changes nothing yet: the driver reports one submit-queue priority and creates every KGSL context
|
||||
# with the same flags, so no priority reaches the GPU. Kept for a driver that maps it, and off
|
||||
# unless the app's Performance page turns it on (BL_GAMESCOPE_REALTIME=1).
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
|
||||
|
||||
# A sleep that starts no process. Under proot every exec is a traced execve plus the loader's
|
||||
# opens and maps, and the watchers below poll for the whole session: `sleep 1` in a loop was 60
|
||||
# execs a minute each. read -t on a pipe nothing ever writes to waits in bash itself.
|
||||
nap() {
|
||||
[ -n "${bl_nap_fd:-}" ] || exec {bl_nap_fd}<> <(:)
|
||||
read -r -t "$1" -u "$bl_nap_fd" _ 2>/dev/null || :
|
||||
}
|
||||
export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
|
||||
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
|
||||
@@ -266,7 +277,7 @@ case $mode in
|
||||
for _p in $(pgrep -f 'steamwebhelper|steamrtarm64/steam|gamescope' 2>/dev/null); do
|
||||
taskset -cp "$BL_CLIENT_CPUS" "$_p" >/dev/null 2>&1 || true
|
||||
done
|
||||
sleep 5
|
||||
nap 5
|
||||
done
|
||||
) &
|
||||
fi
|
||||
@@ -374,8 +385,9 @@ case $mode in
|
||||
[ -z "${BL_DESKTOP:-}" ] && (
|
||||
last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}
|
||||
while :; do
|
||||
sleep 1
|
||||
want=$(cat "$HOME/.droiddeck-fill" 2>/dev/null) || continue
|
||||
nap 1
|
||||
want=
|
||||
{ read -r want || [ -n "$want" ]; } < "$HOME/.droiddeck-fill" 2>/dev/null || continue
|
||||
case $want in 0|1) ;; *) continue ;; esac
|
||||
[ "$want" = "$last" ] && continue
|
||||
if xprop -root -f GAMESCOPE_FORCE_WINDOWS_FULLSCREEN 32c -set GAMESCOPE_FORCE_WINDOWS_FULLSCREEN "$want" 2>/dev/null; then
|
||||
@@ -387,7 +399,7 @@ case $mode in
|
||||
(
|
||||
beat=0
|
||||
for _ in $(seq 1 720); do
|
||||
sleep 5
|
||||
nap 5
|
||||
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
|
||||
beat=$((beat + 1))
|
||||
if [ "$((beat % 3))" -eq 0 ]; then
|
||||
@@ -501,20 +513,19 @@ BUSCONF
|
||||
# desktop's launchers ask for; gamescope shows one window at a time and the game takes over.
|
||||
_bl_ui=(-gamepadui)
|
||||
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
|
||||
# Deck mode (BL_STEAMDECK=1, the Performance page): -steamdeck is the flag that puts the Quick
|
||||
# Access Menu, the performance overlay and the Deck settings pages on screen. Only -steamdeck,
|
||||
# never -steamos3: that one makes the client manage itself as SteamOS does - it calls
|
||||
# steamos-select-branch and opts the install into the steamdeck_stable client branch, writes
|
||||
# that to package/beta, and from then on the branch on disk loses to the -clientbeta below on
|
||||
# every launch; the updater finds "installed version 0", marks an update pending and exits 42
|
||||
# to apply it, which never happens, so the client restarts for ever - under a running game,
|
||||
# taking Steam Input with it each time. (Bannerlator 38996ea1, on device.) The SteamOS helpers
|
||||
# the client calls in this mode are staged as no-op stubs under /usr/bin.
|
||||
# Deck mode (BL_STEAMDECK=1, the Steam page's Client settings): -steamdeck is the flag that puts
|
||||
# the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay
|
||||
# controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes
|
||||
# the client manage itself as SteamOS does - it calls steamos-select-branch and writes
|
||||
# steamdeck_stable to package/beta, and a branch on disk that loses to the -clientbeta below
|
||||
# is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The
|
||||
# loop below puts package/beta back before every start. The SteamOS helpers the client calls
|
||||
# in this mode are staged as no-op stubs under /usr/bin.
|
||||
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
|
||||
_bl_ui+=(-steamdeck)
|
||||
_bl_ui+=(-steamdeck -steamos3)
|
||||
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
|
||||
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
|
||||
echo "== steam: Deck mode on (-steamdeck)"
|
||||
echo "== steam: Deck mode on (-steamdeck -steamos3)"
|
||||
fi
|
||||
# What a killed session leaves behind. Android ends sessions without warning (backgrounding, the
|
||||
# low-memory killer), and the client then finds .crash - opening its update UI on the spinner -
|
||||
@@ -559,7 +570,7 @@ BUSCONF
|
||||
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.5
|
||||
nap 0.5
|
||||
done
|
||||
) &
|
||||
stop_watcher=$!
|
||||
|
||||
@@ -146,6 +146,26 @@ bl_game_cpus() {
|
||||
# game never gets OpenGL; left at its own size in a window, it draws small in a corner. So these
|
||||
# titles are told to stay windowed at the session's own size - unless the user's launch options
|
||||
# already say something about the video mode, which are left as they are.
|
||||
# The session puts the client's own libraries (steamrtarm64, its panorama/) ahead of everything for
|
||||
# the client, and a game inherits that: Wine then resolved gnutls, SDL, FFmpeg and the rest against
|
||||
# the client's private copies instead of the rootfs it was built for. Inside Steam's runtime
|
||||
# container that path never reaches Proton either. (WinNative's winnative-proton-launch.)
|
||||
BL_LIBPATH_SETUP = """
|
||||
bl_libpath() {
|
||||
[ -n "${LD_LIBRARY_PATH:-}" ] || return 0
|
||||
local kept= dir old_ifs=$IFS
|
||||
IFS=:
|
||||
for dir in $LD_LIBRARY_PATH; do
|
||||
case $dir in
|
||||
"" | */steamrtarm64 | */steamrtarm64/*) ;;
|
||||
*) kept=${kept:+$kept:}$dir ;;
|
||||
esac
|
||||
done
|
||||
IFS=$old_ifs
|
||||
if [ -n "$kept" ]; then export LD_LIBRARY_PATH="$kept"; else unset LD_LIBRARY_PATH; fi
|
||||
}
|
||||
"""
|
||||
|
||||
BL_IDTECH3_SETUP = """
|
||||
bl_idtech3() {
|
||||
BL_EXTRA_ARGS=()
|
||||
@@ -233,6 +253,7 @@ for name in %s; do
|
||||
bl_directaudio "$depot"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
bl_libpath
|
||||
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$depot/proton" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
|
||||
fi
|
||||
done
|
||||
@@ -244,7 +265,7 @@ exit 1
|
||||
# something else.
|
||||
LAUNCHER_SH = LAUNCHER_SH.replace(
|
||||
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
|
||||
+ BL_COMPONENTS_SETUP, 1)
|
||||
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
|
||||
# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of
|
||||
# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot
|
||||
@@ -280,6 +301,7 @@ bl_components "$here"
|
||||
bl_directaudio "$here"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
bl_libpath
|
||||
# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be
|
||||
# started from its own tree.
|
||||
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$here/%s" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
|
||||
@@ -369,6 +391,28 @@ def serialize(tokens):
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def write_changed(path, text, mode=None):
|
||||
"""Writes `text` to `path` only when it differs, through a rename so a reader never sees half a
|
||||
file. The session runs this script every 15 s for as long as the client does; rewriting files
|
||||
that have not changed cost a python start's worth of writes each time, and config.vdf is the
|
||||
client's own, which it may be writing at that moment. Returns whether anything was written."""
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
same = f.read() == text
|
||||
except OSError:
|
||||
same = False
|
||||
if not same:
|
||||
tmp = path + ".bannerlator"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
if mode is not None:
|
||||
os.chmod(tmp, mode)
|
||||
os.replace(tmp, path)
|
||||
elif mode is not None and (os.stat(path).st_mode & 0o7777) != mode:
|
||||
os.chmod(path, mode)
|
||||
return not same
|
||||
|
||||
|
||||
def first_key(path, section):
|
||||
"""The single key naming a block inside `section`, or None. Used to keep a third-party tool's
|
||||
own internal name, so a mapping the user already made against it keeps working."""
|
||||
@@ -442,14 +486,10 @@ def adopt_extras(tools):
|
||||
body = (EXTRA_WRAPPER_SH % entry_point).replace(
|
||||
'here=$(dirname "$0")',
|
||||
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
|
||||
+ BL_COMPONENTS_SETUP, 1)
|
||||
with open(wrapper, "w", encoding="utf-8") as f:
|
||||
f.write(body)
|
||||
os.chmod(wrapper, 0o755)
|
||||
with open(manifest, "w", encoding="utf-8") as f:
|
||||
f.write(EXTRA_MANIFEST_VDF)
|
||||
with open(compat_vdf, "w", encoding="utf-8") as f:
|
||||
f.write(EXTRA_COMPAT_TOOL_VDF % (name, entry))
|
||||
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
write_changed(wrapper, body, 0o755)
|
||||
write_changed(manifest, EXTRA_MANIFEST_VDF)
|
||||
write_changed(compat_vdf, EXTRA_COMPAT_TOOL_VDF % (name, entry))
|
||||
except OSError as e:
|
||||
print("bannerlator-steam-compat: could not adopt %s (%s)" % (entry, e))
|
||||
continue
|
||||
@@ -494,7 +534,8 @@ def register_default(config_path, apps=(), name=None, protect=()):
|
||||
# Tool names the user may deliberately have chosen (see adopt_extras); never repointed.
|
||||
keep = {n.lower() for n in protect}
|
||||
with open(config_path, encoding="utf-8", errors="replace") as f:
|
||||
tokens = tokenize(f.read())
|
||||
original = f.read()
|
||||
tokens = tokenize(original)
|
||||
steam = find_block(tokens, ["InstallConfigStore", "Software", "Valve", "Steam"])
|
||||
if steam is None:
|
||||
return
|
||||
@@ -521,10 +562,10 @@ def register_default(config_path, apps=(), name=None, protect=()):
|
||||
continue
|
||||
tokens[mapping + 1:mapping + 1] = [
|
||||
'"%s"' % app, "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"250"', "}"]
|
||||
tmp = config_path + ".bannerlator"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write(serialize(tokens))
|
||||
os.replace(tmp, config_path)
|
||||
# Nothing to change is the usual case on the session's beat: the client's own file is left alone.
|
||||
text = serialize(tokens)
|
||||
if text != serialize(tokenize(original)):
|
||||
write_changed(config_path, text)
|
||||
|
||||
|
||||
def find_source(steam_root):
|
||||
@@ -542,18 +583,14 @@ def build_tool(tool_dir):
|
||||
"""Our own manifests and the launcher. The depot is found at launch time, not here."""
|
||||
if os.path.islink(tool_dir):
|
||||
os.unlink(tool_dir)
|
||||
elif os.path.isdir(tool_dir):
|
||||
# Earlier versions mirrored the depot in here as symlinks; start clean.
|
||||
elif os.path.isdir(tool_dir) and set(os.listdir(tool_dir)) - set(OWN_FILES):
|
||||
# Earlier versions mirrored the depot in here as symlinks; start clean. A directory holding
|
||||
# only our own files is kept, and each file rewritten only when it changed.
|
||||
shutil.rmtree(tool_dir)
|
||||
os.makedirs(tool_dir)
|
||||
with open(os.path.join(tool_dir, "toolmanifest.vdf"), "w", encoding="utf-8") as f:
|
||||
f.write(TOOL_MANIFEST_VDF)
|
||||
with open(os.path.join(tool_dir, "compatibilitytool.vdf"), "w", encoding="utf-8") as f:
|
||||
f.write(COMPAT_TOOL_VDF % TOOL)
|
||||
launcher = os.path.join(tool_dir, LAUNCHER)
|
||||
with open(launcher, "w", encoding="utf-8") as f:
|
||||
f.write(LAUNCHER_SH)
|
||||
os.chmod(launcher, 0o755)
|
||||
os.makedirs(tool_dir, exist_ok=True)
|
||||
write_changed(os.path.join(tool_dir, "toolmanifest.vdf"), TOOL_MANIFEST_VDF)
|
||||
write_changed(os.path.join(tool_dir, "compatibilitytool.vdf"), COMPAT_TOOL_VDF % TOOL)
|
||||
write_changed(os.path.join(tool_dir, LAUNCHER), LAUNCHER_SH, 0o755)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -135,6 +135,10 @@ def watch(steam_root, listing):
|
||||
candidates, sources = {}, {}
|
||||
next_lookup = 0
|
||||
last_status = None
|
||||
# The probe is a fresh python that loads steamclient.so, seconds of CPU under proot. Ownership
|
||||
# only moves with a purchase or a new match, so it runs when the matches or their sources
|
||||
# change, after a failed probe, and otherwise every five minutes - not on every 30 s beat.
|
||||
probed, next_probe = None, 0
|
||||
while True:
|
||||
now = time.monotonic()
|
||||
for game in games:
|
||||
@@ -150,13 +154,16 @@ def watch(steam_root, listing):
|
||||
if now >= next_lookup:
|
||||
next_lookup = now + 300
|
||||
status = "waiting for a matching Steam title"
|
||||
if candidates:
|
||||
key = (dict(candidates), dict(sources))
|
||||
if candidates and (key != probed or now >= next_probe):
|
||||
probed, next_probe = key, now + 300
|
||||
try:
|
||||
result = subprocess.run([sys.executable, __file__, "probe", steam_root,
|
||||
*map(str, sorted(set(candidates.values())))],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
if result.returncode != 0:
|
||||
status = "ownership probe failed: " + (result.stderr.strip()[-300:] or str(result.returncode))
|
||||
probed = None
|
||||
else:
|
||||
line = next(line for line in result.stdout.splitlines() if line.startswith("DROIDDECK_OWNERSHIP="))
|
||||
snapshot = json.loads(line.removeprefix("DROIDDECK_OWNERSHIP="))
|
||||
@@ -168,6 +175,9 @@ def watch(steam_root, listing):
|
||||
status = "matched %d/%d games; %d owned" % (len(candidates), len(games), sum(snapshot["owned"].values()))
|
||||
except (OSError, ValueError, KeyError, StopIteration, subprocess.TimeoutExpired) as error:
|
||||
status = "ownership check unavailable: " + str(error)
|
||||
probed = None
|
||||
elif candidates:
|
||||
status = last_status
|
||||
if status != last_status:
|
||||
print("bannerlator-steam-games: " + status, flush=True)
|
||||
last_status = status
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 "$@"
|
||||
@@ -44,3 +44,29 @@ Ported from WinNative (`main`, 53836ca9, "Fix/performance and vac"):
|
||||
- `0009-tracee-relatives-sweep.patch` - tracees count their children, so a terminating thread
|
||||
with no children or ptracees no longer walks every tracee; the per-stop memory collector is
|
||||
emptied instead of freed and reallocated.
|
||||
|
||||
Added by DroidDeck:
|
||||
|
||||
- `0011-kompat-utsname-only.patch` - `--kernel-release` (the guest's `DroidDeck` hostname) loads
|
||||
kompat, whose filter traps `futex`, `fcntl`, `epoll_pwait`, `pselect6`, `pipe2`, `eventfd2`,
|
||||
`socket` and more, and which strips `AT_SYSINFO_EHDR` on every `execve`, so glibc runs without
|
||||
the vDSO. When the virtual release is not older than the real kernel and the hwcap is left
|
||||
alone, every one of those handlers is a no-op: kompat now traces only `uname`, `sethostname`
|
||||
and `setdomainname` and leaves the auxv as the kernel wrote it. On an SD 8 Gen 2 guest this
|
||||
took a futex ping-pong from 467 to 97 us, `epoll_pwait` from 60 to 0.8 us and `fcntl` from
|
||||
40-107 to 0.4 us, and `clock_gettime` back to the vDSO.
|
||||
- `0012-fake_id0-identity-only.patch` - `-i uid:gid` (for Xwayland's setgid/setuid before it runs
|
||||
xkbcomp) loads fake_id0, whose filter traps every `fstat`/`newfstatat`/`stat` (entry and exit),
|
||||
every `sendmsg` (all Wayland, X11, Chromium and PulseAudio traffic), `socket`, `getsockopt`, the
|
||||
`get*id` family and the chown/chmod family. When the ids given are the ones proot really has and
|
||||
are not 0, every one of those handlers is a no-op; fake_id0 now traces only the `set*id` family
|
||||
and the xattr permission fixups, and leaves set-user-ID bits alone on `execve` (Android mounts the
|
||||
app's data `nosuid`, so the kernel would not honour them either), which keeps the ids fixed. On an
|
||||
x86_64 host build under the same options: `fstat` 41.7 -> 1.3 us, `sendmsg` 18.0 -> 2.5 us.
|
||||
- `0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch` - every `ioctl` stopped on entry and
|
||||
exit, which is every GPU submit and wait. The filter now traces only the requests enter.c and
|
||||
exit.c act on (`TCSETSF`, the four termios2 requests, `FICLONE`) and allows the rest at once, and
|
||||
the ioctl block is emitted first (after WinNative 53836ca9, which traces only the termios2 ones).
|
||||
The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and
|
||||
the `statx` one (emulation on kernels before 4.11) are dropped when the running kernel is newer.
|
||||
Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop).
|
||||
@@ -0,0 +1,64 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: DroidDeck <[email protected]>
|
||||
Date: Tue, 29 Sep 2026 00:00:00 +0000
|
||||
Subject: [PATCH] kompat: only trace the utsname calls when no older kernel is
|
||||
emulated
|
||||
|
||||
---
|
||||
src/extension/kompat/kompat.c | 29 +++++++++++++++++++++++++++--
|
||||
1 file changed, 27 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/extension/kompat/kompat.c b/src/extension/kompat/kompat.c
|
||||
index 5c39942..4cb7dec 100644
|
||||
--- a/src/extension/kompat/kompat.c
|
||||
+++ b/src/extension/kompat/kompat.c
|
||||
@@ -985,6 +985,29 @@ static FilteredSysnum filtered_sysnums[] = {
|
||||
FILTERED_SYSNUM_END,
|
||||
};
|
||||
|
||||
+/* When the virtual kernel is not older than the actual one and the
|
||||
+ * hwcap is left alone, the only thing left to emulate is the utsname
|
||||
+ * itself: every other handler above returns without a change. */
|
||||
+static FilteredSysnum utsname_only_sysnums[] = {
|
||||
+ { PR_setdomainname, FILTER_SYSEXIT },
|
||||
+ { PR_sethostname, FILTER_SYSEXIT },
|
||||
+ { PR_uname, FILTER_SYSEXIT },
|
||||
+ FILTERED_SYSNUM_END,
|
||||
+};
|
||||
+
|
||||
+/**
|
||||
+ * Return whether @config only renames the kernel identity (nodename,
|
||||
+ * domainname, version string) without emulating an older kernel or
|
||||
+ * another hwcap, in which case futex, fcntl, epoll_pwait, ... don't
|
||||
+ * have to stop in the tracer and the vDSO can be left in place.
|
||||
+ */
|
||||
+static bool utsname_only(const Config *config)
|
||||
+{
|
||||
+ return config->actual_release != 0
|
||||
+ && config->virtual_release >= config->actual_release
|
||||
+ && config->hwcap == (word_t) -1;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* Handler for this @extension. It is triggered each time an @event
|
||||
* occured. See ExtensionEvent for the meaning of @data1 and @data2.
|
||||
@@ -1007,7 +1030,9 @@ int kompat_callback(Extension *extension, ExtensionEvent event,
|
||||
if (status < 0)
|
||||
return -1;
|
||||
|
||||
- extension->filtered_sysnums = filtered_sysnums;
|
||||
+ extension->filtered_sysnums = utsname_only(config)
|
||||
+ ? utsname_only_sysnums
|
||||
+ : filtered_sysnums;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1038,7 +1063,7 @@ int kompat_callback(Extension *extension, ExtensionEvent event,
|
||||
|
||||
/* Note: this can be done only before PRoot pushes the
|
||||
* load script into tracee's stack. */
|
||||
- if ((int) result >= 0 && sysnum == PR_execve)
|
||||
+ if ((int) result >= 0 && sysnum == PR_execve && !utsname_only(config))
|
||||
adjust_elf_auxv(tracee, config);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: DroidDeck <[email protected]>
|
||||
Date: Tue, 29 Sep 2026 00:00:00 +0000
|
||||
Subject: [PATCH] fake_id0: only trace the set*id and xattr calls when the ids
|
||||
are PRoot's own
|
||||
|
||||
---
|
||||
src/extension/fake_id0/config.h | 5 ++++
|
||||
src/extension/fake_id0/fake_id0.c | 44 ++++++++++++++++++++++++++++++-
|
||||
2 files changed, 48 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/extension/fake_id0/config.h b/src/extension/fake_id0/config.h
|
||||
index 2e78783..5227ced 100644
|
||||
--- a/src/extension/fake_id0/config.h
|
||||
+++ b/src/extension/fake_id0/config.h
|
||||
@@ -2,6 +2,7 @@
|
||||
#define FAKE_ID0_CONFIG_H
|
||||
|
||||
#include <sys/types.h> /* uid_t, gid_t */
|
||||
+#include <stdbool.h> /* bool, */
|
||||
|
||||
typedef struct {
|
||||
uid_t ruid;
|
||||
@@ -15,6 +16,10 @@ typedef struct {
|
||||
gid_t fsgid;
|
||||
|
||||
mode_t umask;
|
||||
+
|
||||
+ /* The emulated ids are the ones PRoot really has: see
|
||||
+ * identity_sysnums in fake_id0.c. */
|
||||
+ bool identity;
|
||||
} Config;
|
||||
|
||||
#endif /* FAKE_ID0_CONFIG_H */
|
||||
diff --git a/src/extension/fake_id0/fake_id0.c b/src/extension/fake_id0/fake_id0.c
|
||||
index 9477f1c..fe08372 100644
|
||||
--- a/src/extension/fake_id0/fake_id0.c
|
||||
+++ b/src/extension/fake_id0/fake_id0.c
|
||||
@@ -352,6 +352,40 @@ static FilteredSysnum filtered_sysnums[] = {
|
||||
FILTERED_SYSNUM_END,
|
||||
};
|
||||
|
||||
+/* When the emulated ids are the ones PRoot really has, and the tracee
|
||||
+ * is not root, every handler for the syscalls above but these is a
|
||||
+ * no-op: stat and get*id report the real ids already, SCM_CREDENTIALS
|
||||
+ * and SO_PEERCRED carry them, chown swaps an id for itself, and the
|
||||
+ * permission and chroot fixups only apply to euid 0. The set*id
|
||||
+ * family is still emulated (Android's app seccomp policy traps it) and
|
||||
+ * so are the xattr permission fixups, which apply to any euid. An
|
||||
+ * unprivileged set*id can only take an id the tracee already holds,
|
||||
+ * and set-user-ID bits are not honoured in this mode (Android mounts
|
||||
+ * the app's data nosuid, so the kernel would not honour them either),
|
||||
+ * so the ids never change and the list stays sufficient. */
|
||||
+static FilteredSysnum identity_sysnums[] = {
|
||||
+ { PR_fsetxattr, FILTER_SYSEXIT },
|
||||
+ { PR_lsetxattr, FILTER_SYSEXIT },
|
||||
+ { PR_setfsgid, FILTER_SYSEXIT },
|
||||
+ { PR_setfsgid32, FILTER_SYSEXIT },
|
||||
+ { PR_setfsuid, FILTER_SYSEXIT },
|
||||
+ { PR_setfsuid32, FILTER_SYSEXIT },
|
||||
+ { PR_setgid, FILTER_SYSEXIT },
|
||||
+ { PR_setgid32, FILTER_SYSEXIT },
|
||||
+ { PR_setregid, FILTER_SYSEXIT },
|
||||
+ { PR_setregid32, FILTER_SYSEXIT },
|
||||
+ { PR_setresgid, FILTER_SYSEXIT },
|
||||
+ { PR_setresgid32, FILTER_SYSEXIT },
|
||||
+ { PR_setresuid, FILTER_SYSEXIT },
|
||||
+ { PR_setresuid32, FILTER_SYSEXIT },
|
||||
+ { PR_setreuid, FILTER_SYSEXIT },
|
||||
+ { PR_setreuid32, FILTER_SYSEXIT },
|
||||
+ { PR_setuid, FILTER_SYSEXIT },
|
||||
+ { PR_setuid32, FILTER_SYSEXIT },
|
||||
+ { PR_setxattr, FILTER_SYSEXIT },
|
||||
+ FILTERED_SYSNUM_END,
|
||||
+};
|
||||
+
|
||||
/**
|
||||
* Restore the @node->mode for the given @node->path.
|
||||
*
|
||||
@@ -1051,6 +1085,11 @@ static int handle_sysexit_start(Tracee *tracee, Config *config) {
|
||||
if ((int) result < 0 || tracee->status < 0 || sysnum != PR_execve)
|
||||
return 0;
|
||||
|
||||
+ /* The auxv already carries the real ids, and set-user-ID bits
|
||||
+ * are not honoured in this mode: see identity_sysnums. */
|
||||
+ if (config->identity)
|
||||
+ return 0;
|
||||
+
|
||||
/* This has to be done before PRoot pushes the load
|
||||
* script into tracee's stack. */
|
||||
if (!tracee->skip_proot_loader)
|
||||
@@ -1119,7 +1158,10 @@ int fake_id0_callback(Extension *extension, ExtensionEvent event, intptr_t data1
|
||||
/* Set the umask to the typical linux value. */
|
||||
config->umask = 022;
|
||||
|
||||
- extension->filtered_sysnums = filtered_sysnums;
|
||||
+ config->identity = (uid != 0 && uid == (int) getuid() && gid == (int) getgid());
|
||||
+ extension->filtered_sysnums = config->identity
|
||||
+ ? identity_sysnums
|
||||
+ : filtered_sysnums;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: DroidDeck <[email protected]>
|
||||
Date: Tue, 29 Sep 2026 00:00:00 +0000
|
||||
Subject: [PATCH] seccomp: trace ioctl by request, drop exit stops the kernel
|
||||
does not need
|
||||
|
||||
---
|
||||
src/syscall/seccomp.c | 98 ++++++++++++++++++++++++++++++++++++++++++-
|
||||
1 file changed, 97 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/syscall/seccomp.c b/src/syscall/seccomp.c
|
||||
index 0b381bb..1116ab2 100644
|
||||
--- a/src/syscall/seccomp.c
|
||||
+++ b/src/syscall/seccomp.c
|
||||
@@ -39,6 +39,11 @@
|
||||
#include <stdint.h> /* uint*_t, UINT*_MAX, */
|
||||
#include <stdbool.h>
|
||||
#include <sys/resource.h>
|
||||
+#include <sys/utsname.h> /* uname(2), */
|
||||
+#include <stdio.h> /* sscanf(3), */
|
||||
+#include <asm/ioctls.h> /* TCGETS2, TCSETS*2, */
|
||||
+#include <asm/termbits.h> /* struct termios2, */
|
||||
+#include <linux/fs.h> /* FICLONE, */
|
||||
#include <assert.h> /* assert(3), */
|
||||
|
||||
#include "syscall/seccomp.h"
|
||||
@@ -166,6 +171,46 @@ static int add_trace_prlimit_stack(struct sock_fprog *program, word_t syscall, i
|
||||
return add_statements(program, LENGTH_TRACE_PRLIMIT_STACK, statements);
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * Append to @program->filter the statements that trace @syscall
|
||||
+ * (ioctl) only for the requests enter.c and exit.c act on - the
|
||||
+ * termios requests Android's policy denies an app on a pty, rewritten
|
||||
+ * to ones it allows, and FICLONE, whose EACCES is turned into
|
||||
+ * EOPNOTSUPP - and allow any other request at once. Every GPU submit
|
||||
+ * and wait is an ioctl, so they no longer stop in the tracer, and the
|
||||
+ * block is emitted first so they are also spared the rest of the list.
|
||||
+ * (After WinNative, maxjivi05, 53836ca9.)
|
||||
+ */
|
||||
+static int add_trace_ioctl_requests(struct sock_fprog *program, word_t syscall, int flag)
|
||||
+{
|
||||
+ const size_t request_offset = offsetof(struct seccomp_data, args[1]);
|
||||
+
|
||||
+ #define LENGTH_TRACE_IOCTL 10
|
||||
+ struct sock_filter statements[LENGTH_TRACE_IOCTL] = {
|
||||
+ /* Not this syscall: skip the block, the accumulator
|
||||
+ * still holds the syscall number. */
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, syscall, 0, 9),
|
||||
+
|
||||
+ /* Load the request's low word into the accumulator. */
|
||||
+ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, request_offset),
|
||||
+
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSF, 5, 0),
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCGETS2, 4, 0),
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETS2, 3, 0),
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSW2, 2, 0),
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSF2, 1, 0),
|
||||
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (uint32_t) FICLONE, 0, 1),
|
||||
+
|
||||
+ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE + flag),
|
||||
+ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
|
||||
+ };
|
||||
+
|
||||
+ DEBUG_FILTER("FILTER: trace if syscall == %ld and a termios2/TCSETSF/FICLONE request\n",
|
||||
+ syscall);
|
||||
+
|
||||
+ return add_statements(program, LENGTH_TRACE_IOCTL, statements);
|
||||
+}
|
||||
+
|
||||
static size_t trace_length(Sysnum sysnum)
|
||||
{
|
||||
switch (sysnum) {
|
||||
@@ -174,6 +219,8 @@ static size_t trace_length(Sysnum sysnum)
|
||||
return LENGTH_TRACE_SYSCALL_IF_ARG;
|
||||
case PR_prlimit64:
|
||||
return LENGTH_TRACE_PRLIMIT_STACK;
|
||||
+ case PR_ioctl:
|
||||
+ return LENGTH_TRACE_IOCTL;
|
||||
default:
|
||||
return LENGTH_TRACE_SYSCALL;
|
||||
}
|
||||
@@ -191,6 +238,8 @@ static int add_trace(struct sock_fprog *program, Sysnum sysnum, word_t syscall,
|
||||
return add_trace_syscall_if_arg(program, syscall, 0, RLIMIT_STACK, flag);
|
||||
case PR_prlimit64:
|
||||
return add_trace_prlimit_stack(program, syscall, flag);
|
||||
+ case PR_ioctl:
|
||||
+ return add_trace_ioctl_requests(program, syscall, flag);
|
||||
default:
|
||||
return add_trace_syscall(program, syscall, flag);
|
||||
}
|
||||
@@ -329,7 +378,7 @@ static int set_seccomp_filters(const FilteredSysnum *sysnums)
|
||||
|
||||
struct sock_fprog program = { .len = 0, .filter = NULL };
|
||||
size_t section_length;
|
||||
- size_t i, j, k;
|
||||
+ size_t i, j, k, pass;
|
||||
int status;
|
||||
|
||||
status = new_program_filter(&program);
|
||||
@@ -355,8 +404,14 @@ static int set_seccomp_filters(const FilteredSysnum *sysnums)
|
||||
if (status < 0)
|
||||
goto end;
|
||||
|
||||
+ /* Two passes: ioctl first, it is by far the most frequent
|
||||
+ * syscall and almost every one of them is allowed. */
|
||||
+ for (pass = 0; pass < 2; pass++)
|
||||
for (j = 0; j < seccomp_archs[i].nb_abis; j++) {
|
||||
for (k = 0; sysnums[k].value != PR_void; k++) {
|
||||
+ if ((sysnums[k].value == PR_ioctl) != (pass == 0))
|
||||
+ continue;
|
||||
+
|
||||
/* Get the architecture specific syscall number. */
|
||||
syscall = detranslate_sysnum(seccomp_archs[i].abis[j], sysnums[k].value);
|
||||
if (syscall == SYSCALL_AVOIDER)
|
||||
@@ -572,12 +627,53 @@ int enable_syscall_filtering(const Tracee *tracee)
|
||||
return 0;
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * Drop the exit stops PRoot only needs on kernels older than the one it
|
||||
+ * runs on. The faccessat2 exit stop restores the arguments for glibc's
|
||||
+ * fallback to faccessat on ENOSYS (kernels before 5.8, termux 5ba8b95);
|
||||
+ * the statx one emulates the call when it fails, which only a kernel
|
||||
+ * before 4.11 does for want of it - a statx Android's policy traps is
|
||||
+ * emulated from SIGSYS instead. Both stops re-read and re-translate
|
||||
+ * the path, and are paid on every call.
|
||||
+ */
|
||||
+static void drop_unneeded_exit_stops(void)
|
||||
+{
|
||||
+ static bool done = false;
|
||||
+ struct utsname utsname;
|
||||
+ int major, minor;
|
||||
+ size_t i;
|
||||
+
|
||||
+ if (done)
|
||||
+ return;
|
||||
+ done = true;
|
||||
+
|
||||
+ if (uname(&utsname) < 0 || sscanf(utsname.release, "%d.%d", &major, &minor) != 2)
|
||||
+ return;
|
||||
+
|
||||
+ for (i = 0; proot_sysnums[i].value != PR_void; i++) {
|
||||
+ switch (proot_sysnums[i].value) {
|
||||
+ case PR_faccessat2:
|
||||
+ if (major > 5 || (major == 5 && minor >= 8))
|
||||
+ proot_sysnums[i].flags &= ~FILTER_SYSEXIT;
|
||||
+ break;
|
||||
+ case PR_statx:
|
||||
+ if (major > 4 || (major == 4 && minor >= 11))
|
||||
+ proot_sysnums[i].flags &= ~FILTER_SYSEXIT;
|
||||
+ break;
|
||||
+ default:
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
static int collect_filtered_sysnums(TALLOC_CTX *context, const Tracee *tracee,
|
||||
FilteredSysnum **filtered_sysnums)
|
||||
{
|
||||
Extension *extension;
|
||||
int status;
|
||||
|
||||
+ drop_unneeded_exit_stops();
|
||||
+
|
||||
/* Add the sysnums required by PRoot to the list of filtered
|
||||
* sysnums. TODO: only if path translation is required. */
|
||||
status = merge_filtered_sysnums(context, filtered_sysnums, proot_sysnums);
|
||||
Reference in new issue
Block a user