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" 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 abdcc13c4..d2f5f0595 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -238,6 +238,8 @@ struct surface { struct dmabuf_buffer *dmabuf_buf; /* its imported image, kept until replaced or the surface goes */ struct wl_listener dmabuf_destroy; int buf_w, buf_h, has_content; + int buf_alpha; + int opaque[4], pending_opaque[4], pending_opaque_set; /* x, y, w, h; w = 0: none */ int src_set, dst_set; float src[4]; int dst[2]; @@ -722,6 +724,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res wl_buffer_send_release(buffer); s->buf_w = w; s->buf_h = h; + s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888; s->has_content = s->shm_img != NULL; g_stat_shm++; struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); @@ -773,6 +776,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re } s->buf_w = b->width; s->buf_h = b->height; + s->buf_alpha = (b->format & 0xff) == 'A'; s->has_content = b->img != NULL; g_stat_dmabuf++; const int announced_now = !s->announced_vulkan; @@ -944,8 +948,27 @@ static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t c wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy); wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback)); } +/* The bounding box serves pointer confinement. The exact flag also tells opaque-region handling + * whether the bounding box itself is the whole region. */ +struct region { int set, exact; int x, y, w, h; }; + +/* A wl_surface's opaque region is pending until its next commit. An exact region that covers + * the surface lets the blit path keep handling an otherwise alpha-capable buffer. */ static void surface_set_opaque(struct wl_client *c, struct wl_resource *r, - struct wl_resource *region) {} + struct wl_resource *region) { + struct surface *s = wl_resource_get_user_data(r); + struct region *rg = region ? wl_resource_get_user_data(region) : NULL; + memset(s->pending_opaque, 0, sizeof(s->pending_opaque)); + s->pending_opaque_set = 1; + if (rg && rg->set && rg->exact) { + s->pending_opaque[0] = rg->x; + s->pending_opaque[1] = rg->y; + s->pending_opaque[2] = rg->w; + s->pending_opaque[3] = rg->h; + } + struct client_info *ci = client_info_of(c); + if (ci) ci->declares_opaque = 1; +} static void surface_set_input(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) {} @@ -1036,6 +1059,10 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { memcpy(s->dst, s->pending_dst, sizeof(s->dst)); s->pending_dst_set = 0; } + if (s->pending_opaque_set) { + memcpy(s->opaque, s->pending_opaque, sizeof(s->opaque)); + s->pending_opaque_set = 0; + } if (s->pending_attach) { struct wl_resource *buffer = s->pending_buffer; @@ -1174,24 +1201,29 @@ static void surface_resource_destroy(struct wl_resource *r) { /* ------------------------------------------------------------------ wl_region */ -/* A region is kept as the bounding box of its rectangles: enough for pointer confinement, - * where winewayland sends one rectangle (the ClipCursor area). Subtractions are ignored. */ -struct region { int set; int x, y, w, h; }; - static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void region_add(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y, int32_t w, int32_t h) { struct region *rg = wl_resource_get_user_data(r); if (!rg || w <= 0 || h <= 0) return; - if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = 1; return; } - int x2 = rg->x + rg->w > x + w ? rg->x + rg->w : x + w; - int y2 = rg->y + rg->h > y + h ? rg->y + rg->h : y + h; + if (!rg->set) { rg->x = x; rg->y = y; rg->w = w; rg->h = h; rg->set = rg->exact = 1; return; } + long long x2 = (long long)rg->x + rg->w, y2 = (long long)rg->y + rg->h; + long long nx2 = (long long)x + w, ny2 = (long long)y + h; + int inside = x >= rg->x && y >= rg->y && nx2 <= x2 && ny2 <= y2; + int around = x <= rg->x && y <= rg->y && nx2 >= x2 && ny2 >= y2; + if (!inside && !around) rg->exact = 0; + if (nx2 > x2) x2 = nx2; + if (ny2 > y2) y2 = ny2; if (x < rg->x) rg->x = x; if (y < rg->y) rg->y = y; - rg->w = x2 - rg->x; rg->h = y2 - rg->y; + rg->w = (int)(x2 - rg->x > INT32_MAX ? INT32_MAX : x2 - rg->x); + rg->h = (int)(y2 - rg->y > INT32_MAX ? INT32_MAX : y2 - rg->y); } static void region_subtract(struct wl_client *c, struct wl_resource *r, - int32_t x, int32_t y, int32_t w, int32_t h) {} + int32_t x, int32_t y, int32_t w, int32_t h) { + struct region *rg = wl_resource_get_user_data(r); + if (rg && w > 0 && h > 0) rg->exact = 0; +} static void region_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); } static const struct wl_region_interface region_impl = { .destroy = region_destroy, @@ -1665,6 +1697,17 @@ static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, i g_hdr_unimported_below = dl->n; } +/* A client that describes opaque regions can intentionally leave part of an alpha buffer see-through. + * Gamescope uses this for its full-size Steam notification and overlay planes. */ +static int surface_translucent(const struct surface *s, int w, int h) { + if (!s->buf_alpha) return 0; + const struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); + if (!ci || !ci->declares_opaque) return 0; + const int *o = s->opaque; + return !(o[2] > 0 && o[0] <= 0 && o[1] <= 0 && + (long long)o[0] + o[2] >= w && (long long)o[1] + o[3] >= h); +} + static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy) { struct vkp_image *img = surface_image(s); float sx = 0, sy = 0, sw = (float)s->buf_w, sh = (float)s->buf_h; @@ -1684,7 +1727,8 @@ static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy) dl->d = d; dl->cap = cap; } - dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh}; + dl->d[dl->n++] = (struct vkp_draw){img, sx, sy, sw, sh, ox, oy, dw, dh, + surface_translucent(s, dw, dh)}; s->drawn = 1; } @@ -1755,6 +1799,7 @@ static int on_frame_timer(void *data) { static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) { for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) { const struct vkp_draw *d = &dl->d[i]; + if (d->blend) continue; /* the translucent plane must not replace the opaque game layer */ if (!vkp_image_is_dmabuf(d->img)) continue; if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue; if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) || @@ -2093,7 +2138,7 @@ static void render_scene(void) { * its own layer, cropped and placed by the display. */ int go[8], ov = 0; if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go)) - ov = sc_layer_present_overlay(dl.d[li + 1].img, go) == 0 ? 1 : -1; + ov = sc_layer_present_overlay(dl.d[li + 1].img, go, dl.d[li + 1].blend) == 0 ? 1 : -1; if (ov <= 0) sc_layer_hide_overlay(); if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */ sc_layer_hide(); 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. diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 7e36d9bdb..90d21864d 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -813,6 +813,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(), @@ -864,6 +865,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 @@ -894,14 +900,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, guestHostname = guestHostname, phantomWarning = phantomWarning, onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on }, onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on }, onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on }, onGlThread = { on -> SessionPrefs.setGlThread(this, on); glThread = on }, onNoGlError = { on -> SessionPrefs.setNoGlError(this, on); noGlError = on }, - onSteamDeckMode = { on -> SessionPrefs.setSteamDeckMode(this, on); steamDeckMode = on }, onNoXalia = { on -> SessionPrefs.setNoXalia(this, on); noXalia = on }, onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on }, onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) }, 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 9440c0ec3..6ad52da27 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -236,6 +236,8 @@ public final class LinuxRuntime { } } bindGpuNode(context, cmd); + bindAdrenoStats(cmd); + bindCpuTemps(cmd, root); if (extraBinds != null) { for (String spec : extraBinds) bind(cmd, spec); } @@ -288,6 +290,82 @@ public final class LinuxRuntime { bind(cmd, KGSL_DEVICE + ":/dev/dri/" + node); } + /** + * Valve's mangoapp (Deck mode's performance overlay) reads an Adreno GPU's load, clock and + * temperatures from where they are on Valve's own hardware; every Adreno under Android keeps + * them in KGSL's sysfs, readable by the app, though which files a kernel has differs between + * Snapdragon generations - so each value takes the first source that exists, the order other + * Android PC emulators (GameNative, Winlator forks) read them in. A value found nowhere is left + * alone. + */ + private static void bindAdrenoStats(List cmd) { + String kgsl = "/sys/class/kgsl/kgsl-3d0/"; + String gpuTemp = firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu")); + String[][] stats = { + {firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"), "/sys/kernel/debug/dri/0/perf_now"}, + {firstReadable(kgsl + "devfreq/cur_freq", kgsl + "gpuclk"), + "/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"}, + {gpuTemp, "/sys/class/thermal/thermal_zone28/temp"}, + {gpuTemp, "/sys/class/thermal/thermal_zone26/temp"}, + {thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"}, + }; + for (String[] stat : stats) { + if (stat[0] != null) bind(cmd, stat[0] + ":" + stat[1]); + } + } + + /** + * mangoapp's CPU temperature is the mean of the thermal zones named cpuN-thermal or + * cpuN-top-thermal, as mainline kernels name them; Android kernels name the same sensors + * cpuss-0, cpu-1-0, cpu0-silver-usr, apc1-cpu0-usr and the like, so those zones are shown + * under the mainline name. + */ + private static void bindCpuTemps(List 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.contains("cpu") && !type.contains("gpu") && !type.matches("cpu\\d-(top-)?thermal")) { + bind(cmd, name.getPath() + ":" + new File(zone.getValue(), "type").getPath()); + } + } + } + + private static String firstReadable(String... paths) { + for (String path : paths) { + if (path != null && new File(path).canRead()) return path; + } + return null; + } + + /** The temp file of the thermal zone for a sensor: the one named so, else the first whose name has it. */ + private static String thermalZone(String sensor) { + java.util.Map 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().toLowerCase(java.util.Locale.ROOT), zone); + } catch (IOException ignored) { + } + } + return byType; + } + private static void bind(List cmd, String spec) { cmd.add("-b"); cmd.add(spec); 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 18e1db90d..ec0d09559 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -248,7 +248,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 f7f8d32e1..00bb92839 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -68,6 +68,8 @@ class ModeSettings( val forceFullscreen: Boolean? = null, /** Steam only: the client branch forced on the command line. */ val steamChannel: String? = null, + /** Steam only: enable the SteamOS client interface and its performance controls. */ + val steamDeckMode: Boolean = false, /** Steam only: start a Steam session when DroidDeck opens. */ val runSteamAtStartup: Boolean = false, /** Steam only: the user's chosen Games folders; null outside Steam. */ @@ -123,6 +125,7 @@ class ModeSettingsActions( val onFexPreset: (String) -> Unit = {}, val onForceFullscreen: (Boolean) -> Unit = {}, val onSteamChannel: (String) -> Unit = {}, + val onSteamDeckMode: (Boolean) -> Unit = {}, val onRunSteamAtStartup: (Boolean) -> Unit = {}, val onPickAddedGamesDir: () -> Unit = {}, val onForgetAddedGamesDir: (path: String) -> Unit = {}, @@ -338,6 +341,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { } } if (steam && s.steamChannel != null) SettingsGroup("Client") { + ToggleRow( + host, "steamdeck", "Steam Deck mode", + "Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.", + s.steamDeckMode, onChange = a.onSteamDeckMode, + ) ChoiceRow( host, "channel", "Client branch", "The Steam client build the session forces. Applies at the next session start; the client may update itself once.", listOf("publicbeta" to "Public beta", "steamdeck_publicbeta" to "Steam Deck public beta"), s.steamChannel, 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 41b425092..7756cd72f 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt @@ -36,7 +36,6 @@ fun PerformancePage( zinkLazy: Boolean, glThread: Boolean, noGlError: Boolean, - steamDeckMode: Boolean, noXalia: Boolean, prootNoSeccomp: Boolean, guestHostname: String, @@ -46,7 +45,6 @@ fun PerformancePage( onZinkLazy: (Boolean) -> Unit, onGlThread: (Boolean) -> Unit, onNoGlError: (Boolean) -> Unit, - onSteamDeckMode: (Boolean) -> Unit, onNoXalia: (Boolean) -> Unit, onProotNoSeccomp: (Boolean) -> Unit, onGuestHostname: (String) -> Unit, @@ -96,11 +94,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 fa71328f9..1d0ee603a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session @@ -505,20 +505,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 - 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..17d45a01a --- /dev/null +++ b/tools/linuxfs/preload/tracefs.c @@ -0,0 +1,101 @@ +/* + * A trace instance for mangoapp where the kernel's tracefs is closed to the app. + * + * Valve's mangoapp (Deck mode's performance overlay) reads the GPU memory total on a Qualcomm + * display driver (msm_dpu / msm_drm) from the gpu_mem_total event, through a tracefs instance of + * its own - and aborts when tracefs_instance_create() fails, taking the whole overlay with it. + * Android mounts tracefs for root and the readtracefs group only, so it always fails here. + * + * The real calls run first. Only when they fail is the instance placed in an empty directory of + * the session's own, and reading its events answers with one gpu_mem_total record carrying the + * total KGSL - the GPU driver of every Adreno under Android - reports to the app. + */ +#define _GNU_SOURCE +#include +#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"); + struct tracefs_instance *instance = real ? real(name) : NULL; + if (instance || !name) return instance; + struct tracefs_instance *(*alloc)(const char *, const char *) = dlsym(RTLD_NEXT, "tracefs_instance_alloc"); + if (!alloc) return NULL; + char path[PATH_MAX]; + mkdir(STANDIN_DIR, 0700); + mkdir(STANDIN_DIR "/instances", 0700); + snprintf(path, sizeof(path), STANDIN_DIR "/instances/%s", name); + mkdir(path, 0700); + return standin = alloc(STANDIN_DIR, name); +} + +struct tep_handle *tracefs_local_events(const char *tracing_dir) { + if (!tracing_dir || strncmp(tracing_dir, STANDIN_DIR "/", sizeof(STANDIN_DIR))) { + struct tep_handle *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_local_events"); + return real ? real(tracing_dir) : NULL; + } + struct tep_handle *(*tep_alloc)(void) = dlsym(RTLD_DEFAULT, "tep_alloc"); + int (*parse)(struct tep_handle *, const char *, unsigned long, const char *) = dlsym(RTLD_DEFAULT, "tep_parse_event"); + struct tep_handle *tep = tep_alloc ? tep_alloc() : NULL; + if (tep && parse) parse(tep, gpu_mem_total_format, sizeof(gpu_mem_total_format) - 1, "gpu_mem"); + return tep; +} + +int tracefs_iterate_raw_events(struct tep_handle *tep, struct tracefs_instance *instance, void *cpus, int cpu_size, + int (*callback)(struct tep_event *, struct tep_record *, int, void *), void *context) { + if (!standin || instance != standin) { + int (*real)(struct tep_handle *, struct tracefs_instance *, void *, int, + int (*)(struct tep_event *, struct tep_record *, int, void *), void *) = + dlsym(RTLD_NEXT, "tracefs_iterate_raw_events"); + return real ? real(tep, instance, cpus, cpu_size, callback, context) : -1; + } + struct tep_event *(*find)(struct tep_handle *, int) = dlsym(RTLD_DEFAULT, "tep_find_event"); + struct tep_event *event = find && tep ? find(tep, 1) : NULL; + unsigned long long total; + FILE *f = fopen(KGSL_TOTAL, "r"); + int have = f && fscanf(f, "%llu", &total) == 1; + if (f) fclose(f); + if (!event || !have || !callback) return -1; + struct { uint16_t type; uint8_t flags, preempt; int32_t common_pid; uint32_t gpu_id, pid; uint64_t size; } data = { + .type = 1, .size = total}; + struct tep_record record = {.record_size = sizeof(data), .size = sizeof(data), .data = &data}; + return callback(event, &record, 0, context); +} 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 "$@"