"""Offscreen render target with colour + depth attachments."""
import logging
from typing import Any
import vulkan as vk
from ..gpu.memory import create_image, transition_image_layout
from .passes import create_offscreen_pass, create_overlay_pass
log = logging.getLogger(__name__)
__all__ = ["RenderTarget"]
[docs]
class RenderTarget:
"""Manages an offscreen render target for render-to-texture.
The colour image is transitioned to ``initial_layout`` at construction so
it is safe to sample (or bind to a descriptor) before the first render
pass writes to it. The default :data:`VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`
matches the offscreen render pass's ``finalLayout``: the render pass
itself uses ``initialLayout=UNDEFINED`` with ``LOAD_OP_CLEAR``, so the
pre-transition is discarded harmlessly on the first frame.
"""
def __init__(
self,
device: Any,
physical_device: Any,
width: int,
height: int,
colour_format: int = vk.VK_FORMAT_R8G8B8A8_UNORM,
use_depth: bool = True,
samplable_depth: bool = False,
*,
initial_layout: int = vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
queue: Any = None,
command_pool: Any = None,
) -> None:
self.device = device
self.width = width
self.height = height
self.colour_format = colour_format
# Colour attachment (samplable)
self.colour_image, self.colour_memory = create_image(
device, physical_device, width, height, colour_format,
vk.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | vk.VK_IMAGE_USAGE_SAMPLED_BIT
| vk.VK_IMAGE_USAGE_STORAGE_BIT | vk.VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
)
self.colour_view = vk.vkCreateImageView(device, vk.VkImageViewCreateInfo(
image=self.colour_image,
viewType=vk.VK_IMAGE_VIEW_TYPE_2D,
format=colour_format,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0, levelCount=1,
baseArrayLayer=0, layerCount=1,
),
), None)
# Depth attachment
depth_fmt = vk.VK_FORMAT_D32_SFLOAT if use_depth else 0
if use_depth:
self.depth_image, self.depth_memory = create_image(
device, physical_device, width, height, depth_fmt,
vk.VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | vk.VK_IMAGE_USAGE_SAMPLED_BIT,
)
self.depth_view = vk.vkCreateImageView(device, vk.VkImageViewCreateInfo(
image=self.depth_image,
viewType=vk.VK_IMAGE_VIEW_TYPE_2D,
format=depth_fmt,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_DEPTH_BIT,
baseMipLevel=0, levelCount=1,
baseArrayLayer=0, layerCount=1,
),
), None)
else:
self.depth_image = self.depth_memory = self.depth_view = None
# Render pass and framebuffer
self.render_pass = create_offscreen_pass(
device, colour_format, depth_fmt, samplable_depth=samplable_depth,
)
attachments = [self.colour_view]
if self.depth_view:
attachments.append(self.depth_view)
self.framebuffer = vk.vkCreateFramebuffer(device, vk.VkFramebufferCreateInfo(
renderPass=self.render_pass,
attachmentCount=len(attachments),
pAttachments=attachments,
width=width, height=height, layers=1,
), None)
# Overlay render pass: colour-only LOAD_OP_LOAD for drawing 2D on top of 3D
self.overlay_render_pass = create_overlay_pass(device, colour_format)
self.overlay_framebuffer = vk.vkCreateFramebuffer(device, vk.VkFramebufferCreateInfo(
renderPass=self.overlay_render_pass,
attachmentCount=1,
pAttachments=[self.colour_view],
width=width, height=height, layers=1,
), None)
# One-shot transition so the colour image is in a sampler-safe layout
# before any render pass writes to it. Without this, a descriptor that
# points at this RT (e.g. a disabled bloom pass's output) and a shader
# access on the first frame trips a validation error because the image
# is still in UNDEFINED. Skipped when initial_layout=UNDEFINED (caller
# opts out) or when no queue/pool was provided (legacy callsite).
if initial_layout != vk.VK_IMAGE_LAYOUT_UNDEFINED and queue is not None and command_pool is not None:
transition_image_layout(
device, queue, command_pool, self.colour_image,
vk.VK_IMAGE_LAYOUT_UNDEFINED, initial_layout,
)
[docs]
def destroy(self) -> None:
"""Clean up all resources."""
vk.vkDestroyFramebuffer(self.device, self.overlay_framebuffer, None)
vk.vkDestroyRenderPass(self.device, self.overlay_render_pass, None)
vk.vkDestroyFramebuffer(self.device, self.framebuffer, None)
vk.vkDestroyRenderPass(self.device, self.render_pass, None)
if self.depth_view:
vk.vkDestroyImageView(self.device, self.depth_view, None)
if self.depth_image:
vk.vkDestroyImage(self.device, self.depth_image, None)
if self.depth_memory:
vk.vkFreeMemory(self.device, self.depth_memory, None)
vk.vkDestroyImageView(self.device, self.colour_view, None)
vk.vkDestroyImage(self.device, self.colour_image, None)
vk.vkFreeMemory(self.device, self.colour_memory, None)