"""Reflection-probe capture: desktop (Vulkan) backend for ``ReflectionProbe3D``.
Captures a local environment cubemap per probe and feeds the engine's
existing split-sum IBL precompute (irradiance + GGX-prefiltered specular,
:class:`IBLPass`) so meshes inside a probe's influence box pick up the local
room reflection instead of the global skybox IBL.
Pipeline (per probe, run **once** on enter + on explicit ``request_update()``):
1. Render the scene six times from the probe's ``capture_position`` into a
square offscreen target (``GameViewportRenderer``), one per cube face, with a
90° FOV perspective camera. After each face, ``vkCmdCopyImage`` the colour
image into face *f* of a per-probe source cubemap.
2. Run :meth:`IBLPass.process_cubemap` on that source cube → irradiance +
prefiltered specular for this probe (reusing the global IBL compute shaders).
3. ``vkCmdCopyImage`` the probe's irradiance cube (6 layers) and prefiltered
cube (6 layers × N mips) into the shared cubemap **arrays** at the probe's
array slice (slice = ``probe_index * 6``).
The shared arrays + a probe-box SSBO are bound to the forward set (bindings
10/11/12). The fragment shader tests each fragment's world position against the
probe boxes (bounded loop, ``MAX_PROBES``) and, when inside, samples that
probe's maps (optionally box-projected) instead of the global IBL.
Capture uses its own one-time, waited command buffers: it is decoupled from
the per-frame render loop and never iterates draws in Python on the hot path.
"""
from __future__ import annotations
import logging
from typing import Any
import numpy as np
import vulkan as vk
from ..gpu.memory import begin_single_time_commands, end_single_time_commands
from .buffer_manager import MAX_PROBES, PROBE_BUFFER_SIZE
from .game_viewport import GameViewportRenderer
from .ibl_pass import IRRADIANCE_SIZE, PREFILTER_MIP_LEVELS, PREFILTER_SIZE, IBLPass
from .render_target import RenderTarget # noqa: F401 (referenced in docstring / type intent)
__all__ = ["ReflectionProbePass"]
log = logging.getLogger(__name__)
# Per-face capture resolution. Small: the IBL convolution downsamples to
# 32×32 irradiance / 128×128 prefilter anyway, so a high-res face buys nothing.
FACE_SIZE = 128
_CUBE_FORMAT = vk.VK_FORMAT_R16G16B16A16_SFLOAT
# Six cube-face look directions (+X, -X, +Y, -Y, +Z, -Z) and up vectors,
# matching the standard Vulkan/GL cubemap face ordering used by the skybox
# and IBL shaders. forward/up are world-space.
_FACE_DIRS = [
((1.0, 0.0, 0.0), (0.0, -1.0, 0.0)), # +X
((-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)), # -X
((0.0, 1.0, 0.0), (0.0, 0.0, 1.0)), # +Y
((0.0, -1.0, 0.0), (0.0, 0.0, -1.0)), # -Y
((0.0, 0.0, 1.0), (0.0, -1.0, 0.0)), # +Z
((0.0, 0.0, -1.0), (0.0, -1.0, 0.0)), # -Z
]
class _FaceCamera:
"""Lightweight duck-typed camera for one cube face.
Satisfies the slice of the camera protocol that ``SceneAdapter.submit_scene``
reads: ``view_matrix`` (property), ``projection_matrix(aspect)``,
``cull_mask``, and ``_visible_in_hierarchy``. Built per-face from the probe's
world capture position so we never mutate a real scene camera.
"""
def __init__(self, eye: np.ndarray, forward: tuple, up: tuple, near: float, far: float, cull_mask: int):
from simvx.core.math.matrices import look_at, perspective
self._eye = np.asarray(eye, dtype=np.float32)
self._fwd = np.asarray(forward, dtype=np.float32)
self._up = np.asarray(up, dtype=np.float32)
self._near = near
self._far = far
self.cull_mask = cull_mask
self._visible_in_hierarchy = True
self._look_at = look_at
self._perspective = perspective
@property
def view_matrix(self) -> np.ndarray:
return self._look_at(self._eye, self._eye + self._fwd, self._up)
def projection_matrix(self, aspect: float = 1.0) -> np.ndarray:
import math
proj = self._perspective(math.radians(90.0), aspect, self._near, self._far)
proj[1, 1] *= -1 # Vulkan Y-flip (matches Camera3D.projection_matrix)
return proj
[docs]
class ReflectionProbePass:
"""Owns the shared probe cubemap arrays + box SSBO and drives per-probe capture."""
def __init__(self, engine: Any) -> None:
self._engine = engine
self._ready = False
# Shared cubemap arrays (MAX_PROBES * 6 layers each).
self._irr_image: Any = None
self._irr_memory: Any = None
self._irr_view: Any = None
self._pre_image: Any = None
self._pre_memory: Any = None
self._pre_view: Any = None
self._sampler: Any = None
# Per-probe source cube (reused across probes) + offscreen face target.
self._src_image: Any = None
self._src_memory: Any = None
self._src_view: Any = None
self._src_sampler: Any = None
self._face_target: GameViewportRenderer | None = None
self._ibl: IBLPass | None = None
# Probe id -> assigned array slot (0..MAX_PROBES-1).
self._slots: dict[int, int] = {}
# Last uploaded probe-box payload hash (skip redundant SSBO uploads).
self._box_hash: int | None = None
# ------------------------------------------------------------------ setup
[docs]
def setup(self) -> None:
"""Allocate the shared cubemap arrays + capture scratch resources."""
e = self._engine
device = e.ctx.device
phys = e.ctx.physical_device
self._irr_image, self._irr_memory, self._irr_view = self._create_cube_array(
device, phys, IRRADIANCE_SIZE, 1,
)
self._pre_image, self._pre_memory, self._pre_view = self._create_cube_array(
device, phys, PREFILTER_SIZE, PREFILTER_MIP_LEVELS,
)
self._sampler = vk.vkCreateSampler(device, vk.VkSamplerCreateInfo(
magFilter=vk.VK_FILTER_LINEAR, minFilter=vk.VK_FILTER_LINEAR,
mipmapMode=vk.VK_SAMPLER_MIPMAP_MODE_LINEAR,
addressModeU=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeV=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeW=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
minLod=0.0, maxLod=float(PREFILTER_MIP_LEVELS),
), None)
self._src_sampler = vk.vkCreateSampler(device, vk.VkSamplerCreateInfo(
magFilter=vk.VK_FILTER_LINEAR, minFilter=vk.VK_FILTER_LINEAR,
mipmapMode=vk.VK_SAMPLER_MIPMAP_MODE_LINEAR,
addressModeU=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeV=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeW=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
), None)
# Transition both arrays to SHADER_READ_ONLY so the descriptor is valid
# before any probe captures (the shader's probe_count gate skips them).
self._transition_array(self._irr_image, 1,
vk.VK_IMAGE_LAYOUT_UNDEFINED,
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
self._transition_array(self._pre_image, PREFILTER_MIP_LEVELS,
vk.VK_IMAGE_LAYOUT_UNDEFINED,
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
self._ready = True
log.debug("ReflectionProbePass initialised (max_probes=%d)", MAX_PROBES)
def _create_cube_array(self, device: Any, phys: Any, size: int, mips: int) -> tuple[Any, Any, Any]:
"""Create a CUBE_ARRAY image (MAX_PROBES*6 layers) + a CUBE_ARRAY view."""
from ..gpu.memory import _find_memory_type
ffi = vk.ffi
ci = ffi.new("VkImageCreateInfo*")
ci.sType = vk.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO
ci.imageType = vk.VK_IMAGE_TYPE_2D
ci.format = _CUBE_FORMAT
ci.extent.width = size
ci.extent.height = size
ci.extent.depth = 1
ci.mipLevels = mips
ci.arrayLayers = MAX_PROBES * 6
ci.samples = vk.VK_SAMPLE_COUNT_1_BIT
ci.tiling = vk.VK_IMAGE_TILING_OPTIMAL
ci.usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT | vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT
ci.sharingMode = vk.VK_SHARING_MODE_EXCLUSIVE
ci.initialLayout = vk.VK_IMAGE_LAYOUT_UNDEFINED
ci.flags = vk.VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
img_out = ffi.new("VkImage*")
if vk._vulkan._callApi(vk._vulkan.lib.vkCreateImage, device, ci, ffi.NULL, img_out) != vk.VK_SUCCESS:
raise RuntimeError("vkCreateImage (probe cube array) failed")
image = img_out[0]
req = vk.vkGetImageMemoryRequirements(device, image)
mem = vk.vkAllocateMemory(device, vk.VkMemoryAllocateInfo(
allocationSize=req.size,
memoryTypeIndex=_find_memory_type(phys, req.memoryTypeBits, vk.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
), None)
vk.vkBindImageMemory(device, image, mem, 0)
view = vk.vkCreateImageView(device, vk.VkImageViewCreateInfo(
image=image,
viewType=vk.VK_IMAGE_VIEW_TYPE_CUBE_ARRAY,
format=_CUBE_FORMAT,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0, levelCount=mips,
baseArrayLayer=0, layerCount=MAX_PROBES * 6,
),
), None)
return image, mem, view
# --------------------------------------------------------------- accessors
[docs]
def get_irradiance_array_view(self) -> Any:
return self._irr_view
[docs]
def get_prefilter_array_view(self) -> Any:
return self._pre_view
[docs]
def get_sampler(self) -> Any:
return self._sampler
# ----------------------------------------------------------------- capture
[docs]
def update_probes(self, adapter: Any, tree: Any, probes: list) -> bool:
"""Capture any probe that is new or has ``_update_requested`` set.
Returns ``True`` if at least one probe captured this frame (the caller
must then re-submit the main scene, since face rendering clobbers the
renderer's per-frame submission lists: same contract as SubViewports).
"""
if not self._ready or adapter is None or tree is None:
return False
# Bound the active set to MAX_PROBES: no closest-to-origin priority;
# the first MAX_PROBES in tree order win.
active = probes[:MAX_PROBES]
# (Re)assign slots for the active set; drop stale ones.
live_ids = {id(p) for p in active}
self._slots = {pid: s for pid, s in self._slots.items() if pid in live_ids}
for p in active:
if id(p) not in self._slots:
self._slots[id(p)] = self._next_free_slot()
captured = False
for probe in active:
need = getattr(probe, "_update_requested", False) or getattr(probe, "_cubemap_version", 0) == 0
if need:
self._capture_probe(adapter, tree, probe, self._slots[id(probe)])
probe._update_requested = False
probe._cubemap_version = getattr(probe, "_cubemap_version", 0) + 1
sig = getattr(probe, "cubemap_updated", None)
if sig is not None:
sig.emit()
captured = True
self._upload_boxes(active)
return captured
def _next_free_slot(self) -> int:
used = set(self._slots.values())
for s in range(MAX_PROBES):
if s not in used:
return s
return 0 # full: reuse slot 0 (bounded by MAX_PROBES anyway)
def _capture_probe(self, adapter: Any, tree: Any, probe: Any, slot: int) -> None:
"""Render 6 faces from *probe*, run IBL precompute, copy into array *slot*."""
e = self._engine
device = e.ctx.device
self._ensure_capture_scratch()
eye = np.asarray(probe.capture_position, dtype=np.float32)
near, far = 0.05, max(2.0, float(max(probe.size)) * 4.0 + 50.0)
cull = int(getattr(probe, "cull_mask", 0xFFFFFFFF))
# --- 1. Render the six faces into the source cube ---
cmd = begin_single_time_commands(device, e.ctx.command_pool)
# Source cube: UNDEFINED -> TRANSFER_DST for the per-face copies.
self._barrier(cmd, self._src_image, 6, 1,
vk.VK_IMAGE_LAYOUT_UNDEFINED, vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
0, vk.VK_ACCESS_TRANSFER_WRITE_BIT,
vk.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, vk.VK_PIPELINE_STAGE_TRANSFER_BIT)
for face, (fwd, up) in enumerate(_FACE_DIRS):
cam = _FaceCamera(eye, fwd, up, near, far, cull)
adapter.render_to_target(cmd, self._face_target, tree, camera=cam)
# The offscreen pass leaves colour in SHADER_READ_ONLY; move it to
# TRANSFER_SRC, copy into source-cube face `face`, then back so the
# next face render's pass (initialLayout=UNDEFINED) is happy.
ct = self._face_target._target.colour_image
self._barrier(cmd, ct, 1, 1,
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
vk.VK_ACCESS_SHADER_READ_BIT, vk.VK_ACCESS_TRANSFER_READ_BIT,
vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, vk.VK_PIPELINE_STAGE_TRANSFER_BIT)
self._copy_face(cmd, ct, self._src_image, dst_layer=face, size=FACE_SIZE)
self._barrier(cmd, ct, 1, 1,
vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
vk.VK_ACCESS_TRANSFER_READ_BIT, vk.VK_ACCESS_SHADER_READ_BIT,
vk.VK_PIPELINE_STAGE_TRANSFER_BIT, vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
# Source cube -> SHADER_READ_ONLY so IBLPass can sample it.
self._barrier(cmd, self._src_image, 6, 1,
vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
vk.VK_ACCESS_TRANSFER_WRITE_BIT, vk.VK_ACCESS_SHADER_READ_BIT,
vk.VK_PIPELINE_STAGE_TRANSFER_BIT, vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
end_single_time_commands(device, e.ctx.graphics_queue, e.ctx.command_pool, cmd)
# --- 2. IBL precompute on the source cube (own one-time cmd) ---
self._ibl.process_cubemap(self._src_view, self._src_sampler)
# --- 3. Copy IBL outputs into the shared arrays at this slot ---
cmd = begin_single_time_commands(device, e.ctx.command_pool)
base = slot * 6
self._copy_cube_into_array(cmd, self._ibl.get_irradiance_image(), self._irr_image,
IRRADIANCE_SIZE, 1, base)
self._copy_cube_into_array(cmd, self._ibl.get_prefiltered_image(), self._pre_image,
PREFILTER_SIZE, PREFILTER_MIP_LEVELS, base)
end_single_time_commands(device, e.ctx.graphics_queue, e.ctx.command_pool, cmd)
log.debug("Reflection probe captured into slot %d", slot)
def _ensure_capture_scratch(self) -> None:
"""Lazily create the offscreen face target, source cube, and IBLPass."""
if self._face_target is not None:
return
e = self._engine
device = e.ctx.device
phys = e.ctx.physical_device
self._face_target = GameViewportRenderer(e)
self._face_target.create(FACE_SIZE, FACE_SIZE)
# Source cube (6 layers, sampled by IBLPass; transfer-dst from faces).
from ..gpu.memory import _find_memory_type
ffi = vk.ffi
ci = ffi.new("VkImageCreateInfo*")
ci.sType = vk.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO
ci.imageType = vk.VK_IMAGE_TYPE_2D
ci.format = _CUBE_FORMAT
ci.extent.width = FACE_SIZE
ci.extent.height = FACE_SIZE
ci.extent.depth = 1
ci.mipLevels = 1
ci.arrayLayers = 6
ci.samples = vk.VK_SAMPLE_COUNT_1_BIT
ci.tiling = vk.VK_IMAGE_TILING_OPTIMAL
ci.usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT | vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT
ci.sharingMode = vk.VK_SHARING_MODE_EXCLUSIVE
ci.initialLayout = vk.VK_IMAGE_LAYOUT_UNDEFINED
ci.flags = vk.VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
img_out = ffi.new("VkImage*")
if vk._vulkan._callApi(vk._vulkan.lib.vkCreateImage, device, ci, ffi.NULL, img_out) != vk.VK_SUCCESS:
raise RuntimeError("vkCreateImage (probe source cube) failed")
self._src_image = img_out[0]
req = vk.vkGetImageMemoryRequirements(device, self._src_image)
self._src_memory = vk.vkAllocateMemory(device, vk.VkMemoryAllocateInfo(
allocationSize=req.size,
memoryTypeIndex=_find_memory_type(phys, req.memoryTypeBits, vk.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
), None)
vk.vkBindImageMemory(device, self._src_image, self._src_memory, 0)
self._src_view = vk.vkCreateImageView(device, vk.VkImageViewCreateInfo(
image=self._src_image,
viewType=vk.VK_IMAGE_VIEW_TYPE_CUBE,
format=_CUBE_FORMAT,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0, levelCount=1, baseArrayLayer=0, layerCount=6,
),
), None)
self._ibl = IBLPass(e)
self._ibl.setup()
# ------------------------------------------------------------- box SSBO
def _upload_boxes(self, probes: list) -> None:
"""Build + upload the probe-box SSBO (count header + Probe array)."""
data = bytearray(PROBE_BUFFER_SIZE)
count = min(len(probes), MAX_PROBES)
data[0:4] = np.array([count], dtype=np.uint32).tobytes()
for probe in probes[:count]:
slot = self._slots[id(probe)]
centre = np.asarray(probe.capture_position, dtype=np.float32)
half = np.asarray(probe.size, dtype=np.float32)
intensity = float(getattr(probe, "intensity", 1.0))
box_proj = 1.0 if getattr(probe, "box_projection", False) else 0.0
off = 16 + slot * 48
# vec4 centre_slice (xyz centre, w = array slice as float)
data[off:off + 16] = np.array([centre[0], centre[1], centre[2], float(slot * 6)],
dtype=np.float32).tobytes()
# vec4 half_extent_intensity (xyz half extents, w = intensity)
data[off + 16:off + 32] = np.array([half[0], half[1], half[2], intensity],
dtype=np.float32).tobytes()
# vec4 flags (x = box_projection, yzw reserved)
data[off + 32:off + 48] = np.array([box_proj, 0.0, 0.0, 0.0], dtype=np.float32).tobytes()
payload = np.frombuffer(bytes(data), dtype=np.uint8)
h = hash(payload.tobytes())
if h == self._box_hash:
return
self._box_hash = h
self._engine.renderer._buffers.write_probe_buffer(payload)
# --------------------------------------------------------- Vulkan helpers
def _copy_face(self, cmd: Any, src_image: Any, dst_cube: Any, dst_layer: int, size: int) -> None:
"""Copy a 2D colour image into one layer of a cube image."""
region = vk.VkImageCopy(
srcSubresource=vk.VkImageSubresourceLayers(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, mipLevel=0, baseArrayLayer=0, layerCount=1),
srcOffset=vk.VkOffset3D(x=0, y=0, z=0),
dstSubresource=vk.VkImageSubresourceLayers(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, mipLevel=0, baseArrayLayer=dst_layer, layerCount=1),
dstOffset=vk.VkOffset3D(x=0, y=0, z=0),
extent=vk.VkExtent3D(width=size, height=size, depth=1),
)
vk.vkCmdCopyImage(
cmd,
src_image, vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dst_cube, vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, [region],
)
def _copy_cube_into_array(self, cmd: Any, src_cube: Any, dst_array: Any,
size: int, mips: int, base_layer: int) -> None:
"""Copy a 6-layer cube (all mips) into the array at ``base_layer``.
IBLPass leaves its outputs in SHADER_READ_ONLY; transition src->TRANSFER_SRC
and the destination slice->TRANSFER_DST, copy each mip, then restore both.
"""
self._barrier(cmd, src_cube, 6, mips,
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
vk.VK_ACCESS_SHADER_READ_BIT, vk.VK_ACCESS_TRANSFER_READ_BIT,
vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, vk.VK_PIPELINE_STAGE_TRANSFER_BIT)
self._barrier(cmd, dst_array, 6, mips,
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
vk.VK_ACCESS_SHADER_READ_BIT, vk.VK_ACCESS_TRANSFER_WRITE_BIT,
vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, vk.VK_PIPELINE_STAGE_TRANSFER_BIT,
base_layer=base_layer)
regions = []
for mip in range(mips):
msize = max(1, size >> mip)
regions.append(vk.VkImageCopy(
srcSubresource=vk.VkImageSubresourceLayers(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, mipLevel=mip, baseArrayLayer=0, layerCount=6),
srcOffset=vk.VkOffset3D(x=0, y=0, z=0),
dstSubresource=vk.VkImageSubresourceLayers(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, mipLevel=mip,
baseArrayLayer=base_layer, layerCount=6),
dstOffset=vk.VkOffset3D(x=0, y=0, z=0),
extent=vk.VkExtent3D(width=msize, height=msize, depth=1),
))
vk.vkCmdCopyImage(
cmd,
src_cube, vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dst_array, vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
len(regions), regions,
)
self._barrier(cmd, src_cube, 6, mips,
vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
vk.VK_ACCESS_TRANSFER_READ_BIT, vk.VK_ACCESS_SHADER_READ_BIT,
vk.VK_PIPELINE_STAGE_TRANSFER_BIT, vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
self._barrier(cmd, dst_array, 6, mips,
vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
vk.VK_ACCESS_TRANSFER_WRITE_BIT, vk.VK_ACCESS_SHADER_READ_BIT,
vk.VK_PIPELINE_STAGE_TRANSFER_BIT, vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
base_layer=base_layer)
def _barrier(self, cmd: Any, image: Any, layer_count: int, mips: int,
old: int, new: int, src_access: int, dst_access: int,
src_stage: int, dst_stage: int, base_layer: int = 0) -> None:
barrier = vk.VkImageMemoryBarrier(
srcAccessMask=src_access, dstAccessMask=dst_access,
oldLayout=old, newLayout=new,
srcQueueFamilyIndex=vk.VK_QUEUE_FAMILY_IGNORED,
dstQueueFamilyIndex=vk.VK_QUEUE_FAMILY_IGNORED,
image=image,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0, levelCount=mips,
baseArrayLayer=base_layer, layerCount=layer_count,
),
)
vk.vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, None, 0, None, 1, [barrier])
def _transition_array(self, image: Any, mips: int, old: int, new: int) -> None:
"""One-shot full-array layout transition (init)."""
e = self._engine
cmd = begin_single_time_commands(e.ctx.device, e.ctx.command_pool)
self._barrier(cmd, image, MAX_PROBES * 6, mips, old, new,
0, vk.VK_ACCESS_SHADER_READ_BIT,
vk.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
end_single_time_commands(e.ctx.device, e.ctx.graphics_queue, e.ctx.command_pool, cmd)
# ----------------------------------------------------------------- cleanup
[docs]
def cleanup(self) -> None:
if not self._ready:
return
device = self._engine.ctx.device
vk.vkDeviceWaitIdle(device)
if self._ibl is not None:
self._ibl.cleanup()
self._ibl = None
if self._face_target is not None:
self._face_target.destroy()
self._face_target = None
for view, img, mem in (
(self._src_view, self._src_image, self._src_memory),
(self._irr_view, self._irr_image, self._irr_memory),
(self._pre_view, self._pre_image, self._pre_memory),
):
if view:
vk.vkDestroyImageView(device, view, None)
if img:
vk.vkDestroyImage(device, img, None)
if mem:
vk.vkFreeMemory(device, mem, None)
for sampler in (self._sampler, self._src_sampler):
if sampler:
vk.vkDestroySampler(device, sampler, None)
self._ready = False