Program Listing for File WaylandOwnedWindow.cpp

Return to documentation for file (include\input\DefaultDevs\linux\wayland_things\WaylandOwnedWindow.cpp)

#include "WaylandOwnedWindow.hpp"
#include "wayland_protocols/xdg-shell-client-protocol.h"

#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <string>
#include <utility>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wayland-client.h>

namespace {

int
CreateAnonymousShmFile(std::size_t size)
{
    const char *runtime_dir = std::getenv("XDG_RUNTIME_DIR");
    std::string base        = (runtime_dir && runtime_dir[0] != '\0')
                                  ? runtime_dir
                                  : "/tmp";
    std::string tmpl = base + "/pdje-wayland-shm-XXXXXX";
    std::string path = tmpl;
    int         fd   = mkstemp(path.data());
    if (fd < 0) {
        return -1;
    }
    unlink(path.c_str());
    if (ftruncate(fd, static_cast<off_t>(size)) != 0) {
        close(fd);
        return -1;
    }
    return fd;
}

template <typename T>
void
ProxyDestroy(T *&proxy) noexcept
{
    if (proxy != nullptr) {
        wl_proxy_destroy(reinterpret_cast<wl_proxy *>(proxy));
        proxy = nullptr;
    }
}

void
ReleaseSeatCompat(wl_seat *&seat) noexcept
{
    if (seat == nullptr) {
        return;
    }
    if (wl_seat_get_version(seat) >= WL_SEAT_RELEASE_SINCE_VERSION) {
        wl_seat_release(seat);
    } else {
        wl_seat_destroy(seat);
    }
    seat = nullptr;
}

} // namespace

void
WaylandOwnedWindow::SetError(std::string msg)
{
    last_error_ = std::move(msg);
}

WaylandOwnedWindow::~WaylandOwnedWindow()
{
    Destroy();
}

void
WaylandOwnedWindow::OnRegistryGlobal(void       *data,
                                     wl_registry *registry,
                                     uint32_t     name,
                                     const char  *interface,
                                     uint32_t     version)
{
    auto *self = static_cast<WaylandOwnedWindow *>(data);
    if (self == nullptr || interface == nullptr) {
        return;
    }

    if (std::strcmp(interface, wl_compositor_interface.name) == 0) {
        if (self->compositor_ == nullptr) {
            const uint32_t bind_version = std::min<uint32_t>(version, 4);
            self->compositor_ = static_cast<wl_compositor *>(
                wl_registry_bind(registry,
                                 name,
                                 &wl_compositor_interface,
                                 bind_version));
        }
        return;
    }

    if (std::strcmp(interface, wl_shm_interface.name) == 0) {
        if (self->shm_ == nullptr) {
            self->shm_ = static_cast<wl_shm *>(
                wl_registry_bind(registry, name, &wl_shm_interface, 1));
        }
        return;
    }

    if (std::strcmp(interface, wl_seat_interface.name) == 0) {
        if (self->seat_ == nullptr) {
            const uint32_t bind_version = std::min<uint32_t>(version, 5);
            self->seat_ = static_cast<wl_seat *>(
                wl_registry_bind(registry, name, &wl_seat_interface, bind_version));
            if (self->seat_ != nullptr) {
                static constexpr wl_seat_listener kSeatListener = {
                    .capabilities = &WaylandOwnedWindow::OnSeatCapabilities,
                    .name         = &WaylandOwnedWindow::OnSeatName,
                };
                wl_seat_add_listener(self->seat_, &kSeatListener, self);
            }
        }
        return;
    }

    if (std::strcmp(interface, xdg_wm_base_interface.name) == 0) {
        if (self->wm_base_ == nullptr) {
            self->wm_base_ = static_cast<xdg_wm_base *>(
                wl_registry_bind(registry, name, &xdg_wm_base_interface, 1));
        }
    }
}

void
WaylandOwnedWindow::OnRegistryGlobalRemove(void *, wl_registry *, uint32_t)
{
}

void
WaylandOwnedWindow::OnWmBasePing(void *, xdg_wm_base *wm_base, uint32_t serial)
{
    if (wm_base != nullptr) {
        xdg_wm_base_pong(wm_base, serial);
    }
}

void
WaylandOwnedWindow::OnXdgSurfaceConfigure(void *data,
                                          xdg_surface *surface,
                                          uint32_t     serial)
{
    auto *self = static_cast<WaylandOwnedWindow *>(data);
    if (surface != nullptr) {
        xdg_surface_ack_configure(surface, serial);
    }
    if (self != nullptr) {
        self->configured_ = true;
    }
}

