Files
BPI-CM9-Linux/aml-comp/vendor/amlogic/aml_tvserver/libtv/CTvWayland.cpp
T
2026-08-24 10:03:24 +08:00

1023 lines
35 KiB
C++

/*
* 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 <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <wayland-client.h>
#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 <drm/drm_fourcc.h>
#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<typename T>
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<BufferHolder*>(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<ShmBuffer*>(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<Window*>(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<Display*>(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<Display*>(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_compositor*>(
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_subcompositor*>(
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_shm*>(
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_output*>(
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<xdg_wm_base*>(
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<zwp_fullscreen_shell_v1*>(
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<zwp_linux_dmabuf_v1*>(
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<weston_direct_display_v1*>(
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<wp_viewporter*>(
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<Display*>(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<BufferHolder*>(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<BufferHolder*>(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<Window*>(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<Impl>()) {}
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