OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge commits / a8221cd

Fix reconnect loop and echo duplication bugs
- Handle EINTR in UDS server read/write (signal interruption was closing client connections, triggering reconnect loops) - Replace best-effort write with _write_all loop to prevent partial writes from corrupting message framing - Pre-cache transform and geometry hashes in suppress() so objects aren't falsely re-detected as changed when suppression expires - Cache transform hash for suppressed objects in depsgraph handler - Stop clearing hash caches on reconnect (they reflect valid scene state; clearing caused all objects to echo back as duplicates) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a8221cd0200f52ef0280d4a71f2bebc30cb9a0ac
by plankatron <fleeky@gmail.com> · 2026-03-04T19:19:07.000Z

Changed files (4) +45 -11

blender_addon/__init__.py +4 -2
……@@ -294,8 +294,10 @@ def _attempt_reconnect():
294294 _detector._shm = _shm
295295 _detector._pending.clear()
296296 _detector._last_depsgraph = None
297 _detector._transform_hashes.clear() # Force re-detect all transforms
298 _detector._geometry_hashes.clear() # Force re-detect all geometry
297 # Keep _transform_hashes and _geometry_hashes intact — they reflect
298 # the current Blender scene state and are still valid. Clearing them
299 # causes ALL objects to be falsely re-detected as changed, triggering
300 # an echo loop that duplicates objects in Godot.
299301 _detector.register()
300302 _detector.set_resize_callback(_on_shm_resized)
301303 else:
blender_addon/change_detector.py +18 -1
……@@ -128,11 +128,24 @@ class ChangeDetector:
128128 Only the named objects are suppressed. Other objects (e.g. those
129129 with modifiers that depend on the moved object) still get their
130130 updates captured and flushed to Godot.
131
132 Pre-caches transform and geometry hashes so that when suppression
133 expires, the objects won't be falsely detected as "changed".
131134 """
132135 for name in names:
133136 self._suppressed_objects[name] = max(
134137 self._suppressed_objects.get(name, 0), frames
135138 )
139 # Pre-cache hashes to prevent false detection after unsuppress
140 obj = bpy.data.objects.get(name) if bpy else None
141 if obj is not None:
142 self._transform_hashes[name] = self._hash_transform(obj.matrix_world)
143 if obj.type == "MESH" and obj.data is not None:
144 verts = obj.data.vertices
145 if len(verts) > 0:
146 coords = np.empty(len(verts) * 3, dtype=np.float32)
147 verts.foreach_get("co", coords)
148 self._geometry_hashes[name] = hash(coords.tobytes())
136149
137150 def unsuppress(self):
138151 """Decrement suppression counters (called each timer tick)."""
……@@ -222,8 +235,12 @@ class ChangeDetector:
222235
223236 name = obj.name
224237
225 # Skip echoes for objects that Godot just told us about
238 # Skip echoes for objects that Godot just told us about,
239 # but still cache the transform hash so unsuppress doesn't
240 # trigger false change detection
226241 if name in self._suppressed_objects:
242 if update.is_updated_transform:
243 self._transform_hashes[name] = self._hash_transform(obj.matrix_world)
227244 continue
228245
229246 existing = self._pending.get(name, -1)
blender_bridge_addon.zip +0 -0
(no content changes — binary or rename)
src/uds_server.h +23 -8
……@@ -173,8 +173,8 @@ private:
173173 c.fd = -1;
174174 break;
175175 } else {
176 // EAGAIN/EWOULDBLOCK = no more data
177 if (errno == EAGAIN || errno == EWOULDBLOCK)
176 // EAGAIN/EWOULDBLOCK = no more data; EINTR = signal interrupted
177 if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
178178 break;
179179 // Other error = disconnect
180180 close(c.fd);
……@@ -223,14 +223,29 @@ private:
223223 _clients.end());
224224 }
225225
226 static bool _write_all(int fd, const void *buf, size_t len) {
227 const uint8_t *p = static_cast<const uint8_t *>(buf);
228 size_t remaining = len;
229 while (remaining > 0) {
230 ssize_t n = write(fd, p, remaining);
231 if (n > 0) {
232 p += n;
233 remaining -= (size_t)n;
234 } else if (n < 0) {
235 if (errno == EINTR)
236 continue;
237 if (errno == EAGAIN || errno == EWOULDBLOCK)
238 continue; // Spin briefly for non-blocking (messages are small)
239 return false; // Real error
240 }
241 }
242 return true;
243 }
244
226245 static void _send_to(int fd, const void *data, uint32_t size) {
227246 uint32_t le_size = size;
228 // Best-effort write (non-blocking may partial write, but for signaling
229 // messages this is fine — they're small)
230 ssize_t n1 = write(fd, &le_size, 4);
231 if (n1 == 4) {
232 ssize_t n2 = write(fd, data, size);
233 (void)n2;
247 if (_write_all(fd, &le_size, 4)) {
248 _write_all(fd, data, size);
234249 }
235250 }
236251};