├── .editorconfig ├── .gitignore ├── .clangd ├── .clang-format ├── include ├── funnel-vk.h ├── funnel-gbm.h ├── funnel-egl.h └── funnel.h ├── src ├── vulkan.c ├── funnel_internal.h ├── egl.c └── funnel.c ├── meson.build ├── LICENSE ├── README.md └── test-egl.c /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 4 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | compile_commands.json 2 | .cache 3 | -------------------------------------------------------------------------------- /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | CompilationDatabase: builddir 3 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | 3 | TabWidth: 4 4 | IndentWidth: 4 5 | 6 | ForEachMacros: [pw_array_for_each] 7 | -------------------------------------------------------------------------------- /include/funnel-vk.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "funnel.h" 4 | 5 | int funnel_stream_init_vulkan(struct funnel_stream *stream, void *todo); 6 | -------------------------------------------------------------------------------- /src/vulkan.c: -------------------------------------------------------------------------------- 1 | #include "funnel_internal.h" 2 | #include 3 | 4 | void funnel_vk_alloc_buffer(struct funnel_buffer *buffer) { assert(0); } 5 | void funnel_vk_free_buffer(struct funnel_buffer *buffer) { assert(0); } 6 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('funnel-test', 'c') 2 | 3 | includes = include_directories('include') 4 | 5 | pipewire = dependency('libpipewire-0.3') 6 | gbm = dependency('gbm') 7 | egl = dependency('egl') 8 | 9 | lib_funnel = library('funnel', 'src/funnel.c', 10 | dependencies: [gbm, egl, pipewire], 11 | include_directories : includes) 12 | funnel = declare_dependency(link_with : lib_funnel, include_directories : includes) 13 | 14 | lib_funnel_egl = library('funnel-egl', 'src/egl.c', dependencies: [funnel, pipewire, egl]) 15 | lib_funnel_vk = library('funnel-vk', 'src/vulkan.c', dependencies: [funnel, pipewire]) 16 | 17 | funnel_egl = declare_dependency(link_with : lib_funnel_egl, include_directories : includes) 18 | funnel_vk = declare_dependency(link_with : lib_funnel_vk, include_directories : includes) 19 | 20 | gl = dependency('GL') 21 | x11 = dependency('x11') 22 | executable('funnel-test-egl', 'test-egl.c', dependencies: [gl, egl, x11, funnel, funnel_egl]) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright the libfunnel contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /include/funnel-gbm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "funnel.h" 4 | #include 5 | 6 | /** 7 | * Set up a stream for GBM integration. 8 | * 9 | * @param stream Stream 10 | * @param gbm_fd File descriptor of the GBM device (borrow, must be closed by 11 | * caller at any time) 12 | */ 13 | int funnel_stream_init_gbm(struct funnel_stream *stream, int gbm_fd); 14 | 15 | /** 16 | * Add a supported GBM format. Must be called in preference order (highest to 17 | * lowest). 18 | * 19 | * @param stream Stream 20 | * @param format DRM format (FOURCC) 21 | * @param modifiers Pointer to a list of modifiers (borrow) 22 | * @param num_modifiers Number of modifiers passed 23 | */ 24 | int funnel_stream_gbm_add_format(struct funnel_stream *stream, 25 | uint32_t format, uint64_t *modifiers, 26 | size_t num_modifiers); 27 | 28 | /** 29 | * Get the GBM buffer object for a Funnel buffer. 30 | * 31 | * The BO will only be valid until `buf` is returned or enqueued, or the 32 | * stream is destroyed. 33 | * 34 | * @param buf Buffer 35 | * @param bo Output GBM BO for the buffer (borrowed) 36 | */ 37 | int funnel_buffer_get_gbm_bo(struct funnel_buffer *buf, struct gbm_bo **bo); 38 | -------------------------------------------------------------------------------- /include/funnel-egl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "funnel.h" 4 | #include 5 | 6 | enum funnel_egl_format { 7 | FUNNEL_EGL_FORMAT_RGB888, 8 | FUNNEL_EGL_FORMAT_RGBA8888, 9 | }; 10 | 11 | /** 12 | * Set up a stream for EGL integration. 13 | * 14 | * @param stream Stream 15 | * @param display EGLDisplay to attach to the stream (must outlive stream) 16 | */ 17 | int funnel_stream_init_egl(struct funnel_stream *stream, EGLDisplay display); 18 | 19 | /** 20 | * Add a supported EGL format. Must be called in preference order (highest to 21 | * lowest). 22 | * 23 | * @param stream Stream 24 | * @param format `struct funnel_egl_format` format 25 | */ 26 | int funnel_stream_egl_add_format(struct funnel_stream *stream, 27 | enum funnel_egl_format format); 28 | 29 | /** 30 | * Get the EGLImage for a Funnel buffer. 31 | * 32 | * The EGLImage will only be valid until `buf` is returned or enqueued, or the 33 | * stream is destroyed. 34 | * 35 | * @param buf Buffer 36 | * @param bo Output EGLImage for the buffer (borrowed) 37 | */ 38 | int funnel_buffer_get_egl_image(struct funnel_buffer *buf, EGLImage *image); 39 | 40 | /** 41 | * Get the EGL format for a Funnel buffer. 42 | * 43 | * @param buf Buffer 44 | * @param format Output EGL format 45 | */ 46 | int funnel_buffer_get_egl_format(struct funnel_buffer *buf, 47 | enum funnel_egl_format *format); 48 | -------------------------------------------------------------------------------- /src/funnel_internal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "funnel.h" 4 | #include 5 | #include 6 | #include 7 | 8 | #define ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a))) 9 | 10 | #define UNLOCK_RETURN(ret) \ 11 | do { \ 12 | int _ret = ret; \ 13 | pw_thread_loop_unlock(ctx->loop); \ 14 | return _ret; \ 15 | } while (0) 16 | 17 | static inline struct spa_fraction to_spa_fraction(struct funnel_fraction frac) { 18 | return SPA_FRACTION(frac.num, frac.den); 19 | } 20 | 21 | struct funnel_ctx { 22 | bool dead; 23 | struct pw_thread_loop *loop; 24 | struct pw_core *core; 25 | struct pw_context *context; 26 | struct spa_hook core_listener; 27 | }; 28 | 29 | struct funnel_format { 30 | uint32_t format; 31 | enum spa_video_format spa_format; 32 | uint64_t *modifiers; 33 | size_t num_modifiers; 34 | }; 35 | 36 | struct funnel_stream_config { 37 | enum funnel_mode mode; 38 | 39 | struct { 40 | int def, min, max; 41 | } buffers; 42 | 43 | struct { 44 | struct funnel_fraction def, min, max; 45 | } rate; 46 | 47 | uint32_t width; 48 | uint32_t height; 49 | 50 | struct pw_array formats; 51 | }; 52 | 53 | enum funnel_api { 54 | API_UNSET = 0, 55 | API_GBM, 56 | API_EGL, 57 | API_VULKAN, 58 | }; 59 | 60 | struct funnel_stream_funcs { 61 | void (*alloc_buffer)(struct funnel_buffer *); 62 | void (*free_buffer)(struct funnel_buffer *); 63 | }; 64 | 65 | enum funnel_sync_cycle { 66 | SYNC_CYCLE_INACTIVE, 67 | SYNC_CYCLE_WAITING, 68 | SYNC_CYCLE_ACTIVE, 69 | }; 70 | 71 | struct funnel_stream { 72 | struct funnel_ctx *ctx; 73 | const char *name; 74 | enum funnel_api api; 75 | 76 | const struct funnel_stream_funcs *funcs; 77 | void *api_ctx; 78 | 79 | struct gbm_device *gbm; 80 | struct spa_hook stream_listener; 81 | struct pw_stream *stream; 82 | struct spa_source *timer; 83 | 84 | struct funnel_stream_config config; 85 | bool config_pending; 86 | 87 | uint32_t cur_format; 88 | uint64_t cur_modifier; 89 | 90 | bool active; 91 | int num_buffers; 92 | enum funnel_sync_cycle cycle_state; 93 | int buffers_dequeued; 94 | struct funnel_buffer *pending_buffer; 95 | 96 | struct { 97 | struct funnel_stream_config config; 98 | bool ready; 99 | struct spa_video_info_raw video_format; 100 | 101 | struct funnel_fraction rate; 102 | uint32_t plane_count; 103 | uint32_t width; 104 | uint32_t height; 105 | uint32_t format; 106 | uint64_t modifier; 107 | uint32_t strides[4]; 108 | uint32_t offsets[4]; 109 | } cur; 110 | }; 111 | 112 | struct funnel_buffer { 113 | struct funnel_stream *stream; 114 | struct pw_buffer *pw_buffer; 115 | bool dequeued; 116 | bool driving; 117 | struct gbm_bo *bo; 118 | int fds[6]; 119 | void *api_buf; 120 | }; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libfunnel 2 | 3 | A library to make creating PipeWire video streams easy, using zero-copy DMA-BUF frame sharing. "Spout2 / Syphon, but for Linux". 4 | 5 | ## Status 6 | 7 | This is still rough around the edges and the API is not considered stable yet. 8 | 9 | Features: 10 | 11 | - [x] Sending frames 12 | - [ ] Receiving frames 13 | - [ ] Multiple synchronized streams 14 | - [x] Implicit sync 15 | - [ ] Explicit sync 16 | - [x] Async and multiple sync modes (with and without buffering) 17 | - [x] Raw GBM API 18 | - [x] EGL API integration 19 | - [ ] Vulkan API integration 20 | - [ ] GLX API integration (if someone really really needs it... ideally apps should switch to X11+EGL) 21 | - [x] Cross-GPU frame sharing for reasonable drivers (untested but it should work?) 22 | - [ ] Cross-GPU frame sharing between Nvidia prop driver and other GPUs/drivers 23 | - [ ] Automatic optimization for cross-GPU frame sharing (framebuffer optimization/conversion blits) 24 | 25 | Note: Due to missing explicit sync support, the Nvidia proprietary driver is not currently well supported (frame sharing will work, but you might experience tearing or frame pacing issues). This is a planned feature, but it will require work by application developers (it cannot be made transparent in the API), so some apps might choose not to support it. In addition, frame sharing between an Nvidia GPU and a non-Nvidia GPU requires special support code and extra blitting, which will probably not be implemented until much later. If you have an Nvidia GPU, please consider using [NVK](https://docs.mesa3d.org/drivers/nvk.html) instead, since it implements all the missing features that Nvidia refuses to implement in their proprietary driver, and therefore doesn't require us application and library developers to add Nvidia-specific workaround code. 26 | 27 | ### Sending frames 28 | 29 | The library initially targets sending frames to other applications (output). This covers the common use case of apps sending video to OBS for streaming. 30 | 31 | ### Receiving frames 32 | 33 | Receiving is not implemented yet, but is in scope. Before jumping into this, I'd like to hear from maintainers of applications that receive video via Spout2/Syphon. If you maintain such an app (with existing or planned Linux support), please file an issue and let me know! 34 | 35 | Of course, that doesn't make the sending side useless. To receive video streams in OBS, you can use [obs-pwvideo](https://github.com/hoshinolina/obs-pwvideo), which comes with its own PipeWire code (not using libfunnel itself). 36 | 37 | It is also possible to hijack the input of any app using PipeWire screen sharing (such as Firefox, etc.) and redirect a libfunnel input stream into it, though this requires some hacky pw-cli shenanigans. If you have a use case for this, please let me know, as it should be possible to write a tool to do it mostly automatically. 38 | 39 | ### Multiple streams (multiple senders, multiple receivers, or combinations) 40 | 41 | Multiple streams are supported the "obvious" way, but the streams behave completely independently (no synchronization) in the current implementation. For applications that synchronously process video, such as filters (input => output), there are better ways to do it. If you maintain an app that expects to input and/or output multiple video streams with locked frame sync, please file an issue and let me know your needs! The underlying PipeWire API is extremely powerful and complicated, and I need some example use cases to decide what a simplified libfunnel API for this should look like. 42 | 43 | ## Usage 44 | 45 | TL;DR for EGL 46 | 47 | ```c 48 | 49 | struct funnel_ctx *ctx; 50 | struct funnel_stream *stream; 51 | 52 | funnel_init(&ctx); 53 | funnel_stream_create(ctx, "Funnel Test", &stream); 54 | funnel_stream_init_egl(stream, egl_display); 55 | funnel_stream_set_size(stream, width, height); 56 | 57 | // FUNNEL_ASYNC if you are rendering to the screen at screen FPS 58 | // (in the same thread) and just sharing the frames, 59 | // FUNNEL_DOUBLE_BUFFERED if you are rendering frames mainly for sharing 60 | // and the frame rate is set by the consumer, 61 | // FUNNEL_SINGLE_BUFFERED same but with lower latency, 62 | // FUNNEL_SYNC if you are just copying frames out of somewhere 63 | // else on demand (like a Spout2 texture written by 64 | // another process) and want zero added latency. 65 | funnel_stream_set_mode(stream, FUNNEL_ASYNC); 66 | 67 | // Formats in priority order 68 | // If you don't want alpha, remove the first line 69 | // Alternatively, you can just demote it (and make sure you always render 70 | // 1.0 alpha in case it is chosen). 71 | // 72 | // Note: Alpha is always premultiplied. That's what you want, trust me. 73 | funnel_stream_egl_add_format(stream, FUNNEL_EGL_FORMAT_RGBA8888); 74 | funnel_stream_egl_add_format(stream, FUNNEL_EGL_FORMAT_RGB888); 75 | 76 | funnel_stream_start(stream); 77 | 78 | GLuint fb, color_tex; 79 | glGenFramebuffers(1, &fb); 80 | 81 | while (keep_rendering) { 82 | struct funnel_buffer *buf; 83 | 84 | // If you need to change the settings 85 | if (size_has_changed) { 86 | funnel_stream_set_size(stream, new_width, new_height); 87 | funnel_stream_configure(stream); 88 | // Change does not necessarily apply immediately, see below 89 | } 90 | 91 | funnel_stream_dequeue(stream, &buf); 92 | if (!buf) { 93 | // Skip this frame 94 | continue; 95 | } 96 | 97 | EGLImage image; 98 | funnel_buffer_get_egl_image(buf, &image); 99 | 100 | // If the size might change, this is how you know the size 101 | // of this specific buffer you have to render to: 102 | funnel_buffer_get_size(buf, &width, &height); 103 | 104 | glGenTextures(1, &color_tex); 105 | glBindTexture(GL_TEXTURE_2D, color_tex); 106 | glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); 107 | glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, fb); 108 | glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, 109 | GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 110 | color_tex, 0); 111 | 112 | // Draw or blit your scene here! 113 | 114 | glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, 115 | GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 116 | 0, 0); 117 | glDeleteTextures(1, &color_tex); 118 | glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); 119 | 120 | glFlush(); 121 | 122 | funnel_stream_enqueue(stream, buf); 123 | 124 | } 125 | 126 | funnel_stream_stop(stream); 127 | funnel_stream_destroy(stream); 128 | funnel_shutdown(ctx); 129 | ``` 130 | -------------------------------------------------------------------------------- /src/egl.c: -------------------------------------------------------------------------------- 1 | #include "funnel-egl.h" 2 | #include "funnel-gbm.h" 3 | #include "funnel_internal.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | static const struct { 12 | EGLAttrib fd, offset, pitch, modlo, modhi; 13 | } egl_attributes[4] = { 14 | {EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE0_OFFSET_EXT, 15 | EGL_DMA_BUF_PLANE0_PITCH_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 16 | EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT}, 17 | {EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, 18 | EGL_DMA_BUF_PLANE1_PITCH_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 19 | EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT}, 20 | {EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, 21 | EGL_DMA_BUF_PLANE2_PITCH_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, 22 | EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT}, 23 | {EGL_DMA_BUF_PLANE3_FD_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT, 24 | EGL_DMA_BUF_PLANE3_PITCH_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, 25 | EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT}, 26 | }; 27 | 28 | static void funnel_egl_alloc_buffer(struct funnel_buffer *buffer) { 29 | struct funnel_stream *stream = buffer->stream; 30 | 31 | int idx = 0; 32 | EGLAttrib attribute_list[7 + stream->cur.plane_count * 10]; 33 | 34 | attribute_list[idx++] = EGL_WIDTH; 35 | attribute_list[idx++] = stream->cur.width; 36 | attribute_list[idx++] = EGL_HEIGHT; 37 | attribute_list[idx++] = stream->cur.height; 38 | attribute_list[idx++] = EGL_LINUX_DRM_FOURCC_EXT; 39 | attribute_list[idx++] = stream->cur.format; 40 | 41 | for (int i = 0; i < stream->cur.plane_count; ++i) { 42 | attribute_list[idx++] = egl_attributes[i].fd; 43 | attribute_list[idx++] = buffer->fds[i]; 44 | attribute_list[idx++] = egl_attributes[i].offset; 45 | attribute_list[idx++] = stream->cur.offsets[i], 46 | attribute_list[idx++] = egl_attributes[i].pitch; 47 | attribute_list[idx++] = stream->cur.strides[i], 48 | attribute_list[idx++] = egl_attributes[i].modlo; 49 | attribute_list[idx++] = stream->cur.modifier, 50 | attribute_list[idx++] = egl_attributes[i].modhi; 51 | attribute_list[idx++] = (uint32_t)(stream->cur.modifier >> 32); 52 | } 53 | attribute_list[idx++] = EGL_NONE; 54 | 55 | EGLImage image = 56 | eglCreateImage(stream->api_ctx, NULL, EGL_LINUX_DMA_BUF_EXT, 57 | (EGLClientBuffer)NULL, attribute_list); 58 | assert(image != EGL_NO_IMAGE); 59 | 60 | buffer->api_buf = image; 61 | } 62 | 63 | static void funnel_egl_free_buffer(struct funnel_buffer *buffer) { 64 | eglDestroyImage(buffer->stream->api_ctx, buffer->api_buf); 65 | } 66 | 67 | static const struct funnel_stream_funcs egl_funcs = { 68 | .alloc_buffer = funnel_egl_alloc_buffer, 69 | .free_buffer = funnel_egl_free_buffer, 70 | }; 71 | 72 | static PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT; 73 | static PFNEGLQUERYDISPLAYATTRIBEXTPROC eglQueryDisplayAttribEXT; 74 | static PFNEGLQUERYDMABUFMODIFIERSEXTPROC eglQueryDmaBufModifiersEXT; 75 | 76 | int funnel_stream_init_egl(struct funnel_stream *stream, EGLDisplay display) { 77 | if (stream->api != API_UNSET) 78 | return -EEXIST; 79 | 80 | if (!eglQueryDeviceStringEXT) 81 | eglQueryDeviceStringEXT = 82 | (PFNEGLQUERYDEVICESTRINGEXTPROC)eglGetProcAddress( 83 | "eglQueryDeviceStringEXT"); 84 | if (!eglQueryDisplayAttribEXT) 85 | eglQueryDisplayAttribEXT = 86 | (PFNEGLQUERYDISPLAYATTRIBEXTPROC)eglGetProcAddress( 87 | "eglQueryDisplayAttribEXT"); 88 | if (!eglQueryDmaBufModifiersEXT) 89 | eglQueryDmaBufModifiersEXT = 90 | (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress( 91 | "eglQueryDmaBufModifiersEXT"); 92 | 93 | assert(eglQueryDeviceStringEXT); 94 | assert(eglQueryDisplayAttribEXT); 95 | assert(eglQueryDmaBufModifiersEXT); 96 | 97 | EGLAttrib device_attr = 0; 98 | if (!eglQueryDisplayAttribEXT(display, EGL_DEVICE_EXT, &device_attr) || 99 | !device_attr) { 100 | pw_log_error("failed to query EGLDeviceExt"); 101 | return -EIO; 102 | } 103 | 104 | EGLDeviceEXT device = (EGLDeviceEXT *)device_attr; 105 | 106 | const char *render_node = 107 | eglQueryDeviceStringEXT(device, EGL_DRM_RENDER_NODE_FILE_EXT); 108 | 109 | if (!render_node) 110 | render_node = eglQueryDeviceStringEXT(device, EGL_DRM_DEVICE_FILE_EXT); 111 | 112 | if (!render_node) { 113 | pw_log_error("failed to get device node"); 114 | return -EIO; 115 | } 116 | 117 | fprintf(stderr, "DRM render node: %s\n", render_node); 118 | 119 | int gbm_fd = open(render_node, O_RDONLY); 120 | if (gbm_fd < 0) { 121 | pw_log_error("failed to open device node %s: %d", render_node, errno); 122 | return -errno; 123 | } 124 | 125 | int ret = funnel_stream_init_gbm(stream, gbm_fd); 126 | close(gbm_fd); 127 | 128 | if (ret < 0) 129 | return ret; 130 | 131 | stream->funcs = &egl_funcs; 132 | stream->api = API_EGL; 133 | stream->api_ctx = display; 134 | 135 | return 0; 136 | } 137 | 138 | static bool try_format(struct funnel_stream *stream, uint32_t format) { 139 | EGLint count; 140 | if (eglQueryDmaBufModifiersEXT(stream->api_ctx, format, 0, NULL, NULL, 141 | &count) != EGL_TRUE) { 142 | return false; 143 | } 144 | 145 | EGLuint64KHR *modifiers = malloc(sizeof(EGLuint64KHR) * count); 146 | EGLBoolean *external = malloc(sizeof(EGLBoolean) * count); 147 | 148 | assert(eglQueryDmaBufModifiersEXT(stream->api_ctx, format, count, modifiers, 149 | external, &count)); 150 | 151 | fprintf(stderr, "Check format: 0x%x [%d modifiers]\n", format, count); 152 | for (unsigned i = 0; i < count; i++) { 153 | fprintf(stderr, " - 0x%llx [external=%d]\n", (long long)modifiers[i], 154 | external[i]); 155 | } 156 | 157 | for (unsigned i = 0, j = 0; i < count; j++) { 158 | if (external[j]) { 159 | memmove(&modifiers[i], &modifiers[i + 1], 160 | sizeof(uint64_t) * (count - i - 1)); 161 | count--; 162 | } else { 163 | i++; 164 | } 165 | } 166 | 167 | int ret = -ENOENT; 168 | if (count) { 169 | fprintf(stderr, "%d usable modifiers\n", count); 170 | ret = funnel_stream_gbm_add_format(stream, format, modifiers, count); 171 | } 172 | 173 | free(modifiers); 174 | free(external); 175 | 176 | return ret >= 0; 177 | } 178 | 179 | int funnel_stream_egl_add_format(struct funnel_stream *stream, 180 | enum funnel_egl_format format) { 181 | bool success = false; 182 | if (stream->api != API_EGL) 183 | return -EINVAL; 184 | 185 | switch (format) { 186 | case FUNNEL_EGL_FORMAT_RGB888: 187 | success |= try_format(stream, GBM_FORMAT_XRGB8888); 188 | success |= try_format(stream, GBM_FORMAT_RGBX8888); 189 | success |= try_format(stream, GBM_FORMAT_XBGR8888); 190 | success |= try_format(stream, GBM_FORMAT_BGRX8888); 191 | break; 192 | case FUNNEL_EGL_FORMAT_RGBA8888: 193 | success |= try_format(stream, GBM_FORMAT_ARGB8888); 194 | success |= try_format(stream, GBM_FORMAT_RGBA8888); 195 | success |= try_format(stream, GBM_FORMAT_ABGR8888); 196 | success |= try_format(stream, GBM_FORMAT_BGRA8888); 197 | break; 198 | } 199 | 200 | return success ? 0 : -ENOTSUP; 201 | } 202 | 203 | int funnel_buffer_get_egl_image(struct funnel_buffer *buf, EGLImage *image) { 204 | *image = NULL; 205 | if (!buf || buf->stream->api != API_EGL) 206 | return -EINVAL; 207 | 208 | *image = buf->api_buf; 209 | return 0; 210 | } 211 | -------------------------------------------------------------------------------- /include/funnel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct funnel_ctx; 8 | struct funnel_stream; 9 | struct funnel_buffer; 10 | 11 | /** 12 | * A rational frame rate 13 | */ 14 | struct funnel_fraction { 15 | uint32_t num; 16 | uint32_t den; 17 | }; 18 | 19 | /** 20 | * Indicates that the frame rate is variable 21 | */ 22 | static const struct funnel_fraction FUNNEL_RATE_VARIABLE = {0, 1}; 23 | 24 | static inline struct funnel_fraction FUNNEL_FRACTION(uint32_t num, 25 | uint32_t den) { 26 | return (struct funnel_fraction){num, den}; 27 | } 28 | 29 | /** 30 | * Synchronization modes for the frame transfer 31 | */ 32 | enum funnel_mode { 33 | /** 34 | * Produce frames asynchronously to the consumer. 35 | * 36 | * In this mode, libfunnel calls never block and you 37 | * must be able to handle the lack of a buffer (by 38 | * skipping rendering/copying to it). This mode only 39 | * makes sense if your application is FPS-limited by 40 | * some other consumer (for example, if it renders to 41 | * the screen, usually with VSync). You should configure 42 | * the frame rate you expect to produce frames at with 43 | * `funnel_stream_set_rate()`. 44 | * 45 | * This mode essentially behaves like triple buffering. 46 | * Whenever the PipeWire cycle runs, the consumer will 47 | * receive the frame that was most recently submitted 48 | * to funnel_stream_enqueue(). 49 | */ 50 | FUNNEL_ASYNC, 51 | /** 52 | * Produce frames synchronously to the consumer with 53 | * double buffering. 54 | * 55 | * In this mode, after a frame is produced, it is 56 | * queued to be sent out to the consumer in the next 57 | * PipeWire process cycle, and you may immediately 58 | * dequeue a new buffer to start rendering the next 59 | * frame. libfunnel will block at `funnel_stream_enqueue()` 60 | * until the previously queued frame has been consumed. 61 | * In this mode, `funnel_stream_dequeue()` will only 62 | * block if there are no free buffers (if the consumer is 63 | * not freeing buffers quickly enough). 64 | * 65 | * This mode effectively adds two frames of latency, 66 | * as up to two frames can be rendered ahead of the 67 | * PipeWire cycle (one ready to be submitted, and 68 | * one blocked at `funnel_stream_enqueue()`). 69 | */ 70 | FUNNEL_DOUBLE_BUFFERED, 71 | /** 72 | * Produce frames synchronously to the consumer with 73 | * single buffering. 74 | * 75 | * In this mode, after a frame is produced, it is 76 | * queued to be sent out to the consumer in the next 77 | * PipeWire process cycle. When you are ready to begin 78 | * rendering a new frame, libfunnel will block 79 | * at `funnel_stream_dequeue()` until the previous frame 80 | * has been sent to the consumer. In this mode, 81 | * `funnel_stream_enqueue()` will never block. 82 | * 83 | * This mode effectively adds one frame of latency, 84 | * as only one frame can be rendered ahead of the 85 | * PipeWire cycle. 86 | */ 87 | FUNNEL_SINGLE_BUFFERED, 88 | /** 89 | * Produce frames synchronously with the PipeWire process 90 | * cycle. 91 | * 92 | * In this mode, `funnel_stream_dequeue()` will wait for 93 | * the beginning of a PipeWire process cycle, and the 94 | * process cycle will be blocked until the frame is 95 | * submitted with `funnel_stream_enqueue()`. 96 | * 97 | * This mode provides the lowest possible latency, but 98 | * is only suitable for applications that do not do much 99 | * work to render frames (for example, just a copy), as 100 | * the PipeWire graph will be blocked while the buffer 101 | * is dequeued. It adds no latency. 102 | */ 103 | FUNNEL_SYNC, 104 | }; 105 | 106 | /** 107 | * Create a Funnel context. 108 | * 109 | * @param ctx New context 110 | */ 111 | int funnel_init(struct funnel_ctx **pctx); 112 | 113 | /** 114 | * Shut down a Funnel context. 115 | * 116 | * @param ctx Context 117 | */ 118 | void funnel_shutdown(struct funnel_ctx *ctx); 119 | 120 | /** 121 | * Create a new stream. 122 | * 123 | * @param ctx Context 124 | * @param name Name of the new stream (borrow) 125 | * @param stream New stream (must not outlive context) 126 | */ 127 | int funnel_stream_create(struct funnel_ctx *ctx, const char *name, 128 | struct funnel_stream **pstream); 129 | 130 | /** 131 | * Set the frame dimensions for a stream. 132 | * 133 | * @param stream Stream 134 | * @param width Width in pixels 135 | * @param height Height in pixels 136 | */ 137 | int funnel_stream_set_size(struct funnel_stream *stream, uint32_t width, 138 | uint32_t height); 139 | 140 | /** 141 | * Configure the queueing mode for the stream. 142 | * 143 | * @param stream Stream 144 | * @param mode Queueing mode for the stream 145 | */ 146 | int funnel_stream_set_mode(struct funnel_stream *stream, enum funnel_mode mode); 147 | 148 | /** 149 | * Set the frame rate of a stream. 150 | * 151 | * @param stream Stream 152 | * @param def Default frame rate (FUNNEL_RATE_VARIABLE for no default or 153 | * variable) 154 | * @param min Minimum frame rate (FUNNEL_RATE_VARIABLE if variable) 155 | * @param max Maximum frame rate (FUNNEL_RATE_VARIABLE if variable) 156 | */ 157 | int funnel_stream_set_rate(struct funnel_stream *stream, 158 | struct funnel_fraction def, 159 | struct funnel_fraction min, 160 | struct funnel_fraction max); 161 | 162 | /** 163 | * Get the currently negotiated frame rate of a stream. 164 | * 165 | * @param stream Stream 166 | * @param rate Output frame rate 167 | */ 168 | int funnel_stream_get_rate(struct funnel_stream *stream, 169 | struct funnel_fraction *prate); 170 | 171 | /** 172 | * Clear the supported format list. Used for reconfiguration. 173 | * 174 | * @param stream Stream 175 | */ 176 | void funnel_stream_clear_formats(struct funnel_stream *stream); 177 | 178 | /** 179 | * Apply the stream configuration and register the stream with PipeWire. 180 | * 181 | * If called on an already configured stream, this will update the 182 | * configuration. 183 | * 184 | * @param stream Stream 185 | */ 186 | int funnel_stream_configure(struct funnel_stream *stream); 187 | 188 | /** 189 | * Start running a stream. 190 | * 191 | * @param stream Stream 192 | */ 193 | int funnel_stream_start(struct funnel_stream *stream); 194 | 195 | /** 196 | * Stop running a stream. 197 | 198 | * This function may be called from any thread, which is 199 | * useful to abort a call to `funnel_stream_dequeue()` 200 | * in one of the synchronous modes. 201 | * 202 | * @param stream Stream 203 | */ 204 | int funnel_stream_stop(struct funnel_stream *stream); 205 | 206 | /** 207 | * Destroy a stream. 208 | * 209 | * The stream will be stopped if it is running. 210 | * 211 | * @param stream Stream 212 | */ 213 | void funnel_stream_destroy(struct funnel_stream *stream); 214 | 215 | /** 216 | * Dequeue a buffer from a stream. 217 | * 218 | * Note that, currently, you may only have one buffer 219 | * dequeued at a time. 220 | * 221 | * @param stream Stream 222 | * @param buf Output buffer (NULL if no buffer is available) 223 | */ 224 | int funnel_stream_dequeue(struct funnel_stream *stream, 225 | struct funnel_buffer **pbuf); 226 | 227 | /** 228 | * Enqueue a buffer to a stream. 229 | * 230 | * After this call, the buffer is no longer owned by the user and may not be 231 | * queued again until it is dequeued. 232 | * 233 | * @param stream Stream 234 | * @param buf Buffer to enqueue (must have been dequeued) 235 | */ 236 | int funnel_stream_enqueue(struct funnel_stream *stream, 237 | struct funnel_buffer *buf); 238 | 239 | /** 240 | * Return a buffer to the pool without enqueueing it. 241 | * 242 | * After this call, the buffer is no longer owned by the user and may not be 243 | * queued again until it is dequeued. 244 | * 245 | * @param stream Stream 246 | * @param buf Buffer to return (must have been dequeued) 247 | */ 248 | int funnel_stream_return(struct funnel_stream *stream, 249 | struct funnel_buffer *buf); 250 | 251 | /** 252 | * Get the dimensions of a Funnel buffer. 253 | * 254 | * @param buf Buffer 255 | * @param width Output width 256 | * @param height Output height 257 | */ 258 | void funnel_buffer_get_size(struct funnel_buffer *buf, uint32_t *pwidth, 259 | uint32_t *pheight); 260 | -------------------------------------------------------------------------------- /test-egl.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #define GL_GLEXT_PROTOTYPES 19 | #include 20 | #define EGL_EGLEXT_PROTOTYPES 21 | #include 22 | #include 23 | 24 | #define ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a))) 25 | 26 | static void initialize_egl(Display *x11_display, Window x11_window, 27 | EGLDisplay *egl_display, EGLContext *egl_context, 28 | EGLSurface *egl_surface) { 29 | 30 | PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT = 31 | (PFNEGLQUERYDEVICESTRINGEXTPROC)eglGetProcAddress( 32 | "eglQueryDeviceStringEXT"); 33 | PFNEGLQUERYDISPLAYATTRIBEXTPROC eglQueryDisplayAttribEXT = 34 | (PFNEGLQUERYDISPLAYATTRIBEXTPROC)eglGetProcAddress( 35 | "eglQueryDisplayAttribEXT"); 36 | PFNEGLQUERYDMABUFMODIFIERSEXTPROC eglQueryDmaBufModifiersEXT = 37 | (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress( 38 | "eglQueryDmaBufModifiersEXT"); 39 | assert(eglQueryDeviceStringEXT); 40 | assert(eglQueryDisplayAttribEXT); 41 | assert(eglQueryDmaBufModifiersEXT); 42 | 43 | // Set OpenGL rendering API 44 | eglBindAPI(EGL_OPENGL_API); 45 | 46 | // get an EGL display connection 47 | EGLDisplay display = eglGetDisplay(x11_display); 48 | 49 | // initialize the EGL display connection 50 | eglInitialize(display, NULL, NULL); 51 | 52 | // get an appropriate EGL frame buffer configuration 53 | EGLConfig config; 54 | EGLint num_config; 55 | EGLint const attribute_list_config[] = {EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, 56 | EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, 57 | EGL_NONE}; 58 | eglChooseConfig(display, attribute_list_config, &config, 1, &num_config); 59 | 60 | // create an EGL rendering context 61 | EGLint const attrib_list[] = {EGL_CONTEXT_MAJOR_VERSION, 3, 62 | EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; 63 | EGLContext context = 64 | eglCreateContext(display, config, EGL_NO_CONTEXT, attrib_list); 65 | 66 | // create an EGL window surface 67 | EGLSurface surface = 68 | eglCreateWindowSurface(display, config, x11_window, NULL); 69 | 70 | // connect the context to the surface 71 | eglMakeCurrent(display, surface, surface, context); 72 | 73 | // Return 74 | *egl_display = display; 75 | *egl_context = context; 76 | *egl_surface = surface; 77 | } 78 | 79 | int u_frame; 80 | 81 | void gl_setup_scene(void) { 82 | // Shader source that draws a textures quad 83 | const char *vertex_shader_source = 84 | "#version 330 core\n" 85 | "uniform float frame;\n" 86 | "layout (location = 0) in vec3 aPos;\n" 87 | "layout (location = 1) in vec2 aTexCoords;\n" 88 | 89 | "out vec2 TexCoords;\n" 90 | 91 | "void main()\n" 92 | "{\n" 93 | " float a = frame * 3.141592 / 4.;" 94 | " mat4 rot = mat4(cos(a), -sin(a), 0., 0.,\n" 95 | " sin(a), cos(a), 0., 0., \n" 96 | " 0., 0., 1., 0., \n" 97 | " 0., 0., 0., 1.);\n" 98 | " TexCoords = aTexCoords;\n" 99 | " vec4 pos = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" 100 | " pos = vec4(0.1,0.1,0.1,1.0) * pos;\n" 101 | " pos += vec4(0.5,0.5,0.0,0.0);\n" 102 | " gl_Position = rot * pos;\n" 103 | "}\0"; 104 | const char *fragment_shader_source = 105 | "#version 330 core\n" 106 | "out vec4 FragColor;\n" 107 | 108 | "in vec2 TexCoords;\n" 109 | 110 | "uniform sampler2D Texture1;\n" 111 | 112 | "void main()\n" 113 | "{\n" 114 | " FragColor = vec4(1., 0., 0., 1.);\n" 115 | "}\0"; 116 | 117 | // vertex shader 118 | int vertex_shader = glCreateShader(GL_VERTEX_SHADER); 119 | glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL); 120 | glCompileShader(vertex_shader); 121 | // fragment shader 122 | int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); 123 | glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL); 124 | glCompileShader(fragment_shader); 125 | // link shaders 126 | int shader_program = glCreateProgram(); 127 | glAttachShader(shader_program, vertex_shader); 128 | glAttachShader(shader_program, fragment_shader); 129 | glLinkProgram(shader_program); 130 | // delete shaders 131 | glDeleteShader(vertex_shader); 132 | glDeleteShader(fragment_shader); 133 | 134 | u_frame = glGetUniformLocation(shader_program, "frame"); 135 | 136 | // quad 137 | float vertices[] = { 138 | 0.f, 1.f, 0.0f, 1.0f, 0.0f, // top center 139 | 1.f, -1.f, 0.0f, 1.0f, 1.0f, // bottom right 140 | -1.f, -1.f, 0.0f, 0.0f, 1.0f, // bottom left 141 | }; 142 | unsigned int indices[] = { 143 | 0, 1, 2, // first Triangle 144 | }; 145 | 146 | unsigned int VBO, VAO, EBO; 147 | glGenVertexArrays(1, &VAO); 148 | glGenBuffers(1, &VBO); 149 | glGenBuffers(1, &EBO); 150 | glBindVertexArray(VAO); 151 | 152 | glBindBuffer(GL_ARRAY_BUFFER, VBO); 153 | glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 154 | 155 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 156 | glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, 157 | GL_STATIC_DRAW); 158 | 159 | glEnableVertexAttribArray(0); 160 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 161 | (void *)0); 162 | glEnableVertexAttribArray(1); 163 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 164 | (void *)(3 * sizeof(float))); 165 | 166 | glBindBuffer(GL_ARRAY_BUFFER, 0); 167 | 168 | glBindVertexArray(0); 169 | 170 | // Prebind needed stuff for drawing 171 | glUseProgram(shader_program); 172 | glBindVertexArray(VAO); 173 | } 174 | 175 | void gl_draw_scene(GLuint texture) { 176 | // clear 177 | glClearColor(0.0f, 0.0f, 0.0f, 0.0f); 178 | glClear(GL_COLOR_BUFFER_BIT); 179 | 180 | // draw quad 181 | // VAO and shader program are already bound from the call to gl_setup_scene 182 | glActiveTexture(GL_TEXTURE0); 183 | glBindTexture(GL_TEXTURE_2D, texture); 184 | glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); 185 | } 186 | 187 | void gl_draw_triangle(void) { 188 | static float frame = 0; 189 | // clear 190 | glClearColor(0.0f, 0.0f, 0.0f, 0.0f); 191 | glClear(GL_COLOR_BUFFER_BIT); 192 | 193 | glUniform1f(u_frame, frame++); 194 | glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0); 195 | } 196 | 197 | void create_x11_window(Display **x11_display, Window *x11_window, int w, 198 | int h) { 199 | // Open X11 display and create window 200 | Display *display = XOpenDisplay(NULL); 201 | int screen = DefaultScreen(display); 202 | Window window = XCreateSimpleWindow( 203 | display, RootWindow(display, screen), 10, 10, w, h, 1, 204 | BlackPixel(display, screen), WhitePixel(display, screen)); 205 | XStoreName(display, window, "Client"); 206 | XMapWindow(display, window); 207 | 208 | // Return 209 | *x11_display = display; 210 | *x11_window = window; 211 | } 212 | 213 | EGLDisplay egl_display; 214 | EGLContext egl_context; 215 | EGLSurface egl_surface; 216 | 217 | int do_init(uint32_t width, uint32_t height) { 218 | // Create X11 window 219 | Display *x11_display; 220 | Window x11_window; 221 | create_x11_window(&x11_display, &x11_window, width, height); 222 | 223 | // Initialize EGL 224 | initialize_egl(x11_display, x11_window, &egl_display, &egl_context, 225 | &egl_surface); 226 | 227 | // Setup GL scene 228 | gl_setup_scene(); 229 | 230 | return 0; 231 | } 232 | 233 | double timef(void) { 234 | double val; 235 | static double base = 0; 236 | struct timeval tv; 237 | gettimeofday(&tv, NULL); 238 | 239 | val = tv.tv_sec + (double)tv.tv_usec / 1000000.f; 240 | if (!base) 241 | base = val; 242 | 243 | return val - base; 244 | } 245 | 246 | int main(int argc, char **argv) { 247 | int ret; 248 | struct funnel_ctx *ctx; 249 | struct funnel_stream *stream; 250 | uint32_t width = 512; 251 | uint32_t height = 512; 252 | 253 | enum funnel_mode mode = FUNNEL_ASYNC; 254 | 255 | if (argc > 1 && !strcmp(argv[1], "-async")) 256 | mode = FUNNEL_ASYNC; 257 | if (argc > 1 && !strcmp(argv[1], "-single")) 258 | mode = FUNNEL_SINGLE_BUFFERED; 259 | if (argc > 1 && !strcmp(argv[1], "-double")) 260 | mode = FUNNEL_DOUBLE_BUFFERED; 261 | if (argc > 1 && !strcmp(argv[1], "-sync")) 262 | mode = FUNNEL_SYNC; 263 | 264 | do_init(width, height); 265 | 266 | eglSwapInterval(egl_display, mode == FUNNEL_ASYNC ? 1 : 0); 267 | 268 | ret = funnel_init(&ctx); 269 | assert(ret == 0); 270 | 271 | ret = funnel_stream_create(ctx, "Funnel Test", &stream); 272 | assert(ret == 0); 273 | 274 | ret = funnel_stream_init_egl(stream, egl_display); 275 | assert(ret == 0); 276 | 277 | ret = funnel_stream_set_size(stream, width, height); 278 | assert(ret == 0); 279 | 280 | ret = funnel_stream_set_mode(stream, mode); 281 | assert(ret == 0); 282 | 283 | ret = 284 | funnel_stream_set_rate(stream, FUNNEL_RATE_VARIABLE, 285 | FUNNEL_FRACTION(1, 1), FUNNEL_FRACTION(1000, 1)); 286 | assert(ret == 0); 287 | 288 | ret = funnel_stream_egl_add_format(stream, FUNNEL_EGL_FORMAT_RGBA8888); 289 | assert(ret == 0); 290 | ret = funnel_stream_egl_add_format(stream, FUNNEL_EGL_FORMAT_RGB888); 291 | assert(ret == 0); 292 | 293 | ret = funnel_stream_start(stream); 294 | assert(ret == 0); 295 | 296 | GLuint fb; 297 | glGenFramebuffers(1, &fb); 298 | 299 | while (1) { 300 | 301 | assert(glGetError() == GL_NO_ERROR); 302 | 303 | struct funnel_buffer *buf; 304 | 305 | ret = funnel_stream_dequeue(stream, &buf); 306 | float t = timef(); 307 | assert(ret == 0); 308 | if (!buf) { 309 | fprintf(stderr, "[%f] No buffers\n", t); 310 | } else { 311 | fprintf(stderr, "[%f] Got buffer\n", t); 312 | } 313 | 314 | gl_draw_triangle(); 315 | 316 | if (buf) { 317 | EGLImage image; 318 | 319 | ret = funnel_buffer_get_egl_image(buf, &image); 320 | assert(ret == 0); 321 | 322 | GLuint color_tex; 323 | glGenTextures(1, &color_tex); 324 | glBindTexture(GL_TEXTURE_2D, color_tex); 325 | glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); 326 | 327 | glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, fb); 328 | glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, 329 | GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 330 | color_tex, 0); 331 | 332 | glBlitFramebuffer(0, height, width, 0, 0, 0, width, height, 333 | GL_COLOR_BUFFER_BIT, GL_NEAREST); 334 | 335 | glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, 336 | GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 337 | 0, 0); 338 | glDeleteTextures(1, &color_tex); 339 | glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); 340 | 341 | glFlush(); 342 | } 343 | 344 | eglSwapBuffers(egl_display, egl_surface); 345 | 346 | if (buf) { 347 | ret = funnel_stream_enqueue(stream, buf); 348 | if (ret < 0) { 349 | fprintf(stderr, "Queue failed: %d\n", ret); 350 | } 351 | assert(ret == 0 || ret == -ESTALE); 352 | } 353 | } 354 | 355 | ret = funnel_stream_stop(stream); 356 | assert(ret == 0); 357 | 358 | funnel_stream_destroy(stream); 359 | 360 | funnel_shutdown(ctx); 361 | } 362 | -------------------------------------------------------------------------------- /src/funnel.c: -------------------------------------------------------------------------------- 1 | #include "funnel.h" 2 | #include "funnel_internal.h" 3 | #include "pipewire/stream.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | 29 | static struct { 30 | uint32_t drm_format; 31 | enum spa_video_format spa_format; 32 | } supported_formats[] = { 33 | { 34 | .drm_format = GBM_FORMAT_ARGB8888, 35 | .spa_format = SPA_VIDEO_FORMAT_BGRA, 36 | }, 37 | { 38 | .drm_format = GBM_FORMAT_RGBA8888, 39 | .spa_format = SPA_VIDEO_FORMAT_ABGR, 40 | }, 41 | { 42 | .drm_format = GBM_FORMAT_ABGR8888, 43 | .spa_format = SPA_VIDEO_FORMAT_RGBA, 44 | }, 45 | { 46 | .drm_format = GBM_FORMAT_BGRA8888, 47 | .spa_format = SPA_VIDEO_FORMAT_ARGB, 48 | }, 49 | { 50 | .drm_format = GBM_FORMAT_XRGB8888, 51 | .spa_format = SPA_VIDEO_FORMAT_BGRx, 52 | }, 53 | { 54 | .drm_format = GBM_FORMAT_RGBX8888, 55 | .spa_format = SPA_VIDEO_FORMAT_xBGR, 56 | }, 57 | { 58 | .drm_format = GBM_FORMAT_XBGR8888, 59 | .spa_format = SPA_VIDEO_FORMAT_RGBx, 60 | }, 61 | { 62 | .drm_format = GBM_FORMAT_BGRX8888, 63 | .spa_format = SPA_VIDEO_FORMAT_xRGB, 64 | }, 65 | }; 66 | 67 | /////////////////////////////////////////////// 68 | 69 | static void free_params(const struct spa_pod **params, size_t count) { 70 | for (size_t i = 0; i < count; i++) 71 | free((void *)params[i]); 72 | } 73 | 74 | static int build_formats(struct funnel_stream *stream, bool fixate, 75 | const struct spa_pod **params); 76 | 77 | static void on_core_error(void *data, uint32_t id, int seq, int res, 78 | const char *message) { 79 | struct funnel_ctx *ctx = data; 80 | 81 | pw_log_error("error id:%u seq:%d res:%d (%s): %s", id, seq, res, 82 | spa_strerror(res), message); 83 | 84 | if (id == PW_ID_CORE) { 85 | ctx->dead = true; 86 | } 87 | } 88 | 89 | static const struct pw_core_events core_events = { 90 | PW_VERSION_CORE_EVENTS, 91 | .error = on_core_error, 92 | }; 93 | 94 | static void on_add_buffer(void *data, struct pw_buffer *pwbuffer) { 95 | struct funnel_stream *stream = data; 96 | 97 | int flags = GBM_BO_USE_RENDERING; 98 | 99 | struct spa_data *spa_data = pwbuffer->buffer->datas; 100 | assert(spa_data[0].type & (1 << SPA_DATA_DmaBuf)); 101 | 102 | struct gbm_bo *bo = NULL; 103 | 104 | bo = gbm_bo_create_with_modifiers2(stream->gbm, stream->cur.width, 105 | stream->cur.height, stream->cur.format, 106 | &stream->cur.modifier, 1, flags); 107 | 108 | assert(bo); 109 | 110 | struct funnel_buffer *buffer = calloc(1, sizeof(struct funnel_buffer)); 111 | buffer->pw_buffer = pwbuffer; 112 | buffer->stream = stream; 113 | buffer->bo = bo; 114 | 115 | fprintf(stderr, "on_add_buffer: %p -> %p\n", pwbuffer, buffer); 116 | 117 | for (int i = 0; i < ARRAY_SIZE(buffer->fds); i++) { 118 | buffer->fds[i] = -1; 119 | } 120 | 121 | pwbuffer->user_data = buffer; 122 | 123 | for (int i = 0; i < stream->cur.plane_count; ++i) { 124 | spa_data[i].type = SPA_DATA_DmaBuf; 125 | spa_data[i].flags = SPA_DATA_FLAG_READWRITE; 126 | spa_data[i].mapoffset = 0; 127 | spa_data[i].maxsize = 128 | i == 0 ? stream->cur.strides[i] * stream->cur.height : 0; 129 | spa_data[i].fd = buffer->fds[i] = gbm_bo_get_fd(bo); 130 | spa_data[i].data = NULL; 131 | spa_data[i].chunk->offset = stream->cur.offsets[i]; 132 | spa_data[i].chunk->size = spa_data[i].maxsize; 133 | spa_data[i].chunk->stride = stream->cur.strides[i]; 134 | spa_data[i].chunk->flags = SPA_CHUNK_FLAG_NONE; 135 | }; 136 | 137 | if (stream->funcs) 138 | stream->funcs->alloc_buffer(buffer); 139 | 140 | stream->num_buffers++; 141 | } 142 | 143 | static void funnel_buffer_free(struct funnel_buffer *buffer) { 144 | struct funnel_stream *stream = buffer->stream; 145 | if (stream->funcs) 146 | stream->funcs->free_buffer(buffer); 147 | 148 | gbm_bo_destroy(buffer->bo); 149 | for (int i = 0; i < ARRAY_SIZE(buffer->fds); i++) { 150 | if (buffer->fds[i] >= 0) 151 | close(buffer->fds[i]); 152 | } 153 | 154 | free(buffer); 155 | } 156 | 157 | static void on_remove_buffer(void *data, struct pw_buffer *pwbuffer) { 158 | fprintf(stderr, "on_remove_buffer: %p -> %p\n", pwbuffer, 159 | pwbuffer->user_data); 160 | 161 | if (pwbuffer->user_data) { 162 | struct funnel_buffer *buffer = pwbuffer->user_data; 163 | struct funnel_stream *stream = buffer->stream; 164 | 165 | if (!buffer->dequeued) { 166 | funnel_buffer_free(buffer); 167 | if (buffer == stream->pending_buffer) 168 | stream->pending_buffer = NULL; 169 | } else { 170 | buffer->pw_buffer = NULL; 171 | fprintf(stderr, "defer buffer free: %p\n", buffer); 172 | } 173 | 174 | pwbuffer->user_data = NULL; 175 | stream->num_buffers--; 176 | } 177 | } 178 | 179 | static void update_timeouts(struct funnel_stream *stream) { 180 | struct timespec timeout, interval, *to, *iv; 181 | enum pw_stream_state state = pw_stream_get_state(stream->stream, NULL); 182 | 183 | bool timeouts_active = false; 184 | 185 | if (state == PW_STREAM_STATE_STREAMING && 186 | pw_stream_is_driving(stream->stream) && 187 | !pw_stream_is_lazy(stream->stream) && 188 | stream->cur.config.mode != FUNNEL_ASYNC) 189 | timeouts_active = true; 190 | 191 | if (!timeouts_active) { 192 | to = iv = NULL; 193 | } else { 194 | struct spa_fraction rate = stream->cur.video_format.framerate; 195 | 196 | if (rate.num == 0 || rate.denom == 0) { 197 | // Pick a default rate of 60 FPS 198 | rate.num = 60; 199 | rate.denom = 1; 200 | fprintf(stderr, "default rate: 60 FPS\n"); 201 | } else { 202 | fprintf(stderr, "negotiated rate: %d/%d FPS\n", rate.num, 203 | rate.denom); 204 | } 205 | uint64_t nsec = rate.denom * 1000000000L / rate.num; 206 | 207 | timeout.tv_sec = 0; 208 | timeout.tv_nsec = 1; 209 | interval.tv_sec = nsec / 1000000000L; 210 | interval.tv_nsec = nsec % 1000000000L; 211 | to = &timeout; 212 | iv = &interval; 213 | } 214 | pw_loop_update_timer(pw_thread_loop_get_loop(stream->ctx->loop), 215 | stream->timer, to, iv, false); 216 | } 217 | 218 | static int return_buffer(struct funnel_stream *stream, 219 | struct funnel_buffer *buf) { 220 | if (!buf->pw_buffer) { 221 | funnel_buffer_free(buf); 222 | return -ESTALE; 223 | } 224 | 225 | return pw_stream_return_buffer(stream->stream, buf->pw_buffer); 226 | } 227 | 228 | static void reset_buffers(struct funnel_stream *stream) { 229 | if (stream->pending_buffer) { 230 | return_buffer(stream, stream->pending_buffer); 231 | stream->pending_buffer = NULL; 232 | } 233 | } 234 | 235 | static void on_state_changed(void *data, enum pw_stream_state old, 236 | enum pw_stream_state state, 237 | const char *error_message) { 238 | struct funnel_stream *stream = data; 239 | 240 | fprintf(stderr, "on_state_changed: %s -> %s %s\n", 241 | pw_stream_state_as_string(old), pw_stream_state_as_string(state), 242 | error_message); 243 | switch (state) { 244 | case PW_STREAM_STATE_ERROR: 245 | fprintf(stderr, "PW_STREAM_STATE_ERROR\n"); 246 | reset_buffers(stream); 247 | break; 248 | case PW_STREAM_STATE_PAUSED: 249 | fprintf(stderr, "PW_STREAM_STATE_PAUSED\n"); 250 | reset_buffers(stream); 251 | update_timeouts(stream); 252 | break; 253 | case PW_STREAM_STATE_STREAMING: 254 | fprintf(stderr, "PW_STREAM_STATE_STREAMING\n"); 255 | printf("driving:%d lazy:%d\n", pw_stream_is_driving(stream->stream), 256 | pw_stream_is_lazy(stream->stream)); 257 | update_timeouts(stream); 258 | break; 259 | case PW_STREAM_STATE_CONNECTING: 260 | fprintf(stderr, "PW_STREAM_STATE_CONNECTING\n"); 261 | update_timeouts(stream); 262 | reset_buffers(stream); 263 | break; 264 | case PW_STREAM_STATE_UNCONNECTED: 265 | fprintf(stderr, "PW_STREAM_STATE_UNCONNECTED\n"); 266 | update_timeouts(stream); 267 | reset_buffers(stream); 268 | break; 269 | } 270 | } 271 | 272 | static bool test_create_dmabuf(struct funnel_stream *stream, uint32_t format, 273 | uint64_t *modifiers, size_t num_modifiers) { 274 | int flags = GBM_BO_USE_RENDERING; 275 | 276 | struct gbm_bo *bo; 277 | 278 | bo = gbm_bo_create_with_modifiers2(stream->gbm, 279 | stream->cur.video_format.size.width, 280 | stream->cur.video_format.size.height, 281 | format, modifiers, num_modifiers, flags); 282 | if (!bo) 283 | return false; 284 | 285 | stream->cur.width = gbm_bo_get_width(bo); 286 | stream->cur.height = gbm_bo_get_height(bo); 287 | assert(stream->cur.width == stream->cur.video_format.size.width); 288 | assert(stream->cur.height == stream->cur.video_format.size.height); 289 | stream->cur.plane_count = gbm_bo_get_plane_count(bo); 290 | fprintf(stderr, "planes: %d\n", stream->cur.plane_count); 291 | for (int i = 0; i < stream->cur.plane_count; i++) { 292 | stream->cur.strides[i] = gbm_bo_get_stride_for_plane(bo, i); 293 | stream->cur.offsets[i] = gbm_bo_get_offset(bo, i); 294 | } 295 | stream->cur.format = gbm_bo_get_format(bo); 296 | stream->cur.modifier = gbm_bo_get_modifier(bo); 297 | 298 | gbm_bo_destroy(bo); 299 | 300 | return true; 301 | } 302 | 303 | static void on_param_changed(void *data, uint32_t id, 304 | const struct spa_pod *format) { 305 | fprintf(stderr, "on_param_changed: %d %p\n", id, format); 306 | 307 | struct funnel_stream *stream = data; 308 | 309 | if (!format || id != SPA_PARAM_Format) { 310 | fprintf(stderr, " ->ignored\n"); 311 | return; 312 | } 313 | 314 | int i; 315 | uint32_t dmabuf_format; 316 | 317 | spa_format_video_raw_parse(format, &stream->cur.video_format); 318 | 319 | for (i = 0; i < ARRAY_SIZE(supported_formats); i++) { 320 | if (supported_formats[i].spa_format == 321 | stream->cur.video_format.format) { 322 | dmabuf_format = supported_formats[i].drm_format; 323 | break; 324 | } 325 | } 326 | if (i >= ARRAY_SIZE(supported_formats)) { 327 | pw_log_error("unsupported format %d", stream->cur.video_format.format); 328 | return; 329 | } 330 | 331 | const struct spa_pod_prop *mod_prop = 332 | spa_pod_find_prop(format, NULL, SPA_FORMAT_VIDEO_modifier); 333 | 334 | assert(mod_prop); 335 | 336 | const uint32_t value_count = SPA_POD_CHOICE_N_VALUES(&mod_prop->value); 337 | const uint64_t *values = (uint64_t *)SPA_POD_CHOICE_VALUES( 338 | &mod_prop->value); // values[0] is the preferred choice 339 | 340 | int mod_count = 0; 341 | uint64_t *modifiers = malloc(value_count * sizeof(uint64_t)); 342 | 343 | for (int i = 0; i < value_count; i++) { 344 | bool found = false; 345 | for (int j = 0; j < mod_count; j++) { 346 | if (values[i] == modifiers[j]) { 347 | found = true; 348 | break; 349 | } 350 | } 351 | if (!found) { 352 | modifiers[mod_count++] = values[i]; 353 | } 354 | } 355 | 356 | if (mod_count > 1) { 357 | for (int j = 0; j < mod_count; j++) { 358 | if (modifiers[j] == DRM_FORMAT_MOD_INVALID) { 359 | mod_count--; 360 | memmove(&modifiers[j], &modifiers[j + 1], mod_count - j); 361 | break; 362 | } 363 | } 364 | } 365 | 366 | if (stream->cur.width != stream->cur.video_format.size.width || 367 | stream->cur.height != stream->cur.video_format.size.height || 368 | stream->cur.format != dmabuf_format) { 369 | 370 | if (!test_create_dmabuf(stream, dmabuf_format, modifiers, mod_count)) { 371 | pw_log_error("failed to create dmabuf for format 0x%x", 372 | dmabuf_format); 373 | return; 374 | } 375 | 376 | fprintf(stderr, "Created buffer with format 0x%x and modifier 0x%llx\n", 377 | stream->cur.format, (long long)stream->cur.modifier); 378 | 379 | size_t num_formats = 380 | pw_array_get_len(&stream->cur.config.formats, struct funnel_format); 381 | 382 | const struct spa_pod **params = 383 | calloc(num_formats + 1, sizeof(struct spa_pod *)); 384 | 385 | int num_params = build_formats(stream, true, params); 386 | assert(num_params <= (num_formats + 1)); 387 | 388 | stream->cur.ready = false; 389 | pw_stream_update_params(stream->stream, params, num_params); 390 | free_params(params, num_params); 391 | return; 392 | } 393 | 394 | const int buffertypes = (1 << SPA_DATA_DmaBuf); 395 | 396 | spa_auto(spa_pod_dynamic_builder) pod_builder = {0}; 397 | struct spa_pod_frame f; 398 | spa_pod_dynamic_builder_init(&pod_builder, NULL, 0, 1024); 399 | 400 | int num_params = 0; 401 | const struct spa_pod *params[8]; 402 | 403 | // Fallback buffer parameters for DmaBuf with implicit sync or MemFd 404 | spa_pod_builder_push_object( 405 | &pod_builder.b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); 406 | spa_pod_builder_add( 407 | &pod_builder.b, SPA_PARAM_BUFFERS_buffers, 408 | SPA_POD_CHOICE_RANGE_Int(stream->cur.config.buffers.def, 409 | stream->cur.config.buffers.min, 410 | stream->cur.config.buffers.max), 411 | SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes), 0); 412 | spa_pod_builder_add(&pod_builder.b, SPA_PARAM_BUFFERS_blocks, 413 | SPA_POD_Int(stream->cur.plane_count), 0); 414 | params[num_params++] = 415 | (struct spa_pod *)spa_pod_builder_pop(&pod_builder.b, &f); 416 | 417 | params[num_params++] = (struct spa_pod *)spa_pod_builder_add_object( 418 | &pod_builder.b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, 419 | SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, 420 | SPA_POD_Int(sizeof(struct spa_meta_header))); 421 | 422 | pw_stream_update_params(stream->stream, params, num_params); 423 | stream->cur.ready = true; 424 | } 425 | 426 | static void on_command(void *data, const struct spa_command *command) { 427 | struct funnel_stream *stream = data; 428 | 429 | switch (SPA_NODE_COMMAND_ID(command)) { 430 | case SPA_NODE_COMMAND_RequestProcess: 431 | fprintf(stderr, "TRIGGER %p\n", stream); 432 | pw_stream_trigger_process(stream->stream); 433 | break; 434 | default: 435 | break; 436 | } 437 | } 438 | 439 | static void unblock_process_thread(struct funnel_stream *stream) { 440 | if (stream->cycle_state == SYNC_CYCLE_ACTIVE) { 441 | pw_thread_loop_accept(stream->ctx->loop); 442 | } 443 | stream->cycle_state = SYNC_CYCLE_INACTIVE; 444 | } 445 | 446 | static void on_process(void *data) { 447 | struct funnel_stream *stream = data; 448 | 449 | static int frame = 0; 450 | fprintf(stderr, "PROCESS %d\n", ++frame); 451 | 452 | if (!stream->active) 453 | return; 454 | 455 | if (stream->cur.config.mode == FUNNEL_SYNC) { 456 | // Sync mode handshake 457 | if (stream->cycle_state == SYNC_CYCLE_WAITING) { 458 | stream->cycle_state = SYNC_CYCLE_ACTIVE; 459 | fprintf(stderr, "PROCESS %d SIGNAL SYNC\n", frame); 460 | pw_thread_loop_signal(stream->ctx->loop, true); 461 | fprintf(stderr, "PROCESS %d ACCEPTED\n", frame); 462 | } 463 | // We should have a buffer now, if the cycle succeeded 464 | } 465 | 466 | if (stream->pending_buffer) { 467 | struct funnel_buffer *buf = stream->pending_buffer; 468 | stream->pending_buffer = NULL; 469 | 470 | assert(buf->pw_buffer); 471 | fprintf(stderr, "PROCESS %d QUEUED BUFFER\n", frame); 472 | pw_stream_queue_buffer(stream->stream, buf->pw_buffer); 473 | } 474 | 475 | pw_thread_loop_signal(stream->ctx->loop, false); 476 | 477 | fprintf(stderr, "PROCESS %d DONE\n", frame); 478 | } 479 | 480 | static const struct pw_stream_events stream_events = { 481 | PW_VERSION_STREAM_EVENTS, 482 | .add_buffer = on_add_buffer, 483 | .remove_buffer = on_remove_buffer, 484 | .state_changed = on_state_changed, 485 | .param_changed = on_param_changed, 486 | .command = on_command, 487 | .process = on_process, 488 | }; 489 | 490 | static void on_timeout(void *userdata, uint64_t expirations) { 491 | struct funnel_stream *stream = userdata; 492 | 493 | fprintf(stderr, "TIMEOUT %p\n", stream); 494 | pw_stream_trigger_process(stream->stream); 495 | } 496 | 497 | static struct spa_pod * 498 | build_format(enum spa_video_format format, struct spa_rectangle *resolution, 499 | struct spa_fraction *def_rate, struct spa_fraction *min_rate, 500 | struct spa_fraction *max_rate, const uint64_t *modifiers, 501 | size_t num_modifiers, uint32_t modifiers_flags) { 502 | struct spa_pod_frame f[2]; 503 | 504 | struct spa_pod_dynamic_builder pod_builder; 505 | spa_pod_dynamic_builder_init(&pod_builder, NULL, 0, 1024); 506 | struct spa_pod_builder *b = &pod_builder.b; 507 | 508 | spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, 509 | SPA_PARAM_EnumFormat); 510 | spa_pod_builder_add(b, SPA_FORMAT_mediaType, 511 | SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); 512 | spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, 513 | SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); 514 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(resolution), 515 | 0); 516 | // spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, 517 | // SPA_POD_Fraction(def_rate), 0); 518 | spa_pod_builder_add( 519 | b, SPA_FORMAT_VIDEO_framerate, 520 | SPA_POD_CHOICE_RANGE_Fraction(def_rate, min_rate, max_rate), 0); 521 | spa_pod_builder_add( 522 | b, SPA_FORMAT_VIDEO_maxFramerate, 523 | SPA_POD_CHOICE_RANGE_Fraction(def_rate, min_rate, max_rate), 0); 524 | 525 | spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); 526 | 527 | if (num_modifiers) { 528 | spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, modifiers_flags); 529 | spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); 530 | 531 | for (size_t i = 0; i < num_modifiers; i++) { 532 | spa_pod_builder_long(b, modifiers[i]); 533 | if (i == 0) { 534 | spa_pod_builder_long(b, modifiers[i]); 535 | } 536 | } 537 | spa_pod_builder_pop(b, &f[1]); 538 | } 539 | return (struct spa_pod *)spa_pod_builder_pop(b, &f[0]); 540 | } 541 | 542 | static int build_formats(struct funnel_stream *stream, bool fixate, 543 | const struct spa_pod **params) { 544 | struct funnel_stream_config *config = &stream->cur.config; 545 | 546 | struct spa_fraction def_rate = to_spa_fraction(config->rate.def); 547 | struct spa_fraction min_rate = to_spa_fraction(config->rate.min); 548 | struct spa_fraction max_rate = to_spa_fraction(config->rate.max); 549 | 550 | struct spa_rectangle resolution = 551 | SPA_RECTANGLE(config->width, config->height); 552 | 553 | int num_params = 0; 554 | if (fixate) { 555 | num_params++; 556 | *params++ = build_format( 557 | stream->cur.video_format.format, &resolution, &def_rate, &min_rate, 558 | &max_rate, &stream->cur.modifier, 1, SPA_POD_PROP_FLAG_MANDATORY); 559 | } 560 | 561 | struct funnel_format *format; 562 | pw_array_for_each (format, &config->formats) { 563 | num_params++; 564 | *params++ = build_format( 565 | format->spa_format, &resolution, &def_rate, &min_rate, &max_rate, 566 | format->modifiers, format->num_modifiers, 567 | SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); 568 | } 569 | 570 | return num_params; 571 | } 572 | 573 | int funnel_init(struct funnel_ctx **pctx) { 574 | struct funnel_ctx *ctx; 575 | 576 | *pctx = NULL; 577 | ctx = calloc(1, sizeof(*ctx)); 578 | assert(ctx); 579 | 580 | pw_init(NULL, NULL); 581 | 582 | ctx->loop = pw_thread_loop_new("funnel_loop", NULL); 583 | assert(ctx->loop); 584 | 585 | pw_thread_loop_lock(ctx->loop); 586 | 587 | pw_thread_loop_start(ctx->loop); 588 | 589 | ctx->context = pw_context_new(pw_thread_loop_get_loop(ctx->loop), NULL, 0); 590 | assert(ctx->context); 591 | 592 | if ((ctx->core = pw_context_connect(ctx->context, NULL, 0)) == NULL) { 593 | pw_log_error("failed to connect to PipeWire"); 594 | pw_thread_loop_unlock(ctx->loop); 595 | funnel_shutdown(ctx); 596 | return -ECONNREFUSED; 597 | } 598 | 599 | pw_core_add_listener(ctx->core, &ctx->core_listener, &core_events, ctx); 600 | 601 | pw_thread_loop_unlock(ctx->loop); 602 | 603 | *pctx = ctx; 604 | return 0; 605 | } 606 | 607 | void funnel_shutdown(struct funnel_ctx *ctx) { 608 | if (!ctx) 609 | return; 610 | 611 | assert(ctx->loop); 612 | 613 | /* Thread loop should be unlocked here */ 614 | pw_thread_loop_stop(ctx->loop); 615 | 616 | if (ctx->core) 617 | pw_core_disconnect(ctx->core); 618 | 619 | if (ctx->context) 620 | pw_context_destroy(ctx->context); 621 | 622 | pw_thread_loop_destroy(ctx->loop); 623 | 624 | free(ctx); 625 | pw_deinit(); 626 | } 627 | 628 | int funnel_stream_create(struct funnel_ctx *ctx, const char *name, 629 | struct funnel_stream **pstream) { 630 | struct funnel_stream *stream; 631 | assert(ctx); 632 | 633 | pw_thread_loop_lock(ctx->loop); 634 | 635 | if (ctx->dead) 636 | UNLOCK_RETURN(-EIO); 637 | 638 | *pstream = NULL; 639 | stream = calloc(1, sizeof(*stream)); 640 | assert(stream); 641 | 642 | stream->ctx = ctx; 643 | stream->name = strdup(name); 644 | 645 | funnel_stream_set_mode(stream, FUNNEL_ASYNC); 646 | 647 | stream->config.rate.def = FUNNEL_RATE_VARIABLE; 648 | stream->config.rate.min = FUNNEL_RATE_VARIABLE; 649 | stream->config.rate.max = FUNNEL_RATE_VARIABLE; 650 | 651 | stream->config_pending = true; 652 | 653 | pw_array_init(&stream->config.formats, 32); 654 | pw_array_init(&stream->cur.config.formats, 32); 655 | 656 | stream->timer = pw_loop_add_timer(pw_thread_loop_get_loop(ctx->loop), 657 | on_timeout, stream); 658 | assert(stream->timer); 659 | 660 | *pstream = stream; 661 | 662 | UNLOCK_RETURN(0); 663 | } 664 | 665 | int funnel_stream_init_gbm(struct funnel_stream *stream, int gbm_fd) { 666 | if (stream->gbm) 667 | return -EEXIST; 668 | 669 | if (stream->api != API_UNSET) 670 | return -EEXIST; 671 | 672 | stream->gbm = gbm_create_device(gbm_fd); 673 | if (!stream->gbm) 674 | return -EINVAL; 675 | 676 | stream->api = API_GBM; 677 | return 0; 678 | } 679 | 680 | /** 681 | * Add a supported GBM format. Must be called in preference order (highest to 682 | * lowest). 683 | * 684 | * @param stream Stream 685 | * @param format DRM format (FOURCC) 686 | * @param modifiers Pointer to a list of modifiers (borrow) 687 | * @param num_modifiers Number of modifiers passed 688 | */ 689 | int funnel_stream_gbm_add_format(struct funnel_stream *stream, uint32_t format, 690 | uint64_t *modifiers, size_t num_modifiers) { 691 | int i; 692 | enum spa_video_format spa_format; 693 | 694 | if (!num_modifiers) 695 | return -EINVAL; 696 | 697 | for (i = 0; i < ARRAY_SIZE(supported_formats); i++) { 698 | if (supported_formats[i].drm_format == format) { 699 | spa_format = supported_formats[i].spa_format; 700 | break; 701 | } 702 | } 703 | if (i >= ARRAY_SIZE(supported_formats)) { 704 | return -ENOTSUP; 705 | } 706 | 707 | struct funnel_format *fmt = 708 | pw_array_add(&stream->config.formats, sizeof(struct funnel_format)); 709 | assert(fmt); 710 | 711 | fmt->format = format; 712 | fmt->spa_format = spa_format; 713 | fmt->modifiers = calloc(num_modifiers, sizeof(uint64_t)); 714 | fmt->num_modifiers = num_modifiers; 715 | memcpy(fmt->modifiers, modifiers, num_modifiers * sizeof(uint64_t)); 716 | fprintf(stderr, "modifiers=%p fmt=%p base=%p\n", fmt->modifiers, fmt, 717 | stream->config.formats.data); 718 | 719 | stream->config_pending = true; 720 | return 0; 721 | } 722 | 723 | static void funnel_reset_formats(struct pw_array *formats) { 724 | struct funnel_format *format; 725 | 726 | pw_array_for_each (format, formats) { 727 | free(format->modifiers); 728 | } 729 | pw_array_reset(formats); 730 | } 731 | 732 | static void funnel_free_formats(struct pw_array *formats) { 733 | funnel_reset_formats(formats); 734 | pw_array_clear(formats); 735 | } 736 | 737 | void funnel_stream_clear_formats(struct funnel_stream *stream) { 738 | funnel_reset_formats(&stream->config.formats); 739 | } 740 | 741 | static void funnel_copy_formats(struct pw_array *dst, struct pw_array *src) { 742 | struct funnel_format *sfmt; 743 | 744 | funnel_reset_formats(dst); 745 | 746 | pw_array_for_each (sfmt, src) { 747 | struct funnel_format *fmt = 748 | pw_array_add(dst, sizeof(struct funnel_format)); 749 | assert(fmt); 750 | fprintf(stderr, "fmt %p <- %p [%x %zd]\n", fmt, sfmt, sfmt->format, 751 | sfmt->num_modifiers); 752 | 753 | fmt->format = sfmt->format; 754 | fmt->spa_format = sfmt->spa_format; 755 | fmt->modifiers = calloc(sfmt->num_modifiers, sizeof(uint64_t)); 756 | fmt->num_modifiers = sfmt->num_modifiers; 757 | memcpy(fmt->modifiers, sfmt->modifiers, 758 | sfmt->num_modifiers * sizeof(uint64_t)); 759 | } 760 | } 761 | 762 | int funnel_stream_set_size(struct funnel_stream *stream, uint32_t width, 763 | uint32_t height) { 764 | assert(stream); 765 | 766 | if (!width || !height) 767 | return -EINVAL; 768 | 769 | stream->config.width = width; 770 | stream->config.height = height; 771 | stream->config_pending = true; 772 | 773 | return 0; 774 | } 775 | 776 | int funnel_stream_set_mode(struct funnel_stream *stream, 777 | enum funnel_mode mode) { 778 | 779 | switch (mode) { 780 | case FUNNEL_ASYNC: 781 | case FUNNEL_DOUBLE_BUFFERED: 782 | stream->config.buffers.def = 5; 783 | stream->config.buffers.min = 4; 784 | stream->config.buffers.max = 8; 785 | break; 786 | case FUNNEL_SINGLE_BUFFERED: 787 | case FUNNEL_SYNC: 788 | stream->config.buffers.def = 4; 789 | stream->config.buffers.min = 3; 790 | stream->config.buffers.max = 8; 791 | break; 792 | default: 793 | return -EINVAL; 794 | } 795 | 796 | stream->config.mode = mode; 797 | stream->config_pending = true; 798 | 799 | return 0; 800 | } 801 | 802 | int funnel_stream_set_rate(struct funnel_stream *stream, 803 | struct funnel_fraction def, 804 | struct funnel_fraction min, 805 | struct funnel_fraction max) { 806 | if (!def.den || !min.den || !max.den) 807 | return -EINVAL; 808 | 809 | stream->config.rate.def = def; 810 | stream->config.rate.min = min; 811 | stream->config.rate.max = max; 812 | stream->config_pending = true; 813 | 814 | return 0; 815 | } 816 | 817 | int funnel_stream_get_rate(struct funnel_stream *stream, 818 | struct funnel_fraction *rate) { 819 | struct funnel_ctx *ctx = stream->ctx; 820 | pw_thread_loop_lock(ctx->loop); 821 | 822 | if (!stream->cur.ready) { 823 | *rate = FUNNEL_FRACTION(0, 0); 824 | UNLOCK_RETURN(-EINPROGRESS); 825 | } 826 | 827 | rate->num = stream->cur.video_format.framerate.num; 828 | rate->den = stream->cur.video_format.framerate.denom; 829 | 830 | UNLOCK_RETURN(0); 831 | } 832 | 833 | int funnel_stream_configure(struct funnel_stream *stream) { 834 | struct funnel_ctx *ctx = stream->ctx; 835 | 836 | if (!stream->config_pending) 837 | return 0; 838 | 839 | if (stream->api == API_UNSET) { 840 | pw_log_error("funnel_stream_set_size() must be called before " 841 | "funnel_stream_configure()"); 842 | } 843 | 844 | if (!stream->config.width || !stream->config.width) { 845 | pw_log_error("funnel_stream_set_size() must be called before " 846 | "funnel_stream_configure()"); 847 | return -EINVAL; 848 | } 849 | 850 | size_t num_formats = 851 | pw_array_get_len(&stream->config.formats, struct funnel_format); 852 | 853 | if (!num_formats) { 854 | pw_log_error("no formats configured"); 855 | return -EINVAL; 856 | } 857 | 858 | pw_thread_loop_lock(ctx->loop); 859 | 860 | if (ctx->dead) 861 | UNLOCK_RETURN(-EIO); 862 | 863 | const char *driver_prio = NULL; 864 | bool lazy = false, request = false; 865 | switch (stream->config.mode) { 866 | case FUNNEL_ASYNC: 867 | driver_prio = "1"; 868 | request = true; 869 | break; 870 | case FUNNEL_DOUBLE_BUFFERED: 871 | case FUNNEL_SINGLE_BUFFERED: 872 | case FUNNEL_SYNC: 873 | lazy = true; 874 | break; 875 | } 876 | 877 | bool new_stream = false; 878 | if (!stream->stream) { 879 | new_stream = true; 880 | 881 | struct pw_properties *props; 882 | // clang-format off 883 | props = pw_properties_new( 884 | PW_KEY_MEDIA_TYPE, "Video", 885 | PW_KEY_MEDIA_CLASS, "Stream/Output/Video", 886 | PW_KEY_NODE_SUPPORTS_LAZY, lazy ? "1" : NULL, 887 | PW_KEY_NODE_SUPPORTS_REQUEST, request ? "1" : NULL, 888 | PW_KEY_PRIORITY_DRIVER, driver_prio, 889 | NULL 890 | ); 891 | // clang-format on 892 | assert(props); 893 | 894 | stream->stream = pw_stream_new(ctx->core, stream->name, props); 895 | assert(stream->stream); 896 | 897 | pw_stream_add_listener(stream->stream, &stream->stream_listener, 898 | &stream_events, stream); 899 | } else { 900 | struct pw_properties *props; 901 | 902 | // clang-format off 903 | props = pw_properties_new( 904 | PW_KEY_NODE_SUPPORTS_LAZY, lazy ? "1" : NULL, 905 | PW_KEY_NODE_SUPPORTS_REQUEST, request ? "1" : NULL, 906 | PW_KEY_PRIORITY_DRIVER, driver_prio, 907 | NULL 908 | ); 909 | // clang-format on 910 | assert(props); 911 | 912 | pw_stream_update_properties(stream->stream, &props->dict); 913 | pw_properties_free(props); 914 | } 915 | 916 | funnel_free_formats(&stream->cur.config.formats); 917 | stream->cur.config = stream->config; 918 | pw_array_init(&stream->cur.config.formats, 32); 919 | funnel_copy_formats(&stream->cur.config.formats, &stream->config.formats); 920 | 921 | enum pw_stream_flags flags = 922 | PW_STREAM_FLAG_ALLOC_BUFFERS | PW_STREAM_FLAG_DRIVER; 923 | 924 | const struct spa_pod **params = 925 | calloc(num_formats, sizeof(struct spa_pod *)); 926 | 927 | int num_params = build_formats(stream, false, params); 928 | assert(num_params <= num_formats); 929 | 930 | if (!new_stream) { 931 | stream->cur.ready = false; 932 | pw_stream_update_params(stream->stream, params, num_params); 933 | } else if (pw_stream_connect(stream->stream, PW_DIRECTION_OUTPUT, 934 | SPA_ID_INVALID, flags, params, 935 | num_params) != 0) { 936 | free_params(params, num_params); 937 | pw_log_error("failed to connect to stream"); 938 | pw_stream_destroy(stream->stream); 939 | stream->stream = NULL; 940 | UNLOCK_RETURN(-EIO); 941 | } 942 | 943 | free_params(params, num_params); 944 | 945 | update_timeouts(stream); 946 | 947 | stream->config_pending = false; 948 | 949 | UNLOCK_RETURN(0); 950 | } 951 | 952 | int funnel_buffer_get_gbm_bo(struct funnel_buffer *buf, struct gbm_bo **bo) { 953 | assert(buf->bo); 954 | *bo = buf->bo; 955 | return 0; 956 | } 957 | 958 | int funnel_stream_start(struct funnel_stream *stream) { 959 | int ret = funnel_stream_configure(stream); 960 | if (ret) 961 | return ret; 962 | 963 | assert(stream->stream); 964 | 965 | struct funnel_ctx *ctx = stream->ctx; 966 | pw_thread_loop_lock(ctx->loop); 967 | 968 | if (ctx->dead) 969 | UNLOCK_RETURN(-EIO); 970 | 971 | stream->active = true; 972 | UNLOCK_RETURN(pw_stream_set_active(stream->stream, true)); 973 | } 974 | 975 | int funnel_stream_stop(struct funnel_stream *stream) { 976 | if (!stream->stream) 977 | return -EINVAL; 978 | 979 | struct funnel_ctx *ctx = stream->ctx; 980 | pw_thread_loop_lock(ctx->loop); 981 | 982 | if (ctx->dead) 983 | UNLOCK_RETURN(-EIO); 984 | 985 | // Unblock the process call if blocked 986 | stream->active = false; 987 | unblock_process_thread(stream); 988 | 989 | UNLOCK_RETURN(pw_stream_set_active(stream->stream, false)); 990 | } 991 | 992 | void funnel_stream_destroy(struct funnel_stream *stream) { 993 | if (!stream) 994 | return; 995 | 996 | funnel_stream_stop(stream); 997 | 998 | struct funnel_ctx *ctx = stream->ctx; 999 | pw_thread_loop_lock(ctx->loop); 1000 | 1001 | if (stream->gbm) 1002 | gbm_device_destroy(stream->gbm); 1003 | 1004 | funnel_free_formats(&stream->config.formats); 1005 | funnel_free_formats(&stream->cur.config.formats); 1006 | 1007 | if (stream->stream) { 1008 | spa_hook_remove(&stream->stream_listener); 1009 | pw_stream_disconnect(stream->stream); 1010 | pw_stream_destroy(stream->stream); 1011 | } 1012 | 1013 | if (stream->timer) { 1014 | pw_loop_destroy_source(pw_thread_loop_get_loop(stream->ctx->loop), 1015 | stream->timer); 1016 | } 1017 | 1018 | pw_thread_loop_unlock(ctx->loop); 1019 | 1020 | free((void *)stream->name); 1021 | free(stream); 1022 | } 1023 | 1024 | int funnel_stream_dequeue(struct funnel_stream *stream, 1025 | struct funnel_buffer **pbuf) { 1026 | if (!stream->stream) 1027 | return -EINVAL; 1028 | 1029 | *pbuf = NULL; 1030 | struct funnel_ctx *ctx = stream->ctx; 1031 | pw_thread_loop_lock(ctx->loop); 1032 | 1033 | if (stream->buffers_dequeued > 0) { 1034 | fprintf(stderr, 1035 | "libfunnel: Dequeueing multiple buffers not supported\n"); 1036 | UNLOCK_RETURN(-EINVAL); 1037 | } 1038 | 1039 | enum pw_stream_state state; 1040 | struct pw_buffer *pwbuffer; 1041 | 1042 | for (pwbuffer = NULL;; pw_thread_loop_wait(ctx->loop)) { 1043 | if (ctx->dead) 1044 | UNLOCK_RETURN(-EIO); 1045 | 1046 | if (!stream->active) 1047 | UNLOCK_RETURN(-ESHUTDOWN); 1048 | 1049 | state = pw_stream_get_state(stream->stream, NULL); 1050 | if (state != PW_STREAM_STATE_STREAMING) { 1051 | if (stream->cur.config.mode == FUNNEL_ASYNC) 1052 | UNLOCK_RETURN(0); 1053 | fprintf(stderr, "dequeue: Wait for stream start\n"); 1054 | unblock_process_thread(stream); 1055 | continue; 1056 | } 1057 | 1058 | if (stream->cur.config.mode == FUNNEL_SINGLE_BUFFERED && 1059 | stream->pending_buffer) { 1060 | fprintf(stderr, "dequeue: 1B, waiting for pending frame\n"); 1061 | unblock_process_thread(stream); 1062 | continue; 1063 | } 1064 | 1065 | if (stream->cur.config.mode == FUNNEL_SYNC && 1066 | stream->cycle_state != SYNC_CYCLE_ACTIVE) { 1067 | /* 1068 | * Tell the process callback that we are ready to start processing 1069 | * a frame. 1070 | */ 1071 | fprintf(stderr, "## Wait for process (sync)\n"); 1072 | stream->cycle_state = SYNC_CYCLE_WAITING; 1073 | continue; 1074 | } 1075 | 1076 | fprintf(stderr, "Try dequeue\n"); 1077 | int retries = stream->num_buffers; 1078 | 1079 | assert(stream->num_buffers > 0); 1080 | 1081 | /* 1082 | * Work around PipeWire weirdness with in-use buffers 1083 | * by trying to dequeue every possible buffer until we 1084 | * find one that is not in use. 1085 | */ 1086 | do { 1087 | pwbuffer = pw_stream_dequeue_buffer(stream->stream); 1088 | } while (!pwbuffer && errno == EBUSY && --retries); 1089 | 1090 | if (pwbuffer) 1091 | break; 1092 | 1093 | fprintf(stderr, "dequeue: out of buffers?\n"); 1094 | if (stream->cur.config.mode == FUNNEL_ASYNC) 1095 | UNLOCK_RETURN(0); 1096 | } 1097 | 1098 | struct funnel_buffer *buf = pwbuffer->user_data; 1099 | fprintf(stderr, " Dequeue buffer %p (%p)\n", pwbuffer, buf); 1100 | 1101 | assert(!buf->dequeued); 1102 | stream->buffers_dequeued++; 1103 | buf->dequeued = true; 1104 | 1105 | *pbuf = buf; 1106 | 1107 | UNLOCK_RETURN(0); 1108 | } 1109 | int funnel_stream_enqueue(struct funnel_stream *stream, 1110 | struct funnel_buffer *buf) { 1111 | if (!stream->stream) 1112 | return -EINVAL; 1113 | if (!buf) 1114 | return -EINVAL; 1115 | assert(buf->stream == stream); 1116 | 1117 | struct funnel_ctx *ctx = stream->ctx; 1118 | pw_thread_loop_lock(ctx->loop); 1119 | 1120 | assert(stream->buffers_dequeued > 0); 1121 | assert(buf->dequeued); 1122 | buf->dequeued = false; 1123 | stream->buffers_dequeued--; 1124 | 1125 | while (1) { 1126 | if (!buf->pw_buffer) { 1127 | funnel_buffer_free(buf); 1128 | unblock_process_thread(stream); 1129 | UNLOCK_RETURN(-ESTALE); 1130 | } 1131 | 1132 | if (ctx->dead || !stream->active) { 1133 | pw_stream_return_buffer(stream->stream, buf->pw_buffer); 1134 | UNLOCK_RETURN(ctx->dead ? -EIO : -ESHUTDOWN); 1135 | } 1136 | 1137 | enum pw_stream_state state = pw_stream_get_state(stream->stream, NULL); 1138 | if (state != PW_STREAM_STATE_STREAMING) { 1139 | pw_stream_return_buffer(stream->stream, buf->pw_buffer); 1140 | unblock_process_thread(stream); 1141 | UNLOCK_RETURN(-EAGAIN); 1142 | } 1143 | 1144 | if (stream->cur.config.mode == FUNNEL_ASYNC) { 1145 | if (stream->pending_buffer) 1146 | return_buffer(stream, stream->pending_buffer); 1147 | stream->pending_buffer = NULL; 1148 | } else if (stream->pending_buffer) { 1149 | unblock_process_thread(stream); 1150 | pw_thread_loop_wait(ctx->loop); 1151 | continue; 1152 | } 1153 | break; 1154 | } 1155 | 1156 | if (stream->cur.config.mode == FUNNEL_SYNC && 1157 | stream->cycle_state != SYNC_CYCLE_ACTIVE) { 1158 | fprintf(stderr, "enqueue: Aborted sync cycle, dropping buffer\n"); 1159 | UNLOCK_RETURN(-ESTALE); 1160 | } 1161 | 1162 | assert(!stream->pending_buffer); 1163 | stream->pending_buffer = buf; 1164 | unblock_process_thread(stream); 1165 | 1166 | if (stream->cur.config.mode == FUNNEL_ASYNC) 1167 | pw_stream_trigger_process(stream->stream); 1168 | 1169 | UNLOCK_RETURN(0); 1170 | } 1171 | 1172 | int funnel_stream_return(struct funnel_stream *stream, 1173 | struct funnel_buffer *buf) { 1174 | if (!stream->stream) 1175 | return -EINVAL; 1176 | if (!buf) 1177 | return -EINVAL; 1178 | assert(buf->stream == stream); 1179 | 1180 | struct funnel_ctx *ctx = stream->ctx; 1181 | pw_thread_loop_lock(ctx->loop); 1182 | 1183 | assert(stream->buffers_dequeued > 0); 1184 | assert(buf->dequeued); 1185 | buf->dequeued = false; 1186 | stream->buffers_dequeued--; 1187 | 1188 | pw_thread_loop_accept(ctx->loop); 1189 | stream->cycle_state = SYNC_CYCLE_INACTIVE; 1190 | 1191 | UNLOCK_RETURN(return_buffer(stream, buf)); 1192 | } 1193 | 1194 | void funnel_buffer_get_size(struct funnel_buffer *buf, uint32_t *width, 1195 | uint32_t *height) { 1196 | *width = gbm_bo_get_width(buf->bo); 1197 | *height = gbm_bo_get_height(buf->bo); 1198 | } 1199 | --------------------------------------------------------------------------------