1023 lines
35 KiB
C++
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
|