vulkan: draw only in requested regions
This commit is contained in:
parent
07fb198eb4
commit
993df71c80
1 changed files with 225 additions and 65 deletions
|
|
@ -29,22 +29,25 @@ use {
|
||||||
io_uring::IoUring,
|
io_uring::IoUring,
|
||||||
rect::Region,
|
rect::Region,
|
||||||
theme::Color,
|
theme::Color,
|
||||||
utils::{copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, stack::Stack},
|
utils::{
|
||||||
|
copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, once::Once,
|
||||||
|
stack::Stack,
|
||||||
|
},
|
||||||
video::dmabuf::{dma_buf_export_sync_file, DMA_BUF_SYNC_READ, DMA_BUF_SYNC_WRITE},
|
video::dmabuf::{dma_buf_export_sync_file, DMA_BUF_SYNC_READ, DMA_BUF_SYNC_WRITE},
|
||||||
},
|
},
|
||||||
ahash::AHashMap,
|
ahash::AHashMap,
|
||||||
ash::{
|
ash::{
|
||||||
vk,
|
vk,
|
||||||
vk::{
|
vk::{
|
||||||
AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, ClearColorValue, ClearValue,
|
AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, ClearAttachment, ClearColorValue,
|
||||||
CommandBuffer, CommandBufferBeginInfo, CommandBufferSubmitInfo,
|
ClearRect, ClearValue, CommandBuffer, CommandBufferBeginInfo, CommandBufferSubmitInfo,
|
||||||
CommandBufferUsageFlags, CopyImageInfo2, DependencyInfoKHR,
|
CommandBufferUsageFlags, CopyImageInfo2, DependencyInfoKHR,
|
||||||
DescriptorBufferBindingInfoEXT, DescriptorImageInfo, DescriptorType, DeviceSize,
|
DescriptorBufferBindingInfoEXT, DescriptorImageInfo, DescriptorType, DeviceSize,
|
||||||
Extent2D, Extent3D, ImageAspectFlags, ImageCopy2, ImageLayout, ImageMemoryBarrier2,
|
Extent2D, Extent3D, ImageAspectFlags, ImageCopy2, ImageLayout, ImageMemoryBarrier2,
|
||||||
ImageSubresourceLayers, ImageSubresourceRange, PipelineBindPoint, PipelineStageFlags2,
|
ImageSubresourceLayers, ImageSubresourceRange, Offset2D, Offset3D, PipelineBindPoint,
|
||||||
Rect2D, RenderingAttachmentInfo, RenderingInfo, SemaphoreSubmitInfo,
|
PipelineStageFlags2, Rect2D, RenderingAttachmentInfo, RenderingInfo,
|
||||||
SemaphoreSubmitInfoKHR, ShaderStageFlags, SubmitInfo2, Viewport, WriteDescriptorSet,
|
SemaphoreSubmitInfo, SemaphoreSubmitInfoKHR, ShaderStageFlags, SubmitInfo2, Viewport,
|
||||||
QUEUE_FAMILY_FOREIGN_EXT,
|
WriteDescriptorSet, QUEUE_FAMILY_FOREIGN_EXT,
|
||||||
},
|
},
|
||||||
Device,
|
Device,
|
||||||
},
|
},
|
||||||
|
|
@ -139,6 +142,17 @@ pub(super) struct Memory {
|
||||||
release_fence: Option<Rc<VulkanFence>>,
|
release_fence: Option<Rc<VulkanFence>>,
|
||||||
release_sync_file: Option<SyncFile>,
|
release_sync_file: Option<SyncFile>,
|
||||||
descriptor_buffer: Option<VulkanDescriptorBuffer>,
|
descriptor_buffer: Option<VulkanDescriptorBuffer>,
|
||||||
|
paint_regions: Vec<PaintRegion>,
|
||||||
|
clear_rects: Vec<ClearRect>,
|
||||||
|
image_copy_regions: Vec<ImageCopy2<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PaintRegion {
|
||||||
|
rect: Rect2D,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
x2: f32,
|
||||||
|
y2: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct PendingFrame {
|
pub(super) struct PendingFrame {
|
||||||
|
|
@ -486,20 +500,31 @@ impl VulkanRenderer {
|
||||||
|
|
||||||
fn begin_rendering(&self, buf: CommandBuffer, fb: &VulkanImage, clear: Option<&Color>) {
|
fn begin_rendering(&self, buf: CommandBuffer, fb: &VulkanImage, clear: Option<&Color>) {
|
||||||
zone!("begin_rendering");
|
zone!("begin_rendering");
|
||||||
|
let memory = &mut *self.memory.borrow_mut();
|
||||||
|
let clear_value = clear.map(|clear| ClearValue {
|
||||||
|
color: ClearColorValue {
|
||||||
|
float32: clear.to_array_srgb(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let load_clear = memory.paint_regions.len() == 1 && {
|
||||||
|
let rect = &memory.paint_regions[0].rect;
|
||||||
|
rect.offset.x == 0
|
||||||
|
&& rect.offset.y == 0
|
||||||
|
&& rect.extent.width == fb.width
|
||||||
|
&& rect.extent.height == fb.height
|
||||||
|
};
|
||||||
|
let (load_clear, manual_clear) = match load_clear {
|
||||||
|
false => (None, clear_value),
|
||||||
|
true => (clear_value, None),
|
||||||
|
};
|
||||||
let rendering_attachment_info = {
|
let rendering_attachment_info = {
|
||||||
let mut rai = RenderingAttachmentInfo::default()
|
let mut rai = RenderingAttachmentInfo::default()
|
||||||
.image_view(fb.render_view.unwrap_or(fb.texture_view))
|
.image_view(fb.render_view.unwrap_or(fb.texture_view))
|
||||||
.image_layout(ImageLayout::GENERAL)
|
.image_layout(ImageLayout::GENERAL)
|
||||||
.load_op(AttachmentLoadOp::LOAD)
|
.load_op(AttachmentLoadOp::LOAD)
|
||||||
.store_op(AttachmentStoreOp::STORE);
|
.store_op(AttachmentStoreOp::STORE);
|
||||||
if let Some(clear) = clear {
|
if let Some(clear) = load_clear {
|
||||||
rai = rai
|
rai = rai.clear_value(clear).load_op(AttachmentLoadOp::CLEAR);
|
||||||
.clear_value(ClearValue {
|
|
||||||
color: ClearColorValue {
|
|
||||||
float32: clear.to_array_srgb(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.load_op(AttachmentLoadOp::CLEAR);
|
|
||||||
}
|
}
|
||||||
rai
|
rai
|
||||||
};
|
};
|
||||||
|
|
@ -516,6 +541,27 @@ impl VulkanRenderer {
|
||||||
unsafe {
|
unsafe {
|
||||||
self.device.device.cmd_begin_rendering(buf, &rendering_info);
|
self.device.device.cmd_begin_rendering(buf, &rendering_info);
|
||||||
}
|
}
|
||||||
|
if let Some(clear) = manual_clear {
|
||||||
|
let clear_attachment = ClearAttachment::default()
|
||||||
|
.color_attachment(0)
|
||||||
|
.clear_value(clear)
|
||||||
|
.aspect_mask(ImageAspectFlags::COLOR);
|
||||||
|
memory.clear_rects.clear();
|
||||||
|
for region in &memory.paint_regions {
|
||||||
|
memory.clear_rects.push(ClearRect {
|
||||||
|
rect: region.rect,
|
||||||
|
base_array_layer: 0,
|
||||||
|
layer_count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
self.device.device.cmd_clear_attachments(
|
||||||
|
buf,
|
||||||
|
&[clear_attachment],
|
||||||
|
&memory.clear_rects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_viewport(&self, buf: CommandBuffer, fb: &VulkanImage) {
|
fn set_viewport(&self, buf: CommandBuffer, fb: &VulkanImage) {
|
||||||
|
|
@ -552,6 +598,7 @@ impl VulkanRenderer {
|
||||||
opts: &[GfxApiOpt],
|
opts: &[GfxApiOpt],
|
||||||
) -> Result<(), VulkanError> {
|
) -> Result<(), VulkanError> {
|
||||||
zone!("record_draws");
|
zone!("record_draws");
|
||||||
|
let memory = &*self.memory.borrow();
|
||||||
let pipelines = self.get_or_create_pipelines(fb.format.vk_format)?;
|
let pipelines = self.get_or_create_pipelines(fb.format.vk_format)?;
|
||||||
let dev = &self.device.device;
|
let dev = &self.device.device;
|
||||||
let mut current_pipeline = None;
|
let mut current_pipeline = None;
|
||||||
|
|
@ -567,20 +614,27 @@ impl VulkanRenderer {
|
||||||
match opt {
|
match opt {
|
||||||
GfxApiOpt::Sync => {}
|
GfxApiOpt::Sync => {}
|
||||||
GfxApiOpt::FillRect(r) => {
|
GfxApiOpt::FillRect(r) => {
|
||||||
bind(&pipelines.fill);
|
|
||||||
let push = FillPushConstants {
|
let push = FillPushConstants {
|
||||||
pos: r.rect.to_points(),
|
pos: r.rect.to_points(),
|
||||||
color: r.color.to_array_srgb(),
|
color: r.color.to_array_srgb(),
|
||||||
};
|
};
|
||||||
unsafe {
|
for region in &memory.paint_regions {
|
||||||
dev.cmd_push_constants(
|
let mut push = push;
|
||||||
buf,
|
let draw = region.constrain(&mut push.pos, None);
|
||||||
pipelines.fill.pipeline_layout,
|
if !draw {
|
||||||
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
|
continue;
|
||||||
0,
|
}
|
||||||
uapi::as_bytes(&push),
|
bind(&pipelines.fill);
|
||||||
);
|
unsafe {
|
||||||
dev.cmd_draw(buf, 4, 1, 0, 0);
|
dev.cmd_push_constants(
|
||||||
|
buf,
|
||||||
|
pipelines.fill.pipeline_layout,
|
||||||
|
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
|
||||||
|
0,
|
||||||
|
uapi::as_bytes(&push),
|
||||||
|
);
|
||||||
|
dev.cmd_draw(buf, 4, 1, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GfxApiOpt::CopyTexture(c) => {
|
GfxApiOpt::CopyTexture(c) => {
|
||||||
|
|
@ -603,7 +657,6 @@ impl VulkanRenderer {
|
||||||
false => TexSourceType::Opaque,
|
false => TexSourceType::Opaque,
|
||||||
};
|
};
|
||||||
let pipeline = &pipelines.tex[copy_type][source_type];
|
let pipeline = &pipelines.tex[copy_type][source_type];
|
||||||
bind(pipeline);
|
|
||||||
let push = TexPushConstants {
|
let push = TexPushConstants {
|
||||||
pos: c.target.to_points(),
|
pos: c.target.to_points(),
|
||||||
tex_pos: c.source.to_points(),
|
tex_pos: c.source.to_points(),
|
||||||
|
|
@ -612,36 +665,47 @@ impl VulkanRenderer {
|
||||||
let image_info = DescriptorImageInfo::default()
|
let image_info = DescriptorImageInfo::default()
|
||||||
.image_view(tex.texture_view)
|
.image_view(tex.texture_view)
|
||||||
.image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
.image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
||||||
unsafe {
|
let init = Once::default();
|
||||||
if let Some(db) = &self.device.descriptor_buffer {
|
for region in &memory.paint_regions {
|
||||||
db.cmd_set_descriptor_buffer_offsets(
|
let mut push = push;
|
||||||
buf,
|
let draw = region.constrain(&mut push.pos, Some(&mut push.tex_pos));
|
||||||
PipelineBindPoint::GRAPHICS,
|
if !draw {
|
||||||
pipeline.pipeline_layout,
|
continue;
|
||||||
0,
|
}
|
||||||
&[0],
|
init.exec(|| unsafe {
|
||||||
&[tex.descriptor_buffer_offset.get()],
|
bind(pipeline);
|
||||||
);
|
if let Some(db) = &self.device.descriptor_buffer {
|
||||||
} else {
|
db.cmd_set_descriptor_buffer_offsets(
|
||||||
let write_descriptor_set = WriteDescriptorSet::default()
|
buf,
|
||||||
.descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER)
|
PipelineBindPoint::GRAPHICS,
|
||||||
.image_info(slice::from_ref(&image_info));
|
pipeline.pipeline_layout,
|
||||||
self.device.push_descriptor.cmd_push_descriptor_set(
|
0,
|
||||||
buf,
|
&[0],
|
||||||
PipelineBindPoint::GRAPHICS,
|
&[tex.descriptor_buffer_offset.get()],
|
||||||
pipeline.pipeline_layout,
|
);
|
||||||
0,
|
} else {
|
||||||
slice::from_ref(&write_descriptor_set),
|
let write_descriptor_set = WriteDescriptorSet::default()
|
||||||
);
|
.descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
|
.image_info(slice::from_ref(&image_info));
|
||||||
|
self.device.push_descriptor.cmd_push_descriptor_set(
|
||||||
|
buf,
|
||||||
|
PipelineBindPoint::GRAPHICS,
|
||||||
|
pipeline.pipeline_layout,
|
||||||
|
0,
|
||||||
|
slice::from_ref(&write_descriptor_set),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
unsafe {
|
||||||
|
dev.cmd_push_constants(
|
||||||
|
buf,
|
||||||
|
pipeline.pipeline_layout,
|
||||||
|
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
|
||||||
|
0,
|
||||||
|
uapi::as_bytes(&push),
|
||||||
|
);
|
||||||
|
dev.cmd_draw(buf, 4, 1, 0, 0);
|
||||||
}
|
}
|
||||||
dev.cmd_push_constants(
|
|
||||||
buf,
|
|
||||||
pipeline.pipeline_layout,
|
|
||||||
ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
|
|
||||||
0,
|
|
||||||
uapi::as_bytes(&push),
|
|
||||||
);
|
|
||||||
dev.cmd_draw(buf, 4, 1, 0, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -697,20 +761,33 @@ impl VulkanRenderer {
|
||||||
.layer_count(1)
|
.layer_count(1)
|
||||||
.base_array_layer(0)
|
.base_array_layer(0)
|
||||||
.mip_level(0);
|
.mip_level(0);
|
||||||
let image_copy = ImageCopy2::default()
|
memory.image_copy_regions.clear();
|
||||||
.src_subresource(image_subresource_layers)
|
for region in &memory.paint_regions {
|
||||||
.dst_subresource(image_subresource_layers)
|
let offset = Offset3D {
|
||||||
.extent(Extent3D {
|
x: region.rect.offset.x,
|
||||||
width: fb.width,
|
y: region.rect.offset.y,
|
||||||
height: fb.height,
|
z: 0,
|
||||||
|
};
|
||||||
|
let extent = Extent3D {
|
||||||
|
width: region.rect.extent.width,
|
||||||
|
height: region.rect.extent.height,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
});
|
};
|
||||||
|
memory.image_copy_regions.push(
|
||||||
|
ImageCopy2::default()
|
||||||
|
.src_subresource(image_subresource_layers)
|
||||||
|
.dst_subresource(image_subresource_layers)
|
||||||
|
.src_offset(offset)
|
||||||
|
.dst_offset(offset)
|
||||||
|
.extent(extent),
|
||||||
|
);
|
||||||
|
}
|
||||||
let copy_image_info = CopyImageInfo2::default()
|
let copy_image_info = CopyImageInfo2::default()
|
||||||
.src_image(fb.image)
|
.src_image(fb.image)
|
||||||
.src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL)
|
.src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL)
|
||||||
.dst_image(bridge.dmabuf_image)
|
.dst_image(bridge.dmabuf_image)
|
||||||
.dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL)
|
.dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||||
.regions(slice::from_ref(&image_copy));
|
.regions(&memory.image_copy_regions);
|
||||||
unsafe {
|
unsafe {
|
||||||
self.device.device.cmd_copy_image2(buf, ©_image_info);
|
self.device.device.cmd_copy_image2(buf, ©_image_info);
|
||||||
}
|
}
|
||||||
|
|
@ -966,10 +1043,10 @@ impl VulkanRenderer {
|
||||||
fb_release_sync: ReleaseSync,
|
fb_release_sync: ReleaseSync,
|
||||||
opts: &[GfxApiOpt],
|
opts: &[GfxApiOpt],
|
||||||
clear: Option<&Color>,
|
clear: Option<&Color>,
|
||||||
_region: &Region,
|
region: &Region,
|
||||||
) -> Result<Option<SyncFile>, VulkanError> {
|
) -> Result<Option<SyncFile>, VulkanError> {
|
||||||
zone!("execute");
|
zone!("execute");
|
||||||
let res = self.try_execute(fb, fb_acquire_sync, fb_release_sync, opts, clear);
|
let res = self.try_execute(fb, fb_acquire_sync, fb_release_sync, opts, clear, region);
|
||||||
let sync_file = {
|
let sync_file = {
|
||||||
let mut memory = self.memory.borrow_mut();
|
let mut memory = self.memory.borrow_mut();
|
||||||
memory.textures.clear();
|
memory.textures.clear();
|
||||||
|
|
@ -991,6 +1068,39 @@ impl VulkanRenderer {
|
||||||
Ok(semaphore)
|
Ok(semaphore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_paint_regions(&self, fb: &VulkanImage, region: &Region) {
|
||||||
|
let memory = &mut *self.memory.borrow_mut();
|
||||||
|
memory.paint_regions.clear();
|
||||||
|
for rect in region.rects() {
|
||||||
|
let x1 = rect.x1().max(0);
|
||||||
|
let y1 = rect.y1().max(0);
|
||||||
|
let x2 = rect.x2();
|
||||||
|
let y2 = rect.y2();
|
||||||
|
if x1 as u32 > fb.width || y1 as u32 > fb.height || x2 <= 0 || y2 <= 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x2 = x2.min(fb.width as i32);
|
||||||
|
let y2 = y2.min(fb.height as i32);
|
||||||
|
let to_fb = |c: i32, max: u32| 2.0 * (c as f32 / max as f32) - 1.0;
|
||||||
|
memory.paint_regions.push(PaintRegion {
|
||||||
|
rect: Rect2D {
|
||||||
|
offset: Offset2D {
|
||||||
|
x: x1 as _,
|
||||||
|
y: y1 as _,
|
||||||
|
},
|
||||||
|
extent: Extent2D {
|
||||||
|
width: (x2 - x1) as u32,
|
||||||
|
height: (y2 - y1) as u32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x1: to_fb(x1, fb.width),
|
||||||
|
x2: to_fb(x2, fb.width),
|
||||||
|
y1: to_fb(y1, fb.height),
|
||||||
|
y2: to_fb(y2, fb.height),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn try_execute(
|
fn try_execute(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
fb: &VulkanImage,
|
fb: &VulkanImage,
|
||||||
|
|
@ -998,8 +1108,10 @@ impl VulkanRenderer {
|
||||||
fb_release_sync: ReleaseSync,
|
fb_release_sync: ReleaseSync,
|
||||||
opts: &[GfxApiOpt],
|
opts: &[GfxApiOpt],
|
||||||
clear: Option<&Color>,
|
clear: Option<&Color>,
|
||||||
|
region: &Region,
|
||||||
) -> Result<(), VulkanError> {
|
) -> Result<(), VulkanError> {
|
||||||
self.check_defunct()?;
|
self.check_defunct()?;
|
||||||
|
self.create_paint_regions(fb, region);
|
||||||
let buf = self.gfx_command_buffers.allocate()?;
|
let buf = self.gfx_command_buffers.allocate()?;
|
||||||
self.collect_memory(opts);
|
self.collect_memory(opts);
|
||||||
self.begin_command_buffer(buf.buffer)?;
|
self.begin_command_buffer(buf.buffer)?;
|
||||||
|
|
@ -1125,3 +1237,51 @@ async fn await_release(
|
||||||
}
|
}
|
||||||
renderer.pending_frames.remove(&frame.point);
|
renderer.pending_frames.remove(&frame.point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PaintRegion {
|
||||||
|
fn constrain(&self, pos: &mut [[f32; 2]; 4], tex_pos: Option<&mut [[f32; 2]; 4]>) -> bool {
|
||||||
|
zone!("constrain");
|
||||||
|
let mut npos = *pos;
|
||||||
|
for [x, y] in &mut npos {
|
||||||
|
*x = x.clamp(self.x1, self.x2);
|
||||||
|
*y = y.clamp(self.y1, self.y2);
|
||||||
|
}
|
||||||
|
if npos == *pos {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if npos[0] == npos[1] && npos[2] == npos[3] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if npos[0] == npos[2] && npos[1] == npos[3] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(tp) = tex_pos {
|
||||||
|
let mut ntp = *tp;
|
||||||
|
for i in 0..4 {
|
||||||
|
if npos[i] == pos[i] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
macro_rules! sub {
|
||||||
|
($l:expr, $r:expr) => {
|
||||||
|
[$l[0] - $r[0], $l[1] - $r[1]]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let dx = sub!(npos[i], pos[i]);
|
||||||
|
let dy = sub!(pos[(i + 1) & 3], pos[i]);
|
||||||
|
let dz = sub!(pos[(i + 2) & 3], pos[i]);
|
||||||
|
let det = 1.0 / (dy[0] * dz[1] - dy[1] * dz[0]);
|
||||||
|
let alpha = [
|
||||||
|
(dx[0] * dz[1] - dx[1] * dz[0]) * det,
|
||||||
|
(dx[1] * dy[0] - dx[0] * dy[1]) * det,
|
||||||
|
];
|
||||||
|
let dy = sub!(tp[(i + 1) & 3], tp[i]);
|
||||||
|
let dz = sub!(tp[(i + 2) & 3], tp[i]);
|
||||||
|
ntp[i][0] += alpha[0] * dy[0] + alpha[1] * dz[0];
|
||||||
|
ntp[i][1] += alpha[0] * dy[1] + alpha[1] * dz[1];
|
||||||
|
}
|
||||||
|
*tp = ntp;
|
||||||
|
}
|
||||||
|
*pos = npos;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue