We got exr loading
This commit is contained in:
+188
-127
@@ -104,9 +104,8 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync)
|
||||
VkPhysicalDeviceProperties props{};
|
||||
vkGetPhysicalDeviceProperties(physicalDevice, &props);
|
||||
|
||||
imageCache.bindlessSetManager.init(device, props.limits.maxSamplerAnisotropy);
|
||||
|
||||
{ // create white texture
|
||||
imageCache.bindlessSetManager.init(device, props.limits.maxSamplerAnisotropy); {
|
||||
// create white texture
|
||||
std::uint32_t pixel = 0xFFFFFFFF;
|
||||
whiteImageId = createImage(
|
||||
{
|
||||
@@ -170,7 +169,7 @@ VulkanImmediateExecutor& GfxDevice::GetImmediateExecuter() {
|
||||
}
|
||||
|
||||
void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const EndFrameProps& props) {
|
||||
// get swapchain image
|
||||
// get swapchain image
|
||||
const auto [swapchainImage, swapchainImageIndex] = swapchain.acquireNextImage(getCurrentFrameIndex());
|
||||
if (swapchainImage == VK_NULL_HANDLE) {
|
||||
spdlog::info("Swapchain is freaky, skipping frame...");
|
||||
@@ -180,9 +179,7 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E
|
||||
// Fences are reset here to prevent the deadlock in case swapchain becomes dirty
|
||||
swapchain.resetFences(getCurrentFrameIndex());
|
||||
|
||||
auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
{
|
||||
auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED; {
|
||||
const VkImageSubresourceRange clearRange = vkinit::imageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_GENERAL);
|
||||
swapchainLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
@@ -277,8 +274,7 @@ void GfxDevice::immediateSubmit(ImmediateExecuteFunction&& f) const {
|
||||
GPUBuffer GfxDevice::createBuffer(
|
||||
std::size_t allocSize,
|
||||
VkBufferUsageFlags usage,
|
||||
VmaMemoryUsage memoryUsage) const
|
||||
{
|
||||
VmaMemoryUsage memoryUsage) const {
|
||||
const auto bufferInfo = VkBufferCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
||||
.size = allocSize,
|
||||
@@ -305,8 +301,7 @@ GPUBuffer GfxDevice::createBuffer(
|
||||
return buffer;
|
||||
}
|
||||
|
||||
VkDeviceAddress GfxDevice::getBufferAddress(const GPUBuffer& buffer) const
|
||||
{
|
||||
VkDeviceAddress GfxDevice::getBufferAddress(const GPUBuffer& buffer) const {
|
||||
const auto deviceAdressInfo = VkBufferDeviceAddressInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO,
|
||||
.buffer = buffer.buffer,
|
||||
@@ -314,10 +309,10 @@ VkDeviceAddress GfxDevice::getBufferAddress(const GPUBuffer& buffer) const
|
||||
return vkGetBufferDeviceAddress(device, &deviceAdressInfo);
|
||||
}
|
||||
|
||||
void GfxDevice::destroyBuffer(const GPUBuffer& buffer) const
|
||||
{
|
||||
void GfxDevice::destroyBuffer(const GPUBuffer& buffer) const {
|
||||
vmaDestroyBuffer(allocator, buffer.buffer, buffer.allocation);
|
||||
}
|
||||
|
||||
//
|
||||
// GPUImage GfxDevice::loadImageFromFileRaw(
|
||||
// const std::filesystem::path& path,
|
||||
@@ -356,15 +351,20 @@ ImageID GfxDevice::createImage(
|
||||
const vkutil::CreateImageInfo& createInfo,
|
||||
const std::string& debugName,
|
||||
void* pixelData,
|
||||
ImageID imageId)
|
||||
{
|
||||
ImageID imageId) {
|
||||
auto image = createImageRaw(createInfo);
|
||||
if (!debugName.empty()) {
|
||||
vkutil::addDebugLabel(device, image.image, debugName.c_str());
|
||||
image.debugName = debugName;
|
||||
}
|
||||
if (pixelData) {
|
||||
uploadImageData(image, pixelData);
|
||||
const std::size_t bytes =
|
||||
std::size_t(image.extent.width) *
|
||||
std::size_t(image.extent.height) *
|
||||
std::size_t(image.extent.depth) *
|
||||
BytesPerTexel(image.format);
|
||||
|
||||
uploadImageDataSized(image, pixelData, bytes, 0);
|
||||
}
|
||||
if (imageId != NULL_IMAGE_ID) {
|
||||
return imageCache.addImage(imageId, std::move(image));
|
||||
@@ -377,8 +377,7 @@ ImageID GfxDevice::createDrawImage(
|
||||
VkFormat format,
|
||||
glm::ivec2 size,
|
||||
const std::string& debugName,
|
||||
ImageID imageId)
|
||||
{
|
||||
ImageID imageId) {
|
||||
assert(size.x > 0 && size.y > 0);
|
||||
const auto extent = VkExtent3D{
|
||||
.width = (std::uint32_t)size.x,
|
||||
@@ -400,29 +399,21 @@ ImageID GfxDevice::createDrawImage(
|
||||
return createImage(createImageInfo, debugName, nullptr, imageId);
|
||||
}
|
||||
|
||||
ImageID GfxDevice::loadImageFromFile(
|
||||
const std::filesystem::path& path,
|
||||
VkFormat format,
|
||||
VkImageUsageFlags usage,
|
||||
bool mipMap)
|
||||
{
|
||||
return imageCache.loadImageFromFile(path, format, usage, mipMap);
|
||||
ImageID GfxDevice::loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) {
|
||||
return imageCache.loadImageFromFile(path, usage, mipMap, intent);
|
||||
}
|
||||
|
||||
const GPUImage& GfxDevice::getImage(ImageID id) const
|
||||
{
|
||||
const GPUImage& GfxDevice::getImage(ImageID id) const {
|
||||
return imageCache.getImage(id);
|
||||
}
|
||||
|
||||
ImageID GfxDevice::addImageToCache(GPUImage img)
|
||||
{
|
||||
ImageID GfxDevice::addImageToCache(GPUImage img) {
|
||||
return imageCache.addImage(std::move(img));
|
||||
}
|
||||
|
||||
GPUImage GfxDevice::createImageRaw(
|
||||
const vkutil::CreateImageInfo& createInfo,
|
||||
std::optional<VmaAllocationCreateInfo> customAllocationCreateInfo) const
|
||||
{
|
||||
std::optional<VmaAllocationCreateInfo> customAllocationCreateInfo) const {
|
||||
std::uint32_t mipLevels = 1;
|
||||
if (createInfo.mipMap) {
|
||||
const auto maxExtent = std::max(createInfo.extent.width, createInfo.extent.height);
|
||||
@@ -452,9 +443,7 @@ GPUImage GfxDevice::createImageRaw(
|
||||
.usage = VMA_MEMORY_USAGE_AUTO,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
||||
};
|
||||
const auto allocInfo = customAllocationCreateInfo.has_value() ?
|
||||
customAllocationCreateInfo.value() :
|
||||
defaultAllocInfo;
|
||||
const auto allocInfo = customAllocationCreateInfo.has_value() ? customAllocationCreateInfo.value() : defaultAllocInfo;
|
||||
|
||||
GPUImage image{};
|
||||
image.format = createInfo.format;
|
||||
@@ -464,8 +453,7 @@ GPUImage GfxDevice::createImageRaw(
|
||||
image.numLayers = createInfo.numLayers;
|
||||
image.isCubemap = createInfo.isCubemap;
|
||||
|
||||
VK_CHECK(
|
||||
vmaCreateImage(allocator, &imgInfo, &allocInfo, &image.image, &image.allocation, nullptr));
|
||||
VK_CHECK(vmaCreateImage(allocator, &imgInfo, &allocInfo, &image.image, &image.allocation, nullptr));
|
||||
|
||||
// create view only when usage flags allow it
|
||||
bool shouldCreateView = ((createInfo.usage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0) ||
|
||||
@@ -475,12 +463,13 @@ GPUImage GfxDevice::createImageRaw(
|
||||
|
||||
if (shouldCreateView) {
|
||||
VkImageAspectFlags aspectFlag = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
if (createInfo.format == VK_FORMAT_D32_SFLOAT) { // TODO: support other depth formats
|
||||
if (createInfo.format == VK_FORMAT_D32_SFLOAT) {
|
||||
// TODO: support other depth formats
|
||||
aspectFlag = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
|
||||
auto viewType =
|
||||
createInfo.numLayers == 1 ? VK_IMAGE_VIEW_TYPE_2D : VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
createInfo.numLayers == 1 ? VK_IMAGE_VIEW_TYPE_2D : VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
if (createInfo.isCubemap && createInfo.numLayers == 6) {
|
||||
viewType = VK_IMAGE_VIEW_TYPE_CUBE;
|
||||
}
|
||||
@@ -491,13 +480,13 @@ GPUImage GfxDevice::createImageRaw(
|
||||
.viewType = viewType,
|
||||
.format = createInfo.format,
|
||||
.subresourceRange =
|
||||
VkImageSubresourceRange{
|
||||
.aspectMask = aspectFlag,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = mipLevels,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = createInfo.numLayers,
|
||||
},
|
||||
VkImageSubresourceRange{
|
||||
.aspectMask = aspectFlag,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = mipLevels,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = createInfo.numLayers,
|
||||
},
|
||||
};
|
||||
|
||||
VK_CHECK(vkCreateImageView(device, &viewCreateInfo, nullptr, &image.imageView));
|
||||
@@ -506,104 +495,176 @@ GPUImage GfxDevice::createImageRaw(
|
||||
return image;
|
||||
}
|
||||
|
||||
void GfxDevice::uploadImageData(const GPUImage& image, void* pixelData, std::uint32_t layer) const
|
||||
{
|
||||
VkDeviceSize dataSize =
|
||||
VkDeviceSize(image.extent.depth) *
|
||||
image.extent.width *
|
||||
image.extent.height *
|
||||
BytesPerTexel(image.format);
|
||||
|
||||
auto uploadBuffer = createBuffer(dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
memcpy(uploadBuffer.info.pMappedData, pixelData, size_t(dataSize));
|
||||
|
||||
executor.immediateSubmit([&](VkCommandBuffer cmd) {
|
||||
assert(
|
||||
(image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 &&
|
||||
"Image needs to have VK_IMAGE_USAGE_TRANSFER_DST_BIT to upload data to it");
|
||||
vkutil::transitionImage(
|
||||
cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
|
||||
const auto copyRegion = VkBufferImageCopy{
|
||||
.bufferOffset = 0,
|
||||
.bufferRowLength = 0,
|
||||
.bufferImageHeight = 0,
|
||||
.imageSubresource =
|
||||
{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = layer,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.imageExtent = image.extent,
|
||||
};
|
||||
|
||||
vkCmdCopyBufferToImage(
|
||||
cmd,
|
||||
uploadBuffer.buffer,
|
||||
image.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1,
|
||||
©Region);
|
||||
|
||||
if (image.mipLevels > 1) {
|
||||
assert(
|
||||
(image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 &&
|
||||
(image.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0 &&
|
||||
"Image needs to have VK_IMAGE_USAGE_TRANSFER_{DST,SRC}_BIT to generate mip maps");
|
||||
// graphics::generateMipmaps(
|
||||
// cmd,
|
||||
// image.image,
|
||||
// VkExtent2D{image.extent.width, image.extent.height},
|
||||
// image.mipLevels);
|
||||
spdlog::warn("Yea dawg, i ain't written this yet :pray:");
|
||||
} else {
|
||||
vkutil::transitionImage(
|
||||
cmd,
|
||||
image.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
}
|
||||
});
|
||||
|
||||
destroyBuffer(uploadBuffer);
|
||||
}
|
||||
|
||||
GPUImage GfxDevice::loadImageFromFileRaw(
|
||||
const std::filesystem::path& path,
|
||||
VkFormat format,
|
||||
VkImageUsageFlags usage,
|
||||
bool mipMap) const
|
||||
{
|
||||
auto data = util::loadImage(path);
|
||||
if (!data.pixels) {
|
||||
fmt::println("[error] failed to load image from '{}'", path.string());
|
||||
bool mipMap,
|
||||
TextureIntent intent) const {
|
||||
// 1) Decode file to CPU pixels (stb for png/jpg/hdr, tinyexr for exr)
|
||||
// IMPORTANT: util::loadImage should fill:
|
||||
// - width/height/channels(=4)
|
||||
// - hdr + (pixels OR hdrPixels)
|
||||
// - vkFormat (RGBA8_SRGB or RGBA8_UNORM or RGBA32F)
|
||||
// - byteSize (exact bytes to upload)
|
||||
const auto data = util::loadImage(path, intent);
|
||||
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 ||
|
||||
(data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
|
||||
spdlog::error("Failed to load image '{}'", path.string());
|
||||
return getImage(errorImageId);
|
||||
}
|
||||
|
||||
// 2) Create GPU image using the format the loader chose (matches CPU memory layout)
|
||||
auto image = createImageRaw({
|
||||
.format = format,
|
||||
.usage = usage | //
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | // for uploading pixel data to image
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT, // for generating mips
|
||||
.extent =
|
||||
VkExtent3D{
|
||||
.width = (std::uint32_t)data.width,
|
||||
.height = (std::uint32_t)data.height,
|
||||
.depth = 1,
|
||||
},
|
||||
.format = data.vkFormat,
|
||||
.usage = usage |
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
(mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0),
|
||||
.extent = VkExtent3D{
|
||||
.width = static_cast<std::uint32_t>(data.width),
|
||||
.height = static_cast<std::uint32_t>(data.height),
|
||||
.depth = 1,
|
||||
},
|
||||
.mipMap = mipMap,
|
||||
});
|
||||
uploadImageData(image, data.pixels);
|
||||
|
||||
// 3) Upload *exactly* byteSize bytes from the correct pointer
|
||||
const void* src = data.hdr
|
||||
? static_cast<const void*>(data.hdrPixels)
|
||||
: static_cast<const void*>(data.pixels);
|
||||
|
||||
// Use the "sized" upload to avoid BytesPerTexel mismatches
|
||||
uploadImageDataSized(image, src, data.byteSize, 0);
|
||||
|
||||
// 4) Debug label
|
||||
image.debugName = path.string();
|
||||
vkutil::addDebugLabel(device, image.image, path.string().c_str());
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void GfxDevice::destroyImage(const GPUImage& image) const
|
||||
{
|
||||
// void GfxDevice::uploadImageData(const GPUImage& image, void* pixelData, std::uint32_t layer) const {
|
||||
// VkDeviceSize dataSize =
|
||||
// VkDeviceSize(image.extent.depth) *
|
||||
// image.extent.width *
|
||||
// image.extent.height *
|
||||
// BytesPerTexel(image.format);
|
||||
//
|
||||
// auto uploadBuffer = createBuffer(dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
// memcpy(uploadBuffer.info.pMappedData, pixelData, size_t(dataSize));
|
||||
//
|
||||
// executor.immediateSubmit([&] (VkCommandBuffer cmd) {
|
||||
// assert(
|
||||
// (image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 &&
|
||||
// "Image needs to have VK_IMAGE_USAGE_TRANSFER_DST_BIT to upload data to it");
|
||||
// vkutil::transitionImage(
|
||||
// cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
//
|
||||
// const auto copyRegion = VkBufferImageCopy{
|
||||
// .bufferOffset = 0,
|
||||
// .bufferRowLength = 0,
|
||||
// .bufferImageHeight = 0,
|
||||
// .imageSubresource =
|
||||
// {
|
||||
// .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
// .mipLevel = 0,
|
||||
// .baseArrayLayer = layer,
|
||||
// .layerCount = 1,
|
||||
// },
|
||||
// .imageExtent = image.extent,
|
||||
// };
|
||||
//
|
||||
// vkCmdCopyBufferToImage(
|
||||
// cmd,
|
||||
// uploadBuffer.buffer,
|
||||
// image.image,
|
||||
// VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
// 1,
|
||||
// ©Region);
|
||||
//
|
||||
// if (image.mipLevels > 1) {
|
||||
// assert(
|
||||
// (image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 &&
|
||||
// (image.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0 &&
|
||||
// "Image needs to have VK_IMAGE_USAGE_TRANSFER_{DST,SRC}_BIT to generate mip maps");
|
||||
// // graphics::generateMipmaps(
|
||||
// // cmd,
|
||||
// // image.image,
|
||||
// // VkExtent2D{image.extent.width, image.extent.height},
|
||||
// // image.mipLevels);
|
||||
// spdlog::warn("Yea dawg, i ain't written this yet :pray:");
|
||||
// } else {
|
||||
// vkutil::transitionImage(
|
||||
// cmd,
|
||||
// image.image,
|
||||
// VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
// VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// destroyBuffer(uploadBuffer);
|
||||
// }
|
||||
|
||||
void GfxDevice::uploadImageDataSized(const GPUImage& image, const void* pixelData, std::size_t byteSize, std::uint32_t layer) const {
|
||||
auto uploadBuffer = createBuffer(byteSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
|
||||
|
||||
// safety checks
|
||||
assert(uploadBuffer.info.pMappedData);
|
||||
assert(pixelData);
|
||||
assert(byteSize > 0);
|
||||
|
||||
std::memcpy(uploadBuffer.info.pMappedData, pixelData, byteSize);
|
||||
|
||||
executor.immediateSubmit([&] (VkCommandBuffer cmd) {
|
||||
vkutil::transitionImage(cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
|
||||
VkBufferImageCopy copyRegion{};
|
||||
copyRegion.bufferOffset = 0;
|
||||
copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
copyRegion.imageSubresource.mipLevel = 0;
|
||||
copyRegion.imageSubresource.baseArrayLayer = layer;
|
||||
copyRegion.imageSubresource.layerCount = 1;
|
||||
copyRegion.imageExtent = image.extent;
|
||||
|
||||
vkCmdCopyBufferToImage(cmd, uploadBuffer.buffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region);
|
||||
|
||||
vkutil::transitionImage(cmd, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
});
|
||||
|
||||
destroyBuffer(uploadBuffer);
|
||||
}
|
||||
|
||||
// GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap) const {
|
||||
// const auto data = util::loadImage(path);
|
||||
// const bool isHdr = data.hdr && data.hdrPixels;
|
||||
// const bool isLdr = !data.hdr && data.pixels;
|
||||
//
|
||||
// if (!isHdr && !isLdr || data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0) {
|
||||
// spdlog::error("failed to load image from '{}'", path.string());
|
||||
// return getImage(errorImageId);
|
||||
// }
|
||||
//
|
||||
// auto image = createImageRaw({
|
||||
// .format = data.vkFormat,
|
||||
// .usage = usage |
|
||||
// VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
// (mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0),
|
||||
// .extent = VkExtent3D{
|
||||
// .width = (std::uint32_t)data.width,
|
||||
// .height = (std::uint32_t)data.height,
|
||||
// .depth = 1,
|
||||
// },
|
||||
// .mipMap = mipMap,
|
||||
// });
|
||||
//
|
||||
// const void* src = isHdr ? (const void*)data.hdrPixels : (const void*)data.pixels;
|
||||
// uploadImageDataSized(image, src, data.byteSize, 0);
|
||||
//
|
||||
// image.debugName = path.string();
|
||||
// vkutil::addDebugLabel(device, image.image, path.string().c_str());
|
||||
// return image;
|
||||
// }
|
||||
|
||||
void GfxDevice::destroyImage(const GPUImage& image) const {
|
||||
vkDestroyImageView(device, image.imageView, nullptr);
|
||||
vmaDestroyImage(allocator, image.image, image.allocation);
|
||||
// TODO: if image has bindless id, update the set
|
||||
|
||||
Reference in New Issue
Block a user