Source code for simvx.graphics._types

"""Shared constants, numpy dtypes, and enums for SimVX Graphics."""


from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import IntFlag
from pathlib import Path
from typing import Any, NamedTuple

import numpy as np

log = logging.getLogger(__name__)

__all__ = [
    "Feature",
    "MeshHandle",
    "Viewport",
    "VERTEX_DTYPE",
    "TRANSFORM_DTYPE",
    "MATERIAL_DTYPE",
    "INDIRECT_DRAW_DTYPE",
    "LIGHT_DTYPE",
    "MAX_TEXTURES",
    "MAX_LIGHTS",
    "MAX_OBJECTS",
    "SKINNED_VERTEX_DTYPE",
    "FRAMES_IN_FLIGHT",
    "ALPHA_OPAQUE",
    "ALPHA_BLEND",
    "ALPHA_CUTOFF",
    "SHADER_DIR",
    "UI_VERTEX_STRIDE",
    # Vulkan handle type aliases
    "VkInstance",
    "VkDevice",
    "VkPhysicalDevice",
    "VkQueue",
    "VkSurfaceKHR",
    "VkRenderPass",
    "VkPipeline",
    "VkPipelineLayout",
    "VkCommandBuffer",
    "VkFramebuffer",
    "VkShaderModule",
    "VkImage",
    "VkImageView",
    "VkDeviceMemory",
    "VkDescriptorPool",
    "VkDescriptorSetLayout",
    "VkDescriptorSet",
    "VkSampler",
    "VkDebugUtilsMessengerEXT",
]

# Path to GLSL/SPIR-V shaders (packages/graphics/shaders/)
# On Android, shaders are pre-compiled and bundled in APK assets.
def _resolve_shader_dir() -> Path:
    # Standard path: packages/graphics/shaders/
    default = Path(__file__).resolve().parent.parent.parent.parent / "shaders"
    if default.is_dir():
        return default
    # Android APK assets path (extracted by p4a)
    import sys
    if hasattr(sys, "_android_apk_path"):
        apk_shaders = Path(sys._android_apk_path) / "shaders"  # type: ignore[attr-defined]
        if apk_shaders.is_dir():
            return apk_shaders
    # Fallback: check common Android extraction paths
    for candidate in [Path("/data/data") / "org.simvx.simvx" / "files" / "app" / "shaders"]:
        if candidate.is_dir():
            return candidate
    return default

SHADER_DIR = _resolve_shader_dir()

# UI/2D vertex stride: pos(vec2) + uv(vec2) + colour(vec4) = 32 bytes
UI_VERTEX_STRIDE = 32

# Alpha mode constants (match GLSL #defines)
ALPHA_OPAQUE: int = 0
ALPHA_BLEND: int = 1
ALPHA_CUTOFF: int = 2

# Limits
MAX_TEXTURES = 4096
MAX_LIGHTS = 1024
MAX_OBJECTS = 65536
FRAMES_IN_FLIGHT = 2


[docs] class MeshHandle(NamedTuple): """Opaque handle to a registered GPU mesh.""" id: int vertex_count: int index_count: int bounding_radius: float
[docs] @dataclass class Viewport: """Viewport configuration for rendering.""" x: int y: int width: int height: int camera_view: np.ndarray # 4x4 view matrix camera_proj: np.ndarray # 4x4 projection matrix render_target: Any | None = None # None = main swapchain
[docs] class Feature(IntFlag): """Material feature bitmask — matches shader #defines.""" NONE = 0 HAS_ALBEDO = 1 << 0 HAS_NORMAL = 1 << 1 HAS_METALLIC_ROUGHNESS = 1 << 2 HAS_EMISSIVE = 1 << 3 HAS_AO = 1 << 4 HAS_EMISSIVE_COLOR = 1 << 5
# Vertex format: position(vec3) + normal(vec3) + uv(vec2) = 32 bytes VERTEX_DTYPE = np.dtype( [ ("position", np.float32, 3), ("normal", np.float32, 3), ("uv", np.float32, 2), ] ) # GPU-aligned structured dtypes (match common.glsl layouts) TRANSFORM_DTYPE = np.dtype( [ ("model", np.float32, (4, 4)), # mat4 (64 bytes) ("normal_mat", np.float32, (4, 4)), # mat4 (64 bytes) ("material_index", np.uint32), # uint (4 bytes) ("_pad", np.uint32, 3), # padding to 16-byte alignment (12 bytes) ] ) # Total: 144 bytes MATERIAL_DTYPE = np.dtype( [ ("albedo", np.float32, 4), # vec4 ("metallic", np.float32), ("roughness", np.float32), ("albedo_tex", np.int32), # bindless texture index (-1 = none) ("normal_tex", np.int32), ("metallic_roughness_tex", np.int32), ("emissive_tex", np.int32), ("ao_tex", np.int32), ("features", np.uint32), # Feature bitmask ("emissive_colour", np.float32, 4), # vec4 (rgb=colour, a=intensity) ("alpha_mode", np.uint32), # 0=OPAQUE, 1=ALPHA_BLEND, 2=ALPHA_CUTOFF ("alpha_cutoff", np.float32), # cutoff threshold (used when alpha_mode==2) ("double_sided", np.uint32), # 1=disable backface culling ("_pad", np.uint32, 1), # padding to 16-byte alignment ] ) INDIRECT_DRAW_DTYPE = np.dtype( [ ("index_count", np.uint32), ("instance_count", np.uint32), ("first_index", np.uint32), ("vertex_offset", np.int32), ("first_instance", np.uint32), ] ) LIGHT_DTYPE = np.dtype( [ ("position", np.float32, 4), # vec4 (w = type: 0=dir, 1=point, 2=spot) ("direction", np.float32, 4), # vec4 ("colour", np.float32, 4), # vec4 (w = intensity) ("params", np.float32, 4), # vec4 (range, inner_cone, outer_cone, shadow_flag) ] ) # Skinned vertex: standard + joints(uvec4) + weights(vec4) = 48 bytes SKINNED_VERTEX_DTYPE = np.dtype( [ ("position", np.float32, 3), ("normal", np.float32, 3), ("uv", np.float32, 2), ("joints", np.uint16, 4), # 4 bone indices (8 bytes) ("weights", np.float32, 4), # 4 bone weights (16 bytes) ] ) # Vulkan handle type aliases — the `vulkan` CFFI bindings return opaque ffi.CData pointers. # These aliases document intent without introducing runtime dependencies on internal CFFI types. VkInstance = Any VkDevice = Any VkPhysicalDevice = Any VkQueue = Any VkSurfaceKHR = Any VkRenderPass = Any VkPipeline = Any VkPipelineLayout = Any VkCommandBuffer = Any VkFramebuffer = Any VkShaderModule = Any VkImage = Any VkImageView = Any VkDeviceMemory = Any VkDescriptorPool = Any VkDescriptorSetLayout = Any VkDescriptorSet = Any VkSampler = Any VkDebugUtilsMessengerEXT = Any