Source code for simvx.editor.game_render_hook

"""Offscreen render hook for the editor's game viewport.

Wires into the engine's ``pre_render_callback`` so that the play-mode game
scene (and edit-mode textured viewports) are rendered into offscreen targets
before the editor's own draw pass. Composed by :class:`Root`.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .panels.scene2d_view import Scene2DView
    from .panels.scene3d_view import Scene3DView
    from .play_mode import PlayMode
    from .state import State


[docs] class GameRenderHook: """Renders the play-mode game scene into an offscreen viewport. Owns the engine ``pre_render`` chaining and the lifecycle of the game viewport target (created on play, destroyed on stop). Reads the editor viewport rects to size the offscreen target. """ def __init__(self, state: State, play_mode: PlayMode): self.state = state self.play_mode = play_mode self._viewport_3d: Scene3DView | None = None self._viewport_2d: Scene2DView | None = None self._tree = None self._original_pre_render = None
[docs] def attach(self, tree, viewport_3d, viewport_2d) -> None: """Install the pre_render callback and remember viewport refs. Called once during ``Root.ready()`` after viewports are built. ``tree`` must expose ``app._engine`` for the engine hook; missing engine (test harness) is tolerated as a silent no-op. """ self._tree = tree self._viewport_3d = viewport_3d self._viewport_2d = viewport_2d if tree is None or not hasattr(tree, "app"): return app = tree.app if app is None or not hasattr(app, "_engine"): return self._wire(app) self.state.play_state_changed.connect(self._on_play_state_changed)
def _on_play_state_changed(self) -> None: if self.state.is_playing: self._create_game_viewport() else: self.play_mode.destroy_game_viewport() def _create_game_viewport(self) -> None: if self._tree is None or not hasattr(self._tree, "app"): return app = self._tree.app if app is None or not hasattr(app, "_engine"): return engine = app._engine vp = self._viewport_3d if vp is not None: rect = vp.get_global_rect() w, h = max(int(rect[2]), 64), max(int(rect[3]), 64) else: w, h = 800, 600 self.play_mode.create_game_viewport(engine, w, h) game_tree = self.play_mode.game_tree if game_tree is not None: game_tree.screen_size = (w, h) sx, sy = engine.content_scale game_tree.play_viewport_rect = (0, 0, w / sx, h / sy) def _wire(self, app) -> None: """Extend ``engine.pre_render_callback`` to render game/edit targets. Original callback runs first so its SSBO writes commit before our offscreen passes. Without this the in-shader tonemap flag races the HDR output flag the editor sets per frame. """ engine = app._engine adapter = app.scene_adapter original = engine.pre_render_callback def _pre_render(cmd): if original: original(cmd) gvp = self.play_mode.game_viewport if (gvp is not None and gvp.ready and self.state.is_playing and adapter is not None): game_tree = self.play_mode.game_tree if game_tree is not None and game_tree.root is not None: from simvx.graphics.draw2d import Draw2D game_batches = None with Draw2D._isolated(): game_tree.draw(Draw2D) game_batches = Draw2D._get_batches() adapter.render_to_target( cmd, gvp, game_tree, draw2d_batches=game_batches, ) return if not self.state.is_playing and adapter is not None: edited = self.state.edited_scene if edited is not None and edited.root is not None: if self.state.view_mode_3d == "textured" and self._viewport_3d is not None: evp = getattr(self._viewport_3d, "_edit_viewport", None) if evp is not None: adapter.render_to_target(cmd, evp, edited, camera=self.state.editor_camera) vp2d = self._viewport_2d if vp2d is not None and getattr(vp2d, "_view_mode", None) == "shaded": evp2 = getattr(vp2d, "_edit_viewport", None) if evp2 is not None: adapter.render_to_target( cmd, evp2, edited, camera=self.state.editor_camera, screen_size=(float(evp2.width), float(evp2.height)), ) engine.pre_render_callback = _pre_render self._original_pre_render = original