summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/graphics/frame.rs68
-rw-r--r--src/graphics/model.rs12
-rw-r--r--src/graphics/permanent.rs120
-rw-r--r--src/graphics/render.rs105
-rw-r--r--src/graphics/scene.rs18
-rw-r--r--src/graphics/texture.rs35
-rw-r--r--src/main.rs22
-rw-r--r--src/shader_data.rs6
8 files changed, 284 insertions, 102 deletions
diff --git a/src/graphics/frame.rs b/src/graphics/frame.rs
index d8d4bcd..82e5c1c 100644
--- a/src/graphics/frame.rs
+++ b/src/graphics/frame.rs
@@ -1,6 +1,6 @@
 #![deny(unsafe_code)]
 use crate::error::*;
-use crate::graphics::{ Permanent, ForReinit, WindowDressing, Texture };
+use crate::graphics::{ Permanent, ForReinit, WindowDressing };
 use crate::graphics::util::allocate_buffer;
 use crate::shader_data::UniformBlock;
 
@@ -27,6 +27,10 @@ pub struct Frame {
   pub uniform_buffer: vk::Buffer,
   pub uniform_buffer_memory: vk::DeviceMemory,
 
+  //   Notice that our descriptor logic is split between the descriptor sets,
+  // which are per-frame and defined here, and the descriptor update template
+  // for the push descriptors, which is shared by all frames and is kept in
+  // Render.
   pub descriptor_set: vk::DescriptorSet,
 }
 
@@ -39,14 +43,16 @@ impl Frame {
   // an indidivual Frame.
   #[allow(unsafe_code)]
   pub fn new(permanent: &Permanent, for_reinit: &ForReinit,
-             window_dressing: &WindowDressing, texture: &Texture,
-             render_pass: &vk::RenderPass)
+             window_dressing: &WindowDressing, render_pass: &vk::RenderPass)
       -> Result<Vec<Self>>
   {
     let mut frames = Vec::new();
 
+    //   The amount of interesting work done in the top level methods is a bit
+    // more for Frame than it is for our various other state objects, so we
+    // consolidate the implementation details in reinit().
     Frame::reinit(&mut frames, permanent, for_reinit, window_dressing,
-                  texture, render_pass)?;
+                  render_pass)?;
 
     Ok(frames)
   }
