Source code for simvx.graphics.renderer._base

"""Renderer protocol — defines the contract any rendering backend must satisfy.

The ForwardRenderer (Vulkan) is the reference implementation. Future backends
(WebGPU, streaming) implement the same protocol so SceneAdapter, App, and
testing infrastructure work identically across backends.
"""


from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any

import numpy as np

if TYPE_CHECKING:
    from .._types import MeshHandle
    from .viewport_manager import ViewportManager

log = logging.getLogger(__name__)

__all__ = ["Renderer"]


[docs] class Renderer(ABC): """Base class for rendering pipelines (forward, deferred, webgpu, etc.). Backends implement these methods. SceneAdapter calls the submission methods each frame; Engine calls the lifecycle methods. The numpy dtype boundary (VERTEX_DTYPE, MATERIAL_DTYPE, LIGHT_DTYPE, TRANSFORM_DTYPE) is the contract. """ # -- Frame lifecycle --
[docs] @abstractmethod def begin_frame(self) -> None: """Reset per-frame state, prepare for new submissions."""
[docs] @abstractmethod def pre_render(self, cmd: Any) -> None: """Offscreen passes (shadows, SSAO) — after submissions, before main pass."""
[docs] @abstractmethod def render(self, cmd: Any) -> None: """Record draw commands into *cmd* during the main render pass."""
[docs] @abstractmethod def resize(self, width: int, height: int) -> None: """Handle framebuffer resize."""
[docs] @abstractmethod def destroy(self) -> None: """Release all GPU resources."""
# -- Scene submissions (called by SceneAdapter each frame) --
[docs] @abstractmethod def submit_instance( self, mesh_handle: MeshHandle, transform: np.ndarray, material_id: int, viewport_id: int = 0, ) -> None: """Submit a single mesh instance for rendering."""
[docs] @abstractmethod def submit_multimesh( self, mesh_handle: MeshHandle, transforms: np.ndarray, material_id: int = 0, material_ids: np.ndarray | None = None, viewport_id: int = 0, count: int = 0, ) -> None: """Submit multiple instances of the same mesh."""
[docs] @abstractmethod def submit_skinned_instance( self, mesh_handle: MeshHandle, transform: np.ndarray, material_id: int, joint_matrices: np.ndarray, ) -> None: """Submit a skinned mesh with joint matrices."""
[docs] @abstractmethod def set_materials(self, materials: np.ndarray) -> None: """Upload material array (MATERIAL_DTYPE) to GPU."""
[docs] @abstractmethod def set_lights(self, lights: np.ndarray) -> None: """Upload light array (LIGHT_DTYPE) to GPU."""
[docs] @abstractmethod def submit_text( self, text: str, x: float, y: float, size: float, colour: tuple[float, float, float, float], **kwargs: Any, ) -> None: """Submit 2D text for rendering."""
[docs] @abstractmethod def submit_particles(self, particle_data: np.ndarray) -> None: """Submit CPU particle data for rendering."""
[docs] @abstractmethod def submit_light2d(self, **kwargs: Any) -> None: """Submit a 2D light source."""
# -- Resource management --
[docs] @abstractmethod def register_mesh(self, vertices: np.ndarray, indices: np.ndarray) -> MeshHandle: """Register mesh data on GPU, return a handle for submissions."""
[docs] @abstractmethod def register_texture(self, pixels: np.ndarray, width: int, height: int) -> int: """Upload RGBA pixel data to GPU, return bindless texture index."""
# -- Frame capture (for headless testing) --
[docs] @abstractmethod def capture_frame(self) -> np.ndarray: """Capture the last rendered frame as (H, W, 4) uint8 RGBA numpy array."""
# -- Viewport -- # viewport_manager: ViewportManager — expected as an attribute, not enforced by ABC