이전 장의 코드에는 마지막 정리(cleanup) 조각 하나가 빠져있습니다:
ashen-aetna: vendor/src/vk_mem_alloc.h:11791: void VmaDeviceMemoryBlock::Destroy(VmaAllocator): Assertion `m_pMetadata->IsEmpty() && "Some allocations were not freed before destruction of this memory block!"' failed.
이것은 우리가 해제하지 않은 텍스처의 할당(allocation)입니다.
aetna.allocator.destroy_image(texture.vk_image,&texture.allocation); 코드가 필요합니다.
하지만 이걸 어디에 두어야 할까요?
Texture에 Drop을 구현할 수는 없습니다. 이 파괴 과정에는 외부 정보(vk_mem::Allocator)가 필요하기 때문입니다.
이 줄을 작성할 수 있는 “main() 함수의 끝” 같은 곳은 없습니다 (winit이 스레드를 장악하고 eventloop.run()의 클로저 밖으로 제어권을 절대 반환하지 않기 때문입니다. 즉, 이 run 호출 다음에 작성된 모든 것은 무시됩니다). Event::LoopDestroyed 이벤트에 반응할 수는 있을 겁니다.
아니면 텍스처를 Aetna의 일부로 만들고 Aetna의 drop 함수에서 할당을 파괴할 수도 있습니다.
또한 (제목에서처럼) 하나 이상의 텍스처를 갖고 싶기 때문에, 여러 텍스처를 보관하는 구조체, 가령 TextureStorage를 만들고 cleanup 함수(모든 텍스처의 이미지를 파괴하는)를 갖게 하는 것이 합리적일 것입니다. 그런 다음 이 구조체를 Aetna의 일부로 만들고 volcano(화산, 즉 Aetna)를 드롭할 때 cleanup을 호출할 수 있습니다.
대략 이런 식입니다:
pub struct TextureStorage {
textures: Vec<Texture>,
}
impl TextureStorage {
pub fn new() -> Self {
TextureStorage { textures: vec![] }
}
pub fn cleanup(&mut self, allocator: &vk_mem::Allocator) {
for texture in &self.textures {
allocator.destroy_image(texture.vk_image, &texture.allocation);
}
}
pub fn new_texture_from_file<P: AsRef<std::path::Path>>(
&mut self,
path: P,
aetna: &Aetna,
) -> Result<usize, Box<dyn std::error::Error>> {
let new_texture = Texture::from_file(path, aetna)?;
let new_id = self.textures.len();
self.textures.push(new_texture);
Ok(new_id)
}
pub fn get(&self, index: usize) -> Option<&Texture> {
self.textures.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut Texture> {
self.textures.get_mut(index)
}
}
하지만 이런 텍스처 저장소를 Aetna의 일부로 만들고 싶다면, new_from_file이나 결과적으로 Texture::from_file 함수의 인자로 aetna: &Aetna를 받는 것은 상당히 불편합니다.
분리하는 것이 낫습니다:
impl Texture {
pub fn from_file<P: AsRef<std::path::Path>>(
path: P,
device: &ash::Device,
allocator: &vk_mem::Allocator,
commandpool_graphics: &vk::CommandPool,
graphics_queue: &vk::Queue,
) -> Result<Self, Box<dyn std::error::Error>> {
(함수 본문 전체에 걸쳐 명백한 수정과 함께), 그리고 비슷하게
pub fn new_texture_from_file<P: AsRef<std::path::Path>>(
&mut self,
path: P,
device: &ash::Device,
allocator: &vk_mem::Allocator,
commandpool_graphics: &vk::CommandPool,
graphics_queue: &vk::Queue,
) -> Result<usize, Box<dyn std::error::Error>> {
let new_texture = Texture::from_file(
path,
device,
allocator,
commandpool_graphics,
graphics_queue,
)?;
let new_id = self.textures.len();
self.textures.push(new_texture);
Ok(new_id)
}
aetna.rs에 use crate::texture::TextureStorage;를 추가하고, Aetna는 새 필드 pub texture_storage: TextureStorage,를 얻으며, Aetna::init()의 반환값에 texture_storage: TextureStorage::new(), 줄이 추가됩니다.
Aetna::drop()에서는 의도했던 대로 TextureStorage::cleanup을 호출합니다:
impl Drop for Aetna {
fn drop(&mut self) {
unsafe {
self.device
.device_wait_idle()
.expect("something wrong while waiting");
self.texture_storage.cleanup(&self.allocator);
self.device
.destroy_descriptor_pool(self.descriptor_pool, None);
Aetna는 이미지를 로드하는 함수를 가져야 합니다.
pub fn new_texture_from_file<P: AsRef<std::path::Path>>(
&mut self,
path: P,
) -> Result<usize, Box<dyn std::error::Error>> {
self.texture_storage.new_texture_from_file(
path,
&self.device,
&self.allocator,
&self.pools.commandpool_graphics,
&self.queues.graphics_queue,
)
}
(main에서) 텍스처를 로드하는 것은 이제 다음을 통해 이루어집니다:
let texture_id = aetna.new_texture_from_file("../gfx/image.png")?;
그리고 이전에 텍스처를 사용했던 곳들은 이렇게 바뀝니다:
if let Some(texture) = aetna.texture_storage.get(texture_id) {
let imageinfo = vk::DescriptorImageInfo {
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
image_view: texture.imageview,
sampler: texture.sampler,
..Default::default()
};
let descriptorwrite_image = vk::WriteDescriptorSet {
dst_set: aetna.descriptor_sets_texture[aetna.swapchain.current_image],
dst_binding: 0,
dst_array_element: 0,
descriptor_type: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: 1,
p_image_info: [imageinfo].as_ptr(),
..Default::default()
};
unsafe {
aetna
.device
.update_descriptor_sets(&[descriptorwrite_image], &[]);
}
}
에러 메시지가 사라졌습니다. 대신 ImageView가 파괴되지 않았다는 불평이 나타납니다.
TextureStorage::cleanup 함수를 업데이트합니다:
pub fn cleanup(&mut self, device: &ash::Device, allocator: &vk_mem::Allocator) {
for texture in &self.textures {
unsafe { device.destroy_image_view(texture.imageview, None) };
allocator.destroy_image(texture.vk_image, &texture.allocation);
}
}
새로운 device 인자를 추가했습니다. 에러가 사라지고, 유효성 검사 레이어(validation layers)에서 새로운 불평이 나옵니다: 샘플러(sampler)도 파괴해야 한다는군요.
좋습니다:
pub fn cleanup(&mut self, device: &ash::Device, allocator: &vk_mem::Allocator) {
for texture in &self.textures {
unsafe {
device.destroy_sampler(texture.sampler, None);
device.destroy_image_view(texture.imageview, None);
}
allocator.destroy_image(texture.vk_image, &texture.allocation);
}
}
좋습니다. 이제 모든 문제가 처리되었습니다.
두 개의 텍스처를 사용해 봅시다.
let second_texture_id = aetna.new_texture_from_file("../gfx/image2.png")?;
이 변수(와 이전의 texture_id)를 가변으로 만들면, 예를 들어 다음 줄들을
winit::event::VirtualKeyCode::F11 => {
std::mem::swap(&mut texture_id, &mut second_texture_id);
}
이벤트 처리 코드에 포함시켜 두 텍스처 사이를 전환할 수 있습니다.
두 번째 사각형(quad)을 삽입해 봅시다:
quad.insert_visibly(TexturedInstanceData::from_matrix(
na::Matrix4::new_translation(&na::Vector3::new(2.0, 0., 0.3)),
));
둘 다 같은 텍스처를 가집니다.
질문: 하나에는 첫 번째 텍스처를, 다른 하나에는 두 번째 텍스처를 줄 수 있을까요?
답변: 네. 어떻게든 가능해야 합니다.
새로운 질문: 어떻게?
첫 번째 아이디어: 먼저 텍스처들을 하나로 합치고, 정점(vertex)의 텍스처 좌표를 조정한 다음, 문제를 우회하는 것입니다. 이건 작동할 겁니다. 검색할 키워드는 “텍스처 아틀라스(texture atlas)”입니다.
두 번째 아이디어: 여러 디스크립터 집합(descriptor set)을 가지고, 각 텍스처를 하나에 바인딩한 다음, Model::draw()에서 cmd_bind_vertex_buffer 명령어들 사이에 cmd_bind_descriptor_set 명령어들을 끼워 넣는 것입니다. 이것 또한 가능할 겁니다. 모든 사각형이 공통으로 가지는 텍스처가 하나 있고, 모든 구(sphere)가 동일하게 가지는 다른 텍스처가 있다면, 이 방법은 심지어 편리할 것입니다.
만약 사각형 모델에 대해 다른 텍스처를 원하고 (그리고 그것을 하나의 Model로 유지하고 싶다면), 저는 “어떤 텍스처를 사용할지”를 인스턴스별 데이터(per-instance data)의 일부로 만드는 것을 선호할 것입니다. 따라서:
세 번째 아이디어: 샘플러/이미지 배열 전체를 바인딩하고, 인덱스를 인스턴스별 데이터의 일부로 전달하는 것입니다. 이것은 아마도 이 방법들 중 가장 복잡하겠지만, 그럼에도 불구하고 제가 시도해보고자 하는 방법입니다.
프래그먼트 셰이더(fragment shader)를 약간 변경하여 두 개의 텍스처를 허용하도록 합니다. 배열로 작동하는지 봅시다:
#version 450
layout (location=0) out vec4 theColour;
layout (location=0) in vec2 uv;
layout(set=1,binding=0) uniform sampler2D texturesamplers[2];
void main(){
theColour=texture(texturesamplers[0],uv);
}
음:
[Debug][error][validation] "Shader expects at least 2 descriptors for binding 1.0 but only 1 provided"
(셰이더는 바인딩 1.0에 대해 최소 2개의 디스크립터를 기대하지만 1개만 제공됨)
예상했던 바입니다. 그럼 두 번째 디스크립터를 제공해 봅시다. 파이프라인 설정 중에:
let descriptorset_layout_binding_descs1 = [vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(2) // <- 이 부분이 변경됨
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build()];
(디스크립터 개수가 변경되었습니다.)
이제 디스크립터 풀(descriptor pool)이 너무 작습니다:
[Debug][error][validation] "Unable to allocate 6 descriptors of type VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER from VkDescriptorPool 0x260000000026[]. This pool only has 3 descrip
tors of this type remaining..."
(VkDescriptorPool ...에서 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER 타입의 디스크립터 6개를 할당할 수 없습니다. 이 풀에는 해당 타입의 디스크립터가 3개만 남아 있습니다...)
자,
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: 2*swapchain.amount_of_images,
},
(Aetna::init()에서).
이제 텍스처 ID를 인스턴스별 데이터의 일부로 만들 시간입니다:
#[repr(C)]
pub struct TexturedInstanceData {
pub modelmatrix: [[f32; 4]; 4],
pub inverse_modelmatrix: [[f32; 4]; 4],
pub texture_id: u32,
}
impl TexturedInstanceData {
pub fn from_matrix(modelmatrix: na::Matrix4<f32>) -> TexturedInstanceData {
TexturedInstanceData {
modelmatrix: modelmatrix.into(),
inverse_modelmatrix: modelmatrix.try_inverse().unwrap().into(),
texture_id: 0,
}
}
pub fn from_matrix_and_texture(
modelmatrix: na::Matrix4<f32>,
texture_id: usize,
) -> TexturedInstanceData {
TexturedInstanceData {
modelmatrix: modelmatrix.into(),
inverse_modelmatrix: modelmatrix.try_inverse().unwrap().into(),
texture_id: texture_id as u32,
}
}
}
참고로, 지금 프로그램을 실행하면 두 번째 사각형이 사라집니다. 왜일까요? 파이프라인 생성 시 알려주는 인스턴스별 데이터에 대한 정보를 조정하지 않았기 때문입니다.
vk::VertexInputAttributeDescription{
binding: 1,
location: 10,
offset: 128,
format: vk::Format::R8G8B8A8_UINT,
},
그리고
vk::VertexInputBindingDescription {
binding: 1,
stride: 132,
input_rate: vk::VertexInputRate::INSTANCE,
},
더 낫군요. 하지만:
[Debug][warning][performance] "Vertex attribute at location 10 not consumed by vertex shader"
(location 10의 정점 속성이 정점 셰이더에서 사용되지 않음)
그다지 놀랍지는 않습니다. 사용하도록 합시다. 정점 셰이더(Vertex shader):
#version 450
layout (location=0) in vec3 position;
layout (location=1) in vec2 texcoord;
layout (location=2) in mat4 model_matrix;
layout (location=6) in mat4 inverse_model_matrix;
layout (location=10) in uint texture_id;
layout (set=0, binding=0) uniform UniformBufferObject {
mat4 view_matrix;
mat4 projection_matrix;
} ubo;
layout (location=0) out vec2 uv;
layout (location=1) out uint tex_id;
void main() {
vec4 worldpos = model_matrix*vec4(position,1.0);
gl_Position = ubo.projection_matrix*ubo.view_matrix*worldpos;
uv = texcoord;
tex_id=texture_id;
}
그리고 프래그먼트 셰이더:
#version 450
layout (location=0) out vec4 theColour;
layout (location=0) in vec2 uv;
layout (location=1) in uint texture_id;
layout(set=1,binding=0) uniform sampler2D texturesamplers[2];
void main(){
theColour=texture(texturesamplers[0],uv);
}
컴파일 에러가 발생합니다:
/shaders/shader_textured.frag:6: error: 'int' : must be qualified as flat in
('int'는 flat으로 한정되어야 합니다)
이게 뭘까요?
삼각형의 모든 정점에서 어떤 변수가 동일하지 않을 때마다, 프래그먼트 셰이더는 프래그먼트의 위치에 따라 보간된 값을 받습니다. 우리는 이 특성을 색상에 사용했고 텍스처 좌표에서는 크게 의존하고 있습니다. 정수(integer)의 보간은 문제가 됩니다: 1과 2를 어떻게 보간해야 int 값을 얻을 수 있을까요? 간단한 해결책: 보간하지 않습니다. 첫 번째 정점의 값을 사용하고 두 번째와 세 번째 정점의 값은 무시합니다. 셰이더에 이를 알리는 방법은 flat 키워드를 포함하는 것입니다.
layout (location=1) flat in uint texture_id;
아니요, 여기서 값들이 인스턴스별 데이터의 일부이기 때문에 다른 정점들 사이에서 절대 달라지지 않는다는 사실은 중요하지 않습니다. 그것은 셰이더가 알지 못하는 사실입니다.
좋습니다, 그 문제를 해결했으니, 이제 ID를 사용해 봅시다.
theColour=texture(texturesamplers[texture_id],uv);
그리고:
quad.insert_visibly(TexturedInstanceData::from_matrix_and_texture(
na::Matrix4::identity(),
texture_id,
));
quad.insert_visibly(TexturedInstanceData::from_matrix_and_texture(
na::Matrix4::new_translation(&na::Vector3::new(2.0, 0., 0.3)),
second_texture_id,
));
…이렇게 해서 서로 다른 텍스처를 가진 두 개의 사각형이 생겼습니다.
세 번째 텍스처는 어떨까요?
셰이더가 우리가 보낼 텍스처의 개수를 알 필요가 없게 만들 수 있을까요?
이렇게요?
layout(set=1,binding=0) uniform sampler2D texturesamplers[];
불행히도, 그러면 변수 인덱스를 사용할 수 없습니다. (비슷한 문제가 조명에 관한 장에서 발생했지만, 거기서는 다른 방식으로 피했습니다.) 보세요:
shader_textured.frag:12: error: 'variable index' : required extension not requested: GL_EXT_nonuniform_qualifier
('variable index': 필요한 확장(GL_EXT_nonuniform_qualifier)이 요청되지 않음)
이 확장은 GLSL 확장이며, 셰이더에 다음 줄을 포함하여 요청합니다:
#extension GL_EXT_nonuniform_qualifier : require
새로운 에러 (또는 유효성 검사 레이어의 힌트):
[Debug][error][validation] "Shader requires VkPhysicalDeviceDescriptorIndexingFeatures::runtimeDescriptorArray but is not enabled on the device"
(셰이더는 VkPhysicalDeviceDescriptorIndexingFeatures::runtimeDescriptorArray가 필요하지만 장치에서 활성화되지 않았습니다)
따라서, 장치 생성 시 이 기능을 활성화해야 합니다. init_device_and_queues() (intance_device_queues.rs 파일)에서:
let indexing_features =
vk::PhysicalDeviceDescriptorIndexingFeatures::builder().runtime_descriptor_array(true);
이걸 어디에 둘까요? 이 구조체는 ExtendsDeviceCreateInfo를 구현하는데, 이는 DeviceCreateInfo에 확장으로 연결할 수 있다는 의미입니다. 확장을 연결한다는 것은 해당 push_next에 전달하는 것을 의미합니다:
let device_create_info = vk::DeviceCreateInfo::builder()
.queue_create_infos(&queue_infos)
.enabled_extension_names(&device_extension_name_pointers)
.enabled_layer_names(&layer_name_pointers)
.enabled_features(&features)
.push_next(&indexing_features);
다음 에러 메시지(이번엔 Rust 컴파일러)는 가변 참조(mutable reference)를 사용해야 한다고 알려줍니다. mut 두 개를 추가하면 끝입니다.
이것으로, 셰이더에 텍스처의 개수를 알려줄 필요가 없어졌습니다. 하지만 파이프라인을 생성할 때는 여전히 알아야 합니다. 거기서도 숫자를 없앨 수 있을까요?
디스크립터 개수가 가변적일 수 있음을 나타내는 DescriptorBindingFlags를 포함시켜 봅시다.
let descriptorset_layout_binding_descs1 = [vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(2)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build()];
let descriptor_binding_flags = [vk::DescriptorBindingFlags::VARIABLE_DESCRIPTOR_COUNT];
let mut descriptorset_layout_binding_flags =
vk::DescriptorSetLayoutBindingFlagsCreateInfo::builder()
.binding_flags(&descriptor_binding_flags);
let descriptorset_layout_info1 = vk::DescriptorSetLayoutCreateInfo::builder()
.bindings(&descriptorset_layout_binding_descs1)
.push_next(&mut descriptorset_layout_binding_flags);
let descriptorsetlayout1 = unsafe {
logical_device.create_descriptor_set_layout(&descriptorset_layout_info1, None)
}?;
이것도 push_next에 들어가는 확장입니다. 또한 다른 장치 기능의 활성화가 필요합니다:
let mut indexing_features = vk::PhysicalDeviceDescriptorIndexingFeatures::builder()
.runtime_descriptor_array(true)
.descriptor_binding_variable_descriptor_count(true);
만약 디스크립터 집합 중 하나를 바인딩하는 것을 “잊어버리면”:
if let Some(texture) = aetna.texture_storage.get(texture_id) {
// ... 첫 번째 텍스처 바인딩 코드 ...
}
/*if let Some(texture) = aetna.texture_storage.get(second_texture_id) {
// ... 두 번째 텍스처 바인딩 코드 ...
}*/
.push_next(&mut descriptorset_layout_binding_flags)가 있을 때와 없을 때 사이에 상당한 차이가 있습니다. (좋습니다, “상당한 차이”라고 해봤자 유효성 검사 레이어에서 나오는 (반복되는) 메시지 하나입니다:
[Debug][error][validation] "VkDescriptorSet 0x2c000000002c[] bound as set #1 encountered the following validation error at vkCmdDrawIndexed() time: Descriptor in binding #0 index
1 is being used in draw but has never been updated via vkUpdateDescriptorSets() or a similar call."
(#1로 바인딩된 VkDescriptorSet ... 이 vkCmdDrawIndexed() 시간에 다음 유효성 검사 오류를 만났습니다: 바인딩 #0 인덱스 1의 디스크립터가 드로우에서 사용되고 있지만 vkUpdateDescriptorSets()나 유사한 호출을 통해 업데이트된 적이 없습니다.)
— 그래도, 사라지게 하는 편이 낫습니다.)
이제 디스크립터 집합들을 개별적으로가 아니라 함께 바인딩해 봅시다. 모든 텍스처마다 긴 생성 코드를 갖지 않도록 말입니다.
(if let Some(texture) = aetna.texture_storage.get(texture_id) { 부터 시작하는) 긴 부분을 다음으로 대체합니다:
let imageinfos = aetna.texture_storage.get_descriptor_image_info();
let descriptorwrite_image = vk::WriteDescriptorSet::builder()
.dst_set(aetna.descriptor_sets_texture[aetna.swapchain.current_image])
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&imageinfos)
.build();
unsafe {
aetna
.device
.update_descriptor_sets(&[descriptorwrite_image], &[]);
}
변경 사항: 이제 디스크립터 집합 생성을 위해 builder를 사용하고 있고, 모든 집합을 한 번에 업데이트하고 있으며(imageinfos는 모든 텍스처에 대한 DescriptorImageInfo를 담고 있는 Vec입니다), 디스크립터 이미지 정보 수집을 별도의 함수(TextureStorage의 일부)로 옮겼습니다:
pub fn get_descriptor_image_info(&self) -> Vec<vk::DescriptorImageInfo> {
self.textures
.iter()
.map(|t| vk::DescriptorImageInfo {
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
image_view: t.imageview,
sampler: t.sampler,
..Default::default()
})
.collect()
}
만약 실제로 세 번째 텍스처를 포함하고 싶다면, 이 부분은 바꿀 필요가 없을 겁니다. 하지만:
let mut third_texture_id = aetna.new_texture_from_file("../gfx/image2.png")?;
를 포함시키면 여전히 다음을 유발합니다:
[Debug][error][validation] "vkUpdateDescriptorSets() failed write update validation for VkDescriptorSet ... with error: Attempting write update to descriptor set ... binding #0 with #1 descriptors being updated but this update oversteps the bounds of this binding..."
(vkUpdateDescriptorSets()가 VkDescriptorSet ...에 대한 쓰기 업데이트 유효성 검사에 실패했습니다. 오류: ... 바인딩 #0에 대해 #1개의 디스크립터를 업데이트하려고 시도했지만 이 업데이트가 이 바인딩의 경계를 벗어납니다...)
비록
let descriptorset_layout_binding_descs1 = ...
// ...
let descriptorset_layout_info1 = vk::DescriptorSetLayoutCreateInfo::builder()
.bindings(&descriptorset_layout_binding_descs1)
.push_next(&mut descriptorset_layout_binding_flags);
// ...
에서 개수가 가변적일 수 있다고 말했지만, 여전히 우리가 준수해야 할 최대 디스크립터 개수가 있습니다.
알겠습니다:
.descriptor_count(MAXIMAL_NUMBER_OF_TEXTURES)
그리고
const MAXIMAL_NUMBER_OF_TEXTURES: u32 = 1024;
(혹은 다른 숫자. 이 숫자를 초과하면 파이프라인을 재생성하는 것이 합리적일 것입니다. (또는: 텍스처의 개수가 알려진 후에야 생성하는 것.) 하지만 이 간단한 실험에서는 이 정도로 충분할 것입니다.)
물론, 그 숫자에 의존하는 또 다른 곳이 있었습니다: 디스크립터 풀의 크기입니다.
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: renderpass_and_pipeline::MAXIMAL_NUMBER_OF_TEXTURES
* swapchain.amount_of_images,
},
(참고: 이 코드가 작동하려면 상수를 pub으로 만들어야 합니다.)
그리고 이제 됐습니다. 우리는 이제 많은 양의 텍스처를 다룰 수 있습니다.
참고로, 가능한 가장 큰 숫자를 원한다면,
const MAXIMAL_NUMBER_OF_TEXTURES: u32 = u32::MAX;
는 (적어도 제 그래픽 카드에서는) 작동하지 않을 것입니다.
하지만 유효성 검사 레이어가 얼마나 가능한지 알려줄 것입니다:
[Debug][error][validation] "vkCreatePipelineLayout(): max per-stage sampler bindings count (-1) exceeds device maxPerStageDescriptorSamplers limit (1048576). ..."
(vkCreatePipelineLayout(): 스테이지당 최대 샘플러 바인딩 개수(-1)가 장치 한계인 maxPerStageDescriptorSamplers (1048576)를 초과합니다.)
음, 그렇다면:
pub const MAXIMAL_NUMBER_OF_TEXTURES: u32 = 1048576;