aboutsummaryrefslogtreecommitdiff
path: root/stockton-render/src/texture
diff options
context:
space:
mode:
authortcmal <me@aria.rip>2024-08-25 17:44:23 +0100
committertcmal <me@aria.rip>2024-08-25 17:44:23 +0100
commit6ab13f2d0cb345795f761181a06777ade61ff09c (patch)
tree42007acef9846d5e79f1bf418a96647f34b530d1 /stockton-render/src/texture
parentccf0074b08ce835cf22e7d46153d1cb3f3d06d32 (diff)
refactor(all): separate rendering from framework
stockton-passes is mostly just a stand-in until this is properly separated
Diffstat (limited to 'stockton-render/src/texture')
-rw-r--r--stockton-render/src/texture/block.rs62
-rw-r--r--stockton-render/src/texture/image.rs41
-rw-r--r--stockton-render/src/texture/load.rs191
-rw-r--r--stockton-render/src/texture/loader.rs712
-rw-r--r--stockton-render/src/texture/mod.rs18
-rw-r--r--stockton-render/src/texture/repo.rs201
-rw-r--r--stockton-render/src/texture/resolver.rs55
-rw-r--r--stockton-render/src/texture/staging_buffer.rs59
8 files changed, 1339 insertions, 0 deletions
diff --git a/stockton-render/src/texture/block.rs b/stockton-render/src/texture/block.rs
new file mode 100644
index 0000000..5ac3a94
--- /dev/null
+++ b/stockton-render/src/texture/block.rs
@@ -0,0 +1,62 @@
+use super::{loader::BlockRef, repo::BLOCK_SIZE};
+use crate::types::*;
+
+use arrayvec::ArrayVec;
+use rendy_memory::{Allocator, Block};
+use std::{iter::once, mem::ManuallyDrop};
+
+pub struct TexturesBlock<B: Block<back::Backend>> {
+ pub id: BlockRef,
+ pub descriptor_set: ManuallyDrop<RDescriptorSet>,
+ pub imgs: ArrayVec<[LoadedImage<B>; BLOCK_SIZE]>,
+}
+
+impl<B: Block<back::Backend>> TexturesBlock<B> {
+ pub fn deactivate<T: Allocator<back::Backend, Block = B>>(
+ mut self,
+ device: &mut DeviceT,
+ tex_alloc: &mut T,
+ desc_alloc: &mut DescriptorAllocator,
+ ) {
+ unsafe {
+ use std::ptr::read;
+
+ // Descriptor set
+ desc_alloc.free(once(read(&*self.descriptor_set)));
+
+ // Images
+ self.imgs
+ .drain(..)
+ .map(|x| x.deactivate(device, tex_alloc))
+ .for_each(|_| {});
+ }
+ }
+}
+
+pub struct LoadedImage<B: Block<back::Backend>> {
+ pub mem: ManuallyDrop<B>,
+ pub img: ManuallyDrop<ImageT>,
+ pub img_view: ManuallyDrop<ImageViewT>,
+ pub sampler: ManuallyDrop<SamplerT>,
+ pub row_size: usize,
+ pub height: u32,
+ pub width: u32,
+}
+
+impl<B: Block<back::Backend>> LoadedImage<B> {
+ pub fn deactivate<T: Allocator<back::Backend, Block = B>>(
+ self,
+ device: &mut DeviceT,
+ alloc: &mut T,
+ ) {
+ unsafe {
+ use std::ptr::read;
+
+ device.destroy_image_view(read(&*self.img_view));
+ device.destroy_image(read(&*self.img));
+ device.destroy_sampler(read(&*self.sampler));
+
+ alloc.free(device, read(&*self.mem));
+ }
+ }
+}
diff --git a/stockton-render/src/texture/image.rs b/stockton-render/src/texture/image.rs
new file mode 100644
index 0000000..0e272e9
--- /dev/null
+++ b/stockton-render/src/texture/image.rs
@@ -0,0 +1,41 @@
+use super::PIXEL_SIZE;
+
+use core::ptr::copy_nonoverlapping;
+use std::convert::TryInto;
+
+use image::RgbaImage;
+
+/// An object that can be loaded as an image into GPU memory
+pub trait LoadableImage {
+ fn width(&self) -> u32;
+ fn height(&self) -> u32;
+ fn copy_row(&self, y: u32, ptr: *mut u8);
+ unsafe fn copy_into(&self, ptr: *mut u8, row_size: usize);
+}
+
+impl LoadableImage for RgbaImage {
+ fn width(&self) -> u32 {
+ self.width()
+ }
+
+ fn height(&self) -> u32 {
+ self.height()
+ }
+
+ fn copy_row(&self, y: u32, ptr: *mut u8) {
+ let row_size_bytes = self.width() as usize * PIXEL_SIZE;
+ let raw: &Vec<u8> = self.as_raw();
+ let row = &raw[y as usize * row_size_bytes..(y as usize + 1) * row_size_bytes];
+
+ unsafe {
+ copy_nonoverlapping(row.as_ptr(), ptr, row.len());
+ }
+ }
+
+ unsafe fn copy_into(&self, ptr: *mut u8, row_size: usize) {
+ for y in 0..self.height() as usize {
+ let dest_base: isize = (y * row_size).try_into().unwrap();
+ self.copy_row(y as u32, ptr.offset(dest_base));
+ }
+ }
+}
diff --git a/stockton-render/src/texture/load.rs b/stockton-render/src/texture/load.rs
new file mode 100644
index 0000000..1f33ad5
--- /dev/null
+++ b/stockton-render/src/texture/load.rs
@@ -0,0 +1,191 @@
+use super::{
+ block::LoadedImage, block::TexturesBlock, repo::BLOCK_SIZE, resolver::TextureResolver,
+ staging_buffer::StagingBuffer, LoadableImage, PIXEL_SIZE,
+};
+use crate::types::*;
+
+use anyhow::{Context, Result};
+use arrayvec::ArrayVec;
+use hal::{
+ format::{Aspects, Format, Swizzle},
+ image::{
+ Filter, SamplerDesc, SubresourceLayers, SubresourceRange, Usage as ImgUsage, ViewKind,
+ WrapMode,
+ },
+ memory::SparseFlags,
+ MemoryTypeId,
+};
+use rendy_memory::{Allocator, Block};
+use std::mem::ManuallyDrop;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum TextureLoadError {
+ #[error("No available resources")]
+ NoResources,
+}
+
+pub const FORMAT: Format = Format::Rgba8Srgb;
+pub const RESOURCES: SubresourceRange = SubresourceRange {
+ aspects: Aspects::COLOR,
+ level_start: 0,
+ level_count: Some(1),
+ layer_start: 0,
+ layer_count: Some(1),
+};
+pub const LAYERS: SubresourceLayers = SubresourceLayers {
+ aspects: Aspects::COLOR,
+ level: 0,
+ layers: 0..1,
+};
+
+pub struct TextureLoadConfig<R: TextureResolver> {
+ pub resolver: R,
+ pub filter: Filter,
+ pub wrap_mode: WrapMode,
+}
+
+pub struct QueuedLoad<B: Block<back::Backend>> {
+ pub fence: FenceT,
+ pub buf: CommandBufferT,
+ pub block: TexturesBlock<B>,
+ pub staging_bufs: ArrayVec<[StagingBuffer; BLOCK_SIZE]>,
+}
+
+impl<B: Block<back::Backend>> QueuedLoad<B> {
+ pub fn dissolve(
+ self,
+ ) -> (
+ (FenceT, CommandBufferT),
+ ArrayVec<[StagingBuffer; BLOCK_SIZE]>,
+ TexturesBlock<B>,
+ ) {
+ ((self.fence, self.buf), self.staging_bufs, self.block)
+ }
+}
+
+pub fn tex_size_info<T: LoadableImage>(img: &T, obcpa: hal::buffer::Offset) -> (usize, usize) {
+ let initial_row_size = PIXEL_SIZE * img.width() as usize;
+ let row_alignment_mask = obcpa as u32 - 1;
+
+ let row_size = ((initial_row_size as u32 + row_alignment_mask) & !row_alignment_mask) as usize;
+ let total_size = (row_size * (img.height() as usize)) as u64;
+ debug_assert!(row_size as usize >= initial_row_size);
+
+ (row_size, total_size as usize)
+}
+
+pub fn create_image_view<T, I>(
+ device: &mut DeviceT,
+ allocator: &mut T,
+ format: Format,
+ usage: ImgUsage,
+ img: &I,
+) -> Result<(T::Block, ImageT)>
+where
+ T: Allocator<back::Backend>,
+ I: LoadableImage,
+{
+ // Make the image
+ let mut image_ref = unsafe {
+ use hal::image::{Kind, Tiling, ViewCapabilities};
+
+ device.create_image(
+ Kind::D2(img.width(), img.height(), 1, 1),
+ 1,
+ format,
+ Tiling::Optimal,
+ usage,
+ SparseFlags::empty(),
+ ViewCapabilities::empty(),
+ )
+ }
+ .context("Error creating image")?;
+
+ // Allocate memory
+ let (block, _) = unsafe {
+ let requirements = device.get_image_requirements(&image_ref);
+
+ allocator.alloc(device, requirements.size, requirements.alignment)
+ }
+ .context("Error allocating memory")?;
+
+ unsafe {
+ device
+ .bind_image_memory(block.memory(), block.range().start, &mut image_ref)
+ .context("Error binding memory to image")?;
+ }
+
+ Ok((block, image_ref))
+}
+
+pub unsafe fn load_image<I: LoadableImage, R: TextureResolver>(
+ device: &mut DeviceT,
+ staging_allocator: &mut DynamicAllocator,
+ tex_allocator: &mut DynamicAllocator,
+ staging_memory_type: MemoryTypeId,
+ obcpa: u64,
+ img_data: I,
+ config: &TextureLoadConfig<R>,
+) -> Result<(StagingBuffer, LoadedImage<DynamicBlock>)> {
+ // Calculate buffer size
+ let (row_size, total_size) = tex_size_info(&img_data, obcpa);
+
+ // Create staging buffer
+ let mut staging_buffer = StagingBuffer::new(
+ device,
+ staging_allocator,
+ total_size as u64,
+ staging_memory_type,
+ )
+ .context("Error creating staging buffer")?;
+
+ // Write to staging buffer
+ let mapped_memory = staging_buffer
+ .map_memory(device)
+ .context("Error mapping staged memory")?;
+
+ img_data.copy_into(mapped_memory, row_size);
+
+ staging_buffer.unmap_memory(device);
+
+ // Create image
+ let (img_mem, img) = create_image_view(
+ device,
+ tex_allocator,
+ FORMAT,
+ ImgUsage::SAMPLED | ImgUsage::TRANSFER_DST,
+ &img_data,
+ )
+ .context("Error creating image")?;
+
+ // Create image view
+ let img_view = device
+ .create_image_view(
+ &img,
+ ViewKind::D2,
+ FORMAT,
+ Swizzle::NO,
+ ImgUsage::SAMPLED | ImgUsage::TRANSFER_DST,
+ RESOURCES,
+ )
+ .context("Error creating image view")?;
+
+ // Create sampler
+ let sampler = device
+ .create_sampler(&SamplerDesc::new(config.filter, config.wrap_mode))
+ .context("Error creating sampler")?;
+
+ Ok((
+ staging_buffer,
+ LoadedImage {
+ mem: ManuallyDrop::new(img_mem),
+ img: ManuallyDrop::new(img),
+ img_view: ManuallyDrop::new(img_view),
+ sampler: ManuallyDrop::new(sampler),
+ row_size,
+ height: img_data.height(),
+ width: img_data.width(),
+ },
+ ))
+}
diff --git a/stockton-render/src/texture/loader.rs b/stockton-render/src/texture/loader.rs
new file mode 100644
index 0000000..f9c643c
--- /dev/null
+++ b/stockton-render/src/texture/loader.rs
@@ -0,0 +1,712 @@
+//! Manages the loading/unloading of textures
+
+use super::{
+ block::{LoadedImage, TexturesBlock},
+ load::{load_image, QueuedLoad, TextureLoadConfig, TextureLoadError, LAYERS, RESOURCES},
+ repo::BLOCK_SIZE,
+ resolver::TextureResolver,
+ PIXEL_SIZE,
+};
+use crate::{error::LockPoisoned, types::*, utils::find_memory_type_id};
+
+use std::{
+ array::IntoIter,
+ collections::VecDeque,
+ iter::{empty, once},
+ mem::{drop, ManuallyDrop},
+ sync::{
+ mpsc::{Receiver, Sender},
+ Arc, RwLock,
+ },
+ thread::sleep,
+ time::Duration,
+};
+
+use anyhow::{Context, Result};
+use arrayvec::ArrayVec;
+use hal::{
+ command::{BufferImageCopy, CommandBufferFlags},
+ format::{Aspects, Format},
+ image::{Access, Extent, Layout, Offset, SubresourceLayers, SubresourceRange},
+ memory::{Barrier, Dependencies, Properties as MemProps, SparseFlags},
+ pso::{Descriptor, DescriptorSetWrite, ImageDescriptorType, PipelineStage, ShaderStageFlags},
+ queue::family::QueueFamilyId,
+ MemoryTypeId,
+};
+use image::{Rgba, RgbaImage};
+use log::*;
+use rendy_descriptor::{DescriptorRanges, DescriptorSetLayoutBinding, DescriptorType};
+use rendy_memory::DynamicConfig;
+use thiserror::Error;
+
+/// The number of command buffers to have in flight simultaneously.
+pub const NUM_SIMULTANEOUS_CMDS: usize = 2;
+
+/// A reference to a texture of the current map
+pub type BlockRef = usize;
+
+/// Manages the loading/unloading of textures
+/// This is expected to load the textures, then send the loaded blocks back
+pub struct TextureLoader<R: TextureResolver> {
+ /// Blocks for which commands have been queued and are done loading once the fence is triggered.
+ commands_queued: ArrayVec<[QueuedLoad<DynamicBlock>; NUM_SIMULTANEOUS_CMDS]>,
+
+ /// The command buffers used and a fence to go with them
+ buffers: VecDeque<(FenceT, CommandBufferT)>,
+
+ /// The command pool buffers were allocated from
+ pool: ManuallyDrop<CommandPoolT>,
+
+ /// The GPU we're submitting to
+ device: Arc<RwLock<DeviceT>>,
+
+ /// The command queue being used
+ queue: Arc<RwLock<QueueT>>,
+
+ /// The memory allocator being used for textures
+ tex_allocator: ManuallyDrop<DynamicAllocator>,
+
+ /// The memory allocator for staging memory
+ staging_allocator: ManuallyDrop<DynamicAllocator>,
+
+ /// Allocator for descriptor sets
+ descriptor_allocator: ManuallyDrop<DescriptorAllocator>,
+
+ ds_layout: Arc<RwLock<DescriptorSetLayoutT>>,
+
+ /// Type ID for staging memory
+ staging_memory_type: MemoryTypeId,
+
+ /// From adapter, used for determining alignment
+ optimal_buffer_copy_pitch_alignment: hal::buffer::Offset,
+
+ /// Configuration for how to find and load textures
+ config: TextureLoadConfig<R>,
+
+ /// The channel requests come in.
+ /// Requests should reference a texture **block**, for example textures 8..16 is block 1.
+ request_channel: Receiver<LoaderRequest>,
+
+ /// The channel blocks are returned to.
+ return_channel: Sender<TexturesBlock<DynamicBlock>>,
+
+ /// A filler image for descriptors that aren't needed but still need to be written to
+ blank_image: ManuallyDrop<LoadedImage<DynamicBlock>>,
+}
+
+#[derive(Error, Debug)]
+pub enum TextureLoaderError {
+ #[error("Couldn't find a suitable memory type")]
+ NoMemoryTypes,
+}
+
+impl<R: TextureResolver> TextureLoader<R> {
+ pub fn loop_until_exit(mut self) -> Result<TextureLoaderRemains> {
+ debug!("TextureLoader starting main loop");
+ let mut res = Ok(false);
+ while res.is_ok() {
+ res = self.main();
+ if let Ok(true) = res {
+ break;
+ }
+
+ sleep(Duration::from_secs(0));
+ }
+
+ match res {
+ Ok(true) => {
+ debug!("Starting to deactivate TextureLoader");
+
+ Ok(self.deactivate())
+ }
+ Err(r) => Err(r.context("Error in TextureLoader loop")),
+ _ => unreachable!(),
+ }
+ }
+ fn main(&mut self) -> Result<bool> {
+ let mut device = self
+ .device
+ .write()
+ .map_err(|_| LockPoisoned::Device)
+ .context("Error getting device lock")?;
+ // Check for blocks that are finished, then send them back
+ let mut i = 0;
+ while i < self.commands_queued.len() {
+ let signalled = unsafe { device.get_fence_status(&self.commands_queued[i].fence) }
+ .context("Error checking fence status")?;
+
+ if signalled {
+ let (assets, mut staging_bufs, block) = self.commands_queued.remove(i).dissolve();
+ debug!("Load finished for texture block {:?}", block.id);
+
+ // Destroy staging buffers
+ for buf in staging_bufs.drain(..) {
+ buf.deactivate(&mut device, &mut self.staging_allocator);
+ }
+
+ self.buffers.push_back(assets);
+ self.return_channel
+ .send(block)
+ .context("Error returning texture block")?;
+ } else {
+ i += 1;
+ }
+ }
+
+ drop(device);
+
+ // Check for messages to start loading blocks
+ let req_iter: Vec<_> = self.request_channel.try_iter().collect();
+ for to_load in req_iter {
+ match to_load {
+ LoaderRequest::Load(to_load) => {
+ // Attempt to load given block
+ debug!("Attempting to queue load for texture block {:?}", to_load);
+
+ let result = unsafe { self.attempt_queue_load(to_load) };
+ match result {
+ Ok(queued_load) => self.commands_queued.push(queued_load),
+ Err(x) => match x.downcast_ref::<TextureLoadError>() {
+ Some(TextureLoadError::NoResources) => {
+ debug!("No resources, trying again later");
+ }
+ _ => return Err(x).context("Error queuing texture load"),
+ },
+ }
+ }
+ LoaderRequest::End => return Ok(true),
+ }
+ }
+
+ Ok(false)
+ }
+
+ pub fn new(
+ adapter: &Adapter,
+ device_lock: Arc<RwLock<DeviceT>>,
+ family: QueueFamilyId,
+ queue_lock: Arc<RwLock<QueueT>>,
+ ds_layout: Arc<RwLock<DescriptorSetLayoutT>>,
+ request_channel: Receiver<LoaderRequest>,
+ return_channel: Sender<TexturesBlock<DynamicBlock>>,
+ config: TextureLoadConfig<R>,
+ ) -> Result<Self> {
+ let mut device = device_lock
+ .write()
+ .map_err(|_| LockPoisoned::Device)
+ .context("Error getting device lock")?;
+ let device_props = adapter.physical_device.properties();
+
+ let type_mask = unsafe {
+ use hal::image::{Kind, Tiling, Usage, ViewCapabilities};
+
+ // We create an empty image with the same format as used for textures
+ // this is to get the type_mask required, which will stay the same for
+ // all colour images of the same tiling. (certain memory flags excluded).
+
+ // Size and alignment don't necessarily stay the same, so we're forced to
+ // guess at the alignment for our allocator.
+
+ // TODO: Way to tune these options
+ let img = device
+ .create_image(
+ Kind::D2(16, 16, 1, 1),
+ 1,
+ Format::Rgba8Srgb,
+ Tiling::Optimal,
+ Usage::SAMPLED,
+ SparseFlags::empty(),
+ ViewCapabilities::empty(),
+ )
+ .context("Error creating test image to get buffer settings")?;
+
+ let type_mask = device.get_image_requirements(&img).type_mask;
+
+ device.destroy_image(img);
+
+ type_mask
+ };
+
+ debug!("Using type mask {:?}", type_mask);
+
+ // Tex Allocator
+ let mut tex_allocator = {
+ let props = MemProps::DEVICE_LOCAL;
+
+ DynamicAllocator::new(
+ find_memory_type_id(adapter, type_mask, props)
+ .ok_or(TextureLoaderError::NoMemoryTypes)
+ .context("Couldn't create tex memory allocator")?,
+ props,
+ DynamicConfig {
+ block_size_granularity: 4 * 32 * 32, // 32x32 image
+ max_chunk_size: u64::pow(2, 63),
+ min_device_allocation: 4 * 32 * 32,
+ },
+ device_props.limits.non_coherent_atom_size as u64,
+ )
+ };
+
+ let (staging_memory_type, mut staging_allocator) = {
+ let props = MemProps::CPU_VISIBLE | MemProps::COHERENT;
+ let t = find_memory_type_id(adapter, u32::MAX, props)
+ .ok_or(TextureLoaderError::NoMemoryTypes)
+ .context("Couldn't create staging memory allocator")?;
+ (
+ t,
+ DynamicAllocator::new(
+ t,
+ props,
+ DynamicConfig {
+ block_size_granularity: 4 * 32 * 32, // 32x32 image
+ max_chunk_size: u64::pow(2, 63),
+ min_device_allocation: 4 * 32 * 32,
+ },
+ device_props.limits.non_coherent_atom_size as u64,
+ ),
+ )
+ };
+
+ // Pool
+ let mut pool = unsafe {
+ use hal::pool::CommandPoolCreateFlags;
+
+ device.create_command_pool(family, CommandPoolCreateFlags::RESET_INDIVIDUAL)
+ }
+ .context("Error creating command pool")?;
+
+ // Command buffers and fences
+ debug!("Creating resources...");
+ let mut buffers = {
+ let mut data = VecDeque::with_capacity(NUM_SIMULTANEOUS_CMDS);
+
+ for _ in 0..NUM_SIMULTANEOUS_CMDS {
+ unsafe {
+ data.push_back((
+ device.create_fence(false).context("Error creating fence")?,
+ pool.allocate_one(hal::command::Level::Primary),
+ ));
+ };
+ }
+
+ data
+ };
+
+ let optimal_buffer_copy_pitch_alignment =
+ device_props.limits.optimal_buffer_copy_pitch_alignment;
+
+ let blank_image = unsafe {
+ Self::get_blank_image(
+ &mut device,
+ &mut buffers[0].1,
+ &queue_lock,
+ &mut staging_allocator,
+ &mut tex_allocator,
+ staging_memory_type,
+ optimal_buffer_copy_pitch_alignment,
+ &config,
+ )
+ }
+ .context("Error creating blank image")?;
+
+ drop(device);
+
+ Ok(TextureLoader {
+ commands_queued: ArrayVec::new(),
+ buffers,
+ pool: ManuallyDrop::new(pool),
+ device: device_lock,
+ queue: queue_lock,
+ ds_layout,
+
+ tex_allocator: ManuallyDrop::new(tex_allocator),
+ staging_allocator: ManuallyDrop::new(staging_allocator),
+ descriptor_allocator: ManuallyDrop::new(DescriptorAllocator::new()),
+
+ staging_memory_type,
+ optimal_buffer_copy_pitch_alignment,
+
+ request_channel,
+ return_channel,
+ config,
+ blank_image: ManuallyDrop::new(blank_image),
+ })
+ }
+
+ unsafe fn attempt_queue_load(&mut self, block_ref: usize) -> Result<QueuedLoad<DynamicBlock>> {
+ let mut device = self
+ .device
+ .write()
+ .map_err(|_| LockPoisoned::Device)
+ .context("Error getting device lock")?;
+
+ // Get assets to use
+ let (mut fence, mut buf) = self
+ .buffers
+ .pop_front()
+ .ok_or(TextureLoadError::NoResources)
+ .context("Error getting resources to use")?;
+
+ // Create descriptor set
+ let mut descriptor_set = {
+ let mut v: ArrayVec<[RDescriptorSet; 1]> = ArrayVec::new();
+ self.descriptor_allocator
+ .allocate(
+ &device,
+ &*self
+ .ds_layout
+ .read()
+ .map_err(|_| LockPoisoned::Other)
+ .context("Error reading descriptor set layout")?,
+ DescriptorRanges::from_bindings(&[
+ DescriptorSetLayoutBinding {
+ binding: 0,
+ ty: DescriptorType::Image {
+ ty: ImageDescriptorType::Sampled {
+ with_sampler: false,
+ },
+ },
+ count: BLOCK_SIZE,
+ stage_flags: ShaderStageFlags::FRAGMENT,
+ immutable_samplers: false,
+ },
+ DescriptorSetLayoutBinding {
+ binding: 1,
+ ty: DescriptorType::Sampler,
+ count: BLOCK_SIZE,
+ stage_flags: ShaderStageFlags::FRAGMENT,
+ immutable_samplers: false,
+ },
+ ]),
+ 1,
+ &mut v,
+ )
+ .context("Error creating descriptor set")?;
+
+ v.pop().unwrap()
+ };
+
+ // Get a command buffer
+ buf.begin_primary(CommandBufferFlags::ONE_TIME_SUBMIT);
+
+ let mut imgs: ArrayVec<[_; BLOCK_SIZE]> = ArrayVec::new();
+ let mut staging_bufs: ArrayVec<[_; BLOCK_SIZE]> = ArrayVec::new();
+
+ // For each texture in block
+ for tex_idx in (block_ref * BLOCK_SIZE)..(block_ref + 1) * BLOCK_SIZE {
+ // Resolve texture
+ let img_data = self.config.resolver.resolve(tex_idx as u32);
+ if img_data.is_none() {
+ // Write a blank descriptor
+ device.write_descriptor_set(DescriptorSetWrite {
+ set: descriptor_set.raw_mut(),
+ binding: 0,
+ array_offset: tex_idx % BLOCK_SIZE,
+ descriptors: once(Descriptor::Image(
+ &*self.blank_image.img_view,
+ Layout::ShaderReadOnlyOptimal,
+ )),
+ });
+ device.write_descriptor_set(DescriptorSetWrite {
+ set: descriptor_set.raw_mut(),
+ binding: 1,
+ array_offset: tex_idx % BLOCK_SIZE,
+ descriptors: once(Descriptor::Sampler(&*self.blank_image.sampler)),
+ });
+
+ continue;
+ }
+
+ let img_data = img_data.unwrap();
+
+ let array_offset = tex_idx % BLOCK_SIZE;
+
+ let (staging_buffer, img) = load_image(
+ &mut device,
+ &mut self.staging_allocator,
+ &mut self.tex_allocator,
+ self.staging_memory_type,
+ self.optimal_buffer_copy_pitch_alignment,
+ img_data,
+ &self.config,
+ )?;
+
+ // Write to descriptor set
+ {
+ device.write_descriptor_set(DescriptorSetWrite {
+ set: descriptor_set.raw_mut(),
+ binding: 0,
+ array_offset,
+ descriptors: once(Descriptor::Image(
+ &*img.img_view,
+ Layout::ShaderReadOnlyOptimal,
+ )),
+ });
+ device.write_descriptor_set(DescriptorSetWrite {
+ set: descriptor_set.raw_mut(),
+ binding: 1,
+ array_offset,
+ descriptors: once(Descriptor::Sampler(&*img.sampler)),
+ });
+ }
+
+ imgs.push(img);
+
+ staging_bufs.push(staging_buffer);
+ }
+
+ // Add start pipeline barrier
+ buf.pipeline_barrier(
+ PipelineStage::TOP_OF_PIPE..PipelineStage::TRANSFER,
+ Dependencies::empty(),
+ imgs.iter().map(|li| Barrier::Image {
+ states: (Access::empty(), Layout::Undefined)
+ ..(Access::TRANSFER_WRITE, Layout::TransferDstOptimal),
+ target: &*li.img,
+ families: None,
+ range: SubresourceRange {
+ aspects: Aspects::COLOR,
+ level_start: 0,
+ level_count: None,
+ layer_start: 0,
+ layer_count: None,
+ },
+ }),
+ );
+
+ // Record copy commands
+ for (li, sb) in imgs.iter().zip(staging_bufs.iter()) {
+ buf.copy_buffer_to_image(
+ &*sb.buf,
+ &*li.img,
+ Layout::TransferDstOptimal,
+ once(BufferImageCopy {
+ buffer_offset: 0,
+ buffer_width: (li.row_size / super::PIXEL_SIZE) as u32,
+ buffer_height: li.height,
+ image_layers: SubresourceLayers {
+ aspects: Aspects::COLOR,
+ level: 0,
+ layers: 0..1,
+ },
+ image_offset: Offset { x: 0, y: 0, z: 0 },
+ image_extent: gfx_hal::image::Extent {
+ width: li.width,
+ height: li.height,
+ depth: 1,
+ },
+ }),
+ );
+ }
+ buf.pipeline_barrier(
+ PipelineStage::TRANSFER..PipelineStage::BOTTOM_OF_PIPE,
+ Dependencies::empty(),
+ imgs.iter().map(|li| Barrier::Image {
+ states: (Access::TRANSFER_WRITE, Layout::TransferDstOptimal)
+ ..(Access::empty(), Layout::ShaderReadOnlyOptimal),
+ target: &*li.img,
+ families: None,
+ range: RESOURCES,
+ }),
+ );
+
+ buf.finish();
+
+ // Submit command buffer
+ {
+ let mut queue = self.queue.write().map_err(|_| LockPoisoned::Queue)?;
+
+ queue.submit(IntoIter::new([&buf]), empty(), empty(), Some(&mut fence));
+ }
+
+ Ok(QueuedLoad {
+ staging_bufs,
+ fence,
+ buf,
+ block: TexturesBlock {
+ id: block_ref,
+ imgs,
+ descriptor_set: ManuallyDrop::new(descriptor_set),
+ },
+ })
+ }
+
+ unsafe fn get_blank_image(
+ device: &mut DeviceT,
+ buf: &mut CommandBufferT,
+ queue_lock: &Arc<RwLock<QueueT>>,
+ staging_allocator: &mut DynamicAllocator,
+ tex_allocator: &mut DynamicAllocator,
+ staging_memory_type: MemoryTypeId,
+ obcpa: u64,
+ config: &TextureLoadConfig<R>,
+ ) -> Result<LoadedImage<DynamicBlock>> {
+ let img_data = RgbaImage::from_pixel(1, 1, Rgba([255, 0, 255, 255]));
+
+ let height = img_data.height();
+ let width = img_data.width();
+ let row_alignment_mask = obcpa as u32 - 1;
+ let initial_row_size = PIXEL_SIZE * img_data.width() as usize;
+ let row_size =
+ ((initial_row_size as u32 + row_alignment_mask) & !row_alignment_mask) as usize;
+
+ let (staging_buffer, img) = load_image(
+ device,
+ staging_allocator,
+ tex_allocator,
+ staging_memory_type,
+ obcpa,
+ img_data,
+ config,
+ )?;
+
+ buf.begin_primary(CommandBufferFlags::ONE_TIME_SUBMIT);
+
+ buf.pipeline_barrier(
+ PipelineStage::TOP_OF_PIPE..PipelineStage::TRANSFER,
+ Dependencies::empty(),
+ once(Barrier::Image {
+ states: (Access::empty(), Layout::Undefined)
+ ..(Access::TRANSFER_WRITE, Layout::TransferDstOptimal),
+ target: &*img.img,
+ families: None,
+ range: SubresourceRange {
+ aspects: Aspects::COLOR,
+ level_start: 0,
+ level_count: None,
+ layer_start: 0,
+ layer_count: None,
+ },
+ }),
+ );
+ buf.copy_buffer_to_image(
+ &*staging_buffer.buf,
+ &*img.img,
+ Layout::TransferDstOptimal,
+ once(BufferImageCopy {
+ buffer_offset: 0,
+ buffer_width: (row_size / super::PIXEL_SIZE) as u32,
+ buffer_height: height,
+ image_layers: LAYERS,
+ image_offset: Offset { x: 0, y: 0, z: 0 },
+ image_extent: Extent {
+ width,
+ height,
+ depth: 1,
+ },
+ }),
+ );
+
+ buf.pipeline_barrier(
+ PipelineStage::TRANSFER..PipelineStage::BOTTOM_OF_PIPE,
+ Dependencies::empty(),
+ once(Barrier::Image {
+ states: (Access::TRANSFER_WRITE, Layout::TransferDstOptimal)
+ ..(Access::empty(), Layout::ShaderReadOnlyOptimal),
+ target: &*img.img,
+ families: None,
+ range: RESOURCES,
+ }),
+ );
+ buf.finish();
+
+ let mut fence = device.create_fence(false).context("Error creating fence")?;
+
+ {
+ let mut queue = queue_lock.write().map_err(|_| LockPoisoned::Queue)?;
+
+ queue.submit(
+ IntoIter::new([buf as &CommandBufferT]),
+ empty(),
+ empty(),
+ Some(&mut fence),
+ );
+ }
+
+ device
+ .wait_for_fence(&fence, std::u64::MAX)
+ .context("Error waiting for copy")?;
+
+ device.destroy_fence(fence);
+
+ staging_buffer.deactivate(device, staging_allocator);
+
+ Ok(img)
+ }
+
+ /// Safely destroy all the vulkan stuff in this instance
+ /// Note that this returns the memory allocators, from which should be freed any TextureBlocks
+ /// All in-progress things are sent to return_channel.
+ fn deactivate(mut self) -> TextureLoaderRemains {
+ use std::ptr::read;
+
+ let mut device = self.device.write().unwrap();
+
+ unsafe {
+ // Wait for any currently queued loads to be done
+ while self.commands_queued.len() > 0 {
+ let mut i = 0;
+ while i < self.commands_queued.len() {
+ let signalled = device
+ .get_fence_status(&self.commands_queued[i].fence)
+ .expect("Device lost by TextureManager");
+
+ if signalled {
+ // Destroy finished ones
+ let (assets, mut staging_bufs, block) =
+ self.commands_queued.remove(i).dissolve();
+
+ device.destroy_fence(assets.0);
+ // Command buffer will be freed when we reset the command pool
+ // Destroy staging buffers
+ for buf in staging_bufs.drain(..) {
+ buf.deactivate(&mut device, &mut self.staging_allocator);
+ }
+
+ self.return_channel
+ .send(block)
+ .expect("Sending through return channel failed");
+ } else {
+ i += 1;
+ }
+ }
+
+ sleep(Duration::from_secs(0));
+ }
+
+ // Destroy blank image
+ read(&*self.blank_image).deactivate(&mut device, &mut *self.tex_allocator);
+
+ // Destroy fences
+
+ self.buffers
+ .drain(..)
+ .map(|(f, _)| device.destroy_fence(f))
+ .for_each(|_| {});
+
+ // Free command pool
+ self.pool.reset(true);
+ device.destroy_command_pool(read(&*self.pool));
+
+ debug!("Done deactivating TextureLoader");
+
+ TextureLoaderRemains {
+ tex_allocator: ManuallyDrop::new(read(&*self.tex_allocator)),
+ descriptor_allocator: ManuallyDrop::new(read(&*self.descriptor_allocator)),
+ }
+ }
+ }
+}
+
+pub struct TextureLoaderRemains {
+ pub tex_allocator: ManuallyDrop<DynamicAllocator>,
+ pub descriptor_allocator: ManuallyDrop<DescriptorAllocator>,
+}
+
+pub enum LoaderRequest {
+ /// Load the given block
+ Load(BlockRef),
+
+ /// Stop looping and deactivate
+ End,
+}
diff --git a/stockton-render/src/texture/mod.rs b/stockton-render/src/texture/mod.rs
new file mode 100644
index 0000000..aef1b03
--- /dev/null
+++ b/stockton-render/src/texture/mod.rs
@@ -0,0 +1,18 @@
+//! Everything related to loading textures into GPU memory
+
+mod block;
+mod image;
+mod load;
+mod loader;
+mod repo;
+pub mod resolver;
+mod staging_buffer;
+
+pub use self::block::TexturesBlock;
+pub use self::image::LoadableImage;
+pub use self::load::TextureLoadConfig;
+pub use self::loader::BlockRef;
+pub use self::repo::{TexLoadQueue, TextureRepo};
+
+/// The size of each pixel in an image
+pub const PIXEL_SIZE: usize = std::mem::size_of::<u8>() * 4;
diff --git a/stockton-render/src/texture/repo.rs b/stockton-render/src/texture/repo.rs
new file mode 100644
index 0000000..e29625b
--- /dev/null
+++ b/stockton-render/src/texture/repo.rs
@@ -0,0 +1,201 @@
+use super::{
+ block::TexturesBlock,
+ load::TextureLoadConfig,
+ loader::{BlockRef, LoaderRequest, TextureLoader, TextureLoaderRemains, NUM_SIMULTANEOUS_CMDS},
+ resolver::TextureResolver,
+};
+use crate::error::LockPoisoned;
+use crate::queue_negotiator::QueueFamilySelector;
+use crate::types::*;
+
+use std::{
+ array::IntoIter,
+ collections::HashMap,
+ iter::empty,
+ mem::ManuallyDrop,
+ sync::{
+ mpsc::{channel, Receiver, Sender},
+ Arc, RwLock, RwLockReadGuard,
+ },
+ thread::JoinHandle,
+};
+
+use anyhow::{Context, Result};
+use hal::{
+ pso::{DescriptorSetLayoutBinding, DescriptorType, ImageDescriptorType, ShaderStageFlags},
+ queue::family::QueueFamilyId,
+};
+use log::debug;
+
+/// The number of textures in one 'block'
+/// The textures of the loaded file are divided into blocks of this size.
+/// Whenever a texture is needed, the whole block its in is loaded.
+pub const BLOCK_SIZE: usize = 8;
+
+pub struct TextureRepo {
+ joiner: ManuallyDrop<JoinHandle<Result<TextureLoaderRemains>>>,
+ ds_layout: Arc<RwLock<DescriptorSetLayoutT>>,
+ req_send: Sender<LoaderRequest>,
+ resp_recv: Receiver<TexturesBlock<DynamicBlock>>,
+ blocks: HashMap<BlockRef, Option<TexturesBlock<DynamicBlock>>>,
+}
+
+impl TextureRepo {
+ pub fn new<R: 'static + TextureResolver + Send + Sync>(
+ device_lock: Arc<RwLock<DeviceT>>,
+ family: QueueFamilyId,
+ queue: Arc<RwLock<QueueT>>,
+ adapter: &Adapter,
+ config: TextureLoadConfig<R>,
+ ) -> Result<Self> {
+ // Create Channels
+ let (req_send, req_recv) = channel();
+ let (resp_send, resp_recv) = channel();
+ let device = device_lock
+ .write()
+ .map_err(|_| LockPoisoned::Device)
+ .context("Error getting device lock")?;
+
+ // Create descriptor set layout
+ let ds_lock = Arc::new(RwLock::new(
+ unsafe {
+ device.create_descriptor_set_layout(
+ IntoIter::new([
+ DescriptorSetLayoutBinding {
+ binding: 0,
+ ty: DescriptorType::Image {
+ ty: ImageDescriptorType::Sampled {
+ with_sampler: false,
+ },
+ },
+ count: BLOCK_SIZE,
+ stage_flags: ShaderStageFlags::FRAGMENT,
+ immutable_samplers: false,
+ },
+ DescriptorSetLayoutBinding {
+ binding: 1,
+ ty: DescriptorType::Sampler,
+ count: BLOCK_SIZE,
+ stage_flags: ShaderStageFlags::FRAGMENT,
+ immutable_samplers: false,
+ },
+ ]),
+ empty(),
+ )
+ }
+ .context("Error creating descriptor set layout")?,
+ ));
+
+ debug!("Created descriptor set layout {:?}", ds_lock);
+
+ drop(device);
+
+ let joiner = {
+ let loader = TextureLoader::new(
+ adapter,
+ device_lock.clone(),
+ family,
+ queue,
+ ds_lock.clone(),
+ req_recv,
+ resp_send,
+ config,
+ )?;
+
+ std::thread::spawn(move || loader.loop_until_exit())
+ };
+
+ Ok(TextureRepo {
+ joiner: ManuallyDrop::new(joiner),
+ ds_layout: ds_lock,
+ blocks: HashMap::new(),
+ req_send,
+ resp_recv,
+ })
+ }
+
+ pub fn get_ds_layout(&self) -> Result<RwLockReadGuard<DescriptorSetLayoutT>> {
+ self.ds_layout
+ .read()
+ .map_err(|_| LockPoisoned::Other)
+ .context("Error locking descriptor set layout")
+ }
+
+ pub fn queue_load(&mut self, block_id: BlockRef) -> Result<()> {
+ if self.blocks.contains_key(&block_id) {
+ return Ok(());
+ }
+
+ self.force_queue_load(block_id)
+ }
+
+ pub fn force_queue_load(&mut self, block_id: BlockRef) -> Result<()> {
+ self.req_send
+ .send(LoaderRequest::Load(block_id))
+ .context("Error queuing texture block load")?;
+
+ self.blocks.insert(block_id, None);
+
+ Ok(())
+ }
+
+ pub fn attempt_get_descriptor_set(&mut self, block_id: BlockRef) -> Option<&DescriptorSetT> {
+ self.blocks
+ .get(&block_id)
+ .and_then(|opt| opt.as_ref().map(|z| z.descriptor_set.raw()))
+ }
+
+ pub fn process_responses(&mut self) {
+ let resp_iter: Vec<_> = self.resp_recv.try_iter().collect();
+ for resp in resp_iter {
+ debug!("Got block {:?} back from loader", resp.id);
+ self.blocks.insert(resp.id, Some(resp));
+ }
+ }
+
+ pub fn deactivate(mut self, device_lock: &Arc<RwLock<DeviceT>>) {
+ unsafe {
+ use std::ptr::read;
+
+ // Join the loader thread
+ self.req_send.send(LoaderRequest::End).unwrap();
+ let mut remains = read(&*self.joiner).join().unwrap().unwrap();
+
+ // Process any ones that just got done loading
+ self.process_responses();
+
+ // Only now can we lock device without deadlocking
+ let mut device = device_lock.write().unwrap();
+
+ // Return all the texture memory and descriptors.
+ for (_, v) in self.blocks.drain() {
+ if let Some(block) = v {
+ block.deactivate(
+ &mut device,
+ &mut *remains.tex_allocator,
+ &mut remains.descriptor_allocator,
+ );
+ }
+ }
+
+ // Dispose of both allocators
+ read(&*remains.tex_allocator).dispose();
+ read(&*remains.descriptor_allocator).dispose(&device);
+
+ // Deactivate DS Layout
+ let ds_layout = Arc::try_unwrap(self.ds_layout)
+ .unwrap()
+ .into_inner()
+ .unwrap();
+ device.destroy_descriptor_set_layout(ds_layout);
+ }
+ }
+}
+
+pub struct TexLoadQueue;
+
+impl QueueFamilySelector for TexLoadQueue {
+ fn is_suitable(&self, family: &QueueFamilyT) -> bool {
+ family.queue_type().supports_transfer() && family.max_queues() >= NUM_SIMULTANEOUS_CMDS
+ }
+}
diff --git a/stockton-render/src/texture/resolver.rs b/stockton-render/src/texture/resolver.rs
new file mode 100644
index 0000000..f66b724
--- /dev/null
+++ b/stockton-render/src/texture/resolver.rs
@@ -0,0 +1,55 @@
+//! Resolves a texture in a BSP File to an image
+
+use crate::texture::image::LoadableImage;
+use stockton_levels::{parts::IsTexture, prelude::HasTextures};
+
+use std::{
+ path::Path,
+ sync::{Arc, RwLock},
+};
+
+use image::{io::Reader, RgbaImage};
+
+/// An object that can be used to resolve a texture from a BSP File
+pub trait TextureResolver {
+ type Image: LoadableImage;
+
+ /// Get the given texture, or None if it's corrupt/not there.
+ fn resolve(&mut self, texture_id: u32) -> Option<Self::Image>;
+}
+
+/// A basic filesystem resolver which gets the texture name from any HasTextures Object.
+pub struct FsResolver<'a, T: HasTextures> {
+ path: &'a Path,
+ map_lock: Arc<RwLock<T>>,
+}
+
+impl<'a, T: HasTextures> FsResolver<'a, T> {
+ pub fn new(path: &'a Path, map_lock: Arc<RwLock<T>>) -> Self {
+ FsResolver { path, map_lock }
+ }
+}
+
+impl<'a, T: HasTextures> TextureResolver for FsResolver<'a, T> {
+ type Image = RgbaImage;
+
+ fn resolve(&mut self, tex: u32) -> Option<Self::Image> {
+ let map = self.map_lock.read().unwrap();
+ let tex = map.get_texture(tex)?;
+ let path = self.path.join(&tex.name());
+
+ // drop(tex);
+ // drop(map);
+
+ if let Ok(file) = Reader::open(path) {
+ if let Ok(guessed) = file.with_guessed_format() {
+ if let Ok(decoded) = guessed.decode() {
+ return Some(decoded.into_rgba8());
+ }
+ }
+ }
+
+ log::warn!("Couldn't resolve texture {:?}", tex.name());
+ None
+ }
+}
diff --git a/stockton-render/src/texture/staging_buffer.rs b/stockton-render/src/texture/staging_buffer.rs
new file mode 100644
index 0000000..8d2ae17
--- /dev/null
+++ b/stockton-render/src/texture/staging_buffer.rs
@@ -0,0 +1,59 @@
+#![allow(mutable_transmutes)]
+use crate::types::*;
+
+use std::mem::ManuallyDrop;
+
+use anyhow::{Context, Result};
+use hal::{device::MapError, memory::SparseFlags, MemoryTypeId};
+use rendy_memory::{Allocator, Block};
+
+pub struct StagingBuffer {
+ pub buf: ManuallyDrop<BufferT>,
+ pub mem: ManuallyDrop<DynamicBlock>,
+}
+
+impl StagingBuffer {
+ const USAGE: hal::buffer::Usage = hal::buffer::Usage::TRANSFER_SRC;
+
+ pub fn new(
+ device: &mut DeviceT,
+ alloc: &mut DynamicAllocator,
+ size: u64,
+ _memory_type_id: MemoryTypeId,
+ ) -> Result<StagingBuffer> {
+ let mut buffer = unsafe { device.create_buffer(size, Self::USAGE, SparseFlags::empty()) }
+ .context("Error creating buffer")?;
+
+ let requirements = unsafe { device.get_buffer_requirements(&buffer) };
+
+ let (memory, _) = alloc
+ .alloc(device, requirements.size, requirements.alignment)
+ .context("Error allocating staging memory")?;
+
+ unsafe { device.bind_buffer_memory(memory.memory(), 0, &mut buffer) }
+ .context("Error binding staging memory to buffer")?;
+
+ Ok(StagingBuffer {
+ buf: ManuallyDrop::new(buffer),
+ mem: ManuallyDrop::new(memory),
+ })
+ }
+
+ pub unsafe fn map_memory(&mut self, device: &mut DeviceT) -> Result<*mut u8, MapError> {
+ let range = 0..(self.mem.range().end - self.mem.range().start);
+ Ok(self.mem.map(device, range)?.ptr().as_mut())
+ }
+ pub unsafe fn unmap_memory(&mut self, device: &mut DeviceT) {
+ self.mem.unmap(device);
+ }
+
+ pub fn deactivate(self, device: &mut DeviceT, alloc: &mut DynamicAllocator) {
+ unsafe {
+ use std::ptr::read;
+ // Destroy buffer
+ device.destroy_buffer(read(&*self.buf));
+ // Free memory
+ alloc.free(device, read(&*self.mem));
+ }
+ }
+}