add compression and minimal data flags to AvatarData

This commit is contained in:
Brad Hefta-Gaub 2016-12-14 11:47:45 -08:00
parent 7189f766a2
commit 2290179281
4 changed files with 250 additions and 188 deletions

View file

@ -227,7 +227,7 @@ void MyAvatar::simulateAttachments(float deltaTime) {
// don't update attachments here, do it in harvestResultsFromPhysicsSimulation()
}
QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) {
QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll, bool sendMinimum, bool compressed) {
CameraMode mode = qApp->getCamera()->getMode();
_globalPosition = getPosition();
_globalBoundingBoxCorner.x = _characterController.getCapsuleRadius();
@ -238,12 +238,12 @@ QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) {
// fake the avatar position that is sent up to the AvatarMixer
glm::vec3 oldPosition = getPosition();
setPosition(getSkeletonPosition());
QByteArray array = AvatarData::toByteArray(cullSmallChanges, sendAll);
QByteArray array = AvatarData::toByteArray(cullSmallChanges, sendAll, sendMinimum, compressed);
// copy the correct position back
setPosition(oldPosition);
return array;
}
return AvatarData::toByteArray(cullSmallChanges, sendAll);
return AvatarData::toByteArray(cullSmallChanges, sendAll, sendMinimum, compressed);
}
void MyAvatar::centerBody() {

View file

@ -333,7 +333,7 @@ private:
glm::vec3 getWorldBodyPosition() const;
glm::quat getWorldBodyOrientation() const;
QByteArray toByteArray(bool cullSmallChanges, bool sendAll) override;
QByteArray toByteArray(bool cullSmallChanges, bool sendAll, bool sendMinimum, bool compressed) override;
void simulate(float deltaTime);
void updateFromTrackers(float deltaTime);
virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPositio) override;

View file

