Source code for picogl.backend.state

"""
Render-state descriptors and command helpers for PicoGL backends.

The classes in this module are intentionally backend-neutral: they describe
desired OpenGL state and delegate the actual gl calls to a backend object.
"""

from dataclasses import dataclass
from typing import Any, Protocol

from numpy import ndarray
from OpenGL.GL import glBlendFunc, glViewport
from OpenGL.raw.GL.VERSION.GL_1_1 import (
    GL_COLOR_ARRAY,
    GL_NORMAL_ARRAY,
    GL_VERTEX_ARRAY,
)

from picogl.backend.gl.capability import (
    GLBlendFactor,
    GLFixedFunctionCapability,
    GLPipelineCapability,
)
from picogl.backend.gl.enums import GLDrawMode, GLIndexType, GLNumeric
from picogl.backend.gl.enums.point_size import GLPointCapability
from picogl.backend.gl.state.fill import GLCapability, GLFace, GLFillMode
from picogl.backend.gl.wrappers import gl_draw_elements, gl_enable_legacy_client_state
from picogl.backend.gl.wrappers.pointer import (
    gl_color_array_pointer,
    gl_normal_array_pointer,
    gl_vertex_array_pointer,
)
from picogl.backend.value import gl_value
from picogl.texture.gltexture_driver import GLTextureDriver


class CapabilityDriver(Protocol):
    """Capability Driver"""

    def enable(self, cap: int) -> None: ...
    def disable(self, cap: int) -> None: ...


