mirror of
https://github.com/vulkano-rs/vulkano.git
synced 2024-11-22 23:05:43 +00:00
d60677b59e
* Replace cgmath with glam in the examples * Implement type_for_format! for glam * Remove comment where I'm freaking out because of OpenGL flashbacks * Update Cargo.toml Co-authored-by: marc0246 <40955683+marc0246@users.noreply.github.com> * Update vulkano/autogen/formats.rs Co-authored-by: marc0246 <40955683+marc0246@users.noreply.github.com> * Fix glam type_for_format * Format the code --------- Co-authored-by: marc0246 <40955683+marc0246@users.noreply.github.com>
583 lines
23 KiB
Rust
583 lines
23 KiB
Rust
use super::{
|
|
ambient_lighting_system::AmbientLightingSystem,
|
|
directional_lighting_system::DirectionalLightingSystem,
|
|
point_lighting_system::PointLightingSystem,
|
|
};
|
|
use glam::f32::{Mat4, Vec3};
|
|
use std::sync::Arc;
|
|
use vulkano::{
|
|
command_buffer::{
|
|
allocator::StandardCommandBufferAllocator, CommandBuffer, CommandBufferBeginInfo,
|
|
CommandBufferLevel, CommandBufferUsage, RecordingCommandBuffer, RenderPassBeginInfo,
|
|
SubpassBeginInfo, SubpassContents,
|
|
},
|
|
descriptor_set::allocator::StandardDescriptorSetAllocator,
|
|
device::Queue,
|
|
format::Format,
|
|
image::{view::ImageView, Image, ImageCreateInfo, ImageType, ImageUsage},
|
|
memory::allocator::{AllocationCreateInfo, StandardMemoryAllocator},
|
|
render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass, Subpass},
|
|
sync::GpuFuture,
|
|
};
|
|
|
|
/// System that contains the necessary facilities for rendering a single frame.
|
|
pub struct FrameSystem {
|
|
// Queue to use to render everything.
|
|
gfx_queue: Arc<Queue>,
|
|
|
|
// Render pass used for the drawing. See the `new` method for the actual render pass content.
|
|
// We need to keep it in `FrameSystem` because we may want to recreate the intermediate buffers
|
|
// in of a change in the dimensions.
|
|
render_pass: Arc<RenderPass>,
|
|
|
|
memory_allocator: Arc<StandardMemoryAllocator>,
|
|
command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
|
|
|
|
// Intermediate render target that will contain the albedo of each pixel of the scene.
|
|
diffuse_buffer: Arc<ImageView>,
|
|
// Intermediate render target that will contain the normal vector in world coordinates of each
|
|
// pixel of the scene.
|
|
// The normal vector is the vector perpendicular to the surface of the object at this point.
|
|
normals_buffer: Arc<ImageView>,
|
|
// Intermediate render target that will contain the depth of each pixel of the scene.
|
|
// This is a traditional depth buffer. `0.0` means "near", and `1.0` means "far".
|
|
depth_buffer: Arc<ImageView>,
|
|
|
|
// Will allow us to add an ambient lighting to a scene during the second subpass.
|
|
ambient_lighting_system: AmbientLightingSystem,
|
|
// Will allow us to add a directional light to a scene during the second subpass.
|
|
directional_lighting_system: DirectionalLightingSystem,
|
|
// Will allow us to add a spot light source to a scene during the second subpass.
|
|
point_lighting_system: PointLightingSystem,
|
|
}
|
|
|
|
impl FrameSystem {
|
|
/// Initializes the frame system.
|
|
///
|
|
/// Should be called at initialization, as it can take some time to build.
|
|
///
|
|
/// - `gfx_queue` is the queue that will be used to perform the main rendering.
|
|
/// - `final_output_format` is the format of the image that will later be passed to the
|
|
/// `frame()` method. We need to know that in advance. If that format ever changes, we have
|
|
/// to create a new `FrameSystem`.
|
|
pub fn new(
|
|
gfx_queue: Arc<Queue>,
|
|
final_output_format: Format,
|
|
memory_allocator: Arc<StandardMemoryAllocator>,
|
|
command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
|
|
) -> FrameSystem {
|
|
// Creating the render pass.
|
|
//
|
|
// The render pass has two subpasses. In the first subpass, we draw all the objects of the
|
|
// scene. Note that it is not the `FrameSystem` that is responsible for the drawing,
|
|
// instead it only provides an API that allows the user to do so.
|
|
//
|
|
// The drawing of the objects will write to the `diffuse`, `normals` and `depth`
|
|
// attachments.
|
|
//
|
|
// Then in the second subpass, we read these three attachments as input attachments and
|
|
// draw to `final_color`. Each draw operation performed in this second subpass has its
|
|
// value added to `final_color` and not replaced, thanks to blending.
|
|
//
|
|
// > **Warning**: If the red, green or blue component of the final image goes over `1.0`
|
|
// > then it will be clamped. For example a pixel of `[2.0, 1.0, 1.0]` (which is red) will
|
|
// > be clamped to `[1.0, 1.0, 1.0]` (which is white) instead of being converted to
|
|
// > `[1.0, 0.5, 0.5]` as desired. In a real-life application you want to use an additional
|
|
// > intermediate image with a floating-point format, then perform additional passes to
|
|
// > convert all the colors in the correct range. These techniques are known as HDR and
|
|
// > tone mapping.
|
|
//
|
|
// Input attachments are a special kind of way to read images. You can only read from them
|
|
// from a fragment shader, and you can only read the pixel corresponding to the pixel
|
|
// currently being processed by the fragment shader. If you want to read from attachments
|
|
// but can't deal with these restrictions, then you should create multiple render passes
|
|
// instead.
|
|
let render_pass = vulkano::ordered_passes_renderpass!(
|
|
gfx_queue.device().clone(),
|
|
attachments: {
|
|
// The image that will contain the final rendering (in this example the swapchain
|
|
// image, but it could be another image).
|
|
final_color: {
|
|
format: final_output_format,
|
|
samples: 1,
|
|
load_op: Clear,
|
|
store_op: Store,
|
|
},
|
|
// Will be bound to `self.diffuse_buffer`.
|
|
diffuse: {
|
|
format: Format::A2B10G10R10_UNORM_PACK32,
|
|
samples: 1,
|
|
load_op: Clear,
|
|
store_op: DontCare,
|
|
},
|
|
// Will be bound to `self.normals_buffer`.
|
|
normals: {
|
|
format: Format::R16G16B16A16_SFLOAT,
|
|
samples: 1,
|
|
load_op: Clear,
|
|
store_op: DontCare,
|
|
},
|
|
// Will be bound to `self.depth_buffer`.
|
|
depth_stencil: {
|
|
format: Format::D16_UNORM,
|
|
samples: 1,
|
|
load_op: Clear,
|
|
store_op: DontCare,
|
|
},
|
|
},
|
|
passes: [
|
|
// Write to the diffuse, normals and depth attachments.
|
|
{
|
|
color: [diffuse, normals],
|
|
depth_stencil: {depth_stencil},
|
|
input: [],
|
|
},
|
|
// Apply lighting by reading these three attachments and writing to `final_color`.
|
|
{
|
|
color: [final_color],
|
|
depth_stencil: {},
|
|
input: [diffuse, normals, depth_stencil],
|
|
},
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
// For now we create three temporary images with a dimension of 1 by 1 pixel. These images
|
|
// will be replaced the first time we call `frame()`.
|
|
let diffuse_buffer = ImageView::new_default(
|
|
Image::new(
|
|
memory_allocator.clone(),
|
|
ImageCreateInfo {
|
|
image_type: ImageType::Dim2d,
|
|
format: Format::A2B10G10R10_UNORM_PACK32,
|
|
extent: [1, 1, 1],
|
|
usage: ImageUsage::COLOR_ATTACHMENT
|
|
| ImageUsage::TRANSIENT_ATTACHMENT
|
|
| ImageUsage::INPUT_ATTACHMENT,
|
|
..Default::default()
|
|
},
|
|
AllocationCreateInfo::default(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
let normals_buffer = ImageView::new_default(
|
|
Image::new(
|
|
memory_allocator.clone(),
|
|
ImageCreateInfo {
|
|
image_type: ImageType::Dim2d,
|
|
format: Format::R16G16B16A16_SFLOAT,
|
|
extent: [1, 1, 1],
|
|
usage: ImageUsage::TRANSIENT_ATTACHMENT | ImageUsage::INPUT_ATTACHMENT,
|
|
..Default::default()
|
|
},
|
|
AllocationCreateInfo::default(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
let depth_buffer = ImageView::new_default(
|
|
Image::new(
|
|
memory_allocator.clone(),
|
|
ImageCreateInfo {
|
|
image_type: ImageType::Dim2d,
|
|
format: Format::D16_UNORM,
|
|
extent: [1, 1, 1],
|
|
usage: ImageUsage::TRANSIENT_ATTACHMENT | ImageUsage::INPUT_ATTACHMENT,
|
|
..Default::default()
|
|
},
|
|
AllocationCreateInfo::default(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
|
|
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
|
|
gfx_queue.device().clone(),
|
|
Default::default(),
|
|
));
|
|
|
|
// Initialize the three lighting systems. Note that we need to pass to them the subpass
|
|
// where they will be executed.
|
|
let lighting_subpass = Subpass::from(render_pass.clone(), 1).unwrap();
|
|
let ambient_lighting_system = AmbientLightingSystem::new(
|
|
gfx_queue.clone(),
|
|
lighting_subpass.clone(),
|
|
memory_allocator.clone(),
|
|
command_buffer_allocator.clone(),
|
|
descriptor_set_allocator.clone(),
|
|
);
|
|
let directional_lighting_system = DirectionalLightingSystem::new(
|
|
gfx_queue.clone(),
|
|
lighting_subpass.clone(),
|
|
memory_allocator.clone(),
|
|
command_buffer_allocator.clone(),
|
|
descriptor_set_allocator.clone(),
|
|
);
|
|
let point_lighting_system = PointLightingSystem::new(
|
|
gfx_queue.clone(),
|
|
lighting_subpass,
|
|
memory_allocator.clone(),
|
|
command_buffer_allocator.clone(),
|
|
descriptor_set_allocator,
|
|
);
|
|
|
|
FrameSystem {
|
|
gfx_queue,
|
|
render_pass,
|
|
memory_allocator,
|
|
command_buffer_allocator,
|
|
diffuse_buffer,
|
|
normals_buffer,
|
|
depth_buffer,
|
|
ambient_lighting_system,
|
|
directional_lighting_system,
|
|
point_lighting_system,
|
|
}
|
|
}
|
|
|
|
/// Returns the subpass of the render pass where the rendering should write info to gbuffers.
|
|
///
|
|
/// Has two outputs: the diffuse color (3 components) and the normals in world coordinates
|
|
/// (3 components). Also has a depth attachment.
|
|
///
|
|
/// This method is necessary in order to initialize the pipelines that will draw the objects
|
|
/// of the scene.
|
|
#[inline]
|
|
pub fn deferred_subpass(&self) -> Subpass {
|
|
Subpass::from(self.render_pass.clone(), 0).unwrap()
|
|
}
|
|
|
|
/// Starts drawing a new frame.
|
|
///
|
|
/// - `before_future` is the future after which the main rendering should be executed.
|
|
/// - `final_image` is the image we are going to draw to.
|
|
/// - `world_to_framebuffer` is the matrix that will be used to convert from 3D coordinates in
|
|
/// the world into 2D coordinates on the framebuffer.
|
|
pub fn frame<F>(
|
|
&mut self,
|
|
before_future: F,
|
|
final_image_view: Arc<ImageView>,
|
|
world_to_framebuffer: Mat4,
|
|
) -> Frame
|
|
where
|
|
F: GpuFuture + 'static,
|
|
{
|
|
// First of all we recreate `self.diffuse_buffer`, `self.normals_buffer` and
|
|
// `self.depth_buffer` if their extent doesn't match the extent of the final image.
|
|
let extent = final_image_view.image().extent();
|
|
if self.diffuse_buffer.image().extent() != extent {
|
|
// Note that we create "transient" images here. This means that the content of the
|
|
// image is only defined when within a render pass. In other words you can draw to
|
|
// them in a subpass then read them in another subpass, but as soon as you leave the
|
|
// render pass their content becomes undefined.
|
|
self.diffuse_buffer = ImageView::new_default(
|
|
Image::new(
|
|
self.memory_allocator.clone(),
|
|
ImageCreateInfo {
|
|
extent,
|
|
format: Format::A2B10G10R10_UNORM_PACK32,
|
|
usage: ImageUsage::COLOR_ATTACHMENT
|
|
| ImageUsage::TRANSIENT_ATTACHMENT
|
|
| ImageUsage::INPUT_ATTACHMENT,
|
|
..Default::default()
|
|
},
|
|
AllocationCreateInfo::default(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
self.normals_buffer = ImageView::new_default(
|
|
Image::new(
|
|
self.memory_allocator.clone(),
|
|
ImageCreateInfo {
|
|
extent,
|
|
format: Format::R16G16B16A16_SFLOAT,
|
|
usage: ImageUsage::COLOR_ATTACHMENT
|
|
| ImageUsage::TRANSIENT_ATTACHMENT
|
|
| ImageUsage::INPUT_ATTACHMENT,
|
|
..Default::default()
|
|
},
|
|
AllocationCreateInfo::default(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
self.depth_buffer = ImageView::new_default(
|
|
Image::new(
|
|
self.memory_allocator.clone(),
|
|
ImageCreateInfo {
|
|
extent,
|
|
format: Format::D16_UNORM,
|
|
usage: ImageUsage::DEPTH_STENCIL_ATTACHMENT
|
|
| ImageUsage::TRANSIENT_ATTACHMENT
|
|
| ImageUsage::INPUT_ATTACHMENT,
|
|
..Default::default()
|
|
},
|
|
AllocationCreateInfo::default(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
// Build the framebuffer. The image must be attached in the same order as they were defined
|
|
// with the `ordered_passes_renderpass!` macro.
|
|
let framebuffer = Framebuffer::new(
|
|
self.render_pass.clone(),
|
|
FramebufferCreateInfo {
|
|
attachments: vec![
|
|
final_image_view,
|
|
self.diffuse_buffer.clone(),
|
|
self.normals_buffer.clone(),
|
|
self.depth_buffer.clone(),
|
|
],
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
// Start the command buffer builder that will be filled throughout the frame handling.
|
|
let mut command_buffer_builder = RecordingCommandBuffer::new(
|
|
self.command_buffer_allocator.clone(),
|
|
self.gfx_queue.queue_family_index(),
|
|
CommandBufferLevel::Primary,
|
|
CommandBufferBeginInfo {
|
|
usage: CommandBufferUsage::OneTimeSubmit,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
command_buffer_builder
|
|
.begin_render_pass(
|
|
RenderPassBeginInfo {
|
|
clear_values: vec![
|
|
Some([0.0, 0.0, 0.0, 0.0].into()),
|
|
Some([0.0, 0.0, 0.0, 0.0].into()),
|
|
Some([0.0, 0.0, 0.0, 0.0].into()),
|
|
Some(1.0f32.into()),
|
|
],
|
|
..RenderPassBeginInfo::framebuffer(framebuffer.clone())
|
|
},
|
|
SubpassBeginInfo {
|
|
contents: SubpassContents::SecondaryCommandBuffers,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
Frame {
|
|
system: self,
|
|
before_main_cb_future: Some(Box::new(before_future)),
|
|
framebuffer,
|
|
num_pass: 0,
|
|
command_buffer_builder: Some(command_buffer_builder),
|
|
world_to_framebuffer,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents the active process of rendering a frame.
|
|
///
|
|
/// This struct mutably borrows the `FrameSystem`.
|
|
pub struct Frame<'a> {
|
|
// The `FrameSystem`.
|
|
system: &'a mut FrameSystem,
|
|
|
|
// The active pass we are in. This keeps track of the step we are in.
|
|
// - If `num_pass` is 0, then we haven't start anything yet.
|
|
// - If `num_pass` is 1, then we have finished drawing all the objects of the scene.
|
|
// - If `num_pass` is 2, then we have finished applying lighting.
|
|
// - Otherwise the frame is finished.
|
|
// In a more complex application you can have dozens of passes, in which case you probably
|
|
// don't want to document them all here.
|
|
num_pass: u8,
|
|
|
|
// Future to wait upon before the main rendering.
|
|
before_main_cb_future: Option<Box<dyn GpuFuture>>,
|
|
// Framebuffer that was used when starting the render pass.
|
|
framebuffer: Arc<Framebuffer>,
|
|
// The command buffer builder that will be built during the lifetime of this object.
|
|
command_buffer_builder: Option<RecordingCommandBuffer>,
|
|
// Matrix that was passed to `frame()`.
|
|
world_to_framebuffer: Mat4,
|
|
}
|
|
|
|
impl<'a> Frame<'a> {
|
|
/// Returns an enumeration containing the next pass of the rendering.
|
|
pub fn next_pass<'f>(&'f mut self) -> Option<Pass<'f, 'a>> {
|
|
// This function reads `num_pass` increments its value, and returns a struct corresponding
|
|
// to that pass that the user will be able to manipulate in order to customize the pass.
|
|
let current_pass = self.num_pass;
|
|
self.num_pass += 1;
|
|
|
|
match current_pass {
|
|
0 => {
|
|
// If we are in the pass 0 then we haven't start anything yet.
|
|
// We already called `begin_render_pass` (in the `frame()` method), and that's the
|
|
// state we are in.
|
|
// We return an object that will allow the user to draw objects on the scene.
|
|
Some(Pass::Deferred(DrawPass { frame: self }))
|
|
}
|
|
|
|
1 => {
|
|
// If we are in pass 1 then we have finished drawing the objects on the scene.
|
|
// Going to the next subpass.
|
|
self.command_buffer_builder
|
|
.as_mut()
|
|
.unwrap()
|
|
.next_subpass(
|
|
Default::default(),
|
|
SubpassBeginInfo {
|
|
contents: SubpassContents::SecondaryCommandBuffers,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
// And returning an object that will allow the user to apply lighting to the scene.
|
|
Some(Pass::Lighting(LightingPass { frame: self }))
|
|
}
|
|
|
|
2 => {
|
|
// If we are in pass 2 then we have finished applying lighting.
|
|
// We take the builder, call `end_render_pass()`, and then `build()` it to obtain
|
|
// an actual command buffer.
|
|
self.command_buffer_builder
|
|
.as_mut()
|
|
.unwrap()
|
|
.end_render_pass(Default::default())
|
|
.unwrap();
|
|
let command_buffer = self.command_buffer_builder.take().unwrap().end().unwrap();
|
|
|
|
// Extract `before_main_cb_future` and append the command buffer execution to it.
|
|
let after_main_cb = self
|
|
.before_main_cb_future
|
|
.take()
|
|
.unwrap()
|
|
.then_execute(self.system.gfx_queue.clone(), command_buffer)
|
|
.unwrap();
|
|
// We obtain `after_main_cb`, which we give to the user.
|
|
Some(Pass::Finished(Box::new(after_main_cb)))
|
|
}
|
|
|
|
// If the pass is over 2 then the frame is in the finished state and can't do anything
|
|
// more.
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Struct provided to the user that allows them to customize or handle the pass.
|
|
pub enum Pass<'f, 's: 'f> {
|
|
/// We are in the pass where we draw objects on the scene. The `DrawPass` allows the user to
|
|
/// draw the objects.
|
|
Deferred(DrawPass<'f, 's>),
|
|
|
|
/// We are in the pass where we add lighting to the scene. The `LightingPass` allows the user
|
|
/// to add light sources.
|
|
Lighting(LightingPass<'f, 's>),
|
|
|
|
/// The frame has been fully prepared, and here is the future that will perform the drawing
|
|
/// on the image.
|
|
Finished(Box<dyn GpuFuture>),
|
|
}
|
|
|
|
/// Allows the user to draw objects on the scene.
|
|
pub struct DrawPass<'f, 's: 'f> {
|
|
frame: &'f mut Frame<'s>,
|
|
}
|
|
|
|
impl<'f, 's: 'f> DrawPass<'f, 's> {
|
|
/// Appends a command that executes a secondary command buffer that performs drawing.
|
|
pub fn execute(&mut self, command_buffer: Arc<CommandBuffer>) {
|
|
self.frame
|
|
.command_buffer_builder
|
|
.as_mut()
|
|
.unwrap()
|
|
.execute_commands(command_buffer)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Returns the dimensions in pixels of the viewport.
|
|
pub fn viewport_dimensions(&self) -> [u32; 2] {
|
|
self.frame.framebuffer.extent()
|
|
}
|
|
|
|
/// Returns the 4x4 matrix that turns world coordinates into 2D coordinates on the framebuffer.
|
|
#[allow(dead_code)]
|
|
pub fn world_to_framebuffer_matrix(&self) -> Mat4 {
|
|
self.frame.world_to_framebuffer
|
|
}
|
|
}
|
|
|
|
/// Allows the user to apply lighting on the scene.
|
|
pub struct LightingPass<'f, 's: 'f> {
|
|
frame: &'f mut Frame<'s>,
|
|
}
|
|
|
|
impl<'f, 's: 'f> LightingPass<'f, 's> {
|
|
/// Applies an ambient lighting to the scene.
|
|
///
|
|
/// All the objects will be colored with an intensity of `color`.
|
|
pub fn ambient_light(&mut self, color: [f32; 3]) {
|
|
let command_buffer = self.frame.system.ambient_lighting_system.draw(
|
|
self.frame.framebuffer.extent(),
|
|
self.frame.system.diffuse_buffer.clone(),
|
|
color,
|
|
);
|
|
self.frame
|
|
.command_buffer_builder
|
|
.as_mut()
|
|
.unwrap()
|
|
.execute_commands(command_buffer)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Applies an directional lighting to the scene.
|
|
///
|
|
/// All the objects will be colored with an intensity varying between `[0, 0, 0]` and `color`,
|
|
/// depending on the dot product of their normal and `direction`.
|
|
pub fn directional_light(&mut self, direction: Vec3, color: [f32; 3]) {
|
|
let command_buffer = self.frame.system.directional_lighting_system.draw(
|
|
self.frame.framebuffer.extent(),
|
|
self.frame.system.diffuse_buffer.clone(),
|
|
self.frame.system.normals_buffer.clone(),
|
|
direction,
|
|
color,
|
|
);
|
|
self.frame
|
|
.command_buffer_builder
|
|
.as_mut()
|
|
.unwrap()
|
|
.execute_commands(command_buffer)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Applies a spot lighting to the scene.
|
|
///
|
|
/// All the objects will be colored with an intensity varying between `[0, 0, 0]` and `color`,
|
|
/// depending on their distance with `position`. Objects that aren't facing `position` won't
|
|
/// receive any light.
|
|
pub fn point_light(&mut self, position: Vec3, color: [f32; 3]) {
|
|
let command_buffer = {
|
|
self.frame.system.point_lighting_system.draw(
|
|
self.frame.framebuffer.extent(),
|
|
self.frame.system.diffuse_buffer.clone(),
|
|
self.frame.system.normals_buffer.clone(),
|
|
self.frame.system.depth_buffer.clone(),
|
|
self.frame.world_to_framebuffer.inverse(),
|
|
position,
|
|
color,
|
|
)
|
|
};
|
|
|
|
self.frame
|
|
.command_buffer_builder
|
|
.as_mut()
|
|
.unwrap()
|
|
.execute_commands(command_buffer)
|
|
.unwrap();
|
|
}
|
|
}
|