"""Load cubemap textures from 6 face images or equirectangular HDR."""
from __future__ import annotations
import logging
from typing import Any
import numpy as np
import vulkan as vk
from ..gpu.memory import create_sampler
__all__ = ["load_cubemap"]
log = logging.getLogger(__name__)
[docs]
def load_cubemap(
device: Any,
physical_device: Any,
queue: Any,
cmd_pool: Any,
face_paths: list[str] | None = None,
colour: tuple[float, float, float] | None = None,
) -> tuple[Any, Any, Any, Any]:
"""Load a cubemap from 6 face images or a solid colour.
Args:
face_paths: List of 6 image paths [+X, -X, +Y, -Y, +Z, -Z].
colour: Solid colour (r, g, b) in 0-1 range (fallback if no paths).
Returns:
(image_view, sampler, image, memory) tuple for the cubemap.
"""
ffi = vk.ffi
if face_paths and len(face_paths) == 6:
faces, width, height = _load_face_images(face_paths)
else:
# Solid colour fallback
c = colour or (0.2, 0.3, 0.5)
width, height = 64, 64
pixel = np.array([c[0], c[1], c[2], 1.0], dtype=np.float32)
face_data = np.tile(pixel, width * height).astype(np.float32)
faces = [face_data.tobytes() for _ in range(6)]
fmt = vk.VK_FORMAT_R32G32B32A32_SFLOAT
pixel_size = 16 # 4 floats * 4 bytes
# Create cubemap image
image_ci = ffi.new("VkImageCreateInfo*")
image_ci.sType = vk.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO
image_ci.imageType = vk.VK_IMAGE_TYPE_2D
image_ci.format = fmt
image_ci.extent.width = width
image_ci.extent.height = height
image_ci.extent.depth = 1
image_ci.mipLevels = 1
image_ci.arrayLayers = 6
image_ci.samples = vk.VK_SAMPLE_COUNT_1_BIT
image_ci.tiling = vk.VK_IMAGE_TILING_OPTIMAL
image_ci.usage = vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT | vk.VK_IMAGE_USAGE_SAMPLED_BIT
image_ci.sharingMode = vk.VK_SHARING_MODE_EXCLUSIVE
image_ci.initialLayout = vk.VK_IMAGE_LAYOUT_UNDEFINED
image_ci.flags = vk.VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
image_out = ffi.new("VkImage*")
result = vk._vulkan._callApi(
vk._vulkan.lib.vkCreateImage,
device,
image_ci,
ffi.NULL,
image_out,
)
if result != vk.VK_SUCCESS:
raise RuntimeError(f"vkCreateImage failed: {result}")
image = image_out[0]
# Allocate and bind memory
mem_req = vk.vkGetImageMemoryRequirements(device, image)
from ..gpu.memory import _find_memory_type
mem_type = _find_memory_type(physical_device, mem_req.memoryTypeBits, vk.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
alloc_info = vk.VkMemoryAllocateInfo(
allocationSize=mem_req.size,
memoryTypeIndex=mem_type,
)
memory = vk.vkAllocateMemory(device, alloc_info, None)
vk.vkBindImageMemory(device, image, memory, 0)
# Upload face data via staging buffer
face_size = width * height * pixel_size
from ..gpu.memory import create_buffer, upload_numpy
staging_buf, staging_mem = create_buffer(
device,
physical_device,
face_size,
vk.VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
vk.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | vk.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
)
# Transition to TRANSFER_DST, copy each face, transition to SHADER_READ
cmd_ai = vk.VkCommandBufferAllocateInfo(
commandPool=cmd_pool,
level=vk.VK_COMMAND_BUFFER_LEVEL_PRIMARY,
commandBufferCount=1,
)
cmds = vk.vkAllocateCommandBuffers(device, cmd_ai)
cmd = cmds[0]
vk.vkBeginCommandBuffer(
cmd,
vk.VkCommandBufferBeginInfo(
flags=vk.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
),
)
# Transition entire cubemap to TRANSFER_DST
barrier = vk.VkImageMemoryBarrier(
srcAccessMask=0,
dstAccessMask=vk.VK_ACCESS_TRANSFER_WRITE_BIT,
oldLayout=vk.VK_IMAGE_LAYOUT_UNDEFINED,
newLayout=vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
image=image,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0,
levelCount=1,
baseArrayLayer=0,
layerCount=6,
),
)
vk.vkCmdPipelineBarrier(
cmd, vk.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, vk.VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, None, 0, None, 1, [barrier]
)
for face_idx in range(6):
# Upload face to staging
face_bytes = faces[face_idx]
if isinstance(face_bytes, np.ndarray):
face_bytes = face_bytes.tobytes()
upload_numpy(device, staging_mem, np.frombuffer(face_bytes, dtype=np.uint8))
# Copy staging → cubemap face
region = vk.VkBufferImageCopy(
bufferOffset=0,
bufferRowLength=0,
bufferImageHeight=0,
imageSubresource=vk.VkImageSubresourceLayers(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
mipLevel=0,
baseArrayLayer=face_idx,
layerCount=1,
),
imageOffset=vk.VkOffset3D(x=0, y=0, z=0),
imageExtent=vk.VkExtent3D(width=width, height=height, depth=1),
)
vk.vkCmdCopyBufferToImage(cmd, staging_buf, image, vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, [region])
# Transition to SHADER_READ
barrier2 = vk.VkImageMemoryBarrier(
srcAccessMask=vk.VK_ACCESS_TRANSFER_WRITE_BIT,
dstAccessMask=vk.VK_ACCESS_SHADER_READ_BIT,
oldLayout=vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
newLayout=vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
image=image,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0,
levelCount=1,
baseArrayLayer=0,
layerCount=6,
),
)
vk.vkCmdPipelineBarrier(
cmd,
vk.VK_PIPELINE_STAGE_TRANSFER_BIT,
vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0,
0,
None,
0,
None,
1,
[barrier2],
)
vk.vkEndCommandBuffer(cmd)
vk.vkQueueSubmit(
queue,
1,
[
vk.VkSubmitInfo(
commandBufferCount=1,
pCommandBuffers=[cmd],
)
],
None,
)
vk.vkQueueWaitIdle(queue)
vk.vkFreeCommandBuffers(device, cmd_pool, 1, [cmd])
# Cleanup staging
vk.vkDestroyBuffer(device, staging_buf, None)
vk.vkFreeMemory(device, staging_mem, None)
# Create cubemap image view
view = vk.vkCreateImageView(
device,
vk.VkImageViewCreateInfo(
image=image,
viewType=vk.VK_IMAGE_VIEW_TYPE_CUBE,
format=fmt,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0,
levelCount=1,
baseArrayLayer=0,
layerCount=6,
),
),
None,
)
sampler = create_sampler(device)
log.debug("Cubemap loaded (%dx%d)", width, height)
return view, sampler, image, memory
def _load_face_images(paths: list[str]) -> tuple[list[bytes], int, int]:
"""Load 6 face images and return (face_data_list, width, height)."""
try:
from PIL import Image
except ImportError:
raise ImportError("Pillow is required for cubemap face loading: pip install Pillow") from None
faces = []
width = height = 0
for path in paths:
img = Image.open(path).convert("RGBA")
if width == 0:
width, height = img.size
else:
img = img.resize((width, height))
# Convert to float32 RGBA
data = np.array(img, dtype=np.float32) / 255.0
faces.append(data.tobytes())
return faces, width, height