22 changed files with 662 additions and 952 deletions
-
3src/video_core/CMakeLists.txt
-
1src/video_core/engines/maxwell_dma.h
-
12src/video_core/host_shaders/opengl_present_scaleforce.frag
-
12src/video_core/host_shaders/present_bicubic.frag
-
12src/video_core/host_shaders/present_gaussian.frag
-
2src/video_core/host_shaders/vulkan_present.frag
-
33src/video_core/host_shaders/vulkan_present.vert
-
1src/video_core/host_shaders/vulkan_present_scaleforce_fp16.frag
-
2src/video_core/host_shaders/vulkan_present_scaleforce_fp32.frag
-
34src/video_core/renderer_vulkan/present/filters.cpp
-
24src/video_core/renderer_vulkan/present/filters.h
-
336src/video_core/renderer_vulkan/present/layer.cpp
-
92src/video_core/renderer_vulkan/present/layer.h
-
34src/video_core/renderer_vulkan/present/present_push_constants.h
-
31src/video_core/renderer_vulkan/present/util.cpp
-
6src/video_core/renderer_vulkan/present/util.h
-
503src/video_core/renderer_vulkan/present/window_adapt_pass.cpp
-
29src/video_core/renderer_vulkan/present/window_adapt_pass.h
-
8src/video_core/renderer_vulkan/renderer_vulkan.cpp
-
2src/video_core/renderer_vulkan/renderer_vulkan.h
-
378src/video_core/renderer_vulkan/vk_blit_screen.cpp
-
59src/video_core/renderer_vulkan/vk_blit_screen.h
@ -0,0 +1,336 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
|||
|
|||
#include "common/settings.h"
|
|||
#include "video_core/framebuffer_config.h"
|
|||
#include "video_core/renderer_vulkan/present/fsr.h"
|
|||
#include "video_core/renderer_vulkan/present/fxaa.h"
|
|||
#include "video_core/renderer_vulkan/present/layer.h"
|
|||
#include "video_core/renderer_vulkan/present/present_push_constants.h"
|
|||
#include "video_core/renderer_vulkan/present/smaa.h"
|
|||
#include "video_core/renderer_vulkan/present/util.h"
|
|||
#include "video_core/renderer_vulkan/vk_blit_screen.h"
|
|||
#include "video_core/textures/decoders.h"
|
|||
|
|||
namespace Vulkan { |
|||
|
|||
namespace { |
|||
|
|||
u32 GetBytesPerPixel(const Tegra::FramebufferConfig& framebuffer) { |
|||
using namespace VideoCore::Surface; |
|||
return BytesPerBlock(PixelFormatFromGPUPixelFormat(framebuffer.pixel_format)); |
|||
} |
|||
|
|||
std::size_t GetSizeInBytes(const Tegra::FramebufferConfig& framebuffer) { |
|||
return static_cast<std::size_t>(framebuffer.stride) * |
|||
static_cast<std::size_t>(framebuffer.height) * GetBytesPerPixel(framebuffer); |
|||
} |
|||
|
|||
VkFormat GetFormat(const Tegra::FramebufferConfig& framebuffer) { |
|||
switch (framebuffer.pixel_format) { |
|||
case Service::android::PixelFormat::Rgba8888: |
|||
case Service::android::PixelFormat::Rgbx8888: |
|||
return VK_FORMAT_A8B8G8R8_UNORM_PACK32; |
|||
case Service::android::PixelFormat::Rgb565: |
|||
return VK_FORMAT_R5G6B5_UNORM_PACK16; |
|||
case Service::android::PixelFormat::Bgra8888: |
|||
return VK_FORMAT_B8G8R8A8_UNORM; |
|||
default: |
|||
UNIMPLEMENTED_MSG("Unknown framebuffer pixel format: {}", |
|||
static_cast<u32>(framebuffer.pixel_format)); |
|||
return VK_FORMAT_A8B8G8R8_UNORM_PACK32; |
|||
} |
|||
} |
|||
|
|||
} // Anonymous namespace
|
|||
|
|||
Layer::Layer(const Device& device_, MemoryAllocator& memory_allocator_, Scheduler& scheduler_, |
|||
Tegra::MaxwellDeviceMemoryManager& device_memory_, size_t image_count_, |
|||
VkExtent2D output_size, VkDescriptorSetLayout layout) |
|||
: device(device_), memory_allocator(memory_allocator_), scheduler(scheduler_), |
|||
device_memory(device_memory_), image_count(image_count_) { |
|||
CreateDescriptorPool(); |
|||
CreateDescriptorSets(layout); |
|||
if (Settings::values.scaling_filter.GetValue() == Settings::ScalingFilter::Fsr) { |
|||
CreateFSR(output_size); |
|||
} |
|||
} |
|||
|
|||
Layer::~Layer() { |
|||
ReleaseRawImages(); |
|||
} |
|||
|
|||
void Layer::ConfigureDraw(PresentPushConstants* out_push_constants, |
|||
VkDescriptorSet* out_descriptor_set, RasterizerVulkan& rasterizer, |
|||
VkSampler sampler, size_t image_index, |
|||
const Tegra::FramebufferConfig& framebuffer, |
|||
const Layout::FramebufferLayout& layout) { |
|||
const auto texture_info = rasterizer.AccelerateDisplay( |
|||
framebuffer, framebuffer.address + framebuffer.offset, framebuffer.stride); |
|||
const u32 texture_width = texture_info ? texture_info->width : framebuffer.width; |
|||
const u32 texture_height = texture_info ? texture_info->height : framebuffer.height; |
|||
const u32 scaled_width = texture_info ? texture_info->scaled_width : texture_width; |
|||
const u32 scaled_height = texture_info ? texture_info->scaled_height : texture_height; |
|||
const bool use_accelerated = texture_info.has_value(); |
|||
|
|||
RefreshResources(framebuffer); |
|||
SetAntiAliasPass(); |
|||
|
|||
// Finish any pending renderpass
|
|||
scheduler.RequestOutsideRenderPassOperationContext(); |
|||
scheduler.Wait(resource_ticks[image_index]); |
|||
SCOPE_EXIT({ resource_ticks[image_index] = scheduler.CurrentTick(); }); |
|||
|
|||
if (!use_accelerated) { |
|||
UpdateRawImage(framebuffer, image_index); |
|||
} |
|||
|
|||
VkImage source_image = texture_info ? texture_info->image : *raw_images[image_index]; |
|||
VkImageView source_image_view = |
|||
texture_info ? texture_info->image_view : *raw_image_views[image_index]; |
|||
|
|||
anti_alias->Draw(scheduler, image_index, &source_image, &source_image_view); |
|||
|
|||
auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height); |
|||
const VkExtent2D render_extent{ |
|||
.width = scaled_width, |
|||
.height = scaled_height, |
|||
}; |
|||
|
|||
if (fsr) { |
|||
source_image_view = fsr->Draw(scheduler, image_index, source_image, source_image_view, |
|||
render_extent, crop_rect); |
|||
crop_rect = {0, 0, 1, 1}; |
|||
} |
|||
|
|||
SetMatrixData(*out_push_constants, layout); |
|||
SetVertexData(*out_push_constants, layout, crop_rect); |
|||
|
|||
UpdateDescriptorSet(source_image_view, sampler, image_index); |
|||
*out_descriptor_set = descriptor_sets[image_index]; |
|||
} |
|||
|
|||
void Layer::CreateDescriptorPool() { |
|||
descriptor_pool = CreateWrappedDescriptorPool(device, image_count, image_count); |
|||
} |
|||
|
|||
void Layer::CreateDescriptorSets(VkDescriptorSetLayout layout) { |
|||
const std::vector layouts(image_count, layout); |
|||
descriptor_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); |
|||
} |
|||
|
|||
void Layer::CreateStagingBuffer(const Tegra::FramebufferConfig& framebuffer) { |
|||
const VkBufferCreateInfo ci{ |
|||
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = 0, |
|||
.size = CalculateBufferSize(framebuffer), |
|||
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | |
|||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, |
|||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, |
|||
.queueFamilyIndexCount = 0, |
|||
.pQueueFamilyIndices = nullptr, |
|||
}; |
|||
|
|||
buffer = memory_allocator.CreateBuffer(ci, MemoryUsage::Upload); |
|||
} |
|||
|
|||
void Layer::CreateRawImages(const Tegra::FramebufferConfig& framebuffer) { |
|||
const auto format = GetFormat(framebuffer); |
|||
resource_ticks.resize(image_count); |
|||
raw_images.resize(image_count); |
|||
raw_image_views.resize(image_count); |
|||
|
|||
for (size_t i = 0; i < image_count; ++i) { |
|||
raw_images[i] = |
|||
CreateWrappedImage(memory_allocator, {framebuffer.width, framebuffer.height}, format); |
|||
raw_image_views[i] = CreateWrappedImageView(device, raw_images[i], format); |
|||
} |
|||
} |
|||
|
|||
void Layer::CreateFSR(VkExtent2D output_size) { |
|||
fsr = std::make_unique<FSR>(device, memory_allocator, image_count, output_size); |
|||
} |
|||
|
|||
void Layer::RefreshResources(const Tegra::FramebufferConfig& framebuffer) { |
|||
if (framebuffer.width == raw_width && framebuffer.height == raw_height && |
|||
framebuffer.pixel_format == pixel_format && !raw_images.empty()) { |
|||
return; |
|||
} |
|||
|
|||
raw_width = framebuffer.width; |
|||
raw_height = framebuffer.height; |
|||
pixel_format = framebuffer.pixel_format; |
|||
anti_alias.reset(); |
|||
|
|||
ReleaseRawImages(); |
|||
CreateStagingBuffer(framebuffer); |
|||
CreateRawImages(framebuffer); |
|||
} |
|||
|
|||
void Layer::SetAntiAliasPass() { |
|||
if (anti_alias && anti_alias_setting == Settings::values.anti_aliasing.GetValue()) { |
|||
return; |
|||
} |
|||
|
|||
anti_alias_setting = Settings::values.anti_aliasing.GetValue(); |
|||
|
|||
const VkExtent2D render_area{ |
|||
.width = Settings::values.resolution_info.ScaleUp(raw_width), |
|||
.height = Settings::values.resolution_info.ScaleUp(raw_height), |
|||
}; |
|||
|
|||
switch (anti_alias_setting) { |
|||
case Settings::AntiAliasing::Fxaa: |
|||
anti_alias = std::make_unique<FXAA>(device, memory_allocator, image_count, render_area); |
|||
break; |
|||
case Settings::AntiAliasing::Smaa: |
|||
anti_alias = std::make_unique<SMAA>(device, memory_allocator, image_count, render_area); |
|||
break; |
|||
default: |
|||
anti_alias = std::make_unique<NoAA>(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void Layer::ReleaseRawImages() { |
|||
for (const u64 tick : resource_ticks) { |
|||
scheduler.Wait(tick); |
|||
} |
|||
raw_images.clear(); |
|||
buffer.reset(); |
|||
} |
|||
|
|||
u64 Layer::CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const { |
|||
return GetSizeInBytes(framebuffer) * image_count; |
|||
} |
|||
|
|||
u64 Layer::GetRawImageOffset(const Tegra::FramebufferConfig& framebuffer, |
|||
size_t image_index) const { |
|||
return GetSizeInBytes(framebuffer) * image_index; |
|||
} |
|||
|
|||
void Layer::SetMatrixData(PresentPushConstants& data, |
|||
const Layout::FramebufferLayout& layout) const { |
|||
data.modelview_matrix = |
|||
MakeOrthographicMatrix(static_cast<f32>(layout.width), static_cast<f32>(layout.height)); |
|||
} |
|||
|
|||
void Layer::SetVertexData(PresentPushConstants& data, const Layout::FramebufferLayout& layout, |
|||
const Common::Rectangle<f32>& crop) const { |
|||
// Map the coordinates to the screen.
|
|||
const auto& screen = layout.screen; |
|||
const auto x = static_cast<f32>(screen.left); |
|||
const auto y = static_cast<f32>(screen.top); |
|||
const auto w = static_cast<f32>(screen.GetWidth()); |
|||
const auto h = static_cast<f32>(screen.GetHeight()); |
|||
|
|||
data.vertices[0] = ScreenRectVertex(x, y, crop.left, crop.top); |
|||
data.vertices[1] = ScreenRectVertex(x + w, y, crop.right, crop.top); |
|||
data.vertices[2] = ScreenRectVertex(x, y + h, crop.left, crop.bottom); |
|||
data.vertices[3] = ScreenRectVertex(x + w, y + h, crop.right, crop.bottom); |
|||
} |
|||
|
|||
void Layer::UpdateDescriptorSet(VkImageView image_view, VkSampler sampler, size_t image_index) { |
|||
const VkDescriptorImageInfo image_info{ |
|||
.sampler = sampler, |
|||
.imageView = image_view, |
|||
.imageLayout = VK_IMAGE_LAYOUT_GENERAL, |
|||
}; |
|||
|
|||
const VkWriteDescriptorSet sampler_write{ |
|||
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, |
|||
.pNext = nullptr, |
|||
.dstSet = descriptor_sets[image_index], |
|||
.dstBinding = 0, |
|||
.dstArrayElement = 0, |
|||
.descriptorCount = 1, |
|||
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, |
|||
.pImageInfo = &image_info, |
|||
.pBufferInfo = nullptr, |
|||
.pTexelBufferView = nullptr, |
|||
}; |
|||
|
|||
device.GetLogical().UpdateDescriptorSets(std::array{sampler_write}, {}); |
|||
} |
|||
|
|||
void Layer::UpdateRawImage(const Tegra::FramebufferConfig& framebuffer, size_t image_index) { |
|||
const std::span<u8> mapped_span = buffer.Mapped(); |
|||
|
|||
const u64 image_offset = GetRawImageOffset(framebuffer, image_index); |
|||
|
|||
const DAddr framebuffer_addr = framebuffer.address + framebuffer.offset; |
|||
const u8* const host_ptr = device_memory.GetPointer<u8>(framebuffer_addr); |
|||
|
|||
// TODO(Rodrigo): Read this from HLE
|
|||
constexpr u32 block_height_log2 = 4; |
|||
const u32 bytes_per_pixel = GetBytesPerPixel(framebuffer); |
|||
const u64 linear_size{GetSizeInBytes(framebuffer)}; |
|||
const u64 tiled_size{Tegra::Texture::CalculateSize( |
|||
true, bytes_per_pixel, framebuffer.stride, framebuffer.height, 1, block_height_log2, 0)}; |
|||
Tegra::Texture::UnswizzleTexture( |
|||
mapped_span.subspan(image_offset, linear_size), std::span(host_ptr, tiled_size), |
|||
bytes_per_pixel, framebuffer.width, framebuffer.height, 1, block_height_log2, 0); |
|||
|
|||
const VkBufferImageCopy copy{ |
|||
.bufferOffset = image_offset, |
|||
.bufferRowLength = 0, |
|||
.bufferImageHeight = 0, |
|||
.imageSubresource = |
|||
{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.mipLevel = 0, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = 1, |
|||
}, |
|||
.imageOffset = {.x = 0, .y = 0, .z = 0}, |
|||
.imageExtent = |
|||
{ |
|||
.width = framebuffer.width, |
|||
.height = framebuffer.height, |
|||
.depth = 1, |
|||
}, |
|||
}; |
|||
scheduler.Record([this, copy, index = image_index](vk::CommandBuffer cmdbuf) { |
|||
const VkImage image = *raw_images[index]; |
|||
const VkImageMemoryBarrier base_barrier{ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|||
.pNext = nullptr, |
|||
.srcAccessMask = 0, |
|||
.dstAccessMask = 0, |
|||
.oldLayout = VK_IMAGE_LAYOUT_GENERAL, |
|||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, |
|||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.image = image, |
|||
.subresourceRange{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.baseMipLevel = 0, |
|||
.levelCount = 1, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = 1, |
|||
}, |
|||
}; |
|||
VkImageMemoryBarrier read_barrier = base_barrier; |
|||
read_barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; |
|||
read_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; |
|||
read_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; |
|||
|
|||
VkImageMemoryBarrier write_barrier = base_barrier; |
|||
write_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; |
|||
write_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; |
|||
write_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; |
|||
|
|||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, |
|||
read_barrier); |
|||
cmdbuf.CopyBufferToImage(*buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copy); |
|||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, |
|||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | |
|||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, |
|||
0, write_barrier); |
|||
}); |
|||
} |
|||
|
|||
} // namespace Vulkan
|
|||
@ -0,0 +1,92 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include "common/math_util.h" |
|||
#include "video_core/host1x/gpu_device_memory_manager.h" |
|||
#include "video_core/vulkan_common/vulkan_wrapper.h" |
|||
|
|||
namespace Layout { |
|||
struct FramebufferLayout; |
|||
} |
|||
|
|||
namespace Tegra { |
|||
struct FramebufferConfig; |
|||
} |
|||
|
|||
namespace Service::android { |
|||
enum class PixelFormat : u32; |
|||
} |
|||
|
|||
namespace Settings { |
|||
enum class AntiAliasing : u32; |
|||
} |
|||
|
|||
namespace Vulkan { |
|||
|
|||
class AntiAliasPass; |
|||
class Device; |
|||
class FSR; |
|||
class MemoryAllocator; |
|||
struct PresentPushConstants; |
|||
class RasterizerVulkan; |
|||
class Scheduler; |
|||
|
|||
class Layer final { |
|||
public: |
|||
explicit Layer(const Device& device, MemoryAllocator& memory_allocator, Scheduler& scheduler, |
|||
Tegra::MaxwellDeviceMemoryManager& device_memory, size_t image_count, |
|||
VkExtent2D output_size, VkDescriptorSetLayout layout); |
|||
~Layer(); |
|||
|
|||
void ConfigureDraw(PresentPushConstants* out_push_constants, |
|||
VkDescriptorSet* out_descriptor_set, RasterizerVulkan& rasterizer, |
|||
VkSampler sampler, size_t image_index, |
|||
const Tegra::FramebufferConfig& framebuffer, |
|||
const Layout::FramebufferLayout& layout); |
|||
|
|||
private: |
|||
void CreateDescriptorPool(); |
|||
void CreateDescriptorSets(VkDescriptorSetLayout layout); |
|||
void CreateStagingBuffer(const Tegra::FramebufferConfig& framebuffer); |
|||
void CreateRawImages(const Tegra::FramebufferConfig& framebuffer); |
|||
void CreateFSR(VkExtent2D output_size); |
|||
|
|||
void RefreshResources(const Tegra::FramebufferConfig& framebuffer); |
|||
void SetAntiAliasPass(); |
|||
void ReleaseRawImages(); |
|||
|
|||
u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const; |
|||
u64 GetRawImageOffset(const Tegra::FramebufferConfig& framebuffer, size_t image_index) const; |
|||
|
|||
void SetMatrixData(PresentPushConstants& data, const Layout::FramebufferLayout& layout) const; |
|||
void SetVertexData(PresentPushConstants& data, const Layout::FramebufferLayout& layout, |
|||
const Common::Rectangle<f32>& crop) const; |
|||
void UpdateDescriptorSet(VkImageView image_view, VkSampler sampler, size_t image_index); |
|||
void UpdateRawImage(const Tegra::FramebufferConfig& framebuffer, size_t image_index); |
|||
|
|||
private: |
|||
const Device& device; |
|||
MemoryAllocator& memory_allocator; |
|||
Scheduler& scheduler; |
|||
Tegra::MaxwellDeviceMemoryManager& device_memory; |
|||
const size_t image_count{}; |
|||
vk::DescriptorPool descriptor_pool{}; |
|||
vk::DescriptorSets descriptor_sets{}; |
|||
|
|||
vk::Buffer buffer{}; |
|||
std::vector<vk::Image> raw_images{}; |
|||
std::vector<vk::ImageView> raw_image_views{}; |
|||
u32 raw_width{}; |
|||
u32 raw_height{}; |
|||
Service::android::PixelFormat pixel_format{}; |
|||
|
|||
Settings::AntiAliasing anti_alias_setting{}; |
|||
std::unique_ptr<AntiAliasPass> anti_alias{}; |
|||
|
|||
std::unique_ptr<FSR> fsr{}; |
|||
std::vector<u64> resource_ticks{}; |
|||
}; |
|||
|
|||
} // namespace Vulkan |
|||
@ -0,0 +1,34 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include "common/common_types.h" |
|||
|
|||
namespace Vulkan { |
|||
|
|||
struct ScreenRectVertex { |
|||
ScreenRectVertex() = default; |
|||
explicit ScreenRectVertex(f32 x, f32 y, f32 u, f32 v) : position{{x, y}}, tex_coord{{u, v}} {} |
|||
|
|||
std::array<f32, 2> position; |
|||
std::array<f32, 2> tex_coord; |
|||
}; |
|||
|
|||
static inline std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) { |
|||
// clang-format off |
|||
return { 2.f / width, 0.f, 0.f, 0.f, |
|||
0.f, 2.f / height, 0.f, 0.f, |
|||
0.f, 0.f, 1.f, 0.f, |
|||
-1.f, -1.f, 0.f, 1.f}; |
|||
// clang-format on |
|||
} |
|||
|
|||
struct PresentPushConstants { |
|||
std::array<f32, 4 * 4> modelview_matrix; |
|||
std::array<ScreenRectVertex, 4> vertices; |
|||
}; |
|||
|
|||
static_assert(sizeof(PresentPushConstants) <= 128, "Push constants are too large"); |
|||
|
|||
} // namespace Vulkan |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue