Fix Newton marker world rendering#6281
Conversation
Greptile SummaryThis PR fixes environment-major ordering in Newton marker filtering (previously the loop was world-major, producing wrong instance indices), adds per-world visual offset application to marker overlays, exposes
Confidence Score: 4/5Safe to merge; the core filter rewrite is correct and well-tested, and the two concerns are edge cases in the new public API surface rather than bugs in the primary render path. The env-major slicing fix is the right approach and all three new test cases verify the expected translation values end-to-end. The two flagged items are both in source/isaaclab_visualizers/isaaclab_visualizers/newton/newton_visualization_markers.py — specifically Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant NewtonVisualizer
participant render_newton_visualization_markers
participant NewtonVisualizationMarkers
participant _filter_marker_state
participant _apply_marker_world_offsets
participant ViewerGL
Caller->>NewtonVisualizer: render_markers(viewer, num_envs)
Note over NewtonVisualizer: guards: enable_markers, resolves num_envs
NewtonVisualizer->>render_newton_visualization_markers: (viewer, resolved_env_ids, num_envs)
loop each registered marker group
render_newton_visualization_markers->>NewtonVisualizationMarkers: marker.render(viewer, env_ids, num_envs)
NewtonVisualizationMarkers->>_filter_marker_state: (marker, env_ids, num_envs)
Note over _filter_marker_state: env-major slicing: env_id x repeat_count builds world_indices tensor
_filter_marker_state-->>NewtonVisualizationMarkers: state with translations and world_indices
NewtonVisualizationMarkers->>_apply_marker_world_offsets: (viewer, translations, world_indices, num_envs)
Note over _apply_marker_world_offsets: reads viewer.world_offsets, index_select by world_indices
_apply_marker_world_offsets-->>NewtonVisualizationMarkers: offset translations
NewtonVisualizationMarkers->>ViewerGL: log_instances / log_lines
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Caller
participant NewtonVisualizer
participant render_newton_visualization_markers
participant NewtonVisualizationMarkers
participant _filter_marker_state
participant _apply_marker_world_offsets
participant ViewerGL
Caller->>NewtonVisualizer: render_markers(viewer, num_envs)
Note over NewtonVisualizer: guards: enable_markers, resolves num_envs
NewtonVisualizer->>render_newton_visualization_markers: (viewer, resolved_env_ids, num_envs)
loop each registered marker group
render_newton_visualization_markers->>NewtonVisualizationMarkers: marker.render(viewer, env_ids, num_envs)
NewtonVisualizationMarkers->>_filter_marker_state: (marker, env_ids, num_envs)
Note over _filter_marker_state: env-major slicing: env_id x repeat_count builds world_indices tensor
_filter_marker_state-->>NewtonVisualizationMarkers: state with translations and world_indices
NewtonVisualizationMarkers->>_apply_marker_world_offsets: (viewer, translations, world_indices, num_envs)
Note over _apply_marker_world_offsets: reads viewer.world_offsets, index_select by world_indices
_apply_marker_world_offsets-->>NewtonVisualizationMarkers: offset translations
NewtonVisualizationMarkers->>ViewerGL: log_instances / log_lines
end
Reviews (1): Last reviewed commit: "Fix Newton marker world rendering" | Re-trigger Greptile |
| spacing = getattr(viewer, "_user_spacing", None) | ||
| if spacing is not None and all(float(spacing[axis]) == 0.0 for axis in range(3)): | ||
| return translations | ||
|
|
||
| world_offsets = getattr(viewer, "world_offsets", None) |
There was a problem hiding this comment.
The
_user_spacing early-exit guard reads a private attribute (_user_spacing) that only exists on NewtonViewerGL. When render_markers is called with an arbitrary external viewer this attribute won't be present and getattr silently returns None, which is fine — but the inverse is also true: any viewer that happens to carry _user_spacing = (0, 0, 0) for unrelated reasons will silently suppress offset application even when world_offsets is non-zero. The safe check is the presence of world_offsets itself, which is already tested two lines later.
| spacing = getattr(viewer, "_user_spacing", None) | |
| if spacing is not None and all(float(spacing[axis]) == 0.0 for axis in range(3)): | |
| return translations | |
| world_offsets = getattr(viewer, "world_offsets", None) | |
| world_offsets = getattr(viewer, "world_offsets", None) |
| indices = world_indices.to(device=offsets.device) | ||
| selected_offsets = offsets.index_select(0, indices).to(device=translations.device, dtype=translations.dtype) | ||
| return translations + selected_offsets |
There was a problem hiding this comment.
Missing bounds guard before
index_select
world_indices values are env-ids in [0, num_envs), but offsets.shape[0] equals however many worlds the passed viewer was initialised for. Via the new public render_markers(other_viewer, num_envs) API it's easy to pass a viewer whose world_offsets was built for a different (smaller) world count, causing index_select to throw an out-of-range error at runtime. Adding a guard like if offsets.shape[0] < num_envs: return translations before this block would prevent the crash and match the defensive style used elsewhere in the function.
Render dense marker batches directly in environment-major order and apply viewer offsets inline. Cache marker specifications and resolve visible worlds once so marker and model layout share one explicit contract.
Select dense marker state along its environment axis and broadcast world offsets instead of constructing flattened gather and repeated-offset tensors on every frame. Preserve prototype selections on partial updates and expand small defaults only after prototype filtering.
Summary
NewtonVisualizerCfg.world_spacingfor visual-only separation of co-located simulation worldsNon-divisible marker batches remain global and are neither filtered nor offset.
Testing
22 passed:source/isaaclab_visualizers/test/test_newton_adapter.py10 passed: scoped Newton marker render and partial-update tests[12, 13, 36, 37][0, 1, 12, 13, 24, 25, 36, 37][0, 1, 2][1, 3, 5, 7]instead of[12, 13, 36, 37]; the old allocation path also invoked the forbiddentorch.arangeregression guard[3, 1]and[1, 3]preserve transform, scale, and source tensor state./isaaclab.sh -dpassed with warnings treated as errors