blob: 6991c3fa9958d1d9faa69890e9256d681fcc9962 [file] [edit]
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* kms_sink.cpp - KMS Sink
*/
#include "kms_sink.h"
#include <array>
#include <algorithm>
#include <assert.h>
#include <iostream>
#include <limits.h>
#include <memory>
#include <stdint.h>
#include <string.h>
#include <libcamera/camera.h>
#include <libcamera/formats.h>
#include <libcamera/framebuffer.h>
#include <libcamera/stream.h>
#include "drm.h"
KMSSink::KMSSink(const std::string &connectorName)
: connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr)
{
int ret = dev_.init();
if (ret < 0)
return;
/*
* Find the requested connector. If no specific connector is requested,
* pick the first connected connector or, if no connector is connected,
* the first connector with unknown status.
*/
for (const DRM::Connector &conn : dev_.connectors()) {
if (!connectorName.empty()) {
if (conn.name() != connectorName)
continue;
connector_ = &conn;
break;
}
if (conn.status() == DRM::Connector::Connected) {
connector_ = &conn;
break;
}
if (!connector_ && conn.status() == DRM::Connector::Unknown)
connector_ = &conn;
}
if (!connector_) {
if (!connectorName.empty())
std::cerr
<< "Connector " << connectorName << " not found"
<< std::endl;
else
std::cerr << "No connected connector found" << std::endl;
return;
}
dev_.requestComplete.connect(this, &KMSSink::requestComplete);
}
void KMSSink::mapBuffer(libcamera::FrameBuffer *buffer)
{
std::array<uint32_t, 4> strides = {};
/* \todo Should libcamera report per-plane strides ? */
unsigned int uvStrideMultiplier;
switch (format_) {
case libcamera::formats::NV24:
case libcamera::formats::NV42:
uvStrideMultiplier = 4;
break;
case libcamera::formats::YUV420:
case libcamera::formats::YVU420:
case libcamera::formats::YUV422:
uvStrideMultiplier = 1;
break;
default:
uvStrideMultiplier = 2;
break;
}
strides[0] = stride_;
for (unsigned int i = 1; i < buffer->planes().size(); ++i)
strides[i] = stride_ * uvStrideMultiplier / 2;
std::unique_ptr<DRM::FrameBuffer> drmBuffer =
dev_.createFrameBuffer(*buffer, format_, size_, strides);
if (!drmBuffer)
return;
buffers_.emplace(std::piecewise_construct,
std::forward_as_tuple(buffer),
std::forward_as_tuple(std::move(drmBuffer)));
}
int KMSSink::configure(const libcamera::CameraConfiguration &config)
{
if (!connector_)
return -EINVAL;
crtc_ = nullptr;
plane_ = nullptr;
mode_ = nullptr;
const libcamera::StreamConfiguration &cfg = config.at(0);
/* Find the best mode for the stream size. */
const std::vector<DRM::Mode> &modes = connector_->modes();
unsigned int cfgArea = cfg.size.width * cfg.size.height;
unsigned int bestDistance = UINT_MAX;
for (const DRM::Mode &mode : modes) {
unsigned int modeArea = mode.hdisplay * mode.vdisplay;
unsigned int distance = modeArea > cfgArea ? modeArea - cfgArea
: cfgArea - modeArea;
if (distance < bestDistance) {
mode_ = &mode;
bestDistance = distance;
/*
* If the sizes match exactly, there will be no better
* match.
*/
if (distance == 0)
break;
}
}
if (!mode_) {
std::cerr << "No modes\n";
return -EINVAL;
}
int ret = configurePipeline(cfg.pixelFormat);
if (ret < 0)
return ret;
size_ = cfg.size;
stride_ = cfg.stride;
/* Configure color space. */
colorEncoding_ = std::nullopt;
colorRange_ = std::nullopt;
if (cfg.colorSpace->ycbcrEncoding == libcamera::ColorSpace::YcbcrEncoding::None)
return 0;
/*
* The encoding and range enums are defined in the kernel but not
* exposed in public headers.
*/
enum drm_color_encoding {
DRM_COLOR_YCBCR_BT601,
DRM_COLOR_YCBCR_BT709,
DRM_COLOR_YCBCR_BT2020,
};
enum drm_color_range {
DRM_COLOR_YCBCR_LIMITED_RANGE,
DRM_COLOR_YCBCR_FULL_RANGE,
};
const DRM::Property *colorEncoding = plane_->property("COLOR_ENCODING");
const DRM::Property *colorRange = plane_->property("COLOR_RANGE");
if (colorEncoding) {
drm_color_encoding encoding;
switch (cfg.colorSpace->ycbcrEncoding) {
case libcamera::ColorSpace::YcbcrEncoding::Rec601:
default:
encoding = DRM_COLOR_YCBCR_BT601;
break;
case libcamera::ColorSpace::YcbcrEncoding::Rec709:
encoding = DRM_COLOR_YCBCR_BT709;
break;
case libcamera::ColorSpace::YcbcrEncoding::Rec2020:
encoding = DRM_COLOR_YCBCR_BT2020;
break;
}
for (const auto &[id, name] : colorEncoding->enums()) {
if (id == encoding) {
colorEncoding_ = encoding;
break;
}
}
}
if (colorRange) {
drm_color_range range;
switch (cfg.colorSpace->range) {
case libcamera::ColorSpace::Range::Limited:
default:
range = DRM_COLOR_YCBCR_LIMITED_RANGE;
break;
case libcamera::ColorSpace::Range::Full:
range = DRM_COLOR_YCBCR_FULL_RANGE;
break;
}
for (const auto &[id, name] : colorRange->enums()) {
if (id == range) {
colorRange_ = range;
break;
}
}
}
if (!colorEncoding_ || !colorRange_)
std::cerr << "Color space " << cfg.colorSpace->toString()
<< " not supported by the display device."
<< " Colors may be wrong." << std::endl;
return 0;
}
int KMSSink::selectPipeline(const libcamera::PixelFormat &format)
{
/*
* If the requested format has an alpha channel, also consider the X
* variant.
*/
libcamera::PixelFormat xFormat;
switch (format) {
case libcamera::formats::ABGR8888:
xFormat = libcamera::formats::XBGR8888;
break;
case libcamera::formats::ARGB8888:
xFormat = libcamera::formats::XRGB8888;
break;
case libcamera::formats::BGRA8888:
xFormat = libcamera::formats::BGRX8888;
break;
case libcamera::formats::RGBA8888:
xFormat = libcamera::formats::RGBX8888;
break;
}
/*
* Find a CRTC and plane suitable for the request format and the
* connector at the end of the pipeline. Restrict the search to primary
* planes for now.
*/
for (const DRM::Encoder *encoder : connector_->encoders()) {
for (const DRM::Crtc *crtc : encoder->possibleCrtcs()) {
for (const DRM::Plane *plane : crtc->planes()) {
if (plane->type() != DRM::Plane::TypePrimary)
continue;
if (plane->supportsFormat(format)) {
crtc_ = crtc;
plane_ = plane;
format_ = format;
return 0;
}
if (plane->supportsFormat(xFormat)) {
crtc_ = crtc;
plane_ = plane;
format_ = xFormat;
return 0;
}
}
}
}
return -EPIPE;
}
int KMSSink::configurePipeline(const libcamera::PixelFormat &format)
{
const int ret = selectPipeline(format);
if (ret) {
std::cerr
<< "Unable to find display pipeline for format "
<< format << std::endl;
return ret;
}
std::cout
<< "Using KMS plane " << plane_->id() << ", CRTC " << crtc_->id()
<< ", connector " << connector_->name()
<< " (" << connector_->id() << "), mode " << mode_->hdisplay
<< "x" << mode_->vdisplay << "@" << mode_->vrefresh << std::endl;
return 0;
}
int KMSSink::start()
{
int ret = FrameSink::start();
if (ret < 0)
return ret;
/* Disable all CRTCs and planes to start from a known valid state. */
DRM::AtomicRequest request(&dev_);
for (const DRM::Crtc &crtc : dev_.crtcs())
request.addProperty(&crtc, "ACTIVE", 0);
for (const DRM::Plane &plane : dev_.planes()) {
request.addProperty(&plane, "CRTC_ID", 0);
request.addProperty(&plane, "FB_ID", 0);
}
ret = request.commit(DRM::AtomicRequest::FlagAllowModeset);
if (ret < 0) {
std::cerr
<< "Failed to disable CRTCs and planes: "
<< strerror(-ret) << std::endl;
return ret;
}
return 0;
}
int KMSSink::stop()
{
/* Display pipeline. */
DRM::AtomicRequest request(&dev_);
request.addProperty(connector_, "CRTC_ID", 0);
request.addProperty(crtc_, "ACTIVE", 0);
request.addProperty(crtc_, "MODE_ID", 0);
request.addProperty(plane_, "CRTC_ID", 0);
request.addProperty(plane_, "FB_ID", 0);
int ret = request.commit(DRM::AtomicRequest::FlagAllowModeset);
if (ret < 0) {
std::cerr
<< "Failed to stop display pipeline: "
<< strerror(-ret) << std::endl;
return ret;
}
/* Free all buffers. */
pending_.reset();
queued_.reset();
active_.reset();
buffers_.clear();
return FrameSink::stop();
}
bool KMSSink::testModeSet(DRM::FrameBuffer *drmBuffer,
const libcamera::Rectangle &src,
const libcamera::Rectangle &dst)
{
DRM::AtomicRequest drmRequest{ &dev_ };
drmRequest.addProperty(connector_, "CRTC_ID", crtc_->id());
drmRequest.addProperty(crtc_, "ACTIVE", 1);
drmRequest.addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_));
drmRequest.addProperty(plane_, "CRTC_ID", crtc_->id());
drmRequest.addProperty(plane_, "FB_ID", drmBuffer->id());
drmRequest.addProperty(plane_, "SRC_X", src.x << 16);
drmRequest.addProperty(plane_, "SRC_Y", src.y << 16);
drmRequest.addProperty(plane_, "SRC_W", src.width << 16);
drmRequest.addProperty(plane_, "SRC_H", src.height << 16);
drmRequest.addProperty(plane_, "CRTC_X", dst.x);
drmRequest.addProperty(plane_, "CRTC_Y", dst.y);
drmRequest.addProperty(plane_, "CRTC_W", dst.width);
drmRequest.addProperty(plane_, "CRTC_H", dst.height);
return !drmRequest.commit(DRM::AtomicRequest::FlagAllowModeset |
DRM::AtomicRequest::FlagTestOnly);
}
bool KMSSink::setupComposition(DRM::FrameBuffer *drmBuffer)
{
/*
* Test composition options, from most to least desirable, to select the
* best one.
*/
const libcamera::Rectangle framebuffer{ size_ };
const libcamera::Rectangle display{ 0, 0, mode_->hdisplay, mode_->vdisplay };
/* 1. Scale the frame buffer to full screen, preserving aspect ratio. */
libcamera::Rectangle src = framebuffer;
libcamera::Rectangle dst = display.size().boundedToAspectRatio(framebuffer.size())
.centeredTo(display.center());
if (testModeSet(drmBuffer, src, dst)) {
std::cout << "KMS: full-screen scaled output, square pixels"
<< std::endl;
src_ = src;
dst_ = dst;
return true;
}
/*
* 2. Scale the frame buffer to full screen, without preserving aspect
* ratio.
*/
src = framebuffer;
dst = display;
if (testModeSet(drmBuffer, src, dst)) {
std::cout << "KMS: full-screen scaled output, non-square pixels"
<< std::endl;
src_ = src;
dst_ = dst;
return true;
}
/* 3. Center the frame buffer on the display. */
src = display.size().centeredTo(framebuffer.center()).boundedTo(framebuffer);
dst = framebuffer.size().centeredTo(display.center()).boundedTo(display);
if (testModeSet(drmBuffer, src, dst)) {
std::cout << "KMS: centered output" << std::endl;
src_ = src;
dst_ = dst;
return true;
}
/* 4. Align the frame buffer on the top-left of the display. */
src = framebuffer.boundedTo(display);
dst = display.boundedTo(framebuffer);
if (testModeSet(drmBuffer, src, dst)) {
std::cout << "KMS: top-left aligned output" << std::endl;
src_ = src;
dst_ = dst;
return true;
}
return false;
}
bool KMSSink::processRequest(libcamera::Request *camRequest)
{
/*
* Perform a very crude rate adaptation by simply dropping the request
* if the display queue is full.
*/
if (pending_)
return true;
libcamera::FrameBuffer *buffer = camRequest->buffers().begin()->second;
auto iter = buffers_.find(buffer);
if (iter == buffers_.end())
return true;
DRM::FrameBuffer *drmBuffer = iter->second.get();
unsigned int flags = DRM::AtomicRequest::FlagAsync;
std::unique_ptr<DRM::AtomicRequest> drmRequest =
std::make_unique<DRM::AtomicRequest>(&dev_);
drmRequest->addProperty(plane_, "FB_ID", drmBuffer->id());
if (!active_ && !queued_) {
/* Enable the display pipeline on the first frame. */
if (!setupComposition(drmBuffer)) {
std::cerr << "Failed to setup composition" << std::endl;
return true;
}
drmRequest->addProperty(connector_, "CRTC_ID", crtc_->id());
drmRequest->addProperty(crtc_, "ACTIVE", 1);
drmRequest->addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_));
drmRequest->addProperty(plane_, "CRTC_ID", crtc_->id());
drmRequest->addProperty(plane_, "SRC_X", src_.x << 16);
drmRequest->addProperty(plane_, "SRC_Y", src_.y << 16);
drmRequest->addProperty(plane_, "SRC_W", src_.width << 16);
drmRequest->addProperty(plane_, "SRC_H", src_.height << 16);
drmRequest->addProperty(plane_, "CRTC_X", dst_.x);
drmRequest->addProperty(plane_, "CRTC_Y", dst_.y);
drmRequest->addProperty(plane_, "CRTC_W", dst_.width);
drmRequest->addProperty(plane_, "CRTC_H", dst_.height);
if (colorEncoding_)
drmRequest->addProperty(plane_, "COLOR_ENCODING", *colorEncoding_);
if (colorRange_)
drmRequest->addProperty(plane_, "COLOR_RANGE", *colorRange_);
flags |= DRM::AtomicRequest::FlagAllowModeset;
}
pending_ = std::make_unique<Request>(std::move(drmRequest), camRequest);
std::lock_guard<std::mutex> lock(lock_);
if (!queued_) {
int ret = pending_->drmRequest_->commit(flags);
if (ret < 0) {
std::cerr
<< "Failed to commit atomic request: "
<< strerror(-ret) << std::endl;
/* \todo Implement error handling */
}
queued_ = std::move(pending_);
}
return false;
}
void KMSSink::requestComplete([[maybe_unused]] DRM::AtomicRequest *request)
{
std::lock_guard<std::mutex> lock(lock_);
assert(queued_ && queued_->drmRequest_.get() == request);
/* Complete the active request, if any. */
if (active_)
requestProcessed.emit(active_->camRequest_);
/* The queued request becomes active. */
active_ = std::move(queued_);
/* Queue the pending request, if any. */
if (pending_) {
pending_->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
queued_ = std::move(pending_);
}
}