Move test applications to src/apps/

The cam and qcam test application share code, currently through a crude
hack that references the cam source files directly from the qcam
meson.build file. To prepare for the introduction of hosting that code
in a static library, move all applications to src/apps/.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
This commit is contained in:
Laurent Pinchart
2022-10-20 13:36:25 +03:00
parent daf3f4b59f
commit 84ad104499
350 changed files with 7 additions and 4 deletions
+450
View File
@@ -0,0 +1,450 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* camera_session.cpp - Camera capture session
*/
#include <iomanip>
#include <iostream>
#include <limits.h>
#include <sstream>
#include <libcamera/control_ids.h>
#include <libcamera/property_ids.h>
#include "camera_session.h"
#include "capture_script.h"
#include "event_loop.h"
#include "file_sink.h"
#ifdef HAVE_KMS
#include "kms_sink.h"
#endif
#include "main.h"
#ifdef HAVE_SDL
#include "sdl_sink.h"
#endif
#include "stream_options.h"
using namespace libcamera;
CameraSession::CameraSession(CameraManager *cm,
const std::string &cameraId,
unsigned int cameraIndex,
const OptionsParser::Options &options)
: options_(options), cameraIndex_(cameraIndex), last_(0),
queueCount_(0), captureCount_(0), captureLimit_(0),
printMetadata_(false)
{
char *endptr;
unsigned long index = strtoul(cameraId.c_str(), &endptr, 10);
if (*endptr == '\0' && index > 0 && index <= cm->cameras().size())
camera_ = cm->cameras()[index - 1];
else
camera_ = cm->get(cameraId);
if (!camera_) {
std::cerr << "Camera " << cameraId << " not found" << std::endl;
return;
}
if (camera_->acquire()) {
std::cerr << "Failed to acquire camera " << cameraId
<< std::endl;
return;
}
StreamRoles roles = StreamKeyValueParser::roles(options_[OptStream]);
std::unique_ptr<CameraConfiguration> config =
camera_->generateConfiguration(roles);
if (!config || config->size() != roles.size()) {
std::cerr << "Failed to get default stream configuration"
<< std::endl;
return;
}
/* Apply configuration if explicitly requested. */
if (StreamKeyValueParser::updateConfiguration(config.get(),
options_[OptStream])) {
std::cerr << "Failed to update configuration" << std::endl;
return;
}
bool strictFormats = options_.isSet(OptStrictFormats);
#ifdef HAVE_KMS
if (options_.isSet(OptDisplay)) {
if (options_.isSet(OptFile)) {
std::cerr << "--display and --file options are mutually exclusive"
<< std::endl;
return;
}
if (roles.size() != 1) {
std::cerr << "Display doesn't support multiple streams"
<< std::endl;
return;
}
if (roles[0] != StreamRole::Viewfinder) {
std::cerr << "Display requires a viewfinder stream"
<< std::endl;
return;
}
}
#endif
if (options_.isSet(OptCaptureScript)) {
std::string scriptName = options_[OptCaptureScript].toString();
script_ = std::make_unique<CaptureScript>(camera_, scriptName);
if (!script_->valid()) {
std::cerr << "Invalid capture script '" << scriptName
<< "'" << std::endl;
return;
}
}
switch (config->validate()) {
case CameraConfiguration::Valid:
break;
case CameraConfiguration::Adjusted:
if (strictFormats) {
std::cout << "Adjusting camera configuration disallowed by --strict-formats argument"
<< std::endl;
return;
}
std::cout << "Camera configuration adjusted" << std::endl;
break;
case CameraConfiguration::Invalid:
std::cout << "Camera configuration invalid" << std::endl;
return;
}
config_ = std::move(config);
}
CameraSession::~CameraSession()
{
if (camera_)
camera_->release();
}
void CameraSession::listControls() const
{
for (const auto &[id, info] : camera_->controls()) {
std::cout << "Control: " << id->name() << ": "
<< info.toString() << std::endl;
}
}
void CameraSession::listProperties() const
{
for (const auto &[key, value] : camera_->properties()) {
const ControlId *id = properties::properties.at(key);
std::cout << "Property: " << id->name() << " = "
<< value.toString() << std::endl;
}
}
void CameraSession::infoConfiguration() const
{
unsigned int index = 0;
for (const StreamConfiguration &cfg : *config_) {
std::cout << index << ": " << cfg.toString() << std::endl;
const StreamFormats &formats = cfg.formats();
for (PixelFormat pixelformat : formats.pixelformats()) {
std::cout << " * Pixelformat: "
<< pixelformat << " "
<< formats.range(pixelformat).toString()
<< std::endl;
for (const Size &size : formats.sizes(pixelformat))
std::cout << " - " << size << std::endl;
}
index++;
}
}
int CameraSession::start()
{
int ret;
queueCount_ = 0;
captureCount_ = 0;
captureLimit_ = options_[OptCapture].toInteger();
printMetadata_ = options_.isSet(OptMetadata);
ret = camera_->configure(config_.get());
if (ret < 0) {
std::cout << "Failed to configure camera" << std::endl;
return ret;
}
streamNames_.clear();
for (unsigned int index = 0; index < config_->size(); ++index) {
StreamConfiguration &cfg = config_->at(index);
streamNames_[cfg.stream()] = "cam" + std::to_string(cameraIndex_)
+ "-stream" + std::to_string(index);
}
camera_->requestCompleted.connect(this, &CameraSession::requestComplete);
#ifdef HAVE_KMS
if (options_.isSet(OptDisplay))
sink_ = std::make_unique<KMSSink>(options_[OptDisplay].toString());
#endif
#ifdef HAVE_SDL
if (options_.isSet(OptSDL))
sink_ = std::make_unique<SDLSink>();
#endif
if (options_.isSet(OptFile)) {
if (!options_[OptFile].toString().empty())
sink_ = std::make_unique<FileSink>(camera_.get(), streamNames_,
options_[OptFile]);
else
sink_ = std::make_unique<FileSink>(camera_.get(), streamNames_);
}
if (sink_) {
ret = sink_->configure(*config_);
if (ret < 0) {
std::cout << "Failed to configure frame sink"
<< std::endl;
return ret;
}
sink_->requestProcessed.connect(this, &CameraSession::sinkRelease);
}
allocator_ = std::make_unique<FrameBufferAllocator>(camera_);
return startCapture();
}
void CameraSession::stop()
{
int ret = camera_->stop();
if (ret)
std::cout << "Failed to stop capture" << std::endl;
if (sink_) {
ret = sink_->stop();
if (ret)
std::cout << "Failed to stop frame sink" << std::endl;
}
sink_.reset();
requests_.clear();
allocator_.reset();
}
int CameraSession::startCapture()
{
int ret;
/* Identify the stream with the least number of buffers. */
unsigned int nbuffers = UINT_MAX;
for (StreamConfiguration &cfg : *config_) {
ret = allocator_->allocate(cfg.stream());
if (ret < 0) {
std::cerr << "Can't allocate buffers" << std::endl;
return -ENOMEM;
}
unsigned int allocated = allocator_->buffers(cfg.stream()).size();
nbuffers = std::min(nbuffers, allocated);
}
/*
* TODO: make cam tool smarter to support still capture by for
* example pushing a button. For now run all streams all the time.
*/
for (unsigned int i = 0; i < nbuffers; i++) {
std::unique_ptr<Request> request = camera_->createRequest();
if (!request) {
std::cerr << "Can't create request" << std::endl;
return -ENOMEM;
}
for (StreamConfiguration &cfg : *config_) {
Stream *stream = cfg.stream();
const std::vector<std::unique_ptr<FrameBuffer>> &buffers =
allocator_->buffers(stream);
const std::unique_ptr<FrameBuffer> &buffer = buffers[i];
ret = request->addBuffer(stream, buffer.get());
if (ret < 0) {
std::cerr << "Can't set buffer for request"
<< std::endl;
return ret;
}
if (sink_)
sink_->mapBuffer(buffer.get());
}
requests_.push_back(std::move(request));
}
if (sink_) {
ret = sink_->start();
if (ret) {
std::cout << "Failed to start frame sink" << std::endl;
return ret;
}
}
ret = camera_->start();
if (ret) {
std::cout << "Failed to start capture" << std::endl;
if (sink_)
sink_->stop();
return ret;
}
for (std::unique_ptr<Request> &request : requests_) {
ret = queueRequest(request.get());
if (ret < 0) {
std::cerr << "Can't queue request" << std::endl;
camera_->stop();
if (sink_)
sink_->stop();
return ret;
}
}
if (captureLimit_)
std::cout << "cam" << cameraIndex_
<< ": Capture " << captureLimit_ << " frames"
<< std::endl;
else
std::cout << "cam" << cameraIndex_
<< ": Capture until user interrupts by SIGINT"
<< std::endl;
return 0;
}
int CameraSession::queueRequest(Request *request)
{
if (captureLimit_ && queueCount_ >= captureLimit_)
return 0;
if (script_)
request->controls() = script_->frameControls(queueCount_);
queueCount_++;
return camera_->queueRequest(request);
}
void CameraSession::requestComplete(Request *request)
{
if (request->status() == Request::RequestCancelled)
return;
/*
* Defer processing of the completed request to the event loop, to avoid
* blocking the camera manager thread.
*/
EventLoop::instance()->callLater([=]() { processRequest(request); });
}
void CameraSession::processRequest(Request *request)
{
/*
* If we've reached the capture limit, we're done. This doesn't
* duplicate the check below that emits the captureDone signal, as this
* function will be called for each request still in flight after the
* capture limit is reached and we don't want to emit the signal every
* single time.
*/
if (captureLimit_ && captureCount_ >= captureLimit_)
return;
const Request::BufferMap &buffers = request->buffers();
/*
* Compute the frame rate. The timestamp is arbitrarily retrieved from
* the first buffer, as all buffers should have matching timestamps.
*/
uint64_t ts = buffers.begin()->second->metadata().timestamp;
double fps = ts - last_;
fps = last_ != 0 && fps ? 1000000000.0 / fps : 0.0;
last_ = ts;
bool requeue = true;
std::stringstream info;
info << ts / 1000000000 << "."
<< std::setw(6) << std::setfill('0') << ts / 1000 % 1000000
<< " (" << std::fixed << std::setprecision(2) << fps << " fps)";
for (const auto &[stream, buffer] : buffers) {
const FrameMetadata &metadata = buffer->metadata();
info << " " << streamNames_[stream]
<< " seq: " << std::setw(6) << std::setfill('0') << metadata.sequence
<< " bytesused: ";
unsigned int nplane = 0;
for (const FrameMetadata::Plane &plane : metadata.planes()) {
info << plane.bytesused;
if (++nplane < metadata.planes().size())
info << "/";
}
}
if (sink_) {
if (!sink_->processRequest(request))
requeue = false;
}
std::cout << info.str() << std::endl;
if (printMetadata_) {
const ControlList &requestMetadata = request->metadata();
for (const auto &[key, value] : requestMetadata) {
const ControlId *id = controls::controls.at(key);
std::cout << "\t" << id->name() << " = "
<< value.toString() << std::endl;
}
}
/*
* Notify the user that capture is complete if the limit has just been
* reached.
*/
captureCount_++;
if (captureLimit_ && captureCount_ >= captureLimit_) {
captureDone.emit();
return;
}
/*
* If the frame sink holds on the request, we'll requeue it later in the
* complete handler.
*/
if (!requeue)
return;
request->reuse(Request::ReuseBuffers);
queueRequest(request);
}
void CameraSession::sinkRelease(Request *request)
{
request->reuse(Request::ReuseBuffers);
queueRequest(request);
}
+79
View File
@@ -0,0 +1,79 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* camera_session.h - Camera capture session
*/
#pragma once
#include <memory>
#include <stdint.h>
#include <string>
#include <vector>
#include <libcamera/base/signal.h>
#include <libcamera/camera.h>
#include <libcamera/camera_manager.h>
#include <libcamera/framebuffer.h>
#include <libcamera/framebuffer_allocator.h>
#include <libcamera/request.h>
#include <libcamera/stream.h>
#include "options.h"
class CaptureScript;
class FrameSink;
class CameraSession
{
public:
CameraSession(libcamera::CameraManager *cm,
const std::string &cameraId, unsigned int cameraIndex,
const OptionsParser::Options &options);
~CameraSession();
bool isValid() const { return config_ != nullptr; }
const OptionsParser::Options &options() { return options_; }
libcamera::Camera *camera() { return camera_.get(); }
libcamera::CameraConfiguration *config() { return config_.get(); }
void listControls() const;
void listProperties() const;
void infoConfiguration() const;
int start();
void stop();
libcamera::Signal<> captureDone;
private:
int startCapture();
int queueRequest(libcamera::Request *request);
void requestComplete(libcamera::Request *request);
void processRequest(libcamera::Request *request);
void sinkRelease(libcamera::Request *request);
const OptionsParser::Options &options_;
std::shared_ptr<libcamera::Camera> camera_;
std::unique_ptr<libcamera::CameraConfiguration> config_;
std::unique_ptr<CaptureScript> script_;
std::map<const libcamera::Stream *, std::string> streamNames_;
std::unique_ptr<FrameSink> sink_;
unsigned int cameraIndex_;
uint64_t last_;
unsigned int queueCount_;
unsigned int captureCount_;
unsigned int captureLimit_;
bool printMetadata_;
std::unique_ptr<libcamera::FrameBufferAllocator> allocator_;
std::vector<std::unique_ptr<libcamera::Request>> requests_;
};
+71
View File
@@ -0,0 +1,71 @@
# SPDX-License-Identifier: CC0-1.0
# Capture script example
#
# A capture script allows to associate a list of controls and their values
# to frame numbers.
#
# The script allows defining a list of frames associated with controls
# and an optional list of properties that can control the script behaviour.
# properties:
# # Repeat the controls every 'idx' frames.
# - loop: idx
#
# # List of frame number with associated a list of controls to be applied
# frames:
# - frame-number:
# Control1: value1
# Control2: value2
# \todo Formally define the capture script structure with a schema
# Notes:
# - Controls have to be specified by name, as defined in the
# libcamera::controls:: enumeration
# - Controls not supported by the camera currently operated are ignored
# - Frame numbers shall be monotonically incrementing, gaps are allowed
# - If a loop limit is specified, frame numbers in the 'frames' list shall be
# less than the loop control
# Example: Turn brightness up and down every 460 frames
properties:
- loop: 460
frames:
- 0:
Brightness: 0.0
- 40:
Brightness: 0.2
- 80:
Brightness: 0.4
- 120:
Brightness: 0.8
- 160:
Brightness: 0.4
- 200:
Brightness: 0.2
- 240:
Brightness: 0.0
- 280:
Brightness: -0.2
- 300:
Brightness: -0.4
- 340:
Brightness: -0.8
- 380:
Brightness: -0.4
- 420:
Brightness: -0.2
+535
View File
@@ -0,0 +1,535 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* capture_script.cpp - Capture session configuration script
*/
#include "capture_script.h"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace libcamera;
CaptureScript::CaptureScript(std::shared_ptr<Camera> camera,
const std::string &fileName)
: camera_(camera), loop_(0), valid_(false)
{
FILE *fh = fopen(fileName.c_str(), "r");
if (!fh) {
int ret = -errno;
std::cerr << "Failed to open capture script " << fileName
<< ": " << strerror(-ret) << std::endl;
return;
}
/*
* Map the camera's controls to their name so that they can be
* easily identified when parsing the script file.
*/
for (const auto &[control, info] : camera_->controls())
controls_[control->name()] = control;
int ret = parseScript(fh);
fclose(fh);
if (ret)
return;
valid_ = true;
}
/* Retrieve the control list associated with a frame number. */
const ControlList &CaptureScript::frameControls(unsigned int frame)
{
static ControlList controls{};
unsigned int idx = frame;
/* If we loop, repeat the controls every 'loop_' frames. */
if (loop_)
idx = frame % loop_;
auto it = frameControls_.find(idx);
if (it == frameControls_.end())
return controls;
return it->second;
}
CaptureScript::EventPtr CaptureScript::nextEvent(yaml_event_type_t expectedType)
{
EventPtr event(new yaml_event_t);
if (!yaml_parser_parse(&parser_, event.get()))
return nullptr;
if (expectedType != YAML_NO_EVENT && !checkEvent(event, expectedType))
return nullptr;
return event;
}
bool CaptureScript::checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const
{
if (event->type != expectedType) {
std::cerr << "Capture script error on line " << event->start_mark.line
<< " column " << event->start_mark.column << ": "
<< "Expected " << eventTypeName(expectedType)
<< " event, got " << eventTypeName(event->type)
<< std::endl;
return false;
}
return true;
}
std::string CaptureScript::eventScalarValue(const EventPtr &event)
{
return std::string(reinterpret_cast<char *>(event->data.scalar.value),
event->data.scalar.length);
}
std::string CaptureScript::eventTypeName(yaml_event_type_t type)
{
static const std::map<yaml_event_type_t, std::string> typeNames = {
{ YAML_STREAM_START_EVENT, "stream-start" },
{ YAML_STREAM_END_EVENT, "stream-end" },
{ YAML_DOCUMENT_START_EVENT, "document-start" },
{ YAML_DOCUMENT_END_EVENT, "document-end" },
{ YAML_ALIAS_EVENT, "alias" },
{ YAML_SCALAR_EVENT, "scalar" },
{ YAML_SEQUENCE_START_EVENT, "sequence-start" },
{ YAML_SEQUENCE_END_EVENT, "sequence-end" },
{ YAML_MAPPING_START_EVENT, "mapping-start" },
{ YAML_MAPPING_END_EVENT, "mapping-end" },
};
auto it = typeNames.find(type);
if (it == typeNames.end())
return "[type " + std::to_string(type) + "]";
return it->second;
}
int CaptureScript::parseScript(FILE *script)
{
int ret = yaml_parser_initialize(&parser_);
if (!ret) {
std::cerr << "Failed to initialize yaml parser" << std::endl;
return ret;
}
/* Delete the parser upon function exit. */
struct ParserDeleter {
ParserDeleter(yaml_parser_t *parser) : parser_(parser) { }
~ParserDeleter() { yaml_parser_delete(parser_); }
yaml_parser_t *parser_;
} deleter(&parser_);
yaml_parser_set_input_file(&parser_, script);
EventPtr event = nextEvent(YAML_STREAM_START_EVENT);
if (!event)
return -EINVAL;
event = nextEvent(YAML_DOCUMENT_START_EVENT);
if (!event)
return -EINVAL;
event = nextEvent(YAML_MAPPING_START_EVENT);
if (!event)
return -EINVAL;
while (1) {
event = nextEvent();
if (!event)
return -EINVAL;
if (event->type == YAML_MAPPING_END_EVENT)
return 0;
if (!checkEvent(event, YAML_SCALAR_EVENT))
return -EINVAL;
std::string section = eventScalarValue(event);
if (section == "properties") {
ret = parseProperties();
if (ret)
return ret;
} else if (section == "frames") {
ret = parseFrames();
if (ret)
return ret;
} else {
std::cerr << "Unsupported section '" << section << "'"
<< std::endl;
return -EINVAL;
}
}
}
int CaptureScript::parseProperty()
{
EventPtr event = nextEvent(YAML_MAPPING_START_EVENT);
if (!event)
return -EINVAL;
std::string prop = parseScalar();
if (prop.empty())
return -EINVAL;
if (prop == "loop") {
event = nextEvent();
if (!event)
return -EINVAL;
std::string value = eventScalarValue(event);
if (value.empty())
return -EINVAL;
loop_ = atoi(value.c_str());
if (!loop_) {
std::cerr << "Invalid loop limit '" << loop_ << "'"
<< std::endl;
return -EINVAL;
}
} else {
std::cerr << "Unsupported property '" << prop << "'" << std::endl;
return -EINVAL;
}
event = nextEvent(YAML_MAPPING_END_EVENT);
if (!event)
return -EINVAL;
return 0;
}
int CaptureScript::parseProperties()
{
EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
if (!event)
return -EINVAL;
while (1) {
if (event->type == YAML_SEQUENCE_END_EVENT)
return 0;
int ret = parseProperty();
if (ret)
return ret;
event = nextEvent();
if (!event)
return -EINVAL;
}
return 0;
}
int CaptureScript::parseFrames()
{
EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
if (!event)
return -EINVAL;
while (1) {
event = nextEvent();
if (!event)
return -EINVAL;
if (event->type == YAML_SEQUENCE_END_EVENT)
return 0;
int ret = parseFrame(std::move(event));
if (ret)
return ret;
}
}
int CaptureScript::parseFrame(EventPtr event)
{
if (!checkEvent(event, YAML_MAPPING_START_EVENT))
return -EINVAL;
std::string key = parseScalar();
if (key.empty())
return -EINVAL;
unsigned int frameId = atoi(key.c_str());
if (loop_ && frameId >= loop_) {
std::cerr
<< "Frame id (" << frameId << ") shall be smaller than"
<< "loop limit (" << loop_ << ")" << std::endl;
return -EINVAL;
}
event = nextEvent(YAML_MAPPING_START_EVENT);
if (!event)
return -EINVAL;
ControlList controls{};
while (1) {
event = nextEvent();
if (!event)
return -EINVAL;
if (event->type == YAML_MAPPING_END_EVENT)
break;
int ret = parseControl(std::move(event), controls);
if (ret)
return ret;
}
frameControls_[frameId] = std::move(controls);
event = nextEvent(YAML_MAPPING_END_EVENT);
if (!event)
return -EINVAL;
return 0;
}
int CaptureScript::parseControl(EventPtr event, ControlList &controls)
{
/* We expect a value after a key. */
std::string name = eventScalarValue(event);
if (name.empty())
return -EINVAL;
/* If the camera does not support the control just ignore it. */
auto it = controls_.find(name);
if (it == controls_.end()) {
std::cerr << "Unsupported control '" << name << "'" << std::endl;
return -EINVAL;
}
const ControlId *controlId = it->second;
ControlValue val = unpackControl(controlId);
if (val.isNone()) {
std::cerr << "Error unpacking control '" << name << "'"
<< std::endl;
return -EINVAL;
}
controls.set(controlId->id(), val);
return 0;
}
std::string CaptureScript::parseScalar()
{
EventPtr event = nextEvent(YAML_SCALAR_EVENT);
if (!event)
return "";
return eventScalarValue(event);
}
ControlValue CaptureScript::parseRectangles()
{
std::vector<libcamera::Rectangle> rectangles;
std::vector<std::vector<std::string>> arrays = parseArrays();
if (arrays.empty())
return {};
for (const std::vector<std::string> &values : arrays) {
if (values.size() != 4) {
std::cerr << "Error parsing Rectangle: expected "
<< "array with 4 parameters" << std::endl;
return {};
}
Rectangle rect = unpackRectangle(values);
rectangles.push_back(rect);
}
ControlValue controlValue;
controlValue.set(Span<const Rectangle>(rectangles));
return controlValue;
}
std::vector<std::vector<std::string>> CaptureScript::parseArrays()
{
EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
if (!event)
return {};
event = nextEvent();
if (!event)
return {};
std::vector<std::vector<std::string>> valueArrays;
/* Parse single array. */
if (event->type == YAML_SCALAR_EVENT) {
std::string firstValue = eventScalarValue(event);
if (firstValue.empty())
return {};
std::vector<std::string> remaining = parseSingleArray();
std::vector<std::string> values = { firstValue };
values.insert(std::end(values),
std::begin(remaining), std::end(remaining));
valueArrays.push_back(values);
return valueArrays;
}
/* Parse array of arrays. */
while (1) {
switch (event->type) {
case YAML_SEQUENCE_START_EVENT: {
std::vector<std::string> values = parseSingleArray();
valueArrays.push_back(values);
break;
}
case YAML_SEQUENCE_END_EVENT:
return valueArrays;
default:
return {};
}
event = nextEvent();
if (!event)
return {};
}
}
std::vector<std::string> CaptureScript::parseSingleArray()
{
std::vector<std::string> values;
while (1) {
EventPtr event = nextEvent();
if (!event)
return {};
switch (event->type) {
case YAML_SCALAR_EVENT: {
std::string value = eventScalarValue(event);
if (value.empty())
return {};
values.push_back(value);
break;
}
case YAML_SEQUENCE_END_EVENT:
return values;
default:
return {};
}
}
}
void CaptureScript::unpackFailure(const ControlId *id, const std::string &repr)
{
static const std::map<unsigned int, const char *> typeNames = {
{ ControlTypeNone, "none" },
{ ControlTypeBool, "bool" },
{ ControlTypeByte, "byte" },
{ ControlTypeInteger32, "int32" },
{ ControlTypeInteger64, "int64" },
{ ControlTypeFloat, "float" },
{ ControlTypeString, "string" },
{ ControlTypeRectangle, "Rectangle" },
{ ControlTypeSize, "Size" },
};
const char *typeName;
auto it = typeNames.find(id->type());
if (it != typeNames.end())
typeName = it->second;
else
typeName = "unknown";
std::cerr << "Unsupported control '" << repr << "' for "
<< typeName << " control " << id->name() << std::endl;
}
ControlValue CaptureScript::unpackControl(const ControlId *id)
{
/* Parse complex types. */
switch (id->type()) {
case ControlTypeRectangle:
return parseRectangles();
case ControlTypeSize:
/* \todo Parse Sizes. */
return {};
default:
break;
}
/* Parse basic types represented by a single scalar. */
const std::string repr = parseScalar();
if (repr.empty())
return {};
ControlValue value{};
switch (id->type()) {
case ControlTypeNone:
break;
case ControlTypeBool: {
bool val;
if (repr == "true") {
val = true;
} else if (repr == "false") {
val = false;
} else {
unpackFailure(id, repr);
return value;
}
value.set<bool>(val);
break;
}
case ControlTypeByte: {
uint8_t val = strtol(repr.c_str(), NULL, 10);
value.set<uint8_t>(val);
break;
}
case ControlTypeInteger32: {
int32_t val = strtol(repr.c_str(), NULL, 10);
value.set<int32_t>(val);
break;
}
case ControlTypeInteger64: {
int64_t val = strtoll(repr.c_str(), NULL, 10);
value.set<int64_t>(val);
break;
}
case ControlTypeFloat: {
float val = strtof(repr.c_str(), NULL);
value.set<float>(val);
break;
}
case ControlTypeString: {
value.set<std::string>(repr);
break;
}
default:
std::cerr << "Unsupported control type" << std::endl;
break;
}
return value;
}
libcamera::Rectangle CaptureScript::unpackRectangle(const std::vector<std::string> &strVec)
{
int x = strtol(strVec[0].c_str(), NULL, 10);
int y = strtol(strVec[1].c_str(), NULL, 10);
unsigned int width = strtoul(strVec[2].c_str(), NULL, 10);
unsigned int height = strtoul(strVec[3].c_str(), NULL, 10);
return Rectangle(x, y, width, height);
}
+68
View File
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* capture_script.h - Capture session configuration script
*/
#pragma once
#include <map>
#include <memory>
#include <string>
#include <libcamera/camera.h>
#include <libcamera/controls.h>
#include <yaml.h>
class CaptureScript
{
public:
CaptureScript(std::shared_ptr<libcamera::Camera> camera,
const std::string &fileName);
bool valid() const { return valid_; }
const libcamera::ControlList &frameControls(unsigned int frame);
private:
struct EventDeleter {
void operator()(yaml_event_t *event) const
{
yaml_event_delete(event);
delete event;
}
};
using EventPtr = std::unique_ptr<yaml_event_t, EventDeleter>;
std::map<std::string, const libcamera::ControlId *> controls_;
std::map<unsigned int, libcamera::ControlList> frameControls_;
std::shared_ptr<libcamera::Camera> camera_;
yaml_parser_t parser_;
unsigned int loop_;
bool valid_;
EventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT);
bool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const;
static std::string eventScalarValue(const EventPtr &event);
static std::string eventTypeName(yaml_event_type_t type);
int parseScript(FILE *script);
int parseProperties();
int parseProperty();
int parseFrames();
int parseFrame(EventPtr event);
int parseControl(EventPtr event, libcamera::ControlList &controls);
std::string parseScalar();
libcamera::ControlValue parseRectangles();
std::vector<std::vector<std::string>> parseArrays();
std::vector<std::string> parseSingleArray();
void unpackFailure(const libcamera::ControlId *id,
const std::string &repr);
libcamera::ControlValue unpackControl(const libcamera::ControlId *id);
libcamera::Rectangle unpackRectangle(const std::vector<std::string> &strVec);
};
+653
View File
@@ -0,0 +1,653 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2020, Raspberry Pi Ltd
*
* dng_writer.cpp - DNG writer
*/
#include "dng_writer.h"
#include <algorithm>
#include <iostream>
#include <map>
#include <tiffio.h>
#include <libcamera/control_ids.h>
#include <libcamera/formats.h>
#include <libcamera/property_ids.h>
using namespace libcamera;
enum CFAPatternColour : uint8_t {
CFAPatternRed = 0,
CFAPatternGreen = 1,
CFAPatternBlue = 2,
};
struct FormatInfo {
uint8_t bitsPerSample;
CFAPatternColour pattern[4];
void (*packScanline)(void *output, const void *input,
unsigned int width);
void (*thumbScanline)(const FormatInfo &info, void *output,
const void *input, unsigned int width,
unsigned int stride);
};
struct Matrix3d {
Matrix3d()
{
}
Matrix3d(float m0, float m1, float m2,
float m3, float m4, float m5,
float m6, float m7, float m8)
{
m[0] = m0, m[1] = m1, m[2] = m2;
m[3] = m3, m[4] = m4, m[5] = m5;
m[6] = m6, m[7] = m7, m[8] = m8;
}
Matrix3d(const Span<const float> &span)
: Matrix3d(span[0], span[1], span[2],
span[3], span[4], span[5],
span[6], span[7], span[8])
{
}
static Matrix3d diag(float diag0, float diag1, float diag2)
{
return Matrix3d(diag0, 0, 0, 0, diag1, 0, 0, 0, diag2);
}
static Matrix3d identity()
{
return Matrix3d(1, 0, 0, 0, 1, 0, 0, 0, 1);
}
Matrix3d transpose() const
{
return { m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8] };
}
Matrix3d cofactors() const
{
return { m[4] * m[8] - m[5] * m[7],
-(m[3] * m[8] - m[5] * m[6]),
m[3] * m[7] - m[4] * m[6],
-(m[1] * m[8] - m[2] * m[7]),
m[0] * m[8] - m[2] * m[6],
-(m[0] * m[7] - m[1] * m[6]),
m[1] * m[5] - m[2] * m[4],
-(m[0] * m[5] - m[2] * m[3]),
m[0] * m[4] - m[1] * m[3] };
}
Matrix3d adjugate() const
{
return cofactors().transpose();
}
float determinant() const
{
return m[0] * (m[4] * m[8] - m[5] * m[7]) -
m[1] * (m[3] * m[8] - m[5] * m[6]) +
m[2] * (m[3] * m[7] - m[4] * m[6]);
}
Matrix3d inverse() const
{
return adjugate() * (1.0 / determinant());
}
Matrix3d operator*(const Matrix3d &other) const
{
Matrix3d result;
for (unsigned int i = 0; i < 3; i++) {
for (unsigned int j = 0; j < 3; j++) {
result.m[i * 3 + j] =
m[i * 3 + 0] * other.m[0 + j] +
m[i * 3 + 1] * other.m[3 + j] +
m[i * 3 + 2] * other.m[6 + j];
}
}
return result;
}
Matrix3d operator*(float f) const
{
Matrix3d result;
for (unsigned int i = 0; i < 9; i++)
result.m[i] = m[i] * f;
return result;
}
float m[9];
};
void packScanlineSBGGR8(void *output, const void *input, unsigned int width)
{
const uint8_t *in = static_cast<const uint8_t *>(input);
uint8_t *out = static_cast<uint8_t *>(output);
std::copy(in, in + width, out);
}
void packScanlineSBGGR10P(void *output, const void *input, unsigned int width)
{
const uint8_t *in = static_cast<const uint8_t *>(input);
uint8_t *out = static_cast<uint8_t *>(output);
/* \todo Can this be made more efficient? */
for (unsigned int x = 0; x < width; x += 4) {
*out++ = in[0];
*out++ = (in[4] & 0x03) << 6 | in[1] >> 2;
*out++ = (in[1] & 0x03) << 6 | (in[4] & 0x0c) << 2 | in[2] >> 4;
*out++ = (in[2] & 0x0f) << 4 | (in[4] & 0x30) >> 2 | in[3] >> 6;
*out++ = (in[3] & 0x3f) << 2 | (in[4] & 0xc0) >> 6;
in += 5;
}
}
void packScanlineSBGGR12P(void *output, const void *input, unsigned int width)
{
const uint8_t *in = static_cast<const uint8_t *>(input);
uint8_t *out = static_cast<uint8_t *>(output);
/* \todo Can this be made more efficient? */
for (unsigned int i = 0; i < width; i += 2) {
*out++ = in[0];
*out++ = (in[2] & 0x0f) << 4 | in[1] >> 4;
*out++ = (in[1] & 0x0f) << 4 | in[2] >> 4;
in += 3;
}
}
void thumbScanlineSBGGRxxP(const FormatInfo &info, void *output,
const void *input, unsigned int width,
unsigned int stride)
{
const uint8_t *in = static_cast<const uint8_t *>(input);
uint8_t *out = static_cast<uint8_t *>(output);
/* Number of bytes corresponding to 16 pixels. */
unsigned int skip = info.bitsPerSample * 16 / 8;
for (unsigned int x = 0; x < width; x++) {
uint8_t value = (in[0] + in[1] + in[stride] + in[stride + 1]) >> 2;
*out++ = value;
*out++ = value;
*out++ = value;
in += skip;
}
}
void packScanlineIPU3(void *output, const void *input, unsigned int width)
{
const uint8_t *in = static_cast<const uint8_t *>(input);
uint16_t *out = static_cast<uint16_t *>(output);
/*
* Upscale the 10-bit format to 16-bit as it's not trivial to pack it
* as 10-bit without gaps.
*
* \todo Improve packing to keep the 10-bit sample size.
*/
unsigned int x = 0;
while (true) {
for (unsigned int i = 0; i < 6; i++) {
*out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
if (++x >= width)
return;
*out++ = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
if (++x >= width)
return;
*out++ = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
if (++x >= width)
return;
*out++ = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
if (++x >= width)
return;
in += 5;
}
*out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
if (++x >= width)
return;
in += 2;
}
}
void thumbScanlineIPU3([[maybe_unused]] const FormatInfo &info, void *output,
const void *input, unsigned int width,
unsigned int stride)
{
uint8_t *out = static_cast<uint8_t *>(output);
for (unsigned int x = 0; x < width; x++) {
unsigned int pixel = x * 16;
unsigned int block = pixel / 25;
unsigned int pixelInBlock = pixel - block * 25;
/*
* If the pixel is the last in the block cheat a little and
* move one pixel backward to avoid reading between two blocks
* and having to deal with the padding bits.
*/
if (pixelInBlock == 24)
pixelInBlock--;
const uint8_t *in = static_cast<const uint8_t *>(input)
+ block * 32 + (pixelInBlock / 4) * 5;
uint16_t val1, val2, val3, val4;
switch (pixelInBlock % 4) {
case 0:
val1 = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
val2 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
val3 = (in[stride + 1] & 0x03) << 14 | (in[stride + 0] & 0xff) << 6;
val4 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
break;
case 1:
val1 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
val2 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
val3 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
val4 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
break;
case 2:
val1 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
val2 = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
val3 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
val4 = (in[stride + 4] & 0xff) << 8 | (in[stride + 3] & 0xc0) << 0;
break;
case 3:
val1 = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
val2 = (in[6] & 0x03) << 14 | (in[5] & 0xff) << 6;
val3 = (in[stride + 4] & 0xff) << 8 | (in[stride + 3] & 0xc0) << 0;
val4 = (in[stride + 6] & 0x03) << 14 | (in[stride + 5] & 0xff) << 6;
break;
}
uint8_t value = (val1 + val2 + val3 + val4) >> 10;
*out++ = value;
*out++ = value;
*out++ = value;
}
}
static const std::map<PixelFormat, FormatInfo> formatInfo = {
{ formats::SBGGR8, {
.bitsPerSample = 8,
.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
.packScanline = packScanlineSBGGR8,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SGBRG8, {
.bitsPerSample = 8,
.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
.packScanline = packScanlineSBGGR8,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SGRBG8, {
.bitsPerSample = 8,
.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
.packScanline = packScanlineSBGGR8,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SRGGB8, {
.bitsPerSample = 8,
.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
.packScanline = packScanlineSBGGR8,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SBGGR10_CSI2P, {
.bitsPerSample = 10,
.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
.packScanline = packScanlineSBGGR10P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SGBRG10_CSI2P, {
.bitsPerSample = 10,
.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
.packScanline = packScanlineSBGGR10P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SGRBG10_CSI2P, {
.bitsPerSample = 10,
.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
.packScanline = packScanlineSBGGR10P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SRGGB10_CSI2P, {
.bitsPerSample = 10,
.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
.packScanline = packScanlineSBGGR10P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SBGGR12_CSI2P, {
.bitsPerSample = 12,
.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
.packScanline = packScanlineSBGGR12P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SGBRG12_CSI2P, {
.bitsPerSample = 12,
.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
.packScanline = packScanlineSBGGR12P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SGRBG12_CSI2P, {
.bitsPerSample = 12,
.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
.packScanline = packScanlineSBGGR12P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SRGGB12_CSI2P, {
.bitsPerSample = 12,
.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
.packScanline = packScanlineSBGGR12P,
.thumbScanline = thumbScanlineSBGGRxxP,
} },
{ formats::SBGGR10_IPU3, {
.bitsPerSample = 16,
.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
.packScanline = packScanlineIPU3,
.thumbScanline = thumbScanlineIPU3,
} },
{ formats::SGBRG10_IPU3, {
.bitsPerSample = 16,
.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
.packScanline = packScanlineIPU3,
.thumbScanline = thumbScanlineIPU3,
} },
{ formats::SGRBG10_IPU3, {
.bitsPerSample = 16,
.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
.packScanline = packScanlineIPU3,
.thumbScanline = thumbScanlineIPU3,
} },
{ formats::SRGGB10_IPU3, {
.bitsPerSample = 16,
.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
.packScanline = packScanlineIPU3,
.thumbScanline = thumbScanlineIPU3,
} },
};
int DNGWriter::write(const char *filename, const Camera *camera,
const StreamConfiguration &config,
const ControlList &metadata,
[[maybe_unused]] const FrameBuffer *buffer,
const void *data)
{
const ControlList &cameraProperties = camera->properties();
const auto it = formatInfo.find(config.pixelFormat);
if (it == formatInfo.cend()) {
std::cerr << "Unsupported pixel format" << std::endl;
return -EINVAL;
}
const FormatInfo *info = &it->second;
TIFF *tif = TIFFOpen(filename, "w");
if (!tif) {
std::cerr << "Failed to open tiff file" << std::endl;
return -EINVAL;
}
/*
* Scanline buffer, has to be large enough to store both a RAW scanline
* or a thumbnail scanline. The latter will always be much smaller than
* the former as we downscale by 16 in both directions.
*/
uint8_t scanline[(config.size.width * info->bitsPerSample + 7) / 8];
toff_t rawIFDOffset = 0;
toff_t exifIFDOffset = 0;
/*
* Start with a thumbnail in IFD 0 for compatibility with TIFF baseline
* readers, as required by the TIFF/EP specification. Tags that apply to
* the whole file are stored here.
*/
const uint8_t version[] = { 1, 2, 0, 0 };
TIFFSetField(tif, TIFFTAG_DNGVERSION, version);
TIFFSetField(tif, TIFFTAG_DNGBACKWARDVERSION, version);
TIFFSetField(tif, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
TIFFSetField(tif, TIFFTAG_MAKE, "libcamera");
const auto &model = cameraProperties.get(properties::Model);
if (model) {
TIFFSetField(tif, TIFFTAG_MODEL, model->c_str());
/* \todo set TIFFTAG_UNIQUECAMERAMODEL. */
}
TIFFSetField(tif, TIFFTAG_SOFTWARE, "qcam");
TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
/*
* Thumbnail-specific tags. The thumbnail is stored as an RGB image
* with 1/16 of the raw image resolution. Greyscale would save space,
* but doesn't seem well supported by RawTherapee.
*/
TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE);
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width / 16);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height / 16);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
/*
* Fill in some reasonable colour information in the DNG. We supply
* the "neutral" colour values which determine the white balance, and the
* "ColorMatrix1" which converts XYZ to (un-white-balanced) camera RGB.
* Note that this is not a "proper" colour calibration for the DNG,
* nonetheless, many tools should be able to render the colours better.
*/
float neutral[3] = { 1, 1, 1 };
Matrix3d wbGain = Matrix3d::identity();
/* From http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
const Matrix3d rgb2xyz(0.4124564, 0.3575761, 0.1804375,
0.2126729, 0.7151522, 0.0721750,
0.0193339, 0.1191920, 0.9503041);
Matrix3d ccm = Matrix3d::identity();
/*
* Pick a reasonable number eps to protect against singularities. It
* should be comfortably larger than the point at which we run into
* numerical trouble, yet smaller than any plausible gain that we might
* apply to a colour, either explicitly or as part of the colour matrix.
*/
const double eps = 1e-2;
const auto &colourGains = metadata.get(controls::ColourGains);
if (colourGains) {
if ((*colourGains)[0] > eps && (*colourGains)[1] > eps) {
wbGain = Matrix3d::diag((*colourGains)[0], 1, (*colourGains)[1]);
neutral[0] = 1.0 / (*colourGains)[0]; /* red */
neutral[2] = 1.0 / (*colourGains)[1]; /* blue */
}
}
const auto &ccmControl = metadata.get(controls::ColourCorrectionMatrix);
if (ccmControl) {
Matrix3d ccmSupplied(*ccmControl);
if (ccmSupplied.determinant() > eps)
ccm = ccmSupplied;
}
/*
* rgb2xyz is known to be invertible, and we've ensured above that both
* the ccm and wbGain matrices are non-singular, so the product of all
* three is guaranteed to be invertible too.
*/
Matrix3d colorMatrix1 = (rgb2xyz * ccm * wbGain).inverse();
TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, colorMatrix1.m);
TIFFSetField(tif, TIFFTAG_ASSHOTNEUTRAL, 3, neutral);
/*
* Reserve space for the SubIFD and ExifIFD tags, pointing to the IFD
* for the raw image and EXIF data respectively. The real offsets will
* be set later.
*/
TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
/* Write the thumbnail. */
const uint8_t *row = static_cast<const uint8_t *>(data);
for (unsigned int y = 0; y < config.size.height / 16; y++) {
info->thumbScanline(*info, &scanline, row,
config.size.width / 16, config.stride);
if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
std::cerr << "Failed to write thumbnail scanline"
<< std::endl;
TIFFClose(tif);
return -EINVAL;
}
row += config.stride * 16;
}
TIFFWriteDirectory(tif);
/* Create a new IFD for the RAW image. */
const uint16_t cfaRepeatPatternDim[] = { 2, 2 };
const uint8_t cfaPlaneColor[] = {
CFAPatternRed,
CFAPatternGreen,
CFAPatternBlue
};
TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, info->bitsPerSample);
TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
TIFFSetField(tif, TIFFTAG_CFAREPEATPATTERNDIM, cfaRepeatPatternDim);
if (TIFFLIB_VERSION < 20201219)
TIFFSetField(tif, TIFFTAG_CFAPATTERN, info->pattern);
else
TIFFSetField(tif, TIFFTAG_CFAPATTERN, 4, info->pattern);
TIFFSetField(tif, TIFFTAG_CFAPLANECOLOR, 3, cfaPlaneColor);
TIFFSetField(tif, TIFFTAG_CFALAYOUT, 1);
const uint16_t blackLevelRepeatDim[] = { 2, 2 };
float blackLevel[] = { 0.0f, 0.0f, 0.0f, 0.0f };
uint32_t whiteLevel = (1 << info->bitsPerSample) - 1;
const auto &blackLevels = metadata.get(controls::SensorBlackLevels);
if (blackLevels) {
Span<const int32_t, 4> levels = *blackLevels;
/*
* The black levels control is specified in R, Gr, Gb, B order.
* Map it to the TIFF tag that is specified in CFA pattern
* order.
*/
unsigned int green = (info->pattern[0] == CFAPatternRed ||
info->pattern[1] == CFAPatternRed)
? 0 : 1;
for (unsigned int i = 0; i < 4; ++i) {
unsigned int level;
switch (info->pattern[i]) {
case CFAPatternRed:
level = levels[0];
break;
case CFAPatternGreen:
level = levels[green + 1];
green = (green + 1) % 2;
break;
case CFAPatternBlue:
default:
level = levels[3];
break;
}
/* Map the 16-bit value to the bits per sample range. */
blackLevel[i] = level >> (16 - info->bitsPerSample);
}
}
TIFFSetField(tif, TIFFTAG_BLACKLEVELREPEATDIM, &blackLevelRepeatDim);
TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 4, &blackLevel);
TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &whiteLevel);
/* Write RAW content. */
row = static_cast<const uint8_t *>(data);
for (unsigned int y = 0; y < config.size.height; y++) {
info->packScanline(&scanline, row, config.size.width);
if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
std::cerr << "Failed to write RAW scanline"
<< std::endl;
TIFFClose(tif);
return -EINVAL;
}
row += config.stride;
}
/* Checkpoint the IFD to retrieve its offset, and write it out. */
TIFFCheckpointDirectory(tif);
rawIFDOffset = TIFFCurrentDirOffset(tif);
TIFFWriteDirectory(tif);
/* Create a new IFD for the EXIF data and fill it. */
TIFFCreateEXIFDirectory(tif);
/* Store creation time. */
time_t rawtime;
struct tm *timeinfo;
char strTime[20];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(strTime, 20, "%Y:%m:%d %H:%M:%S", timeinfo);
/*
* \todo Handle timezone information by setting OffsetTimeOriginal and
* OffsetTimeDigitized once libtiff catches up to the specification and
* has EXIFTAG_ defines to handle them.
*/
TIFFSetField(tif, EXIFTAG_DATETIMEORIGINAL, strTime);
TIFFSetField(tif, EXIFTAG_DATETIMEDIGITIZED, strTime);
const auto &analogGain = metadata.get(controls::AnalogueGain);
if (analogGain) {
uint16_t iso = std::min(std::max(*analogGain * 100, 0.0f), 65535.0f);
TIFFSetField(tif, EXIFTAG_ISOSPEEDRATINGS, 1, &iso);
}
const auto &exposureTime = metadata.get(controls::ExposureTime);
if (exposureTime)
TIFFSetField(tif, EXIFTAG_EXPOSURETIME, *exposureTime / 1e6);
TIFFWriteCustomDirectory(tif, &exifIFDOffset);
/* Update the IFD offsets and close the file. */
TIFFSetDirectory(tif, 0);
TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
TIFFWriteDirectory(tif);
TIFFClose(tif);
return 0;
}
+27
View File
@@ -0,0 +1,27 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2020, Raspberry Pi Ltd
*
* dng_writer.h - DNG writer
*/
#pragma once
#ifdef HAVE_TIFF
#define HAVE_DNG
#include <libcamera/camera.h>
#include <libcamera/controls.h>
#include <libcamera/framebuffer.h>
#include <libcamera/stream.h>
class DNGWriter
{
public:
static int write(const char *filename, const libcamera::Camera *camera,
const libcamera::StreamConfiguration &config,
const libcamera::ControlList &metadata,
const libcamera::FrameBuffer *buffer, const void *data);
};
#endif /* HAVE_TIFF */
+717
View File
@@ -0,0 +1,717 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* drm.cpp - DRM/KMS Helpers
*/
#include "drm.h"
#include <algorithm>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <set>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <libcamera/framebuffer.h>
#include <libcamera/geometry.h>
#include <libcamera/pixel_format.h>
#include <libdrm/drm_mode.h>
#include "event_loop.h"
namespace DRM {
Object::Object(Device *dev, uint32_t id, Type type)
: id_(id), dev_(dev), type_(type)
{
/* Retrieve properties from the objects that support them. */
if (type != TypeConnector && type != TypeCrtc &&
type != TypeEncoder && type != TypePlane)
return;
/*
* We can't distinguish between failures due to the object having no
* property and failures due to other conditions. Assume we use the API
* correctly and consider the object has no property.
*/
drmModeObjectProperties *properties = drmModeObjectGetProperties(dev->fd(), id, type);
if (!properties)
return;
properties_.reserve(properties->count_props);
for (uint32_t i = 0; i < properties->count_props; ++i)
properties_.emplace_back(properties->props[i],
properties->prop_values[i]);
drmModeFreeObjectProperties(properties);
}
Object::~Object()
{
}
const Property *Object::property(const std::string &name) const
{
for (const PropertyValue &pv : properties_) {
const Property *property = static_cast<const Property *>(dev_->object(pv.id()));
if (property && property->name() == name)
return property;
}
return nullptr;
}
const PropertyValue *Object::propertyValue(const std::string &name) const
{
for (const PropertyValue &pv : properties_) {
const Property *property = static_cast<const Property *>(dev_->object(pv.id()));
if (property && property->name() == name)
return &pv;
}
return nullptr;
}
Property::Property(Device *dev, drmModePropertyRes *property)
: Object(dev, property->prop_id, TypeProperty),
name_(property->name), flags_(property->flags),
values_(property->values, property->values + property->count_values),
blobs_(property->blob_ids, property->blob_ids + property->count_blobs)
{
if (drm_property_type_is(property, DRM_MODE_PROP_RANGE))
type_ = TypeRange;
else if (drm_property_type_is(property, DRM_MODE_PROP_ENUM))
type_ = TypeEnum;
else if (drm_property_type_is(property, DRM_MODE_PROP_BLOB))
type_ = TypeBlob;
else if (drm_property_type_is(property, DRM_MODE_PROP_BITMASK))
type_ = TypeBitmask;
else if (drm_property_type_is(property, DRM_MODE_PROP_OBJECT))
type_ = TypeObject;
else if (drm_property_type_is(property, DRM_MODE_PROP_SIGNED_RANGE))
type_ = TypeSignedRange;
else
type_ = TypeUnknown;
for (int i = 0; i < property->count_enums; ++i)
enums_[property->enums[i].value] = property->enums[i].name;
}
Blob::Blob(Device *dev, const libcamera::Span<const uint8_t> &data)
: Object(dev, 0, Object::TypeBlob)
{
drmModeCreatePropertyBlob(dev->fd(), data.data(), data.size(), &id_);
}
Blob::~Blob()
{
if (isValid())
drmModeDestroyPropertyBlob(device()->fd(), id());
}
Mode::Mode(const drmModeModeInfo &mode)
: drmModeModeInfo(mode)
{
}
std::unique_ptr<Blob> Mode::toBlob(Device *dev) const
{
libcamera::Span<const uint8_t> data{ reinterpret_cast<const uint8_t *>(this),
sizeof(*this) };
return std::make_unique<Blob>(dev, data);
}
Crtc::Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index)
: Object(dev, crtc->crtc_id, Object::TypeCrtc), index_(index)
{
}
Encoder::Encoder(Device *dev, const drmModeEncoder *encoder)
: Object(dev, encoder->encoder_id, Object::TypeEncoder),
type_(encoder->encoder_type)
{
const std::list<Crtc> &crtcs = dev->crtcs();
possibleCrtcs_.reserve(crtcs.size());
for (const Crtc &crtc : crtcs) {
if (encoder->possible_crtcs & (1 << crtc.index()))
possibleCrtcs_.push_back(&crtc);
}
possibleCrtcs_.shrink_to_fit();
}
namespace {
const std::map<uint32_t, const char *> connectorTypeNames{
{ DRM_MODE_CONNECTOR_Unknown, "Unknown" },
{ DRM_MODE_CONNECTOR_VGA, "VGA" },
{ DRM_MODE_CONNECTOR_DVII, "DVI-I" },
{ DRM_MODE_CONNECTOR_DVID, "DVI-D" },
{ DRM_MODE_CONNECTOR_DVIA, "DVI-A" },
{ DRM_MODE_CONNECTOR_Composite, "Composite" },
{ DRM_MODE_CONNECTOR_SVIDEO, "S-Video" },
{ DRM_MODE_CONNECTOR_LVDS, "LVDS" },
{ DRM_MODE_CONNECTOR_Component, "Component" },
{ DRM_MODE_CONNECTOR_9PinDIN, "9-Pin-DIN" },
{ DRM_MODE_CONNECTOR_DisplayPort, "DP" },
{ DRM_MODE_CONNECTOR_HDMIA, "HDMI-A" },
{ DRM_MODE_CONNECTOR_HDMIB, "HDMI-B" },
{ DRM_MODE_CONNECTOR_TV, "TV" },
{ DRM_MODE_CONNECTOR_eDP, "eDP" },
{ DRM_MODE_CONNECTOR_VIRTUAL, "Virtual" },
{ DRM_MODE_CONNECTOR_DSI, "DSI" },
{ DRM_MODE_CONNECTOR_DPI, "DPI" },
};
} /* namespace */
Connector::Connector(Device *dev, const drmModeConnector *connector)
: Object(dev, connector->connector_id, Object::TypeConnector),
type_(connector->connector_type)
{
auto typeName = connectorTypeNames.find(connector->connector_type);
if (typeName == connectorTypeNames.end()) {
std::cerr
<< "Invalid connector type "
<< connector->connector_type << std::endl;
typeName = connectorTypeNames.find(DRM_MODE_CONNECTOR_Unknown);
}
name_ = std::string(typeName->second) + "-"
+ std::to_string(connector->connector_type_id);
switch (connector->connection) {
case DRM_MODE_CONNECTED:
status_ = Status::Connected;
break;
case DRM_MODE_DISCONNECTED:
status_ = Status::Disconnected;
break;
case DRM_MODE_UNKNOWNCONNECTION:
default:
status_ = Status::Unknown;
break;
}
const std::list<Encoder> &encoders = dev->encoders();
encoders_.reserve(connector->count_encoders);
for (int i = 0; i < connector->count_encoders; ++i) {
uint32_t encoderId = connector->encoders[i];
auto encoder = std::find_if(encoders.begin(), encoders.end(),
[=](const Encoder &e) {
return e.id() == encoderId;
});
if (encoder == encoders.end()) {
std::cerr
<< "Encoder " << encoderId << " not found"
<< std::endl;
continue;
}
encoders_.push_back(&*encoder);
}
encoders_.shrink_to_fit();
modes_ = { connector->modes, connector->modes + connector->count_modes };
}
Plane::Plane(Device *dev, const drmModePlane *plane)
: Object(dev, plane->plane_id, Object::TypePlane),
possibleCrtcsMask_(plane->possible_crtcs)
{
formats_ = { plane->formats, plane->formats + plane->count_formats };
const std::list<Crtc> &crtcs = dev->crtcs();
possibleCrtcs_.reserve(crtcs.size());
for (const Crtc &crtc : crtcs) {
if (plane->possible_crtcs & (1 << crtc.index()))
possibleCrtcs_.push_back(&crtc);
}
possibleCrtcs_.shrink_to_fit();
}
bool Plane::supportsFormat(const libcamera::PixelFormat &format) const
{
return std::find(formats_.begin(), formats_.end(), format.fourcc())
!= formats_.end();
}
int Plane::setup()
{
const PropertyValue *pv = propertyValue("type");
if (!pv)
return -EINVAL;
switch (pv->value()) {
case DRM_PLANE_TYPE_OVERLAY:
type_ = TypeOverlay;
break;
case DRM_PLANE_TYPE_PRIMARY:
type_ = TypePrimary;
break;
case DRM_PLANE_TYPE_CURSOR:
type_ = TypeCursor;
break;
default:
return -EINVAL;
}
return 0;
}
FrameBuffer::FrameBuffer(Device *dev)
: Object(dev, 0, Object::TypeFb)
{
}
FrameBuffer::~FrameBuffer()
{
for (const auto &plane : planes_) {
struct drm_gem_close gem_close = {
.handle = plane.second.handle,
.pad = 0,
};
int ret;
do {
ret = ioctl(device()->fd(), DRM_IOCTL_GEM_CLOSE, &gem_close);
} while (ret == -1 && (errno == EINTR || errno == EAGAIN));
if (ret == -1) {
ret = -errno;
std::cerr
<< "Failed to close GEM object: "
<< strerror(-ret) << std::endl;
}
}
drmModeRmFB(device()->fd(), id());
}
AtomicRequest::AtomicRequest(Device *dev)
: dev_(dev), valid_(true)
{
request_ = drmModeAtomicAlloc();
if (!request_)
valid_ = false;
}
AtomicRequest::~AtomicRequest()
{
if (request_)
drmModeAtomicFree(request_);
}
int AtomicRequest::addProperty(const Object *object, const std::string &property,
uint64_t value)
{
if (!valid_)
return -EINVAL;
const Property *prop = object->property(property);
if (!prop) {
valid_ = false;
return -EINVAL;
}
return addProperty(object->id(), prop->id(), value);
}
int AtomicRequest::addProperty(const Object *object, const std::string &property,
std::unique_ptr<Blob> blob)
{
if (!valid_)
return -EINVAL;
const Property *prop = object->property(property);
if (!prop) {
valid_ = false;
return -EINVAL;
}
int ret = addProperty(object->id(), prop->id(), blob->id());
if (ret < 0)
return ret;
blobs_.emplace_back(std::move(blob));
return 0;
}
int AtomicRequest::addProperty(uint32_t object, uint32_t property, uint64_t value)
{
int ret = drmModeAtomicAddProperty(request_, object, property, value);
if (ret < 0) {
valid_ = false;
return ret;
}
return 0;
}
int AtomicRequest::commit(unsigned int flags)
{
if (!valid_)
return -EINVAL;
uint32_t drmFlags = 0;
if (flags & FlagAllowModeset)
drmFlags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
if (flags & FlagAsync)
drmFlags |= DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_NONBLOCK;
if (flags & FlagTestOnly)
drmFlags |= DRM_MODE_ATOMIC_TEST_ONLY;
return drmModeAtomicCommit(dev_->fd(), request_, drmFlags, this);
}
Device::Device()
: fd_(-1)
{
}
Device::~Device()
{
if (fd_ != -1)
drmClose(fd_);
}
int Device::init()
{
int ret = openCard();
if (ret < 0) {
std::cerr << "Failed to open any DRM/KMS device: "
<< strerror(-ret) << std::endl;
return ret;
}
/*
* Enable the atomic APIs. This also automatically enables the
* universal planes API.
*/
ret = drmSetClientCap(fd_, DRM_CLIENT_CAP_ATOMIC, 1);
if (ret < 0) {
ret = -errno;
std::cerr
<< "Failed to enable atomic capability: "
<< strerror(-ret) << std::endl;
return ret;
}
/* List all the resources. */
ret = getResources();
if (ret < 0)
return ret;
EventLoop::instance()->addFdEvent(fd_, EventLoop::Read,
std::bind(&Device::drmEvent, this));
return 0;
}
int Device::openCard()
{
const std::string dirName = "/dev/dri/";
bool found = false;
int ret;
/*
* Open the first DRM/KMS device beginning with /dev/dri/card. The
* libdrm drmOpen*() functions require either a module name or a bus ID,
* which we don't have, so bypass them. The automatic module loading and
* device node creation from drmOpen() is of no practical use as any
* modern system will handle that through udev or an equivalent
* component.
*/
DIR *folder = opendir(dirName.c_str());
if (!folder) {
ret = -errno;
std::cerr << "Failed to open " << dirName
<< " directory: " << strerror(-ret) << std::endl;
return ret;
}
for (struct dirent *res; (res = readdir(folder));) {
uint64_t cap;
if (strncmp(res->d_name, "card", 4))
continue;
const std::string devName = dirName + res->d_name;
fd_ = open(devName.c_str(), O_RDWR | O_CLOEXEC);
if (fd_ < 0) {
ret = -errno;
std::cerr << "Failed to open DRM/KMS device " << devName << ": "
<< strerror(-ret) << std::endl;
continue;
}
/*
* Skip devices that don't support the modeset API, to avoid
* selecting a DRM device corresponding to a GPU. There is no
* modeset capability, but the kernel returns an error for most
* caps if mode setting isn't support by the driver. The
* DRM_CAP_DUMB_BUFFER capability is one of those, other would
* do as well. The capability value itself isn't relevant.
*/
ret = drmGetCap(fd_, DRM_CAP_DUMB_BUFFER, &cap);
if (ret < 0) {
drmClose(fd_);
fd_ = -1;
continue;
}
found = true;
break;
}
closedir(folder);
return found ? 0 : -ENOENT;
}
int Device::getResources()
{
int ret;
std::unique_ptr<drmModeRes, decltype(&drmModeFreeResources)> resources{
drmModeGetResources(fd_),
&drmModeFreeResources
};
if (!resources) {
ret = -errno;
std::cerr
<< "Failed to get DRM/KMS resources: "
<< strerror(-ret) << std::endl;
return ret;
}
for (int i = 0; i < resources->count_crtcs; ++i) {
drmModeCrtc *crtc = drmModeGetCrtc(fd_, resources->crtcs[i]);
if (!crtc) {
ret = -errno;
std::cerr
<< "Failed to get CRTC: " << strerror(-ret)
<< std::endl;
return ret;
}
crtcs_.emplace_back(this, crtc, i);
drmModeFreeCrtc(crtc);
Crtc &obj = crtcs_.back();
objects_[obj.id()] = &obj;
}
for (int i = 0; i < resources->count_encoders; ++i) {
drmModeEncoder *encoder =
drmModeGetEncoder(fd_, resources->encoders[i]);
if (!encoder) {
ret = -errno;
std::cerr
<< "Failed to get encoder: " << strerror(-ret)
<< std::endl;
return ret;
}
encoders_.emplace_back(this, encoder);
drmModeFreeEncoder(encoder);
Encoder &obj = encoders_.back();
objects_[obj.id()] = &obj;
}
for (int i = 0; i < resources->count_connectors; ++i) {
drmModeConnector *connector =
drmModeGetConnector(fd_, resources->connectors[i]);
if (!connector) {
ret = -errno;
std::cerr
<< "Failed to get connector: " << strerror(-ret)
<< std::endl;
return ret;
}
connectors_.emplace_back(this, connector);
drmModeFreeConnector(connector);
Connector &obj = connectors_.back();
objects_[obj.id()] = &obj;
}
std::unique_ptr<drmModePlaneRes, decltype(&drmModeFreePlaneResources)> planes{
drmModeGetPlaneResources(fd_),
&drmModeFreePlaneResources
};
if (!planes) {
ret = -errno;
std::cerr
<< "Failed to get DRM/KMS planes: "
<< strerror(-ret) << std::endl;
return ret;
}
for (uint32_t i = 0; i < planes->count_planes; ++i) {
drmModePlane *plane =
drmModeGetPlane(fd_, planes->planes[i]);
if (!plane) {
ret = -errno;
std::cerr
<< "Failed to get plane: " << strerror(-ret)
<< std::endl;
return ret;
}
planes_.emplace_back(this, plane);
drmModeFreePlane(plane);
Plane &obj = planes_.back();
objects_[obj.id()] = &obj;
}
/* Set the possible planes for each CRTC. */
for (Crtc &crtc : crtcs_) {
for (const Plane &plane : planes_) {
if (plane.possibleCrtcsMask_ & (1 << crtc.index()))
crtc.planes_.push_back(&plane);
}
}
/* Collect all property IDs and create Property instances. */
std::set<uint32_t> properties;
for (const auto &object : objects_) {
for (const PropertyValue &value : object.second->properties())
properties.insert(value.id());
}
for (uint32_t id : properties) {
drmModePropertyRes *property = drmModeGetProperty(fd_, id);
if (!property) {
ret = -errno;
std::cerr
<< "Failed to get property: " << strerror(-ret)
<< std::endl;
continue;
}
properties_.emplace_back(this, property);
drmModeFreeProperty(property);
Property &obj = properties_.back();
objects_[obj.id()] = &obj;
}
/* Finally, perform all delayed setup of mode objects. */
for (auto &object : objects_) {
ret = object.second->setup();
if (ret < 0) {
std::cerr
<< "Failed to setup object " << object.second->id()
<< ": " << strerror(-ret) << std::endl;
return ret;
}
}
return 0;
}
const Object *Device::object(uint32_t id)
{
const auto iter = objects_.find(id);
if (iter == objects_.end())
return nullptr;
return iter->second;
}
std::unique_ptr<FrameBuffer> Device::createFrameBuffer(
const libcamera::FrameBuffer &buffer,
const libcamera::PixelFormat &format,
const libcamera::Size &size,
const std::array<uint32_t, 4> &strides)
{
std::unique_ptr<FrameBuffer> fb{ new FrameBuffer(this) };
uint32_t handles[4] = {};
uint32_t offsets[4] = {};
int ret;
const std::vector<libcamera::FrameBuffer::Plane> &planes = buffer.planes();
unsigned int i = 0;
for (const libcamera::FrameBuffer::Plane &plane : planes) {
int fd = plane.fd.get();
uint32_t handle;
auto iter = fb->planes_.find(fd);
if (iter == fb->planes_.end()) {
ret = drmPrimeFDToHandle(fd_, plane.fd.get(), &handle);
if (ret < 0) {
ret = -errno;
std::cerr
<< "Unable to import framebuffer dmabuf: "
<< strerror(-ret) << std::endl;
return nullptr;
}
fb->planes_[fd] = { handle };
} else {
handle = iter->second.handle;
}
handles[i] = handle;
offsets[i] = plane.offset;
++i;
}
ret = drmModeAddFB2(fd_, size.width, size.height, format.fourcc(), handles,
strides.data(), offsets, &fb->id_, 0);
if (ret < 0) {
ret = -errno;
std::cerr
<< "Failed to add framebuffer: "
<< strerror(-ret) << std::endl;
return nullptr;
}
return fb;
}
void Device::drmEvent()
{
drmEventContext ctx{};
ctx.version = DRM_EVENT_CONTEXT_VERSION;
ctx.page_flip_handler = &Device::pageFlipComplete;
drmHandleEvent(fd_, &ctx);
}
void Device::pageFlipComplete([[maybe_unused]] int fd,
[[maybe_unused]] unsigned int sequence,
[[maybe_unused]] unsigned int tv_sec,
[[maybe_unused]] unsigned int tv_usec,
void *user_data)
{
AtomicRequest *request = static_cast<AtomicRequest *>(user_data);
request->device()->requestComplete.emit(request);
}
} /* namespace DRM */
+334
View File
@@ -0,0 +1,334 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* drm.h - DRM/KMS Helpers
*/
#pragma once
#include <array>
#include <list>
#include <map>
#include <memory>
#include <stdint.h>
#include <string>
#include <vector>
#include <libcamera/base/signal.h>
#include <libcamera/base/span.h>
#include <libdrm/drm.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
namespace libcamera {
class FrameBuffer;
class PixelFormat;
class Size;
} /* namespace libcamera */
namespace DRM {
class Device;
class Plane;
class Property;
class PropertyValue;
class Object
{
public:
enum Type {
TypeCrtc = DRM_MODE_OBJECT_CRTC,
TypeConnector = DRM_MODE_OBJECT_CONNECTOR,
TypeEncoder = DRM_MODE_OBJECT_ENCODER,
TypeMode = DRM_MODE_OBJECT_MODE,
TypeProperty = DRM_MODE_OBJECT_PROPERTY,
TypeFb = DRM_MODE_OBJECT_FB,
TypeBlob = DRM_MODE_OBJECT_BLOB,
TypePlane = DRM_MODE_OBJECT_PLANE,
TypeAny = DRM_MODE_OBJECT_ANY,
};
Object(Device *dev, uint32_t id, Type type);
virtual ~Object();
Device *device() const { return dev_; }
uint32_t id() const { return id_; }
Type type() const { return type_; }
const Property *property(const std::string &name) const;
const PropertyValue *propertyValue(const std::string &name) const;
const std::vector<PropertyValue> &properties() const { return properties_; }
protected:
virtual int setup()
{
return 0;
}
uint32_t id_;
private:
friend Device;
Device *dev_;
Type type_;
std::vector<PropertyValue> properties_;
};
class Property : public Object
{
public:
enum Type {
TypeUnknown = 0,
TypeRange,
TypeEnum,
TypeBlob,
TypeBitmask,
TypeObject,
TypeSignedRange,
};
Property(Device *dev, drmModePropertyRes *property);
Type type() const { return type_; }
const std::string &name() const { return name_; }
bool isImmutable() const { return flags_ & DRM_MODE_PROP_IMMUTABLE; }
const std::vector<uint64_t> values() const { return values_; }
const std::map<uint32_t, std::string> &enums() const { return enums_; }
const std::vector<uint32_t> blobs() const { return blobs_; }
private:
Type type_;
std::string name_;
uint32_t flags_;
std::vector<uint64_t> values_;
std::map<uint32_t, std::string> enums_;
std::vector<uint32_t> blobs_;
};
class PropertyValue
{
public:
PropertyValue(uint32_t id, uint64_t value)
: id_(id), value_(value)
{
}
uint32_t id() const { return id_; }
uint32_t value() const { return value_; }
private:
uint32_t id_;
uint64_t value_;
};
class Blob : public Object
{
public:
Blob(Device *dev, const libcamera::Span<const uint8_t> &data);
~Blob();
bool isValid() const { return id() != 0; }
};
class Mode : public drmModeModeInfo
{
public:
Mode(const drmModeModeInfo &mode);
std::unique_ptr<Blob> toBlob(Device *dev) const;
};
class Crtc : public Object
{
public:
Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index);
unsigned int index() const { return index_; }
const std::vector<const Plane *> &planes() const { return planes_; }
private:
friend Device;
unsigned int index_;
std::vector<const Plane *> planes_;
};
class Encoder : public Object
{
public:
Encoder(Device *dev, const drmModeEncoder *encoder);
uint32_t type() const { return type_; }
const std::vector<const Crtc *> &possibleCrtcs() const { return possibleCrtcs_; }
private:
uint32_t type_;
std::vector<const Crtc *> possibleCrtcs_;
};
class Connector : public Object
{
public:
enum Status {
Connected,
Disconnected,
Unknown,
};
Connector(Device *dev, const drmModeConnector *connector);
uint32_t type() const { return type_; }
const std::string &name() const { return name_; }
Status status() const { return status_; }
const std::vector<const Encoder *> &encoders() const { return encoders_; }
const std::vector<Mode> &modes() const { return modes_; }
private:
uint32_t type_;
std::string name_;
Status status_;
std::vector<const Encoder *> encoders_;
std::vector<Mode> modes_;
};
class Plane : public Object
{
public:
enum Type {
TypeOverlay,
TypePrimary,
TypeCursor,
};
Plane(Device *dev, const drmModePlane *plane);
Type type() const { return type_; }
const std::vector<uint32_t> &formats() const { return formats_; }
const std::vector<const Crtc *> &possibleCrtcs() const { return possibleCrtcs_; }
bool supportsFormat(const libcamera::PixelFormat &format) const;
protected:
int setup() override;
private:
friend class Device;
Type type_;
std::vector<uint32_t> formats_;
std::vector<const Crtc *> possibleCrtcs_;
uint32_t possibleCrtcsMask_;
};
class FrameBuffer : public Object
{
public:
struct Plane {
uint32_t handle;
};
~FrameBuffer();
private:
friend class Device;
FrameBuffer(Device *dev);
std::map<int, Plane> planes_;
};
class AtomicRequest
{
public:
enum Flags {
FlagAllowModeset = (1 << 0),
FlagAsync = (1 << 1),
FlagTestOnly = (1 << 2),
};
AtomicRequest(Device *dev);
~AtomicRequest();
Device *device() const { return dev_; }
bool isValid() const { return valid_; }
int addProperty(const Object *object, const std::string &property,
uint64_t value);
int addProperty(const Object *object, const std::string &property,
std::unique_ptr<Blob> blob);
int commit(unsigned int flags = 0);
private:
AtomicRequest(const AtomicRequest &) = delete;
AtomicRequest(const AtomicRequest &&) = delete;
AtomicRequest &operator=(const AtomicRequest &) = delete;
AtomicRequest &operator=(const AtomicRequest &&) = delete;
int addProperty(uint32_t object, uint32_t property, uint64_t value);
Device *dev_;
bool valid_;
drmModeAtomicReq *request_;
std::list<std::unique_ptr<Blob>> blobs_;
};
class Device
{
public:
Device();
~Device();
int init();
int fd() const { return fd_; }
const std::list<Crtc> &crtcs() const { return crtcs_; }
const std::list<Encoder> &encoders() const { return encoders_; }
const std::list<Connector> &connectors() const { return connectors_; }
const std::list<Plane> &planes() const { return planes_; }
const std::list<Property> &properties() const { return properties_; }
const Object *object(uint32_t id);
std::unique_ptr<FrameBuffer> createFrameBuffer(
const libcamera::FrameBuffer &buffer,
const libcamera::PixelFormat &format,
const libcamera::Size &size,
const std::array<uint32_t, 4> &strides);
libcamera::Signal<AtomicRequest *> requestComplete;
private:
Device(const Device &) = delete;
Device(const Device &&) = delete;
Device &operator=(const Device &) = delete;
Device &operator=(const Device &&) = delete;
int openCard();
int getResources();
void drmEvent();
static void pageFlipComplete(int fd, unsigned int sequence,
unsigned int tv_sec, unsigned int tv_usec,
void *user_data);
int fd_;
std::list<Crtc> crtcs_;
std::list<Encoder> encoders_;
std::list<Connector> connectors_;
std::list<Plane> planes_;
std::list<Property> properties_;
std::map<uint32_t, Object *> objects_;
};
} /* namespace DRM */
+150
View File
@@ -0,0 +1,150 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* event_loop.cpp - cam - Event loop
*/
#include "event_loop.h"
#include <assert.h>
#include <event2/event.h>
#include <event2/thread.h>
#include <iostream>
EventLoop *EventLoop::instance_ = nullptr;
EventLoop::EventLoop()
{
assert(!instance_);
evthread_use_pthreads();
base_ = event_base_new();
instance_ = this;
}
EventLoop::~EventLoop()
{
instance_ = nullptr;
events_.clear();
event_base_free(base_);
libevent_global_shutdown();
}
EventLoop *EventLoop::instance()
{
return instance_;
}
int EventLoop::exec()
{
exitCode_ = -1;
event_base_loop(base_, EVLOOP_NO_EXIT_ON_EMPTY);
return exitCode_;
}
void EventLoop::exit(int code)
{
exitCode_ = code;
event_base_loopbreak(base_);
}
void EventLoop::callLater(const std::function<void()> &func)
{
{
std::unique_lock<std::mutex> locker(lock_);
calls_.push_back(func);
}
event_base_once(base_, -1, EV_TIMEOUT, dispatchCallback, this, nullptr);
}
void EventLoop::addFdEvent(int fd, EventType type,
const std::function<void()> &callback)
{
std::unique_ptr<Event> event = std::make_unique<Event>(callback);
short events = (type & Read ? EV_READ : 0)
| (type & Write ? EV_WRITE : 0)
| EV_PERSIST;
event->event_ = event_new(base_, fd, events, &EventLoop::Event::dispatch,
event.get());
if (!event->event_) {
std::cerr << "Failed to create event for fd " << fd << std::endl;
return;
}
int ret = event_add(event->event_, nullptr);
if (ret < 0) {
std::cerr << "Failed to add event for fd " << fd << std::endl;
return;
}
events_.push_back(std::move(event));
}
void EventLoop::addTimerEvent(const std::chrono::microseconds period,
const std::function<void()> &callback)
{
std::unique_ptr<Event> event = std::make_unique<Event>(callback);
event->event_ = event_new(base_, -1, EV_PERSIST, &EventLoop::Event::dispatch,
event.get());
if (!event->event_) {
std::cerr << "Failed to create timer event" << std::endl;
return;
}
struct timeval tv;
tv.tv_sec = period.count() / 1000000ULL;
tv.tv_usec = period.count() % 1000000ULL;
int ret = event_add(event->event_, &tv);
if (ret < 0) {
std::cerr << "Failed to add timer event" << std::endl;
return;
}
events_.push_back(std::move(event));
}
void EventLoop::dispatchCallback([[maybe_unused]] evutil_socket_t fd,
[[maybe_unused]] short flags, void *param)
{
EventLoop *loop = static_cast<EventLoop *>(param);
loop->dispatchCall();
}
void EventLoop::dispatchCall()
{
std::function<void()> call;
{
std::unique_lock<std::mutex> locker(lock_);
if (calls_.empty())
return;
call = calls_.front();
calls_.pop_front();
}
call();
}
EventLoop::Event::Event(const std::function<void()> &callback)
: callback_(callback), event_(nullptr)
{
}
EventLoop::Event::~Event()
{
event_del(event_);
event_free(event_);
}
void EventLoop::Event::dispatch([[maybe_unused]] int fd,
[[maybe_unused]] short events, void *arg)
{
Event *event = static_cast<Event *>(arg);
event->callback_();
}
+68
View File
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* event_loop.h - cam - Event loop
*/
#pragma once
#include <chrono>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <event2/util.h>
struct event_base;
class EventLoop
{
public:
enum EventType {
Read = 1,
Write = 2,
};
EventLoop();
~EventLoop();
static EventLoop *instance();
int exec();
void exit(int code = 0);
void callLater(const std::function<void()> &func);
void addFdEvent(int fd, EventType type,
const std::function<void()> &handler);
using duration = std::chrono::steady_clock::duration;
void addTimerEvent(const std::chrono::microseconds period,
const std::function<void()> &handler);
private:
struct Event {
Event(const std::function<void()> &callback);
~Event();
static void dispatch(int fd, short events, void *arg);
std::function<void()> callback_;
struct event *event_;
};
static EventLoop *instance_;
struct event_base *base_;
int exitCode_;
std::list<std::function<void()>> calls_;
std::list<std::unique_ptr<Event>> events_;
std::mutex lock_;
static void dispatchCallback(evutil_socket_t fd, short flags,
void *param);
void dispatchCall();
};
+137
View File
@@ -0,0 +1,137 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* file_sink.cpp - File Sink
*/
#include <assert.h>
#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string.h>
#include <unistd.h>
#include <libcamera/camera.h>
#include "dng_writer.h"
#include "file_sink.h"
#include "image.h"
using namespace libcamera;
FileSink::FileSink(const libcamera::Camera *camera,
const std::map<const libcamera::Stream *, std::string> &streamNames,
const std::string &pattern)
: camera_(camera), streamNames_(streamNames), pattern_(pattern)
{
}
FileSink::~FileSink()
{
}
int FileSink::configure(const libcamera::CameraConfiguration &config)
{
int ret = FrameSink::configure(config);
if (ret < 0)
return ret;
return 0;
}
void FileSink::mapBuffer(FrameBuffer *buffer)
{
std::unique_ptr<Image> image =
Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly);
assert(image != nullptr);
mappedBuffers_[buffer] = std::move(image);
}
bool FileSink::processRequest(Request *request)
{
for (auto [stream, buffer] : request->buffers())
writeBuffer(stream, buffer, request->metadata());
return true;
}
void FileSink::writeBuffer(const Stream *stream, FrameBuffer *buffer,
[[maybe_unused]] const ControlList &metadata)
{
std::string filename;
size_t pos;
int fd, ret = 0;
if (!pattern_.empty())
filename = pattern_;
#ifdef HAVE_TIFF
bool dng = filename.find(".dng", filename.size() - 4) != std::string::npos;
#endif /* HAVE_TIFF */
if (filename.empty() || filename.back() == '/')
filename += "frame-#.bin";
pos = filename.find_first_of('#');
if (pos != std::string::npos) {
std::stringstream ss;
ss << streamNames_[stream] << "-" << std::setw(6)
<< std::setfill('0') << buffer->metadata().sequence;
filename.replace(pos, 1, ss.str());
}
Image *image = mappedBuffers_[buffer].get();
#ifdef HAVE_TIFF
if (dng) {
ret = DNGWriter::write(filename.c_str(), camera_,
stream->configuration(), metadata,
buffer, image->data(0).data());
if (ret < 0)
std::cerr << "failed to write DNG file `" << filename
<< "'" << std::endl;
return;
}
#endif /* HAVE_TIFF */
fd = open(filename.c_str(), O_CREAT | O_WRONLY |
(pos == std::string::npos ? O_APPEND : O_TRUNC),
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fd == -1) {
ret = -errno;
std::cerr << "failed to open file " << filename << ": "
<< strerror(-ret) << std::endl;
return;
}
for (unsigned int i = 0; i < buffer->planes().size(); ++i) {
const FrameMetadata::Plane &meta = buffer->metadata().planes()[i];
Span<uint8_t> data = image->data(i);
unsigned int length = std::min<unsigned int>(meta.bytesused, data.size());
if (meta.bytesused > data.size())
std::cerr << "payload size " << meta.bytesused
<< " larger than plane size " << data.size()
<< std::endl;
ret = ::write(fd, data.data(), length);
if (ret < 0) {
ret = -errno;
std::cerr << "write error: " << strerror(-ret)
<< std::endl;
break;
} else if (ret != (int)length) {
std::cerr << "write error: only " << ret
<< " bytes written instead of "
<< length << std::endl;
break;
}
}
close(fd);
}
+43
View File
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* file_sink.h - File Sink
*/
#pragma once
#include <map>
#include <memory>
#include <string>
#include <libcamera/stream.h>
#include "frame_sink.h"
class Image;
class FileSink : public FrameSink
{
public:
FileSink(const libcamera::Camera *camera,
const std::map<const libcamera::Stream *, std::string> &streamNames,
const std::string &pattern = "");
~FileSink();
int configure(const libcamera::CameraConfiguration &config) override;
void mapBuffer(libcamera::FrameBuffer *buffer) override;
bool processRequest(libcamera::Request *request) override;
private:
void writeBuffer(const libcamera::Stream *stream,
libcamera::FrameBuffer *buffer,
const libcamera::ControlList &metadata);
const libcamera::Camera *camera_;
std::map<const libcamera::Stream *, std::string> streamNames_;
std::string pattern_;
std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_;
};
+67
View File
@@ -0,0 +1,67 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* frame_sink.cpp - Base Frame Sink Class
*/
#include "frame_sink.h"
/**
* \class FrameSink
* \brief Abstract class to model a consumer of frames
*
* The FrameSink class models the consumer that processes frames after a request
* completes. It receives requests through processRequest(), and processes them
* synchronously or asynchronously. This allows frame sinks to hold onto frames
* for an extended period of time, for instance to display them until a new
* frame arrives.
*
* A frame sink processes whole requests, and is solely responsible for deciding
* how to handle different frame buffers in case multiple streams are captured.
*/
FrameSink::~FrameSink()
{
}
int FrameSink::configure([[maybe_unused]] const libcamera::CameraConfiguration &config)
{
return 0;
}
void FrameSink::mapBuffer([[maybe_unused]] libcamera::FrameBuffer *buffer)
{
}
int FrameSink::start()
{
return 0;
}
int FrameSink::stop()
{
return 0;
}
/**
* \fn FrameSink::processRequest()
* \param[in] request The request
*
* This function is called to instruct the sink to process a request. The sink
* may process the request synchronously or queue it for asynchronous
* processing.
*
* When the request is processed synchronously, this function shall return true.
* The \a request shall not be accessed by the FrameSink after the function
* returns.
*
* When the request is processed asynchronously, the FrameSink temporarily takes
* ownership of the \a request. The function shall return false, and the
* FrameSink shall emit the requestProcessed signal when the request processing
* completes. If the stop() function is called before the request processing
* completes, it shall release the request synchronously.
*
* \return True if the request has been processed synchronously, false if
* processing has been queued
*/
+32
View File
@@ -0,0 +1,32 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* frame_sink.h - Base Frame Sink Class
*/
#pragma once
#include <libcamera/base/signal.h>
namespace libcamera {
class CameraConfiguration;
class FrameBuffer;
class Request;
} /* namespace libcamera */
class FrameSink
{
public:
virtual ~FrameSink();
virtual int configure(const libcamera::CameraConfiguration &config);
virtual void mapBuffer(libcamera::FrameBuffer *buffer);
virtual int start();
virtual int stop();
virtual bool processRequest(libcamera::Request *request) = 0;
libcamera::Signal<libcamera::Request *> requestProcessed;
};
+109
View File
@@ -0,0 +1,109 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* image.cpp - Multi-planar image with access to pixel data
*/
#include "image.h"
#include <assert.h>
#include <errno.h>
#include <iostream>
#include <map>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
using namespace libcamera;
std::unique_ptr<Image> Image::fromFrameBuffer(const FrameBuffer *buffer, MapMode mode)
{
std::unique_ptr<Image> image{ new Image() };
assert(!buffer->planes().empty());
int mmapFlags = 0;
if (mode & MapMode::ReadOnly)
mmapFlags |= PROT_READ;
if (mode & MapMode::WriteOnly)
mmapFlags |= PROT_WRITE;
struct MappedBufferInfo {
uint8_t *address = nullptr;
size_t mapLength = 0;
size_t dmabufLength = 0;
};
std::map<int, MappedBufferInfo> mappedBuffers;
for (const FrameBuffer::Plane &plane : buffer->planes()) {
const int fd = plane.fd.get();
if (mappedBuffers.find(fd) == mappedBuffers.end()) {
const size_t length = lseek(fd, 0, SEEK_END);
mappedBuffers[fd] = MappedBufferInfo{ nullptr, 0, length };
}
const size_t length = mappedBuffers[fd].dmabufLength;
if (plane.offset > length ||
plane.offset + plane.length > length) {
std::cerr << "plane is out of buffer: buffer length="
<< length << ", plane offset=" << plane.offset
<< ", plane length=" << plane.length
<< std::endl;
return nullptr;
}
size_t &mapLength = mappedBuffers[fd].mapLength;
mapLength = std::max(mapLength,
static_cast<size_t>(plane.offset + plane.length));
}
for (const FrameBuffer::Plane &plane : buffer->planes()) {
const int fd = plane.fd.get();
auto &info = mappedBuffers[fd];
if (!info.address) {
void *address = mmap(nullptr, info.mapLength, mmapFlags,
MAP_SHARED, fd, 0);
if (address == MAP_FAILED) {
int error = -errno;
std::cerr << "Failed to mmap plane: "
<< strerror(-error) << std::endl;
return nullptr;
}
info.address = static_cast<uint8_t *>(address);
image->maps_.emplace_back(info.address, info.mapLength);
}
image->planes_.emplace_back(info.address + plane.offset, plane.length);
}
return image;
}
Image::Image() = default;
Image::~Image()
{
for (Span<uint8_t> &map : maps_)
munmap(map.data(), map.size());
}
unsigned int Image::numPlanes() const
{
return planes_.size();
}
Span<uint8_t> Image::data(unsigned int plane)
{
assert(plane <= planes_.size());
return planes_[plane];
}
Span<const uint8_t> Image::data(unsigned int plane) const
{
assert(plane <= planes_.size());
return planes_[plane];
}
+50
View File
@@ -0,0 +1,50 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* image.h - Multi-planar image with access to pixel data
*/
#pragma once
#include <memory>
#include <stdint.h>
#include <vector>
#include <libcamera/base/class.h>
#include <libcamera/base/flags.h>
#include <libcamera/base/span.h>
#include <libcamera/framebuffer.h>
class Image
{
public:
enum class MapMode {
ReadOnly = 1 << 0,
WriteOnly = 1 << 1,
ReadWrite = ReadOnly | WriteOnly,
};
static std::unique_ptr<Image> fromFrameBuffer(const libcamera::FrameBuffer *buffer,
MapMode mode);
~Image();
unsigned int numPlanes() const;
libcamera::Span<uint8_t> data(unsigned int plane);
libcamera::Span<const uint8_t> data(unsigned int plane) const;
private:
LIBCAMERA_DISABLE_COPY(Image)
Image();
std::vector<libcamera::Span<uint8_t>> maps_;
std::vector<libcamera::Span<uint8_t>> planes_;
};
namespace libcamera {
LIBCAMERA_FLAGS_ENABLE_OPERATORS(Image::MapMode)
}
+538
View File
@@ -0,0 +1,538 @@
/* 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()
{
std::unique_ptr<DRM::AtomicRequest> request;
int ret = FrameSink::start();
if (ret < 0)
return ret;
/* Disable all CRTCs and planes to start from a known valid state. */
request = std::make_unique<DRM::AtomicRequest>(&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(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_);
}
}
+83
View File
@@ -0,0 +1,83 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Ideas on Board Oy
*
* kms_sink.h - KMS Sink
*/
#pragma once
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <libcamera/base/signal.h>
#include <libcamera/geometry.h>
#include <libcamera/pixel_format.h>
#include "drm.h"
#include "frame_sink.h"
class KMSSink : public FrameSink
{
public:
KMSSink(const std::string &connectorName);
void mapBuffer(libcamera::FrameBuffer *buffer) override;
int configure(const libcamera::CameraConfiguration &config) override;
int start() override;
int stop() override;
bool processRequest(libcamera::Request *request) override;
private:
class Request
{
public:
Request(std::unique_ptr<DRM::AtomicRequest> drmRequest,
libcamera::Request *camRequest)
: drmRequest_(std::move(drmRequest)), camRequest_(camRequest)
{
}
std::unique_ptr<DRM::AtomicRequest> drmRequest_;
libcamera::Request *camRequest_;
};
int selectPipeline(const libcamera::PixelFormat &format);
int configurePipeline(const libcamera::PixelFormat &format);
bool testModeSet(DRM::FrameBuffer *drmBuffer,
const libcamera::Rectangle &src,
const libcamera::Rectangle &dst);
bool setupComposition(DRM::FrameBuffer *drmBuffer);
void requestComplete(DRM::AtomicRequest *request);
DRM::Device dev_;
const DRM::Connector *connector_;
const DRM::Crtc *crtc_;
const DRM::Plane *plane_;
const DRM::Mode *mode_;
libcamera::PixelFormat format_;
libcamera::Size size_;
unsigned int stride_;
std::optional<unsigned int> colorEncoding_;
std::optional<unsigned int> colorRange_;
libcamera::Rectangle src_;
libcamera::Rectangle dst_;
std::map<libcamera::FrameBuffer *, std::unique_ptr<DRM::FrameBuffer>> buffers_;
std::mutex lock_;
std::unique_ptr<Request> pending_;
std::unique_ptr<Request> queued_;
std::unique_ptr<Request> active_;
};
+362
View File
@@ -0,0 +1,362 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* main.cpp - cam - The libcamera swiss army knife
*/
#include <atomic>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <string.h>
#include <libcamera/libcamera.h>
#include <libcamera/property_ids.h>
#include "camera_session.h"
#include "event_loop.h"
#include "main.h"
#include "options.h"
#include "stream_options.h"
using namespace libcamera;
class CamApp
{
public:
CamApp();
static CamApp *instance();
int init(int argc, char **argv);
void cleanup();
int exec();
void quit();
private:
void cameraAdded(std::shared_ptr<Camera> cam);
void cameraRemoved(std::shared_ptr<Camera> cam);
void captureDone();
int parseOptions(int argc, char *argv[]);
int run();
static std::string cameraName(const Camera *camera);
static CamApp *app_;
OptionsParser::Options options_;
std::unique_ptr<CameraManager> cm_;
std::atomic_uint loopUsers_;
EventLoop loop_;
};
CamApp *CamApp::app_ = nullptr;
CamApp::CamApp()
: loopUsers_(0)
{
CamApp::app_ = this;
}
CamApp *CamApp::instance()
{
return CamApp::app_;
}
int CamApp::init(int argc, char **argv)
{
int ret;
ret = parseOptions(argc, argv);
if (ret < 0)
return ret;
cm_ = std::make_unique<CameraManager>();
ret = cm_->start();
if (ret) {
std::cout << "Failed to start camera manager: "
<< strerror(-ret) << std::endl;
return ret;
}
return 0;
}
void CamApp::cleanup()
{
cm_->stop();
}
int CamApp::exec()
{
int ret;
ret = run();
cleanup();
return ret;
}
void CamApp::quit()
{
loop_.exit();
}
int CamApp::parseOptions(int argc, char *argv[])
{
StreamKeyValueParser streamKeyValue;
OptionsParser parser;
parser.addOption(OptCamera, OptionString,
"Specify which camera to operate on, by id or by index", "camera",
ArgumentRequired, "camera", true);
parser.addOption(OptHelp, OptionNone, "Display this help message",
"help");
parser.addOption(OptInfo, OptionNone,
"Display information about stream(s)", "info");
parser.addOption(OptList, OptionNone, "List all cameras", "list");
parser.addOption(OptListControls, OptionNone, "List cameras controls",
"list-controls");
parser.addOption(OptListProperties, OptionNone, "List cameras properties",
"list-properties");
parser.addOption(OptMonitor, OptionNone,
"Monitor for hotplug and unplug camera events",
"monitor");
/* Sub-options of OptCamera: */
parser.addOption(OptCapture, OptionInteger,
"Capture until interrupted by user or until <count> frames captured",
"capture", ArgumentOptional, "count", false,
OptCamera);
#ifdef HAVE_KMS
parser.addOption(OptDisplay, OptionString,
"Display viewfinder through DRM/KMS on specified connector",
"display", ArgumentOptional, "connector", false,
OptCamera);
#endif
parser.addOption(OptFile, OptionString,
"Write captured frames to disk\n"
"If the file name ends with a '/', it sets the directory in which\n"
"to write files, using the default file name. Otherwise it sets the\n"
"full file path and name. The first '#' character in the file name\n"
"is expanded to the camera index, stream name and frame sequence number.\n"
#ifdef HAVE_TIFF
"If the file name ends with '.dng', then the frame will be written to\n"
"the output file(s) in DNG format.\n"
#endif
"The default file name is 'frame-#.bin'.",
"file", ArgumentOptional, "filename", false,
OptCamera);
#ifdef HAVE_SDL
parser.addOption(OptSDL, OptionNone, "Display viewfinder through SDL",
"sdl", ArgumentNone, "", false, OptCamera);
#endif
parser.addOption(OptStream, &streamKeyValue,
"Set configuration of a camera stream", "stream", true,
OptCamera);
parser.addOption(OptStrictFormats, OptionNone,
"Do not allow requested stream format(s) to be adjusted",
"strict-formats", ArgumentNone, nullptr, false,
OptCamera);
parser.addOption(OptMetadata, OptionNone,
"Print the metadata for completed requests",
"metadata", ArgumentNone, nullptr, false,
OptCamera);
parser.addOption(OptCaptureScript, OptionString,
"Load a capture session configuration script from a file",
"script", ArgumentRequired, "script", false,
OptCamera);
options_ = parser.parse(argc, argv);
if (!options_.valid())
return -EINVAL;
if (options_.empty() || options_.isSet(OptHelp)) {
parser.usage();
return options_.empty() ? -EINVAL : -EINTR;
}
return 0;
}
void CamApp::cameraAdded(std::shared_ptr<Camera> cam)
{
std::cout << "Camera Added: " << cam->id() << std::endl;
}
void CamApp::cameraRemoved(std::shared_ptr<Camera> cam)
{
std::cout << "Camera Removed: " << cam->id() << std::endl;
}
void CamApp::captureDone()
{
if (--loopUsers_ == 0)
EventLoop::instance()->exit(0);
}
int CamApp::run()
{
int ret;
/* 1. List all cameras. */
if (options_.isSet(OptList)) {
std::cout << "Available cameras:" << std::endl;
unsigned int index = 1;
for (const std::shared_ptr<Camera> &cam : cm_->cameras()) {
std::cout << index << ": " << cameraName(cam.get()) << std::endl;
index++;
}
}
/* 2. Create the camera sessions. */
std::vector<std::unique_ptr<CameraSession>> sessions;
if (options_.isSet(OptCamera)) {
unsigned int index = 0;
for (const OptionValue &camera : options_[OptCamera].toArray()) {
std::unique_ptr<CameraSession> session =
std::make_unique<CameraSession>(cm_.get(),
camera.toString(),
index,
camera.children());
if (!session->isValid()) {
std::cout << "Failed to create camera session" << std::endl;
return -EINVAL;
}
std::cout << "Using camera " << session->camera()->id()
<< " as cam" << index << std::endl;
session->captureDone.connect(this, &CamApp::captureDone);
sessions.push_back(std::move(session));
index++;
}
}
/* 3. Print camera information. */
if (options_.isSet(OptListControls) ||
options_.isSet(OptListProperties) ||
options_.isSet(OptInfo)) {
for (const auto &session : sessions) {
if (options_.isSet(OptListControls))
session->listControls();
if (options_.isSet(OptListProperties))
session->listProperties();
if (options_.isSet(OptInfo))
session->infoConfiguration();
}
}
/* 4. Start capture. */
for (const auto &session : sessions) {
if (!session->options().isSet(OptCapture))
continue;
ret = session->start();
if (ret) {
std::cout << "Failed to start camera session" << std::endl;
return ret;
}
loopUsers_++;
}
/* 5. Enable hotplug monitoring. */
if (options_.isSet(OptMonitor)) {
std::cout << "Monitoring new hotplug and unplug events" << std::endl;
std::cout << "Press Ctrl-C to interrupt" << std::endl;
cm_->cameraAdded.connect(this, &CamApp::cameraAdded);
cm_->cameraRemoved.connect(this, &CamApp::cameraRemoved);
loopUsers_++;
}
if (loopUsers_)
loop_.exec();
/* 6. Stop capture. */
for (const auto &session : sessions) {
if (!session->options().isSet(OptCapture))
continue;
session->stop();
}
return 0;
}
std::string CamApp::cameraName(const Camera *camera)
{
const ControlList &props = camera->properties();
bool addModel = true;
std::string name;
/*
* Construct the name from the camera location, model and ID. The model
* is only used if the location isn't present or is set to External.
*/
const auto &location = props.get(properties::Location);
if (location) {
switch (*location) {
case properties::CameraLocationFront:
addModel = false;
name = "Internal front camera ";
break;
case properties::CameraLocationBack:
addModel = false;
name = "Internal back camera ";
break;
case properties::CameraLocationExternal:
name = "External camera ";
break;
}
}
if (addModel) {
/*
* If the camera location is not availble use the camera model
* to build the camera name.
*/
const auto &model = props.get(properties::Model);
if (model)
name = "'" + *model + "' ";
}
name += "(" + camera->id() + ")";
return name;
}
void signalHandler([[maybe_unused]] int signal)
{
std::cout << "Exiting" << std::endl;
CamApp::instance()->quit();
}
int main(int argc, char **argv)
{
CamApp app;
int ret;
ret = app.init(argc, argv);
if (ret)
return ret == -EINTR ? 0 : EXIT_FAILURE;
struct sigaction sa = {};
sa.sa_handler = &signalHandler;
sigaction(SIGINT, &sa, nullptr);
if (app.exec())
return EXIT_FAILURE;
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* main.h - Cam application
*/
#pragma once
enum {
OptCamera = 'c',
OptCapture = 'C',
OptDisplay = 'D',
OptFile = 'F',
OptHelp = 'h',
OptInfo = 'I',
OptList = 'l',
OptListProperties = 'p',
OptMonitor = 'm',
OptSDL = 'S',
OptStream = 's',
OptListControls = 256,
OptStrictFormats = 257,
OptMetadata = 258,
OptCaptureScript = 259,
};
+74
View File
@@ -0,0 +1,74 @@
# SPDX-License-Identifier: CC0-1.0
libevent = dependency('libevent_pthreads', required : get_option('cam'))
if not libevent.found()
cam_enabled = false
subdir_done()
endif
cam_enabled = true
cam_sources = files([
'camera_session.cpp',
'capture_script.cpp',
'event_loop.cpp',
'file_sink.cpp',
'frame_sink.cpp',
'image.cpp',
'main.cpp',
'options.cpp',
'stream_options.cpp',
])
cam_cpp_args = []
libdrm = dependency('libdrm', required : false)
libjpeg = dependency('libjpeg', required : false)
libsdl2 = dependency('SDL2', required : false)
libtiff = dependency('libtiff-4', required : false)
if libdrm.found()
cam_cpp_args += [ '-DHAVE_KMS' ]
cam_sources += files([
'drm.cpp',
'kms_sink.cpp'
])
endif
if libsdl2.found()
cam_cpp_args += ['-DHAVE_SDL']
cam_sources += files([
'sdl_sink.cpp',
'sdl_texture.cpp',
'sdl_texture_yuv.cpp',
])
if libjpeg.found()
cam_cpp_args += ['-DHAVE_LIBJPEG']
cam_sources += files([
'sdl_texture_mjpg.cpp'
])
endif
endif
if libtiff.found()
cam_cpp_args += ['-DHAVE_TIFF']
cam_sources += files([
'dng_writer.cpp',
])
endif
cam = executable('cam', cam_sources,
dependencies : [
libatomic,
libcamera_public,
libdrm,
libevent,
libjpeg,
libsdl2,
libtiff,
libyaml,
],
cpp_args : cam_cpp_args,
install : true)
File diff suppressed because it is too large Load Diff
+157
View File
@@ -0,0 +1,157 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* options.h - cam - Options parsing
*/
#pragma once
#include <ctype.h>
#include <list>
#include <map>
#include <tuple>
#include <vector>
class KeyValueParser;
class OptionValue;
struct Option;
enum OptionArgument {
ArgumentNone,
ArgumentRequired,
ArgumentOptional,
};
enum OptionType {
OptionNone,
OptionInteger,
OptionString,
OptionKeyValue,
};
template<typename T>
class OptionsBase
{
public:
OptionsBase() : valid_(false) {}
bool empty() const;
bool valid() const;
bool isSet(const T &opt) const;
const OptionValue &operator[](const T &opt) const;
void invalidate();
private:
friend class KeyValueParser;
friend class OptionsParser;
bool parseValue(const T &opt, const Option &option, const char *value);
std::map<T, OptionValue> values_;
bool valid_;
};
class KeyValueParser
{
public:
class Options : public OptionsBase<std::string>
{
};
KeyValueParser();
virtual ~KeyValueParser();
bool addOption(const char *name, OptionType type, const char *help,
OptionArgument argument = ArgumentNone);
virtual Options parse(const char *arguments);
private:
KeyValueParser(const KeyValueParser &) = delete;
KeyValueParser &operator=(const KeyValueParser &) = delete;
friend class OptionsParser;
unsigned int maxOptionLength() const;
void usage(int indent);
std::map<std::string, Option> optionsMap_;
};
class OptionsParser
{
public:
class Options : public OptionsBase<int>
{
};
OptionsParser();
~OptionsParser();
bool addOption(int opt, OptionType type, const char *help,
const char *name = nullptr,
OptionArgument argument = ArgumentNone,
const char *argumentName = nullptr, bool array = false,
int parent = 0);
bool addOption(int opt, KeyValueParser *parser, const char *help,
const char *name = nullptr, bool array = false,
int parent = 0);
Options parse(int argc, char *argv[]);
void usage();
private:
OptionsParser(const OptionsParser &) = delete;
OptionsParser &operator=(const OptionsParser &) = delete;
void usageOptions(const std::list<Option> &options, unsigned int indent);
std::tuple<OptionsParser::Options *, const Option *>
childOption(const Option *parent, Options *options);
bool parseValue(const Option &option, const char *arg, Options *options);
std::list<Option> options_;
std::map<unsigned int, Option *> optionsMap_;
};
class OptionValue
{
public:
enum ValueType {
ValueNone,
ValueInteger,
ValueString,
ValueKeyValue,
ValueArray,
};
OptionValue();
OptionValue(int value);
OptionValue(const char *value);
OptionValue(const std::string &value);
OptionValue(const KeyValueParser::Options &value);
void addValue(const OptionValue &value);
ValueType type() const { return type_; }
bool empty() const { return type_ == ValueType::ValueNone; }
operator int() const;
operator std::string() const;
int toInteger() const;
std::string toString() const;
const KeyValueParser::Options &toKeyValues() const;
const std::vector<OptionValue> &toArray() const;
const OptionsParser::Options &children() const;
private:
ValueType type_;
int integer_;
std::string string_;
KeyValueParser::Options keyValues_;
std::vector<OptionValue> array_;
OptionsParser::Options children_;
};
+214
View File
@@ -0,0 +1,214 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_sink.h - SDL Sink
*/
#include "sdl_sink.h"
#include <assert.h>
#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <sstream>
#include <string.h>
#include <unistd.h>
#include <libcamera/camera.h>
#include <libcamera/formats.h>
#include "event_loop.h"
#include "image.h"
#ifdef HAVE_LIBJPEG
#include "sdl_texture_mjpg.h"
#endif
#include "sdl_texture_yuv.h"
using namespace libcamera;
using namespace std::chrono_literals;
SDLSink::SDLSink()
: window_(nullptr), renderer_(nullptr), rect_({}),
init_(false)
{
}
SDLSink::~SDLSink()
{
stop();
}
int SDLSink::configure(const libcamera::CameraConfiguration &config)
{
int ret = FrameSink::configure(config);
if (ret < 0)
return ret;
if (config.size() > 1) {
std::cerr
<< "SDL sink only supports one camera stream at present, streaming first camera stream"
<< std::endl;
} else if (config.empty()) {
std::cerr << "Require at least one camera stream to process"
<< std::endl;
return -EINVAL;
}
const libcamera::StreamConfiguration &cfg = config.at(0);
rect_.w = cfg.size.width;
rect_.h = cfg.size.height;
switch (cfg.pixelFormat) {
#ifdef HAVE_LIBJPEG
case libcamera::formats::MJPEG:
texture_ = std::make_unique<SDLTextureMJPG>(rect_);
break;
#endif
#if SDL_VERSION_ATLEAST(2, 0, 16)
case libcamera::formats::NV12:
texture_ = std::make_unique<SDLTextureNV12>(rect_, cfg.stride);
break;
#endif
case libcamera::formats::YUYV:
texture_ = std::make_unique<SDLTextureYUYV>(rect_, cfg.stride);
break;
default:
std::cerr << "Unsupported pixel format "
<< cfg.pixelFormat.toString() << std::endl;
return -EINVAL;
};
return 0;
}
int SDLSink::start()
{
int ret = SDL_Init(SDL_INIT_VIDEO);
if (ret) {
std::cerr << "Failed to initialize SDL: " << SDL_GetError()
<< std::endl;
return ret;
}
init_ = true;
window_ = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, rect_.w,
rect_.h,
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
if (!window_) {
std::cerr << "Failed to create SDL window: " << SDL_GetError()
<< std::endl;
return -EINVAL;
}
renderer_ = SDL_CreateRenderer(window_, -1, 0);
if (!renderer_) {
std::cerr << "Failed to create SDL renderer: " << SDL_GetError()
<< std::endl;
return -EINVAL;
}
/*
* Set for scaling purposes, not critical, don't return in case of
* error.
*/
ret = SDL_RenderSetLogicalSize(renderer_, rect_.w, rect_.h);
if (ret)
std::cerr << "Failed to set SDL render logical size: "
<< SDL_GetError() << std::endl;
ret = texture_->create(renderer_);
if (ret) {
return ret;
}
/* \todo Make the event cancellable to support stop/start cycles. */
EventLoop::instance()->addTimerEvent(
10ms, std::bind(&SDLSink::processSDLEvents, this));
return 0;
}
int SDLSink::stop()
{
texture_.reset();
if (renderer_) {
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
}
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
}
if (init_) {
SDL_Quit();
init_ = false;
}
return FrameSink::stop();
}
void SDLSink::mapBuffer(FrameBuffer *buffer)
{
std::unique_ptr<Image> image =
Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly);
assert(image != nullptr);
mappedBuffers_[buffer] = std::move(image);
}
bool SDLSink::processRequest(Request *request)
{
for (auto [stream, buffer] : request->buffers()) {
renderBuffer(buffer);
break; /* to be expanded to launch SDL window per buffer */
}
return true;
}
/*
* Process SDL events, required for things like window resize and quit button
*/
void SDLSink::processSDLEvents()
{
for (SDL_Event e; SDL_PollEvent(&e);) {
if (e.type == SDL_QUIT) {
/* Click close icon then quit */
EventLoop::instance()->exit(0);
}
}
}
void SDLSink::renderBuffer(FrameBuffer *buffer)
{
Image *image = mappedBuffers_[buffer].get();
std::vector<Span<const uint8_t>> planes;
unsigned int i = 0;
planes.reserve(buffer->metadata().planes().size());
for (const FrameMetadata::Plane &meta : buffer->metadata().planes()) {
Span<uint8_t> data = image->data(i);
if (meta.bytesused > data.size())
std::cerr << "payload size " << meta.bytesused
<< " larger than plane size " << data.size()
<< std::endl;
planes.push_back(data);
i++;
}
texture_->update(planes);
SDL_RenderClear(renderer_);
SDL_RenderCopy(renderer_, texture_->get(), nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
+48
View File
@@ -0,0 +1,48 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_sink.h - SDL Sink
*/
#pragma once
#include <map>
#include <memory>
#include <libcamera/stream.h>
#include <SDL2/SDL.h>
#include "frame_sink.h"
class Image;
class SDLTexture;
class SDLSink : public FrameSink
{
public:
SDLSink();
~SDLSink();
int configure(const libcamera::CameraConfiguration &config) override;
int start() override;
int stop() override;
void mapBuffer(libcamera::FrameBuffer *buffer) override;
bool processRequest(libcamera::Request *request) override;
private:
void renderBuffer(libcamera::FrameBuffer *buffer);
void processSDLEvents();
std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>>
mappedBuffers_;
std::unique_ptr<SDLTexture> texture_;
SDL_Window *window_;
SDL_Renderer *renderer_;
SDL_Rect rect_;
bool init_;
};
+36
View File
@@ -0,0 +1,36 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_texture.cpp - SDL Texture
*/
#include "sdl_texture.h"
#include <iostream>
SDLTexture::SDLTexture(const SDL_Rect &rect, uint32_t pixelFormat,
const int stride)
: ptr_(nullptr), rect_(rect), pixelFormat_(pixelFormat), stride_(stride)
{
}
SDLTexture::~SDLTexture()
{
if (ptr_)
SDL_DestroyTexture(ptr_);
}
int SDLTexture::create(SDL_Renderer *renderer)
{
ptr_ = SDL_CreateTexture(renderer, pixelFormat_,
SDL_TEXTUREACCESS_STREAMING, rect_.w,
rect_.h);
if (!ptr_) {
std::cerr << "Failed to create SDL texture: " << SDL_GetError()
<< std::endl;
return -ENOMEM;
}
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_texture.h - SDL Texture
*/
#pragma once
#include <vector>
#include <SDL2/SDL.h>
#include "image.h"
class SDLTexture
{
public:
SDLTexture(const SDL_Rect &rect, uint32_t pixelFormat, const int stride);
virtual ~SDLTexture();
int create(SDL_Renderer *renderer);
virtual void update(const std::vector<libcamera::Span<const uint8_t>> &data) = 0;
SDL_Texture *get() const { return ptr_; }
protected:
SDL_Texture *ptr_;
const SDL_Rect rect_;
const uint32_t pixelFormat_;
const int stride_;
};
+83
View File
@@ -0,0 +1,83 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_texture_mjpg.cpp - SDL Texture MJPG
*/
#include "sdl_texture_mjpg.h"
#include <iostream>
#include <setjmp.h>
#include <stdio.h>
#include <jpeglib.h>
using namespace libcamera;
struct JpegErrorManager : public jpeg_error_mgr {
JpegErrorManager()
{
jpeg_std_error(this);
error_exit = errorExit;
output_message = outputMessage;
}
static void errorExit(j_common_ptr cinfo)
{
JpegErrorManager *self =
static_cast<JpegErrorManager *>(cinfo->err);
longjmp(self->escape_, 1);
}
static void outputMessage([[maybe_unused]] j_common_ptr cinfo)
{
}
jmp_buf escape_;
};
SDLTextureMJPG::SDLTextureMJPG(const SDL_Rect &rect)
: SDLTexture(rect, SDL_PIXELFORMAT_RGB24, rect.w * 3),
rgb_(std::make_unique<unsigned char[]>(stride_ * rect.h))
{
}
int SDLTextureMJPG::decompress(Span<const uint8_t> data)
{
struct jpeg_decompress_struct cinfo;
JpegErrorManager errorManager;
if (setjmp(errorManager.escape_)) {
/* libjpeg found an error */
jpeg_destroy_decompress(&cinfo);
std::cerr << "JPEG decompression error" << std::endl;
return -EINVAL;
}
cinfo.err = &errorManager;
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, data.data(), data.size());
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);
for (int i = 0; cinfo.output_scanline < cinfo.output_height; ++i) {
JSAMPROW rowptr = rgb_.get() + i * stride_;
jpeg_read_scanlines(&cinfo, &rowptr, 1);
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return 0;
}
void SDLTextureMJPG::update(const std::vector<libcamera::Span<const uint8_t>> &data)
{
decompress(data[0]);
SDL_UpdateTexture(ptr_, nullptr, rgb_.get(), stride_);
}
+23
View File
@@ -0,0 +1,23 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_texture_mjpg.h - SDL Texture MJPG
*/
#pragma once
#include "sdl_texture.h"
class SDLTextureMJPG : public SDLTexture
{
public:
SDLTextureMJPG(const SDL_Rect &rect);
void update(const std::vector<libcamera::Span<const uint8_t>> &data) override;
private:
int decompress(libcamera::Span<const uint8_t> data);
std::unique_ptr<unsigned char[]> rgb_;
};
+33
View File
@@ -0,0 +1,33 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_texture_yuv.cpp - SDL YUV Textures
*/
#include "sdl_texture_yuv.h"
using namespace libcamera;
#if SDL_VERSION_ATLEAST(2, 0, 16)
SDLTextureNV12::SDLTextureNV12(const SDL_Rect &rect, unsigned int stride)
: SDLTexture(rect, SDL_PIXELFORMAT_NV12, stride)
{
}
void SDLTextureNV12::update(const std::vector<libcamera::Span<const uint8_t>> &data)
{
SDL_UpdateNVTexture(ptr_, &rect_, data[0].data(), stride_,
data[1].data(), stride_);
}
#endif
SDLTextureYUYV::SDLTextureYUYV(const SDL_Rect &rect, unsigned int stride)
: SDLTexture(rect, SDL_PIXELFORMAT_YUY2, stride)
{
}
void SDLTextureYUYV::update(const std::vector<libcamera::Span<const uint8_t>> &data)
{
SDL_UpdateTexture(ptr_, &rect_, data[0].data(), stride_);
}
+26
View File
@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2022, Ideas on Board Oy
*
* sdl_texture_yuv.h - SDL YUV Textures
*/
#pragma once
#include "sdl_texture.h"
#if SDL_VERSION_ATLEAST(2, 0, 16)
class SDLTextureNV12 : public SDLTexture
{
public:
SDLTextureNV12(const SDL_Rect &rect, unsigned int stride);
void update(const std::vector<libcamera::Span<const uint8_t>> &data) override;
};
#endif
class SDLTextureYUYV : public SDLTexture
{
public:
SDLTextureYUYV(const SDL_Rect &rect, unsigned int stride);
void update(const std::vector<libcamera::Span<const uint8_t>> &data) override;
};
+134
View File
@@ -0,0 +1,134 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020, Raspberry Pi Ltd
*
* stream_options.cpp - Helper to parse options for streams
*/
#include "stream_options.h"
#include <iostream>
#include <libcamera/color_space.h>
using namespace libcamera;
StreamKeyValueParser::StreamKeyValueParser()
{
addOption("role", OptionString,
"Role for the stream (viewfinder, video, still, raw)",
ArgumentRequired);
addOption("width", OptionInteger, "Width in pixels",
ArgumentRequired);
addOption("height", OptionInteger, "Height in pixels",
ArgumentRequired);
addOption("pixelformat", OptionString, "Pixel format name",
ArgumentRequired);
addOption("colorspace", OptionString, "Color space",
ArgumentRequired);
}
KeyValueParser::Options StreamKeyValueParser::parse(const char *arguments)
{
KeyValueParser::Options options = KeyValueParser::parse(arguments);
StreamRole role;
if (options.valid() && options.isSet("role") &&
!parseRole(&role, options)) {
std::cerr << "Unknown stream role "
<< options["role"].toString() << std::endl;
options.invalidate();
}
return options;
}
StreamRoles StreamKeyValueParser::roles(const OptionValue &values)
{
/* If no configuration values to examine default to viewfinder. */
if (values.empty())
return { StreamRole::Viewfinder };
const std::vector<OptionValue> &streamParameters = values.toArray();
StreamRoles roles;
for (auto const &value : streamParameters) {
StreamRole role;
/* If role is invalid or not set default to viewfinder. */
if (!parseRole(&role, value.toKeyValues()))
role = StreamRole::Viewfinder;
roles.push_back(role);
}
return roles;
}
int StreamKeyValueParser::updateConfiguration(CameraConfiguration *config,
const OptionValue &values)
{
if (!config) {
std::cerr << "No configuration provided" << std::endl;
return -EINVAL;
}
/* If no configuration values nothing to do. */
if (values.empty())
return 0;
const std::vector<OptionValue> &streamParameters = values.toArray();
if (config->size() != streamParameters.size()) {
std::cerr
<< "Number of streams in configuration "
<< config->size()
<< " does not match number of streams parsed "
<< streamParameters.size()
<< std::endl;
return -EINVAL;
}
unsigned int i = 0;
for (auto const &value : streamParameters) {
KeyValueParser::Options opts = value.toKeyValues();
StreamConfiguration &cfg = config->at(i++);
if (opts.isSet("width") && opts.isSet("height")) {
cfg.size.width = opts["width"];
cfg.size.height = opts["height"];
}
if (opts.isSet("pixelformat"))
cfg.pixelFormat = PixelFormat::fromString(opts["pixelformat"].toString());
if (opts.isSet("colorspace"))
cfg.colorSpace = ColorSpace::fromString(opts["colorspace"].toString());
}
return 0;
}
bool StreamKeyValueParser::parseRole(StreamRole *role,
const KeyValueParser::Options &options)
{
if (!options.isSet("role"))
return false;
std::string name = options["role"].toString();
if (name == "viewfinder") {
*role = StreamRole::Viewfinder;
return true;
} else if (name == "video") {
*role = StreamRole::VideoRecording;
return true;
} else if (name == "still") {
*role = StreamRole::StillCapture;
return true;
} else if (name == "raw") {
*role = StreamRole::Raw;
return true;
}
return false;
}
+28
View File
@@ -0,0 +1,28 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020, Raspberry Pi Ltd
*
* stream_options.h - Helper to parse options for streams
*/
#pragma once
#include <libcamera/camera.h>
#include "options.h"
class StreamKeyValueParser : public KeyValueParser
{
public:
StreamKeyValueParser();
KeyValueParser::Options parse(const char *arguments) override;
static libcamera::StreamRoles roles(const OptionValue &values);
static int updateConfiguration(libcamera::CameraConfiguration *config,
const OptionValue &values);
private:
static bool parseRole(libcamera::StreamRole *role,
const KeyValueParser::Options &options);
};
+128
View File
@@ -0,0 +1,128 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020, Google Inc.
* Copyright (C) 2021, Collabora Ltd.
*
* capture_test.cpp - Test camera capture
*/
#include <iostream>
#include <gtest/gtest.h>
#include "environment.h"
#include "simple_capture.h"
using namespace libcamera;
const std::vector<int> NUMREQUESTS = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
const std::vector<StreamRole> ROLES = { Raw, StillCapture, VideoRecording, Viewfinder };
class SingleStream : public testing::TestWithParam<std::tuple<StreamRole, int>>
{
public:
static std::string nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info);
protected:
void SetUp() override;
void TearDown() override;
std::shared_ptr<Camera> camera_;
};
/*
* We use gtest's SetUp() and TearDown() instead of constructor and destructor
* in order to be able to assert on them.
*/
void SingleStream::SetUp()
{
Environment *env = Environment::get();
camera_ = env->cm()->get(env->cameraId());
ASSERT_EQ(camera_->acquire(), 0);
}
void SingleStream::TearDown()
{
if (!camera_)
return;
camera_->release();
camera_.reset();
}
std::string SingleStream::nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info)
{
std::map<StreamRole, std::string> rolesMap = { { Raw, "Raw" },
{ StillCapture, "StillCapture" },
{ VideoRecording, "VideoRecording" },
{ Viewfinder, "Viewfinder" } };
std::string roleName = rolesMap[std::get<0>(info.param)];
std::string numRequestsName = std::to_string(std::get<1>(info.param));
return roleName + "_" + numRequestsName;
}
/*
* Test single capture cycles
*
* Makes sure the camera completes the exact number of requests queued. Example
* failure is a camera that completes less requests than the number of requests
* queued.
*/
TEST_P(SingleStream, Capture)
{
auto [role, numRequests] = GetParam();
SimpleCaptureBalanced capture(camera_);
capture.configure(role);
capture.capture(numRequests);
}
/*
* Test multiple start/stop cycles
*
* Makes sure the camera supports multiple start/stop cycles. Example failure is
* a camera that does not clean up correctly in its error path but is only
* tested by single-capture applications.
*/
TEST_P(SingleStream, CaptureStartStop)
{
auto [role, numRequests] = GetParam();
unsigned int numRepeats = 3;
SimpleCaptureBalanced capture(camera_);
capture.configure(role);
for (unsigned int starts = 0; starts < numRepeats; starts++)
capture.capture(numRequests);
}
/*
* Test unbalanced stop
*
* Makes sure the camera supports a stop with requests queued. Example failure
* is a camera that does not handle cancelation of buffers coming back from the
* video device while stopping.
*/
TEST_P(SingleStream, UnbalancedStop)
{
auto [role, numRequests] = GetParam();
SimpleCaptureUnbalanced capture(camera_);
capture.configure(role);
capture.capture(numRequests);
}
INSTANTIATE_TEST_SUITE_P(CaptureTests,
SingleStream,
testing::Combine(testing::ValuesIn(ROLES),
testing::ValuesIn(NUMREQUESTS)),
SingleStream::nameParameters);
+22
View File
@@ -0,0 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Collabora Ltd.
*
* environment.cpp - Common environment for tests
*/
#include "environment.h"
using namespace libcamera;
Environment *Environment::get()
{
static Environment instance;
return &instance;
}
void Environment::setup(CameraManager *cm, std::string cameraId)
{
cm_ = cm;
cameraId_ = cameraId;
}
+27
View File
@@ -0,0 +1,27 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2021, Collabora Ltd.
*
* environment.h - Common environment for tests
*/
#pragma once
#include <libcamera/libcamera.h>
class Environment
{
public:
static Environment *get();
void setup(libcamera::CameraManager *cm, std::string cameraId);
const std::string &cameraId() const { return cameraId_; }
libcamera::CameraManager *cm() const { return cm_; }
private:
Environment() = default;
std::string cameraId_;
libcamera::CameraManager *cm_;
};
+193
View File
@@ -0,0 +1,193 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020, Google Inc.
* Copyright (C) 2021, Collabora Ltd.
*
* main.cpp - lc-compliance - The libcamera compliance tool
*/
#include <iomanip>
#include <iostream>
#include <string.h>
#include <gtest/gtest.h>
#include <libcamera/libcamera.h>
#include "environment.h"
#include "../cam/options.h"
using namespace libcamera;
enum {
OptCamera = 'c',
OptList = 'l',
OptFilter = 'f',
OptHelp = 'h',
};
/*
* Make asserts act like exceptions, otherwise they only fail (or skip) the
* current function. From gtest documentation:
* https://google.github.io/googletest/advanced.html#asserting-on-subroutines-with-an-exception
*/
class ThrowListener : public testing::EmptyTestEventListener
{
void OnTestPartResult(const testing::TestPartResult &result) override
{
if (result.type() == testing::TestPartResult::kFatalFailure ||
result.type() == testing::TestPartResult::kSkip)
throw testing::AssertionException(result);
}
};
static void listCameras(CameraManager *cm)
{
for (const std::shared_ptr<Camera> &cam : cm->cameras())
std::cout << "- " << cam.get()->id() << std::endl;
}
static int initCamera(CameraManager *cm, OptionsParser::Options options)
{
std::shared_ptr<Camera> camera;
int ret = cm->start();
if (ret) {
std::cout << "Failed to start camera manager: "
<< strerror(-ret) << std::endl;
return ret;
}
if (!options.isSet(OptCamera)) {
std::cout << "No camera specified, available cameras:" << std::endl;
listCameras(cm);
return -ENODEV;
}
const std::string &cameraId = options[OptCamera];
camera = cm->get(cameraId);
if (!camera) {
std::cout << "Camera " << cameraId << " not found, available cameras:" << std::endl;
listCameras(cm);
return -ENODEV;
}
Environment::get()->setup(cm, cameraId);
std::cout << "Using camera " << cameraId << std::endl;
return 0;
}
static int initGtestParameters(char *arg0, OptionsParser::Options options)
{
const std::map<std::string, std::string> gtestFlags = { { "list", "--gtest_list_tests" },
{ "filter", "--gtest_filter" } };
int argc = 0;
std::string filterParam;
/*
* +2 to have space for both the 0th argument that is needed but not
* used and the null at the end.
*/
char **argv = new char *[(gtestFlags.size() + 2)];
if (!argv)
return -ENOMEM;
argv[0] = arg0;
argc++;
if (options.isSet(OptList)) {
argv[argc] = const_cast<char *>(gtestFlags.at("list").c_str());
argc++;
}
if (options.isSet(OptFilter)) {
/*
* The filter flag needs to be passed as a single parameter, in
* the format --gtest_filter=filterStr
*/
filterParam = gtestFlags.at("filter") + "=" +
static_cast<const std::string &>(options[OptFilter]);
argv[argc] = const_cast<char *>(filterParam.c_str());
argc++;
}
argv[argc] = nullptr;
::testing::InitGoogleTest(&argc, argv);
delete[] argv;
return 0;
}
static int initGtest(char *arg0, OptionsParser::Options options)
{
int ret = initGtestParameters(arg0, options);
if (ret)
return ret;
testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener);
return 0;
}
static int parseOptions(int argc, char **argv, OptionsParser::Options *options)
{
OptionsParser parser;
parser.addOption(OptCamera, OptionString,
"Specify which camera to operate on, by id", "camera",
ArgumentRequired, "camera");
parser.addOption(OptList, OptionNone, "List all tests and exit", "list");
parser.addOption(OptFilter, OptionString,
"Specify which tests to run", "filter",
ArgumentRequired, "filter");
parser.addOption(OptHelp, OptionNone, "Display this help message",
"help");
*options = parser.parse(argc, argv);
if (!options->valid())
return -EINVAL;
if (options->isSet(OptHelp)) {
parser.usage();
std::cerr << "Further options from Googletest can be passed as environment variables"
<< std::endl;
return -EINTR;
}
return 0;
}
int main(int argc, char **argv)
{
OptionsParser::Options options;
int ret = parseOptions(argc, argv, &options);
if (ret == -EINTR)
return EXIT_SUCCESS;
if (ret < 0)
return EXIT_FAILURE;
std::unique_ptr<CameraManager> cm = std::make_unique<CameraManager>();
/* No need to initialize the camera if we'll just list tests */
if (!options.isSet(OptList)) {
ret = initCamera(cm.get(), options);
if (ret)
return ret;
}
ret = initGtest(argv[0], options);
if (ret)
return ret;
ret = RUN_ALL_TESTS();
if (!options.isSet(OptList))
cm->stop();
return ret;
}
+31
View File
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: CC0-1.0
libevent = dependency('libevent_pthreads', required : get_option('lc-compliance'))
libgtest = dependency('gtest', required : get_option('lc-compliance'),
fallback : ['gtest', 'gtest_dep'])
if not (libevent.found() and libgtest.found())
lc_compliance_enabled = false
subdir_done()
endif
lc_compliance_enabled = true
lc_compliance_sources = files([
'../cam/event_loop.cpp',
'../cam/options.cpp',
'environment.cpp',
'main.cpp',
'simple_capture.cpp',
'capture_test.cpp',
])
lc_compliance = executable('lc-compliance', lc_compliance_sources,
cpp_args : [ '-fexceptions' ],
dependencies : [
libatomic,
libcamera_public,
libevent,
libgtest,
],
install : true)
+191
View File
@@ -0,0 +1,191 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020-2021, Google Inc.
*
* simple_capture.cpp - Simple capture helper
*/
#include <gtest/gtest.h>
#include "simple_capture.h"
using namespace libcamera;
SimpleCapture::SimpleCapture(std::shared_ptr<Camera> camera)
: loop_(nullptr), camera_(camera),
allocator_(std::make_unique<FrameBufferAllocator>(camera))
{
}
SimpleCapture::~SimpleCapture()
{
stop();
}
void SimpleCapture::configure(StreamRole role)
{
config_ = camera_->generateConfiguration({ role });
if (!config_) {
std::cout << "Role not supported by camera" << std::endl;
GTEST_SKIP();
}
if (config_->validate() != CameraConfiguration::Valid) {
config_.reset();
FAIL() << "Configuration not valid";
}
if (camera_->configure(config_.get())) {
config_.reset();
FAIL() << "Failed to configure camera";
}
}
void SimpleCapture::start()
{
Stream *stream = config_->at(0).stream();
int count = allocator_->allocate(stream);
ASSERT_GE(count, 0) << "Failed to allocate buffers";
EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected";
camera_->requestCompleted.connect(this, &SimpleCapture::requestComplete);
ASSERT_EQ(camera_->start(), 0) << "Failed to start camera";
}
void SimpleCapture::stop()
{
if (!config_ || !allocator_->allocated())
return;
camera_->stop();
camera_->requestCompleted.disconnect(this);
Stream *stream = config_->at(0).stream();
allocator_->free(stream);
}
/* SimpleCaptureBalanced */
SimpleCaptureBalanced::SimpleCaptureBalanced(std::shared_ptr<Camera> camera)
: SimpleCapture(camera)
{
}
void SimpleCaptureBalanced::capture(unsigned int numRequests)
{
start();
Stream *stream = config_->at(0).stream();
const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);
/* No point in testing less requests then the camera depth. */
if (buffers.size() > numRequests) {
std::cout << "Camera needs " + std::to_string(buffers.size())
+ " requests, can't test only "
+ std::to_string(numRequests) << std::endl;
GTEST_SKIP();
}
queueCount_ = 0;
captureCount_ = 0;
captureLimit_ = numRequests;
/* Queue the recommended number of reqeuests. */
std::vector<std::unique_ptr<libcamera::Request>> requests;
for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
std::unique_ptr<Request> request = camera_->createRequest();
ASSERT_TRUE(request) << "Can't create request";
ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request";
ASSERT_EQ(queueRequest(request.get()), 0) << "Failed to queue request";
requests.push_back(std::move(request));
}
/* Run capture session. */
loop_ = new EventLoop();
loop_->exec();
stop();
delete loop_;
ASSERT_EQ(captureCount_, captureLimit_);
}
int SimpleCaptureBalanced::queueRequest(Request *request)
{
queueCount_++;
if (queueCount_ > captureLimit_)
return 0;
return camera_->queueRequest(request);
}
void SimpleCaptureBalanced::requestComplete(Request *request)
{
captureCount_++;
if (captureCount_ >= captureLimit_) {
loop_->exit(0);
return;
}
request->reuse(Request::ReuseBuffers);
if (queueRequest(request))
loop_->exit(-EINVAL);
}
/* SimpleCaptureUnbalanced */
SimpleCaptureUnbalanced::SimpleCaptureUnbalanced(std::shared_ptr<Camera> camera)
: SimpleCapture(camera)
{
}
void SimpleCaptureUnbalanced::capture(unsigned int numRequests)
{
start();
Stream *stream = config_->at(0).stream();
const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);
captureCount_ = 0;
captureLimit_ = numRequests;
/* Queue the recommended number of reqeuests. */
std::vector<std::unique_ptr<libcamera::Request>> requests;
for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
std::unique_ptr<Request> request = camera_->createRequest();
ASSERT_TRUE(request) << "Can't create request";
ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request";
ASSERT_EQ(camera_->queueRequest(request.get()), 0) << "Failed to queue request";
requests.push_back(std::move(request));
}
/* Run capture session. */
loop_ = new EventLoop();
int status = loop_->exec();
stop();
delete loop_;
ASSERT_EQ(status, 0);
}
void SimpleCaptureUnbalanced::requestComplete(Request *request)
{
captureCount_++;
if (captureCount_ >= captureLimit_) {
loop_->exit(0);
return;
}
request->reuse(Request::ReuseBuffers);
if (camera_->queueRequest(request))
loop_->exit(-EINVAL);
}
+65
View File
@@ -0,0 +1,65 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020-2021, Google Inc.
*
* simple_capture.h - Simple capture helper
*/
#pragma once
#include <memory>
#include <libcamera/libcamera.h>
#include "../cam/event_loop.h"
class SimpleCapture
{
public:
void configure(libcamera::StreamRole role);
protected:
SimpleCapture(std::shared_ptr<libcamera::Camera> camera);
virtual ~SimpleCapture();
void start();
void stop();
virtual void requestComplete(libcamera::Request *request) = 0;
EventLoop *loop_;
std::shared_ptr<libcamera::Camera> camera_;
std::unique_ptr<libcamera::FrameBufferAllocator> allocator_;
std::unique_ptr<libcamera::CameraConfiguration> config_;
};
class SimpleCaptureBalanced : public SimpleCapture
{
public:
SimpleCaptureBalanced(std::shared_ptr<libcamera::Camera> camera);
void capture(unsigned int numRequests);
private:
int queueRequest(libcamera::Request *request);
void requestComplete(libcamera::Request *request) override;
unsigned int queueCount_;
unsigned int captureCount_;
unsigned int captureLimit_;
};
class SimpleCaptureUnbalanced : public SimpleCapture
{
public:
SimpleCaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera);
void capture(unsigned int numRequests);
private:
void requestComplete(libcamera::Request *request) override;
unsigned int captureCount_;
unsigned int captureLimit_;
};
+6
View File
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: CC0-1.0
subdir('lc-compliance')
subdir('cam')
subdir('qcam')
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 282 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-airplay"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon points="12 15 17 21 7 21 12 15"></polygon></svg>

