아직 셰이딩(shading)이 끝나지도 않았고, 더 근본적이지만 아직 다루지 않은 주제들도 남아있습니다. 하지만 저는 정말로 스크린샷을 찍는 방법을 가지고 싶어서, 이번 챕터에서는 우리 프로그램에 스크린샷 기능을 추가해 보려고 합니다.
(네, 제 컴퓨터에는 스크린샷 프로그램이 설치되어 있습니다. 하지만 자기 프로그램이 직접 화면을 저장하는 것과는 다른 이야기겠죠?)
자, 스크린샷입니다. 무엇이 필요할까요?
그래픽스 파이프라인의 끝에서, 우리는 모든 색상 정보를 어떤 프레임버퍼/이미지에 쓰고 화면에 표시합니다. 우리는 이 이미지에서 데이터를 직접 읽을 수 없습니다. 이 이미지를 애플리케이션에서 읽을 수 있는 다른 이미지로 복사하고, 최종적으로 파일에 저장할 방법이 필요합니다.
작은 단계부터 시작해 봅시다.
winit::event::VirtualKeyCode::F12 => {
println!("screenshot");
}
이 단계는 좀 너무 작네요.
winit::event::VirtualKeyCode::F12 => {
println!("screenshot");
screenshot(&aetna).expect("screenshot trouble");
}
좋습니다. 이제 우리가 작업할 함수를 함께 만들어 봅시다.
fn screenshot(aetna: &Aetna) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
이 함수는 Result를 반환하는데, 실패할 수 있는 여러 함수 호출이 있을 것이기 때문입니다.
이 함수에서 우리가 정확히 무엇을 하든, 그것은 GPU에 명령을 보내는 일로 구성될 것입니다. 그중 핵심은 “이 이미지를 복사하라” (…우리 프로그램에서 사용하고 스크린샷으로 저장할 수 있는 어떤 변수로)는 명령일 것입니다. 명령을 보내려면 커맨드 버퍼가 필요합니다. 하나 만들어 봅시다.
let commandbuf_allocate_info = vk::CommandBufferAllocateInfo::builder()
.command_pool(aetna.pools.commandpool_graphics)
.command_buffer_count(1);
let copybuffer = unsafe {
aetna
.device
.allocate_command_buffers(&commandbuf_allocate_info)
}
.unwrap()[0];
let cmdbegininfo =
vk::CommandBufferBeginInfo::builder().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
unsafe { aetna.device.begin_command_buffer(copybuffer, &cmdbegininfo) }?;
다시 한번, 우리는 이미지를 다룰 것이고, 그래픽스용 큐 패밀리(와 그에 해당하는 커맨드 풀)를 사용할 것이며, 하나의 커맨드
버퍼가 필요합니다. 우리는 그것을 생성하고( .allocate_command_buffers는 원래 여러 커맨드 버퍼를 동시에 생성하기 위한 것이지만 우리는 하나만 필요합니다),
이 커맨드 버퍼에 대한 명령어 기록을 시작합니다. (생성을 위해 이전에 만들었던 함수를 사용할 수도 있지만, 이 코드가 충분히 짧아 보입니다.)
CommandBufferBeginInfo에서 우리는 이 버퍼를 한 번만 제출할 것이라는 플래그를 설정합니다.
다음으로, 이미지가 필요합니다. 이 이미지는 CPU에서 접근 가능해야 하며(AllocationCreateInfo에서 MemoryUsage::GpuToCpu를 사용), 이것이 이 이미지를 만드는 주된 목적입니다.
또한 전송(실제로는 복사) 작업의 대상이 될 수 있어야 하며, 이를 .usage로 명시합니다.
let ici = vk::ImageCreateInfo::builder()
.format(vk::Format::R8G8B8A8_UNORM)
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width: aetna.swapchain.extent.width,
height: aetna.swapchain.extent.height,
depth: 1,
})
.array_layers(1)
.mip_levels(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::LINEAR)
.usage(vk::ImageUsageFlags::TRANSFER_DST)
.initial_layout(vk::ImageLayout::UNDEFINED);
let allocinfo = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::GpuToCpu,
..Default::default()
};
let (destination_image, dst_alloc, _allocinfo) =
aetna.allocator.create_image(&ici, &allocinfo)?;
생성 시, 이 새로운 이미지는 UNDEFINED 레이아웃을 가집니다. 우리는 이 이미지가 전송 작업의 대상이 되기에 적합한 메모리 레이아웃을 갖기를 원합니다. (그리고 아닙니다, 후자는 ImageCreateInfo에서 선택할 수 있는 레이아웃이 아닙니다.)
이 레이아웃을 변경하는 방법은 ImageMemoryBarrier를 설정하는 것입니다. 이는 이미지 접근을 제어하고 레이아웃 전환에 유용한 동기화 명령입니다. 이전 레이아웃과 새 레이아웃, 이미지(그리고 특별한 설정이 없는 서브리소스 범위), 접근 마스크(전환 전후에 어떤 작업이 수행되기를 원하는지), 그리고 이 전환이 발생해야 하는 파이프라인 단계가 필요합니다(후자는 이미 cmd_pipeline_barrier의 인자에 포함되어 있습니다):
let barrier = vk::ImageMemoryBarrier::builder()
.image(destination_image)
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
이후에, destination_image는 복사 명령으로부터 그림을 받을 준비가 됩니다.
하지만 우리가 복사하려는 원본 이미지(스왑체인 이미지 중 하나)는 잘못된 레이아웃을 가지고 있습니다. 우리는 비슷한 레이아웃 전환을 통해 전송의 원본으로 적합하게 만듭니다(ImageLayout::TRANSFER_SRC_OPTIMAL):
let source_image = aetna.swapchain.images[aetna.swapchain.current_image];
let barrier = vk::ImageMemoryBarrier::builder()
.image(source_image)
.src_access_mask(vk::AccessFlags::MEMORY_READ)
.dst_access_mask(vk::AccessFlags::TRANSFER_READ)
.old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
이것은 유효성 검사 레이어(validation layers)가 마땅히 불평할 만한 부분입니다. 우리가 이 이미지를 생성할 때, 데이터 전송의 원본으로 사용하겠다는 말을 한 적이 없습니다. (당연하죠. 그때는 몰랐으니까요.)
하지만 우리는 이것을 수정할 수 있습니다: swapchain.rs 파일에서, swapchain_create_info를 정의하는 부분의 usage 관련 라인을 다음과 같이 수정합니다.
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC)
아무튼, 레이아웃 전환이 완료되면 복사 명령을 사용할 수 있습니다. 물론 (이건 Vulkan이니까요!), 복사 명령을 사용한다는 것은 우리가 복사하려는 내용에 대한 더 상세한 지침을 먼저 작성해야 한다는 것을 의미합니다.
let zero_offset = vk::Offset3D::default();
let copy_area = vk::ImageCopy::builder()
.src_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
})
.src_offset(zero_offset)
.dst_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
})
.dst_offset(zero_offset)
.extent(vk::Extent3D {
width: aetna.swapchain.extent.width,
height: aetna.swapchain.extent.height,
depth: 1,
})
.build();
그리고 나서 복사할 수 있습니다:
unsafe {
aetna.device.cmd_copy_image(
copybuffer,
source_image,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
destination_image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[copy_area],
)
};
그 후에, 우리는 destination_image에서 데이터를 읽기를 원하므로, 또다시 다른 이미지 레이아웃이 필요합니다. 따라서: 또 다른 이미지 메모리 배리어입니다.
let barrier = vk::ImageMemoryBarrier::builder()
.image(destination_image)
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::MEMORY_READ)
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.new_layout(vk::ImageLayout::GENERAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
그리고 source_image 역시 원래 있어야 할 레이아웃으로 되돌려 놓아야 합니다.
let barrier = vk::ImageMemoryBarrier::builder()
.image(source_image)
.src_access_mask(vk::AccessFlags::TRANSFER_READ)
.dst_access_mask(vk::AccessFlags::MEMORY_READ)
.old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
이것이 우리가 GPU 측에서 해야 할 일입니다. 아직 destination_image에서 정보를 읽지는 않았지만, (이 이미지에 어떤 종류의 메모리 사용법을 선택했는지 기억하세요!) 이것은 CPU에서 접근 가능합니다.
다시 말해, 우리는 GPU에 대한 명령을 마쳤습니다.
unsafe { aetna.device.end_command_buffer(copybuffer) }?;
그런 다음 이 커맨드 버퍼를 제출해야 합니다. 그리고 destination_image를 읽기 전에, 위의 모든 작업이 일어날 때까지 기다려야 합니다. (지금까지는 단지 우리가 일어나기를 바라는 희망 사항 목록에 불과합니다.)
이 기다림을 위해 펜스(fence)를 생성합니다.
그런 다음 커맨드 버퍼를 제출합니다.
let submit_infos = [vk::SubmitInfo::builder()
.command_buffers(&[copybuffer])
.build()];
let fence = unsafe {
aetna
.device
.create_fence(&vk::FenceCreateInfo::default(), None)
}?;
unsafe {
aetna
.device
.queue_submit(aetna.queues.graphics_queue, &submit_infos, fence)
}?;
그리고 기다립니다.
unsafe { aetna.device.wait_for_fences(&[fence], true, std::u64::MAX) }?;
그리고 더 이상 커맨드 버퍼나 펜스가 필요 없으니, 정리합시다.
unsafe { aetna.device.destroy_fence(fence, None) };
unsafe {
aetna
.device
.free_command_buffers(aetna.pools.commandpool_graphics, &[copybuffer])
};
다음 목표: 이미지에서 데이터를 꺼내 Vec::<u8> 같은 곳에 넣기.
먼저, 메모리를 map핑하여 접근할 수 있도록 합니다.
let source_ptr = aetna.allocator.map_memory(&dst_alloc)? as *mut u8;
그다음 이 바이트들을 복사하고 싶은데, 벡터의 크기는 얼마나 되어야 할까요?
물어봅시다:
let subresource_layout = unsafe {
aetna.device.get_image_subresource_layout(
destination_image,
vk::ImageSubresource {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
array_layer: 0,
},
)
};
그러면 subresource_layout.size가 바이트 단위의 크기를 알려줍니다; subresource_layout에는 행(row) 간의 바이트 수 등도 포함되어 있습니다.
이제 데이터를 손에 넣을 시간입니다. 벡터를 만들고 복사합니다.
let mut data = Vec::<u8>::with_capacity(subresource_layout.size as usize);
unsafe {
std::ptr::copy(
source_ptr,
data.as_mut_ptr(),
subresource_layout.size as usize,
);
data.set_len(subresource_layout.size as usize);
}
(포인터를 통한 복사는 unsafe 영역에서 일어나므로, data의 길이를 수동으로 설정합니다.)
우리가 얻은 것을 한번 살펴봅시다.
dbg!(&data[0..20]);
(전체 벡터는 너무 기니 보지 않는 게 좋습니다…)
[src/main.rs:387] &data[0..20] = [
20,
0,
0,
255,
20,
0,
0,
255,
20,
0,
0,
255,
20,
0,
0,
255,
20,
0,
0,
255,
]
좋습니다. 창의 구석에서는 구체가 아닌 배경이 보여야 하고, 실제로 [20,0,0,255] 값이 반복되며 모든 픽셀이 같은 색을 가집니다.
Aetna의 update_commandbuffer 함수를 보면,
vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.0, 0.0, 0.08, 1.0],
},
},
라는 코드를 찾을 수 있고, 이 float 값을 0에서 255 범위의 u8로 변환하면, 이 clear 색상은 [0,0,20,255]에 해당합니다. 순서를 제외하면 맞는 것 같습니다. 우리의 Vec<u8>에 들어 있는 것은 RGBA가 아니라 BGRA입니다. 하지만 우리가
.format(vk::Format::R8G8B8A8_UNORM)
라고 destination_image의 ImageCreateInfo에서 말하지 않았나요? 그랬습니다. 하지만 원본 이미지는 Format::B8G8R8A8이었고, 레이아웃 전환과 cmd_copy_image는 색상 형식을 변환하지 않습니다.
이제 이미지는 더 이상 필요 없으니, 다시 정리합시다: 메모리 언매핑과 이미지 파괴:
aetna.allocator.unmap_memory(&dst_alloc)?;
aetna
.allocator
.destroy_image(destination_image, &dst_alloc)?;
마지막 단계: u8 벡터를 저장된 스크린샷으로 어떻게 바꿀까요? 값을 파일에 (작은 헤더와 함께) 덤프해서 거의 바로 .bmp 파일을 만들 수도 있겠지만, 이미지용 크레이트(crate)를 사용합시다.
Cargo.toml에 새로운 의존성을 추가합니다:
image = "0.23.4"
우리 벡터를 ImageBuffer로 바꿉니다. BGRA 형식을 사용하도록 합니다.
let screen: image::ImageBuffer<image::Bgra<u8>, _> = image::ImageBuffer::from_raw(
aetna.swapchain.extent.width,
aetna.swapchain.extent.height,
data,
)
.expect("ImageBuffer creation");
BGRA 이미지는 직접 저장이 지원되지 않지만, 이미지를 RGBA로 쉽게 변환할 수 있고, 그 후 저장하는 것은 매우 짧은 명령입니다.
let screen_image = image::DynamicImage::ImageBgra8(screen).to_rgba();
screen_image.save("screenshot.jpg")?;
그리고 결과는:

