# The Clear BSD License
#
# Copyright (c) 2026 Tobias Heibges
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted (subject to the limitations in the disclaimer
# below) provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Surface Type Registry
A flexible registry for surface types (plane, sphere, wave, etc.).
Each surface type is registered with a name, capability ("gpu" or "cpu"),
an integer ID for GPU kernel dispatch, and the surface class itself.
The registry ensures:
- Each surface type name is unique
- Each (capability, id) combination is unique (no GPU id conflicts)
- GPU IDs are positive integers (used by GPU kernels)
- CPU-only surfaces use id=0
Usage
-----
Register new surface types (typically done in the surface class module):
from lsurf.surfaces.registry import register_surface_type
@register_surface_type("cylinder", "gpu", 3)
class CylinderSurface(Surface):
...
Or register manually:
register_surface_type("cylinder", "gpu", 3, CylinderSurface)
Query the registry:
from lsurf.surfaces.registry import get_surface_type, get_surface_class
info = get_surface_type("plane")
# -> SurfaceTypeInfo('plane', 'gpu', id=1, cls=<class 'PlaneSurface'>)
cls = get_surface_class("plane")
# -> <class 'PlaneSurface'>
"""
from dataclasses import dataclass
from typing import Literal, TYPE_CHECKING
if TYPE_CHECKING:
from .protocol import Surface
[docs]
@dataclass(frozen=True)
class SurfaceTypeInfo:
"""Information about a registered surface type."""
name: str
capability: Literal["gpu", "cpu"]
id: int
cls: type["Surface"] | None = None
def __repr__(self) -> str:
cls_name = self.cls.__name__ if self.cls else "None"
return f"SurfaceTypeInfo({self.name!r}, {self.capability!r}, id={self.id}, cls={cls_name})"
class SurfaceTypeRegistry:
"""
Registry for surface types.
Ensures uniqueness of:
- Surface type names
- (capability, id) combinations for GPU surfaces
"""
def __init__(self) -> None:
self._by_name: dict[str, SurfaceTypeInfo] = {}
self._gpu_ids: dict[int, str] = {} # id -> name mapping for GPU surfaces
self._by_class: dict[type, SurfaceTypeInfo] = {} # class -> info mapping
def register(
self,
name: str,
capability: Literal["gpu", "cpu"],
surface_id: int | None = None,
cls: type["Surface"] | None = None,
) -> SurfaceTypeInfo:
"""
Register a new surface type.
Parameters
----------
name : str
Unique name for the surface type (e.g., "plane", "sphere").
capability : "gpu" or "cpu"
Whether this surface supports GPU acceleration.
surface_id : int, optional
GPU kernel ID. Required for GPU surfaces (must be > 0).
CPU surfaces always use id=0.
cls : type, optional
The surface class to register.
Returns
-------
SurfaceTypeInfo
The registered surface type information.
Raises
------
ValueError
If name already exists, or if GPU id is already taken.
"""
# Validate name uniqueness
if name in self._by_name:
existing = self._by_name[name]
# Allow re-registration with the same class (for adding class after initial registration)
if cls is not None and existing.cls is None:
# Update existing entry with class
new_info = SurfaceTypeInfo(
name=existing.name,
capability=existing.capability,
id=existing.id,
cls=cls,
)
self._by_name[name] = new_info
self._by_class[cls] = new_info
return new_info
elif cls is None or cls is existing.cls:
return existing
else:
raise ValueError(
f"Surface type '{name}' already registered as "
f"({existing.capability}, id={existing.id})"
)
# Handle GPU vs CPU
if capability == "gpu":
if surface_id is None:
raise ValueError("GPU surfaces require a surface_id > 0")
if surface_id <= 0:
raise ValueError(f"GPU surface_id must be > 0, got {surface_id}")
if surface_id in self._gpu_ids:
existing_name = self._gpu_ids[surface_id]
raise ValueError(
f"GPU id {surface_id} already used by '{existing_name}'"
)
self._gpu_ids[surface_id] = name
final_id = surface_id
else:
# CPU surfaces always have id=0
if surface_id is not None and surface_id != 0:
raise ValueError(
f"CPU surfaces must have id=0 (or omit it), got {surface_id}"
)
final_id = 0
info = SurfaceTypeInfo(name=name, capability=capability, id=final_id, cls=cls)
self._by_name[name] = info
if cls is not None:
self._by_class[cls] = info
return info
def get(self, name: str) -> SurfaceTypeInfo:
"""
Get surface type info by name.
Raises KeyError if not found.
"""
if name not in self._by_name:
available = ", ".join(sorted(self._by_name.keys()))
raise KeyError(f"Unknown surface type '{name}'. Registered: {available}")
return self._by_name[name]
def get_by_class(self, cls: type) -> SurfaceTypeInfo:
"""
Get surface type info by class.
Raises KeyError if not found.
"""
if cls not in self._by_class:
raise KeyError(
f"Class {cls.__name__} not registered in surface type registry"
)
return self._by_class[cls]
def get_id(self, name: str) -> int:
"""Get the GPU kernel ID for a surface type."""
return self.get(name).id
def get_class(self, name: str) -> type["Surface"] | None:
"""Get the surface class for a surface type."""
return self.get(name).cls
def get_capability(self, name: str) -> Literal["gpu", "cpu"]:
"""Get the capability type for a surface type."""
return self.get(name).capability
def is_gpu_capable(self, name: str) -> bool:
"""Check if a surface type is GPU-capable."""
return self.get(name).capability == "gpu"
def list_all(self) -> list[SurfaceTypeInfo]:
"""List all registered surface types."""
return list(self._by_name.values())
def list_by_capability(
self, capability: Literal["gpu", "cpu"]
) -> list[SurfaceTypeInfo]:
"""List surface types filtered by capability."""
return [
info for info in self._by_name.values() if info.capability == capability
]
def __contains__(self, name: str) -> bool:
"""Check if a surface type name is registered."""
return name in self._by_name
def __len__(self) -> int:
"""Number of registered surface types."""
return len(self._by_name)
def __repr__(self) -> str:
gpu_count = len(self._gpu_ids)
cpu_count = len(self._by_name) - gpu_count
return f"SurfaceTypeRegistry({gpu_count} GPU, {cpu_count} CPU)"
# Global registry instance
_registry = SurfaceTypeRegistry()
# --- Module-level convenience functions ---
[docs]
def register_surface_type(
name: str,
capability: Literal["gpu", "cpu"],
surface_id: int | None = None,
cls: type["Surface"] | None = None,
) -> SurfaceTypeInfo | type["Surface"]:
"""
Register a new surface type.
Can be used as a function or decorator.
Parameters
----------
name : str
Unique name for the surface type (e.g., "plane", "sphere").
capability : "gpu" or "cpu"
Whether this surface supports GPU acceleration.
surface_id : int, optional
GPU kernel ID. Required for GPU surfaces (must be > 0).
CPU surfaces always use id=0.
cls : type, optional
The surface class. If None, returns a decorator.
Returns
-------
SurfaceTypeInfo or decorator
If cls is provided, returns SurfaceTypeInfo.
If cls is None, returns a decorator that registers the class.
Examples
--------
>>> # Direct registration
>>> register_surface_type("plane", "gpu", 1, PlaneSurface)
SurfaceTypeInfo('plane', 'gpu', id=1, cls=PlaneSurface)
>>> # As decorator
>>> @register_surface_type("cylinder", "gpu", 3)
... class CylinderSurface(Surface):
... pass
"""
if cls is not None:
return _registry.register(name, capability, surface_id, cls)
# Return decorator
def decorator(cls: type["Surface"]) -> type["Surface"]:
_registry.register(name, capability, surface_id, cls)
return cls
return decorator
[docs]
def get_surface_type(name: str) -> SurfaceTypeInfo:
"""Get full surface type info by name."""
return _registry.get(name)
[docs]
def get_surface_type_id(name: str) -> int:
"""Get the GPU kernel ID for a surface type."""
return _registry.get_id(name)
[docs]
def get_surface_class(name: str) -> type["Surface"] | None:
"""Get the surface class for a surface type."""
return _registry.get_class(name)
[docs]
def is_gpu_capable(name: str) -> bool:
"""Check if a surface type supports GPU acceleration."""
return _registry.is_gpu_capable(name)
[docs]
def list_surface_types(
capability: Literal["gpu", "cpu"] | None = None,
) -> list[SurfaceTypeInfo]:
"""
List registered surface types.
Parameters
----------
capability : "gpu", "cpu", or None
Filter by capability. None returns all.
"""
if capability is None:
return _registry.list_all()
return _registry.list_by_capability(capability)
[docs]
def surface_type_exists(name: str) -> bool:
"""Check if a surface type is registered."""
return name in _registry
# --- Backwards compatibility aliases ---
register_geometry = register_surface_type
get_geometry_info = get_surface_type
get_geometry_id = get_surface_type_id
list_geometries = list_surface_types
geometry_exists = surface_type_exists
GeometryInfo = SurfaceTypeInfo
# --- Register built-in surface types ---
# Note: Classes are registered when their modules are imported.
# Here we just reserve the names and IDs.
# GPU-capable surfaces (used by GPU kernels)
register_surface_type("plane", "gpu", 1)
register_surface_type("sphere", "gpu", 2)
# CPU-only surfaces (complex shapes requiring ray marching)
register_surface_type("gerstner_wave", "cpu")
register_surface_type("curved_wave", "cpu")