use std::{
path::Path,
rc::Rc,
sync::atomic::{AtomicU64, Ordering},
};
type RenderTargetImageInfo = (
ash::vk::Image,
ash::vk::ImageView,
u32,
u32,
Rc<dyn std::any::Any>,
);
#[derive(Clone, Copy)]
pub(super) struct RenderTargetLease {
pub(super) slot: usize,
pub(super) generation: u64,
}
use crate::{Color, DType, Engine, Error, Image, ImageFormat, ImageLayout, Matrix, Result};
#[derive(Clone)]
enum TextureBacking {
BufferBacked {
data: Matrix,
native: Rc<crate::runtime::NativeRgbaImage>,
},
RenderTarget {
engine_handle: crate::runtime::EngineHandle,
owner: Rc<std::cell::RefCell<super::renderer::RendererState>>,
lease: RenderTargetLease,
image: ash::vk::Image,
view: ash::vk::ImageView,
width: u32,
height: u32,
},
}
#[derive(Clone)]
pub(crate) struct TextureSemantic {
pub(crate) id: u64,
}
impl TextureSemantic {
fn new() -> crate::Result<Self> {
Ok(Self {
id: next_texture_value_id()?,
})
}
}
#[derive(Clone)]
pub struct Texture {
backing: TextureBacking,
semantic: Rc<TextureSemantic>,
}
impl Texture {
#[allow(dead_code)]
pub(crate) fn value_id(&self) -> u64 {
self.semantic.id
}
pub(crate) fn engine_handle(&self) -> &crate::runtime::EngineHandle {
match &self.backing {
TextureBacking::BufferBacked { data, .. } => data.engine_handle(),
TextureBacking::RenderTarget { engine_handle, .. } => engine_handle,
}
}
pub fn width(&self) -> usize {
match &self.backing {
TextureBacking::BufferBacked { data, .. } => data.shape()[1],
TextureBacking::RenderTarget { width, .. } => *width as usize,
}
}
pub fn height(&self) -> usize {
match &self.backing {
TextureBacking::BufferBacked { data, .. } => data.shape()[0],
TextureBacking::RenderTarget { height, .. } => *height as usize,
}
}
pub fn dtype(&self) -> DType {
match &self.backing {
TextureBacking::BufferBacked { data, .. } => data.dtype(),
TextureBacking::RenderTarget { .. } => DType::U8,
}
}
pub fn read_rgba8(&self) -> Result<Vec<u8>> {
match &self.backing {
TextureBacking::BufferBacked { data, .. } => data.read::<u8>(),
TextureBacking::RenderTarget { .. } => Err(Error::failed_precondition(
"render::Texture::read_rgba8 is not available for render-target-backed textures; \
use RenderFrame::consume_readback",
)),
}
}
pub(crate) fn native(&self) -> Option<Rc<crate::runtime::NativeRgbaImage>> {
match &self.backing {
TextureBacking::BufferBacked { native, .. } => Some(native.clone()),
TextureBacking::RenderTarget { .. } => None,
}
}
pub(crate) fn packed_storage(&self) -> Option<&crate::runtime::Storage> {
match &self.backing {
TextureBacking::BufferBacked { data, .. } => Some(data.storage()),
TextureBacking::RenderTarget { .. } => None,
}
}
pub(crate) const fn is_render_target(&self) -> bool {
matches!(self.backing, TextureBacking::RenderTarget { .. })
}
pub(crate) fn mark_consumed(&self, event: &crate::Event) -> Result<()> {
match &self.backing {
TextureBacking::RenderTarget {
engine_handle,
owner,
lease,
..
} => {
if !engine_handle.owns_event(event) {
return Err(Error::invalid_argument(
"render target consumer event belongs to another Engine",
));
}
owner
.borrow_mut()
.mark_texture_consumed(lease.slot, lease.generation, event)
}
TextureBacking::BufferBacked { .. } => Err(Error::failed_precondition(
"consumer completion applies only to render-target-backed textures",
)),
}
}
pub(crate) fn render_target_image(&self) -> Option<RenderTargetImageInfo> {
match &self.backing {
TextureBacking::RenderTarget {
owner,
image,
view,
width,
height,
..
} => Some((*image, *view, *width, *height, owner.clone())),
TextureBacking::BufferBacked { .. } => None,
}
}
pub(crate) fn lease_weak(&self) -> std::rc::Weak<TextureSemantic> {
Rc::downgrade(&self.semantic)
}
pub(crate) fn lease_strong_count(&self) -> usize {
Rc::strong_count(&self.semantic)
}
}
pub fn texture_from_rgba8(
engine: &Engine,
rgba: &[u8],
width: usize,
height: usize,
) -> Result<Texture> {
if width == 0 || height == 0 {
return Err(Error::invalid_argument(
"render::texture_from_rgba8 dimensions must be non-zero",
));
}
let expected = width
.checked_mul(height)
.and_then(|pixels| pixels.checked_mul(4))
.ok_or_else(|| Error::out_of_range("texture byte count overflows usize"))?;
if rgba.len() != expected {
return Err(Error::invalid_argument(
"render::texture_from_rgba8 requires exactly width * height * 4 bytes",
));
}
let data = Matrix::from_slice(engine, [height, width, 4], rgba)?;
texture_from_matrix(data, width, height)
}
pub fn texture_from_image(image: &Image) -> Result<Texture> {
if image.layout() != ImageLayout::Hwc
|| image.format() != ImageFormat::Rgba
|| image.dtype() != DType::U8
{
return Err(Error::invalid_argument(
"render::texture_from_image requires an HWC RGBA U8 image",
));
}
let data = crate::matrix::copy(image.as_matrix())?;
let width = image.as_matrix().shape()[1];
let height = image.as_matrix().shape()[0];
texture_from_matrix(data, width, height)
}
pub fn blit(texture: &Texture) -> Result<Texture> {
let data = match &texture.backing {
TextureBacking::BufferBacked { data, .. } => crate::matrix::copy(data)?,
TextureBacking::RenderTarget { .. } => {
return Err(Error::failed_precondition(
"render::blit is not available for render-target-backed textures",
));
}
};
texture_from_matrix(data, texture.width(), texture.height())
}
pub fn clear(texture: &Texture, color: Color) -> Result<Texture> {
let rgba = color.to_u32().to_be_bytes();
let pixel_count = texture
.width()
.checked_mul(texture.height())
.ok_or_else(|| Error::out_of_range("texture pixel count overflows usize"))?;
let mut pixels = vec![
0_u8;
pixel_count.checked_mul(4).ok_or_else(|| {
Error::out_of_range("texture clear byte count overflows usize")
})?
];
for pixel in pixels.chunks_mut(4) {
pixel.copy_from_slice(&rgba);
}
texture_from_rgba8_from_engine(texture, &pixels)
}
fn texture_from_rgba8_from_engine(texture: &Texture, rgba: &[u8]) -> Result<Texture> {
let engine_handle = texture.engine_handle().clone();
let element_count = rgba.len();
let data = Matrix::allocate(
&engine_handle,
vec![texture.height(), texture.width(), 4],
element_count,
DType::U8,
)?;
data.write_values(rgba)?;
texture_from_matrix(data, texture.width(), texture.height())
}
fn texture_from_matrix(data: Matrix, width: usize, height: usize) -> Result<Texture> {
let w32 =
u32::try_from(width).map_err(|_| Error::out_of_range("texture width exceeds Vulkan limits"))?;
let h32 = u32::try_from(height)
.map_err(|_| Error::out_of_range("texture height exceeds Vulkan limits"))?;
let native = data
.engine_handle()
.upload_native_rgba_image(data.storage(), w32, h32)?;
let semantic = Rc::new(TextureSemantic::new()?);
let _ = data
.engine_handle()
.record_texture_semantic(semantic.id, width, height);
Ok(Texture {
backing: TextureBacking::BufferBacked { data, native },
semantic,
})
}
pub(super) fn texture_from_render_target(
engine_handle: crate::runtime::EngineHandle,
owner: Rc<std::cell::RefCell<super::renderer::RendererState>>,
lease: RenderTargetLease,
image: ash::vk::Image,
view: ash::vk::ImageView,
width: u32,
height: u32,
) -> Texture {
let semantic = TextureSemantic::new().unwrap_or(TextureSemantic { id: 0 });
Texture {
backing: TextureBacking::RenderTarget {
engine_handle,
owner,
lease,
image,
view,
width,
height,
},
semantic: Rc::new(semantic),
}
}
fn next_texture_value_id() -> crate::Result<u64> {
static NEXT_TEXTURE_VALUE_ID: AtomicU64 = AtomicU64::new(1);
let id = NEXT_TEXTURE_VALUE_ID.fetch_add(1, Ordering::Relaxed);
if id == u64::MAX {
return Err(crate::Error::resource_exhausted(
"texture semantic value identity space is exhausted",
));
}
Ok(id)
}
pub fn save_texture_file(texture: &Texture, path: impl AsRef<Path>, quality: u32) -> Result<()> {
let width = u32::try_from(texture.width())
.map_err(|_| Error::out_of_range("texture width exceeds codec limits"))?;
let height = u32::try_from(texture.height())
.map_err(|_| Error::out_of_range("texture height exceeds codec limits"))?;
let rgba = texture.read_rgba8()?;
crate::image::save_rgba_file(&rgba, width, height, path, quality)
}