diff --git a/apps/SimpleStreamer/main.cpp b/apps/SimpleStreamer/main.cpp index 6927204..c90b2be 100644 --- a/apps/SimpleStreamer/main.cpp +++ b/apps/SimpleStreamer/main.cpp @@ -254,9 +254,6 @@ struct Image image.data.resize(image.width * image.height * 4); glReadPixels(0, 0, image.width, image.height, GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)image.data.data()); - - deflect::ImageWrapper::swapYAxis(image.data.data(), image.width, - image.height, 4); return image; } @@ -292,6 +289,7 @@ bool send(const Image& image, const deflect::View view) : deflect::COMPRESSION_OFF; deflectImage.compressionQuality = deflectCompressionQuality; deflectImage.view = view; + deflectImage.rowOrder = deflect::RowOrder::bottom_up; return deflectStream->send(deflectImage).get(); } diff --git a/deflect/CMakeLists.txt b/deflect/CMakeLists.txt index 43b916f..8b79837 100644 --- a/deflect/CMakeLists.txt +++ b/deflect/CMakeLists.txt @@ -29,6 +29,7 @@ set(DEFLECT_HEADERS Socket.h SourceBuffer.h StreamPrivate.h + TaskBuilder.h ) set(DEFLECT_SOURCES @@ -48,6 +49,7 @@ set(DEFLECT_SOURCES Stream.cpp StreamPrivate.cpp StreamSendWorker.cpp + TaskBuilder.cpp ) set(DEFLECT_LINK_LIBRARIES PRIVATE Qt5::Concurrent Qt5::Core Qt5::Network) diff --git a/deflect/Frame.cpp b/deflect/Frame.cpp index bcfa60e..a50a3d0 100644 --- a/deflect/Frame.cpp +++ b/deflect/Frame.cpp @@ -45,9 +45,9 @@ QSize Frame::computeDimensions() const { QSize size(0, 0); - for (size_t i = 0; i < segments.size(); ++i) + for (const auto& segment : segments) { - const deflect::SegmentParameters& params = segments[i].parameters; + const auto& params = segment.parameters; size.setWidth(std::max(size.width(), (int)(params.width + params.x))); size.setHeight( std::max(size.height(), (int)(params.height + params.y))); @@ -55,4 +55,20 @@ QSize Frame::computeDimensions() const return size; } + +RowOrder Frame::determineRowOrder() const +{ + if (segments.empty()) + throw std::runtime_error("frame has no segements"); + + const auto frameRowOrder = segments[0].rowOrder; + + for (const auto& segment : segments) + { + if (segment.rowOrder != frameRowOrder) + throw std::runtime_error("frame has incoherent row orders"); + } + + return frameRowOrder; +} } diff --git a/deflect/Frame.h b/deflect/Frame.h index d8206e8..9d6ca22 100644 --- a/deflect/Frame.h +++ b/deflect/Frame.h @@ -63,6 +63,12 @@ class Frame /** Get the total dimensions of this frame. */ DEFLECT_API QSize computeDimensions() const; + + /** + * @return the row order of all frame segments + * @throws std::runtime_error if not all segments have the same RowOrder + */ + DEFLECT_API RowOrder determineRowOrder() const; }; } diff --git a/deflect/FrameDispatcher.cpp b/deflect/FrameDispatcher.cpp index f572161..35af8f6 100644 --- a/deflect/FrameDispatcher.cpp +++ b/deflect/FrameDispatcher.cpp @@ -63,12 +63,22 @@ class FrameDispatcher::Impl assert(!frame->segments.empty()); + if (frame->determineRowOrder() == RowOrder::bottom_up) + mirrorSegmentsPositionsVertically(*frame); + // receiver will request a new frame once this frame was consumed buffer.setAllowedToSend(false); return frame; } + void mirrorSegmentsPositionsVertically(Frame& frame) const + { + const auto height = frame.computeDimensions().height(); + for (auto& s : frame.segments) + s.parameters.y = height - s.parameters.y - s.parameters.height; + } + typedef std::map StreamBuffers; StreamBuffers streamBuffers; std::map observers; @@ -143,17 +153,13 @@ void FrameDispatcher::processFrameFinished(const QString uri, try { buffer.finishFrameForSource(sourceIndex); + if (buffer.isAllowedToSend() && buffer.hasCompleteFrame()) + emit sendFrame(_impl->consumeLatestFrame(uri)); } catch (const std::runtime_error& e) { - std::cerr << "processFrameFinished got exception, closing stream: " - << e.what() << std::endl; - emit bufferSizeExceeded(uri); - return; + emit pixelStreamException(uri, e.what()); } - - if (buffer.isAllowedToSend() && buffer.hasCompleteFrame()) - emit sendFrame(_impl->consumeLatestFrame(uri)); } void FrameDispatcher::requestFrame(const QString uri) @@ -163,8 +169,15 @@ void FrameDispatcher::requestFrame(const QString uri) ReceiveBuffer& buffer = _impl->streamBuffers[uri]; buffer.setAllowedToSend(true); - if (buffer.hasCompleteFrame()) - emit sendFrame(_impl->consumeLatestFrame(uri)); + try + { + if (buffer.hasCompleteFrame()) + emit sendFrame(_impl->consumeLatestFrame(uri)); + } + catch (const std::runtime_error& e) + { + emit pixelStreamException(uri, e.what()); + } } void FrameDispatcher::deleteStream(const QString uri) diff --git a/deflect/FrameDispatcher.h b/deflect/FrameDispatcher.h index 836c1ea..67266f9 100644 --- a/deflect/FrameDispatcher.h +++ b/deflect/FrameDispatcher.h @@ -58,7 +58,7 @@ class FrameDispatcher : public QObject public: /** Construct a dispatcher */ - FrameDispatcher(QObject* parent); + FrameDispatcher(QObject* parent = nullptr); /** Destructor. */ ~FrameDispatcher(); @@ -142,6 +142,14 @@ public slots: */ void pixelStreamOpened(QString uri); + /** + * Notify that an exception occured and the stream should be closed. + * + * @param uri Identifier for the stream + * @param what The description of the exception that occured + */ + void pixelStreamException(QString uri, QString what); + /** * Notify that a pixel stream has been closed. * @@ -156,13 +164,6 @@ public slots: */ void sendFrame(deflect::FramePtr frame); - /** - * Notify that a pixel stream has exceeded its maximum allowed size. - * - * @param uri Identifier for the stream - */ - void bufferSizeExceeded(QString uri); - private: class Impl; std::unique_ptr _impl; diff --git a/deflect/ImageSegmenter.cpp b/deflect/ImageSegmenter.cpp index 1beef69..08637ad 100644 --- a/deflect/ImageSegmenter.cpp +++ b/deflect/ImageSegmenter.cpp @@ -62,7 +62,7 @@ bool _isOnRightSideOfSideBySideImage(const Segment& segment) } } -bool ImageSegmenter::generate(const ImageWrapper& image, const Handler& handler) +bool ImageSegmenter::generate(const ImageWrapper& image, Handler handler) { if (image.compressionPolicy == COMPRESSION_ON) return _generateJpeg(image, handler); @@ -240,6 +240,7 @@ Segments ImageSegmenter::_generateSegments(const ImageWrapper& image) const segment.view = image.view == View::side_by_side ? View::left_eye : image.view; segment.sourceImage = ℑ + segment.rowOrder = image.rowOrder; segments.push_back(segment); } diff --git a/deflect/ImageSegmenter.h b/deflect/ImageSegmenter.h index c9b6a2d..fc9525e 100644 --- a/deflect/ImageSegmenter.h +++ b/deflect/ImageSegmenter.h @@ -75,8 +75,7 @@ class ImageSegmenter * @return true if all image handlers returned true, false on failure * @see setNominalSegmentDimensions() */ - DEFLECT_API bool generate(const ImageWrapper& image, - const Handler& handler); + DEFLECT_API bool generate(const ImageWrapper& image, Handler handler); /** * Set the nominal segment dimensions. diff --git a/deflect/ImageWrapper.h b/deflect/ImageWrapper.h index 470de9e..27720bb 100644 --- a/deflect/ImageWrapper.h +++ b/deflect/ImageWrapper.h @@ -137,6 +137,22 @@ struct ImageWrapper */ View view = View::mono; + /** + * The order of the image's data rows in memory. + * + * Set this value to bottom_up if the image data is stored following the + * OpenGL convention. + * + * All images that form a frame (possibly from multiple Streams) must have + * the same row order, otherwise the frame is invalid. This is because the + * frame's segments need to be reordered in addtion to flipping them + * individually. + * + * This is an alternative to calling swapYAxis() before sending. + * @version 1.7 + */ + RowOrder rowOrder = RowOrder::top_down; + /** * Get the number of bytes per pixel based on the pixelFormat. * @version 1.0 diff --git a/deflect/MessageHeader.h b/deflect/MessageHeader.h index 3adac37..090cd6c 100644 --- a/deflect/MessageHeader.h +++ b/deflect/MessageHeader.h @@ -68,7 +68,8 @@ enum MessageType MESSAGE_TYPE_SIZE_HINTS = 13, MESSAGE_TYPE_DATA = 14, MESSAGE_TYPE_IMAGE_VIEW = 15, - MESSAGE_TYPE_OBSERVER_OPEN = 16 + MESSAGE_TYPE_OBSERVER_OPEN = 16, + MESSAGE_TYPE_IMAGE_ROW_ORDER = 17 }; #define MESSAGE_HEADER_URI_LENGTH 64 diff --git a/deflect/Observer.cpp b/deflect/Observer.cpp index aa4e85c..eb7f1b3 100644 --- a/deflect/Observer.cpp +++ b/deflect/Observer.cpp @@ -100,7 +100,7 @@ bool Observer::registerForEvents(const bool exclusive) return true; // Send the bind message - if (!_impl->sendWorker.enqueueBindRequest(exclusive).get()) + if (!_impl->bindEvents(exclusive).get()) { std::cerr << "deflect::Stream::registerForEvents: sending bind message " << "failed" << std::endl; @@ -175,13 +175,11 @@ void Observer::setDisconnectedCallback(const std::function callback) void Observer::sendSizeHints(const SizeHints& hints) { - _impl->sendWorker.enqueueSizeHints(hints); + _impl->send(hints); } bool Observer::sendData(const char* data, const size_t count) { - return _impl->sendWorker - .enqueueData(QByteArray::fromRawData(data, int(count))) - .get(); + return _impl->send(QByteArray::fromRawData(data, int(count))).get(); } } diff --git a/deflect/Segment.h b/deflect/Segment.h index 6ff1791..ae409d0 100644 --- a/deflect/Segment.h +++ b/deflect/Segment.h @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2016, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -61,6 +61,8 @@ struct Segment /** Image data of the segment. */ QByteArray imageData; + RowOrder rowOrder = RowOrder::top_down; //!< imageData row order + /** @internal raw, uncompressed source image, used for compression */ const ImageWrapper* sourceImage = nullptr; diff --git a/deflect/Server.cpp b/deflect/Server.cpp index 016d988..2a1630e 100644 --- a/deflect/Server.cpp +++ b/deflect/Server.cpp @@ -83,8 +83,11 @@ Server::Server(const int port) &Server::pixelStreamClosed); connect(_impl->frameDispatcher, &FrameDispatcher::sendFrame, this, &Server::receivedFrame); - connect(_impl->frameDispatcher, &FrameDispatcher::bufferSizeExceeded, this, - &Server::closePixelStream); + connect(_impl->frameDispatcher, &FrameDispatcher::pixelStreamException, + [this](const QString uri, const QString what) { + emit pixelStreamException(uri, what); + closePixelStream(uri); + }); } Server::~Server() diff --git a/deflect/Server.h b/deflect/Server.h index 777112c..960a94e 100644 --- a/deflect/Server.h +++ b/deflect/Server.h @@ -105,6 +105,16 @@ public slots: */ void pixelStreamOpened(QString uri); + /** + * Notify that a stream has encountered an exception and will be closed. + * + * Used for error reporting. + * + * @param uri Identifier for the stream + * @param what The error message + */ + void pixelStreamException(QString uri, QString what); + /** * Notify that a pixel stream has been closed. * diff --git a/deflect/ServerWorker.cpp b/deflect/ServerWorker.cpp index 1e97b21..ee4e3ab 100644 --- a/deflect/ServerWorker.cpp +++ b/deflect/ServerWorker.cpp @@ -61,6 +61,7 @@ ServerWorker::ServerWorker(const int socketDescriptor) , _clientProtocolVersion{NETWORK_PROTOCOL_VERSION} , _registeredToEvents{false} , _activeView{View::mono} + , _activeRowOrder{RowOrder::top_down} { if (!_tcpSocket->setSocketDescriptor(socketDescriptor)) { @@ -266,6 +267,14 @@ void ServerWorker::_handleMessage(const MessageHeader& messageHeader, break; } + case MESSAGE_TYPE_IMAGE_ROW_ORDER: + { + const auto order = reinterpret_cast(byteArray.data()); + if (*order >= RowOrder::top_down && *order <= RowOrder::bottom_up) + _activeRowOrder = *order; + break; + } + case MESSAGE_TYPE_BIND_EVENTS: case MESSAGE_TYPE_BIND_EVENTS_EX: if (_registeredToEvents) @@ -311,6 +320,8 @@ void ServerWorker::_handlePixelStreamMessage(const QByteArray& message) segment.imageData = message.right(message.size() - sizeof(SegmentParameters)); segment.view = _activeView; + segment.rowOrder = _activeRowOrder; + emit(receivedSegment(_streamId, _sourceId, segment)); } diff --git a/deflect/ServerWorker.h b/deflect/ServerWorker.h index 5b7627a..2f9a86b 100644 --- a/deflect/ServerWorker.h +++ b/deflect/ServerWorker.h @@ -106,6 +106,7 @@ private slots: QQueue _events; View _activeView; + RowOrder _activeRowOrder; void _receiveMessage(); MessageHeader _receiveMessageHeader(); diff --git a/deflect/Stream.cpp b/deflect/Stream.cpp index b533b56..2a8761e 100644 --- a/deflect/Stream.cpp +++ b/deflect/Stream.cpp @@ -61,16 +61,16 @@ Stream::~Stream() Stream::Future Stream::send(const ImageWrapper& image) { - return _impl->sendWorker.enqueueImage(image, false); + return _impl->sendImage(image, false); } Stream::Future Stream::finishFrame() { - return _impl->sendWorker.enqueueFinish(); + return _impl->sendFinishFrame(); } Stream::Future Stream::sendAndFinish(const ImageWrapper& image) { - return _impl->sendWorker.enqueueImage(image, true); + return _impl->sendImage(image, true); } } diff --git a/deflect/StreamPrivate.cpp b/deflect/StreamPrivate.cpp index 84142dd..6e4029c 100644 --- a/deflect/StreamPrivate.cpp +++ b/deflect/StreamPrivate.cpp @@ -43,6 +43,7 @@ #include +#include #include namespace @@ -50,6 +51,9 @@ namespace const char* STREAM_ID_ENV_VAR = "DEFLECT_ID"; const char* STREAM_HOST_ENV_VAR = "DEFLECT_HOST"; +const unsigned int SEGMENT_SIZE = 512; +const unsigned int SMALL_IMAGE_SIZE = 64; + std::string _getStreamHost(const std::string& host) { if (!host.empty()) @@ -79,12 +83,44 @@ std::string _getStreamId(const std::string& id) namespace deflect { +namespace +{ +void _checkParameters(const ImageWrapper& image) +{ + if (image.compressionPolicy != COMPRESSION_ON && image.pixelFormat != RGBA) + { + throw std::invalid_argument( + "Currently, RAW images can only be sent in RGBA format. Other " + "formats support remain to be implemented."); + } + + if (image.compressionPolicy == COMPRESSION_ON) + { + if (image.compressionQuality < 1 || image.compressionQuality > 100) + { + std::stringstream msg; + msg << "JPEG compression quality must be between 1 and 100, got " + << image.compressionQuality << std::endl; + throw std::invalid_argument(msg.str()); + } + } +} + +bool _canSendAsSingleSegment(const ImageWrapper& image) +{ + return image.width <= SMALL_IMAGE_SIZE && image.height <= SMALL_IMAGE_SIZE; +} +} + StreamPrivate::StreamPrivate(const std::string& id_, const std::string& host, const unsigned short port, const bool observer) : id{_getStreamId(id_)} , socket{_getStreamHost(host), port} , sendWorker{socket, id} + , task{&sendWorker} { + _imageSegmenter.setNominalSegmentDimensions(SEGMENT_SIZE, SEGMENT_SIZE); + socket.connect(&socket, &Socket::disconnected, [this]() { if (disconnectedCallback) disconnectedCallback(); @@ -94,14 +130,65 @@ StreamPrivate::StreamPrivate(const std::string& id_, const std::string& host, sendWorker.start(); if (observer) - sendWorker.enqueueObserverOpen().wait(); + sendWorker.enqueueRequest(task.openObserver()).wait(); else - sendWorker.enqueueOpen().wait(); + sendWorker.enqueueRequest(task.openStream()).wait(); } StreamPrivate::~StreamPrivate() { if (socket.isConnected()) - sendWorker.enqueueClose().wait(); + sendWorker.enqueueRequest(task.close()).wait(); +} + +Stream::Future StreamPrivate::bindEvents(const bool exclusive) +{ + return sendWorker.enqueueRequest(task.bindEvents(exclusive)); +} + +Stream::Future StreamPrivate::send(const SizeHints& hints) +{ + return sendWorker.enqueueRequest(task.send(hints)); +} + +Stream::Future StreamPrivate::send(QByteArray&& data) +{ + return sendWorker.enqueueRequest(task.send(std::move(data))); +} + +Stream::Future StreamPrivate::sendImage(const ImageWrapper& image, + const bool finish) +{ + try + { + if (!sendWorker.canAcceptNewImageSend()) + throw std::runtime_error("Pending finish, no send allowed"); + + _checkParameters(image); + + if (_canSendAsSingleSegment(image)) + { + // OPT for OSPRay-KNL with external thread pool - compress directly + // in caller thread. + auto segment = _imageSegmenter.createSingleSegment(image); + // As we expect to encounter a lot of these small sends, be + // optimistic and fulfill the promise already to reduce load in the + // send thread (c.f. lock ops performance on KNL). + sendWorker.enqueueFastRequest(task.send(std::move(segment))); + return finish ? sendFinishFrame() : make_ready_future(true); + } + + return sendWorker.enqueueRequest( + task.sendUsingMTCompression(image, _imageSegmenter, finish)); + } + catch (...) + { + return make_exception_future(std::current_exception()); + } +} + +Stream::Future StreamPrivate::sendFinishFrame() +{ + return sendWorker.enqueueRequest(task.finishFrame(), true); } } diff --git a/deflect/StreamPrivate.h b/deflect/StreamPrivate.h index b12b8bd..3e0b646 100644 --- a/deflect/StreamPrivate.h +++ b/deflect/StreamPrivate.h @@ -42,8 +42,10 @@ #ifndef DEFLECT_STREAMPRIVATE_H #define DEFLECT_STREAMPRIVATE_H +#include "ImageSegmenter.h" // member #include "Socket.h" // member #include "StreamSendWorker.h" // member +#include "TaskBuilder.h" // member #include #include @@ -82,6 +84,20 @@ class StreamPrivate /** The worker doing all the socket send operations. */ StreamSendWorker sendWorker; + + /** Prepare tasks for the sendWorker. */ + TaskBuilder task; + + /** The segmenter for doing multithreaded image segmentation + send. */ + ImageSegmenter _imageSegmenter; + + Stream::Future bindEvents(bool exclusive); + Stream::Future send(const SizeHints& hints); + Stream::Future send(QByteArray&& data); + Stream::Future sendImage(const ImageWrapper& image, bool finish); + Stream::Future sendSingleSegment(Segment&& segment, RowOrder orientation, + bool finish); + Stream::Future sendFinishFrame(); }; } #endif diff --git a/deflect/StreamSendWorker.cpp b/deflect/StreamSendWorker.cpp index 7fbd95a..14ea127 100644 --- a/deflect/StreamSendWorker.cpp +++ b/deflect/StreamSendWorker.cpp @@ -45,13 +45,6 @@ #include "SizeHints.h" #include -#include - -namespace -{ -const unsigned int SEGMENT_SIZE = 512; -const unsigned int SMALL_IMAGE_SIZE = 64; -} namespace deflect { @@ -60,7 +53,6 @@ StreamSendWorker::StreamSendWorker(Socket& socket, const std::string& id) , _id(id) , _dequeuedRequests(std::thread::hardware_concurrency() / 2) { - _imageSegmenter.setNominalSegmentDimensions(SEGMENT_SIZE, SEGMENT_SIZE); } StreamSendWorker::~StreamSendWorker() @@ -68,6 +60,24 @@ StreamSendWorker::~StreamSendWorker() stop(); } +void StreamSendWorker::stop() +{ + { + _running = false; + enqueueRequest(std::vector()); + } + + quit(); + wait(); + + Request request; + while (_requests.try_dequeue(request)) + { + if (request.promise) + request.promise->set_value(false); + } +} + void StreamSendWorker::run() { _running = true; @@ -101,7 +111,6 @@ void StreamSendWorker::run() for (size_t i = 0; i < count; ++i) { - bool success = true; auto& request = _dequeuedRequests[i]; // postpone a finish request to maintain order (as the lockfree @@ -123,6 +132,7 @@ void StreamSendWorker::run() try { + bool success = true; for (auto& task : request.tasks) { if (!task()) @@ -144,176 +154,105 @@ void StreamSendWorker::run() } } -void StreamSendWorker::stop() +Stream::Future StreamSendWorker::enqueueRequest(Task&& action, bool isFinish) { - { - _running = false; - _enqueueRequest(std::vector()); - } - - quit(); - wait(); - - Request request; - while (_requests.try_dequeue(request)) - { - if (request.promise) - request.promise->set_value(false); - } + return enqueueRequest(std::vector{std::move(action)}, isFinish); } -Stream::Future StreamSendWorker::enqueueImage(const ImageWrapper& image, - const bool finish) +Stream::Future StreamSendWorker::enqueueRequest(std::vector&& tasks, + const bool isFinish) { - if (_pendingFinish) - { - return make_exception_future( - std::runtime_error("Pending finish, no send allowed")); - } - - if (image.compressionPolicy != COMPRESSION_ON && image.pixelFormat != RGBA) - { - return make_exception_future(std::invalid_argument( - "Currently, RAW images can only be sent in RGBA format. Other " - "formats support remain to be implemented.")); - } - - if (image.compressionPolicy == COMPRESSION_ON) - { - if (image.compressionQuality < 1 || image.compressionQuality > 100) - { - std::stringstream msg; - msg << "JPEG compression quality must be between 1 and 100, got " - << image.compressionQuality << std::endl; - return make_exception_future( - std::invalid_argument(msg.str())); - } - } - - std::vector tasks; - - if (image.width <= SMALL_IMAGE_SIZE && image.height <= SMALL_IMAGE_SIZE) - { - try - { - auto segment = _imageSegmenter.createSingleSegment(image); - - tasks.emplace_back( - [this, segment] { return _sendSegment(segment); }); - - // as we expect to encounter a lot of these small sends, be - // optimistic and fulfill the promise already to reduce load in the - // send thread (c.f. lock ops performance on KNL) - _requests.enqueue({nullptr, tasks, false}); - if (finish) - return enqueueFinish(); - return make_ready_future(true); - } - catch (...) - { - return make_exception_future(std::current_exception()); - } - } - else - tasks.emplace_back([this, image] { return _sendImage(image); }); - - if (finish) - tasks.emplace_back([this] { return _sendFinish(); }); - - return _enqueueRequest(std::move(tasks)); + auto promise = std::make_shared(); + auto future = promise->get_future(); + _requests.enqueue({std::move(promise), std::move(tasks), isFinish}); + return future; } -Stream::Future StreamSendWorker::enqueueFinish() +void StreamSendWorker::enqueueFastRequest(Task&& task) { - return _enqueueRequest({[this] { return _sendFinish(); }}, true); + _requests.enqueue({nullptr, std::vector{std::move(task)}, false}); } -Stream::Future StreamSendWorker::enqueueOpen() +bool StreamSendWorker::_sendOpenObserver() { - return _enqueueRequest({[this] { - return _send(MESSAGE_TYPE_PIXELSTREAM_OPEN, - QByteArray::number(NETWORK_PROTOCOL_VERSION)); - }}); + return _send(MESSAGE_TYPE_OBSERVER_OPEN, + QByteArray::number(NETWORK_PROTOCOL_VERSION)); } -Stream::Future StreamSendWorker::enqueueClose() +bool StreamSendWorker::_sendOpenStream() { - return _enqueueRequest({[this] { return _send(MESSAGE_TYPE_QUIT, {}); }}); + return _send(MESSAGE_TYPE_PIXELSTREAM_OPEN, + QByteArray::number(NETWORK_PROTOCOL_VERSION)); } -Stream::Future StreamSendWorker::enqueueObserverOpen() +bool StreamSendWorker::_sendClose() { - return _enqueueRequest({[this] { - return _send(MESSAGE_TYPE_OBSERVER_OPEN, - QByteArray::number(NETWORK_PROTOCOL_VERSION)); - }}); + return _send(MESSAGE_TYPE_QUIT, {}); } -Stream::Future StreamSendWorker::enqueueBindRequest(const bool exclusive) +bool StreamSendWorker::_sendSegment(const Segment& segment) { - return _enqueueRequest({[this, exclusive] { - return _send(exclusive ? MESSAGE_TYPE_BIND_EVENTS_EX - : MESSAGE_TYPE_BIND_EVENTS, - {}); - }}); + if (segment.exception) + std::rethrow_exception(segment.exception); + + if (segment.view != _currentView) + { + if (!_sendImageView(segment.view)) + return false; + _currentView = segment.view; + } + _sendRowOrderIfChanged(segment.rowOrder); + + auto message = QByteArray{(const char*)(&segment.parameters), + sizeof(SegmentParameters)}; + message.append(segment.imageData); + return _send(MESSAGE_TYPE_PIXELSTREAM, message, false); } -Stream::Future StreamSendWorker::enqueueSizeHints(const SizeHints& hints) +bool StreamSendWorker::_sendImageView(const View view) { - return _enqueueRequest({[this, hints] { - return _send(MESSAGE_TYPE_SIZE_HINTS, - QByteArray{(const char*)(&hints), sizeof(SizeHints)}); - }}); + return _send(MESSAGE_TYPE_IMAGE_VIEW, + QByteArray{(const char*)(&view), sizeof(View)}); } -Stream::Future StreamSendWorker::enqueueData(const QByteArray data) +bool StreamSendWorker::_sendRowOrderIfChanged(const RowOrder rowOrder) { - return _enqueueRequest( - {[this, data] { return _send(MESSAGE_TYPE_DATA, data); }}); + if (rowOrder != _currentRowOrder) + { + if (!_sendImageRowOrder(rowOrder)) + return false; + _currentRowOrder = rowOrder; + } + return true; } -Stream::Future StreamSendWorker::_enqueueRequest(std::vector&& tasks, - const bool isFinish) +bool StreamSendWorker::_sendImageRowOrder(const RowOrder rowOrder) { - PromisePtr promise(new Promise); - _requests.enqueue({promise, tasks, isFinish}); - return promise->get_future(); + return _send(MESSAGE_TYPE_IMAGE_ROW_ORDER, + QByteArray{(const char*)(&rowOrder), sizeof(RowOrder)}); } -bool StreamSendWorker::_sendImage(const ImageWrapper& image) +bool StreamSendWorker::_sendFinish() { - const auto sendFunc = - std::bind(&StreamSendWorker::_sendSegment, this, std::placeholders::_1); - return _imageSegmenter.generate(image, sendFunc); + return _send(MESSAGE_TYPE_PIXELSTREAM_FINISH_FRAME, {}); } -bool StreamSendWorker::_sendImageView(const View view) +bool StreamSendWorker::_sendData(const QByteArray data) { - return _send(MESSAGE_TYPE_IMAGE_VIEW, - QByteArray{(const char*)(&view), sizeof(View)}); + return _send(MESSAGE_TYPE_DATA, data); } -bool StreamSendWorker::_sendSegment(const Segment& segment) +bool StreamSendWorker::_sendSizeHints(const SizeHints& hints) { - if (segment.exception) - std::rethrow_exception(segment.exception); - - if (segment.view != _currentView) - { - if (!_sendImageView(segment.view)) - return false; - _currentView = segment.view; - } - - auto message = QByteArray{(const char*)(&segment.parameters), - sizeof(SegmentParameters)}; - message.append(segment.imageData); - return _send(MESSAGE_TYPE_PIXELSTREAM, message, false); + return _send(MESSAGE_TYPE_SIZE_HINTS, + QByteArray{(const char*)(&hints), sizeof(SizeHints)}); } -bool StreamSendWorker::_sendFinish() +bool StreamSendWorker::_sendBindEvents(const bool exclusive) { - return _send(MESSAGE_TYPE_PIXELSTREAM_FINISH_FRAME, {}); + return _send(exclusive ? MESSAGE_TYPE_BIND_EVENTS_EX + : MESSAGE_TYPE_BIND_EVENTS, + {}); } bool StreamSendWorker::_send(const MessageType type, const QByteArray& message, diff --git a/deflect/StreamSendWorker.h b/deflect/StreamSendWorker.h index a56ac30..825237f 100644 --- a/deflect/StreamSendWorker.h +++ b/deflect/StreamSendWorker.h @@ -41,10 +41,9 @@ #ifndef DEFLECT_STREAMSENDWORKER_H #define DEFLECT_STREAMSENDWORKER_H -#include "ImageSegmenter.h" // member -#include "MessageHeader.h" // MessageType -#include "Socket.h" // member -#include "Stream.h" // Stream::Future +#include "MessageHeader.h" // MessageType +#include "Socket.h" // member +#include "Stream.h" // Stream::Future #ifdef __GNUC__ #pragma GCC diagnostic push @@ -59,6 +58,8 @@ namespace deflect { +using Task = std::function; + /** * Worker thread class that sends images and messages through a Socket. * @@ -77,29 +78,21 @@ class StreamSendWorker : public QThread /** Stop and destroy the worker. */ ~StreamSendWorker(); - /** Stop the worker and clear any pending send tasks. */ - void stop(); - - /** Enqueue an image to be send during the execution of run(). */ - Stream::Future enqueueImage(const ImageWrapper& image, bool finish); - Stream::Future enqueueFinish(); //!< Enqueue a finishFrame() - Stream::Future enqueueOpen(); //!< Enqueue an open message - Stream::Future enqueueClose(); //!< Enqueue a close message - Stream::Future enqueueObserverOpen(); //!< Enqueue an observer open message + /** Enqueue a request to be send during the execution of run(). */ + Stream::Future enqueueRequest(Task&& action, bool isFinish = false); - /** @sa Stream::registerForEvents */ - Stream::Future enqueueBindRequest(bool exclusive); + /** Enqueue a request to be send during the execution of run(). */ + Stream::Future enqueueRequest(std::vector&& actions, + bool isFinish = false); - /** @sa Stream::sendSizeHints */ - Stream::Future enqueueSizeHints(const SizeHints& hints); - - /** @sa Stream::sendData */ - Stream::Future enqueueData(QByteArray data); + /** Enqueue a request with no future to check for its completion. */ + void enqueueFastRequest(Task&& task); + /** @return true if a finsh frame operation is pending. */ + bool canAcceptNewImageSend() const { return !_pendingFinish; } private: using Promise = std::promise; using PromisePtr = std::shared_ptr; - using Task = std::function; struct Request { @@ -111,26 +104,36 @@ class StreamSendWorker : public QThread Socket& _socket; const std::string& _id; - ImageSegmenter _imageSegmenter; moodycamel::BlockingConcurrentQueue _requests; bool _running = false; View _currentView = View::mono; + RowOrder _currentRowOrder = RowOrder::top_down; std::vector _dequeuedRequests; bool _pendingFinish = false; Request _finishRequest; + /** Stop the worker and clear any pending send tasks. */ + void stop(); + /** Main QThread loop doing asynchronous processing of queued tasks. */ void run() final; - Stream::Future _enqueueRequest(std::vector&& actions, - bool isFinish = false); - friend class deflect::test::Application; // to send pre-compressed segments - bool _sendImage(const ImageWrapper& image); - bool _sendImageView(View view); + friend class TaskBuilder; + + bool _sendOpenObserver(); + bool _sendOpenStream(); + bool _sendClose(); bool _sendSegment(const Segment& segment); + bool _sendImageView(View view); + bool _sendRowOrderIfChanged(RowOrder rowOrder); + bool _sendImageRowOrder(RowOrder rowOrder); bool _sendFinish(); + bool _sendData(const QByteArray data); + bool _sendSizeHints(const SizeHints& hints); + bool _sendBindEvents(const bool exclusive); + bool _send(MessageType type, const QByteArray& message, bool waitForBytesWritten = true); }; diff --git a/deflect/TaskBuilder.cpp b/deflect/TaskBuilder.cpp new file mode 100644 index 0000000..137cc15 --- /dev/null +++ b/deflect/TaskBuilder.cpp @@ -0,0 +1,112 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of Ecole polytechnique federale de Lausanne. */ +/*********************************************************************/ + +#include "TaskBuilder.h" + +#include "ImageSegmenter.h" +#include "SizeHints.h" + +namespace deflect +{ +TaskBuilder::TaskBuilder(StreamSendWorker* worker) + : _worker{worker} +{ +} + +Task TaskBuilder::openStream() +{ + return std::bind(&StreamSendWorker::_sendOpenStream, _worker); +} + +Task TaskBuilder::close() +{ + return std::bind(&StreamSendWorker::_sendClose, _worker); +} + +Task TaskBuilder::openObserver() +{ + return std::bind(&StreamSendWorker::_sendOpenObserver, _worker); +} + +Task TaskBuilder::bindEvents(const bool exclusive) +{ + return std::bind(&StreamSendWorker::_sendBindEvents, _worker, exclusive); +} + +Task TaskBuilder::send(const SizeHints& hints) +{ + return std::bind(&StreamSendWorker::_sendSizeHints, _worker, hints); +} + +Task TaskBuilder::send(const QByteArray& data) +{ + return std::bind(&StreamSendWorker::_sendData, _worker, data); +} + +std::vector TaskBuilder::sendUsingMTCompression( + const ImageWrapper& image, ImageSegmenter& imageSegmenter, + const bool finish) +{ + std::vector tasks; + tasks.emplace_back(send(image, imageSegmenter)); + if (finish) + tasks.emplace_back(finishFrame()); + return tasks; +} + +Task TaskBuilder::finishFrame() +{ + return std::bind(&StreamSendWorker::_sendFinish, _worker); +} + +Task TaskBuilder::send(Segment&& segment) +{ + return std::bind(&StreamSendWorker::_sendSegment, _worker, segment); +} + +Task TaskBuilder::send(const ImageWrapper& image, + ImageSegmenter& imageSegmenter) +{ + auto sendFunc = std::bind(&StreamSendWorker::_sendSegment, _worker, + std::placeholders::_1); + return [&imageSegmenter, image, sendFunc]() { + return imageSegmenter.generate(image, sendFunc); + }; +} +} diff --git a/deflect/TaskBuilder.h b/deflect/TaskBuilder.h new file mode 100644 index 0000000..cb2249f --- /dev/null +++ b/deflect/TaskBuilder.h @@ -0,0 +1,76 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of Ecole polytechnique federale de Lausanne. */ +/*********************************************************************/ + +#ifndef DEFLECT_TASKBUILDER_H +#define DEFLECT_TASKBUILDER_H + +#include "StreamSendWorker.h" +#include "types.h" + +namespace deflect +{ +/** + * Create tasks to be executed asynchrounously by the StreamSendWorker. + */ +class TaskBuilder +{ +public: + explicit TaskBuilder(StreamSendWorker* worker); + + Task openStream(); + Task openObserver(); + Task bindEvents(bool exclusive); + Task close(); + + Task send(const SizeHints& hints); + Task send(const QByteArray& data); + Task send(Segment&& segment); + std::vector sendUsingMTCompression(const ImageWrapper& image, + ImageSegmenter& imageSegmenter, + bool finish); + Task finishFrame(); + +private: + StreamSendWorker* _worker = nullptr; + + Task send(const ImageWrapper& image, ImageSegmenter& imageSegmenter); +}; +} + +#endif diff --git a/deflect/types.h b/deflect/types.h index ee316b9..53d6c2d 100644 --- a/deflect/types.h +++ b/deflect/types.h @@ -66,6 +66,13 @@ enum class ChromaSubsampling YUV420 /**< 50% vertical + horizontal sub-sampling */ }; +/** Row order for images memory layout. */ +enum class RowOrder +{ + top_down, /**< Standard image with (0,0) at the top-left corner. */ + bottom_up /**< OpenGL image with (0,0) at the bottom-left corner. */ +}; + /** Cast an enum class value to its underlying type. */ template constexpr typename std::underlying_type::type as_underlying_type(E e) @@ -97,6 +104,7 @@ std::future make_exception_future(Exception&& e) class EventReceiver; class Frame; class FrameDispatcher; +class ImageSegmenter; class SegmentDecoder; class Server; class Stream; diff --git a/doc/Changelog.md b/doc/Changelog.md index 110e80c..ee35183 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -5,6 +5,9 @@ Changelog {#Changelog} ### 0.14.0 (git master) +* [184](https://github.com/BlueBrain/Deflect/pull/184): + Images in OpenGL format (bottom-up row order) can be streamed directly, + replacing the need for ImageWrapper::swapYAxis() in user code. * [179](https://github.com/BlueBrain/Deflect/pull/179): Added stopping() signal to qt::QuickRenderer for GL cleanup operations. * [177](https://github.com/BlueBrain/Deflect/pull/177): diff --git a/tests/cpp/FrameDispatcherTests.cpp b/tests/cpp/FrameDispatcherTests.cpp new file mode 100644 index 0000000..5d40351 --- /dev/null +++ b/tests/cpp/FrameDispatcherTests.cpp @@ -0,0 +1,116 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of The University of Texas at Austin. */ +/*********************************************************************/ + +#define BOOST_TEST_MODULE FrameDispatcherTests + +#include +namespace ut = boost::unit_test; + +#include "FrameUtils.h" + +#include + +namespace +{ +const char* streamId = "test"; +} + +struct Fixture +{ + deflect::FrameDispatcher dispatcher; + deflect::FramePtr receivedFrame; + Fixture() + { + QObject::connect(&dispatcher, &deflect::FrameDispatcher::sendFrame, + [&](deflect::FramePtr f) { receivedFrame = f; }); + + dispatcher.addSource(streamId, 0); + } + + void dispatch(const deflect::Frame& frame) + { + for (auto& segment : frame.segments) + dispatcher.processSegment(streamId, 0, segment); + dispatcher.processFrameFinished(streamId, 0); + dispatcher.requestFrame(streamId); + } +}; + +BOOST_FIXTURE_TEST_CASE(dispatch_multiple_frames, Fixture) +{ + auto frame = makeTestFrame(640, 480, 64); + + dispatch(frame); + BOOST_REQUIRE(receivedFrame); + compare(frame, *receivedFrame); + + receivedFrame = nullptr; + dispatch(frame); + BOOST_REQUIRE(receivedFrame); + compare(frame, *receivedFrame); + + receivedFrame = nullptr; + dispatch(frame); + BOOST_REQUIRE(receivedFrame); + compare(frame, *receivedFrame); +} + +BOOST_FIXTURE_TEST_CASE(dispatch_frame_bottom_up, Fixture) +{ + auto frame = makeTestFrame(640, 480, 64); + for (auto& segment : frame.segments) + segment.rowOrder = deflect::RowOrder::bottom_up; + + dispatch(frame); + BOOST_REQUIRE(receivedFrame); + + // mirror segments positions vertically + for (auto& s : frame.segments) + s.parameters.y = 480 - s.parameters.y - s.parameters.height; + + compare(frame, *receivedFrame); +} + +BOOST_FIXTURE_TEST_CASE(dispatch_frame_with_inconsistent_row_order, Fixture) +{ + auto frame = makeTestFrame(640, 480, 64); + frame.segments[2].rowOrder = deflect::RowOrder::bottom_up; + dispatch(frame); + BOOST_CHECK(!receivedFrame); +} diff --git a/tests/cpp/FrameTests.cpp b/tests/cpp/FrameTests.cpp new file mode 100644 index 0000000..4537abe --- /dev/null +++ b/tests/cpp/FrameTests.cpp @@ -0,0 +1,76 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of The University of Texas at Austin. */ +/*********************************************************************/ + +#define BOOST_TEST_MODULE FrameTests + +#include +namespace ut = boost::unit_test; + +#include "FrameUtils.h" + +BOOST_AUTO_TEST_CASE(compute_frame_dimensions) +{ + auto frame = makeTestFrame(640, 480, 64); + BOOST_CHECK_EQUAL(frame.computeDimensions(), QSize(640, 480)); + + frame = makeTestFrame(1920, 1200, 64); + BOOST_CHECK_EQUAL(frame.computeDimensions(), QSize(1920, 1200)); + + frame = makeTestFrame(1920, 1080, 32); + BOOST_CHECK_EQUAL(frame.computeDimensions(), QSize(1920, 1080)); + + frame = makeTestFrame(2158, 1786, 56); + BOOST_CHECK_EQUAL(frame.computeDimensions(), QSize(2158, 1786)); +} + +BOOST_AUTO_TEST_CASE(determine_frame_row_order) +{ + auto frame = makeTestFrame(640, 480, 64); + BOOST_CHECK(frame.determineRowOrder() == deflect::RowOrder::top_down); + + frame.segments[0].rowOrder = deflect::RowOrder::bottom_up; + BOOST_CHECK_THROW(frame.determineRowOrder(), std::runtime_error); + + for (auto& segment : frame.segments) + segment.rowOrder = deflect::RowOrder::bottom_up; + BOOST_CHECK(frame.determineRowOrder() == deflect::RowOrder::bottom_up); + + frame.segments[0].rowOrder = deflect::RowOrder::top_down; + BOOST_CHECK_THROW(frame.determineRowOrder(), std::runtime_error); +} diff --git a/tests/mock/CMakeLists.txt b/tests/mock/CMakeLists.txt index e822ef0..828c01a 100644 --- a/tests/mock/CMakeLists.txt +++ b/tests/mock/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2013-2016, EPFL/Blue Brain Project +# Copyright (c) 2013-2017, EPFL/Blue Brain Project # Daniel Nachbaur # Raphael Dumusc # @@ -8,6 +8,7 @@ set(DEFLECTMOCK_HEADERS boost_test_thread_safe.h DeflectServer.h + FrameUtils.h MinimalGlobalQtApp.h MinimalDeflectServer.h MockServer.h diff --git a/tests/mock/FrameUtils.h b/tests/mock/FrameUtils.h new file mode 100644 index 0000000..36a51d6 --- /dev/null +++ b/tests/mock/FrameUtils.h @@ -0,0 +1,98 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of The University of Texas at Austin. */ +/*********************************************************************/ + +#include + +#include // std::ceil + +inline deflect::Frame makeTestFrame(int width, int height, int segmentSize) +{ + const int numSegmentsX = std::ceil((float)width / (float)segmentSize); + const int numSegmentsY = std::ceil((float)height / (float)segmentSize); + + const int lastSegmentWidth = + (width % segmentSize) > 0 ? (width % segmentSize) : segmentSize; + const int lastSegmentHeight = + (height % segmentSize) > 0 ? (height % segmentSize) : segmentSize; + + deflect::Frame frame; + for (int y = 0; y < numSegmentsY; ++y) + { + for (int x = 0; x < numSegmentsX; ++x) + { + deflect::Segment segment; + segment.parameters.x = x * segmentSize; + segment.parameters.y = y * segmentSize; + segment.parameters.width = segmentSize; + segment.parameters.height = segmentSize; + if (x == numSegmentsX - 1) + segment.parameters.width = lastSegmentWidth; + if (y == numSegmentsY - 1) + segment.parameters.height = lastSegmentHeight; + frame.segments.push_back(segment); + } + } + return frame; +} + +inline void compare(const deflect::Frame& frame1, const deflect::Frame& frame2) +{ + BOOST_REQUIRE_EQUAL(frame1.segments.size(), frame2.segments.size()); + + for (size_t i = 0; i < frame1.segments.size(); ++i) + { + const auto& s1 = frame1.segments[i]; + const auto& s2 = frame2.segments[i]; + BOOST_CHECK(s1.view == s2.view); + BOOST_CHECK(s1.rowOrder == s2.rowOrder); + + const auto& p1 = s1.parameters; + const auto& p2 = s2.parameters; + BOOST_CHECK_EQUAL(p1.x, p2.x); + BOOST_CHECK_EQUAL(p1.y, p2.y); + BOOST_CHECK_EQUAL(p1.width, p2.width); + BOOST_CHECK_EQUAL(p1.height, p2.height); + } +} + +inline std::ostream& operator<<(std::ostream& str, const QSize& s) +{ + str << s.width() << 'x' << s.height(); + return str; +}