Source code for simvx.graphics.materials.texture

"""Texture loading and bindless descriptor array management."""


from __future__ import annotations

import hashlib
import io
import logging
from pathlib import Path
from typing import Any

import numpy as np
from PIL import Image

__all__ = ["TextureManager"]

log = logging.getLogger(__name__)


[docs] class TextureManager: """Loads textures into the bindless descriptor array with path-based caching. Wraps Engine.load_texture() to avoid duplicate GPU uploads for the same file. """ def __init__(self, engine: Any) -> None: self._engine = engine self._cache: dict[str, int] = {} # resolved path or hash → bindless index
[docs] def load(self, path: str | Path) -> int: """Load a texture from disk. Returns its bindless index. Cached — loading the same path twice returns the same index. """ key = str(Path(path).resolve()) if key in self._cache: return self._cache[key] idx = self._engine.load_texture(key) self._cache[key] = idx log.debug("TextureManager: %s → index %d", Path(key).name, idx) return idx
[docs] def load_from_bytes(self, data: bytes) -> int: """Load a texture from in-memory image bytes (PNG/JPG). Returns bindless index. Cached by content hash — identical bytes return the same index. """ key = "bytes:" + hashlib.sha256(data).hexdigest() if key in self._cache: return self._cache[key] img = Image.open(io.BytesIO(data)).convert("RGBA") width, height = img.size pixels = np.ascontiguousarray(np.array(img, dtype=np.uint8)) idx = self._engine.upload_texture_pixels(pixels, width, height) self._cache[key] = idx log.debug("TextureManager: embedded %dx%d → index %d", width, height, idx) return idx
[docs] def load_if_exists(self, path: str | Path) -> int: """Load a texture if the file exists. Returns -1 if not found.""" p = Path(path) if not p.exists(): return -1 return self.load(p)
[docs] def get_texture_size(self, tex_idx: int) -> tuple[int, int]: """Return (width, height) for a loaded texture index. (0, 0) if unknown.""" return self._engine._texture_sizes.get(tex_idx, (0, 0))
@property def count(self) -> int: """Number of unique textures loaded.""" return len(self._cache)
[docs] def destroy(self) -> None: """Cleanup (GPU resources owned by Engine, just clear cache).""" self._cache.clear()