Skip to content

Commit e7f34eb

Browse files
authored
Adds Newton + Isaac RTX Rendering Performance Optimizations (#5017)
# Newton + Isaac RTX Rendering Performance Optimizations This document describes four performance optimizations applied to the Newton physics simulator when used with the Isaac Sim RTX renderer inside Isaac Lab. Together they reduce per-frame time from **~323 ms to ~60 ms** (a **5.4x speedup**), making Newton's rendering path slightly faster than PhysX's equivalent (~65 ms). All live primarily in two files: - `source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py` - `source/isaaclab_newton/isaaclab_newton/physics/_cubric.py` (new) with small additions to `PhysicsManager` and `SimulationContext` in the core `isaaclab` package. --- ## Baseline: ~323 ms per frame The starting point is the unoptimized Newton + RTX rendering loop. A Nsight Systems trace reveals the structure: - **Two physics steps** execute per frame (typical for 2× physics substeps per render frame). - **After each physics step**, Newton writes updated body transforms to Fabric (Omniverse's GPU scene-graph cache) and then triggers a full CPU hierarchy update via `update_world_xforms()`. This hierarchy walk recomputes every world-space transform in the scene from parent-child relationships — even though Newton already computed the correct world transforms and wrote them directly. - The Kit renderer also runs its own, lighter, internal hierarchy update. The per-step Fabric sync and hierarchy update dominates the frame. Because it runs after *every* physics step (not just before rendering), the cost is multiplied by the number of substeps. <img width="2169" height="750" alt="newton-rtx-baseline" src="https://github.com/user-attachments/assets/f7fc0079-9cca-43d2-9ade-9069e29718d4" /> --- ## Optimization 1 — Dirty-Flag Deferred Sync: ~244 ms per frame ### Problem Every physics substep was calling `sync_transforms_to_usd()`, which writes Newton body poses to Fabric and then invokes `update_world_xforms()`. The hierarchy update is expensive and only needs to happen once before the renderer reads the scene — not after every substep. ### Solution A **dirty-flag pattern** decouples physics stepping from Fabric synchronization: 1. **`_mark_transforms_dirty()`** — called at the end of each `_simulate()` call, sets `_transforms_dirty = True`. This is cheap (a boolean assignment). 2. **`sync_transforms_to_usd()`** — now checks `_transforms_dirty` at the top and returns immediately if transforms haven't changed. When dirty, it writes transforms and calls the hierarchy update, then clears the flag. 3. **`pre_render()`** — a new method added to `PhysicsManager` (base class) and overridden by `NewtonManager`. It calls `sync_transforms_to_usd()`. The `SimulationContext.render()` method calls `physics_manager.pre_render()` before updating visualizers and cameras, ensuring transforms are flushed exactly once per render frame. The key insight is that the renderer only reads scene transforms during `render()`, not during `step()`. By deferring the Fabric write and hierarchy update to render time, we eliminate redundant work when multiple physics substeps run per render frame. For 2 substeps per frame, this cuts the hierarchy update count in half. ### Key code paths - `_simulate()` → `_mark_transforms_dirty()` (just sets a flag) - `SimulationContext.render()` → `PhysicsManager.pre_render()` → `NewtonManager.sync_transforms_to_usd()` (runs once, clears the flag) <img width="2174" height="765" alt="newton-rtx-dirty" src="https://github.com/user-attachments/assets/eae6dbd9-7936-492e-a922-4fff5c0d7861" /> --- ## Optimization 2 — CUDA Graph Capture (Relaxed Mode): ~144 ms per frame ### Problem Looking at the physics steps in the trace, the GPU is underutilized. Each Warp kernel launch (collision detection, constraint solve, integration, FK evaluation) incurs a round-trip to the CPU via Python — launch overhead, GIL acquisition, and driver calls. For a simulation with many small kernels per substep, this CPU-side overhead becomes the bottleneck while the GPU sits idle between dispatches. Newton already supported CUDA graphs (pre-recording a sequence of kernel launches and replaying them with a single driver call), but CUDA graph capture was **disabled when RTX rendering was active**. The original code had: ```python use_cuda_graph = cfg.use_cuda_graph and (cls._usdrt_stage is None) ``` This was necessary because RTX's background threads use CUDA's legacy stream (stream 0) for async operations like `cudaImportExternalMemory`. Warp's standard `ScopedCapture()` uses `cudaStreamCaptureModeThreadLocal` on a blocking stream, which implicitly synchronizes with legacy stream 0. If RTX ops happen during capture, the CUDA runtime raises error 906 (`cudaErrorStreamCaptureImplicit`). ### Solution A **deferred, relaxed-mode CUDA graph capture** strategy that is compatible with RTX: **Deferral:** Graph capture is postponed from `initialize_solver()` to the first `step()` call. By that time, RTX has finished its initialization (all `cudaImportExternalMemory` calls are done) and is idle between render frames, providing a clean capture window. ```python # In initialize_solver(): cls._graph = None cls._graph_capture_pending = True # In step(): if cls._graph_capture_pending: cls._graph = cls._capture_relaxed_graph(device) ``` **Relaxed-mode capture** (`_capture_relaxed_graph`): This method works around two conflicting requirements: 1. **RTX compatibility**: RTX threads use legacy stream 0. A blocking stream (Warp's default) implicitly syncs with it, causing capture failures. Solution: create a **non-blocking stream** (`cudaStreamNonBlocking = 0x01`) that has no implicit synchronization with stream 0. 2. **Warp compatibility**: `mujoco_warp` internally calls `wp.capture_while`, which checks Warp's `device.captures` registry to decide whether to insert a conditional graph node or synchronize eagerly. Without a registered capture, it calls `wp.synchronize_stream` on the capturing stream — which is illegal inside graph capture. Solution: call `wp.capture_begin(external=True, stream=fresh_stream)` to register the capture in Warp's tracking without calling `cudaStreamBeginCapture` again (already done externally). The capture sequence: 1. **Warmup run** — execute `_simulate_physics_only()` eagerly to pre-allocate all MuJoCo-Warp scratch buffers (allocations are forbidden inside graph capture). 2. **Create a non-blocking CUDA stream** via `cudaStreamCreateWithFlags(..., NonBlocking)`. 3. **Begin capture** with `cudaStreamBeginCapture(..., cudaStreamCaptureModeRelaxed)` — relaxed mode allows other streams to operate freely during capture. 4. **Register with Warp** via `wp.capture_begin(external=True, stream=...)`. 5. **Record physics kernels** — `_simulate_physics_only()` inside `wp.ScopedStream(fresh_stream)`. 6. **Finalize** — `wp.capture_end()` then `cudaStreamEndCapture()` to obtain the graph. **Physics-only capture:** `_simulate_physics_only()` was factored out of `_simulate()` to exclude Fabric sync operations (`wp.synchronize_device`, `wp.fabricarray`) that are incompatible with graph capture. After graph replay, `step()` marks transforms dirty, and `pre_render()` handles the Fabric sync eagerly. The ctypes binding to `libcudart.so` is used directly because Warp's `ScopedCapture` doesn't expose control over capture mode or stream type. <img width="2168" height="745" alt="newton-rtx-cuda-graph" src="https://github.com/user-attachments/assets/eecfde04-41f4-488e-97e4-4d87cf617830" /> --- ## Optimization 3 — GPU Transform Hierarchy via cubric: ~60 ms per frame ### Problem Even with the dirty-flag pattern reducing hierarchy updates to once per render frame, the `update_world_xforms()` call is still a **CPU-side tree walk** over the entire Fabric scene graph. For scenes with thousands of prims (typical in multi-environment RL), this CPU hierarchy propagation is a significant bottleneck. The PhysX backend avoids this problem by using **cubric** — a GPU-accelerated transform hierarchy library. cubric runs the parent-child transform propagation entirely on the GPU via `IAdapter::compute()`, which is dramatically faster than the CPU walk. However, cubric has no Python bindings. ### Solution **Pure-Python ctypes bindings to cubric's Carbonite interface** (`_cubric.py`), allowing Newton to use the same GPU hierarchy propagation that PhysX uses. cubric is implemented as a Carbonite plugin and exposes its API through the `omni::cubric::IAdapter` interface. The bindings work by: 1. **Acquiring the Carbonite Framework** — `libcarb.so`'s `acquireFramework()` returns the singleton `Framework*`. 2. **Acquiring the IAdapter interface** — calling `tryAcquireInterfaceWithClient()` with the interface descriptor `omni::cubric::IAdapter` version `0.1`. 3. **Wrapping function pointers** — the `IAdapter` struct is a C++ vtable-like struct with function pointers at known offsets. Each function pointer is read from the struct at its byte offset and wrapped with `ctypes.CFUNCTYPE` to make it callable from Python. **Integration in `sync_transforms_to_usd()`:** The sync method now mirrors PhysX's `ScopedUSDRT` pattern: 1. **Pause Fabric change tracking** — `track_world_xform_changes(False)` and `track_local_xform_changes(False)`. This is critical: `SelectPrims` with `ReadWrite` access internally calls `getAttributeArrayGpu`, which marks Fabric buffers dirty. If tracking is still active, the hierarchy records the change and Kit's `updateWorldXforms` will do an expensive connectivity rebuild every frame. 2. **Write transforms** — the existing Warp kernel writes Newton body poses to Fabric's `omni:fabric:worldMatrix`. 3. **Resume tracking** — re-enable change tracking (in a `finally` block for safety). 4. **Run cubric compute** — `IAdapter::compute()` with `eRigidBody | eForceUpdate` options and `eAll` dirty mode. The `eRigidBody` flag tells cubric to use **inverse propagation** on prims tagged with `PhysicsRigidBodyAPI` (preserve the world matrix that Newton wrote, derive the local transform) and **forward propagation** on everything else (propagate parent transforms to children). `eForceUpdate` bypasses cubric's change-listener dirty check since we know transforms have changed. The adapter is lazily created on the first `sync_transforms_to_usd()` call rather than during `initialize_solver()`, to avoid startup-ordering issues with the cubric plugin. When cubric is unavailable (e.g., plugin not loaded, CPU-only), the code falls back gracefully to the CPU `update_world_xforms()` path. ``` sync_transforms_to_usd(): ┌─────────────────────────────────┐ │ Pause Fabric change tracking │ ├─────────────────────────────────┤ │ SelectPrims (ReadWrite) │ │ wp.launch(_set_fabric_transforms) │ ← GPU: write Newton poses to Fabric │ wp.synchronize_device() │ ├─────────────────────────────────┤ │ cubric IAdapter::compute() │ ← GPU: propagate hierarchy ├─────────────────────────────────┤ │ Resume Fabric change tracking │ └─────────────────────────────────┘ ``` A future Kit release is expected to ship official Python bindings for cubric, at which point the ctypes approach can be replaced. The result is a frame time of **~60 ms** — slightly faster than PhysX's **~65 ms** on the same scene. <img width="2169" height="896" alt="newton-rtx-cubric" src="https://github.com/user-attachments/assets/1474b806-fe82-44be-add3-324971ec37a0" /> --- ## Summary | Optimization | Frame Time | Speedup vs. Baseline | Key Technique | |---|---|---|---| | Baseline | ~323 ms | — | Sync + hierarchy after every substep | | Dirty-flag deferred sync | ~244 ms | 1.3× | Sync once per render frame, not per substep | | CUDA graph (relaxed mode) | ~144 ms | 2.2× | Eliminate per-kernel CPU launch overhead | | cubric GPU hierarchy | ~60 ms | 5.4× | GPU hierarchy propagation via ctypes bindings | All four optimizations are complementary and stack on top of each other. The final result matches or slightly beats the PhysX rendering path (~65 ms) while using Newton as the physics backend. *Co-developed with Toby Jones (NVIDIA).*
1 parent dc36792 commit e7f34eb

4 files changed

Lines changed: 596 additions & 44 deletions

File tree

source/isaaclab/isaaclab/physics/physics_manager.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,17 @@ def step(cls) -> None:
265265
"""Step physics simulation by one timestep (physics only, no rendering)."""
266266
pass
267267

268+
@classmethod
269+
def pre_render(cls) -> None:
270+
"""Sync deferred physics state to the rendering backend.
271+
272+
Called by :meth:`~isaaclab.sim.SimulationContext.render` before cameras
273+
and visualizers read scene data. The default implementation is a no-op.
274+
Backends that defer transform writes (e.g. Newton's dirty-flag pattern)
275+
should override this to flush pending updates.
276+
"""
277+
pass
278+
268279
@classmethod
269280
def close(cls) -> None:
270281
"""Clean up physics resources.

source/isaaclab/isaaclab/sim/simulation_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,7 @@ def render(self, mode: int | None = None) -> None:
669669
every physics step). Camera sensors drive their configured renderer when
670670
fetching data, so this method remains backend-agnostic.
671671
"""
672+
self.physics_manager.pre_render()
672673
self.update_visualizers(self.get_rendering_dt())
673674

674675
# Call render callbacks
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Pure-Python ctypes bindings for the cubric GPU transform-hierarchy API.
7+
8+
Acquires the ``omni::cubric::IAdapter`` carb interface directly from the
9+
Carbonite framework and wraps its function-pointer methods so that Newton
10+
can call cubric's GPU transform propagation without C++ pybind11 changes.
11+
12+
The flow mirrors PhysX's ``DirectGpuHelper::updateXForms_GPU()``:
13+
14+
1. ``IAdapter::create`` → allocate a cubric adapter ID
15+
2. ``IAdapter::bindToStage`` → bind to the current Fabric stage
16+
3. ``IAdapter::compute`` → GPU kernel: propagate world transforms
17+
4. ``IAdapter::release`` → free the adapter
18+
19+
When cubric is unavailable (e.g. CPU-only machine, plugin not loaded), the
20+
caller falls back to the CPU ``update_world_xforms()`` path.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import ctypes
26+
import logging
27+
28+
logger = logging.getLogger(__name__)
29+
30+
# ---------------------------------------------------------------------------
31+
# Carb Framework struct layout (CARB_ABI function-pointer offsets, x86_64)
32+
# ---------------------------------------------------------------------------
33+
# Counting only CARB_ABI fields from the top of ``struct Framework``:
34+
# 0: loadPluginsEx
35+
# 8: unloadAllPlugins
36+
# 16: acquireInterfaceWithClient
37+
# 24: tryAcquireInterfaceWithClient ← we use this one
38+
_FW_OFF_TRY_ACQUIRE = 24
39+
40+
# ---------------------------------------------------------------------------
41+
# IAdapter struct layout (from omni/cubric/IAdapter.h)
42+
# ---------------------------------------------------------------------------
43+
# 0: getAttribute
44+
# 8: create(AdapterId*)
45+
# 16: refcount
46+
# 24: retain
47+
# 32: release(AdapterId)
48+
# 40: bindToStage(AdapterId, const FabricId&)
49+
# 48: unbind
50+
# 56: compute(AdapterId, options, dirtyMode, outFlags*)
51+
_IA_OFF_CREATE = 8
52+
_IA_OFF_RELEASE = 32
53+
_IA_OFF_BIND = 40
54+
_IA_OFF_COMPUTE = 56
55+
56+
# AdapterId sentinel
57+
_INVALID_ADAPTER_ID = ctypes.c_uint64(~0).value
58+
59+
# AdapterComputeOptions flags (from IAdapter.h)
60+
_OPT_FORCE_UPDATE = 1 << 0 # Force update, ignoring invalidation status
61+
_OPT_FORCE_STATE_RECONSTRUCTION = 1 << 1 # Force full rebuild of internal accel structures
62+
_OPT_SKIP_ISOLATED = 1 << 2 # Skip prims with connectivity degree 0
63+
_OPT_RIGID_BODY = 1 << 3 # Use PhysicsRigidBodyAPI tag for inverse propagation
64+
65+
# Newton prims get tagged with PhysicsRigidBodyAPI at init time so
66+
# cubric's eRigidBody mode can distinguish rigid-body buckets
67+
# (Inverse: preserve world matrix written by Newton, derive local)
68+
# from non-rigid-body buckets (Forward: propagate to children).
69+
# eForceUpdate is ORed in to bypass the change-listener check.
70+
_OPT_DEFAULT = _OPT_RIGID_BODY | _OPT_FORCE_UPDATE
71+
72+
# AdapterDirtyMode
73+
_DIRTY_ALL = 0 # eAll — dirty all prims in the stage
74+
_DIRTY_COARSE = 1 # eCoarse — dirty all prims in visited buckets
75+
76+
77+
# ---------------------------------------------------------------------------
78+
# ctypes struct mirrors
79+
# ---------------------------------------------------------------------------
80+
class _Version(ctypes.Structure):
81+
_fields_ = [("major", ctypes.c_uint32), ("minor", ctypes.c_uint32)]
82+
83+
84+
class _InterfaceDesc(ctypes.Structure):
85+
"""``carb::InterfaceDesc`` — {const char* name, Version version}."""
86+
87+
_fields_ = [
88+
("name", ctypes.c_char_p),
89+
("version", _Version),
90+
]
91+
92+
93+
def _read_u64(addr: int) -> int:
94+
return ctypes.c_uint64.from_address(addr).value
95+
96+
97+
# ---------------------------------------------------------------------------
98+
# Public API
99+
# ---------------------------------------------------------------------------
100+
class CubricBindings:
101+
"""Typed wrappers around the cubric ``IAdapter`` API.
102+
103+
Call :meth:`initialize` once; if it returns ``True``, the four adapter
104+
methods are available.
105+
"""
106+
107+
def __init__(self) -> None:
108+
self._ia_ptr: int = 0
109+
self._create_fn = None
110+
self._release_fn = None
111+
self._bind_fn = None
112+
self._compute_fn = None
113+
114+
# -- lifecycle -----------------------------------------------------------
115+
116+
def initialize(self) -> bool:
117+
"""Acquire the cubric ``IAdapter`` from the carb framework."""
118+
# Ensure the omni.cubric extension (native carb plugin) is loaded.
119+
try:
120+
import omni.kit.app
121+
122+
ext_mgr = omni.kit.app.get_app().get_extension_manager()
123+
if not ext_mgr.is_extension_enabled("omni.cubric"):
124+
ext_mgr.set_extension_enabled_immediate("omni.cubric", True)
125+
if not ext_mgr.is_extension_enabled("omni.cubric"):
126+
logger.warning("Failed to enable omni.cubric extension")
127+
return False
128+
except Exception as exc:
129+
logger.warning("Cannot enable omni.cubric: %s", exc)
130+
return False
131+
132+
# Get Framework* via libcarb.so acquireFramework (singleton).
133+
try:
134+
libcarb = ctypes.CDLL("libcarb.so")
135+
except OSError:
136+
logger.warning("Could not load libcarb.so")
137+
return False
138+
139+
libcarb.acquireFramework.restype = ctypes.c_void_p
140+
libcarb.acquireFramework.argtypes = [ctypes.c_char_p, _Version]
141+
fw_ptr = libcarb.acquireFramework(b"isaaclab.cubric", _Version(0, 0))
142+
if not fw_ptr:
143+
logger.warning("acquireFramework returned null")
144+
return False
145+
146+
# Read tryAcquireInterfaceWithClient fn-ptr from Framework vtable.
147+
try_acquire_addr = _read_u64(fw_ptr + _FW_OFF_TRY_ACQUIRE)
148+
if try_acquire_addr == 0:
149+
logger.warning("tryAcquireInterfaceWithClient is null in Framework")
150+
return False
151+
152+
try_acquire_fn = ctypes.CFUNCTYPE(
153+
ctypes.c_void_p, # return: void* (IAdapter*)
154+
ctypes.c_char_p, # clientName
155+
_InterfaceDesc, # desc (by value)
156+
ctypes.c_char_p, # pluginName
157+
)(try_acquire_addr)
158+
159+
desc = _InterfaceDesc(
160+
name=b"omni::cubric::IAdapter",
161+
version=_Version(0, 1),
162+
)
163+
164+
# Try several acquisition strategies — the required client name
165+
# varies across Kit configurations.
166+
ia_ptr = try_acquire_fn(b"carb.scripting-python.plugin", desc, None)
167+
if not ia_ptr:
168+
ia_ptr = try_acquire_fn(None, desc, None)
169+
if not ia_ptr:
170+
acquire_addr = _read_u64(fw_ptr + 16) # acquireInterfaceWithClient
171+
if acquire_addr:
172+
acquire_fn = ctypes.CFUNCTYPE(
173+
ctypes.c_void_p,
174+
ctypes.c_char_p,
175+
_InterfaceDesc,
176+
ctypes.c_char_p,
177+
)(acquire_addr)
178+
ia_ptr = acquire_fn(b"isaaclab.cubric", desc, None)
179+
if not ia_ptr:
180+
logger.warning(
181+
"Could not acquire omni::cubric::IAdapter — "
182+
"cubric plugin may not be registered or interface version mismatch"
183+
)
184+
return False
185+
self._ia_ptr = ia_ptr
186+
187+
# Wrap the four IAdapter function pointers we need.
188+
create_addr = _read_u64(ia_ptr + _IA_OFF_CREATE)
189+
release_addr = _read_u64(ia_ptr + _IA_OFF_RELEASE)
190+
bind_addr = _read_u64(ia_ptr + _IA_OFF_BIND)
191+
compute_addr = _read_u64(ia_ptr + _IA_OFF_COMPUTE)
192+
193+
if not all([create_addr, release_addr, bind_addr, compute_addr]):
194+
logger.warning("One or more IAdapter function pointers are null")
195+
return False
196+
197+
self._create_fn = ctypes.CFUNCTYPE(
198+
ctypes.c_bool,
199+
ctypes.POINTER(ctypes.c_uint64),
200+
)(create_addr)
201+
202+
self._release_fn = ctypes.CFUNCTYPE(
203+
ctypes.c_bool,
204+
ctypes.c_uint64,
205+
)(release_addr)
206+
207+
# FabricId is uint64, passed by const-ref -> pointer on x86_64
208+
self._bind_fn = ctypes.CFUNCTYPE(
209+
ctypes.c_bool,
210+
ctypes.c_uint64,
211+
ctypes.POINTER(ctypes.c_uint64),
212+
)(bind_addr)
213+
214+
self._compute_fn = ctypes.CFUNCTYPE(
215+
ctypes.c_bool,
216+
ctypes.c_uint64, # adapterId
217+
ctypes.c_uint32, # options (AdapterComputeOptions)
218+
ctypes.c_int32, # dirtyMode (AdapterDirtyMode)
219+
ctypes.c_void_p, # outAccountFlags* (nullable)
220+
)(compute_addr)
221+
222+
logger.info("cubric IAdapter bindings ready")
223+
return True
224+
225+
@property
226+
def available(self) -> bool:
227+
return self._ia_ptr != 0
228+
229+
# -- cubric adapter methods ----------------------------------------------
230+
231+
def create_adapter(self) -> int | None:
232+
"""Create a cubric adapter. Returns an adapter ID or ``None``."""
233+
if not self._create_fn:
234+
return None
235+
adapter_id = ctypes.c_uint64(_INVALID_ADAPTER_ID)
236+
ok = self._create_fn(ctypes.byref(adapter_id))
237+
if not ok or adapter_id.value == _INVALID_ADAPTER_ID:
238+
logger.warning("IAdapter::create failed")
239+
return None
240+
return adapter_id.value
241+
242+
def bind_to_stage(self, adapter_id: int, fabric_id: int) -> bool:
243+
"""Bind the adapter to a Fabric stage."""
244+
if not self._bind_fn:
245+
return False
246+
fid = ctypes.c_uint64(fabric_id)
247+
ok = self._bind_fn(adapter_id, ctypes.byref(fid))
248+
if not ok:
249+
logger.warning("IAdapter::bindToStage failed (adapter=%d, fabricId=%d)", adapter_id, fabric_id)
250+
return ok
251+
252+
def compute(self, adapter_id: int) -> bool:
253+
"""Run the GPU transform-hierarchy compute pass.
254+
255+
Uses ``eRigidBody | eForceUpdate`` with ``eAll`` dirty mode.
256+
``eRigidBody`` makes cubric apply Inverse propagation on buckets
257+
tagged with ``PhysicsRigidBodyAPI`` (keeps Newton's world transforms,
258+
derives local) and Forward on everything else (propagates to children).
259+
``eForceUpdate`` bypasses the change-listener dirty check.
260+
"""
261+
if not self._compute_fn:
262+
return False
263+
flags = ctypes.c_uint32(0)
264+
ok = self._compute_fn(adapter_id, _OPT_DEFAULT, _DIRTY_ALL, ctypes.byref(flags))
265+
if not ok:
266+
logger.warning("IAdapter::compute returned false (flags=0x%x)", flags.value)
267+
return ok
268+
269+
def release_adapter(self, adapter_id: int) -> None:
270+
"""Release an adapter."""
271+
if not adapter_id or not self._release_fn:
272+
return
273+
self._release_fn(adapter_id)

0 commit comments

Comments
 (0)