Repository: Genesis-Embodied-AI/Genesis
File: genesis/engine/sensors/camera.py (Python)

Code before:
        self._options: RasterizerCameraOptions
        self._camera_node = None
        self._camera_target = None
        self._camera_wrapper = None
        self._camera_registered = False

    # ========================== Sensor Lifecycle ==========================

    def build(self):
        """Initialize the rasterizer and register this camera."""
        super().build()

        scene = self._manager._sim.scene

        if self._shared_metadata.sensors is None:
            self._shared_metadata.sensors = []
            self._shared_metadata.lights = gs.List()
            self._shared_metadata.image_cache = {}

            # Check if scene has a viewer - if so, use visualizer's rasterizer (like add_camera does)
            # This avoids OpenGL context conflicts since visualizer's rasterizer routes through viewer.render_offscreen()
            visualizer = getattr(scene, "visualizer", None)
            viewer = getattr(visualizer, "_viewer", None) if visualizer else None

            if viewer is not None:
                # Use visualizer's rasterizer - it will use viewer.render_offscreen() which handles context properly
                self._shared_metadata.context = visualizer.context
                self._shared_metadata.renderer = visualizer.rasterizer
            else:
                # No viewer - create standalone rasterizer with offscreen context
                self._shared_metadata.context = self._create_standalone_context(scene)
                self._shared_metadata.renderer = Rasterizer(viewer=None, context=self._shared_metadata.context)
                self._shared_metadata.renderer.build()

        self._shared_metadata.sensors.append(self)

        # Register camera now if standalone (offscreen), or defer to first render if using visualizer's rasterizer
        # (visualizer isn't built yet at sensor.build() time)
        if self._shared_metadata.renderer.offscreen:
            self._register_camera_with_renderer()

        _B = max(self._manager._sim.n_envs, 1)
        w, h = self._options.res
        self._shared_metadata.image_cache[self._idx] = torch.zeros((_B, h, w, 3), dtype=torch.uint8, device=gs.device)

    def _register_camera_with_renderer(self):
        """Register this camera with the renderer."""
        if self._camera_registered:
            return

        # Add lights from options to the context

Review comment (suggestion):
The point is more about the fact that the same windowed OpenGL context will be reused, which is capable of handling both offscreen and onscreen rendering, whereas managing two contexts, with one being headless and the other windowed, is proven to be very painful and fragile.

Code after:
        super().__init__(options, idx, data_cls, manager)
        self._options: RasterizerCameraOptions
        self._camera_node = None
        self._camera_target = None
        self._camera_wrapper = None
        self._is_camera_registered = False

    # ========================== Sensor Lifecycle ==========================

    def build(self):
        """Initialize the rasterizer and register this camera."""
        super().build()

        scene = self._manager._sim.scene

        if self._shared_metadata.sensors is None:
            self._shared_metadata.sensors = []
            self._shared_metadata.lights = gs.List()
            self._shared_metadata.image_cache = {}

            # If a viewer is active, reuse its windowed OpenGL context for both offscreen and onscreen
            # rendering, rather than creating a separate headless context which is fragile.
            if scene.viewer is not None:
                self._shared_metadata.context = scene.visualizer.context
                self._shared_metadata.renderer = scene.visualizer.rasterizer
            else:
                # No viewer - create standalone rasterizer with offscreen context
                self._shared_metadata.context = self._create_standalone_context(scene)
                self._shared_metadata.renderer = Rasterizer(viewer=None, context=self._shared_metadata.context)
                self._shared_metadata.renderer.build()

        self._shared_metadata.sensors.append(self)

        # Register camera now if standalone (offscreen), or defer to first render if using visualizer's rasterizer
        # (visualizer isn't built yet at sensor.build() time)
        if self._shared_metadata.renderer.offscreen:
            self._ensure_camera_registered()

        _B = max(self._manager._sim.n_envs, 1)
        w, h = self._options.res
        self._shared_metadata.image_cache[self._idx] = torch.zeros((_B, h, w, 3), dtype=torch.uint8, device=gs.device)

    def _ensure_camera_registered(self):
        """Register this camera with the renderer (no-op if already registered)."""
        if self._is_camera_registered:
            return

        # Add lights from options to the context
        for light_config in self._options.lights:
            if self._shared_metadata.lights is not None:
                light_dict = self._convert_light_config_to_rasterizer(light_config)