@dataclass(frozen=True)
[docs] class RasterState: """Raster State"""
[docs] polygon_mode: Any = GLFillMode.FILL
[docs] line_width: float = 1.0
[docs] polygon_offset: tuple[float, float] = (0.0, 0.0)
[docs] point_size: float | None = None
[docs] def apply(self, backend: Any) -> None: if hasattr(backend, "raster"): backend.raster.apply(self) return if hasattr(backend, "set_line_width"): backend.set_polygon_mode(GLFace.FRONT_AND_BACK, gl_value(self.polygon_mode)) backend.set_line_width(self.line_width)
[docs] class GLStateManager: """Tracks capability state without querying OpenGL.""" def __init__(self, backend: CapabilityDriver):
[docs] self.backend = backend
[docs] self._caps: dict[int, bool] = {}
[docs] def set_enabled(self, cap: int, enabled: bool) -> None: cap = gl_value(cap) enabled = bool(enabled) if self._caps.get(cap) == enabled: return self._caps[cap] = enabled if enabled: self.backend.enable(cap) else: self.backend.disable(cap)
[docs] def is_enabled(self, cap: int) -> bool: return self._caps.get(gl_value(cap), False)
@dataclass(frozen=True)
[docs] class BlendState: """Blend State"""
[docs] enabled: bool = False
[docs] src: Any = GLBlendFactor.SRC_ALPHA
[docs] dst: Any = GLBlendFactor.ONE_MINUS_SRC_ALPHA
[docs] def apply(self, state: GLStateManager): backend = state.backend if hasattr(backend, "blend"): backend.blend.apply(self) return state.set_enabled(GLPipelineCapability.BLEND, self.enabled) if self.enabled: glBlendFunc(gl_value(self.src), gl_value(self.dst))
@dataclass(frozen=True, init=False)
[docs] class DepthState: """Depth State"""
[docs] test: bool = True
[docs] write: bool = True
def __init__( self, test: bool = True, write: bool = True, enabled: bool | None = None, ): if enabled is not None: test = enabled object.__setattr__(self, "test", bool(test)) object.__setattr__(self, "write", bool(write)) @property
[docs] def enabled(self) -> bool: return self.test
[docs] def apply(self, state: GLStateManager): backend = state.backend if hasattr(backend, "depth"): backend.depth.apply(self) return state.set_enabled(GLPipelineCapability.DEPTH_TEST, self.test) state.backend.set_depth_write(self.write)
@dataclass(frozen=True, init=False)
[docs] class RenderState: """Flat render-state descriptor with nested-state constructor support."""
[docs] blend: bool = False
[docs] blend_src: Any = GLBlendFactor.SRC_ALPHA
[docs] blend_dst: Any = GLBlendFactor.ONE_MINUS_SRC_ALPHA
[docs] depth_test: bool = True
[docs] depth_write: bool = True
[docs] line_width: float = 1.0
[docs] polygon_mode: Any = GLFillMode.FILL
[docs] polygon_offset: tuple[float, float] = (0.0, 0.0)
[docs] point_size: float | None = None
[docs] program_point_size: bool = False
[docs] cull_face: bool = False
[docs] lighting: bool = False
def __init__( self, *, raster: RasterState | None = None, depth: DepthState | None = None, blend: BlendState | bool | None = None, blend_src: Any = GLBlendFactor.SRC_ALPHA, blend_dst: Any = GLBlendFactor.ONE_MINUS_SRC_ALPHA, depth_test: bool | None = None, depth_write: bool | None = None, line_width: float | None = None, polygon_mode: Any | None = None, polygon_offset: tuple[float, float] | None = None, point_size: float | None = None, program_point_size: bool = False, cull_face: bool = False, lighting: bool = False, ): if raster is not None: line_width = raster.line_width if line_width is None else line_width polygon_mode = raster.polygon_mode if polygon_mode is None else polygon_mode polygon_offset = ( raster.polygon_offset if polygon_offset is None else polygon_offset ) point_size = raster.point_size if point_size is None else point_size if depth is not None: depth_test = depth.test if depth_test is None else depth_test depth_write = depth.write if depth_write is None else depth_write if isinstance(blend, BlendState): blend_src = blend.src blend_dst = blend.dst blend_enabled = blend.enabled else: blend_enabled = bool(blend) if blend is not None else False object.__setattr__(self, "blend", bool(blend_enabled)) object.__setattr__(self, "blend_src", blend_src) object.__setattr__(self, "blend_dst", blend_dst) object.__setattr__( self, "depth_test", True if depth_test is None else bool(depth_test), ) object.__setattr__( self, "depth_write", True if depth_write is None else bool(depth_write), ) object.__setattr__( self, "line_width", 1.0 if line_width is None else float(line_width), ) object.__setattr__( self, "polygon_mode", GLFillMode.FILL if polygon_mode is None else polygon_mode, ) object.__setattr__( self, "polygon_offset", (0.0, 0.0) if polygon_offset is None else tuple(polygon_offset), ) object.__setattr__(self, "point_size", point_size) object.__setattr__(self, "program_point_size", bool(program_point_size)) object.__setattr__(self, "cull_face", bool(cull_face)) object.__setattr__(self, "lighting", bool(lighting)) @property
[docs] def raster(self) -> RasterState: return RasterState( polygon_mode=self.polygon_mode, line_width=self.line_width, polygon_offset=self.polygon_offset, point_size=self.point_size, )
@property
[docs] def depth(self) -> DepthState: return DepthState(test=self.depth_test, write=self.depth_write)
@property
[docs] def blend_state(self) -> BlendState: return BlendState( enabled=self.blend, src=self.blend_src, dst=self.blend_dst, )
[docs] class RenderStateApplier: """Applies render-state deltas through a gl backend.""" def __init__(self, backend: Any):
[docs] self.backend = backend
[docs] self.current: RenderState | None = None
[docs] def apply(self, state: RenderState): if self.current == state: return prev = self.current self.current = state if prev is None or prev.raster != state.raster: self.backend.raster.apply(state.raster) if prev is None or prev.depth != state.depth: self.backend.depth.apply(state.depth) if prev is None or prev.blend_state != state.blend_state: self.backend.blend.apply(state.blend_state) if prev is None or prev.cull_face != state.cull_face: self.backend.capabilities.set_enabled( GLPipelineCapability.CULL_FACE, state.cull_face, ) # Core-profile contexts reject GL_LIGHTING; modern draws use shader lighting. if state.lighting and (prev is None or not prev.lighting): self.backend.capabilities.set_enabled( GLFixedFunctionCapability.LIGHTING, True, ) if state.program_point_size and (prev is None or not prev.program_point_size): self.backend.capabilities.enable(GLPointCapability.PROGRAM_POINT_SIZE) elif ( prev is not None and prev.program_point_size and not state.program_point_size ): self.backend.capabilities.disable(GLPointCapability.PROGRAM_POINT_SIZE)
[docs] class GLVertexBuffer: def __init__(self, data: ndarray):
[docs] self.data = data
[docs] def bind_legacy(self): # fallback path pass
@dataclass
[docs] class GLAttributeArray: """gl Attribute Array"""
[docs] size: int
[docs] dtype: Any
[docs] stride: int
[docs] pointer: Any
[docs] def enable_legacy(self, kind): gl_enable_legacy_client_state(kind) if kind == GL_VERTEX_ARRAY: gl_vertex_array_pointer( pointer=self.pointer, size=self.size, num_type=GLNumeric.FLOAT, stride=self.stride, ) elif kind == GL_NORMAL_ARRAY: gl_normal_array_pointer( pointer=self.pointer, num_type=GLNumeric.FLOAT, stride=self.stride, ) elif kind == GL_COLOR_ARRAY: gl_color_array_pointer( pointer=self.pointer, size=self.size, num_type=GLNumeric.FLOAT, stride=self.stride, )
@dataclass
[docs] class GLViewport:
[docs] x: int
[docs] y: int
[docs] width: int
[docs] height: int
[docs] def apply(self): glViewport(self.x, self.y, self.width, self.height)
[docs] class TestGLMesh: """Test gl Mesh""" def __init__(self, vertices, indices=None):
[docs] self.vertices = vertices
[docs] self.indices = indices
[docs] self.attributes: list[GLAttributeArray] = []
[docs] def add_attribute(self, attr: GLAttributeArray): self.attributes.append(attr)
[docs] def draw(self): for attr in self.attributes: attr.enable_legacy(GL_VERTEX_ARRAY) # refine mapping if self.indices is not None: gl_draw_elements( len(self.indices), GLIndexType.UNSIGNED_INT, GLDrawMode.TRIANGLES, pointer=self.indices, )
@dataclass
[docs] class DrawCommand: """Draw Command"""
[docs] mesh: Any
[docs] mode: int | None = None
[docs] texture: GLTextureDriver | int | None = None
[docs] state: RenderState | None = None
[docs] def execute(self, backend: Any): if self.state is not None: if hasattr(backend, "apply_state"): backend.apply_state(self.state) else: RenderStateApplier(backend).apply(self.state) if self.texture: if isinstance(self.texture, int) and hasattr(backend, "textures"): backend.textures.bind_texture(self.texture) elif isinstance(self.texture, int) and hasattr(backend, "bind_texture"): backend.bind_texture(self.texture) elif hasattr(self.texture, "bind"): self.texture.bind() if self.mode is not None and hasattr(backend, "geometry"): backend.geometry.draw_mesh(self.mesh, self.mode) elif self.mode is not None and hasattr(backend, "draw_mesh"): backend.draw_mesh(self.mesh, self.mode) elif hasattr(self.mesh, "draw"): self.mesh.draw() else: raise TypeError( "DrawCommand requires a mode/backend draw_mesh or a drawable mesh." )
@dataclass
[docs] class GLClipPlaneState: """gl Clipping Plane State"""
[docs] enabled0: bool = False
[docs] enabled1: bool = False
[docs] def apply(self, state: GLStateManager): state.set_enabled(GLCapability.CLIP_DISTANCE0, self.enabled0) state.set_enabled(GLCapability.CLIP_DISTANCE1, self.enabled1)
__all__ = [ "BlendState", "DepthState", "DrawCommand", "GLAttributeArray", "GLClipPlaneState", "GLStateManager", "GLVertexBuffer", "GLViewport", "RasterState", "RenderState", "RenderStateApplier", "TestGLMesh", "gl_value", ]