9d2b167f70
Add a RenderTarget resource so the scene can render offscreen instead of straight to the swapchain. The editor uses it to show the scene inside the Viewport panel. - Rename AssetRegistry to ResourceRegistry (AssetTraits -> ResourceTraits, assets() -> resources()) and move the pools from engine/src/assets to engine/src/resources. It now covers non-asset GPU resources as well. - Replace loadMesh() with create<T>(...), and add create<RenderTarget>(), requestResize(), getExtent() and isAlive(). - Add RenderTarget and RenderTargetPool with RenderTargetHandle. Resizes are deferred and applied at the start of the next frame, when the GPU is idle. - Add Application::setSceneTarget(). With no target set, the scene still renders to the swapchain. With a target set, the scene renders offscreen and the swapchain receives only the UI. Frame extent and camera projection follow the target's size. - Add sophia::UI (Application::ui()) with image() and getTextureId() to draw a render target in an ImGui window, sizing the target to the available space. ImGuiRenderer now registers the target textures for this. - ViewportPanel takes the UI and a target, and draws the scene image in place of the flat background fill. - Update the testbed, the boids example, and the glossary for the rename.
343 lines
13 KiB
C++
343 lines
13 KiB
C++
#include "image.hpp"
|
|
|
|
#include <cstring>
|
|
#include <sophia/util/logger.hpp>
|
|
#include <stdexcept>
|
|
|
|
#include "device.hpp"
|
|
|
|
namespace sophia {
|
|
|
|
void Image::transitionLayout(vk::CommandBuffer commandBuffer,
|
|
vk::ImageLayout newLayout) {
|
|
vk::ImageMemoryBarrier barrier{};
|
|
barrier.oldLayout = this->layout;
|
|
barrier.newLayout = newLayout;
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.image = this->get();
|
|
barrier.subresourceRange.aspectMask = this->aspectMask;
|
|
barrier.subresourceRange.baseMipLevel = 0;
|
|
barrier.subresourceRange.levelCount = this->mipLevels;
|
|
barrier.subresourceRange.baseArrayLayer = 0;
|
|
barrier.subresourceRange.layerCount = this->arrayLayers;
|
|
|
|
vk::PipelineStageFlags srcStage;
|
|
vk::PipelineStageFlags dstStage;
|
|
|
|
if (this->layout == vk::ImageLayout::eUndefined &&
|
|
newLayout == vk::ImageLayout::eTransferDstOptimal) {
|
|
barrier.srcAccessMask = {};
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
|
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
|
dstStage = vk::PipelineStageFlagBits::eTransfer;
|
|
} else if (this->layout == vk::ImageLayout::eTransferDstOptimal &&
|
|
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
|
|
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
|
|
srcStage = vk::PipelineStageFlagBits::eTransfer;
|
|
dstStage = vk::PipelineStageFlagBits::eFragmentShader;
|
|
} else if (this->layout == vk::ImageLayout::eUndefined &&
|
|
newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
|
|
barrier.srcAccessMask = {};
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead |
|
|
vk::AccessFlagBits::eDepthStencilAttachmentWrite;
|
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
|
dstStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
|
|
} else if (this->layout == vk::ImageLayout::eUndefined &&
|
|
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
|
|
barrier.srcAccessMask = {};
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
|
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
} else if (this->layout == vk::ImageLayout::eUndefined &&
|
|
newLayout == vk::ImageLayout::eGeneral) {
|
|
barrier.srcAccessMask = {};
|
|
barrier.dstAccessMask =
|
|
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
|
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
|
dstStage = vk::PipelineStageFlagBits::eComputeShader;
|
|
} else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal &&
|
|
newLayout == vk::ImageLayout::ePresentSrcKHR) {
|
|
barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
|
barrier.dstAccessMask = {};
|
|
srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
dstStage = vk::PipelineStageFlagBits::eBottomOfPipe;
|
|
} else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal &&
|
|
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
|
|
barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
|
|
srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
dstStage = vk::PipelineStageFlagBits::eFragmentShader;
|
|
} else if (this->layout == vk::ImageLayout::eShaderReadOnlyOptimal &&
|
|
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
|
|
barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead;
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
|
srcStage = vk::PipelineStageFlagBits::eFragmentShader;
|
|
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
} else if (this->layout == vk::ImageLayout::ePresentSrcKHR &&
|
|
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
|
|
barrier.srcAccessMask = {};
|
|
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
|
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
} else {
|
|
log::fatal("Unsupported image layout transition.");
|
|
throw std::runtime_error("Unsupported image layout transition.");
|
|
}
|
|
|
|
commandBuffer.pipelineBarrier(srcStage, dstStage, {}, nullptr, nullptr,
|
|
barrier);
|
|
|
|
this->layout = newLayout;
|
|
}
|
|
|
|
void Image::transitionLayoutImmediate(vk::ImageLayout newLayout) {
|
|
vk::CommandBuffer commandBuffer = this->device.beginSingleTimeCommands();
|
|
this->transitionLayout(commandBuffer, newLayout);
|
|
this->device.endSingleTimeCommands(commandBuffer);
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setExtent(u32 width, u32 height, u32 depth) {
|
|
this->extent = vk::Extent3D{width, height, depth};
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setFormat(vk::Format format) {
|
|
this->format = format;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setUsage(vk::ImageUsageFlags usage) {
|
|
this->usage = usage;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setTiling(vk::ImageTiling tiling) {
|
|
this->tiling = tiling;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setMemoryProperties(
|
|
vk::MemoryPropertyFlags properties) {
|
|
this->memoryProperties = properties;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setSamples(vk::SampleCountFlagBits samples) {
|
|
this->samples = samples;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setImageType(vk::ImageType imageType) {
|
|
this->imageType = imageType;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setMipLevels(u32 mipLevels) {
|
|
this->mipLevels = mipLevels;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setArrayLayers(u32 arrayLayers) {
|
|
this->arrayLayers = arrayLayers;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setAspectMask(vk::ImageAspectFlags aspectMask) {
|
|
this->aspectMask = aspectMask;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::asComputeStorage() {
|
|
// Storage images commonly can't use sRGB formats without an extension, so
|
|
// default to a plain UNORM format; override with setFormat() if needed.
|
|
this->format = vk::Format::eR8G8B8A8Unorm;
|
|
this->usage = vk::ImageUsageFlagBits::eStorage;
|
|
this->aspectMask = vk::ImageAspectFlagBits::eColor;
|
|
this->tiling = vk::ImageTiling::eOptimal;
|
|
this->memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::asColorAttachment() {
|
|
this->usage = vk::ImageUsageFlagBits::eColorAttachment |
|
|
vk::ImageUsageFlagBits::eSampled;
|
|
this->aspectMask = vk::ImageAspectFlagBits::eColor;
|
|
this->tiling = vk::ImageTiling::eOptimal;
|
|
this->memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::asDepthAttachment() {
|
|
this->format = vk::Format::eD32Sfloat;
|
|
this->usage = vk::ImageUsageFlagBits::eDepthStencilAttachment;
|
|
this->aspectMask = vk::ImageAspectFlagBits::eDepth;
|
|
this->tiling = vk::ImageTiling::eOptimal;
|
|
this->memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::wrapExisting(vk::Image image) {
|
|
this->isExternalImage = true;
|
|
this->externalImage = image;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::setInitialData(const void* data,
|
|
vk::DeviceSize size) {
|
|
this->initialData = data;
|
|
this->initialDataSize = size;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::withView(bool enabled) {
|
|
this->createView = enabled;
|
|
return *this;
|
|
}
|
|
|
|
Image::Builder& Image::Builder::withSampler(
|
|
vk::Filter filter, vk::SamplerAddressMode addressMode) {
|
|
this->createSampler = true;
|
|
this->samplerFilter = filter;
|
|
this->samplerAddressMode = addressMode;
|
|
return *this;
|
|
}
|
|
|
|
std::unique_ptr<Image> Image::Builder::build() {
|
|
vk::ImageUsageFlags usage = this->usage;
|
|
|
|
auto result = std::unique_ptr<Image>(new Image(this->device));
|
|
result->format = this->format;
|
|
result->extent = this->extent;
|
|
result->aspectMask = this->aspectMask;
|
|
result->mipLevels = this->mipLevels;
|
|
result->arrayLayers = this->arrayLayers;
|
|
|
|
if (this->isExternalImage) {
|
|
if (this->initialData != nullptr) {
|
|
log::warning(
|
|
"Image::Builder::setInitialData is ignored when wrapExisting is "
|
|
"used.");
|
|
}
|
|
result->externalImage = this->externalImage;
|
|
// Wrapped images (e.g. swapchain images) are handed to us already in a
|
|
// known layout controlled by their owner; we can't observe it here, so
|
|
// leave `layout` at eUndefined and let the caller call
|
|
// transitionLayout() to bring it in sync before relying on it.
|
|
} else {
|
|
if (this->initialData != nullptr) {
|
|
usage |= vk::ImageUsageFlagBits::eTransferDst;
|
|
}
|
|
if (this->createView || this->createSampler) {
|
|
usage |= vk::ImageUsageFlagBits::eSampled;
|
|
}
|
|
|
|
vk::ImageCreateInfo imageInfo{};
|
|
imageInfo.imageType = this->imageType;
|
|
imageInfo.extent = this->extent;
|
|
imageInfo.mipLevels = this->mipLevels;
|
|
imageInfo.arrayLayers = this->arrayLayers;
|
|
imageInfo.format = this->format;
|
|
imageInfo.tiling = this->tiling;
|
|
imageInfo.initialLayout = vk::ImageLayout::eUndefined;
|
|
imageInfo.usage = usage;
|
|
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
|
imageInfo.samples = this->samples;
|
|
|
|
result->image = this->device.get()->createImageUnique(imageInfo);
|
|
|
|
vk::MemoryRequirements memoryRequirements =
|
|
this->device.get()->getImageMemoryRequirements(result->image.get());
|
|
|
|
vk::MemoryAllocateInfo allocInfo{};
|
|
allocInfo.allocationSize = memoryRequirements.size;
|
|
allocInfo.memoryTypeIndex = this->device.findMemoryType(
|
|
memoryRequirements.memoryTypeBits, this->memoryProperties);
|
|
|
|
result->memory = this->device.get()->allocateMemoryUnique(allocInfo);
|
|
this->device.get()->bindImageMemory(result->image.get(),
|
|
result->memory.get(), 0);
|
|
}
|
|
|
|
if (!this->isExternalImage && this->initialData != nullptr) {
|
|
vk::Buffer stagingBuffer;
|
|
vk::DeviceMemory stagingMemory;
|
|
this->device.createBuffer(this->initialDataSize,
|
|
vk::BufferUsageFlagBits::eTransferSrc,
|
|
vk::MemoryPropertyFlagBits::eHostVisible |
|
|
vk::MemoryPropertyFlagBits::eHostCoherent,
|
|
stagingBuffer, stagingMemory);
|
|
|
|
void* mapped =
|
|
this->device.get()->mapMemory(stagingMemory, 0, this->initialDataSize);
|
|
memcpy(mapped, this->initialData,
|
|
static_cast<size_t>(this->initialDataSize));
|
|
this->device.get()->unmapMemory(stagingMemory);
|
|
|
|
vk::BufferImageCopy region{};
|
|
region.bufferOffset = 0;
|
|
region.bufferRowLength = 0;
|
|
region.bufferImageHeight = 0;
|
|
region.imageSubresource.aspectMask = this->aspectMask;
|
|
region.imageSubresource.mipLevel = 0;
|
|
region.imageSubresource.baseArrayLayer = 0;
|
|
region.imageSubresource.layerCount = this->arrayLayers;
|
|
region.imageOffset = vk::Offset3D{0, 0, 0};
|
|
region.imageExtent = this->extent;
|
|
|
|
vk::CommandBuffer commandBuffer = this->device.beginSingleTimeCommands();
|
|
result->transitionLayout(commandBuffer,
|
|
vk::ImageLayout::eTransferDstOptimal);
|
|
commandBuffer.copyBufferToImage(stagingBuffer, result->get(),
|
|
vk::ImageLayout::eTransferDstOptimal,
|
|
region);
|
|
if (usage & vk::ImageUsageFlagBits::eSampled) {
|
|
result->transitionLayout(commandBuffer,
|
|
vk::ImageLayout::eShaderReadOnlyOptimal);
|
|
}
|
|
this->device.endSingleTimeCommands(commandBuffer);
|
|
|
|
this->device.get()->destroyBuffer(stagingBuffer);
|
|
this->device.get()->freeMemory(stagingMemory);
|
|
}
|
|
|
|
if (this->createView) {
|
|
vk::ImageViewCreateInfo viewInfo{};
|
|
viewInfo.image = result->get();
|
|
viewInfo.viewType = vk::ImageViewType::e2D;
|
|
viewInfo.format = this->format;
|
|
viewInfo.subresourceRange.aspectMask = this->aspectMask;
|
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
|
viewInfo.subresourceRange.levelCount = this->mipLevels;
|
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
|
viewInfo.subresourceRange.layerCount = this->arrayLayers;
|
|
|
|
result->view = this->device.get()->createImageViewUnique(viewInfo);
|
|
}
|
|
|
|
if (this->createSampler) {
|
|
vk::SamplerCreateInfo samplerInfo{};
|
|
samplerInfo.magFilter = this->samplerFilter;
|
|
samplerInfo.minFilter = this->samplerFilter;
|
|
samplerInfo.addressModeU = this->samplerAddressMode;
|
|
samplerInfo.addressModeV = this->samplerAddressMode;
|
|
samplerInfo.addressModeW = this->samplerAddressMode;
|
|
samplerInfo.anisotropyEnable = VK_FALSE;
|
|
samplerInfo.maxAnisotropy = 1.0f;
|
|
samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack;
|
|
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
|
samplerInfo.compareEnable = VK_FALSE;
|
|
samplerInfo.compareOp = vk::CompareOp::eAlways;
|
|
samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
|
|
samplerInfo.minLod = 0.0f;
|
|
samplerInfo.maxLod = static_cast<float>(this->mipLevels);
|
|
|
|
result->sampler = this->device.get()->createSamplerUnique(samplerInfo);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
} // namespace sophia
|