@@ -55,7 +61,7 @@ impl Frame {
   #[allow(unsafe_code)]
   pub fn reinit(frames: &mut Vec<Self>, permanent: &Permanent,
                 for_reinit: &ForReinit, window_dressing: &WindowDressing,
-                texture: &Texture, render_pass: &vk::RenderPass)
+                render_pass: &vk::RenderPass)
       -> Result<()>
   {
     Frame::destroy_replaceable(frames, permanent);
@@ -63,7 +69,8 @@ impl Frame {
     let instance = &permanent.instance;
     let device = &permanent.device;
     let primary_command_pool = &permanent.primary_command_pool;
-    let descriptor_set_layout = &for_reinit.descriptor_set_layout;
+    let primary_descriptor_set_layout
+            = &for_reinit.primary_descriptor_set_layout;
     let swapchain = &window_dressing.swapchain;
     let color_image_view = &window_dressing.color_image_view;
     let depth_image_view = &window_dressing.depth_image_view;
@@ -79,7 +86,7 @@ impl Frame {
                                                    primary_command_pool)?;
 
     let descriptor_sets = allocate_descriptor_sets(
-            count, device, descriptor_set_layout, descriptor_pool)?;
+            count, device, primary_descriptor_set_layout, descriptor_pool)?;
 
     for (index, color_resolve_image_view)
         in swapchain.image_views.iter().enumerate()
@@ -95,7 +102,7 @@ impl Frame {
               = init_uniform_buffer(instance, device)?;
 
       configure_descriptor_set(&mut descriptor_set, device, &uniform_buffer,
-                               &texture.image_view, sampler)?;
+                               sampler)?;
 
       frames.push(Frame {
         framebuffer, command_buffer, uniform_buffer, uniform_buffer_memory,
@@ -189,6 +196,20 @@ fn allocate_command_buffers(count: usize, device: &Device,
 }
 
 
+fn init_uniform_buffer(instance: &Instance, device: &Device)
+    -> Result<(vk::Buffer, vk::DeviceMemory)>
+{
+  let (buffer, memory) = allocate_buffer(
+          instance, device,
+          size_of::<UniformBlock<f32>>() as vk::DeviceSize,
+          vk::BufferUsageFlags::UNIFORM_BUFFER,
+          vk::MemoryPropertyFlags::HOST_COHERENT
+          | vk::MemoryPropertyFlags::HOST_VISIBLE)?;
+
+  Ok((buffer, memory))
+}
+
+
 #[allow(unsafe_code)]
 fn allocate_descriptor_sets(count: usize, device: &Device,
                             descriptor_set_layout: &vk::DescriptorSetLayout,
@@ -211,7 +232,6 @@ fn allocate_descriptor_sets(count: usize, device: &Device,
 fn configure_descriptor_set(descriptor_set: &mut vk::DescriptorSet,
                             device: &Device,
                             uniform_buffer: &vk::Buffer,
-                            texture_image_view: &vk::ImageView,
                             sampler: &vk::Sampler)
     -> Result<()>
 {
@@ -238,20 +258,7 @@ fn configure_descriptor_set(descriptor_set: &mut vk::DescriptorSet,
           .descriptor_type(vk::DescriptorType::SAMPLER)
           .image_info(&sampler_image_info_list);
 
-  let texture_image_info = vk::DescriptorImageInfo::builder()
-          .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
-          .image_view(*texture_image_view);
-  let texture_image_info_list = [texture_image_info];
-  let texture_write_info = vk::WriteDescriptorSet::builder()
-          .dst_set(*descriptor_set)
-          .dst_binding(2)
-          .dst_array_element(0)
-          .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
-          .image_info(&texture_image_info_list);
-
-  let write_info_list = [
-          uniform_block_write_info, sampler_write_info, texture_write_info
-  ];
+  let write_info_list = [ uniform_block_write_info, sampler_write_info ];
   let copy_info_list: [vk::CopyDescriptorSet; 0] = [];
 
   unsafe {
@@ -261,18 +268,3 @@ fn configure_descriptor_set(descriptor_set: &mut vk::DescriptorSet,
   Ok(())
 }
 
-
-fn init_uniform_buffer(instance: &Instance, device: &Device)
-    -> Result<(vk::Buffer, vk::DeviceMemory)>
-{
-  let (buffer, memory) = allocate_buffer(
-          instance, device,
-          size_of::<UniformBlock<f32>>() as vk::DeviceSize,
-          vk::BufferUsageFlags::UNIFORM_BUFFER,
-          vk::MemoryPropertyFlags::HOST_COHERENT
-          | vk::MemoryPropertyFlags::HOST_VISIBLE)?;
-
-  Ok((buffer, memory))
-}
-
-
diff --git a/src/graphics/model.rs b/src/graphics/model.rs
index 9c62d51..096de86 100644
--- a/src/graphics/model.rs
+++ b/src/graphics/model.rs
@@ -61,8 +61,7 @@ impl Model {
 
   #[allow(unsafe_code)]
   pub fn render(&self, time: f32, device: &Device, render: &Render,
-                command_buffer: &vk::CommandBuffer,
-                descriptor_set: &vk::DescriptorSet)
+                command_buffer: &vk::CommandBuffer)
   {
     unsafe {
       device.cmd_bind_vertex_buffers(*command_buffer, 0,
@@ -74,15 +73,6 @@ impl Model {
                                    0, vk::IndexType::UINT32)
     };
 
-    unsafe {
-      device.cmd_bind_descriptor_sets(*command_buffer,
-                                      vk::PipelineBindPoint::GRAPHICS,
-                                      render.pipeline_layout,
-                                      0,
-                                      &[*descriptor_set],
-                                      &[])
-    };
-
     let scale = Vec3::new(1.0, 1.0, 1.0);
     let model = Transformation {
       rotation: Vec4::rotation_quaternion(&Vec3::new(0.0, 1.0, 0.0),
diff --git a/src/graphics/permanent.rs b/src/graphics/permanent.rs
index b39dbdd..6c4e02d 100644
--- a/src/graphics/permanent.rs
+++ b/src/graphics/permanent.rs
@@ -8,7 +8,7 @@ use vulkanalia::bytecode::Bytecode;
 use vulkanalia::loader::{ LibloadingLoader, LIBRARY };
 use vulkanalia::vk::{ self, HasBuilder,
                       ApplicationInfo, InstanceCreateInfo,
-                      DeviceV1_0, EntryV1_0, InstanceV1_0,
+                      DeviceV1_0, EntryV1_0, InstanceV1_0, InstanceV1_1,
                       ExtDebugUtilsExtensionInstanceCommands,
                       KhrSurfaceExtensionInstanceCommands };
 use winit::dpi::LogicalSize;
@@ -16,6 +16,7 @@ use winit::event_loop::ActiveEventLoop;
 use winit::window::{ Window, WindowAttributes };
 
 
+const VULKAN_MINIMUM_VERSION: Version = Version::new(1, 4, 0);
 const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216);
 
 
@@ -135,7 +136,8 @@ impl Permanent {
         = init_vulkan_device(&instance, &surface,
                              enable_validation, enable_portability)?;
 
-    let descriptor_set_layout = init_descriptor_set_layout(&device)?;
+    let (primary_descriptor_set_layout, push_descriptor_set_layout)
+        = init_descriptor_set_layouts(&device)?;
 
     let (primary_command_pool, transient_command_pool)
             = init_command_pools(&device, &indices)?;
@@ -145,7 +147,8 @@ impl Permanent {
       graphics_queue, presentation_queue,
       primary_command_pool, transient_command_pool,
     }, ForReinit {
-      indices, sample_count, descriptor_set_layout,
+      indices, sample_count,
+      primary_descriptor_set_layout, push_descriptor_set_layout,
     }, enable_anisotropy, enable_swapchain))
   }
 
@@ -236,7 +239,8 @@ impl Permanent {
 pub struct ForReinit {
   pub indices: QueueFamilyIndices,
   pub sample_count: vk::SampleCountFlags,
-  pub descriptor_set_layout: vk::DescriptorSetLayout,
+  pub primary_descriptor_set_layout: vk::DescriptorSetLayout,
+  pub push_descriptor_set_layout: vk::DescriptorSetLayout,
 }
 
 
@@ -244,7 +248,13 @@ impl ForReinit {
   #[allow(unsafe_code)]
   pub fn destroy(self, device: &Device) -> () {
     unsafe {
-      device.destroy_descriptor_set_layout(self.descriptor_set_layout, None)
+      device.destroy_descriptor_set_layout(self.primary_descriptor_set_layout,
+                                           None)
+    };
+
+    unsafe {
+      device.destroy_descriptor_set_layout(self.push_descriptor_set_layout,
+                                           None)
     };
   }
 }
@@ -366,12 +376,28 @@ fn init_vulkan(window: &Window)
                this may mean other messages don't show up.");
   }
 
+  //   The api_version field here is the version of Vulkan we actually get.
+  // The official documentation is a bit misleading, since it says "maximum",
+  // but on close reading what it means is that we guarantee that we as the
+  // application will not require anything newer than that. So from our
+  // perspective it is the MINIMUM we will accept.
+  //
+  //   We define a constant for it to make it easy to find, since it's far
+  // more important operationally than any of the other versions declared
+  // here.
+  //
+  //   The validation layer enforces this API version on us, interposing
+  // itself when we attempt to load functions. Vulkanalia will panic!() on us
+  // when loading fails, with a message in the form "could not load
+  // vkWhateverWhatever", which can be quite confusing, so this comment text
+  // is here to make that easy to diagnose. If you got here for that reason,
+  // give thanks to the Waiting and its foresight.
   let application_info = ApplicationInfo::builder()
           .application_name(b"Surreality\0")
           .application_version(vk::make_version(1, 0, 0))
           .engine_name(b"Surreality\0")
           .engine_version(vk::make_version(1, 0, 0))
-          .api_version(vk::make_version(1, 0, 0));
+          .api_version(u32::from(VULKAN_MINIMUM_VERSION));
 
   //   Deceptively, this DOES get mutated later, but Vulkanalia doesn't see
   // it that way.
@@ -454,9 +480,24 @@ fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR,
   //   Old versions of Vulkan want layers to be enabled at the device
   // level as well. Newer ones will ignore this and just use the instance
   // layers.
-  let available_features = unsafe {
-    instance.get_physical_device_features(physical_device)
+  //
+  //   Dealing with this struct chain is kind of risky lifetime-wise, because
+  // Vulkanalia unsafely discards the reference for our "convenience". Alas.
+  let mut available_features_14
+          = vk::PhysicalDeviceVulkan14Features::default();
+  let mut available_features = vk::PhysicalDeviceFeatures2::builder()
+          .push_next(&mut available_features_14);
+  unsafe {
+    instance.get_physical_device_features2(physical_device,
+                                           &mut available_features)
   };
+
+  //   Another struct chain with a risky lifetime. Vulkanalia's mutation
+  // features return a copy, so we have to avoid mutating features_14 once we
+  // add it to the chain. Fortunately we only need it for one thing right now,
+  // so we do that here at the top and treat it as non-mutable.
+  let mut features_14 = vk::PhysicalDeviceVulkan14Features::builder()
+          .push_descriptor(true);
   let mut features = vk::PhysicalDeviceFeatures::builder();
   let mut extensions = Vec::new();
   let mut layers = Vec::new();
@@ -506,7 +547,7 @@ fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR,
     EnableSwapchain(false)
   };
 
-  let enable_anisotropy = if available_features.sampler_anisotropy
+  let enable_anisotropy = if available_features.features.sampler_anisotropy
                              == vk::TRUE
   {
     features = features.sampler_anisotropy(true);
@@ -535,11 +576,14 @@ fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR,
                     .queue_priorities(&[1.0]));
   }
 
+  let mut features2 = vk::PhysicalDeviceFeatures2::builder()
+          .features(features);
   let device_info = vk::DeviceCreateInfo::builder()
           .queue_create_infos(&queues)
           .enabled_layer_names(&layers)
           .enabled_extension_names(&extensions)
-          .enabled_features(&features);
+          .push_next(&mut features2)
+          .push_next(&mut features_14);
 
   let device = unsafe {
     instance.create_device(physical_device, &device_info, None)
@@ -564,8 +608,8 @@ fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR,
 
 
 #[allow(unsafe_code)]
-fn init_descriptor_set_layout(device: &Device)
-    -> Result<vk::DescriptorSetLayout>
+fn init_descriptor_set_layouts(device: &Device)
+    -> Result<(vk::DescriptorSetLayout, vk::DescriptorSetLayout)>
 {
   let uniform_block_binding = vk::DescriptorSetLayoutBinding::builder()
           .binding(0)
@@ -579,21 +623,32 @@ fn init_descriptor_set_layout(device: &Device)
           .descriptor_count(1)
           .stage_flags(vk::ShaderStageFlags::FRAGMENT);
 
+  let primary_bindings = [uniform_block_binding, sampler_binding];
+  let primary_descriptor_set_layout_info
+          = vk::DescriptorSetLayoutCreateInfo::builder()
+                .bindings(&primary_bindings);
+  let primary_descriptor_set_layout = unsafe {
+    device.create_descriptor_set_layout(&primary_descriptor_set_layout_info,
+                                        None)
+  }?;
+
   let texture_binding = vk::DescriptorSetLayoutBinding::builder()
-          .binding(2)
+          .binding(0)
           .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
           .descriptor_count(1)
           .stage_flags(vk::ShaderStageFlags::FRAGMENT);
 
-  let bindings = [uniform_block_binding, sampler_binding, texture_binding];
-  let descriptor_set_layout_info
+  let push_bindings = [texture_binding];
+  let push_descriptor_set_layout_info
           = vk::DescriptorSetLayoutCreateInfo::builder()
-                .bindings(&bindings);
-  let descriptor_set_layout = unsafe {
-    device.create_descriptor_set_layout(&descriptor_set_layout_info, None)
+                .bindings(&push_bindings)
+                .flags(vk::DescriptorSetLayoutCreateFlags::PUSH_DESCRIPTOR);
+  let push_descriptor_set_layout = unsafe {
+    device.create_descriptor_set_layout(&push_descriptor_set_layout_info,
+                                        None)
   }?;
 
-  Ok(descriptor_set_layout)
+  Ok((primary_descriptor_set_layout, push_descriptor_set_layout))
 }
 
 
@@ -770,10 +825,15 @@ fn score_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR,
     // selected.
   }
 
-  let features = unsafe {
-    instance.get_physical_device_features(*physical_device)
+  let mut features_14
+          = vk::PhysicalDeviceVulkan14Features::default();
+  let mut features = vk::PhysicalDeviceFeatures2::builder()
+          .push_next(&mut features_14);
+  unsafe {
+    instance.get_physical_device_features2(*physical_device, &mut features)
   };
-  if features.sampler_anisotropy == vk::TRUE {
+
+  if features.features.sampler_anisotropy == vk::TRUE {
     score += 1;
   }
 
@@ -786,7 +846,17 @@ fn score_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR,
   score += shift;
   let sample_count = vk::SampleCountFlags::from_bits(1 << shift).unwrap();
 
-  Ok(Acceptable::Accepted((score, indices, sample_count)))
+  //   We prefer to report the most important reason, rather than
+  // less-fundamental ones. To make that easy, we try to keep this logic
+  // together at the end. Note that, before we get here, we may have already
+  // rejected the device thanks to find_device_queue_family_indices(), called
+  // above, but that's okay because all those reasons are in fact more
+  // important than the ones tested here.
+  if features_14.push_descriptor != vk::TRUE {
+    Ok(Acceptable::Rejected("Doesn't support push descriptors.".to_string()))
+  } else {
+    Ok(Acceptable::Accepted((score, indices, sample_count)))
+  }
 }
 
 
@@ -819,6 +889,9 @@ fn find_device_queue_family_indices(instance: &Instance,
     }
   }
 
+  //   We prefer to report the most important reason, rather than
+  // less-fundamental ones. To make that easy, we keep all this logic together
+  // at the end.
   if let Some(graphics) = graphics {
     if let Some(presentation) = presentation {
       Ok(Acceptable::Accepted(QueueFamilyIndices {
@@ -885,3 +958,4 @@ extern "system" fn debug_messager_callback(
   // test so anything it does is fine with us.
   vk::FALSE
 }
+
diff --git a/src/graphics/render.rs b/src/graphics/render.rs
index 30f4b62..368bb96 100644
--- a/src/graphics/render.rs
+++ b/src/graphics/render.rs
@@ -4,12 +4,12 @@ use crate::assets::asset;
 use crate::graphics::{
   Permanent, ForReinit, WindowDressing, Frame, Model, Texture
 };
-use crate::shader_data::{ Vertex, VertexPushBlock };
+use crate::shader_data::{ Vertex, VertexPushBlock, TexturePushBlock };
 
 use std::mem::size_of;
 
 use vulkanalia::Device;
-use vulkanalia::vk::{ self, Handle, HasBuilder, DeviceV1_0 };
+use vulkanalia::vk::{ self, Handle, HasBuilder, DeviceV1_0, DeviceV1_1 };
 
 
 //   Render is a state object that collects the Vulkan graphics objects which
@@ -23,6 +23,12 @@ pub struct Render {
   pub pipeline_layout: vk::PipelineLayout,
 
   pub model: Option<Model>,
+  pub texture: Option<Texture>,
+
+  //   Notice that our descriptor logic is split between the descriptor update
+  // template for the push descriptors, which is defined here, and the
+  // descriptor sets, which are per-frame and are kept in Frame.
+  pub texture_descriptor_update_template: vk::DescriptorUpdateTemplate,
 
   pub per_frame: Vec<Frame>,
 }
@@ -30,12 +36,14 @@ pub struct Render {
 
 impl Render {
   pub fn new(permanent: &Permanent, for_reinit: &ForReinit,
-             window_dressing: &WindowDressing, texture: &Texture)
+             window_dressing: &WindowDressing)
       -> Result<Self>
   {
     let device = &permanent.device;
     let sample_count = for_reinit.sample_count;
-    let descriptor_set_layout = &for_reinit.descriptor_set_layout;
+    let primary_descriptor_set_layout
+            = &for_reinit.primary_descriptor_set_layout;
+    let push_descriptor_set_layout = &for_reinit.push_descriptor_set_layout;
     let swapchain = &window_dressing.swapchain;
     let depth_format = &window_dressing.depth_format;
 
@@ -43,19 +51,26 @@ impl Render {
                                        &swapchain.format, &depth_format)?;
 
     let (pipeline_layout, pipeline)
-            = init_pipeline(device, descriptor_set_layout, &swapchain.extent,
+            = init_pipeline(device, primary_descriptor_set_layout,
+                            push_descriptor_set_layout, &swapchain.extent,
                             sample_count, &render_pass)?;
 
+    let texture_descriptor_update_template
+            = init_descriptor_update_template(device, &pipeline_layout)?;
+
     let per_frame = Frame::new(permanent, for_reinit, window_dressing,
-                               texture, &render_pass)?;
+                               &render_pass)?;
 
     let model = None;
+    let texture = None;
 
     Ok(Render {
       render_pass,
       pipeline,
       pipeline_layout,
       model,
+      texture,
+      texture_descriptor_update_template,
       per_frame,
     })
   }
@@ -64,14 +79,16 @@ impl Render {
   // idle.
   pub fn reinit(&mut self,
                 permanent: &Permanent, for_reinit: &ForReinit,
-                window_dressing: &WindowDressing, texture: &Texture)
+                window_dressing: &WindowDressing)
       -> Result<()>
   {
     self.destroy_replaceable(permanent);
 
     let device = &permanent.device;
     let sample_count = for_reinit.sample_count;
-    let descriptor_set_layout = &for_reinit.descriptor_set_layout;
+    let primary_descriptor_set_layout
+            = &for_reinit.primary_descriptor_set_layout;
+    let push_descriptor_set_layout = &for_reinit.push_descriptor_set_layout;
     let swapchain = &window_dressing.swapchain;
     let depth_format = &window_dressing.depth_format;
 
@@ -79,15 +96,21 @@ impl Render {
                                        &swapchain.format, &depth_format)?;
 
     let (pipeline_layout, pipeline)
-            = init_pipeline(device, descriptor_set_layout, &swapchain.extent,
+            = init_pipeline(device, primary_descriptor_set_layout,
+                            push_descriptor_set_layout, &swapchain.extent,
                             sample_count, &render_pass)?;
 
+    let texture_descriptor_update_template
+            = init_descriptor_update_template(device, &pipeline_layout)?;
+
     Frame::reinit(&mut self.per_frame, permanent, for_reinit, window_dressing,
-                  texture, &render_pass)?;
+                  &render_pass)?;
 
     self.render_pass = render_pass;
     self.pipeline = pipeline;
     self.pipeline_layout = pipeline_layout;
+    self.texture_descriptor_update_template
+        = texture_descriptor_update_template;
 
     Ok(())
   }
@@ -103,6 +126,10 @@ impl Render {
     if let Some(model) = self.model {
       model.destroy(&permanent.device);
     }
+
+    if let Some(texture) = self.texture {
+      texture.destroy(&permanent.device);
+    }
   }
 
   #[allow(unsafe_code)]
@@ -112,6 +139,11 @@ impl Render {
 
     let device = &permanent.device;
 
+    unsafe {
+      device.destroy_descriptor_update_template(
+          self.texture_descriptor_update_template, None)
+    };
+
     unsafe { device.destroy_pipeline(self.pipeline, None) };
     unsafe { device.destroy_pipeline_layout(self.pipeline_layout, None) };
     unsafe { device.destroy_render_pass(self.render_pass, None) };
@@ -120,6 +152,10 @@ impl Render {
   pub fn set_model(&mut self, model: Model) {
     self.model = Some(model);
   }
+
+  pub fn set_texture(&mut self, texture: Texture) {
+    self.texture = Some(texture);
+  }
 }
 
 
@@ -209,7 +245,8 @@ fn init_render_pass(device: &Device, sample_count: vk::SampleCountFlags,
 
 #[allow(unsafe_code)]
 fn init_pipeline(device: &Device,
-                 descriptor_set_layout: &vk::DescriptorSetLayout,
+                 primary_descriptor_set_layout: &vk::DescriptorSetLayout,
+                 push_descriptor_set_layout: &vk::DescriptorSetLayout,
                  extent: &vk::Extent2D, sample_count: vk::SampleCountFlags,
                  render_pass: &vk::RenderPass)
     -> Result<(vk::PipelineLayout, vk::Pipeline)>
@@ -308,7 +345,16 @@ fn init_pipeline(device: &Device,
           .offset(0)
           .size(size_of::<VertexPushBlock<f32>>() as u32);
 
-  let layouts = [*descriptor_set_layout];
+  //   Indices into this layout array will appear as magic constants in
+  // several places: the call to cmd_bind_descriptor_sets() in
+  // scene.rs's generate_scene_commands(); the update template's definition
+  // in init_descriptor_update_template(), below; and the call to
+  // cmd_push_descriptor_set_with_template() in texture.rs's make_active().
+  //
+  //   When the list of layouts is changed, all those places need to be
+  // updated. Keep the breadcrumb comments on the other end in sync with this
+  // one, as well.
+  let layouts = [*primary_descriptor_set_layout, *push_descriptor_set_layout];
   let push_constant_ranges = [vertex_push_constant_range];
   let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder()
                                  .set_layouts(&layouts)
@@ -345,3 +391,38 @@ fn init_pipeline(device: &Device,
   Ok((pipeline_layout, pipeline))
 }
 
+
+#[allow(unsafe_code)]
+fn init_descriptor_update_template(
+       device: &Device,
+       pipeline_layout: &vk::PipelineLayout)
+    -> Result<vk::DescriptorUpdateTemplate>
+{
+  let entry_info = vk::DescriptorUpdateTemplateEntry::builder()
+          .dst_binding(0)
+          .dst_array_element(0)
+          .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
+          .descriptor_count(1)
+          .offset(0)
+          .stride(size_of::<TexturePushBlock>());
+
+  let entries = &[entry_info];
+
+  //   Please notice the magic constant set(1). This is a zero-based index
+  // into the array of descriptor set layouts passed to
+  // create_pipeline_layout() in init_pipeline(), above.
+  let template_info = vk::DescriptorUpdateTemplateCreateInfo::builder()
+          .template_type(vk::DescriptorUpdateTemplateType::PUSH_DESCRIPTORS)
+          .descriptor_update_entries(entries)
+          .set(1)
+          .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
+          .pipeline_layout(*pipeline_layout)
+          .flags(vk::DescriptorUpdateTemplateCreateFlags::empty());
+
+  let texture_descriptor_update_template = unsafe {
+    device.create_descriptor_update_template(&template_info, None)
+  }?;
+
+  Ok(texture_descriptor_update_template)
+}
+
diff --git a/src/graphics/scene.rs b/src/graphics/scene.rs
index f73ebf2..9631dba 100644
--- a/src/graphics/scene.rs
+++ b/src/graphics/scene.rs
@@ -61,8 +61,24 @@ pub fn generate_scene_commands<'a>(image_index: usize, time: f32,
                              render.pipeline)
   };
 
+  //   Please notice the magic constant 0. This is a zero-based index into
+  // the array of descriptor set layouts passed to create_pipeline_layout()
+  // in init_pipeline() in render.rs.
+  unsafe {
+    device.cmd_bind_descriptor_sets(*command_buffer,
+                                    vk::PipelineBindPoint::GRAPHICS,
+                                    render.pipeline_layout,
+                                    0,
+                                    &[*descriptor_set],
+                                    &[])
+  };
+
+  if let Some(texture) = &render.texture {
+    texture.make_active(device, render, command_buffer);
+  }
+
   if let Some(model) = &render.model {
-    model.render(time, device, render, command_buffer, descriptor_set);
+    model.render(time, device, render, command_buffer);
   }
 
   unsafe { device.cmd_end_render_pass(*command_buffer) };
diff --git a/src/graphics/texture.rs b/src/graphics/texture.rs
index b28d540..da40387 100644
--- a/src/graphics/texture.rs
+++ b/src/graphics/texture.rs
@@ -1,13 +1,18 @@
 #![deny(unsafe_code)]
 use crate::error::*;
-use crate::graphics::Permanent;
+use crate::graphics::{ Permanent, Render };
 use crate::graphics::util::{
   stage_in_buffer, allocate_image, init_image_view,
   begin_transient_commands, end_transient_commands
 };
+use crate::shader_data::TexturePushBlock;
+
+use std::ffi::c_void;
 
 use vulkanalia::{ Device, Instance };
-use vulkanalia::vk::{ self, HasBuilder, InstanceV1_0, DeviceV1_0 };
+use vulkanalia::vk::{
+  self, HasBuilder, InstanceV1_0, DeviceV1_0, DeviceV1_4
+};
 
 
 #[derive(Debug)]
@@ -47,6 +52,32 @@ impl Texture {
     unsafe { device.free_memory(self.image_memory, None) };
     unsafe { device.destroy_image_view(self.image_view, None) };
   }
+
+  #[allow(unsafe_code)]
+  pub fn make_active(&self, device: &Device, render: &Render,
+                     command_buffer: &vk::CommandBuffer)
+  {
+    let template = &render.texture_descriptor_update_template;
+    let layout = &render.pipeline_layout;
+
+    //   Constructing this every time may appear slow, but it's all CPU-side
+    // work, and the real cost we're trying to avoid with push descriptors is
+    // excessive CPU-GPU synchronziation.
+    let image_info = vk::DescriptorImageInfo::builder()
+            .image_view(self.image_view)
+            .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
+            .build();
+    let push_block = TexturePushBlock { image_info };
+    let push_block_bytes = &raw const push_block as *const c_void;
+
+    //   Please notice the magic constant 1. This is a one-based index into
+    // the array of descriptor set layouts passed to create_pipeline_layout()
+    // in init_pipeline() in render.rs.
+    unsafe {
+      device.cmd_push_descriptor_set_with_template(
+          *command_buffer, *template, *layout, 1, push_block_bytes)
+    };
+  }
 }
 
 
diff --git a/src/main.rs b/src/main.rs
index d461a63..4d0748f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 #![deny(unsafe_code)]
 use crate::error::*;
 use crate::assets::asset;
-use crate::graphics::{ Permanent, ForReinit, Render, Texture };
+use crate::graphics::{ Permanent, ForReinit, Render };
 use crate::graphics::scene::generate_scene_commands;
 use crate::graphics::window_dressing::{
   WindowDressing, N_SIMULTANEOUS_FRAMES
@@ -36,7 +36,6 @@ struct Surreality {
   for_reinit: RefCell<Option<ForReinit>>,
   window_dressing: RefCell<Option<WindowDressing>>,
   render: RefCell<Option<Render>>,
-  texture: RefCell<Option<Texture>>,
   is_minimized: bool,
   is_reinit_queued: bool,
   frame_index: usize,
@@ -54,7 +53,6 @@ impl Surreality {
       for_reinit: RefCell::new(None),
       window_dressing: RefCell::new(None),
       render: RefCell::new(None),
-      texture: RefCell::new(None),
       is_minimized: false,
       is_reinit_queued: false,
       frame_index: 0,
@@ -70,14 +68,14 @@ impl Surreality {
     let (permanent, for_reinit, enable_anisotropy, enable_swapchain)
         = Permanent::new(event_loop)?;
 
-    let png = asset("textures/forest_leaves_04_diff.png")?;
-    let (texture, mip_count) = load_png(png, &permanent)?;
-
     if enable_swapchain.0 {
+      let png = asset("textures/forest_leaves_04_diff.png")?;
+      let (texture, mip_count) = load_png(png, &permanent)?;
+
       let window_dressing = WindowDressing::new(&permanent, &for_reinit,
                                                 enable_anisotropy, mip_count)?;
-      let mut render = Render::new(&permanent, &for_reinit, &window_dressing,
-                                   &texture)?;
+      let mut render = Render::new(&permanent, &for_reinit, &window_dressing)?;
+      render.set_texture(texture);
 
       let obj = asset("models/teapot.obj")?;
       let model = load_obj(obj, &permanent)?;
@@ -87,7 +85,6 @@ impl Surreality {
       *self.render.get_mut() = Some(render);
     }
 
-    *self.texture.get_mut() = Some(texture);
     *self.permanent.get_mut() = Some(permanent);
     *self.for_reinit.get_mut() = Some(for_reinit);
 
@@ -100,10 +97,9 @@ impl Surreality {
        && let Some(window_dressing)
               = self.window_dressing.borrow_mut().as_mut()
        && let Some(render) = self.render.borrow_mut().as_mut()
-       && let Some(texture) = self.texture.borrow_mut().as_mut()
     {
       window_dressing.reinit(permanent, for_reinit)?;
-      render.reinit(permanent, for_reinit, &window_dressing, &texture)?;
+      render.reinit(permanent, for_reinit, &window_dressing)?;
     }
 
     Ok(())
@@ -234,10 +230,6 @@ impl Drop for Surreality {
         window_dressing.destroy(&permanent.device);
       }
 
-      if let Some(texture) = self.texture.replace(None) {
-        texture.destroy(&permanent.device);
-      }
-
       if let Some(for_reinit) = self.for_reinit.replace(None) {
         for_reinit.destroy(&permanent.device);
       }
diff --git a/src/shader_data.rs b/src/shader_data.rs
index 892260d..96e6a1b 100644
--- a/src/shader_data.rs
+++ b/src/shader_data.rs
@@ -27,6 +27,12 @@ pub struct VertexPushBlock<T: Copy> {
   pub model: Transformation<T>,
 }
 
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct TexturePushBlock {
+  pub image_info: vk::DescriptorImageInfo,
+}
+
 impl<T: Copy> Vertex<T> {
   pub const fn new(position: Vec3<T>, color: Vec3<T>,
                    texture_coordinates: Vec2<T>)