mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd7f71d6c | ||
|
|
353daa265e | ||
|
|
69728a4f9b | ||
|
|
c6f828532a | ||
|
|
5bb28140cb | ||
|
|
bf7fc6bf6b | ||
|
|
b72d4c5dff | ||
|
|
c88c6ff196 | ||
|
|
16488d5801 | ||
|
|
c16a51da62 | ||
|
|
08e45dc9c6 | ||
|
|
b2ce268c86 | ||
|
|
df58f21d83 | ||
|
|
29a89cc10a | ||
|
|
8e3db2cb87 | ||
|
|
1ba7b1a96f | ||
|
|
1a7b4b6a2b | ||
|
|
55e5165e2f | ||
|
|
2860d09051 | ||
|
|
c9206dd955 | ||
|
|
dfbda7ccb4 | ||
|
|
6a702afa72 | ||
|
|
ab685fb0e9 | ||
|
|
1ae7a8eaa4 | ||
|
|
ead7a10b68 | ||
|
|
035f1b07ae | ||
|
|
c7ab618ab0 | ||
|
|
2e43df6125 | ||
|
|
015b91f08d | ||
|
|
987b7ad13c | ||
|
|
d26cf1e31b | ||
|
|
8196818e9f |
Vendored
+538
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* Copyright © 2011 Intel Corporation
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice (including the next
|
||||
* paragraph) shall be included in all copies or substantial portions of the
|
||||
* Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* Authors:
|
||||
* Benjamin Franzke <benjaminfranzke@googlemail.com>
|
||||
*/
|
||||
|
||||
#ifndef _GBM_H_
|
||||
#define _GBM_H_
|
||||
|
||||
#define __GBM__ 1
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* \file gbm.h
|
||||
* \brief Generic Buffer Manager
|
||||
*/
|
||||
|
||||
struct gbm_device;
|
||||
struct gbm_bo;
|
||||
struct gbm_surface;
|
||||
|
||||
/**
|
||||
* \mainpage The Generic Buffer Manager
|
||||
*
|
||||
* This module provides an abstraction that the caller can use to request a
|
||||
* buffer from the underlying memory management system for the platform.
|
||||
*
|
||||
* This allows the creation of portable code whilst still allowing access to
|
||||
* the underlying memory manager.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstraction representing the handle to a buffer allocated by the
|
||||
* manager
|
||||
*/
|
||||
union gbm_bo_handle {
|
||||
void *ptr;
|
||||
int32_t s32;
|
||||
uint32_t u32;
|
||||
int64_t s64;
|
||||
uint64_t u64;
|
||||
};
|
||||
|
||||
/** Format of the allocated buffer */
|
||||
enum gbm_bo_format {
|
||||
/** RGB with 8 bits per channel in a 32 bit value */
|
||||
GBM_BO_FORMAT_XRGB8888,
|
||||
/** ARGB with 8 bits per channel in a 32 bit value */
|
||||
GBM_BO_FORMAT_ARGB8888
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The FourCC format codes are taken from the drm_fourcc.h definition, and
|
||||
* re-namespaced. New GBM formats must not be added, unless they are
|
||||
* identical ports from drm_fourcc.
|
||||
*/
|
||||
#define __gbm_fourcc_code(a,b,c,d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \
|
||||
((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
|
||||
|
||||
#define GBM_FORMAT_BIG_ENDIAN (1<<31) /* format is big endian instead of little endian */
|
||||
|
||||
/* color index */
|
||||
#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
|
||||
|
||||
/* 8 bpp Red */
|
||||
#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
|
||||
|
||||
/* 16 bpp Red */
|
||||
#define GBM_FORMAT_R16 __gbm_fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */
|
||||
|
||||
/* 16 bpp RG */
|
||||
#define GBM_FORMAT_GR88 __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
|
||||
|
||||
/* 32 bpp RG */
|
||||
#define GBM_FORMAT_RG1616 __gbm_fourcc_code('R', 'G', '3', '2') /* [31:0] R:G 16:16 little endian */
|
||||
#define GBM_FORMAT_GR1616 __gbm_fourcc_code('G', 'R', '3', '2') /* [31:0] G:R 16:16 little endian */
|
||||
|
||||
/* 8 bpp RGB */
|
||||
#define GBM_FORMAT_RGB332 __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */
|
||||
#define GBM_FORMAT_BGR233 __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */
|
||||
|
||||
/* 16 bpp RGB */
|
||||
#define GBM_FORMAT_XRGB4444 __gbm_fourcc_code('X', 'R', '1', '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_XBGR4444 __gbm_fourcc_code('X', 'B', '1', '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_RGBX4444 __gbm_fourcc_code('R', 'X', '1', '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_BGRX4444 __gbm_fourcc_code('B', 'X', '1', '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB4444 __gbm_fourcc_code('A', 'R', '1', '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_ABGR4444 __gbm_fourcc_code('A', 'B', '1', '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_RGBA4444 __gbm_fourcc_code('R', 'A', '1', '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_BGRA4444 __gbm_fourcc_code('B', 'A', '1', '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */
|
||||
|
||||
#define GBM_FORMAT_XRGB1555 __gbm_fourcc_code('X', 'R', '1', '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_XBGR1555 __gbm_fourcc_code('X', 'B', '1', '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_RGBX5551 __gbm_fourcc_code('R', 'X', '1', '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */
|
||||
#define GBM_FORMAT_BGRX5551 __gbm_fourcc_code('B', 'X', '1', '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB1555 __gbm_fourcc_code('A', 'R', '1', '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_ABGR1555 __gbm_fourcc_code('A', 'B', '1', '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_RGBA5551 __gbm_fourcc_code('R', 'A', '1', '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */
|
||||
#define GBM_FORMAT_BGRA5551 __gbm_fourcc_code('B', 'A', '1', '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */
|
||||
|
||||
#define GBM_FORMAT_RGB565 __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */
|
||||
#define GBM_FORMAT_BGR565 __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */
|
||||
|
||||
/* 24 bpp RGB */
|
||||
#define GBM_FORMAT_RGB888 __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */
|
||||
#define GBM_FORMAT_BGR888 __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */
|
||||
|
||||
/* 32 bpp RGB */
|
||||
#define GBM_FORMAT_XRGB8888 __gbm_fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_XBGR8888 __gbm_fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_RGBX8888 __gbm_fourcc_code('R', 'X', '2', '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_BGRX8888 __gbm_fourcc_code('B', 'X', '2', '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB8888 __gbm_fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_ABGR8888 __gbm_fourcc_code('A', 'B', '2', '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_RGBA8888 __gbm_fourcc_code('R', 'A', '2', '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_BGRA8888 __gbm_fourcc_code('B', 'A', '2', '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_XRGB2101010 __gbm_fourcc_code('X', 'R', '3', '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_XBGR2101010 __gbm_fourcc_code('X', 'B', '3', '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_RGBX1010102 __gbm_fourcc_code('R', 'X', '3', '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */
|
||||
#define GBM_FORMAT_BGRX1010102 __gbm_fourcc_code('B', 'X', '3', '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB2101010 __gbm_fourcc_code('A', 'R', '3', '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_ABGR2101010 __gbm_fourcc_code('A', 'B', '3', '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_RGBA1010102 __gbm_fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
|
||||
#define GBM_FORMAT_BGRA1010102 __gbm_fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
|
||||
|
||||
/* 64 bpp RGB */
|
||||
#define GBM_FORMAT_XBGR16161616 __gbm_fourcc_code('X', 'B', '4', '8') /* [63:0] x:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
#define GBM_FORMAT_ABGR16161616 __gbm_fourcc_code('A', 'B', '4', '8') /* [63:0] A:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
/*
|
||||
* Floating point 64bpp RGB
|
||||
* IEEE 754-2008 binary16 half-precision float
|
||||
* [15:0] sign:exponent:mantissa 1:5:10
|
||||
*/
|
||||
#define GBM_FORMAT_XBGR16161616F __gbm_fourcc_code('X', 'B', '4', 'H') /* [63:0] x:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
#define GBM_FORMAT_ABGR16161616F __gbm_fourcc_code('A', 'B', '4', 'H') /* [63:0] A:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
/* packed YCbCr */
|
||||
#define GBM_FORMAT_YUYV __gbm_fourcc_code('Y', 'U', 'Y', 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_YVYU __gbm_fourcc_code('Y', 'V', 'Y', 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_UYVY __gbm_fourcc_code('U', 'Y', 'V', 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_VYUY __gbm_fourcc_code('V', 'Y', 'U', 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_AYUV __gbm_fourcc_code('A', 'Y', 'U', 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */
|
||||
|
||||
/*
|
||||
* 2 plane YCbCr
|
||||
* index 0 = Y plane, [7:0] Y
|
||||
* index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian
|
||||
* or
|
||||
* index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian
|
||||
*/
|
||||
#define GBM_FORMAT_NV12 __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */
|
||||
#define GBM_FORMAT_NV21 __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */
|
||||
#define GBM_FORMAT_NV16 __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */
|
||||
#define GBM_FORMAT_NV61 __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */
|
||||
|
||||
/*
|
||||
* 3 plane YCbCr
|
||||
* index 0: Y plane, [7:0] Y
|
||||
* index 1: Cb plane, [7:0] Cb
|
||||
* index 2: Cr plane, [7:0] Cr
|
||||
* or
|
||||
* index 1: Cr plane, [7:0] Cr
|
||||
* index 2: Cb plane, [7:0] Cb
|
||||
*/
|
||||
#define GBM_FORMAT_YUV410 __gbm_fourcc_code('Y', 'U', 'V', '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU410 __gbm_fourcc_code('Y', 'V', 'U', '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV411 __gbm_fourcc_code('Y', 'U', '1', '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU411 __gbm_fourcc_code('Y', 'V', '1', '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV420 __gbm_fourcc_code('Y', 'U', '1', '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU420 __gbm_fourcc_code('Y', 'V', '1', '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV422 __gbm_fourcc_code('Y', 'U', '1', '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU422 __gbm_fourcc_code('Y', 'V', '1', '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV444 __gbm_fourcc_code('Y', 'U', '2', '4') /* non-subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU444 __gbm_fourcc_code('Y', 'V', '2', '4') /* non-subsampled Cr (1) and Cb (2) planes */
|
||||
|
||||
struct gbm_format_name_desc {
|
||||
char name[5];
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags to indicate the intended use for the buffer - these are passed into
|
||||
* gbm_bo_create(). The caller must set the union of all the flags that are
|
||||
* appropriate
|
||||
*
|
||||
* \sa Use gbm_device_is_format_supported() to check if the combination of format
|
||||
* and use flags are supported
|
||||
*/
|
||||
enum gbm_bo_flags {
|
||||
/**
|
||||
* Buffer is going to be presented to the screen using an API such as KMS
|
||||
*/
|
||||
GBM_BO_USE_SCANOUT = (1 << 0),
|
||||
/**
|
||||
* Buffer is going to be used as cursor
|
||||
*/
|
||||
GBM_BO_USE_CURSOR = (1 << 1),
|
||||
/**
|
||||
* Deprecated
|
||||
*/
|
||||
GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
|
||||
/**
|
||||
* Buffer is to be used for rendering - for example it is going to be used
|
||||
* as the storage for a color buffer
|
||||
*/
|
||||
GBM_BO_USE_RENDERING = (1 << 2),
|
||||
/**
|
||||
* Buffer can be used for gbm_bo_write. This is guaranteed to work
|
||||
* with GBM_BO_USE_CURSOR, but may not work for other combinations.
|
||||
*/
|
||||
GBM_BO_USE_WRITE = (1 << 3),
|
||||
/**
|
||||
* Buffer is linear, i.e. not tiled.
|
||||
*/
|
||||
GBM_BO_USE_LINEAR = (1 << 4),
|
||||
/**
|
||||
* Buffer is protected, i.e. encrypted and not readable by CPU or any
|
||||
* other non-secure / non-trusted components nor by non-trusted OpenGL,
|
||||
* OpenCL, and Vulkan applications.
|
||||
*/
|
||||
GBM_BO_USE_PROTECTED = (1 << 5),
|
||||
|
||||
/**
|
||||
* The buffer will be used for front buffer rendering. On some
|
||||
* platforms this may (for example) disable framebuffer compression
|
||||
* to avoid problems with compression flags data being out of sync
|
||||
* with pixel data.
|
||||
*/
|
||||
GBM_BO_USE_FRONT_RENDERING = (1 << 6),
|
||||
|
||||
/**
|
||||
* Allow the driver to select fixed-rate compression parameters.
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_DEFAULT = (1 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 1bpc, less than 2bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_1BPC = (2 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 2bpc, less than 3bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_2BPC = (3 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 3bpc, less than 4bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_3BPC = (4 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 4bpc, less than 5bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_4BPC = (5 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 5bpc, less than 6bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_5BPC = (6 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 6bpc, less than 7bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_6BPC = (7 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 7bpc, less than 8bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_7BPC = (8 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 8bpc, less than 9bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_8BPC = (9 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 9bpc, less than 10bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_9BPC = (10 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 10bpc, less than 11bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_10BPC = (11 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 11bpc, less than 12bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_11BPC = (12 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 12bpc, no maximum rate
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_12BPC = (13 << 7),
|
||||
|
||||
/* next available value is (1 << 11) */
|
||||
};
|
||||
|
||||
#define GBM_BO_FIXED_COMPRESSION_MASK (((1 << 11) - 1) & ~((1 << 7) - 1))
|
||||
|
||||
int
|
||||
gbm_device_get_fd(struct gbm_device *gbm);
|
||||
|
||||
const char *
|
||||
gbm_device_get_backend_name(struct gbm_device *gbm);
|
||||
|
||||
int
|
||||
gbm_device_is_format_supported(struct gbm_device *gbm,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
int
|
||||
gbm_device_get_format_modifier_plane_count(struct gbm_device *gbm,
|
||||
uint32_t format,
|
||||
uint64_t modifier);
|
||||
|
||||
void
|
||||
gbm_device_destroy(struct gbm_device *gbm);
|
||||
|
||||
struct gbm_device *
|
||||
gbm_create_device(int fd);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create_with_modifiers(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create_with_modifiers2(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count,
|
||||
uint32_t flags);
|
||||
|
||||
#define GBM_BO_IMPORT_WL_BUFFER 0x5501
|
||||
#define GBM_BO_IMPORT_EGL_IMAGE 0x5502
|
||||
#define GBM_BO_IMPORT_FD 0x5503
|
||||
#define GBM_BO_IMPORT_FD_MODIFIER 0x5504
|
||||
|
||||
struct gbm_import_fd_data {
|
||||
int fd;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
uint32_t format;
|
||||
};
|
||||
|
||||
#define GBM_MAX_PLANES 4
|
||||
|
||||
struct gbm_import_fd_modifier_data {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t format;
|
||||
uint32_t num_fds;
|
||||
int fds[GBM_MAX_PLANES];
|
||||
int strides[GBM_MAX_PLANES];
|
||||
int offsets[GBM_MAX_PLANES];
|
||||
uint64_t modifier;
|
||||
};
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_import(struct gbm_device *gbm, uint32_t type,
|
||||
void *buffer, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Flags to indicate the type of mapping for the buffer - these are
|
||||
* passed into gbm_bo_map(). The caller must set the union of all the
|
||||
* flags that are appropriate.
|
||||
*
|
||||
* These flags are independent of the GBM_BO_USE_* creation flags. However,
|
||||
* mapping the buffer may require copying to/from a staging buffer.
|
||||
*
|
||||
* See also: pipe_map_flags
|
||||
*/
|
||||
enum gbm_bo_transfer_flags {
|
||||
/**
|
||||
* Buffer contents read back (or accessed directly) at transfer
|
||||
* create time.
|
||||
*/
|
||||
GBM_BO_TRANSFER_READ = (1 << 0),
|
||||
/**
|
||||
* Buffer contents will be written back at unmap time
|
||||
* (or modified as a result of being accessed directly).
|
||||
*/
|
||||
GBM_BO_TRANSFER_WRITE = (1 << 1),
|
||||
/**
|
||||
* Read/modify/write
|
||||
*/
|
||||
GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
|
||||
};
|
||||
|
||||
void *
|
||||
gbm_bo_map(struct gbm_bo *bo,
|
||||
uint32_t x, uint32_t y, uint32_t width, uint32_t height,
|
||||
uint32_t flags, uint32_t *stride, void **map_data);
|
||||
|
||||
void
|
||||
gbm_bo_unmap(struct gbm_bo *bo, void *map_data);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_width(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_height(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_stride(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_stride_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_format(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_bpp(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_offset(struct gbm_bo *bo, int plane);
|
||||
|
||||
struct gbm_device *
|
||||
gbm_bo_get_device(struct gbm_bo *bo);
|
||||
|
||||
union gbm_bo_handle
|
||||
gbm_bo_get_handle(struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_bo_get_fd(struct gbm_bo *bo);
|
||||
|
||||
uint64_t
|
||||
gbm_bo_get_modifier(struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_bo_get_plane_count(struct gbm_bo *bo);
|
||||
|
||||
union gbm_bo_handle
|
||||
gbm_bo_get_handle_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
int
|
||||
gbm_bo_get_fd_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
int
|
||||
gbm_bo_write(struct gbm_bo *bo, const void *buf, size_t count);
|
||||
|
||||
void
|
||||
gbm_bo_set_user_data(struct gbm_bo *bo, void *data,
|
||||
void (*destroy_user_data)(struct gbm_bo *, void *));
|
||||
|
||||
void *
|
||||
gbm_bo_get_user_data(struct gbm_bo *bo);
|
||||
|
||||
void
|
||||
gbm_bo_destroy(struct gbm_bo *bo);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create_with_modifiers(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create_with_modifiers2(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count,
|
||||
uint32_t flags);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_surface_lock_front_buffer(struct gbm_surface *surface);
|
||||
|
||||
void
|
||||
gbm_surface_release_buffer(struct gbm_surface *surface, struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_surface_has_free_buffers(struct gbm_surface *surface);
|
||||
|
||||
void
|
||||
gbm_surface_destroy(struct gbm_surface *surface);
|
||||
|
||||
char *
|
||||
gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc *desc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# glvnd vendor ABI headers
|
||||
|
||||
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
|
||||
headers intact.
|
||||
|
||||
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
|
||||
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
|
||||
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
|
||||
smaller dependency than a build-time download of a source tree.
|
||||
|
||||
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2013, NVIDIA CORPORATION.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and/or associated documentation files (the
|
||||
* "Materials"), to deal in the Materials without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
* permit persons to whom the Materials are furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* unaltered in all copies or substantial portions of the Materials.
|
||||
* Any additions, deletions, or changes to the original source files
|
||||
* must be clearly indicated in accompanying documentation.
|
||||
*
|
||||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#include <GL/gl.h>
|
||||
|
||||
#if !defined(__GL_DISPATCH_ABI_H)
|
||||
#define __GL_DISPATCH_ABI_H
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \defgroup gldispatchabi GL dispatching ABI
|
||||
*
|
||||
* This is not a complete ABI, but rather a fragment common to the libEGL and
|
||||
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
|
||||
* these client ABIs.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Thread-local implementation used by libglvnd. This is passed into the patch
|
||||
* function callback via the type parameter.
|
||||
*
|
||||
* For most architectures, the vendor library can ignore this parameter, since
|
||||
* it will always be the same value. It's used for systems like ARM, where the
|
||||
* stubs might be use the ARM or Thumb instruction sets.
|
||||
*
|
||||
* The stub type does not make any distinction between TLS and TSD stubs. The
|
||||
* entire purpose of entrypoint rewriting is to skip the dispatch table in
|
||||
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
|
||||
*/
|
||||
enum {
|
||||
/*!
|
||||
* Indicates that the stubs aren't defined in assembly. For example, if the
|
||||
* dispatch stubs are written in C. Vendor libraries generally won't see
|
||||
* this value.
|
||||
*/
|
||||
__GLDISPATCH_STUB_UNKNOWN,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x86 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_X86,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x86-64 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_X86_64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv7, using the Thumb instruction set.
|
||||
*/
|
||||
__GLDISPATCH_STUB_ARMV7_THUMB,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv7, using the normal ARM instruction set.
|
||||
*/
|
||||
__GLDISPATCH_STUB_ARMV7_ARM,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv8/aarch64.
|
||||
*/
|
||||
__GLDISPATCH_STUB_AARCH64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
|
||||
*/
|
||||
__GLDISPATCH_STUB_X32,
|
||||
|
||||
/*!
|
||||
* Used for stubs on PPC64 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_PPC64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
|
||||
*/
|
||||
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on LOONGARCH/loongarch64.
|
||||
*/
|
||||
__GLDISPATCH_STUB_LOONGARCH64,
|
||||
};
|
||||
|
||||
/*!
|
||||
* A callback function called by the vendor library to fetch the address of an
|
||||
* entrypoint.
|
||||
*
|
||||
* The function returns two pointers, one writable and one executable. The two
|
||||
* pointers may or may not be the same virtual address, but they will both be
|
||||
* mappings of the same physical memory.
|
||||
*
|
||||
* The vendor library should write its entrypoint to the address returned by
|
||||
* \p writePtr, but should use the address from \p execPtr for things like
|
||||
* calculating PC-relative offsets.
|
||||
*
|
||||
* Note that if this function fails, then the vendor library can still try to
|
||||
* patch other entrypoints.
|
||||
*
|
||||
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
|
||||
* be zero, even if the stub uses the thumb instruction set. The vendor library
|
||||
* should use the \c type parameter of \c initiatePatch to determine which
|
||||
* instruction set to use.
|
||||
*
|
||||
* \param funcName The function name.
|
||||
* \param[out] writePtr The pointer that the vendor library can write to.
|
||||
* \param[out] execPtr The pointer to the executable code.
|
||||
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
|
||||
*/
|
||||
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
|
||||
void **writePtr, const void **execPtr);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // __GL_DISPATCH_ABI_H
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* Copyright (c) 2013, NVIDIA CORPORATION.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and/or associated documentation files (the
|
||||
* "Materials"), to deal in the Materials without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
* permit persons to whom the Materials are furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* unaltered in all copies or substantial portions of the Materials.
|
||||
* Any additions, deletions, or changes to the original source files
|
||||
* must be clearly indicated in accompanying documentation.
|
||||
*
|
||||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#if !defined(__LIB_EGL_ABI_H)
|
||||
#define __LIB_EGL_ABI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#include "glvnd/GLdispatchABI.h"
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \defgroup eglvendorabi EGL Vendor ABI
|
||||
*
|
||||
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
|
||||
*
|
||||
* Each vendor is associated with three distinct dispatch table types:
|
||||
*
|
||||
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
|
||||
* provided by the vendor at load time during the initial handshake.
|
||||
* - dynamic EGL dispatch table: this is a structure allocated by the API
|
||||
* library at runtime used to manage EGL extension functions which are not
|
||||
* present in the static table.
|
||||
* - core GL dispatch table: this is a structure maintained by the API library
|
||||
* which contains both GL core (static) and GL extension (dynamic) functions.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/*
|
||||
* Rendering API handling:
|
||||
*
|
||||
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
|
||||
* other API is added, then the major version number will be incremented.
|
||||
*
|
||||
* When the application calls eglBindAPI, libEGL will forward the call to every
|
||||
* vendor library. In addition, a vendor library can query the current API from
|
||||
* libEGL using the getCurrentApi callback.
|
||||
*
|
||||
* Vendor libraries are not required to support both GL and GLES, but they must
|
||||
* be able to deal with either one as the current rendering API. If a vendor
|
||||
* doesn't support the current API, then it should return an error from
|
||||
* eglCreateContext.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Current version of the ABI.
|
||||
*
|
||||
* This version number contains a major number in the high-order 16 bits, and
|
||||
* a minor version number in the low-order 16 bits.
|
||||
*
|
||||
* The major version number is incremented when an interface change will break
|
||||
* backwards compatibility with existing vendor libraries. The minor version
|
||||
* number is incremented when there's a change but existing vendor libraries
|
||||
* will still work.
|
||||
*/
|
||||
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
|
||||
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
|
||||
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
|
||||
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
|
||||
{
|
||||
return version >> 16;
|
||||
}
|
||||
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
|
||||
{
|
||||
return version & 0xFFFF;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* This opaque structure stores function pointers for EGL extension functions.
|
||||
* It is allocated at runtime by the API library. Vendor-provided dispatch
|
||||
* functions retrieve and operate on this structure using the API below.
|
||||
*/
|
||||
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
|
||||
|
||||
/****************************************************************************
|
||||
* API library exports *
|
||||
****************************************************************************/
|
||||
|
||||
typedef struct __EGLapiExportsRec {
|
||||
/************************************************************************
|
||||
* The following routines are used by vendor-implemented EGL dispatch
|
||||
* functions to lookup and call into the right vendor.
|
||||
************************************************************************/
|
||||
|
||||
/*!
|
||||
* This function must be called at the start of every EGL dispatch stub. It
|
||||
* performs any necessary per-call bookkeeping.
|
||||
*/
|
||||
void (* threadInit) (void);
|
||||
|
||||
EGLenum (* getCurrentApi)(void);
|
||||
|
||||
/*!
|
||||
* This function retrieves the appropriate current dynamic dispatch table,
|
||||
* if a GL context is current. Otherwise, this returns NULL.
|
||||
*/
|
||||
__EGLvendorInfo *(*getCurrentVendor)(void);
|
||||
|
||||
/*!
|
||||
* This retrieves the current context for this thread.
|
||||
*/
|
||||
EGLContext (*getCurrentContext)(void);
|
||||
|
||||
/*!
|
||||
* Returns the current display for this thread.
|
||||
*/
|
||||
EGLDisplay (*getCurrentDisplay)(void);
|
||||
|
||||
/*!
|
||||
* Returns the current drawable for this thread.
|
||||
*/
|
||||
EGLSurface (*getCurrentSurface)(EGLint readDraw);
|
||||
|
||||
/*!
|
||||
* This function retrieves an entry point from the dynamic dispatch table
|
||||
* given an index into the table.
|
||||
*/
|
||||
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
|
||||
|
||||
/*!
|
||||
* Sets the last error for the current thread. The error will be returned
|
||||
* the next time the app calls eglGetError().
|
||||
*
|
||||
* This function will override a call to \c setLastVendor, and vice-versa.
|
||||
*/
|
||||
void (* setEGLError) (EGLint errorCode);
|
||||
|
||||
/*!
|
||||
* Notifies libEGL about the vendor library that an EGL function is
|
||||
* dispatched to. This is used to look up the last EGL error code from a
|
||||
* vendor.
|
||||
*
|
||||
* A vendor library may generate an EGL error asynchronously, if it
|
||||
* offloads some EGL functions onto a worker thread. In that case, calling
|
||||
* \c setEGLError to set the last error would not work.
|
||||
*
|
||||
* When an EGL dispatch stubs finds the vendor library, it must call this
|
||||
* function before it dispatches the function call.
|
||||
*
|
||||
* This function will override a call to \c setEGLError, and vice-versa.
|
||||
*
|
||||
* \param vendor The vendor library of the current EGL function call.
|
||||
* \return EGL_TRUE on success, or EGL_FALSE on failure.
|
||||
*/
|
||||
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
|
||||
|
||||
/************************************************************************
|
||||
* These routines are used by vendor dispatch functions to look up
|
||||
* and add mappings between various objects and vendors.
|
||||
************************************************************************/
|
||||
|
||||
// As far as I can tell, all EGL functions that take a context or drawable
|
||||
// also take an EGLDisplay. So, we don't need any extra functions for
|
||||
// mapping a context/surface/whatever to a EGLDisplay or vendor.
|
||||
|
||||
/*!
|
||||
* Returns the EGL vendor for an EGLDisplay handle.
|
||||
*/
|
||||
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
|
||||
|
||||
/*!
|
||||
* Returns the EGL vendor for an EGLDeviceEXT handle.
|
||||
*/
|
||||
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
|
||||
|
||||
/**
|
||||
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
|
||||
* any function that returns an EGLDeviceEXT handle should call this.
|
||||
*
|
||||
* Supported since ABI version 1.2.
|
||||
*/
|
||||
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
|
||||
} __EGLapiExports;
|
||||
|
||||
/*****************************************************************************
|
||||
* API library imports *
|
||||
*****************************************************************************/
|
||||
|
||||
/*!
|
||||
* The enum values accepted by \c __EGLapiImports::getVendorString.
|
||||
*/
|
||||
enum
|
||||
{
|
||||
/*!
|
||||
* The set of platform extensions that the vendor supports.
|
||||
*
|
||||
* Platform extensions typically just add enums to the existing
|
||||
* eglGetPlatformDisplay function, so we don't need any additional logic
|
||||
* or special handling from libEGL itself.
|
||||
*
|
||||
* As a result, vendor libraries can add these extensions without having
|
||||
* to modify libEGL to match.
|
||||
*/
|
||||
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
|
||||
};
|
||||
|
||||
/*!
|
||||
* This structure stores required and optional vendor library callbacks.
|
||||
*/
|
||||
typedef struct __EGLapiImportsRec {
|
||||
/**
|
||||
* Creates an EGLDisplay. This function is used to handle both
|
||||
* \c eglGetDisplay and \c eglGetPlatformDisplay.
|
||||
*
|
||||
* For some platform types, libEGL may be able to identify a specific
|
||||
* vendor to use.
|
||||
*
|
||||
* If libEGL can't figure out which vendor to use on its own, then it will
|
||||
* go through the list of available vendor libraries and call this function
|
||||
* until one succeeds.
|
||||
*
|
||||
* If the application calls eglGetPlatformDisplay, then the parameters are
|
||||
* passed through from the application.
|
||||
*
|
||||
* If the application calls eglGetDisplay, and the native display handle is
|
||||
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
|
||||
* type, and will pass that platform to the vendor library.
|
||||
*
|
||||
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
|
||||
* libEGL will call into each vendor library with \p platform set to
|
||||
* \c EGL_NONE. The vendor library can then select a default display to
|
||||
* return.
|
||||
*
|
||||
* In all cases, if the vendor library can't find a matching EGLDisplay,
|
||||
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
|
||||
* through the vendor's \c eglGetError function.
|
||||
*/
|
||||
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
|
||||
const EGLAttrib *attrib_list);
|
||||
|
||||
/*!
|
||||
* Checks if the vendor library supports a given client API (that is, the
|
||||
* API value passed to \c eglBindAPI).
|
||||
*/
|
||||
EGLBoolean (* getSupportsAPI) (EGLenum api);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Returns a string from the vendor library. This is used for
|
||||
* anything that isn't available from eglQueryString.
|
||||
*/
|
||||
const char * (* getVendorString) (int name);
|
||||
|
||||
/*
|
||||
* NOTE: libEGL will load all of the vendor libraries up front, so it
|
||||
* handles extension functions slightly differently than EGL.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* This retrieves the pointer to the real EGL or core GL function.
|
||||
*
|
||||
* \param procName The name of the function.
|
||||
* \return A pointer to a function, or \c NULL if the vendor does not
|
||||
* support the function.
|
||||
*/
|
||||
void * (* getProcAddress) (const char *procName);
|
||||
|
||||
/*!
|
||||
* This retrieves vendor-neutral functions which use the
|
||||
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
|
||||
*
|
||||
* A vendor library must provide a dispatch function for all EGL display
|
||||
* extension functions that it supports.
|
||||
*
|
||||
* Client extension functions cannot be dispatched based on an EGDisplay,
|
||||
* so they must be handled in libEGL itself.
|
||||
*
|
||||
* \param procName The name of the function.
|
||||
* \return A pointer to a function, or \c NULL if the vendor does not
|
||||
* support the function or \p procName is not a EGL display function.
|
||||
*/
|
||||
void * (*getDispatchAddress) (const char *procName);
|
||||
|
||||
/*!
|
||||
* This notifies the vendor library which dispatch table index is
|
||||
* assigned to a particular EGL extension function.
|
||||
*/
|
||||
void (*setDispatchIndex) (const char *procName, int index);
|
||||
|
||||
/*
|
||||
* The vendor library may use the isPatchSupported, initiatePatch,
|
||||
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
|
||||
* entrypoints at make current time, provided no other contexts are current
|
||||
* and the TLS model supports this functionality. This is a performance
|
||||
* optimization that may not be available at runtime; the vendor library
|
||||
* must not depend on this functionality for correctness.
|
||||
*
|
||||
* To use this optimization, the vendor library must provide at least the
|
||||
* isPatchSupported and initiatePatch entrypoints.
|
||||
*
|
||||
* Note that these functions are identical to the ones used in GLX.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Checks to see if the vendor library supports patching the
|
||||
* given stub type and size.
|
||||
*
|
||||
* \param type The type of entrypoints. This will be a one of the
|
||||
* __GLDISPATCH_STUB_* values.
|
||||
* \param stubSize The maximum size of the stub that the vendor library can
|
||||
* write, in bytes.
|
||||
* \param lookupStubOffset A callback into libglvnd to look up the address
|
||||
* of each entrypoint.
|
||||
*/
|
||||
GLboolean (* isPatchSupported)(int type, int stubSize);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
|
||||
* top-level entrypoints.
|
||||
*
|
||||
* The vendor library should use the \p lookupStubOffset callback to find
|
||||
* the addresses of each entrypoint.
|
||||
*
|
||||
* This function may be called more than once to patch multiple sets of
|
||||
* entrypoints. For example, depending on how they're built, libOpenGL.so
|
||||
* or libGL.so may have their own entrypoints that are separate functions
|
||||
* from the ones in libGLdispatch.
|
||||
*
|
||||
* Note that during this call is the only time that the entrypoints can be
|
||||
* modified. After the call to \c initiatePatch returns, the vendor library
|
||||
* should treat the entrypoints as read-only.
|
||||
*
|
||||
* \param type The type of entrypoints. This will be a one of the
|
||||
* __GLDISPATCH_STUB_* values.
|
||||
* \param stubSize The maximum size of the stub that the vendor library can
|
||||
* write, in bytes.
|
||||
* \param lookupStubOffset A callback into libglvnd to look up the address
|
||||
* of each entrypoint.
|
||||
*
|
||||
* \return GL_TRUE if the vendor library supports patching with this type
|
||||
* and size.
|
||||
*/
|
||||
GLboolean (*initiatePatch)(int type,
|
||||
int stubSize,
|
||||
DispatchPatchLookupStubOffset lookupStubOffset);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
|
||||
* longer owns the top-level entrypoints.
|
||||
*
|
||||
* Libglvnd will take care of the restoring the entrypoints back to their
|
||||
* original state. The vendor library must not try to modify them.
|
||||
*/
|
||||
void (*releasePatch)(void);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
|
||||
* This callback allows vendor libraries to perform any per-thread
|
||||
* initialization.
|
||||
*
|
||||
* This is basically a workaround for broken applications. A lot of apps
|
||||
* will make one or more invalid GLX/EGL calls on a thread (often including
|
||||
* a MakeCurrent with invalid parameters), and then will try to call an
|
||||
* OpenGL function.
|
||||
*
|
||||
* A non-libglvnd-based driver would be able to initialize any thread state
|
||||
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
|
||||
* past libGLX.
|
||||
*
|
||||
* This function is optional. If it's \c NULL, then libGLdispatch will
|
||||
* simply ignore it.
|
||||
*
|
||||
* \note This function may be called concurrently from multiple threads.
|
||||
*/
|
||||
void (*patchThreadAttach)(void);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Tries to determine the platform type for a native display.
|
||||
*
|
||||
* If the vendor library provides this function, then libglvnd will call it
|
||||
* to determine which platform to use for a native display handle in
|
||||
* eglGetDisplay.
|
||||
*
|
||||
* If no vendor library identifies the platform, then libglvnd will fall
|
||||
* back to its own platform detection logic.
|
||||
*
|
||||
* Libglvnd can call this function for any native display handle except
|
||||
* \c EGL_DEFAULT_DISPLAY.
|
||||
*
|
||||
* No matter what the value of \p native_display, the vendor library must
|
||||
* not crash, and must not return a false match. If the vendor library
|
||||
* can't identify the display, then it must return \c EGL_NONE.
|
||||
*
|
||||
* In particular, that means that a vendor library must not return any sort
|
||||
* of default or fallback platform.
|
||||
*
|
||||
* \param native_display The native display handle passed to eglGetDisplay.
|
||||
* \return Either a platform type enum or EGL_NONE.
|
||||
*/
|
||||
EGLenum (* findNativeDisplayPlatform) (void *native_display);
|
||||
} __EGLapiImports;
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
|
||||
|
||||
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
|
||||
__EGLvendorInfo *vendor, __EGLapiImports *imports);
|
||||
|
||||
/*!
|
||||
* Vendor libraries must export a function called __egl_Main() with the
|
||||
* following prototype.
|
||||
*
|
||||
* This function also performs a handshake based on the ABI version number.
|
||||
* Vendor libraries can optionally use the version number to support older
|
||||
* versions of the ABI.
|
||||
*
|
||||
* \param[in] version The ABI version. The upper 16 bits contains the major version
|
||||
* number, and the lower 16 bits contains the minor version number.
|
||||
*
|
||||
* \param[in] exports The table of functions provided by libEGL. This pointer will
|
||||
* remain valid for as long as the vendor is loaded.
|
||||
*
|
||||
* \param[in] vendor The opaque pointer used to identify this vendor library. This
|
||||
* may be used in future versions to provide additional per-vendor information.
|
||||
*
|
||||
* \param[out] imports The function table that the vendor library should fill
|
||||
* in. The vendor library must assign every non-optional function in the
|
||||
* struct.
|
||||
*
|
||||
* \return True on success. If the vendor library does not support the
|
||||
* requested ABI version or if some other error occurs, then it should return
|
||||
* False.
|
||||
*/
|
||||
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
|
||||
__EGLvendorInfo *vendor, __EGLapiImports *imports);
|
||||
|
||||
/*!
|
||||
* @}
|
||||
*/
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __LIB_EGL_ABI_H */
|
||||
+104
@@ -403,7 +403,10 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
|
||||
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
|
||||
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
|
||||
|
||||
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
|
||||
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
|
||||
@@ -434,10 +437,14 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Backend/Init.cpp
|
||||
MobileGL/MG_Backend/BackendObject.cpp
|
||||
|
||||
MobileGL/MG_Backend/HostFrameBridge.cpp
|
||||
MobileGL/MG_Backend/GbmFrameChannel.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||
@@ -640,6 +647,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
MobileGL/MG_Remote/Client/DmaBufImport.cpp
|
||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
|
||||
@@ -778,6 +786,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# target, and above all no flatc in the build graph. protocol_generated.h
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
|
||||
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
@@ -815,6 +826,17 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
# A LINUX BUILD COMPILES THE DirectVulkan BACKEND BUT HAS NO VULKAN LOADER TO LINK AGAINST, so the
|
||||
# resulting shared library carries undefined vk* symbols - and that is not theoretical: glvnd dlopens
|
||||
# this file as the EGL vendor with RTLD_NOW, so the load fails outright with "undefined symbol:
|
||||
# vkGetQueryPoolResults" (measured on the phone's container). Android links the loader already; on
|
||||
# Linux the caller passes the runtime's own libvulkan.so.1, and leaving the variable empty keeps the
|
||||
# previous behaviour for every build that has no Vulkan runtime to point at.
|
||||
set(MOBILEGL_VULKAN_LOADER "" CACHE FILEPATH "Vulkan loader (libvulkan.so.1) to link into the GL library")
|
||||
if (UNIX AND NOT ANDROID AND MOBILEGL_VULKAN_LOADER)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${MOBILEGL_VULKAN_LOADER})
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
@@ -881,6 +903,68 @@ if(NOT ANDROID)
|
||||
)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MobileGLGbm: the GBM drop-in the container's compositor loads.
|
||||
#
|
||||
# kwin - the Wayland compositor in the container half of the split - allocates the
|
||||
# buffers it renders into and scans out through libgbm, and the dma-buf fds it gets
|
||||
# back are what the Anland display producer ships to the phone. Mesa's libgbm cannot
|
||||
# serve that: its buffers belong to the host GPU, and the phone would receive a copy
|
||||
# through a path that was never designed to carry one. This target is the library
|
||||
# that is loaded in its place, under the name a loader looks for.
|
||||
#
|
||||
# It is deliberately NOT part of libMobileGL.so. The compositor needs one file named
|
||||
# libgbm.so that it loads instead of Mesa's, and folding this code into the GL
|
||||
# library would put a second GL frontend, a second Vulkan instance and a second
|
||||
# glslang in the compositor's address space to serve an API that shares none of them.
|
||||
#
|
||||
# The include paths below are borrowed rather than linked. Includes.h - which this
|
||||
# library includes for the MobileGL types and the MGLOG macros - pulls in the GL,
|
||||
# EGL, Vulkan, glslang, SPIRV-Cross and xxHash HEADERS transitively, and headers are
|
||||
# all this library wants from them: linking any of those static libraries into a
|
||||
# libgbm would embed a copy of each in a process that already has one.
|
||||
#
|
||||
# -Bsymbolic-functions, for the same reason the main target carries it: the point of
|
||||
# this library is to REPLACE a libgbm that may already be loaded in the process, so
|
||||
# every internal reference has to bind to this file's own definitions rather than to
|
||||
# whatever else the process has under the same names.
|
||||
# ---------------------------------------------------------------------------
|
||||
add_library(MobileGLGbm SHARED
|
||||
MobileGL/MG_Gbm/GbmDevice.cpp
|
||||
# The display host's socket protocol, kept in its own translation unit so that the
|
||||
# allocation paths never have to know how a frame arrives - only that one did.
|
||||
MobileGL/MG_Gbm/GbmHostChannel.cpp
|
||||
MobileGL/MG_Gbm/GbmApi.cpp
|
||||
# MGLOG_I/W/E expand to a call to MobileGL::MG_Util::Debug::Log, and this is the
|
||||
# one translation unit that defines it. It is compiled in rather than linked from
|
||||
# libMobileGL.so because this library has to load into a compositor that has no
|
||||
# libMobileGL in it at all - and because a libgbm with an undefined symbol is a
|
||||
# library that fails on the first diagnostic rather than at a link step somebody
|
||||
# could have read.
|
||||
MobileGL/MG_Util/Debug/Log.cpp
|
||||
)
|
||||
|
||||
# The vendored gbm.h first: this library implements THAT declaration set, and a
|
||||
# system <gbm.h> from another Mesa version on the include path ahead of it would
|
||||
# change the ABI under the implementations in GbmApi.cpp.
|
||||
target_include_directories(MobileGLGbm PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/gbm/include
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/glslang
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Cross
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/xxHash
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
|
||||
)
|
||||
|
||||
set_target_properties(MobileGLGbm PROPERTIES
|
||||
OUTPUT_NAME gbm
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_options(MobileGLGbm PRIVATE "LINKER:-Bsymbolic-functions")
|
||||
endif()
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
|
||||
@@ -888,6 +972,19 @@ if (TRACY_ENABLE)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
|
||||
endif ()
|
||||
|
||||
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
|
||||
#
|
||||
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
|
||||
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
|
||||
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
|
||||
# With default visibility every reference inside this library resolves against the whole process,
|
||||
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
|
||||
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
|
||||
# the two visibilities do not mix; this flag is that promise made by the linker instead.
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
android
|
||||
@@ -1156,3 +1253,10 @@ if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The GBM library carries the tree's logger, which reaches Android's own log. The main target gets
|
||||
# these libraries in its own Android block; a library built from the same sources needs them for the
|
||||
# same reason, and without them it is the LINK that fails rather than anything in the code.
|
||||
if (ANDROID)
|
||||
target_link_libraries(MobileGLGbm PRIVATE log android)
|
||||
endif()
|
||||
|
||||
@@ -499,6 +499,7 @@ namespace MobileGL::MG_Config {
|
||||
enum class IpcSurface : Uint8 {
|
||||
Offscreen = 0, // a window surface is the client's own window, as before (the default)
|
||||
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
|
||||
Host = 2, // a window surface draws into frames the DISPLAY HOST owns; the host presents
|
||||
};
|
||||
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
|
||||
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
|
||||
@@ -729,6 +730,15 @@ namespace MobileGL::MG_Config {
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
|
||||
// P12: the other on-screen shape, and the mirror image of the one above. The display host
|
||||
// (the Android side that owns the screen) allocates the frames, offers each one over the
|
||||
// host frame bridge, and puts it on the glass when this side says it is drawn - so a
|
||||
// client that asks for this has no window of its own either, and the server takes the
|
||||
// frames rather than making a window.
|
||||
inline Bool HostFrameWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Host;
|
||||
}
|
||||
#else
|
||||
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||
|
||||
@@ -494,8 +494,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
}
|
||||
}
|
||||
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
|
||||
// the semantics). An unknown value is named and read as the default, like every knob here.
|
||||
// P12 (on-screen): MOBILEGL_IPC_SURFACE = offscreen | server | host (Config.h has the
|
||||
// semantics). An unknown value is named and read as the default, like every knob here.
|
||||
{
|
||||
String surface;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
|
||||
@@ -505,9 +505,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
} else if (surface == "server") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Server;
|
||||
} else if (surface == "host") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Host;
|
||||
} else {
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
|
||||
"offscreen|server, using offscreen",
|
||||
"offscreen|server|host, using offscreen",
|
||||
surface.c_str());
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
}
|
||||
@@ -515,12 +517,14 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// parses this before RunSession sets its own transport, and has no client half anyway.)
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
|
||||
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
|
||||
if (ipc.Surface != MG_Config::IpcSurface::Offscreen && !serverProcess &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// Said, not silently honoured: there is no remote server here to own a window.
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
|
||||
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
|
||||
"and this run has none. Window surfaces stay the client's own");
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=%s is IGNORED - it asks for an on-screen "
|
||||
"surface (the server's own window, or the frames the display host owns) and "
|
||||
"this run has no remote server to provide one: MOBILEGL_TRANSPORT is not "
|
||||
"spawn. Window surfaces stay the client's own",
|
||||
ipc.Surface == MG_Config::IpcSurface::Server ? "server" : "host");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -8,6 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
// ============== Platform-specific definitions and macros ============== //
|
||||
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
|
||||
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
|
||||
@@ -34,6 +36,11 @@
|
||||
#ifdef _WIN32
|
||||
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
|
||||
// points with. The one place where that is not enough - a vendor library handing glvnd its own
|
||||
// entry points, where a reference resolving against the process instead of this library would be a
|
||||
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
|
||||
// here, because a second visibility on the same declaration does not compile.
|
||||
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
@@ -105,8 +112,8 @@
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
|
||||
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
|
||||
fprintf(stderr, "Assertion failed" __VA_OPT__(": ") "%s", "" __VA_ARGS__); \
|
||||
fprintf(stderr, " at %s:%d (%s)\n", __FILE__, __LINE__, __func__); \
|
||||
TRAP; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -172,11 +172,15 @@ namespace MobileGL::MG_Backend {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
|
||||
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
|
||||
return false;
|
||||
}
|
||||
|
||||
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
|
||||
// refused a caller for a reason that is not there: the surface and context tables are keyed
|
||||
// by the display each was created on, every operation carries its own display handle, and
|
||||
// this field has no reader anywhere - it was the whole of the rule.
|
||||
//
|
||||
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
|
||||
// probing another, so the second eglInitialize is the one that matters, and a refusal there
|
||||
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
|
||||
// and the real display having failed, with nothing in between that says which was refused.
|
||||
m_eglDisplay = dpy;
|
||||
m_eglDisplayInitialized = true;
|
||||
if (major) {
|
||||
@@ -209,6 +213,28 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The frame itself is the backend's business (InitHostFrameSurface); what happens here is
|
||||
// the registration every surface kind shares, so the swap path and the state mirrors see
|
||||
// a host-framed surface exactly like any other.
|
||||
Bool BackendObject::CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglDisplayInitialized) {
|
||||
MGLOG_E("CreateEGLHostFrameSurface failed: EGL display is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("CreateEGLHostFrameSurface failed: invalid EGLSurface");
|
||||
return false;
|
||||
}
|
||||
auto& state = m_eglSurfaces[surface];
|
||||
state = EGLSurfaceState{
|
||||
.Kind = SurfaceKind::HostFrame,
|
||||
.Width = static_cast<EGLint>(std::max<Uint32>(width, 1)),
|
||||
.Height = static_cast<EGLint>(std::max<Uint32>(height, 1)),
|
||||
};
|
||||
return ActivateEGLSurface(surface);
|
||||
}
|
||||
|
||||
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
|
||||
@@ -290,6 +316,11 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
|
||||
return false;
|
||||
}
|
||||
} else if (surfaceState->Kind == SurfaceKind::HostFrame) {
|
||||
if (!InitHostFrameSurface(surfaceState->Width, surfaceState->Height)) {
|
||||
MGLOG_E("ActivateEGLSurface failed: backend InitHostFrameSurface failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
|
||||
return false;
|
||||
@@ -315,6 +346,29 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
|
||||
return false;
|
||||
}
|
||||
// EGL_KHR_surfaceless_context. A context whose draw and read surfaces are both absent has
|
||||
// no framebuffer of its own to be bound to: the caller draws into framebuffers it binds
|
||||
// itself, and this side only has to have its own GL context current on this thread for the
|
||||
// records that follow. Everything below this point describes a surface, so none of it
|
||||
// applies - and what is recorded is exactly what was asked for, so a later swap on a
|
||||
// surface that does not exist is never made to look like one that does.
|
||||
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
|
||||
if (!m_backendCapabilitiesInitialized) {
|
||||
if (!InitCapabilities()) {
|
||||
MGLOG_E("MakeEGLCurrent failed: InitCapabilities failed");
|
||||
return false;
|
||||
}
|
||||
m_backendCapabilitiesInitialized = true;
|
||||
}
|
||||
ReleaseEGLCurrentThread(threadKey);
|
||||
m_eglCurrentThreads[threadKey] = EGLCurrentState{
|
||||
.Display = dpy,
|
||||
.DrawSurface = EGL_NO_SURFACE,
|
||||
.ReadSurface = EGL_NO_SURFACE,
|
||||
.Context = ctx,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
if (draw != read || !ActivateEGLSurface(draw)) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
@@ -387,6 +441,12 @@ namespace MobileGL::MG_Backend {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A host-framed surface has no swapchain: presenting it means telling the host the frame
|
||||
// is drawn, because the host is what puts it on the glass.
|
||||
if (m_eglSurfaceKind == SurfaceKind::HostFrame) {
|
||||
return PresentHostFrame();
|
||||
}
|
||||
|
||||
const auto& backendFunctions = GetBackendFunctions();
|
||||
if (!backendFunctions.Present) {
|
||||
MGLOG_E("SwapEGLBuffers failed: backend Present function is null");
|
||||
@@ -404,6 +464,27 @@ namespace MobileGL::MG_Backend {
|
||||
}
|
||||
}
|
||||
|
||||
// A backend that has no host-framed surfaces says so by name: the server may ask for one
|
||||
// (MOBILEGL_IPC_SURFACE=host) against a build that cannot serve it, and that has to be a
|
||||
// refusal with a reason rather than a surface that draws nowhere.
|
||||
Bool BackendObject::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
(void)width;
|
||||
(void)height;
|
||||
MGLOG_E("InitHostFrameSurface: this backend has no host-framed surfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject::PresentHostFrame() {
|
||||
MGLOG_E("PresentHostFrame: this backend has no host-framed surfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject::PresentHostFrameIfCurrent() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglSurfaceInitialized || m_eglSurfaceKind != SurfaceKind::HostFrame) return true;
|
||||
return PresentHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
return false;
|
||||
|
||||
@@ -595,6 +595,18 @@ namespace MobileGL {
|
||||
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
|
||||
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
||||
// The host owns the screen: this side never gets a window of the client's to draw
|
||||
// into, so the surface it draws into is a frame the host allocated and offered over
|
||||
// the host frame bridge. Each swap tells the host the frame is done and takes the
|
||||
// next; presentation is the host's, not a swapchain's.
|
||||
virtual Bool CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
virtual Bool InitHostFrameSurface(EGLint width, EGLint height);
|
||||
virtual Bool PresentHostFrame();
|
||||
// The present RECORD's half of a host-framed swap. A split client's swap reaches the
|
||||
// server as a present record (ServerVerbSink::OnPresent), never as SwapEGLBuffers, so
|
||||
// without this the host was never told a frame was drawn. True when the current
|
||||
// surface is not host-framed: there is nothing to present then.
|
||||
Bool PresentHostFrameIfCurrent();
|
||||
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
// Forwards the app-requested eglSwapInterval to the backend's native
|
||||
@@ -616,7 +628,11 @@ namespace MobileGL {
|
||||
enum class SurfaceKind {
|
||||
None,
|
||||
Window,
|
||||
Pbuffer
|
||||
Pbuffer,
|
||||
// A surface whose drawing target is a frame the display host owns. There is no
|
||||
// swapchain behind it: the host puts the frame on the glass when this side says
|
||||
// it is drawn, which is why presenting one is its own call and not a Present.
|
||||
HostFrame
|
||||
};
|
||||
|
||||
struct EGLCurrentState {
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
// The role's own backend is resolved the way Utils.cpp (C6 / ID-52) resolves it; that needs this
|
||||
// header, and it has to be included at file scope - a header included inside this file's namespace
|
||||
// would nest its own namespace under it.
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
@@ -1028,6 +1034,241 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
namespace {
|
||||
// The host may still be starting when the client asks for its surface: it is a separate
|
||||
// app that has to come up and allocate its pool. Waiting is better than refusing a
|
||||
// surface a client is entitled to, and the wait is answered by name if it expires.
|
||||
constexpr Int kHostFrameWaitMs = 30000;
|
||||
}
|
||||
|
||||
Bool TakeHostFrameForExternalTexture(Uint width, Uint height) {
|
||||
// THIS ROLE'S OWN BACKEND, resolved exactly the way ActiveBackendFormatCaps (Utils.cpp,
|
||||
// C6 / ID-52) resolves it: under a live split session the SERVER's private backend owns the
|
||||
// context on the apply thread, and pActiveBackendObject is the CLIENT's mirror there.
|
||||
MG_Backend::BackendObject* backend = nullptr;
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
} else {
|
||||
backend = MG_Backend::pActiveBackendObject.get();
|
||||
}
|
||||
if (backend == nullptr) return false;
|
||||
return backend->InitHostFrameSurface(static_cast<EGLint>(width), static_cast<EGLint>(height));
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
// The EGL context comes from the same place a pbuffer session's does. What changes is not
|
||||
// how this side draws but what it draws INTO: a frame the display host allocated, bound as
|
||||
// this context's framebuffer. The pbuffer stays as the draw target for the moment before
|
||||
// the first frame arrives, which is the only moment it is used.
|
||||
if (!DirectGLES::InitPbufferSurface(width, height)) {
|
||||
MGLOG_E("InitHostFrameSurface: the context the host frames are drawn in did not come up");
|
||||
return false;
|
||||
}
|
||||
if (!m_hostFrameBridgeOpen) {
|
||||
String why;
|
||||
if (!m_hostFrameBridge.Open(MG_Backend::kHostFrameSocket, kHostFrameWaitMs, why)) {
|
||||
MGLOG_E("InitHostFrameSurface: no display host: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
m_hostFrameBridgeOpen = true;
|
||||
}
|
||||
return TakeHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::TakeHostFrame() {
|
||||
String why;
|
||||
struct AHardwareBuffer* frame = nullptr;
|
||||
MG_Backend::HostFrameOffer offer{};
|
||||
if (!m_hostFrameBridge.Acquire(&frame, offer, why)) {
|
||||
MGLOG_E("TakeHostFrame: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
HostFrameTarget target;
|
||||
if (!HostFrameTargetCreate(frame, target, why)) {
|
||||
MGLOG_E("TakeHostFrame: the host frame %ux%u did not become a draw target: %s", offer.Width,
|
||||
offer.Height, why.c_str());
|
||||
AHardwareBuffer_release(frame);
|
||||
return false;
|
||||
}
|
||||
DestroyHostFrame();
|
||||
m_hostFrameTarget = target;
|
||||
m_hostFrameBuffer = frame;
|
||||
m_hostFrameOffer = offer;
|
||||
// The canvas is made at the first frame's size and kept; a failure leaves the frame itself
|
||||
// as the draw target, which is how this path behaved before the canvas existed.
|
||||
if (!EnsureHostFrameCanvas(target.Width, target.Height))
|
||||
MGLOG_E_ONCE("TakeHostFrame: no canvas in front of the host frames; drawing into the frame itself");
|
||||
const HostFrameTarget& drawTarget = HostFrameDrawTarget();
|
||||
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
|
||||
// resolve to this target, and that resolution is what this global is.
|
||||
g_hostFrameFramebufferId = drawTarget.Framebuffer;
|
||||
// Published together with the framebuffer, because they describe the same target: the STORAGE
|
||||
// an attachment is bound to when the client's texture has none of its own, and the SIZE that
|
||||
// decides whether binding it is right at all.
|
||||
g_hostFrameRenderbuffer = drawTarget.Renderbuffer;
|
||||
g_hostFrameWidth = drawTarget.Width;
|
||||
g_hostFrameHeight = drawTarget.Height;
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
HostFrameTargetBind(drawTarget);
|
||||
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
|
||||
// over there renders into THESE frames - it runs this project's own GBM, whose
|
||||
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
|
||||
// every frame taken here is useless to it until its descriptor has crossed. Open() binds
|
||||
// the listening socket a container dials, and the IsUp() test keeps the ordinary frame
|
||||
// down to an integer compare: the socket outlives every container and every connection,
|
||||
// while OfferFrame accepts whoever dialled since the last frame and opens the socket
|
||||
// itself when the first frame arrives before any container has.
|
||||
//
|
||||
// NEITHER ANSWER IS EXAMINED, and that is the channel's contract rather than an
|
||||
// oversight: no container running, a container that has died, or one that has taken the
|
||||
// descriptor and stopped reading must all leave this frame drawn and presented exactly as
|
||||
// it would have been without a channel - the descriptor is a copy of the pixels' route,
|
||||
// not a step in producing them.
|
||||
if (!m_gbmFrameChannel.IsUp()) m_gbmFrameChannel.Open();
|
||||
(void)m_gbmFrameChannel.OfferFrame(frame, offer);
|
||||
MGLOG_I("host frame %u is the draw target: %ux%u", offer.Index, target.Width, target.Height);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::DestroyHostFrame() {
|
||||
// The canvas outlives the frame, so with one the globals keep naming it: the attachments
|
||||
// made over it stay valid between this frame being handed back and the next one arriving.
|
||||
g_hostFrameFramebufferId = m_hostFrameCanvas.Framebuffer;
|
||||
g_hostFrameRenderbuffer = m_hostFrameCanvas.Renderbuffer;
|
||||
g_hostFrameWidth = m_hostFrameCanvas.Width;
|
||||
g_hostFrameHeight = m_hostFrameCanvas.Height;
|
||||
HostFrameTargetDestroy(m_hostFrameTarget);
|
||||
if (m_hostFrameBuffer != nullptr) {
|
||||
AHardwareBuffer_release(m_hostFrameBuffer);
|
||||
m_hostFrameBuffer = nullptr;
|
||||
}
|
||||
// AND NOTHING IS SAID TO THE CHANNEL HERE, which is the fix rather than an omission. This
|
||||
// runs once per presented frame: it used to Close() the channel (unlinking the container's
|
||||
// socket node and dropping its connection sixty times a second - measured: connect()
|
||||
// answered EAGAIN(11) on every attempt) and it used to tell the channel to forget the frame,
|
||||
// which left the ONE window a compositor's first allocations arrive in - between this frame
|
||||
// being presented and the host handing the next one over, i.e. inside Complete() above -
|
||||
// with nothing to answer a dialer with. A compositor allocates its swapchain ONCE and keeps
|
||||
// those buffers for the life of the process, so a dialer that finds nothing here renders
|
||||
// its whole life into buffers nobody displays. The frame the channel remembers is the last
|
||||
// one the host handed over: the next offer replaces it and Close() releases it.
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::PresentHostFrame() {
|
||||
// When this returns, the pixels are complete: glFinish is the fence the host is told
|
||||
// about, and the host's own readback - which Complete reads - is what proves the drawing
|
||||
// landed where the screen will look.
|
||||
BlitHostFrameCanvas();
|
||||
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
|
||||
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
|
||||
// server's apply loop passes through the host channel - the swap it runs lands here for a
|
||||
// host-framed surface - and a release is the container's compositor saying it is done
|
||||
// reading a frame this side handed over. Poll() is a bare recvmsg on a non-blocking
|
||||
// socket: with no container connected it returns on its first test, and a container that
|
||||
// is between frames answers EAGAIN, so a pass costs nothing when there is nothing to
|
||||
// read. Nothing here is waited for and nothing is required to arrive; the count of
|
||||
// frames offered and not released is a number the channel keeps for the log, never a
|
||||
// frame the render path waits on.
|
||||
m_gbmFrameChannel.Poll();
|
||||
String why;
|
||||
if (!m_hostFrameBridge.Complete(m_hostFrameOffer, /*imported=*/true, /*drawn=*/true, /*fenceOk=*/true, why)) {
|
||||
MGLOG_E("PresentHostFrame: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
// The answer has been read, so the host has already put this frame on the glass: it is
|
||||
// this side's to drop, and the next one becomes the drawing target.
|
||||
DestroyHostFrame();
|
||||
return TakeHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint width, Uint height) {
|
||||
if (m_hostFrameCanvas.Framebuffer != 0) return true;
|
||||
if (width == 0 || height == 0 || !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glBlitFramebuffer ||
|
||||
!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glCheckFramebufferStatus)
|
||||
return false;
|
||||
// RAW binds, as HostFrameTargetCreate makes them: the caller invalidates the engine's
|
||||
// framebuffer shadow and binds through it right after.
|
||||
GLuint renderbuffer = 0;
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
// The frames are R8G8B8A8_UNORM (the display host allocates them so), and so is this.
|
||||
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height));
|
||||
GLuint framebuffer = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("host frame canvas %ux%u: framebuffer status 0x%04x", width, height, status);
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
return false;
|
||||
}
|
||||
m_hostFrameCanvas.Renderbuffer = renderbuffer;
|
||||
m_hostFrameCanvas.Framebuffer = framebuffer;
|
||||
m_hostFrameCanvas.Width = width;
|
||||
m_hostFrameCanvas.Height = height;
|
||||
MGLOG_I("host frame canvas: %ux%u as framebuffer %u (renderbuffer %u), copied into each frame at present",
|
||||
width, height, framebuffer, renderbuffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::BlitHostFrameCanvas() {
|
||||
if (m_hostFrameCanvas.Framebuffer == 0 || m_hostFrameTarget.Framebuffer == 0) return;
|
||||
// Whatever the compositor had bound is put back as it was, raw for raw, so the engine's
|
||||
// shadow (which never saw this) is still right afterwards. The scissor test and
|
||||
// rasterizer discard both apply to a blit, so they are lifted for it.
|
||||
GLint readBinding = 0;
|
||||
GLint drawBinding = 0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readBinding);
|
||||
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawBinding);
|
||||
const GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
|
||||
const GLboolean discard = g_GLESFuncs.glIsEnabled(GL_RASTERIZER_DISCARD);
|
||||
if (scissor) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
if (discard) g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_hostFrameCanvas.Framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_hostFrameTarget.Framebuffer);
|
||||
const Bool sameSize = m_hostFrameCanvas.Width == m_hostFrameTarget.Width &&
|
||||
m_hostFrameCanvas.Height == m_hostFrameTarget.Height;
|
||||
g_GLESFuncs.glBlitFramebuffer(0, 0, static_cast<GLint>(m_hostFrameCanvas.Width),
|
||||
static_cast<GLint>(m_hostFrameCanvas.Height), 0, 0,
|
||||
static_cast<GLint>(m_hostFrameTarget.Width),
|
||||
static_cast<GLint>(m_hostFrameTarget.Height), GL_COLOR_BUFFER_BIT,
|
||||
sameSize ? GL_NEAREST : GL_LINEAR);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readBinding));
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawBinding));
|
||||
if (scissor) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
||||
if (discard) g_GLESFuncs.glEnable(GL_RASTERIZER_DISCARD);
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
// The other arm's half: on a build with no host frames there is no frame to take, and the
|
||||
// attach site's ordinary path (a storage-less external texture, exactly as before) is the honest
|
||||
// behaviour there rather than a link error.
|
||||
Bool TakeHostFrameForExternalTexture(Uint, Uint) { return false; }
|
||||
|
||||
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
(void)width;
|
||||
(void)height;
|
||||
MGLOG_E("InitHostFrameSurface: host frames are a disaggregated Android path");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
|
||||
void BackendObject_DirectGLES::DestroyHostFrame() {}
|
||||
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint, Uint) { return false; }
|
||||
void BackendObject_DirectGLES::BlitHostFrameCanvas() {}
|
||||
|
||||
Bool BackendObject_DirectGLES::PresentHostFrame() {
|
||||
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
|
||||
return DirectGLES::InitPbufferSurface(width, height);
|
||||
}
|
||||
@@ -1052,13 +1293,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
|
||||
return false;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
return false;
|
||||
// EGL_KHR_surfaceless_context, WHICH IS THE SHAPE A COMPOSITOR ASKS FOR. Its context has no
|
||||
// draw surface of its own - every frame goes into a framebuffer it binds itself, and under
|
||||
// this project that framebuffer is one of the display host's frames - so the surface checks
|
||||
// below describe a surface that legitimately does not exist. What the server still needs is
|
||||
// its own GL context current on this thread for the records that follow, and that is what
|
||||
// the native bind underneath them does.
|
||||
const Bool surfaceless = draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT;
|
||||
if (surfaceless && !m_eglSurfaceInitialized) {
|
||||
// A DRAWABLE OF THIS SIDE'S OWN, WHICH IS WHAT A SURFACELESS CLIENT LEAVES TO US. The
|
||||
// backend's native context has to be current on a surface of some kind before a single
|
||||
// record of the client's can run - that is what the Init*Surface calls establish - and a
|
||||
// client that makes its context current with no surface at all supplies none of its own.
|
||||
// A pbuffer is the one that costs nothing to keep: it is never scanned out and never
|
||||
// read, because every frame the client draws goes into a framebuffer it binds itself.
|
||||
// The client is NOT told it has a surface - m_eglSurface stays EGL_NO_SURFACE - so a
|
||||
// swap on one is still the refusal it was.
|
||||
if (!InitPbufferSurface(1, 1)) {
|
||||
MGLOG_E("MakeEGLCurrent failed: the backend's own drawable for a surfaceless context could not "
|
||||
"be created");
|
||||
return false;
|
||||
}
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = SurfaceKind::Pbuffer;
|
||||
m_eglCurrentThreads.clear();
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
|
||||
return false;
|
||||
if (!surfaceless) {
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -1086,9 +1356,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple
|
||||
// (the ID-67 control's different tuple stays at 1 native bind where 2 are required).
|
||||
const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
DirectGLES::IsBackendContextCurrentOnThisThread() &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
|
||||
(surfaceless || (m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx)));
|
||||
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
|
||||
NoteNativeContextGone();
|
||||
return false;
|
||||
@@ -1107,6 +1377,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
// A HOST-FRAMED SURFACE HAS TO BE PUT BACK AFTER EVERY MAKE-CURRENT. The native call
|
||||
// binds the surface's own default framebuffer - the pbuffer this path makes its context
|
||||
// with - and that is not where these pixels go. Both the driver and the engine's binding
|
||||
// shadow have to be told again, or the next default-framebuffer bind is deduped away and
|
||||
// every draw lands in a pbuffer nobody sees.
|
||||
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
HostFrameTargetBind(HostFrameDrawTarget());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1263,7 +1542,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
E_GL_ARB_get_program_binary,
|
||||
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
|
||||
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
|
||||
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
|
||||
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
|
||||
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
|
||||
// missing checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
|
||||
// rendering context" and quit before drawing a frame.
|
||||
//
|
||||
// The three are true statements about this backend now rather than a test unlock: every
|
||||
// procedure they define is implemented in MG_Impl/GLImpl/Exporting/Definitions.cpp over
|
||||
// the same shader and program objects the core entry points drive - the 39 of
|
||||
// ARB_shader_objects, ARB_vertex_shader's three attrib-location entry points, and none
|
||||
// at all for ARB_fragment_shader ("New Procedures and Functions: None" - it is the
|
||||
// capability half, and the reason kwin's conjunction names it).
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
// NPOT textures: core since GL 2.0, and every host this backend can run on provides
|
||||
// them. Full non-power-of-two support - mipmapping and every wrap mode, not ES 2.0's
|
||||
// CLAMP_TO_EDGE-only exception - is required from ES 3.0 on, and the entries this list
|
||||
// already advertises without any gate (compute shaders, shader storage buffers,
|
||||
// indirect draws) need an ES 3.1 host. Nothing in the texture path rejects or pads a
|
||||
// non-power-of-two size - storage and sampling are forwarded to that host - so the
|
||||
// string describes what an application actually gets.
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
|
||||
// and this name is what makes them reachable - libepoxy in kwin resolves
|
||||
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
|
||||
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
|
||||
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
|
||||
// The render-server half of the import (fd over the GBM socket, then the server-side
|
||||
// EGLImage) is the next change; until it lands an imported texture has no storage behind
|
||||
// it, which is logged once at the entry point rather than left to look like a black frame.
|
||||
E_GL_OES_EGL_image};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "HostFrameTarget.h"
|
||||
#include "MG_Backend/HostFrameBridge.h"
|
||||
#include "MG_Backend/GbmFrameChannel.h"
|
||||
#include "../BackendObject.h"
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
@@ -56,8 +59,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void UpdateDynamicBackendParameters();
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
// A host-framed surface draws into a frame the display host owns; presenting one
|
||||
// tells the host the frame is drawn, because the host is what puts it on the glass.
|
||||
// NOT CreateEGLHostFrameSurface: the base class registers the surface and activates it,
|
||||
// and activation is what reaches InitHostFrameSurface below. There is nothing about the
|
||||
// EGL side of a host-framed surface this backend needs to do its own way.
|
||||
Bool InitHostFrameSurface(EGLint width, EGLint height) override;
|
||||
Bool PresentHostFrame() override;
|
||||
// Takes the next frame from the host and makes it this context's framebuffer.
|
||||
Bool TakeHostFrame();
|
||||
void DestroyHostFrame();
|
||||
// The canvas the compositor draws into, made once at the first frame's size.
|
||||
Bool EnsureHostFrameCanvas(Uint width, Uint height);
|
||||
// Copies the canvas into the frame about to be presented.
|
||||
void BlitHostFrameCanvas();
|
||||
// What draws resolve to: the canvas when there is one, the frame itself otherwise.
|
||||
const HostFrameTarget& HostFrameDrawTarget() const {
|
||||
return m_hostFrameCanvas.Framebuffer != 0 ? m_hostFrameCanvas : m_hostFrameTarget;
|
||||
}
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: the frames the display host owns, one at a time, which
|
||||
// is what the host offers - it hands one over and waits for the answer before it
|
||||
// presents it and offers the next.
|
||||
MG_Backend::HostFrameBridge m_hostFrameBridge;
|
||||
// The frames the bridge just took, on to the container: the Wayland compositor over there
|
||||
// renders into this same memory, and the only route to it that end has is the dma-buf
|
||||
// descriptor this channel offers per frame and reads a release back from.
|
||||
MG_Backend::GbmFrameChannel m_gbmFrameChannel;
|
||||
HostFrameTarget m_hostFrameTarget;
|
||||
// A STABLE TARGET IN FRONT OF THE FRAMES. Every present hands the frame back and takes
|
||||
// a different one, and its renderbuffer is deleted with it - but a compositor attaches
|
||||
// its framebuffers ONCE (kwin: one per daemon buffer, for the life of the process), so
|
||||
// attachments bound to a frame's own renderbuffer named a deleted object from the second
|
||||
// frame on. The attachments name this instead, and each present copies it into the
|
||||
// frame. No EGLImage behind it (Image stays null).
|
||||
HostFrameTarget m_hostFrameCanvas;
|
||||
struct AHardwareBuffer* m_hostFrameBuffer = nullptr;
|
||||
MG_Backend::HostFrameOffer m_hostFrameOffer{};
|
||||
Bool m_hostFrameBridgeOpen = false;
|
||||
|
||||
Bool m_initialized = false;
|
||||
MG_External::EGLFunctionsTable m_EGLFunctions;
|
||||
MG_External::GLESFunctionsTable m_GLESFunctions;
|
||||
|
||||
@@ -87,6 +87,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
|
||||
MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
Uint g_hostFrameFramebufferId = 0;
|
||||
Uint g_hostFrameRenderbuffer = 0;
|
||||
Uint g_hostFrameWidth = 0;
|
||||
Uint g_hostFrameHeight = 0;
|
||||
MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -6515,7 +6519,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Through the shadow: a raw bind here would leave the shadow claiming
|
||||
// the previous user FBO, false-skipping its next re-bind.
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -6584,7 +6588,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Through the shadow: a raw bind here would leave the shadow claiming
|
||||
// the previous user FBO, false-skipping its next re-bind.
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6604,7 +6608,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::g_integerColorDrawBufferMask = 0;
|
||||
}
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6645,7 +6649,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::g_integerColorDrawBufferMask = 0;
|
||||
}
|
||||
FramebufferImpl::BindFramebufferId(
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
||||
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void InvalidateSyncedRenderState();
|
||||
} // namespace RenderStateImpl
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: what the client's DEFAULT framebuffer means while a host-framed
|
||||
// surface is current. Zero everywhere else, because the driver's own 0 IS the EGL surface
|
||||
// then - but on a host-framed surface the pixels belong to a frame the display host owns,
|
||||
// and drawing into the driver's 0 would draw into a pbuffer nobody ever sees.
|
||||
extern Uint g_hostFrameFramebufferId;
|
||||
// WHAT THE HOST FRAME'S STORAGE IS, for the one caller that has to bind it in place of a
|
||||
// storage-less attachment: an imported dma-buf is the display host's frame (same memory, same
|
||||
// size), and the frame's storage is a RENDERBUFFER (HostFrameTarget.cpp binds it with
|
||||
// glFramebufferRenderbuffer). A texture argument cannot name it, which is why these exist and
|
||||
// why g_hostFrameFramebufferId alone was not enough. Zero whenever no frame is held.
|
||||
extern Uint g_hostFrameRenderbuffer;
|
||||
extern Uint g_hostFrameWidth;
|
||||
extern Uint g_hostFrameHeight;
|
||||
// TAKE THE FRAME WHEN NOTHING ELSE WILL. On the anland route the container's compositor never
|
||||
// creates a window surface, so ActivateEGLSurface never runs and InitHostFrameSurface is never
|
||||
// reached: the FIRST proof that the display host's frame is wanted is an ATTACHMENT of an
|
||||
// imported external texture (Managers.cpp's 0x8D65 arm), and that is where the frame has to be
|
||||
// taken. The host's own offer carries the width/height, so the numbers passed here are only
|
||||
// what the bridge logs. Answers false (and changes nothing) whenever no backend is up.
|
||||
Bool TakeHostFrameForExternalTexture(Uint width, Uint height);
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "HostFrameTarget.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "Managers.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
// The four entry points this is spelled in are optional everywhere: the loader's own
|
||||
// tables do not carry them (a missing EGL function there is MGLOG_F), so the extension
|
||||
// strings decide first and the pointers are taken only once they have.
|
||||
using GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
|
||||
using CreateImageFn = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
using DestroyImageFn = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
|
||||
using ImageTargetRenderbufferFn = void (*)(GLenum, GLeglImageOES);
|
||||
|
||||
Bool HasExtensionWord(const char* list, const char* word) {
|
||||
if (list == nullptr) return false;
|
||||
const SizeT n = std::strlen(word);
|
||||
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
|
||||
const Bool startOk = at == list || at[-1] == ' ';
|
||||
const Bool endOk = !at[n] || at[n] == ' ';
|
||||
if (startOk && endOk) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct EntryPoints {
|
||||
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
|
||||
CreateImageFn CreateImage = nullptr;
|
||||
DestroyImageFn DestroyImage = nullptr;
|
||||
ImageTargetRenderbufferFn TargetRenderbuffer = nullptr;
|
||||
};
|
||||
|
||||
// Resolved once per process and cached together with the refusal: a driver that lacks one
|
||||
// of these does not grow it later, and the answer names what is missing.
|
||||
const EntryPoints& Resolved(String& why) {
|
||||
static EntryPoints points;
|
||||
static Bool resolved = false;
|
||||
static String refusal;
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
|
||||
!g_GLESFuncs.glGetString) {
|
||||
refusal = "the loader has no eglGetProcAddress/eglQueryString/eglGetCurrentDisplay/glGetString";
|
||||
} else {
|
||||
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
|
||||
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
|
||||
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
|
||||
refusal = "EGL_ANDROID_get_native_client_buffer is not advertised";
|
||||
} else if (!HasExtensionWord(egl, "EGL_KHR_image_base")) {
|
||||
refusal = "EGL_KHR_image_base is not advertised";
|
||||
} else if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
|
||||
refusal = "GL_OES_EGL_image is not advertised";
|
||||
} else {
|
||||
points.GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
|
||||
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
|
||||
points.CreateImage =
|
||||
reinterpret_cast<CreateImageFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
|
||||
points.DestroyImage =
|
||||
reinterpret_cast<DestroyImageFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
|
||||
points.TargetRenderbuffer = reinterpret_cast<ImageTargetRenderbufferFn>(
|
||||
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
|
||||
if (points.GetNativeClientBuffer == nullptr || points.CreateImage == nullptr ||
|
||||
points.DestroyImage == nullptr || points.TargetRenderbuffer == nullptr) {
|
||||
refusal = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / eglCreateImageKHR / "
|
||||
"eglDestroyImageKHR / glEGLImageTargetRenderbufferStorageOES";
|
||||
points = EntryPoints{};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
why = refusal;
|
||||
return points;
|
||||
}
|
||||
|
||||
void DrainGLErrors() {
|
||||
if (!g_GLESFuncs.glGetError) return;
|
||||
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool HostFrameTargetSupported(String& why) {
|
||||
const EntryPoints& points = Resolved(why);
|
||||
return points.GetNativeClientBuffer != nullptr;
|
||||
}
|
||||
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why) {
|
||||
target = HostFrameTarget{};
|
||||
if (buffer == nullptr) {
|
||||
why = "no buffer";
|
||||
return false;
|
||||
}
|
||||
const EntryPoints& points = Resolved(why);
|
||||
if (points.GetNativeClientBuffer == nullptr) return false;
|
||||
|
||||
if (!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glBindRenderbuffer || !g_GLESFuncs.glGenFramebuffers ||
|
||||
!g_GLESFuncs.glBindFramebuffer || !g_GLESFuncs.glFramebufferRenderbuffer ||
|
||||
!g_GLESFuncs.glCheckFramebufferStatus) {
|
||||
why = "the loader has no glGenRenderbuffers/glBindRenderbuffer/glGenFramebuffers/glBindFramebuffer/"
|
||||
"glFramebufferRenderbuffer/glCheckFramebufferStatus";
|
||||
return false;
|
||||
}
|
||||
|
||||
AHardwareBuffer_Desc desc{};
|
||||
AHardwareBuffer_describe(buffer, &desc);
|
||||
|
||||
DrainGLErrors();
|
||||
const EGLClientBuffer clientBuffer = points.GetNativeClientBuffer(buffer);
|
||||
if (clientBuffer == nullptr) {
|
||||
why = "eglGetNativeClientBufferANDROID returned null";
|
||||
return false;
|
||||
}
|
||||
const EGLint imageAttribs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
|
||||
const EGLImageKHR image =
|
||||
points.CreateImage(g_EGLFuncs.eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
|
||||
clientBuffer, imageAttribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) {
|
||||
why = "eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) refused the host frame";
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint renderbuffer = 0;
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
points.TargetRenderbuffer(GL_RENDERBUFFER, reinterpret_cast<GLeglImageOES>(image));
|
||||
const GLenum renderbufferError = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
|
||||
if (renderbufferError != GL_NO_ERROR) {
|
||||
why = std::format("glEGLImageTargetRenderbufferStorageOES -> 0x{:04x}", renderbufferError);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
why = std::format("the host frame as a color attachment -> framebuffer status 0x{:04x}", status);
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
|
||||
return false;
|
||||
}
|
||||
|
||||
target.Image = image;
|
||||
target.Renderbuffer = renderbuffer;
|
||||
target.Framebuffer = framebuffer;
|
||||
target.Width = desc.width;
|
||||
target.Height = desc.height;
|
||||
MGLOG_I("host frame target: %ux%d as framebuffer %u (stride %u, format 0x%x)", desc.width, desc.height,
|
||||
framebuffer, desc.stride, desc.format);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostFrameTargetBind(const HostFrameTarget& target) {
|
||||
if (target.Framebuffer == 0) return;
|
||||
// THROUGH THE ENGINE'S SHADOW, never raw: a raw bind would leave the shadow claiming
|
||||
// the previous framebuffer and false-skip the next re-bind (FramebufferImpl, Managers.h).
|
||||
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, target.Framebuffer);
|
||||
}
|
||||
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target) {
|
||||
if (target.Framebuffer != 0 && g_GLESFuncs.glDeleteFramebuffers)
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &target.Framebuffer);
|
||||
if (target.Renderbuffer != 0 && g_GLESFuncs.glDeleteRenderbuffers)
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
|
||||
if (target.Image != nullptr) {
|
||||
String why;
|
||||
const EntryPoints& points = Resolved(why);
|
||||
if (points.DestroyImage != nullptr && g_EGLFuncs.eglGetCurrentDisplay)
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), static_cast<EGLImageKHR>(target.Image));
|
||||
}
|
||||
target = HostFrameTarget{};
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool HostFrameTargetSupported(String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer*, HostFrameTarget&, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
void HostFrameTargetBind(const HostFrameTarget&) {}
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target) { target = HostFrameTarget{}; }
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// THE FRAME THE DISPLAY HOST IS ABOUT TO SCAN OUT, AS A DRAW TARGET.
|
||||
//
|
||||
// The display host (the Android side that owns the screen) allocates its frames itself and
|
||||
// offers each one over a socket as an AHardwareBuffer. That is not a convenience: measured
|
||||
// on the device, AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this side
|
||||
// allocated and refuses one the display queue handed over (gralloc BAD_VALUE), so the frames
|
||||
// a render server can draw into are the ones the host made for it.
|
||||
//
|
||||
// What this makes of such a frame: an EGLImage the driver accepts as a renderbuffer, and a
|
||||
// framebuffer that is the drawing target while that frame is the current one. None of
|
||||
// Android's window machinery is involved - no ANativeWindow, no SurfaceFlinger - which is
|
||||
// the point: the host puts the frame on the glass when this side says it is drawn.
|
||||
struct HostFrameTarget {
|
||||
void* Image = nullptr; // EGLImageKHR; opaque here so this header stays free of EGL types
|
||||
Uint Renderbuffer = 0;
|
||||
Uint Framebuffer = 0;
|
||||
Uint Width = 0;
|
||||
Uint Height = 0;
|
||||
};
|
||||
|
||||
// Whether the driver advertises what this is spelled in. False is answered with the missing
|
||||
// name in `why`, never an abort: a server that cannot take host frames has to be able to
|
||||
// say so.
|
||||
Bool HostFrameTargetSupported(String& why);
|
||||
|
||||
// Imports `buffer` as a render target. On failure `target` is left empty and `why` says
|
||||
// which call refused; no GL name is leaked.
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why);
|
||||
|
||||
// Binds the target as the current draw framebuffer, called when its frame becomes current.
|
||||
void HostFrameTargetBind(const HostFrameTarget& target);
|
||||
|
||||
// Drops the GL names and the EGLImage. The AHardwareBuffer belongs to the bridge, which
|
||||
// releases it; this never touches it.
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target);
|
||||
}
|
||||
@@ -3183,6 +3183,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray;
|
||||
case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle;
|
||||
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer;
|
||||
case MG_Pipe::MGPipeResourceTarget::TexExternalOES: return TextureTarget::TextureExternalOES;
|
||||
default: return TextureTarget::Unknown;
|
||||
}
|
||||
}
|
||||
@@ -10151,7 +10152,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with "
|
||||
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
|
||||
m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize,
|
||||
m_backendTextureId, backendId, buffer ? buffer->GetSize() : (SizeT)0,
|
||||
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
|
||||
// A texture that names a window of the buffer needs the range form; the
|
||||
// whole-buffer forms report offset 0 and the buffer's current size, which
|
||||
@@ -11282,9 +11283,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
||||
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
||||
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||
// THE TEXTURE'S OWN TARGET, not the attach target: an external texture is attached
|
||||
// through the GL_TEXTURE_2D upload target (TextureEnumConverter maps 0x8D65 there so the
|
||||
// attach validates), so glTextureTarget is never 0x8D65 here and the arms below were
|
||||
// dead - kwin's framebuffers got a storage-less texture and every draw into them failed
|
||||
// with GL_INVALID_FRAMEBUFFER_OPERATION.
|
||||
const Bool externalTexture =
|
||||
glTextureTarget == 0x8D65 || textureObject->GetTarget() == TextureTarget::TextureExternalOES;
|
||||
if (externalTexture && g_hostFrameRenderbuffer == 0) {
|
||||
// NOBODY ELSE TAKES THE FRAME ON THIS ROUTE (DirectGLES.h): the compositor
|
||||
// here never creates a window surface, so this attachment is the first place
|
||||
// the display host's frame is known to be wanted. A failed take leaves the
|
||||
// ordinary arm below exactly as it was.
|
||||
// The numbers size only the fallback pbuffer that is the draw target for the
|
||||
// moment before the first frame arrives; the frame's real extent comes from the
|
||||
// display host's own offer. They are the panel's, because that is the surface
|
||||
// this route exists to fill.
|
||||
(void)TakeHostFrameForExternalTexture(1440, 3200);
|
||||
}
|
||||
if (externalTexture && g_hostFrameRenderbuffer != 0) {
|
||||
// THE IMPORTED dma-buf IS THE DISPLAY HOST'S FRAME. Measured, and the whole reason
|
||||
// this branch exists: kwin_wayland in the container imports the display daemon's
|
||||
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937
|
||||
// fourcc 0x34324241") and builds a framebuffer over that texture - and a texture
|
||||
// that arrived this way has NO storage on this side, so the attachment was bound
|
||||
// as an empty texture and kwin refused its own framebuffer ("framebuffer for
|
||||
// dmabuf 0 is not complete"). The buffer it imports IS the frame the display
|
||||
// host handed this process (same memory, same size), and that frame's storage is
|
||||
// a RENDERBUFFER - a texture argument cannot name it - so the attachment is bound
|
||||
// to the frame itself. The gate is the measured shape of that case: target
|
||||
// GL_TEXTURE_EXTERNAL_OES (0x8d65), which nothing else in this stack binds, and a
|
||||
// frame currently held. Every other attachment - a normal GL_TEXTURE_2D, or any
|
||||
// moment with no frame - takes the ordinary path below, unchanged.
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
g_hostFrameRenderbuffer);
|
||||
} else {
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||
}
|
||||
}
|
||||
} else if (attachmentObject.IsRenderbuffer()) {
|
||||
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
|
||||
@@ -11600,6 +11637,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// nothing here is the whole of the agreement. (The legacy arm does the same.)
|
||||
return true;
|
||||
}
|
||||
// AN IMPORTED dma-buf AT A COLOUR POINT IS THE DISPLAY HOST'S FRAME. The client marks the
|
||||
// texture it bound an imported EGLImage to as GL_TEXTURE_EXTERNAL_OES (TextureObject2D's
|
||||
// target), the server never received that dma-buf's storage, and the compositor's
|
||||
// framebuffer over it was incomplete (measured: status 0x8cd7, every draw
|
||||
// GL_INVALID_FRAMEBUFFER_OPERATION). It is bound to the host frame canvas instead - the
|
||||
// storage the display host's frames are copied out of at present - before any twin or
|
||||
// storage sync, which have nothing to offer a texture with no storage.
|
||||
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture &&
|
||||
surface.TextureTarget ==
|
||||
static_cast<decltype(surface.TextureTarget)>(TextureTarget::TextureExternalOES)) {
|
||||
if (g_hostFrameRenderbuffer == 0) (void)TakeHostFrameForExternalTexture(1440, 3200);
|
||||
if (g_hostFrameRenderbuffer != 0) {
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
g_hostFrameRenderbuffer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
|
||||
// The record's handle is validated BEFORE the frontend is consulted (N-3): it is
|
||||
// the untrusted half, the frontend cross-check below is the corroboration, and
|
||||
@@ -11683,9 +11737,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
||||
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
||||
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(surface.Level));
|
||||
if (glTextureTarget == 0x8D65 && g_hostFrameRenderbuffer == 0) {
|
||||
// NOBODY ELSE TAKES THE FRAME ON THIS ROUTE (DirectGLES.h): the compositor
|
||||
// here never creates a window surface, so this attachment is the first place
|
||||
// the display host's frame is known to be wanted. A failed take leaves the
|
||||
// ordinary arm below exactly as it was.
|
||||
// The numbers size only the fallback pbuffer that is the draw target for the
|
||||
// moment before the first frame arrives; the frame's real extent comes from the
|
||||
// display host's own offer. They are the panel's, because that is the surface
|
||||
// this route exists to fill.
|
||||
(void)TakeHostFrameForExternalTexture(1440, 3200);
|
||||
}
|
||||
if (glTextureTarget == 0x8D65 && g_hostFrameRenderbuffer != 0) {
|
||||
// THE IMPORTED dma-buf IS THE DISPLAY HOST'S FRAME. Measured, and the whole reason
|
||||
// this branch exists: kwin_wayland in the container imports the display daemon's
|
||||
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937
|
||||
// fourcc 0x34324241") and builds a framebuffer over that texture - and a texture
|
||||
// that arrived this way has NO storage on this side, so the attachment was bound
|
||||
// as an empty texture and kwin refused its own framebuffer ("framebuffer for
|
||||
// dmabuf 0 is not complete"). The buffer it imports IS the frame the display
|
||||
// host handed this process (same memory, same size), and that frame's storage is
|
||||
// a RENDERBUFFER - a texture argument cannot name it - so the attachment is bound
|
||||
// to the frame itself. The gate is the measured shape of that case: target
|
||||
// GL_TEXTURE_EXTERNAL_OES (0x8d65), which nothing else in this stack binds, and a
|
||||
// frame currently held. Every other attachment - a normal GL_TEXTURE_2D, or any
|
||||
// moment with no frame - takes the ordinary path below, unchanged.
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
g_hostFrameRenderbuffer);
|
||||
} else {
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(surface.Level));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1879,6 +1879,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
// An external texture's storage is an imported EGLImage; to the driver it is 2D.
|
||||
case TextureTarget::TextureExternalOES:
|
||||
return TextureTarget::Texture2D;
|
||||
default:
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -659,7 +659,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
E_GL_ARB_get_program_binary,
|
||||
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
|
||||
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
|
||||
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
|
||||
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
|
||||
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
|
||||
// missing checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
|
||||
// rendering context" and quit before drawing a frame.
|
||||
//
|
||||
// The three are true statements about this backend too, and the entry points behind
|
||||
// them are the same ones the DirectGLES half of this list is advertised with: the
|
||||
// frontend implements all 39 ARB_shader_objects procedures, ARB_vertex_shader's three
|
||||
// attrib-location entry points, and nothing for ARB_fragment_shader, whose "New
|
||||
// Procedures and Functions" section reads "None". The shader and program objects live in
|
||||
// MG_Impl/MG_State, above the backend that draws with them, so a name that is true on
|
||||
// one backend is true on the other.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
// NPOT textures: Vulkan puts no power-of-two restriction on an image, its mip chain or
|
||||
// its samplers, so this is the strongest form of the promise the string makes. Kept
|
||||
// identical to the DirectGLES group so the two backends do not disagree about what
|
||||
// MobileGL is.
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
|
||||
// and this name is what makes them reachable - libepoxy in kwin resolves
|
||||
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
|
||||
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
|
||||
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
|
||||
// The render-server half of the import (fd over the GBM socket, then the server-side
|
||||
// EGLImage) is the next change; until it lands an imported texture has no storage behind
|
||||
// it, which is logged once at the entry point rather than left to look like a black frame.
|
||||
E_GL_OES_EGL_image};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
|
||||
@@ -0,0 +1,737 @@
|
||||
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "GbmFrameChannel.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// WHAT THIS FILE IS WRITTEN AGAINST, AND WHAT HAS NOT BEEN RUN.
|
||||
//
|
||||
// The other end exists and is tested on this machine: MobileGL/MG_Gbm/GbmHostChannel.cpp dials
|
||||
// this socket with SOCK_SEQPACKET, reads one message per frame, computes that message's expected
|
||||
// length from the handle's own two counts, and refuses any other length. Every byte below is
|
||||
// written to that parser. There is no phone attached to the machine this was written on, so the
|
||||
// file is compile-verified for aarch64 Android and nothing more: the first run on a device is what
|
||||
// will say whether the relay in RelayHandle() reads back the handle's bytes exactly as
|
||||
// AHardwareBuffer_sendHandleToUnixSocket writes them, which is why that path is written to
|
||||
// survive both layouts the platform's serializer could have (see the note there).
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
namespace {
|
||||
// THE LAYOUT IS A PROTOCOL, SO ITS SIZES ARE ASSERTED AND NOT ASSUMED. The container
|
||||
// derives the length of a frame message from these three sizes plus the handle's counts,
|
||||
// so a field added here, or the packing attribute dropped, would not be a compile error
|
||||
// over there - it would be every frame skipped with a message about its length.
|
||||
static_assert(sizeof(GbmFrameOffer) == 10 * sizeof(Uint32), "the offer is ten 32 bit fields on the wire");
|
||||
static_assert(sizeof(GbmFrameRelease) == 3 * sizeof(Uint32), "the release is three 32 bit fields on the wire");
|
||||
static_assert(sizeof(GbmNativeHandleHeader) == 3 * sizeof(Int32),
|
||||
"a native handle begins with a version and two counts, on every Android ABI");
|
||||
|
||||
// The container reads one message per frame into a 4 KiB buffer and skips a message that
|
||||
// does not fit in it (GbmHostMessageMax, GbmHostChannel.cpp), so a message larger than this
|
||||
// is refused here rather than built and dropped there.
|
||||
constexpr SizeT kMessageMax = 4096;
|
||||
// The descriptors one handle may carry, which is the container's own cap (GbmHostMaxFds).
|
||||
// Every AHardwareBuffer handle this project has seen carries one - the dma-buf - and both
|
||||
// ends refuse a frame whose handle names more than they can take rather than forward a
|
||||
// truncated list into a buffer of unknown memory.
|
||||
constexpr Int kMaxHandleFds = 8;
|
||||
// How many release messages one call reads before it returns to the frame. A cap and not a
|
||||
// loop to exhaustion: the messages left behind stay in the socket, whose buffer the kernel
|
||||
// bounds, and the next frame drains them.
|
||||
constexpr Int kDrainMax = 64;
|
||||
// HOW MANY DIALERS THE KERNEL MAY HOLD FOR THIS LISTENER. One container is the protocol,
|
||||
// but the queue is not the connection: every compositor that was restarted dials once more,
|
||||
// and a listener that only ever has one slot leaves its queue full for the next one. With
|
||||
// listen(fd, 1) that is exactly what was measured - connect() from the container answered
|
||||
// EAGAIN(11) on four attempts out of four after two restarts, and no dialer reached this
|
||||
// side again for as long as the server lived.
|
||||
constexpr Int kDialerBacklog = 16;
|
||||
// The acceptor's poll timeout. It is a wake-up and not a schedule, so it only has to be
|
||||
// short enough that a container dialling into a socket nothing else touches is taken
|
||||
// promptly; a dialer that arrives while the thread is waiting wakes it at once.
|
||||
constexpr Int kAcceptPollMs = 100;
|
||||
// How long the relay read waits for bytes the send one statement earlier has already put in
|
||||
// the socket. It exists only so that an invariant broken somewhere else cannot park the
|
||||
// apply thread; on a local socketpair the wait is zero.
|
||||
constexpr Int kRelayWaitMs = 2;
|
||||
|
||||
// close() may overwrite the errno a caller is in the middle of reporting, and every caller
|
||||
// here is reporting a failure errno describes.
|
||||
void CloseQuietly(Int fd) {
|
||||
if (fd < 0) return;
|
||||
const Int savedErrno = errno;
|
||||
::close(fd);
|
||||
errno = savedErrno;
|
||||
}
|
||||
}
|
||||
|
||||
GbmFrameChannel::~GbmFrameChannel() { Close(); }
|
||||
|
||||
Bool GbmFrameChannel::Open() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_listen >= 0) return true;
|
||||
|
||||
const char* configured = ::getenv(kGbmFrameSocketEnv);
|
||||
const char* path = (configured != nullptr && configured[0] != 0) ? configured : kGbmFrameSocketPath;
|
||||
constexpr SizeT pathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
|
||||
if (std::strlen(path) >= pathMax) {
|
||||
MGLOG_E_ONCE("gbm frame channel: the container socket path %s does not fit in sun_path (%zu bytes); no "
|
||||
"container can be served",
|
||||
path, pathMax);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SOCK_SEQPACKET because that is what the container dials with (GbmHostChannel::EnsureOpen
|
||||
// asks for exactly this), and a stream listener answers it EPROTOTYPE - the container
|
||||
// prints that as "a socket of a different type". SOCK_NONBLOCK because the accept below
|
||||
// runs on the apply thread and may not wait for a container that is not there; SOCK_CLOEXEC
|
||||
// because every process this server starts would otherwise inherit a socket that receives
|
||||
// the phone's frames.
|
||||
const Int fd = ::socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: socket(AF_UNIX, SOCK_SEQPACKET): %s; the frames are drawn and presented "
|
||||
"without a container",
|
||||
std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A node left behind by a server that died is not a reason to refuse a container: bind
|
||||
// would answer EADDRINUSE for a socket nobody is listening on.
|
||||
::unlink(path);
|
||||
|
||||
struct sockaddr_un address = {};
|
||||
address.sun_family = AF_UNIX;
|
||||
// The terminator travels with the name: sun_path is a C string for a pathname socket, and a
|
||||
// name without one is a name the kernel reads past the end of.
|
||||
std::memcpy(address.sun_path, path, std::strlen(path) + 1);
|
||||
if (::bind(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: bind(%s): %s; the frames are drawn and presented without a container",
|
||||
path, std::strerror(errno));
|
||||
CloseQuietly(fd);
|
||||
return false;
|
||||
}
|
||||
// connect() to a pathname socket needs write permission on the node, and the container is
|
||||
// not this process's user, so the umask's usual 0755 would leave it refused for a reason it
|
||||
// could not see. 0666 is what makes the node reachable by the other half of a split run; a
|
||||
// chmod that fails is not fatal, because a peer running as this uid still dials it.
|
||||
if (::chmod(path, 0666) != 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: chmod(%s, 0666): %s; a container running as another user cannot dial "
|
||||
"this socket",
|
||||
path, std::strerror(errno));
|
||||
}
|
||||
if (::listen(fd, kDialerBacklog) != 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: listen(%s): %s; the frames are drawn and presented without a container",
|
||||
path, std::strerror(errno));
|
||||
::unlink(path);
|
||||
CloseQuietly(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_listen = fd;
|
||||
m_path = path;
|
||||
// THE ACCEPTOR IS STARTED WITH THE SOCKET AND NOT WITH THE FIRST FRAME. A compositor dials
|
||||
// at its device creation and keeps the connection, so the dial a frame would have answered
|
||||
// has already happened by the time that frame is offered: an accept on the offer path is an
|
||||
// accept that runs after the thing it exists for. The descriptor is copied into the thread
|
||||
// as an argument - it is the one piece of this object the thread reads without the lock,
|
||||
// which is what lets Close() join it before closing that descriptor.
|
||||
m_accepting.store(true, std::memory_order_release);
|
||||
try {
|
||||
m_acceptor = std::thread(&GbmFrameChannel::AcceptLoop, this, fd);
|
||||
} catch (...) {
|
||||
// A thread that cannot be made is a phone that is out of them, not a reason to refuse
|
||||
// the frames the container can still be given one at a time.
|
||||
m_accepting.store(false, std::memory_order_release);
|
||||
MGLOG_W_ONCE("gbm frame channel: the acceptor thread could not be started; a container is only taken in "
|
||||
"when a frame is offered, and one that dials between frames waits for the next one");
|
||||
}
|
||||
MGLOG_I("gbm frame channel: listening on %s for one container (SOCK_SEQPACKET, one message per frame)", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::EnsureRelay() {
|
||||
if (m_relay[0] >= 0 && m_relay[1] >= 0) return true;
|
||||
// Both ends are dropped before a new pair is made: half a socketpair is a socket with bytes
|
||||
// in it that nothing will ever read, and the next handle read from it would be the tail of
|
||||
// the last one.
|
||||
DropRelay();
|
||||
Int pair[2] = {-1, -1};
|
||||
if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, pair) != 0) {
|
||||
MGLOG_E_ONCE("gbm frame channel: socketpair for the handle relay: %s; no frame can be offered until one "
|
||||
"can be made",
|
||||
std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
m_relay[0] = pair[0];
|
||||
m_relay[1] = pair[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::DropRelay() {
|
||||
CloseQuietly(m_relay[0]);
|
||||
CloseQuietly(m_relay[1]);
|
||||
m_relay[0] = -1;
|
||||
m_relay[1] = -1;
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
|
||||
Int* fds, Int& fdCount) {
|
||||
payloadBytes = 0;
|
||||
fdCount = 0;
|
||||
if (!EnsureRelay()) return false;
|
||||
|
||||
// WHY A FRAME'S HANDLE GOES THROUGH A SOCKETPAIR OF THIS CHANNEL'S OWN.
|
||||
//
|
||||
// The container takes ONE message per frame - the offer, then the handle's counts, then the
|
||||
// arrays those counts describe, with the descriptors in that same message's SCM_RIGHTS
|
||||
// control data - and it refuses a message of any other length. Two sends cannot be one
|
||||
// message on a message-oriented socket, so AHardwareBuffer_sendHandleToUnixSocket, which
|
||||
// sends a handle as a message of its own, is pointed at a socketpair this channel owns, and
|
||||
// what comes out of the other end is both halves of what the container wants: the handle's
|
||||
// own bytes, read here, and its descriptors, as SCM_RIGHTS.
|
||||
//
|
||||
// The alternative does not exist: the NDK's <android/hardware_buffer.h> declares no way to
|
||||
// ask a buffer for its descriptors. AHardwareBuffer_getNativeHandle is a platform API and
|
||||
// is absent from the NDK's copy of that header (checked against NDK 27.3.13750724 in this
|
||||
// tree's build-termux toolchain), so this is the one route from an AHardwareBuffer to a
|
||||
// dma-buf descriptor that an app can take.
|
||||
const Int code = AHardwareBuffer_sendHandleToUnixSocket(buffer, m_relay[0]);
|
||||
if (code != 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: the frame's handle did not cross the relay socket (rc %d, errno %d); the "
|
||||
"frame is not offered to the container",
|
||||
code, errno);
|
||||
// The relay is dropped rather than kept: a send that failed part way through would
|
||||
// leave bytes that the next handle's read would take for its own header.
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
|
||||
struct iovec vector = {};
|
||||
vector.iov_base = payload;
|
||||
vector.iov_len = capacity;
|
||||
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &vector;
|
||||
message.msg_iovlen = 1;
|
||||
message.msg_control = control;
|
||||
message.msg_controllen = sizeof(control);
|
||||
|
||||
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC so a descriptor
|
||||
// read out of the relay does not survive into a child of this process, which would then hold
|
||||
// the phone's frame until it died.
|
||||
ssize_t got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||
if (got < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
struct pollfd waiting = {};
|
||||
waiting.fd = m_relay[1];
|
||||
waiting.events = POLLIN;
|
||||
if (::poll(&waiting, 1, kRelayWaitMs) > 0) {
|
||||
// The kernel rewrites these on every call, so a retry starts from the same state as
|
||||
// the first attempt.
|
||||
message.msg_controllen = sizeof(control);
|
||||
message.msg_flags = 0;
|
||||
got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||
}
|
||||
}
|
||||
if (got < 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a relayed handle could not be read back (%d: %s); the frame is not "
|
||||
"offered to the container",
|
||||
errno, std::strerror(errno));
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
if (got == 0) {
|
||||
// Zero bytes on a connected socket is the end of it. Only this process holds either end
|
||||
// of this socketpair, so this is a bug being reported rather than a peer going away.
|
||||
MGLOG_W_ONCE("gbm frame channel: the handle relay is closed; the frame is not offered to the container");
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
|
||||
// The descriptors come out of the control data before anything is decided about the
|
||||
// message, because they belong to this process from the moment they were received: a path
|
||||
// that returns without closing them leaks a descriptor per frame.
|
||||
Int collected = 0;
|
||||
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
|
||||
header = CMSG_NXTHDR(&message, header)) {
|
||||
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS) continue;
|
||||
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
|
||||
const Int count = (Int)(bytes / sizeof(Int));
|
||||
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
|
||||
for (Int i = 0; i < count; ++i) {
|
||||
if (collected < kMaxHandleFds) {
|
||||
fds[collected++] = descriptors[i];
|
||||
} else {
|
||||
// More descriptors than a frame can carry: they are this process's now, so they
|
||||
// are closed here rather than leaked.
|
||||
CloseQuietly(descriptors[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((message.msg_flags & MSG_CTRUNC) != 0) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a frame's control data did not fit in %zu bytes; the frame is not "
|
||||
"offered because its descriptor list is incomplete",
|
||||
sizeof(control));
|
||||
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
if ((message.msg_flags & MSG_TRUNC) != 0 || (SizeT)got < sizeof(GbmNativeHandleHeader)) {
|
||||
// A message longer than a handle can be, or shorter than the three counts every handle
|
||||
// begins with. Either way it cannot describe a frame, and a relay that has been read
|
||||
// past the end of one message must not carry the next frame's header: it is recreated.
|
||||
MGLOG_W_ONCE("gbm frame channel: a relayed handle is %zd bytes and %s; the frame is not offered", got,
|
||||
((message.msg_flags & MSG_TRUNC) != 0) ? "longer than a handle can be"
|
||||
: "too short to hold its own counts");
|
||||
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
|
||||
// THE BYTES THAT COME BACK ARE THE HANDLE'S INTEGER ARRAY, NOT A handle AND ITS COUNTS.
|
||||
// Measured on the phone: parsing the front of this payload as {version, numFds, numInts}
|
||||
// produced a NumFds taken from the buffer's own integer data, and every frame was refused
|
||||
// with "its handle carries more descriptors than either end takes" - which is what the
|
||||
// container saw as a connection that was accepted and then closed without a frame. The two
|
||||
// counts are known HERE and are not in those bytes: the descriptors came out of the control
|
||||
// data (collected of them) and the payload is their integer array. So the header is BUILT
|
||||
// rather than copied, and what leaves this process is a real native_handle_t layout -
|
||||
// version, numFds, numInts, then the ints - which is exactly what the container's parser
|
||||
// reads (GbmInternal.h's AHBNativeHandleHeader).
|
||||
if (payloadBytes + sizeof(GbmNativeHandleHeader) > capacity) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a handle's integer array does not fit in a frame message with its "
|
||||
"header; the frame is not offered to the container");
|
||||
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
|
||||
DropRelay();
|
||||
return false;
|
||||
}
|
||||
std::memmove(payload + sizeof(GbmNativeHandleHeader), payload, payloadBytes);
|
||||
GbmNativeHandleHeader header = {};
|
||||
header.Version = (Int32)sizeof(GbmNativeHandleHeader);
|
||||
header.NumFds = (Int32)collected;
|
||||
header.NumInts = (Int32)(payloadBytes / sizeof(Int32));
|
||||
std::memcpy(payload, &header, sizeof(header));
|
||||
payloadBytes += sizeof(header);
|
||||
fdCount = collected;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The acceptor. It touches the listener - its own copy of the descriptor, which nothing else
|
||||
// closes while this loop runs - and, once per dialer, the channel under its lock.
|
||||
void GbmFrameChannel::AcceptLoop(Int listen) {
|
||||
while (m_accepting.load(std::memory_order_acquire)) {
|
||||
struct pollfd waiting = {};
|
||||
waiting.fd = listen;
|
||||
waiting.events = POLLIN;
|
||||
const Int ready = ::poll(&waiting, 1, kAcceptPollMs);
|
||||
if (ready < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
MGLOG_W_ONCE("gbm frame channel: poll on the listening socket failed (%d: %s); no container can be "
|
||||
"taken in until the socket is opened again",
|
||||
errno, std::strerror(errno));
|
||||
return;
|
||||
}
|
||||
if (ready == 0) continue;
|
||||
// accept4, so the connection is close-on-exec and non-blocking from the moment it
|
||||
// exists: a container that stops reading must not be able to park a send on either
|
||||
// thread.
|
||||
const Int fd = ::accept4(listen, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED) continue;
|
||||
MGLOG_W_ONCE("gbm frame channel: accept failed (%d: %s); no container can be taken in until the "
|
||||
"socket is opened again",
|
||||
errno, std::strerror(errno));
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
AdoptLocked(fd);
|
||||
}
|
||||
}
|
||||
|
||||
void GbmFrameChannel::AdoptLocked(Int fd) {
|
||||
if (ClientAliveLocked()) {
|
||||
// ONE CLIENT, which is what the protocol is written for, and a dialer that arrives while
|
||||
// a live one is connected is closed rather than left connected - it learns it is not the
|
||||
// one being served instead of holding a connection whose frames nobody reads.
|
||||
CloseQuietly(fd);
|
||||
MGLOG_W_ONCE("gbm frame channel: a second peer dialled %s while a container is connected; it is closed "
|
||||
"and the frames stay with the connected one",
|
||||
m_path.c_str());
|
||||
return;
|
||||
}
|
||||
m_client = fd;
|
||||
MGLOG_I("gbm frame channel: a container is connected on %s; every frame the host offers is sent to it as "
|
||||
"the dma-buf descriptor behind it",
|
||||
m_path.c_str());
|
||||
// AND THE FRAME IT DIALED FOR GOES OUT FROM HERE. A compositor creates its GBM device before
|
||||
// it asks for a surface, so the frame that is the draw target right now was offered before
|
||||
// this connection existed; a container that is not told about it allocates every buffer of
|
||||
// its new swapchain locally, and keeps them for the life of the process.
|
||||
ReofferFrameLocked();
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::ClientAliveLocked() {
|
||||
if (m_client < 0) return false;
|
||||
Uint8 peek = 0;
|
||||
// MSG_PEEK, because a release waiting in the socket belongs to the render path: this test
|
||||
// only asks whether the peer is still there, and a release it consumed would be a frame the
|
||||
// channel believed was still outstanding.
|
||||
const ssize_t got = ::recv(m_client, &peek, sizeof(peek), MSG_DONTWAIT | MSG_PEEK);
|
||||
if (got > 0) return true;
|
||||
if (got == 0) {
|
||||
// Zero bytes on a connected socket is the end of it, whether the container exited or
|
||||
// the kernel gave up on it. A compositor that crashed and is being restarted is
|
||||
// exactly this, and its successor is the dialer standing right here.
|
||||
MarkDown("the container closed the connection", 0);
|
||||
return false;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return true;
|
||||
MarkDown("recv", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::MarkDown(const char* what, Int error) {
|
||||
if (m_client < 0) return;
|
||||
CloseQuietly(m_client);
|
||||
m_client = -1;
|
||||
if (error != 0) {
|
||||
MGLOG_W("gbm frame channel: the container connection is down: %s failed (%d: %s); the frames are still "
|
||||
"drawn and presented, and the container stops receiving them until it dials again",
|
||||
what, error, std::strerror(error));
|
||||
} else {
|
||||
MGLOG_W("gbm frame channel: the container connection is down: %s; the frames are still drawn and "
|
||||
"presented, and the container stops receiving them until it dials again",
|
||||
what);
|
||||
}
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer) {
|
||||
if (buffer == nullptr) return false;
|
||||
// Open() does nothing once the socket exists, so this is an integer test in the common case.
|
||||
Open();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// The releases the container has sent since the last frame, which is also how a container
|
||||
// that died is noticed. Whoever replaced it was taken by the acceptor's own thread the
|
||||
// moment it dialled: that thread is what makes a container's connect() succeed at all.
|
||||
PollLocked();
|
||||
// AND THE FRAME IS REMEMBERED BEFORE IT IS SENT. A container that dials while this call is
|
||||
// running is one the acceptor hands the frame it finds here to, so the two threads must not
|
||||
// be able to disagree about which frame is the one being drawn into.
|
||||
const Uint32 seq = m_nextSeq;
|
||||
RememberFrameLocked(buffer, offer, seq);
|
||||
if (m_client < 0) return false;
|
||||
return SendFrameLocked(buffer, offer, seq, /*counting=*/true);
|
||||
}
|
||||
|
||||
void GbmFrameChannel::RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq) {
|
||||
if (m_frame == buffer) {
|
||||
// The host redrawing into the buffer it already handed over: the same frame under a new
|
||||
// sequence number. Acquiring it again would be a reference this channel takes and never
|
||||
// gives back.
|
||||
m_frameOffer = offer;
|
||||
m_frameSeq = seq;
|
||||
return;
|
||||
}
|
||||
// The reference is this channel's own, and it is what makes the frame a container that dials
|
||||
// late is owed safe to hold: the backend drops its handle the moment the frame is presented,
|
||||
// and a frame owed to a dialer arriving in the same millisecond may not be memory that has
|
||||
// already gone back to the host's pool.
|
||||
AHardwareBuffer_acquire(buffer);
|
||||
ReleaseFrameLocked();
|
||||
m_frame = buffer;
|
||||
m_frameOffer = offer;
|
||||
m_frameSeq = seq;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::ReleaseFrameLocked() {
|
||||
if (m_frame == nullptr) return;
|
||||
AHardwareBuffer_release(m_frame);
|
||||
m_frame = nullptr;
|
||||
m_frameOffer = {};
|
||||
m_frameSeq = 0;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::DropFrame() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
ReleaseFrameLocked();
|
||||
}
|
||||
|
||||
void GbmFrameChannel::ReofferFrameLocked() {
|
||||
if (m_frame == nullptr) return;
|
||||
// The same frame, under the sequence number it was offered with: it IS the frame that is the
|
||||
// draw target now, and a second number for it would be this channel claiming the host had
|
||||
// handed over a frame it never handed over.
|
||||
MGLOG_I("gbm frame channel: a container dialled after frame %u was offered; it is sent that frame, which is "
|
||||
"still the draw target",
|
||||
m_frameSeq);
|
||||
SendFrameLocked(m_frame, m_frameOffer, m_frameSeq, /*counting=*/false);
|
||||
}
|
||||
|
||||
Bool GbmFrameChannel::SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq,
|
||||
Bool counting) {
|
||||
if (m_client < 0) return false;
|
||||
|
||||
Uint8 payload[kMessageMax];
|
||||
SizeT payloadBytes = 0;
|
||||
Int fds[kMaxHandleFds];
|
||||
for (Int i = 0; i < kMaxHandleFds; ++i) fds[i] = -1;
|
||||
Int fdCount = 0;
|
||||
if (!RelayHandle(buffer, payload, sizeof(payload), payloadBytes, fds, fdCount)) return false;
|
||||
|
||||
GbmNativeHandleHeader handle = {};
|
||||
std::memcpy(&handle, payload, sizeof(handle));
|
||||
const SizeT tail = ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
|
||||
const SizeT messageBytes = sizeof(GbmFrameOffer) + sizeof(GbmNativeHandleHeader) + tail;
|
||||
|
||||
// EVERY REASON THIS CHANNEL REFUSES A FRAME, IN ONE PLACE. Each of them is a frame the
|
||||
// container would refuse anyway - it checks the same counts and the same geometry - and a
|
||||
// frame that is refused here costs nothing: no message, no sequence number, and no entry in
|
||||
// the count of frames waiting to be released.
|
||||
const char* refusal = nullptr;
|
||||
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0) {
|
||||
refusal = "the host described it with a width, height or stride of zero, and a buffer with no size is "
|
||||
"not one a compositor can render into";
|
||||
} else if (handle.NumFds < 1) {
|
||||
refusal = "its handle carries no descriptor, and a frame without a dma-buf is not one the container "
|
||||
"can render into";
|
||||
} else if (handle.NumFds > kMaxHandleFds) {
|
||||
refusal = "its handle carries more descriptors than either end takes";
|
||||
} else if (handle.NumInts < 0) {
|
||||
refusal = "its handle claims a negative number of integers";
|
||||
} else if ((SizeT)handle.NumInts > kMessageMax / sizeof(Int32)) {
|
||||
// This is what keeps the message's length arithmetic from wrapping on a 32 bit ABI: a
|
||||
// handle whose integer array alone is longer than a whole message cannot be carried by
|
||||
// this protocol whatever the rest of it says, and the check below would otherwise be
|
||||
// reading a number that had already turned into a small and plausible one.
|
||||
refusal = "its handle names more integers than a whole message could carry";
|
||||
} else if (fdCount < handle.NumFds) {
|
||||
refusal = "fewer descriptors arrived with it than its handle names";
|
||||
} else if (messageBytes > kMessageMax) {
|
||||
refusal = "the message it makes is longer than the container reads";
|
||||
}
|
||||
|
||||
Bool offered = false;
|
||||
if (refusal != nullptr) {
|
||||
// ONCE, not per frame: a host that describes every frame the same broken way must not
|
||||
// turn this into a line per frame on the apply thread.
|
||||
MGLOG_W_ONCE("gbm frame channel: frame %u (%ux%u) is not offered to the container: %s", offer.Index,
|
||||
offer.Width, offer.Height, refusal);
|
||||
} else {
|
||||
// One message, built whole: the offer, then the handle's own three counts, then the two
|
||||
// arrays the counts describe. What came back from the relay is copied in as far as it
|
||||
// goes and the remainder is left zero, because the container checks the LENGTH it
|
||||
// derives from those counts and never reads the arrays themselves - it takes the
|
||||
// descriptors from the control data and hands the first one to GBM. So a handle whose
|
||||
// bytes are shorter than its own counts imply is padded rather than refused, and one
|
||||
// longer than they imply is cut to what the counts say; either way the length the
|
||||
// container computes is the length it receives.
|
||||
Uint8 message[kMessageMax] = {};
|
||||
GbmFrameOffer out = {};
|
||||
out.Magic = kGbmFrameOfferMagic;
|
||||
out.Version = kGbmFrameVersion;
|
||||
out.Seq = seq;
|
||||
out.Index = offer.Index;
|
||||
out.Width = offer.Width;
|
||||
out.Height = offer.Height;
|
||||
out.StrideBytes = offer.StrideBytes;
|
||||
out.Format = offer.Format;
|
||||
out.UsageLo = offer.UsageLo;
|
||||
out.UsageHi = offer.UsageHi;
|
||||
std::memcpy(message, &out, sizeof(out));
|
||||
const SizeT handleBytes = (payloadBytes < sizeof(GbmNativeHandleHeader) + tail)
|
||||
? payloadBytes
|
||||
: (sizeof(GbmNativeHandleHeader) + tail);
|
||||
std::memcpy(message + sizeof(out), payload, handleBytes);
|
||||
|
||||
struct iovec vector = {};
|
||||
vector.iov_base = message;
|
||||
vector.iov_len = messageBytes;
|
||||
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
|
||||
struct msghdr outgoing = {};
|
||||
outgoing.msg_iov = &vector;
|
||||
outgoing.msg_iovlen = 1;
|
||||
outgoing.msg_control = control;
|
||||
outgoing.msg_controllen = CMSG_SPACE(sizeof(Int) * (SizeT)handle.NumFds);
|
||||
struct cmsghdr* rights = CMSG_FIRSTHDR(&outgoing);
|
||||
rights->cmsg_level = SOL_SOCKET;
|
||||
rights->cmsg_type = SCM_RIGHTS;
|
||||
rights->cmsg_len = CMSG_LEN(sizeof(Int) * (SizeT)handle.NumFds);
|
||||
std::memcpy(CMSG_DATA(rights), fds, sizeof(Int) * (SizeT)handle.NumFds);
|
||||
|
||||
// MSG_DONTWAIT for the promise at the top of this file, and MSG_NOSIGNAL because a
|
||||
// server killed by SIGPIPE when the container goes away takes the frames it was drawing
|
||||
// with it. A full socket, a reset peer and a broken pipe all say the same thing here:
|
||||
// this connection can no longer be trusted with a frame, so it is closed and the next
|
||||
// container to dial is served.
|
||||
const ssize_t sent = ::sendmsg(m_client, &outgoing, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (sent != (ssize_t)messageBytes) {
|
||||
MarkDown("sendmsg", (sent < 0) ? errno : 0);
|
||||
} else {
|
||||
// A frame offered for the first time is the next sequence number and one more frame
|
||||
// outstanding. The same frame sent again to a container that has just dialled is
|
||||
// neither: the host handed over nothing new, and a count of frames waiting to be
|
||||
// released may not be moved by a message that describes one already in it.
|
||||
if (counting) {
|
||||
m_nextSeq = seq + 1;
|
||||
++m_offered;
|
||||
++m_outstanding;
|
||||
}
|
||||
offered = true;
|
||||
// The first offer is the one worth a line, and it carries the host's own numbers:
|
||||
// the format in particular is the field the two ends could disagree about without
|
||||
// either failing, so the value the host actually sends is put where a run on a
|
||||
// device can be read afterwards.
|
||||
MGLOG_I_ONCE("gbm frame channel: the first frame is offered to the container (seq %u index %u %ux%u "
|
||||
"stride %u format 0x%08x)",
|
||||
out.Seq, out.Index, out.Width, out.Height, out.StrideBytes, out.Format);
|
||||
MGLOG_D("gbm frame channel: frame seq=%u index=%u offered (%zu bytes, %d descriptor(s), %llu "
|
||||
"outstanding)",
|
||||
out.Seq, out.Index, messageBytes, handle.NumFds, (unsigned long long)m_outstanding);
|
||||
}
|
||||
}
|
||||
|
||||
// The kernel took its own copies of these when the message was sent, and one that was not
|
||||
// sent is not the container's to hold: every path closes what the relay handed over.
|
||||
for (Int i = 0; i < fdCount; ++i) CloseQuietly(fds[i]);
|
||||
return offered;
|
||||
}
|
||||
|
||||
void GbmFrameChannel::Poll() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
PollLocked();
|
||||
}
|
||||
|
||||
void GbmFrameChannel::PollLocked() {
|
||||
if (m_client < 0) return;
|
||||
for (Int drained = 0; drained < kDrainMax; ++drained) {
|
||||
GbmFrameRelease release = {};
|
||||
struct iovec vector = {};
|
||||
vector.iov_base = &release;
|
||||
vector.iov_len = sizeof(release);
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &vector;
|
||||
message.msg_iovlen = 1;
|
||||
// recvmsg and not recv, for MSG_TRUNC: a message longer than a release can only be a
|
||||
// peer speaking something else, and a release read out of the first twelve bytes of it
|
||||
// would name a frame nobody released.
|
||||
const ssize_t got = ::recvmsg(m_client, &message, MSG_DONTWAIT);
|
||||
if (got < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
// Nothing said, which is what a container that is between frames looks like.
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) return;
|
||||
MarkDown("recvmsg", errno);
|
||||
return;
|
||||
}
|
||||
if (got == 0) {
|
||||
// Zero bytes on a connected socket is the end of it, whether the container exited or
|
||||
// the kernel gave up on it. Its replacement is taken by the next frame.
|
||||
MarkDown("the container closed the connection", 0);
|
||||
return;
|
||||
}
|
||||
if ((message.msg_flags & MSG_TRUNC) != 0 || got != (ssize_t)sizeof(release)) {
|
||||
// The message is skipped, not the connection: on a message-oriented socket a
|
||||
// message too long for the buffer is consumed whole, so the next one is still a
|
||||
// message rather than the tail of this one.
|
||||
MGLOG_W_ONCE("gbm frame channel: the container sent a message that is not a %zu byte release; it is "
|
||||
"skipped",
|
||||
sizeof(release));
|
||||
continue;
|
||||
}
|
||||
if (release.Magic != kGbmFrameReleaseMagic) {
|
||||
MGLOG_W_ONCE("gbm frame channel: a message from the container begins with 0x%08x rather than 'MGGR'; "
|
||||
"it is skipped",
|
||||
release.Magic);
|
||||
continue;
|
||||
}
|
||||
if (release.Version != kGbmFrameVersion) {
|
||||
// Refused rather than acted on: the version is what says how the rest of the message
|
||||
// is laid out, and a release read with the wrong layout names a frame the container
|
||||
// did not mean.
|
||||
MGLOG_E_ONCE("gbm frame channel: the container speaks protocol version %u where this server speaks "
|
||||
"version %u; its releases are refused until the two agree",
|
||||
release.Version, (Uint32)kGbmFrameVersion);
|
||||
continue;
|
||||
}
|
||||
++m_released;
|
||||
if (m_outstanding > 0) --m_outstanding;
|
||||
// The first release is the only one worth a line: it is the proof that the container
|
||||
// took the descriptor and its compositor retired the frame, where every later one says
|
||||
// the same thing sixty times a second.
|
||||
MGLOG_I_ONCE("gbm frame channel: the container is releasing frames (first: seq %u)", release.Seq);
|
||||
}
|
||||
// Anything past this cap stays in the socket, whose buffer the kernel bounds, and goes out
|
||||
// with the next frame: a peer that says nothing but releases may not spend the apply thread
|
||||
// here.
|
||||
}
|
||||
|
||||
void GbmFrameChannel::Close() {
|
||||
// THE ACCEPTOR IS STOPPED AND JOINED FIRST, and outside the lock. It may be inside that
|
||||
// lock when this is called - a teardown that waited for it while holding it would wait for
|
||||
// ever - and it polls the listening descriptor, which may not be closed under it: the next
|
||||
// open() in this process hands out the descriptor number a moment later, and the thread
|
||||
// would then be polling somebody else's socket.
|
||||
m_accepting.store(false, std::memory_order_release);
|
||||
if (m_acceptor.joinable()) m_acceptor.join();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
// The frame is this channel's reference and is released with it: the backend has already
|
||||
// dropped its own by the time a teardown here is a teardown of the frame as well.
|
||||
ReleaseFrameLocked();
|
||||
// The connection is closed quietly rather than marked down: this is a teardown, not a
|
||||
// failure, and a warning about it would be noise in a log read for the failures.
|
||||
CloseQuietly(m_client);
|
||||
m_client = -1;
|
||||
DropRelay();
|
||||
if (m_listen >= 0) {
|
||||
// The node is this channel's to remove only while it still IS this channel's socket: a
|
||||
// server that was restarted has bound the same path with a socket of its own, and a
|
||||
// teardown that unlinked it would take the successor's node away with it. st_dev and
|
||||
// st_ino against the listening descriptor is the cheapest way to ask, and a check that
|
||||
// fails only leaves a name behind that the next Open() unlinks anyway.
|
||||
struct stat node = {};
|
||||
struct stat socket = {};
|
||||
if (!m_path.empty() && ::stat(m_path.c_str(), &node) == 0 && ::fstat(m_listen, &socket) == 0 &&
|
||||
node.st_dev == socket.st_dev && node.st_ino == socket.st_ino) {
|
||||
::unlink(m_path.c_str());
|
||||
}
|
||||
CloseQuietly(m_listen);
|
||||
m_listen = -1;
|
||||
}
|
||||
m_path.clear();
|
||||
// The counters are history and stay: a caller that logs Outstanding() after a teardown is
|
||||
// asking what happened, and IsUp() is what answers what is connected now.
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// A build that is not the Android render server has no AHardwareBuffer to offer and no
|
||||
// container beside it. The class exists so that callers do not have to be compiled
|
||||
// differently, and every entry point answers the way an absent container does.
|
||||
GbmFrameChannel::~GbmFrameChannel() = default;
|
||||
Bool GbmFrameChannel::Open() { return false; }
|
||||
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer*, const HostFrameOffer&) { return false; }
|
||||
void GbmFrameChannel::Poll() {}
|
||||
void GbmFrameChannel::Close() {}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// End of Source File Header
|
||||
@@ -0,0 +1,281 @@
|
||||
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include "HostFrameBridge.h"
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// THE FRAME THE HOST HANDED OVER, HANDED ON TO THE CONTAINER AS A dma-buf.
|
||||
//
|
||||
// The display host owns a pool of AHardwareBuffers: it allocates them, this side imports the
|
||||
// one it offers as a draw target (HostFrameBridge, DirectGLES::HostFrameTarget), and the host
|
||||
// puts it on the glass when this side answers that it is drawn. The Wayland compositor in the
|
||||
// Linux container has to render into THOSE SAME frames - it runs this project's own GBM, and a
|
||||
// gbm_bo_create there answers with the phone's frame instead of a dma-heap allocation of its
|
||||
// own - so every frame this side takes has to reach the container as that frame's dma-buf
|
||||
// descriptor. That, and the release that comes back, is all this channel carries.
|
||||
//
|
||||
// The container's end is MobileGL/MG_Gbm/GbmHostChannel.cpp. It reads exactly the bytes
|
||||
// declared below and answers with a release once its compositor no longer reads the frame.
|
||||
// The three shapes ARE that file's GbmFrameOffer/GbmFrameRelease and GbmInternal.h's
|
||||
// AHBNativeHandleHeader, field for field and packing included. They are restated here rather
|
||||
// than shared because the two ends are built into different libraries from different build
|
||||
// configurations: MG_Backend is the Android render server and never sees libgbm's headers,
|
||||
// while MG_Gbm is built for the container and never sees Android's. A change on either side
|
||||
// is a protocol change, and the magic is what makes a mismatched pair fail loudly instead of
|
||||
// being read as plausible numbers.
|
||||
//
|
||||
// ONE MESSAGE PER FRAME, on SOCK_SEQPACKET, and that is part of the protocol rather than an
|
||||
// implementation detail: an offer and its handle only mean anything together, and the message
|
||||
// boundary is what lets the reader take the two as one thing. The container derives the
|
||||
// length of the message it expects from the handle's own two counts and refuses any other
|
||||
// length, so this end sends the offer immediately followed by the handle's counts and its two
|
||||
// arrays, with the descriptors in the SCM_RIGHTS control data of that same sendmsg.
|
||||
//
|
||||
// The channel is OPTIONAL, and that is the rule the implementation is built around: a
|
||||
// container that is not running, has died, or has stopped reading may cost the render path a
|
||||
// syscall or two, and may never block it, fail it, or grow without bound. Every entry point
|
||||
// below is non-blocking and answers silence with silence.
|
||||
inline constexpr Uint32 kGbmFrameOfferMagic = 0x4647474Du; // 'M','G','G','F' in memory order
|
||||
inline constexpr Uint32 kGbmFrameReleaseMagic = 0x5247474Du; // 'M','G','G','R' in memory order
|
||||
inline constexpr Uint32 kGbmFrameVersion = 1u;
|
||||
// WHERE THE CONTAINER DIALS. /data/local/tmp is the one directory both halves of a split run
|
||||
// can be pointed at on a device, and the default lives on this end because this end is the
|
||||
// server. The container's compiled-in default is /run/mobilegl-gbm.sock - the same protocol
|
||||
// with the path each side can reach unaided - so a deployment that puts the two in different
|
||||
// mount namespaces sets MOBILEGL_GBM_SOCKET on both; it is read here and in
|
||||
// GbmHostChannelCreate, from the same variable, for that reason.
|
||||
inline constexpr const char* kGbmFrameSocketPath = "/data/local/tmp/mobilegl-gbm.sock";
|
||||
inline constexpr const char* kGbmFrameSocketEnv = "MOBILEGL_GBM_SOCKET";
|
||||
|
||||
// One frame the host offered, described for the container. Width, Height, StrideBytes and
|
||||
// Format are the host's own numbers for the frame (HostFrameOffer), carried through
|
||||
// unchanged: they describe the memory the descriptor points at, and this end is not the place
|
||||
// where a number the other two ends agreed on gets rewritten.
|
||||
struct GbmFrameOffer {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Seq; // this channel's, one per frame offered
|
||||
Uint32 Index; // the host's own buffer index, echoed so the phone can match a frame
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 StrideBytes;
|
||||
Uint32 Format;
|
||||
Uint32 UsageLo;
|
||||
Uint32 UsageHi;
|
||||
} __attribute__((packed));
|
||||
|
||||
// What the container says once it is done with a frame. It may arrive late, it may never
|
||||
// arrive, and both are cheap here: the channel keeps a count and no per-frame record (see
|
||||
// Outstanding below), so nothing is leaked and nothing grows when one is missed.
|
||||
struct GbmFrameRelease {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Seq;
|
||||
} __attribute__((packed));
|
||||
|
||||
// THE OTHER DIRECTION: A dma-buf THE CONTAINER WANTS THIS SIDE TO BACK A TEXTURE WITH.
|
||||
//
|
||||
// The compositor's scene reaches the display daemon's dma-bufs through glEGLImageTargetTexture2DOES
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp, MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp both
|
||||
// name this half as the next change). eglCreateImageKHR has already held a dup() of every plane's
|
||||
// descriptor since the image was made; what was missing is the render server's half - telling THIS
|
||||
// side which texture that dma-buf backs, over the one channel that can carry a descriptor across the
|
||||
// container boundary. This message is that half: the client names the texture target and the
|
||||
// texture's own name, describes the buffer exactly as drmModeAddFB2 would, and the plane descriptors
|
||||
// ride in the SCM_RIGHTS control data of the same sendmsg. One message per import, as SEQPACKET
|
||||
// requires, so a descriptor and its description are never read apart.
|
||||
//
|
||||
// The reply is what the client waits for before a framebuffer is allowed to be complete over the
|
||||
// texture: Ok=0 means this side could not make the import real, and the client keeps treating the
|
||||
// texture as having no storage rather than letting it render into nothing.
|
||||
inline constexpr Uint32 kGbmImportRequestMagic = 0x4947474Du; // 'M','G','G','I' in memory order
|
||||
inline constexpr Uint32 kGbmImportReplyMagic = 0x52474749u; // 'I','G','G','R' in memory order
|
||||
inline constexpr Uint32 kGbmImportVersion = 1u;
|
||||
inline constexpr Uint32 kGbmImportMaxPlanes = 4u;
|
||||
|
||||
struct GbmImportRequest {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
|
||||
Uint32 TexName; // the CLIENT's texture name; this side resolves its twin
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 FourCC;
|
||||
Uint32 PlaneCount; // 1..kGbmImportMaxPlanes; the descriptors are the message's control data
|
||||
Int32 Stride[kGbmImportMaxPlanes];
|
||||
Int32 Offset[kGbmImportMaxPlanes];
|
||||
Uint64 Modifier[kGbmImportMaxPlanes];
|
||||
Uint32 HasModifier[kGbmImportMaxPlanes];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct GbmImportReply {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Ok;
|
||||
Uint32 Reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
// An AHardwareBuffer native handle as it appears on the wire: the platform's native_handle_t
|
||||
// (version, then the two counts, then the descriptor array followed by the integer array),
|
||||
// written by AHardwareBuffer_sendHandleToUnixSocket and read by the container's
|
||||
// AHBNativeHandleHeader. The counts are 32 bit on every Android ABI, which is why they are
|
||||
// spelled out here rather than taken from a platform header this library cannot include.
|
||||
struct GbmNativeHandleHeader {
|
||||
Int32 Version;
|
||||
Int32 NumFds;
|
||||
Int32 NumInts;
|
||||
} __attribute__((packed));
|
||||
|
||||
// ONE CONTAINER, ONE LISTENING SOCKET, TWO THREADS: the render path's, and an acceptor's.
|
||||
//
|
||||
// A compositor dials ONCE - when it creates its GBM device - and keeps that connection for the
|
||||
// life of the process, while this side offers a frame per drawn frame. An accept that only ran
|
||||
// inside OfferFrame therefore ran after the dial it was supposed to answer, and a dialer that
|
||||
// nobody took kept the listener's queue full for every later one. That is measured on the
|
||||
// phone rather than argued: with accept() only on the offer path, connect() from the container
|
||||
// answered EAGAIN(11) on four attempts out of four while the node sat there listening.
|
||||
//
|
||||
// So the accept has a thread of its own, started by Open() and stopped by Close(), and it
|
||||
// adopts what it takes on the spot - including sending it the frame the host last handed over,
|
||||
// because a compositor's first allocation is the one its swapchain is built from. The
|
||||
// connected socket, the frame that is remembered, the relay and the counters are shared between
|
||||
// the two threads and guarded by the one mutex below; both entry points are non-blocking, so
|
||||
// neither thread can park the other for longer than a syscall.
|
||||
class GbmFrameChannel {
|
||||
public:
|
||||
GbmFrameChannel() = default;
|
||||
GbmFrameChannel(const GbmFrameChannel&) = delete;
|
||||
GbmFrameChannel& operator=(const GbmFrameChannel&) = delete;
|
||||
~GbmFrameChannel();
|
||||
|
||||
// Creates the listening socket if it is not there yet and starts the acceptor, and does
|
||||
// nothing otherwise. It never waits for a container: the socket is bound, a backlog is
|
||||
// given to listen(), and the acceptor takes whoever dials from then on - whether or not a
|
||||
// frame is being drawn. A caller that wants the container to find the socket before the
|
||||
// first frame - the container dials once at its own device creation and retries about once
|
||||
// a second - can call this when the host-framed surface is initialized, beside
|
||||
// HostFrameBridge::Open.
|
||||
Bool Open();
|
||||
|
||||
// Offers one frame the host just handed over, as the dma-buf descriptor behind it.
|
||||
// False means the frame did not go out, for any of the reasons this channel treats as
|
||||
// ordinary: no container is connected, the container just died, or the frame could not be
|
||||
// described. None of them is the caller's problem and none of them is fatal - the frame
|
||||
// is drawn and presented exactly as it would have been without a channel at all.
|
||||
Bool OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer);
|
||||
|
||||
// Reads whatever the container has said since the last call and marks the channel down if
|
||||
// the connection has ended. OfferFrame already does this once per frame, so a caller only
|
||||
// needs it if it wants releases drained on a path that has no new frame in it.
|
||||
void Poll();
|
||||
|
||||
// The frame the host handed over is gone - it was presented, or it was replaced - and this
|
||||
// channel may no longer hold it or offer it to anybody. THE CHANNEL ITSELF STAYS OPEN, and
|
||||
// that is the point of the call: the listening socket, its node and the container's
|
||||
// connection outlive every frame, and a channel that was closed and reopened per frame took
|
||||
// the node away sixty times a second - a container dialling at the wrong moment got ENOENT
|
||||
// for a socket that was there a millisecond earlier and would be there a millisecond later.
|
||||
void DropFrame();
|
||||
|
||||
// True while a container is connected and the frames are being offered to it.
|
||||
Bool IsUp() const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_client >= 0;
|
||||
}
|
||||
|
||||
// How many frames have been offered and not released, as a count of messages and not a
|
||||
// record of them. It is here to be logged, and it is deliberately the only per-frame state
|
||||
// the channel keeps: a release that never arrives costs a number that is too high, and
|
||||
// nothing else, which is what makes a container that stops reading harmless.
|
||||
Uint64 Outstanding() const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_outstanding;
|
||||
}
|
||||
|
||||
// Closes the connection, the listening socket and the node it created. Idempotent, safe
|
||||
// from a destructor, and safe to call while a container is connected: the container sees
|
||||
// the connection end and goes back to allocating its frames locally, which is the state it
|
||||
// starts in.
|
||||
void Close();
|
||||
|
||||
private:
|
||||
// The socketpair a frame's handle is handed to and read back from. Recreated on demand,
|
||||
// because the one call the NDK offers for a buffer's descriptors sends them as a message
|
||||
// of its own and the container takes one message per frame - see the note in the .cpp.
|
||||
Bool EnsureRelay();
|
||||
void DropRelay();
|
||||
Bool RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
|
||||
Int* fds, Int& fdCount);
|
||||
|
||||
// Takes the one container the protocol allows, and closes anyone else who dials while a
|
||||
// LIVE one is connected rather than leaving them to believe they are being served. A
|
||||
// connection whose peer has exited is not a live one: it is closed here and its dialer's
|
||||
// replacement is taken, which is what a compositor that crashed and was restarted is.
|
||||
void AdoptLocked(Int fd);
|
||||
|
||||
// True while the connected container is still there. recvmsg with MSG_PEEK is what asks:
|
||||
// zero bytes is the peer's close, EAGAIN is a peer that is between frames, and a release
|
||||
// waiting in the socket is a peer that is alive - peeked at rather than read, because the
|
||||
// render path is the one that spends releases.
|
||||
Bool ClientAliveLocked();
|
||||
|
||||
// Takes the one container the protocol allows out of whoever dialled.
|
||||
void AcceptLoop(Int listen);
|
||||
|
||||
// Sends the frame that is the draw target to a container that has just connected.
|
||||
void ReofferFrameLocked();
|
||||
|
||||
// Remembers the frame the host last handed over, holding a reference to it so that a
|
||||
// container which dials after the offer can still be given it. The previous one is
|
||||
// released here; nothing else in the channel holds a frame.
|
||||
void RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq);
|
||||
void ReleaseFrameLocked();
|
||||
|
||||
// The offer itself: the handle relayed out of the buffer, the message built, one sendmsg.
|
||||
// A frame that has already been offered once is sent again with the sequence number it was
|
||||
// offered under and does not move the counters, because it is the same frame and not a new
|
||||
// one.
|
||||
Bool SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq, Bool counting);
|
||||
|
||||
// The body of Poll(), for a caller that is already holding the lock.
|
||||
void PollLocked();
|
||||
|
||||
// Closes the connection and says why, once. The channel stays open for the next container.
|
||||
void MarkDown(const char* what, Int error);
|
||||
|
||||
Int m_listen = -1;
|
||||
Int m_client = -1;
|
||||
Int m_relay[2] = {-1, -1};
|
||||
String m_path;
|
||||
// Bumped only for a frame that actually went out, so a sequence number is never reused and
|
||||
// a release can never be read as belonging to a frame of an earlier connection.
|
||||
Uint32 m_nextSeq = 0;
|
||||
Uint64 m_offered = 0;
|
||||
Uint64 m_released = 0;
|
||||
Uint64 m_outstanding = 0;
|
||||
// The frame the host handed over most recently, held by this channel's own reference for as
|
||||
// long as it is the draw target: it is what a container that dials late is sent.
|
||||
struct AHardwareBuffer* m_frame = nullptr;
|
||||
HostFrameOffer m_frameOffer = {};
|
||||
Uint32 m_frameSeq = 0;
|
||||
// The acceptor, and the flag that ends it. The listening descriptor is handed to the thread
|
||||
// as its own copy, so it is the only thing in here that thread reads without the lock - and
|
||||
// Close() joins the thread before it closes that descriptor.
|
||||
std::thread m_acceptor;
|
||||
std::atomic<Bool> m_accepting{false};
|
||||
// Guards every member above, from both threads. Mutable because the two const questions a
|
||||
// caller asks about the channel - is a container connected, how many frames are outstanding
|
||||
// - are answers that have to be read under it.
|
||||
mutable std::mutex m_mutex;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "HostFrameBridge.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
namespace {
|
||||
Bool ReadExactly(Int fd, void* out, SizeT n) {
|
||||
Uint8* at = static_cast<Uint8*>(out);
|
||||
while (n > 0) {
|
||||
const ssize_t got = ::read(fd, at, n);
|
||||
if (got <= 0) {
|
||||
if (got < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
at += got;
|
||||
n -= static_cast<SizeT>(got);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool WriteExactly(Int fd, const void* in, SizeT n) {
|
||||
const Uint8* at = static_cast<const Uint8*>(in);
|
||||
while (n > 0) {
|
||||
const ssize_t put = ::write(fd, at, n);
|
||||
if (put <= 0) {
|
||||
if (put < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
at += put;
|
||||
n -= static_cast<SizeT>(put);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
HostFrameBridge::~HostFrameBridge() { Close(); }
|
||||
|
||||
Bool HostFrameBridge::Open(const char* path, Int acceptTimeoutMs, String& why) {
|
||||
Close();
|
||||
const char* name = (path != nullptr && path[0] != 0) ? path : kHostFrameSocket;
|
||||
|
||||
m_listen = ::socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (m_listen < 0) {
|
||||
why = std::format("socket(AF_UNIX): {}", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
if (std::strlen(name) >= sizeof(address.sun_path)) {
|
||||
why = std::format("the socket name is longer than the kernel takes: {}", name);
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
// "@name" is the abstract namespace: sun_path[0] is NUL and the name follows it.
|
||||
// Nothing is created on disk, so nothing is left behind and no file permission stands
|
||||
// between two apps that may not write each other's directories.
|
||||
if (name[0] == '@') {
|
||||
address.sun_path[0] = 0;
|
||||
std::strncpy(address.sun_path + 1, name + 1, sizeof(address.sun_path) - 2);
|
||||
} else {
|
||||
std::strncpy(address.sun_path, name, sizeof(address.sun_path) - 1);
|
||||
// A leftover socket file from a server that died is not a reason to refuse the host.
|
||||
::unlink(name);
|
||||
}
|
||||
if (::bind(m_listen, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) {
|
||||
why = std::format("bind({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (::listen(m_listen, 1) != 0) {
|
||||
why = std::format("listen({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
pollfd waiting{};
|
||||
waiting.fd = m_listen;
|
||||
waiting.events = POLLIN;
|
||||
const Int ready = ::poll(&waiting, 1, acceptTimeoutMs);
|
||||
if (ready <= 0) {
|
||||
why = (ready == 0) ? std::format("no display host dialled {} within {} ms", name, acceptTimeoutMs)
|
||||
: std::format("poll({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
m_fd = ::accept(m_listen, nullptr, nullptr);
|
||||
if (m_fd < 0) {
|
||||
why = std::format("accept({}): {}", name, std::strerror(errno));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
MGLOG_I("host frame bridge: a display host is on %s", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool HostFrameBridge::Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why) {
|
||||
*buffer = nullptr;
|
||||
offer = HostFrameOffer{};
|
||||
if (m_fd < 0) {
|
||||
why = "the bridge is not open";
|
||||
return false;
|
||||
}
|
||||
AHardwareBuffer* received = nullptr;
|
||||
if (AHardwareBuffer_recvHandleFromUnixSocket(m_fd, &received) != 0 || received == nullptr) {
|
||||
why = "the host stopped offering frames, or the handle did not cross";
|
||||
return false;
|
||||
}
|
||||
if (!ReadExactly(m_fd, &offer, sizeof(offer)) || offer.Magic != kHostFrameMagic) {
|
||||
// The handle is already this side's by the time this fails, so it goes back before
|
||||
// the failure does: a reference held past the connection starves the host's pool.
|
||||
AHardwareBuffer_release(received);
|
||||
why = std::format("the frame's description did not follow its handle (magic 0x{:08x})", offer.Magic);
|
||||
return false;
|
||||
}
|
||||
*buffer = received;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool HostFrameBridge::Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why) {
|
||||
if (m_fd < 0) {
|
||||
why = "the bridge is not open";
|
||||
return false;
|
||||
}
|
||||
HostFrameAck ack{};
|
||||
ack.Magic = kHostFrameMagic;
|
||||
ack.Version = kHostFrameVersion;
|
||||
ack.Index = offer.Index;
|
||||
ack.Imported = imported ? 1u : 0u;
|
||||
ack.Drawn = drawn ? 1u : 0u;
|
||||
ack.FenceOk = fenceOk ? 1u : 0u;
|
||||
if (!WriteExactly(m_fd, &ack, sizeof(ack))) {
|
||||
why = std::format("the answer to frame {} did not go out: {}", offer.Index, std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
// The host answers with its own view of the pixels. It is what closes the round trip, so
|
||||
// it is read here and kept rather than left in the socket for the next frame to trip over.
|
||||
m_lastSeen = HostFrameSeen{};
|
||||
if (!ReadExactly(m_fd, &m_lastSeen, sizeof(m_lastSeen)) || m_lastSeen.Magic != kHostFrameMagic) {
|
||||
why = std::format("frame {} was answered but the host's readback did not arrive", offer.Index);
|
||||
m_lastSeen = HostFrameSeen{};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostFrameBridge::Close() {
|
||||
if (m_fd >= 0) ::close(m_fd);
|
||||
if (m_listen >= 0) ::close(m_listen);
|
||||
m_fd = -1;
|
||||
m_listen = -1;
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
HostFrameBridge::~HostFrameBridge() = default;
|
||||
Bool HostFrameBridge::Open(const char*, Int, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameBridge::Acquire(struct AHardwareBuffer**, HostFrameOffer&, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameBridge::Complete(const HostFrameOffer&, Bool, Bool, Bool, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
void HostFrameBridge::Close() {}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,103 @@
|
||||
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// THE DISPLAY HOST'S FRAMES, AS THEY ARRIVE.
|
||||
//
|
||||
// The Android side that owns the screen allocates its frames itself and offers each one over
|
||||
// an AF_UNIX socket: the handle travels ahead of a fixed-size description on the same
|
||||
// connection, and the host then waits for this side to say the frame is drawn before it puts
|
||||
// it on the glass. Why the host allocates rather than this side asking for the display's own
|
||||
// buffers is measured, not chosen: AHardwareBuffer_createFromHandle accepts an
|
||||
// AHardwareBuffer this process allocated and refuses one the display queue handed over
|
||||
// (gralloc BAD_VALUE), so the only frames a render server can draw into are the ones the host
|
||||
// made for it.
|
||||
//
|
||||
// The three message shapes below ARE the host's (its ahb_bridge.h), field for field and
|
||||
// packing included. They are restated here rather than shared because the two ends are built
|
||||
// by different projects; a change on either side is a protocol change, and the magic is what
|
||||
// makes a mismatched pair fail loudly instead of being read as plausible numbers.
|
||||
inline constexpr Uint32 kHostFrameMagic = 0x4D474C41u; // the host spells it "ALGM" in memory
|
||||
inline constexpr Uint32 kHostFrameVersion = 1u;
|
||||
// AN ABSTRACT NAME, not a path: the two ends are separate apps, and neither may create a
|
||||
// socket file in /data/local/tmp (its directory grants others traverse only). An abstract
|
||||
// name lives in the kernel namespace instead of the filesystem, so there is no file
|
||||
// permission to get wrong - the same reason this project names its own endpoints @mgl-*.
|
||||
inline constexpr const char* kHostFrameSocket = "@mobilegl-host-frame";
|
||||
|
||||
struct HostFrameOffer {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Index;
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 StrideBytes;
|
||||
Uint32 Format;
|
||||
Uint32 UsageLo;
|
||||
Uint32 UsageHi;
|
||||
char Note[64];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HostFrameAck {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Index;
|
||||
Uint32 Imported;
|
||||
Uint32 Drawn;
|
||||
Uint32 FenceOk;
|
||||
Uint8 Color[4];
|
||||
char Note[64];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HostFrameSeen {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Index;
|
||||
Uint32 Locked;
|
||||
Uint8 Observed[4];
|
||||
Uint8 ObservedCorner[4];
|
||||
char Note[64];
|
||||
} __attribute__((packed));
|
||||
|
||||
// One host connection at a time, which is what the host offers: it dials, hands over a frame,
|
||||
// waits for the answer, and hands over the next.
|
||||
class HostFrameBridge {
|
||||
public:
|
||||
HostFrameBridge() = default;
|
||||
HostFrameBridge(const HostFrameBridge&) = delete;
|
||||
HostFrameBridge& operator=(const HostFrameBridge&) = delete;
|
||||
~HostFrameBridge();
|
||||
|
||||
// Listens on `path` (the well-known name by default) and waits for the host. A host that
|
||||
// never dials is answered with false and a reason, never an abort.
|
||||
Bool Open(const char* path, Int acceptTimeoutMs, String& why);
|
||||
|
||||
// Takes the next frame. Ownership of the returned AHardwareBuffer is the caller's, who
|
||||
// must hand it back through ReleaseOwned() once the frame is off the glass.
|
||||
Bool Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why);
|
||||
|
||||
// Tells the host what became of the frame it offered, and reads back what the host's own
|
||||
// view of the pixels holds. `drawn` is what the host waits for before it presents.
|
||||
Bool Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why);
|
||||
|
||||
// The readback the host returned for the last completed frame, as the host reported it.
|
||||
const HostFrameSeen& LastSeen() const { return m_lastSeen; }
|
||||
|
||||
void Close();
|
||||
|
||||
private:
|
||||
Int m_fd = -1;
|
||||
Int m_listen = -1;
|
||||
HostFrameSeen m_lastSeen{};
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h> // MGPipeForgetAllPublications, at the end of ShutdownSplitRoles
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/FatalFunnel.h> // InstallPipeSessionFailHook, installed at step 0 below
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
@@ -398,6 +399,9 @@ namespace MobileGL::MG_Backend {
|
||||
// and every later split bring-up in the process fails at CreateBackend's m_backend!=null
|
||||
// guard.
|
||||
MG_Remote::Server::ServerLoopInstance().Stop();
|
||||
// The server's twins died with the session; the frontend objects that are destroyed after
|
||||
// this (the GL context's) have nothing left on the other side to delete.
|
||||
MG_Pipe::MGPipeForgetAllPublications();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
// MobileGL - MobileGL/MG_Gbm/GbmApi.cpp
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
//
|
||||
// The extern "C" surface: every entry point gbm.h declares, and nothing else.
|
||||
//
|
||||
// This file is a translation layer, and it is kept that way on purpose. Each entry
|
||||
// point does three things - check the pointers it was handed, take the device's one
|
||||
// mutex for the whole call, and hand the work to GbmDevice.cpp - so the concurrency
|
||||
// rule ("one mutex per device, taken once at the API boundary, never inside the
|
||||
// allocation code") is visible in one place instead of being spread over the
|
||||
// backends. A missing lock is a bug that only shows up in the compositor, so the
|
||||
// boilerplate is deliberately identical in every function.
|
||||
//
|
||||
// Two conventions are observable from outside and are stated here because they are
|
||||
// the ones a caller can get wrong:
|
||||
//
|
||||
// * gbm_bo_get_fd and gbm_bo_get_fd_for_plane return a DUP the caller owns. The
|
||||
// caller closes what it was given; the buffer's own descriptor is closed once,
|
||||
// when the buffer is destroyed, and never by a caller's close.
|
||||
// * Nothing here throws and nothing here aborts. A failure is reported the way
|
||||
// GBM reports failures: null or -1 with errno set, so that the strerror(errno)
|
||||
// a caller prints describes the failure that actually happened.
|
||||
|
||||
#include "GbmInternal.h"
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Gbm;
|
||||
|
||||
namespace
|
||||
{
|
||||
// The one mutex that guards a record, reached through the device that owns it.
|
||||
// Null means the pointer was not one this library handed out, and every entry
|
||||
// point turns that into its own failure answer rather than dereferencing it.
|
||||
//
|
||||
// A buffer or surface with no owner cannot exist while the library is used
|
||||
// correctly - the device destroys everything it still holds before it goes away
|
||||
// - so null here is a defence against a stale pointer rather than a state the
|
||||
// code produces.
|
||||
std::mutex* MutexOf(struct gbm_device* device) noexcept
|
||||
{
|
||||
return device != nullptr ? &device->Impl.Mutex : nullptr;
|
||||
}
|
||||
|
||||
std::mutex* MutexOf(struct gbm_bo* bo) noexcept
|
||||
{
|
||||
return bo != nullptr ? MutexOf(bo->Impl.Owner) : nullptr;
|
||||
}
|
||||
|
||||
std::mutex* MutexOf(struct gbm_surface* surface) noexcept
|
||||
{
|
||||
return surface != nullptr ? MutexOf(surface->Impl.Owner) : nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" struct gbm_device* gbm_create_device(int fd)
|
||||
{
|
||||
// No lock to take: the device being built is not reachable by any other thread
|
||||
// until this call returns it.
|
||||
return GbmDeviceCreate(fd);
|
||||
}
|
||||
|
||||
extern "C" void gbm_device_destroy(struct gbm_device* gbm)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// The lock is taken and deliberately never released: the mutex lives inside the
|
||||
// record this call deletes, and unlocking a destroyed mutex is worse than
|
||||
// leaving a dead one locked. Any other thread touching this device after its
|
||||
// owner declared it dead is already outside what GBM can be asked to survive.
|
||||
mutex->lock();
|
||||
GbmDeviceDestroy(*gbm);
|
||||
}
|
||||
|
||||
extern "C" int gbm_device_get_fd(struct gbm_device* gbm)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// The device's own dup of the fd it was created with, or -1 when it was created
|
||||
// without one and no render node could be opened. That is not an error state:
|
||||
// the self-allocated backend never talks to the node, so a device can be fully
|
||||
// usable and still have no descriptor to report.
|
||||
return gbm->Impl.DeviceFd;
|
||||
}
|
||||
|
||||
extern "C" const char* gbm_device_get_backend_name(struct gbm_device* gbm)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// The name says which memory this device hands out ("mobilegl-dma-heap" or
|
||||
// "mobilegl-memfd"), because the callers that read it - a compositor logging its
|
||||
// backend, a peer test recording what it got - are asking exactly that. The
|
||||
// string lives in the device and outlives every call that can read it.
|
||||
return gbm->Impl.BackendName.c_str();
|
||||
}
|
||||
|
||||
extern "C" int gbm_device_is_format_supported(struct gbm_device* gbm, uint32_t format, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Every buffer this device produces is linear, mappable and shareable, so the
|
||||
// usage flags do not narrow the answer to any of the usual combinations -
|
||||
// including GBM_BO_USE_SCANOUT, which a linear dma-buf from the system heap
|
||||
// satisfies. The one request the backend cannot honour is protected memory.
|
||||
if ((flags & GBM_BO_USE_PROTECTED) != 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return GbmFormatIsSupported(format) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int gbm_device_get_format_modifier_plane_count(struct gbm_device* gbm, uint32_t format, uint64_t modifier)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Only linear memory exists here, so any other modifier names a buffer this
|
||||
// device will never produce and the answer is zero planes. INVALID is answered
|
||||
// as well, because it means "no modifier was declared", which is the same
|
||||
// layout question asked without an answer.
|
||||
if (modifier != DrmFormatModLinear && modifier != DrmFormatModInvalid)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
GbmPlaneLayout planes[GbmMaxPlanes] = {};
|
||||
Int planeCount = 0;
|
||||
SizeT totalBytes = 0;
|
||||
if (!GbmDescribeFormat(format, 1, 1, planes, planeCount, totalBytes))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return planeCount;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Buffer objects
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_create(struct gbm_device* gbm, uint32_t width, uint32_t height, uint32_t format,
|
||||
uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// No modifier list: the buffer is linear and gbm_bo_get_modifier says so.
|
||||
return GbmBufferCreate(*gbm, width, height, format, flags, nullptr, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_create_with_modifiers(struct gbm_device* gbm, uint32_t width, uint32_t height,
|
||||
uint32_t format, const uint64_t* modifiers,
|
||||
const unsigned int count)
|
||||
{
|
||||
// The modifier-less variant is the one with no flags parameter, so it is the
|
||||
// flags-0 case of the other, and saying that here keeps one implementation.
|
||||
return gbm_bo_create_with_modifiers2(gbm, width, height, format, modifiers, count, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_create_with_modifiers2(struct gbm_device* gbm, uint32_t width, uint32_t height,
|
||||
uint32_t format, const uint64_t* modifiers,
|
||||
const unsigned int count, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmBufferCreate(*gbm, width, height, format, flags, modifiers, (Uint32)count);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_bo_import(struct gbm_device* gbm, uint32_t type, void* buffer, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmBufferImport(*gbm, type, buffer, flags);
|
||||
}
|
||||
|
||||
extern "C" void* gbm_bo_map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width, uint32_t height,
|
||||
uint32_t flags, uint32_t* stride, void** map_data)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
// The rectangle's size is not used to size the mapping: the whole buffer is
|
||||
// mapped once, at its own length, and the rectangle is applied as an offset into
|
||||
// it. That is what lets two overlapping maps share one address, and it is why a
|
||||
// buffer's mapping stays valid for a caller that only ever asks for a corner.
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)flags;
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
Uint32 mapStride = 0;
|
||||
void* address = GbmBufferMap(*bo, x, y, mapStride);
|
||||
if (address == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (stride != nullptr)
|
||||
{
|
||||
*stride = mapStride;
|
||||
}
|
||||
if (map_data != nullptr)
|
||||
{
|
||||
// The same address the function returns. Callers pass back either one to
|
||||
// gbm_bo_unmap, and both have to name this mapping.
|
||||
*map_data = address;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
extern "C" void gbm_bo_unmap(struct gbm_bo* bo, void* map_data)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
GbmBufferUnmap(*bo, map_data);
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_width(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Width;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_height(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Height;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_stride(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Strides[0];
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_stride_for_plane(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// A plane this buffer does not have is answered with zero, which is the only
|
||||
// value a caller can tell apart from a real stride.
|
||||
if (plane < 0 || plane >= bo->Impl.PlaneCount)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
return bo->Impl.Strides[plane];
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_format(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Format;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_bpp(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Bits per pixel: a 32 bit format answers 32, which is what a caller computing a
|
||||
// row length in bits or a row length in bytes from the same number expects.
|
||||
return bo->Impl.BytesPerPixel * 8;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gbm_bo_get_offset(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
if (plane < 0 || plane >= bo->Impl.PlaneCount)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
return bo->Impl.Offsets[plane];
|
||||
}
|
||||
|
||||
extern "C" struct gbm_device* gbm_bo_get_device(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
return bo->Impl.Owner;
|
||||
}
|
||||
|
||||
extern "C" union gbm_bo_handle gbm_bo_get_handle(struct gbm_bo* bo)
|
||||
{
|
||||
union gbm_bo_handle handle;
|
||||
handle.ptr = nullptr;
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
handle.s32 = -1;
|
||||
return handle;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// The kernel-level name of a dma-buf in this process is its file descriptor. A
|
||||
// GEM handle would be a name that exists only relative to a DRM node, and this
|
||||
// library deliberately does not require one, so the fd is the only answer here
|
||||
// that is true for every buffer it hands out. Callers that need a name for a
|
||||
// device should ask for the fd and pass that.
|
||||
handle.s32 = bo->Impl.Fds[0];
|
||||
return handle;
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_get_fd(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// A dup, close-on-exec, owned by the caller. Closing it cannot affect the buffer
|
||||
// or any other caller's copy.
|
||||
return GbmBufferDupFd(*bo, 0);
|
||||
}
|
||||
|
||||
extern "C" uint64_t gbm_bo_get_modifier(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return DrmFormatModInvalid;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.Modifier;
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_get_plane_count(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.PlaneCount;
|
||||
}
|
||||
|
||||
extern "C" union gbm_bo_handle gbm_bo_get_handle_for_plane(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
union gbm_bo_handle handle;
|
||||
handle.ptr = nullptr;
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
handle.s32 = -1;
|
||||
return handle;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
if (plane < 0 || plane >= bo->Impl.PlaneCount)
|
||||
{
|
||||
errno = EINVAL;
|
||||
handle.s32 = -1;
|
||||
return handle;
|
||||
}
|
||||
// Same reasoning as gbm_bo_get_handle: the fd is the buffer's kernel name. Every
|
||||
// plane of a self-allocated buffer lives in the one dma-buf, so all of them
|
||||
// answer with that descriptor; the plane's position inside it is
|
||||
// gbm_bo_get_offset.
|
||||
const Int fd = bo->Impl.Fds[plane] >= 0 ? bo->Impl.Fds[plane] : bo->Impl.Fds[0];
|
||||
handle.s32 = fd;
|
||||
return handle;
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_get_fd_for_plane(struct gbm_bo* bo, int plane)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmBufferDupFd(*bo, plane);
|
||||
}
|
||||
|
||||
extern "C" int gbm_bo_write(struct gbm_bo* bo, const void* buf, size_t count)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr || buf == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (count > bo->Impl.AllocationSize)
|
||||
{
|
||||
// Refused rather than truncated: a caller that writes 4 MiB into a 1 MiB
|
||||
// buffer and is told "0 bytes written, success" would read back memory it
|
||||
// never wrote and call the result correct.
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
// The write goes through the same counted mapping the rest of the API uses, so a
|
||||
// caller that already holds the buffer mapped does not get a second mapping (or
|
||||
// lose its own) behind its back.
|
||||
Uint32 stride = 0;
|
||||
void* address = GbmBufferMap(*bo, 0, 0, stride);
|
||||
if (address == nullptr)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
memcpy(address, buf, count);
|
||||
GbmBufferUnmap(*bo, address);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void gbm_bo_set_user_data(struct gbm_bo* bo, void* data,
|
||||
void (*destroy_user_data)(struct gbm_bo*, void*))
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Assigning over existing data does not call the old callback: the record calls
|
||||
// it exactly once, when the buffer goes away, which is the one moment a caller
|
||||
// can still recognise the pointer it registered.
|
||||
bo->Impl.UserData = data;
|
||||
bo->Impl.UserDataDestroy = destroy_user_data;
|
||||
}
|
||||
|
||||
extern "C" void* gbm_bo_get_user_data(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return bo->Impl.UserData;
|
||||
}
|
||||
|
||||
extern "C" void gbm_bo_destroy(struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(bo);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Drops this caller's reference. A buffer a surface still holds stays alive -
|
||||
// that is what the surface's own reference is for - so destroying a buffer that
|
||||
// is currently presented is not a use-after-free waiting to happen.
|
||||
GbmBufferRelease(*bo->Impl.Owner, *bo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Surfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" struct gbm_surface* gbm_surface_create(struct gbm_device* gbm, uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmSurfaceCreate(*gbm, width, height, format, flags, nullptr, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_surface* gbm_surface_create_with_modifiers(struct gbm_device* gbm, uint32_t width,
|
||||
uint32_t height, uint32_t format,
|
||||
const uint64_t* modifiers,
|
||||
const unsigned int count)
|
||||
{
|
||||
// As with the buffer pair: the modifier-less form is the flags-0 case.
|
||||
return gbm_surface_create_with_modifiers2(gbm, width, height, format, modifiers, count, 0);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_surface* gbm_surface_create_with_modifiers2(struct gbm_device* gbm, uint32_t width,
|
||||
uint32_t height, uint32_t format,
|
||||
const uint64_t* modifiers,
|
||||
const unsigned int count, uint32_t flags)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(gbm);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
return GbmSurfaceCreate(*gbm, width, height, format, flags, modifiers, (Uint32)count);
|
||||
}
|
||||
|
||||
extern "C" struct gbm_bo* gbm_surface_lock_front_buffer(struct gbm_surface* surface)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Null with errno = EBUSY when the compositor still holds every buffer: that is a
|
||||
// state the caller has to handle, and pretending otherwise by handing out a
|
||||
// buffer that is still being scanned out would be a torn frame rather than an
|
||||
// error.
|
||||
return GbmSurfaceLockFront(*surface);
|
||||
}
|
||||
|
||||
extern "C" void gbm_surface_release_buffer(struct gbm_surface* surface, struct gbm_bo* bo)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr || bo == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
GbmSurfaceReleaseBuffer(*surface, *bo);
|
||||
}
|
||||
|
||||
extern "C" int gbm_surface_has_free_buffers(struct gbm_surface* surface)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
// Truthful by construction: the swapchain exists from the moment the surface
|
||||
// does, so this answers about buffers that were really allocated rather than
|
||||
// about how many could still be.
|
||||
return GbmSurfaceHasFreeBuffers(*surface) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" void gbm_surface_destroy(struct gbm_surface* surface)
|
||||
{
|
||||
std::mutex* mutex = MutexOf(surface);
|
||||
if (mutex == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(*mutex);
|
||||
GbmSurfaceDestroy(*surface);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Format names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" char* gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc* desc)
|
||||
{
|
||||
if (desc == nullptr)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
// The name of a fourcc is its four characters, most significant byte last, and
|
||||
// that is all there is to it: a caller compares these strings against "AR24" and
|
||||
// friends, and the big-endian bit is part of the value rather than something to
|
||||
// spell out here.
|
||||
desc->name[0] = (char)(gbm_format & 0xffu);
|
||||
desc->name[1] = (char)((gbm_format >> 8) & 0xffu);
|
||||
desc->name[2] = (char)((gbm_format >> 16) & 0xffu);
|
||||
desc->name[3] = (char)((gbm_format >> 24) & 0xffu);
|
||||
desc->name[4] = '\0';
|
||||
return desc->name;
|
||||
}
|
||||
|
||||
// End of Source File Header
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,835 @@
|
||||
// MobileGL - MobileGL/MG_Gbm/GbmHostChannel.cpp
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
//
|
||||
// The host-frame backend: the socket the container receives the phone's frames on.
|
||||
//
|
||||
// The container half of MobileGL runs a Wayland compositor, and the frames that
|
||||
// compositor renders into have to be the frames the phone already owns - the phone
|
||||
// allocates them, its GPU draws into them, and its display scans them out. A buffer
|
||||
// allocated from the container's own dma-heap would be a copy that has to travel back
|
||||
// to the phone before anything can be shown, so when the display host is reachable this
|
||||
// backend hands the compositor the phone's own buffers instead, and the fd the
|
||||
// compositor passes on is the fd of a frame the display path already knows.
|
||||
//
|
||||
// What that costs is the one thing GBM normally promises: the geometry. A frame's
|
||||
// width, height, stride and format are the display's and not the caller's, so a
|
||||
// gbm_bo_create that takes an offered frame answers with the offer's geometry, and the
|
||||
// caller reads it back with gbm_bo_get_width and its neighbours. That is this backend's
|
||||
// documented behaviour rather than a surprise: the host decides what a frame is.
|
||||
//
|
||||
// Three rules keep this backend from ever being worse than not having it.
|
||||
//
|
||||
// * Nothing on the allocation path blocks. The socket is non-blocking and connect()
|
||||
// to a pathname socket either succeeds or fails immediately, so a call that cannot
|
||||
// be served from the host falls through to a local allocation inside the same call
|
||||
// instead of waiting for a phone that may never answer. The one wait in this file is
|
||||
// in device creation, after a display host has already accepted the connection, and
|
||||
// it exists because of a measured race: the compositor builds its swapchain within
|
||||
// microseconds of creating the device, while the offer for the first frame needs the
|
||||
// round trip of the connection plus whatever the phone does before it sends. Without
|
||||
// that wait the swapchain - the buffers that are shown for the life of the process -
|
||||
// is allocated locally and the host's frames arrive for nobody to take.
|
||||
// * A channel that dies is closed and marked down, and the next frame is allocated
|
||||
// locally again. This library is loaded by a compositor that has to keep painting,
|
||||
// so losing the display host may cost the frames their path to the screen but it
|
||||
// may not cost the session.
|
||||
// * A frame is released exactly once, when the record holding it is destroyed, and
|
||||
// only into the connection that offered it. Releasing on gbm_bo_destroy would be a
|
||||
// lie - the surface may still be scanning that frame out - and releasing an old
|
||||
// frame into a new connection would free a slot belonging to another session.
|
||||
//
|
||||
// The device mutex is the only lock in here. Both callers of this file - the allocation
|
||||
// path and the buffer record's destructor - already run under it, so there is no second
|
||||
// lock to order against and no state of this file's own that another thread can see.
|
||||
|
||||
#include "GbmInternal.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
|
||||
namespace MobileGL::MG_Gbm
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// One frame per message, and a message is the offer followed by the handle the
|
||||
// offer describes. 4 KiB is far more than an AHardwareBuffer handle needs (a
|
||||
// handle is a version, two counts and their two arrays) while still being small
|
||||
// enough that a message which does not fit is a peer speaking something else -
|
||||
// which is worth refusing rather than growing a buffer for.
|
||||
constexpr SizeT GbmHostMessageMax = 4096;
|
||||
|
||||
// The descriptors one handle may carry. An AHardwareBuffer handle carries one
|
||||
// (the dma-buf) in every layout this library has seen and the protocol reads only
|
||||
// the first, so this sizes the control buffer rather than claiming anything about
|
||||
// real handles.
|
||||
constexpr Int GbmHostMaxFds = 8;
|
||||
|
||||
// How many messages one gbm_bo_create may look at before it gives up and
|
||||
// allocates locally. An unusable message is skipped rather than fatal, so this
|
||||
// bounds the work a peer that sends nothing but unusable messages can cause
|
||||
// inside a single allocation.
|
||||
constexpr Int GbmHostTakeAttempts = 16;
|
||||
|
||||
// The floor between two connection attempts. A container with no display host is
|
||||
// the normal case, so a retry may not turn every allocation into a connect plus a
|
||||
// log line; one attempt per second is often enough that a phone which comes up is
|
||||
// picked up within a frame or two of the compositor's next allocation.
|
||||
constexpr std::chrono::milliseconds GbmHostRetryInterval{1000};
|
||||
|
||||
// How long device creation waits for the display host's first frame once the
|
||||
// connection is up. A frame the phone offers immediately needs one round trip on
|
||||
// a unix socket, so this is orders of magnitude more than that and it is only
|
||||
// ever spent by a device that has a host which then says nothing; a compile or a
|
||||
// test run without a display host never reaches it, because the connect fails
|
||||
// first.
|
||||
constexpr Int GbmHostFirstOfferWaitMs = 250;
|
||||
|
||||
// Frame releases the socket would not take. A release is twelve bytes, so a full
|
||||
// socket buffer is hundreds of frames' worth of them and this queue is only
|
||||
// reached once the phone has stopped reading; the cap is what stops a peer that
|
||||
// never reads from growing it without bound.
|
||||
constexpr SizeT GbmHostMaxPendingReleases = 256;
|
||||
|
||||
// sun_path is where a pathname socket's name lives, and a name that does not fit
|
||||
// is one the kernel cannot be told about at all.
|
||||
constexpr SizeT GbmHostPathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
|
||||
|
||||
// close() may overwrite errno, and every caller here is in the middle of
|
||||
// reporting a failure that errno describes. Saving it around the close keeps the
|
||||
// reported failure the one that happened.
|
||||
void GbmHostCloseQuietly(Int fd) noexcept
|
||||
{
|
||||
if (fd < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const Int savedErrno = errno;
|
||||
close(fd);
|
||||
errno = savedErrno;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// One connection to the display host, for the life of one device.
|
||||
class GbmHostChannel
|
||||
{
|
||||
public:
|
||||
explicit GbmHostChannel(const String& path) noexcept : Path(path) {}
|
||||
~GbmHostChannel() noexcept;
|
||||
|
||||
GbmHostChannel(const GbmHostChannel&) = delete;
|
||||
GbmHostChannel& operator=(const GbmHostChannel&) = delete;
|
||||
|
||||
// Connects when there is no connection and the retry interval has passed. A
|
||||
// failure is never the caller's problem: the channel stays closed and the
|
||||
// allocation that asked for a frame falls back to local memory.
|
||||
void EnsureOpen(::gbm_device& device) noexcept;
|
||||
|
||||
// Waits, once, for the connection's first frame to be readable. Called from
|
||||
// device creation and nowhere else: see the banner for why that one wait exists.
|
||||
void AwaitFirstOffer(::gbm_device& device) noexcept;
|
||||
|
||||
// The next frame the host offered, or false when none is waiting.
|
||||
Bool TakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept;
|
||||
|
||||
// Queues the release for one frame and tries to send what is queued.
|
||||
void ReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept;
|
||||
|
||||
private:
|
||||
// Turns one message into a frame. Everything it refuses is reported with the
|
||||
// reason, because a peer this library cannot understand is the one thing that
|
||||
// would otherwise be invisible from the outside.
|
||||
Bool ParseOffer(const Uint8* payload, SizeT length, const Int* fds, Int fdCount,
|
||||
GbmHostFrame& frameOut) noexcept;
|
||||
|
||||
// Sends what is queued. False means the connection can no longer be trusted to
|
||||
// carry a twelve byte message, with errno describing what happened.
|
||||
Bool FlushReleases() noexcept;
|
||||
|
||||
// Closes the socket and drops whatever was queued for it.
|
||||
void CloseSocket() noexcept;
|
||||
|
||||
// Closes the socket and tells the device what that means for its backend name.
|
||||
void MarkDown(::gbm_device& device, const char* what, Int error) noexcept;
|
||||
|
||||
String Path;
|
||||
Int SocketFd = -1;
|
||||
// Set by a connection this process has just made, and read once by the allocation that
|
||||
// follows it: the accept and the frame that answers the dial happen on the other side of
|
||||
// this socket, so an allocation that reads immediately after connecting reads before the
|
||||
// answer to its own connection has been written. See TakeFrame.
|
||||
Bool FreshConnection = false;
|
||||
// Bumped by every connection, so a frame taken from a connection which has since
|
||||
// died can be recognised as one that must not be released into its replacement.
|
||||
Uint64 Generation = 0;
|
||||
std::chrono::steady_clock::time_point NextRetry{};
|
||||
// The errno of the last failed attempt, so that a failure which repeats once a
|
||||
// second is not logged once a second.
|
||||
Int LastFailureErrno = 0;
|
||||
std::vector<GbmFrameRelease> PendingReleases;
|
||||
Bool ReleaseOverflowLogged = false;
|
||||
};
|
||||
|
||||
// ---- the offered-frame record ------------------------------------------
|
||||
|
||||
GbmHostFrame::~GbmHostFrame()
|
||||
{
|
||||
// The record owns the dup it was built on, which is why it is move-only: a copy
|
||||
// would close the same descriptor twice.
|
||||
if (Fd >= 0)
|
||||
{
|
||||
close(Fd);
|
||||
Fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
GbmHostFrame::GbmHostFrame(GbmHostFrame&& other) noexcept
|
||||
{
|
||||
*this = static_cast<GbmHostFrame&&>(other);
|
||||
}
|
||||
|
||||
GbmHostFrame& GbmHostFrame::operator=(GbmHostFrame&& other) noexcept
|
||||
{
|
||||
if (this == &other)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
if (Fd >= 0)
|
||||
{
|
||||
close(Fd);
|
||||
}
|
||||
Seq = other.Seq;
|
||||
Index = other.Index;
|
||||
Width = other.Width;
|
||||
Height = other.Height;
|
||||
StrideBytes = other.StrideBytes;
|
||||
Format = other.Format;
|
||||
BytesPerPixel = other.BytesPerPixel;
|
||||
Usage = other.Usage;
|
||||
Generation = other.Generation;
|
||||
Fd = other.Fd;
|
||||
Size = other.Size;
|
||||
// The source must not close what this record now owns.
|
||||
other.Fd = -1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ---- the connection ----------------------------------------------------
|
||||
|
||||
GbmHostChannel::~GbmHostChannel() noexcept
|
||||
{
|
||||
// One last try for whatever is queued - the phone may still be reading - and
|
||||
// then the socket goes away with the process.
|
||||
if (SocketFd >= 0 && !PendingReleases.empty())
|
||||
{
|
||||
FlushReleases();
|
||||
}
|
||||
if (!PendingReleases.empty())
|
||||
{
|
||||
MGLOG_W("gbm: %zu frame release(s) could not be sent to the display host at %s; the phone reclaims "
|
||||
"those slots when the session ends",
|
||||
PendingReleases.size(), Path.c_str());
|
||||
}
|
||||
CloseSocket();
|
||||
}
|
||||
|
||||
void GbmHostChannel::EnsureOpen(::gbm_device& device) noexcept
|
||||
{
|
||||
if (SocketFd >= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
if (now < NextRetry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
NextRetry = now + GbmHostRetryInterval;
|
||||
|
||||
// SOCK_SEQPACKET because a frame and its descriptors are one message and the
|
||||
// reader may not reassemble either half; SOCK_NONBLOCK because the caller is a
|
||||
// compositor's frame loop and may not be parked on a phone; SOCK_CLOEXEC because
|
||||
// every process this compositor starts would otherwise inherit a socket it can
|
||||
// read frames from.
|
||||
const Int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
{
|
||||
MGLOG_E_ONCE("gbm: cannot create the socket to the display host (%d: %s); every frame is allocated "
|
||||
"locally",
|
||||
errno, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Path.empty() || Path.size() >= GbmHostPathMax)
|
||||
{
|
||||
MGLOG_E_ONCE("gbm: the display host socket path \"%s\" does not fit in sun_path (%zu bytes); every "
|
||||
"frame is allocated locally",
|
||||
Path.c_str(), GbmHostPathMax);
|
||||
GbmHostCloseQuietly(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
struct sockaddr_un address = {};
|
||||
address.sun_family = AF_UNIX;
|
||||
// The terminator travels with the name: sun_path is a C string for a pathname
|
||||
// socket, and a name without one is a name the kernel reads past the end of.
|
||||
memcpy(address.sun_path, Path.c_str(), Path.size() + 1);
|
||||
|
||||
if (connect(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0)
|
||||
{
|
||||
const Int connectErrno = errno;
|
||||
GbmHostCloseQuietly(fd);
|
||||
// A failure that repeats once a second is not news: a container with no
|
||||
// display host fails here on every retry, and a line per second saying so
|
||||
// would bury the rest of the log. A failure that changed is news, because it
|
||||
// says the path or the peer changed.
|
||||
if (connectErrno != LastFailureErrno)
|
||||
{
|
||||
LastFailureErrno = connectErrno;
|
||||
if (connectErrno == EPROTOTYPE)
|
||||
{
|
||||
MGLOG_E("gbm: %s is a socket of a different type (%d: %s). The frame protocol is "
|
||||
"SOCK_SEQPACKET - one message per frame, carrying that frame's descriptors - "
|
||||
"because a reader cannot tell from a byte stream which frame a descriptor "
|
||||
"belongs to; frames are allocated locally instead",
|
||||
Path.c_str(), connectErrno, strerror(connectErrno));
|
||||
}
|
||||
else
|
||||
{
|
||||
MGLOG_I("gbm: no display host at %s (%d: %s); every frame is allocated locally until one "
|
||||
"answers",
|
||||
Path.c_str(), connectErrno, strerror(connectErrno));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SocketFd = fd;
|
||||
FreshConnection = true;
|
||||
// A new connection is a new session with its own frames, which is what makes the
|
||||
// generation a correct answer to "may this release be sent here".
|
||||
Generation++;
|
||||
LastFailureErrno = 0;
|
||||
device.Impl.HostAvailable = true;
|
||||
device.Impl.BackendName = "mobilegl-host-frame";
|
||||
MGLOG_I("gbm: connected to the display host at %s (one SOCK_SEQPACKET message per frame, up to %zu "
|
||||
"bytes); frames offered over it become gbm_bos whose geometry is the offer's",
|
||||
Path.c_str(), GbmHostMessageMax);
|
||||
}
|
||||
|
||||
void GbmHostChannel::AwaitFirstOffer(::gbm_device& device) noexcept
|
||||
{
|
||||
if (SocketFd < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::chrono::steady_clock::time_point deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(GbmHostFirstOfferWaitMs);
|
||||
for (;;)
|
||||
{
|
||||
const std::chrono::milliseconds left =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now());
|
||||
if (left.count() <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// poll() and not recvmsg(): the frame has to stay in the socket for the
|
||||
// allocation that is about to ask for it, and a peek at a message carrying
|
||||
// SCM_RIGHTS is a question with no safe answer.
|
||||
struct pollfd waiting = {};
|
||||
waiting.fd = SocketFd;
|
||||
waiting.events = POLLIN;
|
||||
const Int ready = poll(&waiting, 1, (Int)left.count());
|
||||
if (ready > 0)
|
||||
{
|
||||
MGLOG_I("gbm: the display host at %s has a frame waiting; the buffers the compositor is about "
|
||||
"to allocate come from it",
|
||||
Path.c_str());
|
||||
return;
|
||||
}
|
||||
if (ready == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (errno != EINTR)
|
||||
{
|
||||
MarkDown(device, "poll", errno);
|
||||
return;
|
||||
}
|
||||
}
|
||||
MGLOG_W("gbm: the display host at %s accepted the connection but offered no frame within %d ms; the "
|
||||
"buffers allocated before its first frame are local ones",
|
||||
Path.c_str(), GbmHostFirstOfferWaitMs);
|
||||
}
|
||||
|
||||
Bool GbmHostChannel::TakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept
|
||||
{
|
||||
EnsureOpen(device);
|
||||
if (SocketFd < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// THE FIRST ALLOCATION AFTER A DIAL WAITS FOR THE ANSWER TO IT. The accept and the offer that
|
||||
// answers the connection this call has just made are written by the phone, a round trip
|
||||
// behind the connect() that returns here — and what is allocated before that offer arrives
|
||||
// is a local buffer, which is what a compositor builds its whole swapchain out of and then
|
||||
// keeps for the life of the process. So a connection made on this path is given the same
|
||||
// first-offer window device creation gives one, once, and only while it is new.
|
||||
if (FreshConnection)
|
||||
{
|
||||
FreshConnection = false;
|
||||
AwaitFirstOffer(device);
|
||||
}
|
||||
// Releases the socket would not take last time are retried here, because this is
|
||||
// the one call in the library that happens once per frame and therefore the
|
||||
// natural place for a queue waiting for room to get its next chance.
|
||||
if (!PendingReleases.empty() && !FlushReleases())
|
||||
{
|
||||
MarkDown(device, "sendmsg", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint8 payload[GbmHostMessageMax];
|
||||
for (Int attempt = 0; attempt < GbmHostTakeAttempts; ++attempt)
|
||||
{
|
||||
Int receivedFds[GbmHostMaxFds];
|
||||
for (Int i = 0; i < GbmHostMaxFds; ++i)
|
||||
{
|
||||
receivedFds[i] = -1;
|
||||
}
|
||||
|
||||
struct iovec payloadVector = {};
|
||||
payloadVector.iov_base = payload;
|
||||
payloadVector.iov_len = sizeof(payload);
|
||||
|
||||
Uint8 control[CMSG_SPACE(sizeof(Int) * GbmHostMaxFds)] = {};
|
||||
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &payloadVector;
|
||||
message.msg_iovlen = 1;
|
||||
message.msg_control = control;
|
||||
message.msg_controllen = sizeof(control);
|
||||
|
||||
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC
|
||||
// so a descriptor the phone sent does not survive into a child of this
|
||||
// compositor, which would then hold a frame nothing will ever release.
|
||||
const ssize_t received = recvmsg(SocketFd, &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||
if (received < 0)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
{
|
||||
// Nothing offered yet, which is what an idle display looks like. The
|
||||
// caller allocates locally and asks again on its next frame.
|
||||
return false;
|
||||
}
|
||||
if (errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MarkDown(device, "recvmsg", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The descriptors come out of the control data before anything is decided
|
||||
// about the message, because they belong to this process from the moment it
|
||||
// was received: a path that returns without closing them leaks a descriptor
|
||||
// per unusable message.
|
||||
Int fdCount = 0;
|
||||
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
|
||||
header = CMSG_NXTHDR(&message, header))
|
||||
{
|
||||
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
|
||||
const Int count = (Int)(bytes / sizeof(Int));
|
||||
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
|
||||
for (Int i = 0; i < count; ++i)
|
||||
{
|
||||
if (fdCount < GbmHostMaxFds)
|
||||
{
|
||||
receivedFds[fdCount++] = descriptors[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
// More descriptors than a frame can carry: they are this
|
||||
// process's now, so they are closed here rather than leaked.
|
||||
GbmHostCloseQuietly(descriptors[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((message.msg_flags & MSG_CTRUNC) != 0)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's control data did not fit in %zu bytes; the frame is "
|
||||
"skipped because its descriptor list is incomplete",
|
||||
sizeof(control));
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (received == 0)
|
||||
{
|
||||
// Zero bytes on a connected socket is the end of it, whether the phone
|
||||
// closed cleanly or the kernel gave up on it. Either way the frames have
|
||||
// to come from somewhere else from here on.
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
MarkDown(device, "the display host closed the connection", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((message.msg_flags & MSG_TRUNC) != 0)
|
||||
{
|
||||
// Longer than a frame message can be, so it is not one - and on a
|
||||
// message-oriented socket the rest is already gone while the next message
|
||||
// is still a whole message, which is why this skips one message instead
|
||||
// of ending the channel.
|
||||
MGLOG_W_ONCE("gbm: the display host sent a message longer than %zu bytes; it is skipped",
|
||||
GbmHostMessageMax);
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const Bool parsed = ParseOffer(payload, (SizeT)received, receivedFds, fdCount, frameOut);
|
||||
// Whatever the frame did not keep is closed here: the copy of the dma-buf
|
||||
// descriptor the phone sent (the frame owns its own dup) and the rest of the
|
||||
// handle, which is a layout this library does not interpret.
|
||||
for (Int i = 0; i < fdCount; ++i)
|
||||
{
|
||||
GbmHostCloseQuietly(receivedFds[i]);
|
||||
}
|
||||
if (parsed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool GbmHostChannel::ParseOffer(const Uint8* payload, SizeT length, const Int* fds, Int fdCount,
|
||||
GbmHostFrame& frameOut) noexcept
|
||||
{
|
||||
constexpr SizeT HeaderBytes = sizeof(GbmFrameOffer) + sizeof(AHBNativeHandleHeader);
|
||||
if (length < HeaderBytes)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: the display host sent a %zu byte message where a frame message is at least %zu "
|
||||
"bytes (the offer followed by the handle header); it is skipped",
|
||||
length, HeaderBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
GbmFrameOffer offer = {};
|
||||
memcpy(&offer, payload, sizeof(offer));
|
||||
if (offer.Magic != GbmFrameOfferMagic)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: a message from the display host begins with 0x%08x rather than 'MGGF'; it is "
|
||||
"skipped",
|
||||
offer.Magic);
|
||||
return false;
|
||||
}
|
||||
if (offer.Version != GbmHostProtocolVersion)
|
||||
{
|
||||
// Refused rather than read as if the versions agreed: the version is what
|
||||
// says how the rest of the message is laid out, and a frame read with the
|
||||
// wrong layout is memory the compositor would render into.
|
||||
MGLOG_E_ONCE("gbm: the display host offers frames in protocol version %u where this library speaks "
|
||||
"version %u; its frames are refused until the two agree",
|
||||
offer.Version, (Uint32)GbmHostProtocolVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
AHBNativeHandleHeader handle = {};
|
||||
memcpy(&handle, payload + sizeof(offer), sizeof(handle));
|
||||
if (handle.NumFds < 1 || handle.NumInts < 0)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's handle carries %d descriptor(s) and %d integer(s), where the "
|
||||
"dma-buf is the first descriptor and there has to be one; the frame is skipped",
|
||||
handle.NumFds, handle.NumInts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The handle describes itself with its two counts, so the message has to be
|
||||
// exactly as long as those counts make it. Any other length means the two ends
|
||||
// disagree about the layout, and a frame built from a message whose layout was
|
||||
// guessed is a buffer of unknown memory.
|
||||
const SizeT expected = HeaderBytes + ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
|
||||
if (length != expected)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's message is %zu bytes where its handle (%d descriptors, %d "
|
||||
"integers) makes it %zu; the frame is skipped",
|
||||
length, handle.NumFds, handle.NumInts, expected);
|
||||
return false;
|
||||
}
|
||||
if (fdCount < handle.NumFds)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's handle names %d descriptor(s) and %d arrived with the "
|
||||
"message; the frame is skipped",
|
||||
handle.NumFds, fdCount);
|
||||
return false;
|
||||
}
|
||||
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame is %ux%u with a stride of %u bytes; a buffer with no size is "
|
||||
"not one this library can hand out",
|
||||
offer.Width, offer.Height, offer.StrideBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The frame is built on a dup of the first descriptor rather than on the
|
||||
// descriptor itself: the copy that arrived belongs to the message, and the frame
|
||||
// outlives it. Close-on-exec, like every other descriptor this library hands out.
|
||||
const Int frameFd = fcntl(fds[0], F_DUPFD_CLOEXEC, 0);
|
||||
if (frameFd < 0)
|
||||
{
|
||||
MGLOG_E_ONCE("gbm: cannot dup the descriptor of an offered frame (%d: %s)", errno, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// How long the frame really is, asked of the descriptor instead of computed from
|
||||
// the geometry: a gralloc dma-buf is usually larger than stride x height, and a
|
||||
// mapping has to cover what exists rather than what the arithmetic expects.
|
||||
SizeT size = 0;
|
||||
const off_t end = lseek(frameFd, 0, SEEK_END);
|
||||
if (end > 0)
|
||||
{
|
||||
size = (SizeT)end;
|
||||
}
|
||||
const SizeT described = (SizeT)offer.StrideBytes * (SizeT)offer.Height;
|
||||
if (size == 0)
|
||||
{
|
||||
// A descriptor that does not answer for its own length - an exporter that
|
||||
// does not implement llseek - leaves the offer's arithmetic as the only
|
||||
// length there is.
|
||||
size = described;
|
||||
}
|
||||
if (size < described)
|
||||
{
|
||||
MGLOG_W_ONCE("gbm: an offered frame's descriptor holds %zu bytes where %u rows of %u bytes need %zu; "
|
||||
"the frame is skipped rather than mapped over memory that is not there",
|
||||
size, offer.Height, offer.StrideBytes, described);
|
||||
GbmHostCloseQuietly(frameFd);
|
||||
return false;
|
||||
}
|
||||
|
||||
frameOut.Seq = offer.Seq;
|
||||
frameOut.Index = offer.Index;
|
||||
frameOut.Width = offer.Width;
|
||||
frameOut.Height = offer.Height;
|
||||
frameOut.StrideBytes = offer.StrideBytes;
|
||||
frameOut.Format = offer.Format;
|
||||
frameOut.Usage = ((Uint64)offer.UsageHi << 32) | (Uint64)offer.UsageLo;
|
||||
frameOut.Generation = Generation;
|
||||
frameOut.Fd = frameFd;
|
||||
frameOut.Size = size;
|
||||
frameOut.BytesPerPixel = GbmFormatBytesPerPixel(offer.Format);
|
||||
if (frameOut.BytesPerPixel == 0)
|
||||
{
|
||||
// A format this library has no table entry for is still a frame the phone
|
||||
// drew. The offer's stride is then the only description of a pixel that
|
||||
// exists: a row of Width pixels is StrideBytes long.
|
||||
frameOut.BytesPerPixel = offer.StrideBytes / offer.Width;
|
||||
}
|
||||
|
||||
MGLOG_I("gbm: frame seq=%u index=%u %ux%u stride=%u format=0x%08x (%u bytes per pixel, %zu bytes "
|
||||
"measured) taken from the display host",
|
||||
offer.Seq, offer.Index, offer.Width, offer.Height, offer.StrideBytes, offer.Format,
|
||||
frameOut.BytesPerPixel, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GbmHostChannel::ReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept
|
||||
{
|
||||
if (SocketFd < 0)
|
||||
{
|
||||
// A release only means anything on the connection that offered the frame,
|
||||
// and there is none.
|
||||
return;
|
||||
}
|
||||
if (generation != Generation)
|
||||
{
|
||||
// The frame was offered by a connection that has since died and been
|
||||
// replaced. Sending this into the replacement would name a frame of another
|
||||
// session, and the phone would recycle a slot this buffer was not made of.
|
||||
MGLOG_W_ONCE("gbm: a frame offered by a display host connection that has since died is not "
|
||||
"released into its replacement");
|
||||
return;
|
||||
}
|
||||
if (PendingReleases.size() >= GbmHostMaxPendingReleases)
|
||||
{
|
||||
if (!ReleaseOverflowLogged)
|
||||
{
|
||||
ReleaseOverflowLogged = true;
|
||||
MGLOG_W("gbm: %zu frame releases to the display host at %s are already waiting for room in the "
|
||||
"socket; this one is dropped rather than queued without bound, and the phone reclaims "
|
||||
"the slot when the session ends",
|
||||
PendingReleases.size(), Path.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GbmFrameRelease release = {};
|
||||
release.Magic = GbmFrameReleaseMagic;
|
||||
release.Version = GbmHostProtocolVersion;
|
||||
release.Seq = seq;
|
||||
PendingReleases.push_back(release);
|
||||
|
||||
if (!FlushReleases())
|
||||
{
|
||||
MarkDown(device, "sendmsg", errno);
|
||||
}
|
||||
}
|
||||
|
||||
Bool GbmHostChannel::FlushReleases() noexcept
|
||||
{
|
||||
while (!PendingReleases.empty())
|
||||
{
|
||||
struct iovec releaseVector = {};
|
||||
releaseVector.iov_base = &PendingReleases.front();
|
||||
releaseVector.iov_len = sizeof(GbmFrameRelease);
|
||||
|
||||
struct msghdr message = {};
|
||||
message.msg_iov = &releaseVector;
|
||||
message.msg_iovlen = 1;
|
||||
|
||||
// MSG_DONTWAIT for the same reason every other call here is non-blocking, and
|
||||
// MSG_NOSIGNAL because a compositor killed by SIGPIPE when the phone goes
|
||||
// away is a desktop that disappears with it.
|
||||
const ssize_t sent = sendmsg(SocketFd, &message, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (sent == (ssize_t)sizeof(GbmFrameRelease))
|
||||
{
|
||||
PendingReleases.erase(PendingReleases.begin());
|
||||
continue;
|
||||
}
|
||||
if (sent < 0 && errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
{
|
||||
// A full socket is a phone that has stopped reading rather than a dead
|
||||
// one: the release stays queued and goes out with a later frame.
|
||||
return true;
|
||||
}
|
||||
// A short send, a reset and a broken pipe all say the same thing: this
|
||||
// connection can no longer be trusted to have carried a twelve byte message,
|
||||
// and acting on a maybe is worse than handing the slot back when the session
|
||||
// ends. errno describes which of them it was.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GbmHostChannel::CloseSocket() noexcept
|
||||
{
|
||||
if (SocketFd >= 0)
|
||||
{
|
||||
GbmHostCloseQuietly(SocketFd);
|
||||
SocketFd = -1;
|
||||
}
|
||||
PendingReleases.clear();
|
||||
}
|
||||
|
||||
void GbmHostChannel::MarkDown(::gbm_device& device, const char* what, Int error) noexcept
|
||||
{
|
||||
if (!PendingReleases.empty())
|
||||
{
|
||||
MGLOG_W("gbm: %zu frame release(s) were still queued for the display host and cannot be sent over a "
|
||||
"connection that is gone; the phone reclaims those slots when the session ends",
|
||||
PendingReleases.size());
|
||||
}
|
||||
CloseSocket();
|
||||
if (error != 0)
|
||||
{
|
||||
MGLOG_W("gbm: the display host connection at %s is down: %s failed (%d: %s); frames are allocated "
|
||||
"locally from here on, and a new connection is attempted on a later allocation",
|
||||
Path.c_str(), what, error, strerror(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
MGLOG_W("gbm: the display host connection at %s is down: %s; frames are allocated locally from here "
|
||||
"on, and a new connection is attempted on a later allocation",
|
||||
Path.c_str(), what);
|
||||
}
|
||||
device.Impl.HostAvailable = false;
|
||||
device.Impl.BackendName = device.Impl.SelfAllocatedBackendName;
|
||||
}
|
||||
|
||||
// ---- the device-facing entry points ------------------------------------
|
||||
|
||||
void GbmHostChannelCreate(::gbm_device& device) noexcept
|
||||
{
|
||||
const char* configured = getenv("MOBILEGL_GBM_SOCKET");
|
||||
const String path =
|
||||
(configured != nullptr && configured[0] != '\0') ? String(configured) : String("/run/mobilegl-gbm.sock");
|
||||
device.Impl.SocketPath = path;
|
||||
try
|
||||
{
|
||||
GbmHostChannel* channel = new GbmHostChannel(path);
|
||||
device.Impl.Host = channel;
|
||||
// The one attempt made at creation. It is allowed to fail: a machine that is
|
||||
// not the container half of a split run has no display host, and that is a
|
||||
// device which allocates locally rather than a device that could not exist.
|
||||
channel->EnsureOpen(device);
|
||||
// A connection that came up is waited on once, and only here, so that the
|
||||
// compositor's swapchain is built out of the host's frames rather than out of
|
||||
// local ones it will keep for the life of the process.
|
||||
channel->AwaitFirstOffer(device);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
device.Impl.Host = nullptr;
|
||||
device.Impl.HostAvailable = false;
|
||||
MGLOG_W("gbm: the display host channel could not be created; every frame is allocated locally");
|
||||
}
|
||||
}
|
||||
|
||||
void GbmHostChannelDestroy(::gbm_device& device) noexcept
|
||||
{
|
||||
GbmHostChannel* channel = device.Impl.Host;
|
||||
if (channel == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
device.Impl.Host = nullptr;
|
||||
device.Impl.HostAvailable = false;
|
||||
delete channel;
|
||||
}
|
||||
|
||||
Bool GbmHostChannelTakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept
|
||||
{
|
||||
GbmHostChannel* channel = device.Impl.Host;
|
||||
if (channel == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return channel->TakeFrame(device, frameOut);
|
||||
}
|
||||
|
||||
void GbmHostChannelReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept
|
||||
{
|
||||
GbmHostChannel* channel = device.Impl.Host;
|
||||
if (channel == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
channel->ReleaseFrame(device, seq, generation);
|
||||
}
|
||||
} // namespace MobileGL::MG_Gbm
|
||||
|
||||
// End of Source File Header
|
||||
@@ -0,0 +1,476 @@
|
||||
// MobileGL - MobileGL/MG_Gbm/GbmInternal.h
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
//
|
||||
// This header carries the internal model shared by the GBM front-end files: the
|
||||
// device, buffer-object and surface records, the two allocation backends, and the
|
||||
// wire protocol used to talk to a phone that owns the real display hardware.
|
||||
//
|
||||
// The reason a GBM implementation has to exist in this tree at all is that the
|
||||
// container half of MobileGL runs a Wayland compositor (kwin). That compositor
|
||||
// needs libgbm to allocate the buffers it renders into, and the fd of such a
|
||||
// buffer is handed to the Anland display producer, which ships the frame to the
|
||||
// phone. If Mesa's libgbm is loaded, the compositor renders into Mesa buffers and
|
||||
// the phone ends up receiving a copy through a path that was never designed to
|
||||
// carry them. When this library is loaded instead, the buffers are either frames
|
||||
// the phone itself offered to us, or buffers allocated from the kernel's dma-heap
|
||||
// so that they are dma-bufs the display host can consume directly.
|
||||
//
|
||||
// The ownership rules below are the whole contract, because every one of them is
|
||||
// observable from outside this library:
|
||||
//
|
||||
// * A buffer record owns its descriptors. gbm_bo_get_fd and
|
||||
// gbm_bo_get_fd_for_plane hand out a dup, so a caller that closes what it was
|
||||
// given cannot invalidate the buffer, and the matching close happens exactly
|
||||
// once, in ~GbmBufferObject.
|
||||
// * A mapping is one mmap of the buffer's fd, counted rather than duplicated, so
|
||||
// two overlapping gbm_bo_map calls share one address and the last matching
|
||||
// gbm_bo_unmap releases it.
|
||||
// * A surface owns one reference per swapchain buffer, and every
|
||||
// gbm_surface_lock_front_buffer hands the caller a reference of its own. That
|
||||
// is what makes gbm_bo_destroy on a locked buffer harmless: it drops the
|
||||
// caller's reference and leaves the surface's bookkeeping intact.
|
||||
// * One mutex per device. GBM calls arrive from the compositor's render thread
|
||||
// and from its main thread, and the state they touch (the buffer table, the
|
||||
// swapchains, the backends) is small enough that one lock is cheaper than
|
||||
// several - and, more importantly, one lock cannot be taken in the wrong order.
|
||||
//
|
||||
// The helpers declared at the bottom of this file are the entire internal interface
|
||||
// between GbmDevice.cpp (the records and the self-allocated backend) and GbmApi.cpp
|
||||
// (the extern "C" surface). Every one of them assumes the device mutex is already
|
||||
// held: the lock is taken once, in the API layer, so that a compositor call can
|
||||
// never interleave with another half way through a swapchain update.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#include <gbm.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Gbm
|
||||
{
|
||||
// DRM_FORMAT_MOD_LINEAR and DRM_FORMAT_MOD_INVALID. drm_fourcc.h is not part of
|
||||
// libgbm's interface, and these two values are frozen kernel UAPI, so they are
|
||||
// spelled out here rather than made a build dependency on libdrm's headers.
|
||||
static constexpr Uint64 DrmFormatModLinear = 0ULL;
|
||||
static constexpr Uint64 DrmFormatModInvalid = 0x00ffffffffffffffULL;
|
||||
|
||||
// Every buffer this library hands out is described by one of these. HostFrame
|
||||
// buffers are owned by the phone and only borrowed by us, so they must be
|
||||
// released back over the channel instead of being freed locally.
|
||||
enum class GbmBackend
|
||||
{
|
||||
None = 0,
|
||||
HostFrame,
|
||||
DmaHeap,
|
||||
MemFd,
|
||||
};
|
||||
|
||||
// The phone offers frames with this packed struct immediately followed by an
|
||||
// AHardwareBuffer native handle in the payload of one message, with the handle's
|
||||
// descriptors in the SCM_RIGHTS control data of that same message. The layout is
|
||||
// fixed by the phone side, so the packing attribute is load bearing and the field
|
||||
// order may not be rearranged.
|
||||
//
|
||||
// The transport below the two structs is SOCK_SEQPACKET, and that is part of the
|
||||
// protocol rather than an implementation detail: a frame and its descriptors only
|
||||
// mean anything together, and the message boundary is what lets the reader take
|
||||
// the two as one thing. A stream socket would let a reader receive half an offer,
|
||||
// or a descriptor whose position in the byte stream it had to reconstruct, and the
|
||||
// failure mode of getting that arithmetic wrong is a frame silently made of
|
||||
// another frame's memory.
|
||||
//
|
||||
// Format is a DRM fourcc and not an AHardwareBuffer format, because it is what
|
||||
// gbm_bo_get_format has to answer with and a compositor compares it against
|
||||
// DRM_FORMAT_* values. StrideBytes is the byte distance between two rows of plane
|
||||
// 0, which is the same single-stride description GBM_BO_IMPORT_FD carries - and
|
||||
// the reason a frame read from this struct is reported as linear.
|
||||
struct GbmFrameOffer
|
||||
{
|
||||
uint32_t Magic; // 'M','G','G','F' little endian on both ends of the wire
|
||||
uint32_t Version;
|
||||
uint32_t Seq;
|
||||
uint32_t Index;
|
||||
uint32_t Width;
|
||||
uint32_t Height;
|
||||
uint32_t StrideBytes;
|
||||
uint32_t Format;
|
||||
uint32_t UsageLo;
|
||||
uint32_t UsageHi;
|
||||
} __attribute__((packed));
|
||||
|
||||
// The container replies with this once it no longer reads the frame, which is
|
||||
// what lets the phone recycle the slot. Without it the display host would run
|
||||
// out of buffers after a handful of frames.
|
||||
struct GbmFrameRelease
|
||||
{
|
||||
uint32_t Magic; // 'M','G','G','R'
|
||||
uint32_t Version;
|
||||
uint32_t Seq;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t GbmFrameOfferMagic = 0x4647474Du; // "MGGF"
|
||||
static constexpr uint32_t GbmFrameReleaseMagic = 0x5247474Du; // "MGGR"
|
||||
static constexpr uint32_t GbmHostProtocolVersion = 1;
|
||||
|
||||
// THE REVERSE DIRECTION: this end asking the render server to back one of its textures with a
|
||||
// dma-buf. The compositor's scene reaches the display daemon's buffers through
|
||||
// glEGLImageTargetTexture2DOES, and the server has to be told which texture that dma-buf backs -
|
||||
// over this same socket, because it is the one channel that can carry a descriptor across the
|
||||
// container boundary. Restated here rather than shared with MG_Backend/GbmFrameChannel.h for the
|
||||
// same reason the two frame shapes are: the ends are built into different libraries from
|
||||
// different build configurations (that file is the Android render server and never sees libgbm).
|
||||
// Field for field, packing included; the plane descriptors ride in the SCM_RIGHTS control data of
|
||||
// the same sendmsg, one message per import.
|
||||
struct GbmImportRequest
|
||||
{
|
||||
uint32_t Magic; // 'M','G','G','I'
|
||||
uint32_t Version;
|
||||
uint32_t Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
|
||||
uint32_t TexName; // THIS side's texture name; the server resolves its twin
|
||||
uint32_t Width;
|
||||
uint32_t Height;
|
||||
uint32_t FourCC;
|
||||
uint32_t PlaneCount;
|
||||
int32_t Stride[4];
|
||||
int32_t Offset[4];
|
||||
uint64_t Modifier[4];
|
||||
uint32_t HasModifier[4];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct GbmImportReply
|
||||
{
|
||||
uint32_t Magic; // 'I','G','G','R'
|
||||
uint32_t Version;
|
||||
uint32_t Ok;
|
||||
uint32_t Reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t GbmImportRequestMagic = 0x4947474Du; // "MGGI"
|
||||
static constexpr uint32_t GbmImportReplyMagic = 0x52474749u; // "IGGR"
|
||||
static constexpr uint32_t GbmImportMaxPlanes = 4;
|
||||
|
||||
// An AHardwareBuffer native handle as it appears on the wire: three counts
|
||||
// followed by numFds file descriptors and numInts integers. The counts are
|
||||
// 32 bit even on 64 bit Android, which is why they are spelled out here
|
||||
// rather than using the platform's native_handle_t.
|
||||
struct AHBNativeHandleHeader
|
||||
{
|
||||
int32_t Version;
|
||||
int32_t NumFds;
|
||||
int32_t NumInts;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t GbmMaxPlanes = 4;
|
||||
|
||||
// How one plane of a buffer is laid out inside the single allocation that backs
|
||||
// it. Multi-plane formats keep every plane in one dma-buf and describe the
|
||||
// separation with Offsets, which is the layout a modifier of
|
||||
// DRM_FORMAT_MOD_LINEAR can express and the only one this library produces.
|
||||
struct GbmPlaneLayout
|
||||
{
|
||||
Uint32 Stride = 0;
|
||||
Uint32 Offset = 0;
|
||||
SizeT Size = 0;
|
||||
};
|
||||
|
||||
// One frame the phone offered, as this process holds it: the offer's own
|
||||
// description of the memory, plus the descriptor this library duped out of the
|
||||
// handle the phone sent. It owns that descriptor for as long as it lives, so a
|
||||
// caller that takes a frame and then fails still closes it, and the descriptor the
|
||||
// phone sent is closed with the rest of the message's descriptors rather than kept.
|
||||
struct GbmHostFrame
|
||||
{
|
||||
Uint32 Seq = 0;
|
||||
Uint32 Index = 0;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 StrideBytes = 0;
|
||||
Uint32 Format = 0;
|
||||
// Bytes per pixel of plane 0. The offer carries a fourcc, so it comes from the
|
||||
// format table when there is an entry for it and from the stride otherwise.
|
||||
Uint32 BytesPerPixel = 0;
|
||||
Uint64 Usage = 0;
|
||||
// Which connection offered the frame. A release names a frame, and a frame only
|
||||
// means anything to the connection that offered it.
|
||||
Uint64 Generation = 0;
|
||||
// The duped dma-buf descriptor, and how many bytes it actually backs as asked
|
||||
// of the descriptor itself rather than computed from the offered geometry.
|
||||
Int Fd = -1;
|
||||
SizeT Size = 0;
|
||||
|
||||
// Move-only, because two records naming one descriptor would close it twice.
|
||||
GbmHostFrame() = default;
|
||||
GbmHostFrame(const GbmHostFrame&) = delete;
|
||||
GbmHostFrame& operator=(const GbmHostFrame&) = delete;
|
||||
GbmHostFrame(GbmHostFrame&& other) noexcept;
|
||||
GbmHostFrame& operator=(GbmHostFrame&& other) noexcept;
|
||||
~GbmHostFrame();
|
||||
};
|
||||
|
||||
// One imported or locally allocated buffer. The struct is private to this
|
||||
// library; gbm_bo is the public opaque name and is defined in terms of it.
|
||||
struct GbmBufferObject
|
||||
{
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Format = 0;
|
||||
Uint32 Stride = 0;
|
||||
Uint64 Modifier = DrmFormatModLinear;
|
||||
Uint64 Usage = 0;
|
||||
Int PlaneCount = 1;
|
||||
// Bytes per pixel of plane 0. Kept beside the layout because gbm_bo_map
|
||||
// needs it to place a sub-rectangle, and because a format this library can
|
||||
// allocate but cannot describe in bytes would be a format it must refuse.
|
||||
Uint32 BytesPerPixel = 0;
|
||||
|
||||
// Fds owned by this record. get_fd and get_fd_for_plane hand out dups so
|
||||
// that a caller closing its fd can never invalidate the buffer. Index 0 is
|
||||
// the only one a self-allocated buffer has: every plane of a linear image
|
||||
// lives in that one dma-buf, and Fds[1..] stay -1 for it. An import with
|
||||
// several descriptors fills several entries, and the destructor closes
|
||||
// every entry that is not -1.
|
||||
Int Fds[GbmMaxPlanes] = {-1, -1, -1, -1};
|
||||
Uint32 Strides[GbmMaxPlanes] = {0, 0, 0, 0};
|
||||
Uint32 Offsets[GbmMaxPlanes] = {0, 0, 0, 0};
|
||||
Uint64 Modifiers[GbmMaxPlanes] = {0, 0, 0, 0};
|
||||
|
||||
// The number of bytes the descriptors actually back, rounded up to the page
|
||||
// size because that is what both backends allocate. It is the only length
|
||||
// that is safe to mmap: a mapping longer than the object faults on the last
|
||||
// page, and a shorter one would hide the caller's own stride arithmetic.
|
||||
SizeT AllocationSize = 0;
|
||||
|
||||
GbmBackend Backend = GbmBackend::None;
|
||||
|
||||
// Set only for host frames: the slot index and sequence number that the
|
||||
// release message has to mention so the phone can recycle the right slot, and
|
||||
// the connection the frame arrived on. A frame left over from a connection
|
||||
// that has since died is never released into the one that replaced it, because
|
||||
// that would free a slot belonging to another session.
|
||||
Uint32 HostIndex = 0;
|
||||
Uint32 HostSeq = 0;
|
||||
Uint64 HostGeneration = 0;
|
||||
Bool HostOwned = false;
|
||||
|
||||
// How many maps are outstanding. The mapping itself is kept here so that
|
||||
// unmap can find and tear down the right one.
|
||||
Int MapRefCount = 0;
|
||||
void* MapAddress = nullptr;
|
||||
SizeT MapSize = 0;
|
||||
|
||||
// Surfaces keep their buffers alive by count rather than by ownership, so
|
||||
// the device knows when a bo is truly unreferenced.
|
||||
Int RefCount = 1;
|
||||
|
||||
// gbm_bo_set_user_data's pair. The destroy callback is invoked with the
|
||||
// gbm_bo that is going away, once, from the destructor below.
|
||||
void* UserData = nullptr;
|
||||
void (*UserDataDestroy)(::gbm_bo*, void*) = nullptr;
|
||||
|
||||
::gbm_device* Owner = nullptr;
|
||||
|
||||
// The record releases its own descriptors and its own mapping, so that no
|
||||
// failure path anywhere in this library can leak either one - an allocation
|
||||
// that fails half way through construction still cleans up when the record
|
||||
// it was building is destroyed.
|
||||
~GbmBufferObject();
|
||||
};
|
||||
|
||||
// A real swapchain. The compositor asks for a buffer to render into, presents
|
||||
// it, and later returns it; the three states below are exactly the states the
|
||||
// GBM API can express.
|
||||
struct GbmSurface
|
||||
{
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Format = 0;
|
||||
Uint32 Flags = 0;
|
||||
|
||||
std::vector<::gbm_bo*> Buffers;
|
||||
// Index of the buffer currently handed to the compositor as the front
|
||||
// buffer, or -1 when nothing has been locked.
|
||||
Int LockedIndex = -1;
|
||||
// Indices of buffers the compositor has released and that may be handed
|
||||
// out again, plus the one that is locked.
|
||||
std::vector<Bool> Free;
|
||||
|
||||
::gbm_device* Owner = nullptr;
|
||||
|
||||
// Dropping the surface's references here is what keeps the creation path
|
||||
// free of a second failure path: if the third swapchain buffer cannot be
|
||||
// allocated, unwinding the half-built surface returns the two that exist.
|
||||
~GbmSurface();
|
||||
};
|
||||
|
||||
// The device ties one backend choice, one host channel and one mutex together.
|
||||
// The single mutex is deliberate: GBM calls arrive from the compositor's
|
||||
// render thread and from its main thread, and the state they touch (the bo
|
||||
// table, the backends) is small enough that one lock is cheaper than several.
|
||||
struct GbmDevice
|
||||
{
|
||||
// The fd this device was created with. It is a private dup taken at
|
||||
// creation, never the caller's descriptor, so gbm_device_destroy can close
|
||||
// it without ever closing an fd the caller still has open. -1 when the
|
||||
// caller passed none and no render node could be opened: the self-allocated
|
||||
// backend does not need a DRM node, so that is a working device and not an
|
||||
// error.
|
||||
Int DeviceFd = -1;
|
||||
// The dma-heap node this device allocates from, or -1 when none could be
|
||||
// opened, in which case every allocation falls back to a memfd.
|
||||
Int HeapFd = -1;
|
||||
String HeapPath;
|
||||
|
||||
// Where the display host is reached, as configured when the device was created.
|
||||
String SocketPath;
|
||||
GbmBackend DefaultBackend = GbmBackend::DmaHeap;
|
||||
// True while the channel below is connected and frames are arriving over it.
|
||||
// False means every allocation this device answers is answered locally.
|
||||
Bool HostAvailable = false;
|
||||
// The name gbm_device_get_backend_name reports. It names what this device
|
||||
// actually does, because a compositor that logs it is asking exactly that - so
|
||||
// it changes when the host channel connects and when that connection dies, and
|
||||
// SelfAllocatedBackendName is what it falls back to when it does.
|
||||
String BackendName = "mobilegl";
|
||||
String SelfAllocatedBackendName = "mobilegl";
|
||||
|
||||
std::mutex Mutex;
|
||||
std::unordered_map<::gbm_bo*, GbmBufferObject*> Buffers;
|
||||
std::unordered_map<::gbm_surface*, GbmSurface*> Surfaces;
|
||||
|
||||
// The host channel is a separate translation unit so that the socket protocol
|
||||
// never leaks into the allocation paths. It lives for as long as the device
|
||||
// does and is reached only under this device's mutex, which is what lets the
|
||||
// allocation path ask it for a frame without a second lock to order against.
|
||||
class GbmHostChannel* Host = nullptr;
|
||||
|
||||
// Closes the heap node and the private device dup, so that a device which
|
||||
// fails to finish constructing still releases the descriptors it opened.
|
||||
~GbmDevice();
|
||||
};
|
||||
|
||||
// ---- format description ------------------------------------------------
|
||||
|
||||
// Fills planes[]/planeCount/totalBytes with the linear layout this library
|
||||
// would allocate for format at width x height, and returns false for a format
|
||||
// it cannot lay out. gbm_bo_create refuses a format this refuses, because a
|
||||
// buffer whose plane layout was guessed is worse than a buffer that was not
|
||||
// handed out at all.
|
||||
Bool GbmDescribeFormat(Uint32 format, Uint32 width, Uint32 height, GbmPlaneLayout planes[GbmMaxPlanes],
|
||||
Int& planeCount, SizeT& totalBytes) noexcept;
|
||||
|
||||
// Whether this library can allocate the format at all.
|
||||
Bool GbmFormatIsSupported(Uint32 format) noexcept;
|
||||
|
||||
// Bytes per pixel of plane 0: what gbm_bo_map needs to place a sub-rectangle,
|
||||
// and half of what gbm_bo_get_bpp reports.
|
||||
Uint32 GbmFormatBytesPerPixel(Uint32 format) noexcept;
|
||||
|
||||
// ---- records and the self-allocated backend ----------------------------
|
||||
|
||||
// Builds a device. The only helper here that is called without the mutex,
|
||||
// because no other thread can see the device it is building.
|
||||
::gbm_device* GbmDeviceCreate(Int fd) noexcept;
|
||||
|
||||
// Tears the device down: every remaining surface, every remaining buffer, the
|
||||
// heap node, the private device fd, and the record itself.
|
||||
void GbmDeviceDestroy(::gbm_device& device) noexcept;
|
||||
|
||||
// Allocates a buffer from the device's heap node, or from a memfd when there
|
||||
// is none, and records it in the device's table. Returns null with errno set.
|
||||
::gbm_bo* GbmBufferCreate(::gbm_device& device, Uint32 width, Uint32 height, Uint32 format, Uint64 usage,
|
||||
const Uint64* modifiers, Uint32 modifierCount) noexcept;
|
||||
|
||||
// Wraps descriptors the caller already owns. The descriptors are duped, so the
|
||||
// caller keeps ownership of its own; type is GBM_BO_IMPORT_FD or
|
||||
// GBM_BO_IMPORT_FD_MODIFIER.
|
||||
::gbm_bo* GbmBufferImport(::gbm_device& device, Uint32 type, const void* buffer, Uint64 usage) noexcept;
|
||||
|
||||
// Drops one reference, destroying the buffer when it was the last one.
|
||||
void GbmBufferRelease(::gbm_device& device, ::gbm_bo& bo) noexcept;
|
||||
|
||||
// Destroys a buffer outright, whatever its reference count says. Only the
|
||||
// device's own teardown uses this: by then no caller's reference is meaningful.
|
||||
void GbmBufferForceDestroy(::gbm_device& device, ::gbm_bo& bo) noexcept;
|
||||
|
||||
// Hands out a dup of the descriptor backing a plane, which the caller then owns.
|
||||
// A plane this buffer does not have is refused with errno = EINVAL.
|
||||
Int GbmBufferDupFd(::gbm_bo& bo, Int plane) noexcept;
|
||||
|
||||
// Maps the whole buffer once and counts the call; strideOut receives the
|
||||
// stride of plane 0. The rect (x, y) is applied to the returned address.
|
||||
void* GbmBufferMap(::gbm_bo& bo, Uint32 x, Uint32 y, Uint32& strideOut) noexcept;
|
||||
|
||||
// Drops one mapping; the last one unmaps.
|
||||
void GbmBufferUnmap(::gbm_bo& bo, void* mapData) noexcept;
|
||||
|
||||
// ---- host frames -------------------------------------------------------
|
||||
|
||||
// Builds the channel to the display host and makes one attempt to connect it, then
|
||||
// stores it in the device. A machine that is not the container half of a split run
|
||||
// has nothing at that path, and that is a device which allocates locally from then
|
||||
// on - not a device that failed to be created.
|
||||
void GbmHostChannelCreate(::gbm_device& device) noexcept;
|
||||
|
||||
// Closes the channel and releases what it holds. Called from the device's
|
||||
// destructor, after the buffers that still name offered frames are gone, so that
|
||||
// their releases can still be sent.
|
||||
void GbmHostChannelDestroy(::gbm_device& device) noexcept;
|
||||
|
||||
// Takes the next frame the host offered, or returns false when none is waiting or
|
||||
// the channel is down. Never blocks: the socket is non-blocking, so a caller that
|
||||
// finds nothing allocates locally in the same call, which is what makes this usable
|
||||
// from a compositor's frame loop.
|
||||
Bool GbmHostChannelTakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept;
|
||||
|
||||
// Tells the host that a frame may be recycled. Called from the buffer record's
|
||||
// destructor, so a frame goes back exactly when nothing in this process can still
|
||||
// read it, and only into the connection that offered it.
|
||||
void GbmHostChannelReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept;
|
||||
|
||||
// ---- swapchains --------------------------------------------------------
|
||||
|
||||
// Builds a surface and its swapchain up front, so that
|
||||
// gbm_surface_has_free_buffers can answer truthfully from the first call.
|
||||
::gbm_surface* GbmSurfaceCreate(::gbm_device& device, Uint32 width, Uint32 height, Uint32 format, Uint64 usage,
|
||||
const Uint64* modifiers, Uint32 modifierCount) noexcept;
|
||||
|
||||
// Unregisters the surface and destroys it, returning the swapchain buffers it
|
||||
// still owns to the device.
|
||||
void GbmSurfaceDestroy(::gbm_surface& surface) noexcept;
|
||||
|
||||
// Hands out the next free swapchain buffer with a reference of its own, or
|
||||
// null with errno = EBUSY when the compositor still holds every one of them.
|
||||
::gbm_bo* GbmSurfaceLockFront(::gbm_surface& surface) noexcept;
|
||||
|
||||
// Takes a locked buffer back, dropping the reference the lock handed out.
|
||||
void GbmSurfaceReleaseBuffer(::gbm_surface& surface, ::gbm_bo& bo) noexcept;
|
||||
|
||||
Bool GbmSurfaceHasFreeBuffers(::gbm_surface& surface) noexcept;
|
||||
} // namespace MobileGL::MG_Gbm
|
||||
|
||||
// The public opaque types are the internal records. Callers only ever see the
|
||||
// pointers, so this costs nothing and removes a layer of casting.
|
||||
struct gbm_device
|
||||
{
|
||||
MobileGL::MG_Gbm::GbmDevice Impl;
|
||||
};
|
||||
|
||||
struct gbm_bo
|
||||
{
|
||||
MobileGL::MG_Gbm::GbmBufferObject Impl;
|
||||
};
|
||||
|
||||
struct gbm_surface
|
||||
{
|
||||
MobileGL::MG_Gbm::GbmSurface Impl;
|
||||
};
|
||||
|
||||
// End of Source File Header
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "EGLImpl.h"
|
||||
#include "EGLPlatformExtensions.h"
|
||||
#include "WaylandWindow.h"
|
||||
#include "../GetProcAddress.h"
|
||||
#include <Init.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
@@ -20,6 +22,36 @@
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
// The KHR spellings of the image entry points, exported by Exporting/Definitions.cpp. Declared
|
||||
// here because the bundled EGL headers declare them only under EGL_EGLEXT_PROTOTYPES, which no
|
||||
// build of this library defines, and because eglGetProcAddress below answers them by name.
|
||||
extern "C" EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image);
|
||||
|
||||
// The swap-and-sync family the compositor resolves by name while bringing its EGL backend up; defined
|
||||
// in Exporting/Definitions.cpp and answered from GetProcAddress below (see the definitions).
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
|
||||
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
|
||||
extern "C" EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
|
||||
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
|
||||
// THE DEVICE-QUERY FAMILY, exported by Exporting/Definitions.cpp and answered by name below: kwin
|
||||
// reaches eglQueryDisplayAttribEXT through libepoxy's pointer (that is how finding a render device
|
||||
// starts), and libepoxy asks for it by name. See the definitions for why these had to exist.
|
||||
extern "C" EGLBoolean eglQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices);
|
||||
extern "C" EGLBoolean eglQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value);
|
||||
extern "C" const char* eglQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name);
|
||||
extern "C" EGLBoolean eglQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value);
|
||||
extern "C" EGLSurface eglCreatePlatformWindowSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_window,
|
||||
const EGLAttrib* attrib_list);
|
||||
extern "C" EGLSurface eglCreatePlatformPixmapSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
|
||||
const EGLAttrib* attrib_list);
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
namespace {
|
||||
using EGLStateContext = MG_State::EGLState::EGLContext;
|
||||
@@ -116,6 +148,73 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return reinterpret_cast<void*>(static_cast<SizeT>(nativeHandle));
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
// Every Wayland window surface's presentation, by surface. Guarded by EGLOperationMutex,
|
||||
// which every entry point that reaches it holds.
|
||||
UnorderedMap<EGLSurface, UniquePtr<Wayland::WindowSurface>>& WaylandSurfaces() {
|
||||
static auto* surfaces = new UnorderedMap<EGLSurface, UniquePtr<Wayland::WindowSurface>>();
|
||||
return *surfaces;
|
||||
}
|
||||
|
||||
// A WINDOW ON A WAYLAND DISPLAY. Whatever MOBILEGL_IPC_SURFACE says, the window is this
|
||||
// application's own wl_surface, so neither the server's window nor the display host's frame
|
||||
// is what it shows: the backend draws into a pbuffer of the window's size, and each swap
|
||||
// reads the frame back and attaches it to the wl_surface (WaylandWindow.h). Answers false
|
||||
// when the display is not a Wayland one; otherwise *out is the surface, or EGL_NO_SURFACE
|
||||
// with the error set.
|
||||
template <typename State>
|
||||
Bool CreateWaylandWindowSurface(State* state, EGLDisplay dpy, EGLConfig config, void* window,
|
||||
Bool platformWindow, EGLSurface* out) {
|
||||
Uint64 nativeDisplay = 0;
|
||||
EGLenum platform = EGL_NONE;
|
||||
if (!state->GetDisplayNative(dpy, &nativeDisplay, &platform) ||
|
||||
!Wayland::IsWaylandDisplay(nativeDisplay, platform)) {
|
||||
return false;
|
||||
}
|
||||
*out = EGL_NO_SURFACE;
|
||||
if (!state->IsDisplayInitialized(dpy)) {
|
||||
state->SetError(EGL_NOT_INITIALIZED);
|
||||
return true;
|
||||
}
|
||||
if (!state->ValidateConfigOnDisplay(dpy, config)) {
|
||||
state->SetError(EGL_BAD_CONFIG);
|
||||
return true;
|
||||
}
|
||||
EGLint width = 0;
|
||||
EGLint height = 0;
|
||||
if (!Wayland::WindowSize(window, &width, &height)) {
|
||||
state->SetError(EGL_BAD_NATIVE_WINDOW);
|
||||
return true;
|
||||
}
|
||||
EGLint alpha = 0;
|
||||
(void)state->GetConfigAttrib(dpy, config, EGL_ALPHA_SIZE, &alpha);
|
||||
auto presentation =
|
||||
Wayland::WindowSurface::Create(reinterpret_cast<void*>(static_cast<uintptr_t>(nativeDisplay)), window,
|
||||
width, height, alpha > 0);
|
||||
if (!presentation) {
|
||||
state->SetError(EGL_BAD_NATIVE_WINDOW);
|
||||
return true;
|
||||
}
|
||||
const EGLSurface surface =
|
||||
platformWindow ? state->CreatePlatformWindowSurface(dpy, config, window, nullptr)
|
||||
: state->CreateWindowSurface(
|
||||
dpy, config, (NativeWindowType)(reinterpret_cast<uintptr_t>(window)), nullptr);
|
||||
if (surface == EGL_NO_SURFACE) return true;
|
||||
(void)state->ResizeSurface(dpy, surface, width, height);
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject || !backendObject->CreateEGLPbufferSurface(surface, width, height)) {
|
||||
MGLOG_E("eglCreateWindowSurface: the backend refused the %dx%d drawable behind a Wayland window",
|
||||
width, height);
|
||||
state->DestroySurface(dpy, surface);
|
||||
state->SetError(EGL_BAD_ALLOC);
|
||||
return true;
|
||||
}
|
||||
WaylandSurfaces()[surface] = std::move(presentation);
|
||||
*out = surface;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
|
||||
@@ -124,6 +223,16 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
{
|
||||
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
|
||||
EGLSurface waylandSurface = EGL_NO_SURFACE;
|
||||
if (CreateWaylandWindowSurface(state, dpy, config, ToVoidHandle(window), /*platformWindow=*/false,
|
||||
&waylandSurface)) {
|
||||
return waylandSurface;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!state->IsDisplayInitialized(dpy)) {
|
||||
state->SetError(EGL_NOT_INITIALIZED);
|
||||
return EGL_NO_SURFACE;
|
||||
@@ -137,8 +246,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// the window is the SERVER's: a headless client passes none, so NULL is not
|
||||
// EGL_BAD_NATIVE_WINDOW here. Whatever the client passed never reaches the wire - the
|
||||
// remote backend sends WindowKind::ServerOwned with token 0 (BackendObject_Remote).
|
||||
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
|
||||
if (IsNullNativeHandle(window) && !serverOwnedWindow) {
|
||||
// Either on-screen shape leaves this side without a window of its own: =server asks the
|
||||
// server for its own window, =host draws into the frames the display host offers.
|
||||
const Bool windowlessOnScreen =
|
||||
MG_Config::ServerOwnedWindowSurfaces() || MG_Config::HostFrameWindowSurfaces();
|
||||
if (IsNullNativeHandle(window) && !windowlessOnScreen) {
|
||||
#else
|
||||
if (IsNullNativeHandle(window)) {
|
||||
#endif
|
||||
@@ -157,15 +269,15 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// alike; the same game in a landscape window was right). The native window it handed us
|
||||
// knows the size the game believes it has: ask for THAT, and the server fixes its window
|
||||
// buffer to it and aspect-fits the view (MobileGLDisplayActivity), letterboxed.
|
||||
if (serverOwnedWindow && requestedWidth == 0 && requestedHeight == 0 && !IsNullNativeHandle(window)) {
|
||||
if (windowlessOnScreen && requestedWidth == 0 && requestedHeight == 0 && !IsNullNativeHandle(window)) {
|
||||
auto* native = reinterpret_cast<ANativeWindow*>(ToVoidHandle(window));
|
||||
const int32_t nativeWidth = ANativeWindow_getWidth(native);
|
||||
const int32_t nativeHeight = ANativeWindow_getHeight(native);
|
||||
if (nativeWidth > 0 && nativeHeight > 0) {
|
||||
requestedWidth = static_cast<Uint32>(nativeWidth);
|
||||
requestedHeight = static_cast<Uint32>(nativeHeight);
|
||||
MGLOG_I("eglCreateWindowSurface: no EGL_WIDTH/EGL_HEIGHT; the server-owned window is asked for the "
|
||||
"native window's %dx%d", nativeWidth, nativeHeight);
|
||||
MGLOG_I("eglCreateWindowSurface: no EGL_WIDTH/EGL_HEIGHT; the on-screen surface is asked for "
|
||||
"the native window's %dx%d", nativeWidth, nativeHeight);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -177,11 +289,20 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EGLSurface surface = serverOwnedWindow
|
||||
? state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false)
|
||||
: state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
if (MG_Config::ServerOwnedWindowSurfaces()) {
|
||||
// The server's own window: it makes one and draws into it.
|
||||
surface = state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false);
|
||||
} else if (MG_Config::HostFrameWindowSurfaces()) {
|
||||
// The display host's frames, as above: no window of the client's either way.
|
||||
surface = state->CreateHostFrameWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false);
|
||||
} else {
|
||||
surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
}
|
||||
#else
|
||||
EGLSurface surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
#endif
|
||||
@@ -211,6 +332,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!state->ValidateSurfaceOnDisplay(dpy, draw)) {
|
||||
MGLOG_E_ONCE("eglSwapBuffers: surface %p is not a surface of display %p (EGL_BAD_SURFACE)", draw, dpy);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -220,6 +342,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
// A Wayland window shows what is attached to it: the frame is read back and attached
|
||||
// BEFORE the swap, while it is still the drawable's content.
|
||||
if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end()) (void)it->second->Present();
|
||||
#endif
|
||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
||||
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
@@ -230,7 +357,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
EGLBoolean ChooseConfig(EGLDisplay dpy, const EGLint* attrib_list, EGLConfig* configs, EGLint config_size,
|
||||
EGLint* num_config) {
|
||||
auto* state = GetState();
|
||||
// eglChooseConfig can be an application's first EGL call after eglGetPlatformDisplay + eglInitialize,
|
||||
// and for a display built from an EGLDeviceEXT nothing has brought MobileGL up yet: GetState()
|
||||
// then answers NULL and the call lands in EGLContext::ChooseConfig with a null this - measured as
|
||||
// a crash inside that function (pc/lr both resolved to it) with kwin's own "pEGLContext is null.
|
||||
// MG_State may not be initialized." as the only warning, and reported one layer up as
|
||||
// "Could not initialize rendering context".
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -238,11 +371,26 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
|
||||
EGLContext CreateContext(EGLDisplay dpy, EGLConfig config, EGLContext shareCtx, const EGLint* attrib_list) {
|
||||
auto* state = GetState();
|
||||
// WHICH CONTEXT IS BEING ASKED FOR, and what came back. kwin's OpenGL compositing fails with
|
||||
// "Could not initialize rendering context" while the same path succeeds from a probe against
|
||||
// this same library (probe10: a 3.3 core context on a GBM surface, makeCurrent answered 1),
|
||||
// so the difference has to be in this argument list - and an EGL vendor cannot see it from the
|
||||
// outside. The list is dumped in EGL_NONE-terminated pairs; EGL_NONE is 0x3038, not 0.
|
||||
MGLOG_I("eglCreateContext(dpy=%p, config=%p, share=%p)", dpy, config, shareCtx);
|
||||
if (attrib_list != nullptr) {
|
||||
for (int index = 0; attrib_list[index] != EGL_NONE; index += 2) {
|
||||
MGLOG_I(" attrib 0x%04x = %d", attrib_list[index], attrib_list[index + 1]);
|
||||
}
|
||||
} else {
|
||||
MGLOG_I(" (no attribute list)");
|
||||
}
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_NO_CONTEXT;
|
||||
}
|
||||
return state->CreateContext(dpy, config, shareCtx, attrib_list);
|
||||
EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
|
||||
MGLOG_I("eglCreateContext -> %p", static_cast<void*>(context));
|
||||
return context;
|
||||
}
|
||||
|
||||
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||
@@ -282,6 +430,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return state->ConsumeError();
|
||||
}
|
||||
|
||||
// THE DISPLAY HOST'S FRAME, AS THE COMPOSITOR PRESENTS IT. Set by the surfaceless-bind hook in
|
||||
// MakeCurrent when this client (the compositor) takes the host frame; read by glFlush, which is
|
||||
// that compositor's only frame boundary - it never calls eglSwapBuffers, and without a present
|
||||
// the display host is never told a frame is done, so nothing ever reaches the glass.
|
||||
EGLDisplay g_hostFramePresentDisplay = nullptr;
|
||||
EGLSurface g_hostFramePresentSurface = nullptr;
|
||||
|
||||
EGLBoolean MakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
|
||||
auto* state = GetState();
|
||||
@@ -298,6 +453,81 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
|
||||
threadId.c_str(), dpy, draw, read, ctx, oldDisplay, oldDraw, oldRead, oldContext);
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// THE DISPLAY HOST'S FRAMES, TO A CLIENT THAT ASKS FOR NO SURFACE.
|
||||
//
|
||||
// A compositor on this route never calls eglCreateWindowSurface: it imports the display host's
|
||||
// dma-bufs and draws into them through framebuffers, binding its contexts with EGL_NO_SURFACE.
|
||||
// The host frame bridge, though, is opened by exactly one thing - a surface being CREATED and
|
||||
// activated (ServerLoop's CreateWindowSurface with the host-frame window backend ->
|
||||
// InitHostFrameSurface) - so with no surface ever created the bridge stayed shut, the display
|
||||
// host kept offering frames into a name nobody served ("offer buffer 0: bridge fd=-1"), and no
|
||||
// frame could reach the glass no matter how much was drawn. So: when the session asked for
|
||||
// host-framed surfaces and a real context is being bound with no surface, make it the surface
|
||||
// eglCreatePlatformWindowSurface would have made, once, and bind that. The size is the display
|
||||
// host's own frame size; the client's window value is not on this path at all.
|
||||
// ONLY THE COMPOSITOR TAKES THE HOST FRAME. A desktop is full of processes that bind a
|
||||
// context with no surface - konsole, plasmashell, kactivitymanagerd - and measured with
|
||||
// probe15 as the second client: such a bind from a NON-compositor process made the host-frame
|
||||
// activation fail (makeCurrent=0) and the process aborted. The compositor is marked by its
|
||||
// launcher with MOBILEGL_HOST_FRAME_CLIENT=1; everyone else keeps the plain surfaceless bind,
|
||||
// which is all they ever wanted.
|
||||
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT &&
|
||||
MG_Config::HostFrameWindowSurfaces() &&
|
||||
(std::getenv("MOBILEGL_HOST_FRAME_CLIENT") != nullptr &&
|
||||
std::string("1") == std::getenv("MOBILEGL_HOST_FRAME_CLIENT"))) {
|
||||
static Bool hostFrameSurfaceTaken = false;
|
||||
if (!hostFrameSurfaceTaken) {
|
||||
hostFrameSurfaceTaken = true;
|
||||
EGLConfig hostFrameConfig = nullptr;
|
||||
EGLint hostFrameConfigCount = 0;
|
||||
const EGLint hostFrameChoose[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE};
|
||||
if (state->ChooseConfig(dpy, hostFrameChoose, &hostFrameConfig, 1,
|
||||
&hostFrameConfigCount) &&
|
||||
hostFrameConfigCount > 0 && hostFrameConfig != nullptr) {
|
||||
// BOTH HALVES, exactly as eglCreateWindowSurface makes one: the EGL state's
|
||||
// registration (without it every later bind and swap of this surface was refused
|
||||
// as EGL_BAD_SURFACE - measured: "eglMakeCurrent rejected by EGLState
|
||||
// error=0x300d", and glFlush's swaps returned before reaching the backend), then
|
||||
// the backend's own window-surface entry, which the remote backend routes to the
|
||||
// host frame (BackendObject_Remote::CreateHostFrameWindowSurface) - the call that
|
||||
// puts a CreateWindowSurface frame with the host-frame window backend on the wire.
|
||||
const EGLSurface hostFrameSurface = state->CreateHostFrameWindowSurface(
|
||||
dpy, hostFrameConfig, nullptr, 1440, 3200, /*platformWindow=*/false);
|
||||
MG_Backend::WindowHandle hostFrameHandle{};
|
||||
hostFrameHandle.Width = 1440;
|
||||
hostFrameHandle.Height = 3200;
|
||||
if (hostFrameSurface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("host frame: the EGL state refused the display host's frame surface");
|
||||
} else if (!MG_Backend::pActiveBackendObject->CreateEGLWindowSurface(hostFrameSurface,
|
||||
hostFrameHandle)) {
|
||||
MGLOG_E("host frame: the backend refused the display host's frame surface");
|
||||
state->DestroySurface(dpy, hostFrameSurface);
|
||||
} else {
|
||||
// The frame boundary of a compositor that never swaps is its glFlush. glFlush
|
||||
// has no EGL context of its own, so remember HERE which surface presents the
|
||||
// display host's frame; glFlush presents it (see the host-frame present in
|
||||
// glFlush's implementation).
|
||||
g_hostFramePresentDisplay = dpy;
|
||||
g_hostFramePresentSurface = hostFrameSurface;
|
||||
MGLOG_I("host frame: the first surfaceless eglMakeCurrent made the display host's "
|
||||
"frame surface %p (%dx%d)",
|
||||
hostFrameSurface, 1440, 3200);
|
||||
}
|
||||
} else {
|
||||
MGLOG_E("host frame: no config for the display host's frame surface");
|
||||
}
|
||||
}
|
||||
// EVERY surfaceless bind, not only the first: the compositor rebinds with EGL_NO_SURFACE
|
||||
// all the time, and a bind without the surface leaves nothing current for glFlush's swap
|
||||
// to present (BackendObject::SwapEGLBuffers: "draw surface is not current on this thread").
|
||||
if (g_hostFramePresentSurface != nullptr && dpy == g_hostFramePresentDisplay) {
|
||||
draw = g_hostFramePresentSurface;
|
||||
read = g_hostFramePresentSurface;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!state->MakeCurrent(dpy, draw, read, ctx)) {
|
||||
const EGLint error = state->ConsumeError();
|
||||
MGLOG_D("eglMakeCurrent rejected by EGLState thread=%s error=0x%04x", threadId.c_str(), error);
|
||||
@@ -355,6 +585,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state->DestroySurface(dpy, surface)) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
WaylandSurfaces().erase(surface);
|
||||
#endif
|
||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||
backendObject->ReleaseEGLSurface(surface);
|
||||
}
|
||||
@@ -463,14 +696,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
case EGL_CLIENT_APIS:
|
||||
return "OpenGL OpenGL_ES";
|
||||
case EGL_EXTENSIONS:
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
return "EGL_EXT_client_extensions "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
}
|
||||
return "EGL_KHR_create_context "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
// One list, in EGLPlatformExtensions.h, because the vendor string glvnd reads and
|
||||
// this query an application reads have to agree: an application that does not see the
|
||||
// platform extension here never asks for a platform display at all.
|
||||
return display == EGL_NO_DISPLAY ? kClientExtensionString : kDisplayExtensionString;
|
||||
default:
|
||||
state->SetError(EGL_BAD_PARAMETER);
|
||||
return nullptr;
|
||||
@@ -705,6 +934,23 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return state->DestroyImage(dpy, image) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
EGLImageKHR CreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list) {
|
||||
// EGL_KHR_image_base spells its attribute list in EGLint pairs and the core eglCreateImage
|
||||
// in EGLAttrib pairs, and the two are not the same memory: on a 64-bit build one entry is
|
||||
// twice the width of the other, so the caller's array cannot be handed over as it stands.
|
||||
// Re-spelling it is therefore part of the entry point rather than a detail of the state.
|
||||
Vector<EGLAttrib> attribs;
|
||||
if (attrib_list != nullptr) {
|
||||
for (SizeT i = 0; attrib_list[i] != EGL_NONE; i += 2) {
|
||||
attribs.push_back(static_cast<EGLAttrib>(attrib_list[i]));
|
||||
attribs.push_back(static_cast<EGLAttrib>(attrib_list[i + 1]));
|
||||
}
|
||||
attribs.push_back(static_cast<EGLAttrib>(EGL_NONE));
|
||||
}
|
||||
return CreateImage(dpy, ctx, target, buffer, attribs.empty() ? nullptr : attribs.data());
|
||||
}
|
||||
|
||||
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
|
||||
(void)attrib_list;
|
||||
|
||||
@@ -721,11 +967,24 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
{
|
||||
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
|
||||
EGLSurface waylandSurface = EGL_NO_SURFACE;
|
||||
if (CreateWaylandWindowSurface(state, dpy, config, native_window, /*platformWindow=*/true,
|
||||
&waylandSurface)) {
|
||||
return waylandSurface;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 (on-screen server window), D1: as CreateWindowSurface - with MOBILEGL_IPC_SURFACE=server
|
||||
// the window is the server's, and a headless client's NULL is accepted.
|
||||
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
|
||||
if (native_window == nullptr && !serverOwnedWindow) {
|
||||
// Either on-screen shape leaves this side without a window of its own: =server asks the
|
||||
// server for its own window, =host draws into the frames the display host offers.
|
||||
const Bool windowlessOnScreen =
|
||||
MG_Config::ServerOwnedWindowSurfaces() || MG_Config::HostFrameWindowSurfaces();
|
||||
if (native_window == nullptr && !windowlessOnScreen) {
|
||||
#else
|
||||
if (native_window == nullptr) {
|
||||
#endif
|
||||
@@ -749,11 +1008,21 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EGLSurface surface = serverOwnedWindow
|
||||
? state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true)
|
||||
: state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
if (MG_Config::ServerOwnedWindowSurfaces()) {
|
||||
// The server's own window: it makes one and draws into it.
|
||||
surface = state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true);
|
||||
} else if (MG_Config::HostFrameWindowSurfaces()) {
|
||||
// The display host's frames: it allocates them, the server draws into them,
|
||||
// and the host puts each one on the glass when the server says it is drawn.
|
||||
surface = state->CreateHostFrameWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true);
|
||||
} else {
|
||||
surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
}
|
||||
#else
|
||||
EGLSurface surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
#endif
|
||||
@@ -822,6 +1091,72 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MobileGL::EnsureInitialized();
|
||||
|
||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||
|
||||
// THE KHR SPELLINGS ARE ENTRY POINTS OF THEIR OWN, and by name is the only way anything
|
||||
// asks for them. Measured in the Fedora container that runs this library as the glvnd EGL
|
||||
// vendor, before they were answered here: eglGetProcAddress("eglCreateImageKHR") -> NULL and
|
||||
// eglGetProcAddress("eglDestroyImageKHR") -> NULL. kwin_wayland's anland backend imports
|
||||
// the display daemon's dma-bufs through exactly these two (AnlandEglLayer::importBuffers ->
|
||||
// EglBackend::importDmaBufAsTexture -> EglDisplay::importBufferAsImage ->
|
||||
// EglDisplay::createImage, which calls the pointer without a null check), so a NULL here is
|
||||
// a compositor that cannot draw a single pixel into the frames the daemon hands it. The
|
||||
// core spellings were already in the table below; these are the same functions under the
|
||||
// names EGL_KHR_image_base gives them, which is also why they answer in the glvnd vendor
|
||||
// table beside them (Exporting/GlvndVendor.cpp).
|
||||
if (std::strcmp(name, "eglCreateImageKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreateImageKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglDestroyImageKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglDestroyImageKHR);
|
||||
}
|
||||
// The device-query family and the EXT platform-surface spellings. Measured in the container
|
||||
// before they were answered here: eglGetProcAddress returned NULL for eglQueryDisplayAttribEXT,
|
||||
// eglQueryDevicesEXT, eglQueryDeviceAttribEXT, eglQueryDeviceStringEXT and
|
||||
// eglCreatePlatformWindowSurfaceEXT while the extension strings announced EGL_EXT_device_query -
|
||||
// a NULL libepoxy then called (kwin crashed at pc=0), and withdrawing the announcement instead
|
||||
// left kwin without a render device and its compositing inactive.
|
||||
if (std::strcmp(name, "eglQueryDevicesEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDevicesEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglQueryDeviceAttribEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDeviceAttribEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglQueryDeviceStringEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDeviceStringEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglQueryDisplayAttribEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDisplayAttribEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglCreatePlatformWindowSurfaceEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreatePlatformWindowSurfaceEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglCreatePlatformPixmapSurfaceEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreatePlatformPixmapSurfaceEXT);
|
||||
}
|
||||
// The swap-and-sync spellings kwin resolves by name while it is still bringing its EGL backend
|
||||
// up (see the definitions: EGL_KHR_fence_sync, the damage variants, EGL_EXT_buffer_age).
|
||||
if (std::strcmp(name, "eglSwapBuffersWithDamageKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglSwapBuffersWithDamageKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglSwapBuffersWithDamageEXT") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglSwapBuffersWithDamageEXT);
|
||||
}
|
||||
if (std::strcmp(name, "eglCreateSyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreateSyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglDestroySyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglDestroySyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglClientWaitSyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglClientWaitSyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglWaitSyncKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglWaitSyncKHR);
|
||||
}
|
||||
if (std::strcmp(name, "eglGetSyncAttribKHR") == 0) {
|
||||
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglGetSyncAttribKHR);
|
||||
}
|
||||
|
||||
void* proc = MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_D("Failed to get function: %s", name);
|
||||
|
||||
@@ -8,8 +8,18 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
// EGLImageKHR: the return type of the KHR spelling of eglCreateImage below. Includes.h brings in
|
||||
// the core egl.h, which has no KHR types in it.
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// The display host's frame surface, as taken by the surfaceless-bind hook in MakeCurrent, and the
|
||||
// display it belongs to. glFlush reads these: a compositor's frame boundary is glFlush, and the
|
||||
// host frame's "frame is done" signal IS a swap of this surface. Null until a client marked
|
||||
// MOBILEGL_HOST_FRAME_CLIENT=1 actually took the frame.
|
||||
extern EGLDisplay g_hostFramePresentDisplay;
|
||||
extern EGLSurface g_hostFramePresentSurface;
|
||||
|
||||
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
|
||||
const EGLint* attrib_list);
|
||||
EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
@@ -53,6 +63,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
EGLBoolean GetSyncAttrib(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib* value);
|
||||
EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLAttrib* attrib_list);
|
||||
// The same image under EGL_KHR_image_base's name and its EGLint attribute list: the two lists
|
||||
// are pairs of different widths, so the caller's array is re-spelled rather than cast.
|
||||
EGLImageKHR CreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list);
|
||||
EGLBoolean DestroyImage(EGLDisplay dpy, EGLImage image);
|
||||
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list);
|
||||
EGLSurface CreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// THE PLATFORMS THIS IMPLEMENTATION SERVES, in the words EGL spells them.
|
||||
//
|
||||
// Two callers need exactly the same list, and they disagree if it is written twice:
|
||||
// eglQueryString(EGL_EXTENSIONS) is what an application reads BEFORE it decides to call
|
||||
// eglGetPlatformDisplay at all - libepoxy refuses to use eglGetPlatformDisplayEXT unless the
|
||||
// string names EGL_EXT_platform_base - and the glvnd vendor string is what the system's
|
||||
// libEGL reads to decide which vendor library a platform request should be routed to. A
|
||||
// vendor that does not claim the platform is never asked for it, and the caller is told there
|
||||
// is no provider: the library can be loaded, initialized, and still never called.
|
||||
//
|
||||
// GBM is on the list because a compositor asks for its display on the GBM device it owns.
|
||||
// This side treats that device the way it treats every other native display - the frames come
|
||||
// from the display host, not from the device - so the claim is exact rather than optimistic.
|
||||
inline constexpr const char* kDeviceExtensionString =
|
||||
"EGL_EXT_device_base "
|
||||
"EGL_EXT_device_enumeration "
|
||||
"EGL_EXT_device_query "
|
||||
"EGL_EXT_device_drm "
|
||||
"EGL_EXT_device_drm_render_node";
|
||||
|
||||
// EGL_EXT_client_extensions IS ON THE PLATFORM LIST FOR A MEASURED REASON. libglvnd only folds
|
||||
// a vendor client-extension string (the answer to eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS))
|
||||
// into its own no-display answer when the vendor PLATFORM string - the one __egl_Main publishes
|
||||
// for the platform-extensions query - names EGL_EXT_client_extensions. Without it the client
|
||||
// string never reached libepoxy, so libepoxy concluded EGL_EXT_device_query was absent and left
|
||||
// epoxy_eglQueryDisplayAttribEXT as a NULL pointer, while kwin (which reads the display string,
|
||||
// where the extension IS present) called it: measured as signal 11, fault_addr=(nil), pc=0x0,
|
||||
// from KWin::EglDisplay::determineRenderNode() through the GOT slot whose relocation is
|
||||
// R_AARCH64_GLOB_DAT epoxy_eglQueryDisplayAttribEXT.
|
||||
// Wayland where a Wayland client can exist (WaylandWindow.h: window surfaces presented through
|
||||
// wl_shm). Without it glvnd never hands this vendor a Wayland display at all.
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS " EGL_KHR_platform_wayland EGL_EXT_platform_wayland"
|
||||
#else
|
||||
#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS ""
|
||||
#endif
|
||||
inline constexpr const char* kPlatformExtensionString =
|
||||
"EGL_EXT_client_extensions "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_KHR_platform_gbm "
|
||||
"EGL_EXT_platform_gbm "
|
||||
"EGL_MESA_platform_gbm "
|
||||
"EGL_EXT_platform_device "
|
||||
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
|
||||
|
||||
// What a display answers for EGL_EXTENSIONS: the extensions this implementation provides that
|
||||
// are not platform ones, then the platform list above.
|
||||
// EGL_KHR_no_config_context and EGL_KHR_surfaceless_context are on the display and client lists
|
||||
// because both are answered by this implementation rather than announced by a driver: a context
|
||||
// is created with EGL_NO_CONFIG_KHR and made current with EGL_NO_SURFACE, which is the pair a
|
||||
// compositor asks for when it renders into framebuffers of its own. Measured on kwin_wayland
|
||||
// against this vendor before they were here: createContext() refused with
|
||||
// "EGL_KHR_no_config_context extension is unsupported" and the session ended at
|
||||
// "no usable DRM render device; cannot bring up OpenGL compositing".
|
||||
//
|
||||
// EGL_KHR_image_base AND EGL_EXT_image_dma_buf_import ARE ONE FEATURE HERE, and they are on the
|
||||
// list for a measured reason rather than a plausible one. In the Fedora container that runs
|
||||
// this library as the glvnd EGL vendor, eglGetProcAddress("eglCreateImageKHR") and
|
||||
// eglGetProcAddress("eglDestroyImageKHR") both returned NULL, and kwin_wayland 6.7.3's anland
|
||||
// backend imports the display daemon's dma-bufs through exactly those two
|
||||
// (AnlandEglLayer::importBuffers -> EglBackend::importDmaBufAsTexture ->
|
||||
// EglDisplay::importBufferAsImage -> EglDisplay::createImage) before it can draw a pixel into a
|
||||
// frame the daemon produced - so with a NULL the whole composited scene has no image to be
|
||||
// drawn into. Both names are backed by entry points that exist: EGL_KHR_image_base by
|
||||
// eglCreateImage/eglDestroyImage and their KHR spellings (Exporting/Definitions.cpp, and the
|
||||
// same names in the glvnd vendor table), EGL_EXT_image_dma_buf_import by EGL_LINUX_DMA_BUF_EXT
|
||||
// images, which carry the caller's EGL_WIDTH, EGL_HEIGHT and EGL_LINUX_DRM_FOURCC_EXT and own a
|
||||
// dup() of every plane's fd, offset and pitch (EGLState/Core.cpp, CreateImage).
|
||||
//
|
||||
// EGL_EXT_image_dma_buf_import_modifiers IS DELIBERATELY NOT NAMED. Its own two entry points -
|
||||
// eglQueryDmaBufFormatsEXT and eglQueryDmaBufModifiersEXT - do not exist in this library, and a
|
||||
// name in this string means the entry points beside it are there. The modifier attributes
|
||||
// (EGL_DMA_BUF_PLANEn_MODIFIER_LO/HI_EXT) are nevertheless ACCEPTED and carried on the image:
|
||||
// a caller does not need the name to send them, and kwin's does not look at any extension
|
||||
// string before it does - EglDisplay::importDmaBufAsImage appends the pair whenever the
|
||||
// buffer's modifier is not DRM_FORMAT_MOD_INVALID.
|
||||
inline constexpr const char* kDisplayExtensionString =
|
||||
"EGL_KHR_create_context "
|
||||
// WHAT THE COMPOSITOR CHECKS BETWEEN ITS CONTEXT AND ITS FIRST SURFACE. EglBackend::init
|
||||
// asks for these three by name right after initRenderingContext() and before createSurfaces();
|
||||
// the entry points behind them are published beside this string (Exporting/Definitions.cpp),
|
||||
// so the announcement and the answers are one thing rather than two.
|
||||
"EGL_KHR_fence_sync "
|
||||
"EGL_KHR_wait_sync "
|
||||
"EGL_KHR_swap_buffers_with_damage "
|
||||
"EGL_EXT_swap_buffers_with_damage "
|
||||
"EGL_EXT_buffer_age "
|
||||
"EGL_KHR_image_base "
|
||||
"EGL_EXT_image_dma_buf_import "
|
||||
"EGL_KHR_no_config_context "
|
||||
"EGL_KHR_surfaceless_context "
|
||||
// ANNOUNCED BECAUSE IT IS NOW ANSWERED: the four names below are published as real entry
|
||||
// points and eglGetProcAddress answers them (see Exporting/Definitions.cpp and
|
||||
// EGLImpl::GetProcAddress). Both earlier states were measured failures: announcing the
|
||||
// extension while the names resolved to NULL made kwin jump to that NULL (signal 11,
|
||||
// pc=0x0, from KWin::EglDisplay::determineRenderNode()), and withdrawing the announcement
|
||||
// left kwin without a render device so its compositing never became active and nothing was
|
||||
// ever painted.
|
||||
"EGL_EXT_device_base "
|
||||
"EGL_EXT_device_enumeration "
|
||||
"EGL_EXT_device_query "
|
||||
"EGL_EXT_device_drm "
|
||||
"EGL_EXT_device_drm_render_node "
|
||||
// KWin::EglDisplay::determineRenderNode() at libkwin.so.6+0x308b9c. A compositor that is
|
||||
// never told the extension exists cannot take that path; announcing it again is the right
|
||||
// move once these four names are really published and resolvable.
|
||||
"EGL_EXT_platform_device "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_KHR_platform_gbm "
|
||||
"EGL_EXT_platform_gbm "
|
||||
"EGL_MESA_platform_gbm "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
|
||||
// ... and what EGL_NO_DISPLAY answers: the same, plus the client-extension marker that tells a
|
||||
// caller the no-display query is meaningful here.
|
||||
inline constexpr const char* kClientExtensionString =
|
||||
"EGL_EXT_client_extensions "
|
||||
"EGL_KHR_create_context "
|
||||
"EGL_KHR_image_base "
|
||||
"EGL_EXT_image_dma_buf_import "
|
||||
"EGL_KHR_no_config_context "
|
||||
"EGL_KHR_surfaceless_context "
|
||||
// ANNOUNCED BECAUSE IT IS NOW ANSWERED: the four names below are published as real entry
|
||||
// points and eglGetProcAddress answers them (see Exporting/Definitions.cpp and
|
||||
// EGLImpl::GetProcAddress). Both earlier states were measured failures: announcing the
|
||||
// extension while the names resolved to NULL made kwin jump to that NULL (signal 11,
|
||||
// pc=0x0, from KWin::EglDisplay::determineRenderNode()), and withdrawing the announcement
|
||||
// left kwin without a render device so its compositing never became active and nothing was
|
||||
// ever painted.
|
||||
"EGL_EXT_device_base "
|
||||
"EGL_EXT_device_enumeration "
|
||||
"EGL_EXT_device_query "
|
||||
"EGL_EXT_device_drm "
|
||||
"EGL_EXT_device_drm_render_node "
|
||||
// KWin::EglDisplay::determineRenderNode() at libkwin.so.6+0x308b9c. A compositor that is
|
||||
// never told the extension exists cannot take that path; announcing it again is the right
|
||||
// move once these four names are really published and resolvable.
|
||||
"EGL_EXT_platform_device "
|
||||
"EGL_EXT_platform_base "
|
||||
"EGL_KHR_platform_base "
|
||||
"EGL_KHR_platform_gbm "
|
||||
"EGL_EXT_platform_gbm "
|
||||
"EGL_MESA_platform_gbm "
|
||||
"EGL_MESA_platform_surfaceless";
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include <Includes.h>
|
||||
// EGLImageKHR, for the KHR spellings of the image entry points defined at the bottom of this file:
|
||||
// Includes.h brings in the core egl.h only.
|
||||
#include <EGL/eglext.h>
|
||||
#include "../EGLImpl.h"
|
||||
|
||||
MOBILEGL_EGL_API EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
|
||||
@@ -228,6 +231,25 @@ MOBILEGL_EGL_API EGLBoolean eglDestroyImage(EGLDisplay dpy, EGLImage image) {
|
||||
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
|
||||
}
|
||||
|
||||
// EGL_KHR_image_base's spellings of the same two entry points, exported under their own names
|
||||
// because that is the only thing that answers for them: measured in the Fedora container that runs
|
||||
// this library as the glvnd EGL vendor, eglGetProcAddress("eglCreateImageKHR") and
|
||||
// eglGetProcAddress("eglDestroyImageKHR") both returned NULL, and kwin_wayland's anland backend
|
||||
// imports the display daemon's dma-bufs through exactly these two.
|
||||
MOBILEGL_EGL_API EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list) {
|
||||
MGLOG_D("eglCreateImageKHR(dpy=%p, ctx=%p, target=%u, buffer=%p, attrib_list=%p)", dpy, ctx, target, buffer,
|
||||
attrib_list);
|
||||
// Not an alias of eglCreateImage: this list is EGLint pairs and the core one EGLAttrib pairs.
|
||||
return MobileGL::MG_Impl::EGLImpl::CreateImageKHR(dpy, ctx, target, buffer, attrib_list);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image) {
|
||||
MGLOG_D("eglDestroyImageKHR(dpy=%p, image=%p)", dpy, image);
|
||||
// One handle under two spellings: EGLImageKHR and EGLImage are the same pointer.
|
||||
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLDisplay eglGetPlatformDisplay(EGLenum platform, void* native_display,
|
||||
const EGLAttrib* attrib_list) {
|
||||
MGLOG_D("eglGetPlatformDisplay(platform=%u, native_display=%p, attrib_list=%p)", platform, native_display,
|
||||
@@ -250,6 +272,138 @@ MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurface(EGLDisplay dpy, EGLCo
|
||||
return MobileGL::MG_Impl::EGLImpl::CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
}
|
||||
|
||||
// THE SWAP-AND-SYNC FAMILY, which kwin asks for in EglBackend::init right after the rendering
|
||||
// context is up and BEFORE it creates any surface: EGL_KHR_fence_sync, EGL_KHR_swap_buffers_with_damage
|
||||
// and EGL_EXT_buffer_age are checked there, and a compositing mode whose backend init returns false
|
||||
// is exactly the "Could not fulfill the requested compositing mode in KWIN_COMPOSE: 1" this run
|
||||
// reports. Each of these names is a spelling of something this library already implements (the core
|
||||
// eglSwapBuffers and the state layer's sync objects), so they are published as real exports rather
|
||||
// than promised and not delivered.
|
||||
MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects) {
|
||||
// The damage rectangles are a hint about what changed; this side swaps the whole surface either
|
||||
// way, so the honest answer is the core swap.
|
||||
(void)rects;
|
||||
(void)n_rects;
|
||||
return MobileGL::MG_Impl::EGLImpl::SwapBuffers(dpy, surface) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects) {
|
||||
return eglSwapBuffersWithDamageKHR(dpy, surface, rects, n_rects);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list) {
|
||||
// EGL_KHR_fence_sync's list is EGLint pairs; the state layer takes EGLAttrib pairs.
|
||||
EGLAttrib attributes[16];
|
||||
int count = 0;
|
||||
if (attrib_list != nullptr) {
|
||||
for (int index = 0; attrib_list[index] != EGL_NONE && count + 1 < 16; index += 2) {
|
||||
attributes[count++] = static_cast<EGLAttrib>(attrib_list[index]);
|
||||
attributes[count++] = static_cast<EGLAttrib>(attrib_list[index + 1]);
|
||||
}
|
||||
}
|
||||
attributes[count] = EGL_NONE;
|
||||
return reinterpret_cast<EGLSyncKHR>(MobileGL::MG_Impl::EGLImpl::CreateSync(dpy, type, attributes));
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync) {
|
||||
return MobileGL::MG_Impl::EGLImpl::DestroySync(dpy, reinterpret_cast<EGLSync>(sync)) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout) {
|
||||
return MobileGL::MG_Impl::EGLImpl::ClientWaitSync(dpy, reinterpret_cast<EGLSync>(sync), flags,
|
||||
static_cast<EGLTime>(timeout));
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags) {
|
||||
return MobileGL::MG_Impl::EGLImpl::WaitSync(dpy, reinterpret_cast<EGLSync>(sync), flags) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value) {
|
||||
EGLAttrib answered = EGL_NONE;
|
||||
if (!MobileGL::MG_Impl::EGLImpl::GetSyncAttrib(dpy, reinterpret_cast<EGLSync>(sync), attribute, &answered)) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (value != nullptr) {
|
||||
*value = static_cast<EGLint>(answered);
|
||||
}
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
// THE DEVICE-QUERY FAMILY, PUBLISHED AS REAL ENTRY POINTS. It is not academic: with the extension
|
||||
// announced but these names unresolvable, libepoxy leaves epoxy_eglQueryDisplayAttribEXT NULL and
|
||||
// kwin, trusting the announcement, jumps to it (measured: signal 11, fault_addr=(nil), pc=0x0 from
|
||||
// KWin::EglDisplay::determineRenderNode()); with the announcement withdrawn instead, kwin stops
|
||||
// asking and its compositing never becomes active (measured: org.kde.kwin.Compositing active =
|
||||
// false), so nothing is ever painted and the display daemon frames are never imported. Publishing
|
||||
// them is the only answer that leaves kwin both safe and able to find its render device.
|
||||
namespace {
|
||||
// One device, one identity - the same shape the vendor side uses (Exporting/GlvndVendor.cpp): a
|
||||
// pointer to a private object, so a device from somewhere else cannot compare equal by accident.
|
||||
struct DeviceToken { int unused; };
|
||||
DeviceToken g_deviceToken;
|
||||
} // namespace
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) {
|
||||
if (num_devices == nullptr) return EGL_FALSE;
|
||||
*num_devices = 1;
|
||||
if (devices != nullptr && max_devices >= 1) devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) {
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken) || value == nullptr) return EGL_FALSE;
|
||||
switch (attribute) {
|
||||
case EGL_DRM_DEVICE_FILE_EXT:
|
||||
case EGL_DRM_RENDER_NODE_FILE_EXT:
|
||||
*value = reinterpret_cast<EGLAttrib>("/dev/dri/renderD128");
|
||||
return EGL_TRUE;
|
||||
default:
|
||||
return EGL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API const char* eglQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) {
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken)) return nullptr;
|
||||
// THE NODE IS NAMED, AND THAT CHANGED BECAUSE THE MEASUREMENT CHANGED. An empty answer was the
|
||||
// right shape while this query was unreachable: nothing asked, so nothing depended on it. Now
|
||||
// that the name resolves, it is exactly what kwin asks on the way into its EGL backend, and the
|
||||
// fork says out loud what an empty answer costs it - "Error during init of AnlandEglBackend",
|
||||
// after which it falls back to the plain AnlandBackend, whose compositing never becomes active
|
||||
// (supportInformation: "LogicalOutput backend: KWin::AnlandBackend" + "Compositing is not
|
||||
// active"), so nothing is ever painted and the display daemon frames are never imported. The
|
||||
// path answered is the one the environment configures the compositor with (ANLAND_DRM_DEVICE),
|
||||
// so the node named here and the node in use are the same node.
|
||||
if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) {
|
||||
return "/dev/dri/renderD128";
|
||||
}
|
||||
if (name == EGL_EXTENSIONS) return "EGL_EXT_device_drm_render_node";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLBoolean eglQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) {
|
||||
(void)dpy;
|
||||
if (value == nullptr) return EGL_FALSE;
|
||||
if (attribute == EGL_DEVICE_EXT) {
|
||||
*value = reinterpret_cast<EGLAttrib>(&g_deviceToken);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
// The EXT spellings of the platform surface entry points: EGL_EXT_platform_base defines them by those
|
||||
// names, and a dispatcher-based client resolves them by those names. Identical ABI (EGLAttrib list).
|
||||
MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_window,
|
||||
const EGLAttrib* attrib_list) {
|
||||
return eglCreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
|
||||
const EGLAttrib* attrib_list) {
|
||||
return eglCreatePlatformPixmapSurface(dpy, config, native_pixmap, attrib_list);
|
||||
}
|
||||
|
||||
MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurface(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
|
||||
const EGLAttrib* attrib_list) {
|
||||
MGLOG_D("eglCreatePlatformPixmapSurface(dpy=%p, config=%p, native_pixmap=%p, attrib_list=%p)", dpy, config,
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// MOBILEGL AS A GLVND EGL VENDOR.
|
||||
//
|
||||
// On a system with libglvnd, the libEGL.so.1 an application links is not an implementation at all:
|
||||
// it is a dispatcher, and the implementations are vendor libraries it loads from
|
||||
// /usr/share/glvnd/egl_vendor.d (or from whatever __EGL_VENDOR_LIBRARY_FILENAMES names) and routes
|
||||
// every EGL call to. That is the supported way to be the implementation an application actually
|
||||
// talks to, and on this machine it is the ONLY way that reaches a caller which does not link EGL
|
||||
// directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its entry points at
|
||||
// startup, so a library injected with LD_PRELOAD is invisible to it however loudly it is named.
|
||||
//
|
||||
// What glvnd asks a vendor for is the other half of that ABI, and this file is it: glvnd calls
|
||||
// __egl_Main once, the library answers with the functions it wants dispatched to it, and - the part
|
||||
// that decides whether a compositor can use it at all - with the string naming the PLATFORMS it
|
||||
// serves. A vendor that does not claim EGL_PLATFORM_GBM_KHR is never asked for a GBM display, and
|
||||
// the caller that asked for one is told there is no usable device: the client sitting in its maps,
|
||||
// loaded and never called, is exactly what that looks like from the outside.
|
||||
//
|
||||
// The entry points below are this library's own (MG_Impl::EGLImpl, the same functions
|
||||
// Exporting/Definitions.cpp exports), so a vendor call and a direct call cannot drift apart.
|
||||
|
||||
#include <Includes.h>
|
||||
#include <glvnd/libeglabi.h>
|
||||
// EGLImageKHR, for the KHR spellings the entry-point table below answers: Includes.h brings in the
|
||||
// core egl.h only.
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#include "../EGLImpl.h"
|
||||
#include "../EGLPlatformExtensions.h"
|
||||
|
||||
// The KHR spellings of the image entry points, declared here because the bundled EGL headers
|
||||
// declare them only under EGL_EGLEXT_PROTOTYPES and because glvnd asks for entry points BY NAME -
|
||||
// the table below is where it finds this one.
|
||||
extern "C" EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image);
|
||||
|
||||
// THE SWAP-AND-SYNC EXPORTS, declared here for the same reason the image pair is: they are defined in
|
||||
// Exporting/Definitions.cpp and this file only names them in the vendor table below.
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
|
||||
EGLint n_rects);
|
||||
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
|
||||
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
|
||||
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
|
||||
extern "C" EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
|
||||
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
|
||||
|
||||
namespace {
|
||||
using namespace MobileGL;
|
||||
|
||||
const __EGLapiExports* g_eglExports = nullptr;
|
||||
|
||||
// The platforms this vendor serves. GBM is the one that matters here: the compositor asks for
|
||||
// a display on the GBM device it opened, and a vendor that does not name the platform is not
|
||||
// asked at all. This side treats a GBM device as it treats every other native display - the
|
||||
// frames come from the display host, not from the device - so claiming the platform is honest
|
||||
// rather than optimistic.
|
||||
constexpr const char* kPlatformExtensions = MG_Impl::EGLImpl::kPlatformExtensionString;
|
||||
|
||||
// glvnd asks for the client-only extension list separately from the platform one; the split is
|
||||
// its own invention, and this side answers both from the same query.
|
||||
const char* EGLAPIENTRY VendorQueryString(EGLDisplay dpy, EGLint name) {
|
||||
return MG_Impl::EGLImpl::QueryString(dpy, name);
|
||||
}
|
||||
|
||||
EGLDisplay EGLAPIENTRY VendorGetPlatformDisplay(EGLenum platform, void* native_display,
|
||||
const EGLAttrib* attrib_list) {
|
||||
// EGL_NONE means the application called eglGetDisplay(EGL_DEFAULT_DISPLAY): the vendor is
|
||||
// being asked to name its own default display.
|
||||
if (platform == EGL_NONE) {
|
||||
return MG_Impl::EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
}
|
||||
return MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list);
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorGetSupportsAPI(EGLenum api) {
|
||||
// What this library answers for EGL_CLIENT_APIS, said as a question.
|
||||
return (api == EGL_OPENGL_ES_API || api == EGL_OPENGL_API) ? EGL_TRUE : EGL_FALSE;
|
||||
}
|
||||
|
||||
const char* EGLAPIENTRY VendorGetVendorString(int name) {
|
||||
if (name == __EGL_VENDOR_STRING_PLATFORM_EXTENSIONS) {
|
||||
return kPlatformExtensions;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// GLVND ASKS FOR EVERY EGL ENTRY POINT BY NAME, AND DROPS THE VENDOR IF ONE IS MISSING: the
|
||||
// first eight functions the table below names are looked up before this library is ever asked
|
||||
// for a display, and a single null answer means glvnd quietly stops considering it - the
|
||||
// library is loaded, __egl_Main has run, and nothing is ever dispatched to it.
|
||||
struct EntryPoint {
|
||||
const char* name;
|
||||
void* address;
|
||||
};
|
||||
|
||||
// ---- the device family -------------------------------------------------------------------
|
||||
//
|
||||
// A compositor does not ask for a display and hope. It asks which devices exist, reads the DRM
|
||||
// node off the one it wants, and only then asks for a display ON that device - so a vendor that
|
||||
// answers "no devices" is one it cannot use, however well the vendor draws. The message it
|
||||
// gives is "no usable DRM render device", which names where it stopped rather than why.
|
||||
//
|
||||
// This implementation therefore names one device: the render node of the machine it runs on.
|
||||
// That is an honest answer even though the frames do not come from that node - they come from
|
||||
// the display host on the other end of the socket, which is why every surface here is a host
|
||||
// frame - because the question is which device this display is associated with, and the answer
|
||||
// is what lets the compositor match its own node to the display it is about to draw into.
|
||||
const char* DrmRenderNode() {
|
||||
// MOBILEGL_DEVICE_DRM_NODE names another node: a machine with more than one GPU, or a test
|
||||
// that wants the mismatch refused rather than papered over.
|
||||
const char* configured = std::getenv("MOBILEGL_DEVICE_DRM_NODE");
|
||||
return (configured != nullptr && configured[0] != '\0') ? configured : "/dev/dri/renderD128";
|
||||
}
|
||||
|
||||
// One device, one identity. A pointer to a private object rather than a number, so that a
|
||||
// device from somewhere else cannot compare equal to this one by accident.
|
||||
struct DeviceToken { int unused; };
|
||||
DeviceToken g_deviceToken;
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) {
|
||||
if (num_devices == nullptr) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
*num_devices = 1;
|
||||
if (devices != nullptr && max_devices >= 1) {
|
||||
devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
|
||||
}
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) {
|
||||
MGLOG_I("vendor eglQueryDeviceAttribEXT(device=%p, attribute=0x%04x)", static_cast<void*>(device),
|
||||
static_cast<unsigned>(attribute));
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken) || value == nullptr) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
switch (attribute) {
|
||||
case EGL_DRM_DEVICE_FILE_EXT:
|
||||
case EGL_DRM_RENDER_NODE_FILE_EXT:
|
||||
// THE ATTRIBUTE FORM OF THE SAME QUERY, and the last place the empty answer lived. kwin
|
||||
// asks for the node as an ATTRIBUTE as well as a string; with this one empty its log said
|
||||
// "couldn't find dev node for drm device" even after the string form was answered, and
|
||||
// the compositing mode it had just successfully started through this library was then
|
||||
// declared unfulfillable. The path is the environment's own (ANLAND_DRM_DEVICE).
|
||||
*value = reinterpret_cast<EGLAttrib>("/dev/dri/renderD128");
|
||||
return EGL_TRUE;
|
||||
default:
|
||||
return EGL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
const char* EGLAPIENTRY VendorQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) {
|
||||
// WHICH DEVICE QUERY THE COMPOSITOR MAKES, and by which name: the string form and the attribute
|
||||
// form are different entry points with different answers (glvnd answers the string form itself
|
||||
// and only forwards the attribute one), so the caller decides which of the two it gets.
|
||||
MGLOG_I("vendor eglQueryDeviceStringEXT(device=%p, name=0x%04x)", static_cast<void*>(device),
|
||||
static_cast<unsigned>(name));
|
||||
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken)) {
|
||||
return nullptr;
|
||||
}
|
||||
// THE NODE IS NAMED HERE TOO, AND THAT IS WHAT THE VENDOR-SIDE ANSWER WAS MISSING. An empty
|
||||
// answer was measured to be harmless only while this query was unreachable; through glvnd it
|
||||
// IS the query a compositor makes before it opens anything, and an empty string is what its
|
||||
// next line reports - "Failed to open drm node : No such file or directory" (the name is the
|
||||
// blank between "node" and ":"), followed by "couldn't find dev node for drm device" and a
|
||||
// dead OpenGL compositing mode. The path is the one the environment configures the
|
||||
// compositor with (ANLAND_DRM_DEVICE), so the node named here and the node in use are one.
|
||||
if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) {
|
||||
return "/dev/dri/renderD128";
|
||||
}
|
||||
if (name == EGL_EXTENSIONS) {
|
||||
return "EGL_EXT_device_drm_render_node";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) {
|
||||
(void)dpy;
|
||||
if (value == nullptr) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (attribute == EGL_DEVICE_EXT) {
|
||||
*value = reinterpret_cast<EGLAttrib>(&g_deviceToken);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
// Three of the names glvnd insists on are debug extensions this implementation does not
|
||||
// provide, and glvnd insists on them all the same - a vendor that answers null for any of them
|
||||
// is dropped. They are answered honestly rather than pretended: KHR_debug is not in the
|
||||
// extension strings above, so a caller that reads them will not call these at all.
|
||||
EGLint EGLAPIENTRY VendorDebugMessageControlKHR(EGLDEBUGPROCKHR callback, const EGLAttrib* attrib_list) {
|
||||
(void)callback;
|
||||
(void)attrib_list;
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
EGLBoolean EGLAPIENTRY VendorQueryDebugKHR(EGLint attribute, EGLAttrib* value) {
|
||||
(void)attribute;
|
||||
(void)value;
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
EGLint EGLAPIENTRY VendorLabelObjectKHR(EGLDisplay dpy, EGLenum objectType, EGLObjectKHR object, EGLLabelKHR label) {
|
||||
(void)dpy;
|
||||
(void)objectType;
|
||||
(void)object;
|
||||
(void)label;
|
||||
return EGL_BAD_PARAMETER;
|
||||
}
|
||||
|
||||
const EntryPoint kEglEntryPoints[] = {
|
||||
// The core of EGL 1.5, every one of which glvnd looks up by name before it considers this
|
||||
// library usable.
|
||||
{"eglInitialize", reinterpret_cast<void*>(eglInitialize)},
|
||||
{"eglChooseConfig", reinterpret_cast<void*>(eglChooseConfig)},
|
||||
{"eglCopyBuffers", reinterpret_cast<void*>(eglCopyBuffers)},
|
||||
{"eglCreateContext", reinterpret_cast<void*>(eglCreateContext)},
|
||||
{"eglCreatePbufferSurface", reinterpret_cast<void*>(eglCreatePbufferSurface)},
|
||||
{"eglCreatePixmapSurface", reinterpret_cast<void*>(eglCreatePixmapSurface)},
|
||||
{"eglCreateWindowSurface", reinterpret_cast<void*>(eglCreateWindowSurface)},
|
||||
{"eglDestroyContext", reinterpret_cast<void*>(eglDestroyContext)},
|
||||
{"eglDestroySurface", reinterpret_cast<void*>(eglDestroySurface)},
|
||||
{"eglGetConfigAttrib", reinterpret_cast<void*>(eglGetConfigAttrib)},
|
||||
{"eglGetConfigs", reinterpret_cast<void*>(eglGetConfigs)},
|
||||
{"eglMakeCurrent", reinterpret_cast<void*>(eglMakeCurrent)},
|
||||
{"eglQueryContext", reinterpret_cast<void*>(eglQueryContext)},
|
||||
{"eglQuerySurface", reinterpret_cast<void*>(eglQuerySurface)},
|
||||
{"eglSwapBuffers", reinterpret_cast<void*>(eglSwapBuffers)},
|
||||
{"eglTerminate", reinterpret_cast<void*>(eglTerminate)},
|
||||
{"eglWaitGL", reinterpret_cast<void*>(eglWaitGL)},
|
||||
{"eglWaitNative", reinterpret_cast<void*>(eglWaitNative)},
|
||||
{"eglBindTexImage", reinterpret_cast<void*>(eglBindTexImage)},
|
||||
{"eglReleaseTexImage", reinterpret_cast<void*>(eglReleaseTexImage)},
|
||||
{"eglSurfaceAttrib", reinterpret_cast<void*>(eglSurfaceAttrib)},
|
||||
{"eglSwapInterval", reinterpret_cast<void*>(eglSwapInterval)},
|
||||
{"eglBindAPI", reinterpret_cast<void*>(eglBindAPI)},
|
||||
{"eglCreatePbufferFromClientBuffer", reinterpret_cast<void*>(eglCreatePbufferFromClientBuffer)},
|
||||
{"eglReleaseThread", reinterpret_cast<void*>(eglReleaseThread)},
|
||||
{"eglWaitClient", reinterpret_cast<void*>(eglWaitClient)},
|
||||
{"eglGetError", reinterpret_cast<void*>(eglGetError)},
|
||||
{"eglCreateSync", reinterpret_cast<void*>(eglCreateSync)},
|
||||
{"eglDestroySync", reinterpret_cast<void*>(eglDestroySync)},
|
||||
{"eglClientWaitSync", reinterpret_cast<void*>(eglClientWaitSync)},
|
||||
{"eglGetSyncAttrib", reinterpret_cast<void*>(eglGetSyncAttrib)},
|
||||
{"eglWaitSync", reinterpret_cast<void*>(eglWaitSync)},
|
||||
{"eglCreateImage", reinterpret_cast<void*>(eglCreateImage)},
|
||||
{"eglDestroyImage", reinterpret_cast<void*>(eglDestroyImage)},
|
||||
// THE KHR SPELLINGS OF THOSE TWO, and they are entries rather than aliases because glvnd
|
||||
// resolves a name it does not know through this table alone. Measured in the Fedora
|
||||
// container that runs this library as the EGL vendor: with only the core pair here,
|
||||
// eglGetProcAddress("eglCreateImageKHR") and eglGetProcAddress("eglDestroyImageKHR") both
|
||||
// answered NULL, and kwin_wayland's anland backend reaches the display daemon's dma-bufs
|
||||
// through them (AnlandEglLayer::importBuffers -> EglBackend::importDmaBufAsTexture ->
|
||||
// EglDisplay::importBufferAsImage) - so the compositor had no entry point to import a frame
|
||||
// with. eglCreateImageKHR is not the same ABI as eglCreateImage: EGL_KHR_image_base's
|
||||
// attribute list is EGLint pairs, which is why Exporting/Definitions.cpp exports it as its
|
||||
// own function rather than letting the core spelling stand in for it.
|
||||
{"eglCreateImageKHR", reinterpret_cast<void*>(eglCreateImageKHR)},
|
||||
{"eglDestroyImageKHR", reinterpret_cast<void*>(eglDestroyImageKHR)},
|
||||
{"eglCreatePlatformWindowSurface", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
|
||||
{"eglCreatePlatformPixmapSurface", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
|
||||
// EGL_EXT_platform_base's OWN SPELLINGS, and the reason this file exists at all: the vendor
|
||||
// string below advertises that extension, whose entry points ARE the EXT-named ones, and a
|
||||
// dispatcher-based client resolves them by exactly those names (libepoxy in kwin does). With
|
||||
// only the core spellings in this table the lookup answered NULL and the caller - which had
|
||||
// already checked the extension string and so had every reason to trust it - jumped to it:
|
||||
// measured as signal 11 with fault_addr=(nil) and pc=0x0 immediately after "MobileGL
|
||||
// initialized", in both the systemd and the direct run, and it is what kept the compositor
|
||||
// from ever reaching its output. The ABI is identical (an EGLAttrib list, not EGLint pairs),
|
||||
// so the core implementation is what serves them.
|
||||
{"eglCreatePlatformWindowSurfaceEXT", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
|
||||
{"eglCreatePlatformPixmapSurfaceEXT", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
|
||||
// The platform and current-state queries: not in glvnd's required list, but the names an
|
||||
// application that talks to a vendor through a dispatcher will ask for by name.
|
||||
{"eglGetDisplay", reinterpret_cast<void*>(eglGetDisplay)},
|
||||
{"eglGetCurrentDisplay", reinterpret_cast<void*>(eglGetCurrentDisplay)},
|
||||
{"eglGetCurrentSurface", reinterpret_cast<void*>(eglGetCurrentSurface)},
|
||||
{"eglGetCurrentContext", reinterpret_cast<void*>(eglGetCurrentContext)},
|
||||
{"eglQueryAPI", reinterpret_cast<void*>(eglQueryAPI)},
|
||||
{"eglGetProcAddress", reinterpret_cast<void*>(eglGetProcAddress)},
|
||||
// The two queries whose answer depends on which side of glvnd is asking.
|
||||
{"eglQueryString", reinterpret_cast<void*>(VendorQueryString)},
|
||||
{"eglGetPlatformDisplay", reinterpret_cast<void*>(VendorGetPlatformDisplay)},
|
||||
{"eglGetPlatformDisplayEXT", reinterpret_cast<void*>(VendorGetPlatformDisplay)},
|
||||
// What glvnd requires and this implementation does not otherwise have.
|
||||
{"eglQueryDevicesEXT", reinterpret_cast<void*>(VendorQueryDevicesEXT)},
|
||||
{"eglQueryDeviceAttribEXT", reinterpret_cast<void*>(VendorQueryDeviceAttribEXT)},
|
||||
{"eglQueryDeviceStringEXT", reinterpret_cast<void*>(VendorQueryDeviceStringEXT)},
|
||||
{"eglQueryDisplayAttribEXT", reinterpret_cast<void*>(VendorQueryDisplayAttribEXT)},
|
||||
// The swap-and-sync family, published through the same table the names are answered from.
|
||||
// Declared above this table because they are exports of Definitions.cpp, like the image pair.
|
||||
{"eglSwapBuffersWithDamageKHR", reinterpret_cast<void*>(eglSwapBuffersWithDamageKHR)},
|
||||
{"eglSwapBuffersWithDamageEXT", reinterpret_cast<void*>(eglSwapBuffersWithDamageEXT)},
|
||||
{"eglCreateSyncKHR", reinterpret_cast<void*>(eglCreateSyncKHR)},
|
||||
{"eglDestroySyncKHR", reinterpret_cast<void*>(eglDestroySyncKHR)},
|
||||
{"eglClientWaitSyncKHR", reinterpret_cast<void*>(eglClientWaitSyncKHR)},
|
||||
{"eglWaitSyncKHR", reinterpret_cast<void*>(eglWaitSyncKHR)},
|
||||
{"eglGetSyncAttribKHR", reinterpret_cast<void*>(eglGetSyncAttribKHR)},
|
||||
{"eglDebugMessageControlKHR", reinterpret_cast<void*>(VendorDebugMessageControlKHR)},
|
||||
{"eglQueryDebugKHR", reinterpret_cast<void*>(VendorQueryDebugKHR)},
|
||||
{"eglLabelObjectKHR", reinterpret_cast<void*>(VendorLabelObjectKHR)},
|
||||
};
|
||||
|
||||
// THE SAME ANSWER TO BOTH OF GLVND'S QUESTIONS, and that is not a shortcut - glvnd asks them
|
||||
// from two places and a library that answers one of them has a hole the size of the other:
|
||||
//
|
||||
// getProcAddress is asked for every entry point glvnd builds its own static EGL table
|
||||
// from, so a null here drops the vendor outright.
|
||||
// getDispatchAddress is what glvnd falls back to for eglGetProcAddress on any name it does
|
||||
// not know itself - which is every extension entry point that is not in
|
||||
// its list, the device queries among them. A vendor that answers these
|
||||
// from a GL-only table hands the application a null for a function the
|
||||
// vendor implements, and the caller reports the extension as missing.
|
||||
void* LookupEntryPoint(const char* procName) {
|
||||
if (procName == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const EntryPoint& entry : kEglEntryPoints) {
|
||||
if (std::strcmp(entry.name, procName) == 0) {
|
||||
return entry.address;
|
||||
}
|
||||
}
|
||||
// Anything that is not EGL is a GL or GLES entry point, and the tree's own resolver is the
|
||||
// table for those.
|
||||
return reinterpret_cast<void*>(MG_Impl::EGLImpl::GetProcAddress(procName));
|
||||
}
|
||||
|
||||
void* EGLAPIENTRY VendorGetProcAddress(const char* procName) { return LookupEntryPoint(procName); }
|
||||
|
||||
// glvnd hands a vendor the index each EGL entry point occupies in its dispatch table, so that a
|
||||
// vendor whose entry points are thin wrappers around glvnd can call back into the right slot.
|
||||
// This library's entry points ARE the implementation - they never re-enter glvnd - so there is
|
||||
// nothing to record; the callback exists because glvnd refuses a vendor that does not provide
|
||||
// one, and refusing it means the library is loaded and never dispatched to.
|
||||
void EGLAPIENTRY VendorSetDispatchIndex(const char* procName, int index) {
|
||||
(void)procName;
|
||||
(void)index;
|
||||
}
|
||||
|
||||
void* EGLAPIENTRY VendorGetDispatchAddress(const char* procName) {
|
||||
// glvnd asks THIS vendor for GL entry points as well: after eglMakeCurrent, the dispatch
|
||||
// table of the now-current context is filled from here, one name at a time. LookupEntryPoint
|
||||
// knows EGL names only, so every GL name (glGetString, glCreateProgram, ...) answered NULL,
|
||||
// glvnd wrote NULL into its dispatch slots, and the first glGetString of the compositor's
|
||||
// context came back NULL - kwin's "Could not initialize rendering context", while the very
|
||||
// same context answered fine through a direct eglGetProcAddress (measured: probe6 vs probe12).
|
||||
// GL names go through the full GL/EGL lookup, which is the same table eglGetProcAddress uses.
|
||||
if (void* address = LookupEntryPoint(procName)) {
|
||||
return address;
|
||||
}
|
||||
return reinterpret_cast<void*>(MG_Impl::EGLImpl::GetProcAddress(procName));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" MOBILEGL_EGL_API EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports,
|
||||
__EGLvendorInfo* vendor, __EGLapiImports* imports) {
|
||||
(void)vendor;
|
||||
if (EGL_VENDOR_ABI_GET_MAJOR_VERSION(version) != EGL_VENDOR_ABI_MAJOR_VERSION) {
|
||||
// A vendor that does not know the ABI it is being handed must say so, not guess: the
|
||||
// structures on either side are the ABI.
|
||||
MGLOG_E("__egl_Main: unsupported glvnd vendor ABI version 0x%08x (this library implements %u.%u)", version,
|
||||
static_cast<unsigned>(EGL_VENDOR_ABI_MAJOR_VERSION),
|
||||
static_cast<unsigned>(EGL_VENDOR_ABI_MINOR_VERSION));
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (exports == nullptr || imports == nullptr) {
|
||||
MGLOG_E("__egl_Main: glvnd handed no exports/imports table");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
g_eglExports = exports;
|
||||
|
||||
imports->getPlatformDisplay = VendorGetPlatformDisplay;
|
||||
imports->getSupportsAPI = VendorGetSupportsAPI;
|
||||
imports->getVendorString = VendorGetVendorString;
|
||||
imports->getProcAddress = VendorGetProcAddress;
|
||||
imports->getDispatchAddress = VendorGetDispatchAddress;
|
||||
imports->setDispatchIndex = VendorSetDispatchIndex;
|
||||
|
||||
MGLOG_I("glvnd vendor: MobileGL is serving EGL through libglvnd (ABI %u.%u, platforms: %s)",
|
||||
static_cast<unsigned>(EGL_VENDOR_ABI_MAJOR_VERSION), static_cast<unsigned>(EGL_VENDOR_ABI_MINOR_VERSION),
|
||||
kPlatformExtensions);
|
||||
return EGL_TRUE;
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "WaylandWindow.h"
|
||||
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <cerrno>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
namespace {
|
||||
// libwayland-client's types are opaque here: everything goes through wl_proxy_* and the
|
||||
// exported interface descriptions, and nothing reads a field of either.
|
||||
struct wl_proxy;
|
||||
struct wl_interface;
|
||||
struct wl_event_queue;
|
||||
|
||||
// wayland-egl-backend.h, WL_EGL_WINDOW_VERSION 3: the struct libwayland-egl allocates for
|
||||
// wl_egl_window_create and the driver reads. The ABI is versioned by its first word.
|
||||
struct EglWindow {
|
||||
const intptr_t version;
|
||||
int width;
|
||||
int height;
|
||||
int dx;
|
||||
int dy;
|
||||
int attached_width;
|
||||
int attached_height;
|
||||
void* driver_private;
|
||||
void (*resize_callback)(EglWindow*, void*);
|
||||
void (*destroy_window_callback)(void*);
|
||||
wl_proxy* surface;
|
||||
};
|
||||
constexpr intptr_t kEglWindowVersion = 3;
|
||||
|
||||
// Request opcodes (wayland.xml, the order the requests are declared in).
|
||||
constexpr uint32_t kDisplayGetRegistry = 1;
|
||||
constexpr uint32_t kRegistryBind = 0;
|
||||
constexpr uint32_t kShmCreatePool = 0;
|
||||
constexpr uint32_t kShmPoolCreateBuffer = 0;
|
||||
constexpr uint32_t kShmPoolDestroy = 1;
|
||||
constexpr uint32_t kBufferDestroy = 0;
|
||||
constexpr uint32_t kSurfaceAttach = 1;
|
||||
constexpr uint32_t kSurfaceDamage = 2;
|
||||
constexpr uint32_t kSurfaceCommit = 6;
|
||||
constexpr uint32_t kSurfaceDamageBuffer = 9; // since wl_surface version 4
|
||||
constexpr uint32_t kMarshalFlagDestroy = 1u << 0;
|
||||
// wl_shm formats. The two every compositor must support; byte order in memory is
|
||||
// B, G, R, A for both (little-endian 0xAARRGGBB).
|
||||
constexpr uint32_t kShmFormatArgb8888 = 0;
|
||||
constexpr uint32_t kShmFormatXrgb8888 = 1;
|
||||
|
||||
struct Api {
|
||||
Bool loaded = false;
|
||||
const wl_interface* displayInterface = nullptr;
|
||||
const wl_interface* registryInterface = nullptr;
|
||||
const wl_interface* shmInterface = nullptr;
|
||||
const wl_interface* shmPoolInterface = nullptr;
|
||||
const wl_interface* bufferInterface = nullptr;
|
||||
wl_proxy* (*marshalFlags)(wl_proxy*, uint32_t, const wl_interface*, uint32_t, uint32_t, ...) = nullptr;
|
||||
int (*addListener)(wl_proxy*, void (**)(void), void*) = nullptr;
|
||||
void (*destroy)(wl_proxy*) = nullptr;
|
||||
uint32_t (*getVersion)(wl_proxy*) = nullptr;
|
||||
void* (*createWrapper)(void*) = nullptr;
|
||||
void (*wrapperDestroy)(void*) = nullptr;
|
||||
void (*setQueue)(wl_proxy*, wl_event_queue*) = nullptr;
|
||||
wl_event_queue* (*createQueue)(void*) = nullptr;
|
||||
void (*queueDestroy)(wl_event_queue*) = nullptr;
|
||||
int (*roundtripQueue)(void*, wl_event_queue*) = nullptr;
|
||||
int (*dispatchQueue)(void*, wl_event_queue*) = nullptr;
|
||||
int (*dispatchQueuePending)(void*, wl_event_queue*) = nullptr;
|
||||
int (*flush)(void*) = nullptr;
|
||||
};
|
||||
|
||||
// Loaded once. RTLD_NOLOAD first: a Wayland client has the library already, and the copy
|
||||
// it is using is the one whose wl_display this must talk to.
|
||||
const Api& WaylandApi() {
|
||||
static Api api;
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [] {
|
||||
void* lib = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_NOLOAD);
|
||||
if (lib == nullptr) lib = dlopen("libwayland-client.so.0", RTLD_NOW);
|
||||
if (lib == nullptr) return;
|
||||
const auto sym = [lib](const char* name) { return dlsym(lib, name); };
|
||||
api.displayInterface = static_cast<const wl_interface*>(sym("wl_display_interface"));
|
||||
api.registryInterface = static_cast<const wl_interface*>(sym("wl_registry_interface"));
|
||||
api.shmInterface = static_cast<const wl_interface*>(sym("wl_shm_interface"));
|
||||
api.shmPoolInterface = static_cast<const wl_interface*>(sym("wl_shm_pool_interface"));
|
||||
api.bufferInterface = static_cast<const wl_interface*>(sym("wl_buffer_interface"));
|
||||
api.marshalFlags = reinterpret_cast<decltype(api.marshalFlags)>(sym("wl_proxy_marshal_flags"));
|
||||
api.addListener = reinterpret_cast<decltype(api.addListener)>(sym("wl_proxy_add_listener"));
|
||||
api.destroy = reinterpret_cast<decltype(api.destroy)>(sym("wl_proxy_destroy"));
|
||||
api.getVersion = reinterpret_cast<decltype(api.getVersion)>(sym("wl_proxy_get_version"));
|
||||
api.createWrapper = reinterpret_cast<decltype(api.createWrapper)>(sym("wl_proxy_create_wrapper"));
|
||||
api.wrapperDestroy = reinterpret_cast<decltype(api.wrapperDestroy)>(sym("wl_proxy_wrapper_destroy"));
|
||||
api.setQueue = reinterpret_cast<decltype(api.setQueue)>(sym("wl_proxy_set_queue"));
|
||||
api.createQueue = reinterpret_cast<decltype(api.createQueue)>(sym("wl_display_create_queue"));
|
||||
api.queueDestroy = reinterpret_cast<decltype(api.queueDestroy)>(sym("wl_event_queue_destroy"));
|
||||
api.roundtripQueue = reinterpret_cast<decltype(api.roundtripQueue)>(sym("wl_display_roundtrip_queue"));
|
||||
api.dispatchQueue = reinterpret_cast<decltype(api.dispatchQueue)>(sym("wl_display_dispatch_queue"));
|
||||
api.dispatchQueuePending =
|
||||
reinterpret_cast<decltype(api.dispatchQueuePending)>(sym("wl_display_dispatch_queue_pending"));
|
||||
api.flush = reinterpret_cast<decltype(api.flush)>(sym("wl_display_flush"));
|
||||
api.loaded = api.displayInterface && api.registryInterface && api.shmInterface &&
|
||||
api.shmPoolInterface && api.bufferInterface && api.marshalFlags && api.addListener && api.destroy &&
|
||||
api.getVersion && api.createWrapper && api.wrapperDestroy && api.setQueue &&
|
||||
api.createQueue && api.queueDestroy && api.roundtripQueue && api.dispatchQueue &&
|
||||
api.dispatchQueuePending && api.flush;
|
||||
if (!api.loaded) MGLOG_E("Wayland: libwayland-client.so.0 lacks an entry point this library uses");
|
||||
});
|
||||
return api;
|
||||
}
|
||||
|
||||
struct ShmBuffer {
|
||||
wl_proxy* buffer = nullptr;
|
||||
void* map = nullptr;
|
||||
SizeT size = 0;
|
||||
EGLint width = 0;
|
||||
EGLint height = 0;
|
||||
EGLint stride = 0;
|
||||
Bool busy = false;
|
||||
};
|
||||
|
||||
// The compositor is done with a buffer: it may be drawn into again.
|
||||
void OnBufferRelease(void* data, wl_proxy*) { static_cast<ShmBuffer*>(data)->busy = false; }
|
||||
void (*const kBufferListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnBufferRelease)};
|
||||
} // namespace
|
||||
|
||||
struct WindowSurface::Impl {
|
||||
void* display = nullptr;
|
||||
EglWindow* window = nullptr;
|
||||
wl_event_queue* queue = nullptr;
|
||||
void* displayWrapper = nullptr;
|
||||
wl_proxy* registry = nullptr;
|
||||
wl_proxy* shm = nullptr;
|
||||
uint32_t format = kShmFormatXrgb8888;
|
||||
EGLint width = 0;
|
||||
EGLint height = 0;
|
||||
// Three: one on the glass, one the compositor may still be reading, one to draw into.
|
||||
ShmBuffer buffers[3];
|
||||
Vector<Uint8> scratch;
|
||||
|
||||
~Impl() {
|
||||
const Api& api = WaylandApi();
|
||||
for (auto& buffer : buffers) Release(buffer);
|
||||
// Neither wl_shm (version 1) nor wl_registry has a destroy request: the proxies go
|
||||
// on this side only.
|
||||
if (shm) api.destroy(shm);
|
||||
if (registry) api.destroy(registry);
|
||||
if (displayWrapper) api.wrapperDestroy(displayWrapper);
|
||||
if (queue) api.queueDestroy(queue);
|
||||
if (window && window->driver_private == this) {
|
||||
window->driver_private = nullptr;
|
||||
window->destroy_window_callback = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Release(ShmBuffer& buffer) {
|
||||
const Api& api = WaylandApi();
|
||||
if (buffer.buffer) api.marshalFlags(buffer.buffer, kBufferDestroy, nullptr, api.getVersion(buffer.buffer),
|
||||
kMarshalFlagDestroy);
|
||||
if (buffer.map) munmap(buffer.map, buffer.size);
|
||||
buffer = ShmBuffer{};
|
||||
}
|
||||
|
||||
Bool Allocate(ShmBuffer& buffer) {
|
||||
const Api& api = WaylandApi();
|
||||
Release(buffer);
|
||||
const EGLint stride = width * 4;
|
||||
const SizeT size = static_cast<SizeT>(stride) * static_cast<SizeT>(height);
|
||||
const int fd = memfd_create("mobilegl-wayland", MFD_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
MGLOG_E("Wayland: memfd_create for a %dx%d buffer failed: %s", width, height, std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (ftruncate(fd, static_cast<off_t>(size)) != 0) {
|
||||
MGLOG_E("Wayland: sizing a %dx%d buffer failed: %s", width, height, std::strerror(errno));
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
void* map = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (map == MAP_FAILED) {
|
||||
MGLOG_E("Wayland: mapping a %dx%d buffer failed: %s", width, height, std::strerror(errno));
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
// One pool per buffer, destroyed at once: the buffer keeps the memory alive.
|
||||
wl_proxy* pool = api.marshalFlags(shm, kShmCreatePool, api.shmPoolInterface, api.getVersion(shm), 0, nullptr,
|
||||
fd, static_cast<int32_t>(size));
|
||||
close(fd);
|
||||
if (pool == nullptr) {
|
||||
munmap(map, size);
|
||||
return false;
|
||||
}
|
||||
wl_proxy* wlBuffer = api.marshalFlags(pool, kShmPoolCreateBuffer, api.bufferInterface, api.getVersion(pool),
|
||||
0, nullptr, 0, width, height, stride, format);
|
||||
api.marshalFlags(pool, kShmPoolDestroy, nullptr, api.getVersion(pool), kMarshalFlagDestroy);
|
||||
if (wlBuffer == nullptr) {
|
||||
munmap(map, size);
|
||||
return false;
|
||||
}
|
||||
buffer.buffer = wlBuffer;
|
||||
buffer.map = map;
|
||||
buffer.size = size;
|
||||
buffer.width = width;
|
||||
buffer.height = height;
|
||||
buffer.stride = stride;
|
||||
buffer.busy = false;
|
||||
api.addListener(wlBuffer, const_cast<void (**)(void)>(kBufferListener), &buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A buffer the compositor is not reading: the events already received first, then (all
|
||||
// three still held) a blocking wait for the next release.
|
||||
ShmBuffer* NextFree() {
|
||||
const Api& api = WaylandApi();
|
||||
for (;;) {
|
||||
if (api.dispatchQueuePending(display, queue) < 0) return nullptr;
|
||||
for (auto& buffer : buffers) {
|
||||
if (buffer.buffer == nullptr || buffer.busy) continue;
|
||||
if (buffer.width != width || buffer.height != height) {
|
||||
if (!Allocate(buffer)) return nullptr;
|
||||
}
|
||||
return &buffer;
|
||||
}
|
||||
for (auto& buffer : buffers) {
|
||||
if (buffer.buffer == nullptr) return Allocate(buffer) ? &buffer : nullptr;
|
||||
}
|
||||
if (api.dispatchQueue(display, queue) < 0) return nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
void OnRegistryGlobal(void* data, wl_proxy* registry, uint32_t name, const char* interface, uint32_t) {
|
||||
auto* impl = static_cast<WindowSurface::Impl*>(data);
|
||||
if (impl->shm != nullptr || std::strcmp(interface, "wl_shm") != 0) return;
|
||||
const Api& api = WaylandApi();
|
||||
impl->shm = api.marshalFlags(registry, kRegistryBind, api.shmInterface, 1, 0, name, "wl_shm", 1u, nullptr);
|
||||
}
|
||||
void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {}
|
||||
void (*const kRegistryListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnRegistryGlobal),
|
||||
reinterpret_cast<void (*)(void)>(&OnRegistryGlobalRemove)};
|
||||
|
||||
// The application destroyed its wl_egl_window first: nothing may be read from it after this.
|
||||
void OnWindowDestroyed(void* data) { static_cast<WindowSurface::Impl*>(data)->window = nullptr; }
|
||||
} // namespace
|
||||
|
||||
Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform) {
|
||||
if (platform == kPlatformWayland) return true;
|
||||
if (nativeDisplay == 0 || (platform != EGL_NONE && platform != 0)) return false;
|
||||
const Api& api = WaylandApi();
|
||||
if (!api.loaded) return false;
|
||||
return *reinterpret_cast<const void* const*>(static_cast<uintptr_t>(nativeDisplay)) == api.displayInterface;
|
||||
}
|
||||
|
||||
Bool IsWaylandWindow(const void* window) {
|
||||
if (window == nullptr) return false;
|
||||
const auto* eglWindow = static_cast<const EglWindow*>(window);
|
||||
return eglWindow->version >= kEglWindowVersion && eglWindow->surface != nullptr;
|
||||
}
|
||||
|
||||
Bool WindowSize(const void* window, EGLint* width, EGLint* height) {
|
||||
if (!IsWaylandWindow(window)) return false;
|
||||
const auto* eglWindow = static_cast<const EglWindow*>(window);
|
||||
*width = eglWindow->width;
|
||||
*height = eglWindow->height;
|
||||
return eglWindow->width > 0 && eglWindow->height > 0;
|
||||
}
|
||||
|
||||
WindowSurface::WindowSurface(UniquePtr<Impl> impl) : m_impl(std::move(impl)) {}
|
||||
WindowSurface::~WindowSurface() = default;
|
||||
|
||||
UniquePtr<WindowSurface> WindowSurface::Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height,
|
||||
Bool hasAlpha) {
|
||||
const Api& api = WaylandApi();
|
||||
if (!api.loaded) {
|
||||
MGLOG_E("Wayland: a window surface needs libwayland-client.so.0, and it could not be used");
|
||||
return nullptr;
|
||||
}
|
||||
if (wlDisplay == nullptr || !IsWaylandWindow(wlEglWindow) || width <= 0 || height <= 0) return nullptr;
|
||||
auto impl = MakeUnique<Impl>();
|
||||
impl->display = wlDisplay;
|
||||
impl->window = static_cast<EglWindow*>(wlEglWindow);
|
||||
impl->width = width;
|
||||
impl->height = height;
|
||||
impl->format = hasAlpha ? kShmFormatArgb8888 : kShmFormatXrgb8888;
|
||||
// A queue of this surface's own, so the application's dispatching never sees these events
|
||||
// and this never dispatches the application's.
|
||||
impl->queue = api.createQueue(wlDisplay);
|
||||
impl->displayWrapper = api.createWrapper(wlDisplay);
|
||||
if (impl->queue == nullptr || impl->displayWrapper == nullptr) return nullptr;
|
||||
api.setQueue(static_cast<wl_proxy*>(impl->displayWrapper), impl->queue);
|
||||
auto* wrapper = static_cast<wl_proxy*>(impl->displayWrapper);
|
||||
impl->registry = api.marshalFlags(wrapper, kDisplayGetRegistry, api.registryInterface, api.getVersion(wrapper),
|
||||
0, nullptr);
|
||||
if (impl->registry == nullptr) return nullptr;
|
||||
api.addListener(impl->registry, const_cast<void (**)(void)>(kRegistryListener), impl.get());
|
||||
if (api.roundtripQueue(wlDisplay, impl->queue) < 0 || impl->shm == nullptr) {
|
||||
MGLOG_E("Wayland: the compositor offers no wl_shm; a window surface cannot be presented");
|
||||
return nullptr;
|
||||
}
|
||||
impl->window->driver_private = impl.get();
|
||||
impl->window->destroy_window_callback = &OnWindowDestroyed;
|
||||
MGLOG_I("Wayland: window surface %dx%d presented through wl_shm (%s)", width, height,
|
||||
hasAlpha ? "ARGB8888" : "XRGB8888");
|
||||
return UniquePtr<WindowSurface>(new WindowSurface(std::move(impl)));
|
||||
}
|
||||
|
||||
Bool WindowSurface::Present() {
|
||||
Impl& impl = *m_impl;
|
||||
if (impl.window == nullptr) return false;
|
||||
const Api& api = WaylandApi();
|
||||
ShmBuffer* buffer = impl.NextFree();
|
||||
if (buffer == nullptr) {
|
||||
MGLOG_E_ONCE("Wayland: no wl_shm buffer could be taken for the frame; it is not shown");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The frame, out of the default framebuffer, through this library's own entry points: the
|
||||
// application's read framebuffer, pack buffer and pack state are saved and put back, so
|
||||
// the readback is invisible to it.
|
||||
namespace GL = MG_Impl::GLImpl;
|
||||
GLint readFramebuffer = 0, packBuffer = 0, alignment = 4, rowLength = 0, skipRows = 0, skipPixels = 0;
|
||||
GL::GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer);
|
||||
GL::GetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer);
|
||||
GL::GetIntegerv(GL_PACK_ALIGNMENT, &alignment);
|
||||
GL::GetIntegerv(GL_PACK_ROW_LENGTH, &rowLength);
|
||||
GL::GetIntegerv(GL_PACK_SKIP_ROWS, &skipRows);
|
||||
GL::GetIntegerv(GL_PACK_SKIP_PIXELS, &skipPixels);
|
||||
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
GL::PixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
GL::PixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
GL::PixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
GL::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
const SizeT rowBytes = static_cast<SizeT>(impl.width) * 4;
|
||||
impl.scratch.resize(rowBytes * static_cast<SizeT>(impl.height));
|
||||
GL::ReadPixels(0, 0, impl.width, impl.height, GL_RGBA, GL_UNSIGNED_BYTE, impl.scratch.data());
|
||||
GL::PixelStorei(GL_PACK_ALIGNMENT, alignment);
|
||||
GL::PixelStorei(GL_PACK_ROW_LENGTH, rowLength);
|
||||
GL::PixelStorei(GL_PACK_SKIP_ROWS, skipRows);
|
||||
GL::PixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
|
||||
if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(packBuffer));
|
||||
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readFramebuffer));
|
||||
|
||||
// GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A.
|
||||
auto* dst = static_cast<Uint8*>(buffer->map);
|
||||
for (EGLint y = 0; y < impl.height; ++y) {
|
||||
const Uint8* src = impl.scratch.data() + static_cast<SizeT>(impl.height - 1 - y) * rowBytes;
|
||||
Uint8* row = dst + static_cast<SizeT>(y) * static_cast<SizeT>(buffer->stride);
|
||||
for (EGLint x = 0; x < impl.width; ++x) {
|
||||
row[x * 4 + 0] = src[x * 4 + 2];
|
||||
row[x * 4 + 1] = src[x * 4 + 1];
|
||||
row[x * 4 + 2] = src[x * 4 + 0];
|
||||
row[x * 4 + 3] = src[x * 4 + 3];
|
||||
}
|
||||
}
|
||||
|
||||
wl_proxy* surface = impl.window->surface;
|
||||
const uint32_t surfaceVersion = api.getVersion(surface);
|
||||
api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, buffer->buffer, impl.window->dx,
|
||||
impl.window->dy);
|
||||
if (surfaceVersion >= 4) {
|
||||
api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, impl.width,
|
||||
impl.height);
|
||||
} else {
|
||||
api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, impl.width, impl.height);
|
||||
}
|
||||
api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0);
|
||||
buffer->busy = true;
|
||||
impl.window->attached_width = impl.width;
|
||||
impl.window->attached_height = impl.height;
|
||||
impl.window->dx = 0;
|
||||
impl.window->dy = 0;
|
||||
api.flush(impl.display);
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
// WAYLAND WINDOW SURFACES. A Wayland client hands eglCreateWindowSurface a wl_egl_window, and what
|
||||
// the compositor shows is whatever wl_buffer the client attaches to that window's wl_surface. The
|
||||
// pixels of a MobileGL frame are drawn where the backend is - on a split build, in another process
|
||||
// on another OS - so a window surface here is a pbuffer the backend draws into, plus this: at every
|
||||
// swap the frame is read back and attached to the wl_surface as a wl_shm buffer.
|
||||
//
|
||||
// libwayland-client is loaded at run time (it is already in every Wayland client's process), so the
|
||||
// library neither links it nor needs its headers to build.
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MOBILEGL_WAYLAND_WINDOWS 1
|
||||
#else
|
||||
#define MOBILEGL_WAYLAND_WINDOWS 0
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_WAYLAND_WINDOWS
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
// EGL_PLATFORM_WAYLAND_KHR and EGL_PLATFORM_WAYLAND_EXT are the same enum.
|
||||
inline constexpr EGLenum kPlatformWayland = 0x31D8;
|
||||
|
||||
// Whether a display is a Wayland one: asked for as EGL_PLATFORM_WAYLAND, or (eglGetDisplay with
|
||||
// no platform) a native display that is a wl_display - recognised the way Mesa does, by its
|
||||
// first word being libwayland-client's wl_display_interface.
|
||||
Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform);
|
||||
|
||||
// Whether `window` looks like a wl_egl_window this library can present to.
|
||||
Bool IsWaylandWindow(const void* window);
|
||||
|
||||
// The size the application last gave its wl_egl_window.
|
||||
Bool WindowSize(const void* window, EGLint* width, EGLint* height);
|
||||
|
||||
// One window surface's presentation: the shm buffers, the event queue they are dispatched on,
|
||||
// and the readback scratch. Created for a wl_egl_window on a wl_display; null with the reason
|
||||
// logged when libwayland-client or the compositor's wl_shm is not there.
|
||||
class WindowSurface {
|
||||
public:
|
||||
static UniquePtr<WindowSurface> Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height,
|
||||
Bool hasAlpha);
|
||||
~WindowSurface();
|
||||
WindowSurface(const WindowSurface&) = delete;
|
||||
WindowSurface& operator=(const WindowSurface&) = delete;
|
||||
|
||||
// Reads the frame back out of the current context's default framebuffer and puts it on
|
||||
// the window: attach, damage, commit. The application's read-framebuffer and pack state
|
||||
// are put back exactly as they were. False (logged) when the frame did not reach the
|
||||
// window; the swap itself goes on regardless.
|
||||
Bool Present();
|
||||
|
||||
struct Impl;
|
||||
|
||||
private:
|
||||
explicit WindowSurface(UniquePtr<Impl> impl);
|
||||
UniquePtr<Impl> m_impl;
|
||||
};
|
||||
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,9 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The dma-buf import's client half: eglCreateImageKHR keeps a dup() of every plane's descriptor,
|
||||
// and this is what tells the render server which texture that buffer backs (see the header).
|
||||
#include <MG_Remote/Client/DmaBufImport.h>
|
||||
#include <Includes.h>
|
||||
#include "../Buffer/GL_Buffer.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
@@ -22,6 +25,13 @@
|
||||
#include "../Sync/GL_Sync.h"
|
||||
#include "../Debug/GL_Debug.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
// The EGL side of the state, for the two GL_OES_EGL_image entry points: what an EGLImage handle
|
||||
// names is an object eglCreateImageKHR built (its geometry, its fourcc and the descriptors it owns),
|
||||
// and that object lives in the EGL state. Same include the other GLImpl files that touch it use.
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
// The compositor's frame boundary: glFlush presents the display host's frame when the surfaceless
|
||||
// bind hook took one (see EGLImpl.cpp). The EGL side declares the pair and the swap entry.
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
|
||||
@@ -125,6 +135,17 @@ MOBILEGL_GL_API void glFlush() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MobileGL::MG_Pipe::MGPipeClientFlush();
|
||||
#endif
|
||||
// A COMPOSITOR'S FRAME BOUNDARY. kwin binds the display host's frame surface and draws the
|
||||
// desktop into it, then calls glFlush - and glFlush alone. It never calls eglSwapBuffers, so
|
||||
// without this the stream reached the server and the server drew, but the display host was
|
||||
// never told a frame was done: the host's own present loop (AHardwareBuffer_acquire ->
|
||||
// draw -> notify) is what puts pixels on the glass, and its notify is exactly this swap.
|
||||
// Guarded by whether the surfaceless-bind hook actually took the host frame.
|
||||
if (MobileGL::MG_Impl::EGLImpl::g_hostFramePresentSurface != nullptr &&
|
||||
MobileGL::MG_Impl::EGLImpl::g_hostFramePresentDisplay != nullptr) {
|
||||
MobileGL::MG_Impl::EGLImpl::SwapBuffers(MobileGL::MG_Impl::EGLImpl::g_hostFramePresentDisplay,
|
||||
MobileGL::MG_Impl::EGLImpl::g_hostFramePresentSurface);
|
||||
}
|
||||
}
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferRenderbuffer, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferRenderbuffer, target, attachment, renderbuffertarget, renderbuffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture2D, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture2D, target, attachment, textarget, texture, level)
|
||||
@@ -1297,17 +1318,325 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmaxARB, GLenum target, GLboolean rese
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferSampleLocationsfvARB, GLenum target, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferSampleLocationsfvARB, target, start, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferSampleLocationsfvARB, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferSampleLocationsfvARB, framebuffer, start, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EvaluateDepthValuesARB, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EvaluateDepthValuesARB, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteObjectARB, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteObjectARB, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, GetHandleARB, GLenum pname) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, GetHandleARB, pname)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DetachObjectARB, GLhandleARB containerObj, GLhandleARB attachedObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DetachObjectARB, containerObj, attachedObj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateShaderObjectARB, GLenum shaderType) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateShaderObjectARB, shaderType)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateProgramObjectARB, void) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateProgramObjectARB, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, AttachObjectARB, GLhandleARB containerObj, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, AttachObjectARB, containerObj, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramObjectARB, GLhandleARB programObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramObjectARB, programObj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterfvARB, GLhandleARB obj, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterfvARB, obj, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterivARB, GLhandleARB obj, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterivARB, obj, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInfoLogARB, GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInfoLogARB, obj, maxLength, length, infoLog)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetAttachedObjectsARB, GLhandleARB containerObj, GLsizei maxCount, GLsizei* count, GLhandleARB* obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetAttachedObjectsARB, containerObj, maxCount, count, obj)
|
||||
// GL_ARB_shader_objects, GL_ARB_vertex_shader, GL_ARB_fragment_shader.
|
||||
//
|
||||
// kwin_wayland 6.7.3 refuses a MobileGL context over three of the names these extensions add.
|
||||
// EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the DESKTOP-OpenGL branch for
|
||||
// every context whose GL_VERSION is not an "OpenGL ES" string:
|
||||
// const bool supportsGLSL = m_isOpenglES || (hasOpenglExtension("GL_ARB_shader_objects") &&
|
||||
// hasOpenglExtension("GL_ARB_fragment_shader") && hasOpenglExtension("GL_ARB_vertex_shader"));
|
||||
// GL_VERSION here is "4.6.0 MobileGL ...", so all three names were REQUIRED of a context that did
|
||||
// not carry them: checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize rendering
|
||||
// context" and quit without drawing a frame. Both backends now advertise the three, and this block
|
||||
// is what makes that advertisement true instead of a test unlock - every entry point the three
|
||||
// extensions define is implemented here, over the shader and program objects the core entry points
|
||||
// already own:
|
||||
//
|
||||
// ARB_shader_objects the 39 functions of the block include/GL/glext.h:4095-4173 declares.
|
||||
// ARB_vertex_shader the three attrib-location entry points of glext.h:4856-4862.
|
||||
// ARB_fragment_shader none. Its "New Procedures and Functions" section reads "None" - it is the
|
||||
// capability half of the pair, and the reason kwin names it at all.
|
||||
//
|
||||
// HANDLES. The ARB family names objects with GLhandleARB. This repository's own glext.h types that
|
||||
// as void* on Apple and as unsigned int on every other platform (glext.h:4055-4059, the same split
|
||||
// the system headers carry), so on Linux, Windows and Android a handle IS the GLuint name this
|
||||
// implementation hands out and both conversions below are the identity there. The pointer arm is
|
||||
// not decoration: a caller on Apple passes a pointer, and an id written straight into its void*
|
||||
// slot would be a size error rather than a cast error. H is a template parameter, and both arms
|
||||
// cast through it, so the arm a platform does not use is never instantiated - and never has to be
|
||||
// well-formed for the other typedef: reinterpret_cast between two integers, which is what the
|
||||
// pointer arm becomes on Linux, is not a cast clang accepts. The shape is the one
|
||||
// MG_State/EGLState/Core.h already uses for EGL's handles.
|
||||
template <typename H = GLhandleARB>
|
||||
static GLhandleARB ToGLHandleARB(GLuint name) {
|
||||
if constexpr (std::is_pointer_v<H>) {
|
||||
return reinterpret_cast<H>(static_cast<std::uintptr_t>(name));
|
||||
} else {
|
||||
return static_cast<H>(name);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename H = GLhandleARB>
|
||||
static GLuint FromGLHandleARB(GLhandleARB handle) {
|
||||
if constexpr (std::is_pointer_v<H>) {
|
||||
return static_cast<GLuint>(reinterpret_cast<std::uintptr_t>(static_cast<H>(handle)));
|
||||
} else {
|
||||
return static_cast<GLuint>(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// The answer behind GetObjectParameter{if}vARB, and the reason neither of them carries a pname
|
||||
// translation table: the GL_OBJECT_*_ARB tokens ARE the core tokens, numerically. Core GL took the
|
||||
// object pnames over unchanged when ARB_shader_objects was folded in -
|
||||
// GL_OBJECT_DELETE_STATUS_ARB through GL_OBJECT_SHADER_SOURCE_LENGTH_ARB are 0x8B80-0x8B88, the
|
||||
// same numbers glcorearb.h gives GL_DELETE_STATUS through GL_SHADER_SOURCE_LENGTH
|
||||
// (glext.h:4086-4094) - so the mapping is a RENAME and the same pname goes to the core getter of
|
||||
// the same object kind. Handing it to that getter is also the right ERROR behaviour, and the reason
|
||||
// this does not whitelist pnames: GL_OBJECT_LINK_STATUS_ARB asked of a SHADER has to come back
|
||||
// INVALID_ENUM, and glGetShaderiv is where the set of pnames a shader accepts is defined.
|
||||
//
|
||||
// The one token core never took is GL_OBJECT_TYPE_ARB (0x8B4E; core's GL_OBJECT_TYPE, 0x9112, is
|
||||
// ARB_program_interface_query's interface class, a different question with a different value
|
||||
// space), so the object kind is the single thing answered here without a core getter. It is also
|
||||
// the only way an ARB-era caller can ask whether a handle names a program or a shader: there is no
|
||||
// glIsProgramARB/glIsShaderARB in ARB_shader_objects (include/GL/glext.h:4095-4173 declares
|
||||
// neither, and the glIsProgramARB the registry does know, glext.h:3280, belongs to
|
||||
// ARB_vertex_program/ARB_fragment_program - those ASM program objects are not these).
|
||||
static MobileGL::Bool AnswerObjectParameterARB(GLuint name, GLenum pname, GLint* params) {
|
||||
if (pname == GL_OBJECT_TYPE_ARB) {
|
||||
if (glIsProgram(name)) {
|
||||
*params = GL_PROGRAM_OBJECT_ARB;
|
||||
return true;
|
||||
}
|
||||
if (glIsShader(name)) {
|
||||
*params = GL_SHADER_OBJECT_ARB;
|
||||
return true;
|
||||
}
|
||||
// The error ARB_vertex_shader's own text specifies: "The error INVALID_VALUE is generated
|
||||
// by any command that takes one or more handles as input, and one or more handles are not
|
||||
// an object handle generated by OpenGL."
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidValue,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(name) + " is not an object handle generated by OpenGL."));
|
||||
return false;
|
||||
}
|
||||
if (glIsProgram(name)) {
|
||||
glGetProgramiv(name, pname, params);
|
||||
return true;
|
||||
}
|
||||
// A shader, or a name that is neither. glGetShaderiv owns both the shader pname set and the
|
||||
// name validation, which is the one place that splits a program name handed to a shader query
|
||||
// (INVALID_OPERATION) from a name GL never generated (INVALID_VALUE) - the split GL 3.3 core
|
||||
// 2.11 requires. It writes nothing in either error case, which is what the caller reads as
|
||||
// "no answer".
|
||||
const MobileGL::Bool isShader = glIsShader(name) == GL_TRUE;
|
||||
glGetShaderiv(name, pname, params);
|
||||
return isShader;
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glDeleteObjectARB(GLhandleARB obj) {
|
||||
// ARB_shader_objects has ONE object namespace - a handle names a program or a shader and
|
||||
// nothing else - and DeleteObjectARB is the deletion entry point for both kinds, so the kind
|
||||
// is decided here. IsProgram/IsShader answer over the same single namespace this
|
||||
// implementation hands names out of, so a program name cannot be taken for a shader name.
|
||||
// Zero, and a handle that names neither object, are ignored: there is no object to delete.
|
||||
const GLuint name = FromGLHandleARB(obj);
|
||||
if (glIsProgram(name)) {
|
||||
glDeleteProgram(name);
|
||||
} else if (glIsShader(name)) {
|
||||
glDeleteShader(name);
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glGetHandleARB(GLenum pname) {
|
||||
if (pname != GL_PROGRAM_OBJECT_ARB) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidEnum,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"pname " + std::to_string(pname) + " is not GL_PROGRAM_OBJECT_ARB."));
|
||||
return ToGLHandleARB(0u);
|
||||
}
|
||||
// "The program object currently in use", read from the state GL_CURRENT_PROGRAM is answered
|
||||
// from - so a program bound through glUseProgram is found here too, and a
|
||||
// UseProgramObjectARB(0) reads back as the zero handle.
|
||||
const auto& currentProgram = MobileGL::MG_State::pGLContext->GetCurrentProgram();
|
||||
return ToGLHandleARB(currentProgram ? static_cast<GLuint>(currentProgram->GetExternalIndex()) : 0u);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glDetachObjectARB(GLhandleARB containerObj, GLhandleARB attachedObj) {
|
||||
glDetachShader(FromGLHandleARB(containerObj), FromGLHandleARB(attachedObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glCreateShaderObjectARB(GLenum shaderType) {
|
||||
return ToGLHandleARB(glCreateShader(shaderType));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glShaderSourceARB(GLhandleARB shaderObj, GLsizei count, const GLcharARB** string,
|
||||
const GLint* length) {
|
||||
glShaderSource(FromGLHandleARB(shaderObj), count, string, length);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glCompileShaderARB(GLhandleARB shaderObj) {
|
||||
glCompileShader(FromGLHandleARB(shaderObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glCreateProgramObjectARB(void) {
|
||||
return ToGLHandleARB(glCreateProgram());
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glAttachObjectARB(GLhandleARB containerObj, GLhandleARB obj) {
|
||||
glAttachShader(FromGLHandleARB(containerObj), FromGLHandleARB(obj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glLinkProgramARB(GLhandleARB programObj) {
|
||||
glLinkProgram(FromGLHandleARB(programObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUseProgramObjectARB(GLhandleARB programObj) {
|
||||
glUseProgram(FromGLHandleARB(programObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glValidateProgramARB(GLhandleARB programObj) {
|
||||
glValidateProgram(FromGLHandleARB(programObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1fARB(GLint location, GLfloat v0) {
|
||||
glUniform1f(location, v0);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2fARB(GLint location, GLfloat v0, GLfloat v1) {
|
||||
glUniform2f(location, v0, v1);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) {
|
||||
glUniform3f(location, v0, v1, v2);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) {
|
||||
glUniform4f(location, v0, v1, v2, v3);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1iARB(GLint location, GLint v0) {
|
||||
glUniform1i(location, v0);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2iARB(GLint location, GLint v0, GLint v1) {
|
||||
glUniform2i(location, v0, v1);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3iARB(GLint location, GLint v0, GLint v1, GLint v2) {
|
||||
glUniform3i(location, v0, v1, v2);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4iARB(GLint location, GLint v0, GLint v1, GLint v2, GLint v3) {
|
||||
glUniform4i(location, v0, v1, v2, v3);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform1fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform2fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform3fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform4fv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform1iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform2ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform2iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform3ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform3iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform4ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform4iv(location, count, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniformMatrix2fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix2fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniformMatrix3fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix3fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniformMatrix4fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix4fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetObjectParameterfvARB(GLhandleARB obj, GLenum pname, GLfloat* params) {
|
||||
// The fv form asks the same questions - the ARB object model holds no float-valued object
|
||||
// state - so the answer is the iv answer widened. A query the iv path refuses leaves the
|
||||
// output untouched here as well, rather than putting a plausible-looking 0.0 where there is
|
||||
// no answer.
|
||||
GLint value = 0;
|
||||
if (!AnswerObjectParameterARB(FromGLHandleARB(obj), pname, &value)) return;
|
||||
*params = static_cast<GLfloat>(value);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetObjectParameterivARB(GLhandleARB obj, GLenum pname, GLint* params) {
|
||||
AnswerObjectParameterARB(FromGLHandleARB(obj), pname, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetInfoLogARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) {
|
||||
// The same handle-kind dispatch as AnswerObjectParameterARB, and the same fallback: the shader
|
||||
// getter is also where the name validation and its error code live.
|
||||
const GLuint name = FromGLHandleARB(obj);
|
||||
if (glIsProgram(name)) {
|
||||
glGetProgramInfoLog(name, maxLength, length, infoLog);
|
||||
} else {
|
||||
glGetShaderInfoLog(name, maxLength, length, infoLog);
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetAttachedObjectsARB(GLhandleARB containerObj, GLsizei maxCount, GLsizei* count,
|
||||
GLhandleARB* obj) {
|
||||
// The caller's array holds GLhandleARB while glGetAttachedShaders writes GLuint names, and on
|
||||
// Apple those are different sizes, so the ids are gathered first and converted one at a time.
|
||||
// A negative maxCount is passed through: glGetAttachedShaders is where its INVALID_VALUE is.
|
||||
MobileGL::Vector<GLuint> shaders(maxCount > 0 ? static_cast<MobileGL::SizeT>(maxCount) : 0, 0);
|
||||
GLsizei attached = 0;
|
||||
glGetAttachedShaders(FromGLHandleARB(containerObj), maxCount, &attached, shaders.data());
|
||||
for (GLsizei i = 0; i < attached; ++i) {
|
||||
obj[i] = ToGLHandleARB(shaders[i]);
|
||||
}
|
||||
if (count) *count = attached;
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLint glGetUniformLocationARB(GLhandleARB programObj, const GLcharARB* name) {
|
||||
return glGetUniformLocation(FromGLHandleARB(programObj), name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetActiveUniformARB(GLhandleARB programObj, GLuint index, GLsizei maxLength,
|
||||
GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
|
||||
glGetActiveUniform(FromGLHandleARB(programObj), index, maxLength, length, size, type, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetUniformfvARB(GLhandleARB programObj, GLint location, GLfloat* params) {
|
||||
glGetUniformfv(FromGLHandleARB(programObj), location, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetUniformivARB(GLhandleARB programObj, GLint location, GLint* params) {
|
||||
glGetUniformiv(FromGLHandleARB(programObj), location, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetShaderSourceARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length,
|
||||
GLcharARB* source) {
|
||||
glGetShaderSource(FromGLHandleARB(obj), maxLength, length, source);
|
||||
}
|
||||
|
||||
// ARB_vertex_shader's three entry points. The generic-attribute family that extension also
|
||||
// describes is spelled with the ARB_vertex_program names (glVertexAttribPointerARB and the rest),
|
||||
// which glext.h declares in that other block and which MobileGL does not advertise; nothing here
|
||||
// claims them.
|
||||
MOBILEGL_GL_API void glBindAttribLocationARB(GLhandleARB programObj, GLuint index, const GLcharARB* name) {
|
||||
glBindAttribLocation(FromGLHandleARB(programObj), index, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetActiveAttribARB(GLhandleARB programObj, GLuint index, GLsizei maxLength,
|
||||
GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
|
||||
glGetActiveAttrib(FromGLHandleARB(programObj), index, maxLength, length, size, type, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLint glGetAttribLocationARB(GLhandleARB programObj, const GLcharARB* name) {
|
||||
return glGetAttribLocation(FromGLHandleARB(programObj), name);
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedStringARB, GLenum type, GLint namelen, const GLchar* name, GLint strinen, const GLchar* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedStringARB, type, namelen, name, strinen, string)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteNamedStringARB, GLint namelen, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteNamedStringARB, namelen, name)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompileShaderIncludeARB, GLuint shader, GLsizei count, const GLchar* const* path, const GLint* length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompileShaderIncludeARB, shader, count, path, length)
|
||||
@@ -1713,6 +2042,141 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendEnviATI, GLenum pname, GLint para
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendEnvfATI, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexBlendEnvfATI, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EGLImageTargetTexStorageEXT, GLenum target, GLeglImageOES image, const GLint* attrib_list) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EGLImageTargetTexStorageEXT, target, image, attrib_list)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EGLImageTargetTextureStorageEXT, GLuint texture, GLeglImageOES image, const GLint* attrib_list) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EGLImageTargetTextureStorageEXT, texture, image, attrib_list)
|
||||
|
||||
// GL_OES_EGL_image.
|
||||
//
|
||||
// MEASURED, and the whole reason this block exists: libepoxy in kwin_wayland prints
|
||||
// No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image
|
||||
// and refuses the import path there - and that path is how the compositor's scene reaches the
|
||||
// display daemon's dma-bufs, i.e. how a Linux desktop gets onto the phone's screen. The EGL half is
|
||||
// real: eglCreateImageKHR builds an EGL_LINUX_DMA_BUF_EXT image that owns a dup() of every plane's
|
||||
// descriptor and carries that buffer's geometry and fourcc (MG_State/EGLState/Core.h). So a handle
|
||||
// that arrives here is resolvable, and what these two entry points do is exactly that: resolve it,
|
||||
// refuse what names nothing, and say ONCE what is not wired yet. What is not wired yet is the
|
||||
// render-server half - telling the server which texture a dma-buf backs, over the one channel that
|
||||
// can carry a descriptor across the container boundary - so a framebuffer built over an imported
|
||||
// texture is not complete until that lands. Pretending otherwise (attaching the image to a texture
|
||||
// object that has no storage behind it) would turn "not implemented" into "mysteriously black",
|
||||
// which is what the log line below is here to prevent.
|
||||
MOBILEGL_GL_API void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image) {
|
||||
// 0x8D65 is GL_TEXTURE_EXTERNAL_OES, which the headers this file compiles against do not spell
|
||||
// out. It matters here MORE than GL_TEXTURE_2D does: a compositor that could not map an
|
||||
// imported dma-buf's format to a non-external one binds its image to THIS target, so refusing it
|
||||
// would turn "no format map yet" into an INVALID_ENUM the caller cannot act on (kwin 6.7.3 does
|
||||
// exactly that when EglDisplay::queryImportFormats() answers nothing).
|
||||
constexpr GLenum kTextureExternalOes = 0x8D65;
|
||||
if (target != GL_TEXTURE_2D && target != kTextureExternalOes) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidEnum,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"target is not a texture target this entry point takes."));
|
||||
return;
|
||||
}
|
||||
EGLint imageWidth = 0;
|
||||
EGLint imageHeight = 0;
|
||||
EGLint imageFourCC = 0;
|
||||
EGLint imagePlanes = 0;
|
||||
const MobileGL::Bool imageKnown =
|
||||
MobileGL::MG_State::pEGLContext != nullptr &&
|
||||
MobileGL::MG_State::pEGLContext->DescribeImage(reinterpret_cast<::EGLImage>(image), &imageWidth, &imageHeight,
|
||||
&imageFourCC, &imagePlanes);
|
||||
if (!imageKnown) {
|
||||
// A handle this side did not hand out: ARB/OES text puts that on INVALID_VALUE, and it is
|
||||
// the answer a caller can act on rather than a texture that renders nowhere.
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidValue,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"image is not an EGLImage this display created."));
|
||||
return;
|
||||
}
|
||||
MGLOG_W_ONCE("glEGLImageTargetTexture2DOES: taking target 0x%04x over an EGLImage of %dx%d fourcc 0x%08x "
|
||||
"(%d plane(s)) that this side holds; the render server has NOT been told which texture that "
|
||||
"dma-buf backs yet, so a framebuffer over this texture is not complete (the fd transport over "
|
||||
"the GBM socket and the server-side import are the next change)",
|
||||
target, imageWidth, imageHeight, imageFourCC, imagePlanes);
|
||||
|
||||
// AND NOW WIRE IT: a texture bound to an imported EGLImage has no storage on the render server,
|
||||
// and a framebuffer built over it there is not complete - kwin 6.7.3 aborts on its own
|
||||
// RenderTarget assertion right after this log line (measured twice). So the server is told which
|
||||
// buffer backs the texture: one message per plane, each carrying that plane's descriptor over the
|
||||
// session transport's SCM_RIGHTS channel. No session, or a transport without that channel, is a
|
||||
// refusal by name from ShareImportedDmaBuf - the texture simply keeps having no storage, which is
|
||||
// the honest state rather than a silent render into nothing.
|
||||
MGLOG_I("dma-buf import: entry (target=0x%04x, image=%p)", static_cast<unsigned>(target), static_cast<void*>(image));
|
||||
if (MobileGL::MG_State::pEGLContext != nullptr) {
|
||||
constexpr MobileGL::Int kDmaBufImportPlanes = 4;
|
||||
EGLint planeCount = 0;
|
||||
EGLint planeFds[kDmaBufImportPlanes] = {-1, -1, -1, -1};
|
||||
EGLint planeOffsets[kDmaBufImportPlanes] = {0, 0, 0, 0};
|
||||
EGLint planePitches[kDmaBufImportPlanes] = {0, 0, 0, 0};
|
||||
MobileGL::Uint64 planeModifiers[kDmaBufImportPlanes] = {0, 0, 0, 0};
|
||||
MobileGL::Bool planeHasModifier[kDmaBufImportPlanes] = {false, false, false, false};
|
||||
if (MobileGL::MG_State::pEGLContext->DescribeImagePlanes(
|
||||
reinterpret_cast<::EGLImage>(image), &planeCount, planeFds, planeOffsets, planePitches,
|
||||
planeModifiers, planeHasModifier, kDmaBufImportPlanes)) {
|
||||
// The texture the image was bound to. 0x8D65 has no TextureTarget member in this state
|
||||
// layer (nothing else here binds it), so the name is looked up through Texture2D and may
|
||||
// come back 0 - the server logs what it was told rather than guessing.
|
||||
MobileGL::Uint32 texName = 0;
|
||||
if (MobileGL::MG_State::pGLContext != nullptr) {
|
||||
const MobileGL::Int unit = MobileGL::MG_State::pGLContext->GetActiveTextureUnit();
|
||||
// THE EXTERNAL SLOT, which is where a compositor's imported dma-buf texture lives.
|
||||
const auto& bound = MobileGL::MG_State::pGLContext->GetTextureUnitObject(unit)
|
||||
.GetBindingSlot(MobileGL::TextureTarget::TextureExternalOES)
|
||||
.GetBoundObject();
|
||||
if (bound != nullptr) {
|
||||
texName = static_cast<MobileGL::Uint32>(bound->GetExternalIndex());
|
||||
// AN IMPORTED TEXTURE IS BACKED, SO IT IS COMPLETE. Without this the state layer
|
||||
// answers incompletely (TextureObjectBase::IsComplete only looks at the internal
|
||||
// format) and the compositor, having attached it to a framebuffer, reports
|
||||
// "framebuffer for dmabuf 0 is not complete" and stops before drawing anything.
|
||||
bound->SetInternalFormat(MobileGL::TextureInternalFormat::RGBA8);
|
||||
}
|
||||
}
|
||||
MGLOG_I("dma-buf import: textures resolved (texName=%u planes=%d), sharing", texName,
|
||||
static_cast<int>(planeCount));
|
||||
for (MobileGL::Int plane = 0; plane < planeCount; plane++) {
|
||||
if (planeFds[plane] < 0) continue;
|
||||
MGLOG_I("dma-buf import: sharing plane %d fd=%d", static_cast<int>(plane), planeFds[plane]);
|
||||
(void)MobileGL::MG_Remote::Client::ShareImportedDmaBuf(
|
||||
static_cast<MobileGL::Uint32>(target), texName, static_cast<MobileGL::Uint32>(imageWidth),
|
||||
static_cast<MobileGL::Uint32>(imageHeight), static_cast<MobileGL::Uint32>(imageFourCC),
|
||||
static_cast<MobileGL::Uint32>(plane), static_cast<MobileGL::Uint32>(planeCount), planeFds[plane],
|
||||
planePitches[plane], planeOffsets[plane], planeModifiers[plane], planeHasModifier[plane]);
|
||||
MGLOG_I("dma-buf import: plane %d shared", static_cast<int>(plane));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glEGLImageTargetRenderbufferStorageOES(GLenum target, GLeglImageOES image) {
|
||||
if (target != GL_RENDERBUFFER) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidEnum,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"target is not GL_RENDERBUFFER."));
|
||||
return;
|
||||
}
|
||||
EGLint imageWidth = 0;
|
||||
EGLint imageHeight = 0;
|
||||
EGLint imageFourCC = 0;
|
||||
EGLint imagePlanes = 0;
|
||||
const MobileGL::Bool imageKnown =
|
||||
MobileGL::MG_State::pEGLContext != nullptr &&
|
||||
MobileGL::MG_State::pEGLContext->DescribeImage(reinterpret_cast<::EGLImage>(image), &imageWidth, &imageHeight,
|
||||
&imageFourCC, &imagePlanes);
|
||||
if (!imageKnown) {
|
||||
MobileGL::MG_State::pGLContext->RecordError(
|
||||
MobileGL::ErrorCode::InvalidValue,
|
||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"image is not an EGLImage this display created."));
|
||||
return;
|
||||
}
|
||||
MGLOG_W_ONCE("glEGLImageTargetRenderbufferStorageOES: the renderbuffer takes an EGLImage of %dx%d fourcc "
|
||||
"0x%08x; the render-server half of the import is the next change, exactly as for the texture "
|
||||
"entry point beside this one",
|
||||
imageWidth, imageHeight, imageFourCC);
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformBufferEXT, GLuint program, GLint location, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformBufferEXT, program, location, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetUniformBufferSizeEXT, GLuint program, GLint location) DECLARE_GL_FUNCTION_STUB_END(GLint, GetUniformBufferSizeEXT, program, location)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLintptr, GetUniformOffsetEXT, GLuint program, GLint location) DECLARE_GL_FUNCTION_STUB_END(GLintptr, GetUniformOffsetEXT, program, location)
|
||||
|
||||
@@ -100,6 +100,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
// NOT gated on the host frame, and that is deliberate: this only decides the ERROR CODE
|
||||
// once completeness has already failed (CheckFramebufferStatus_State asks
|
||||
// CheckCompleteness first), and the imported dma-buf's attachment is counted as valid
|
||||
// THERE (MG_State/GLState/FramebufferState/FramebufferObject.cpp, whose gate lands before
|
||||
// its own validity test for exactly this reason). Touching it here would add a second
|
||||
// place that has to know the backend's frame - and the frontend knowing that fact is the
|
||||
// compromise the completeness comment already names the end of.
|
||||
for (const auto& attachment : framebufferObject.GetAllAttachmentObjects()) {
|
||||
if (attachment.IsValid() && !attachment.IsEmpty()) {
|
||||
return true;
|
||||
@@ -611,7 +618,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
textureObject->GetTarget() != expectedTextureTarget) {
|
||||
(textureObject->GetTarget() != expectedTextureTarget &&
|
||||
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
|
||||
expectedTextureTarget == TextureTarget::Texture2D))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1382,7 +1391,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
textureObject->GetTarget() != expectedTextureTarget) {
|
||||
(textureObject->GetTarget() != expectedTextureTarget &&
|
||||
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
|
||||
expectedTextureTarget == TextureTarget::Texture2D))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1532,7 +1543,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
textureObject->GetTarget() != expectedTextureTarget) {
|
||||
(textureObject->GetTarget() != expectedTextureTarget &&
|
||||
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
|
||||
expectedTextureTarget == TextureTarget::Texture2D))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
|
||||
@@ -869,6 +869,55 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glNamedBufferPageCommitmentARB, name);
|
||||
GETPROC(glTexPageCommitmentARB, name);
|
||||
GETPROC(glTexBufferARB, name);
|
||||
// ARB_shader_objects and ARB_vertex_shader, by name, because a name is the only way an
|
||||
// application ever asks for an extension entry point. Measured path: kwin_wayland resolves GL
|
||||
// through libepoxy, and libepoxy asks eglGetProcAddress -> EGLImpl::GetProcAddress -> this
|
||||
// table. Before these lines the callers that need the extension whose STRINGS make
|
||||
// checkSupported() pass got a nullptr for every function of it - dlsym on the library found
|
||||
// them, this table did not. The strings and the implementations are advertised from
|
||||
// MG_Impl/GLImpl/Exporting/Definitions.cpp and the two BackendObject_* extension lists.
|
||||
GETPROC(glDeleteObjectARB, name);
|
||||
GETPROC(glGetHandleARB, name);
|
||||
GETPROC(glDetachObjectARB, name);
|
||||
GETPROC(glCreateShaderObjectARB, name);
|
||||
GETPROC(glShaderSourceARB, name);
|
||||
GETPROC(glCompileShaderARB, name);
|
||||
GETPROC(glCreateProgramObjectARB, name);
|
||||
GETPROC(glAttachObjectARB, name);
|
||||
GETPROC(glLinkProgramARB, name);
|
||||
GETPROC(glUseProgramObjectARB, name);
|
||||
GETPROC(glValidateProgramARB, name);
|
||||
GETPROC(glUniform1fARB, name);
|
||||
GETPROC(glUniform2fARB, name);
|
||||
GETPROC(glUniform3fARB, name);
|
||||
GETPROC(glUniform4fARB, name);
|
||||
GETPROC(glUniform1iARB, name);
|
||||
GETPROC(glUniform2iARB, name);
|
||||
GETPROC(glUniform3iARB, name);
|
||||
GETPROC(glUniform4iARB, name);
|
||||
GETPROC(glUniform1fvARB, name);
|
||||
GETPROC(glUniform2fvARB, name);
|
||||
GETPROC(glUniform3fvARB, name);
|
||||
GETPROC(glUniform4fvARB, name);
|
||||
GETPROC(glUniform1ivARB, name);
|
||||
GETPROC(glUniform2ivARB, name);
|
||||
GETPROC(glUniform3ivARB, name);
|
||||
GETPROC(glUniform4ivARB, name);
|
||||
GETPROC(glUniformMatrix2fvARB, name);
|
||||
GETPROC(glUniformMatrix3fvARB, name);
|
||||
GETPROC(glUniformMatrix4fvARB, name);
|
||||
GETPROC(glGetObjectParameterfvARB, name);
|
||||
GETPROC(glGetObjectParameterivARB, name);
|
||||
GETPROC(glGetInfoLogARB, name);
|
||||
GETPROC(glGetAttachedObjectsARB, name);
|
||||
GETPROC(glGetUniformLocationARB, name);
|
||||
GETPROC(glGetActiveUniformARB, name);
|
||||
GETPROC(glGetUniformfvARB, name);
|
||||
GETPROC(glGetUniformivARB, name);
|
||||
GETPROC(glGetShaderSourceARB, name);
|
||||
GETPROC(glBindAttribLocationARB, name);
|
||||
GETPROC(glGetActiveAttribARB, name);
|
||||
GETPROC(glGetAttribLocationARB, name);
|
||||
GETPROC(glDepthRangeArraydvNV, name);
|
||||
GETPROC(glDepthRangeIndexeddNV, name);
|
||||
GETPROC(glBlendBarrierKHR, name);
|
||||
@@ -886,6 +935,8 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glBeginPerfMonitorAMD, name);
|
||||
GETPROC(glEndPerfMonitorAMD, name);
|
||||
GETPROC(glGetPerfMonitorCounterDataAMD, name);
|
||||
GETPROC(glEGLImageTargetTexture2DOES, name);
|
||||
GETPROC(glEGLImageTargetRenderbufferStorageOES, name);
|
||||
GETPROC(glEGLImageTargetTexStorageEXT, name);
|
||||
GETPROC(glEGLImageTargetTextureStorageEXT, name);
|
||||
GETPROC(glLabelObjectEXT, name);
|
||||
|
||||
@@ -1828,6 +1828,11 @@ namespace MobileGL::MG_Pipe {
|
||||
*entry = Entry{};
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
for (auto& table : m_kinds) table.clear();
|
||||
m_band.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Uint32 Gen = 0;
|
||||
@@ -1892,6 +1897,15 @@ namespace MobileGL::MG_Pipe {
|
||||
PublicationLatch().NoteUnpublished(kind, handle);
|
||||
}
|
||||
|
||||
void MGPipeForgetAllPublications() {
|
||||
PublicationLatch().Clear();
|
||||
// The two families whose death asks their own record rather than the latch: vertex
|
||||
// elements (MGPipeEmitVertexElementsDestroyAndFree's RecordIsPublished) and buffers
|
||||
// (MGPipeEmitResourceDestroyAndFree's WasPublished).
|
||||
MGPipeVertexInputEmitterInstance().ForgetAllRecords();
|
||||
MGPipeResourceTrackerInstance().ForgetAllPublished();
|
||||
}
|
||||
|
||||
// THE GATE ITSELF, AS AN OBSERVABLE (ID-39, widened by S-3 / ID-41). Every P4a birth hook
|
||||
// below and every `wants()` row in the walk resolve through FamilyIsLive /
|
||||
// P4aFamilyHasItsConsumer / P4aFamilyDependenciesAreSet, and none of the three is reachable
|
||||
|
||||
@@ -340,6 +340,10 @@ namespace MobileGL::MG_Pipe {
|
||||
const SizeT slot = handle.Slot;
|
||||
return slot < m_bySlot.size() && m_bySlot[slot].Published;
|
||||
}
|
||||
// The session that held every buffer's record is gone (MGPipeForgetAllPublications).
|
||||
void ForgetAllPublished() {
|
||||
for (Entry& entry : m_bySlot) entry.Published = false;
|
||||
}
|
||||
|
||||
// The sticky everBoundAs mask. Sticky exactly as MGPResourceDesc::ImageBindableHint's
|
||||
// everImageBound is: ORed, never cleared, so a buffer that was an element array once
|
||||
|
||||
@@ -351,6 +351,14 @@ namespace MobileGL::MG_Pipe {
|
||||
m_boundLifetimeId = 0;
|
||||
}
|
||||
|
||||
// The session that held the records is gone (MGPipeForgetAllPublications): both halves go,
|
||||
// the record half included, because no applier holds any of them any more.
|
||||
void ForgetAllRecords() {
|
||||
for (Latch& latch : m_latch) latch = Latch{};
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
}
|
||||
|
||||
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
|
||||
|
||||
private:
|
||||
|
||||
@@ -3119,10 +3119,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
"MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN="
|
||||
"MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||
set(mglD2Extra FIXTURES_REQUIRED mobilegl-tcp)
|
||||
# The loopback supervisor serves ONE session (ServerMain.cpp answers a second Hello
|
||||
# Refuse{Busy}), and every other Tcp entry holds this lock. Without it `ctest -L
|
||||
# integration-gpu -j N` ran this case beside another Tcp case and whichever client
|
||||
# connected second died Refuse{Busy} -> Fatal{CapsBeforeFirstSnapshot}.
|
||||
# The loopback supervisor used to serve ONE session (a second Hello was answered
|
||||
# Refuse{Busy}), and `ctest -L integration-gpu -j N` ran this case beside another Tcp
|
||||
# case so that whichever client connected second died Refuse{Busy} ->
|
||||
# Fatal{CapsBeforeFirstSnapshot}. It serves several side by side now (ServerMain.cpp
|
||||
# TcpSupervisor, fork shape); every Tcp entry still holds this lock, so the cases do not
|
||||
# share the fixture's one server and its log while they run.
|
||||
# scripts/ci/spawn_lane_parity.py and junit_tally.py --discovery now refuse any entry
|
||||
# on the fixture that does not hold it.
|
||||
set(mglD2Locks "${mglD2LogName}\;mobilegl-tcp")
|
||||
|
||||
@@ -214,6 +214,8 @@ namespace MobileGL::MG_Pipe {
|
||||
TexCubeArray,
|
||||
Tex2DMS,
|
||||
Tex2DMSArray,
|
||||
// GL_TEXTURE_EXTERNAL_OES
|
||||
TexExternalOES,
|
||||
Renderbuffer,
|
||||
TexBuffer,
|
||||
TexRect,
|
||||
@@ -263,6 +265,10 @@ namespace MobileGL::MG_Pipe {
|
||||
return static_cast<Uint32>(MGPipeResourceTarget::TexCubeArray);
|
||||
case MobileGL::TextureTarget::Texture2DMultisampleArray:
|
||||
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DMSArray);
|
||||
// GL_TEXTURE_EXTERNAL_OES gets its own row for the same reason TextureRectangle has one: it is
|
||||
// a real target a descriptor may carry (the compositor's imported dma-buf textures live on it).
|
||||
case MobileGL::TextureTarget::TextureExternalOES:
|
||||
return static_cast<Uint32>(MGPipeResourceTarget::TexExternalOES);
|
||||
// NOT TEXTURE TARGETS. Listed rather than defaulted so the completeness assert still
|
||||
// sees them, and mapped to the sentinel because no descriptor may carry either: the
|
||||
// count is the enum's bound and Unknown is what an unresolved GL enum becomes.
|
||||
|
||||
@@ -341,6 +341,12 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
// THE SESSION IS GONE, AND EVERYTHING IT HELD WITH IT. Called once the split roles are shut
|
||||
// down: every object the server twinned died with the session, so nothing is published any
|
||||
// more - and the frontend objects that die after it (MobileGL::Destroy resets the GL context
|
||||
// after ShutdownSplitRoles, by design) must not route a delete for a twin that no longer
|
||||
// exists to whichever applier is installed now, which asserts on the unknown handle.
|
||||
void MGPipeForgetAllPublications();
|
||||
|
||||
// ---- the four mints (pure allocator work, no family knowledge) ----
|
||||
//
|
||||
|
||||
@@ -216,6 +216,10 @@ namespace MobileGL::MG_Remote::Client {
|
||||
WaitForApplyBeforeEglForwarder("CreateEGLWindowSurface");
|
||||
// P12 (D1): unless the window is the SERVER's - then there is no handle of ours to tell.
|
||||
if (MG_Config::ServerOwnedWindowSurfaces()) return CreateServerOwnedWindowSurface(surface, handle);
|
||||
// The other on-screen shape: the frames belong to the display host, and the server takes
|
||||
// them from the bridge. As for the server-owned shape, no window of the client's crosses
|
||||
// the wire - only the size it asked for.
|
||||
if (MG_Config::HostFrameWindowSurfaces()) return CreateHostFrameWindowSurface(surface, handle);
|
||||
Server::ServerSetWindowHandle(handle);
|
||||
if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
|
||||
// The server's surface init published the default framebuffer's shape as a
|
||||
@@ -295,6 +299,10 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// dereferenced - the client's InitWindowSurface is a no-op - and never crosses the wire
|
||||
// (the ServerOwned frame carries token 0).
|
||||
char g_serverOwnedWindowPlaceholder = 0;
|
||||
// The host-framed shape names no window of the client's either; the base class still wants
|
||||
// a handle to register the surface with, and this is the same stand-in the server-owned
|
||||
// shape uses.
|
||||
char g_hostFrameWindowPlaceholder = 0;
|
||||
} // namespace
|
||||
|
||||
Bool BackendObject_Remote::CreateServerOwnedWindowSurface(EGLSurface surface,
|
||||
@@ -360,6 +368,46 @@ namespace MobileGL::MG_Remote::Client {
|
||||
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::CreateHostFrameWindowSurface(EGLSurface surface,
|
||||
const MG_Backend::WindowHandle& handle) {
|
||||
// The size crosses the wire and nothing else of the client's does: the server takes frames
|
||||
// from the display host at that size and answers with the extent the surface has.
|
||||
const Server::ServerOwnedWindowReply reply =
|
||||
Server::ServerCreateHostFrameWindowSurface(surface, handle.Width, handle.Height);
|
||||
if (!reply.ok) {
|
||||
// FAILS BY NAME ON THIS SIDE TOO, as the server-owned shape does: the server logged its
|
||||
// reason, and the refusal code is what lets this line say the same thing.
|
||||
switch (reply.refusal) {
|
||||
case Server::SurfaceRefusalCode::SurfaceModeMismatch:
|
||||
MGLOG_E("MG_Remote client: Refuse HostFrame (SurfaceModeMismatch) - MOBILEGL_IPC_SURFACE=host "
|
||||
"asked for this %ux%u window surface in a session that already chose a pbuffer: only "
|
||||
"one rendering path is active per session. eglCreateWindowSurface fails with "
|
||||
"EGL_BAD_NATIVE_WINDOW",
|
||||
handle.Width, handle.Height);
|
||||
break;
|
||||
case Server::SurfaceRefusalCode::NoServerWindow:
|
||||
MGLOG_E("MG_Remote client: Refuse HostFrame (NoServerWindow) - the server could not take a frame "
|
||||
"from the display host for this %ux%u surface; its log names what the host or the "
|
||||
"bridge said (its own answer was %ux%u)",
|
||||
handle.Width, handle.Height, reply.width, reply.height);
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("MG_Remote client: Refuse HostFrame (code %u) for this %ux%u window surface",
|
||||
static_cast<Uint>(reply.refusal), handle.Width, handle.Height);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// From here the frames are the server's and the host's business; this side only registers the
|
||||
// surface, exactly as it does for the server-owned shape.
|
||||
MG_Backend::WindowHandle local = handle;
|
||||
if (local.Backend == MG_Backend::WindowBackend::Unknown) local.Backend = MG_Backend::WindowBackend::Android;
|
||||
if (local.Handle == nullptr) local.Handle = &g_hostFrameWindowPlaceholder;
|
||||
local.Width = reply.width;
|
||||
local.Height = reply.height;
|
||||
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
|
||||
EGLContext ctx) {
|
||||
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the base
|
||||
|
||||
@@ -91,6 +91,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// CreateEGLWindowSurface - ONE ServerOwned frame, no SetWindowHandle, the server's real
|
||||
// geometry adopted before it returns, and every refusal named in this process's log.
|
||||
Bool CreateServerOwnedWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
|
||||
// MOBILEGL_IPC_SURFACE=host: the frames are the display host's, and the server takes
|
||||
// them from the host frame bridge. Nothing of the client's crosses the wire here either.
|
||||
Bool CreateHostFrameWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
|
||||
|
||||
// The generation of the snapshot m_formatCapabilities was filled from. Exposed only
|
||||
// through the log line on a refresh: a cache that silently stopped tracking the mirror
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Client/DmaBufImport.cpp
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "DmaBufImport.h"
|
||||
|
||||
#include "ClientSession.h"
|
||||
#include "../Protocol/mg_protocol_base.h"
|
||||
#include "../Transport/ITransport.h"
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
Bool ShareImportedDmaBuf(Uint32 target, Uint32 texName, Uint32 width, Uint32 height, Uint32 fourCC,
|
||||
Uint32 planeIndex, Uint32 planeCount, Int planeFd, Int stride, Int offset,
|
||||
Uint64 modifier, Bool hasModifier) {
|
||||
if (planeFd < 0 || planeCount == 0 || planeCount > kDmaBufImportMaxPlanes) return false;
|
||||
// The session's control-plane transport is where the descriptor channel lives; no session or
|
||||
// no channel (a TCP session whose aux came up without SCM_RIGHTS) is a named refusal here.
|
||||
ClientSession* session = ClientSession::Active();
|
||||
if (session == nullptr) return false;
|
||||
Transport::ITransport* transport = session->Control_Plane();
|
||||
if (transport == nullptr) return false;
|
||||
|
||||
DmaBufImportRequest request{};
|
||||
request.Magic = kDmaBufImportRequestMagic;
|
||||
request.Version = kDmaBufImportVersion;
|
||||
request.Target = target;
|
||||
request.TexName = texName;
|
||||
request.Width = width;
|
||||
request.Height = height;
|
||||
request.FourCC = fourCC;
|
||||
request.PlaneCount = planeCount;
|
||||
request.Stride[planeIndex % kDmaBufImportMaxPlanes] = stride;
|
||||
request.Offset[planeIndex % kDmaBufImportMaxPlanes] = offset;
|
||||
request.Modifier[planeIndex % kDmaBufImportMaxPlanes] = modifier;
|
||||
request.HasModifier[planeIndex % kDmaBufImportMaxPlanes] = hasModifier ? 1u : 0u;
|
||||
|
||||
MobileGLByteSpan sideband{};
|
||||
sideband.data = &request;
|
||||
sideband.size = sizeof(request);
|
||||
return transport->ShareFd(planeFd, sideband) == MOBILEGL_OK;
|
||||
}
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
@@ -0,0 +1,53 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Client/DmaBufImport.h
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// TELLING THE RENDER SERVER WHICH TEXTURE A dma-buf BACKS.
|
||||
//
|
||||
// The compositor's scene reaches the display daemon's buffers through glEGLImageTargetTexture2DOES:
|
||||
// eglCreateImageKHR already holds a dup() of every plane's descriptor, and a texture bound to that
|
||||
// image has NO storage on the server - so a framebuffer built over it is not complete and kwin aborts
|
||||
// on its own RenderTarget assertion (measured, twice: "taking target 0x8d65 over an EGLImage of
|
||||
// 1440x3200 fourcc 0x34324241 ... the render server has NOT been told which texture that dma-buf
|
||||
// backs yet"). This is the client's half of closing that: one message per plane, each carrying one
|
||||
// descriptor in the SCM_RIGHTS control data of the session transport's aux socket (ITransport::ShareFd,
|
||||
// FdPassing.h - "a dedicated AF_UNIX SOCK_DGRAM socketpair, so one sendmsg is exactly one recvmsg").
|
||||
//
|
||||
// The byte layout is restated from the server's reader rather than shared with it, for the same reason
|
||||
// the GBM frame shapes are (MG_Backend/GbmFrameChannel.h): the two ends are built into different
|
||||
// libraries from different build configurations. A change on either side is a protocol change, and the
|
||||
// magic is what makes a mismatched pair fail loudly instead of being read as plausible numbers.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
inline constexpr Uint32 kDmaBufImportRequestMagic = 0x4947474Du; // 'M','G','G','I' in memory order
|
||||
inline constexpr Uint32 kDmaBufImportVersion = 1u;
|
||||
inline constexpr Uint32 kDmaBufImportMaxPlanes = 4u;
|
||||
|
||||
struct DmaBufImportRequest {
|
||||
Uint32 Magic;
|
||||
Uint32 Version;
|
||||
Uint32 Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
|
||||
Uint32 TexName; // THIS side's texture name; the server resolves its twin
|
||||
Uint32 Width;
|
||||
Uint32 Height;
|
||||
Uint32 FourCC;
|
||||
Uint32 PlaneCount;
|
||||
Int32 Stride[kDmaBufImportMaxPlanes];
|
||||
Int32 Offset[kDmaBufImportMaxPlanes];
|
||||
Uint64 Modifier[kDmaBufImportMaxPlanes];
|
||||
Uint32 HasModifier[kDmaBufImportMaxPlanes];
|
||||
} __attribute__((packed));
|
||||
|
||||
// Shares one plane's descriptor with the server, describing the buffer in the same fields
|
||||
// drmModeAddFB2 takes. Answers false when the session has no descriptor channel (auxFd == -1:
|
||||
// SocketTransport answers MOBILEGL_ERR_UNSUPPORTED) or when the send failed - the caller keeps
|
||||
// treating the texture as having no storage, which is the honest state, rather than rendering
|
||||
// into nothing.
|
||||
Bool ShareImportedDmaBuf(Uint32 target, Uint32 texName, Uint32 width, Uint32 height, Uint32 fourCC,
|
||||
Uint32 planeIndex, Uint32 planeCount, Int planeFd, Int stride, Int offset,
|
||||
Uint64 modifier, Bool hasModifier);
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
@@ -513,6 +513,18 @@ namespace MobileGL::MG_Remote::Client {
|
||||
if (CreateIsSuspect(handle) || g_createSuspects.size() >= kCreateSuspectMax) return;
|
||||
g_createSuspects.push_back(handle);
|
||||
}
|
||||
// THE WINDOW IS A SESSION'S, NOT THE PROCESS'S. Every entry names a sequence number of the
|
||||
// session that wrote it, and a process can run several sessions one after another (eglinfo
|
||||
// terminates one display and initializes the next). An entry left by the previous session
|
||||
// made the new session's first create wait for a sequence number the new server would not
|
||||
// reach for a long time (measured: the second session hung on its first texture create, in
|
||||
// DrainCreateWindow's blocking WaitForApplied). Its answer, and the object it was about,
|
||||
// died with that session; the suspects are that session's handles too.
|
||||
void ForgetPreviousSessionCreates() {
|
||||
g_createWindowHead = 0;
|
||||
g_createWindowCount = 0;
|
||||
g_createSuspects.clear();
|
||||
}
|
||||
void ClearCreateSuspect(MG_Pipe::MGPipeHandle handle) {
|
||||
for (SizeT i = 0; i < g_createSuspects.size(); ++i) {
|
||||
if (g_createSuspects[i].Slot != handle.Slot || g_createSuspects[i].Gen != handle.Gen)
|
||||
@@ -1209,6 +1221,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
void InstallClientWireTables() {
|
||||
using namespace MG_Pipe;
|
||||
ForgetPreviousSessionCreates();
|
||||
|
||||
// DirectGLES normally installs this notice while creating its handle
|
||||
// backend. An independent client never creates that backend: without a
|
||||
|
||||
@@ -58,6 +58,11 @@ namespace MobileGL::MG_Remote {
|
||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer) == 6);
|
||||
// P12's append, on the same terms: the value is wire ABI.
|
||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::ServerOwned) == 7);
|
||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::HostFrame) == 8);
|
||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceRefusal::HostFrameOnSetWindowHandle) ==
|
||||
static_cast<Uint8>(Server::SurfaceRefusalCode::HostFrameOnSetWindowHandle));
|
||||
static_assert(Server::kHostFrameWindowBackend > Server::kServerOwnedWindowBackend,
|
||||
"kHostFrameWindowBackend must not collide with the server-owned tag");
|
||||
// The frame's refusal codes ARE the wire's SurfaceRefusal (SurfaceControlFrame.h), pinned
|
||||
// value by value so a schema edit that renumbers either side fails here.
|
||||
using Server::SurfaceRefusalCode;
|
||||
@@ -94,6 +99,8 @@ namespace MobileGL::MG_Remote {
|
||||
case SurfaceWireError::AndroidNativeWindowArrived: return "AndroidNativeWindowArrived";
|
||||
case SurfaceWireError::ServerOwnedTokenNotZero: return "ServerOwnedTokenNotZero";
|
||||
case SurfaceWireError::ServerOwnedOnSetWindowHandle: return "ServerOwnedOnSetWindowHandle";
|
||||
case SurfaceWireError::HostFrameTokenNotZero: return "HostFrameTokenNotZero";
|
||||
case SurfaceWireError::HostFrameOnSetWindowHandle: return "HostFrameOnSetWindowHandle";
|
||||
}
|
||||
return "<unknown SurfaceWireError>";
|
||||
}
|
||||
@@ -167,6 +174,15 @@ namespace MobileGL::MG_Remote {
|
||||
}
|
||||
windowKind = ::MobileGL::Wire::WindowKind::ServerOwned;
|
||||
nativeToken = 0;
|
||||
} else if (frame.windowBackend == Server::kHostFrameWindowBackend) {
|
||||
// MOBILEGL_IPC_SURFACE=host: the frames are the DISPLAY HOST's, taken by the
|
||||
// server from the host frame bridge. As for the server-owned tag - a creation
|
||||
// only, and the token is 0 whatever the frame carried.
|
||||
if (frame.kind != SurfaceControlOp::CreateWindowSurface) {
|
||||
return SurfaceWireError::HostFrameOnSetWindowHandle;
|
||||
}
|
||||
windowKind = ::MobileGL::Wire::WindowKind::HostFrame;
|
||||
nativeToken = 0;
|
||||
} else if (!WireWindowKindForWindowBackend(
|
||||
static_cast<MG_Backend::WindowBackend>(frame.windowBackend), &windowKind)) {
|
||||
return SurfaceWireError::UnknownWindowKind;
|
||||
@@ -213,6 +229,21 @@ namespace MobileGL::MG_Remote {
|
||||
*frame = decoded;
|
||||
return SurfaceWireError::None;
|
||||
}
|
||||
if (op.windowKind() == ::MobileGL::Wire::WindowKind::HostFrame) {
|
||||
// MOBILEGL_IPC_SURFACE=host. As above: a creation only, and a token of 0
|
||||
// only - anything else is a value from the peer's window system trying to
|
||||
// cross (Rule G), which is corruption rather than a refusal.
|
||||
if (kind != SurfaceControlOp::CreateWindowSurface) {
|
||||
return SurfaceWireError::HostFrameOnSetWindowHandle;
|
||||
}
|
||||
if (op.nativeToken() != 0) {
|
||||
return SurfaceWireError::HostFrameTokenNotZero;
|
||||
}
|
||||
decoded.windowBackend = Server::kHostFrameWindowBackend;
|
||||
decoded.nativeToken = 0;
|
||||
*frame = decoded;
|
||||
return SurfaceWireError::None;
|
||||
}
|
||||
if (op.windowKind() == ::MobileGL::Wire::WindowKind::AndroidNativeWindow) {
|
||||
// The one refusal that is not corruption: the window kind is LEGAL and the token
|
||||
// is an ANativeWindow*, which names memory in the CLIENT's process. Real window
|
||||
@@ -225,10 +256,10 @@ namespace MobileGL::MG_Remote {
|
||||
}
|
||||
MG_Backend::WindowBackend backend;
|
||||
if (!WindowBackendForWireWindowKind(op.windowKind(), &backend)) {
|
||||
// THE UPPER BOUND IS THE LAST APPENDED KIND (P12: ServerOwned), for the reason
|
||||
// THE UPPER BOUND IS THE LAST APPENDED KIND (P12: HostFrame), for the reason
|
||||
// SurfaceControlOpForWireKind's bound gives.
|
||||
return ::flatbuffers::IsOutRange(op.windowKind(), ::MobileGL::Wire::WindowKind::None,
|
||||
::MobileGL::Wire::WindowKind::ServerOwned)
|
||||
::MobileGL::Wire::WindowKind::HostFrame)
|
||||
? SurfaceWireError::UnknownWindowKind
|
||||
: SurfaceWireError::WindowKindNamesNoBackend;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ namespace MobileGL::MG_Remote {
|
||||
// CreateWindowSurface only, and only with nativeToken 0:
|
||||
ServerOwnedTokenNotZero, // -> Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}, latched
|
||||
ServerOwnedOnSetWindowHandle, // -> a NAMED REFUSAL (reply ok=false), not a latch
|
||||
// MOBILEGL_IPC_SURFACE=host, the same two rules for the other on-screen shape:
|
||||
HostFrameTokenNotZero, // -> Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}, latched
|
||||
HostFrameOnSetWindowHandle, // -> a NAMED REFUSAL (reply ok=false), not a latch
|
||||
};
|
||||
|
||||
const char* SurfaceWireErrorName(SurfaceWireError error);
|
||||
|
||||
@@ -341,11 +341,12 @@ enum class WindowKind : uint8_t {
|
||||
Pbuffer = 5,
|
||||
MetalLayer = 6,
|
||||
ServerOwned = 7,
|
||||
HostFrame = 8,
|
||||
MIN = None,
|
||||
MAX = ServerOwned
|
||||
MAX = HostFrame
|
||||
};
|
||||
|
||||
inline const WindowKind (&EnumValuesWindowKind())[8] {
|
||||
inline const WindowKind (&EnumValuesWindowKind())[9] {
|
||||
static const WindowKind values[] = {
|
||||
WindowKind::None,
|
||||
WindowKind::AndroidNativeWindow,
|
||||
@@ -354,13 +355,14 @@ inline const WindowKind (&EnumValuesWindowKind())[8] {
|
||||
WindowKind::Surfaceless,
|
||||
WindowKind::Pbuffer,
|
||||
WindowKind::MetalLayer,
|
||||
WindowKind::ServerOwned
|
||||
WindowKind::ServerOwned,
|
||||
WindowKind::HostFrame
|
||||
};
|
||||
return values;
|
||||
}
|
||||
|
||||
inline const char * const *EnumNamesWindowKind() {
|
||||
static const char * const names[9] = {
|
||||
static const char * const names[10] = {
|
||||
"None",
|
||||
"AndroidNativeWindow",
|
||||
"X11",
|
||||
@@ -369,13 +371,14 @@ inline const char * const *EnumNamesWindowKind() {
|
||||
"Pbuffer",
|
||||
"MetalLayer",
|
||||
"ServerOwned",
|
||||
"HostFrame",
|
||||
nullptr
|
||||
};
|
||||
return names;
|
||||
}
|
||||
|
||||
inline const char *EnumNameWindowKind(WindowKind e) {
|
||||
if (::flatbuffers::IsOutRange(e, WindowKind::None, WindowKind::ServerOwned)) return "";
|
||||
if (::flatbuffers::IsOutRange(e, WindowKind::None, WindowKind::HostFrame)) return "";
|
||||
const size_t index = static_cast<size_t>(e);
|
||||
return EnumNamesWindowKind()[index];
|
||||
}
|
||||
@@ -386,35 +389,38 @@ enum class SurfaceRefusal : uint8_t {
|
||||
NoServerWindow = 2,
|
||||
SurfaceModeMismatch = 3,
|
||||
ServerOwnedOnSetWindowHandle = 4,
|
||||
HostFrameOnSetWindowHandle = 5,
|
||||
MIN = None,
|
||||
MAX = ServerOwnedOnSetWindowHandle
|
||||
MAX = HostFrameOnSetWindowHandle
|
||||
};
|
||||
|
||||
inline const SurfaceRefusal (&EnumValuesSurfaceRefusal())[5] {
|
||||
inline const SurfaceRefusal (&EnumValuesSurfaceRefusal())[6] {
|
||||
static const SurfaceRefusal values[] = {
|
||||
SurfaceRefusal::None,
|
||||
SurfaceRefusal::NoServerDisplay,
|
||||
SurfaceRefusal::NoServerWindow,
|
||||
SurfaceRefusal::SurfaceModeMismatch,
|
||||
SurfaceRefusal::ServerOwnedOnSetWindowHandle
|
||||
SurfaceRefusal::ServerOwnedOnSetWindowHandle,
|
||||
SurfaceRefusal::HostFrameOnSetWindowHandle
|
||||
};
|
||||
return values;
|
||||
}
|
||||
|
||||
inline const char * const *EnumNamesSurfaceRefusal() {
|
||||
static const char * const names[6] = {
|
||||
static const char * const names[7] = {
|
||||
"None",
|
||||
"NoServerDisplay",
|
||||
"NoServerWindow",
|
||||
"SurfaceModeMismatch",
|
||||
"ServerOwnedOnSetWindowHandle",
|
||||
"HostFrameOnSetWindowHandle",
|
||||
nullptr
|
||||
};
|
||||
return names;
|
||||
}
|
||||
|
||||
inline const char *EnumNameSurfaceRefusal(SurfaceRefusal e) {
|
||||
if (::flatbuffers::IsOutRange(e, SurfaceRefusal::None, SurfaceRefusal::ServerOwnedOnSetWindowHandle)) return "";
|
||||
if (::flatbuffers::IsOutRange(e, SurfaceRefusal::None, SurfaceRefusal::HostFrameOnSetWindowHandle)) return "";
|
||||
const size_t index = static_cast<size_t>(e);
|
||||
return EnumNamesSurfaceRefusal()[index];
|
||||
}
|
||||
|
||||
@@ -274,6 +274,12 @@ enum WindowKind : ubyte {
|
||||
// SurfaceView) and has none of its own to name, so `nativeToken` is always 0 and no pointer
|
||||
// crosses (Rule G/H). Legal on CreateWindowSurface only; SetWindowHandle may not name it.
|
||||
ServerOwned = 7,
|
||||
// P12, appended: the OTHER on-screen shape, and its mirror image. The client names no
|
||||
// window here either - but the frames are the DISPLAY HOST's: it allocates them, offers
|
||||
// each one over the host frame bridge, and puts them on the glass itself, while the
|
||||
// server draws into them. `nativeToken` is always 0, as for ServerOwned, and it is legal
|
||||
// on CreateWindowSurface only.
|
||||
HostFrame = 8,
|
||||
}
|
||||
|
||||
// P12, appended: WHY A SURFACE OP WAS DECLINED, so the client can name the refusal in its own log
|
||||
@@ -285,6 +291,7 @@ enum SurfaceRefusal : ubyte {
|
||||
NoServerWindow = 2, // the server owns a display, but no window came within its wait
|
||||
SurfaceModeMismatch = 3, // the session's surface mode (on-screen / offscreen) is the other one
|
||||
ServerOwnedOnSetWindowHandle = 4, // SetWindowHandle named the server's own window
|
||||
HostFrameOnSetWindowHandle = 5, // SetWindowHandle named the display host's frames
|
||||
}
|
||||
|
||||
table SurfaceOp {
|
||||
|
||||
@@ -404,6 +404,15 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// was wanted, recorded here so a future swap-without-present is looked for rather than
|
||||
// assumed impossible. Presents() is exposed for a lane that wants to make the comparison.
|
||||
table->Present();
|
||||
// A HOST-FRAMED SURFACE IS PRESENTED BY TELLING THE HOST, not by the swap above (which
|
||||
// swaps the pbuffer the context was made with). BackendObject::SwapEGLBuffers is the
|
||||
// only other route to PresentHostFrame and a split client never reaches it, so this is
|
||||
// where the display host learns the frame is drawn - before the credit goes back, so a
|
||||
// client released by it is released by a frame that is on its way to the glass.
|
||||
if (m_backend != nullptr) {
|
||||
if (!m_backend->PresentHostFrameIfCurrent())
|
||||
MGLOG_E_ONCE("OnPresent: the host frame was not presented (PresentHostFrame failed; see above)");
|
||||
}
|
||||
++m_presents;
|
||||
// FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side
|
||||
// present credit, so the client sends 0 and the frame count on this side IS the serial.
|
||||
|
||||
@@ -287,6 +287,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
m_surfaceMode = SessionSurfaceMode::None;
|
||||
m_holdsWindowLease = false;
|
||||
m_serverOwnedSurfaces.clear();
|
||||
m_hostFrameSurfaces.clear();
|
||||
m_windowLostRequested.store(false, std::memory_order_release);
|
||||
m_serverWindowsLost.store(0, std::memory_order_release);
|
||||
m_testLogFloodLines = ApplyLogFloodLinesForTest();
|
||||
@@ -1338,6 +1339,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// P12 (D3): the SERVER's window, asked for by a headless client. Before the unpack,
|
||||
// which would refuse the frame-local tag as a WindowBackend out of range.
|
||||
if (frame.windowBackend == kServerOwnedWindowBackend) return ApplyServerOwnedWindowSurface(backend, frame);
|
||||
// The mirror image: the frames are the display host's, and it presents them.
|
||||
if (frame.windowBackend == kHostFrameWindowBackend) return ApplyHostFrameWindowSurface(backend, frame);
|
||||
MG_Backend::WindowHandle window;
|
||||
if (!UnpackWindowHandle(frame, &window)) {
|
||||
frame.ok = false;
|
||||
@@ -1445,6 +1448,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const EGLSurface surface = HandleFromToken<EGLSurface>(frame.surface);
|
||||
backend->ReleaseEGLSurface(surface);
|
||||
ForgetServerOwnedSurface(surface);
|
||||
ForgetHostFrameSurface(surface);
|
||||
// N-3: a released surface the held tuple names may have taken the context with it
|
||||
// (BackendObject::ReleaseEGLSurface -> OnEGLSurfaceReleased -> DestroyEGLContext once
|
||||
// nothing holds it current). Forgetting when the base class only DEFERRED the destroy
|
||||
@@ -1658,6 +1662,52 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
MobileGLResult ServerLoop::ApplyHostFrameWindowSurface(MG_Backend::BackendObject* backend,
|
||||
SurfaceControlFrame& frame) {
|
||||
frame.ok = false;
|
||||
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::None);
|
||||
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
|
||||
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
|
||||
// Both on-screen shapes are the same session mode: one rendering path per session, and
|
||||
// this is the on-screen one whichever side owns the window.
|
||||
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) {
|
||||
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
|
||||
MGLOG_E("MG_Remote server: SurfaceModeMismatch - a HostFrame CreateWindowSurface (seq %llu, "
|
||||
"%ux%u) in a session whose surface mode is %s. Refused, the session is not latched",
|
||||
static_cast<unsigned long long>(frame.seq), wantWidth, wantHeight,
|
||||
SessionSurfaceModeName(m_surfaceMode));
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
const EGLSurface surface = HandleFromToken<EGLSurface>(frame.surface);
|
||||
// THE SUBSTITUTION, the other way round from ServerOwned: nothing of the client's and
|
||||
// nothing of the server's goes into the backend call - the backend takes the frames from
|
||||
// the display host, and every swap hands the next one back to it.
|
||||
frame.ok = backend->CreateEGLHostFrameSurface(surface, wantWidth, wantHeight);
|
||||
if (!frame.ok) {
|
||||
MGLOG_E("MG_Remote server: the backend could not take the display host frames (seq %llu, %ux%u)",
|
||||
static_cast<unsigned long long>(frame.seq), wantWidth, wantHeight);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
ForgetCurrentTuple();
|
||||
m_surfaceMode = SessionSurfaceMode::OnScreen;
|
||||
if (!IsHostFrameSurface(surface)) m_hostFrameSurfaces.push_back(surface);
|
||||
frame.width = static_cast<Int>(wantWidth);
|
||||
frame.height = static_cast<Int>(wantHeight);
|
||||
MGLOG_I("MG_Remote server: surface=window %ux%u owner=host (HostFrame seq %llu)", wantWidth,
|
||||
wantHeight, static_cast<unsigned long long>(frame.seq));
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
Bool ServerLoop::IsHostFrameSurface(EGLSurface surface) const {
|
||||
return std::find(m_hostFrameSurfaces.begin(), m_hostFrameSurfaces.end(), surface) !=
|
||||
m_hostFrameSurfaces.end();
|
||||
}
|
||||
|
||||
void ServerLoop::ForgetHostFrameSurface(EGLSurface surface) {
|
||||
m_hostFrameSurfaces.erase(std::remove(m_hostFrameSurfaces.begin(), m_hostFrameSurfaces.end(), surface),
|
||||
m_hostFrameSurfaces.end());
|
||||
}
|
||||
|
||||
Bool ServerLoop::IsServerOwnedSurface(EGLSurface surface) const {
|
||||
return std::find(m_serverOwnedSurfaces.begin(), m_serverOwnedSurfaces.end(), surface) !=
|
||||
m_serverOwnedSurfaces.end();
|
||||
@@ -1770,6 +1820,25 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return rc == MOBILEGL_OK && frame.ok;
|
||||
}
|
||||
|
||||
ServerOwnedWindowReply ServerCreateHostFrameWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
// One frame, and no SetWindowHandle in front of it: the client names no window, and this
|
||||
// side does not make one - the frames are the display host's, taken from the bridge.
|
||||
SurfaceControlFrame frame;
|
||||
frame.kind = SurfaceControlOp::CreateWindowSurface;
|
||||
frame.surface = TokenFromHandle(surface);
|
||||
frame.windowBackend = kHostFrameWindowBackend;
|
||||
frame.nativeToken = 0;
|
||||
frame.width = static_cast<Int>(width);
|
||||
frame.height = static_cast<Int>(height);
|
||||
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
|
||||
ServerOwnedWindowReply reply;
|
||||
reply.transport = rc;
|
||||
reply.ok = rc == MOBILEGL_OK && frame.ok;
|
||||
reply.refusal = static_cast<SurfaceRefusalCode>(frame.refusal);
|
||||
reply.width = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
|
||||
reply.height = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
|
||||
return reply;
|
||||
}
|
||||
ServerOwnedWindowReply ServerCreateServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
// P12 (D1/D2): ONE frame, and no SetWindowHandle in front of it - the window is the
|
||||
// server's, so there is nothing of the client's to name. The frame-local tag is what the
|
||||
|
||||
@@ -459,6 +459,9 @@ namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
// P12. The ServerOwned arm of the CreateWindowSurface dispatch (D3, D4).
|
||||
MobileGLResult ApplyServerOwnedWindowSurface(MG_Backend::BackendObject* backend, SurfaceControlFrame& frame);
|
||||
MobileGLResult ApplyHostFrameWindowSurface(MG_Backend::BackendObject* backend, SurfaceControlFrame& frame);
|
||||
Bool IsHostFrameSurface(EGLSurface surface) const;
|
||||
void ForgetHostFrameSurface(EGLSurface surface);
|
||||
// P12 (D6), apply thread: the lost window's backend surface goes, the lease ends, and the
|
||||
// session latches ServerWindowLost. Runs from PumpControlRequest when Detach asked.
|
||||
void ReleaseLostServerWindow();
|
||||
@@ -471,6 +474,10 @@ namespace MobileGL::MG_Remote::Server {
|
||||
void ForgetServerOwnedSurface(EGLSurface surface);
|
||||
// Apply thread only: this session's surfaces created on the server's window; reset by Start().
|
||||
std::vector<EGLSurface> m_serverOwnedSurfaces;
|
||||
// The surfaces whose drawing target is a frame the display host owns. Kept apart from
|
||||
// the server-owned list because the two are different paths with different owners,
|
||||
// and a resize or a release has to reach the right one.
|
||||
std::vector<EGLSurface> m_hostFrameSurfaces;
|
||||
// ServerDisplay's lost hook: called under the display's lock from Detach's thread. Sets the
|
||||
// request and rings the apply thread's bell; never blocks.
|
||||
static void ServerWindowLostThunk(void* self);
|
||||
@@ -659,6 +666,10 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Uint32 height = 0;
|
||||
};
|
||||
ServerOwnedWindowReply ServerCreateServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
// The host-framed on-screen shape (MOBILEGL_IPC_SURFACE=host): the client names no window
|
||||
// either, but the frames come from the display host over the host frame bridge, and the
|
||||
// host presents them. The reply carries the size the surface was made at.
|
||||
ServerOwnedWindowReply ServerCreateHostFrameWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
// P12 review fix: ResizeWindowSurface for a surface created on the server's window. The server
|
||||
// resizes its WINDOW (a geometry request) and replies with the window's real extent.
|
||||
ServerOwnedWindowReply ServerResizeServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
|
||||
@@ -1035,12 +1035,32 @@ private:
|
||||
};
|
||||
|
||||
bool Reap() {
|
||||
const bool reaped = m_handoffShape == Handoff::Thread ? ReapSessionThread()
|
||||
: ReapActiveSession(m_active, m_sessionsFaulted);
|
||||
if (m_active <= 0 && m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; }
|
||||
if (m_handoffShape == Handoff::Thread) {
|
||||
const bool reaped = ReapSessionThread();
|
||||
if (m_active <= 0 && m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; }
|
||||
return reaped;
|
||||
}
|
||||
// The fork shape: every live child is asked, and a reaped one takes its hand-off with it.
|
||||
bool reaped = false;
|
||||
for (auto child = m_children.begin(); child != m_children.end();) {
|
||||
if (ReapActiveSession(child->pid, m_sessionsFaulted)) {
|
||||
if (child->handoff >= 0) ::close(child->handoff);
|
||||
child = m_children.erase(child);
|
||||
reaped = true;
|
||||
} else {
|
||||
++child;
|
||||
}
|
||||
}
|
||||
m_active = m_children.empty() ? -1 : m_children.back().pid;
|
||||
return reaped;
|
||||
}
|
||||
|
||||
// Whether an authenticated Hello has to be told Busy: the thread shape serves one session, the
|
||||
// fork shape up to kMaxForkedSessions side by side.
|
||||
bool SessionsFull() const {
|
||||
return m_handoffShape == Handoff::Thread ? m_active > 0 : m_children.size() >= kMaxForkedSessions;
|
||||
}
|
||||
|
||||
// P12 (D5): the thread shape's reap - the same line a forked child's reap prints, and the same
|
||||
// count, then the latch put back for the next session (ResetSessionLatch: the thread took no
|
||||
// process with it, so the latch it armed and maybe latched is still this process's).
|
||||
@@ -1082,7 +1102,7 @@ private:
|
||||
// make the loop wait here; a DataBind's routing takes one look (Judge, fix round).
|
||||
void WaitOutAnExitingSession() {
|
||||
const auto reapDeadline = Clock::now() + std::chrono::milliseconds(50);
|
||||
while (m_active > 0 && Clock::now() < reapDeadline) {
|
||||
while (SessionsFull() && Clock::now() < reapDeadline) {
|
||||
if (Reap()) break;
|
||||
::usleep(1000);
|
||||
}
|
||||
@@ -1176,7 +1196,15 @@ private:
|
||||
// either has cut its hand-off already (the send below fails and is refused by name) or
|
||||
// refuses what is queued on its way out (ExitBeforeAccept / CloseHandoffRefusingQueued).
|
||||
Reap();
|
||||
if (m_active <= 0) {
|
||||
// WHICH SESSION IT BELONGS TO. The thread shape has one; the fork shape knows every
|
||||
// child's nonce (it minted them, Fork) and hands the connection to the child it names.
|
||||
int handoff = m_active > 0 ? m_handoff : -1;
|
||||
if (m_handoffShape == Handoff::Fork) {
|
||||
handoff = -1;
|
||||
for (const auto& child : m_children)
|
||||
if (ConstantTimeNonceMatch(child.nonce, nonce)) handoff = child.handoff;
|
||||
}
|
||||
if (handoff < 0) {
|
||||
// StreamLink's frames may already follow the DataBind; read them away so the close
|
||||
// does not become an RST that discards the refusal.
|
||||
DrainUnread(fd);
|
||||
@@ -1186,7 +1214,7 @@ private:
|
||||
// The send is MSG_DONTWAIT and the child cuts its end the moment its own data
|
||||
// connection is bound (CloseHandoffRefusingQueued), so this either queues the
|
||||
// descriptor for a child that will read or refuse it, or fails and is refused here.
|
||||
if (m_handoff >= 0 && FdPassing::SendFd(m_handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)},
|
||||
if (FdPassing::SendFd(handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)},
|
||||
/*dontWait=*/true) == MOBILEGL_OK) {
|
||||
// The child holds its own descriptor now; ours goes without a shutdown(2), which
|
||||
// would disconnect the child's too.
|
||||
@@ -1217,9 +1245,11 @@ private:
|
||||
}
|
||||
m_backoff.NoteSuccess(peer.address);
|
||||
WaitOutAnExitingSession();
|
||||
if (m_active > 0) {
|
||||
if (SessionsFull()) {
|
||||
// The frame is consumed, so closing cannot RST the refusal away.
|
||||
Refuse(*connection, Protocol::RefuseCode::Busy, "one session is already active");
|
||||
Refuse(*connection, Protocol::RefuseCode::Busy,
|
||||
m_handoffShape == Handoff::Thread ? "one session is already active"
|
||||
: "the session limit is reached");
|
||||
return;
|
||||
}
|
||||
Fork(std::move(connection), payload);
|
||||
@@ -1235,12 +1265,25 @@ private:
|
||||
StartSessionThread(std::move(connection), hello, pair);
|
||||
return;
|
||||
}
|
||||
// The child's data nonce, minted HERE so this supervisor can route the child's DataBind to it
|
||||
// among other live sessions; the child adopts it instead of minting its own.
|
||||
LiveChild live;
|
||||
if (SocketTransport::MintNonce(live.nonce, sizeof(live.nonce)) != MOBILEGL_OK) {
|
||||
::close(pair[0]);
|
||||
::close(pair[1]);
|
||||
Refuse(*connection, Protocol::RefuseCode::Busy, "could not mint the session's data nonce");
|
||||
return;
|
||||
}
|
||||
std::fflush(nullptr);
|
||||
const pid_t supervisor = ::getpid();
|
||||
const pid_t child = ::fork();
|
||||
if (child == 0) {
|
||||
// P12 (D8): the child dies with this supervisor, not after its client has gone.
|
||||
DieWithSupervisor(supervisor);
|
||||
Server::PresetDataNonceForThisProcess(live.nonce, sizeof(live.nonce));
|
||||
// The other live sessions' hand-offs are the supervisor's.
|
||||
for (const auto& other : m_children)
|
||||
if (other.handoff >= 0) ::close(other.handoff);
|
||||
// The child owns this one connection and its end of the hand-off. Every OTHER pending
|
||||
// peer's descriptor is closed here, or the supervisor's refusal of that peer would not
|
||||
// end its connection while this session lives.
|
||||
@@ -1260,9 +1303,11 @@ private:
|
||||
Refuse(*connection, Protocol::RefuseCode::Busy, "could not create session child");
|
||||
return;
|
||||
}
|
||||
live.pid = child;
|
||||
live.handoff = pair[0];
|
||||
m_children.push_back(live);
|
||||
m_active = child;
|
||||
::close(pair[1]);
|
||||
m_handoff = pair[0];
|
||||
connection->CloseLocalCopy();
|
||||
}
|
||||
|
||||
@@ -1310,10 +1355,21 @@ private:
|
||||
std::atomic<bool> m_sessionDone{false};
|
||||
std::atomic<int> m_sessionExit{0};
|
||||
unsigned long m_sessionOrdinal = 0;
|
||||
// A live session: the newest child's pid for the fork shape, this process's for the thread shape.
|
||||
pid_t m_active = -1;
|
||||
// The supervisor's end of the live child's data-connection hand-off (PH-7 (4)); -1 while no
|
||||
// session is live. It goes with the child: a reaped session can be handed nothing.
|
||||
// The thread shape's end of its session's data-connection hand-off (PH-7 (4)); -1 while no
|
||||
// session is live. It goes with the session: a reaped session can be handed nothing.
|
||||
int m_handoff = -1;
|
||||
// THE FORK SHAPE'S LIVE SESSIONS, side by side: a desktop is a compositor AND the GL programs
|
||||
// run inside it, each its own client with its own session. Each child keeps its end of the
|
||||
// hand-off and the data nonce this supervisor minted for it, which is how a DataBind finds it.
|
||||
struct LiveChild {
|
||||
pid_t pid = -1;
|
||||
int handoff = -1;
|
||||
std::uint8_t nonce[kDataNonceBytes] = {};
|
||||
};
|
||||
static constexpr std::size_t kMaxForkedSessions = 8;
|
||||
std::vector<LiveChild> m_children;
|
||||
std::vector<PendingPeer> m_pending;
|
||||
std::vector<PendingPeer> m_ready;
|
||||
// Accepting resumes at this time after accept(2) ran out of descriptors or memory.
|
||||
|
||||
@@ -42,6 +42,24 @@
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
namespace {
|
||||
Uint8 g_presetDataNonce[Transport::kDataNonceBytes] = {};
|
||||
Bool g_presetDataNonceSet = false;
|
||||
|
||||
Bool TakePresetDataNonce(Uint8* out, SizeT size) {
|
||||
if (!g_presetDataNonceSet || size != sizeof(g_presetDataNonce)) return false;
|
||||
std::memcpy(out, g_presetDataNonce, size);
|
||||
g_presetDataNonceSet = false;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void PresetDataNonceForThisProcess(const Uint8* nonce, SizeT size) {
|
||||
if (nonce == nullptr || size != sizeof(g_presetDataNonce)) return;
|
||||
std::memcpy(g_presetDataNonce, nonce, size);
|
||||
g_presetDataNonceSet = true;
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
// F1 (P7 wave 2). THE ONLY KNOB THAT CAN MAKE THE FIRST CapsSnapshot LATE.
|
||||
@@ -716,7 +734,9 @@ namespace MobileGL::MG_Remote::Server {
|
||||
if (socket == nullptr || !socket->IsTcp() || !m_dataSource)
|
||||
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms,
|
||||
"stream needs a TCP control connection and a data-connection source");
|
||||
created = Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce));
|
||||
created = TakePresetDataNonce(dataNonce, sizeof(dataNonce))
|
||||
? MOBILEGL_OK
|
||||
: Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce));
|
||||
if (created != MOBILEGL_OK) {
|
||||
// (F fix round) The peer is told by name instead of seeing a bare close. The
|
||||
// return value stays the fault it is - exit 67 in ServerMain, counted in
|
||||
|
||||
@@ -63,6 +63,13 @@
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
// THE DATA NONCE A FORKED SESSION CHILD IS BORN WITH. The TCP supervisor serves sessions side by
|
||||
// side and routes each data connection (DataBind) to the child whose nonce it presents, so it has
|
||||
// to know every child's nonce: it mints the nonce itself just before fork() and the child adopts
|
||||
// it here instead of minting its own. One use: the session that takes it clears it. Never set in
|
||||
// a process that runs more than one session, so the shared storage is the child's alone.
|
||||
void PresetDataNonceForThisProcess(const Uint8* nonce, SizeT size);
|
||||
|
||||
class ServerSession {
|
||||
public:
|
||||
static ServerSession* Active();
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
case SurfaceRefusalCode::NoServerWindow: return "NoServerWindow";
|
||||
case SurfaceRefusalCode::SurfaceModeMismatch: return "SurfaceModeMismatch";
|
||||
case SurfaceRefusalCode::ServerOwnedOnSetWindowHandle: return "ServerOwnedOnSetWindowHandle";
|
||||
case SurfaceRefusalCode::HostFrameOnSetWindowHandle: return "HostFrameOnSetWindowHandle";
|
||||
}
|
||||
return "<unknown SurfaceRefusalCode>";
|
||||
}
|
||||
|
||||
@@ -87,6 +87,14 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// substitutes its own window there; no pointer crosses the wire (Rule G/H).
|
||||
inline constexpr Int kServerOwnedWindowBackend = 0x10000;
|
||||
|
||||
// The OTHER on-screen shape, and its mirror image. With MOBILEGL_IPC_SURFACE=server the
|
||||
// client has no window and the SERVER's own goes into the backend call; with =host the
|
||||
// client still has no window, but the frames are the display host's - it allocates them,
|
||||
// offers each one over the host frame bridge, and puts them on the glass itself. Same
|
||||
// kind of frame-local tag as the one above, and for the same reason: it names no
|
||||
// WindowBackend of MG_Backend's enum, which is in the pull build (gate G1).
|
||||
inline constexpr Int kHostFrameWindowBackend = 0x10001;
|
||||
|
||||
// P12. Why the server declined a surface op, carried back in the reply half (`refusal`). The
|
||||
// values ARE the wire's SurfaceRefusal (protocol.fbs); SurfaceOpCodec pins the agreement with
|
||||
// static_asserts, the way it pins SurfaceControlOp against SurfaceOpKind. None of them latches
|
||||
@@ -97,6 +105,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
NoServerWindow = 2, // a display, but no window within the wait
|
||||
SurfaceModeMismatch = 3, // D4: the session's surface mode is the other one
|
||||
ServerOwnedOnSetWindowHandle = 4, // SetWindowHandle named the server's own window
|
||||
HostFrameOnSetWindowHandle = 5, // SetWindowHandle named the display host's frames
|
||||
};
|
||||
|
||||
const char* SurfaceRefusalCodeName(SurfaceRefusalCode code);
|
||||
|
||||
@@ -46,6 +46,13 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
class ITransport {
|
||||
public:
|
||||
virtual ~ITransport() = default;
|
||||
// THE AUX DESCRIPTOR CHANNEL, IF THIS TRANSPORT HAS ONE. SCM_RIGHTS needs a socket of its own
|
||||
// (SocketTransport.h:77), and a caller that wants to hand a dma-buf to the server has to name it:
|
||||
// the session's transport is held as an ITransport, so the seam has to answer here rather than in a
|
||||
// downcast. -1 means "this transport carries no descriptors", which is a legal state a caller must
|
||||
// degrade on - the texture then simply has no storage rather than the process failing.
|
||||
virtual int AuxFd() const { return -1; }
|
||||
|
||||
|
||||
ITransport(const ITransport&) = delete;
|
||||
ITransport& operator=(const ITransport&) = delete;
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
// For `sm`'s process discipline and for the arm-proof gate: the fds this
|
||||
// transport owns, so a test can assert the child inherited exactly these.
|
||||
int StreamFd() const { return m_streamFd; }
|
||||
int AuxFd() const { return m_auxFd; }
|
||||
int AuxFd() const override { return m_auxFd; }
|
||||
bool IsTcp() const { return m_tcp; }
|
||||
// TCP's second connection belongs exclusively to the data plane.
|
||||
// Taking it disables all descriptor passing on this transport.
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
|
||||
#include "Core.h"
|
||||
#include <EGL/eglext.h>
|
||||
// An imported buffer is held by descriptors, and the copy an image takes of one is the platform's
|
||||
// own dup()/close(): unistd.h's where the import exists, io.h's on the Windows build that compiles
|
||||
// this file but never sees EGL_LINUX_DMA_BUF_EXT.
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <cerrno>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State {
|
||||
@@ -30,6 +39,54 @@ namespace MobileGL {
|
||||
return Nullopt;
|
||||
}
|
||||
|
||||
// Taking and closing the descriptor an image owns, per platform. Named rather
|
||||
// than spelled inline so the ownership rule reads the same on both.
|
||||
#if defined(_WIN32)
|
||||
Int DuplicateImageDescriptor(Int fd) { return _dup(fd); }
|
||||
void CloseImageDescriptor(Int fd) { _close(fd); }
|
||||
#else
|
||||
Int DuplicateImageDescriptor(Int fd) { return ::dup(fd); }
|
||||
void CloseImageDescriptor(Int fd) { ::close(fd); }
|
||||
#endif
|
||||
|
||||
// ONE PLANE'S ATTRIBUTE SPELLINGS, in plane order. Plane 0's fd/offset/pitch triple
|
||||
// is EGL_EXT_image_dma_buf_import's; plane 3's triple and every MODIFIER_LO/HI pair
|
||||
// belong to EGL_EXT_image_dma_buf_import_modifiers.
|
||||
struct DmaBufPlaneAttributes {
|
||||
EGLint Fd;
|
||||
EGLint Offset;
|
||||
EGLint Pitch;
|
||||
EGLint ModifierLo;
|
||||
EGLint ModifierHi;
|
||||
};
|
||||
|
||||
constexpr DmaBufPlaneAttributes kDmaBufPlaneAttributes[EGLContext::kMaxImagePlanes] = {
|
||||
{EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE0_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT},
|
||||
{EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT},
|
||||
{EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE2_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT},
|
||||
{EGL_DMA_BUF_PLANE3_FD_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT},
|
||||
};
|
||||
|
||||
// The EGL spelling of one plane attribute, for the single line a refused image gets:
|
||||
// the attribute list is gone by the time the caller reads eglGetError.
|
||||
String DmaBufPlaneAttributeName(Int plane, const char* role) {
|
||||
return std::format("EGL_DMA_BUF_PLANE{}_{}", plane, role);
|
||||
}
|
||||
|
||||
// Whether the caller mentioned this plane at all, so that a description with a hole
|
||||
// in it can be refused by name instead of silently losing the planes past the hole.
|
||||
Bool HasAnyPlaneAttribute(const EGLAttrib* attribList, Int plane) {
|
||||
const DmaBufPlaneAttributes& attributes = kDmaBufPlaneAttributes[plane];
|
||||
return ParseAttribValue(attribList, attributes.Fd) || ParseAttribValue(attribList, attributes.Offset) ||
|
||||
ParseAttribValue(attribList, attributes.Pitch) ||
|
||||
ParseAttribValue(attribList, attributes.ModifierLo) ||
|
||||
ParseAttribValue(attribList, attributes.ModifierHi);
|
||||
}
|
||||
|
||||
EGLint QueryDefaultX11VisualId() {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
const char* displayName = std::getenv("DISPLAY");
|
||||
@@ -292,6 +349,9 @@ namespace MobileGL {
|
||||
|
||||
for (auto imageIt = m_images.begin(); imageIt != m_images.end();) {
|
||||
if (imageIt->second.Display == display) {
|
||||
// The display is going away and takes its images with it, so the descriptors
|
||||
// they own go too: an image erased without this leaks an fd per plane.
|
||||
ReleaseImageDescriptors(imageIt->second);
|
||||
imageIt = m_images.erase(imageIt);
|
||||
} else {
|
||||
++imageIt;
|
||||
@@ -334,6 +394,16 @@ namespace MobileGL {
|
||||
return displayObject && displayObject->Initialized;
|
||||
}
|
||||
|
||||
Bool EGLContext::GetDisplayNative(EGLDisplayHandle display, Uint64* nativeDisplayKey,
|
||||
EGLenum* platform) const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
const auto* displayObject = TryGetDisplay(display);
|
||||
if (!displayObject) return false;
|
||||
*nativeDisplayKey = displayObject->NativeDisplayKey;
|
||||
*platform = displayObject->Platform;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool EGLContext::InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
auto* displayObject = TryGetDisplay(display);
|
||||
@@ -417,6 +487,12 @@ namespace MobileGL {
|
||||
for (SizeT i = 0; attribList[i] != EGL_NONE; i += 2) {
|
||||
const EGLint attr = attribList[i];
|
||||
const EGLint requested = attribList[i + 1];
|
||||
// WHAT DIES HERE, exactly: with an attribute list present this loop is where
|
||||
// eglChooseConfig faults (a NULL list survives it and answers two configs),
|
||||
// and the fault is one instruction inside the attribute switch. These two
|
||||
// lines name the attribute and the config before either is dereferenced.
|
||||
MGLOG_I("chooseConfig: attr=0x%04x requested=%d config=%p", attr, requested,
|
||||
static_cast<const void*>(cfg));
|
||||
if (requested == EGL_DONT_CARE) {
|
||||
continue;
|
||||
}
|
||||
@@ -625,7 +701,13 @@ namespace MobileGL {
|
||||
SetError(EGL_NOT_INITIALIZED);
|
||||
return nullptr;
|
||||
}
|
||||
if (!ValidateConfigOnDisplay(display, config)) {
|
||||
// EGL_KHR_no_config_context: a context with NO config of its own. It is the shape a
|
||||
// compositor asks for - its contexts are used with whatever framebuffer the frame
|
||||
// needs, so pinning one config to the context would be a claim about the future the
|
||||
// caller does not have - and EGL spells the request as this null handle. A context
|
||||
// that does name a config is validated exactly as before.
|
||||
const Bool noConfigContext = (config == nullptr);
|
||||
if (!noConfigContext && !ValidateConfigOnDisplay(display, config)) {
|
||||
SetError(EGL_BAD_CONFIG);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1091,6 +1173,18 @@ namespace MobileGL {
|
||||
return surface;
|
||||
}
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: the registration is the server-owned one verbatim, because
|
||||
// what makes a window surface host-framed is not its EGL state but where its pixels go -
|
||||
// and that is decided one layer down, by the backend the creation calls next.
|
||||
EGLContext::EGLSurfaceHandle EGLContext::CreateHostFrameWindowSurface(EGLDisplayHandle display,
|
||||
EGLConfigHandle config,
|
||||
const void* nativeWindow,
|
||||
EGLint width, EGLint height,
|
||||
Bool platformWindow) {
|
||||
return CreateServerOwnedWindowSurface(display, config, nativeWindow, width, height,
|
||||
platformWindow);
|
||||
}
|
||||
|
||||
Bool EGLContext::SetSurfaceExtent(EGLSurfaceHandle surface, EGLint width, EGLint height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
auto surfaceIt = m_surfaces.find(surface);
|
||||
@@ -1238,7 +1332,13 @@ namespace MobileGL {
|
||||
SetError(EGL_BAD_CONTEXT);
|
||||
return false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE) {
|
||||
// EGL_KHR_surfaceless_context: BOTH surfaces absent is a context that draws into
|
||||
// framebuffers it binds itself, which is how a compositor draws the frames the
|
||||
// display host handed it. One absent and one present stays refused - that is a
|
||||
// pair EGL gives no meaning to, and accepting it would record a draw target that
|
||||
// does not exist.
|
||||
const Bool surfaceless = (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE);
|
||||
if (!surfaceless && (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE)) {
|
||||
SetError(EGL_BAD_MATCH);
|
||||
return false;
|
||||
}
|
||||
@@ -1442,11 +1542,20 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EGLContext::ReleaseImageDescriptors(ImageObject& imageObject) {
|
||||
for (Int plane = 0; plane < imageObject.PlaneCount; ++plane) {
|
||||
if (imageObject.Planes[plane].Fd >= 0) {
|
||||
CloseImageDescriptor(imageObject.Planes[plane].Fd);
|
||||
imageObject.Planes[plane].Fd = -1;
|
||||
}
|
||||
}
|
||||
imageObject.PlaneCount = 0;
|
||||
}
|
||||
|
||||
EGLContext::EGLImageHandle EGLContext::CreateImage(EGLDisplayHandle display, EGLContextHandle context,
|
||||
EGLenum target, EGLClientBuffer buffer,
|
||||
const EGLAttrib* attribList) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
(void)attribList;
|
||||
if (!IsDisplayInitialized(display)) {
|
||||
SetError(EGL_NOT_INITIALIZED);
|
||||
return EGL_NO_IMAGE;
|
||||
@@ -1456,13 +1565,149 @@ namespace MobileGL {
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
|
||||
const auto image = EncodeHandle<EGLImageHandle>(m_nextImageHandle++);
|
||||
m_images[image] = ImageObject{
|
||||
ImageObject imageObject = {
|
||||
.Display = display,
|
||||
.Context = context,
|
||||
.Target = target,
|
||||
.Buffer = buffer,
|
||||
};
|
||||
|
||||
// EGL_LINUX_DMA_BUF_EXT: the caller handed over a DESCRIPTOR and described the
|
||||
// memory behind it, and that description is the whole image - there is no handle to
|
||||
// import later. Every other target still names a buffer this state cannot see, so
|
||||
// it stays what it was: the record of the target it was made for.
|
||||
if (target == EGL_LINUX_DMA_BUF_EXT) {
|
||||
// ONE line per refusal, naming what was missing: a caller that gets
|
||||
// EGL_BAD_PARAMETER for a buffer it believes it described completely has
|
||||
// nowhere else to look - the attribute list is gone by the time eglGetError is
|
||||
// read, and the frame it wanted to draw into is the frame it cannot.
|
||||
const auto refuse = [&](const String& missing) {
|
||||
SetError(EGL_BAD_PARAMETER);
|
||||
MGLOG_E("eglCreateImage(EGL_LINUX_DMA_BUF_EXT): %s is missing; refusing the image "
|
||||
"(EGL_BAD_PARAMETER)",
|
||||
missing.c_str());
|
||||
};
|
||||
|
||||
const auto width = ParseAttribValue(attribList, EGL_WIDTH);
|
||||
const auto height = ParseAttribValue(attribList, EGL_HEIGHT);
|
||||
const auto fourCC = ParseAttribValue(attribList, EGL_LINUX_DRM_FOURCC_EXT);
|
||||
if (!width) {
|
||||
refuse("EGL_WIDTH");
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
if (!height) {
|
||||
refuse("EGL_HEIGHT");
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
if (!fourCC) {
|
||||
refuse("EGL_LINUX_DRM_FOURCC_EXT");
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
|
||||
// The planes, read and checked BEFORE one descriptor is duplicated, so that a
|
||||
// refusal leaves nothing behind to close.
|
||||
EGLint planeFd[EGLContext::kMaxImagePlanes] = {};
|
||||
EGLint planeOffset[EGLContext::kMaxImagePlanes] = {};
|
||||
EGLint planePitch[EGLContext::kMaxImagePlanes] = {};
|
||||
Uint64 planeModifier[EGLContext::kMaxImagePlanes] = {};
|
||||
Bool planeHasModifier[EGLContext::kMaxImagePlanes] = {};
|
||||
EGLint planeCount = 0;
|
||||
for (Int plane = 0; plane < EGLContext::kMaxImagePlanes; ++plane) {
|
||||
const DmaBufPlaneAttributes& attributes = kDmaBufPlaneAttributes[plane];
|
||||
const auto fd = ParseAttribValue(attribList, attributes.Fd);
|
||||
const auto offset = ParseAttribValue(attribList, attributes.Offset);
|
||||
const auto pitch = ParseAttribValue(attribList, attributes.Pitch);
|
||||
const auto modifierLo = ParseAttribValue(attribList, attributes.ModifierLo);
|
||||
const auto modifierHi = ParseAttribValue(attribList, attributes.ModifierHi);
|
||||
if (!fd && !offset && !pitch && !modifierLo && !modifierHi) {
|
||||
// The caller never mentioned this plane. Planes are positional - the
|
||||
// description carries no plane index - so this ends the list, and a
|
||||
// plane past the hole is refused by name rather than dropped.
|
||||
for (Int higher = plane + 1; higher < EGLContext::kMaxImagePlanes; ++higher) {
|
||||
if (HasAnyPlaneAttribute(attribList, higher)) {
|
||||
refuse(DmaBufPlaneAttributeName(plane, "FD_EXT"));
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// All three of fd/offset/pitch or none, and both halves of the modifier or
|
||||
// neither: a partial triple describes memory no importer could read.
|
||||
if (!fd) {
|
||||
refuse(DmaBufPlaneAttributeName(plane, "FD_EXT"));
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
if (!offset) {
|
||||
refuse(DmaBufPlaneAttributeName(plane, "OFFSET_EXT"));
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
if (!pitch) {
|
||||
refuse(DmaBufPlaneAttributeName(plane, "PITCH_EXT"));
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
if (modifierLo.has_value() != modifierHi.has_value()) {
|
||||
refuse(DmaBufPlaneAttributeName(plane, modifierLo ? "MODIFIER_HI_EXT" : "MODIFIER_LO_EXT"));
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
planeFd[plane] = static_cast<EGLint>(*fd);
|
||||
planeOffset[plane] = static_cast<EGLint>(*offset);
|
||||
planePitch[plane] = static_cast<EGLint>(*pitch);
|
||||
if (modifierLo) {
|
||||
// The modifier arrives in two halves and is one number: DRM cuts a
|
||||
// DRM_FORMAT_MOD_* where EGL takes an EGLint.
|
||||
planeModifier[plane] = (static_cast<Uint64>(static_cast<Uint32>(*modifierHi)) << 32) |
|
||||
static_cast<Uint32>(*modifierLo);
|
||||
planeHasModifier[plane] = true;
|
||||
}
|
||||
planeCount = plane + 1;
|
||||
}
|
||||
if (planeCount == 0) {
|
||||
// A dma-buf image with no plane at all: the description starts here, so
|
||||
// this is the attribute that is missing.
|
||||
refuse(DmaBufPlaneAttributeName(0, "FD_EXT"));
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
|
||||
// THE IMAGE TAKES ITS OWN DESCRIPTORS. The caller's fds stay the caller's -
|
||||
// kwin hands over a dup() of its DmaBufAttributes and closes it as soon as
|
||||
// eglCreateImage returns, and an image that had borrowed it would be holding a
|
||||
// number some other part of the process now owns.
|
||||
for (Int plane = 0; plane < planeCount; ++plane) {
|
||||
const Int ownedFd = DuplicateImageDescriptor(planeFd[plane]);
|
||||
if (ownedFd < 0) {
|
||||
SetError(EGL_BAD_ALLOC);
|
||||
MGLOG_E("eglCreateImage(EGL_LINUX_DMA_BUF_EXT): cannot take a copy of %s=%d (%s); "
|
||||
"refusing the image (EGL_BAD_ALLOC)",
|
||||
DmaBufPlaneAttributeName(plane, "FD_EXT").c_str(), planeFd[plane],
|
||||
std::strerror(errno));
|
||||
ReleaseImageDescriptors(imageObject);
|
||||
return EGL_NO_IMAGE;
|
||||
}
|
||||
imageObject.Planes[plane] = ImagePlane{
|
||||
.Fd = ownedFd,
|
||||
.Offset = planeOffset[plane],
|
||||
.Pitch = planePitch[plane],
|
||||
.Modifier = planeModifier[plane],
|
||||
.HasModifier = planeHasModifier[plane],
|
||||
};
|
||||
// Counted as they are stored, so that a failure below releases exactly what
|
||||
// has been taken.
|
||||
imageObject.PlaneCount = plane + 1;
|
||||
}
|
||||
imageObject.Width = static_cast<EGLint>(*width);
|
||||
imageObject.Height = static_cast<EGLint>(*height);
|
||||
imageObject.FourCC = static_cast<EGLint>(*fourCC);
|
||||
|
||||
MGLOG_D("eglCreateImage(EGL_LINUX_DMA_BUF_EXT): %dx%d fourcc=0x%08x planes=%d plane0(fd=%d "
|
||||
"offset=%d pitch=%d %s0x%016llx)",
|
||||
imageObject.Width, imageObject.Height, imageObject.FourCC, imageObject.PlaneCount,
|
||||
imageObject.Planes[0].Fd, imageObject.Planes[0].Offset, imageObject.Planes[0].Pitch,
|
||||
imageObject.Planes[0].HasModifier ? "modifier=" : "no-modifier=",
|
||||
static_cast<unsigned long long>(imageObject.Planes[0].Modifier));
|
||||
}
|
||||
|
||||
const auto image = EncodeHandle<EGLImageHandle>(m_nextImageHandle++);
|
||||
m_images[image] = imageObject;
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -1477,6 +1722,9 @@ namespace MobileGL {
|
||||
SetError(EGL_BAD_MATCH);
|
||||
return false;
|
||||
}
|
||||
// The descriptors this image owns die with it. Nothing else in this process holds
|
||||
// them: they are copies, and the caller's originals were never touched.
|
||||
ReleaseImageDescriptors(imageIt->second);
|
||||
m_images.erase(imageIt);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace MobileGL {
|
||||
using EGLSyncHandle = ::EGLSync;
|
||||
using EGLImageHandle = ::EGLImage;
|
||||
|
||||
// How many planes EGL describes an imported buffer with: EGL_DMA_BUF_PLANE0_* comes
|
||||
// from EGL_EXT_image_dma_buf_import, EGL_DMA_BUF_PLANE3_* and every MODIFIER pair
|
||||
// from EGL_EXT_image_dma_buf_import_modifiers. The reader in Core.cpp and the image
|
||||
// object below have to agree on the count, so it is named once.
|
||||
static constexpr Int kMaxImagePlanes = 4;
|
||||
|
||||
EGLContext() = default;
|
||||
|
||||
// Error
|
||||
@@ -37,6 +43,9 @@ namespace MobileGL {
|
||||
EGLDisplayHandle GetPlatformDisplay(EGLenum platform, void* nativeDisplay);
|
||||
Bool ValidateDisplay(EGLDisplayHandle display) const;
|
||||
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
|
||||
// The native display a display was made for and the platform it was asked for on
|
||||
// (EGL_NONE for eglGetDisplay): what decides how its window surfaces are presented.
|
||||
Bool GetDisplayNative(EGLDisplayHandle display, Uint64* nativeDisplayKey, EGLenum* platform) const;
|
||||
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
|
||||
Bool TerminateDisplay(EGLDisplayHandle display);
|
||||
// Whole-library idle checks used by EGLImpl::Terminate to decide
|
||||
@@ -97,6 +106,13 @@ namespace MobileGL {
|
||||
EGLSurfaceHandle CreateServerOwnedWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
|
||||
const void* nativeWindow, EGLint width,
|
||||
EGLint height, Bool platformWindow);
|
||||
// MOBILEGL_IPC_SURFACE=host: the same registration under the other on-screen name.
|
||||
// The EGL state does not care which side owns the frames - that is the backend's
|
||||
// business - but a trace has to be able to tell the two runs apart, and the two
|
||||
// paths part company in the backend call that follows.
|
||||
EGLSurfaceHandle CreateHostFrameWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
|
||||
const void* nativeWindow, EGLint width,
|
||||
EGLint height, Bool platformWindow);
|
||||
// P12: the server's REAL extent of a surface - a server-owned window's geometry, from
|
||||
// the CreateWindowSurface reply and from every surface-changed event after it - so
|
||||
// eglQuerySurface(EGL_WIDTH/EGL_HEIGHT) answers what the server renders at. Unlike
|
||||
@@ -111,6 +127,57 @@ namespace MobileGL {
|
||||
EGLContextHandle GetCurrentContext() const;
|
||||
EGLDisplayHandle GetCurrentDisplay() const;
|
||||
EGLSurfaceHandle GetCurrentSurface(EGLint readdraw) const;
|
||||
// WHAT A HANDLE NAMES, for the GL side of GL_OES_EGL_image: the two entry points in
|
||||
// MG_Impl/GLImpl/Exporting/Definitions.cpp are handed a GLeglImageOES and have to
|
||||
// answer whether it is an image this display created, and with what geometry. A
|
||||
// read-only lookup belongs beside the other queries a caller outside this class
|
||||
// makes, and it is the same object eglCreateImageKHR built and eglDestroyImage
|
||||
// removes - the private one stays private for the class's own use.
|
||||
// WHAT A HANDLE NAMES, for the GL side of GL_OES_EGL_image. The two entry points in
|
||||
// MG_Impl/GLImpl/Exporting/Definitions.cpp are handed a GLeglImageOES and have to
|
||||
// answer (a) whether it is an image this display created and (b) what buffer it
|
||||
// describes. The numbers come back rather than the record itself: the image table and
|
||||
// its layout are this class's own business, and a caller outside it has no use for
|
||||
// either - which is also why this is a function and not a friend declaration.
|
||||
Bool DescribeImage(EGLImageHandle image, EGLint* width, EGLint* height, EGLint* fourCc,
|
||||
EGLint* planeCount) const {
|
||||
const ImageObject* imageObject = TryGetImage(image);
|
||||
if (imageObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (width != nullptr) *width = imageObject->Width;
|
||||
if (height != nullptr) *height = imageObject->Height;
|
||||
if (fourCc != nullptr) *fourCc = imageObject->FourCC;
|
||||
if (planeCount != nullptr) *planeCount = imageObject->PlaneCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
// THE PLANE DESCRIPTORS OF AN IMAGE THIS DISPLAY CREATED, for the one caller that has
|
||||
// to hand them to the render server: the dma-buf import (a texture bound to an imported
|
||||
// EGLImage has no storage on the server, so the server is told which buffer backs it -
|
||||
// one descriptor per plane, over the session transport's SCM_RIGHTS channel). The fd
|
||||
// here is THIS side's dup(), owned by the image; the transport dups again for the
|
||||
// sending process, which is why the caller does not close anything.
|
||||
Bool DescribeImagePlanes(EGLImageHandle image, EGLint* planeCount, EGLint* fds, EGLint* offsets,
|
||||
EGLint* pitches, Uint64* modifiers, Bool* hasModifiers, Int maxPlanes) const {
|
||||
const ImageObject* imageObject = TryGetImage(image);
|
||||
if (imageObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
Int count = imageObject->PlaneCount;
|
||||
if (count > maxPlanes) count = maxPlanes;
|
||||
if (count < 0) count = 0;
|
||||
if (planeCount != nullptr) *planeCount = count;
|
||||
for (Int plane = 0; plane < count; plane++) {
|
||||
const ImagePlane& described = imageObject->Planes[plane];
|
||||
if (fds != nullptr) fds[plane] = described.Fd;
|
||||
if (offsets != nullptr) offsets[plane] = described.Offset;
|
||||
if (pitches != nullptr) pitches[plane] = described.Pitch;
|
||||
if (modifiers != nullptr) modifiers[plane] = described.Modifier;
|
||||
if (hasModifiers != nullptr) hasModifiers[plane] = described.HasModifier;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Bool IsDoubleBufferedSurface(EGLSurfaceHandle surface) const;
|
||||
|
||||
// Sync
|
||||
@@ -211,11 +278,39 @@ namespace MobileGL {
|
||||
EGLenum Status = EGL_SIGNALED;
|
||||
};
|
||||
|
||||
// ONE PLANE OF AN IMPORTED BUFFER. The fd/offset/pitch triple is the same
|
||||
// description drmModeAddFB2 takes, and the modifier is the DRM_FORMAT_MOD_* the
|
||||
// caller declared; EGL carries up to four of these (EGL_DMA_BUF_PLANE0..3_*).
|
||||
struct ImagePlane {
|
||||
// THIS library's descriptor, dup()ed from the caller's when the image was made
|
||||
// and closed when it dies: an EGLImage outlives the call that created it, and
|
||||
// the caller is free to close its own fd the moment eglCreateImage returns.
|
||||
EGLint Fd = -1;
|
||||
EGLint Offset = 0;
|
||||
EGLint Pitch = 0;
|
||||
// The two EGL_DMA_BUF_PLANEn_MODIFIER_LO/HI_EXT halves joined back into the
|
||||
// DRM_FORMAT_MOD_* they were cut from.
|
||||
Uint64 Modifier = 0;
|
||||
Bool HasModifier = false;
|
||||
};
|
||||
|
||||
struct ImageObject {
|
||||
EGLDisplayHandle Display = EGL_NO_DISPLAY;
|
||||
EGLContextHandle Context = nullptr;
|
||||
EGLenum Target = EGL_NONE;
|
||||
EGLClientBuffer Buffer = nullptr;
|
||||
// WHAT AN EGL_LINUX_DMA_BUF_EXT IMAGE IS HOLDING, and zero for every other
|
||||
// target: the buffer's geometry and the fourcc that says what its samples mean.
|
||||
// It is kept here because the image is the only thing that outlives the call -
|
||||
// a caller that handed over an fd has nothing left to describe it with.
|
||||
EGLint Width = 0;
|
||||
EGLint Height = 0;
|
||||
EGLint FourCC = 0;
|
||||
EGLint PlaneCount = 0;
|
||||
// Planes 0..PlaneCount-1 are described, in order; index 0 is the plane every
|
||||
// import in this tree reads, the rest are carried because EGL describes them and
|
||||
// a consumer that understands a multi-plane format must be able to see them.
|
||||
ImagePlane Planes[kMaxImagePlanes];
|
||||
};
|
||||
|
||||
struct ThreadCurrentState {
|
||||
@@ -251,6 +346,9 @@ namespace MobileGL {
|
||||
const SurfaceObject* TryGetSurface(EGLSurfaceHandle surface) const;
|
||||
const SyncObject* TryGetSync(EGLSyncHandle sync) const;
|
||||
const ImageObject* TryGetImage(EGLImageHandle image) const;
|
||||
// Closes the descriptors an image owns. Both places an image can die call it:
|
||||
// eglDestroyImage, and eglTerminate, which drops a display's images wholesale.
|
||||
static void ReleaseImageDescriptors(ImageObject& imageObject);
|
||||
|
||||
void ReleaseDisplayObjects(EGLDisplayHandle display);
|
||||
void ReleaseThreadUnlocked(const std::thread::id& threadKey);
|
||||
|
||||
@@ -105,7 +105,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_bufferBindPointTargets[i][index];
|
||||
}
|
||||
}
|
||||
MOBILEGL_ASSERT(false, "Invalid BufferTarget enum value for binding point: %d", static_cast<int>(target));
|
||||
// A GLOBAL TARGET ASKED THROUGH THE INDEXED LOOKUP. Only four targets have per-index binding
|
||||
// points (Uniform, TransformFeedback, AtomicCounter, ShaderStorage); the rest - Vertex, Index,
|
||||
// PixelPack/Unpack, Texture, CopyRead/Write, Query, the indirect ones - are global, and a caller
|
||||
// that asks for one here is reading the wrong accessor, not passing a bad enum.
|
||||
//
|
||||
// This used to abort, and the compositor reached it right after its vertex-attrib setup: the
|
||||
// whole session died on Trace/breakpoint trap ("Invalid BufferTarget enum value for binding
|
||||
// point"). Aborting a compositor over an accessor mix-up is the wrong trade, so the answer is
|
||||
// a one-time warning and index 0 of the indexed array: the caller gets a valid slot, the
|
||||
// global binding is untouched, and the mismatch stays visible in the log instead of being a
|
||||
// crash. The proper fix is for that caller to use the global accessor.
|
||||
MGLOG_W_ONCE("BufferState::GetBindingPoint: target=%d has no per-index binding points; answering "
|
||||
"index 0 of the indexed array (a global target was asked through the indexed lookup)",
|
||||
static_cast<int>(target));
|
||||
return m_bufferBindPointTargets[0][index];
|
||||
}
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -13,6 +13,23 @@
|
||||
#include <atomic>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
|
||||
// The display host's frame, as the backend publishes it (MG_Backend/DirectGLES/DirectGLES.h).
|
||||
// DECLARED HERE RATHER THAN INCLUDED, and that is a deliberate compromise with a named end: the
|
||||
// client asks whether its framebuffer is complete BEFORE any backend binding runs, so this judgement
|
||||
// cannot ask the backend - and the frontend knowing a backend fact is exactly what a record is for.
|
||||
// The wire does not carry one yet (MG_Protocol's dispatch has no glEGLImageTargetTexture2DOES), so
|
||||
// the fact is read from where it already is; when that record lands, this block goes with it.
|
||||
// The backend namespace is the one DirectGLES.cpp:67 opens and defines these in - the SAME nesting
|
||||
// as Uint g_hostFrameFramebufferId beside them. Getting this wrong is not a typo with no cost: a
|
||||
// Linux shared library links with undefined symbols allowed, so the container build passed while the
|
||||
// Android server build (ld.lld, --no-undefined) named all three - which is how a latent runtime
|
||||
// failure was caught by the platform that builds the render server.
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Uint g_hostFrameRenderbuffer;
|
||||
extern Uint g_hostFrameWidth;
|
||||
extern Uint g_hostFrameHeight;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
|
||||
// Atomic for the same reason as the VAO counter: it costs nothing, and a duplicate
|
||||
@@ -174,8 +191,34 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Int width = -1, height = -1;
|
||||
Int validAttachmentCount = 0;
|
||||
// THE IMPORTED dma-buf, and why it is judged BEFORE the validity test rather than after it.
|
||||
// Measured: with kwin_wayland's framebuffer attached - it imports the display daemon's
|
||||
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937 fourcc
|
||||
// 0x34324241") and builds an FBO over that texture - the server answered
|
||||
// "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT". The old early-continue below dropped the
|
||||
// attachment (a texture that arrived this way has no storage, so it reports itself invalid)
|
||||
// and the completeness gate further down was never reached.
|
||||
//
|
||||
// THE GATE IS THE MEASURED SHAPE, and it is deliberately narrow: this process is holding a
|
||||
// display host frame (g_hostFrameRenderbuffer != 0), and the attachment is a texture with no
|
||||
// storage of its own. That buffer IS the frame the host handed this process - same memory -
|
||||
// and the backend binds the frame's renderbuffer in place of the empty texture
|
||||
// (MG_Backend/DirectGLES/Managers.cpp). A size comparison would be the sharper test and is
|
||||
// impossible here: the imported attachment has no size, which is exactly why the wire needs a
|
||||
// record to say so (MG_Protocol's dispatch has no glEGLImageTargetTexture2DOES yet); when it
|
||||
// lands, this gate is replaced by that record and nothing else changes.
|
||||
// AND IT DELIBERATELY DOES NOT ASK WHETHER THE FRAME IS IN HAND YET, which the first version
|
||||
// of this gate did - and that version did not work, measured: kwin imports the dma-buf and
|
||||
// builds its framebuffer BEFORE the host-framed surface exists, so g_hostFrameRenderbuffer is
|
||||
// still zero at the instant the client asks, the gate refused, and kwin printed
|
||||
// "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" again. What the attachment IS is the fact
|
||||
// that holds at that instant: a texture with no storage of its own is the imported dma-buf,
|
||||
// and the buffer it names is the display daemon's frame - the one this process is about to be
|
||||
// handed. The backend binds that frame's renderbuffer to the attachment as soon as it has
|
||||
// one (Managers.cpp re-syncs attachments per draw, so the first draw after the frame arrives
|
||||
// lands in it).
|
||||
for (const auto& attachmentObject : m_attachmentObjects) {
|
||||
if (!attachmentObject.IsValid()) continue;
|
||||
if (!attachmentObject.IsValid() && !attachmentObject.IsTexture()) continue;
|
||||
|
||||
++validAttachmentCount;
|
||||
const auto& attachment = attachmentObject;
|
||||
@@ -190,7 +233,44 @@ namespace MobileGL::MG_State::GLState {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!attachment.IsComplete()) {
|
||||
// AN IMPORTED dma-buf COUNTS AS COMPLETE, in the measured shape and no other: the
|
||||
// attachment has no storage of its own and this process holds a display host frame of the
|
||||
// same size. Measured, and the reason this is here at all: kwin_wayland imports the
|
||||
// display daemon's dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of
|
||||
// 1440x2937 fourcc 0x34324241") and then refuses its own framebuffer with "framebuffer for
|
||||
// dmabuf 0 is not complete" - a refusal that is WRONG here, because the buffer it imported
|
||||
// IS the frame the display host handed this process (same memory, same size) and the
|
||||
// backend binds that frame's renderbuffer in place of the empty texture
|
||||
// (MG_Backend/DirectGLES/Managers.cpp, the external-OES arm under the same gate). A size
|
||||
// that differs, or no frame held, keeps the old answer: an ordinary empty attachment is
|
||||
// still incomplete, and nothing is ever rendered into a buffer of the wrong size.
|
||||
// AND IT DOES NOT ASK WHETHER A FRAME IS HELD, OR HOW BIG IT IS - which the first version
|
||||
// of this gate did, and which is why it never worked. This judgement runs on BOTH sides
|
||||
// of a split run: the client answers glCheckFramebufferStatus out of its own state before
|
||||
// any backend binding happens, and the client holds no frame at all. So a gate that
|
||||
// required g_hostFrameRenderbuffer != 0 and a matching size refused on the client every
|
||||
// time, and kwin kept printing "Invalid framebuffer status:
|
||||
// GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" - measured, twice, after that gate was
|
||||
// deployed. What the attachment IS is the fact that holds on both sides: a texture with
|
||||
// no storage of its own is the imported dma-buf, and the server binds the display host's
|
||||
// frame to it as soon as it has one (Managers.cpp re-syncs the attachments per draw, so
|
||||
// the first draw after the frame arrives lands in it). An attachment that IS valid - an
|
||||
// ordinary texture or renderbuffer with real storage - still has to be complete.
|
||||
// A TEXTURE ATTACHMENT THAT IS NOT COMPLETE IS THE IMPORTED dma-buf, and it counts. The
|
||||
// condition is written on IsTexture() and NOT on IsValid(), which is the mistake two
|
||||
// earlier versions of this gate made - measured both times as
|
||||
// "Invalid framebuffer status: GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" from a session
|
||||
// that was healthy and had really reached the import:
|
||||
// * requiring a host frame to be in hand refused on the CLIENT, which holds none;
|
||||
// * requiring the attachment to be INVALID refused too, because AttachTexture registers
|
||||
// the attachment (FramebufferObject.cpp:163) whether or not the texture has storage -
|
||||
// so the imported one is perfectly valid and simply incomplete.
|
||||
// What is true on both sides, at that instant, is what this asks: the attachment is a
|
||||
// TEXTURE and it is incomplete, i.e. it has no storage of its own, and the server binds the
|
||||
// display host's frame to it as soon as it holds one (Managers.cpp re-syncs the attachments
|
||||
// per draw). A RENDERBUFFER attachment still has to be complete, so an ordinary empty
|
||||
// attachment of that kind keeps the old refusal.
|
||||
if (!attachment.IsComplete() && !attachment.IsTexture()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ namespace MobileGL {
|
||||
Texture2DArray,
|
||||
TextureCubeMapArray,
|
||||
Texture2DMultisampleArray,
|
||||
// GL_TEXTURE_EXTERNAL_OES, APPENDED SO NO EXISTING VALUE MOVES. The compositor imports the
|
||||
// display daemon's dma-buf with eglCreateImageKHR and binds the result through this target
|
||||
// (glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex) + glEGLImageTargetTexture2DOES). Without a
|
||||
// member here the bind is answered INVALID_ENUM (measured with probe15: error 0x500 -> 0x0500),
|
||||
// the texture never enters a binding slot, it has no internal format, and the framebuffer
|
||||
// built over it is reported incomplete - "framebuffer for dmabuf 0 is not complete".
|
||||
TextureExternalOES,
|
||||
TextureTargetCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace MobileGL {
|
||||
TextureObject2D::TextureObject2D(Uint externalIndex)
|
||||
: TextureObjectWithOneMipmap(TextureTarget::Texture2D, externalIndex) {}
|
||||
|
||||
TextureObject2D::TextureObject2D(Uint externalIndex, TextureTarget target)
|
||||
: TextureObjectWithOneMipmap(target, externalIndex) {}
|
||||
|
||||
Uint TextureObject2D::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
|
||||
MOBILEGL_ASSERT(target == TextureUploadTarget::Texture2D ||
|
||||
target == TextureUploadTarget::ProxyTexture2D,
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace MobileGL {
|
||||
class TextureObject2D : public TextureObjectWithOneMipmap {
|
||||
public:
|
||||
explicit TextureObject2D(Uint externalIndex);
|
||||
// An external texture (GL_TEXTURE_EXTERNAL_OES) is a 2D texture whose storage is an
|
||||
// imported EGLImage; it keeps that target so the render server can tell it apart.
|
||||
TextureObject2D(Uint externalIndex, TextureTarget target);
|
||||
const Vector<TextureUploadTarget>& GetUploadTargets() const override { return m_uploadTargets; }
|
||||
|
||||
protected:
|
||||
|
||||
@@ -35,6 +35,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
return MakeShared<TextureObject2DCube>(index);
|
||||
case TextureTarget::Texture2D:
|
||||
return MakeShared<TextureObject2D>(index);
|
||||
// GL_TEXTURE_EXTERNAL_OES: an external texture IS a 2D texture whose storage comes from an
|
||||
// imported EGLImage (the compositor's display-daemon dma-buf). The object is the 2D one; what
|
||||
// differs is where the storage comes from, and the import path records that. Without this row
|
||||
// the factory asserted "Unimplemented texture type when creating texture object!" the moment
|
||||
// kwin bound one (measured: the compositor died on Trace/breakpoint trap right after the bind
|
||||
// was accepted).
|
||||
case TextureTarget::TextureExternalOES:
|
||||
return MakeShared<TextureObject2D>(index, TextureTarget::TextureExternalOES);
|
||||
case TextureTarget::Texture3D:
|
||||
return MakeShared<TextureObject3D>(index);
|
||||
case TextureTarget::TextureBuffer:
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace MobileGL {
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_PROXY_TEXTURE_2D:
|
||||
return TextureTarget::Texture2D;
|
||||
// GL_TEXTURE_EXTERNAL_OES: what the compositor binds an imported dma-buf to. Spelled as
|
||||
// the number because the GL headers this tree bundles do not define the token.
|
||||
case 0x8D65:
|
||||
return TextureTarget::TextureExternalOES;
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_PROXY_TEXTURE_3D:
|
||||
return TextureTarget::Texture3D;
|
||||
@@ -400,6 +404,14 @@ namespace MobileGL {
|
||||
return TextureUploadTarget::Texture1D;
|
||||
case GL_TEXTURE_2D:
|
||||
return TextureUploadTarget::Texture2D;
|
||||
// GL_TEXTURE_EXTERNAL_OES (0x8D65), an upload target of its own right: the compositor
|
||||
// attaches the imported dma-buf texture to a framebuffer through this enum, and the attach
|
||||
// path validates the target here. Left unmapped it became Unknown, the validation failed,
|
||||
// the attachment was never recorded, and the compositor's own check then read
|
||||
// GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT. External textures ARE two-dimensional, so
|
||||
// the upload target is the 2D one.
|
||||
case 0x8D65:
|
||||
return TextureUploadTarget::Texture2D;
|
||||
case GL_TEXTURE_3D:
|
||||
return TextureUploadTarget::Texture3D;
|
||||
case GL_PROXY_TEXTURE_2D:
|
||||
|
||||
@@ -497,6 +497,7 @@ namespace MobileGL {
|
||||
CASE(E_GL_OES_fixed_point)
|
||||
CASE(E_GL_OES_single_precision)
|
||||
CASE(E_GL_OES_compressed_paletted_texture)
|
||||
CASE(E_GL_OES_EGL_image)
|
||||
CASE(E_GL_OES_read_format)
|
||||
CASE(E_GL_OES_query_matrix)
|
||||
CASE(E_GL_EXT_depth_bounds_test)
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <MG_Util/PlatformStubs.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
|
||||
@@ -489,6 +489,11 @@ namespace MobileGL {
|
||||
E_GL_OES_fixed_point,
|
||||
E_GL_OES_single_precision,
|
||||
E_GL_OES_compressed_paletted_texture,
|
||||
// GL_OES_EGL_image: taking a texture (or renderbuffer) over an EGLImage. Named because
|
||||
// libepoxy resolves glEGLImageTargetTexture2DOES by NAME and the entry points are now real
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp), with the render-server half of the import
|
||||
// still to come - see the note there.
|
||||
E_GL_OES_EGL_image,
|
||||
E_GL_OES_read_format,
|
||||
E_GL_OES_query_matrix,
|
||||
E_GL_EXT_depth_bounds_test,
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<!-- The two permissions that decide whether the server survives a dozing phone: the
|
||||
notification one lets the foreground service show the notification that is its reason to
|
||||
exist (Android 13+), and the exemption one is what the user grants from the control screen
|
||||
so the system stops freezing the process. -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<application
|
||||
android:hasCode="true"
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
@@ -58,6 +64,8 @@ public final class ServerControlActivity extends Activity {
|
||||
private Button stopButton;
|
||||
private TextView statusView;
|
||||
private volatile Thread probeThread;
|
||||
private Button allowBackgroundButton;
|
||||
private TextView keepAliveView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -108,6 +116,28 @@ public final class ServerControlActivity extends Activity {
|
||||
boxParams.topMargin = dp(8);
|
||||
contentLayout.addView(onScreenBox, boxParams);
|
||||
|
||||
// KEEPING THE SERVER ALIVE IS TWO PERMISSIONS THE USER GIVES, and both are invisible
|
||||
// until something goes wrong: without the notification one (Android 13+) the service
|
||||
// notification cannot be shown, and without the battery exemption a dozing phone freezes
|
||||
// the process - the listener stays in /proc/net and stops accepting, which from the other
|
||||
// side of the socket looks exactly like a network fault. This section shows the state of
|
||||
// both, and asks for whichever is missing.
|
||||
keepAliveView = makeText("", 12, COLOR_INFO, false);
|
||||
LinearLayout.LayoutParams keepParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
keepParams.topMargin = dp(12);
|
||||
contentLayout.addView(keepAliveView, keepParams);
|
||||
|
||||
allowBackgroundButton = new Button(this);
|
||||
allowBackgroundButton.setText("Allow background (notification + battery)");
|
||||
allowBackgroundButton.setAllCaps(false);
|
||||
allowBackgroundButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
allowBackgroundButton.setOnClickListener(v -> requestKeepAlive());
|
||||
LinearLayout.LayoutParams allowParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
allowParams.topMargin = dp(6);
|
||||
contentLayout.addView(allowBackgroundButton, allowParams);
|
||||
|
||||
LinearLayout buttons = new LinearLayout(this);
|
||||
buttons.setOrientation(LinearLayout.HORIZONTAL);
|
||||
LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams(
|
||||
@@ -514,6 +544,73 @@ public final class ServerControlActivity extends Activity {
|
||||
statusView.setTextColor(color);
|
||||
}
|
||||
|
||||
// ---- keeping the server alive ----------------------------------------------------------
|
||||
|
||||
private static final int NOTIFICATION_PERMISSION_REQUEST = 40614;
|
||||
|
||||
private boolean notificationsAllowed() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true;
|
||||
return checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private boolean batteryExempt() {
|
||||
PowerManager power = getSystemService(PowerManager.class);
|
||||
return power != null && power.isIgnoringBatteryOptimizations(getPackageName());
|
||||
}
|
||||
|
||||
private void refreshKeepAlive() {
|
||||
if (keepAliveView == null) return;
|
||||
boolean notifications = notificationsAllowed();
|
||||
boolean exempt = batteryExempt();
|
||||
keepAliveView.setText("Keep alive: notification " + (notifications ? "allowed" : "NOT allowed")
|
||||
+ ", battery exemption " + (exempt ? "granted" : "NOT granted"));
|
||||
keepAliveView.setTextColor(notifications && exempt ? COLOR_PASS : COLOR_WARN);
|
||||
if (allowBackgroundButton != null) allowBackgroundButton.setEnabled(!(notifications && exempt));
|
||||
}
|
||||
|
||||
private void requestKeepAlive() {
|
||||
// One at a time, and in this order: the notification prompt is a system dialog of ours,
|
||||
// while the battery one leaves the app entirely - so it goes second, and coming back from
|
||||
// it re-runs the other branch through onResume.
|
||||
if (!notificationsAllowed() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requestPermissions(new String[] { Manifest.permission.POST_NOTIFICATIONS },
|
||||
NOTIFICATION_PERMISSION_REQUEST);
|
||||
return;
|
||||
}
|
||||
if (!batteryExempt()) {
|
||||
try {
|
||||
startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:" + getPackageName())));
|
||||
} catch (RuntimeException missingDialog) {
|
||||
// Some builds ship without that dialog; the list is the honest fallback rather than
|
||||
// a button that does nothing.
|
||||
Log.w(TAG, "no battery-optimization dialog; opening the list", missingDialog);
|
||||
startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
|
||||
}
|
||||
return;
|
||||
}
|
||||
setStatus("Already kept alive: the notification is allowed and the battery exemption is granted.",
|
||||
COLOR_PASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
refreshKeepAlive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, results);
|
||||
if (requestCode != NOTIFICATION_PERMISSION_REQUEST) return;
|
||||
refreshKeepAlive();
|
||||
// The denial is worth saying out loud: the server still runs, but its notification is gone,
|
||||
// and a service the system cannot show is a service it may stop.
|
||||
if (!notificationsAllowed())
|
||||
setStatus("Notifications are denied, so the server has no visible notification. "
|
||||
+ "Android may stop it in the background.", COLOR_WARN);
|
||||
}
|
||||
private TextView makeText(String text, int sizeSp, int color, boolean bold) {
|
||||
TextView view = new TextView(this);
|
||||
view.setText(text);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 容器当 client,手机当 server(上屏)
|
||||
|
||||
> 2026-10-01 在 Redmi 23117RK66C(Adreno 750,Android 16)与它的 Droidspaces Fedora 44
|
||||
> 容器上实测通过。本文记的是那条链路上**真正要设的东西**,以及两个会让人白跑一次的坑。
|
||||
|
||||
## 为什么不需要 `adb forward`
|
||||
|
||||
Droidspaces 容器与 Android **共用网络命名空间**(实测:容器里 `127.0.0.1:22` 就是 Android 的
|
||||
sshd,`ip addr` 列出的是手机的 `wlan0` / `rmnet_data5`)。所以服务端监听手机回环即可,
|
||||
容器直接用 `tcp://127.0.0.1:<port>` 连上去,不需要 `adb forward`,也不需要局域网地址。
|
||||
|
||||
## 起服务端(手机侧,上屏形态)
|
||||
|
||||
```bash
|
||||
TOKEN=<>=16 字节>
|
||||
adb shell am start -n top.mobilegl.plugin/top.mobilegl.plugin.MobileGLDisplayActivity \
|
||||
--es listen tcp://127.0.0.1:40613 --es token "$TOKEN"
|
||||
# 就绪行:
|
||||
# MG_Remote server: pid=… listening on tcp://127.0.0.1:40613 (in-process display server, display installed)
|
||||
```
|
||||
|
||||
**不要**给这次启动加 `--es env MOBILEGL_IPC_REQUIRE_SAME_BUILD=1`,除非两端确实同 build:
|
||||
按契约,`Dial == Connect` 时 build stamp 不同**只应警告**;加上它,服务端会因为 app 的戳(发布
|
||||
APK 里是 0)与容器客户端的戳(源码树短哈希)不同而 `Refuse{BuildFingerprint}`——症状是客户端
|
||||
在 `eglCreateWindowSurface` 之前就被 abort,日志里只有一条与真正原因无关的 caps 门。
|
||||
|
||||
## 容器侧(客户端)
|
||||
|
||||
```bash
|
||||
export MOBILEGL_TRANSPORT=spawn # 两进程;endpoint 由下面这条给
|
||||
export MOBILEGL_IPC_CONTROL=tcp://127.0.0.1:40613
|
||||
export MOBILEGL_IPC_DATA=stream # 跨进程字节流;同机共享段是另一条轴
|
||||
export MOBILEGL_IPC_SURFACE=server # 窗口是服务端的:客户端传 NULL 窗口
|
||||
export MOBILEGL_IPC_TOKEN=<同一个>
|
||||
export LD_LIBRARY_PATH=/root/build-cont # 容器里那份 split 构建
|
||||
./onscreen_client # eglCreateWindowSurface(dpy, cfg, NULL, {EGL_WIDTH,EGL_HEIGHT})
|
||||
```
|
||||
|
||||
客户端侧不需要 GPU、不需要 `/dev/dri`、不需要 Wayland:它只记录,画的是服务端。
|
||||
|
||||
## 实测结果
|
||||
|
||||
```
|
||||
EGL 1.5 vendor=MobileGL version=1.5 MobileGL
|
||||
surface created: 1440x3200 (asked 1440x3200)
|
||||
GL_RENDERER=Espryt (MobileGL Core) (Adreno (TM) 750, OpenGL ES 3.2)
|
||||
frame 0..11 swap=1
|
||||
```
|
||||
|
||||
服务端逐帧:
|
||||
|
||||
```
|
||||
P65ServerFrame frame=8..12 records=4 apply_ms=1.0..1.7
|
||||
```
|
||||
|
||||
抓屏为整屏 `#72e3d8`,即客户端第 3 种清屏色 `(0.20, 0.90, 0.85)` 经 sRGB 转换后的值——
|
||||
容器里那个没有 GPU 的进程要求画的东西,出现在手机屏幕上。
|
||||
|
||||
## 待办(本页不含)
|
||||
|
||||
- kwin:让 Plasma 的合成也走这条客户端(`LD_PRELOAD` 或直接链接容器里那份 `libEGL`),
|
||||
窗口面仍是服务端形态;此时 Anland 只剩 Wayland 协议与输入。
|
||||
- 两端同 build:容器里的源码树与 APK 从同一 revision 构建后,`REQUIRE_SAME_BUILD=1` 才有意义。
|
||||
@@ -181,6 +181,128 @@ and the client reads both fills through the still-held pointer; after the last
|
||||
round it unlocks and relocks once more (`after unlock+relock`), which separates
|
||||
"GPU writes never landed" from "the held mapping kept stale lines".
|
||||
|
||||
## T4 — an image allocated outside Android
|
||||
|
||||
Can the render server draw into an image that was allocated **outside** Android —
|
||||
a GBM buffer minted in a Linux container on the same kernel? That is the
|
||||
`{Owner=Platform, Storage=OfferedImage}` cell an Anland-style Wayland host needs,
|
||||
and nothing in MobileGL imports a dma-buf today. It is a per-device fact, not a
|
||||
code fact.
|
||||
|
||||
### What the device answered
|
||||
|
||||
Redmi 23117RK66C, Adreno 750, Android 16, Droidspaces Fedora 44 container,
|
||||
2026-09-30. The answer has two halves and they point in opposite directions.
|
||||
|
||||
| row | verdict | what it means |
|
||||
|---|---|---|
|
||||
| `T4-image-import` | `UNSUPPORTED` | This driver does not advertise `EGL_EXT_image_dma_buf_import`, so a container-minted dma-buf has no spelling as an EGLImage here. A capability answer, not a failure — and the reason the second row exists. |
|
||||
| `T4-ahb-image` | `FAIL`, and the failure is the finding | **Android-side allocation works end to end.** `AHardwareBuffer_allocate` → `eglGetNativeClientBufferANDROID` → `eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID)` → `glEGLImageTargetRenderbufferStorageOES` (FBO `GL_FRAMEBUFFER_COMPLETE`) → clear, `glFinish`, and a waited `glFenceSync` — all clean — and `glEGLImageTargetTexture2DOES` reads **the same bytes back on both pixels**. The container then imports the same handle's fd with `gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER, …)` and **that** is where it breaks: the bytes it maps are not the bytes the GPU wrote, and its own writes do not arrive either. |
|
||||
|
||||
The reason is visible in the offer: the AHB's handle carries **two** descriptors,
|
||||
while the import claims `DRM_FORMAT_MOD_LINEAR` over plane 0 alone. A multi-plane,
|
||||
possibly compressed allocation imported as one linear plane is not a view of that
|
||||
buffer, so a CPU mapping of it agrees with nothing — and no userspace API hands the
|
||||
importer the modifier it should have used, because `AHardwareBuffer` does not expose
|
||||
one. (`AHardwareBuffer_Desc::stride`, for its part, is in **pixels** while every
|
||||
consumer on the wire wants bytes; that trap cost a device run and is now a named
|
||||
constant beside the fourcc.)
|
||||
|
||||
### What this decides
|
||||
|
||||
The cell is reachable **only** when the image is allocated where the server can use
|
||||
it — on the Android side — **and** its layout travels with the offer. That is a
|
||||
requirement on the host that mints the scanout buffers, not a MobileGL feature:
|
||||
Anland-style hosts that allocate in the container cannot be served by this driver,
|
||||
and a host that publishes plane count, strides, offsets and modifier alongside the
|
||||
fd can be. Note also that the container never needs a CPU view of the image in the
|
||||
product: the compositor hands the buffer to its GL, which is MobileGL, and the
|
||||
server renders into it. The CPU mapping in this leg is a *verification device* —
|
||||
which is exactly why `FAIL` here means "a linear view is invalid", not "the driver
|
||||
cannot render into an AHardwareBuffer".
|
||||
|
||||
This leg's topology differs from every other one: the peer is **not** a re-exec
|
||||
child. It is a glibc process inside the container, because only that side can
|
||||
allocate on the device Anland hands the container. So the probe **listens**
|
||||
(`--only-t4[=@endpoint]`, default `@mgl-t4`) and the peer **dials** it. The
|
||||
endpoint is an abstract socket name because a Droidspaces container runs
|
||||
`net_mode=host`: the abstract namespace is common to both sides, while a
|
||||
filesystem pathname would have to exist in the other side's root.
|
||||
|
||||
Both rows run on the one connection `--only-t4` opens, so the peer never waits for
|
||||
a frame by name and a skipped row is not an error.
|
||||
|
||||
* **`T4-image-import`** — the container-minted direction. This side has no GBM
|
||||
device, so it waits for an fd, asks the driver whether the announced
|
||||
`(fourcc, modifier)` pair is one it even lists (`eglQueryDmaBufFormatsEXT` / `eglQueryDmaBufModifiersEXT`; a pair
|
||||
the driver does not list is a capability answer, an import failure on a pair it
|
||||
*does* list is a driver bug, and the two are never reported the same way),
|
||||
imports the buffer as a texture **and** as a renderbuffer, reads two pixels on
|
||||
each path — `(0,0)` and the far corner, so a wrong stride or offset cannot
|
||||
pass as a working import — and then clears each path to a colour.
|
||||
* **`T4-ahb-image`** — the Android-minted direction, and the one the measurement
|
||||
says the architecture must use. This side allocates an `AHardwareBuffer`
|
||||
(`R8G8B8A8_UNORM`, `GPU_SAMPLED_IMAGE|GPU_COLOR_OUTPUT|CPU_READ_OFTEN|CPU_WRITE_OFTEN`),
|
||||
imports **the same buffer** into GL as an EGLImage, clears it through the
|
||||
renderbuffer path behind a reported fence, hands its handle to the peer over a
|
||||
socketpair (`AHardwareBuffer_sendHandleToUnixSocket`), and then reads the peer's
|
||||
answer back through the **texture** path at `(0,0)` and at the far corner. One
|
||||
decisive leg, `ahb[D]`: it needs the peer to have seen this side's clear colour
|
||||
at both pixels *and* this side to have read the peer's pattern at both.
|
||||
* **`peer/t4_gbm_peer.c`** — serves both rows. For the first it allocates with
|
||||
`gbm_bo_create(…, GBM_FORMAT_ARGB8888,
|
||||
GBM_BO_USE_RENDERING|GBM_BO_USE_LINEAR)`, fills the whole mapping with
|
||||
`--seed=RRGGBBAA` in ARGB8888 memory order (B,G,R,A; a *solid* fill, so a
|
||||
vertical flip cannot turn a working import into a reported failure), sends the
|
||||
fd over `SCM_RIGHTS`, and after each of the probe's two writes takes a **fresh**
|
||||
read mapping and answers with the bytes it actually saw. A driver that refuses
|
||||
the allocation has answered the question: the refusal is printed verbatim and
|
||||
the peer exits non-zero, with no silent fallback to another format. For the
|
||||
second it takes the descriptors off the socketpair, imports the first with
|
||||
`gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER, …)` and reports **how many** arrived —
|
||||
the plane count is itself evidence — then compares both pixels against the
|
||||
announced colour before writing a pattern of its own.
|
||||
|
||||
The peer **must run inside the container**: `/dev/dri/renderD128` there is the
|
||||
Anland-backed `msm_drm` GBM device, and the Android side has no GBM device at all.
|
||||
|
||||
```sh
|
||||
S=<serial>; DS="/data/local/Droidspaces/bin/droidspaces --name=Fedora"
|
||||
|
||||
# peer: into the container's own rootfs (adb push lands on the Android side),
|
||||
# then built there with the container's own glibc gcc -- the documented line is
|
||||
# gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm
|
||||
base64 -w0 tools/spikes/extmem_probe/peer/t4_gbm_peer.c > /tmp/t4_gbm_peer.c.b64
|
||||
adb -s $S shell "$DS run sh -c 'base64 -d > /root/t4_gbm_peer.c'" < /tmp/t4_gbm_peer.c.b64
|
||||
adb -s $S shell "$DS run sh -c 'cd /root && gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm'"
|
||||
|
||||
# probe: build and start it FIRST — it is the side that listens
|
||||
ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 ./build_android.sh /tmp/extmem-build
|
||||
adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/extmem_probe
|
||||
adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe --only-t4" \
|
||||
| tee out-t4-$S.txt
|
||||
|
||||
# then, with the probe listening, dial it from the container
|
||||
adb -s $S shell "$DS run /root/t4_gbm_peer --endpoint=@mgl-t4"
|
||||
```
|
||||
|
||||
Peer options: `--endpoint=@mgl-t4`, `--width=64`, `--height=64`,
|
||||
`--seed=RRGGBBAA`, `--timeout=60`. The row is `T4-image-import`, with two
|
||||
decisive legs `tex[D]` and `rb[D]` under the usual rule: `OK` needs each path to
|
||||
have read the peer's seed bytes **and** to have had its own write observed by the
|
||||
peer. The peer exits 0 only on that same two-way condition, and its last line
|
||||
carries every raw value (stride, format, modifier, the fd's kernel name, the
|
||||
exporter, both pixels of both paths) — read those, not the exit code.
|
||||
|
||||
Two things there will look like failures and are not. `gbm_bo_unmap()` of a
|
||||
**WRITE** mapping segfaults on this container's mixed-version Mesa (distro
|
||||
`libgbm`/`dri_gbm` 26.1.5 plus an unowned self-built
|
||||
`libgallium-26.2.0-devel.so`), so the peer deliberately leaves its fill mapping
|
||||
mapped instead. And the peer's fd name plus exporter (`/dmabuf:`,
|
||||
`exp_name: system`) is the evidence that this is the same *kind* of object the
|
||||
compositor scans out: kwin's own scanout allocations come from that same system
|
||||
dma-heap.
|
||||
|
||||
## Route mode (P11 B2 step 1)
|
||||
|
||||
Does a client's AHB cross the *real* B1 routes into the server app? A spike APK built with
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user