"""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