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>
@@ -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);
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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_();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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_);
|
||||
}
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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_);
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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_);
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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_);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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 |