Add option to reuse renderpass in rendertarget
This commit is contained in:
parent
dca9ab20be
commit
358c3c5182
5 changed files with 217 additions and 246 deletions
|
@ -9,6 +9,7 @@ pub mod sub_pass;
|
|||
use sub_pass::{AttachmentInfo, AttachmentInfoUsage, SubPass};
|
||||
|
||||
pub struct RenderTargetBuilder {
|
||||
old_render_target: Option<RenderTarget>,
|
||||
sub_passes: Vec<SubPass>,
|
||||
}
|
||||
|
||||
|
@ -19,6 +20,12 @@ impl RenderTargetBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn preserve_old_render_pass(mut self, render_target: RenderTarget) -> Self {
|
||||
self.old_render_target = Some(render_target);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self, device: &Arc<Device>) -> Result<RenderTarget> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
|
@ -33,160 +40,143 @@ impl RenderTargetBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
// create render pass
|
||||
|
||||
// gather attachment descriptions
|
||||
let mut attachments = Vec::new();
|
||||
|
||||
self.map_attachment(|attachment| {
|
||||
attachments.push(attachment.description.clone());
|
||||
});
|
||||
|
||||
// create attachment references
|
||||
let mut attachment_references: Vec<SubPassAttachmentReferences> = Vec::new();
|
||||
let mut attachment_index = 0;
|
||||
|
||||
// gather all color, depth and resolve attachment and add input attachments from previous sup passes
|
||||
for sub_pass in self.sub_passes.iter() {
|
||||
let mut references = SubPassAttachmentReferences::default();
|
||||
references.offset = attachment_index as usize;
|
||||
|
||||
for attachment in sub_pass.attachments().iter() {
|
||||
let attachment_reference = VkAttachmentReference {
|
||||
attachment: attachment_index,
|
||||
layout: attachment.layout,
|
||||
};
|
||||
|
||||
match attachment.usage {
|
||||
AttachmentInfoUsage::Output => {
|
||||
references.color_attachments.push(attachment_reference);
|
||||
}
|
||||
AttachmentInfoUsage::Resolve => {
|
||||
references.resolve_attachments.push(attachment_reference);
|
||||
}
|
||||
AttachmentInfoUsage::Depth => {
|
||||
// make sure only 1 depth attachment is used per subpass
|
||||
debug_assert!(
|
||||
references.depth_stencil_attachment.is_none(),
|
||||
"only 1 depth attachment per sub pass allowed"
|
||||
);
|
||||
|
||||
references.depth_stencil_attachment = Some(attachment_reference);
|
||||
}
|
||||
}
|
||||
|
||||
attachment_index += 1;
|
||||
}
|
||||
|
||||
// check if input infos are set
|
||||
if let Some(input_info) = sub_pass.inputs() {
|
||||
debug_assert!(input_info.sub_pass_index < attachment_references.len());
|
||||
|
||||
let input_pass_references = &attachment_references[input_info.sub_pass_index];
|
||||
|
||||
for input_index in input_info.input_indices.iter() {
|
||||
references.input_attachments.push(VkAttachmentReference {
|
||||
attachment: (input_index + input_pass_references.offset) as u32,
|
||||
layout: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attachment_references.push(references);
|
||||
}
|
||||
|
||||
// gather sub pass descriptions
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
for attachment_reference in attachment_references.iter() {
|
||||
descriptions.push(VkSubpassDescription::new(
|
||||
0,
|
||||
&attachment_reference.input_attachments,
|
||||
&attachment_reference.color_attachments,
|
||||
&attachment_reference.resolve_attachments,
|
||||
attachment_reference.depth_stencil_attachment.as_ref(),
|
||||
&[],
|
||||
));
|
||||
}
|
||||
|
||||
// gather sub pass dependencies
|
||||
let mut dependencies = Vec::new();
|
||||
|
||||
dependencies.push(VkSubpassDependency::new(
|
||||
VK_SUBPASS_EXTERNAL,
|
||||
0,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
VK_DEPENDENCY_BY_REGION_BIT,
|
||||
));
|
||||
|
||||
for (index, sub_pass) in self.sub_passes.iter().enumerate() {
|
||||
dependencies.push(VkSubpassDependency::new(
|
||||
index as u32,
|
||||
index as u32 + 1,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
CommandBuffer::access_to_stage(sub_pass.output_usage()),
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
sub_pass.output_usage(),
|
||||
VK_DEPENDENCY_BY_REGION_BIT,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(last_dependency) = dependencies.last_mut() {
|
||||
last_dependency.dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||
last_dependency.dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT.into();
|
||||
last_dependency.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT.into();
|
||||
}
|
||||
|
||||
// dependencies.push(VkSubpassDependency::new(
|
||||
// self.sub_passes.len() as u32 - 1,
|
||||
// VK_SUBPASS_EXTERNAL,
|
||||
// VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
// VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
// VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
// VK_ACCESS_MEMORY_READ_BIT,
|
||||
// VK_DEPENDENCY_BY_REGION_BIT,
|
||||
// ));
|
||||
|
||||
let render_pass =
|
||||
RenderPass::new(device.clone(), &descriptions, &attachments, &dependencies)?;
|
||||
|
||||
// create frame buffers
|
||||
let max_images = self.max_images();
|
||||
let extent = self.sub_passes[0].extent();
|
||||
|
||||
let framebuffers: Result<Vec<Arc<Framebuffer>>> = (0..max_images)
|
||||
.map(|i| {
|
||||
let mut framebuffer_builder = Framebuffer::builder()
|
||||
.set_render_pass(&render_pass)
|
||||
.set_width(extent.width)
|
||||
.set_height(extent.height);
|
||||
|
||||
for sub_pass in self.sub_passes.iter() {
|
||||
for attachment in sub_pass.attachments().iter() {
|
||||
framebuffer_builder =
|
||||
framebuffer_builder.add_attachment(attachment.image(i));
|
||||
}
|
||||
}
|
||||
|
||||
framebuffer_builder.build(render_pass.device().clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut clear_values = Vec::new();
|
||||
|
||||
self.map_attachment(|attachment| {
|
||||
clear_values.push(attachment.clear_value.clone());
|
||||
});
|
||||
|
||||
// create render pass
|
||||
let render_pass = match self.old_render_target {
|
||||
Some(old_render_target) => {
|
||||
if !Self::verify_setups(&old_render_target.sub_passes, &self.sub_passes) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Can't preserve old RenderPass if subpasses don't match!"
|
||||
));
|
||||
}
|
||||
|
||||
old_render_target.render_pass
|
||||
}
|
||||
None => {
|
||||
// gather attachment descriptions
|
||||
let mut attachments = Vec::new();
|
||||
|
||||
self.map_attachment(|attachment| {
|
||||
attachments.push(attachment.description.clone());
|
||||
});
|
||||
|
||||
// create attachment references
|
||||
let mut attachment_references: Vec<SubPassAttachmentReferences> = Vec::new();
|
||||
let mut attachment_index = 0;
|
||||
|
||||
// gather all color, depth and resolve attachment and add input attachments from previous sup passes
|
||||
for sub_pass in self.sub_passes.iter() {
|
||||
let mut references = SubPassAttachmentReferences::default();
|
||||
references.offset = attachment_index as usize;
|
||||
|
||||
for attachment in sub_pass.attachments().iter() {
|
||||
let attachment_reference = VkAttachmentReference {
|
||||
attachment: attachment_index,
|
||||
layout: attachment.layout,
|
||||
};
|
||||
|
||||
match attachment.usage {
|
||||
AttachmentInfoUsage::Output => {
|
||||
references.color_attachments.push(attachment_reference);
|
||||
}
|
||||
AttachmentInfoUsage::Resolve => {
|
||||
references.resolve_attachments.push(attachment_reference);
|
||||
}
|
||||
AttachmentInfoUsage::Depth => {
|
||||
// make sure only 1 depth attachment is used per subpass
|
||||
debug_assert!(
|
||||
references.depth_stencil_attachment.is_none(),
|
||||
"only 1 depth attachment per sub pass allowed"
|
||||
);
|
||||
|
||||
references.depth_stencil_attachment = Some(attachment_reference);
|
||||
}
|
||||
}
|
||||
|
||||
attachment_index += 1;
|
||||
}
|
||||
|
||||
// check if input infos are set
|
||||
if let Some(input_info) = sub_pass.inputs() {
|
||||
debug_assert!(input_info.sub_pass_index < attachment_references.len());
|
||||
|
||||
let input_pass_references =
|
||||
&attachment_references[input_info.sub_pass_index];
|
||||
|
||||
for input_index in input_info.input_indices.iter() {
|
||||
references.input_attachments.push(VkAttachmentReference {
|
||||
attachment: (input_index + input_pass_references.offset) as u32,
|
||||
layout: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attachment_references.push(references);
|
||||
}
|
||||
|
||||
// gather sub pass descriptions
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
for attachment_reference in attachment_references.iter() {
|
||||
descriptions.push(VkSubpassDescription::new(
|
||||
0,
|
||||
&attachment_reference.input_attachments,
|
||||
&attachment_reference.color_attachments,
|
||||
&attachment_reference.resolve_attachments,
|
||||
attachment_reference.depth_stencil_attachment.as_ref(),
|
||||
&[],
|
||||
));
|
||||
}
|
||||
|
||||
// gather sub pass dependencies
|
||||
let mut dependencies = Vec::new();
|
||||
|
||||
dependencies.push(VkSubpassDependency::new(
|
||||
VK_SUBPASS_EXTERNAL,
|
||||
0,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
VK_DEPENDENCY_BY_REGION_BIT,
|
||||
));
|
||||
|
||||
for (index, sub_pass) in self.sub_passes.iter().enumerate() {
|
||||
dependencies.push(VkSubpassDependency::new(
|
||||
index as u32,
|
||||
index as u32 + 1,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
CommandBuffer::access_to_stage(sub_pass.output_usage()),
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
sub_pass.output_usage(),
|
||||
VK_DEPENDENCY_BY_REGION_BIT,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(last_dependency) = dependencies.last_mut() {
|
||||
last_dependency.dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||
last_dependency.dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT.into();
|
||||
last_dependency.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT.into();
|
||||
}
|
||||
|
||||
RenderPass::new(device.clone(), &descriptions, &attachments, &dependencies)?
|
||||
}
|
||||
};
|
||||
|
||||
// create frame buffers
|
||||
let framebuffers = Self::create_framebuffers(&render_pass, &self.sub_passes)?;
|
||||
|
||||
Ok(RenderTarget {
|
||||
render_pass,
|
||||
framebuffers: framebuffers?,
|
||||
framebuffers,
|
||||
clear_values,
|
||||
|
||||
extent,
|
||||
extent: self.sub_passes[0].extent(),
|
||||
|
||||
sub_passes: self.sub_passes,
|
||||
|
||||
|
@ -195,17 +185,56 @@ impl RenderTargetBuilder {
|
|||
})
|
||||
}
|
||||
|
||||
fn create_framebuffers(
|
||||
render_pass: &Arc<RenderPass>,
|
||||
sub_passes: &[SubPass],
|
||||
) -> Result<Vec<Arc<Framebuffer>>> {
|
||||
let extent = sub_passes[0].extent();
|
||||
|
||||
(0..Self::max_images(sub_passes))
|
||||
.map(|i| {
|
||||
let mut framebuffer_builder = Framebuffer::builder()
|
||||
.set_render_pass(render_pass)
|
||||
.set_width(extent.width)
|
||||
.set_height(extent.height);
|
||||
|
||||
for sub_pass in sub_passes.iter() {
|
||||
for attachment in sub_pass.attachments().iter() {
|
||||
framebuffer_builder =
|
||||
framebuffer_builder.add_attachment(attachment.image(i));
|
||||
}
|
||||
}
|
||||
|
||||
framebuffer_builder.build(render_pass.device().clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn max_images(&self) -> usize {
|
||||
fn max_images(sub_passes: &[SubPass]) -> usize {
|
||||
let mut max_images = 0;
|
||||
|
||||
for sub_pass in self.sub_passes.iter() {
|
||||
for sub_pass in sub_passes.iter() {
|
||||
max_images = max_images.max(sub_pass.max_images_per_attachment());
|
||||
}
|
||||
|
||||
max_images
|
||||
}
|
||||
|
||||
fn verify_setups(old_sub_passes: &[SubPass], new_sub_passes: &[SubPass]) -> bool {
|
||||
if old_sub_passes.len() != new_sub_passes.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (new_sub_pass, old_sub_pass) in old_sub_passes.iter().zip(new_sub_passes.iter()) {
|
||||
if !new_sub_pass.verify_setup(old_sub_pass) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map_attachment<'a, F>(&'a self, mut f: F)
|
||||
where
|
||||
|
@ -247,6 +276,7 @@ pub struct RenderTarget {
|
|||
impl RenderTarget {
|
||||
pub fn builder() -> RenderTargetBuilder {
|
||||
RenderTargetBuilder {
|
||||
old_render_target: None,
|
||||
sub_passes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
@ -315,99 +345,3 @@ impl RenderTarget {
|
|||
buffer_recorder.end_render_pass();
|
||||
}
|
||||
}
|
||||
|
||||
// impl<'a> RenderTargetBuilder<'a> {
|
||||
// fn create_images_and_renderpass(&self, device: &Arc<Device>) -> Result<Arc<RenderPass>> {
|
||||
// let subpass_descriptions = [match resolve_reference {
|
||||
// Some(resvole_ref) => VkSubpassDescription::new(
|
||||
// 0,
|
||||
// &[],
|
||||
// color_references.as_slice(),
|
||||
// &[resvole_ref],
|
||||
// match depth_reference {
|
||||
// Some(ref depth_ref) => Some(depth_ref),
|
||||
// None => None,
|
||||
// },
|
||||
// &[],
|
||||
// ),
|
||||
// None => VkSubpassDescription::new(
|
||||
// 0,
|
||||
// &[],
|
||||
// color_references.as_slice(),
|
||||
// &[],
|
||||
// match depth_reference {
|
||||
// Some(ref depth_ref) => Some(depth_ref),
|
||||
// None => None,
|
||||
// },
|
||||
// &[],
|
||||
// ),
|
||||
// }];
|
||||
|
||||
// let dependencies = if color_references.is_empty() {
|
||||
// // assume, that when no color references are given,
|
||||
// // we want to store the depth information for later
|
||||
// if depth_reference.is_some() {
|
||||
// for attachment in &mut attachments {
|
||||
// if attachment.format == VK_FORMAT_D16_UNORM {
|
||||
// attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
// attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// [
|
||||
// VkSubpassDependency::new(
|
||||
// VK_SUBPASS_EXTERNAL,
|
||||
// 0,
|
||||
// VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
// VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
// 0,
|
||||
// VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT
|
||||
// | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
// VK_DEPENDENCY_BY_REGION_BIT,
|
||||
// ),
|
||||
// VkSubpassDependency::new(
|
||||
// 0,
|
||||
// VK_SUBPASS_EXTERNAL,
|
||||
// VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
// VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
// VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT
|
||||
// | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
// VK_ACCESS_SHADER_READ_BIT,
|
||||
// VK_DEPENDENCY_BY_REGION_BIT,
|
||||
// ),
|
||||
// ]
|
||||
// } else {
|
||||
// [
|
||||
// VkSubpassDependency::new(
|
||||
// VK_SUBPASS_EXTERNAL,
|
||||
// 0,
|
||||
// VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
// VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
// VK_ACCESS_MEMORY_READ_BIT,
|
||||
// VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
// VK_DEPENDENCY_BY_REGION_BIT,
|
||||
// ),
|
||||
// VkSubpassDependency::new(
|
||||
// 0,
|
||||
// VK_SUBPASS_EXTERNAL,
|
||||
// VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
// VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
// VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
// VK_ACCESS_MEMORY_READ_BIT,
|
||||
// VK_DEPENDENCY_BY_REGION_BIT,
|
||||
// ),
|
||||
// ]
|
||||
// };
|
||||
|
||||
// let renderpass = RenderPass::new(
|
||||
// device.clone(),
|
||||
// &subpass_descriptions,
|
||||
// attachments.as_slice(),
|
||||
// &dependencies,
|
||||
// )?;
|
||||
|
||||
// Ok(renderpass)
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -157,7 +157,7 @@ impl<'a> From<&'a Vec<Arc<Image>>> for ResolveTarget {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct InputAttachmentInfo {
|
||||
pub sub_pass_index: usize,
|
||||
pub input_indices: Vec<usize>,
|
||||
|
@ -383,6 +383,13 @@ impl AttachmentInfo {
|
|||
|
||||
&self.images[index]
|
||||
}
|
||||
|
||||
fn verify_setup(&self, other: &Self) -> bool {
|
||||
self.clear_value == other.clear_value
|
||||
&& self.layout == other.layout
|
||||
&& self.description == other.description
|
||||
&& self.usage == other.usage
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -439,4 +446,34 @@ impl SubPass {
|
|||
|
||||
max_images
|
||||
}
|
||||
|
||||
pub(crate) fn verify_setup(&self, other: &Self) -> bool {
|
||||
if self.output_usage != other.output_usage {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (&self.input_info, &other.input_info) {
|
||||
(None, None) => (),
|
||||
(Some(self_input_info), Some(other_input_info)) => {
|
||||
if self_input_info != other_input_info {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
(None, Some(_)) | (Some(_), None) => return false,
|
||||
}
|
||||
|
||||
if self.attachments.len() != other.attachments.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (self_attachment, other_attachment) in
|
||||
self.attachments.iter().zip(other.attachments.iter())
|
||||
{
|
||||
if !self_attachment.verify_setup(other_attachment) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VkAttachmentDescription {
|
||||
pub flags: VkAttachmentDescriptionFlagBits,
|
||||
pub format: VkFormat,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::mem;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VkClearColorValue([u32; 4]);
|
||||
|
||||
impl VkClearColorValue {
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::prelude::*;
|
|||
use std::mem;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VkClearValue(VkClearColorValue);
|
||||
|
||||
impl VkClearValue {
|
||||
|
|
Loading…
Reference in a new issue