From b2eaf380dc663955b4d9135e740010057dda57d5 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 12:18:50 -0400 Subject: [PATCH 1/7] Blend translucent Wayland surfaces The compositor composed every surface with a blit, which replaces what is under it. gamescope's Wayland backend hands its overlay and notification planes to the host as full-size translucent subsurfaces, so a Steam toast, the Quick Access menu or Deck mode's performance overlay blacked out what was beneath it. Surfaces whose buffer has alpha and that are not declared opaque now go through an alpha pass (premultiplied source-over); everything else, and any image the driver cannot sample, keeps the blit. wl_surface.set_opaque_region is honoured, and only for programs that declare opaque regions at all, so Wine windows with a meaningless alpha channel are drawn as before. On the SurfaceControl path a translucent plane is never promoted to the game layer, and the overlay layer is marked translucent when its plane is. From WinNative (maxjivi05, feature/wayland-gamescope 69218fc; GPL-3.0), with the SurfaceControl layer handling added. --- app/src/main/cpp/waylandcomp/CMakeLists.txt | 1 + app/src/main/cpp/waylandcomp/src/blend.frag | 9 + app/src/main/cpp/waylandcomp/src/blend.vert | 10 + app/src/main/cpp/waylandcomp/src/blend_frag.h | 22 ++ app/src/main/cpp/waylandcomp/src/blend_pass.c | 315 ++++++++++++++++++ app/src/main/cpp/waylandcomp/src/blend_pass.h | 28 ++ app/src/main/cpp/waylandcomp/src/blend_vert.h | 55 +++ app/src/main/cpp/waylandcomp/src/compositor.c | 69 +++- .../cpp/waylandcomp/src/compositor_internal.h | 3 + app/src/main/cpp/waylandcomp/src/sc_layer.c | 19 +- app/src/main/cpp/waylandcomp/src/sc_layer.h | 3 +- app/src/main/cpp/waylandcomp/src/vk_present.c | 88 +++-- app/src/main/cpp/waylandcomp/src/vk_present.h | 1 + 13 files changed, 578 insertions(+), 45 deletions(-) create mode 100644 app/src/main/cpp/waylandcomp/src/blend.frag create mode 100644 app/src/main/cpp/waylandcomp/src/blend.vert create mode 100644 app/src/main/cpp/waylandcomp/src/blend_frag.h create mode 100644 app/src/main/cpp/waylandcomp/src/blend_pass.c create mode 100644 app/src/main/cpp/waylandcomp/src/blend_pass.h create mode 100644 app/src/main/cpp/waylandcomp/src/blend_vert.h diff --git a/app/src/main/cpp/waylandcomp/CMakeLists.txt b/app/src/main/cpp/waylandcomp/CMakeLists.txt index 5f6733571..d942fd686 100644 --- a/app/src/main/cpp/waylandcomp/CMakeLists.txt +++ b/app/src/main/cpp/waylandcomp/CMakeLists.txt @@ -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 diff --git a/app/src/main/cpp/waylandcomp/src/blend.frag b/app/src/main/cpp/waylandcomp/src/blend.frag new file mode 100644 index 000000000..88739de25 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/blend.frag @@ -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); +} diff --git a/app/src/main/cpp/waylandcomp/src/blend.vert b/app/src/main/cpp/waylandcomp/src/blend.vert new file mode 100644 index 000000000..5c87cc352 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/blend.vert @@ -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); +} diff --git a/app/src/main/cpp/waylandcomp/src/blend_frag.h b/app/src/main/cpp/waylandcomp/src/blend_frag.h new file mode 100644 index 000000000..9d2af54a3 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/blend_frag.h @@ -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 +}; diff --git a/app/src/main/cpp/waylandcomp/src/blend_pass.c b/app/src/main/cpp/waylandcomp/src/blend_pass.c new file mode 100644 index 000000000..471eedc9a --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/blend_pass.c @@ -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 + +/* 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; +} diff --git a/app/src/main/cpp/waylandcomp/src/blend_pass.h b/app/src/main/cpp/waylandcomp/src/blend_pass.h new file mode 100644 index 000000000..2faade252 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/blend_pass.h @@ -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 + +/* 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 diff --git a/app/src/main/cpp/waylandcomp/src/blend_vert.h b/app/src/main/cpp/waylandcomp/src/blend_vert.h new file mode 100644 index 000000000..f402c6a92 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/blend_vert.h @@ -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 +}; diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index c206dfb2c..6a562ffd9 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -237,6 +237,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]; @@ -702,6 +704,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)); @@ -753,6 +756,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; @@ -924,8 +928,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) {} @@ -954,6 +977,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; @@ -1099,24 +1126,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, @@ -1589,6 +1621,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; @@ -1608,7 +1651,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; } @@ -1679,6 +1723,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) || @@ -2017,7 +2062,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(); diff --git a/app/src/main/cpp/waylandcomp/src/compositor_internal.h b/app/src/main/cpp/waylandcomp/src/compositor_internal.h index d6e07c5f5..db739132a 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor_internal.h +++ b/app/src/main/cpp/waylandcomp/src/compositor_internal.h @@ -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; }; diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.c b/app/src/main/cpp/waylandcomp/src/sc_layer.c index cd60fd351..1fcbf3357 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.c +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.c @@ -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(); diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.h b/app/src/main/cpp/waylandcomp/src/sc_layer.h index 94a6f9065..f2f12a704 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.h +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.h @@ -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); diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 8c1d897fc..7a2ecfd48 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -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 #include @@ -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); } diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.h b/app/src/main/cpp/waylandcomp/src/vk_present.h index 899736520..eef9c80cc 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.h +++ b/app/src/main/cpp/waylandcomp/src/vk_present.h @@ -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. From 2341659cea6abae0564394c8e5c905b7106eafdd Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 12:19:01 -0400 Subject: [PATCH 2/7] Deck mode: enable Steam's performance overlay controls The Quick Access menu's performance overlay toggle and level slider stayed greyed out: the client exposes SteamClient.System.Perf only with -steamos3, which SteamOS and Armada pass alongside -steamdeck. Deck mode now launches the client with both. The session already puts package/beta back to the forced branch before every start, which keeps -steamos3's own branch switch from turning into an update loop. The Deck mode switch moves from the Performance page to the Steam page's Client settings, next to the client branch it selects by default. --- .../com/droiddeck/launcher/MainActivity.kt | 9 ++++++-- .../launcher/session/SessionPrefs.kt | 2 +- .../launcher/ui/ModeSettingsDialog.kt | 8 +++++++ .../launcher/ui/PerformanceDialog.kt | 7 ------- .../overlay/usr/local/bin/bannerlator-session | 21 +++++++++---------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index eaec48d8f..d8b9f1c1f 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -782,6 +782,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(), @@ -833,6 +834,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 @@ -863,14 +869,13 @@ 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, prootNoSeccomp = prootNoSeccomp, 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 }, onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on }, onClientCore = { core, on -> diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 13e680c88..1f3f1c69e 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -232,7 +232,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() } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 2bdea340f..5f13007b2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -66,6 +66,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. */ @@ -121,6 +123,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 = {}, @@ -336,6 +339,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, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt index 7fac4329b..8c093a12f 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt @@ -29,7 +29,6 @@ fun PerformancePage( zinkLazy: Boolean, glThread: Boolean, noGlError: Boolean, - steamDeckMode: Boolean, noXalia: Boolean, prootNoSeccomp: Boolean, phantomWarning: String?, @@ -38,7 +37,6 @@ fun PerformancePage( onZinkLazy: (Boolean) -> Unit, onGlThread: (Boolean) -> Unit, onNoGlError: (Boolean) -> Unit, - onSteamDeckMode: (Boolean) -> Unit, onNoXalia: (Boolean) -> Unit, onProotNoSeccomp: (Boolean) -> Unit, onClientCore: (Int, Boolean) -> Unit, @@ -87,11 +85,6 @@ fun PerformancePage( "Disables per-call GL validation.", noGlError, onChange = onNoGlError, ) - ToggleRow( - host, "deck", "Steam Deck mode", - "Runs the client with -droiddeck. SteamOS helpers and battery info are provided.", - steamDeckMode, onChange = onSteamDeckMode, - ) } SettingsGroup("Session fixes") { ToggleRow( diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session index c0adc95e7..b039c8b59 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session @@ -501,20 +501,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 - From c2bcf20172dfee4eeeb9426f704dedfa10ceafb2 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 12:19:01 -0400 Subject: [PATCH 3/7] mangoapp: run the performance overlay under Android Three things kept Valve's mangoapp from drawing the overlay in a session: - GLFW 3.4 takes XDG_SESSION_TYPE=wayland with WAYLAND_DISPLAY set as Wayland-only, and mangoapp needs gamescope's X server; the wrapper sets XDG_SESSION_TYPE=x11. - On a Qualcomm display driver it opens a tracefs instance for the GPU memory total and aborts when that fails, which it always does for an Android app. The session preload now places the instance in an empty directory of its own when the real one cannot be made. - gamescope sends it each frame's timings through a System V message queue, and Android kernels have no System V IPC. sysv.c now emulates the queue operations gamescope and mangoapp use, beside its semaphores and shared memory. --- tools/linuxfs/preload/sysv.c | 125 +++++++++++++++++++++++++++++++- tools/linuxfs/preload/tracefs.c | 46 ++++++++++++ tools/mangoapp/mangoapp | 4 + 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tools/linuxfs/preload/tracefs.c diff --git a/tools/linuxfs/preload/sysv.c b/tools/linuxfs/preload/sysv.c index 6b9a21dc8..36d7bdd70 100644 --- a/tools/linuxfs/preload/sysv.c +++ b/tools/linuxfs/preload/sysv.c @@ -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 #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -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 diff --git a/tools/linuxfs/preload/tracefs.c b/tools/linuxfs/preload/tracefs.c new file mode 100644 index 000000000..0d0b8f36b --- /dev/null +++ b/tools/linuxfs/preload/tracefs.c @@ -0,0 +1,46 @@ +/* + * 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, where it exists but has no events: the overlay runs, and the one reading it + * cannot take shows no GPU memory. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#define STANDIN_DIR "/tmp/droiddeck-tracefs" + +struct tracefs_instance; +struct tep_handle; + +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 alloc(STANDIN_DIR, name); +} + +/* The stand-in has no event formats to read, so it gets an empty handle rather than none. */ +struct tep_handle *tracefs_local_events(const char *tracing_dir) { + struct tep_handle *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_local_events"); + struct tep_handle *tep = real ? real(tracing_dir) : NULL; + if (tep || !tracing_dir || strncmp(tracing_dir, STANDIN_DIR "/", sizeof(STANDIN_DIR))) return tep; + struct tep_handle *(*tep_alloc)(void) = dlsym(RTLD_DEFAULT, "tep_alloc"); + return tep_alloc ? tep_alloc() : NULL; +} diff --git a/tools/mangoapp/mangoapp b/tools/mangoapp/mangoapp index 6557ed559..4d541f73e 100644 --- a/tools/mangoapp/mangoapp +++ b/tools/mangoapp/mangoapp @@ -1,4 +1,8 @@ #!/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 LD_LIBRARY_PATH=/usr/local/lib/mangoapp${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} exec /usr/local/lib/mangoapp/mangoapp "$@" From ab8542e4e58d9acd05613ee81529406f13594df0 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 12:19:01 -0400 Subject: [PATCH 4/7] mangoapp: show Adreno GPU and CPU stats Valve's mangoapp reads an Adreno GPU's load, clock and temperatures from the paths of its own hardware, and the CPU temperature from thermal zones named as mainline kernels name them. Every Adreno under Android keeps the GPU values in KGSL's sysfs, readable by the app, and Android kernels name the CPU sensors cpuss-N / cpu-N-M; the session binds each where mangoapp looks. Thermal zones are found by sensor name, since their numbering differs per device, and anything that cannot be found is left alone. --- .../launcher/runtime/LinuxRuntime.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index 816d7c3ad..b04548b00 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -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,72 @@ 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. Only the GPU temperature has no KGSL file on older + * kernels, and thermal zones are numbered per device, so a zone is found by its sensor's name, + * which differs between Snapdragon generations. A value that cannot be found is left alone. + */ + private static void bindAdrenoStats(List cmd) { + String kgsl = "/sys/class/kgsl/kgsl-3d0/"; + String gpuTemp = new File(kgsl + "temp").canRead() ? kgsl + "temp" + : thermalZone("gpuss-0", "gpuss-0-usr", "gpu0-usr", "gpu-usr", "gpu"); + String[][] stats = { + {kgsl + "gpu_busy_percentage", "/sys/kernel/debug/dri/0/perf_now"}, + {kgsl + "devfreq/cur_freq", "/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", "ddr-usr", "ddr-thermal"), "/sys/class/thermal/thermal_zone22/temp"}, + }; + for (String[] stat : stats) { + if (stat[0] != null && new File(stat[0]).canRead()) 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 and the like, so those zones are shown under the mainline name. + */ + private static void bindCpuTemps(List 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 zone : thermalZones().entrySet()) { + String type = zone.getKey(); + if (type.startsWith("cpu") && !type.matches("cpu\\d-(top-)?thermal")) { + bind(cmd, name.getPath() + ":" + new File(zone.getValue(), "type").getPath()); + } + } + } + + /** The temp file of the first thermal zone whose sensor has one of these names, in that order. */ + private static String thermalZone(String... types) { + java.util.Map zones = thermalZones(); + for (String type : types) { + if (zones.containsKey(type)) return new File(zones.get(type), "temp").getPath(); + } + return null; + } + + /** The device's thermal zones by their sensor's name. */ + private static java.util.Map thermalZones() { + java.util.Map 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(), zone); + } catch (IOException ignored) { + } + } + return byType; + } + private static void bind(List cmd, String spec) { cmd.add("-b"); cmd.add(spec); From 0cc3fe6bca930ae4a15d5932343d6cb3914513bc Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 12:28:45 -0400 Subject: [PATCH 5/7] mangoapp: GPU memory from KGSL mangoapp's VRAM figure on a Qualcomm driver comes only from the gpu_mem_total trace event. The tracefs stand-in's event list now declares that event as Android kernels do, and reading the stand-in's events answers with one record carrying the total KGSL reports in /sys/class/kgsl/kgsl/page_alloc, which every Adreno under Android has and the app can read. --- tools/linuxfs/preload/tracefs.c | 71 +++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/tools/linuxfs/preload/tracefs.c b/tools/linuxfs/preload/tracefs.c index 0d0b8f36b..17d45a01a 100644 --- a/tools/linuxfs/preload/tracefs.c +++ b/tools/linuxfs/preload/tracefs.c @@ -7,20 +7,51 @@ * 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, where it exists but has no events: the overlay runs, and the one reading it - * cannot take shows no GPU memory. + * 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 #include +#include #include #include #include #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"); @@ -33,14 +64,38 @@ struct tracefs_instance *tracefs_instance_create(const char *name) { mkdir(STANDIN_DIR "/instances", 0700); snprintf(path, sizeof(path), STANDIN_DIR "/instances/%s", name); mkdir(path, 0700); - return alloc(STANDIN_DIR, name); + return standin = alloc(STANDIN_DIR, name); } -/* The stand-in has no event formats to read, so it gets an empty handle rather than none. */ struct tep_handle *tracefs_local_events(const char *tracing_dir) { - struct tep_handle *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_local_events"); - struct tep_handle *tep = real ? real(tracing_dir) : NULL; - if (tep || !tracing_dir || strncmp(tracing_dir, STANDIN_DIR "/", sizeof(STANDIN_DIR))) return tep; + 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"); - return tep_alloc ? tep_alloc() : NULL; + 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); } From e797bc5f019b2a7bdbbca25f13aeab611d287769 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 12:28:45 -0400 Subject: [PATCH 6/7] mangoapp: stat sources for more Snapdragon generations KGSL's files and thermal zone names differ between Snapdragon generations. Each value now takes the first source that exists, in the order GameNative and the Winlator forks read them: GPU load from gpu_busy_percentage or devfreq/gpu_load, GPU clock from devfreq/cur_freq or gpuclk, GPU temperature from KGSL's temp, devfreq/temp or a zone whose sensor name has "gpu". CPU zones are any whose name has "cpu" and not "gpu" (cpuss-0, cpu0-silver-usr, apc1-cpu0-usr, ...), matched without regard to case. --- .../launcher/runtime/LinuxRuntime.java | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index b04548b00..ddec26da0 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -249,30 +249,32 @@ public final class LinuxRuntime { /** * 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. Only the GPU temperature has no KGSL file on older - * kernels, and thermal zones are numbered per device, so a zone is found by its sensor's name, - * which differs between Snapdragon generations. A value that cannot be found is left alone. + * 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 cmd) { String kgsl = "/sys/class/kgsl/kgsl-3d0/"; - String gpuTemp = new File(kgsl + "temp").canRead() ? kgsl + "temp" - : thermalZone("gpuss-0", "gpuss-0-usr", "gpu0-usr", "gpu-usr", "gpu"); + String gpuTemp = firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu")); String[][] stats = { - {kgsl + "gpu_busy_percentage", "/sys/kernel/debug/dri/0/perf_now"}, - {kgsl + "devfreq/cur_freq", "/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"}, + {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", "ddr-usr", "ddr-thermal"), "/sys/class/thermal/thermal_zone22/temp"}, + {thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"}, }; for (String[] stat : stats) { - if (stat[0] != null && new File(stat[0]).canRead()) bind(cmd, stat[0] + ":" + stat[1]); + 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 and the like, so those zones are shown under the mainline name. + * 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 cmd, File root) { File name = new File(root, "etc/bannerlator/cpu-thermal-type"); @@ -283,29 +285,37 @@ public final class LinuxRuntime { } for (java.util.Map.Entry zone : thermalZones().entrySet()) { String type = zone.getKey(); - if (type.startsWith("cpu") && !type.matches("cpu\\d-(top-)?thermal")) { + if (type.contains("cpu") && !type.contains("gpu") && !type.matches("cpu\\d-(top-)?thermal")) { bind(cmd, name.getPath() + ":" + new File(zone.getValue(), "type").getPath()); } } } - /** The temp file of the first thermal zone whose sensor has one of these names, in that order. */ - private static String thermalZone(String... types) { - java.util.Map zones = thermalZones(); - for (String type : types) { - if (zones.containsKey(type)) return new File(zones.get(type), "temp").getPath(); + private static String firstReadable(String... paths) { + for (String path : paths) { + if (path != null && new File(path).canRead()) return path; } return null; } - /** The device's thermal zones by their sensor's name. */ + /** 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 zones = thermalZones(); + File zone = zones.get(sensor); + for (java.util.Map.Entry 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 thermalZones() { java.util.Map 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(), zone); + byType.putIfAbsent(String.valueOf(r.readLine()).trim().toLowerCase(java.util.Locale.ROOT), zone); } catch (IOException ignored) { } } From ffd9f92d7ba17a12d7f863c23a8da6d4eea74495 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Tue, 29 Sep 2026 10:46:50 -0400 Subject: [PATCH 7/7] ci: keep main DroidDeck-CI releases published --- .github/workflows/build.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ad123b61..864786267 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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"