mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1799fc6621 | ||
|
|
b711a96863 | ||
|
|
f3c923b472 | ||
|
|
1e804562b1 | ||
|
|
e275fbd788 | ||
|
|
c3bd298ecf | ||
|
|
807f5adcba | ||
|
|
d6bc4da759 | ||
|
|
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];
|
||||
@@ -466,6 +468,12 @@ static struct seat_pointer g_ptrs[MAX_PTRS];
|
||||
static int g_nptrs;
|
||||
static struct seat_keyboard g_kbs[MAX_PTRS];
|
||||
static int g_nkbs;
|
||||
/* Modifier state in the keymap's real-modifier bits (Shift=0x1, Lock=0x2, Control=0x4, Mod1=0x8,
|
||||
* Mod4=0x40). A client of the wayland backend - gamescope, labwc - takes modifiers only from
|
||||
* wl_keyboard.modifiers and ignores what a Shift key event does to its xkb state, so without this
|
||||
* every capital arrived lowercase. */
|
||||
static uint32_t g_mods_depressed, g_mods_locked;
|
||||
static uint32_t g_mod_keys_held; /* one bit per modifier key below, so a repeat press is harmless */
|
||||
static struct seat_touch g_touches[MAX_PTRS];
|
||||
static int g_ntouches;
|
||||
|
||||
@@ -716,6 +724,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
|
||||
wl_buffer_send_release(buffer);
|
||||
s->buf_w = w;
|
||||
s->buf_h = h;
|
||||
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
|
||||
s->has_content = s->shm_img != NULL;
|
||||
g_stat_shm++;
|
||||
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
|
||||
@@ -767,6 +776,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
}
|
||||
s->buf_w = b->width;
|
||||
s->buf_h = b->height;
|
||||
s->buf_alpha = (b->format & 0xff) == 'A';
|
||||
s->has_content = b->img != NULL;
|
||||
g_stat_dmabuf++;
|
||||
const int announced_now = !s->announced_vulkan;
|
||||
@@ -938,8 +948,27 @@ static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t c
|
||||
wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy);
|
||||
wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback));
|
||||
}
|
||||
/* The bounding box serves pointer confinement. The exact flag also tells opaque-region handling
|
||||
* whether the bounding box itself is the whole region. */
|
||||
struct region { int set, exact; int x, y, w, h; };
|
||||
|
||||
/* A wl_surface's opaque region is pending until its next commit. An exact region that covers
|
||||
* the surface lets the blit path keep handling an otherwise alpha-capable buffer. */
|
||||
static void surface_set_opaque(struct wl_client *c, struct wl_resource *r,
|
||||
struct wl_resource *region) {}
|
||||
struct wl_resource *region) {
|
||||
struct surface *s = wl_resource_get_user_data(r);
|
||||
struct region *rg = region ? wl_resource_get_user_data(region) : NULL;
|
||||
memset(s->pending_opaque, 0, sizeof(s->pending_opaque));
|
||||
s->pending_opaque_set = 1;
|
||||
if (rg && rg->set && rg->exact) {
|
||||
s->pending_opaque[0] = rg->x;
|
||||
s->pending_opaque[1] = rg->y;
|
||||
s->pending_opaque[2] = rg->w;
|
||||
s->pending_opaque[3] = rg->h;
|
||||
}
|
||||
struct client_info *ci = client_info_of(c);
|
||||
if (ci) ci->declares_opaque = 1;
|
||||
}
|
||||
static void surface_set_input(struct wl_client *c, struct wl_resource *r,
|
||||
struct wl_resource *region) {}
|
||||
|
||||
@@ -1030,6 +1059,10 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
memcpy(s->dst, s->pending_dst, sizeof(s->dst));
|
||||
s->pending_dst_set = 0;
|
||||
}
|
||||
if (s->pending_opaque_set) {
|
||||
memcpy(s->opaque, s->pending_opaque, sizeof(s->opaque));
|
||||
s->pending_opaque_set = 0;
|
||||
}
|
||||
|
||||
if (s->pending_attach) {
|
||||
struct wl_resource *buffer = s->pending_buffer;
|
||||
@@ -1168,24 +1201,29 @@ static void surface_resource_destroy(struct wl_resource *r) {
|
||||
|
||||
/* ------------------------------------------------------------------ wl_region */
|
||||
|
||||
/* A region is kept as the bounding box of its rectangles: enough for pointer confinement,
|
||||
* where winewayland sends one rectangle (the ClipCursor area). Subtractions are ignored. */
|
||||
struct region { int set; int x, y, w, h; };
|
||||
|
||||
static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
static void region_add(struct wl_client *c, struct wl_resource *r,
|
||||
int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
struct region *rg = wl_resource_get_user_data(r);
|
||||
if (!rg || w <= 0 || h <= 0) return;
|
||||
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = 1; return; }
|
||||
int x2 = rg->x + rg->w > x + w ? rg->x + rg->w : x + w;
|
||||
int y2 = rg->y + rg->h > y + h ? rg->y + rg->h : y + h;
|
||||
if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = rg->exact = 1; return; }
|
||||
long long x2 = (long long)rg->x + rg->w, y2 = (long long)rg->y + rg->h;
|
||||
long long nx2 = (long long)x + w, ny2 = (long long)y + h;
|
||||
int inside = x >= rg->x && y >= rg->y && nx2 <= x2 && ny2 <= y2;
|
||||
int around = x <= rg->x && y <= rg->y && nx2 >= x2 && ny2 >= y2;
|
||||
if (!inside && !around) rg->exact = 0;
|
||||
if (nx2 > x2) x2 = nx2;
|
||||
if (ny2 > y2) y2 = ny2;
|
||||
if (x < rg->x) rg->x = x;
|
||||
if (y < rg->y) rg->y = y;
|
||||
rg->w = x2 - rg->x; rg->h = y2 - rg->y;
|
||||
rg->w = (int)(x2 - rg->x > INT32_MAX ? INT32_MAX : x2 - rg->x);
|
||||
rg->h = (int)(y2 - rg->y > INT32_MAX ? INT32_MAX : y2 - rg->y);
|
||||
}
|
||||
static void region_subtract(struct wl_client *c, struct wl_resource *r,
|
||||
int32_t x, int32_t y, int32_t w, int32_t h) {}
|
||||
int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||
struct region *rg = wl_resource_get_user_data(r);
|
||||
if (rg && w > 0 && h > 0) rg->exact = 0;
|
||||
}
|
||||
static void region_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); }
|
||||
static const struct wl_region_interface region_impl = {
|
||||
.destroy = region_destroy,
|
||||
@@ -1659,6 +1697,17 @@ static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, i
|
||||
g_hdr_unimported_below = dl->n;
|
||||
}
|
||||
|
||||
/* A client that describes opaque regions can intentionally leave part of an alpha buffer see-through.
|
||||
* Gamescope uses this for its full-size Steam notification and overlay planes. */
|
||||
static int surface_translucent(const struct surface *s, int w, int h) {
|
||||
if (!s->buf_alpha) return 0;
|
||||
const struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
|
||||
if (!ci || !ci->declares_opaque) return 0;
|
||||
const int *o = s->opaque;
|
||||
return !(o[2] > 0 && o[0] <= 0 && o[1] <= 0 &&
|
||||
(long long)o[0] + o[2] >= w && (long long)o[1] + o[3] >= h);
|
||||
}
|
||||
|
||||
static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy) {
|
||||
struct vkp_image *img = surface_image(s);
|
||||
float sx = 0, sy = 0, sw = (float)s->buf_w, sh = (float)s->buf_h;
|
||||
@@ -1678,7 +1727,8 @@ static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy)
|
||||
dl->d = d;
|
||||
dl->cap = cap;
|
||||
}
|
||||
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh};
|
||||
dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh,
|
||||
surface_translucent(s, dw, dh)};
|
||||
s->drawn = 1;
|
||||
}
|
||||
|
||||
@@ -1749,6 +1799,7 @@ static int on_frame_timer(void *data) {
|
||||
static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) {
|
||||
for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) {
|
||||
const struct vkp_draw *d = &dl->d[i];
|
||||
if (d->blend) continue; /* the translucent plane must not replace the opaque game layer */
|
||||
if (!vkp_image_is_dmabuf(d->img)) continue;
|
||||
if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue;
|
||||
if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) ||
|
||||
@@ -2087,7 +2138,7 @@ static void render_scene(void) {
|
||||
* its own layer, cropped and placed by the display. */
|
||||
int go[8], ov = 0;
|
||||
if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go))
|
||||
ov = sc_layer_present_overlay(dl.d[li + 1].img, go) == 0 ? 1 : -1;
|
||||
ov = sc_layer_present_overlay(dl.d[li + 1].img, go, dl.d[li + 1].blend) == 0 ? 1 : -1;
|
||||
if (ov <= 0) sc_layer_hide_overlay();
|
||||
if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */
|
||||
sc_layer_hide();
|
||||
@@ -2370,8 +2421,8 @@ static void keyboard_focus(struct wl_resource *target) {
|
||||
if (sk->focus == target) continue;
|
||||
sk->focus = target;
|
||||
wl_keyboard_send_enter(sk->kb, wl_display_next_serial(g_display), target, &keys);
|
||||
/* Baseline modifiers = none; Shift/Ctrl arrive as their own key events. */
|
||||
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display), 0, 0, 0, 0);
|
||||
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display),
|
||||
g_mods_depressed, 0, g_mods_locked, 0);
|
||||
}
|
||||
wl_array_release(&keys);
|
||||
banner_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */
|
||||
@@ -3068,7 +3119,34 @@ static void scroll_event(int steps) {
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
|
||||
/* Folds a key into the modifier state; returns whether the state changed. */
|
||||
static int update_modifiers(uint32_t evdev, int pressed) {
|
||||
static const struct { uint32_t evdev, mask; } mod_keys[] = {
|
||||
{ 42, 0x1 }, { 54, 0x1 }, /* Shift L/R */
|
||||
{ 29, 0x4 }, { 97, 0x4 }, /* Control L/R */
|
||||
{ 56, 0x8 }, { 100, 0x8 }, /* Alt L/R, both Mod1 in this keymap */
|
||||
{ 125, 0x40 }, { 126, 0x40 }, /* Super L/R */
|
||||
};
|
||||
uint32_t depressed = g_mods_depressed, locked = g_mods_locked;
|
||||
if (evdev == 58) { /* Caps Lock toggles on press */
|
||||
if (pressed) locked ^= 0x2;
|
||||
} else {
|
||||
int i, n = (int)(sizeof(mod_keys) / sizeof(mod_keys[0]));
|
||||
for (i = 0; i < n && mod_keys[i].evdev != evdev; i++) {}
|
||||
if (i == n) return 0;
|
||||
if (pressed) g_mod_keys_held |= 1u << i; else g_mod_keys_held &= ~(1u << i);
|
||||
depressed = 0;
|
||||
for (int j = 0; j < n; j++) if (g_mod_keys_held & (1u << j)) depressed |= mod_keys[j].mask;
|
||||
}
|
||||
if (depressed == g_mods_depressed && locked == g_mods_locked) return 0;
|
||||
g_mods_depressed = depressed;
|
||||
g_mods_locked = locked;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void key_event(uint32_t evdev, int pressed) {
|
||||
/* Tracked even with nothing to deliver to, so a Shift released unseen is not left held. */
|
||||
int mods_changed = update_modifiers(evdev, pressed);
|
||||
/* Keys go to the program window the user last clicked (else the topmost non-shell window),
|
||||
* never to Wine's desktop surface: winewayland hands each key to the hwnd of the surface that
|
||||
* holds keyboard focus, and a key handed to explorer's desktop hwnd is queued to explorer's
|
||||
@@ -3088,9 +3166,13 @@ static void key_event(uint32_t evdev, int pressed) {
|
||||
struct seat_keyboard *sk;
|
||||
if (!keyboard_for(client)) return;
|
||||
keyboard_focus(target->resource);
|
||||
for_each_keyboard_of(client, sk)
|
||||
for_each_keyboard_of(client, sk) {
|
||||
wl_keyboard_send_key(sk->kb, wl_display_next_serial(g_display), now_ms(), evdev,
|
||||
pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
|
||||
if (mods_changed)
|
||||
wl_keyboard_send_modifiers(sk->kb, wl_display_next_serial(g_display),
|
||||
g_mods_depressed, 0, g_mods_locked, 0);
|
||||
}
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ struct client_info {
|
||||
/* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's
|
||||
* Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */
|
||||
unsigned asked_feedback : 1, shm_gl_said : 1;
|
||||
/* Clients that describe opaque regions can use alpha outside those regions. Wine windows
|
||||
* never describe them, and their unused alpha channel must not make them translucent. */
|
||||
unsigned declares_opaque : 1;
|
||||
unsigned dmabuf_buffers, shm_frames;
|
||||
struct client_info *next;
|
||||
};
|
||||
|
||||
@@ -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,9 @@ class MainActivity : ComponentActivity() {
|
||||
private var gameCores by mutableStateOf<Set<Int>>(emptySet())
|
||||
private var tuSysmem by mutableStateOf(false)
|
||||
private var zinkLazy by mutableStateOf(false)
|
||||
private var noXalia by mutableStateOf(false)
|
||||
private var noXalia by mutableStateOf(true)
|
||||
private var gamescopeRealtime by mutableStateOf(false)
|
||||
private var sustainedPerformance by mutableStateOf(false)
|
||||
private var prootNoSeccomp by mutableStateOf(false)
|
||||
private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME)
|
||||
private var phantomWarning by mutableStateOf<String?>(null)
|
||||
@@ -503,8 +505,9 @@ class MainActivity : ComponentActivity() {
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
// The microphone is on by default; ask once, with the storage prompt, so voice chat works
|
||||
// without a trip to the settings. A refusal is not asked again - the toggle asks when used.
|
||||
// The microphone is off by default. Someone who turned it on before the permission was
|
||||
// granted is asked once here, with the storage prompt; a refusal is not asked again - the
|
||||
// toggle asks when used.
|
||||
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
|
||||
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.RECORD_AUDIO)
|
||||
@@ -805,6 +808,7 @@ class MainActivity : ComponentActivity() {
|
||||
storageOptions = storageOptions,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
|
||||
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
|
||||
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
|
||||
addedGamesDirs = if (mode == SessionService.MODE_STEAM) addedGamesDirs else null,
|
||||
addedGames = if (mode == SessionService.MODE_STEAM) addedGames else emptyList(),
|
||||
@@ -856,6 +860,11 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
onSteamChannel = { id -> SessionPrefs.setSteamChannel(this, id); steamChannel = id },
|
||||
onSteamDeckMode = { on ->
|
||||
SessionPrefs.setSteamDeckMode(this, on)
|
||||
steamDeckMode = on
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
},
|
||||
onRunSteamAtStartup = { on ->
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
@@ -886,15 +895,17 @@ class MainActivity : ComponentActivity() {
|
||||
PerformancePage(
|
||||
cores = CpuCores.all.map { c -> CoreRow(c, "cpu$c" + (CpuCores.maxGhz(c)?.let { String.format(java.util.Locale.US, " · %.1f GHz", it) } ?: "")) },
|
||||
clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores,
|
||||
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, steamDeckMode = steamDeckMode, noXalia = noXalia,
|
||||
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia,
|
||||
gamescopeRealtime = gamescopeRealtime, sustainedPerformance = sustainedPerformance,
|
||||
prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning,
|
||||
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
|
||||
onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on },
|
||||
onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on },
|
||||
onGlThread = { on -> SessionPrefs.setGlThread(this, on); glThread = on },
|
||||
onNoGlError = { on -> SessionPrefs.setNoGlError(this, on); noGlError = on },
|
||||
onSteamDeckMode = { on -> SessionPrefs.setSteamDeckMode(this, on); steamDeckMode = on },
|
||||
onNoXalia = { on -> SessionPrefs.setNoXalia(this, on); noXalia = on },
|
||||
onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on },
|
||||
onSustainedPerformance = { on -> SessionPrefs.setSustainedPerformance(this, on); sustainedPerformance = on },
|
||||
onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on },
|
||||
onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) },
|
||||
onClientCore = { core, on ->
|
||||
@@ -939,6 +950,7 @@ class MainActivity : ComponentActivity() {
|
||||
customResolution = SessionPrefs.customResolution(this, mode)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
steamDeckMode = SessionPrefs.steamDeckMode(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
|
||||
refreshAddedGames()
|
||||
@@ -988,6 +1000,8 @@ class MainActivity : ComponentActivity() {
|
||||
tuSysmem = SessionPrefs.tuSysmem(this)
|
||||
zinkLazy = SessionPrefs.zinkLazy(this)
|
||||
noXalia = SessionPrefs.noXalia(this)
|
||||
gamescopeRealtime = SessionPrefs.gamescopeRealtime(this)
|
||||
sustainedPerformance = SessionPrefs.sustainedPerformance(this)
|
||||
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
|
||||
guestHostname = SessionPrefs.guestHostname(this)
|
||||
refreshPhantomStatus()
|
||||
|
||||
@@ -460,7 +460,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
performBackAction(double = false)
|
||||
}
|
||||
pendingBackAction = pending
|
||||
uiHandler.postDelayed(pending, BACK_DOUBLE_PRESS_TIMEOUT_MS)
|
||||
// Android's own double-tap window (300 ms): the single action waits it out, so it is felt.
|
||||
uiHandler.postDelayed(pending, android.view.ViewConfiguration.getDoubleTapTimeout().toLong())
|
||||
}
|
||||
|
||||
private fun performBackAction(double: Boolean) {
|
||||
@@ -848,9 +849,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return Pair(width and 1.inv(), height and 1.inv())
|
||||
}
|
||||
|
||||
/**
|
||||
* The rate the panel runs the session at: its fastest mode at the current size, which PerfMode
|
||||
* asks for. The display's current rate is read too early for that - a phone still at 60 Hz
|
||||
* when the surface arrives handed gamescope -r 60 and capped every game at 60.
|
||||
*/
|
||||
private fun refreshHz(): Float {
|
||||
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
|
||||
val hz = display?.refreshRate ?: 60f
|
||||
val cur = display?.mode
|
||||
val best = if (cur == null) null else display.supportedModes
|
||||
.filter { it.physicalWidth == cur.physicalWidth && it.physicalHeight == cur.physicalHeight }
|
||||
.maxOfOrNull { it.refreshRate }
|
||||
val hz = maxOf(best ?: 0f, display?.refreshRate ?: 0f)
|
||||
return if (hz > 1f) hz else 60f
|
||||
}
|
||||
|
||||
@@ -1598,7 +1608,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
companion object {
|
||||
private const val TAG = "SessionActivity"
|
||||
private const val UNBUFFERED_SOURCES = InputDevice.SOURCE_CLASS_JOYSTICK or InputDevice.SOURCE_CLASS_TRACKBALL or InputDevice.SOURCE_CLASS_POSITION
|
||||
private const val BACK_DOUBLE_PRESS_TIMEOUT_MS = 500L
|
||||
private const val CURSOR_PAD_HOLD_MS = 1200L
|
||||
private const val DRAWER_HAT_THRESHOLD = 0.5f
|
||||
private const val DRAWER_STICK_ENTER_THRESHOLD = 0.55f
|
||||
|
||||
@@ -146,6 +146,8 @@ object DeviceReport {
|
||||
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
|
||||
k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" })
|
||||
k("Skip xalia", SessionPrefs.noXalia(context))
|
||||
k("gamescope realtime", SessionPrefs.gamescopeRealtime(context))
|
||||
k("Sustained performance mode", SessionPrefs.sustainedPerformance(context))
|
||||
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
|
||||
k("Guest host name", SessionPrefs.guestHostname(context))
|
||||
k("DirectAudio for games", SessionPrefs.directAudio(context))
|
||||
|
||||
@@ -27,8 +27,9 @@ object GameEnvironment {
|
||||
|
||||
fun defaults(preset: String): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
// The feature level is left to vkd3d-proton, which reports what the driver can do; forcing
|
||||
// 12_2 sent games down DX12 Ultimate paths Turnip only emulates. 6_6 is WinNative's model.
|
||||
"VKD3D_SHADER_MODEL" to "6_6",
|
||||
).apply {
|
||||
FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) }
|
||||
}
|
||||
|
||||
@@ -193,6 +193,8 @@ public final class LinuxRuntime {
|
||||
}
|
||||
}
|
||||
bindGpuNode(context, cmd);
|
||||
bindAdrenoStats(cmd);
|
||||
bindCpuTemps(cmd, root);
|
||||
if (extraBinds != null) {
|
||||
for (String spec : extraBinds) bind(cmd, spec);
|
||||
}
|
||||
@@ -244,6 +246,82 @@ public final class LinuxRuntime {
|
||||
bind(cmd, KGSL_DEVICE + ":/dev/dri/" + node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valve's mangoapp (Deck mode's performance overlay) reads an Adreno GPU's load, clock and
|
||||
* temperatures from where they are on Valve's own hardware; every Adreno under Android keeps
|
||||
* them in KGSL's sysfs, readable by the app, though which files a kernel has differs between
|
||||
* Snapdragon generations - so each value takes the first source that exists, the order other
|
||||
* Android PC emulators (GameNative, Winlator forks) read them in. A value found nowhere is left
|
||||
* alone.
|
||||
*/
|
||||
private static void bindAdrenoStats(List<String> cmd) {
|
||||
String kgsl = "/sys/class/kgsl/kgsl-3d0/";
|
||||
String gpuTemp = firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu"));
|
||||
String[][] stats = {
|
||||
{firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"), "/sys/kernel/debug/dri/0/perf_now"},
|
||||
{firstReadable(kgsl + "devfreq/cur_freq", kgsl + "gpuclk"),
|
||||
"/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"},
|
||||
{gpuTemp, "/sys/class/thermal/thermal_zone28/temp"},
|
||||
{gpuTemp, "/sys/class/thermal/thermal_zone26/temp"},
|
||||
{thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"},
|
||||
};
|
||||
for (String[] stat : stats) {
|
||||
if (stat[0] != null) bind(cmd, stat[0] + ":" + stat[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mangoapp's CPU temperature is the mean of the thermal zones named cpuN-thermal or
|
||||
* cpuN-top-thermal, as mainline kernels name them; Android kernels name the same sensors
|
||||
* cpuss-0, cpu-1-0, cpu0-silver-usr, apc1-cpu0-usr and the like, so those zones are shown
|
||||
* under the mainline name.
|
||||
*/
|
||||
private static void bindCpuTemps(List<String> cmd, File root) {
|
||||
File name = new File(root, "etc/bannerlator/cpu-thermal-type");
|
||||
try {
|
||||
if (!name.isFile()) Files.write(name.toPath(), "cpu0-thermal\n".getBytes(StandardCharsets.US_ASCII));
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
for (java.util.Map.Entry<String, File> zone : thermalZones().entrySet()) {
|
||||
String type = zone.getKey();
|
||||
if (type.contains("cpu") && !type.contains("gpu") && !type.matches("cpu\\d-(top-)?thermal")) {
|
||||
bind(cmd, name.getPath() + ":" + new File(zone.getValue(), "type").getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstReadable(String... paths) {
|
||||
for (String path : paths) {
|
||||
if (path != null && new File(path).canRead()) return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The temp file of the thermal zone for a sensor: the one named so, else the first whose name has it. */
|
||||
private static String thermalZone(String sensor) {
|
||||
java.util.Map<String, File> zones = thermalZones();
|
||||
File zone = zones.get(sensor);
|
||||
for (java.util.Map.Entry<String, File> e : new java.util.TreeMap<>(zones).entrySet()) {
|
||||
if (zone == null && e.getKey().contains(sensor)) zone = e.getValue();
|
||||
}
|
||||
return zone != null ? new File(zone, "temp").getPath() : null;
|
||||
}
|
||||
|
||||
/** The device's thermal zones by their sensor's name, in lower case. */
|
||||
private static java.util.Map<String, File> thermalZones() {
|
||||
java.util.Map<String, File> byType = new java.util.HashMap<>();
|
||||
File[] zones = new File("/sys/class/thermal").listFiles((dir, n) -> n.startsWith("thermal_zone"));
|
||||
if (zones == null) return byType;
|
||||
for (File zone : zones) {
|
||||
try (java.io.BufferedReader r = new java.io.BufferedReader(new java.io.FileReader(new File(zone, "type")))) {
|
||||
byType.putIfAbsent(String.valueOf(r.readLine()).trim().toLowerCase(java.util.Locale.ROOT), zone);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
return byType;
|
||||
}
|
||||
|
||||
private static void bind(List<String> cmd, String spec) {
|
||||
cmd.add("-b");
|
||||
cmd.add(spec);
|
||||
|
||||
@@ -13,8 +13,8 @@ import android.util.Log
|
||||
* The manifest already makes the app a game (appCategory + game_mode_config); this is the
|
||||
* per-window half, applied when the session activity is created:
|
||||
*
|
||||
* - sustained performance mode (Android 7+): a clock floor that does not throttle away after
|
||||
* two minutes, which suits an hour in Big Picture better than a boost that fades;
|
||||
* - sustained performance mode (Android 7+), only when the Performance page asks for it: clocks
|
||||
* the SoC can hold for hours, which on many devices caps the peak a session otherwise gets;
|
||||
* - the panel's fastest mode at its current size (Android 6+): a 120 Hz phone is switched to
|
||||
* 120 Hz for the menu, not only when a game votes for it;
|
||||
* - GameManager's game state (Android 13+): "in gameplay", so an OEM framework that releases
|
||||
@@ -30,7 +30,7 @@ object PerfMode {
|
||||
val parts = ArrayList<String>(3)
|
||||
|
||||
// Sustained performance mode.
|
||||
parts += if (Build.VERSION.SDK_INT >= 24) {
|
||||
parts += if (!SessionPrefs.sustainedPerformance(a)) "sustained mode off" else if (Build.VERSION.SDK_INT >= 24) {
|
||||
val pm = a.getSystemService(PowerManager::class.java)
|
||||
if (pm?.isSustainedPerformanceModeSupported == true) {
|
||||
try { a.window.setSustainedPerformanceMode(true); "sustained mode on" } catch (t: Throwable) { Log.w(TAG, "sustained mode", t); "sustained mode refused" }
|
||||
|
||||
@@ -143,8 +143,8 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("directAudio", on).apply()
|
||||
}
|
||||
|
||||
/** The microphone for voice chat, on unless turned off; used only once RECORD_AUDIO is granted. */
|
||||
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", true)
|
||||
/** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */
|
||||
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false)
|
||||
|
||||
/** Whether the app has already asked for the microphone once at start-up. */
|
||||
fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false)
|
||||
@@ -188,14 +188,39 @@ object SessionPrefs {
|
||||
* xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation.
|
||||
* Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls
|
||||
* reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured -
|
||||
* it storms, and the session dies seconds after Big Picture appears.
|
||||
* it storms, and the session dies seconds after Big Picture appears. On by default: where it
|
||||
* does run, it sits beside every game under FEX for nothing a controller-first session needs
|
||||
* (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2).
|
||||
*/
|
||||
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", false)
|
||||
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true)
|
||||
|
||||
fun setNoXalia(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("noXalia", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1,
|
||||
* which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in
|
||||
* Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device
|
||||
* whose GPU is waiting on the CPU.
|
||||
*/
|
||||
/**
|
||||
* Whether the session window asks for Android's sustained performance mode. Off by default: it
|
||||
* holds the SoC at clocks it can keep for hours, which on many devices is a cap well under
|
||||
* the peak a game session gets without it (WinNative never asks for it).
|
||||
*/
|
||||
fun sustainedPerformance(context: Context): Boolean = prefs(context).getBoolean("sustainedPerformance", false)
|
||||
|
||||
fun setSustainedPerformance(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("sustainedPerformance", on).apply()
|
||||
}
|
||||
|
||||
fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false)
|
||||
|
||||
fun setGamescopeRealtime(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("gamescopeRealtime", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1).
|
||||
*
|
||||
@@ -248,7 +273,7 @@ object SessionPrefs {
|
||||
fun noGlError(context: Context): Boolean = prefs(context).getBoolean("noGlError", true)
|
||||
fun setNoGlError(context: Context, on: Boolean) { prefs(context).edit().putBoolean("noGlError", on).apply() }
|
||||
|
||||
/** `steam -steamdeck -steamos3`: the client as SteamOS runs it. Expects Deck hardware; untested. */
|
||||
/** Runs the SteamOS gamepad client with its Quick Access performance controls. */
|
||||
fun steamDeckMode(context: Context): Boolean = prefs(context).getBoolean("steamDeckMode", false)
|
||||
fun setSteamDeckMode(context: Context, on: Boolean) { prefs(context).edit().putBoolean("steamDeckMode", on).apply() }
|
||||
|
||||
@@ -309,8 +334,12 @@ object SessionPrefs {
|
||||
fun resolutionChosen(context: Context, mode: String): Boolean =
|
||||
prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null
|
||||
|
||||
/** The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults. */
|
||||
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "") ?: ""
|
||||
/**
|
||||
* The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's own
|
||||
* defaults. PERFORMANCE_TSO unless chosen: TSO only on the main path, x87 reduced precision
|
||||
* and multiblock, as WinNative ships it.
|
||||
*/
|
||||
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "PERFORMANCE_TSO") ?: "PERFORMANCE_TSO"
|
||||
|
||||
fun setFexPreset(context: Context, id: String) {
|
||||
prefs(context).edit().putString("fexPreset", id).apply()
|
||||
|
||||
@@ -240,10 +240,12 @@ class SessionService : Service() {
|
||||
val override = File(Environment.getExternalStorageDirectory(), TU_DEBUG_SWITCH)
|
||||
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim() }
|
||||
if (!override.isNullOrEmpty()) return override
|
||||
if (SessionPrefs.tuSysmem(this)) return "sysmem"
|
||||
if (linuxDriverId.isEmpty()) return null
|
||||
val name = LinuxVulkanDriverManager(this).getDriverName(linuxDriverId).lowercase()
|
||||
return if (name.contains("710-720") || name.contains("710_720")) "sysmem" else null
|
||||
// noconform: Turnip keeps the Vulkan features it has not passed conformance for (WinNative's
|
||||
// default beside sysmem), which DXVK and vkd3d-proton reach for.
|
||||
val sysmem = SessionPrefs.tuSysmem(this) || linuxDriverId.isNotEmpty() &&
|
||||
LinuxVulkanDriverManager(this).getDriverName(linuxDriverId).lowercase()
|
||||
.let { it.contains("710-720") || it.contains("710_720") }
|
||||
return if (sysmem) "noconform,sysmem" else "noconform"
|
||||
}
|
||||
|
||||
// ── The session ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -519,6 +521,8 @@ class SessionService : Service() {
|
||||
// BL_STEAMDECK; it is the one that builds the command line).
|
||||
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
|
||||
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
|
||||
// Zink packs its descriptor sets and shader interfaces tighter (WinNative's default).
|
||||
guest.add("ZINK_DEBUG=compact")
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
|
||||
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
|
||||
if (SessionState.mode == MODE_STEAM) {
|
||||
@@ -526,9 +530,12 @@ class SessionService : Service() {
|
||||
SessionPrefs.writeForceFullscreenFlag(this)
|
||||
}
|
||||
// Proton's own gate for its xalia helper (its `proton` script reads this, and sets
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Off by default: xalia is Valve's, and on a device
|
||||
// whose seccomp answers its syscalls normally there is no reason to take it away.
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game
|
||||
// a slice of a core for gamepad navigation the session already has.
|
||||
if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0")
|
||||
// gamescope's realtime Vulkan queues (the session script turns this into
|
||||
// GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on.
|
||||
guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0"))
|
||||
// Anything else, for a device that cannot be reached with a debugger: Downloads/droiddeck-env
|
||||
// holds KEY=VALUE lines that go into the session's environment as written, after ours, so a
|
||||
// line here wins. Zink and Turnip tunables (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's,
|
||||
|
||||
@@ -68,6 +68,8 @@ class ModeSettings(
|
||||
val forceFullscreen: Boolean? = null,
|
||||
/** Steam only: the client branch forced on the command line. */
|
||||
val steamChannel: String? = null,
|
||||
/** Steam only: enable the SteamOS client interface and its performance controls. */
|
||||
val steamDeckMode: Boolean = false,
|
||||
/** Steam only: start a Steam session when DroidDeck opens. */
|
||||
val runSteamAtStartup: Boolean = false,
|
||||
/** Steam only: the user's chosen Games folders; null outside Steam. */
|
||||
@@ -123,6 +125,7 @@ class ModeSettingsActions(
|
||||
val onFexPreset: (String) -> Unit = {},
|
||||
val onForceFullscreen: (Boolean) -> Unit = {},
|
||||
val onSteamChannel: (String) -> Unit = {},
|
||||
val onSteamDeckMode: (Boolean) -> Unit = {},
|
||||
val onRunSteamAtStartup: (Boolean) -> Unit = {},
|
||||
val onPickAddedGamesDir: () -> Unit = {},
|
||||
val onForgetAddedGamesDir: (path: String) -> Unit = {},
|
||||
@@ -338,6 +341,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
}
|
||||
if (steam && s.steamChannel != null) SettingsGroup("Client") {
|
||||
ToggleRow(
|
||||
host, "steamdeck", "Steam Deck mode",
|
||||
"Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.",
|
||||
s.steamDeckMode, onChange = a.onSteamDeckMode,
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "channel", "Client branch", "The Steam client build the session forces. Applies at the next session start; the client may update itself once.",
|
||||
listOf("publicbeta" to "Public beta", "steamdeck_publicbeta" to "Steam Deck public beta"), s.steamChannel,
|
||||
|
||||
@@ -36,8 +36,9 @@ fun PerformancePage(
|
||||
zinkLazy: Boolean,
|
||||
glThread: Boolean,
|
||||
noGlError: Boolean,
|
||||
steamDeckMode: Boolean,
|
||||
noXalia: Boolean,
|
||||
gamescopeRealtime: Boolean,
|
||||
sustainedPerformance: Boolean,
|
||||
prootNoSeccomp: Boolean,
|
||||
guestHostname: String,
|
||||
phantomWarning: String?,
|
||||
@@ -46,8 +47,9 @@ fun PerformancePage(
|
||||
onZinkLazy: (Boolean) -> Unit,
|
||||
onGlThread: (Boolean) -> Unit,
|
||||
onNoGlError: (Boolean) -> Unit,
|
||||
onSteamDeckMode: (Boolean) -> Unit,
|
||||
onNoXalia: (Boolean) -> Unit,
|
||||
onGamescopeRealtime: (Boolean) -> Unit,
|
||||
onSustainedPerformance: (Boolean) -> Unit,
|
||||
onProotNoSeccomp: (Boolean) -> Unit,
|
||||
onGuestHostname: (String) -> Unit,
|
||||
onClientCore: (Int, Boolean) -> Unit,
|
||||
@@ -97,9 +99,9 @@ fun PerformancePage(
|
||||
noGlError, onChange = onNoGlError,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "deck", "Steam Deck mode",
|
||||
"Runs the client with -droiddeck. SteamOS helpers and battery info are provided.",
|
||||
steamDeckMode, onChange = onSteamDeckMode,
|
||||
host, "gsrealtime", "gamescope: realtime GPU queue",
|
||||
"Gives the compositor's GPU work priority over the game's. Can smooth frame pacing, but can cost games GPU time.",
|
||||
gamescopeRealtime, onChange = onGamescopeRealtime,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Session fixes") {
|
||||
@@ -110,9 +112,14 @@ fun PerformancePage(
|
||||
)
|
||||
ToggleRow(
|
||||
host, "xalia", "Skip Steam's xalia helper",
|
||||
"Disables Proton's gamepad navigation helper. Try if sessions crash at startup.",
|
||||
"Disables Proton's gamepad navigation helper, which costs every game CPU time. Turn off only if a game needs it.",
|
||||
noXalia, onChange = onNoXalia,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "sustained", "Android sustained performance mode",
|
||||
"Holds clocks the device can keep for hours. Steadier on hot devices, but usually a lower peak.",
|
||||
sustainedPerformance, onChange = onSustainedPerformance,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "seccomp", "Run proot without seccomp",
|
||||
"May fix missing syscall errors on some kernels, but can reduce performance.",
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# DroidDeck - Performance findings
|
||||
|
||||
Why DroidDeck 0.2.0 ran slower than WinNative and Bannerlator for many people, what was found, what was
|
||||
changed, what was measured, and what is still open. Written 2026-09-29 from the Discord reports, the GitHub
|
||||
issues, and a day of measurement on an AYN Thor (SD 8 Gen 2, Adreno 740). Companion to
|
||||
[progress-log.md](progress-log.md).
|
||||
|
||||
## What people reported
|
||||
|
||||
- **Discord (#general, 2026-09-26 to 09-29).** The same game at ~90-100 fps in WinNative and ~30-40 in
|
||||
DroidDeck 0.1.7, ~25 in 0.2.0 (Adreno 732); "15 fps in a 2D Unity game" on an 8 Elite while Bannerlator's
|
||||
Steam client is fine; "20 fps / 7 fps in the Steam client, all renders on CPU" on an Adreno 8xx; Bannerlator
|
||||
locked at 120 fps where DroidDeck is not; "less performant, more compatible" as the usual summary.
|
||||
- **[#65](https://github.com/Droid-Deck/DroidDeck/issues/65)** - the best report: Silksong at 85-90 fps on
|
||||
0.1.5 and 37-50 on 0.2.0 on a Fold8, GPU 82% busy vs 16%, proot 3-5x busier, the game's main thread waiting
|
||||
on `futex` and the wineserver pipe. History table: 90 (0.1.5), 103 (0.1.6), 58/110/48 (0.1.7), 47-50 (0.2.0).
|
||||
- **[#64](https://github.com/Droid-Deck/DroidDeck/issues/64)** - Samsung's SSRM clamps CPU/GPU to ~60% in a
|
||||
session (Tab S8 Ultra). A platform limit, separate from everything below; no sanctioned way around it found.
|
||||
|
||||
## Cause 1: proot's kompat extension traced every hot syscall (fixed, PR #81)
|
||||
|
||||
`--kernel-release` (added 2026-09-25 to give the guest the `DroidDeck` hostname, `53ce764`) loads proot's
|
||||
kompat extension. Its filter list includes `futex`, `epoll_pwait`, `fcntl`, `pselect6`, `pipe2`, `eventfd2`,
|
||||
`socket`, so each of those from every guest thread stopped in the single tracer, and it drops
|
||||
`AT_SYSINFO_EHDR` on every `execve`, so glibc ran without the vDSO. With the real kernel release passed, every
|
||||
one of those handlers is a no-op. Patch `tools/proot/patches/0011-kompat-utsname-only.patch` traces only
|
||||
`uname`/`sethostname`/`setdomainname` in that case.
|
||||
|
||||
Guest micro-benchmarks (same rootfs, proot with vs without the patch):
|
||||
|
||||
| | old + `--kernel-release` | patched | no flag at all |
|
||||
|---|---|---|---|
|
||||
| futex ping-pong | 465 us | 102 us | 96 us |
|
||||
| `epoll_pwait(0)` | 60 us | 0.87 us | 0.8 us |
|
||||
| `fcntl(F_GETFL)` | 40-107 us | 0.39 us | 0.38 us |
|
||||
| `clock_gettime` | 0.34 us | 0.19 us | 0.11-0.19 us |
|
||||
|
||||
A-B-A in Once Upon a KATAMARI (standing still, only `libproot.so` swapped): +2-5% fps, proot CPU ~56% ->
|
||||
~35%. Small there because Katamari is limited by translated game code; sync-heavy games should gain more.
|
||||
|
||||
## Cause 2: a crash-looping mangoapp in Deck mode (fixed, PR #81)
|
||||
|
||||
`gamescopereaper --respawn -- mangoapp` restarts the overlay the moment it exits, with no backoff. On a build
|
||||
whose session preload lacked the System V message queue emulation, mangoapp died right after creating its
|
||||
swapchain and was restarted 132 times in ~2.5 minutes; each restart is a full exec under proot and the Steam
|
||||
menu itself lagged. Local builds that skip rebuilding `libblsession.so` hit this; CI builds do not. The wrapper
|
||||
(`tools/mangoapp/mangoapp`) now parks after more than 5 starts in 60 s.
|
||||
|
||||
## What Bannerlator and WinNative do differently
|
||||
|
||||
Read from their sources and a device that has Bannerlator installed:
|
||||
|
||||
- **Same proot patches** (0001-0009 from WinNative); the Steam client mode uses the same Valve ARM64 Proton, so
|
||||
**the same server-side Wine synchronisation** (`wineserver: using server-side synchronization` in every
|
||||
session log here). No structural gap remains once kompat is out.
|
||||
- **Defaults differ.** Bannerlator ships Deck mode off (it notes Steam Input takes the pad and breaks games),
|
||||
never passes `-steamos3`, does not set gamescope's realtime queue, runs xalia. WinNative defaults to
|
||||
`TU_DEBUG=noconform,sysmem`, `ZINK_DEBUG=compact`, FEX `PERFORMANCE_TSO`, vkd3d shader model 6_6, and never
|
||||
turns on sustained performance mode.
|
||||
|
||||
## Changes on `perf/steam-client-parity`
|
||||
|
||||
- xalia skipped by default (it ran ~10% of a core beside every game under FEX).
|
||||
- gamescope realtime Vulkan queues: new toggle (Performance -> Client interface), **off** by default.
|
||||
- Microphone off by default; no permission prompt at start-up.
|
||||
- FEX default preset `PERFORMANCE_TSO`; `TU_DEBUG=noconform` always (plus `sysmem` for the 710-720 drivers or
|
||||
when chosen); `ZINK_DEBUG=compact`; vkd3d shader model 6_6, feature level left to vkd3d-proton.
|
||||
- Sustained performance mode is opt-in (Performance page); PerfMode logs "sustained mode off".
|
||||
- The session gets the panel's fastest mode's refresh rate instead of the rate at the moment the surface arrived.
|
||||
- Session polling without forks: the fill-screen and stop watchers wait on a FIFO read timeout and read files
|
||||
with builtins; the redistributable seeder and the compat registrar (Python) start only when a prefix or the
|
||||
Steam config actually changed, not every 5-15 s.
|
||||
- The Steam page shows the saved Deck mode before the Performance page has been opened (it showed "off" while
|
||||
the session ran Deck mode with mangoapp).
|
||||
- `tools/build_local.sh`: caches its pinned downloads, skips an unchanged proot build, and says why it rebuilds
|
||||
the Docker image; a warm build is ~30 s (it used to hang on a registry fetch when the daemon briefly reported
|
||||
the image missing).
|
||||
- Turnip **sysmem** was made a default and then reverted: it gained nothing on an Adreno 740.
|
||||
|
||||
## How it was measured
|
||||
|
||||
- **fps:** `BannerWayland [stats]` lines in logcat (`adb logcat -s BannerWayland:I`), every 10 s. No `su`, no
|
||||
process on the device. The "screen" number counts presents and runs ~1.5x the game's own overlay counter in
|
||||
Alan Wake (60 vs 38-40): compare like with like, and read the overlay for the game's real rate.
|
||||
- **Scene:** Alan Wake's American Nightmare, first gameplay spot ("Secure the primary pipes"), standing still.
|
||||
Reached without touching the screen: `~/.bl-autolaunch` is ignored once Steam is up, so the game is started with
|
||||
`steam steam://rungameid/<id>` from a second proot invocation (the client forwards it), and A is pressed with
|
||||
`sendevent` on the device's own gamepad node (`/dev/input/event12`) - the app only forwards events from a real
|
||||
controller device, so `adb shell input` presses are dropped.
|
||||
- **Thermals:** the SoC reaches ~92-95 C in any run, so each run starts only after the CPU zones are under 55 C.
|
||||
A one-shot comparison made hot was misleading (the new build read 52-54 fps then, 58-61 cooled).
|
||||
|
||||
Alan Wake, cooled starts, fan at max, 60 s each (screen counter, six 10 s samples):
|
||||
|
||||
| Build | screen fps | game overlay | proot CPU | wineserver CPU |
|
||||
|---|---|---|---|---|
|
||||
| `main` (kompat fix, old defaults) | 56.9-58.6 (avg 57.7) | 39 | ~39% | ~30% |
|
||||
| new defaults | 58.6-61.5 (avg 59.6) | 38-39 | ~27% | ~34% |
|
||||
| new defaults, sysmem off | 59.3-62.9 (avg 61.1) | 38 | - | - |
|
||||
|
||||
Read: the defaults are worth at most a few percent here and inside the run-to-run noise (about +-1.5 fps). The
|
||||
game's own frame rate stayed at 38-40 with CPU ~65% and GPU ~56-60%, so this scene is bound by something none
|
||||
of these settings touch.
|
||||
|
||||
## Open leads, biggest first
|
||||
|
||||
1. **Wine synchronisation.** Proton 11 dropped esync; fsync needs `futex_waitv`, which Android's app sandbox
|
||||
blocks even on a 6.12 kernel; ntsync is not in Android kernels. Every lock and event is a pipe round trip to
|
||||
wineserver (30-34% CPU in Alan Wake, 18% of the main thread blocked in `read()` in #65). Winlator-family apps
|
||||
run esync, which needs only `eventfd` and `poll`. Restoring esync in the ARM64 Proton is the largest
|
||||
likely win and the largest piece of work; emulating `futex_waitv` in proot puts every wait back through the
|
||||
tracer and is probably a dead end.
|
||||
2. **The game's main thread under FEX.** Alan Wake's process uses 139-178% CPU with neither the CPU nor the GPU
|
||||
saturated overall. FEX settings beyond the presets (SMC checks, multiblock, TSO variants) are worth a sweep
|
||||
on a game whose main thread is the limit.
|
||||
3. **#65's other 0.1.5 -> 0.2.0 differences, untested:** the fake `power_supply` tree, `BL_HDR`/`DXVK_HDR`, the
|
||||
output width (1280 -> the panel's shape), `FEX_TSOENABLED` (1 -> 0), the tracer binary. Their Silksong setup
|
||||
is sync-heavy and is the best test of the kompat fix.
|
||||
4. **gamescope** calls `XQueryPointer` on every vblank in the Steam UI (patch `0100`); query only when a
|
||||
gamepad cursor move is pending. Needs a new pinned gamescope release.
|
||||
5. **Heavy games on a fresh install.** Metal Gear Solid V: Ground Zeroes sat on Steam's launch splash for 5+ minutes
|
||||
with wineserver at ~50% CPU and the GPU at 12% (no window ever appeared); Bulletstorm took ~4.5 minutes to reach
|
||||
its first logo, the game process at 45-105% CPU and the GPU at 9%, and the session ended a minute later. Neither
|
||||
reached a scene worth measuring. Both are first launches (redistributable install scripts, shader work), and a
|
||||
wineserver spinning at 50% while a game shows nothing is itself the server-side synchronisation cost of lead 1.
|
||||
6. **Reports not yet reproduced:** software rendering on Adreno 8xx ("all renders on CPU"), Deck mode using
|
||||
most of the RAM, Samsung SSRM clamping (#64), two DroidDeck packages both holding a session.
|
||||
7. **Smaller:** proot tracer pinned to a big core (Max's note: no core wake-up lag), `steamrtarm64` on Proton's
|
||||
`LD_LIBRARY_PATH`, the always-on wake and Wi-Fi locks, ADPF hints fed frame intervals instead of work time.
|
||||
@@ -4,6 +4,7 @@ Running engineering log for the DroidDeck app (`com.droiddeck.launcher`; called
|
||||
`com.steamdeck.launcher`, until 2026-09-23 - entries below that date keep the old name). Newest state first, then
|
||||
the timeline, then lessons and backlog. Companion to the README (what the app *does*) and to
|
||||
`docs/releases/` (what each version said) - this is *how it got here and where it stands*.
|
||||
Performance work has its own write-up: [performance.md](performance.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+57
-16
@@ -89,7 +89,17 @@ linuxfs_replaced=1
|
||||
rm -rf -- "${linuxfs_dir}"
|
||||
mkdir -p "${linuxfs_dir}"
|
||||
|
||||
if ! docker image inspect "${image_name}" >/dev/null 2>&1; then
|
||||
# Docker Desktop's VM restarts now and then, and until its engine has loaded its image store it
|
||||
# answers "No such image" for images it has. The rebuild that follows hangs on the registry
|
||||
# (the base image's credentials go through docker-credential-desktop), so give a restarting daemon
|
||||
# up to a minute before deciding the image is really missing.
|
||||
image_present=0
|
||||
for _ in $(seq 1 30); do
|
||||
if inspect_error=$(docker image inspect "${image_name}" 2>&1 >/dev/null); then image_present=1; break; fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "${image_present}" = 0 ]]; then
|
||||
echo "Build image ${image_name} not found (${inspect_error:-no error}); building it." >&2
|
||||
docker build --platform linux/amd64 -t "${image_name}" \
|
||||
-f "${repo_root}/tools/local-cross.Dockerfile" "${repo_root}"
|
||||
fi
|
||||
@@ -164,20 +174,39 @@ if [[ -z "${github_repo}" ]]; then
|
||||
github_repo=${github_repo%.git}
|
||||
fi
|
||||
|
||||
# Pinned downloads are kept between builds, named by their checksum, so a rebuild fetches nothing
|
||||
# it already has. A cached file is checked again before use; a bad one is fetched anew.
|
||||
cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"}
|
||||
mkdir -p "${cache_dir}"
|
||||
# cached <sha256> <name> <command...>: prints the cached path, running the command (which writes
|
||||
# to "$out") only when the cache has no file with that checksum.
|
||||
cached() {
|
||||
local sha=$1 name=$2 out
|
||||
shift 2
|
||||
out="${cache_dir}/${sha}-${name}"
|
||||
if [[ -f "${out}" ]] && printf '%s %s\n' "${sha}" "${out}" | shasum -a 256 -c - >/dev/null 2>&1; then
|
||||
echo "${out}"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${out}.part"
|
||||
out="${out}.part" "$@" >&2
|
||||
printf '%s %s\n' "${sha}" "${out}.part" | shasum -a 256 -c - >&2
|
||||
mv "${out}.part" "${out}"
|
||||
echo "${out}"
|
||||
}
|
||||
|
||||
if [[ -f "${repo_root}/tools/gamescope/release.env" ]]; then
|
||||
. "${repo_root}/tools/gamescope/release.env"
|
||||
gamescope_archive="${staging_dir}/gamescope.tzst"
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${github_repo}" -p gamescope.tzst -O "${gamescope_archive}"
|
||||
printf '%s %s\n' "${GAMESCOPE_SHA256}" "${gamescope_archive}" | shasum -a 256 -c -
|
||||
gamescope_archive=$(cached "${GAMESCOPE_SHA256}" gamescope.tzst \
|
||||
bash -c 'gh release download "$0" -R "$1" -p gamescope.tzst -O "$out"' "${GAMESCOPE_TAG}" "${github_repo}")
|
||||
zstd -dc "${gamescope_archive}" | tar -xf - -C "${linuxfs_dir}"
|
||||
test -f "${linuxfs_dir}/usr/local/bin/gamescope"
|
||||
fi
|
||||
|
||||
if [[ -f "${repo_root}/tools/wlroots/release.env" ]]; then
|
||||
. "${repo_root}/tools/wlroots/release.env"
|
||||
wlroots_archive="${staging_dir}/wlroots.tzst"
|
||||
gh release download "${WLROOTS_TAG}" -R "${github_repo}" -p wlroots.tzst -O "${wlroots_archive}"
|
||||
printf '%s %s\n' "${WLROOTS_SHA256}" "${wlroots_archive}" | shasum -a 256 -c -
|
||||
wlroots_archive=$(cached "${WLROOTS_SHA256}" wlroots.tzst \
|
||||
bash -c 'gh release download "$0" -R "$1" -p wlroots.tzst -O "$out"' "${WLROOTS_TAG}" "${github_repo}")
|
||||
zstd -dc "${wlroots_archive}" | tar -xf - -C "${linuxfs_dir}"
|
||||
test -f "${linuxfs_dir}/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
|
||||
fi
|
||||
@@ -187,9 +216,8 @@ mango_pkgs="${staging_dir}/mango-pkgs"
|
||||
mkdir -p "${mango_dir}" "${mango_pkgs}"
|
||||
while read -r package_sha256 package_url; do
|
||||
[[ -n "${package_url}" ]] || continue
|
||||
package_archive="${staging_dir}/$(basename "${package_url}")"
|
||||
curl -fsSL --retry 3 -o "${package_archive}" "${package_url}"
|
||||
printf '%s %s\n' "${package_sha256}" "${package_archive}" | shasum -a 256 -c -
|
||||
package_archive=$(cached "${package_sha256}" "$(basename "${package_url}")" \
|
||||
bash -c 'curl -fsSL --retry 3 -o "$out" "$0"' "${package_url}")
|
||||
zstd -dc "${package_archive}" | tar -xf - -C "${mango_pkgs}"
|
||||
done < <(grep -v '^#' "${repo_root}/tools/mangoapp/packages.txt")
|
||||
install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp"
|
||||
@@ -201,11 +229,14 @@ install -m644 "${repo_root}/tools/mangoapp/mangoapp" "${linuxfs_dir}/usr/local/b
|
||||
|
||||
pa_source=${DROIDDECK_PA13_SOURCE_DIR:-"${staging_dir}/pulseaudio-13.0"}
|
||||
if [[ -z "${DROIDDECK_PA13_SOURCE_DIR:-}" ]]; then
|
||||
curl -fsSL -o "${staging_dir}/pulseaudio-13.0.tar.gz" \
|
||||
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
||||
pa_tarball="${cache_dir}/pulseaudio-13.0.tar.gz"
|
||||
if [[ ! -s "${pa_tarball}" ]] || ! tar -tzf "${pa_tarball}" >/dev/null 2>&1; then
|
||||
curl -fsSL -o "${pa_tarball}.part" \
|
||||
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
||||
mv "${pa_tarball}.part" "${pa_tarball}"
|
||||
fi
|
||||
mkdir -p "${pa_source}"
|
||||
tar -xzf "${staging_dir}/pulseaudio-13.0.tar.gz" \
|
||||
-C "${pa_source}" --strip-components=1
|
||||
tar -xzf "${pa_tarball}" -C "${pa_source}" --strip-components=1
|
||||
fi
|
||||
if [[ ! -f "${pa_source}/src/pulse/version.h.in" ]]; then
|
||||
echo "PulseAudio 13.0 source not found at ${pa_source}; set DROIDDECK_PA13_SOURCE_DIR." >&2
|
||||
@@ -214,7 +245,17 @@ fi
|
||||
|
||||
sink_output="${staging_dir}/sink-out"
|
||||
"${repo_root}/tools/aaudio-sink/build.sh" "${pa_source}" "${sink_output}"
|
||||
"${repo_root}/tools/proot/build.sh" "${repo_root}/app/src/main/jniLibs/arm64-v8a"
|
||||
# proot is rebuilt only when its sources (source.env, the patches, the build script) changed since
|
||||
# the libraries in jniLibs were built.
|
||||
proot_out="${repo_root}/app/src/main/jniLibs/arm64-v8a"
|
||||
proot_inputs=$(cd "${repo_root}/tools/proot" && find . -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
|
||||
if [[ -f "${proot_out}/libproot.so" && -f "${proot_out}/libproot-loader.so" \
|
||||
&& "$(cat "${proot_out}/.proot-inputs" 2>/dev/null)" = "${proot_inputs}" ]]; then
|
||||
echo "proot: sources unchanged, keeping ${proot_out}/libproot.so"
|
||||
else
|
||||
"${repo_root}/tools/proot/build.sh" "${proot_out}"
|
||||
echo "${proot_inputs}" > "${proot_out}/.proot-inputs"
|
||||
fi
|
||||
|
||||
cp -p "${bundle_asset}" "${bundle_backup}"
|
||||
bundle_dir="${staging_dir}/pulseaudio-bundle"
|
||||
@@ -229,7 +270,7 @@ install -m755 "${sink_output}/module-aaudio-sink.so" \
|
||||
"${bundle_dir}/modules/arm64/module-aaudio-sink.so"
|
||||
install -m755 "${sink_output}/module-directaudio-sink.so" \
|
||||
"${bundle_dir}/modules/arm64/module-directaudio-sink.so"
|
||||
tar -cf - -C "${bundle_dir}" . | zstd -19 -c > "${staging_dir}/pulseaudio.tzst"
|
||||
tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tzst"
|
||||
bundle_replaced=1
|
||||
mv "${staging_dir}/pulseaudio.tzst" "${bundle_asset}"
|
||||
|
||||
|
||||
@@ -75,8 +75,9 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
|
||||
fi
|
||||
# The app's gamescope build (tools/gamescope) takes realtime-priority Vulkan queues on this
|
||||
# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority
|
||||
# where the driver refuses, and the runtime's own build ignores it.
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-1}
|
||||
# where the driver refuses, and the runtime's own build ignores it. Off unless the app's
|
||||
# Performance page turns it on (BL_GAMESCOPE_REALTIME=1).
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
|
||||
export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
|
||||
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
|
||||
@@ -371,11 +372,18 @@ case $mode in
|
||||
# ~/.droiddeck-fill, and each change goes to gamescope as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on
|
||||
# the root window, which it watches. What gamescope was started with is where this begins.
|
||||
# Not on the desktop (BL_DESKTOP): no gamescope there, and the loop would outlive the client.
|
||||
# The loops below poll for the whole session, and every sleep, cat or script they start is a
|
||||
# fork and exec under proot. They wait with a read timeout on a FIFO nothing writes to and
|
||||
# read files with builtins, so an idle beat costs no process at all.
|
||||
_bl_fifo=$(mktemp -u "${TMPDIR:-/tmp}/bl-nap.XXXXXX") && mkfifo "$_bl_fifo" \
|
||||
&& exec {_bl_nap}<>"$_bl_fifo" && rm -f "$_bl_fifo"
|
||||
nap() { if [ -n "${_bl_nap:-}" ]; then read -r -t "$1" -u "$_bl_nap" _ || true; else sleep "$1"; fi; }
|
||||
[ -z "${BL_DESKTOP:-}" ] && (
|
||||
last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}
|
||||
while :; do
|
||||
sleep 1
|
||||
want=$(cat "$HOME/.droiddeck-fill" 2>/dev/null) || continue
|
||||
nap 1
|
||||
want=
|
||||
read -r want <"$HOME/.droiddeck-fill" 2>/dev/null || [ -n "$want" ] || continue
|
||||
case $want in 0|1) ;; *) continue ;; esac
|
||||
[ "$want" = "$last" ] && continue
|
||||
if xprop -root -f GAMESCOPE_FORCE_WINDOWS_FULLSCREEN 32c -set GAMESCOPE_FORCE_WINDOWS_FULLSCREEN "$want" 2>/dev/null; then
|
||||
@@ -384,14 +392,37 @@ case $mode in
|
||||
fi
|
||||
done
|
||||
) &
|
||||
# Each beat only starts a script when there is something for it: the seeder when a prefix has
|
||||
# a registry it has not marked, the registrar (Python, the heavier start) when a library, the
|
||||
# tools directory or the client's config changed since its last run.
|
||||
(
|
||||
compat_stamp=$HOME/.cache/droiddeck-compat-stamp
|
||||
needs_seed() {
|
||||
local r
|
||||
for r in "$steam_root"/steamapps/compatdata/*/pfx/system.reg \
|
||||
/mnt/bannerlator-sd/steamapps/compatdata/*/pfx/system.reg; do
|
||||
[ -f "$r" ] && [ ! -e "${r%/system.reg}/.bannerlator-redists" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
needs_compat() {
|
||||
local d
|
||||
[ -e "$compat_stamp" ] || return 0
|
||||
for d in "$steam_root/steamapps" "$steam_root/compatibilitytools.d" "$steam_root/config" \
|
||||
/mnt/bannerlator-sd/steamapps; do
|
||||
[[ -e $d && $d -nt $compat_stamp ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
beat=0
|
||||
for _ in $(seq 1 720); do
|
||||
sleep 5
|
||||
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
|
||||
while [ "$beat" -lt 720 ]; do
|
||||
nap 5
|
||||
needs_seed && { bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true; }
|
||||
beat=$((beat + 1))
|
||||
if [ "$((beat % 3))" -eq 0 ]; then
|
||||
if [ "$((beat % 3))" -eq 0 ] && needs_compat; then
|
||||
: >"$compat_stamp"
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
: >"$compat_stamp"
|
||||
fi
|
||||
done
|
||||
) &
|
||||
@@ -501,20 +532,19 @@ BUSCONF
|
||||
# desktop's launchers ask for; gamescope shows one window at a time and the game takes over.
|
||||
_bl_ui=(-gamepadui)
|
||||
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
|
||||
# Deck mode (BL_STEAMDECK=1, the Performance page): -steamdeck is the flag that puts the Quick
|
||||
# Access Menu, the performance overlay and the Deck settings pages on screen. Only -steamdeck,
|
||||
# never -steamos3: that one makes the client manage itself as SteamOS does - it calls
|
||||
# steamos-select-branch and opts the install into the steamdeck_stable client branch, writes
|
||||
# that to package/beta, and from then on the branch on disk loses to the -clientbeta below on
|
||||
# every launch; the updater finds "installed version 0", marks an update pending and exits 42
|
||||
# to apply it, which never happens, so the client restarts for ever - under a running game,
|
||||
# taking Steam Input with it each time. (Bannerlator 38996ea1, on device.) The SteamOS helpers
|
||||
# the client calls in this mode are staged as no-op stubs under /usr/bin.
|
||||
# Deck mode (BL_STEAMDECK=1, the Steam page's Client settings): -steamdeck is the flag that puts
|
||||
# the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay
|
||||
# controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes
|
||||
# the client manage itself as SteamOS does - it calls steamos-select-branch and writes
|
||||
# steamdeck_stable to package/beta, and a branch on disk that loses to the -clientbeta below
|
||||
# is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The
|
||||
# loop below puts package/beta back before every start. The SteamOS helpers the client calls
|
||||
# in this mode are staged as no-op stubs under /usr/bin.
|
||||
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
|
||||
_bl_ui+=(-steamdeck)
|
||||
_bl_ui+=(-steamdeck -steamos3)
|
||||
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
|
||||
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
|
||||
echo "== steam: Deck mode on (-steamdeck)"
|
||||
echo "== steam: Deck mode on (-steamdeck -steamos3)"
|
||||
fi
|
||||
# What a killed session leaves behind. Android ends sessions without warning (backgrounding, the
|
||||
# low-memory killer), and the client then finds .crash - opening its update UI on the spinner -
|
||||
@@ -559,7 +589,7 @@ BUSCONF
|
||||
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.5
|
||||
nap 0.5
|
||||
done
|
||||
) &
|
||||
stop_watcher=$!
|
||||
|
||||
@@ -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,14 @@ Ported from WinNative (`main`, 53836ca9, "Fix/performance and vac"):
|
||||
- `0009-tracee-relatives-sweep.patch` - tracees count their children, so a terminating thread
|
||||
with no children or ptracees no longer walks every tracee; the per-stop memory collector is
|
||||
emptied instead of freed and reallocated.
|
||||
|
||||
Added by DroidDeck:
|
||||
|
||||
- `0011-kompat-utsname-only.patch` - `--kernel-release` (the guest's `DroidDeck` hostname) loads
|
||||
kompat, whose filter traps `futex`, `fcntl`, `epoll_pwait`, `pselect6`, `pipe2`, `eventfd2`,
|
||||
`socket` and more, and which strips `AT_SYSINFO_EHDR` on every `execve`, so glibc runs without
|
||||
the vDSO. When the virtual release is not older than the real kernel and the hwcap is left
|
||||
alone, every one of those handlers is a no-op: kompat now traces only `uname`, `sethostname`
|
||||
and `setdomainname` and leaves the auxv as the kernel wrote it. On an SD 8 Gen 2 guest this
|
||||
took a futex ping-pong from 467 to 97 us, `epoll_pwait` from 60 to 0.8 us and `fcntl` from
|
||||
40-107 to 0.4 us, and `clock_gettime` back to the vDSO.
|
||||
@@ -0,0 +1,64 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: DroidDeck <[email protected]>
|
||||
Date: Tue, 29 Sep 2026 00:00:00 +0000
|
||||
Subject: [PATCH] kompat: only trace the utsname calls when no older kernel is
|
||||
emulated
|
||||
|
||||
---
|
||||
src/extension/kompat/kompat.c | 29 +++++++++++++++++++++++++++--
|
||||
1 file changed, 27 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/extension/kompat/kompat.c b/src/extension/kompat/kompat.c
|
||||
index 5c39942..4cb7dec 100644
|
||||
--- a/src/extension/kompat/kompat.c
|
||||
+++ b/src/extension/kompat/kompat.c
|
||||
@@ -985,6 +985,29 @@ static FilteredSysnum filtered_sysnums[] = {
|
||||
FILTERED_SYSNUM_END,
|
||||
};
|
||||
|
||||
+/* When the virtual kernel is not older than the actual one and the
|
||||
+ * hwcap is left alone, the only thing left to emulate is the utsname
|
||||
+ * itself: every other handler above returns without a change. */
|
||||
+static FilteredSysnum utsname_only_sysnums[] = {
|
||||
+ { PR_setdomainname, FILTER_SYSEXIT },
|
||||
+ { PR_sethostname, FILTER_SYSEXIT },
|
||||
+ { PR_uname, FILTER_SYSEXIT },
|
||||
+ FILTERED_SYSNUM_END,
|
||||
+};
|
||||
+
|
||||
+/**
|
||||
+ * Return whether @config only renames the kernel identity (nodename,
|
||||
+ * domainname, version string) without emulating an older kernel or
|
||||
+ * another hwcap, in which case futex, fcntl, epoll_pwait, ... don't
|
||||
+ * have to stop in the tracer and the vDSO can be left in place.
|
||||
+ */
|
||||
+static bool utsname_only(const Config *config)
|
||||
+{
|
||||
+ return config->actual_release != 0
|
||||
+ && config->virtual_release >= config->actual_release
|
||||
+ && config->hwcap == (word_t) -1;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* Handler for this @extension. It is triggered each time an @event
|
||||
* occured. See ExtensionEvent for the meaning of @data1 and @data2.
|
||||
@@ -1007,7 +1030,9 @@ int kompat_callback(Extension *extension, ExtensionEvent event,
|
||||
if (status < 0)
|
||||
return -1;
|
||||
|
||||
- extension->filtered_sysnums = filtered_sysnums;
|
||||
+ extension->filtered_sysnums = utsname_only(config)
|
||||
+ ? utsname_only_sysnums
|
||||
+ : filtered_sysnums;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1038,7 +1063,7 @@ int kompat_callback(Extension *extension, ExtensionEvent event,
|
||||
|
||||
/* Note: this can be done only before PRoot pushes the
|
||||
* load script into tracee's stack. */
|
||||
- if ((int) result >= 0 && sysnum == PR_execve)
|
||||
+ if ((int) result >= 0 && sysnum == PR_execve && !utsname_only(config))
|
||||
adjust_elf_auxv(tracee, config);
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user