WiimoteEmu: Rewrite serialization of DesiredWiimoteState to optimize for network bandwidth.

This commit is contained in:
Admiral H. Curtiss 2022-09-30 04:42:08 +02:00
parent ae4b89441c
commit e137f7f878
No known key found for this signature in database
GPG key ID: F051B4C4044F33FB

View file

@ -8,6 +8,7 @@
#include <type_traits>
#include <variant>
#include "Common/BitUtils.h"
#include "Common/CommonTypes.h"
#include "Core/HW/WiimoteEmu/DesiredWiimoteState.h"
@ -26,55 +27,75 @@ namespace WiimoteEmu
{
SerializedWiimoteState SerializeDesiredState(const DesiredWiimoteState& state)
{
// If no motion or extension data is present, we can serialize into a much smaller representation,
// which reduces data sent over the network.
const bool is_buttons_only =
(state.acceleration == DesiredWiimoteState::DEFAULT_ACCELERATION &&
state.camera_points == DesiredWiimoteState::DEFAULT_CAMERA &&
!state.motion_plus.has_value() && state.extension.data.index() == ExtensionNumber::NONE);
const u8 has_buttons = (state.buttons.hex & WiimoteCommon::ButtonData::BUTTON_MASK) != 0 ? 1 : 0;
const u8 has_accel = state.acceleration != DesiredWiimoteState::DEFAULT_ACCELERATION ? 1 : 0;
const u8 has_camera = state.camera_points != DesiredWiimoteState::DEFAULT_CAMERA ? 1 : 0;
const u8 has_motion_plus = state.motion_plus.has_value() ? 1 : 0;
// Right now we support < 16 extensions so the info which extension is in use fits into 4 bits.
// This allows 'empty' packets to be a single byte, which is very nice for reducing bandwidth.
// If we ever support 16 or more we have to redesign this a bit; ideally use a variable-length
// encoding so that typical extensions (None, Nunchuk, Classic Controller) still fit into the
// initial 4 bits.
static_assert(std::variant_size_v<DesiredExtensionState::ExtensionData> <= (1 << 4));
const u8 extension = u8(state.extension.data.index());
SerializedWiimoteState s;
s.length = 0;
s.data[s.length++] =
u8((state.buttons.a) | (state.buttons.b << 1) | (state.buttons.plus << 2) |
(state.buttons.minus << 3) | (state.buttons.one << 4) | (state.buttons.two << 5) |
(state.buttons.home << 6) | ((is_buttons_only ? 0 : 1) << 7));
const u8 dpad = u8((state.buttons.up) | (state.buttons.down << 1) | (state.buttons.left << 2) |
(state.buttons.right << 3));
if (is_buttons_only)
s.data[s.length++] = u8(has_buttons | (has_accel << 1) | (has_camera << 2) |
(has_motion_plus << 3) | (extension << 4));
if (has_buttons)
{
// There's four unused bits here, which could be used to further optimize for common situations.
const u8 buttons = u8((state.buttons.a) | (state.buttons.b << 1) | (state.buttons.plus << 2) |
(state.buttons.minus << 3) | (state.buttons.one << 4) |
(state.buttons.two << 5) | (state.buttons.home << 6));
const u8 dpad = u8((state.buttons.up) | (state.buttons.down << 1) | (state.buttons.left << 2) |
(state.buttons.right << 3));
s.data[s.length++] = buttons;
s.data[s.length++] = dpad;
return s;
}
const u16 accel_x = state.acceleration.value.x; // 10 bits
const u16 accel_y = state.acceleration.value.y; // 10 bits
const u16 accel_z = state.acceleration.value.z; // 10 bits
const u16 camera_p0_x = state.camera_points[0].position.x; // 10 bits
const u16 camera_p0_y = state.camera_points[0].position.y; // 10 bits
const u16 camera_p1_x = state.camera_points[1].position.x; // 10 bits
const u16 camera_p1_y = state.camera_points[1].position.y; // 10 bits
const u8 camera_p0_size = state.camera_points[0].size; // 4 bits
const u8 camera_p1_size = state.camera_points[1].size; // 4 bits
static_assert(std::variant_size_v<DesiredExtensionState::ExtensionData> < (1 << 5));
const u8 extension = u8(state.extension.data.index()); // maximum of 5 bits
const u8 motion_plus = state.motion_plus.has_value() ? 1 : 0;
s.data[s.length++] = u8(dpad | (camera_p0_size << 4));
s.data[s.length++] = u8(camera_p0_x);
s.data[s.length++] = u8(camera_p0_y);
s.data[s.length++] = u8(camera_p1_x);
s.data[s.length++] = u8(camera_p1_y);
s.data[s.length++] = u8(((camera_p0_x >> 8) & 3) | (((camera_p0_y >> 8) & 3) << 2) |
(((camera_p1_x >> 8) & 3) << 4) | (((camera_p1_y >> 8) & 3) << 6));
s.data[s.length++] =
u8(((accel_x >> 8) & 3) | (((accel_y >> 8) & 3) << 2) | (camera_p1_size << 4));
s.data[s.length++] = u8(accel_x);
s.data[s.length++] = u8(accel_y);
s.data[s.length++] = u8(accel_z);
s.data[s.length++] = u8(((accel_z >> 8) & 3) | (motion_plus << 2) | (extension << 3));
if (has_accel)
{
const u16 accel_x = state.acceleration.value.x; // 10 bits
const u16 accel_y = state.acceleration.value.y; // 9 bits (ignore lowest bit)
const u16 accel_z = state.acceleration.value.z; // 9 bits (ignore lowest bit)
const u8 accel_x_high = u8(accel_x >> 2);
const u8 accel_y_high = u8(accel_y >> 2);
const u8 accel_z_high = u8(accel_z >> 2);
const u8 accel_low = u8((accel_x & 0b11) | (Common::ExtractBit<1>(accel_y) << 2) |
(Common::ExtractBit<1>(accel_z) << 3));
if (motion_plus)
if (has_buttons)
{
// can use the high bits of the dpad field from buttons
s.data[s.length - 1] |= u8(accel_low << 4);
}
else
{
s.data[s.length++] = u8(accel_low << 4);
}
s.data[s.length++] = accel_x_high;
s.data[s.length++] = accel_y_high;
s.data[s.length++] = accel_z_high;
}
if (has_camera)
{
for (size_t i = 0; i < 2; ++i)
{
const u16 camera_x = state.camera_points[i].position.x; // 10 bits
const u16 camera_y = state.camera_points[i].position.y; // 10 bits
const u8 camera_size = state.camera_points[i].size; // 4 bits
s.data[s.length++] = u8((camera_x & 0b11) | ((camera_y & 0b11) << 2) | (camera_size << 4));
s.data[s.length++] = u8(camera_x >> 2);
s.data[s.length++] = u8(camera_y >> 2);
}
}
if (has_motion_plus)
{
const u16 pitch_slow = state.motion_plus->is_slow.x ? 1 : 0;
const u16 roll_slow = state.motion_plus->is_slow.y ? 1 : 0;
@ -83,11 +104,11 @@ SerializedWiimoteState SerializeDesiredState(const DesiredWiimoteState& state)
const u16 roll_value = state.motion_plus->gyro.value.y; // 14 bits
const u16 yaw_value = state.motion_plus->gyro.value.z; // 14 bits
s.data[s.length++] = u8(pitch_value);
s.data[s.length++] = u8((pitch_value >> 8) | (pitch_slow << 7));
s.data[s.length++] = u8(((pitch_value >> 8) & 0x3f) | (pitch_slow << 7));
s.data[s.length++] = u8(roll_value);
s.data[s.length++] = u8((roll_value >> 8) | (roll_slow << 7));
s.data[s.length++] = u8(((roll_value >> 8) & 0x3f) | (roll_slow << 7));
s.data[s.length++] = u8(yaw_value);
s.data[s.length++] = u8((yaw_value >> 8) | (yaw_slow << 7));
s.data[s.length++] = u8(((yaw_value >> 8) & 0x3f) | (yaw_slow << 7));
}
if (extension)
@ -130,93 +151,174 @@ bool DeserializeDesiredState(DesiredWiimoteState* state, const SerializedWiimote
state->motion_plus = std::nullopt;
state->extension.data = std::monostate();
if (serialized.length < 2)
if (serialized.length < 1)
{
// can't be valid
return false;
}
const auto& d = serialized.data;
state->buttons.a = d[0] & 1;
state->buttons.b = (d[0] >> 1) & 1;
state->buttons.plus = (d[0] >> 2) & 1;
state->buttons.minus = (d[0] >> 3) & 1;
state->buttons.one = (d[0] >> 4) & 1;
state->buttons.two = (d[0] >> 5) & 1;
state->buttons.home = (d[0] >> 6) & 1;
const u8 has_motion_or_extension_data = (d[0] >> 7) & 1;
state->buttons.up = d[1] & 1;
state->buttons.down = (d[1] >> 1) & 1;
state->buttons.left = (d[1] >> 2) & 1;
state->buttons.right = (d[1] >> 3) & 1;
const u8 has_buttons = d[0] & 1;
const u8 has_accel = (d[0] >> 1) & 1;
const u8 has_camera = (d[0] >> 2) & 1;
const u8 has_motion_plus = (d[0] >> 3) & 1;
const u8 extension = (d[0] >> 4);
if (!has_motion_or_extension_data)
if (extension >= ExtensionNumber::MAX)
{
// is button-only state, we're done
return true;
}
if (serialized.length < 12)
{
// if it's not a button-only state it needs to have at least 12 bytes for the basic Wiimote data
// invalid extension
return false;
}
state->camera_points[0].size = d[1] >> 4;
state->camera_points[0].position.x = d[2] | ((d[6] & 3) << 8);
state->camera_points[0].position.y = d[3] | (((d[6] >> 2) & 3) << 8);
state->camera_points[1].position.x = d[4] | (((d[6] >> 4) & 3) << 8);
state->camera_points[1].position.y = d[5] | (((d[6] >> 6) & 3) << 8);
state->camera_points[1].size = d[7] >> 4;
state->acceleration.value.x = d[8] | ((d[7] & 3) << 8);
state->acceleration.value.y = d[9] | (((d[7] >> 2) & 3) << 8);
state->acceleration.value.z = d[10] | ((d[11] & 3) << 8);
const u8 has_motion_plus = (d[11] >> 2) & 1;
const u8 extension = d[11] >> 3;
const size_t expected_size = [&]() {
size_t s = 1;
if (has_buttons && has_accel)
s += 5;
else if (has_buttons)
s += 2;
else if (has_accel)
s += 4;
if (has_camera)
s += 6;
if (has_motion_plus)
s += 6;
switch (extension)
{
case ExtensionNumber::NONE:
break;
case ExtensionNumber::NUNCHUK:
s += sizeof(Nunchuk::DataFormat);
break;
case ExtensionNumber::CLASSIC:
s += sizeof(Classic::DataFormat);
break;
case ExtensionNumber::GUITAR:
s += sizeof(Guitar::DataFormat);
break;
case ExtensionNumber::DRUMS:
s += sizeof(Drums::DesiredState);
break;
case ExtensionNumber::TURNTABLE:
s += sizeof(Turntable::DataFormat);
break;
case ExtensionNumber::UDRAW_TABLET:
s += sizeof(UDrawTablet::DataFormat);
break;
case ExtensionNumber::DRAWSOME_TABLET:
s += sizeof(DrawsomeTablet::DataFormat);
break;
case ExtensionNumber::TATACON:
s += sizeof(TaTaCon::DataFormat);
break;
case ExtensionNumber::SHINKANSEN:
s += sizeof(Shinkansen::DesiredState);
break;
default:
break;
}
return s;
}();
if (has_motion_plus && serialized.length < 18)
if (serialized.length != expected_size)
{
// can't hold motion plus data
// invalid length
return false;
}
size_t pos = 1;
if (has_buttons)
{
state->buttons.a = d[pos] & 1;
state->buttons.b = (d[pos] >> 1) & 1;
state->buttons.plus = (d[pos] >> 2) & 1;
state->buttons.minus = (d[pos] >> 3) & 1;
state->buttons.one = (d[pos] >> 4) & 1;
state->buttons.two = (d[pos] >> 5) & 1;
state->buttons.home = (d[pos] >> 6) & 1;
state->buttons.up = d[pos + 1] & 1;
state->buttons.down = (d[pos + 1] >> 1) & 1;
state->buttons.left = (d[pos + 1] >> 2) & 1;
state->buttons.right = (d[pos + 1] >> 3) & 1;
pos += 2;
}
if (has_accel)
{
if (has_buttons)
pos -= 1;
const u8 accel_low = d[pos] >> 4;
const u8 accel_x_high = d[pos + 1];
const u8 accel_y_high = d[pos + 2];
const u8 accel_z_high = d[pos + 3];
state->acceleration.value.x = (accel_x_high << 2) | (accel_low & 0b11);
state->acceleration.value.y =
Common::ExpandValue<u16>((accel_y_high << 1) | Common::ExtractBit<2>(accel_low), 1);
state->acceleration.value.z =
Common::ExpandValue<u16>((accel_z_high << 1) | Common::ExtractBit<3>(accel_low), 1);
pos += 4;
}
if (has_camera)
{
for (size_t i = 0; i < 2; ++i)
{
const u8 camera_misc = d[pos];
const u8 camera_x_high = d[pos + 1];
const u8 camera_y_high = d[pos + 2];
const u16 camera_x = (camera_x_high << 2) | (camera_misc & 0b11);
const u16 camera_y = (camera_y_high << 2) | ((camera_misc >> 2) & 0b11);
const u8 camera_size = camera_misc >> 4;
if (camera_y < CameraLogic::CAMERA_RES_Y)
{
state->camera_points[i] = CameraPoint({camera_x, camera_y}, camera_size);
}
else
{
// indicates an invalid camera point
state->camera_points[i] = CameraPoint();
}
pos += 3;
}
}
if (has_motion_plus)
{
const u16 pitch_value = d[12] | ((d[13] & 0x3f) << 8);
const u16 roll_value = d[14] | ((d[15] & 0x3f) << 8);
const u16 yaw_value = d[16] | ((d[17] & 0x3f) << 8);
const bool pitch_slow = (d[13] & 0x80) != 0;
const bool roll_slow = (d[15] & 0x80) != 0;
const bool yaw_slow = (d[17] & 0x80) != 0;
const u16 pitch_value = d[pos] | ((d[pos + 1] & 0x3f) << 8);
const u16 roll_value = d[pos + 2] | ((d[pos + 3] & 0x3f) << 8);
const u16 yaw_value = d[pos + 4] | ((d[pos + 5] & 0x3f) << 8);
const bool pitch_slow = (d[pos + 1] & 0x80) != 0;
const bool roll_slow = (d[pos + 3] & 0x80) != 0;
const bool yaw_slow = (d[pos + 5] & 0x80) != 0;
state->motion_plus = MotionPlus::DataFormat::Data{
MotionPlus::DataFormat::GyroRawValue{
MotionPlus::DataFormat::GyroType(pitch_value, roll_value, yaw_value)},
MotionPlus::DataFormat::SlowType(pitch_slow, roll_slow, yaw_slow)};
pos += 6;
}
const size_t offset = has_motion_plus ? 18 : 12;
switch (extension)
{
case ExtensionNumber::NONE:
return true;
case ExtensionNumber::NUNCHUK:
return DeserializeExtensionState<Nunchuk::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<Nunchuk::DataFormat>(state, serialized, pos);
case ExtensionNumber::CLASSIC:
return DeserializeExtensionState<Classic::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<Classic::DataFormat>(state, serialized, pos);
case ExtensionNumber::GUITAR:
return DeserializeExtensionState<Guitar::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<Guitar::DataFormat>(state, serialized, pos);
case ExtensionNumber::DRUMS:
return DeserializeExtensionState<Drums::DesiredState>(state, serialized, offset);
return DeserializeExtensionState<Drums::DesiredState>(state, serialized, pos);
case ExtensionNumber::TURNTABLE:
return DeserializeExtensionState<Turntable::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<Turntable::DataFormat>(state, serialized, pos);
case ExtensionNumber::UDRAW_TABLET:
return DeserializeExtensionState<UDrawTablet::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<UDrawTablet::DataFormat>(state, serialized, pos);
case ExtensionNumber::DRAWSOME_TABLET:
return DeserializeExtensionState<DrawsomeTablet::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<DrawsomeTablet::DataFormat>(state, serialized, pos);
case ExtensionNumber::TATACON:
return DeserializeExtensionState<TaTaCon::DataFormat>(state, serialized, offset);
return DeserializeExtensionState<TaTaCon::DataFormat>(state, serialized, pos);
case ExtensionNumber::SHINKANSEN:
return DeserializeExtensionState<Shinkansen::DesiredState>(state, serialized, offset);
return DeserializeExtensionState<Shinkansen::DesiredState>(state, serialized, pos);
default:
break;
}