After

Width:  |  Height:  |  Size: 362 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>

After

Width:  |  Height:  |  Size: 356 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>

After

Width:  |  Height:  |  Size: 416 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

After

Width:  |  Height:  |  Size: 424 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-center"><line x1="18" y1="10" x2="6" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="18" y1="18" x2="6" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 398 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 399 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 396 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-right"><line x1="21" y1="10" x2="7" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="7" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 397 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor"><circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path></svg>

After

Width:  |  Height:  |  Size: 345 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-aperture"><circle cx="12" cy="12" r="10"></circle><line x1="14.31" y1="8" x2="20.05" y2="17.94"></line><line x1="9.69" y1="8" x2="21.17" y2="8"></line><line x1="7.38" y1="12" x2="13.12" y2="2.06"></line><line x1="9.69" y1="16" x2="3.95" y2="6.06"></line><line x1="14.31" y1="16" x2="2.83" y2="16"></line><line x1="16.62" y1="12" x2="10.88" y2="21.94"></line></svg>

After

Width:  |  Height:  |  Size: 568 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 361 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="8 12 12 16 16 12"></polyline><line x1="12" y1="8" x2="12" y2="16"></line></svg>

After

Width:  |  Height:  |  Size: 360 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-left"><line x1="17" y1="7" x2="7" y2="17"></line><polyline points="17 17 7 17 7 7"></polyline></svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-right"><line x1="7" y1="7" x2="17" y2="17"></line><polyline points="17 7 17 17 7 17"></polyline></svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 313 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 8 8 12 12 16"></polyline><line x1="16" y1="12" x2="8" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 359 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>

