| … | … | @@ -2,7 +2,7 @@ schema_version = "1.0.0" |
| 2 | 2 | id = "blender_godot_bridge" |
| 3 | 3 | name = "Blender-Godot Bridge" |
| 4 | 4 | tagline = "Real-time bidirectional scene sync with Godot" |
| 5 | version = "0.17.0" | |
| 5 | version = "0.17.1" | |
| 6 | 6 | blender_version_min = "4.2.0" |
| 7 | 7 | type = "add-on" |
| 8 | 8 | maintainer = "blender-bridge <noreply@localhost>" |
| … | … | @@ -4,6 +4,7 @@ Length-prefixed messages: [4-byte LE length] [payload]. |
| 4 | 4 | Non-blocking receive for use with Blender's timer callbacks. |
| 5 | 5 | """ |
| 6 | 6 | |
| 7 | import select | |
| 7 | 8 | import socket |
| 8 | 9 | import struct |
| 9 | 10 | |
| … | … | @@ -71,13 +72,33 @@ class BridgeConnection: |
| 71 | 72 | # ── Send ───────────────────────────────────────────────────────────── |
| 72 | 73 | |
| 73 | 74 | def send_message(self, data: bytes): |
| 74 | """Send a length-prefixed message.""" | |
| 75 | """Send a length-prefixed message. | |
| 76 | ||
| 77 | Handles BlockingIOError (send buffer full) by waiting for writability | |
| 78 | instead of treating it as a fatal disconnect. This is critical when | |
| 79 | streaming many tile_ready messages — the non-blocking socket's send | |
| 80 | buffer can fill up, and sendall() would raise BlockingIOError. | |
| 81 | """ | |
| 75 | 82 | if self._sock is None: |
| 76 | 83 | return |
| 77 | try: | |
| 78 | self._sock.sendall(struct.pack("<I", len(data)) + data) | |
| 79 | except OSError: | |
| 80 | self._close_unexpected() | |
| 84 | frame = struct.pack("<I", len(data)) + data | |
| 85 | sent = 0 | |
| 86 | while sent < len(frame): | |
| 87 | try: | |
| 88 | n = self._sock.send(frame[sent:]) | |
| 89 | if n == 0: | |
| 90 | self._close_unexpected() | |
| 91 | return | |
| 92 | sent += n | |
| 93 | except BlockingIOError: | |
| 94 | # Send buffer full — wait up to 5s for writability | |
| 95 | _, writable, _ = select.select([], [self._sock], [], 5.0) | |
| 96 | if not writable: | |
| 97 | self._close_unexpected() | |
| 98 | return | |
| 99 | except OSError: | |
| 100 | self._close_unexpected() | |
| 101 | return | |
| 81 | 102 | |
| 82 | 103 | # ── Receive (non-blocking) ─────────────────────────────────────────── |
| 83 | 104 |
| … | … | @@ -264,7 +264,7 @@ Error BlenderBridge::start_server(const String &socket_path) { |
| 264 | 264 | _known_instance_ids.clear(); |
| 265 | 265 | _next_object_id = 1; |
| 266 | 266 | |
| 267 | UtilityFunctions::print("BlenderBridge v0.17.0: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")"); | |
| 267 | UtilityFunctions::print("BlenderBridge v0.17.1: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")"); | |
| 268 | 268 | return OK; |
| 269 | 269 | } |
| 270 | 270 |
| … | … | @@ -234,8 +234,14 @@ private: |
| 234 | 234 | } else if (n < 0) { |
| 235 | 235 | if (errno == EINTR) |
| 236 | 236 | continue; |
| 237 | if (errno == EAGAIN || errno == EWOULDBLOCK) | |
| 238 | continue; // Spin briefly for non-blocking (messages are small) | |
| 237 | if (errno == EAGAIN || errno == EWOULDBLOCK) { | |
| 238 | // Wait for socket to become writable (up to 2s) | |
| 239 | struct pollfd pfd = { fd, POLLOUT, 0 }; | |
| 240 | int r = ::poll(&pfd, 1, 2000); | |
| 241 | if (r <= 0) | |
| 242 | return false; // Timeout or error | |
| 243 | continue; | |
| 244 | } | |
| 239 | 245 | return false; // Real error |
| 240 | 246 | } |
| 241 | 247 | } |