void
WaylandOwnedWindow::OnToplevelConfigure(void *,
                                        xdg_toplevel *,
                                        int32_t,
                                        int32_t,
                                        wl_array *)
{
}

void
WaylandOwnedWindow::OnToplevelClose(void *data, xdg_toplevel *)
{
    auto *self = static_cast<WaylandOwnedWindow *>(data);
    if (self != nullptr) {
        self->closed_.store(true);
    }
}

void
WaylandOwnedWindow::OnBufferRelease(void *, wl_buffer *)
{
}

void
WaylandOwnedWindow::OnSeatCapabilities(void     *data,
                                       wl_seat  *,
                                       uint32_t  capabilities)
{
    auto *self = static_cast<WaylandOwnedWindow *>(data);
    if (self == nullptr) {
        return;
    }
    self->seat_caps_       = capabilities;
    self->seat_caps_known_ = true;
}

void
WaylandOwnedWindow::OnSeatName(void *, wl_seat *, const char *)
{
}

bool
WaylandOwnedWindow::CreateShmBuffer()
{
    if (shm_ == nullptr || surface_ == nullptr) {
        SetError("wl_shm or wl_surface unavailable while creating internal "
                 "Wayland window buffer");
        return false;
    }

    DestroyShmBuffer();

    const int bytes_per_pixel = 4;
    const int stride          = width_ * bytes_per_pixel;
    if (width_ <= 0 || height_ <= 0 || stride <= 0) {
        SetError("invalid internal Wayland window dimensions");
        return false;
    }

    buffer_size_ = static_cast<std::size_t>(stride) *
                   static_cast<std::size_t>(height_);
    shm_fd_ = CreateAnonymousShmFile(buffer_size_);
    if (shm_fd_ < 0) {
        SetError("failed to create shm file for internal Wayland window");
        return false;
    }

    buffer_map_ =
        mmap(nullptr, buffer_size_, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd_, 0);
    if (buffer_map_ == MAP_FAILED) {
        buffer_map_ = nullptr;
        close(shm_fd_);
        shm_fd_ = -1;
        SetError("mmap failed for internal Wayland window buffer");
        return false;
    }

    shm_pool_ = wl_shm_create_pool(
        shm_, shm_fd_, static_cast<int32_t>(buffer_size_));
    if (shm_pool_ == nullptr) {
        DestroyShmBuffer();
        SetError("wl_shm_create_pool failed");
        return false;
    }

    buffer_ = wl_shm_pool_create_buffer(shm_pool_,
                                        0,
                                        width_,
                                        height_,
                                        stride,
                                        WL_SHM_FORMAT_XRGB8888);
    if (buffer_ == nullptr) {
        DestroyShmBuffer();
        SetError("wl_shm_pool_create_buffer failed");
        return false;
    }

    static constexpr wl_buffer_listener kBufferListener = {
        .release = &WaylandOwnedWindow::OnBufferRelease,
    };
    wl_buffer_add_listener(buffer_, &kBufferListener, this);

    auto *pixels = static_cast<uint32_t *>(buffer_map_);
    const std::size_t pixel_count =
        static_cast<std::size_t>(width_) * static_cast<std::size_t>(height_);
    for (std::size_t i = 0; i < pixel_count; ++i) {
        pixels[i] = 0xFF2A3E52u; // opaque XRGB-like solid color
    }
    return true;
}

void
WaylandOwnedWindow::DestroyShmBuffer() noexcept
{
    ProxyDestroy(buffer_);
    ProxyDestroy(shm_pool_);

    if (buffer_map_ != nullptr) {
        munmap(buffer_map_, buffer_size_);
        buffer_map_ = nullptr;
    }
    if (shm_fd_ >= 0) {
        close(shm_fd_);
        shm_fd_ = -1;
    }
    buffer_size_ = 0;
}

void
WaylandOwnedWindow::CleanupWaylandObjects() noexcept
{
    if (toplevel_ != nullptr) {
        xdg_toplevel_destroy(toplevel_);
        toplevel_ = nullptr;
    }
    if (xdg_surface_ != nullptr) {
        xdg_surface_destroy(xdg_surface_);
        xdg_surface_ = nullptr;
    }
    if (surface_ != nullptr) {
        wl_surface_destroy(surface_);
        surface_ = nullptr;
    }

    if (wm_base_ != nullptr) {
        xdg_wm_base_destroy(wm_base_);
        wm_base_ = nullptr;
    }

    ReleaseSeatCompat(seat_);
    ProxyDestroy(shm_);
    ProxyDestroy(compositor_);
    ProxyDestroy(registry_);
}