After

Width:  |  Height:  |  Size: 312 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 16 16 12 12 8"></polyline><line x1="8" y1="12" x2="16" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 361 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>

After

Width:  |  Height:  |  Size: 314 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="16 12 12 8 8 12"></polyline><line x1="12" y1="16" x2="12" y2="8"></line></svg>

After

Width:  |  Height:  |  Size: 357 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-left"><line x1="17" y1="17" x2="7" y2="7"></line><polyline points="7 17 7 7 17 7"></polyline></svg>

After

Width:  |  Height:  |  Size: 312 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-right"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>

After

Width:  |  Height:  |  Size: 314 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 310 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-at-sign"><circle cx="12" cy="12" r="4"></circle><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path></svg>

After

Width:  |  Height:  |  Size: 322 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>

After

Width:  |  Height:  |  Size: 325 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>

After

Width:  |  Height:  |  Size: 355 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg>

After

Width:  |  Height:  |  Size: 353 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery-charging"><path d="M5 18H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.19M15 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.19"></path><line x1="23" y1="13" x2="23" y2="11"></line><polyline points="11 6 7 12 13 12 9 18"></polyline></svg>

After

Width:  |  Height:  |  Size: 427 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery"><rect x="1" y="6" width="18" height="12" rx="2" ry="2"></rect><line x1="23" y1="13" x2="23" y2="11"></line></svg>