@ -181,7 +181,7 @@ void AvatarData::setHandPosition(const glm::vec3& handPosition) {
_handPosition = glm::inverse(getOrientation()) * (handPosition - getPosition());
}
QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) {
QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll, bool sendMinimum, bool compressed) {
// TODO: DRY this up to a shared method
// that can pack any type given the number of bytes
// and return the number of bytes to push the pointer
@ -199,208 +199,242 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) {
unsigned char* destinationBuffer = reinterpret_cast<unsigned char*>(avatarDataByteArray.data());
unsigned char* startPosition = destinationBuffer;
auto header = reinterpret_cast<AvatarDataPacket::Header*>(destinationBuffer);
header->position[0] = getLocalPosition().x;
header->position[1] = getLocalPosition().y;
header->position[2] = getLocalPosition().z;
header->globalPosition[0] = _globalPosition.x;
header->globalPosition[1] = _globalPosition.y;
header->globalPosition[2] = _globalPosition.z;
header->globalBoundingBoxCorner[0] = getPosition().x - _globalBoundingBoxCorner.x;
header->globalBoundingBoxCorner[1] = getPosition().y - _globalBoundingBoxCorner.y;
header->globalBoundingBoxCorner[2] = getPosition().z - _globalBoundingBoxCorner.z;
glm::vec3 bodyEulerAngles = glm::degrees(safeEulerAngles(getLocalOrientation()));
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 0), bodyEulerAngles.y);
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 1), bodyEulerAngles.x);
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 2), bodyEulerAngles.z);
packFloatRatioToTwoByte((uint8_t*)(&header->scale), getDomainLimitedScale());
header->lookAtPosition[0] = _headData->_lookAtPosition.x;
header->lookAtPosition[1] = _headData->_lookAtPosition.y;
header->lookAtPosition[2] = _headData->_lookAtPosition.z;
header->audioLoudness = _headData->_audioLoudness;
glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix();
packOrientationQuatToSixBytes(header->sensorToWorldQuat, glmExtractRotation(sensorToWorldMatrix));
glm::vec3 scale = extractScale(sensorToWorldMatrix);
packFloatScalarToSignedTwoByteFixed((uint8_t*)&header->sensorToWorldScale, scale.x, SENSOR_TO_WORLD_SCALE_RADIX);
header->sensorToWorldTrans[0] = sensorToWorldMatrix[3][0];
header->sensorToWorldTrans[1] = sensorToWorldMatrix[3][1];
header->sensorToWorldTrans[2] = sensorToWorldMatrix[3][2];
setSemiNibbleAt(header->flags, KEY_STATE_START_BIT, _keyState);
// hand state
bool isFingerPointing = _handState & IS_FINGER_POINTING_FLAG;
setSemiNibbleAt(header->flags, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG);
if (isFingerPointing) {
setAtBit(header->flags, HAND_STATE_FINGER_POINTING_BIT);
}
// faceshift state
if (_headData->_isFaceTrackerConnected) {
setAtBit(header->flags, IS_FACESHIFT_CONNECTED);
}
// eye tracker state
if (_headData->_isEyeTrackerConnected) {
setAtBit(header->flags, IS_EYE_TRACKER_CONNECTED);
}
// referential state
QUuid parentID = getParentID();
if (!parentID.isNull()) {
setAtBit(header->flags, HAS_REFERENTIAL);
}
destinationBuffer += sizeof(AvatarDataPacket::Header);
if (!parentID.isNull()) {
auto parentInfo = reinterpret_cast<AvatarDataPacket::ParentInfo*>(destinationBuffer);
QByteArray referentialAsBytes = parentID.toRfc4122();
memcpy(parentInfo->parentUUID, referentialAsBytes.data(), referentialAsBytes.size());
parentInfo->parentJointIndex = _parentJointIndex;
destinationBuffer += sizeof(AvatarDataPacket::ParentInfo);
// Leading flags, to indicate how much data is actually included in the packet...
UINT8 packetStateFlags = 0;
if (sendMinimum) {
setAtBit(packetStateFlags, AVATARDATA_FLAGS_MINIMUM);
}
// If it is connected, pack up the data
if (_headData->_isFaceTrackerConnected) {
auto faceTrackerInfo = reinterpret_cast<AvatarDataPacket::FaceTrackerInfo*>(destinationBuffer);
faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink;
faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink;
faceTrackerInfo->averageLoudness = _headData->_averageLoudness;
faceTrackerInfo->browAudioLift = _headData->_browAudioLift;
faceTrackerInfo->numBlendshapeCoefficients = _headData->_blendshapeCoefficients.size();
destinationBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo);
// followed by a variable number of float coefficients
memcpy(destinationBuffer, _headData->_blendshapeCoefficients.data(), _headData->_blendshapeCoefficients.size() * sizeof(float));
destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float);
// do we need this, or do we just always compress it?
if (compressed) {
setAtBit(packetStateFlags, AVATARDATA_FLAGS_COMPRESSED);
}
memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags));
destinationBuffer += sizeof(packetStateFlags);
QReadLocker readLock(&_jointDataLock);
if (!sendMinimum) {
auto header = reinterpret_cast<AvatarDataPacket::Header*>(destinationBuffer);
header->position[0] = getLocalPosition().x;
header->position[1] = getLocalPosition().y;
header->position[2] = getLocalPosition().z;
header->globalPosition[0] = _globalPosition.x;
header->globalPosition[1] = _globalPosition.y;
header->globalPosition[2] = _globalPosition.z;
header->globalBoundingBoxCorner[0] = getPosition().x - _globalBoundingBoxCorner.x;
header->globalBoundingBoxCorner[1] = getPosition().y - _globalBoundingBoxCorner.y;
header->globalBoundingBoxCorner[2] = getPosition().z - _globalBoundingBoxCorner.z;
// joint rotation data
*destinationBuffer++ = _jointData.size();
unsigned char* validityPosition = destinationBuffer;
unsigned char validity = 0;
int validityBit = 0;
glm::vec3 bodyEulerAngles = glm::degrees(safeEulerAngles(getLocalOrientation()));
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 0), bodyEulerAngles.y);
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 1), bodyEulerAngles.x);
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 2), bodyEulerAngles.z);
packFloatRatioToTwoByte((uint8_t*)(&header->scale), getDomainLimitedScale());
header->lookAtPosition[0] = _headData->_lookAtPosition.x;
header->lookAtPosition[1] = _headData->_lookAtPosition.y;
header->lookAtPosition[2] = _headData->_lookAtPosition.z;
header->audioLoudness = _headData->_audioLoudness;
#ifdef WANT_DEBUG
int rotationSentCount = 0;
unsigned char* beforeRotations = destinationBuffer;
#endif
glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix();
packOrientationQuatToSixBytes(header->sensorToWorldQuat, glmExtractRotation(sensorToWorldMatrix));
glm::vec3 scale = extractScale(sensorToWorldMatrix);
packFloatScalarToSignedTwoByteFixed((uint8_t*)&header->sensorToWorldScale, scale.x, SENSOR_TO_WORLD_SCALE_RADIX);
header->sensorToWorldTrans[0] = sensorToWorldMatrix[3][0];
header->sensorToWorldTrans[1] = sensorToWorldMatrix[3][1];
header->sensorToWorldTrans[2] = sensorToWorldMatrix[3][2];
_lastSentJointData.resize(_jointData.size());
setSemiNibbleAt(header->flags, KEY_STATE_START_BIT, _keyState);
// hand state
bool isFingerPointing = _handState & IS_FINGER_POINTING_FLAG;
setSemiNibbleAt(header->flags, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG);
if (isFingerPointing) {
setAtBit(header->flags, HAND_STATE_FINGER_POINTING_BIT);
}
// faceshift state
if (_headData->_isFaceTrackerConnected) {
setAtBit(header->flags, IS_FACESHIFT_CONNECTED);
}
// eye tracker state
if (_headData->_isEyeTrackerConnected) {
setAtBit(header->flags, IS_EYE_TRACKER_CONNECTED);
}
// referential state
QUuid parentID = getParentID();
if (!parentID.isNull()) {
setAtBit(header->flags, HAS_REFERENTIAL);
}
destinationBuffer += sizeof(AvatarDataPacket::Header);
for (int i=0; i < _jointData.size(); i++) {
const JointData& data = _jointData[i];
if (sendAll || _lastSentJointData[i].rotation != data.rotation) {
if (sendAll ||
!cullSmallChanges ||
fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) {
if (data.rotationSet) {
validity |= (1 << validityBit);
#ifdef WANT_DEBUG
rotationSentCount++;
#endif
if (!parentID.isNull()) {
auto parentInfo = reinterpret_cast<AvatarDataPacket::ParentInfo*>(destinationBuffer);
QByteArray referentialAsBytes = parentID.toRfc4122();
memcpy(parentInfo->parentUUID, referentialAsBytes.data(), referentialAsBytes.size());
parentInfo->parentJointIndex = _parentJointIndex;
destinationBuffer += sizeof(AvatarDataPacket::ParentInfo);
}
// If it is connected, pack up the data
if (_headData->_isFaceTrackerConnected) {
auto faceTrackerInfo = reinterpret_cast<AvatarDataPacket::FaceTrackerInfo*>(destinationBuffer);
faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink;
faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink;
faceTrackerInfo->averageLoudness = _headData->_averageLoudness;
faceTrackerInfo->browAudioLift = _headData->_browAudioLift;
faceTrackerInfo->numBlendshapeCoefficients = _headData->_blendshapeCoefficients.size();
destinationBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo);
// followed by a variable number of float coefficients
memcpy(destinationBuffer, _headData->_blendshapeCoefficients.data(), _headData->_blendshapeCoefficients.size() * sizeof(float));
destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float);
}
QReadLocker readLock(&_jointDataLock);
// joint rotation data
*destinationBuffer++ = _jointData.size();
unsigned char* validityPosition = destinationBuffer;
unsigned char validity = 0;
int validityBit = 0;
#ifdef WANT_DEBUG
int rotationSentCount = 0;
unsigned char* beforeRotations = destinationBuffer;
#endif
_lastSentJointData.resize(_jointData.size());
for (int i=0; i < _jointData.size(); i++) {
const JointData& data = _jointData[i];
if (sendAll || _lastSentJointData[i].rotation != data.rotation) {
if (sendAll ||
!cullSmallChanges ||
fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) {
if (data.rotationSet) {
validity |= (1 << validityBit);
#ifdef WANT_DEBUG
rotationSentCount++;
#endif
}
}
}
}
if (++validityBit == BITS_IN_BYTE) {
*destinationBuffer++ = validity;
validityBit = validity = 0;
}
}
if (validityBit != 0) {
*destinationBuffer++ = validity;
}
validityBit = 0;
validity = *validityPosition++;
for (int i = 0; i < _jointData.size(); i ++) {
const JointData& data = _jointData[i];
if (validity & (1 << validityBit)) {
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation);
}
if (++validityBit == BITS_IN_BYTE) {
validityBit = 0;
validity = *validityPosition++;
}
}
// joint translation data
validityPosition = destinationBuffer;
validity = 0;
validityBit = 0;
#ifdef WANT_DEBUG
int translationSentCount = 0;
unsigned char* beforeTranslations = destinationBuffer;
#endif
float maxTranslationDimension = 0.0;
for (int i=0; i < _jointData.size(); i++) {
const JointData& data = _jointData[i];
if (sendAll || _lastSentJointData[i].translation != data.translation) {
if (sendAll ||
!cullSmallChanges ||
glm::distance(data.translation, _lastSentJointData[i].translation) > AVATAR_MIN_TRANSLATION) {
if (data.translationSet) {
validity |= (1 << validityBit);
#ifdef WANT_DEBUG
translationSentCount++;
#endif
maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension);
}
if (++validityBit == BITS_IN_BYTE) {
*destinationBuffer++ = validity;
validityBit = validity = 0;
}
}
if (++validityBit == BITS_IN_BYTE) {
if (validityBit != 0) {
*destinationBuffer++ = validity;
validityBit = validity = 0;
}
}
if (validityBit != 0) {
*destinationBuffer++ = validity;
}
validityBit = 0;
validity = *validityPosition++;
for (int i = 0; i < _jointData.size(); i ++) {
const JointData& data = _jointData[i];
if (validity & (1 << validityBit)) {
destinationBuffer +=
packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX);
validityBit = 0;
validity = *validityPosition++;
for (int i = 0; i < _jointData.size(); i ++) {
const JointData& data = _jointData[i];
if (validity & (1 << validityBit)) {
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation);
}
if (++validityBit == BITS_IN_BYTE) {
validityBit = 0;
validity = *validityPosition++;
}
}
if (++validityBit == BITS_IN_BYTE) {
validityBit = 0;
validity = *validityPosition++;
// joint translation data
validityPosition = destinationBuffer;
validity = 0;
validityBit = 0;
#ifdef WANT_DEBUG
int translationSentCount = 0;
unsigned char* beforeTranslations = destinationBuffer;
#endif
float maxTranslationDimension = 0.0;
for (int i=0; i < _jointData.size(); i++) {
const JointData& data = _jointData[i];
if (sendAll || _lastSentJointData[i].translation != data.translation) {
if (sendAll ||
!cullSmallChanges ||
glm::distance(data.translation, _lastSentJointData[i].translation) > AVATAR_MIN_TRANSLATION) {
if (data.translationSet) {
validity |= (1 << validityBit);
#ifdef WANT_DEBUG
translationSentCount++;
#endif
maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension);
maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension);
}
}
}
if (++validityBit == BITS_IN_BYTE) {
*destinationBuffer++ = validity;
validityBit = validity = 0;
}
}
if (validityBit != 0) {
*destinationBuffer++ = validity;
}
validityBit = 0;
validity = *validityPosition++;
for (int i = 0; i < _jointData.size(); i ++) {
const JointData& data = _jointData[i];
if (validity & (1 << validityBit)) {
destinationBuffer +=
packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX);
}
if (++validityBit == BITS_IN_BYTE) {
validityBit = 0;
validity = *validityPosition++;
}
}
// faux joints
Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(),
TRANSLATION_COMPRESSION_RADIX);
Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(),
TRANSLATION_COMPRESSION_RADIX);
#ifdef WANT_DEBUG
if (sendAll) {
qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll
<< "rotations:" << rotationSentCount << "translations:" << translationSentCount
<< "largest:" << maxTranslationDimension
<< "size:"
<< (beforeRotations - startPosition) << "+"
<< (beforeTranslations - beforeRotations) << "+"
<< (destinationBuffer - beforeTranslations) << "="
<< (destinationBuffer - startPosition);
}
#endif
}
// faux joints
Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(),
TRANSLATION_COMPRESSION_RADIX);
Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation());
destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(),
TRANSLATION_COMPRESSION_RADIX);
// NOTE: first byte of array should always be uncompressed
#ifdef WANT_DEBUG
if (sendAll) {
qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll
<< "rotations:" << rotationSentCount << "translations:" << translationSentCount
<< "largest:" << maxTranslationDimension
<< "size:"
<< (beforeRotations - startPosition) << "+"
<< (beforeTranslations - beforeRotations) << "+"
<< (destinationBuffer - beforeTranslations) << "="
<< (destinationBuffer - startPosition);
if (compressed) {
static const int SKIP_PACKET_FLAGS = 1;
static const int COMPRESSION_LEVEL = 9;
QByteArray uncompressedPortion = avatarDataByteArray.mid(SKIP_PACKET_FLAGS,
(destinationBuffer - startPosition) - SKIP_PACKET_FLAGS);
QByteArray compressedPortion = qCompress(uncompressedPortion, COMPRESSION_LEVEL);
QByteArray flagsAndCompressed;
flagsAndCompressed.append(packetStateFlags);
flagsAndCompressed.append(compressedPortion);
//qDebug() << __FUNCTION__ << "compressing data was:" << (uncompressedPortion.size() + SKIP_PACKET_FLAGS) << "now:" << flagsAndCompressed.size();
return flagsAndCompressed;
}
#endif
// entire buffer is uncompressed
return avatarDataByteArray.left(destinationBuffer - startPosition);
}
@ -474,9 +508,30 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_headData = new HeadData(this);
}
UINT8 packetStateFlags = buffer.at(0);
bool minimumSent = oneAtBit(packetStateFlags, AVATARDATA_FLAGS_MINIMUM);
bool packetIsCompressed = oneAtBit(packetStateFlags, AVATARDATA_FLAGS_COMPRESSED);
// if this is the minimum, then it only includes the flags
if (minimumSent) {
int numBytesRead = sizeof(packetStateFlags);
_averageBytesReceived.updateAverage(numBytesRead);
return numBytesRead;
}
QByteArray uncompressBuffer;
const unsigned char* startPosition = reinterpret_cast<const unsigned char*>(buffer.data());
const unsigned char* endPosition = startPosition + buffer.size();
const unsigned char* sourceBuffer = startPosition;
if (packetIsCompressed) {
uncompressBuffer = qUncompress(buffer.right(buffer.size() - sizeof(packetStateFlags)));
startPosition = reinterpret_cast<const unsigned char*>(uncompressBuffer.data());
endPosition = startPosition + uncompressBuffer.size();
sourceBuffer = startPosition;
//qDebug() << __FUNCTION__ << "uncompressing compressed data was:" << buffer.size() << "now:" << uncompressBuffer.size();
}
quint64 now = usecTimestampNow();
PACKET_READ_CHECK(Header, sizeof(AvatarDataPacket::Header));
@ -679,8 +734,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
#ifdef WANT_DEBUG
if (numValidJointRotations > 15) {
qDebug() << "RECEIVING -- rotations:" << numValidJointRotations
<< "translations:" << numValidJointTranslations
<< "size:" << (int)(sourceBuffer - startPosition);
<< "translations:" << numValidJointTranslations
<< "size:" << (int)(sourceBuffer - startPosition);
}
#endif

View file

@ -106,6 +106,13 @@ const char LEFT_HAND_POINTING_FLAG = 1;
const char RIGHT_HAND_POINTING_FLAG = 2;
const char IS_FINGER_POINTING_FLAG = 4;
// AvatarData state flags - we store the details about the packet encoding in the first byte,
// before the "header" structure
const char AVATARDATA_FLAGS_MINIMUM = 0;
const char AVATARDATA_FLAGS_COMPRESSED = 1;
static const float MAX_AVATAR_SCALE = 1000.0f;
static const float MIN_AVATAR_SCALE = .005f;
@ -201,7 +208,7 @@ public:
glm::vec3 getHandPosition() const;
void setHandPosition(const glm::vec3& handPosition);
virtual QByteArray toByteArray(bool cullSmallChanges, bool sendAll);
virtual QByteArray toByteArray(bool cullSmallChanges, bool sendAll, bool sendMinimum = false, bool compressed = true);
virtual void doneEncoding(bool cullSmallChanges);
/// \return true if an error should be logged