91 lines
3.6 KiB
Python
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
|