cvcGL: never render synchronously from a state-change handler - #161
Merged
Conversation
added 2 commits
July 27, 2026 07:22
GraphicsNode::handleStateChanged ended with vtkRenderWindow::Render(), and GeometryNode's polydata-update path did the same. The handler runs once per changed state key, so per-frame animation — setTransform on many nodes, each of which serializes its matrix into the state tree — turned every displayed frame into dozens of full scene renders. Measured in VolRover3 on the Austin platoon demo (28 soldiers + 4 vehicles + 27 radio-link segments animated per frame over a ~1M-triangle city): setTransform, unchanged matrix 0.07 ms (state short-circuits; no signal) setTransform, changing matrix 5.6 ms (handler fires -> full Render()) bare state write, no handler 0.01 ms 63 changing transforms x ~5.5 ms of synchronous rendering = ~350 ms per frame; the app ran at 2.7 FPS with the GPU active. The per-call cost is the whole scene render, not the state tree: setColor alone writes three keys and so rendered the scene three times. Both sites now set SceneGraph's render-needed flag via a new requestRender() and let the host's frame loop render once per frame through checkAndResetRenderNeeded() — the contract volrover3's continuous mode already implements. The synchronous Render() remains only as a fallback for a node used without a SceneGraph, where nothing drains the flag.
Sort the added #include <cvc/gl/SceneGraph.h> into alphabetical position in GraphicsNode.cpp and GeometryNode.cpp (clang-format changed-lines check).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GraphicsNode::handleStateChangedended with a synchronousvtkRenderWindow::Render(), andGeometryNode's polydata-update path did the same. The handler runs once per changed state key, so per-frame animation —setTransformon many nodes, each serializing its matrix into the state tree — turned every displayed frame into dozens of full scene renders.Measurements (VolRover3, Austin platoon demo: 32 actors + 27 link segments animated per frame over a ~1M-triangle city)
setTransform, unchanged matrixsetTransform, changing matrixRender())63 changing transforms × ~5.5 ms of synchronous rendering ≈ 350 ms per frame — the app ran at 2.7 FPS with the GPU active (
libGLX_nvidiaconfirmed loaded). The per-call cost is the whole scene render, not the state tree;setColoralone writes three keys and so rendered the scene three times.Also reproducible in isolation: an in-app microbenchmark writing the node's
matrixstate key directly viapycvc.state_setcosts 4.98 ms/call — the C++setTransformwrapper contributes nothing; it's all handler → render.Fix
Both sites now set
SceneGraph's render-needed flag via a newrequestRender()and let the host's frame loop render once per frame throughcheckAndResetRenderNeeded()— the contract volrover3's continuous mode already implements. The synchronousRender()remains only as a fallback for a node used without a SceneGraph, where nothing drains the flag.With the demo additionally driving per-frame motion through actor
UserMatrix(workaround until this ships), the same scene runs at the render cap. After this lands and the family republishes,setTransformin a loop becomes ~0.1 ms/call and the workaround can be retired.Notes for review
requestRender()takes the samem_eventQueueMutexthe flag already uses; no new locking.postEventalready set it; this just adds a public way to set it without queueing a no-op event.