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()