/* * Copyright (c) 2014 Amlogic, Inc. All rights reserved. * * This source code is subject to the terms and conditions defined in the * file 'LICENSE' which is part of this source code package. * * Description: c++ file */ #define LOG_MODULE_TAG "TV" #define LOG_CLASS_TAG "CTvWayland" #include "CTvWayland.h" #ifdef USE_WAYLAND #include #include #include #include #include #include #include #include #include #include #include "xdg-shell-client-protocol.h" #include "fullscreen-shell-unstable-v1-client-protocol.h" #include "linux-dmabuf-unstable-v1-client-protocol.h" #include "weston-direct-display-client-protocol.h" #include "viewporter-client-protocol.h" #include #include "CTvLog.h" namespace { constexpr int MAX_BUFFER_PLANES = 4; constexpr int BUFF_WIDTH = 100; constexpr int BUFF_HEIGHT = 100; constexpr int DUMMY_BUFFER_SIZE = 4096; constexpr const char* UVM_DEVICE = "/dev/uvm"; constexpr int DEFAULT_OUTPUT_WIDTH = 1920; constexpr int DEFAULT_OUTPUT_HEIGHT = 1080; // UVM struct uvm_mesondisplay_data { int fd; int tunnel_id; }; struct uvm_alloc_data { int size; int align; uint32_t flags; int v4l2_fd; int fd; int byte_stride; uint32_t width; uint32_t height; int scalar; int scaled_buf_size; uint32_t slot_id; }; #define UVM_IOC_MAGIC 'U' #define UVM_IOC_SET_MESONDISPLAY_DATA _IOWR(UVM_IOC_MAGIC, 15, struct uvm_mesondisplay_data) #define UVM_IOC_ALLOC _IOWR((UVM_IOC_MAGIC), 0, struct uvm_alloc_data) #define UVM_IMM_ALLOC (1<<0) template static inline void safe_free(T*& p) { if (p) { free(p); p = nullptr; } } static void* zalloc(size_t size) { return std::calloc(1, size); } static int create_anonymous_file(off_t size) { #ifdef MFD_CLOEXEC int fd = memfd_create("wl_shm", MFD_CLOEXEC); if (fd < 0) return -1; if (ftruncate(fd, size) < 0) { close(fd); return -1; } return fd; #else char name[] = "/tmp/wl_shm_XXXXXX"; int fd = mkstemp(name); if (fd < 0) return -1; unlink(name); if (ftruncate(fd, size) < 0) { close(fd); return -1; } return fd; #endif } static inline int clampi(int v, int lo, int hi) { if (v < lo) return lo; if (v > hi) return hi; return v; } struct Display { wl_display* display = nullptr; wl_registry* registry = nullptr; wl_compositor* compositor = nullptr; wl_subcompositor* sub_compositor = nullptr; wl_shm* shm = nullptr; wl_output* output = nullptr; xdg_wm_base* wm_base = nullptr; zwp_fullscreen_shell_v1* fshell = nullptr; zwp_linux_dmabuf_v1* dmabuf = nullptr; weston_direct_display_v1* direct_display = nullptr; wp_viewporter* viewporter = nullptr; int output_width = DEFAULT_OUTPUT_WIDTH; int output_height = DEFAULT_OUTPUT_HEIGHT; // Debug: capture advertised global versions. uint32_t ver_compositor = 0; uint32_t ver_subcompositor = 0; uint32_t ver_shm = 0; uint32_t ver_output = 0; uint32_t ver_wm_base = 0; uint32_t ver_fshell = 0; uint32_t ver_dmabuf = 0; uint32_t ver_direct_display = 0; uint32_t ver_viewporter = 0; }; // A holder whose lifetime can outlive the current plane buffer (for safe retire). struct BufferHolder { wl_buffer* buffer = nullptr; // If not null: on release will set *busyFlag=false but will NOT self-destroy. // If null: on release will self-destroy (retired buffer). bool* busyFlag = nullptr; int plane_count = 0; int fds[MAX_BUFFER_PLANES] = {-1, -1, -1, -1}; ~BufferHolder() { LOGD("~BufferHolder, buffer:%p\n", buffer); if (buffer) { wl_buffer_destroy(buffer); buffer = nullptr; } LOGD("~BufferHolder: plane_count:%d\n", plane_count); for (int i = 0; i < plane_count; ++i) { if (fds[i] >= 0) { close(fds[i]); fds[i] = -1; } } } }; struct DmaBuffer { Display* display = nullptr; // current buffer BufferHolder* holder = nullptr; // owned; may be handed off to compositor for delayed destroy wl_buffer* buffer = nullptr; bool busy = false; int width = 0; int height = 0; uint32_t format = 0; int tunnelIdCurrent = -1; int tunnelIdDesired = -1; // requested from outside; applied lazily }; struct ShmBuffer { wl_buffer* buffer = nullptr; void* shm_data = nullptr; int shm_size = 0; bool busy = false; }; struct VideoPlane { wl_surface* surface = nullptr; wl_subsurface* subsurface = nullptr; wp_viewport* viewport = nullptr; DmaBuffer dma{}; int x = 0, y = 0; int destW = 0, destH = 0; bool visible = true; bool created = false; }; struct Window { Display* display = nullptr; wl_surface* main_surface = nullptr; // UI surface wp_viewport* main_viewport = nullptr; xdg_surface* xdgSurface = nullptr; xdg_toplevel* xdgToplevel = nullptr; bool wait_for_configure = false; VideoPlane mainVideo{}; VideoPlane pipVideo{}; ShmBuffer shm_buf{}; }; static void destroyShmBuffer(ShmBuffer& buf) { if (buf.buffer) { wl_buffer_destroy(buf.buffer); buf.buffer = nullptr; } if (buf.shm_data && buf.shm_size > 0) { munmap(buf.shm_data, buf.shm_size); } buf.shm_data = nullptr; buf.shm_size = 0; } static void retireOrDestroyHolder(BufferHolder*& holder, bool isBusy) { if (!holder) return; LOGD("retireOrDestroyHolder, isBusy[%d]\n", isBusy); if (isBusy) { // Let it self-destroy on release. holder->busyFlag = nullptr; holder = nullptr; return; } // Not busy: safe to destroy now. delete holder; holder = nullptr; } static void destroyDmaBuffer(DmaBuffer& buf, bool force = false) { // If force is true, destroy holder immediately even if busy. // Otherwise, if buffer is busy, defer destruction to release callback. LOGD("destroyDmaBuffer force[%d] busy[%d]\n", force ? 1 : 0, buf.busy ? 1 : 0); if (buf.holder) { if (force) { delete buf.holder; buf.holder = nullptr; } else { retireOrDestroyHolder(buf.holder, buf.busy); } } buf.buffer = nullptr; buf.busy = false; buf.tunnelIdCurrent = -1; } static void destroyPlane(VideoPlane& p, bool force = false) { destroyDmaBuffer(p.dma, force); LOGD("%s: p.viewport[%s], p.surface[%s], p.subsurface[%s]\n", __FUNCTION__, p.viewport ? "enable":"disable", p.surface ? "enable":"disable", p.subsurface ? "enable":"disable"); if (p.viewport) { wp_viewport_destroy(p.viewport); p.viewport = nullptr; } if (p.subsurface) { wl_subsurface_destroy(p.subsurface); p.subsurface = nullptr; } if (p.surface) { wl_surface_destroy(p.surface); p.surface = nullptr; } p.created = false; } static void detachAndCommit(Display* d, wl_surface* s) { if (!d || !d->display || !s) return; LOGD("detachAndCommit\n"); wl_surface_attach(s, nullptr, 0, 0); wl_surface_damage(s, 0, 0, 1, 1); wl_surface_commit(s); wl_display_flush(d->display); } } // namespace // ==================== CTvWayland::Impl ==================== struct CTvWayland::Impl { Display* d = nullptr; Window* w = nullptr; int uiDestW = 0, uiDestH = 0; bool mainVisible = true; bool pipVisible = false; bool pipAboveMain = true; // external-configurable tunnel ids int mainTunnelIdDefault = 6; int pipTunnelIdDefault = 7; // -------- listeners -------- static void on_dma_buffer_release(void* data, wl_buffer* /*buffer*/) { auto* h = static_cast(data); LOGD("wl_buffer.release holder=%p busyFlag=%p\n", h, h ? h->busyFlag : nullptr); if (h->busyFlag) { *h->busyFlag = false; return; } // Retired buffer: destroy now. delete h; } static void on_shm_buffer_release(void* data, wl_buffer* /*buffer*/) { static_cast(data)->busy = false; } static void on_wm_base_ping(void* /*data*/, xdg_wm_base* wm_base, uint32_t serial) { xdg_wm_base_pong(wm_base, serial); } static void on_xdg_surface_configure(void* data, xdg_surface* surface, uint32_t serial) { auto* win = static_cast(data); xdg_surface_ack_configure(surface, serial); win->wait_for_configure = false; } static void on_xdg_toplevel_configure(void*, xdg_toplevel*, int32_t, int32_t, wl_array*) {} static void on_xdg_toplevel_close(void*, xdg_toplevel*) {} static void on_output_mode(void* data, wl_output*, uint32_t flags, int32_t width, int32_t height, int32_t) { auto* disp = static_cast(data); if (flags & WL_OUTPUT_MODE_CURRENT) { disp->output_width = width; disp->output_height = height; LOGI("%s: disp->output_width = %d, disp->output_height = %d\n",__FUNCTION__ , width, height); } } static void on_registry_global(void* data, wl_registry* registry, uint32_t id, const char* interface, uint32_t version) { auto* disp = static_cast(data); LOGD("registry.global id=%u iface=%s ver=%u\n", id, interface ? interface : "(null)", version); if (strcmp(interface, "wl_compositor") == 0) { disp->ver_compositor = version; disp->compositor = static_cast( wl_registry_bind(registry, id, &wl_compositor_interface, version)); } else if (strcmp(interface, "wl_subcompositor") == 0) { disp->ver_subcompositor = version; disp->sub_compositor = static_cast( wl_registry_bind(registry, id, &wl_subcompositor_interface, 1)); } else if (strcmp(interface, "wl_shm") == 0) { disp->ver_shm = version; disp->shm = static_cast( wl_registry_bind(registry, id, &wl_shm_interface, 1)); } else if (strcmp(interface, "wl_output") == 0) { disp->ver_output = version; disp->output = static_cast( wl_registry_bind(registry, id, &wl_output_interface, 2)); static const wl_output_listener ol = { nullptr, on_output_mode, nullptr, nullptr }; wl_output_add_listener(disp->output, &ol, disp); } else if (strcmp(interface, "xdg_wm_base") == 0) { disp->ver_wm_base = version; disp->wm_base = static_cast( wl_registry_bind(registry, id, &xdg_wm_base_interface, 1)); static const xdg_wm_base_listener l = { on_wm_base_ping }; xdg_wm_base_add_listener(disp->wm_base, &l, disp); } else if (strcmp(interface, "zwp_fullscreen_shell_v1") == 0) { disp->ver_fshell = version; disp->fshell = static_cast( wl_registry_bind(registry, id, &zwp_fullscreen_shell_v1_interface, version)); } else if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0) { disp->ver_dmabuf = version; if (version < 3) return; disp->dmabuf = static_cast( wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, 3)); } else if (strcmp(interface, "weston_direct_display_v1") == 0) { disp->ver_direct_display = version; disp->direct_display = static_cast( wl_registry_bind(registry, id, &weston_direct_display_v1_interface, 1)); } else if (strcmp(interface, "wp_viewporter") == 0) { disp->ver_viewporter = version; disp->viewporter = static_cast( wl_registry_bind(registry, id, &wp_viewporter_interface, 1)); } } static void on_registry_remove(void*, wl_registry*, uint32_t) {} // -------- display lifecycle -------- bool createDisplay() { d = static_cast(zalloc(sizeof(Display))); if (!d) return false; d->display = wl_display_connect(nullptr); if (!d->display) { LOGE("wl_display_connect failed\n"); return false; } d->registry = wl_display_get_registry(d->display); static const wl_registry_listener rl = { on_registry_global, on_registry_remove }; wl_registry_add_listener(d->registry, &rl, d); wl_display_roundtrip(d->display); LOGD("globals: compositor=%p(ver=%u) subcomp=%p(ver=%u) shm=%p(ver=%u) output=%p(ver=%u) wm_base=%p(ver=%u) fshell=%p(ver=%u) dmabuf=%p(ver=%u) direct=%p(ver=%u) viewporter=%p(ver=%u)\n", d->compositor, d->ver_compositor, d->sub_compositor, d->ver_subcompositor, d->shm, d->ver_shm, d->output, d->ver_output, d->wm_base, d->ver_wm_base, d->fshell, d->ver_fshell, d->dmabuf, d->ver_dmabuf, d->direct_display, d->ver_direct_display, d->viewporter, d->ver_viewporter); if (!d->compositor || !d->sub_compositor || !d->dmabuf) { LOGE("Missing globals compositor=%p subcomp=%p dmabuf=%p\n", d->compositor, d->sub_compositor, d->dmabuf); return false; } return true; } void destroyDisplay() { if (!d) return; LOGE("destroyDisplay\n"); if (d->dmabuf) zwp_linux_dmabuf_v1_destroy(d->dmabuf); if (d->viewporter) wp_viewporter_destroy(d->viewporter); if (d->direct_display) weston_direct_display_v1_destroy(d->direct_display); if (d->wm_base) xdg_wm_base_destroy(d->wm_base); if (d->fshell) zwp_fullscreen_shell_v1_release(d->fshell); if (d->output) wl_output_destroy(d->output); if (d->shm) wl_shm_destroy(d->shm); if (d->sub_compositor) wl_subcompositor_destroy(d->sub_compositor); if (d->compositor) wl_compositor_destroy(d->compositor); if (d->registry) wl_registry_destroy(d->registry); if (d->display) { wl_display_flush(d->display); wl_display_disconnect(d->display); } safe_free(d); } // -------- shm buffer -------- bool createShmBuffer(ShmBuffer& out, int width, int height, uint32_t format) { if (!d || !d->shm) return false; const int stride = width * 4; const int size = stride * height; int fd = create_anonymous_file(size); if (fd < 0) { LOGE("create shm file fail: %s\n", strerror(errno)); return false; } void* data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { LOGE("mmap fail: %s\n", strerror(errno)); close(fd); return false; } wl_shm_pool* pool = wl_shm_create_pool(d->shm, fd, size); out.buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, format); wl_shm_pool_destroy(pool); close(fd); if (!out.buffer) { munmap(data, size); return false; } static const wl_buffer_listener bl = { on_shm_buffer_release }; wl_buffer_add_listener(out.buffer, &bl, &out); out.shm_data = data; out.shm_size = size; return true; } // -------- dmabuf tunnel buffer -------- bool createDmabufForTunnel(DmaBuffer& out, int tunnelId) { if (!d || !d->dmabuf) return false; LOGD("createDmabufForTunnel tunnelId=%d (busy=%d cur=%d desired=%d)\n", tunnelId, out.busy ? 1 : 0, out.tunnelIdCurrent, out.tunnelIdDesired); // Destroy current buffer first (safe retire) destroyDmaBuffer(out); out.display = d; out.format = DRM_FORMAT_NV21; out.width = BUFF_WIDTH; out.height = BUFF_HEIGHT; int uvm_fd = open(UVM_DEVICE, O_RDONLY | O_CLOEXEC); if (uvm_fd < 0) { LOGE("open %s fail: %s\n", UVM_DEVICE, strerror(errno)); return false; } uvm_alloc_data alloc{}; memset(&alloc, 0, sizeof(alloc)); alloc.fd = -1; alloc.flags |= UVM_IMM_ALLOC; alloc.size = DUMMY_BUFFER_SIZE; if (ioctl(uvm_fd, UVM_IOC_ALLOC, &alloc) != 0 || alloc.fd < 0) { LOGE("UVM_IOC_ALLOC fail uvm_fd=%d alloc.fd=%d\n", uvm_fd, alloc.fd); close(uvm_fd); if (alloc.fd >= 0) close(alloc.fd); return false; } LOGD("UVM alloc ok uvm_fd=%d fd=%d size=%d flags=0x%x\n", uvm_fd, alloc.fd, alloc.size, alloc.flags); uvm_mesondisplay_data md{}; md.tunnel_id = tunnelId; md.fd = alloc.fd; if (ioctl(uvm_fd, UVM_IOC_SET_MESONDISPLAY_DATA, &md) != 0) { LOGE("UVM_IOC_SET_MESONDISPLAY_DATA fail tunnelId=%d\n", tunnelId); close(uvm_fd); close(alloc.fd); return false; } LOGD("UVM set mesondisplay ok tunnelId=%d fd=%d\n", tunnelId, alloc.fd); close(uvm_fd); // Build wl_buffer auto* holder = new BufferHolder(); holder->plane_count = 1; holder->fds[0] = alloc.fd; holder->busyFlag = &out.busy; zwp_linux_buffer_params_v1* params = zwp_linux_dmabuf_v1_create_params(d->dmabuf); if (!params) { delete holder; return false; } if (d->direct_display) { weston_direct_display_v1_enable(d->direct_display, params); } // Note: stride/offset are 0 in your original tunnel usage. zwp_linux_buffer_params_v1_add(params, holder->fds[0], 0, /*offset*/0, /*stride*/0, 0, 0); struct Ctx { static void created(void* data, zwp_linux_buffer_params_v1* p, wl_buffer* new_buffer) { auto* h = static_cast(data); h->buffer = new_buffer; LOGD("dmabuf buffer created holder=%p wl_buffer=%p\n", h, new_buffer); static const wl_buffer_listener bl = { CTvWayland::Impl::on_dma_buffer_release }; wl_buffer_add_listener(h->buffer, &bl, h); zwp_linux_buffer_params_v1_destroy(p); } static void failed(void* data, zwp_linux_buffer_params_v1* p) { auto* h = static_cast(data); h->buffer = nullptr; LOGE("dmabuf buffer create failed holder=%p\n", h); zwp_linux_buffer_params_v1_destroy(p); } }; static const zwp_linux_buffer_params_v1_listener pl = { Ctx::created, Ctx::failed }; zwp_linux_buffer_params_v1_add_listener(params, &pl, holder); zwp_linux_buffer_params_v1_create(params, out.width, out.height, out.format, 0); wl_display_roundtrip(d->display); if (!holder->buffer) { // buffer creation failed -> cleanup immediately delete holder; return false; } out.holder = holder; out.buffer = holder->buffer; out.busy = false; out.tunnelIdCurrent = tunnelId; LOGD("createDmabufForTunnel done tunnelId=%d wl_buffer=%p holder=%p fd=%d\n", tunnelId, out.buffer, out.holder, out.holder ? out.holder->fds[0] : -1); return true; } // Lazily apply tunnel id update when safe. void maybeApplyTunnelId(DmaBuffer& dma, int fallbackId) { const int desired = (dma.tunnelIdDesired >= 0) ? dma.tunnelIdDesired : fallbackId; if (desired < 0) return; if (dma.tunnelIdCurrent == desired) return; if (dma.busy) return; // defer to next commit // recreate buffer (void)createDmabufForTunnel(dma, desired); } // -------- plane creation -------- bool createVideoPlane(VideoPlane& p) { if (!d || !w || !d->compositor || !d->sub_compositor) return false; p.surface = wl_compositor_create_surface(d->compositor); if (!p.surface) return false; p.subsurface = wl_subcompositor_get_subsurface(d->sub_compositor, p.surface, w->main_surface); if (!p.subsurface) return false; if (d->viewporter) p.viewport = wp_viewporter_get_viewport(d->viewporter, p.surface); p.created = true; LOGD("createVideoPlane surface=%p subsurface=%p viewport=%p\n", p.surface, p.subsurface, p.viewport); return true; // default sync } void applyPipStacking() { if (!w || !w->pipVideo.subsurface || !w->mainVideo.surface) return; if (pipAboveMain) wl_subsurface_place_above(w->pipVideo.subsurface, w->mainVideo.surface); else wl_subsurface_place_below(w->pipVideo.subsurface, w->mainVideo.surface); } bool ensurePipCreated() { if (!w) return false; if (w->pipVideo.created && w->pipVideo.dma.buffer) return true; if (!w->pipVideo.created) { if (!createVideoPlane(w->pipVideo)) return false; } // make sure tunnel id desired is set if (w->pipVideo.dma.tunnelIdDesired < 0) { w->pipVideo.dma.tunnelIdDesired = pipTunnelIdDefault; } if (!createDmabufForTunnel(w->pipVideo.dma, w->pipVideo.dma.tunnelIdDesired)) { destroyPlane(w->pipVideo); return false; } applyPipStacking(); return true; } // -------- window lifecycle -------- bool createWindow() { if (!d || !d->compositor) return false; w = static_cast(zalloc(sizeof(Window))); if (!w) return false; w->display = d; w->main_surface = wl_compositor_create_surface(d->compositor); if (!w->main_surface) return false; LOGD("createWindow main_surface=%p main_viewport=%p wm_base=%p fshell=%p\n", w->main_surface, w->main_viewport, d->wm_base, d->fshell); if (d->viewporter) w->main_viewport = wp_viewporter_get_viewport(d->viewporter, w->main_surface); if (d->wm_base) { w->xdgSurface = xdg_wm_base_get_xdg_surface(d->wm_base, w->main_surface); if (!w->xdgSurface) return false; static const xdg_surface_listener sl = { on_xdg_surface_configure }; xdg_surface_add_listener(w->xdgSurface, &sl, w); w->xdgToplevel = xdg_surface_get_toplevel(w->xdgSurface); if (!w->xdgToplevel) return false; static const xdg_toplevel_listener tl = { on_xdg_toplevel_configure, on_xdg_toplevel_close }; xdg_toplevel_add_listener(w->xdgToplevel, &tl, w); xdg_toplevel_set_title(w->xdgToplevel, "CTvWayland"); xdg_toplevel_set_app_id(w->xdgToplevel, "com.amlogic.ctvwayland"); w->wait_for_configure = true; wl_surface_commit(w->main_surface); // xdg-shell requires the client to receive/ack the initial configure // before committing a buffer. We must dispatch events here; otherwise // commitAll() can run immediately (e.g. StartTv()->commitAll()) and // trigger "xdg_surface has never been configured" protocol error. // // roundtrip will dispatch pending events and block until the server // has processed a sync, allowing us to receive configure+ack. for (int i = 0; i < 8 && w->wait_for_configure; ++i) { wl_display_roundtrip(d->display); } if (w->wait_for_configure) { LOGE("xdg_surface initial configure not received/acked\n"); return false; } } else if (d->fshell) { zwp_fullscreen_shell_v1_present_surface( d->fshell, w->main_surface, ZWP_FULLSCREEN_SHELL_V1_PRESENT_METHOD_DEFAULT, nullptr); } else { LOGE("No xdg_wm_base nor fullscreen_shell\n"); return false; } // Main plane always if (!createVideoPlane(w->mainVideo)) return false; // Default tunnel ids w->mainVideo.dma.tunnelIdDesired = mainTunnelIdDefault; if (!createDmabufForTunnel(w->mainVideo.dma, w->mainVideo.dma.tunnelIdDesired)) return false; // PIP not created until enablePip(true) w->pipVideo = VideoPlane{}; pipVisible = false; // UI dummy buffer if (!createShmBuffer(w->shm_buf, 1, 1, WL_SHM_FORMAT_ARGB8888)) return false; return true; } void destroyWindow() { if (!w) return; LOGD("%s: mainVideo[%s], pipVideo[%s]\n",__FUNCTION__, w->mainVideo.surface ? "enable" : "disable", w->pipVideo.surface ? "enable" : "disable"); destroyPlane(w->pipVideo, true); destroyPlane(w->mainVideo, true); destroyShmBuffer(w->shm_buf); LOGD("%s:xdgToplevel[%s], xdgSurface[%s], main_viewport[%s], main_surface[%s]\n",__FUNCTION__, w->xdgToplevel ? "enable" : "disable", w->xdgSurface ? "enable" : "disable", w->main_viewport ? "enable" : "disable", w->main_surface ? "enable" : "disable"); if (w->xdgToplevel) xdg_toplevel_destroy(w->xdgToplevel); if (w->xdgSurface) xdg_surface_destroy(w->xdgSurface); if (w->main_viewport) wp_viewport_destroy(w->main_viewport); if (w->main_surface) wl_surface_destroy(w->main_surface); safe_free(w); } // -------- commit -------- static void commitPlane(const Display* d, VideoPlane& p) { if (!p.created || !p.surface || !p.visible) return; if (!p.dma.buffer) return; wl_subsurface_set_position(p.subsurface, p.x, p.y); // mark busy before attach/commit p.dma.busy = true; wl_surface_attach(p.surface, p.dma.buffer, 0, 0); wl_surface_damage(p.surface, 0, 0, 1, 1); if (p.viewport) { const int dw = (p.destW > 0) ? p.destW : d->output_width; const int dh = (p.destH > 0) ? p.destH : d->output_height; wp_viewport_set_destination(p.viewport, dw, dh); } wl_surface_commit(p.surface); } bool commitAll() { if (!d || !w) return false; static uint32_t sCommitSeq = 0; const uint32_t seq = ++sCommitSeq; if (seq % 60 == 1) { // roughly once per second if called @60fps LOGD("commitAll seq=%u out=%dx%d uiDest=%dx%d mainVis=%d pipVis=%d pipCreated=%d pipAboveMain=%d\n", seq, d->output_width, d->output_height, uiDestW, uiDestH, mainVisible ? 1 : 0, pipVisible ? 1 : 0, (w->pipVideo.created ? 1 : 0), pipAboveMain ? 1 : 0); LOGD(" main rect pos=(%d,%d) dst=(%d,%d) tunnel cur=%d desired=%d busy=%d buf=%p\n", w->mainVideo.x, w->mainVideo.y, w->mainVideo.destW, w->mainVideo.destH, w->mainVideo.dma.tunnelIdCurrent, w->mainVideo.dma.tunnelIdDesired, w->mainVideo.dma.busy ? 1 : 0, w->mainVideo.dma.buffer); if (w->pipVideo.created) { LOGD(" pip rect pos=(%d,%d) dst=(%d,%d) tunnel cur=%d desired=%d busy=%d buf=%p\n", w->pipVideo.x, w->pipVideo.y, w->pipVideo.destW, w->pipVideo.destH, w->pipVideo.dma.tunnelIdCurrent, w->pipVideo.dma.tunnelIdDesired, w->pipVideo.dma.busy ? 1 : 0, w->pipVideo.dma.buffer); } } // Apply runtime tunnel id changes safely (non-busy only) maybeApplyTunnelId(w->mainVideo.dma, mainTunnelIdDefault); if (w->pipVideo.created) { maybeApplyTunnelId(w->pipVideo.dma, pipTunnelIdDefault); } // xdg-shell: never commit a buffer before first configure is acked. if (w->xdgSurface && w->wait_for_configure) { // Try to process the initial configure if it arrived. wl_display_dispatch_pending(d->display); if (w->wait_for_configure) { // As a last attempt, roundtrip once (safe, and prevents protocol error). wl_display_roundtrip(d->display); } if (w->wait_for_configure) { LOGE("commitAll skipped: waiting for xdg_surface configure\n"); return false; } } // UI wl_surface_attach(w->main_surface, w->shm_buf.buffer, 0, 0); wl_surface_damage(w->main_surface, 0, 0, 1, 1); if (w->main_viewport) { const int mw = (uiDestW > 0) ? uiDestW : d->output_width; const int mh = (uiDestH > 0) ? uiDestH : d->output_height; LOGD("commitAll:set UI destination, w:%d, h:%d\n", mw, mh); wp_viewport_set_destination(w->main_viewport, mw, mh); } // main w->mainVideo.visible = mainVisible; if (mainVisible && w->mainVideo.dma.buffer) { commitPlane(d, w->mainVideo); } // pip if (pipVisible && w->pipVideo.created && w->pipVideo.dma.buffer) { // clamp pip within screen based on destination if (w->pipVideo.destW > 0 && w->pipVideo.destH > 0) { const int pw = clampi(w->pipVideo.destW, 1, d->output_width); const int ph = clampi(w->pipVideo.destH, 1, d->output_height); w->pipVideo.x = clampi(w->pipVideo.x, 0, d->output_width - pw); w->pipVideo.y = clampi(w->pipVideo.y, 0, d->output_height - ph); } w->pipVideo.visible = true; commitPlane(d, w->pipVideo); } // parent commit for sync latch wl_surface_commit(w->main_surface); wl_display_flush(d->display); return true; } }; // ==================== CTvWayland public ==================== CTvWayland::CTvWayland() : mImpl(std::make_unique()) {} CTvWayland::~CTvWayland() { shutdown(); } void CTvWayland::setMainTunnelId(int32_t tunnelId) { if (!mImpl) return; mImpl->mainTunnelIdDefault = tunnelId; if (mImpl->w) { // lazy apply mImpl->w->mainVideo.dma.tunnelIdDesired = tunnelId; } } void CTvWayland::setPipTunnelId(int32_t tunnelId) { if (!mImpl) return; mImpl->pipTunnelIdDefault = tunnelId; if (mImpl->w && mImpl->w->pipVideo.created) { // lazy apply mImpl->w->pipVideo.dma.tunnelIdDesired = tunnelId; } } bool CTvWayland::init() { if (!mImpl) return false; // Idempotent: display/window may be destroyed and later recreated. if (mImpl->d && mImpl->w) return true; if (!mImpl->d) { LOGD("CTvWayland::init createDisplay\n"); if (!mImpl->createDisplay()) { shutdown(); return false; } } if (!mImpl->w) { LOGD("CTvWayland::init createWindow\n"); if (!mImpl->createWindow()) { shutdown(); return false; } } return (mImpl->d && mImpl->w); } void CTvWayland::shutdown() { if (!mImpl) return; mImpl->destroyWindow(); mImpl->destroyDisplay(); } void CTvWayland::setUiDestination(int32_t w, int32_t h) { if (!mImpl) return; mImpl->uiDestW = w; mImpl->uiDestH = h; } void CTvWayland::setMainVideoPosition(int32_t x, int32_t y) { if (!mImpl || !mImpl->w) return; mImpl->w->mainVideo.x = x; mImpl->w->mainVideo.y = y; } void CTvWayland::setMainVideoDestination(int32_t w, int32_t h) { if (!mImpl || !mImpl->w) return; mImpl->w->mainVideo.destW = w; mImpl->w->mainVideo.destH = h; } bool CTvWayland::ensureMainReady() { if (!mImpl) return false; if (!init()) return false; // ensure display + window if (!mImpl->w) return false; auto& mainDma = mImpl->w->mainVideo.dma; if (mainDma.buffer) return true; if (mainDma.tunnelIdDesired < 0) { mainDma.tunnelIdDesired = mImpl->mainTunnelIdDefault; } const int tid = mainDma.tunnelIdDesired; if (tid < 0) return false; return mImpl->createDmabufForTunnel(mainDma, tid); } bool CTvWayland::startMainVideo() { if (!mImpl) return false; if (!ensureMainReady()) return false; LOGD("startMainVideo tidDesired=%d tidCurrent=%d buf=%p\n", mImpl->w ? mImpl->w->mainVideo.dma.tunnelIdDesired : -1, mImpl->w ? mImpl->w->mainVideo.dma.tunnelIdCurrent : -1, (mImpl->w ? mImpl->w->mainVideo.dma.buffer : nullptr)); mImpl->mainVisible = true; return true; } void CTvWayland::stopMainVideo() { if (!mImpl) return; if (!mImpl->w) { mImpl->mainVisible = false; return; } LOGD("stopMainVideo\n"); mImpl->mainVisible = false; // Detach first to avoid last-frame residue. detachAndCommit(mImpl->d, mImpl->w->mainVideo.surface); if (!mImpl->pipVisible) { mImpl->destroyWindow(); } else { destroyDmaBuffer(mImpl->w->mainVideo.dma); } } void CTvWayland::setPipPosition(int32_t x, int32_t y) { if (!mImpl || !mImpl->w) return; // allow setting even before pip is created; values will be used when created mImpl->w->pipVideo.x = x; mImpl->w->pipVideo.y = y; } void CTvWayland::setPipDestination(int32_t w, int32_t h) { if (!mImpl || !mImpl->w) return; mImpl->w->pipVideo.destW = w; mImpl->w->pipVideo.destH = h; } void CTvWayland::setPipRect(int32_t x, int32_t y, int32_t w, int32_t h) { setPipPosition(x, y); setPipDestination(w, h); } void CTvWayland::setPipAboveMain(bool above) { if (!mImpl) return; mImpl->pipAboveMain = above; if (mImpl->w && mImpl->w->pipVideo.created) { mImpl->applyPipStacking(); } } bool CTvWayland::startPipVideo() { if (!mImpl) return false; if (!init()) return false; if (!mImpl->ensurePipCreated()) return false; LOGD("startPipVideo tidDesired=%d tidCurrent=%d buf=%p\n", mImpl->w ? mImpl->w->pipVideo.dma.tunnelIdDesired : -1, mImpl->w ? mImpl->w->pipVideo.dma.tunnelIdCurrent : -1, (mImpl->w ? mImpl->w->pipVideo.dma.buffer : nullptr)); mImpl->pipVisible = true; return true; } void CTvWayland::stopPipVideo() { LOGD("stopPipVideo\n"); mImpl->pipVisible = false; if (mImpl->w && mImpl->w->pipVideo.created) { // Detach first to avoid last-frame residue. detachAndCommit(mImpl->d, mImpl->w->pipVideo.surface); destroyPlane(mImpl->w->pipVideo); } if (!mImpl->mainVisible) { mImpl->destroyWindow(); } } bool CTvWayland::commitAll() { return mImpl && mImpl->commitAll(); } int32_t CTvWayland::outputWidth() const { return (mImpl && mImpl->d) ? mImpl->d->output_width : 0; } int32_t CTvWayland::outputHeight() const { return (mImpl && mImpl->d) ? mImpl->d->output_height : 0; } #else // !USE_WAYLAND struct CTvWayland::Impl {}; CTvWayland::CTvWayland() = default; CTvWayland::~CTvWayland() = default; void CTvWayland::setMainTunnelId(int32_t) {} void CTvWayland::setPipTunnelId(int32_t) {} bool CTvWayland::init() { return false; } void CTvWayland::shutdown() {} void CTvWayland::setUiDestination(int32_t, int32_t) {} void CTvWayland::setMainVideoPosition(int32_t, int32_t) {} void CTvWayland::setMainVideoDestination(int32_t, int32_t) {} bool CTvWayland::ensureMainReady() { return false; } bool CTvWayland::startMainVideo() { return false; } void CTvWayland::stopMainVideo() {} bool CTvWayland::startPipVideo() { return false; } void CTvWayland::stopPipVideo() {} void CTvWayland::setPipPosition(int32_t, int32_t) {} void CTvWayland::setPipDestination(int32_t, int32_t) {} void CTvWayland::setPipRect(int32_t, int32_t, int32_t, int32_t) {} void CTvWayland::setPipAboveMain(bool) {} bool CTvWayland::commitAll() { return false; } int32_t CTvWayland::outputWidth() const { return 0; } int32_t CTvWayland::outputHeight() const { return 0; } #endif // USE_WAYLAND