diff --git a/CMakeLists.txt b/CMakeLists.txt index a583d7d951..f20142d698 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,4 +39,5 @@ add_subdirectory(assignment-client) add_subdirectory(domain-server) add_subdirectory(interface) add_subdirectory(pairing-server) -add_subdirectory(voxel-edit) \ No newline at end of file +add_subdirectory(tests) +add_subdirectory(voxel-edit) diff --git a/interface/resources/meshes/defaultAvatar_body.fbx b/interface/resources/meshes/defaultAvatar_body.fbx new file mode 100644 index 0000000000..238410cbbe Binary files /dev/null and b/interface/resources/meshes/defaultAvatar_body.fbx differ diff --git a/interface/resources/meshes/defaultAvatar_head.fbx b/interface/resources/meshes/defaultAvatar_head.fbx new file mode 100644 index 0000000000..30ccca20d3 Binary files /dev/null and b/interface/resources/meshes/defaultAvatar_head.fbx differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1e960bb5a8..160d6b7c2c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1827,6 +1827,9 @@ void Application::init() { _audio.setJitterBufferSamples(Menu::getInstance()->getAudioJitterBufferSamples()); } qDebug("Loaded settings"); + + // fire off an immediate domain-server check in now that settings are loaded + NodeList::getInstance()->sendDomainServerCheckIn(); // Set up VoxelSystem after loading preferences so we can get the desired max voxel count _voxels.setMaxVoxels(Menu::getInstance()->getMaxVoxels()); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 91823f28a9..7eb5807c6f 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -778,11 +778,13 @@ void Menu::editPreferences() { QString faceURLString = applicationInstance->getAvatar()->getHead().getFaceModel().getURL().toString(); QLineEdit* faceURLEdit = new QLineEdit(faceURLString); faceURLEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH); + faceURLEdit->setPlaceholderText(DEFAULT_HEAD_MODEL_URL.toString()); form->addRow("Face URL:", faceURLEdit); QString skeletonURLString = applicationInstance->getAvatar()->getSkeletonModel().getURL().toString(); QLineEdit* skeletonURLEdit = new QLineEdit(skeletonURLString); skeletonURLEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH); + skeletonURLEdit->setPlaceholderText(DEFAULT_BODY_MODEL_URL.toString()); form->addRow("Skeleton URL:", skeletonURLEdit); QSlider* pupilDilation = new QSlider(Qt::Horizontal); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ab8fdbed60..c299c0c617 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -345,12 +345,12 @@ bool Avatar::findSphereCollisionWithSkeleton(const glm::vec3& sphereCenter, floa void Avatar::setFaceModelURL(const QUrl &faceModelURL) { AvatarData::setFaceModelURL(faceModelURL); - _head.getFaceModel().setURL(faceModelURL); + _head.getFaceModel().setURL(_faceModelURL); } void Avatar::setSkeletonModelURL(const QUrl &skeletonModelURL) { AvatarData::setSkeletonModelURL(skeletonModelURL); - _skeletonModel.setURL(skeletonModelURL); + _skeletonModel.setURL(_skeletonModelURL); } int Avatar::parseData(const QByteArray& packet) { diff --git a/interface/src/renderer/FBXReader.cpp b/interface/src/renderer/FBXReader.cpp index b6c74c27cd..237ba7196d 100644 --- a/interface/src/renderer/FBXReader.cpp +++ b/interface/src/renderer/FBXReader.cpp @@ -601,6 +601,7 @@ public: FBXMesh mesh; QMultiHash newIndices; QVector > blendshapeIndexMaps; + QVector > partMaterialTextures; }; class MeshData { @@ -667,6 +668,7 @@ void appendIndex(MeshData& data, QVector& indices, int index) { ExtractedMesh extractMesh(const FBXNode& object) { MeshData data; QVector materials; + QVector textures; foreach (const FBXNode& child, object.children) { if (child.name == "Vertices") { data.vertices = createVec3Vector(getDoubleVector(child.properties, 0)); @@ -703,19 +705,32 @@ ExtractedMesh extractMesh(const FBXNode& object) { materials = getIntVector(subdata.properties, 0); } } + } else if (child.name == "LayerElementTexture") { + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "TextureId") { + textures = getIntVector(subdata.properties, 0); + } + } } } // convert the polygons to quads and triangles int polygonIndex = 0; + QHash, int> materialTextureParts; for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) { int endIndex = beginIndex; while (data.polygonIndices.at(endIndex++) >= 0); - int materialIndex = (polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0; - data.extracted.mesh.parts.resize(max(data.extracted.mesh.parts.size(), materialIndex + 1)); - FBXMeshPart& part = data.extracted.mesh.parts[materialIndex]; - + QPair materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0, + (polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0); + int& partIndex = materialTextureParts[materialTexture]; + if (partIndex == 0) { + data.extracted.partMaterialTextures.append(materialTexture); + data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + partIndex = data.extracted.mesh.parts.size(); + } + FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; + if (endIndex - beginIndex == 4) { appendIndex(data, part.quadIndices, beginIndex++); appendIndex(data, part.quadIndices, beginIndex++); @@ -1265,41 +1280,60 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) glm::mat4 modelTransform = getGlobalTransform(parentMap, models, modelID); // look for textures, material properties - int partIndex = extracted.mesh.parts.size() - 1; + int materialIndex = 0; + int textureIndex = 0; bool generateTangents = false; - foreach (const QString& childID, childMap.values(modelID)) { - if (partIndex < 0) { - break; - } - FBXMeshPart& part = extracted.mesh.parts[partIndex]; - if (textureFilenames.contains(childID)) { - part.diffuseFilename = textureFilenames.value(childID); - continue; - } - if (!materials.contains(childID)) { - continue; - } - Material material = materials.value(childID); - part.diffuseColor = material.diffuse; - part.specularColor = material.specular; - part.shininess = material.shininess; - QString diffuseTextureID = diffuseTextures.value(childID); - if (!diffuseTextureID.isNull()) { - part.diffuseFilename = textureFilenames.value(diffuseTextureID); + QList children = childMap.values(modelID); + for (int i = children.size() - 1; i >= 0; i--) { + const QString& childID = children.at(i); + if (materials.contains(childID)) { + Material material = materials.value(childID); + + QByteArray diffuseFilename; + QString diffuseTextureID = diffuseTextures.value(childID); + if (!diffuseTextureID.isNull()) { + diffuseFilename = textureFilenames.value(diffuseTextureID); - // FBX files generated by 3DSMax have an intermediate texture parent, apparently - foreach (const QString& childTextureID, childMap.values(diffuseTextureID)) { - if (textureFilenames.contains(childTextureID)) { - part.diffuseFilename = textureFilenames.value(childTextureID); + // FBX files generated by 3DSMax have an intermediate texture parent, apparently + foreach (const QString& childTextureID, childMap.values(diffuseTextureID)) { + if (textureFilenames.contains(childTextureID)) { + diffuseFilename = textureFilenames.value(childTextureID); + } } } + + QByteArray normalFilename; + QString bumpTextureID = bumpTextures.value(childID); + if (!bumpTextureID.isNull()) { + normalFilename = textureFilenames.value(bumpTextureID); + generateTangents = true; + } + + for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { + if (extracted.partMaterialTextures.at(j).first == materialIndex) { + FBXMeshPart& part = extracted.mesh.parts[j]; + part.diffuseColor = material.diffuse; + part.specularColor = material.specular; + part.shininess = material.shininess; + if (!diffuseFilename.isNull()) { + part.diffuseFilename = diffuseFilename; + } + if (!normalFilename.isNull()) { + part.normalFilename = normalFilename; + } + } + } + materialIndex++; + + } else if (textureFilenames.contains(childID)) { + QByteArray filename = textureFilenames.value(childID); + for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { + if (extracted.partMaterialTextures.at(j).second == textureIndex) { + extracted.mesh.parts[j].diffuseFilename = filename; + } + } + textureIndex++; } - QString bumpTextureID = bumpTextures.value(childID); - if (!bumpTextureID.isNull()) { - part.normalFilename = textureFilenames.value(bumpTextureID); - generateTangents = true; - } - partIndex--; } // if we have a normal map (and texture coordinates), we must compute tangents diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index bae79eb509..c375f8b82d 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -301,13 +301,15 @@ QByteArray AvatarData::identityByteArray() { } void AvatarData::setFaceModelURL(const QUrl& faceModelURL) { - qDebug() << "Changing face model for avatar to" << faceModelURL.toString(); - _faceModelURL = faceModelURL; + _faceModelURL = faceModelURL.isEmpty() ? DEFAULT_HEAD_MODEL_URL : faceModelURL; + + qDebug() << "Changing face model for avatar to" << _faceModelURL.toString(); } void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { - qDebug() << "Changing skeleton model for avatar to" << skeletonModelURL.toString(); - _skeletonModelURL = skeletonModelURL; + _skeletonModelURL = skeletonModelURL.isEmpty() ? DEFAULT_BODY_MODEL_URL : skeletonModelURL; + + qDebug() << "Changing skeleton model for avatar to" << _skeletonModelURL.toString(); } void AvatarData::setClampedTargetScale(float targetScale) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 48152e0c81..46d92c0f2e 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -52,6 +52,9 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0; // close enough for mouth animation +const QUrl DEFAULT_HEAD_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_head.fbx"); +const QUrl DEFAULT_BODY_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_body.fbx"); + enum KeyState { NO_KEY_DOWN = 0, INSERT_KEY_DOWN, diff --git a/libraries/metavoxels/src/AttributeRegistry.cpp b/libraries/metavoxels/src/AttributeRegistry.cpp index 431eaf62c9..9595d96e1f 100644 --- a/libraries/metavoxels/src/AttributeRegistry.cpp +++ b/libraries/metavoxels/src/AttributeRegistry.cpp @@ -82,6 +82,14 @@ bool AttributeValue::operator==(void* other) const { return _attribute && _attribute->equal(_value, other); } +bool AttributeValue::operator!=(const AttributeValue& other) const { + return _attribute != other._attribute || (_attribute && !_attribute->equal(_value, other._value)); +} + +bool AttributeValue::operator!=(void* other) const { + return !_attribute || !_attribute->equal(_value, other); +} + OwnedAttributeValue::OwnedAttributeValue(const AttributePointer& attribute, void* value) : AttributeValue(attribute, value) { } diff --git a/libraries/metavoxels/src/AttributeRegistry.h b/libraries/metavoxels/src/AttributeRegistry.h index db5e54cc4a..56cff7eeb4 100644 --- a/libraries/metavoxels/src/AttributeRegistry.h +++ b/libraries/metavoxels/src/AttributeRegistry.h @@ -105,6 +105,9 @@ public: bool operator==(const AttributeValue& other) const; bool operator==(void* other) const; + bool operator!=(const AttributeValue& other) const; + bool operator!=(void* other) const; + protected: AttributePointer _attribute; diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index 5a79b10766..cc776a742a 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -415,12 +415,16 @@ public: #define DECLARE_STREAMABLE_METATYPE(X) Q_DECLARE_METATYPE(X) \ Bitstream& operator<<(Bitstream& out, const X& obj); \ Bitstream& operator>>(Bitstream& in, X& obj); \ + bool operator==(const X& first, const X& second); \ + bool operator!=(const X& first, const X& second); \ static const int* _TypePtr##X = &X::Type; #else #define STRINGIFY(x) #x #define DECLARE_STREAMABLE_METATYPE(X) Q_DECLARE_METATYPE(X) \ Bitstream& operator<<(Bitstream& out, const X& obj); \ Bitstream& operator>>(Bitstream& in, X& obj); \ + bool operator==(const X& first, const X& second); \ + bool operator!=(const X& first, const X& second); \ static const int* _TypePtr##X = &X::Type; \ _Pragma(STRINGIFY(unused(_TypePtr##X))) #endif diff --git a/libraries/metavoxels/src/DatagramSequencer.cpp b/libraries/metavoxels/src/DatagramSequencer.cpp index af271d97ba..3cbf4a3ac9 100644 --- a/libraries/metavoxels/src/DatagramSequencer.cpp +++ b/libraries/metavoxels/src/DatagramSequencer.cpp @@ -20,7 +20,8 @@ const int MAX_DATAGRAM_SIZE = MAX_PACKET_SIZE; const int DEFAULT_MAX_PACKET_SIZE = 3000; -DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader) : +DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* parent) : + QObject(parent), _outgoingPacketStream(&_outgoingPacketData, QIODevice::WriteOnly), _outputStream(_outgoingPacketStream), _incomingDatagramStream(&_incomingDatagramBuffer), @@ -174,10 +175,10 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { } // read and dispatch the high-priority messages - int highPriorityMessageCount; + quint32 highPriorityMessageCount; _incomingPacketStream >> highPriorityMessageCount; int newHighPriorityMessages = highPriorityMessageCount - _receivedHighPriorityMessages; - for (int i = 0; i < highPriorityMessageCount; i++) { + for (quint32 i = 0; i < highPriorityMessageCount; i++) { QVariant data; _inputStream >> data; if (i >= _receivedHighPriorityMessages) { @@ -192,10 +193,10 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { // read the reliable data, if any quint32 reliableChannels; _incomingPacketStream >> reliableChannels; - for (int i = 0; i < reliableChannels; i++) { + for (quint32 i = 0; i < reliableChannels; i++) { quint32 channelIndex; _incomingPacketStream >> channelIndex; - getReliableOutputChannel(channelIndex)->readData(_incomingPacketStream); + getReliableInputChannel(channelIndex)->readData(_incomingPacketStream); } _incomingPacketStream.device()->seek(0); @@ -311,6 +312,178 @@ void DatagramSequencer::handleHighPriorityMessage(const QVariant& data) { } } +const int INITIAL_CIRCULAR_BUFFER_CAPACITY = 16; + +CircularBuffer::CircularBuffer(QObject* parent) : + QIODevice(parent), + _data(INITIAL_CIRCULAR_BUFFER_CAPACITY, 0), + _position(0), + _size(0), + _offset(0) { +} + +void CircularBuffer::append(const char* data, int length) { + // resize to fit + int oldSize = _size; + resize(_size + length); + + // write our data in up to two segments: one from the position to the end, one from the beginning + int end = (_position + oldSize) % _data.size(); + int firstSegment = qMin(length, _data.size() - end); + memcpy(_data.data() + end, data, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(_data.data(), data + firstSegment, secondSegment); + } +} + +void CircularBuffer::remove(int length) { + _position = (_position + length) % _data.size(); + _size -= length; +} + +QByteArray CircularBuffer::readBytes(int offset, int length) const { + // write in up to two segments + QByteArray array; + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + array.append(_data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + array.append(_data.constData(), secondSegment); + } + return array; +} + +void CircularBuffer::writeToStream(int offset, int length, QDataStream& out) const { + // write in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + out.writeRawData(_data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + out.writeRawData(_data.constData(), secondSegment); + } +} + +void CircularBuffer::readFromStream(int offset, int length, QDataStream& in) { + // resize to fit + int requiredSize = offset + length; + if (requiredSize > _size) { + resize(requiredSize); + } + + // read in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + in.readRawData(_data.data() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + in.readRawData(_data.data(), secondSegment); + } +} + +void CircularBuffer::appendToBuffer(int offset, int length, CircularBuffer& buffer) const { + // append in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + buffer.append(_data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + buffer.append(_data.constData(), secondSegment); + } +} + +bool CircularBuffer::atEnd() const { + return _offset >= _size; +} + +qint64 CircularBuffer::bytesAvailable() const { + return _size - _offset + QIODevice::bytesAvailable(); +} + +bool CircularBuffer::canReadLine() const { + for (int offset = _offset; offset < _size; offset++) { + if (_data.at((_position + offset) % _data.size()) == '\n') { + return true; + } + } + return false; +} + +bool CircularBuffer::open(OpenMode flags) { + return QIODevice::open(flags | QIODevice::Unbuffered); +} + +qint64 CircularBuffer::pos() const { + return _offset; +} + +bool CircularBuffer::seek(qint64 pos) { + if (pos < 0 || pos > _size) { + return false; + } + _offset = pos; + return true; +} + +qint64 CircularBuffer::size() const { + return _size; +} + +qint64 CircularBuffer::readData(char* data, qint64 length) { + int readable = qMin((int)length, _size - _offset); + + // read in up to two segments + int start = (_position + _offset) % _data.size(); + int firstSegment = qMin((int)length, _data.size() - start); + memcpy(data, _data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(data + firstSegment, _data.constData(), secondSegment); + } + _offset += readable; + return readable; +} + +qint64 CircularBuffer::writeData(const char* data, qint64 length) { + // resize to fit + int requiredSize = _offset + length; + if (requiredSize > _size) { + resize(requiredSize); + } + + // write in up to two segments + int start = (_position + _offset) % _data.size(); + int firstSegment = qMin((int)length, _data.size() - start); + memcpy(_data.data() + start, data, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(_data.data(), data + firstSegment, secondSegment); + } + _offset += length; + return length; +} + +void CircularBuffer::resize(int size) { + if (size > _data.size()) { + // double our capacity until we can fit the desired length + int newCapacity = _data.size(); + do { + newCapacity *= 2; + } while (size > newCapacity); + + int oldCapacity = _data.size(); + _data.resize(newCapacity); + + int trailing = _position + _size - oldCapacity; + if (trailing > 0) { + memcpy(_data.data() + oldCapacity, _data.constData(), trailing); + } + } + _size = size; +} + SpanList::SpanList() : _totalSet(0) { } @@ -369,7 +542,7 @@ int SpanList::set(int offset, int length) { int SpanList::setSpans(QList::iterator it, int length) { int remainingLength = length; int totalRemoved = 0; - for (; it != _spans.end(); it++) { + for (; it != _spans.end(); it = _spans.erase(it)) { if (remainingLength < it->unset) { it->unset -= remainingLength; totalRemoved += remainingLength; @@ -378,7 +551,6 @@ int SpanList::setSpans(QList::iterator it, int length) { int combined = it->unset + it->set; remainingLength = qMax(remainingLength - combined, 0); totalRemoved += combined; - it = _spans.erase(it); _totalSet -= it->set; } return qMax(length, totalRemoved); @@ -424,14 +596,13 @@ void ReliableChannel::writeData(QDataStream& out, int bytes, QVector 0 && leftover > 0) { spanCount++; - remainingBytes -= getBytesToWrite(first, leftover); + remainingBytes -= getBytesToWrite(first, qMin(remainingBytes, leftover)); } } @@ -448,8 +619,9 @@ void ReliableChannel::writeData(QDataStream& out, int bytes, QVector 0 && position < _buffer.pos()) { - remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, (int)(_buffer.pos() - position)), spans); + int leftover = _buffer.pos() - position; + if (remainingBytes > 0 && leftover > 0) { + remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, leftover), spans); } } } @@ -473,16 +645,16 @@ int ReliableChannel::writeSpan(QDataStream& out, bool& first, int position, int spans.append(span); out << (quint32)span.offset; out << (quint32)length; - out.writeRawData(_buffer.data().constData() + position, length); + _buffer.writeToStream(position, length, out); return length; } void ReliableChannel::spanAcknowledged(const DatagramSequencer::ChannelSpan& span) { int advancement = _acknowledged.set(span.offset - _offset, span.length); if (advancement > 0) { - // TODO: better way of pruning buffer - _buffer.buffer() = _buffer.buffer().right(_buffer.size() - advancement); + _buffer.remove(advancement); _buffer.seek(_buffer.size()); + _offset += advancement; _writePosition = qMax(_writePosition - advancement, 0); } @@ -491,38 +663,40 @@ void ReliableChannel::spanAcknowledged(const DatagramSequencer::ChannelSpan& spa void ReliableChannel::readData(QDataStream& in) { quint32 segments; in >> segments; - for (int i = 0; i < segments; i++) { + bool readSome = false; + for (quint32 i = 0; i < segments; i++) { quint32 offset, size; in >> offset >> size; int position = offset - _offset; int end = position + size; - if (_assemblyBuffer.size() < end) { - _assemblyBuffer.resize(end); - } if (end <= 0) { in.skipRawData(size); + } else if (position < 0) { in.skipRawData(-position); - in.readRawData(_assemblyBuffer.data(), size + position); + _assemblyBuffer.readFromStream(0, end, in); + } else { - in.readRawData(_assemblyBuffer.data() + position, size); + _assemblyBuffer.readFromStream(position, size, in); } int advancement = _acknowledged.set(position, size); if (advancement > 0) { - // TODO: better way of pruning buffer - _buffer.buffer().append(_assemblyBuffer.constData(), advancement); - emit _buffer.readyRead(); - _assemblyBuffer = _assemblyBuffer.right(_assemblyBuffer.size() - advancement); + _assemblyBuffer.appendToBuffer(0, advancement, _buffer); + _assemblyBuffer.remove(advancement); _offset += advancement; + readSome = true; } } - // when the read head is sufficiently advanced into the buffer, prune it off. this along - // with other buffer usages should be replaced with a circular buffer - const int PRUNE_SIZE = 8192; - if (_buffer.pos() > PRUNE_SIZE) { - _buffer.buffer() = _buffer.buffer().right(_buffer.size() - _buffer.pos()); + // let listeners know that there's data to read + if (readSome) { + emit _buffer.readyRead(); + } + + // prune any read data from the buffer + if (_buffer.pos() > 0) { + _buffer.remove((int)_buffer.pos()); _buffer.seek(0); } } diff --git a/libraries/metavoxels/src/DatagramSequencer.h b/libraries/metavoxels/src/DatagramSequencer.h index 44d3c83116..27a4f05379 100644 --- a/libraries/metavoxels/src/DatagramSequencer.h +++ b/libraries/metavoxels/src/DatagramSequencer.h @@ -32,7 +32,7 @@ public: int firstPacketNumber; }; - DatagramSequencer(const QByteArray& datagramHeader = QByteArray()); + DatagramSequencer(const QByteArray& datagramHeader = QByteArray(), QObject* parent = NULL); /// Returns the packet number of the last packet sent. int getOutgoingPacketNumber() const { return _outgoingPacketNumber; } @@ -58,7 +58,7 @@ public: /// Returns the output channel at the specified index, creating it if necessary. ReliableChannel* getReliableOutputChannel(int index = 0); - /// Returns the intput channel at the + /// Returns the intput channel at the specified index, creating it if necessary. ReliableChannel* getReliableInputChannel(int index = 0); /// Starts a new packet for transmission. @@ -167,6 +167,56 @@ private: QHash _reliableInputChannels; }; +/// A circular buffer, where one may efficiently append data to the end or remove data from the beginning. +class CircularBuffer : public QIODevice { +public: + + CircularBuffer(QObject* parent = NULL); + + /// Appends data to the end of the buffer. + void append(const QByteArray& data) { append(data.constData(), data.size()); } + + /// Appends data to the end of the buffer. + void append(const char* data, int length); + + /// Removes data from the beginning of the buffer. + void remove(int length); + + /// Reads part of the data from the buffer. + QByteArray readBytes(int offset, int length) const; + + /// Writes part of the buffer to the supplied stream. + void writeToStream(int offset, int length, QDataStream& out) const; + + /// Reads part of the buffer from the supplied stream. + void readFromStream(int offset, int length, QDataStream& in); + + /// Appends part of the buffer to the supplied other buffer. + void appendToBuffer(int offset, int length, CircularBuffer& buffer) const; + + virtual bool atEnd() const; + virtual qint64 bytesAvailable() const; + virtual bool canReadLine() const; + virtual bool open(OpenMode flags); + virtual qint64 pos() const; + virtual bool seek(qint64 pos); + virtual qint64 size() const; + +protected: + + virtual qint64 readData(char* data, qint64 length); + virtual qint64 writeData(const char* data, qint64 length); + +private: + + void resize(int size); + + QByteArray _data; + int _position; + int _size; + int _offset; +}; + /// A list of contiguous spans, alternating between set and unset. Conceptually, the list is preceeded by a set /// span of infinite length and followed by an unset span of infinite length. Within those bounds, it alternates /// between unset and set. @@ -208,6 +258,7 @@ public: int getIndex() const { return _index; } + CircularBuffer& getBuffer() { return _buffer; } QDataStream& getDataStream() { return _dataStream; } Bitstream& getBitstream() { return _bitstream; } @@ -237,8 +288,8 @@ private: void readData(QDataStream& in); int _index; - QBuffer _buffer; - QByteArray _assemblyBuffer; + CircularBuffer _buffer; + CircularBuffer _assemblyBuffer; QDataStream _dataStream; Bitstream _bitstream; float _priority; diff --git a/libraries/metavoxels/src/MetavoxelUtil.cpp b/libraries/metavoxels/src/MetavoxelUtil.cpp index e59cf0fe21..d6b42b11fb 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.cpp +++ b/libraries/metavoxels/src/MetavoxelUtil.cpp @@ -11,12 +11,12 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include @@ -76,27 +76,48 @@ static QItemEditorFactory* getItemEditorFactory() { return factory; } +/// Because Windows doesn't necessarily have the staticMetaObject available when we want to create, +/// this class simply delays the value property name lookup until actually requested. +template class LazyItemEditorCreator : public QItemEditorCreatorBase { +public: + + virtual QWidget* createWidget(QWidget* parent) const { return new T(parent); } + + virtual QByteArray valuePropertyName() const; + +protected: + + QByteArray _valuePropertyName; +}; + +template QByteArray LazyItemEditorCreator::valuePropertyName() const { + if (_valuePropertyName.isNull()) { + const_cast*>(this)->_valuePropertyName = T::staticMetaObject.userProperty().name(); + } + return _valuePropertyName; +} + static QItemEditorCreatorBase* createDoubleEditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } static QItemEditorCreatorBase* createQColorEditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } static QItemEditorCreatorBase* createVec3EditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } static QItemEditorCreatorBase* createParameterizedURLEditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } @@ -120,6 +141,12 @@ QUuid readSessionID(const QByteArray& data, const SharedNodePointer& sendingNode return QUuid::fromRfc4122(QByteArray::fromRawData(data.constData() + headerSize, UUID_BYTES)); } +QByteArray signal(const char* signature) { + static QByteArray prototype = SIGNAL(dummyMethod()); + QByteArray signal = prototype; + return signal.replace("dummyMethod()", signature); +} + bool Box::contains(const Box& other) const { return other.minimum.x >= minimum.x && other.maximum.x <= maximum.x && other.minimum.y >= minimum.y && other.maximum.y <= maximum.y && @@ -301,8 +328,7 @@ void ParameterizedURLEditor::continueUpdatingParameters() { QMetaProperty widgetProperty = widgetMetaObject->property(widgetMetaObject->indexOfProperty(valuePropertyName)); widgetProperty.write(widget, _url.getParameters().value(parameter.name)); if (widgetProperty.hasNotifySignal()) { - connect(widget, QByteArray(SIGNAL()).append(widgetProperty.notifySignal().methodSignature()), - SLOT(updateURL())); + connect(widget, signal(widgetProperty.notifySignal().methodSignature()), SLOT(updateURL())); } } } diff --git a/libraries/metavoxels/src/MetavoxelUtil.h b/libraries/metavoxels/src/MetavoxelUtil.h index 9fa721d21c..7cfedd7a62 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.h +++ b/libraries/metavoxels/src/MetavoxelUtil.h @@ -33,6 +33,9 @@ class NetworkProgram; /// \return the session ID, or a null ID if invalid (in which case a warning will be logged) QUuid readSessionID(const QByteArray& data, const SharedNodePointer& sendingNode, int& headerPlusIDSize); +/// Performs the runtime equivalent of Qt's SIGNAL macro, which is to attach a prefix to the signature. +QByteArray signal(const char* signature); + /// A streamable axis-aligned bounding box. class Box { STREAMABLE diff --git a/libraries/metavoxels/src/SharedObject.cpp b/libraries/metavoxels/src/SharedObject.cpp index a3af307297..f97e285bcf 100644 --- a/libraries/metavoxels/src/SharedObject.cpp +++ b/libraries/metavoxels/src/SharedObject.cpp @@ -13,6 +13,7 @@ #include #include "Bitstream.h" +#include "MetavoxelUtil.h" #include "SharedObject.h" SharedObject::SharedObject() : _referenceCount(0) { @@ -204,8 +205,7 @@ void SharedObjectEditor::updateType() { QMetaProperty widgetProperty = widgetMetaObject->property(widgetMetaObject->indexOfProperty(valuePropertyName)); widgetProperty.write(widget, property.read(newObject)); if (widgetProperty.hasNotifySignal()) { - connect(widget, QByteArray(SIGNAL()).append(widgetProperty.notifySignal().methodSignature()), - SLOT(propertyChanged())); + connect(widget, signal(widgetProperty.notifySignal().methodSignature()), SLOT(propertyChanged())); } } } diff --git a/libraries/shared/src/Logging.cpp b/libraries/shared/src/Logging.cpp index bc0d4af084..a9f71e346e 100644 --- a/libraries/shared/src/Logging.cpp +++ b/libraries/shared/src/Logging.cpp @@ -98,6 +98,9 @@ const char* stringForLogType(QtMsgType msgType) { const char DATE_STRING_FORMAT[] = "%F %H:%M:%S %z"; void Logging::verboseMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + if (message.isEmpty()) { + return; + } // log prefix is in the following format // [DEBUG] [TIMESTAMP] [PID:PARENT_PID] [TARGET] logged string diff --git a/libraries/shared/src/NodeList.cpp b/libraries/shared/src/NodeList.cpp index 2672f3b27b..c0bc7f0010 100644 --- a/libraries/shared/src/NodeList.cpp +++ b/libraries/shared/src/NodeList.cpp @@ -27,7 +27,7 @@ const char SOLO_NODE_TYPES[2] = { NodeType::AudioMixer }; -const QString DEFAULT_DOMAIN_HOSTNAME = "alpha.highfidelity.io"; +const QString DEFAULT_DOMAIN_HOSTNAME = "root.highfidelity.io"; const unsigned short DEFAULT_DOMAIN_SERVER_PORT = 40102; NodeList* NodeList::_sharedInstance = NULL; @@ -59,7 +59,7 @@ NodeList* NodeList::getInstance() { NodeList::NodeList(char newOwnerType, unsigned short int newSocketListenPort) : _nodeHash(), _nodeHashMutex(QMutex::Recursive), - _domainHostname(DEFAULT_DOMAIN_HOSTNAME), + _domainHostname(), _domainSockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), _nodeSocket(this), _ownerType(newOwnerType), @@ -81,13 +81,10 @@ NodeList::~NodeList() { } bool NodeList::packetVersionAndHashMatch(const QByteArray& packet) { - // currently this just checks if the version in the packet matches our return from versionForPacketType - // may need to be expanded in the future for types and versions that take > than 1 byte - if (packet[1] != versionForPacketType(packetTypeForPacket(packet)) && packetTypeForPacket(packet) != PacketTypeStunResponse) { PacketType mismatchType = packetTypeForPacket(packet); - int numPacketTypeBytes = arithmeticCodingValueFromBuffer(packet.data()); + int numPacketTypeBytes = numBytesArithmeticCodingFromBuffer(packet.data()); qDebug() << "Packet version mismatch on" << packetTypeForPacket(packet) << "- Sender" << uuidFromPacketHeader(packet) << "sent" << qPrintable(QString::number(packet[numPacketTypeBytes])) << "but" @@ -498,7 +495,7 @@ void NodeList::sendDomainServerCheckIn() { static bool printedDomainServerIP = false; // Lookup the IP address of the domain server if we need to - if (_domainSockAddr.getAddress().isNull()) { + if (_domainSockAddr.getAddress().isNull() && !_domainHostname.isEmpty()) { qDebug("Looking up DS hostname %s.", _domainHostname.toLocal8Bit().constData()); QHostInfo domainServerHostInfo = QHostInfo::fromName(_domainHostname); @@ -527,8 +524,8 @@ void NodeList::sendDomainServerCheckIn() { // we don't know our public socket and we need to send it to the domain server // send a STUN request to figure it out sendSTUNRequest(); - } else { - // construct the DS check in packet if we need to + } else if (!_domainSockAddr.getAddress().isNull()) { + // construct the DS check in packet if we can // check in packet has header, optional UUID, node type, port, IP, node types of interest, null termination QByteArray domainServerPacket = byteArrayWithPopluatedHeader(PacketTypeDomainListRequest); @@ -807,7 +804,8 @@ void NodeList::loadData(QSettings *settings) { if (domainServerHostname.size() > 0) { _domainHostname = domainServerHostname; - emit domainChanged(_domainHostname); + } else { + _domainHostname = DEFAULT_DOMAIN_HOSTNAME; } settings->endGroup(); diff --git a/libraries/shared/src/PacketHeaders.cpp b/libraries/shared/src/PacketHeaders.cpp index b46a57d4aa..b1f6ef1730 100644 --- a/libraries/shared/src/PacketHeaders.cpp +++ b/libraries/shared/src/PacketHeaders.cpp @@ -57,6 +57,9 @@ PacketVersion versionForPacketType(PacketType type) { case PacketTypeDataServerConfirm: case PacketTypeDataServerSend: return 1; + case PacketTypeVoxelSet: + case PacketTypeVoxelSetDestructive: + return 1; default: return 0; } diff --git a/libraries/voxels/src/VoxelTree.cpp b/libraries/voxels/src/VoxelTree.cpp index e29cfda41d..08ec2cfc3c 100644 --- a/libraries/voxels/src/VoxelTree.cpp +++ b/libraries/voxels/src/VoxelTree.cpp @@ -521,6 +521,8 @@ bool VoxelTree::handlesEditPacketType(PacketType packetType) const { } } +const unsigned int REPORT_OVERFLOW_WARNING_INTERVAL = 100; +unsigned int overflowWarnings = 0; int VoxelTree::processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength, const unsigned char* editData, int maxLength, const SharedNodePointer& node) { @@ -532,9 +534,17 @@ int VoxelTree::processEditPacketData(PacketType packetType, const unsigned char* int octets = numberOfThreeBitSectionsInCode(editData, maxLength); if (octets == OVERFLOWED_OCTCODE_BUFFER) { - printf("WARNING! Got voxel edit record that would overflow buffer in numberOfThreeBitSectionsInCode(), "); - printf("bailing processing of packet!\n"); - return 0; + overflowWarnings++; + if (overflowWarnings % REPORT_OVERFLOW_WARNING_INTERVAL == 1) { + qDebug() << "WARNING! Got voxel edit record that would overflow buffer in numberOfThreeBitSectionsInCode()" + " [NOTE: this is warning number" << overflowWarnings << ", the next" << + (REPORT_OVERFLOW_WARNING_INTERVAL-1) << "will be suppressed.]"; + + QDebug debug = qDebug(); + debug << "edit data contents:"; + outputBufferBits(editData, maxLength, &debug); + } + return maxLength; } const int COLOR_SIZE_IN_BYTES = 3; @@ -542,9 +552,17 @@ int VoxelTree::processEditPacketData(PacketType packetType, const unsigned char* int voxelDataSize = voxelCodeSize + COLOR_SIZE_IN_BYTES; if (voxelDataSize > maxLength) { - printf("WARNING! Got voxel edit record that would overflow buffer, bailing processing of packet!\n"); - printf("bailing processing of packet!\n"); - return 0; + overflowWarnings++; + if (overflowWarnings % REPORT_OVERFLOW_WARNING_INTERVAL == 1) { + qDebug() << "WARNING! Got voxel edit record that would overflow buffer." + " [NOTE: this is warning number" << overflowWarnings << ", the next" << + (REPORT_OVERFLOW_WARNING_INTERVAL-1) << "will be suppressed.]"; + + QDebug debug = qDebug(); + debug << "edit data contents:"; + outputBufferBits(editData, maxLength, &debug); + } + return maxLength; } readCodeColorBufferToTree(editData, destructive); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..e817ffe506 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8) + +# add the test directories +file(GLOB TEST_SUBDIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*) +foreach(DIR ${TEST_SUBDIRS}) + if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${DIR}) + add_subdirectory(${DIR}) + endif() +endforeach() + diff --git a/tests/metavoxels/CMakeLists.txt b/tests/metavoxels/CMakeLists.txt new file mode 100644 index 0000000000..a21dc2f316 --- /dev/null +++ b/tests/metavoxels/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 2.8) + +set(TARGET_NAME metavoxel-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +find_package(Qt5Network REQUIRED) +find_package(Qt5Script REQUIRED) +find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(metavoxels ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) + + diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp new file mode 100644 index 0000000000..15d7463742 --- /dev/null +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -0,0 +1,272 @@ +// +// MetavoxelTests.cpp +// metavoxel-tests +// +// Created by Andrzej Kapolka on 2/7/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include + +#include + +#include "MetavoxelTests.h" + +MetavoxelTests::MetavoxelTests(int& argc, char** argv) : + QCoreApplication(argc, argv) { +} + +static int datagramsSent = 0; +static int datagramsReceived = 0; +static int highPriorityMessagesSent = 0; +static int highPriorityMessagesReceived = 0; +static int unreliableMessagesSent = 0; +static int unreliableMessagesReceived = 0; +static int streamedBytesSent = 0; +static int streamedBytesReceived = 0; +static int lowPriorityStreamedBytesSent = 0; +static int lowPriorityStreamedBytesReceived = 0; + +bool MetavoxelTests::run() { + + qDebug() << "Running metavoxel tests..."; + + // seed the random number generator so that our tests are reproducible + srand(0xBAAAAABE); + + // create two endpoints with the same header + QByteArray datagramHeader("testheader"); + Endpoint alice(datagramHeader), bob(datagramHeader); + + alice.setOther(&bob); + bob.setOther(&alice); + + // perform a large number of simulation iterations + const int SIMULATION_ITERATIONS = 100000; + for (int i = 0; i < SIMULATION_ITERATIONS; i++) { + if (alice.simulate(i) || bob.simulate(i)) { + return true; + } + } + + qDebug() << "Sent" << highPriorityMessagesSent << "high priority messages, received" << highPriorityMessagesReceived; + qDebug() << "Sent" << unreliableMessagesSent << "unreliable messages, received" << unreliableMessagesReceived; + qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; + qDebug() << "Sent" << lowPriorityStreamedBytesSent << "low-priority streamed bytes, received" << + lowPriorityStreamedBytesReceived; + qDebug() << "Sent" << datagramsSent << "datagrams, received" << datagramsReceived; + + qDebug() << "All tests passed!"; + + return false; +} + +static QByteArray createRandomBytes(int minimumSize, int maximumSize) { + QByteArray bytes(randIntInRange(minimumSize, maximumSize), 0); + for (int i = 0; i < bytes.size(); i++) { + bytes[i] = rand(); + } + return bytes; +} + +static QByteArray createRandomBytes() { + const int MIN_BYTES = 4; + const int MAX_BYTES = 16; + return createRandomBytes(MIN_BYTES, MAX_BYTES); +} + +Endpoint::Endpoint(const QByteArray& datagramHeader) : + _sequencer(new DatagramSequencer(datagramHeader, this)), + _highPriorityMessagesToSend(0.0f) { + + connect(_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendDatagram(const QByteArray&))); + connect(_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readMessage(Bitstream&))); + connect(_sequencer, SIGNAL(receivedHighPriorityMessage(const QVariant&)), + SLOT(handleHighPriorityMessage(const QVariant&))); + connect(&_sequencer->getReliableInputChannel()->getBuffer(), SIGNAL(readyRead()), SLOT(readReliableChannel())); + connect(&_sequencer->getReliableInputChannel(1)->getBuffer(), SIGNAL(readyRead()), SLOT(readLowPriorityReliableChannel())); + + // enqueue a large amount of data in a low-priority channel + ReliableChannel* output = _sequencer->getReliableOutputChannel(1); + output->setPriority(0.25f); + const int MIN_LOW_PRIORITY_DATA = 100000; + const int MAX_LOW_PRIORITY_DATA = 200000; + QByteArray bytes = createRandomBytes(MIN_LOW_PRIORITY_DATA, MAX_LOW_PRIORITY_DATA); + _lowPriorityDataStreamed.append(bytes); + output->getBuffer().write(bytes); + lowPriorityStreamedBytesSent += bytes.size(); +} + +static QVariant createRandomMessage() { + switch (randIntInRange(0, 2)) { + case 0: { + TestMessageA message = { randomBoolean(), rand(), randFloat() }; + return QVariant::fromValue(message); + } + case 1: { + TestMessageB message = { createRandomBytes() }; + return QVariant::fromValue(message); + } + case 2: + default: { + TestMessageC message; + message.foo = randomBoolean(); + message.bar = rand(); + message.baz = randFloat(); + message.bong.foo = createRandomBytes(); + return QVariant::fromValue(message); + } + } +} + +static bool messagesEqual(const QVariant& firstMessage, const QVariant& secondMessage) { + int type = firstMessage.userType(); + if (secondMessage.userType() != type) { + return false; + } + if (type == TestMessageA::Type) { + return firstMessage.value() == secondMessage.value(); + } else if (type == TestMessageB::Type) { + return firstMessage.value() == secondMessage.value(); + } else if (type == TestMessageC::Type) { + return firstMessage.value() == secondMessage.value(); + } else { + return firstMessage == secondMessage; + } +} + +bool Endpoint::simulate(int iterationNumber) { + // update/send our delayed datagrams + for (QList >::iterator it = _delayedDatagrams.begin(); it != _delayedDatagrams.end(); ) { + if (it->second-- == 1) { + _other->_sequencer->receivedDatagram(it->first); + datagramsReceived++; + it = _delayedDatagrams.erase(it); + + } else { + it++; + } + } + + // enqueue some number of high priority messages + const float MIN_HIGH_PRIORITY_MESSAGES = 0.0f; + const float MAX_HIGH_PRIORITY_MESSAGES = 2.0f; + _highPriorityMessagesToSend += randFloatInRange(MIN_HIGH_PRIORITY_MESSAGES, MAX_HIGH_PRIORITY_MESSAGES); + while (_highPriorityMessagesToSend >= 1.0f) { + QVariant message = createRandomMessage(); + _highPriorityMessagesSent.append(message); + _sequencer->sendHighPriorityMessage(message); + highPriorityMessagesSent++; + _highPriorityMessagesToSend -= 1.0f; + } + + // stream some random data + const int MIN_BYTES_TO_STREAM = 10; + const int MAX_BYTES_TO_STREAM = 100; + QByteArray bytes = createRandomBytes(MIN_BYTES_TO_STREAM, MAX_BYTES_TO_STREAM); + _dataStreamed.append(bytes); + streamedBytesSent += bytes.size(); + _sequencer->getReliableOutputChannel()->getDataStream().writeRawData(bytes.constData(), bytes.size()); + + // send a packet + try { + Bitstream& out = _sequencer->startPacket(); + SequencedTestMessage message = { iterationNumber, createRandomMessage() }; + _unreliableMessagesSent.append(message); + unreliableMessagesSent++; + out << message; + _sequencer->endPacket(); + + } catch (const QString& message) { + qDebug() << message; + return true; + } + + return false; +} + +void Endpoint::sendDatagram(const QByteArray& datagram) { + datagramsSent++; + + // some datagrams are dropped + const float DROP_PROBABILITY = 0.1f; + if (randFloat() < DROP_PROBABILITY) { + return; + } + + // some are received out of order + const float REORDER_PROBABILITY = 0.1f; + if (randFloat() < REORDER_PROBABILITY) { + const int MIN_DELAY = 1; + const int MAX_DELAY = 5; + // have to copy the datagram; the one we're passed is a reference to a shared buffer + _delayedDatagrams.append(QPair(QByteArray(datagram.constData(), datagram.size()), + randIntInRange(MIN_DELAY, MAX_DELAY))); + + // and some are duplicated + const float DUPLICATE_PROBABILITY = 0.01f; + if (randFloat() > DUPLICATE_PROBABILITY) { + return; + } + } + + _other->_sequencer->receivedDatagram(datagram); + datagramsReceived++; +} + +void Endpoint::handleHighPriorityMessage(const QVariant& message) { + if (_other->_highPriorityMessagesSent.isEmpty()) { + throw QString("Received unsent/already sent high priority message."); + } + QVariant sentMessage = _other->_highPriorityMessagesSent.takeFirst(); + if (!messagesEqual(message, sentMessage)) { + throw QString("Sent/received high priority message mismatch."); + } + highPriorityMessagesReceived++; +} + +void Endpoint::readMessage(Bitstream& in) { + SequencedTestMessage message; + in >> message; + + for (QList::iterator it = _other->_unreliableMessagesSent.begin(); + it != _other->_unreliableMessagesSent.end(); it++) { + if (it->sequenceNumber == message.sequenceNumber) { + if (!messagesEqual(it->submessage, message.submessage)) { + throw QString("Sent/received unreliable message mismatch."); + } + _other->_unreliableMessagesSent.erase(_other->_unreliableMessagesSent.begin(), it + 1); + unreliableMessagesReceived++; + return; + } + } + throw QString("Received unsent/already sent unreliable message."); +} + +void Endpoint::readReliableChannel() { + CircularBuffer& buffer = _sequencer->getReliableInputChannel()->getBuffer(); + QByteArray bytes = buffer.read(buffer.bytesAvailable()); + if (_other->_dataStreamed.size() < bytes.size()) { + throw QString("Received unsent/already sent streamed data."); + } + QByteArray compare = _other->_dataStreamed.readBytes(0, bytes.size()); + _other->_dataStreamed.remove(bytes.size()); + if (compare != bytes) { + throw QString("Sent/received streamed data mismatch."); + } + streamedBytesReceived += bytes.size(); +} + +void Endpoint::readLowPriorityReliableChannel() { + CircularBuffer& buffer = _sequencer->getReliableInputChannel(1)->getBuffer(); + QByteArray bytes = buffer.read(buffer.bytesAvailable()); + if (_other->_lowPriorityDataStreamed.size() < bytes.size()) { + throw QString("Received unsent/already sent low-priority streamed data."); + } + QByteArray compare = _other->_lowPriorityDataStreamed.readBytes(0, bytes.size()); + _other->_lowPriorityDataStreamed.remove(bytes.size()); + if (compare != bytes) { + throw QString("Sent/received low-priority streamed data mismatch."); + } + lowPriorityStreamedBytesReceived += bytes.size(); +} diff --git a/tests/metavoxels/src/MetavoxelTests.h b/tests/metavoxels/src/MetavoxelTests.h new file mode 100644 index 0000000000..b73f7eb07e --- /dev/null +++ b/tests/metavoxels/src/MetavoxelTests.h @@ -0,0 +1,113 @@ +// +// MetavoxelTests.h +// metavoxel-tests +// +// Created by Andrzej Kapolka on 2/7/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__MetavoxelTests__ +#define __interface__MetavoxelTests__ + +#include +#include + +#include + +class SequencedTestMessage; + +/// Tests various aspects of the metavoxel library. +class MetavoxelTests : public QCoreApplication { + Q_OBJECT + +public: + + MetavoxelTests(int& argc, char** argv); + + /// Performs our various tests. + /// \return true if any of the tests failed. + bool run(); +}; + +/// Represents a simulated endpoint. +class Endpoint : public QObject { + Q_OBJECT + +public: + + Endpoint(const QByteArray& datagramHeader); + + void setOther(Endpoint* other) { _other = other; } + + /// Perform a simulation step. + /// \return true if failure was detected + bool simulate(int iterationNumber); + +private slots: + + void sendDatagram(const QByteArray& datagram); + void handleHighPriorityMessage(const QVariant& message); + void readMessage(Bitstream& in); + void readReliableChannel(); + void readLowPriorityReliableChannel(); + +private: + + DatagramSequencer* _sequencer; + Endpoint* _other; + QList > _delayedDatagrams; + float _highPriorityMessagesToSend; + QVariantList _highPriorityMessagesSent; + QList _unreliableMessagesSent; + CircularBuffer _dataStreamed; + CircularBuffer _lowPriorityDataStreamed; +}; + +/// A simple test message. +class TestMessageA { + STREAMABLE + +public: + + STREAM bool foo; + STREAM int bar; + STREAM float baz; +}; + +DECLARE_STREAMABLE_METATYPE(TestMessageA) + +// Another simple test message. +class TestMessageB { + STREAMABLE + +public: + + STREAM QByteArray foo; +}; + +DECLARE_STREAMABLE_METATYPE(TestMessageB) + +// A test message that demonstrates inheritance and composition. +class TestMessageC : public TestMessageA { + STREAMABLE + +public: + + STREAM TestMessageB bong; +}; + +DECLARE_STREAMABLE_METATYPE(TestMessageC) + +/// Combines a sequence number with a submessage; used for testing unreliable transport. +class SequencedTestMessage { + STREAMABLE + +public: + + STREAM int sequenceNumber; + STREAM QVariant submessage; +}; + +DECLARE_STREAMABLE_METATYPE(SequencedTestMessage) + +#endif /* defined(__interface__MetavoxelTests__) */ diff --git a/tests/metavoxels/src/main.cpp b/tests/metavoxels/src/main.cpp new file mode 100644 index 0000000000..10bf786957 --- /dev/null +++ b/tests/metavoxels/src/main.cpp @@ -0,0 +1,14 @@ +// +// main.cpp +// metavoxel-tests +// +// Created by Andrzej Kapolka on 2/7/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. + +#include + +#include "MetavoxelTests.h" + +int main(int argc, char** argv) { + return MetavoxelTests(argc, argv).run(); +} diff --git a/tools/mtc/src/main.cpp b/tools/mtc/src/main.cpp index 77f0a069b5..050fe0e418 100644 --- a/tools/mtc/src/main.cpp +++ b/tools/mtc/src/main.cpp @@ -90,7 +90,7 @@ void generateOutput (QTextStream& out, const QList& streamables) { foreach (const Streamable& str, streamables) { const QString& name = str.clazz.name; - out << "Bitstream& operator<< (Bitstream& out, const " << name << "& obj) {\n"; + out << "Bitstream& operator<<(Bitstream& out, const " << name << "& obj) {\n"; foreach (const QString& base, str.clazz.bases) { out << " out << static_cast(obj);\n"; } @@ -100,7 +100,7 @@ void generateOutput (QTextStream& out, const QList& streamables) { out << " return out;\n"; out << "}\n"; - out << "Bitstream& operator>> (Bitstream& in, " << name << "& obj) {\n"; + out << "Bitstream& operator>>(Bitstream& in, " << name << "& obj) {\n"; foreach (const QString& base, str.clazz.bases) { out << " in >> static_cast<" << base << "&>(obj);\n"; } @@ -110,6 +110,58 @@ void generateOutput (QTextStream& out, const QList& streamables) { out << " return in;\n"; out << "}\n"; + out << "bool operator==(const " << name << "& first, const " << name << "& second) {\n"; + if (str.clazz.bases.isEmpty() && str.fields.isEmpty()) { + out << " return true"; + } else { + out << " return "; + bool first = true; + foreach (const QString& base, str.clazz.bases) { + if (!first) { + out << " &&\n"; + out << " "; + } + out << "static_cast<" << base << "&>(first) == static_cast<" << base << "&>(second)"; + first = false; + } + foreach (const QString& field, str.fields) { + if (!first) { + out << " &&\n"; + out << " "; + } + out << "first." << field << " == second." << field; + first = false; + } + } + out << ";\n"; + out << "}\n"; + + out << "bool operator!=(const " << name << "& first, const " << name << "& second) {\n"; + if (str.clazz.bases.isEmpty() && str.fields.isEmpty()) { + out << " return false"; + } else { + out << " return "; + bool first = true; + foreach (const QString& base, str.clazz.bases) { + if (!first) { + out << " ||\n"; + out << " "; + } + out << "static_cast<" << base << "&>(first) != static_cast<" << base << "&>(second)"; + first = false; + } + foreach (const QString& field, str.fields) { + if (!first) { + out << " ||\n"; + out << " "; + } + out << "first." << field << " != second." << field; + first = false; + } + } + out << ";\n"; + out << "}\n"; + out << "const int " << name << "::Type = registerStreamableMetaType<" << name << ">();\n\n"; } }