Modules and stuff
This commit is contained in:
79
src/vulkan/gameobject.rs
Normal file
79
src/vulkan/gameobject.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use cgmath::{Deg, Euler, Matrix4, Quaternion, Vector3};
|
||||
|
||||
use crate::input::InputState;
|
||||
use crate::vulkan::{RendererDescriptorSets, TextureHandle};
|
||||
use crate::vulkan::{MeshHandle, VulkanRenderer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GameObject {
|
||||
pub mesh_index: usize,
|
||||
pub texture_index: TextureHandle,
|
||||
pub normal_map_index: TextureHandle,
|
||||
pub position: Vector3<f32>,
|
||||
pub rotation: Quaternion<f32>,
|
||||
pub scale: Vector3<f32>,
|
||||
pub children: Vec<GameObject>,
|
||||
pub descriptor_sets: Vec<Arc<RendererDescriptorSets>>
|
||||
}
|
||||
|
||||
impl GameObject {
|
||||
pub fn new(mesh: MeshHandle) -> GameObject {
|
||||
GameObject { mesh_index: mesh.index, texture_index: mesh.diffuse_handle, normal_map_index: mesh.normal_handle, position: Vector3::new(0.0, 0.0, 0.0),
|
||||
rotation: Quaternion::new(1.0, 0.0, 0.0, 0.0), scale: Vector3::new(1.0, 1.0, 1.0), children: vec![],
|
||||
descriptor_sets: vec![] }
|
||||
}
|
||||
|
||||
pub fn _set_position(&mut self, x: f32, y: f32, z: f32) {
|
||||
self.position.x = x;
|
||||
self.position.y = y;
|
||||
self.position.z = z;
|
||||
}
|
||||
|
||||
pub fn _set_scale(&mut self, x: f32, y: f32, z: f32) {
|
||||
self.scale.x = x;
|
||||
self.scale.y = y;
|
||||
self.scale.z = z;
|
||||
}
|
||||
|
||||
pub fn _set_rotation(&mut self, euler_x: f32, euler_y: f32, euler_z: f32) {
|
||||
self.rotation = Quaternion::from(Euler::new(Deg(euler_x), Deg(euler_y), Deg(euler_z)));
|
||||
}
|
||||
|
||||
pub fn _translate(&mut self, x: f32, y: f32, z: f32) {
|
||||
self.position.x += x;
|
||||
self.position.y += y;
|
||||
self.position.z += z;
|
||||
}
|
||||
|
||||
pub fn _rotate(&mut self, x: f32, y: f32, z: f32) {
|
||||
self.rotation = self.rotation * Quaternion::from(Euler::new(Deg(x), Deg(y), Deg(z)));
|
||||
}
|
||||
|
||||
pub fn get_model_matrix(&self) -> Matrix4<f32> {
|
||||
let translation = Matrix4::from_translation(self.position);
|
||||
let rotation: Matrix4<f32> = self.rotation.into();
|
||||
let scale = Matrix4::from_nonuniform_scale(self.scale.x, self.scale.y, self.scale.z);
|
||||
translation * rotation * scale
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GameObjectHandle {
|
||||
pub object_index: usize
|
||||
}
|
||||
|
||||
impl GameObjectHandle {
|
||||
pub fn get_game_object<'a>(&self, renderer: &'a VulkanRenderer) -> Option<&'a GameObject> {
|
||||
renderer.game_data.game_objects.get(self.object_index)
|
||||
}
|
||||
|
||||
pub fn get_game_object_mut<'a>(&mut self, renderer: &'a mut VulkanRenderer) -> Option<&'a mut GameObject> {
|
||||
renderer.game_data.game_objects.get_mut(self.object_index)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Updatable {
|
||||
fn update(&mut self, delta_time: f32, input: &InputState, renderer: &mut VulkanRenderer);
|
||||
}
|
||||
158
src/vulkan/mesh.rs
Normal file
158
src/vulkan/mesh.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use gltf::Error;
|
||||
use gltf::mesh::util::{ReadJoints, ReadNormals, ReadPositions, ReadTangents, ReadTexCoords, ReadWeights};
|
||||
|
||||
use crate::vulkan::mesh::LoadError::{GltfError, MeshDataMissing, NoIndices};
|
||||
use crate::vulkan::Vertex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError {
|
||||
GltfError(gltf::Error),
|
||||
MeshDataMissing(String),
|
||||
NoIndices
|
||||
}
|
||||
|
||||
impl From<gltf::Error> for LoadError {
|
||||
fn from(e: Error) -> Self {
|
||||
GltfError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for LoadError {
|
||||
fn from(e: String) -> Self {
|
||||
MeshDataMissing(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CPUMesh {
|
||||
pub vertices: Vec<Vertex>,
|
||||
pub indices: Vec<u32>,
|
||||
pub local_texture_index: Option<usize>,
|
||||
pub local_normal_map_index: Option<usize>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
pub fn load_mesh(mesh_path: &str, print_status: bool) -> Result<(Vec<CPUMesh>, Vec<gltf::image::Data>), LoadError> {
|
||||
let mut start_time = None;
|
||||
let mut total_vertices = 0;
|
||||
let mut total_indices = 0;
|
||||
|
||||
if print_status {
|
||||
start_time = Some(SystemTime::now());
|
||||
println!("Loading mesh file {}", mesh_path);
|
||||
}
|
||||
let (document, buffers, textures) = gltf::import(mesh_path)?;
|
||||
let mut meshes = vec![];
|
||||
|
||||
if print_status { println!("Mesh file loaded after {} seconds, processing...", start_time.unwrap().elapsed().unwrap().as_secs()); }
|
||||
|
||||
for mesh in document.meshes() {
|
||||
for primitive in mesh.primitives() {
|
||||
let texture_index = primitive.material().pbr_metallic_roughness().base_color_texture().map(|tex_info| tex_info.texture().index());
|
||||
let normal_map_index = primitive.material().normal_texture().map(|tex_info| tex_info.texture().index());
|
||||
|
||||
|
||||
let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
|
||||
let indices = reader.read_indices().ok_or(NoIndices)?;
|
||||
let vertices_result = create_vertices(
|
||||
reader.read_positions(),
|
||||
reader.read_tex_coords(0),
|
||||
reader.read_normals(),
|
||||
reader.read_tangents(),
|
||||
reader.read_joints(0),
|
||||
reader.read_weights(0));
|
||||
let vertices = vertices_result?;
|
||||
let cpu_mesh = CPUMesh {
|
||||
vertices,
|
||||
indices: indices.into_u32().collect(),
|
||||
local_texture_index: texture_index,
|
||||
local_normal_map_index: normal_map_index,
|
||||
name: mesh.name().map(|n| n.to_owned()),
|
||||
};
|
||||
if print_status {
|
||||
let vert_count = cpu_mesh.vertices.len();
|
||||
let index_count = cpu_mesh.indices.len();
|
||||
|
||||
println!("Loaded mesh {} after {} seconds: {} Vertices, {} Indices",
|
||||
mesh.name().unwrap_or(""),
|
||||
start_time.unwrap().elapsed().unwrap().as_secs(),
|
||||
vert_count,
|
||||
index_count);
|
||||
|
||||
total_vertices += vert_count;
|
||||
total_indices += index_count;
|
||||
}
|
||||
meshes.push(cpu_mesh);
|
||||
}
|
||||
}
|
||||
|
||||
if print_status {
|
||||
println!("Finished loading file, total vertices: {}, total indices: {}", total_vertices, total_indices);
|
||||
}
|
||||
|
||||
Ok((meshes, textures))
|
||||
}
|
||||
|
||||
fn create_vertices(positions: Option<ReadPositions>,
|
||||
tex_coords: Option<ReadTexCoords>,
|
||||
normals: Option<ReadNormals>,
|
||||
tangents: Option<ReadTangents>,
|
||||
joints: Option<ReadJoints>,
|
||||
weights: Option<ReadWeights>)
|
||||
-> Result<Vec<Vertex>, String> {
|
||||
match (positions, tex_coords, normals, tangents, joints, weights) {
|
||||
(Some(positions),
|
||||
Some(tex_coords),
|
||||
Some(normals),
|
||||
Some(tangents),
|
||||
Some(joints),
|
||||
Some(weights)) => {
|
||||
Ok(positions
|
||||
.zip(tex_coords.into_f32())
|
||||
.zip(normals)
|
||||
.zip(tangents)
|
||||
.zip(joints.into_u16().map(|arr| {
|
||||
let mut casted_joints: [i32; 4] = [0; 4];
|
||||
for i in 0..4 {
|
||||
casted_joints[i] = arr[i] as i32;
|
||||
}
|
||||
casted_joints
|
||||
}))
|
||||
.zip(weights.into_f32())
|
||||
.map(|(((((p, t), n), ta), i), w)| Vertex {
|
||||
position: p,
|
||||
uv: t,
|
||||
normal: n,
|
||||
tangent: ta,
|
||||
bone_index: i,
|
||||
bone_weight: w
|
||||
}).collect())
|
||||
},
|
||||
(Some(positions),
|
||||
Some(tex_coords),
|
||||
Some(normals),
|
||||
Some(tangents),
|
||||
None, None) => {
|
||||
Ok(positions
|
||||
.zip(tex_coords.into_f32())
|
||||
.zip(normals)
|
||||
.zip(tangents)
|
||||
.map(|(((p, t), n), ta)| Vertex {
|
||||
position: p,
|
||||
uv: t,
|
||||
normal: n,
|
||||
tangent: ta,
|
||||
bone_index: [-1; 4],
|
||||
bone_weight: [0.0; 4]
|
||||
}).collect())
|
||||
},
|
||||
(None, _, _, _, _, _) => Err("Vertex positions missing!".to_string()),
|
||||
(_, None, _, _, _, _) => Err("Tex coords missing!".to_string()),
|
||||
(_, _, None, _, _, _) => Err("Normals missing!".to_string()),
|
||||
(_, _, _, None, _, _) => Err("Tangents missing!".to_string()),
|
||||
(_, _, _, _, Some(_), None) => Err("Bone indices exist, but bone weights are missing!".to_string()),
|
||||
(_, _, _, _, None, Some(_)) => Err("Bone weights exist, but bone incides are missing!".to_string()),
|
||||
}
|
||||
}
|
||||
580
src/vulkan/mod.rs
Normal file
580
src/vulkan/mod.rs
Normal file
@@ -0,0 +1,580 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use cgmath::{Matrix4, SquareMatrix};
|
||||
use image::{ImageBuffer, Rgb, Rgba};
|
||||
use image::buffer::ConvertBuffer;
|
||||
use vulkano::{buffer::{BufferUsage, CpuAccessibleBuffer}, command_buffer::CommandBuffer, image::{ImageLayout, MipmapsCount}};
|
||||
use vulkano::command_buffer::{AutoCommandBuffer, AutoCommandBufferBuilder, DynamicState};
|
||||
use vulkano::descriptor::DescriptorSet;
|
||||
use vulkano::device::{Device, DeviceExtensions, Queue};
|
||||
use vulkano::format::{ClearValue, Format};
|
||||
use vulkano::framebuffer::{Framebuffer, FramebufferAbstract, RenderPassAbstract};
|
||||
use vulkano::image::{AttachmentImage, Dimensions, ImageUsage, ImageViewAccess, ImmutableImage, SwapchainImage};
|
||||
use vulkano::instance::{ApplicationInfo, Instance, InstanceExtensions, PhysicalDevice, Version};
|
||||
use vulkano::instance::debug::{DebugCallback, MessageSeverity, MessageType};
|
||||
use vulkano::pipeline::viewport::Viewport;
|
||||
use vulkano::sampler::{Filter, MipmapMode, Sampler, SamplerAddressMode};
|
||||
use vulkano::swapchain::{AcquireError, ColorSpace, FullscreenExclusive, PresentMode, Surface, SurfaceTransform, Swapchain, SwapchainCreationError};
|
||||
use vulkano::swapchain;
|
||||
use vulkano::sync::{FlushError, GpuFuture};
|
||||
use vulkano::sync;
|
||||
use vulkano_win::VkSurfaceBuild;
|
||||
use winit::event::{Event, WindowEvent};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowBuilder};
|
||||
|
||||
use mesh::CPUMesh;
|
||||
use pipelines::{Drawcall, LineShader};
|
||||
use pipelines::{line_vs::ty::LinePushConstants};
|
||||
use pipelines::DefaultShader;
|
||||
use pipelines::vs;
|
||||
use pipelines::vs::ty::PushConstants;
|
||||
|
||||
use crate::config::RenderConfig;
|
||||
use crate::vulkan::{gameobject::{GameObject, GameObjectHandle}};
|
||||
|
||||
pub mod pipelines;
|
||||
pub mod gameobject;
|
||||
pub mod mesh;
|
||||
|
||||
const VALIDATION_LAYERS: &[&str] = &[
|
||||
"VK_LAYER_KHRONOS_validation"
|
||||
];
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
pub normal: [f32; 3],
|
||||
pub tangent: [f32; 4],
|
||||
pub bone_index: [i32; 4],
|
||||
pub bone_weight: [f32; 4],
|
||||
}
|
||||
vulkano::impl_vertex!(Vertex, position, uv, normal, tangent, bone_index, bone_weight);
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct LinePoint {
|
||||
pub position: [f32; 3],
|
||||
}
|
||||
vulkano::impl_vertex!(LinePoint, position);
|
||||
|
||||
pub trait Game {
|
||||
/// Returns true if event should be ignored by the vulkan handler
|
||||
fn on_window_event(self: &mut Self, event: &Event<()>);
|
||||
|
||||
fn update(self: &mut Self, renderer: &mut VulkanRenderer) -> vs::ty::ObjectUniformData;
|
||||
}
|
||||
|
||||
pub struct Mesh {
|
||||
pub vertex_buffer: Arc<CpuAccessibleBuffer<[Vertex]>>,
|
||||
pub index_buffer: Arc<CpuAccessibleBuffer<[u32]>>,
|
||||
pub original_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MeshHandle {
|
||||
pub index: usize,
|
||||
pub diffuse_handle: TextureHandle,
|
||||
pub normal_handle: TextureHandle,
|
||||
pub original_path: String
|
||||
}
|
||||
|
||||
pub(crate) type TextureHandle = usize;
|
||||
|
||||
pub struct GameData {
|
||||
pub start_time: SystemTime,
|
||||
pub line_vertices: Vec<LinePoint>,
|
||||
pub push_constants: PushConstants,
|
||||
pub line_push_constants: LinePushConstants,
|
||||
pub recreate_pipeline: bool,
|
||||
pub dimensions: [u32; 2],
|
||||
pub shutdown: bool,
|
||||
pub game_objects: Vec<GameObject>,
|
||||
pub meshes: Vec<Mesh>,
|
||||
pub textures: Vec<Arc<ImmutableImage<Format>>>,
|
||||
pub use_line_pipeline: bool,
|
||||
}
|
||||
|
||||
pub(crate) type RendererDescriptorSets = dyn DescriptorSet + Send + Sync;
|
||||
|
||||
pub struct VulkanRenderer {
|
||||
pub game_data: GameData,
|
||||
pub device: Arc<Device>,
|
||||
pub framebuffers: Vec<Arc<dyn FramebufferAbstract + Send + Sync>>,
|
||||
pub sampler: Arc<Sampler>,
|
||||
pub dynamic_state: DynamicState,
|
||||
pub pipelines: Vec<Box<dyn Drawcall>>,
|
||||
pub surface: Arc<Surface<Window>>,
|
||||
pub swapchain: Arc<Swapchain<Window>>,
|
||||
pub render_pass: Arc<dyn RenderPassAbstract + Send + Sync>,
|
||||
pub queue: Arc<Queue>,
|
||||
pub recreate_swapchain: bool,
|
||||
pub debug_callback: Option<DebugCallback>,
|
||||
pub previous_frame_end: Option<Box<dyn GpuFuture>>,
|
||||
pub uniform_buffers: Vec<Arc<CpuAccessibleBuffer<vs::ty::ObjectUniformData>>>,
|
||||
pub msaa_sample_count: u32,
|
||||
}
|
||||
|
||||
impl VulkanRenderer {
|
||||
pub fn init(line_vertices: Vec<LinePoint>, enable_validation_layers: bool, render_config: RenderConfig) -> (VulkanRenderer, EventLoop<()>) {
|
||||
// Create empty game data struct to be filled
|
||||
let mut data = GameData {
|
||||
push_constants: PushConstants {
|
||||
model: Matrix4::identity().into(),
|
||||
},
|
||||
line_push_constants: LinePushConstants {
|
||||
model: Matrix4::identity().into(),
|
||||
view: Matrix4::identity().into(),
|
||||
projection: Matrix4::identity().into(),
|
||||
},
|
||||
start_time: SystemTime::now(),
|
||||
recreate_pipeline: false,
|
||||
shutdown: false,
|
||||
line_vertices,
|
||||
dimensions: [0, 0],
|
||||
meshes: vec![],
|
||||
game_objects: vec![],
|
||||
textures: vec![],
|
||||
use_line_pipeline: true,
|
||||
};
|
||||
|
||||
// Create basic vulkan instance with layers and info
|
||||
let instance = {
|
||||
let extensions = InstanceExtensions {
|
||||
ext_debug_utils: true,
|
||||
..vulkano_win::required_extensions()
|
||||
};
|
||||
|
||||
let app_info = ApplicationInfo {
|
||||
application_name: Some("Asuro's Editor".into()),
|
||||
application_version: Some(Version { major: 0, minor: 1, patch: 0 }),
|
||||
engine_name: Some("Asuro's Rust Engine".into()),
|
||||
engine_version: Some(Version { major: 0, minor: 1, patch: 0 })
|
||||
};
|
||||
|
||||
if enable_validation_layers {
|
||||
println!("Enabling validation layers...");
|
||||
let available_layers = vulkano::instance::layers_list().unwrap().map(|layer| String::from(layer.name())).collect::<Vec<String>>();
|
||||
println!("Available layers: {:?}", available_layers);
|
||||
|
||||
VALIDATION_LAYERS.iter().for_each(|wanted_layer_name| {
|
||||
if !available_layers.iter().any(|available_layer_name| available_layer_name == wanted_layer_name) {
|
||||
panic!("Validation layer not found: {:?}", wanted_layer_name);
|
||||
}
|
||||
});
|
||||
|
||||
Instance::new(Some(&app_info), &extensions, VALIDATION_LAYERS.iter().cloned()).expect("failed to create Vulkan instance")
|
||||
} else {
|
||||
Instance::new(Some(&app_info), &extensions, None).expect("failed to create Vulkan instance")
|
||||
}
|
||||
};
|
||||
|
||||
// lifetime of this is important, even tho it isn't used!
|
||||
let mut debug_callback = None;
|
||||
|
||||
// Debug stuff
|
||||
if enable_validation_layers {
|
||||
let msg_severity = MessageSeverity {
|
||||
verbose: false,
|
||||
information: true,
|
||||
warning: true,
|
||||
error: true
|
||||
};
|
||||
|
||||
let msg_types = MessageType {
|
||||
general: true,
|
||||
performance: true,
|
||||
validation: true
|
||||
};
|
||||
|
||||
debug_callback = DebugCallback::new(&instance, msg_severity, msg_types, |msg| {
|
||||
let type_str = match (msg.severity.error, msg.severity.warning, msg.severity.information, msg.severity.verbose) {
|
||||
(true, _, _, _) => "!!",
|
||||
(_, true, _, _) => "!",
|
||||
(_, _, _, true) => "i",
|
||||
_ => " "
|
||||
};
|
||||
|
||||
let layer_str = msg.layer_prefix;
|
||||
|
||||
println!("[{}][{}]: {}", type_str, layer_str, msg.description);
|
||||
}).ok();
|
||||
}
|
||||
|
||||
// TODO: Just get the first physical device we find, it's fiiiine...
|
||||
let physical = PhysicalDevice::enumerate(&instance).next().unwrap();
|
||||
println!("Using device: {} (type: {:?})", physical.name(), physical.ty());
|
||||
|
||||
let events_loop = EventLoop::new();
|
||||
let surface = WindowBuilder::new().build_vk_surface(&events_loop, instance.clone()).unwrap();
|
||||
let window = surface.window();
|
||||
|
||||
// TODO: Tutorial says we need more queues
|
||||
// In a real-life application, we would probably use at least a graphics queue and a transfers
|
||||
// queue to handle data transfers in parallel. In this example we only use one queue.
|
||||
let queue_family = physical.queue_families().find(|&q| {
|
||||
q.supports_graphics() && surface.is_supported(q).unwrap_or(false)
|
||||
}).unwrap();
|
||||
|
||||
// Queue
|
||||
let device_ext = DeviceExtensions { khr_swapchain: true, ..DeviceExtensions::none() };
|
||||
let (device, mut queues) = Device::new(physical, physical.supported_features(), &device_ext,
|
||||
[(queue_family, 0.5)].iter().cloned()).unwrap();
|
||||
let queue = queues.next().unwrap();
|
||||
|
||||
// Swapchain
|
||||
let (swapchain, images) = {
|
||||
let caps = surface.capabilities(physical).unwrap();
|
||||
let usage = caps.supported_usage_flags;
|
||||
let alpha = caps.supported_composite_alpha.iter().next().unwrap();
|
||||
let format = caps.supported_formats[0].0;
|
||||
let inner_size = window.inner_size();
|
||||
data.dimensions = [inner_size.width, inner_size.height];
|
||||
|
||||
Swapchain::new(device.clone(), surface.clone(), caps.min_image_count, format,
|
||||
data.dimensions, 1, usage, &queue, SurfaceTransform::Identity, alpha,
|
||||
PresentMode::Fifo, FullscreenExclusive::Default, true, ColorSpace::SrgbNonLinear).unwrap()
|
||||
};
|
||||
|
||||
// Render pass
|
||||
let render_pass = Arc::new(vulkano::single_pass_renderpass!(
|
||||
device.clone(),
|
||||
attachments: {
|
||||
color: {
|
||||
load: DontCare,
|
||||
store: Store,
|
||||
format: swapchain.format(),
|
||||
samples: 1,
|
||||
},
|
||||
intermediary: {
|
||||
load: Clear,
|
||||
store: DontCare,
|
||||
format: swapchain.format(),
|
||||
samples: render_config.msaa_samples,
|
||||
},
|
||||
depth: {
|
||||
load: Clear,
|
||||
store: Store,
|
||||
format: Format::D16Unorm,
|
||||
samples: render_config.msaa_samples,
|
||||
initial_layout: ImageLayout::Undefined,
|
||||
final_layout: ImageLayout::DepthStencilAttachmentOptimal,
|
||||
}
|
||||
},
|
||||
pass: {
|
||||
color: [intermediary],
|
||||
depth_stencil: {depth},
|
||||
resolve: [color]
|
||||
}
|
||||
).unwrap());
|
||||
|
||||
let sampler = Sampler::new(device.clone(), Filter::Linear, Filter::Linear,
|
||||
MipmapMode::Nearest, SamplerAddressMode::Repeat, SamplerAddressMode::Repeat,
|
||||
SamplerAddressMode::Repeat, 0.0, 1.0, 0.0, 0.0).unwrap();
|
||||
|
||||
let line_vertex_buffer = CpuAccessibleBuffer::from_iter(device.clone(), BufferUsage::vertex_buffer(), false, data.line_vertices.iter().cloned()).unwrap();
|
||||
|
||||
let pipelines: Vec<Box<dyn Drawcall>> = vec![
|
||||
Box::new(DefaultShader::new(device.clone(), render_pass.clone())),
|
||||
Box::new(LineShader::new(device.clone(), render_pass.clone(), line_vertex_buffer)),
|
||||
];
|
||||
|
||||
// Dynamic viewports allow us to recreate just the viewport when the window is resized
|
||||
// Otherwise we would have to recreate the whole pipeline.
|
||||
let mut dynamic_state = DynamicState { line_width: None, viewports: None, scissors: None, compare_mask: None, write_mask: None, reference: None };
|
||||
|
||||
let msaa_attachments = Self::create_msaa_buffers(device.clone(), data.dimensions, &swapchain, render_config.msaa_samples);
|
||||
|
||||
// The render pass we created above only describes the layout of our framebuffers. Before we
|
||||
// can draw we also need to create the actual framebuffers.
|
||||
let framebuffers = window_size_dependent_setup(device.clone(), &images, &msaa_attachments, render_config.msaa_samples, render_pass.clone(), &mut dynamic_state);
|
||||
|
||||
let mut uniform_buffers = Vec::new();
|
||||
let uniform_buffer = vs::ty::ObjectUniformData {
|
||||
view: Matrix4::identity().into(),
|
||||
projection: Matrix4::identity().into(),
|
||||
time: 0.0,
|
||||
light_position: [0.0, 0.0, 0.0],
|
||||
camera_position: [0.0, 0.0, 0.0],
|
||||
_dummy0: [0; 12],
|
||||
_dummy1: [0; 4],
|
||||
};
|
||||
|
||||
for _ in 0..swapchain.num_images() {
|
||||
uniform_buffers.push(CpuAccessibleBuffer::from_data(
|
||||
device.clone(),
|
||||
BufferUsage::uniform_buffer_transfer_destination(),
|
||||
false,
|
||||
uniform_buffer,
|
||||
).unwrap());
|
||||
}
|
||||
|
||||
// In the loop below we are going to submit commands to the GPU. Submitting a command produces
|
||||
// an object that implements the `GpuFuture` trait, which holds the resources for as long as
|
||||
// they are in use by the GPU.
|
||||
//
|
||||
// Destroying the `GpuFuture` blocks until the GPU is finished executing it. In order to avoid
|
||||
// that, we store the submission of the previous frame here.
|
||||
let previous_frame_end = Some(Box::new(sync::now(device.clone())) as Box<dyn GpuFuture>);
|
||||
|
||||
(VulkanRenderer { game_data: data, device, framebuffers, sampler,
|
||||
dynamic_state, pipelines, uniform_buffers,
|
||||
surface, swapchain, render_pass, queue,
|
||||
recreate_swapchain: false, debug_callback, previous_frame_end,
|
||||
msaa_sample_count: render_config.msaa_samples
|
||||
}, events_loop)
|
||||
}
|
||||
|
||||
fn create_command_buffer(self: &mut Self, fb_index: usize, uniform_buffer_data: vs::ty::ObjectUniformData) -> Arc<AutoCommandBuffer> {
|
||||
// General setup
|
||||
let mut builder = AutoCommandBufferBuilder::primary_simultaneous_use(self.device.clone(), self.queue.family()).unwrap();
|
||||
builder.update_buffer(self.uniform_buffers[fb_index].clone(), uniform_buffer_data).unwrap();
|
||||
builder.begin_render_pass(self.framebuffers[fb_index].clone(), false, vec![ClearValue::None, ClearValue::Float([0.0, 0.0, 0.0, 1.0]), ClearValue::Depth(1.0)]).unwrap();
|
||||
|
||||
// Draw meshes etc.
|
||||
for pipeline in &self.pipelines {
|
||||
pipeline.draw(&mut builder, fb_index, &self.game_data, &self.dynamic_state);
|
||||
}
|
||||
|
||||
// General cleanup
|
||||
builder.end_render_pass().unwrap();
|
||||
Arc::new(builder.build().unwrap())
|
||||
}
|
||||
|
||||
fn create_msaa_buffers(device: Arc<Device>, dimensions: [u32; 2], swapchain: &Arc<Swapchain<Window>>, sample_count: u32) -> Vec<Arc<AttachmentImage>> {
|
||||
let mut msaa_attachments = vec![];
|
||||
for _ in 0..swapchain.num_images() {
|
||||
msaa_attachments.push(AttachmentImage::transient_multisampled(device.clone(), dimensions, sample_count, swapchain.format()).unwrap());
|
||||
}
|
||||
msaa_attachments
|
||||
}
|
||||
|
||||
pub fn render_loop(self: &mut Self, new_ubo: vs::ty::ObjectUniformData) {
|
||||
// cleanup previous frame
|
||||
self.previous_frame_end.as_mut().unwrap().cleanup_finished();
|
||||
|
||||
// recreate swapchain if window size changed
|
||||
if self.recreate_swapchain {
|
||||
let window = self.surface.window();
|
||||
let inner_size = window.inner_size();
|
||||
self.game_data.dimensions = [inner_size.width, inner_size.height];
|
||||
|
||||
let (new_swapchain, new_images) = match self.swapchain.recreate_with_dimensions(self.game_data.dimensions) {
|
||||
Ok(r) => r,
|
||||
// This error tends to happen when the user is manually resizing the window.
|
||||
// Simply restarting the loop is the easiest way to fix this issue.
|
||||
Err(SwapchainCreationError::UnsupportedDimensions) => {
|
||||
println!("Swapchain rejected: UnsupportedDimensions");
|
||||
return;
|
||||
}
|
||||
Err(err) => panic!("{:?}", err),
|
||||
};
|
||||
|
||||
let msaa_buffers = Self::create_msaa_buffers(self.device.clone(), self.game_data.dimensions, &new_swapchain, self.msaa_sample_count);
|
||||
|
||||
self.swapchain = new_swapchain;
|
||||
// Because framebuffers contains an Arc on the old swapchain, we need to
|
||||
// recreate framebuffers as well.
|
||||
self.framebuffers = window_size_dependent_setup(self.device.clone(), &new_images, &msaa_buffers, self.msaa_sample_count, self.render_pass.clone(), &mut self.dynamic_state);
|
||||
|
||||
self.recreate_swapchain = false;
|
||||
}
|
||||
|
||||
// recreate pipeline if requested
|
||||
if self.game_data.recreate_pipeline {
|
||||
let device = self.device.clone();
|
||||
let render_pass = self.render_pass.clone();
|
||||
self.pipelines.iter_mut().for_each(|pipeline| pipeline.recreate_pipeline(device.clone(), render_pass.clone()));
|
||||
self.game_data.recreate_pipeline = false;
|
||||
}
|
||||
|
||||
// Before we can draw on the output, we have to *acquire* an image from the swapchain. If
|
||||
// no image is available (which happens if you submit draw commands too quickly), then the
|
||||
// function will block.
|
||||
// This operation returns the index of the image that we are allowed to draw upon.
|
||||
//
|
||||
// This function can block if no image is available. The parameter is an optional timeout
|
||||
// after which the function call will return an error.
|
||||
let (fb_index, _, acquire_future) = match swapchain::acquire_next_image(self.swapchain.clone(), None) {
|
||||
Ok(r) => r,
|
||||
Err(AcquireError::OutOfDate) => {
|
||||
self.recreate_swapchain = true;
|
||||
return;
|
||||
},
|
||||
Err(err) => panic!("{:?}", err)
|
||||
};
|
||||
|
||||
let command_buffer = self.create_command_buffer(fb_index, new_ubo).clone();
|
||||
|
||||
let future = self.previous_frame_end.take().unwrap()
|
||||
.join(acquire_future)
|
||||
.then_execute(self.queue.clone(), command_buffer).unwrap()
|
||||
.then_swapchain_present(self.queue.clone(), self.swapchain.clone(), fb_index)
|
||||
.then_signal_fence_and_flush();
|
||||
|
||||
match future {
|
||||
Ok(future) => {
|
||||
// we're joining on the previous future but the CPU is running faster than the GPU so
|
||||
// eventually it stutters, and jumps ahead to the newer frames.
|
||||
//
|
||||
// See vulkano issue 1135: https://github.com/vulkano-rs/vulkano/issues/1135
|
||||
// This makes sure the CPU stays in sync with the GPU in situations when the CPU is
|
||||
// running "too fast"
|
||||
#[cfg(target_os = "macos")]
|
||||
future.wait(None).unwrap();
|
||||
|
||||
self.previous_frame_end = Some(Box::new(future) as Box<_>);
|
||||
},
|
||||
Err(FlushError::OutOfDate) => {
|
||||
println!("Swapchain out of date!");
|
||||
self.recreate_swapchain = true;
|
||||
self.previous_frame_end = Some(Box::new(sync::now(self.device.clone())) as Box<_>);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{:?}", e);
|
||||
self.previous_frame_end = Some(Box::new(sync::now(self.device.clone())) as Box<_>);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn upload_mesh(self: &mut Self, mesh: CPUMesh, original_path: String) -> usize {
|
||||
let vertex_buffer = CpuAccessibleBuffer::from_iter(self.device.clone(), BufferUsage::vertex_buffer(), false, mesh.vertices.into_iter()).unwrap();
|
||||
let index_buffer = CpuAccessibleBuffer::from_iter(self.device.clone(), BufferUsage::index_buffer(), false, mesh.indices.into_iter()).unwrap();
|
||||
self.game_data.meshes.push(Mesh { vertex_buffer, index_buffer, original_path });
|
||||
self.game_data.meshes.len() - 1
|
||||
}
|
||||
|
||||
pub fn upload_texture(self: &mut Self, texture_data: &gltf::image::Data) {
|
||||
// Format buffer on cpu for upload
|
||||
let buffer: ImageBuffer<Rgb<u8>, Vec<u8>> = image::ImageBuffer::from_raw(texture_data.width, texture_data.height, texture_data.pixels.clone()).unwrap();
|
||||
let new_buffer: ImageBuffer<Rgba<u8>, Vec<u8>> = buffer.convert();
|
||||
let dimensions = Dimensions::Dim2d { width: texture_data.width, height: texture_data.height };
|
||||
|
||||
let source = CpuAccessibleBuffer::from_iter(
|
||||
self.device.clone(),
|
||||
BufferUsage::transfer_source(),
|
||||
false,
|
||||
new_buffer.iter().cloned(),
|
||||
).unwrap();
|
||||
|
||||
// Create image
|
||||
let (image_view, init) = ImmutableImage::uninitialized(
|
||||
self.device.clone(),
|
||||
dimensions,
|
||||
Format::R8G8B8A8Unorm,
|
||||
MipmapsCount::Log2,
|
||||
ImageUsage {
|
||||
transfer_source: true,
|
||||
transfer_destination: true,
|
||||
sampled: true,
|
||||
..ImageUsage::none()
|
||||
},
|
||||
ImageLayout::ShaderReadOnlyOptimal,
|
||||
self.device.active_queue_families()
|
||||
).unwrap();
|
||||
|
||||
// Upload image data
|
||||
let mut command_buffer_builder = AutoCommandBufferBuilder::new(self.device.clone(), self.queue.family()).unwrap();
|
||||
command_buffer_builder.copy_buffer_to_image_dimensions(
|
||||
source,
|
||||
init,
|
||||
[0, 0, 0],
|
||||
dimensions.width_height_depth(),
|
||||
0,
|
||||
dimensions.array_layers_with_cube(),
|
||||
0,
|
||||
).unwrap();
|
||||
|
||||
// Generate mipmaps
|
||||
// let mut mip_width = image_view.dimensions().width() as i32;
|
||||
// let mut mip_height = image_view.dimensions().height() as i32;
|
||||
|
||||
// for i in 0..image_view.mipmap_levels() {
|
||||
// command_buffer_builder.blit_image(
|
||||
// image_view.clone(),
|
||||
// [0; 3],
|
||||
// [mip_width, mip_height, 1],
|
||||
// 0,
|
||||
// i,
|
||||
// image_view.clone(),
|
||||
// [0; 3],
|
||||
// [mip_width / 2, mip_height / 2, 1],
|
||||
// 0,
|
||||
// i + 1,
|
||||
// dimensions.array_layers_with_cube(),
|
||||
// Filter::Linear).unwrap();
|
||||
|
||||
// if mip_width > 1 { mip_width = mip_width / 2; }
|
||||
// if mip_height > 1 { mip_height = mip_height / 2; }
|
||||
// }
|
||||
|
||||
let command_buffer = command_buffer_builder.build().unwrap();
|
||||
let command_buffer_future = command_buffer.execute(self.queue.clone()).unwrap();
|
||||
command_buffer_future.flush().unwrap();
|
||||
|
||||
self.game_data.textures.push(image_view);
|
||||
}
|
||||
|
||||
pub fn add_game_object(self: &mut Self, mut game_object: GameObject, pipeline_index: usize) -> GameObjectHandle {
|
||||
self.pipelines[pipeline_index].create_descriptor_set(&mut game_object, self);
|
||||
self.game_data.game_objects.push(game_object);
|
||||
|
||||
GameObjectHandle {
|
||||
object_index: self.game_data.game_objects.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
self.game_data.game_objects.clear();
|
||||
self.game_data.meshes.clear();
|
||||
self.game_data.textures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_event_loop(mut renderer: VulkanRenderer, mut game: Box<dyn Game>, event_loop: EventLoop<()>) {
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
game.on_window_event(&event);
|
||||
match event {
|
||||
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
},
|
||||
Event::RedrawEventsCleared => {
|
||||
let ubo = game.update(&mut renderer);
|
||||
renderer.render_loop(ubo);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// This method is called once during initialization, then again whenever the window is resized
|
||||
fn window_size_dependent_setup(device: Arc<Device>, images: &[Arc<SwapchainImage<Window>>], msaa_buffers: &[Arc<AttachmentImage>], msaa_sample_count: u32, render_pass: Arc<dyn RenderPassAbstract + Send + Sync>, dynamic_state: &mut DynamicState) -> Vec<Arc<dyn FramebufferAbstract + Send + Sync>> {
|
||||
let dimensions = images[0].dimensions();
|
||||
let dim_array = [dimensions.width() as f32, dimensions.height() as f32];
|
||||
let dim_array_u32 = [dimensions.width() as u32, dimensions.height() as u32];
|
||||
|
||||
let viewport = Viewport {
|
||||
origin: [0.0, 0.0],
|
||||
dimensions: dim_array,
|
||||
depth_range: 0.0 .. 1.0,
|
||||
};
|
||||
dynamic_state.viewports = Some(vec!(viewport));
|
||||
|
||||
let depth_image = AttachmentImage::multisampled_with_usage(device.clone(), dim_array_u32, msaa_sample_count, Format::D16Unorm, ImageUsage { depth_stencil_attachment: true, ..ImageUsage::none() }).unwrap();
|
||||
|
||||
let mut framebuffers = vec![];
|
||||
for i in 0..images.len() {
|
||||
let image_buffer = &images[i];
|
||||
let msaa_buffer = &msaa_buffers[i];
|
||||
|
||||
framebuffers.push(Arc::new(Framebuffer::start(render_pass.clone())
|
||||
.add(image_buffer.clone()).unwrap()
|
||||
.add(msaa_buffer.clone()).unwrap()
|
||||
.add(depth_image.clone()).unwrap()
|
||||
.build().unwrap()
|
||||
) as Arc<dyn FramebufferAbstract + Send + Sync>);
|
||||
}
|
||||
|
||||
framebuffers
|
||||
}
|
||||
184
src/vulkan/pipelines.rs
Normal file
184
src/vulkan/pipelines.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use vulkano::{command_buffer::AutoCommandBufferBuilder, descriptor::descriptor_set::PersistentDescriptorSet};
|
||||
use vulkano::command_buffer::DynamicState;
|
||||
use vulkano::device::Device;
|
||||
use vulkano::framebuffer::RenderPassAbstract;
|
||||
use vulkano::framebuffer::Subpass;
|
||||
use vulkano::pipeline::GraphicsPipeline;
|
||||
use vulkano::pipeline::GraphicsPipelineAbstract;
|
||||
|
||||
use crate::GameObject;
|
||||
use crate::vulkan::{LinePoint, Vertex};
|
||||
use crate::vulkan::GameData;
|
||||
use crate::VulkanRenderer;
|
||||
|
||||
type RP = Arc<dyn RenderPassAbstract + Send + Sync>;
|
||||
type GP = Arc<dyn GraphicsPipelineAbstract + Send + Sync>;
|
||||
|
||||
pub trait Drawcall {
|
||||
fn draw(self: &Self, builder: &mut AutoCommandBufferBuilder, fb_index: usize, game_data: &GameData, dynamic_state: &DynamicState);
|
||||
fn create_descriptor_set(self: &Self, game_object: &mut GameObject, renderer: &VulkanRenderer);
|
||||
fn recreate_pipeline(self: &mut Self, device: Arc<Device>, render_pass: RP);
|
||||
fn get_pipeline(self: &Self) -> &GP;
|
||||
}
|
||||
|
||||
pub mod vs {
|
||||
vulkano_shaders::shader!{
|
||||
ty: "vertex",
|
||||
path: "shaders/triangle.vert"
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fs {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
path: "shaders/triangle.frag"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DefaultShader {
|
||||
pipeline: GP,
|
||||
}
|
||||
|
||||
impl DefaultShader {
|
||||
pub fn new(device: Arc<Device>, render_pass: RP) -> Self {
|
||||
DefaultShader {
|
||||
pipeline: Self::create_pipeline(device, render_pass)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pipeline(device: Arc<Device>, render_pass: RP) -> GP {
|
||||
let sub_pass = Subpass::from(render_pass.clone(), 0).unwrap();
|
||||
let vertex_shader = vs::Shader::load(device.clone()).unwrap();
|
||||
let fragment_shader = fs::Shader::load(device.clone()).unwrap();
|
||||
|
||||
Arc::new(GraphicsPipeline::start()
|
||||
.vertex_input_single_buffer::<Vertex>()
|
||||
.vertex_shader(vertex_shader.main_entry_point(), ())
|
||||
.triangle_list()
|
||||
.viewports_dynamic_scissors_irrelevant(1)
|
||||
.depth_stencil_simple_depth()
|
||||
.fragment_shader(fragment_shader.main_entry_point(), ())
|
||||
.blend_alpha_blending()
|
||||
.cull_mode_back()
|
||||
.render_pass(sub_pass.clone())
|
||||
.build(device.clone())
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drawcall for DefaultShader {
|
||||
fn draw(self: &Self, builder: &mut AutoCommandBufferBuilder, fb_index: usize, game_data: &GameData, dynamic_state: &DynamicState) {
|
||||
for i in 0..game_data.game_objects.len() {
|
||||
let game_object = &game_data.game_objects[i];
|
||||
let mesh = &game_data.meshes[game_object.mesh_index];
|
||||
let mut push_constants = game_data.push_constants.clone();
|
||||
push_constants.model = game_object.get_model_matrix().into();
|
||||
|
||||
builder.draw_indexed(
|
||||
self.pipeline.clone(),
|
||||
dynamic_state,
|
||||
vec![mesh.vertex_buffer.clone()],
|
||||
mesh.index_buffer.clone(),
|
||||
game_object.descriptor_sets[fb_index].clone(),
|
||||
push_constants).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_descriptor_set(self: &Self, game_object: &mut GameObject, renderer: &VulkanRenderer) {
|
||||
let descriptor_set_layout = self.get_pipeline().descriptor_set_layout(0).unwrap().clone();
|
||||
|
||||
println!("Diff: {:?}, Norm: {:?}", game_object.texture_index, game_object.normal_map_index);
|
||||
|
||||
game_object.descriptor_sets = renderer.uniform_buffers.iter().map(|uniform_buffer| {
|
||||
let descriptor_set: Arc<(dyn vulkano::descriptor::DescriptorSet + std::marker::Send + std::marker::Sync + 'static)>;
|
||||
let builder = PersistentDescriptorSet::start(descriptor_set_layout.clone());
|
||||
|
||||
descriptor_set = Arc::new(builder
|
||||
.add_buffer(uniform_buffer.clone()).unwrap()
|
||||
.add_sampled_image(renderer.game_data.textures[game_object.texture_index].clone(), renderer.sampler.clone()).unwrap()
|
||||
.add_sampled_image(renderer.game_data.textures[game_object.normal_map_index].clone(), renderer.sampler.clone()).unwrap()
|
||||
.build().unwrap());
|
||||
|
||||
descriptor_set
|
||||
}).collect();
|
||||
}
|
||||
|
||||
fn recreate_pipeline(self: &mut Self, device: Arc<Device>, render_pass: RP) {
|
||||
self.pipeline = Self::create_pipeline(device, render_pass);
|
||||
}
|
||||
|
||||
fn get_pipeline(self: &Self) -> &GP {
|
||||
&self.pipeline
|
||||
}
|
||||
}
|
||||
|
||||
pub mod line_vs {
|
||||
vulkano_shaders::shader!{
|
||||
ty: "vertex",
|
||||
path: "shaders/line.vert"
|
||||
}
|
||||
}
|
||||
|
||||
pub mod line_fs {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
path: "shaders/line.frag"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LineShader {
|
||||
pipeline: GP,
|
||||
vertex_buffer: Arc<vulkano::buffer::CpuAccessibleBuffer<[LinePoint]>>
|
||||
}
|
||||
|
||||
impl LineShader {
|
||||
pub fn new(device: Arc<Device>, render_pass: RP, vertex_buffer: Arc<vulkano::buffer::CpuAccessibleBuffer<[LinePoint]>>) -> Self {
|
||||
LineShader {
|
||||
pipeline: Self::create_pipeline(device, render_pass),
|
||||
vertex_buffer
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pipeline(device: Arc<Device>, render_pass: RP) -> GP {
|
||||
let sub_pass = Subpass::from(render_pass.clone(), 0).unwrap();
|
||||
let vertex_shader = line_vs::Shader::load(device.clone()).unwrap();
|
||||
let fragment_shader = line_fs::Shader::load(device.clone()).unwrap();
|
||||
|
||||
Arc::new(GraphicsPipeline::start()
|
||||
.vertex_input_single_buffer::<LinePoint>()
|
||||
.vertex_shader(vertex_shader.main_entry_point(), ())
|
||||
.line_list()
|
||||
.viewports_dynamic_scissors_irrelevant(1)
|
||||
.depth_stencil_simple_depth()
|
||||
.fragment_shader(fragment_shader.main_entry_point(), ())
|
||||
.render_pass(sub_pass.clone())
|
||||
.build(device.clone())
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drawcall for LineShader {
|
||||
fn draw(self: &Self, builder: &mut AutoCommandBufferBuilder, _fb_index: usize, game_data: &GameData, dynamic_state: &DynamicState) {
|
||||
builder.draw(self.pipeline.clone(),
|
||||
&dynamic_state,
|
||||
vec![self.vertex_buffer.clone()],
|
||||
(),
|
||||
game_data.line_push_constants.clone()).unwrap();
|
||||
}
|
||||
|
||||
fn create_descriptor_set(self: &Self, _game_object: &mut GameObject, _renderer: &VulkanRenderer) {
|
||||
|
||||
}
|
||||
|
||||
fn recreate_pipeline(self: &mut Self, device: Arc<Device>, render_pass: RP) {
|
||||
self.pipeline = Self::create_pipeline(device, render_pass);
|
||||
}
|
||||
|
||||
fn get_pipeline(self: &Self) -> &GP {
|
||||
&self.pipeline
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user