🩹 Troubleshooting#
Problems users hit in practice, grouped by where they show up. Quoted
strings are exact panel/log text. For full tracebacks read server.log,
progress.log, or the Blender system console.
Installation#
“No module named paramiko” or “No module named docker”#
Vendored copies are populated on demand. Click Install Paramiko to Add-on Directory (SSH backends) or Install Docker-Py to Add-on Directory (Docker backends).
“No module named cbor2”#
The cbor2 module is needed to encode the scene before each transfer.
It is normally installed for you when the extension is installed, so a
missing copy usually means the install did not complete. Reinstall the
extension through Blender (Get Extensions or Install from Disk),
or click Install cbor2 to Add-on Directory in the Backend
Communicator panel. The button installs cbor2==6.0.1 into Blender’s
scripts/addons/modules directory.
Install operator hangs or fails#
pip exited non-zero or hit the 120 s internal timeout. Read its stderr
in the Blender system console. The install target - Blender’s user
scripts/addons/modules directory, which is both what the buttons write
to and where the add-on looks - must be writable; as a fallback, install
into it by hand with python -m pip install --target <blender user scripts>/addons/modules paramiko docker.
Connection: SSH#
SSH authentication failed#
Verify SSH Key points at the private key (not the .pub), it has
no passphrase or your agent has it loaded, and the matching public key
is in ~/.ssh/authorized_keys on the remote.
“Failed to parse command. Ensure it includes host.”#
The command names no destination. The parser reads [user@]host plus
-p, -i, -l, -J, and -o Port= / -o IdentityFile= /
-o User= / -o ProxyJump=; other ssh options are accepted and
ignored. An unknown option, an option missing its argument, and an
unbalanced quote are reported in their own words rather than under this
message. For a ProxyCommand setup no mode helps: the directive is read
nowhere in the add-on. Bring the tunnel up from a terminal and point the
add-on at the forwarded local port instead, as the next entry
describes.
~/.ssh/config host works in terminal but not in the add-on#
The parser understands only Host, HostName, Port, User,
IdentityFile, ProxyJump, and Include. ProxyCommand, Match,
and certificates are ignored. A bastion needs nothing done by hand:
ProxyJump is honored, and the Proxy Jump field and ssh -J set
the same thing. For the directives that are ignored, bring the tunnel up
from a terminal (ssh -L 2222:gpu01.example.com:22 bastion.example.com) and point the add-on at localhost:2222. See
Supported ssh_config options.
“Jump host … failed”#
The hop named in the message refused the connection or was unreachable;
the hops opened before it were closed. Test the same chain from a
terminal (ssh -J <jump> <host>), which uses the same hops. A message
naming a loop instead means two ProxyJump entries point at each other,
and the trail it followed is printed with it.
Connection: Docker#
“Container ‘…’ does not exist”#
Run docker ps -a on the daemon host and correct the Container
field, or create/start the container.
“Error starting container ‘…’”#
The container exists but docker start failed. Run it manually on the
host and read the error; common causes are publish-time port conflicts
or the user not being in the docker group.
“Docker port N is not exposed on container ‘…’”#
The add-on cannot publish ports on an existing container. Recreate it
with docker run -p <port>:<port> (or update compose.yaml).
Docker daemon or permission errors#
docker version should work as the same user. On Linux: sudo usermod -aG docker $USER, then log out and back in.
Connection: Windows Native#
“Solver path is not set” / “ppf-cts-server.exe not found under the solver root”#
Set Solver Path to the directory that contains ppf-cts-server.exe.
“Embedded Python not found”#
Neither the dev layout (build-win-native\python\python.exe) nor the
bundle layout (python\python.exe) resolved. Rebuild the dev tree, or
unpack the shipped bundle zip next to ppf-cts-server.exe.
CUDA DLL load errors#
server.log shows a missing CUDA runtime DLL. The bundle layout
expects CUDA on the system PATH; install a matching CUDA runtime, or
switch to the developer build (which ships its own).
“Remote path not found (…/ppf-cts-server)”#
The post-connect check looks for <path>/target/release/ppf-cts-server
and did not find it. Point Remote Path / Path / Container
Path at the solver root - the directory that has target/release/
under it, for example /root/ppf-contact-solver - not at
target/release itself. Windows Native does not raise this message: a
Solver Path with no binary under target\release\ or bin\ reports
“ppf-cts-server.exe not found under …” instead.
Connection profiles#
Profile dropdown empty after Open#
The TOML is malformed, unreadable, or empty. load_profiles swallows
all exceptions. Run the file through any TOML validator; common causes
are unclosed quotes or unescaped backslashes in Windows paths (use
\\ or forward slashes).
Profile loads but fields stay blank#
The type value does not match one of Local, SSH, SSH Command,
Docker, Docker over SSH, Docker over SSH Command, Windows Native. Case matters.
Note
Save rewrites the whole TOML; comments and original formatting are lost on round-trip. Keep a backup if comments matter.
Server startup#
Status stuck on “Waiting for Server Start…”#
You connected but did not click Start Server on Remote, or
ppf-cts-server exited before booting. Click Start Server on
Remote; if it then times out, see the next entry.
“Server startup timed out”#
Sixteen seconds passed without a ready marker. The panel pastes the
last 20 lines of server.log. Usual causes:
venv missing at
$HOME/.local/share/ppf-cts/venvCUDA driver missing or mismatched
the bound port is already in use (every backend takes the port from Docker Port, which the panel draws only for the Docker types)
“Port N is in use”#
Something is already bound to the port the server was told to use.
Only Windows Native raises this. There the add-on first probes the
port: if it answers a ppf-cts-server protocol ping, the add-on attaches
to that running server instead of erroring out (this is what lets you
restart Blender without losing the server). If the holder is not a
ppf-cts-server, the panel surfaces the error and shows a Force
Terminate Process button. Clicking it walks the process tree and
force-kills the listener on that port. If the squatter is not yours,
stop it by hand, or move the solver off that port: the field that
carries the port for every backend (Docker Port) is one shared
property drawn only for the Docker connection types, so change it from
a Docker mode and switch back, or set docker_port on a
connection profile entry.
“Server startup failed” with a log line#
progress.log emitted ERROR or FAILED during startup. Open the
full log on the remote; the real error (usually a missing module or
failed import) is higher up.
“Failed to launch server”#
The launch script itself never ran: permission denied, a /tmp that is
not writable or is mounted noexec, or a shell that could not execute
it. The script backgrounds the server with nohup, so it reports
success whatever happens after that - a missing venv or python3
surfaces later as a build-worker ModuleNotFoundError, not here.
Status: “Protocol version mismatch”#
The solver binary and the add-on were built from revisions that speak different wire protocols. The add-on stops the server on this status, so a restart picks up the on-disk binary; if the status comes back after that, the two halves are genuinely out of step. Rebuild the solver from a matching revision, or update the add-on. The add-on’s log names both sides (“server reports X, addon expects Y”), which tells you which one is older.
The add-on and the solver each take this number from the same shipped file, so there is nothing to configure and nothing to match up by hand: a mismatch always means one of the two binaries is stale.
Under the hood
The version is single-sourced in blender_addon/protocol_version.toml.
The add-on reads that file when it starts, and the server binary carries
the value that was compiled into it, so an add-on and a server built from
one revision always hold the same number. That file also carries the
changelog for every bump, which is the fastest way to tell whether a
stale server would merely behave differently or would reject the payload
outright.
Object groups and pins#
“Maximum number of groups reached”#
The cap is 32 (N_MAX_GROUPS). Delete or merge unused groups.
“Object ‘…’ is already in another group”#
Each object’s UUID can live in exactly one active group (the encoder uses it as the routing key). Remove it from the other group first.
“Object ‘…’ is library-linked and cannot be assigned”#
Linked data blocks are read-only and cannot carry the add-on’s UUID
property. Object > Make Local... first.
Pin operator errors in Edit Mode#
“No active edit object” - enter Edit Mode on a mesh/curve in the active group.
“Name cannot be empty” - type a pin name.
“No vertices selected” - select at least one vertex (or curve control point).
“’…’ is not in this group” - add the object via Add Selected Objects first.
Transfer, run, fetch#
“Mesh topology changed since last transfer”#
Topology (vertex count, polygon count, edge count, object count, or a pin group’s vertex count) changed after Transfer, or a Static object’s captured deformation was re-recorded; UVs are not part of the fingerprint and a UV-only edit never raises this. Click Transfer again. Fetching across a mismatch would bind a PC2 to a mesh of a different vertex count.
“Objects missing UUID” / “Stale UUID references”#
Usually after loading an old file or renaming objects. Tick Debug Options in the Backend Communicator panel, then click Run UUID Migration.
ValueError: Object '...' is assigned to both '...' and '...' groups#
The same object (identified by its UUID) appears in more than one active group. Select it in a group’s object list and click Remove Object until it is left in exactly one group.
“Object ‘…’ has N isolated vertex(es)” (the message says “isolated vert”)#
A Static collider mesh has stray points that belong to no face, common in
imported models. Click Remove Isolated Vertices under the error to delete
them, then Transfer again. That button and the matching MCP pair
remove_isolated_static_vertices / detect_isolated_static_vertices work on
active Static colliders only, which is what this error names.
For the same points on any mesh, select the objects and run Scan Selected Meshes in the Utility Tools > Mesh Cleaning panel, then Remove Loose Vertices from the report. That route works on the whole selection whatever the group type, keeps pinned vertices, skips a Sand particle mesh (whose grain centers are legitimately in no face), and names the caches the vertex-count change invalidates instead of leaving them stale. See Mesh Cleaning.
Transfer or build rejects the mesh#
Any rejection that is shaped like “this geometry is wrong” is worth scanning before you debug it. Scan Selected Meshes in the Utility Tools > Mesh Cleaning panel covers every geometry check Transfer performs and adds several that nothing else performs, so anything Transfer would reject on geometry grounds shows up in the scan first, with its fix button attached:
Scan reports |
Fix |
|---|---|
Near-coincident vertex pairs |
Merge by Distance |
Isolated vertices, hanging seam vertices |
Remove Loose Vertices |
Degenerate (zero-area) faces |
Dissolve Degenerate |
Duplicate faces |
Delete Duplicate Faces |
Inconsistently wound edges |
Recalculate Outside |
Linked duplicate of another object |
|
The first three change the vertex count and ask for confirmation first.
It does not work the other way around: a clean scan is not a promise that Transfer will pass. Transfer also checks things that are not geometry, such as an object assigned to two active groups at once, an assigned object that no longer resolves, a Reference Rest Angle object that has gone missing, and a captured deformation whose vertex count no longer matches its mesh. The solver then runs its own intersection test when it builds the scene. None of those are visible to the scan, and each is reported by name in the panel’s error line.
The scan is also stricter than Transfer on two rows, on purpose. Isolated vertices stop a Transfer only on a Static collider, and hanging seam vertices only on a Shell or Solid, while the scan reports both on any mesh you select. Where Transfer tolerates them they are still worth removing: a point that belongs to no face has no surface area and no mass, so it does nothing in the solve.
Boundary edges, non-manifold edges, and faces with more than three corners are reported as notes rather than errors: an open, quad-built cloth mesh is normal, and only a Solid group needs a closed surface. The Triangulate button under the quad note leaves an explicit triangle with no diagonal left for Blender to re-pick as the mesh deforms, which is what keeps the displayed surface on the simulated one.
“N self-intersections (… tri-tri, … rod-tri)”#
Two pieces of geometry already overlap, so the scene is refused before the
first frame is solved: a mesh folding through itself, a mesh through another
mesh, or a mesh through a collider. The count is of overlapping element
pairs, split by whether the pair is two triangles or a rod segment against a
triangle. The solver runs its own intersection test as it steps; an overlap
found there ends the run, and the log line reads ### intersection detected.
The fix is to separate the geometry: move the collider or the character to a pose the garment sits outside of, or edit the mesh so the fold is gone.
When the overlap is expected and the simulation is meant to resolve it, a garment fitted onto a rig-deformed character being the usual case, the group settings under Allow Intersections accept the pairs you name instead of stopping. Set them on the group that is simulated: a Static collider left in its rest pose carries neither setting, so it is the garment’s group that has to allow the pair. For an overlap confined to a pinned region, the narrower per-pin Allow Intersections Here does the same for the elements that pin holds completely. Both suppress the report only: the surfaces are still in contact and the solver still pushes them apart.
Status: “Connection lost” during a run#
Network hiccup, server crash (OOM, driver fault), or host reboot.
Reconnect; if frames were produced, Fetch All Animation pulls them. The
remote server.log has the cause.
“Server error” in the status line#
The server returned a JSON error, usually from a solver exception.
Read server.log for the traceback. Common causes: out of disk on the
remote, permission denied on the project directory, CUDA OOM.
Status: “Simulation Failed”#
The solver crashed instead of finishing. Backend Communicator prints
Solver failed: and a one-line cause — an intersection, a failed
continuous collision detection, a linear solver that would not
converge, a Newton solve that made no progress, out of GPU memory, an
unrecoverable CUDA fault, a kernel past the OS watchdog timeout, a GPU
architecture missing from the solver’s device image, or a process
killed before it could report — with an untranslated detail line under
it carrying the machine data (a CUDA error name, a signal, a file and
line). The full report, solver stdout and stderr tails included, goes
to the add-on console: press Show Console on the row below. Under
that row, Open Session Folder opens the run’s session directory,
where the solver’s logs and status record are written. It is grayed
out with The session folder is not known for this run when the
remote root cannot be resolved, and the path it opens is a path on the
machine the server runs on, so it reaches real files only when
that machine is this one.
Fetch and playback#
“N frames unfetched” in the Solver panel#
The remote has frames the local Blender does not; the panel’s line reads
N frames unfetched. Press "Fetch All Animation". Click Fetch All
Animation.
Render with unfetched frames#
A popup warns “N frames unfetched”. Fetch first, then render. The popup fires once per render and does not block.
“Data path: … does not exist”#
The data/<blend-file-name>/ folder referenced by a MESH_CACHE
modifier is missing (deleted, renamed, or not copied across machines).
Restore from backup, or click Migrate data/… to rebind.
Bake#
“Remove all shape keys except Basis before baking”#
Baking adds one shape key per frame and keyframes their values, which
would double-blend with any shape keys the mesh already carries. In
Object Data Properties, delete every shape key except Basis on the
listed objects.
MCP server#
Port already in use#
The add-on retries the base port a few times, then walks 9634-9642
and binds the first free one. Started from Start MCP Server on
Local it does not do this silently: the substitution is reported as a
warning and written back into the panel’s Port field, so the
MCP Server (Running :port) header names the live port. Check it
there if an external client points at the base.
“Could not find available port in range 9633-9642”#
All ten slots are taken. Kill the holder (lsof -i :9633-9642 on
macOS/Linux, netstat -ano | findstr 963 on Windows), or change the
base port.
“Failed to start MCP server”#
The server thread raised during startup (socket permission, port
collision, import error). The Blender system console prints the
exception as MCP Server error: ....
run_python_script returns "status": "error"#
The snippet you sent raised. Read the error field; the full
traceback is in the Blender system console.
capture_viewport_image: “No 3D viewport found”#
The current Blender screen layout has no VIEW_3D area. Switch to
Layout, Modeling, or Sculpting, then retry.
Debug Options#
Debug Options is the checkbox on the same row as Update Stat and Show Console in the Backend Communicator panel, and it is off by default. Ticking it reveals nine labelled blocks below the panel’s usual contents. They are development and diagnosis tools rather than parts of the simulation workflow: they address the connected machine directly, exercise the transport on its own, or reach into the add-on itself. What they run elsewhere - a shell command, a server query, a render - reports back into the add-on console, which Show Console opens. Controls that need a live connection are greyed out rather than hidden, so the block always shows what would become available once you connect.
Shell Calls#
Two tools that both address the machine at the other end of the connection, at two different levels.
Exec Command via Server reads the Args field as a run of
--key value pairs and sends them to the running solver server as a
single query, over the same socket the panel’s status poll uses; the
JSON reply is printed to the console between two ------ rules. It
speaks the server’s own text-command protocol rather than a shell, so it
needs both a connection and a started server, and it is the way to ask
the server something the panel does not put on screen.
Execute Shell Command on Remote runs the Command field on the
machine the backend runs on, in the directory the connection points at
(the solver root), and prints its stdout and stderr to the console. It
needs a connection but not a running server, which is what makes it the
tool for the case where the server will not start: list the venv, run
nvidia-smi, tail server.log. On a Docker backend the command runs
inside the container, not on the daemon host. Run as Shell, on by
default, is what makes pipes, redirection and && work - it wraps the
command in /bin/sh -c on the SSH and Docker backends and hands it to a
shell on the Local and Windows Native ones - and there is rarely a
reason to untick it.
Data Transfer Tests#
Data Send generates as many megabytes of random bytes as Data Size
(MB) asks for (1 to 256) and uploads them to dummy_data.pickle in
the remote root. Data Receive downloads that same file and compares
it byte for byte against the copy still held in memory, reporting either
“Data received matches test data.” or an error.
The pair exists to take the scene out of the picture. When a Transfer stalls, a fetch never finishes, or a payload arrives damaged, the cause can be the link, the encoder, or the solver, and nothing in the panel separates them. A block of random bytes has no mesh, no encoder and no solver behind it, so a round trip that fails here is the connection, and one that succeeds at a size comparable to your scene moves the suspicion upstream. The progress bar and throughput readout that run alongside it also give you the link’s real speed, which is the number to compare against when a transfer merely feels slow.
Data Receive stays unavailable until a Data Send has run in the
same Blender session: the reference copy it compares against is held in
memory, so before then there is nothing to check a download against.
Both need the server running, not just a connection. Two things worth
knowing before you read the result: on the Local and Windows Native
backends the payload is written straight to the filesystem instead of
through the socket (unless PPF_FORCE_TCP_TRANSFER=1 is set in the
environment), so there the round trip measures a file copy rather than a
network; and the test file is left behind in the remote root - nothing
in the add-on deletes it - so clean it up yourself after a large test.
Options#
A single field, Max Console Lines (default 60, from 8 to 10000). Each time the add-on flushes queued messages into its console text block, it trims the block back to that length. The cap is what keeps a long run from growing the text block without bound; raise it when a traceback or a Simulation Failed report scrolls past before you can read it. It bounds only what Blender keeps in memory - a log file written by the block below still receives every line.
Console Log Export#
The path field is the switch: while it holds a path, every line the
add-on writes to its console is also appended to that file, with the
parent directory created if it does not exist, and clearing the path
turns file logging off again. The folder button opens a file browser
(pre-filled with log.txt in your home directory when nothing is set
yet), the X clears the path, and Delete Log deletes the file
itself.
Reach for it when what you need to read outlives the console. The console is trimmed to Max Console Lines and lives only as long as the Blender session, so a crash, a driver fault, or an unattended overnight run leaves nothing to inspect afterwards, while a file survives all three. It is appended to and never truncated, so it accumulates across sessions, which is what Delete Log is for.
GitHub Repo on Remote#
Git Pull and Compile run git pull and
/root/.cargo/bin/cargo build --release in the connection’s directory,
by the same route as Execute Shell Command on Remote and with their
output in the console. Both need a connection and nothing else in
flight. Together they are the update loop for a remote you develop
against: pull, rebuild, then stop and start the server so the new binary
is the one running. The cargo path is hardcoded to
/root/.cargo/bin/cargo, which is where the project’s container image
installs it; on a host that keeps cargo elsewhere the button fails, and
the shell field above is the way to run the build by hand.
Open GitHub Link sits under the same label but touches no connection at all: it opens the project’s public repository page in the browser on your own machine.
GitHub Repo on Local#
Git Pull (Local) runs git pull as a subprocess whose working
directory is inside the add-on’s own source tree, so it updates the copy
of the add-on that Blender is running - not the solver on the remote.
That only does something when the add-on is installed as a git checkout,
which is the developer layout; an extension unpacked from a zip is not a
repository, and the pull reports git’s own complaint instead. It gives
up after 60 seconds, killing the process and reporting “Local git pull
timed out”. Follow it with Reload Add-on Now below to run what you
just pulled without restarting Blender.
UUID Migration#
Run UUID Migration walks the scene and fills in the stable identifiers the add-on uses to follow things across renames: a group’s own UUID, the UUID on every assigned object, the object UUID and vertex-group content hash on every pin, and the object UUIDs on every snap/stitch pair, including the ones stored inside a pair’s stitch data.
The problem it solves is historical. Earlier versions referenced objects
and vertex groups by NAME, so renaming an object - or letting Blender
append .001 to a duplicate - silently broke the reference. An object
now carries a UUID as a custom property and a vertex group is matched by
a hash of the vertex indices it holds, so both survive a rename. A
.blend saved before that change still carries name-only references,
and this pass is what converts them.
You rarely have to press it. The same pass runs automatically after a
file is loaded whenever anything is found missing, writing its result to
the console prefixed with [auto-migrate], and Transfer refuses a
scene whose identifiers are incomplete with a message that ends “Run
UUID Migration first.” rather than uploading it. The button matters for
what the automatic pass does not cover: a record inserted
programmatically rather than through the panel, and the case where the
automatic attempt raised and was swallowed, which leaves no message at
all. Pressing it always reports what it did.
Two details about the result. It is printed under the button and stays
there until the next run, so you can read it after the panel redraws;
and it ends with “Save to persist”, which is literal - the identifiers
are scene data, and closing the .blend without saving loses them.
Every identifier it writes is one that was empty, so a second run on an
already-migrated scene changes nothing and says so.
Render#
Render Animation renders the scene’s frame range one frame at a time, driven by a timer, with a progress bar naming the frame and the percentage done; Stop halts it.
It exists because of one specific interaction with curve playback. The
built-in Render Animation has been observed to evaluate the
depsgraph between render_pre and frame_change_pre, which leaves the
add-on’s curve cache half-applied - about half the splines render at
their rest pose while the viewport looks correct. This loop sets the
frame first, which runs the add-on’s playback handler to completion, and
only then renders that single frame. So reach for it when a rendered
animation of simulated curves or rods shows strands frozen in the rest
pose that the viewport shows moving.
Output goes to the paths the built-in render would use, one file per frame from the scene’s output settings, so it is a drop-in replacement rather than a separate export. If files matching those paths already exist, a dialog says how many, and they are deleted before the render starts - cancel there if they matter. Stop sets a flag that is read between frames, so the frame in flight finishes first; Blender exposes no mid-frame cancel to Python. The playhead and the output path are restored when the run ends or is cancelled.
Add-on Local Debug Server#
Start binds a TCP socket on localhost at Port (8765 by
default; the field is greyed out while the server runs), and it is what
the blender_addon/debug/main.py CLI talks to - see
Debug CLI below. It is also what makes
reloading possible at all: Reload Add-on Now and Full Reload are
drawn whether or not the server is running, but both fail with an error
naming the reload server as not running until you press Start.
Reload Add-on Now deletes the add-on’s modules from sys.modules,
invalidates the import caches, and disables and re-enables the add-on
within one event-loop tick, so what runs afterwards is the source
currently on disk.
Full Reload does the same work but splits the disable and the enable
across two ticks, which is what lets Blender rebuild RNA for a changed
PropertyGroup; reach for it when an edit to a property definition does
not show up after a plain reload (see
Hot reload). Both are scheduled through
a timer rather than run inline, so the button returns immediately and
the reload happens a tick later; running the reload inline would free
the operator while its own Python frame is still on the stack, which
crashes Blender.
The server is restarted for you after a reload if it was running before it, but it comes back on the default port rather than the one you typed, and the CLI only ever talks to 8765. A custom Port is therefore only useful for a session you drive by hand.
Warning
While this server runs, any process on your machine can send it a JSON
packet that executes arbitrary Python inside Blender. There is no
authentication and no Origin check. Start it when you are debugging
and stop it when you are done; see
Code Execution Risk.
Debug CLI#
“MCP server not reachable on localhost:9633”#
The CLI got no answer in 2 s. Start the MCP server from the Blender
panel or via python blender_addon/debug/main.py start-mcp. If it is
running on a fallback port, pass --mcp-port <port>.
“Debug/reload port (TCP 8765): unreachable”#
The add-on is not loaded, or the reload server never bound. Enable the
add-on in Edit > Preferences > Add-ons; if Blender is running and the
port stays down, restart Blender.
Hot reload#
Change didn’t show up after reload#
Almost always a PropertyGroup schema change; plain reload swaps code
but cannot rebind Blender’s RNA. Run
python blender_addon/debug/main.py full-reload
If even that fails, restart Blender. This is a Blender RNA limitation.
“Reload timed out”#
Plain reload times out at 45 s, full reload at 70 s. Something in
top-level module code, register, or unregister is blocking the
main thread (network call, large mesh op, missing
bpy.app.timers.unregister). The Blender system console shows what
was still running.