"""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