이제 스크린샷을 삽화로 사용할 수 있을 것 같습니다.
마지막으로, 전체 스크린샷 함수입니다:
fn screenshot(aetna: &Aetna) -> Result<(), Box<dyn std::error::Error>> {
let commandbuf_allocate_info = vk::CommandBufferAllocateInfo::builder()
.command_pool(aetna.pools.commandpool_graphics)
.command_buffer_count(1);
let copybuffer = unsafe {
aetna
.device
.allocate_command_buffers(&commandbuf_allocate_info)
}
.unwrap()[0];
let cmdbegininfo =
vk::CommandBufferBeginInfo::builder().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
unsafe { aetna.device.begin_command_buffer(copybuffer, &cmdbegininfo) }?;
let ici = vk::ImageCreateInfo::builder()
.format(vk::Format::R8G8B8A8_UNORM)
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width: aetna.swapchain.extent.width,
height: aetna.swapchain.extent.height,
depth: 1,
})
.array_layers(1)
.mip_levels(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::LINEAR)
.usage(vk::ImageUsageFlags::TRANSFER_DST)
.initial_layout(vk::ImageLayout::UNDEFINED);
let allocinfo = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::GpuToCpu,
..Default::default()
};
let (destination_image, dst_alloc, _allocinfo) =
aetna.allocator.create_image(&ici, &allocinfo)?;
let barrier = vk::ImageMemoryBarrier::builder()
.image(destination_image)
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
let source_image = aetna.swapchain.images[aetna.swapchain.current_image];
let barrier = vk::ImageMemoryBarrier::builder()
.image(source_image)
.src_access_mask(vk::AccessFlags::MEMORY_READ)
.dst_access_mask(vk::AccessFlags::TRANSFER_READ)
.old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
let zero_offset = vk::Offset3D::default();
let copy_area = vk::ImageCopy::builder()
.src_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
})
.src_offset(zero_offset)
.dst_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
})
.dst_offset(zero_offset)
.extent(vk::Extent3D {
width: aetna.swapchain.extent.width,
height: aetna.swapchain.extent.height,
depth: 1,
})
.build();
unsafe {
aetna.device.cmd_copy_image(
copybuffer,
source_image,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
destination_image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[copy_area],
)
};
let barrier = vk::ImageMemoryBarrier::builder()
.image(destination_image)
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::MEMORY_READ)
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.new_layout(vk::ImageLayout::GENERAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
let barrier = vk::ImageMemoryBarrier::builder()
.image(source_image)
.src_access_mask(vk::AccessFlags::TRANSFER_READ)
.dst_access_mask(vk::AccessFlags::MEMORY_READ)
.old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
aetna.device.cmd_pipeline_barrier(
copybuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
unsafe { aetna.device.end_command_buffer(copybuffer) }?;
let submit_infos = [vk::SubmitInfo::builder()
.command_buffers(&[copybuffer])
.build()];
let fence = unsafe {
aetna
.device
.create_fence(&vk::FenceCreateInfo::default(), None)
}?;
unsafe {
aetna
.device
.queue_submit(aetna.queues.graphics_queue, &submit_infos, fence)
}?;
unsafe { aetna.device.wait_for_fences(&[fence], true, std::u64::MAX) }?;
unsafe { aetna.device.destroy_fence(fence, None) };
unsafe {
aetna
.device
.free_command_buffers(aetna.pools.commandpool_graphics, &[copybuffer])
};
let source_ptr = aetna.allocator.map_memory(&dst_alloc)? as *mut u8;
let subresource_layout = unsafe {
aetna.device.get_image_subresource_layout(
destination_image,
vk::ImageSubresource {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
array_layer: 0,
},
)
};
let mut data = Vec::<u8>::with_capacity(subresource_layout.size as usize);
unsafe {
std::ptr::copy(
source_ptr,
data.as_mut_ptr(),
subresource_layout.size as usize,
);
data.set_len(subresource_layout.size as usize);
}
aetna.allocator.unmap_memory(&dst_alloc)?;
aetna
.allocator
.destroy_image(destination_image, &dst_alloc)?;
let screen: image::ImageBuffer<image::Bgra<u8>, _> = image::ImageBuffer::from_raw(
aetna.swapchain.extent.width,
aetna.swapchain.extent.height,
data,
)
.expect("ImageBuffer creation");
let screen_image = image::DynamicImage::ImageBgra8(screen).to_rgba();
screen_image.save("screenshot.jpg")?;
Ok(())
}