vk/11/12: allow gpu-deferred creation of filter chains

This commit is contained in:
chyyran 2023-02-11 02:42:33 -05:00
parent 800b6e7b4b
commit fa6cd87c60
10 changed files with 256 additions and 166 deletions

View file

@ -152,11 +152,13 @@ static GL_DEFAULT_MVP: &[f32; 16] = &[
```
### Thread safety
In general, it is **safe** to create a filter chain instance from a different thread, but drawing filter passes **must be
externally synchronized**. The exceptions to filter chain creation are in OpenGL, where creating the filter chain instance
is safe **if and only if** the thread local OpenGL context is initialized to the same context as the drawing thread, and
in Direct3D 11, where filter chain creation is thread-unsafe if the `ID3D11Device` was created with
`D3D11_CREATE_DEVICE_SINGLETHREADED`.
In general, it is **safe** to create a filter chain instance from a different thread, but drawing frames requires
**externally synchronization** of the filter chain object. Additionally, filter chain creation requires external synchronization
of the graphics devices queue where applicable, as loading LUT textures requires submission of commands to the graphics queue,
unless the `filter_chain_create_deferred` methods are used.
OpenGL has an additional restriction where creating the filter chain instance in a different thread is safe **if and only if**
the thread local OpenGL context is initialized to the same context as the drawing thread.
### Writing a librashader Runtime

View file

@ -90,6 +90,31 @@ impl FilterChainD3D11 {
device: &ID3D11Device,
preset: ShaderPreset,
options: Option<&FilterChainOptionsD3D11>,
) -> error::Result<FilterChainD3D11> {
let immediate_context = unsafe { device.GetImmediateContext()? };
unsafe { Self::load_from_preset_deferred(device, preset, &immediate_context, options) }
}
/// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
/// to the caller. This function is therefore requires no external synchronization of the
/// immediate context, as long as the immediate context is not used as the input context,
/// nor of the device, as long as the device is not single-threaded only.
///
/// ## Safety
/// The provided context must either be immediate, or immediately submitted after this function
/// returns, **before drawing frames**, or lookup textures will fail to load and the filter chain
/// will be in an invalid state.
///
/// If the context is deferred, it must be ready for command recording, and have no prior commands
/// recorded. No commands shall be recorded after, the caller must immediately call [`FinishCommandList`](https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-finishcommandlist)
/// and execute the command list on the immediate context after this function returns.
///
/// If the context is immediate, then access to the immediate context requires external synchronization.
pub unsafe fn load_from_preset_deferred(
device: &ID3D11Device,
preset: ShaderPreset,
ctx: &ID3D11DeviceContext,
options: Option<&FilterChainOptionsD3D11>,
) -> error::Result<FilterChainD3D11> {
let (passes, semantics) = HLSL::compile_preset_passes::<
GlslangCompilation,
@ -104,7 +129,7 @@ impl FilterChainD3D11 {
let immediate_context = unsafe { device.GetImmediateContext()? };
// load luts
let luts = FilterChainD3D11::load_luts(device, &immediate_context, &preset.textures)?;
let luts = FilterChainD3D11::load_luts(device, &ctx, &preset.textures)?;
let framebuffer_gen =
|| OwnedImage::new(device, Size::new(1, 1), ImageFormat::R8G8B8A8Unorm, false);

View file

@ -38,10 +38,11 @@ mod tests {
// const FILTER_PATH: &str =
// "../test/slang-shaders/handheld/console-border/gbc-lcd-grid-v2.slangp";
// "../test/null.slangp",
// const FILTER_PATH: &str = "../test/slang-shaders/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV-GLASS.slangp";
const FILTER_PATH: &str =
"../test/slang-shaders/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV-GLASS.slangp";
// const FILTER_PATH: &str = "../test/slang-shaders/test/history.slangp";
const FILTER_PATH: &str = "../test/slang-shaders/test/feedback.slangp";
// const FILTER_PATH: &str = "../test/slang-shaders/test/feedback.slangp";
// const FILTER_PATH: &str = "../test/slang-shaders/crt/crt-royale.slangp";
const IMAGE_PATH: &str = "../triangle.png";

View file

@ -43,7 +43,7 @@ use windows::Win32::Graphics::Direct3D12::{
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET,
};
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::System::Threading::{CreateEventA, ResetEvent, WaitForSingleObject};
use windows::Win32::System::Threading::{CreateEventA, WaitForSingleObject};
use windows::Win32::System::WindowsProgramming::INFINITE;
use librashader_runtime::framebuffer::FramebufferInit;
@ -51,7 +51,7 @@ use librashader_runtime::render_target::RenderTarget;
use librashader_runtime::scaling::ScaleFramebuffer;
use rayon::prelude::*;
const MIPMAP_RESERVED_WORKHEAP_DESCRIPTORS: usize = 1024;
const MIPMAP_RESERVED_WORKHEAP_DESCRIPTORS: usize = 4096;
type DxilShaderPassMeta =
ShaderPassArtifact<impl CompileReflectShader<DXIL, GlslangCompilation> + Send>;
@ -99,6 +99,7 @@ pub(crate) struct FilterCommon {
pub(crate) struct FrameResiduals {
outputs: Vec<OutputDescriptor>,
mipmaps: Vec<D3D12DescriptorHeapSlot<ResourceWorkHeap>>,
mipmap_luts: Vec<D3D12MipmapGen>,
}
impl FrameResiduals {
@ -106,9 +107,14 @@ impl FrameResiduals {
Self {
outputs: Vec::new(),
mipmaps: Vec::new(),
mipmap_luts: Vec::new(),
}
}
pub fn dispose_mipmap_gen(&mut self, mipmap: D3D12MipmapGen) {
self.mipmap_luts.push(mipmap)
}
pub fn dispose_output(&mut self, descriptor: OutputDescriptor) {
self.outputs.push(descriptor)
}
@ -149,6 +155,52 @@ impl FilterChainD3D12 {
device: &ID3D12Device,
preset: ShaderPreset,
options: Option<&FilterChainOptionsD3D12>,
) -> error::Result<FilterChainD3D12> {
unsafe {
// 1 time queue infrastructure for lut uploads
let command_pool: ID3D12CommandAllocator =
device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT)?;
let cmd: ID3D12GraphicsCommandList =
device.CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, &command_pool, None)?;
let queue: ID3D12CommandQueue =
device.CreateCommandQueue(&D3D12_COMMAND_QUEUE_DESC {
Type: D3D12_COMMAND_LIST_TYPE_DIRECT,
Priority: 0,
Flags: D3D12_COMMAND_QUEUE_FLAG_NONE,
NodeMask: 0,
})?;
let fence_event = CreateEventA(None, false, false, None)?;
let fence: ID3D12Fence = device.CreateFence(0, D3D12_FENCE_FLAG_NONE)?;
let filter_chain = Self::load_from_preset_deferred(device, preset, &cmd, options)?;
cmd.Close()?;
queue.ExecuteCommandLists(&[cmd.cast()?]);
queue.Signal(&fence, 1)?;
if fence.GetCompletedValue() < 1 {
fence.SetEventOnCompletion(1, fence_event)?;
WaitForSingleObject(fence_event, INFINITE);
CloseHandle(fence_event);
}
Ok(filter_chain)
}
}
/// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
/// to the caller. This function therefore requires no external synchronization of the device queue.
///
/// ## Safety
/// The provided command list must be ready for recording and contain no prior commands.
/// The caller is responsible for ending the command list and immediately submitting it to a
/// graphics queue. The command list must be completely executed before calling [`frame`](Self::frame).
pub unsafe fn load_from_preset_deferred(
device: &ID3D12Device,
preset: ShaderPreset,
cmd: &ID3D12GraphicsCommandList,
options: Option<&FilterChainOptionsD3D12>,
) -> error::Result<FilterChainD3D12> {
let shader_count = preset.shaders.len();
let lut_count = preset.textures.len();
@ -171,18 +223,20 @@ impl FilterChainD3D12 {
let draw_quad = DrawQuad::new(device)?;
let mut staging_heap = D3D12DescriptorHeap::new(
device,
(MAX_BINDINGS_COUNT as usize) * shader_count + 2048 + lut_count,
(MAX_BINDINGS_COUNT as usize) * shader_count
+ MIPMAP_RESERVED_WORKHEAP_DESCRIPTORS
+ lut_count,
)?;
let rtv_heap = D3D12DescriptorHeap::new(
device,
(MAX_BINDINGS_COUNT as usize) * shader_count + 2048 + lut_count,
(MAX_BINDINGS_COUNT as usize) * shader_count
+ MIPMAP_RESERVED_WORKHEAP_DESCRIPTORS
+ lut_count,
)?;
let luts = FilterChainD3D12::load_luts(device, &mut staging_heap, &preset.textures)?;
let root_signature = D3D12RootSignature::new(device)?;
let (texture_heap, sampler_heap, filters, mipmap_heap) = FilterChainD3D12::init_passes(
let (texture_heap, sampler_heap, filters, mut mipmap_heap) = FilterChainD3D12::init_passes(
device,
&root_signature,
passes,
@ -191,6 +245,17 @@ impl FilterChainD3D12 {
options.map_or(false, |o| o.force_hlsl_pipeline),
)?;
let mut residuals = FrameResiduals::new();
let luts = FilterChainD3D12::load_luts(
device,
cmd,
&mut staging_heap,
&mut mipmap_heap,
&mut residuals,
&preset.textures,
)?;
let framebuffer_gen =
|| OwnedImage::new(device, Size::new(1, 1), ImageFormat::R8G8B8A8Unorm, false);
let input_gen = || None;
@ -240,91 +305,50 @@ impl FilterChainD3D12 {
sampler_heap,
mipmap_heap,
disable_mipmaps: options.map_or(false, |o| o.force_no_mipmaps),
residuals: FrameResiduals::new(),
residuals,
})
}
fn load_luts(
device: &ID3D12Device,
heap: &mut D3D12DescriptorHeap<CpuStagingHeap>,
cmd: &ID3D12GraphicsCommandList,
staging_heap: &mut D3D12DescriptorHeap<CpuStagingHeap>,
mipmap_heap: &mut D3D12DescriptorHeap<ResourceWorkHeap>,
trash: &mut FrameResiduals,
textures: &[TextureConfig],
) -> error::Result<FxHashMap<usize, LutTexture>> {
// use separate mipgen to load luts.
let mipmap_gen = D3D12MipmapGen::new(device, true)?;
let mut work_heap: D3D12DescriptorHeap<ResourceWorkHeap> =
D3D12DescriptorHeap::new(device, u16::MAX as usize)?;
unsafe {
// 1 time queue infrastructure for lut uploads
let command_pool: ID3D12CommandAllocator =
device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT)?;
let cmd: ID3D12GraphicsCommandList =
device.CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, &command_pool, None)?;
let queue: ID3D12CommandQueue =
device.CreateCommandQueue(&D3D12_COMMAND_QUEUE_DESC {
Type: D3D12_COMMAND_LIST_TYPE_DIRECT,
Priority: 0,
Flags: D3D12_COMMAND_QUEUE_FLAG_NONE,
NodeMask: 0,
})?;
let fence_event = CreateEventA(None, false, false, None)?;
let fence: ID3D12Fence = device.CreateFence(0, D3D12_FENCE_FLAG_NONE)?;
let mut residuals = Vec::new();
let mut luts = FxHashMap::default();
let mut luts = FxHashMap::default();
for (index, texture) in textures.iter().enumerate() {
let image = Image::load(&texture.path, UVDirection::TopLeft)?;
for (index, texture) in textures.iter().enumerate() {
let image = Image::load(&texture.path, UVDirection::TopLeft)?;
let (texture, staging) = LutTexture::new(
device,
heap,
&cmd,
&image,
texture.filter_mode,
texture.wrap_mode,
texture.mipmap,
)?;
luts.insert(index, texture);
residuals.push(staging);
}
cmd.Close()?;
queue.ExecuteCommandLists(&[cmd.cast()?]);
queue.Signal(&fence, 1)?;
// Wait until finished
if fence.GetCompletedValue() < 1 {
fence.SetEventOnCompletion(1, fence_event)?;
WaitForSingleObject(fence_event, INFINITE);
ResetEvent(fence_event);
}
cmd.Reset(&command_pool, None)?;
let residuals = mipmap_gen.mipmapping_context(&cmd, &mut work_heap, |context| {
for lut in luts.values() {
lut.generate_mipmaps(context)?;
}
Ok::<(), FilterChainError>(())
})?;
//
cmd.Close()?;
queue.ExecuteCommandLists(&[cmd.cast()?]);
queue.Signal(&fence, 2)?;
//
if fence.GetCompletedValue() < 2 {
fence.SetEventOnCompletion(2, fence_event)?;
WaitForSingleObject(fence_event, INFINITE);
CloseHandle(fence_event);
}
drop(residuals);
Ok(luts)
let texture = LutTexture::new(
device,
staging_heap,
cmd,
&image,
texture.filter_mode,
texture.wrap_mode,
texture.mipmap,
)?;
luts.insert(index, texture);
}
let residual_mipmap = mipmap_gen.mipmapping_context(cmd, mipmap_heap, |context| {
for lut in luts.values() {
lut.generate_mipmaps(context)?;
}
Ok::<(), FilterChainError>(())
})?;
trash.dispose_mipmap_handles(residual_mipmap);
trash.dispose_mipmap_gen(mipmap_gen);
Ok(luts)
}
fn init_passes(

View file

@ -35,8 +35,9 @@ mod tests {
let sample = hello_triangle::d3d12_hello_triangle::Sample::new(
// "../test/slang-shaders/crt/crt-lottes.slangp",
// "../test/basic.slangp",
// "../test/slang-shaders/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV-GLASS.slangp",
"../test/slang-shaders/test/feedback.slangp",
// "../test/slang-shaders/handheld/console-border/gbc-lcd-grid-v2.slangp",
"../test/slang-shaders/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV-GLASS.slangp",
// "../test/slang-shaders/test/feedback.slangp",
// "../test/slang-shaders/test/history.slangp",
// "../test/slang-shaders/crt/crt-royale.slangp",
// "../test/slang-shaders/vhs/VHSPro.slangp",

View file

@ -27,6 +27,7 @@ pub struct LutTexture {
resource: ID3D12Resource,
view: InputTexture,
miplevels: Option<u16>,
_staging: ID3D12Resource,
}
impl LutTexture {
@ -38,7 +39,7 @@ impl LutTexture {
filter: FilterMode,
wrap_mode: WrapMode,
mipmap: bool,
) -> error::Result<(LutTexture, ID3D12Resource)> {
) -> error::Result<LutTexture> {
let miplevels = source.size.calculate_miplevels() as u16;
let mut desc = D3D12_RESOURCE_DESC {
Dimension: D3D12_RESOURCE_DIMENSION_TEXTURE2D,
@ -186,14 +187,12 @@ impl LutTexture {
filter,
wrap_mode,
);
Ok((
LutTexture {
resource,
view,
miplevels: if mipmap { Some(miplevels) } else { None },
},
upload,
))
Ok(LutTexture {
resource,
_staging: upload,
view,
miplevels: if mipmap { Some(miplevels) } else { None },
})
}
pub fn generate_mipmaps(&self, gen_mips: &mut MipmapGenContext) -> error::Result<()> {

View file

@ -4,6 +4,7 @@ use librashader_preprocess::PreprocessError;
use librashader_presets::ParsePresetError;
use librashader_reflect::error::{ShaderCompileError, ShaderReflectError};
use librashader_runtime::image::ImageError;
use std::convert::Infallible;
use thiserror::Error;
/// Cumulative error type for Vulkan filter chains.
@ -31,5 +32,11 @@ pub enum FilterChainError {
AllocationDoesNotExist,
}
impl From<Infallible> for FilterChainError {
fn from(_value: Infallible) -> Self {
panic!("uninhabited error")
}
}
/// Result type for Vulkan filter chains.
pub type Result<T> = std::result::Result<T, FilterChainError>;

View file

@ -28,6 +28,7 @@ use librashader_runtime::uniforms::UniformStorage;
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use std::collections::VecDeque;
use std::convert::Infallible;
use std::path::Path;
use std::sync::Arc;
@ -218,27 +219,109 @@ impl Drop for FrameResiduals {
impl FilterChainVulkan {
/// Load the shader preset at the given path into a filter chain.
pub fn load_from_path(
vulkan: impl TryInto<VulkanObjects, Error = FilterChainError>,
pub fn load_from_path<V, E>(
vulkan: V,
path: impl AsRef<Path>,
options: Option<&FilterChainOptionsVulkan>,
) -> error::Result<FilterChainVulkan> {
) -> error::Result<FilterChainVulkan>
where
V: TryInto<VulkanObjects, Error = E>,
FilterChainError: From<E>,
{
// load passes from preset
let preset = ShaderPreset::try_parse(path)?;
Self::load_from_preset(vulkan, preset, options)
}
/// Load a filter chain from a pre-parsed `ShaderPreset`.
pub fn load_from_preset(
vulkan: impl TryInto<VulkanObjects, Error = FilterChainError>,
pub fn load_from_preset<V, E>(
vulkan: V,
preset: ShaderPreset,
options: Option<&FilterChainOptionsVulkan>,
) -> error::Result<FilterChainVulkan> {
) -> error::Result<FilterChainVulkan>
where
V: TryInto<VulkanObjects, Error = E>,
FilterChainError: From<E>,
{
let vulkan = vulkan.try_into().map_err(|e| e.into())?;
let device = Arc::clone(&vulkan.device);
let queue = vulkan.queue.clone();
let command_pool = unsafe {
device.create_command_pool(
&vk::CommandPoolCreateInfo::builder()
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
.build(),
None,
)?
};
let command_buffer = unsafe {
// panic safety: command buffer count = 1
device.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::builder()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1)
.build(),
)?[0]
};
unsafe {
device.begin_command_buffer(
command_buffer,
&vk::CommandBufferBeginInfo::builder()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT)
.build(),
)?
}
let filter_chain = unsafe {
Self::load_from_preset_deferred::<_, Infallible>(
vulkan,
preset,
command_buffer,
options,
)?
};
unsafe {
device.end_command_buffer(command_buffer)?;
let buffers = [command_buffer];
let submits = [vk::SubmitInfo::builder().command_buffers(&buffers).build()];
device.queue_submit(queue, &submits, vk::Fence::null())?;
device.queue_wait_idle(queue)?;
device.free_command_buffers(command_pool, &buffers);
device.destroy_command_pool(command_pool, None);
}
Ok(filter_chain)
}
/// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
/// to the caller. This function therefore requires no external synchronization of the device queue.
///
/// ## Safety
/// The provided command buffer must be ready for recording and contain no prior commands.
/// The caller is responsible for ending the command buffer and immediately submitting it to a
/// graphics queue. The command buffer must be completely executed before calling [`frame`](Self::frame).
pub unsafe fn load_from_preset_deferred<V, E>(
vulkan: V,
preset: ShaderPreset,
cmd: vk::CommandBuffer,
options: Option<&FilterChainOptionsVulkan>,
) -> error::Result<FilterChainVulkan>
where
V: TryInto<VulkanObjects, Error = E>,
FilterChainError: From<E>,
{
let (passes, semantics) = SPIRV::compile_preset_passes::<
GlslangCompilation,
FilterChainError,
>(preset.shaders, &preset.textures)?;
let device = vulkan.try_into()?;
let device = vulkan.try_into().map_err(From::from)?;
let mut frames_in_flight = options.map_or(0, |o| o.frames_in_flight);
if frames_in_flight == 0 {
@ -254,7 +337,7 @@ impl FilterChainVulkan {
options.map_or(false, |o| o.use_render_pass),
)?;
let luts = FilterChainVulkan::load_luts(&device, &preset.textures)?;
let luts = FilterChainVulkan::load_luts(&device, cmd, &preset.textures)?;
let samplers = SamplerSet::new(&device.device)?;
let framebuffer_gen =
@ -383,66 +466,21 @@ impl FilterChainVulkan {
fn load_luts(
vulkan: &VulkanObjects,
command_buffer: vk::CommandBuffer,
textures: &[TextureConfig],
) -> error::Result<FxHashMap<usize, LutTexture>> {
let mut luts = FxHashMap::default();
let command_pool = unsafe {
vulkan.device.create_command_pool(
&vk::CommandPoolCreateInfo::builder()
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
.build(),
None,
)?
};
let command_buffer = unsafe {
// panic safety: command buffer count = 1
vulkan.device.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::builder()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1)
.build(),
)?[0]
};
unsafe {
vulkan.device.begin_command_buffer(
command_buffer,
&vk::CommandBufferBeginInfo::builder()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT)
.build(),
)?
}
for (index, texture) in textures.iter().enumerate() {
let image = Image::load(&texture.path, UVDirection::TopLeft)?;
let texture = LutTexture::new(vulkan, command_buffer, image, texture)?;
luts.insert(index, texture);
}
unsafe {
vulkan.device.end_command_buffer(command_buffer)?;
let buffers = [command_buffer];
let submits = [vk::SubmitInfo::builder().command_buffers(&buffers).build()];
vulkan
.device
.queue_submit(vulkan.queue, &submits, vk::Fence::null())?;
vulkan.device.queue_wait_idle(vulkan.queue)?;
vulkan.device.free_command_buffers(command_pool, &buffers);
vulkan.device.destroy_command_pool(command_pool, None);
}
Ok(luts)
}
// image must be in SHADER_READ_OPTIMAL
pub fn push_history(
fn push_history(
&mut self,
input: &VulkanImage,
cmd: vk::CommandBuffer,

View file

@ -44,8 +44,8 @@ mod tests {
let filter = FilterChainVulkan::load_from_path(
&base,
// "../test/slang-shaders/crt/crt-royale.slangp",
// "../test/slang-shaders/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV-GLASS.slangp",
"../test/slang-shaders/test/feedback.slangp",
"../test/slang-shaders/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV-GLASS.slangp",
// "../test/slang-shaders/test/feedback.slancargogp",
// "../test/basic.slangp",
Some(&FilterChainOptionsVulkan {
frames_in_flight: 3,

View file

@ -8,10 +8,8 @@ use librashader_runtime::image::{Image, BGRA8};
use librashader_runtime::scaling::MipmapSize;
pub(crate) struct LutTexture {
#[allow(dead_code)]
memory: VulkanImageMemory,
#[allow(dead_code)]
staging: VulkanBuffer,
_memory: VulkanImageMemory,
_staging: VulkanBuffer,
pub image: InputImage,
}
@ -46,11 +44,6 @@ impl LutTexture {
let memory = unsafe {
let mem_reqs = vulkan.device.get_image_memory_requirements(texture);
// let mem_type = util::find_vulkan_memory_type(
// &vulkan.memory_properties,
// mem_reqs.memory_type_bits,
// vk::MemoryPropertyFlags::DEVICE_LOCAL,
// )?;
VulkanImageMemory::new(&vulkan.device, &vulkan.alloc, mem_reqs, &texture)?
};
@ -223,8 +216,8 @@ impl LutTexture {
}
Ok(LutTexture {
memory,
staging,
_memory: memory,
_staging: staging,
image: InputImage {
image_view: texture_view,
image: VulkanImage {