After

Width:  |  Height:  |  Size: 326 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell-off"><path d="M13.73 21a2 2 0 0 1-3.46 0"></path><path d="M18.63 13A17.89 17.89 0 0 1 18 8"></path><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"></path><path d="M18 8a6 6 0 0 0-9.33-5"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>

After

Width:  |  Height:  |  Size: 460 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>

After

Width:  |  Height:  |  Size: 321 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bluetooth"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline></svg>

After

Width:  |  Height:  |  Size: 298 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bold"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path></svg>

After

Width:  |  Height:  |  Size: 327 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book-open"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>

After

Width:  |  Height:  |  Size: 339 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>

After

Width:  |  Height:  |  Size: 345 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bookmark"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>

After

Width:  |  Height:  |  Size: 287 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-box"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 462 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-briefcase"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>

After

Width:  |  Height:  |  Size: 410 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"></path></svg>

After

Width:  |  Height:  |  Size: 385 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>

After

Width:  |  Height:  |  Size: 356 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cast"><path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path><line x1="2" y1="20" x2="2.01" y2="20"></line></svg>

After

Width:  |  Height:  |  Size: 387 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>

After

Width:  |  Height:  |  Size: 328 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>

After

Width:  |  Height:  |  Size: 345 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 262 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 269 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>

After

Width:  |  Height:  |  Size: 270 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>

After

Width:  |  Height:  |  Size: 270 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>

After

Width:  |  Height:  |  Size: 268 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-left"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>

After

Width:  |  Height:  |  Size: 318 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>

After

Width:  |  Height:  |  Size: 318 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>

After

Width:  |  Height:  |  Size: 316 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chrome"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>

After

Width:  |  Height:  |  Size: 448 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-circle"><circle cx="12" cy="12" r="10"></circle></svg>

After

Width:  |  Height:  |  Size: 258 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clipboard"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>

After

Width:  |  Height:  |  Size: 371 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>

After

Width:  |  Height:  |  Size: 304 B

Some files were not shown because too many files have changed in this diff Show More