bool
WaylandOwnedWindow::Create(const char *title, int width, int height)
{
    Destroy();
    last_error_.clear();
    closed_.store(false);

    width_      = std::max(width, 64);
    height_     = std::max(height, 64);
    configured_ = false;

    display_ = wl_display_connect(nullptr);
    if (display_ == nullptr) {
        SetError("wl_display_connect failed for internal Wayland window");
        return false;
    }

    registry_ = wl_display_get_registry(display_);
    if (registry_ == nullptr) {
        SetError("wl_display_get_registry failed");
        Destroy();
        return false;
    }

    static constexpr wl_registry_listener kRegistryListener = {
        .global        = &WaylandOwnedWindow::OnRegistryGlobal,
        .global_remove = &WaylandOwnedWindow::OnRegistryGlobalRemove,
    };
    wl_registry_add_listener(registry_, &kRegistryListener, this);

    if (wl_display_roundtrip(display_) < 0) {
        SetError("wl_display_roundtrip failed while discovering globals");
        Destroy();
        return false;
    }

    if (wm_base_ != nullptr) {
        static constexpr xdg_wm_base_listener kWmBaseListener = {
            .ping = &WaylandOwnedWindow::OnWmBasePing,
        };
        xdg_wm_base_add_listener(wm_base_, &kWmBaseListener, this);
    }

    if (compositor_ == nullptr || shm_ == nullptr || wm_base_ == nullptr) {
        SetError("required Wayland globals missing (compositor/shm/xdg_wm_base)");
        Destroy();
        return false;
    }

    surface_ = wl_compositor_create_surface(compositor_);
    if (surface_ == nullptr) {
        SetError("wl_compositor_create_surface failed");
        Destroy();
        return false;
    }

    xdg_surface_ = xdg_wm_base_get_xdg_surface(wm_base_, surface_);
    if (xdg_surface_ == nullptr) {
        SetError("xdg_wm_base_get_xdg_surface failed");
        Destroy();
        return false;
    }

    static constexpr xdg_surface_listener kXdgSurfaceListener = {
        .configure = &WaylandOwnedWindow::OnXdgSurfaceConfigure,
    };
    xdg_surface_add_listener(xdg_surface_, &kXdgSurfaceListener, this);

    toplevel_ = xdg_surface_get_toplevel(xdg_surface_);
    if (toplevel_ == nullptr) {
        SetError("xdg_surface_get_toplevel failed");
        Destroy();
        return false;
    }

    static constexpr xdg_toplevel_listener kToplevelListener = {
        .configure         = &WaylandOwnedWindow::OnToplevelConfigure,
        .close             = &WaylandOwnedWindow::OnToplevelClose,
        .configure_bounds  = nullptr,
        .wm_capabilities   = nullptr,
    };
    xdg_toplevel_add_listener(toplevel_, &kToplevelListener, this);
    if (title != nullptr && title[0] != '\0') {
        xdg_toplevel_set_title(toplevel_, title);
    } else {
        xdg_toplevel_set_title(toplevel_, "PDJE Input Fallback (Wayland)");
    }
    xdg_toplevel_set_app_id(toplevel_, "pdje-input-fallback");

    wl_surface_commit(surface_);

    if (wl_display_roundtrip(display_) < 0) {
        SetError("wl_display_roundtrip failed while awaiting xdg configure");
        Destroy();
        return false;
    }
    if (!configured_) {
        SetError("internal Wayland window was not configured by compositor");
        Destroy();
        return false;
    }

    if (!CreateShmBuffer()) {
        Destroy();
        return false;
    }

    wl_surface_attach(surface_, buffer_, 0, 0);
    wl_surface_damage_buffer(surface_, 0, 0, width_, height_);
    wl_surface_commit(surface_);

    if (wl_display_roundtrip(display_) < 0) {
        SetError("wl_display_roundtrip failed after attaching internal window "
                 "buffer");
        Destroy();
        return false;
    }

    return true;
}

void
WaylandOwnedWindow::Destroy() noexcept
{
    closed_.store(false);
    DestroyShmBuffer();
    CleanupWaylandObjects();

    if (display_ != nullptr) {
        wl_display_disconnect(display_);
        display_ = nullptr;
    }

    configured_ = false;
    seat_caps_known_ = false;
    seat_caps_       = 0;
    width_      = 640;
    height_     = 360;
}