From 2effb64ea4e196391fb70f210a2b685281e56969 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Tue, 17 Feb 2026 14:07:21 +0200 Subject: [PATCH 01/12] Add Velocity transmission toggle and calculation logic. --- .../src/main/java/dev/slimevr/VRServer.kt | 18 +++++ .../main/java/dev/slimevr/config/VRConfig.kt | 14 ++++ .../java/dev/slimevr/config/VelocityConfig.kt | 10 +++ .../processor/skeleton/HumanSkeleton.kt | 1 + .../dev/slimevr/tracking/trackers/Tracker.kt | 71 +++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index f5e5cf6a71..1a0f7380ad 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -186,6 +186,21 @@ class VRServer @JvmOverloads constructor( instance = this } + /** + * TODO: The design of this method is chosen for future expandability in case we want to have more complex velocity policies that depend on tracker properties or other config values. + * Initiates the velocity policy application process for the specified [Tracker] (or all trackers if null). + * + * This method serves as the initiator. + * The actual policy logic is not handled here, + * but is delegated to [dev.slimevr.config.VRConfig.applyVelocityPolicy]. + */ + private fun applyVelocityPolicyTo(tracker: Tracker?) { + val targets = tracker?.let { listOf(it) } ?: trackers + for (t in targets) { + configManager.vrConfig.applyVelocityPolicy(t) + } + } + fun hasBridge(bridgeClass: Class): Boolean { for (bridge in bridges) { if (bridgeClass.isAssignableFrom(bridge.javaClass)) { @@ -231,6 +246,9 @@ class VRServer @JvmOverloads constructor( refreshTrackersDriftCompensationEnabled() configManager.vrConfig.writeTrackerConfig(tracker) configManager.saveConfig() + + // Requires a fresh TrackerConfig on Update, so executed after we save a new state. + applyVelocityPolicyTo(tracker) } } diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index 9ef33f5e00..dd100f55ef 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -58,6 +58,8 @@ class VRConfig { val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig() + var velocityConfig: VelocityConfig = VelocityConfig() + val vrcConfig: VRCConfig = VRCConfig() init { @@ -104,6 +106,17 @@ class VRConfig { return config } + /** + * Applies the velocity policy to the given [Tracker]. + * + * This method determines whether the tracker—be it a physical sensor or a computed virtual device—is + * permitted to calculate and broadcast velocity data. + * It resolves the active [VelocityConfig] value, updating [Tracker.allowVelocity] accordingly. + */ + fun applyVelocityPolicy(tracker: Tracker) { + tracker.allowVelocity = velocityConfig.sendDerivedVelocity + } + fun readTrackerConfig(tracker: Tracker) { if (tracker.userEditable) { val config = getTracker(tracker) @@ -119,6 +132,7 @@ class VRConfig { .readFilteringConfig(filters, tracker.getRotation()) } } + applyVelocityPolicy(tracker) } fun writeTrackerConfig(tracker: Tracker?) { diff --git a/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt b/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt new file mode 100644 index 0000000000..a6516b1e24 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt @@ -0,0 +1,10 @@ +package dev.slimevr.config + +/** + * Allows to enable/disable sending of optional derived velocity data via Protobuf. + * Enables Natural Locomotion Support + * May create overprediction in certain titles causing excessive jitter when moving upper body. + */ +class VelocityConfig { + var sendDerivedVelocity: Boolean = false // Disables derived velocity for all trackers. Driver zeroes out velocity if nothing is returned in protobuf message. +} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index 2baef0bcd5..d64e8a4930 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -1171,6 +1171,7 @@ class HumanSkeleton( it.position = trackerBone.getTailPosition() it.setRotation(trackerBone.getGlobalRotation() * trackerBone.rotationOffset.inv()) it.dataTick() + it.updateDerivedVelocity(System.nanoTime()) } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index a3d03ea10b..ae2a64edea 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -75,6 +75,11 @@ class Tracker @JvmOverloads constructor( */ val allowMounting: Boolean = false, + /** + * If true, the tracker will send Derived Velocity. + */ + var allowVelocity: Boolean = false, + val isHmd: Boolean = false, /** @@ -111,7 +116,19 @@ class Tracker @JvmOverloads constructor( // IMU: +z forward, +x left, +y up // SlimeVR: +z backward, +x right, +y up private var _acceleration = Vector3.NULL + private var _velocity = Vector3.NULL private var _magVector = Vector3.NULL + + /** + * Velocity state server-side differentiation based on sent poses + */ + private data class VelocityState( + var prevTimeNs: Long = 0L, + var prevPos: Vector3 = Vector3(0f, 0f, 0f), + ) + + private var velocityState: VelocityState? = null + var position = Vector3.NULL val resetsHandler: TrackerResetsHandler = TrackerResetsHandler(this) val filteringHandler: TrackerFilteringHandler = TrackerFilteringHandler() @@ -345,6 +362,42 @@ class Tracker @JvmOverloads constructor( } } + /** + * Updates the derived velocity of the tracker by differentiating position over time. + * + * This method enforces the [allowVelocity] policy and checks for valid position data before + * proceeding. If conditions are met, it calculates velocity based on the displacement since the + * last update, applying a sanity check on the time delta to filter out noise and ensure data stability. + * + */ + fun updateDerivedVelocity(nowNs: Long) { + if (!allowVelocity || !hasPosition) { + velocityState = null + _velocity = Vector3.NULL + return + } + + val pos = position + val state = velocityState ?: VelocityState().also { + velocityState = it + } + + if (state.prevTimeNs != 0L) { + val dt = (nowNs - state.prevTimeNs) * 1e-9 + if (dt in 1e-4..0.25) { + _velocity = Vector3( + ((pos.x - state.prevPos.x) / dt).toFloat(), + ((pos.y - state.prevPos.y) / dt).toFloat(), + ((pos.z - state.prevPos.z) / dt).toFloat(), + ) + } else { + _velocity = Vector3.NULL + } + } + state.prevTimeNs = nowNs + state.prevPos = pos + } + /** * Gets the identity-adjusted tracker rotation after the resetsHandler's corrections * (identity reset, drift and identity mounting). @@ -421,6 +474,24 @@ class Tracker @JvmOverloads constructor( this._acceleration = vec } + /** + * Sets the derived velocity of the tracker. + */ + fun setVelocity(vec: Vector3) { + this._velocity = if (allowVelocity) vec else Vector3.NULL + } + + /** + * Gets the derived velocity of the tracker. + */ + fun getVelocity(): Vector3 = _velocity + + /** + * True if the tracker has valid velocity data. + */ + val hasVelocity: Boolean + get() = allowVelocity && _velocity != Vector3.NULL + /** * True if the raw rotation is coming directly from an IMU (no cameras or lighthouses) * For example, flex sensor trackers are not considered as IMU trackers (see TrackerDataType) From 5b821ead4daeba32047523838437032ae49392d7 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Tue, 17 Feb 2026 14:33:10 +0200 Subject: [PATCH 02/12] Add Velocity data send via ProtobufBridge. Update ProtobufMessages to latest definition from the Driver's main branch. --- .../desktop/platform/ProtobufBridge.kt | 42 +- .../desktop/platform/ProtobufMessages.java | 445 ++++++++++++++++-- 2 files changed, 446 insertions(+), 41 deletions(-) diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt index e6b60aaf52..9a116b5efd 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt @@ -82,6 +82,7 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte target.position = source.position target.setRotation(source.getRotation()) target.status = source.status + target.setVelocity(source.getVelocity()) target.batteryLevel = source.batteryLevel target.batteryVoltage = source.batteryVoltage target.dataTick() @@ -103,23 +104,38 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte @VRServerThread protected fun writeTrackerUpdate(localTracker: Tracker?) { - val builder = ProtobufMessages.Position.newBuilder().setTrackerId( - localTracker!!.id, - ) - if (localTracker.hasPosition) { - val pos = localTracker.position + val tracker = localTracker ?: return + + val builder = ProtobufMessages.Position.newBuilder() + .setTrackerId(tracker.id) + + if (tracker.hasPosition) { + val pos = tracker.position builder.setX(pos.x) builder.setY(pos.y) builder.setZ(pos.z) } - if (localTracker.hasRotation) { - val rot = localTracker.getRotation() + + if (tracker.hasRotation) { + val rot = tracker.getRotation() builder.setQx(rot.x) builder.setQy(rot.y) builder.setQz(rot.z) builder.setQw(rot.w) } - sendMessage(ProtobufMessage.newBuilder().setPosition(builder).build()) + + if (tracker.allowVelocity) { + val vel = tracker.getVelocity() + builder.setVx(vel.x) + builder.setVy(vel.y) + builder.setVz(vel.z) + } + + sendMessage( + ProtobufMessage.newBuilder() + .setPosition(builder) + .build(), + ) } @VRServerThread @@ -169,6 +185,16 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte ), ) + if (positionMessage.hasVx()) { + tracker + .setVelocity( + Vector3( + positionMessage.vx, + positionMessage.vy, + positionMessage.vz, + ), + ) + } tracker.dataTick() } } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java index ebb8e0f03c..703938ad6d 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java @@ -1052,6 +1052,48 @@ public interface PositionOrBuilder extends * @return The dataSource. */ dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource getDataSource(); + + /** + * optional float vx = 10; + * + * @return Whether the vx field is set. + */ + boolean hasVx(); + + /** + * optional float vx = 10; + * + * @return The vx. + */ + float getVx(); + + /** + * optional float vy = 11; + * + * @return Whether the vy field is set. + */ + boolean hasVy(); + + /** + * optional float vy = 11; + * + * @return The vy. + */ + float getVy(); + + /** + * optional float vz = 12; + * + * @return Whether the vz field is set. + */ + boolean hasVz(); + + /** + * optional float vz = 12; + * + * @return The vz. + */ + float getVz(); } /** @@ -1416,6 +1458,75 @@ public dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource getData : result; } + public static final int VX_FIELD_NUMBER = 10; + private float vx_ = 0F; + + /** + * optional float vx = 10; + * + * @return Whether the vx field is set. + */ + @java.lang.Override + public boolean hasVx() { + return ((bitField0_ & 0x00000010) != 0); + } + + /** + * optional float vx = 10; + * + * @return The vx. + */ + @java.lang.Override + public float getVx() { + return vx_; + } + + public static final int VY_FIELD_NUMBER = 11; + private float vy_ = 0F; + + /** + * optional float vy = 11; + * + * @return Whether the vy field is set. + */ + @java.lang.Override + public boolean hasVy() { + return ((bitField0_ & 0x00000020) != 0); + } + + /** + * optional float vy = 11; + * + * @return The vy. + */ + @java.lang.Override + public float getVy() { + return vy_; + } + + public static final int VZ_FIELD_NUMBER = 12; + private float vz_ = 0F; + + /** + * optional float vz = 12; + * + * @return Whether the vz field is set. + */ + @java.lang.Override + public boolean hasVz() { + return ((bitField0_ & 0x00000040) != 0); + } + + /** + * optional float vz = 12; + * + * @return The vz. + */ + @java.lang.Override + public float getVz() { + return vz_; + } + private byte memoizedIsInitialized = -1; @java.lang.Override @@ -1460,6 +1571,15 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (((bitField0_ & 0x00000008) != 0)) { output.writeEnum(9, dataSource_); } + if (((bitField0_ & 0x00000010) != 0)) { + output.writeFloat(10, vx_); + } + if (((bitField0_ & 0x00000020) != 0)) { + output.writeFloat(11, vy_); + } + if (((bitField0_ & 0x00000040) != 0)) { + output.writeFloat(12, vz_); + } getUnknownFields().writeTo(output); } @@ -1506,6 +1626,18 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeEnumSize(9, dataSource_); } + if (((bitField0_ & 0x00000010) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeFloatSize(10, vx_); + } + if (((bitField0_ & 0x00000020) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeFloatSize(11, vy_); + } + if (((bitField0_ & 0x00000040) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeFloatSize(12, vz_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -1600,6 +1732,42 @@ public boolean equals(final java.lang.Object obj) { if (dataSource_ != other.dataSource_) return false; } + if (hasVx() != other.hasVx()) + return false; + if (hasVx()) { + if ( + java.lang.Float.floatToIntBits(getVx()) + != java.lang.Float + .floatToIntBits( + other.getVx() + ) + ) + return false; + } + if (hasVy() != other.hasVy()) + return false; + if (hasVy()) { + if ( + java.lang.Float.floatToIntBits(getVy()) + != java.lang.Float + .floatToIntBits( + other.getVy() + ) + ) + return false; + } + if (hasVz() != other.hasVz()) + return false; + if (hasVz()) { + if ( + java.lang.Float.floatToIntBits(getVz()) + != java.lang.Float + .floatToIntBits( + other.getVz() + ) + ) + return false; + } if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; @@ -1666,6 +1834,30 @@ public int hashCode() { hash = (37 * hash) + DATA_SOURCE_FIELD_NUMBER; hash = (53 * hash) + dataSource_; } + if (hasVx()) { + hash = (37 * hash) + VX_FIELD_NUMBER; + hash = (53 * hash) + + java.lang.Float + .floatToIntBits( + getVx() + ); + } + if (hasVy()) { + hash = (37 * hash) + VY_FIELD_NUMBER; + hash = (53 * hash) + + java.lang.Float + .floatToIntBits( + getVy() + ); + } + if (hasVz()) { + hash = (37 * hash) + VZ_FIELD_NUMBER; + hash = (53 * hash) + + java.lang.Float + .floatToIntBits( + getVz() + ); + } hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -1841,6 +2033,9 @@ public Builder clear() { qz_ = 0F; qw_ = 0F; dataSource_ = 0; + vx_ = 0F; + vy_ = 0F; + vz_ = 0F; return this; } @@ -1911,6 +2106,18 @@ private void buildPartial0( result.dataSource_ = dataSource_; to_bitField0_ |= 0x00000008; } + if (((from_bitField0_ & 0x00000200) != 0)) { + result.vx_ = vx_; + to_bitField0_ |= 0x00000010; + } + if (((from_bitField0_ & 0x00000400) != 0)) { + result.vy_ = vy_; + to_bitField0_ |= 0x00000020; + } + if (((from_bitField0_ & 0x00000800) != 0)) { + result.vz_ = vz_; + to_bitField0_ |= 0x00000040; + } result.bitField0_ |= to_bitField0_; } @@ -1960,6 +2167,15 @@ public Builder mergeFrom(dev.slimevr.desktop.platform.ProtobufMessages.Position if (other.hasDataSource()) { setDataSourceValue(other.getDataSourceValue()); } + if (other.hasVx()) { + setVx(other.getVx()); + } + if (other.hasVy()) { + setVy(other.getVy()); + } + if (other.hasVz()) { + setVz(other.getVz()); + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -2032,6 +2248,21 @@ public Builder mergeFrom( bitField0_ |= 0x00000100; break; } // case 72 + case 85: { + vx_ = input.readFloat(); + bitField0_ |= 0x00000200; + break; + } // case 85 + case 93: { + vy_ = input.readFloat(); + bitField0_ |= 0x00000400; + break; + } // case 93 + case 101: { + vz_ = input.readFloat(); + bitField0_ |= 0x00000800; + break; + } // case 101 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -2464,6 +2695,150 @@ public Builder clearDataSource() { return this; } + private float vx_; + + /** + * optional float vx = 10; + * + * @return Whether the vx field is set. + */ + @java.lang.Override + public boolean hasVx() { + return ((bitField0_ & 0x00000200) != 0); + } + + /** + * optional float vx = 10; + * + * @return The vx. + */ + @java.lang.Override + public float getVx() { + return vx_; + } + + /** + * optional float vx = 10; + * + * @param value The vx to set. + * @return This builder for chaining. + */ + public Builder setVx(float value) { + + vx_ = value; + bitField0_ |= 0x00000200; + onChanged(); + return this; + } + + /** + * optional float vx = 10; + * + * @return This builder for chaining. + */ + public Builder clearVx() { + bitField0_ = (bitField0_ & ~0x00000200); + vx_ = 0F; + onChanged(); + return this; + } + + private float vy_; + + /** + * optional float vy = 11; + * + * @return Whether the vy field is set. + */ + @java.lang.Override + public boolean hasVy() { + return ((bitField0_ & 0x00000400) != 0); + } + + /** + * optional float vy = 11; + * + * @return The vy. + */ + @java.lang.Override + public float getVy() { + return vy_; + } + + /** + * optional float vy = 11; + * + * @param value The vy to set. + * @return This builder for chaining. + */ + public Builder setVy(float value) { + + vy_ = value; + bitField0_ |= 0x00000400; + onChanged(); + return this; + } + + /** + * optional float vy = 11; + * + * @return This builder for chaining. + */ + public Builder clearVy() { + bitField0_ = (bitField0_ & ~0x00000400); + vy_ = 0F; + onChanged(); + return this; + } + + private float vz_; + + /** + * optional float vz = 12; + * + * @return Whether the vz field is set. + */ + @java.lang.Override + public boolean hasVz() { + return ((bitField0_ & 0x00000800) != 0); + } + + /** + * optional float vz = 12; + * + * @return The vz. + */ + @java.lang.Override + public float getVz() { + return vz_; + } + + /** + * optional float vz = 12; + * + * @param value The vz to set. + * @return This builder for chaining. + */ + public Builder setVz(float value) { + + vz_ = value; + bitField0_ |= 0x00000800; + onChanged(); + return this; + } + + /** + * optional float vz = 12; + * + * @return This builder for chaining. + */ + public Builder clearVz() { + bitField0_ = (bitField0_ & ~0x00000800); + vz_ = 0F; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:messages.Position) } @@ -9023,7 +9398,7 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "ngPong\"#\n\007Version\022\030\n\020protocol_version\030\001 " + - "\001(\005\"\223\002\n\010Position\022\022\n\ntracker_id\030\001 \001(\005\022\016\n\001" + "\001(\005\"\333\002\n\010Position\022\022\n\ntracker_id\030\001 \001(\005\022\016\n\001" + "x\030\002 \001(\002H\000\210\001\001\022\016\n\001y\030\003 \001(\002H\001\210\001\001\022\016\n\001z\030\004 \001(\002H" + @@ -9031,67 +9406,71 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\022\n\n\002qw\030\010 \001(\002\0227\n\013data_source\030\t \001(\0162\035.mess" + - "ages.Position.DataSourceH\003\210\001\001\"8\n\nDataSou" + "ages.Position.DataSourceH\003\210\001\001\022\017\n\002vx\030\n \001(" + + + "\002H\004\210\001\001\022\017\n\002vy\030\013 \001(\002H\005\210\001\001\022\017\n\002vz\030\014 \001(\002H\006\210\001\001" + + + "\"8\n\nDataSource\022\010\n\004NONE\020\000\022\007\n\003IMU\020\001\022\r\n\tPRE" + - "rce\022\010\n\004NONE\020\000\022\007\n\003IMU\020\001\022\r\n\tPRECISION\020\002\022\010\n" + "CISION\020\002\022\010\n\004FULL\020\003B\004\n\002_xB\004\n\002_yB\004\n\002_zB\016\n\014" + - "\004FULL\020\003B\004\n\002_xB\004\n\002_yB\004\n\002_zB\016\n\014_data_sourc" + "_data_sourceB\005\n\003_vxB\005\n\003_vyB\005\n\003_vz\"\227\001\n\nUs" + - "e\"\227\001\n\nUserAction\022\014\n\004name\030\001 \001(\t\022C\n\020action" + "erAction\022\014\n\004name\030\001 \001(\t\022C\n\020action_argumen" + - "_arguments\030\002 \003(\0132).messages.UserAction.A" + "ts\030\002 \003(\0132).messages.UserAction.ActionArg" + - "ctionArgumentsEntry\0326\n\024ActionArgumentsEn" + "umentsEntry\0326\n\024ActionArgumentsEntry\022\013\n\003k" + - "try\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"|\n\014T" + "ey\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"|\n\014TrackerAd" + - "rackerAdded\022\022\n\ntracker_id\030\001 \001(\005\022\026\n\016track" + "ded\022\022\n\ntracker_id\030\001 \001(\005\022\026\n\016tracker_seria" + - "er_serial\030\002 \001(\t\022\024\n\014tracker_name\030\003 \001(\t\022\024\n" + "l\030\002 \001(\t\022\024\n\014tracker_name\030\003 \001(\t\022\024\n\014tracker" + - "\014tracker_role\030\004 \001(\005\022\024\n\014manufacturer\030\005 \001(" + "_role\030\004 \001(\005\022\024\n\014manufacturer\030\005 \001(\t\"\374\002\n\rTr" + - "\t\"\374\002\n\rTrackerStatus\022\022\n\ntracker_id\030\001 \001(\005\022" + "ackerStatus\022\022\n\ntracker_id\030\001 \001(\005\022.\n\006statu" + - ".\n\006status\030\002 \001(\0162\036.messages.TrackerStatus" + "s\030\002 \001(\0162\036.messages.TrackerStatus.Status\022" + - ".Status\0221\n\005extra\030\003 \003(\0132\".messages.Tracke" + "1\n\005extra\030\003 \003(\0132\".messages.TrackerStatus." + - "rStatus.ExtraEntry\022;\n\nconfidence\030\004 \001(\0162\"" + "ExtraEntry\022;\n\nconfidence\030\004 \001(\0162\".message" + - ".messages.TrackerStatus.ConfidenceH\000\210\001\001\032" + "s.TrackerStatus.ConfidenceH\000\210\001\001\032,\n\nExtra" + - ",\n\nExtraEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(" + "Entry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"E\n" + - "\t:\0028\001\"E\n\006Status\022\020\n\014DISCONNECTED\020\000\022\006\n\002OK\020" + "\006Status\022\020\n\014DISCONNECTED\020\000\022\006\n\002OK\020\001\022\010\n\004BUS" + - "\001\022\010\n\004BUSY\020\002\022\t\n\005ERROR\020\003\022\014\n\010OCCLUDED\020\004\"3\n\n" + "Y\020\002\022\t\n\005ERROR\020\003\022\014\n\010OCCLUDED\020\004\"3\n\nConfiden" + - "Confidence\022\006\n\002NO\020\000\022\007\n\003LOW\020\001\022\n\n\006MEDIUM\020\005\022" + "ce\022\006\n\002NO\020\000\022\007\n\003LOW\020\001\022\n\n\006MEDIUM\020\005\022\010\n\004HIGH\020" + - "\010\n\004HIGH\020\nB\r\n\013_confidence\"I\n\007Battery\022\022\n\nt" + "\nB\r\n\013_confidence\"I\n\007Battery\022\022\n\ntracker_i" + - "racker_id\030\001 \001(\005\022\025\n\rbattery_level\030\002 \001(\002\022\023" + "d\030\001 \001(\005\022\025\n\rbattery_level\030\002 \001(\002\022\023\n\013is_cha" + - "\n\013is_charging\030\003 \001(\010\"\241\002\n\017ProtobufMessage\022" + "rging\030\003 \001(\010\"\241\002\n\017ProtobufMessage\022&\n\010posit" + - "&\n\010position\030\001 \001(\0132\022.messages.PositionH\000\022" + "ion\030\001 \001(\0132\022.messages.PositionH\000\022+\n\013user_" + - "+\n\013user_action\030\002 \001(\0132\024.messages.UserActi" + "action\030\002 \001(\0132\024.messages.UserActionH\000\022/\n\r" + - "onH\000\022/\n\rtracker_added\030\003 \001(\0132\026.messages.T" + "tracker_added\030\003 \001(\0132\026.messages.TrackerAd" + - "rackerAddedH\000\0221\n\016tracker_status\030\004 \001(\0132\027." + "dedH\000\0221\n\016tracker_status\030\004 \001(\0132\027.messages" + - "messages.TrackerStatusH\000\022$\n\007battery\030\005 \001(" + ".TrackerStatusH\000\022$\n\007battery\030\005 \001(\0132\021.mess" + - "\0132\021.messages.BatteryH\000\022$\n\007version\030\006 \001(\0132" + "ages.BatteryH\000\022$\n\007version\030\006 \001(\0132\021.messag" + - "\021.messages.VersionH\000B\t\n\007messageB2\n\034dev.s" + "es.VersionH\000B\t\n\007messageB2\n\034dev.slimevr.d" + - "limevr.desktop.platformB\020ProtobufMessage" + "esktop.platformB\020ProtobufMessagesH\003b\006pro" + - "sH\003b\006proto3" + "to3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom( @@ -9113,7 +9492,7 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { internal_static_messages_Position_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_messages_Position_descriptor, new java.lang.String[] { "TrackerId", "X", "Y", "Z", "Qx", "Qy", "Qz", "Qw", - "DataSource", } + "DataSource", "Vx", "Vy", "Vz", } ); internal_static_messages_UserAction_descriptor = getDescriptor().getMessageTypes().get(3); internal_static_messages_UserAction_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( From f58d4b3169265f7be6004d9af0c643d9f74de0a7 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Fri, 20 Mar 2026 23:56:42 +0200 Subject: [PATCH 03/12] Added velocity transmission toggle to GUI and RPC (NaLo support) --- gui/public/i18n/en/translation.ftl | 3 ++ .../settings/pages/GeneralSettings.tsx | 42 +++++++++++++++++++ package.json | 4 +- .../main/java/dev/slimevr/config/VRConfig.kt | 10 +---- .../java/dev/slimevr/config/VelocityConfig.kt | 11 ++++- .../rpc/settings/RPCSettingsBuilder.kt | 12 ++++++ .../rpc/settings/RPCSettingsHandler.kt | 6 +++ solarxr-protocol | 2 +- 8 files changed, 78 insertions(+), 12 deletions(-) diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index dffb2a281c..6fe99de31a 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -635,6 +635,9 @@ settings-general-fk_settings-enforce_joint_constraints-correct_constraints-descr settings-general-fk_settings-ik = Position data settings-general-fk_settings-ik-use_position = Use Position data settings-general-fk_settings-ik-use_position-description = Enables the use of position data from trackers that provide it. When enabling this make sure to full reset and recalibrate in game. +settings-general-fk_settings-velocity_settings = Velocity Settings +settings-general-fk_settings-velocity_settings-description = Send derived velocity data to SteamVR. Required for Natural Locomotion support. +settings-general-fk_settings-velocity_settings-send_derived_velocity = Send derived velocity to driver settings-general-fk_settings-arm_fk = Arm tracking settings-general-fk_settings-arm_fk-description = Force arms to be tracked from the headset (HMD) even if positional hand data is available. settings-general-fk_settings-arm_fk-force_arms = Force arms from HMD diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index 11197c65b4..115480777d 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -15,6 +15,7 @@ import { SteamVRTrackersSettingT, TapDetectionSettingsT, HIDSettingsT, + VelocitySettingsT, BodyPart, } from 'solarxr-protocol'; import { useConfig } from '@/hooks/config'; @@ -114,6 +115,9 @@ export type SettingsForm = { hidSettings: { trackersOverHID: boolean; }; + velocitySettings: { + sendDerivedVelocity: boolean; + }; }; const defaultValues: SettingsForm = { @@ -173,6 +177,7 @@ const defaultValues: SettingsForm = { resetsSettings: defaultResetSettings, stayAligned: defaultStayAlignedSettings, hidSettings: { trackersOverHID: false }, + velocitySettings: { sendDerivedVelocity: false }, }; const settingsAtom = atom(new SettingsResponseT()); @@ -350,6 +355,11 @@ export function GeneralSettings() { hidSettings.trackersOverHid = values.hidSettings.trackersOverHID; settingsReq.hidSettings = hidSettings; + const velocitySettings = new VelocitySettingsT(); + velocitySettings.sendDerivedVelocity = + values.velocitySettings.sendDerivedVelocity; + settings.velocitySettings = velocitySettings; + if (values.resetsSettings) { settingsReq.resetsSettings = loadResetSettings(values.resetsSettings); } @@ -483,6 +493,12 @@ export function GeneralSettings() { }; } + if (settings.velocitySettings) { + formData.velocitySettings = { + sendDerivedVelocity: settings.velocitySettings.sendDerivedVelocity, + }; + } + reset({ ...getValues(), ...formData }); }, [settings]); @@ -1053,6 +1069,32 @@ export function GeneralSettings() { /> +
+ + {l10n.getString( + 'settings-general-fk_settings-velocity_settings' + )} + +
+ + {l10n.getString( + 'settings-general-fk_settings-velocity_settings-description' + )} + +
+
+
+ +
+ {config?.debug && ( <>
diff --git a/package.json b/package.json index 4b500ef7c1..6fb1460219 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "preinstall": "npx only-allow pnpm" }, "devDependencies": { - "husky": "^9.1.6", - "lint-staged": "^15.2.10" + "husky": "^9.1.7", + "lint-staged": "^15.5.2" } } diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index dd100f55ef..5aa36e64f9 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -58,7 +58,7 @@ class VRConfig { val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig() - var velocityConfig: VelocityConfig = VelocityConfig() + val velocityConfig: VelocityConfig = VelocityConfig() val vrcConfig: VRCConfig = VRCConfig() @@ -106,13 +106,7 @@ class VRConfig { return config } - /** - * Applies the velocity policy to the given [Tracker]. - * - * This method determines whether the tracker—be it a physical sensor or a computed virtual device—is - * permitted to calculate and broadcast velocity data. - * It resolves the active [VelocityConfig] value, updating [Tracker.allowVelocity] accordingly. - */ + /** Resolves value from global config and applies to tracker. */ fun applyVelocityPolicy(tracker: Tracker) { tracker.allowVelocity = velocityConfig.sendDerivedVelocity } diff --git a/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt b/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt index a6516b1e24..204b951e32 100644 --- a/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt @@ -1,10 +1,19 @@ package dev.slimevr.config +import dev.slimevr.VRServer + /** * Allows to enable/disable sending of optional derived velocity data via Protobuf. * Enables Natural Locomotion Support * May create overprediction in certain titles causing excessive jitter when moving upper body. */ class VelocityConfig { - var sendDerivedVelocity: Boolean = false // Disables derived velocity for all trackers. Driver zeroes out velocity if nothing is returned in protobuf message. + // Disables derived velocity for all trackers. Driver zeroes out velocity if nothing is returned in protobuf message. + var sendDerivedVelocity: Boolean = false + + fun updateTrackersVelocityPolicy() { + for (t in VRServer.instance.allTrackers) { + t.allowVelocity = sendDerivedVelocity + } + } } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt index bc6dd73098..cf2feca96b 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt @@ -15,6 +15,7 @@ import dev.slimevr.config.StayAlignedConfig import dev.slimevr.config.TapDetectionConfig import dev.slimevr.config.VMCConfig import dev.slimevr.config.VRCOSCConfig +import dev.slimevr.config.VelocityConfig import dev.slimevr.filtering.TrackerFilters.Companion.getByConfigkey import dev.slimevr.tracking.processor.HumanPoseManager import dev.slimevr.tracking.processor.config.SkeletonConfigToggles @@ -34,6 +35,7 @@ import solarxr_protocol.rpc.SteamVRTrackersSetting import solarxr_protocol.rpc.TapDetectionSettings import solarxr_protocol.rpc.VMCOSCSettings import solarxr_protocol.rpc.VRCOSCSettings +import solarxr_protocol.rpc.VelocitySettings import solarxr_protocol.rpc.settings.LegTweaksSettings import solarxr_protocol.rpc.settings.ModelRatios import solarxr_protocol.rpc.settings.ModelSettings @@ -427,6 +429,7 @@ fun createSettingsResponse(fbb: FlatBufferBuilder, server: VRServer): Int { ), createHIDSettings(fbb, server.configManager.vrConfig.hidConfig), 0, + createVelocitySettings(fbb, server.configManager.vrConfig.velocityConfig), ) } @@ -462,3 +465,12 @@ fun createHIDSettings( fbb, config.trackersOverHID, ) + +fun createVelocitySettings( + fbb: FlatBufferBuilder, + config: VelocityConfig, +): Int = VelocitySettings + .createVelocitySettings( + fbb, + config.sendDerivedVelocity, + ) diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 8cbb87c139..3777a4041e 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -367,6 +367,12 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { config.trackersOverHID = requestConfig.trackersOverHid() } + if (req.velocitySettings() != null) { + val velocityConfig = api.server.configManager.vrConfig.velocityConfig + velocityConfig.sendDerivedVelocity = req.velocitySettings().sendDerivedVelocity() + velocityConfig.updateTrackersVelocityPolicy() + } + api.server.configManager.saveConfig() } diff --git a/solarxr-protocol b/solarxr-protocol index 5d572629c9..9e83ea846e 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 5d572629c94f895574162225ec69443705ce35af +Subproject commit 9e83ea846e846dbc27615c59b0598bb288b1e7f8 From da666d3b144e3d04c7e26e5360d48ef10fe2b5df Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Tue, 21 Apr 2026 17:10:13 +0300 Subject: [PATCH 04/12] Remove applyVelocityPolicyTo(), Replace System.nanoTime with Timesource, Replace allowVelocity with hasVelocity in ProtobufBridge. --- .../core/src/main/java/dev/slimevr/VRServer.kt | 17 +---------------- .../processor/skeleton/HumanSkeleton.kt | 2 +- .../dev/slimevr/tracking/trackers/Tracker.kt | 14 +++++++++----- .../slimevr/desktop/platform/ProtobufBridge.kt | 2 +- 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index 1a0f7380ad..cd3339f97e 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -186,21 +186,6 @@ class VRServer @JvmOverloads constructor( instance = this } - /** - * TODO: The design of this method is chosen for future expandability in case we want to have more complex velocity policies that depend on tracker properties or other config values. - * Initiates the velocity policy application process for the specified [Tracker] (or all trackers if null). - * - * This method serves as the initiator. - * The actual policy logic is not handled here, - * but is delegated to [dev.slimevr.config.VRConfig.applyVelocityPolicy]. - */ - private fun applyVelocityPolicyTo(tracker: Tracker?) { - val targets = tracker?.let { listOf(it) } ?: trackers - for (t in targets) { - configManager.vrConfig.applyVelocityPolicy(t) - } - } - fun hasBridge(bridgeClass: Class): Boolean { for (bridge in bridges) { if (bridgeClass.isAssignableFrom(bridge.javaClass)) { @@ -248,7 +233,7 @@ class VRServer @JvmOverloads constructor( configManager.saveConfig() // Requires a fresh TrackerConfig on Update, so executed after we save a new state. - applyVelocityPolicyTo(tracker) + tracker?.let { configManager.vrConfig.applyVelocityPolicy(it) } } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index d64e8a4930..da16526691 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -1171,7 +1171,7 @@ class HumanSkeleton( it.position = trackerBone.getTailPosition() it.setRotation(trackerBone.getGlobalRotation() * trackerBone.rotationOffset.inv()) it.dataTick() - it.updateDerivedVelocity(System.nanoTime()) + it.updateDerivedVelocity() } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index ae2a64edea..00d79d5fa7 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -12,6 +12,9 @@ import io.eiren.util.BufferedTimer import io.github.axisangles.ktmath.Quaternion import io.github.axisangles.ktmath.Vector3 import kotlin.properties.Delegates +import kotlin.time.DurationUnit +import kotlin.time.TimeMark +import kotlin.time.TimeSource const val TIMEOUT_MS = 2_000L const val DISCONNECT_MS = 3_000L + TIMEOUT_MS @@ -123,7 +126,7 @@ class Tracker @JvmOverloads constructor( * Velocity state server-side differentiation based on sent poses */ private data class VelocityState( - var prevTimeNs: Long = 0L, + var prevMark: TimeMark? = null, var prevPos: Vector3 = Vector3(0f, 0f, 0f), ) @@ -370,7 +373,7 @@ class Tracker @JvmOverloads constructor( * last update, applying a sanity check on the time delta to filter out noise and ensure data stability. * */ - fun updateDerivedVelocity(nowNs: Long) { + fun updateDerivedVelocity() { if (!allowVelocity || !hasPosition) { velocityState = null _velocity = Vector3.NULL @@ -382,8 +385,9 @@ class Tracker @JvmOverloads constructor( velocityState = it } - if (state.prevTimeNs != 0L) { - val dt = (nowNs - state.prevTimeNs) * 1e-9 + val prevMark = state.prevMark + if (prevMark != null) { + val dt = prevMark.elapsedNow().toDouble(DurationUnit.SECONDS) if (dt in 1e-4..0.25) { _velocity = Vector3( ((pos.x - state.prevPos.x) / dt).toFloat(), @@ -394,7 +398,7 @@ class Tracker @JvmOverloads constructor( _velocity = Vector3.NULL } } - state.prevTimeNs = nowNs + state.prevMark = TimeSource.Monotonic.markNow() state.prevPos = pos } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt index 9a116b5efd..d20d8f9e13 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt @@ -124,7 +124,7 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte builder.setQw(rot.w) } - if (tracker.allowVelocity) { + if (tracker.hasVelocity) { val vel = tracker.getVelocity() builder.setVx(vel.x) builder.setVy(vel.y) From e39202686fb95e93515b1fa811847750dd79fb1b Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Tue, 21 Apr 2026 18:18:51 +0300 Subject: [PATCH 05/12] Add build stubs for solarxr-protocol schema additions pending #1811 # Conflicts: # server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt --- .../dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 3777a4041e..5e0e588bdd 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -390,7 +390,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { val settings = SettingsResponse .createSettingsResponse( fbb, - createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ) val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings) From d47dd3a423a8f22b1cc4fb41005552c945d80164 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Tue, 21 Apr 2026 18:32:00 +0300 Subject: [PATCH 06/12] Add FBT jitter warning to velocity settings description  Conflicts:  server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt --- gui/public/i18n/en/translation.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 6fe99de31a..fd804a0ec6 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -636,7 +636,7 @@ settings-general-fk_settings-ik = Position data settings-general-fk_settings-ik-use_position = Use Position data settings-general-fk_settings-ik-use_position-description = Enables the use of position data from trackers that provide it. When enabling this make sure to full reset and recalibrate in game. settings-general-fk_settings-velocity_settings = Velocity Settings -settings-general-fk_settings-velocity_settings-description = Send derived velocity data to SteamVR. Required for Natural Locomotion support. +settings-general-fk_settings-velocity_settings-description = Send derived velocity data to SteamVR. Required for Natural Locomotion support. May cause jitter in FBT. settings-general-fk_settings-velocity_settings-send_derived_velocity = Send derived velocity to driver settings-general-fk_settings-arm_fk = Arm tracking settings-general-fk_settings-arm_fk-description = Force arms to be tracked from the headset (HMD) even if positional hand data is available. From 70a98a35f7654c7da35aa1eb87553fb72c6d433a Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Wed, 22 Apr 2026 18:18:52 +0300 Subject: [PATCH 07/12] Fix RPC velocity toggle state being sent to response instead of request --- gui/src/components/settings/pages/GeneralSettings.tsx | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index 115480777d..a90e725afc 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -358,7 +358,7 @@ export function GeneralSettings() { const velocitySettings = new VelocitySettingsT(); velocitySettings.sendDerivedVelocity = values.velocitySettings.sendDerivedVelocity; - settings.velocitySettings = velocitySettings; + settingsReq.velocitySettings = velocitySettings; if (values.resetsSettings) { settingsReq.resetsSettings = loadResetSettings(values.resetsSettings); diff --git a/package.json b/package.json index 6fb1460219..4b500ef7c1 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "preinstall": "npx only-allow pnpm" }, "devDependencies": { - "husky": "^9.1.7", - "lint-staged": "^15.5.2" + "husky": "^9.1.6", + "lint-staged": "^15.2.10" } } From 18522536fe993d9e4fdaefb85f431a9571b144a8 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Fri, 24 Apr 2026 13:14:54 +0300 Subject: [PATCH 08/12] Review: Inlined velocity policy, non-nullable velocityState, markNow timing fix, non-nullable writeTrackerUpdate param --- .../src/main/java/dev/slimevr/VRServer.kt | 3 --- .../main/java/dev/slimevr/config/VRConfig.kt | 7 +---- .../dev/slimevr/tracking/trackers/Tracker.kt | 27 ++++++++++--------- .../desktop/platform/ProtobufBridge.kt | 18 ++++++------- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index cd3339f97e..f5e5cf6a71 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -231,9 +231,6 @@ class VRServer @JvmOverloads constructor( refreshTrackersDriftCompensationEnabled() configManager.vrConfig.writeTrackerConfig(tracker) configManager.saveConfig() - - // Requires a fresh TrackerConfig on Update, so executed after we save a new state. - tracker?.let { configManager.vrConfig.applyVelocityPolicy(it) } } } diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index 5aa36e64f9..903750aa8a 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -106,11 +106,6 @@ class VRConfig { return config } - /** Resolves value from global config and applies to tracker. */ - fun applyVelocityPolicy(tracker: Tracker) { - tracker.allowVelocity = velocityConfig.sendDerivedVelocity - } - fun readTrackerConfig(tracker: Tracker) { if (tracker.userEditable) { val config = getTracker(tracker) @@ -126,7 +121,7 @@ class VRConfig { .readFilteringConfig(filters, tracker.getRotation()) } } - applyVelocityPolicy(tracker) + tracker.allowVelocity = velocityConfig.sendDerivedVelocity } fun writeTrackerConfig(tracker: Tracker?) { diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index 00d79d5fa7..b8c69e803f 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -12,8 +12,9 @@ import io.eiren.util.BufferedTimer import io.github.axisangles.ktmath.Quaternion import io.github.axisangles.ktmath.Vector3 import kotlin.properties.Delegates +import kotlin.time.Duration.Companion.microseconds +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit -import kotlin.time.TimeMark import kotlin.time.TimeSource const val TIMEOUT_MS = 2_000L @@ -126,11 +127,11 @@ class Tracker @JvmOverloads constructor( * Velocity state server-side differentiation based on sent poses */ private data class VelocityState( - var prevMark: TimeMark? = null, + var prevMark: TimeSource.Monotonic.ValueTimeMark? = null, var prevPos: Vector3 = Vector3(0f, 0f, 0f), ) - private var velocityState: VelocityState? = null + private var velocityState: VelocityState = VelocityState() var position = Vector3.NULL val resetsHandler: TrackerResetsHandler = TrackerResetsHandler(this) @@ -375,30 +376,30 @@ class Tracker @JvmOverloads constructor( */ fun updateDerivedVelocity() { if (!allowVelocity || !hasPosition) { - velocityState = null + velocityState.prevMark = null _velocity = Vector3.NULL return } val pos = position - val state = velocityState ?: VelocityState().also { - velocityState = it - } + val state = velocityState + val now = TimeSource.Monotonic.markNow() val prevMark = state.prevMark if (prevMark != null) { - val dt = prevMark.elapsedNow().toDouble(DurationUnit.SECONDS) - if (dt in 1e-4..0.25) { + val dt = now - prevMark + if (dt in 100.microseconds..250.milliseconds) { + val dtSeconds = dt.toDouble(DurationUnit.SECONDS) _velocity = Vector3( - ((pos.x - state.prevPos.x) / dt).toFloat(), - ((pos.y - state.prevPos.y) / dt).toFloat(), - ((pos.z - state.prevPos.z) / dt).toFloat(), + ((pos.x - state.prevPos.x) / dtSeconds).toFloat(), + ((pos.y - state.prevPos.y) / dtSeconds).toFloat(), + ((pos.z - state.prevPos.z) / dtSeconds).toFloat(), ) } else { _velocity = Vector3.NULL } } - state.prevMark = TimeSource.Monotonic.markNow() + state.prevMark = now state.prevPos = pos } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt index d20d8f9e13..5cc8a33603 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt @@ -103,29 +103,27 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte } @VRServerThread - protected fun writeTrackerUpdate(localTracker: Tracker?) { - val tracker = localTracker ?: return - + protected fun writeTrackerUpdate(localTracker: Tracker) { val builder = ProtobufMessages.Position.newBuilder() - .setTrackerId(tracker.id) + .setTrackerId(localTracker.id) - if (tracker.hasPosition) { - val pos = tracker.position + if (localTracker.hasPosition) { + val pos = localTracker.position builder.setX(pos.x) builder.setY(pos.y) builder.setZ(pos.z) } - if (tracker.hasRotation) { - val rot = tracker.getRotation() + if (localTracker.hasRotation) { + val rot = localTracker.getRotation() builder.setQx(rot.x) builder.setQy(rot.y) builder.setQz(rot.z) builder.setQw(rot.w) } - if (tracker.hasVelocity) { - val vel = tracker.getVelocity() + if (localTracker.hasVelocity) { + val vel = localTracker.getVelocity() builder.setVx(vel.x) builder.setVy(vel.y) builder.setVz(vel.z) From d9c4ccf17f988ec004f1c796caf7a7f77e512317 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Fri, 24 Apr 2026 15:09:45 +0300 Subject: [PATCH 09/12] Review: velocityState direct use and change to val --- .../java/dev/slimevr/tracking/trackers/Tracker.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index b8c69e803f..324e7e7dbe 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -131,7 +131,7 @@ class Tracker @JvmOverloads constructor( var prevPos: Vector3 = Vector3(0f, 0f, 0f), ) - private var velocityState: VelocityState = VelocityState() + private val velocityState = VelocityState() var position = Vector3.NULL val resetsHandler: TrackerResetsHandler = TrackerResetsHandler(this) @@ -382,25 +382,24 @@ class Tracker @JvmOverloads constructor( } val pos = position - val state = velocityState val now = TimeSource.Monotonic.markNow() - val prevMark = state.prevMark + val prevMark = velocityState.prevMark if (prevMark != null) { val dt = now - prevMark if (dt in 100.microseconds..250.milliseconds) { val dtSeconds = dt.toDouble(DurationUnit.SECONDS) _velocity = Vector3( - ((pos.x - state.prevPos.x) / dtSeconds).toFloat(), - ((pos.y - state.prevPos.y) / dtSeconds).toFloat(), - ((pos.z - state.prevPos.z) / dtSeconds).toFloat(), + ((pos.x - velocityState.prevPos.x) / dtSeconds).toFloat(), + ((pos.y - velocityState.prevPos.y) / dtSeconds).toFloat(), + ((pos.z - velocityState.prevPos.z) / dtSeconds).toFloat(), ) } else { _velocity = Vector3.NULL } } - state.prevMark = now - state.prevPos = pos + velocityState.prevMark = now + velocityState.prevPos = pos } /** From 822a831f3d75c5bb261b8f6f6902d85858073177 Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Fri, 24 Apr 2026 16:00:00 +0300 Subject: [PATCH 10/12] Solar: sync with the updated head --- solarxr-protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarxr-protocol b/solarxr-protocol index 9e83ea846e..ee4d7c1a5f 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 9e83ea846e846dbc27615c59b0598bb288b1e7f8 +Subproject commit ee4d7c1a5f23a368fef0da1069fbc2c8bb62eca6 From a9bfa5e293dc2d9975e01b6021539ba670ffd00b Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Mon, 27 Apr 2026 13:23:02 +0300 Subject: [PATCH 11/12] Update solarxr to track main. --- solarxr-protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarxr-protocol b/solarxr-protocol index ee4d7c1a5f..aa384c0f8b 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit ee4d7c1a5f23a368fef0da1069fbc2c8bb62eca6 +Subproject commit aa384c0f8b0c1c8c8146295ff595ea034d348399 From 4fd5c9a1bb80ccc825d711cb9050a111f1f07a2c Mon Sep 17 00:00:00 2001 From: Vladimir Pankov Date: Tue, 28 Apr 2026 11:58:39 +0300 Subject: [PATCH 12/12] Review: rename updateTrackersVelocityPolicy and rephrase updateDerivedVelocity KDoc --- .../src/main/java/dev/slimevr/config/VelocityConfig.kt | 2 +- .../slimevr/protocol/rpc/settings/RPCSettingsHandler.kt | 2 +- .../main/java/dev/slimevr/tracking/trackers/Tracker.kt | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt b/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt index 204b951e32..82a6d1f673 100644 --- a/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VelocityConfig.kt @@ -11,7 +11,7 @@ class VelocityConfig { // Disables derived velocity for all trackers. Driver zeroes out velocity if nothing is returned in protobuf message. var sendDerivedVelocity: Boolean = false - fun updateTrackersVelocityPolicy() { + fun updateTrackersVelocitySettings() { for (t in VRServer.instance.allTrackers) { t.allowVelocity = sendDerivedVelocity } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 5e0e588bdd..207fc1fe38 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -370,7 +370,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { if (req.velocitySettings() != null) { val velocityConfig = api.server.configManager.vrConfig.velocityConfig velocityConfig.sendDerivedVelocity = req.velocitySettings().sendDerivedVelocity() - velocityConfig.updateTrackersVelocityPolicy() + velocityConfig.updateTrackersVelocitySettings() } api.server.configManager.saveConfig() diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index 324e7e7dbe..4262b2abe9 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -367,12 +367,9 @@ class Tracker @JvmOverloads constructor( } /** - * Updates the derived velocity of the tracker by differentiating position over time. - * - * This method enforces the [allowVelocity] policy and checks for valid position data before - * proceeding. If conditions are met, it calculates velocity based on the displacement since the - * last update, applying a sanity check on the time delta to filter out noise and ensure data stability. - * + * If derived velocity is enabled, updates the derived velocity of the tracker, + * calculating it based on the position displacement since the last update, + * applying a sanity check on the time delta to filter out noise and ensure data stability. */ fun updateDerivedVelocity() { if (!allowVelocity || !hasPosition) {