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