Files
RFP_Infinity-Vis/Infinity_Vis_1/infinity_vis_1/renderer.py

91 lines
3.6 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import time
from .colors import scale
from .models import ControllerSpec, FrameSnapshot, MappingSpec, RGB, SceneState
from .patterns import render_pattern, utility_pattern
@dataclass(slots=True)
class RendererStats:
tile_count: int
controller_count: int
total_leds: int
class FastRenderer:
def __init__(self, mapping: MappingSpec) -> None:
self.mapping = mapping
self.tiles = mapping.ordered_tiles()
self.controllers: list[ControllerSpec] = mapping.build_controllers()
self._tile_index = {tile.tile_id: index for index, tile in enumerate(self.tiles)}
self._controller_buffers = {controller.host: bytearray(controller.payload_bytes) for controller in self.controllers}
self._controller_zero = {host: bytes(len(buffer)) for host, buffer in self._controller_buffers.items()}
self._gamma_table = self._build_gamma_table(mapping.global_gamma)
def stats(self) -> RendererStats:
return RendererStats(
tile_count=len(self.tiles),
controller_count=len(self.controllers),
total_leds=sum(tile.led_total for tile in self.tiles),
)
def render(self, scene: SceneState, timestamp: float | None = None) -> FrameSnapshot:
now = time.perf_counter() if timestamp is None else float(timestamp)
if scene.utility_mode != "none":
tile_colors = utility_pattern(self.mapping, scene.utility_mode, scene.selected_tile_id, now)
else:
tile_colors = render_pattern(self.mapping, scene.pattern_id, scene.params, now, scene.tempo_bpm)
controller_payloads = self._build_controller_payloads(tile_colors)
return FrameSnapshot(
timestamp=now,
pattern_id=scene.pattern_id,
tile_colors=tile_colors,
controller_payloads=controller_payloads,
)
def _build_controller_payloads(self, tile_colors: list[RGB]) -> dict[str, bytes]:
for host, buffer in self._controller_buffers.items():
buffer[:] = self._controller_zero[host]
for controller in self.controllers:
payload = self._controller_buffers[controller.host]
for route in controller.tiles:
tile = self.tiles[self._tile_index[route.tile_id]]
color = tile_colors[self._tile_index[route.tile_id]]
color = self._apply_tile_brightness(color, tile.brightness)
for byte_offset, led_count in route.segment_offsets:
self._fill_segment(payload, byte_offset, led_count, color)
return {host: bytes(buffer) for host, buffer in self._controller_buffers.items()}
def _apply_tile_brightness(self, color: RGB, brightness: float) -> RGB:
scaled = scale(color, brightness)
return (
self._gamma_table[scaled[0]],
self._gamma_table[scaled[1]],
self._gamma_table[scaled[2]],
)
def _build_gamma_table(self, gamma: float) -> list[int]:
gamma = max(0.1, float(gamma))
table = [0] * 256
for value in range(256):
normalized = value / 255.0
table[value] = max(0, min(255, int(round((normalized ** gamma) * 255.0))))
return table
def _fill_segment(self, payload: bytearray, byte_offset: int, led_count: int, color: RGB) -> None:
red, green, blue = color
cursor = byte_offset
for _ in range(max(0, led_count)):
if cursor + 2 >= len(payload):
break
payload[cursor] = red
payload[cursor + 1] = green
payload[cursor + 2] = blue
cursor += 3