def _create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self._hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            x=float((Globals.base.win.get_x_size()) // 2) / self._gui_scale - 465 // 2, y=220,
            parent=self._fullscreen_node)
        self.set_reload_hint_visible(False)

        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                x=((Globals.base.win.get_x_size()/self._gui_scale) - 1054) // 2,
                y=(Globals.base.win.get_y_size()/self._gui_scale) - 118 - 40,
                parent=self._fullscreen_node)

            Sequence(
                python_warning.color_scale_interval(0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"),
                python_warning.color_scale_interval(0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self._keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png", x=30,
            y=Globals.base.win.get_y_size()//self._gui_scale - 510.0,
            parent=self._fullscreen_node, any_filter=False)
Exemple #2
0
    def _create_components(self):
        """ Internal method to init the widgets components """

        # Create the buffer which stores the last FPS values
        self._storage_buffer = Image.create_buffer("FPSValues", 250, "R16")
        self._storage_buffer.set_clear_color(Vec4(0))
        self._storage_buffer.clear_image()

        self._store_index = PTAInt.empty_array(1)
        self._store_index[0] = 0

        self._current_ftime = PTAFloat.empty_array(1)
        self._current_ftime[0] = 16.0

        self._chart_ms_max = PTAFloat.empty_array(1)
        self._chart_ms_max[0] = 40

        # Create the texture where the gui component is rendered inside
        self._display_tex = Image.create_2d("FPSChartRender", 250, 120,
                                            "RGBA8")
        self._display_tex.set_clear_color(Vec4(0))
        self._display_tex.clear_image()
        self._display_img = Sprite(image=self._display_tex,
                                   parent=self._node,
                                   w=250,
                                   h=120,
                                   x=10,
                                   y=10)

        # Defer the further loading
        Globals.base.taskMgr.doMethodLater(0.3, self._late_init,
                                           "FPSChartInit")
    def create_components(self):
        """ Creates the gui components """

        self.debugger_width = 460
        self.debugger_visible = False
        self.debugger_interval = None

        self.create_stats()
        self.create_hints()

        self.pipeline_logo = Sprite(
            image="/$$rp/data/gui/pipeline_logo_text.png", x=30, y=30,
            parent=self.fullscreen_node)

        if self.advanced_info:
            self.exposure_node = self.fullscreen_node.attach_new_node("ExposureWidget")
            self.exposure_widget = ExposureWidget(self.pipeline, self.exposure_node)

        self.fps_node = self.fullscreen_node.attach_new_node("FPSChart")
        self.fps_node.set_pos(Vec3(21, 1, -108 - 40))
        self.fps_widget = FPSChart(self.pipeline, self.fps_node)

        self.pixel_widget = PixelInspector(self.pipeline)
        self.buffer_viewer = BufferViewer(self.pipeline, self.fullscreen_node)
        self.pipe_viewer = PipeViewer(self.pipeline, self.fullscreen_node)
        self.rm_selector = RenderModeSelector(self.pipeline, self.fullscreen_node)
        self.error_msg_handler = ErrorMessageDisplay()

        self.handle_window_resize()
Exemple #4
0
 def _create_topbar(self):
     """ Creates the topbar """
     self._pipeline_logo = Sprite(
         image="/$$rp/data/gui/pipeline_logo_text.png",
         x=30,
         y=30,
         parent=self._fullscreen_node)
Exemple #5
0
    def _create_components(self):
        """ Internal method to init the widgets components """

        self._node.hide()

        # Create the texture where the gui component is rendered inside
        self._storage_tex = Image.create_2d("ExposureDisplay", 140, 20, "RGBA8")
        self._storage_tex.set_clear_color(Vec4(0.2, 0.6, 1.0, 1.0))
        self._storage_tex.clear_image()

        self._bg_frame = DirectFrame(
            parent=self._node, frameColor=(0.1, 0.1, 0.1, 1.0),
            frameSize=(200, 0, -10, -85), pos=(0, 0, 0))

        self._display_img = Sprite(
            image=self._storage_tex, parent=self._node, w=140, h=20, x=20, y=50)

        self._display_txt = Text(
            text="Current Exposure".upper(), parent=self._node, x=160, y=40,
            size=13, color=Vec3(0.8), align="right")

        # Create the shader which generates the visualization texture
        self._cshader_node = ComputeNode("ExposureWidget")
        self._cshader_node.add_dispatch(140 // 10, 20 // 4, 1)

        self._cshader_np = self._node.attach_new_node(self._cshader_node)

        # Defer the further loading
        Globals.base.taskMgr.doMethodLater(1.0, self._late_init, "ExposureLateInit")
Exemple #6
0
    def _create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self._hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            x=float((Globals.base.win.get_x_size()) // 2) / self._gui_scale -
            465 // 2,
            y=220,
            parent=self._fullscreen_node)
        self.set_reload_hint_visible(False)

        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                x=((Globals.base.win.get_x_size() / self._gui_scale) - 1054) //
                2,
                y=(Globals.base.win.get_y_size() / self._gui_scale) - 118 - 40,
                parent=self._fullscreen_node)

            Sequence(
                python_warning.color_scale_interval(0.7,
                                                    Vec4(0.3, 1, 1, 0.7),
                                                    blendType="easeOut"),
                python_warning.color_scale_interval(0.7,
                                                    Vec4(1, 1, 1, 1.0),
                                                    blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self._keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png",
            x=30,
            y=Globals.base.win.get_y_size() // self._gui_scale - 510.0,
            parent=self._fullscreen_node,
            any_filter=False)
Exemple #7
0
    def create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self.hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            parent=self.fullscreen_node)
        self.set_reload_hint_visible(False)

        self.python_warning = None
        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            self.python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                parent=self.fullscreen_node)
            Sequence(
                self.python_warning.color_scale_interval(0.7,
                                                         Vec4(0.3, 1, 1, 0.7),
                                                         blendType="easeOut"),
                self.python_warning.color_scale_interval(0.7,
                                                         Vec4(1, 1, 1, 1.0),
                                                         blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self.keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png",
            parent=self.fullscreen_node,
            any_filter=False)
Exemple #8
0
    def create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self.hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            parent=self.fullscreen_node)
        self.set_reload_hint_visible(False)

        self.python_warning = None
        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            self.python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                parent=self.fullscreen_node)
            Sequence(
                self.python_warning.color_scale_interval(
                    0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"),
                self.python_warning.color_scale_interval(
                    0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self.keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png",
            parent=self.fullscreen_node, any_filter=False)
    def create(self):
        """ Creates the gui components """
        screen_w, screen_h = Globals.base.win.get_x_size(), Globals.base.win.get_y_size()
        self.fullscreen_node = Globals.base.pixel2dp.attach_new_node("LoadingScreen")
        self.fullscreen_node.set_bin("fixed", 10)
        self.fullscreen_node.set_depth_test(False)

        scale_w = screen_w / 1920.0
        scale_h = screen_h / 1080.0
        scale = max(scale_w, scale_h)

        self.fullscreen_bg = Sprite(
            image="/$$rp/data/gui/loading_screen_bg.txo",
            x=(screen_w-1920.0*scale)//2, y=(screen_h-1080.0*scale)//2, w=int(1920 * scale),
            h=int(1080 * scale), parent=self.fullscreen_node, near_filter=False)

        for _ in range(2):
            Globals.base.graphicsEngine.render_frame()
    def create(self):
        """ Creates the gui components """
        screen_w, screen_h = Globals.native_resolution.x, Globals.native_resolution.y
        self.fullscreen_node = Globals.base.pixel2dp.attach_new_node(
            "LoadingScreen")
        self.fullscreen_node.set_bin("fixed", 10)
        self.fullscreen_node.set_depth_test(False)

        scale_w = screen_w / 1920.0
        scale_h = screen_h / 1080.0
        scale = max(scale_w, scale_h)

        self.fullscreen_bg = Sprite(image=self.image_source,
                                    x=(screen_w - 1920.0 * scale) // 2,
                                    y=(screen_h - 1080.0 * scale) // 2,
                                    w=int(1920 * scale),
                                    h=int(1080 * scale),
                                    parent=self.fullscreen_node,
                                    near_filter=False)

        for _ in range(2):
            Globals.base.graphicsEngine.render_frame()
class Debugger(RPObject):

    """ This class manages the onscreen gui and """

    def __init__(self, pipeline):
        RPObject.__init__(self)
        self.debug("Creating debugger")
        self._pipeline = pipeline
        self._analyzer = SceneGraphAnalyzer()

        self._load_config()
        self._fullscreen_node = Globals.base.pixel2d.attach_new_node(
            "PipelineDebugger")
        self._create_components()
        self._init_keybindings()
        self._init_notify()

        Globals.base.doMethodLater(
            0.5, lambda task: self._collect_scene_data(), "RPDebugger_collectSceneData_initial")
        Globals.base.doMethodLater(0.1, self._update_stats, "RPDebugger_updateStats")

    def _load_config(self):
        """ Loads the gui configuration from config/debugging.yaml """
        

    def _create_components(self):
        """ Creates the gui components """

        # When using small resolutions, scale the GUI so its still useable,
        # otherwise the sub-windows are bigger than the main window
        scale_factor = min(1.0, Globals.base.win.get_x_size() / 1920.0)
        self._fullscreen_node.set_scale(scale_factor)
        self._gui_scale = scale_factor

        # Component values
        self._debugger_width = 460

        # Create states
        self._debugger_visible = False

        # Create intervals
        self._debugger_interval = None

        # Create the actual GUI
        self._create_topbar()
        self._create_stats()
        self._create_hints()

        self._exposure_node = self._fullscreen_node.attach_new_node("ExposureWidget")
        self._exposure_node.set_pos(
            Globals.base.win.get_x_size() / self._gui_scale - 200,
            1, -Globals.base.win.get_y_size() / self._gui_scale + 120)
        self._exposure_widget = ExposureWidget(self._pipeline, self._exposure_node)

        self._fps_node = self._fullscreen_node.attach_new_node("FPSChart")
        self._fps_node.set_pos(Vec3(21, 1, -108 - 40))
        self._fps_widget = FPSChart(self._pipeline, self._fps_node)

        self._pixel_widget = PixelInspector(self._pipeline)

        self._buffer_viewer = BufferViewer(self._pipeline, self._fullscreen_node)
        self._pipe_viewer = PipeViewer(self._pipeline, self._fullscreen_node)
        self._rm_selector = RenderModeSelector(self._pipeline, self._fullscreen_node)

    def _init_notify(self):
        """ Inits the notify stream which gets all output from panda and parses
        it """
        self._error_msg_handler = ErrorMessageDisplay()

    def update(self):
        """ Updates the gui """
        self._error_msg_handler.update()
        self._pixel_widget.update()

    def get_error_msg_handler(self):
        """ Returns the error message handler """
        return self._error_msg_handler

    def _create_topbar(self):
        """ Creates the topbar """
        self._pipeline_logo = Sprite(
            image="/$$rp/data/gui/pipeline_logo_text.png", x=30, y=50,
            parent=self._fullscreen_node)

    def _collect_scene_data(self, task=None):
        """ Analyzes the scene graph to provide useful information """
        self._analyzer.clear()
        for geom_node in Globals.base.render.find_all_matches("**/+GeomNode"):
            self._analyzer.add_node(geom_node.node())
        if task:
            return task.again

    def _create_stats(self):
        """ Creates the stats overlay """
        self._overlay_node = Globals.base.aspect2d.attach_new_node("Overlay")
        self._overlay_node.set_pos(Globals.base.get_aspect_ratio() - 0.07, 1, 1.0 - 0.07)
        self._debug_lines = []
        for i in range(5):
            self._debug_lines.append(TextNode(
                pos=Vec2(0, -i * 0.046), parent=self._overlay_node,
                pixel_size=16, align="right", color=Vec3(1)))

    def _create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self._hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            x=float((Globals.base.win.get_x_size()) // 2) / self._gui_scale - 465 // 2, y=220,
            parent=self._fullscreen_node)
        self.set_reload_hint_visible(False)

        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                x=((Globals.base.win.get_x_size()/self._gui_scale) - 1054) // 2,
                y=(Globals.base.win.get_y_size()/self._gui_scale) - 118 - 40,
                parent=self._fullscreen_node)

            Sequence(
                python_warning.color_scale_interval(0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"),
                python_warning.color_scale_interval(0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self._keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png", x=30,
            y=Globals.base.win.get_y_size()//self._gui_scale - 510.0,
            parent=self._fullscreen_node, any_filter=False)

    def _update_stats(self, task=None):
        """ Updates the stats overlay """
        clock = Globals.clock
        self._debug_lines[0].text = "{:3.0f} fps  |  {:3.1f} ms  |  {:3.1f} ms max".format(
            clock.get_average_frame_rate(),
            1000.0 / max(0.001, clock.get_average_frame_rate()),
            clock.get_max_frame_duration() * 1000.0)

        text = "{:4d} render states  |  {:4d} transforms"
        text += "  |  {:4d} commands  |  {:4d} lights  |  {:5d} shadow sources  "
        text += "|  {:3.1f}% atlas usage"
        self._debug_lines[1].text = text.format(
            RenderState.get_num_states(), TransformState.get_num_states(),
            self._pipeline.light_mgr.cmd_queue.num_processed_commands,
            self._pipeline.light_mgr.num_lights,
            self._pipeline.light_mgr.num_shadow_sources,
            self._pipeline.light_mgr.shadow_atlas_coverage)

        text = "Pipeline:   {:3.0f} MiB VRAM  |  {:5d} images  |  {:5d} textures  |  "
        text += "{:5d} render targets  |  {:3d} plugins"
        tex_memory, tex_count = self._buffer_viewer.stage_information
        self._debug_lines[2].text = text.format(
            tex_memory / (1024**2), len(Image.REGISTERED_IMAGES), tex_count,
            RenderTarget.NUM_ALLOCATED_BUFFERS,
            len(self._pipeline.plugin_mgr.enabled_plugins))

        text = "Scene:   {:4.0f} MiB VRAM  |  {:3d} textures  |  {:4d} geoms  "
        text += "|  {:4d} nodes  |  {:7,.0f} vertices  |  {:5.0f} MiB vTX data  "
        scene_tex_size = 0
        for tex in TexturePool.find_all_textures():
            scene_tex_size += tex.estimate_texture_memory()

        self._debug_lines[3].text = text.format(
            scene_tex_size / (1024**2),
            len(TexturePool.find_all_textures()),
            self._analyzer.get_num_geoms(),
            self._analyzer.get_num_nodes(),
            self._analyzer.get_num_vertices(),
            self._analyzer.get_vertex_data_size() / (1024**2),
        )

        sun_vector = Vec3(0)
        if self._pipeline.plugin_mgr.is_plugin_enabled("scattering"):
            sun_vector = self._pipeline.plugin_mgr.instances["scattering"].sun_vector

        text = "{} ({:1.3f})  |  {:0.2f} {:0.2f} {:0.2f}  |  {:3d} daytime settings  |  X {:3.1f}  Y {:3.1f}  Z {:3.1f}"
        text += "    |  Total tasks:  {:2d}   |   scheduled: {:2d}"
        self._debug_lines[4].text = text.format(
            self._pipeline.daytime_mgr.formatted_time,
            self._pipeline.daytime_mgr.time,
            sun_vector.x, sun_vector.y, sun_vector.z,
            len(self._pipeline.plugin_mgr.day_settings),
            Globals.base.camera.get_x(Globals.base.render),
            Globals.base.camera.get_y(Globals.base.render),
            Globals.base.camera.get_z(Globals.base.render),
            self._pipeline.task_scheduler.num_tasks,
            self._pipeline.task_scheduler.num_scheduled_tasks,
        )

        if task:
            return task.again

    def set_reload_hint_visible(self, flag):
        """ Sets whether the shader reload hint is visible """
        if flag:
            self._hint_reloading.show()
        else:
            self._hint_reloading.hide()

    def _init_keybindings(self):
        """ Inits the debugger keybindings """
        Globals.base.accept("v", self._buffer_viewer.toggle)
        Globals.base.accept("c", self._pipe_viewer.toggle)
        Globals.base.accept("z", self._rm_selector.toggle)
        Globals.base.accept("f5", self._toggle_gui_visible)
        Globals.base.accept("f6", self._toggle_fps_visible)

    def _toggle_gui_visible(self):
        """ Shows / Hides the gui """

        if not self._fullscreen_node.is_hidden():
            self._fullscreen_node.hide()
            self._overlay_node.hide()
        else:
            self._fullscreen_node.show()
            self._overlay_node.show()

    def _toggle_fps_visible(self):
        """ Shows / Hides the FPS graph """
        if not self._fps_node.is_hidden():
            self._fps_node.hide()
        else:
            self._fps_node.show()
    def _render_stages(self):
        """ Renders the stages to the window """

        self._remove_components()
        entries_per_row = 6
        aspect = Globals.native_resolution.y / Globals.native_resolution.x
        entry_width = 235
        entry_height = (entry_width - 20) * aspect + 55

        # Store already processed images
        processed = set()
        index = -1
        # Iterate over all stages
        for stage_tex in self._stages:
            if stage_tex in processed:
                continue
            processed.add(stage_tex)
            index += 1
            stage_name = stage_tex.get_name()

            xoffs = index % entries_per_row
            yoffs = index // entries_per_row
            node = self._content_node.attach_new_node("Preview")
            node.set_sz(-1)
            node.set_pos(10 + xoffs * (entry_width - 14), 1, yoffs * (entry_height - 14 + 10))

            r, g, b = 0.2, 0.2, 0.2
            if isinstance(stage_tex, Image):
                r, g, b = 0.2, 0.4, 0.6

            stage_name = stage_name.replace("render_pipeline_internal:", "")
            parts = stage_name.split(":")
            stage_name = parts[-1]
            DirectFrame(
                parent=node, frameSize=(7, entry_width - 17, -7, -entry_height + 17),
                frameColor=(r, g, b, 1.0), pos=(0, 0, 0))

            frame_hover = DirectFrame(
                parent=node, frameSize=(0, entry_width - 10, 0, -entry_height + 10),
                frameColor=(0, 0, 0, 0), pos=(0, 0, 0), state=DGG.NORMAL)
            frame_hover.bind(
                DGG.ENTER, partial(self._on_texture_hovered, frame_hover))
            frame_hover.bind(
                DGG.EXIT, partial(self._on_texture_blurred, frame_hover))
            frame_hover.bind(
                DGG.B1PRESS, partial(self._on_texture_clicked, stage_tex))

            Text(text=stage_name, x=15, y=29, parent=node, size=12, color=Vec3(0.8))

            # Scale image so it always fits
            w, h = stage_tex.get_x_size(), stage_tex.get_y_size()
            padd_x, padd_y = 24, 57
            scale_x = (entry_width - padd_x) / max(1, w)
            scale_y = (entry_height - padd_y) / max(1, h)
            scale_factor = min(scale_x, scale_y)

            if stage_tex.get_texture_type() == Image.TT_buffer_texture:
                scale_factor = 1
                w = entry_width - padd_x
                h = entry_height - padd_y

            preview = Sprite(
                image=stage_tex, w=scale_factor * w, h=scale_factor * h,
                any_filter=False, parent=node, x=7, y=40, transparent=False)

            preview.set_shader_input("mipmap", 0)
            preview.set_shader_input("slice", 0)
            preview.set_shader_input("brightness", 1)
            preview.set_shader_input("tonemap", False)

            preview_shader = DisplayShaderBuilder.build(
                stage_tex, scale_factor * w, scale_factor * h)
            preview.set_shader(preview_shader)

        num_rows = (index + entries_per_row) // entries_per_row

        self._set_scroll_height(50 + (entry_height - 14 + 10) * num_rows)
Exemple #13
0
class Debugger(RPObject):

    """ This class manages the onscreen gui and """

    def __init__(self, pipeline):
        RPObject.__init__(self)
        self.debug("Creating debugger")
        self._pipeline = pipeline
        self._analyzer = SceneGraphAnalyzer()

        self._load_config()
        self._fullscreen_node = Globals.base.pixel2d.attach_new_node(
            "PipelineDebugger")
        self._create_components()
        self._init_keybindings()
        self._init_notify()

        Globals.base.doMethodLater(
            0.5, lambda task: self._collect_scene_data(), "RPDebugger_collectSceneData_initial")
        Globals.base.doMethodLater(0.1, self._update_stats, "RPDebugger_updateStats")

    def _load_config(self):
        """ Loads the gui configuration from config/debugging.yaml """
        self._config = load_yaml_file("/$$rpconfig/debugging.yaml")

    def _create_components(self):
        """ Creates the gui components """

        # When using small resolutions, scale the GUI so its still useable,
        # otherwise the sub-windows are bigger than the main window
        scale_factor = min(1.0, Globals.base.win.get_x_size() / 1920.0)
        self._fullscreen_node.set_scale(scale_factor)
        self._gui_scale = scale_factor

        # Component values
        self._debugger_width = 460

        # Create states
        self._debugger_visible = False

        # Create intervals
        self._debugger_interval = None

        # Create the actual GUI
        self._create_debugger()
        self._create_topbar()
        self._create_stats()
        self._create_hints()

        self._exposure_node = self._fullscreen_node.attach_new_node("ExposureWidget")
        self._exposure_node.set_pos(
            Globals.base.win.get_x_size() / self._gui_scale - 200,
            1, -Globals.base.win.get_y_size() / self._gui_scale + 120)
        self._exposure_widget = ExposureWidget(self._pipeline, self._exposure_node)

        self._fps_node = Globals.base.pixel2d.attach_new_node("FPSChart")
        self._fps_node.set_pos(Vec3(21, 1, -108) * scale_factor)
        self._fps_node.set_scale(scale_factor)
        self._fps_widget = FPSChart(self._pipeline, self._fps_node)

        self._pixel_widget = PixelInspector(self._pipeline)

        self._buffer_viewer = BufferViewer(self._pipeline, self._fullscreen_node)
        self._pipe_viewer = PipeViewer(self._pipeline, self._fullscreen_node)

    def _init_notify(self):
        """ Inits the notify stream which gets all output from panda and parses
        it """
        self._error_msg_handler = ErrorMessageDisplay()

    def update(self):
        """ Updates the gui """
        self._error_msg_handler.update()
        self._pixel_widget.update()

    def get_error_msg_handler(self):
        """ Returns the error message handler """
        return self._error_msg_handler

    def _create_topbar(self):
        """ Creates the topbar """
        self._pipeline_logo = Sprite(
            image="/$$rp/data/gui/pipeline_logo_text.png", x=30, y=30,
            parent=self._fullscreen_node)

    def _collect_scene_data(self, task=None):
        """ Analyzes the scene graph to provide useful information """
        self._analyzer.clear()
        for geom_node in Globals.base.render.find_all_matches("**/+GeomNode"):
            self._analyzer.add_node(geom_node.node())
        if task:
            return task.again

    def _create_stats(self):
        """ Creates the stats overlay """
        self._overlay_node = Globals.base.aspect2d.attach_new_node("Overlay")
        self._overlay_node.set_pos(Globals.base.get_aspect_ratio() - 0.07, 1, 1.0 - 0.07)
        self._debug_lines = []
        for i in range(5):
            self._debug_lines.append(TextNode(
                pos=Vec2(0, -i * 0.046), parent=self._overlay_node,
                pixel_size=16, align="right", color=Vec3(1)))

    def _create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self._hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            x=float((Globals.base.win.get_x_size()) // 2) / self._gui_scale - 465 // 2, y=220,
            parent=self._fullscreen_node)
        self.set_reload_hint_visible(False)

        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                x=((Globals.base.win.get_x_size()/self._gui_scale) - 1054) // 2,
                y=(Globals.base.win.get_y_size()/self._gui_scale) - 118 - 40,
                parent=self._fullscreen_node)

            Sequence(
                python_warning.color_scale_interval(0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"),
                python_warning.color_scale_interval(0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self._keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png", x=30,
            y=Globals.base.win.get_y_size()//self._gui_scale - 510.0,
            parent=self._fullscreen_node, any_filter=False)

    def _update_stats(self, task=None):
        """ Updates the stats overlay """
        clock = Globals.clock
        self._debug_lines[0].text = "{:3.0f} fps  |  {:3.1f} ms  |  {:3.1f} ms max".format(
            clock.get_average_frame_rate(),
            1000.0 / max(0.001, clock.get_average_frame_rate()),
            clock.get_max_frame_duration() * 1000.0)

        text = "{:4d} render states  |  {:4d} transforms"
        text += "  |  {:4d} commands  |  {:4d} lights  |  {:5d} shadow sources  "
        text += "|  {:3.1f}% atlas usage"
        self._debug_lines[1].text = text.format(
            RenderState.get_num_states(), TransformState.get_num_states(),
            self._pipeline.light_mgr.cmd_queue.num_processed_commands,
            self._pipeline.light_mgr.num_lights,
            self._pipeline.light_mgr.num_shadow_sources,
            self._pipeline.light_mgr.shadow_atlas_coverage)

        text = "Pipeline:   {:3.0f} MiB VRAM  |  {:5d} images  |  {:5d} textures  |  "
        text += "{:5d} render targets  |  {:3d} plugins"
        tex_memory, tex_count = self._buffer_viewer.stage_information
        self._debug_lines[2].text = text.format(
            tex_memory / (1024**2), len(Image.REGISTERED_IMAGES), tex_count,
            RenderTarget.NUM_ALLOCATED_BUFFERS,
            len(self._pipeline.plugin_mgr.enabled_plugins))

        text = "Scene:   {:4.0f} MiB VRAM  |  {:3d} textures  |  {:4d} geoms  "
        text += "|  {:4d} nodes  |  {:7,.0f} vertices  |  {:5.0f} MiB vTX data  "
        scene_tex_size = 0
        for tex in TexturePool.find_all_textures():
            scene_tex_size += tex.estimate_texture_memory()

        self._debug_lines[3].text = text.format(
            scene_tex_size / (1024**2),
            len(TexturePool.find_all_textures()),
            self._analyzer.get_num_geoms(),
            self._analyzer.get_num_nodes(),
            self._analyzer.get_num_vertices(),
            self._analyzer.get_vertex_data_size() / (1024**2),
        )

        text = "{} ({:1.3f})  |  {:3d} daytime settings  |  X {:3.1f}  Y {:3.1f}  Z {:3.1f}"
        self._debug_lines[4].text = text.format(
            self._pipeline.daytime_mgr.formatted_time,
            self._pipeline.daytime_mgr.time,
            len(self._pipeline.plugin_mgr.day_settings),
            Globals.base.camera.get_x(Globals.base.render),
            Globals.base.camera.get_y(Globals.base.render),
            Globals.base.camera.get_z(Globals.base.render),)

        if task:
            return task.again

    def _create_debugger(self):
        """ Creates the debugger contents """
        self._debugger_node = self._fullscreen_node.attach_new_node("DebuggerNode")
        self._debugger_node.set_pos(30, 0, -Globals.base.win.get_y_size()//self._gui_scale + 820.0)
        self._debugger_bg_img = Sprite(
            image="/$$rp/data/gui/debugger_background.png", x=0, y=0,
            parent=self._debugger_node, any_filter=False)
        self._create_debugger_content()

    def set_reload_hint_visible(self, flag):
        """ Sets whether the shader reload hint is visible """
        if flag:
            self._hint_reloading.show()
        else:
            self._hint_reloading.hide()


    def _create_debugger_content(self):
        """ Internal method to create the content of the debugger """

        debugger_content = self._debugger_node.attach_new_node("DebuggerContent")
        debugger_content.set_z(-20)
        debugger_content.set_x(20)

        render_modes = [("Default", "", False, "")]

        # Read modes from configuration
        for mode in self._config["render_modes"]:
            data = [mode["name"], mode["key"]]
            data.append("cxx_only" in mode and mode["cxx_only"])
            data.append(mode["requires"] if "requires" in mode else "")
            render_modes.append(data)

        collection = CheckboxCollection()

        for idx, (mode, mode_id, requires_cxx, requires_plugin) in enumerate(render_modes):
            offs_y = idx * 24 + 45
            offs_x = 0
            enabled = True
            if requires_cxx and not NATIVE_CXX_LOADED:
                enabled = False

            if requires_plugin:
                if not self._pipeline.plugin_mgr.is_plugin_enabled(requires_plugin):
                    enabled = False

            box = LabeledCheckbox(
                parent=debugger_content, x=offs_x, y=offs_y, text=mode.upper(),
                text_color=Vec3(0.4), radio=True, chb_checked=(mode == "Default"),
                chb_callback=partial(self._set_render_mode, mode_id),
                text_size=14, expand_width=230, enabled=enabled)
            collection.add(box.checkbox)

    def _set_render_mode(self, mode_id, value):
        """ Callback which gets called when a render mode got selected """
        if not value:
            return

        # Clear old defines
        self._pipeline.stage_mgr.remove_define_if(lambda name: name.startswith("_RM__"))

        if mode_id == "":
            self._pipeline.stage_mgr.defines["ANY_DEBUG_MODE"] = 0
        else:
            self._pipeline.stage_mgr.defines["ANY_DEBUG_MODE"] = 1
            self._pipeline.stage_mgr.define["_RM_" + mode_id] = 1

        # Reload all shaders
        self._pipeline.reload_shaders()

    def _init_keybindings(self):
        """ Inits the debugger keybindings """
        Globals.base.accept("v", self._buffer_viewer.toggle)
        Globals.base.accept("c", self._pipe_viewer.toggle)
        Globals.base.accept("f5", self._toggle_gui_visible)
        Globals.base.accept("f6", self._toggle_fps_visible)

    def _toggle_gui_visible(self):
        """ Shows / Hides the gui """
        if not self._fullscreen_node.is_hidden():
            self._fullscreen_node.hide()
            self._overlay_node.hide()
        else:
            self._fullscreen_node.show()
            self._overlay_node.show()

    def _toggle_fps_visible(self):
        """ Shows / Hides the FPS graph """
        if not self._fps_node.is_hidden():
            self._fps_node.hide()
        else:
            self._fps_node.show()
Exemple #14
0
class Debugger(RPObject):
    """ This class manages the onscreen gui and """
    def __init__(self, pipeline):
        RPObject.__init__(self)
        self.debug("Creating debugger")
        self._pipeline = pipeline
        self._analyzer = SceneGraphAnalyzer()

        self._load_config()
        self._fullscreen_node = Globals.base.pixel2d.attach_new_node(
            "PipelineDebugger")
        self._create_components()
        self._init_keybindings()
        self._init_notify()

        Globals.base.doMethodLater(0.5,
                                   lambda task: self._collect_scene_data(),
                                   "RPDebugger_collectSceneData_initial")
        Globals.base.doMethodLater(0.1, self._update_stats,
                                   "RPDebugger_updateStats")

    def _load_config(self):
        """ Loads the gui configuration from config/debugging.yaml """

    def _create_components(self):
        """ Creates the gui components """

        # When using small resolutions, scale the GUI so its still useable,
        # otherwise the sub-windows are bigger than the main window
        scale_factor = min(1.0, Globals.base.win.get_x_size() / 1920.0)
        self._fullscreen_node.set_scale(scale_factor)
        self._gui_scale = scale_factor

        # Component values
        self._debugger_width = 460

        # Create states
        self._debugger_visible = False

        # Create intervals
        self._debugger_interval = None

        # Create the actual GUI
        self._create_topbar()
        self._create_stats()
        self._create_hints()

        self._exposure_node = self._fullscreen_node.attach_new_node(
            "ExposureWidget")
        self._exposure_node.set_pos(
            Globals.base.win.get_x_size() / self._gui_scale - 200, 1,
            -Globals.base.win.get_y_size() / self._gui_scale + 120)
        self._exposure_widget = ExposureWidget(self._pipeline,
                                               self._exposure_node)

        self._fps_node = self._fullscreen_node.attach_new_node("FPSChart")
        self._fps_node.set_pos(Vec3(21, 1, -108 - 40))
        self._fps_widget = FPSChart(self._pipeline, self._fps_node)

        self._pixel_widget = PixelInspector(self._pipeline)

        self._buffer_viewer = BufferViewer(self._pipeline,
                                           self._fullscreen_node)
        self._pipe_viewer = PipeViewer(self._pipeline, self._fullscreen_node)
        self._rm_selector = RenderModeSelector(self._pipeline,
                                               self._fullscreen_node)

    def _init_notify(self):
        """ Inits the notify stream which gets all output from panda and parses
        it """
        self._error_msg_handler = ErrorMessageDisplay()

    def update(self):
        """ Updates the gui """
        self._error_msg_handler.update()
        self._pixel_widget.update()

    def get_error_msg_handler(self):
        """ Returns the error message handler """
        return self._error_msg_handler

    def _create_topbar(self):
        """ Creates the topbar """
        self._pipeline_logo = Sprite(
            image="/$$rp/data/gui/pipeline_logo_text.png",
            x=30,
            y=30,
            parent=self._fullscreen_node)

    def _collect_scene_data(self, task=None):
        """ Analyzes the scene graph to provide useful information """
        self._analyzer.clear()
        for geom_node in Globals.base.render.find_all_matches("**/+GeomNode"):
            self._analyzer.add_node(geom_node.node())
        if task:
            return task.again

    def _create_stats(self):
        """ Creates the stats overlay """
        self._overlay_node = Globals.base.aspect2d.attach_new_node("Overlay")
        self._overlay_node.set_pos(Globals.base.get_aspect_ratio() - 0.07, 1,
                                   1.0 - 0.07)
        self._debug_lines = []
        for i in range(6):
            self._debug_lines.append(
                TextNode(pos=Vec2(0, -i * 0.046),
                         parent=self._overlay_node,
                         pixel_size=16,
                         align="right",
                         color=Vec3(1)))

    def _create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self._hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            x=float((Globals.base.win.get_x_size()) // 2) / self._gui_scale -
            465 // 2,
            y=220,
            parent=self._fullscreen_node)
        self.set_reload_hint_visible(False)

        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                x=((Globals.base.win.get_x_size() / self._gui_scale) - 1054) //
                2,
                y=(Globals.base.win.get_y_size() / self._gui_scale) - 118 - 40,
                parent=self._fullscreen_node)

            Sequence(
                python_warning.color_scale_interval(0.7,
                                                    Vec4(0.3, 1, 1, 0.7),
                                                    blendType="easeOut"),
                python_warning.color_scale_interval(0.7,
                                                    Vec4(1, 1, 1, 1.0),
                                                    blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self._keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png",
            x=30,
            y=Globals.base.win.get_y_size() // self._gui_scale - 510.0,
            parent=self._fullscreen_node,
            any_filter=False)

    def _update_stats(self, task=None):
        """ Updates the stats overlay """
        clock = Globals.clock
        self._debug_lines[
            0].text = "{:3.0f} fps  |  {:3.1f} ms  |  {:3.1f} ms max".format(
                clock.get_average_frame_rate(),
                1000.0 / max(0.001, clock.get_average_frame_rate()),
                clock.get_max_frame_duration() * 1000.0)

        text = "{:4d} render states  |  {:4d} transforms"
        text += "  |  {:4d} commands  |  {:4d} lights  |  {:5d} shadow sources  "
        text += "|  {:3.1f}% atlas usage"
        self._debug_lines[1].text = text.format(
            RenderState.get_num_states(), TransformState.get_num_states(),
            self._pipeline.light_mgr.cmd_queue.num_processed_commands,
            self._pipeline.light_mgr.num_lights,
            self._pipeline.light_mgr.num_shadow_sources,
            self._pipeline.light_mgr.shadow_atlas_coverage)

        text = "Pipeline:   {:3.0f} MiB VRAM  |  {:5d} images  |  {:5d} textures  |  "
        text += "{:5d} render targets  |  {:3d} plugins  | {:2d}  views  ({:2d} active)"
        tex_memory, tex_count = self._buffer_viewer.stage_information

        views, active_views = 0, 0
        for target in RenderTarget.REGISTERED_TARGETS:
            if not target.create_default_region:
                num_regions = target.internal_buffer.get_num_display_regions()
                for i, region in enumerate(
                        target.internal_buffer.get_display_regions()):
                    if i == 0 and num_regions > 1:
                        # Skip overlay display region
                        continue
                    views += 1
                    active_views += 1 if target.active and region.active else 0

        self._debug_lines[2].text = text.format(
            tex_memory / (1024**2), len(Image.REGISTERED_IMAGES), tex_count,
            RenderTarget.NUM_ALLOCATED_BUFFERS,
            len(self._pipeline.plugin_mgr.enabled_plugins), views,
            active_views)

        text = "Scene:   {:4.0f} MiB VRAM  |  {:3d} textures  |  {:4d} geoms  "
        text += "|  {:4d} nodes  |  {:7,.0f} vertices  |  {:5.0f} MiB vTX data  "
        scene_tex_size = 0
        for tex in TexturePool.find_all_textures():
            scene_tex_size += tex.estimate_texture_memory()

        self._debug_lines[3].text = text.format(
            scene_tex_size / (1024**2),
            len(TexturePool.find_all_textures()),
            self._analyzer.get_num_geoms(),
            self._analyzer.get_num_nodes(),
            self._analyzer.get_num_vertices(),
            self._analyzer.get_vertex_data_size() / (1024**2),
        )

        sun_vector = Vec3(0)
        if self._pipeline.plugin_mgr.is_plugin_enabled("scattering"):
            sun_vector = self._pipeline.plugin_mgr.instances[
                "scattering"].sun_vector

        text = "{} ({:1.3f})  |  {:0.2f} {:0.2f} {:0.2f}  |  {:3d} daytime settings  |  X {:3.1f}  Y {:3.1f}  Z {:3.1f}"
        text += "    |  Total tasks:  {:2d}   |   scheduled: {:2d}"
        self._debug_lines[4].text = text.format(
            self._pipeline.daytime_mgr.formatted_time,
            self._pipeline.daytime_mgr.time,
            sun_vector.x,
            sun_vector.y,
            sun_vector.z,
            len(self._pipeline.plugin_mgr.day_settings),
            Globals.base.camera.get_x(Globals.base.render),
            Globals.base.camera.get_y(Globals.base.render),
            Globals.base.camera.get_z(Globals.base.render),
            self._pipeline.task_scheduler.num_tasks,
            self._pipeline.task_scheduler.num_scheduled_tasks,
        )

        text = "Scene shadows:  "
        if "pssm" in self._pipeline.plugin_mgr.enabled_plugins:
            focus = self._pipeline.plugin_mgr.instances[
                "pssm"].scene_shadow_stage.last_focus
            if focus is not None:
                text += "{:3.1f} {:3.1f} {:3.1f} r {:3.1f}".format(
                    focus[0].x, focus[0].y, focus[0].z, focus[1])
            else:
                text += "none"
        else:
            text += "inactive"

        text += "  |  H {:3.1f} P {:3.1f} R {:3.1f}"

        self._debug_lines[5].text = text.format(
            Globals.base.camera.get_h(Globals.base.render),
            Globals.base.camera.get_p(Globals.base.render),
            Globals.base.camera.get_r(Globals.base.render),
        )

        if task:
            return task.again

    def set_reload_hint_visible(self, flag):
        """ Sets whether the shader reload hint is visible """
        if flag:
            self._hint_reloading.show()
        else:
            self._hint_reloading.hide()

    def _init_keybindings(self):
        """ Inits the debugger keybindings """
        Globals.base.accept("v", self._buffer_viewer.toggle)
        Globals.base.accept("c", self._pipe_viewer.toggle)
        Globals.base.accept("z", self._rm_selector.toggle)
        Globals.base.accept("f5", self._toggle_gui_visible)
        Globals.base.accept("f6", self._toggle_fps_visible)

    def _toggle_gui_visible(self):
        """ Shows / Hides the gui """

        if not self._fullscreen_node.is_hidden():
            self._fullscreen_node.hide()
            self._overlay_node.hide()
        else:
            self._fullscreen_node.show()
            self._overlay_node.show()

    def _toggle_fps_visible(self):
        """ Shows / Hides the FPS graph """
        if not self._fps_node.is_hidden():
            self._fps_node.hide()
        else:
            self._fps_node.show()
    def present(self, tex):
        """ "Presents" a given texture and shows the window """
        self._current_tex = tex

        self.set_title(tex.get_name())

        # tex.write(tex.get_name() + ".png")

        # Remove old content
        self._content_node.node().remove_all_children()

        w, h = tex.get_x_size(), tex.get_y_size()
        if h > 1:
            scale_x = (self._width - 40.0) / w
            scale_y = (self._height - 110.0) / h
            scale_f = min(scale_x, scale_y)
            display_w = scale_f * w
            display_h = scale_f * h

        else:
            display_w = self._width - 40
            display_h = self._height - 110

        image = Sprite(
            image=tex, parent=self._content_node, x=20, y=90, w=display_w,
            h=display_h, any_filter=False, transparent=False)
        description = ""

        # Image size
        description += "{:d} x {:d} x {:d}".format(
            tex.get_x_size(), tex.get_y_size(), tex.get_z_size())

        # Image type
        description += ", {:s}, {:s}".format(
            Image.format_format(tex.get_format()).upper(),
            Image.format_component_type(tex.get_component_type()).upper())

        Text(text=description, parent=self._content_node, x=17, y=70,
             size=16, color=Vec3(0.6, 0.6, 0.6))

        estimated_bytes = tex.estimate_texture_memory()
        size_desc = "Estimated memory: {:2.2f} MB".format(
            estimated_bytes / (1024.0 ** 2))

        Text(text=size_desc, parent=self._content_node, x=self._width - 20.0,
             y=70, size=18, color=Vec3(0.34, 0.564, 0.192), align="right")

        x_pos = len(size_desc) * 9 + 140

        # Slider for viewing different mipmaps
        if tex.uses_mipmaps():
            max_mips = tex.get_expected_num_mipmap_levels() - 1
            self._mip_slider = Slider(
                parent=self._content_node, size=140, min_value=0, max_value=max_mips,
                callback=self._set_mip, x=x_pos, y=65, value=0)
            x_pos += 140 + 5

            self._mip_text = Text(
                text="MIP: 5", parent=self._content_node, x=x_pos, y=72, size=18,
                color=Vec3(1, 0.4, 0.4), may_change=1)
            x_pos += 50 + 30

        # Slider for viewing different Z-layers
        if tex.get_z_size() > 1:
            self._slice_slider = Slider(
                parent=self._content_node, size=250, min_value=0,
                max_value=tex.get_z_size() - 1, callback=self._set_slice, x=x_pos,
                y=65, value=0)
            x_pos += 250 + 5

            self._slice_text = Text(
                text="Z: 5", parent=self._content_node, x=x_pos, y=72, size=18,
                color=Vec3(0.4, 1, 0.4), may_change=1)

            x_pos += 50 + 30

        # Slider to adjust brightness
        self._bright_slider = Slider(
            parent=self._content_node, size=140, min_value=-14, max_value=14,
            callback=self._set_brightness, x=x_pos, y=65, value=0)
        x_pos += 140 + 5
        self._bright_text = Text(
            text="Bright: 1", parent=self._content_node, x=x_pos, y=72, size=18,
            color=Vec3(0.4, 0.4, 1), may_change=1)
        x_pos += 100 + 30

        # Slider to enable reinhard tonemapping
        self._tonemap_box = LabeledCheckbox(
            parent=self._content_node, x=x_pos, y=60, text="Tonemap",
            text_color=Vec3(1, 0.4, 0.4), chb_checked=False,
            chb_callback=self._set_enable_tonemap,
            text_size=18, expand_width=90)
        x_pos += 90 + 30

        image.set_shader_input("slice", 0)
        image.set_shader_input("mipmap", 0)
        image.set_shader_input("brightness", 1)
        image.set_shader_input("tonemap", False)

        preview_shader = DisplayShaderBuilder.build(tex, display_w, display_h)
        image.set_shader(preview_shader)

        self._preview_image = image
        self.show()
    def present(self, tex):
        """ "Presents" a given texture and shows the window """
        self._current_tex = tex

        self.set_title(tex.get_name())

        # tex.write(tex.get_name() + ".png")

        # Remove old content
        self._content_node.node().remove_all_children()

        w, h = tex.get_x_size(), tex.get_y_size()
        if h > 1:
            scale_x = (self._width - 40.0) / w
            scale_y = (self._height - 110.0) / h
            scale_f = min(scale_x, scale_y)
            display_w = scale_f * w
            display_h = scale_f * h

        else:
            display_w = self._width - 40
            display_h = self._height - 110

        image = Sprite(
            image=tex, parent=self._content_node, x=20, y=90, w=display_w,
            h=display_h, any_filter=False, transparent=False)
        description = ""

        # Image size
        description += "{:d} x {:d} x {:d}".format(
            tex.get_x_size(), tex.get_y_size(), tex.get_z_size())

        # Image type
        description += ", {:s}, {:s}".format(
            Image.format_format(tex.get_format()).upper(),
            Image.format_component_type(tex.get_component_type()).upper())

        Text(text=description, parent=self._content_node, x=17, y=70,
             size=16, color=Vec3(0.6, 0.6, 0.6))

        estimated_bytes = tex.estimate_texture_memory()
        size_desc = "Estimated memory: {:2.2f} MB".format(
            estimated_bytes / (1024.0 ** 2))

        Text(text=size_desc, parent=self._content_node, x=self._width - 20.0,
             y=70, size=18, color=Vec3(0.34, 0.564, 0.192), align="right")

        x_pos = len(size_desc) * 9 + 140

        # Slider for viewing different mipmaps
        if tex.uses_mipmaps():
            max_mips = tex.get_expected_num_mipmap_levels() - 1
            self._mip_slider = Slider(
                parent=self._content_node, size=140, min_value=0, max_value=max_mips,
                callback=self._set_mip, x=x_pos, y=65, value=0)
            x_pos += 140 + 5

            self._mip_text = Text(
                text="MIP: 5", parent=self._content_node, x=x_pos, y=72, size=18,
                color=Vec3(1, 0.4, 0.4), may_change=1)
            x_pos += 50 + 30

        # Slider for viewing different Z-layers
        if tex.get_z_size() > 1:
            self._slice_slider = Slider(
                parent=self._content_node, size=250, min_value=0,
                max_value=tex.get_z_size() - 1, callback=self._set_slice, x=x_pos,
                y=65, value=0)
            x_pos += 250 + 5

            self._slice_text = Text(
                text="Z: 5", parent=self._content_node, x=x_pos, y=72, size=18,
                color=Vec3(0.4, 1, 0.4), may_change=1)

            x_pos += 50 + 30

        # Slider to adjust brightness
        self._bright_slider = Slider(
            parent=self._content_node, size=140, min_value=-14, max_value=14,
            callback=self._set_brightness, x=x_pos, y=65, value=0)
        x_pos += 140 + 5
        self._bright_text = Text(
            text="Bright: 1", parent=self._content_node, x=x_pos, y=72, size=18,
            color=Vec3(0.4, 0.4, 1), may_change=1)
        x_pos += 100 + 30

        # Slider to enable reinhard tonemapping
        self._tonemap_box = LabeledCheckbox(
            parent=self._content_node, x=x_pos, y=60, text="Tonemap",
            text_color=Vec3(1, 0.4, 0.4), chb_checked=False,
            chb_callback=self._set_enable_tonemap,
            text_size=18, expand_width=90)
        x_pos += 90 + 30

        image.set_shader_inputs(
            slice=0,
            mipmap=0,
            brightness=1,
            tonemap=False)

        preview_shader = DisplayShaderBuilder.build(tex, display_w, display_h)
        image.set_shader(preview_shader)

        self._preview_image = image
        self.show()
class Debugger(RPObject):

    """ This class manages the onscreen control, and displays statistics. """

    def __init__(self, pipeline):
        RPObject.__init__(self)
        self.debug("Creating debugger")
        self.pipeline = pipeline
        self.analyzer = SceneGraphAnalyzer()

        self.fullscreen_node = Globals.base.pixel2d.attach_new_node("rp_debugger")
        self.create_components()
        self.init_keybindings()

        if self.advanced_info:
            Globals.base.doMethodLater(
                0.5, lambda task: self.collect_scene_data(), "RPDebugger_collectSceneData_initial")
        
        Globals.base.doMethodLater(0.1, self.update_stats, "RPDebugger_updateStats")

    @property
    def advanced_info(self):
        return self.pipeline.settings["pipeline.advanced_debugging_info"]

    def create_components(self):
        """ Creates the gui components """

        self.debugger_width = 460
        self.debugger_visible = False
        self.debugger_interval = None

        self.create_stats()
        self.create_hints()

        self.pipeline_logo = Sprite(
            image="/$$rp/data/gui/pipeline_logo_text.png", x=30, y=30,
            parent=self.fullscreen_node)

        if self.advanced_info:
            self.exposure_node = self.fullscreen_node.attach_new_node("ExposureWidget")
            self.exposure_widget = ExposureWidget(self.pipeline, self.exposure_node)

        self.fps_node = self.fullscreen_node.attach_new_node("FPSChart")
        self.fps_node.set_pos(Vec3(21, 1, -108 - 40))
        self.fps_widget = FPSChart(self.pipeline, self.fps_node)

        self.pixel_widget = PixelInspector(self.pipeline)
        self.buffer_viewer = BufferViewer(self.pipeline, self.fullscreen_node)
        self.pipe_viewer = PipeViewer(self.pipeline, self.fullscreen_node)
        self.rm_selector = RenderModeSelector(self.pipeline, self.fullscreen_node)
        self.error_msg_handler = ErrorMessageDisplay()

        self.handle_window_resize()

    def update(self):
        """ Updates the gui """
        self.error_msg_handler.update()
        self.pixel_widget.update()

    def collect_scene_data(self, task=None):
        """ Analyzes the scene graph to provide useful information """
        self.analyzer.clear()
        for geom_node in Globals.base.render.find_all_matches("**/+GeomNode"):
            self.analyzer.add_node(geom_node.node())
        if task:
            return task.again

    def create_stats(self):
        """ Creates the stats overlay """
        self.overlay_node = Globals.base.aspect2d.attach_new_node("Overlay")
        self.debug_lines = []

        num_lines = 6 if self.advanced_info else 1
        for i in range(num_lines):
            self.debug_lines.append(TextNode(
                pos=Vec2(0, -i * 0.046), parent=self.overlay_node, align="right", color=Vec3(0.7, 1, 1)))
        self.debug_lines[0].color = Vec4(1, 1, 0, 1)

    def create_hints(self):
        """ Creates the hints like keybindings and when reloading shaders """
        self.hint_reloading = Sprite(
            image="/$$rp/data/gui/shader_reload_hint.png",
            parent=self.fullscreen_node)
        self.set_reload_hint_visible(False)

        self.python_warning = None
        if not NATIVE_CXX_LOADED:
            # Warning when using the python version
            self.python_warning = Sprite(
                image="/$$rp/data/gui/python_warning.png",
                parent=self.fullscreen_node)
            Sequence(
                self.python_warning.color_scale_interval(
                    0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"),
                self.python_warning.color_scale_interval(
                    0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"),
            ).loop()

        # Keybinding hints
        self.keybinding_instructions = Sprite(
            image="/$$rp/data/gui/keybindings.png",
            parent=self.fullscreen_node, any_filter=False)

    def set_reload_hint_visible(self, flag):
        """ Sets whether the shader reload hint is visible """
        if flag:
            self.hint_reloading.show()
        else:
            self.hint_reloading.hide()

    def handle_window_resize(self):
        """ Handles the window resize, repositions the GUI elements to fit on
        screen. """
        # When using small resolutions, scale the GUI so its still useable,
        # otherwise the sub-windows are bigger than the main window
        self.gui_scale = max(0.65, min(1.0, Globals.native_resolution.x / 1920.0))
        self.fullscreen_node.set_scale(self.gui_scale)

        if self.advanced_info:
            self.exposure_node.set_pos(
                Globals.native_resolution.x // self.gui_scale - 200,
                1, -Globals.native_resolution.y // self.gui_scale + 120)
        self.hint_reloading.set_pos(
            float((Globals.native_resolution.x) // 2) / self.gui_scale - 465 // 2, 220)
        self.keybinding_instructions.set_pos(
            30, Globals.native_resolution.y // self.gui_scale - 510.0,)
        self.overlay_node.set_pos(Globals.base.get_aspect_ratio() - 0.07, 1, 1.0 - 0.07)
        if self.python_warning:
            self.python_warning.set_pos(
                (Globals.native_resolution.x // self.gui_scale - 1054) // 2,
                (Globals.native_resolution.y // self.gui_scale - 118 - 40))

        for text in self.debug_lines:
            text.set_pixel_size(16 * max(0.8, self.gui_scale))

        self.buffer_viewer.center_on_screen()
        self.pipe_viewer.center_on_screen()
        self.rm_selector.center_on_screen()

    def init_keybindings(self):
        """ Inits the debugger keybindings """
        Globals.base.accept("v", self.buffer_viewer.toggle)
        Globals.base.accept("c", self.pipe_viewer.toggle)
        Globals.base.accept("z", self.rm_selector.toggle)
        Globals.base.accept("f5", self.toggle_gui_visible)
        Globals.base.accept("f6", self.toggle_keybindings_visible)
        Globals.base.accept("r", self.pipeline.reload_shaders)
        Globals.base.accept("m", self.start_material_editor)

    def start_material_editor(self):
        """ Starts the material editor """
        self.debug("Starting material editor")
        pth = sys.executable
        editor = os.path.dirname(os.path.realpath(__file__))
        editor = os.path.join(editor, "..", "..", "toolkit", "material_editor", "main.py")
        subprocess.Popen([pth, editor], shell=True)        

    def toggle_gui_visible(self):
        """ Shows / Hides the gui """

        if not self.fullscreen_node.is_hidden():
            self.fullscreen_node.hide()
            self.overlay_node.hide()
            self.error_msg_handler.hide()
        else:
            self.fullscreen_node.show()
            self.overlay_node.show()
            self.error_msg_handler.show()

    def toggle_keybindings_visible(self):
        """ Shows / Hides the FPS graph """
        if not self.keybinding_instructions.is_hidden():
            self.keybinding_instructions.hide()
        else:
            self.keybinding_instructions.show()

    def update_stats(self, task=None):
        """ Updates the stats overlay """
        clock = Globals.clock
        self.debug_lines[0].text = "{:3.0f} fps  |  {:3.1f} ms  |  {:3.1f} ms max".format(
            clock.get_average_frame_rate(),
            1000.0 / max(0.001, clock.get_average_frame_rate()),
            clock.get_max_frame_duration() * 1000.0)

        if not self.advanced_info:
            return task.again if task else None

        text = "{:4d} states |  {:4d} transforms "
        text += "|  {:4d} cmds |  {:4d} lights |  {:4d} shadows "
        text += "|  {:5.1f}% atlas usage"
        self.debug_lines[1].text = text.format(
            RenderState.get_num_states(), TransformState.get_num_states(),
            self.pipeline.light_mgr.cmd_queue.num_processed_commands,
            self.pipeline.light_mgr.num_lights,
            self.pipeline.light_mgr.num_shadow_sources,
            self.pipeline.light_mgr.shadow_atlas_coverage)

        text = "Internal:  {:3.0f} MB VRAM |  {:5d} img |  {:5d} tex |  "
        text += "{:5d} fbos |  {:3d} plugins |  {:2d}  views  ({:2d} active)"
        tex_memory, tex_count = self.buffer_viewer.stage_information

        views, active_views = 0, 0
        for target in RenderTarget.REGISTERED_TARGETS:
            if not target.create_default_region:
                num_regions = target.internal_buffer.get_num_display_regions()
                for i, region in enumerate(target.internal_buffer.get_display_regions()):
                    # Skip overlay display region
                    if i == 0 and num_regions > 1:
                        continue
                    views += 1
                    active_views += 1 if target.active and region.active else 0

        self.debug_lines[2].text = text.format(
            tex_memory / (1024**2), len(Image.REGISTERED_IMAGES), tex_count,
            RenderTarget.NUM_ALLOCATED_BUFFERS,
            len(self.pipeline.plugin_mgr.enabled_plugins),
            views, active_views)

        text = "Scene:   {:4.0f} MB VRAM |  {:3d} tex |  {:4d} geoms "
        text += "|  {:4d} nodes |  {:7,.0f} vertices"
        scene_tex_size = 0
        for tex in TexturePool.find_all_textures():
            scene_tex_size += tex.estimate_texture_memory()

        self.debug_lines[3].text = text.format(
            scene_tex_size / (1024**2),
            len(TexturePool.find_all_textures()),
            self.analyzer.get_num_geoms(),
            self.analyzer.get_num_nodes(),
            self.analyzer.get_num_vertices(),
        )

        sun_vector = Vec3(0)
        if self.pipeline.plugin_mgr.is_plugin_enabled("scattering"):
            sun_vector = self.pipeline.plugin_mgr.instances["scattering"].sun_vector

        text = "Time:  {} ({:1.3f}) |  Sun  {:0.2f} {:0.2f} {:0.2f}"
        text += " |  X {:3.1f}  Y {:3.1f}  Z {:3.1f}"
        text += " |  {:2d} tasks |  scheduled: {:2d}"
        self.debug_lines[4].text = text.format(
            self.pipeline.daytime_mgr.formatted_time,
            self.pipeline.daytime_mgr.time,
            sun_vector.x, sun_vector.y, sun_vector.z,
            Globals.base.camera.get_x(Globals.base.render),
            Globals.base.camera.get_y(Globals.base.render),
            Globals.base.camera.get_z(Globals.base.render),
            self.pipeline.task_scheduler.num_tasks,
            self.pipeline.task_scheduler.num_scheduled_tasks,)

        text = "Scene shadows:  "
        if "pssm" in self.pipeline.plugin_mgr.enabled_plugins:
            focus = self.pipeline.plugin_mgr.instances["pssm"].scene_shadow_stage.last_focus
            if focus is not None:
                text += "{:3.1f} {:3.1f} {:3.1f} r {:3.1f}".format(
                    focus[0].x, focus[0].y, focus[0].z, focus[1])
            else:
                text += "none"
        else:
            text += "inactive"

        text += "   |  HPR  ({:3.1f}, {:3.1f}, {:3.1f})  |   {:4d} x {:4d} pixels @ {:3.1f} %"
        text += "   |  {:3d} x {:3d} tiles"
        self.debug_lines[5].text = text.format(
            Globals.base.camera.get_h(Globals.base.render),
            Globals.base.camera.get_p(Globals.base.render),
            Globals.base.camera.get_r(Globals.base.render),
            Globals.native_resolution.x,
            Globals.native_resolution.y,
            self.pipeline.settings["pipeline.resolution_scale"] * 100.0,
            self.pipeline.light_mgr.num_tiles.x,
            self.pipeline.light_mgr.num_tiles.y,)
        if task:
            return task.again
    def _render_stages(self):
        """ Renders the stages to the window """

        self._remove_components()
        entries_per_row = 6
        aspect = Globals.native_resolution.y / Globals.native_resolution.x
        entry_width = 235
        entry_height = (entry_width - 20) * aspect + 55

        # Store already processed images
        processed = set()
        index = -1
        # Iterate over all stages
        for stage_tex in self._stages:
            if stage_tex in processed:
                continue
            processed.add(stage_tex)
            index += 1
            stage_name = stage_tex.get_name()

            xoffs = index % entries_per_row
            yoffs = index // entries_per_row
            node = self._content_node.attach_new_node("Preview")
            node.set_sz(-1)
            node.set_pos(10 + xoffs * (entry_width - 14), 1,
                         yoffs * (entry_height - 14 + 10))

            r, g, b = 0.2, 0.2, 0.2
            if isinstance(stage_tex, Image):
                r, g, b = 0.2, 0.4, 0.6

            stage_name = stage_name.replace("render_pipeline_internal:", "")
            parts = stage_name.split(":")
            stage_name = parts[-1]
            DirectFrame(parent=node,
                        frameSize=(7, entry_width - 17, -7,
                                   -entry_height + 17),
                        frameColor=(r, g, b, 1.0),
                        pos=(0, 0, 0))

            frame_hover = DirectFrame(parent=node,
                                      frameSize=(0, entry_width - 10, 0,
                                                 -entry_height + 10),
                                      frameColor=(0, 0, 0, 0),
                                      pos=(0, 0, 0),
                                      state=DGG.NORMAL)
            frame_hover.bind(DGG.ENTER,
                             partial(self._on_texture_hovered, frame_hover))
            frame_hover.bind(DGG.EXIT,
                             partial(self._on_texture_blurred, frame_hover))
            frame_hover.bind(DGG.B1PRESS,
                             partial(self._on_texture_clicked, stage_tex))

            Text(text=stage_name,
                 x=15,
                 y=29,
                 parent=node,
                 size=12,
                 color=Vec3(0.8))

            # Scale image so it always fits
            w, h = stage_tex.get_x_size(), stage_tex.get_y_size()
            padd_x, padd_y = 24, 57
            scale_x = (entry_width - padd_x) / max(1, w)
            scale_y = (entry_height - padd_y) / max(1, h)
            scale_factor = min(scale_x, scale_y)

            if stage_tex.get_texture_type() == Image.TT_buffer_texture:
                scale_factor = 1
                w = entry_width - padd_x
                h = entry_height - padd_y

            preview = Sprite(image=stage_tex,
                             w=scale_factor * w,
                             h=scale_factor * h,
                             any_filter=False,
                             parent=node,
                             x=7,
                             y=40,
                             transparent=False)

            preview.set_shader_input("mipmap", 0)
            preview.set_shader_input("slice", 0)
            preview.set_shader_input("brightness", 1)
            preview.set_shader_input("tonemap", False)

            preview_shader = DisplayShaderBuilder.build(
                stage_tex, scale_factor * w, scale_factor * h)
            preview.set_shader(preview_shader)

        num_rows = (index + entries_per_row) // entries_per_row

        self._set_scroll_height(50 + (entry_height - 14 + 10) * num_rows)
Exemple #19
0
    def _populate_content(self):  # pylint: disable=too-many-branches,too-many-statements
        """ Reads the pipes and stages from the stage manager and renders those
        into the window """
        self._created = True
        self._pipe_node = self._content_node.attach_new_node("pipes")
        self._pipe_node.set_scale(1, 1, -1)
        self._stage_node = self._content_node.attach_new_node("stages")
        current_pipes = []
        pipe_pixel_size = 3
        pipe_height = 100

        # Generate stages
        for offs, stage in enumerate(self._STAGE_MGR.stages):
            node = self._content_node.attach_new_node("stage")
            node.set_pos(220 + offs * 200.0, 0, 20)
            node.set_scale(1, 1, -1)
            DirectFrame(parent=node,
                        frameSize=(10, 150, 0, -3600),
                        frameColor=(0.2, 0.2, 0.2, 1))
            Text(text=str(stage.debug_name.replace("Stage", "")),
                 parent=node,
                 x=20,
                 y=25,
                 size=15)

            for output_pipe, pipe_tex in iteritems(stage.produced_pipes):
                pipe_idx = 0
                r, g, b = rgb_from_string(output_pipe)
                if output_pipe in current_pipes:
                    pipe_idx = current_pipes.index(output_pipe)
                else:
                    current_pipes.append(output_pipe)
                    pipe_idx = len(current_pipes) - 1
                    DirectFrame(parent=node,
                                frameSize=(0, 8000, pipe_pixel_size / 2,
                                           -pipe_pixel_size / 2),
                                frameColor=(r, g, b, 1),
                                pos=(10, 1, -95 - pipe_idx * pipe_height))
                w = 160
                h = Globals.native_resolution.y /\
                    float(Globals.native_resolution.x) * w
                DirectFrame(parent=node,
                            frameSize=(-pipe_pixel_size, w + pipe_pixel_size,
                                       h / 2 + pipe_pixel_size,
                                       -h / 2 - pipe_pixel_size),
                            frameColor=(r, g, b, 1),
                            pos=(0, 1, -95 - pipe_idx * pipe_height))

                if isinstance(pipe_tex, (list, tuple)):
                    pipe_tex = pipe_tex[0]

                if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)):
                    icon_file = "/$$rp/data/gui/icon_ubo.png"
                elif pipe_tex.get_z_size() > 1:
                    icon_file = "/$$rp/data/gui/icon_texture.png"
                elif pipe_tex.get_texture_type() == Texture.TT_buffer_texture:
                    icon_file = "/$$rp/data/gui/icon_buffer_texture.png"
                else:
                    icon_file = None
                    preview = Sprite(image=pipe_tex,
                                     parent=node,
                                     x=0,
                                     y=50 + pipe_idx * pipe_height,
                                     w=w,
                                     h=h,
                                     any_filter=False,
                                     transparent=False)

                    preview_shader = DisplayShaderBuilder.build(
                        pipe_tex, int(w), int(h))
                    preview.set_shader(preview_shader)

                    preview.set_shader_inputs(mipmap=0,
                                              slice=0,
                                              brightness=1,
                                              tonemap=False)

                if icon_file:
                    Sprite(image=icon_file,
                           parent=node,
                           x=55,
                           y=65 + pipe_idx * pipe_height,
                           w=48,
                           h=48,
                           near_filter=False,
                           transparent=True)

                    if isinstance(pipe_tex,
                                  (SimpleInputBlock, GroupedInputBlock)):
                        tex_desc = "UBO"
                    else:
                        tex_desc = pipe_tex.format_texture_type(
                            pipe_tex.get_texture_type())
                        tex_desc += " - " + pipe_tex.format_format(
                            pipe_tex.get_format()).upper()

                    Text(text=tex_desc,
                         parent=node,
                         x=55 + 48 / 2,
                         y=130 + pipe_idx * pipe_height,
                         color=Vec3(0.2),
                         size=12,
                         align="center")

            for input_pipe in stage.required_pipes:
                if input_pipe not in current_pipes:
                    self.warn("Pipe not found:", input_pipe)
                    continue
                idx = current_pipes.index(input_pipe)
                r, g, b = rgb_from_string(input_pipe)
                DirectFrame(parent=node,
                            frameSize=(0, 10, 40, -40),
                            frameColor=(r, g, b, 1),
                            pos=(5, 1, -95 - idx * pipe_height))

        self._pipe_descriptions = self._content_node.attach_new_node(
            "PipeDescriptions")
        self._pipe_descriptions.set_scale(1, 1, -1)

        DirectFrame(parent=self._pipe_descriptions,
                    frameSize=(0, 190, 0, -5000),
                    frameColor=(0.1, 0.1, 0.1, 1.0))

        # Generate the pipe descriptions
        for idx, pipe in enumerate(current_pipes):
            r, g, b = rgb_from_string(pipe)
            DirectFrame(parent=self._pipe_descriptions,
                        frameSize=(0, 180, -95, -135),
                        frameColor=(r, g, b, 1.0),
                        pos=(0, 1, -idx * pipe_height))
            Text(parent=self._pipe_descriptions,
                 text=pipe,
                 x=42,
                 y=121 + idx * pipe_height,
                 size=15,
                 color=Vec3(0.1))
            Sprite(parent=self._pipe_descriptions,
                   x=9,
                   y=103 + idx * pipe_height,
                   image="/$$rp/data/gui/icon_pipe.png",
                   transparent=True,
                   near_filter=False)
Exemple #20
0
    def _populate_content(self):  # pylint: disable=too-many-branches,too-many-statements
        """ Reads the pipes and stages from the stage manager and renders those
        into the window """
        self._created = True
        self._pipe_node = self._content_node.attach_new_node("pipes")
        self._pipe_node.set_scale(1, 1, -1)
        self._stage_node = self._content_node.attach_new_node("stages")
        current_pipes = []
        pipe_pixel_size = 3
        pipe_height = 100

        # Generate stages
        for offs, stage in enumerate(self._STAGE_MGR.stages):
            node = self._content_node.attach_new_node("stage")
            node.set_pos(220 + offs * 200.0, 0, 20)
            node.set_scale(1, 1, -1)
            DirectFrame(parent=node, frameSize=(10, 150, 0, -3600),
                        frameColor=(0.2, 0.2, 0.2, 1))
            Text(text=str(stage.debug_name.replace("Stage", "")),
                 parent=node, x=20, y=25, size=15)

            for output_pipe, pipe_tex in iteritems(stage.produced_pipes):
                pipe_idx = 0
                r, g, b = rgb_from_string(output_pipe)
                if output_pipe in current_pipes:
                    pipe_idx = current_pipes.index(output_pipe)
                else:
                    current_pipes.append(output_pipe)
                    pipe_idx = len(current_pipes) - 1
                    DirectFrame(parent=node,
                                frameSize=(0, 8000, pipe_pixel_size / 2,
                                           -pipe_pixel_size / 2),
                                frameColor=(r, g, b, 1),
                                pos=(10, 1, -95 - pipe_idx * pipe_height))
                w = 160
                h = Globals.native_resolution.y /\
                    float(Globals.native_resolution.x) * w
                DirectFrame(parent=node,
                            frameSize=(-pipe_pixel_size, w + pipe_pixel_size,
                                       h / 2 + pipe_pixel_size,
                                       -h / 2 - pipe_pixel_size),
                            frameColor=(r, g, b, 1),
                            pos=(0, 1, -95 - pipe_idx * pipe_height))

                if isinstance(pipe_tex, (list, tuple)):
                    pipe_tex = pipe_tex[0]

                if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)):
                    icon_file = "/$$rp/data/gui/icon_ubo.png"
                elif pipe_tex.get_z_size() > 1:
                    icon_file = "/$$rp/data/gui/icon_texture.png"
                elif pipe_tex.get_texture_type() == Texture.TT_buffer_texture:
                    icon_file = "/$$rp/data/gui/icon_buffer_texture.png"
                else:
                    icon_file = None
                    preview = Sprite(
                        image=pipe_tex, parent=node, x=0,
                        y=50 + pipe_idx * pipe_height, w=w, h=h, any_filter=False,
                        transparent=False)

                    preview_shader = DisplayShaderBuilder.build(pipe_tex, int(w), int(h))
                    preview.set_shader(preview_shader)

                    preview.set_shader_inputs(
                        mipmap=0,
                        slice=0,
                        brightness=1,
                        tonemap=False)

                if icon_file:
                    Sprite(image=icon_file, parent=node,
                           x=55, y=65 + pipe_idx * pipe_height,
                           w=48, h=48, near_filter=False,
                           transparent=True)

                    if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)):
                        tex_desc = "UBO"
                    else:
                        tex_desc = pipe_tex.format_texture_type(pipe_tex.get_texture_type())
                        tex_desc += " - " + pipe_tex.format_format(pipe_tex.get_format()).upper()

                    Text(text=tex_desc, parent=node, x=55 + 48 / 2,
                         y=130 + pipe_idx * pipe_height, color=Vec3(0.2),
                         size=12, align="center")

            for input_pipe in stage.required_pipes:
                if input_pipe not in current_pipes:
                    self.warn("Pipe not found:", input_pipe)
                    continue
                idx = current_pipes.index(input_pipe)
                r, g, b = rgb_from_string(input_pipe)
                DirectFrame(parent=node, frameSize=(0, 10, 40, -40),
                            frameColor=(r, g, b, 1),
                            pos=(5, 1, -95 - idx * pipe_height))

        self._pipe_descriptions = self._content_node.attach_new_node(
            "PipeDescriptions")
        self._pipe_descriptions.set_scale(1, 1, -1)

        DirectFrame(parent=self._pipe_descriptions, frameSize=(0, 190, 0, -5000),
                    frameColor=(0.1, 0.1, 0.1, 1.0))

        # Generate the pipe descriptions
        for idx, pipe in enumerate(current_pipes):
            r, g, b = rgb_from_string(pipe)
            DirectFrame(parent=self._pipe_descriptions,
                        frameSize=(0, 180, -95, -135),
                        frameColor=(r, g, b, 1.0), pos=(0, 1, -idx * pipe_height))
            Text(parent=self._pipe_descriptions, text=pipe,
                 x=42, y=121 + idx * pipe_height, size=15,
                 color=Vec3(0.1))
            Sprite(parent=self._pipe_descriptions, x=9, y=103 + idx * pipe_height,
                   image="/$$rp/data/gui/icon_pipe.png",
                   transparent=True, near_filter=False)