pub(crate) struct VisibilityPipelineResourceManager {
images: Vec<ResourceStates<(), ()>>,
images_by_resource: HashMap<String, usize>,
materials: Vec<ResourceStates<String, ()>>,
material_by_name: HashMap<String, usize>,
gpu_vertex_data_manager: GPUVertexDataManager,
resource_manager: EntityHandle<ResourceManager>,
pipelines: RwLock<HashMap<String, PipelineStatus>>,
shader_requests: RwLock<StaleHashMap<String, u64, Arc<OwnedShader>>>,
factory: Option<ghi::implementation::Factory>,
material_pipeline_config: Option<MaterialPipelineConfig>,
work_completions: Sender<VisibilityResourceCompletion>,
}
pub(crate) const IBL_SPECULAR_LEVEL_COUNT: usize =
resource_management::resources::image::IBL_PREFILTERED_SPECULAR_MIP_COUNT as usize;
type CompletionList = SmallVec<[VisibilityResourceCompletion; 16]>;
impl VisibilityPipelineResourceManager {
pub(crate) fn spawn(
context: &mut ghi::implementation::Context,
resource_manager: EntityHandle<ResourceManager>,
) -> (
VisibilityPipelineResourceManagerClient,
VisibilityPipelineResourceManagerWorker,
) {
let mesh_data_manager = GPUVertexDataManager::new(context);
let gpu_vertex_data_manager = mesh_data_manager.clone();
let (commands, command_receiver) = mpsc::channel();
let (work_completions, work_completion_receiver) = mpsc::channel();
let resource_manager = Self::new(resource_manager, mesh_data_manager, work_completions.clone());
(
VisibilityPipelineResourceManagerClient {
gpu_vertex_data_manager,
commands,
completions: work_completion_receiver,
},
VisibilityPipelineResourceManagerWorker {
settings: Default::default(),
resource_manager,
commands: command_receiver,
completions: work_completions,
pending_mesh_uploads: VecDeque::new(),
pending_texture_uploads: VecDeque::new(),
pending_environment_uploads: VecDeque::new(),
submitted_uploads: VecDeque::new(),
},
)
}
fn new(
resource_manager: EntityHandle<ResourceManager>,
mesh_data_manager: GPUVertexDataManager,
work_completions: Sender<VisibilityResourceCompletion>,
) -> Self {
Self {
images: Vec::with_capacity(4096),
images_by_resource: HashMap::with_capacity(4096),
materials: Vec::with_capacity(4096),
material_by_name: HashMap::with_capacity(4096),
gpu_vertex_data_manager: mesh_data_manager,
resource_manager,
pipelines: RwLock::new(HashMap::with_capacity(1024)),
shader_requests: RwLock::new(StaleHashMap::with_capacity(1024)),
factory: None,
material_pipeline_config: None,
work_completions,
}
}
fn handle_request(&mut self, request: VisibilityResourceRequest) -> ResourceWorkerFlow {
match request {
VisibilityResourceRequest::Mesh { key: _, source: _ } => {}
VisibilityResourceRequest::Material { id } => self.handle_material_request(id),
VisibilityResourceRequest::Image { key } => self.handle_image_request(key),
VisibilityResourceRequest::Environment { id } => self.handle_environment_request(id),
VisibilityResourceRequest::Shutdown => return ResourceWorkerFlow::Stop,
}
ResourceWorkerFlow::Continue
}
pub(crate) fn configure_material_pipeline(&mut self, mut config: MaterialPipelineConfig) {
self.factory = config.pipeline_factory.take();
self.material_pipeline_config = Some(config);
}
fn handle_material_request(&mut self, id: String) {
let index = self.reserve_material_slot(&id).0;
let result = self.load_variant_metadata(&id, index);
let completion = match result {
Ok(material) => VisibilityResourceCompletion::MaterialReady {
id,
index,
pipeline: material.pipeline,
pending_pipeline: material.pending_pipeline,
alpha_mode: material.alpha_mode,
textures: material.textures,
},
Err(()) => VisibilityResourceCompletion::Failed {
key: VisibilityResourceKey::Material(id),
},
};
if self.work_completions.send(completion).is_err() {
log::error!(
"Visibility material completion failed. The most likely cause is that the render thread stopped receiving worker results."
);
}
}
fn handle_image_request(&mut self, key: VisibilityTextureKey) {
let index = self.reserve_texture_slot(key.as_str()).0;
let result = self.load_texture_with_factory(key.as_str(), index);
let completion = match result {
Ok(texture) => VisibilityResourceCompletion::ImageReady {
key,
index,
image: texture.image,
sampler: texture.sampler,
upload: texture.upload,
},
Err(()) => VisibilityResourceCompletion::Failed { key: key.into() },
};
if self.work_completions.send(completion).is_err() {
log::error!(
"Visibility texture completion failed. The most likely cause is that the render thread stopped receiving worker results."
);
}
}
fn handle_environment_request(&mut self, id: String) {
let completion = match self.load_environment_with_factory(&id) {
Ok(environment) => VisibilityResourceCompletion::EnvironmentReady { id, environment },
Err(()) => VisibilityResourceCompletion::Failed {
key: VisibilityResourceKey::Environment(id),
},
};
if self.work_completions.send(completion).is_err() {
log::error!(
"Visibility environment completion failed. The most likely cause is that the render thread stopped receiving worker results."
);
}
}
fn load_variant_metadata(&mut self, id: &str, index: u32) -> Result<FactoryMaterial, ()> {
let mut reference: Reference<ResourceVariant> = self.resource_manager.request(id).map_err(|_| {
log::error!(
"Visibility material variant request failed for {}. The most likely cause is that the resource id is missing or the asset database is not loaded.",
id
);
})?;
let variant = reference.resource_mut();
let alpha_mode = variant.alpha_mode.clone();
let material = variant.material.resource_mut();
if material.model.name != "Visibility" || material.model.pass != "MaterialEvaluation" {
log::error!(
"Unsupported visibility material model for {}. The most likely cause is that this material targets a different render model or pass.",
id
);
return Err(());
}
let specialization_map_entries = variant
.variables
.iter()
.enumerate()
.filter_map(|(index, variable)| match &variable.value {
Value::Scalar(value) => {
ghi::pipelines::SpecializationMapEntry::new(index as u32, "f32".to_string(), *value).into()
}
Value::Vector3(value) => {
ghi::pipelines::SpecializationMapEntry::new(index as u32, "vec3f".to_string(), *value).into()
}
Value::Vector4(value) => {
ghi::pipelines::SpecializationMapEntry::new(index as u32, "vec4f".to_string(), *value).into()
}
Value::Image(_) => None,
})
.collect::<Vec<_>>();
let textures = variant
.variables
.iter_mut()
.map(|parameter| match parameter.value {
Value::Image(ref image) => {
let key = VisibilityTextureKey::new(image.id());
let texture_index = self.request_texture_dependency(key.clone());
Some((key.as_str().to_string(), texture_index))
}
_ => None,
})
.collect::<Vec<_>>();
let queued_pipeline = self.queue_configured_variant_pipeline(id.to_string(), material, specialization_map_entries);
Ok(FactoryMaterial {
index,
pipeline: queued_pipeline.pipeline,
pending_pipeline: queued_pipeline.pending_pipeline,
alpha_mode,
textures,
})
}
fn request_texture_dependency(&mut self, key: VisibilityTextureKey) -> u32 {
let (index, inserted) = self.reserve_texture_slot(key.as_str());
if inserted {
self.handle_image_request(key);
}
index
}
fn queue_configured_material_pipeline(&mut self, id: String, material: &mut ResourceMaterial) -> QueuedMaterialPipeline {
let Some(config) = self.material_pipeline_config.as_ref() else {
log::error!(
"Visibility material pipeline configuration is unavailable for {}. The most likely cause is that the render pipeline manager has not configured the resource worker yet.",
id
);
return QueuedMaterialPipeline::default();
};
let push_constant_ranges = config.push_constant_ranges.clone();
self.queue_material_pipeline(id, &push_constant_ranges, material)
}
fn queue_configured_variant_pipeline(
&mut self,
id: String,
material: &mut ResourceMaterial,
specialization_map_entries: Vec<ghi::pipelines::SpecializationMapEntry>,
) -> QueuedMaterialPipeline {
let Some(config) = self.material_pipeline_config.as_ref() else {
log::error!(
"Visibility material pipeline configuration is unavailable for {}. The most likely cause is that the render pipeline manager has not configured the resource worker yet.",
id
);
return QueuedMaterialPipeline::default();
};
let push_constant_ranges = config.push_constant_ranges.clone();
self.queue_material_pipeline_with_specialization(id, &push_constant_ranges, material, specialization_map_entries)
}
fn load_texture_with_factory(&mut self, id: &str, index: u32) -> Result<FactoryTexture, ()> {
let mut reference: Reference<ResourceImage> = self.resource_manager.request(id).map_err(|_| {
log::error!(
"Visibility texture resource request failed for {}. The most likely cause is that the resource id is missing or the asset database is not loaded.",
id
);
})?;
let texture = reference.resource();
let format = resource_image_format_to_ghi(texture.format);
let extent = Extent::from(texture.extent);
let mut source = vec![0u8; compact_image_byte_size(format, extent)];
let load_target = reference.load(source.as_mut_slice().into()).map_err(|_| {
log::error!(
"Visibility texture load failed for {}. The most likely cause is that the texture payload could not be read from storage.",
id
);
})?;
let source = load_target.buffer().ok_or_else(|| {
log::error!(
"Visibility texture load target is not CPU-readable for {}. The most likely cause is that the image resource did not load into a byte buffer.",
id
);
})?;
let upload = make_texture_upload(format, extent, source).ok_or_else(|| {
log::error!(
"Visibility texture upload preparation failed for {}. The most likely cause is that the source bytes do not match the texture format and extent.",
id
);
})?;
let device = self.factory.as_mut().ok_or_else(|| {
log::error!(
"Visibility texture creation failed for {}. The most likely cause is that material pipeline creation was configured without a factory.",
id
);
})?;
let image = device.build_image(
ghi::image::Builder::new(format, ghi::Uses::Image | ghi::Uses::TransferDestination)
.name(reference.id())
.extent(extent)
.device_accesses(ghi::DeviceAccesses::DeviceOnly)
.use_case(ghi::UseCases::STATIC),
);
let sampler = device.build_sampler(default_material_sampler_builder());
Ok(FactoryTexture {
index,
image,
sampler,
upload,
})
}
fn load_environment_with_factory(&mut self, id: &str) -> Result<FactoryEnvironment, ()> {
let mut reference: Reference<ResourceImage> = self.resource_manager.request(id).map_err(|_| {
log::error!(
"Visibility environment request failed for {}. The most likely cause is that the image resource is missing or the asset database is not loaded.",
id
);
})?;
let ibl = reference.resource().ibl.clone().ok_or_else(|| {
log::error!(
"Visibility environment IBL data is missing for {}. The most likely cause is that the EXR was baked before IBL generation was enabled.",
id
);
})?;
if ibl.diffuse_irradiance.mip_count != 1
|| ibl.prefiltered_specular.mip_count as usize != IBL_SPECULAR_LEVEL_COUNT
|| ibl.diffuse_irradiance.gamma != resource_management::types::Gamma::Linear
|| ibl.prefiltered_specular.gamma != resource_management::types::Gamma::Linear
{
log::error!(
"Visibility environment IBL metadata is unsupported for {}. The most likely cause is that the baked image does not contain one linear diffuse map and {} linear specular levels.",
id,
IBL_SPECULAR_LEVEL_COUNT
);
return Err(());
}
let diffuse_format = resource_image_format_to_ghi(ibl.diffuse_irradiance.format);
let specular_format = resource_image_format_to_ghi(ibl.prefiltered_specular.format);
let diffuse_extent = Extent::from(ibl.diffuse_irradiance.extent);
let specular_extents: [Extent; IBL_SPECULAR_LEVEL_COUNT] =
std::array::from_fn(|level| environment_mip_extent(ibl.prefiltered_specular.extent, level as u32));
if diffuse_extent.depth().max(1) != 1 || specular_extents.iter().any(|extent| extent.depth().max(1) != 1) {
log::error!(
"Visibility environment IBL extent is unsupported for {}. The most likely cause is that a baked IBL stream is not a two-dimensional lat-long image.",
id
);
return Err(());
}
let mut diffuse_data = vec![0u8; compact_image_byte_size(diffuse_format, diffuse_extent)];
let mut specular_data: [Vec<u8>; IBL_SPECULAR_LEVEL_COUNT] =
std::array::from_fn(|level| vec![0u8; compact_image_byte_size(specular_format, specular_extents[level])]);
let specular_stream_names: [String; IBL_SPECULAR_LEVEL_COUNT] = std::array::from_fn(|level| {
resource_management::resources::image::ibl_prefiltered_specular_stream_name(level as u32)
});
let mut streams = Vec::with_capacity(1 + IBL_SPECULAR_LEVEL_COUNT);
streams.push(resource_management::stream::StreamMut::new(
resource_management::resources::image::IBL_DIFFUSE_IRRADIANCE_STREAM_NAME,
diffuse_data.as_mut_slice(),
));
for (name, data) in specular_stream_names.iter().zip(specular_data.iter_mut()) {
streams.push(resource_management::stream::StreamMut::new(name, data.as_mut_slice()));
}
let loaded = reference.load(streams.into()).map_err(|_| {
log::error!(
"Visibility environment IBL stream load failed for {}. The most likely cause is that the baked image payload is missing one or more named IBL streams.",
id
);
})?;
if !matches!(loaded, ReadTargets::Streams(_)) {
log::error!(
"Visibility environment IBL load returned an unexpected target for {}. The most likely cause is that the resource reader ignored the requested named streams.",
id
);
return Err(());
}
drop(loaded);
let diffuse_upload = make_texture_upload(diffuse_format, diffuse_extent, &diffuse_data).ok_or_else(|| {
log::error!(
"Visibility diffuse IBL upload preparation failed for {}. The most likely cause is that its stream size does not match its format and extent.",
id
);
})?;
let specular_uploads = specular_data
.iter()
.zip(specular_extents)
.map(|(data, extent)| make_texture_upload(specular_format, extent, data))
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
log::error!(
"Visibility specular IBL upload preparation failed for {}. The most likely cause is that a stream size does not match its mip extent.",
id
);
})?;
let specular_uploads: [TextureUpload; IBL_SPECULAR_LEVEL_COUNT] = specular_uploads.try_into().map_err(|_| ())?;
let device = self.factory.as_mut().ok_or_else(|| {
log::error!(
"Visibility environment creation failed for {}. The most likely cause is that the resource worker was configured without a GPU factory.",
id
);
})?;
let diffuse_name = format!("{id} diffuse irradiance");
let diffuse_image = device.build_image(
ghi::image::Builder::new(diffuse_format, ghi::Uses::Image | ghi::Uses::TransferDestination)
.name(&diffuse_name)
.extent(diffuse_extent)
.device_accesses(ghi::DeviceAccesses::DeviceOnly)
.use_case(ghi::UseCases::STATIC),
);
let specular_images = std::array::from_fn(|level| {
let name = format!("{id} prefiltered specular {level}");
device.build_image(
ghi::image::Builder::new(specular_format, ghi::Uses::Image | ghi::Uses::TransferDestination)
.name(&name)
.extent(specular_extents[level])
.device_accesses(ghi::DeviceAccesses::DeviceOnly)
.use_case(ghi::UseCases::STATIC),
)
});
let sampler = device.build_sampler(default_material_sampler_builder());
Ok(FactoryEnvironment {
diffuse_image,
specular_images,
sampler,
diffuse_upload,
specular_uploads,
})
}
fn reserve_texture_slot(&mut self, texture_id: &str) -> (u32, bool) {
if let Some(index) = self.images_by_resource.get(texture_id) {
return (*index as u32, false);
}
let idx = self.images.len() as u32;
if idx as usize >= 1024 {
panic!(
"Visibility texture limit exceeded. The most likely cause is that the scene created more texture variants than the visibility pipeline supports."
);
}
self.images.push(ResourceStates::pending(()));
self.images_by_resource.insert(texture_id.to_string(), idx as usize);
(idx, true)
}
fn request_material(&mut self, material_id: &str) -> u32 {
let (index, inserted) = self.reserve_material_slot(material_id);
if inserted {
self.handle_material_request(material_id.to_string());
}
index
}
fn reserve_material_slot(&mut self, material_id: &str) -> (u32, bool) {
if let Some(index) = self.material_by_name.get(material_id) {
return (*index as u32, false);
}
let idx = self.materials.len() as u32;
if idx as usize >= MAX_MATERIALS {
panic!(
"Visibility material limit exceeded. The most likely cause is that the scene created more material variants than the visibility pipeline supports."
);
}
let material_id = material_id.to_string();
self.materials.push(ResourceStates::pending(material_id.clone()));
self.material_by_name.insert(material_id, idx as usize);
(idx, true)
}
fn load_mesh_source_for_transfer<'buffer>(
&mut self,
transfer: &mut ghi::implementation::CommandBufferRecording,
staging_data_buffer: ghi::BaseBufferHandle,
slice: &mut utils::BufferAllocator<'buffer>,
mesh_source: &MeshSource,
) -> Result<crate::rendering::pipelines::visibility::pipeline_manager::MeshData, ()> {
match mesh_source {
MeshSource::Resource(id) => {
let mut resource: Reference<ResourceMesh> = self.resource_manager.request(id).map_err(|_| {
log::error!(
"Visibility mesh resource request failed for {}. The most likely cause is that the mesh id is missing or the asset database is not loaded.",
id
);
})?;
self.load_mesh_resource_for_transfer(transfer, staging_data_buffer, slice, &mut resource)
}
MeshSource::Generated(generator) => {
let mesh = self
.gpu_vertex_data_manager
.write_gpu_mesh_data_and_return_mesh_object_for_mesh_generator(
generator.as_ref(),
transfer,
staging_data_buffer,
slice,
)
.ok_or(())?;
self.convert_generated_mesh_data(mesh)
}
}
}
fn load_mesh_resource_for_transfer<'buffer>(
&mut self,
transfer: &mut ghi::implementation::CommandBufferRecording,
staging_data_buffer: ghi::BaseBufferHandle,
slice: &mut utils::BufferAllocator<'buffer>,
resource: &mut Reference<ResourceMesh>,
) -> Result<crate::rendering::pipelines::visibility::pipeline_manager::MeshData, ()> {
let mesh = self
.gpu_vertex_data_manager
.write_gpu_mesh_data_and_return_mesh_object_for_mesh_resource(transfer, staging_data_buffer, slice, resource)
.ok_or(())?;
let resource = resource.resource();
let skin_bindings = resource.skins.iter().cloned().map(Arc::new).collect::<Vec<_>>();
let primitives = resource
.primitives
.iter()
.zip(mesh.primitives.iter())
.enumerate()
.map(|(primitive_index, (resource_primitive, primitive))| {
let material_index = self.request_material(&resource_primitive.material.id);
let skin = match resource_primitive.skin {
Some(skin_index) => {
let Some(binding) = skin_bindings.get(skin_index as usize) else {
log::error!(
"Visibility mesh skin index is invalid for primitive {primitive_index}: {skin_index}. The most likely cause is that mesh validation was bypassed or the resource data is corrupted."
);
return Err(());
};
Some(binding.clone())
}
None => None,
};
Ok(crate::rendering::pipelines::visibility::pipeline_manager::MeshPrimitive {
material_index,
meshlet_count: primitive.meshlet_count,
meshlet_offset: primitive.meshlet_offset,
vertex_offset: primitive.vertex_offset,
primitive_offset: primitive.primitive_offset,
triangle_offset: primitive.triangle_offset,
skinning_source_vertex_offset: primitive.skinning_source_vertex_offset,
skinning_vertex_count: primitive.skinning_vertex_count,
skin,
})
})
.collect::<Result<Vec<_>, ()>>()?;
Ok(crate::rendering::pipelines::visibility::pipeline_manager::MeshData {
primitives,
skeleton_node_count: resource
.skeleton
.as_ref()
.map(|skeleton| skeleton.resource().nodes.len() as u32)
.unwrap_or(0),
vertex_offset: mesh.vertex_offset,
primitive_offset: mesh.primitive_offset,
triangle_offset: mesh.triangle_offset,
meshlet_offset: mesh.meshlet_offset,
acceleration_structure: mesh.acceleration_structure,
})
}
fn convert_generated_mesh_data(
&mut self,
mesh: GpuMeshData,
) -> Result<crate::rendering::pipelines::visibility::pipeline_manager::MeshData, ()> {
let material_index = self.request_material("white_solid.bema");
let primitives = mesh
.primitives
.iter()
.map(
|primitive| crate::rendering::pipelines::visibility::pipeline_manager::MeshPrimitive {
material_index,
meshlet_count: primitive.meshlet_count,
meshlet_offset: primitive.meshlet_offset,
vertex_offset: primitive.vertex_offset,
primitive_offset: primitive.primitive_offset,
triangle_offset: primitive.triangle_offset,
skinning_source_vertex_offset: primitive.skinning_source_vertex_offset,
skinning_vertex_count: primitive.skinning_vertex_count,
skin: None,
},
)
.collect();
Ok(crate::rendering::pipelines::visibility::pipeline_manager::MeshData {
primitives,
skeleton_node_count: 0,
vertex_offset: mesh.vertex_offset,
primitive_offset: mesh.primitive_offset,
triangle_offset: mesh.triangle_offset,
meshlet_offset: mesh.meshlet_offset,
acceleration_structure: mesh.acceleration_structure,
})
}
}
pub(crate) struct VisibilityPipelineResourceManagerClient {
pub(super) gpu_vertex_data_manager: GPUVertexDataManager,
commands: Sender<VisibilityTransferCommand>,
completions: Receiver<VisibilityResourceCompletion>,
}
pub(crate) struct VisibilityPipelineResourceManagerWorker {
settings: Settings,
resource_manager: VisibilityPipelineResourceManager,
commands: Receiver<VisibilityTransferCommand>,
completions: Sender<VisibilityResourceCompletion>,
pending_mesh_uploads: VecDeque<(VisibilityMeshKey, MeshSource)>,
pending_texture_uploads: VecDeque<(u32, ghi::BaseImageHandle, ghi::SamplerHandle, TextureUpload)>,
pending_environment_uploads: VecDeque<PendingEnvironmentUpload>,
submitted_uploads: VecDeque<SubmittedUploadBatch>,
}
impl VisibilityPipelineResourceManagerClient {
pub(crate) fn send(&self, command: VisibilityTransferCommand) {
if self.commands.send(command).is_err() {
log::error!(
"Visibility resource request failed. The most likely cause is that the resource worker thread terminated."
);
}
}
pub(crate) fn request_mesh(&self, key: VisibilityMeshKey, source: MeshSource) {
self.send(VisibilityTransferCommand::RequestMesh { key, source });
}
pub(crate) fn request_image(&self, key: VisibilityTextureKey) {
self.send(VisibilityTransferCommand::RequestImage { key });
}
pub(crate) fn request_environment(&self, id: String) {
self.send(VisibilityTransferCommand::RequestEnvironment { id });
}
pub(crate) fn configure_material_pipeline(&self, config: MaterialPipelineConfig) {
self.send(VisibilityTransferCommand::ConfigureMaterialPipeline(config));
}
pub(crate) fn drain_completions(&mut self) -> CompletionList {
let mut completions = CompletionList::new();
while let Ok(completion) = self.completions.try_recv() {
completions.push(completion);
}
completions
}
pub(crate) fn enqueue_texture_upload(
&self,
index: u32,
image: ghi::BaseImageHandle,
sampler: ghi::SamplerHandle,
upload: TextureUpload,
) {
self.send(VisibilityTransferCommand::EnqueueTextureUpload {
index,
image,
sampler,
upload,
});
}
pub(crate) fn enqueue_environment_upload(&self, upload: PendingEnvironmentUpload) {
self.send(VisibilityTransferCommand::EnqueueEnvironmentUpload { upload });
}
}
impl VisibilityPipelineResourceManagerWorker {
pub(crate) fn signal_completed_frame(&mut self, completed_frame: ghi::FrameKey) {
while self
.submitted_uploads
.front()
.is_some_and(|batch| batch.frame_key == completed_frame)
{
let Some(batch) = self.submitted_uploads.pop_front() else {
break;
};
for completion in batch.completions {
if self.completions.send(completion).is_err() {
log::error!(
"Visibility upload completion failed. The most likely cause is that the render thread stopped receiving worker results."
);
}
}
}
}
pub(crate) fn track_submitted_uploads(&mut self, frame_key: ghi::FrameKey, completions: CompletionList) {
if completions.is_empty() {
return;
}
self.submitted_uploads
.push_back(SubmittedUploadBatch { frame_key, completions });
}
pub(crate) fn drain_pending_upload_work(&mut self) -> bool {
self.drain_commands();
self.resource_manager
.drain_pipeline_completions(self.settings.max_pipeline_adoptions_per_frame);
self.has_pending_upload_work()
}
pub(crate) fn prepare_uploads<'buffer>(
&mut self,
transfer: &mut ghi::implementation::CommandBufferRecording,
staging_data_buffer: ghi::BaseBufferHandle,
slice: &mut utils::BufferAllocator<'buffer>,
) -> TransferUploadPrepareResult {
self.drain_pending_upload_work();
self.record_uploads(transfer, staging_data_buffer, slice)
}
fn has_pending_upload_work(&self) -> bool {
!self.pending_mesh_uploads.is_empty()
|| !self.pending_texture_uploads.is_empty()
|| !self.pending_environment_uploads.is_empty()
}
fn drain_commands(&mut self) -> bool {
let mut should_stop = false;
while let Ok(command) = self.commands.try_recv() {
match command {
VisibilityTransferCommand::RequestMesh { key, source } => {
self.pending_mesh_uploads.push_back((key.clone(), source.clone()));
self.resource_manager
.handle_request(VisibilityResourceRequest::Mesh { key, source });
}
VisibilityTransferCommand::RequestImage { key } => {
self.resource_manager.handle_request(VisibilityResourceRequest::Image { key });
}
VisibilityTransferCommand::RequestEnvironment { id } => {
self.resource_manager
.handle_request(VisibilityResourceRequest::Environment { id });
}
VisibilityTransferCommand::EnqueueTextureUpload {
index,
image,
sampler,
upload,
} => {
self.pending_texture_uploads.push_back((index, image, sampler, upload));
}
VisibilityTransferCommand::EnqueueEnvironmentUpload { upload } => {
self.pending_environment_uploads.push_back(upload);
}
VisibilityTransferCommand::ConfigureMaterialPipeline(config) => {
self.resource_manager.configure_material_pipeline(config);
}
VisibilityTransferCommand::Shutdown => should_stop = true,
}
}
should_stop
}
fn record_uploads<'buffer>(
&mut self,
transfer: &mut ghi::implementation::CommandBufferRecording,
staging_data_buffer: ghi::BaseBufferHandle,
slice: &mut utils::BufferAllocator<'buffer>,
) -> TransferUploadPrepareResult {
let mut recorded_work = false;
let mut completions = CompletionList::new();
const TEXTURE_UPLOAD_ALIGNMENT: usize = 256;
while let Some((key, source)) = self.pending_mesh_uploads.pop_front() {
let result = self
.resource_manager
.load_mesh_source_for_transfer(transfer, staging_data_buffer, slice, &source);
match result {
Ok(mesh) => {
let source_kind = match &source {
MeshSource::Resource(_) => "resource",
MeshSource::Generated(_) => "generated",
};
let meshlet_count = mesh.primitives.iter().map(|primitive| primitive.meshlet_count).sum::<u32>();
log::debug!(
"Visibility mesh created: key={}, source={}, primitives={}, meshlets={}, vertex_offset={}, primitive_offset={}, triangle_offset={}, meshlet_offset={}",
key,
source_kind,
mesh.primitives.len(),
meshlet_count,
mesh.vertex_offset,
mesh.primitive_offset,
mesh.triangle_offset,
mesh.meshlet_offset,
);
completions.push(VisibilityResourceCompletion::MeshReady { key, mesh });
recorded_work = true;
}
Err(()) => {
let _ = self
.completions
.send(VisibilityResourceCompletion::Failed { key: key.into() });
}
}
}
while let Some((index, image, sampler, upload)) = self.pending_texture_uploads.pop_front() {
if upload.data.len() > slice.remaining_aligned(TEXTURE_UPLOAD_ALIGNMENT) {
self.pending_texture_uploads.push_front((index, image, sampler, upload));
break;
}
let (source_offset, source_buffer) = slice.take_with_offset_aligned(upload.data.len(), TEXTURE_UPLOAD_ALIGNMENT);
source_buffer.copy_from_slice(&upload.data);
transfer.copy_buffer_to_images(&[ghi::BufferImageCopyDescriptor::new(
staging_data_buffer,
source_offset,
upload.source_bytes_per_row,
upload.source_bytes_per_image,
image,
)]);
completions.push(VisibilityResourceCompletion::TextureUploadReady { index, image, sampler });
recorded_work = true;
}
while let Some(upload) = self.pending_environment_uploads.pop_front() {
let upload_size = upload
.specular
.iter()
.try_fold(
upload.diffuse.upload.data.len().next_multiple_of(TEXTURE_UPLOAD_ALIGNMENT),
|total, image| total.checked_add(image.upload.data.len().next_multiple_of(TEXTURE_UPLOAD_ALIGNMENT)),
)
.expect(
"Visibility environment upload size overflowed. The most likely cause is malformed IBL stream metadata.",
);
if upload_size > slice.remaining_aligned(TEXTURE_UPLOAD_ALIGNMENT) {
self.pending_environment_uploads.push_front(upload);
break;
}
let mut copies = SmallVec::<[ghi::BufferImageCopyDescriptor; 9]>::new();
copies.push(stage_texture_upload(
slice,
staging_data_buffer,
upload.diffuse.image,
&upload.diffuse.upload,
TEXTURE_UPLOAD_ALIGNMENT,
));
for image in &upload.specular {
copies.push(stage_texture_upload(
slice,
staging_data_buffer,
image.image,
&image.upload,
TEXTURE_UPLOAD_ALIGNMENT,
));
}
transfer.copy_buffer_to_images(&copies);
completions.push(VisibilityResourceCompletion::EnvironmentUploadReady {
id: upload.id,
diffuse_image: upload.diffuse.image,
specular_images: upload.specular.map(|image| image.image),
sampler: upload.sampler,
});
recorded_work = true;
}
TransferUploadPrepareResult {
recorded_work,
completions,
}
}
}
pub(crate) struct TransferUploadPrepareResult {
pub(crate) recorded_work: bool,
pub(crate) completions: CompletionList,
}
struct SubmittedUploadBatch {
frame_key: ghi::FrameKey,
completions: CompletionList,
}
#[derive(PartialEq, Eq)]
enum ResourceWorkerFlow {
Continue,
Stop,
}
pub(crate) enum VisibilityResourceRequest {
Mesh { key: VisibilityMeshKey, source: MeshSource },
Material { id: String },
Image { key: VisibilityTextureKey },
Environment { id: String },
Shutdown,
}
pub(crate) enum VisibilityResourceCompletion {
MeshReady {
key: VisibilityMeshKey,
mesh: crate::rendering::pipelines::visibility::pipeline_manager::MeshData,
},
PipelineReady {
name: String,
pipeline: ghi::factory::ComputePipeline,
},
MaterialReady {
id: String,
index: u32,
pipeline: Option<ghi::PipelineHandle>,
pending_pipeline: Option<PendingMaterialPipeline>,
alpha_mode: AlphaMode,
textures: Vec<Option<(String, u32)>>,
},
ImageReady {
key: VisibilityTextureKey,
index: u32,
image: ghi::factory::FactoryImage,
sampler: ghi::factory::FactorySampler,
upload: TextureUpload,
},
EnvironmentReady {
id: String,
environment: FactoryEnvironment,
},
TextureUploadReady {
index: u32,
image: ghi::BaseImageHandle,
sampler: ghi::SamplerHandle,
},
EnvironmentUploadReady {
id: String,
diffuse_image: ghi::BaseImageHandle,
specular_images: [ghi::BaseImageHandle; IBL_SPECULAR_LEVEL_COUNT],
sampler: ghi::SamplerHandle,
},
Failed {
key: VisibilityResourceKey,
},
}
pub(crate) enum VisibilityTransferCommand {
RequestMesh {
key: VisibilityMeshKey,
source: MeshSource,
},
RequestImage {
key: VisibilityTextureKey,
},
RequestEnvironment {
id: String,
},
EnqueueTextureUpload {
index: u32,
image: ghi::BaseImageHandle,
sampler: ghi::SamplerHandle,
upload: TextureUpload,
},
EnqueueEnvironmentUpload {
upload: PendingEnvironmentUpload,
},
ConfigureMaterialPipeline(MaterialPipelineConfig),
Shutdown,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum VisibilityResourceKey {
Mesh(VisibilityMeshKey),
Texture(VisibilityTextureKey),
Material(String),
Environment(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct VisibilityMeshKey(String);
impl VisibilityMeshKey {
pub(crate) fn from_source(source: &MeshSource) -> Self {
match source {
MeshSource::Resource(id) => Self(format!("resource:{id}")),
MeshSource::Generated(generator) => Self(format!("generated:{}", generator.hash())),
}
}
}
impl std::fmt::Display for VisibilityMeshKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl From<VisibilityMeshKey> for VisibilityResourceKey {
fn from(value: VisibilityMeshKey) -> Self {
Self::Mesh(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct VisibilityTextureKey(String);
impl VisibilityTextureKey {
pub(crate) fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub(crate) fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl std::fmt::Display for VisibilityTextureKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl From<VisibilityTextureKey> for VisibilityResourceKey {
fn from(value: VisibilityTextureKey) -> Self {
Self::Texture(value)
}
}
impl std::fmt::Display for VisibilityResourceKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VisibilityResourceKey::Mesh(key) => key.fmt(f),
VisibilityResourceKey::Texture(key) => key.fmt(f),
VisibilityResourceKey::Material(key) => key.fmt(f),
VisibilityResourceKey::Environment(key) => key.fmt(f),
}
}
}
struct FactoryTexture {
index: u32,
image: ghi::implementation::factory::Image,
sampler: ghi::implementation::factory::Sampler,
upload: TextureUpload,
}
pub(crate) struct FactoryEnvironment {
diffuse_image: ghi::implementation::factory::Image,
specular_images: [ghi::implementation::factory::Image; IBL_SPECULAR_LEVEL_COUNT],
sampler: ghi::implementation::factory::Sampler,
diffuse_upload: TextureUpload,
specular_uploads: [TextureUpload; IBL_SPECULAR_LEVEL_COUNT],
}
impl FactoryEnvironment {
pub(crate) fn intern(self, id: String, frame: &mut ghi::implementation::Frame) -> PendingEnvironmentUpload {
let diffuse_image = ghi::BaseImageHandle::from(frame.intern_image(self.diffuse_image));
let specular_images = self
.specular_images
.map(|image| ghi::BaseImageHandle::from(frame.intern_image(image)));
let sampler = frame.intern_sampler(self.sampler);
let mut specular_images = specular_images.into_iter();
let mut specular_uploads = self.specular_uploads.into_iter();
let specular = std::array::from_fn(|_| {
PendingEnvironmentImageUpload {
image: specular_images.next().expect(
"Visibility environment image count changed. The most likely cause is that the fixed IBL array was consumed inconsistently.",
),
upload: specular_uploads.next().expect(
"Visibility environment upload count changed. The most likely cause is that the fixed IBL array was consumed inconsistently.",
),
}
});
PendingEnvironmentUpload {
id,
diffuse: PendingEnvironmentImageUpload {
image: diffuse_image,
upload: self.diffuse_upload,
},
specular,
sampler,
}
}
}
struct PendingEnvironmentImageUpload {
image: ghi::BaseImageHandle,
upload: TextureUpload,
}
pub(crate) struct PendingEnvironmentUpload {
id: String,
diffuse: PendingEnvironmentImageUpload,
specular: [PendingEnvironmentImageUpload; IBL_SPECULAR_LEVEL_COUNT],
sampler: ghi::SamplerHandle,
}
struct FactoryMaterial {
index: u32,
pipeline: Option<ghi::PipelineHandle>,
pending_pipeline: Option<PendingMaterialPipeline>,
alpha_mode: AlphaMode,
textures: Vec<Option<(String, u32)>>,
}
pub(crate) struct PendingMaterialPipeline {
request: ComputePipelineRequest,
}
impl PendingMaterialPipeline {
pub(crate) fn create(self, frame: &mut ghi::implementation::Frame) -> Option<ghi::PipelineHandle> {
let shader = self.request.shader;
let shader_handle = frame
.create_shader(
shader.name.as_deref(),
shader.source.sources(),
shader.stage,
shader.resource_descriptors.iter().copied(),
)
.map_err(|_| {
log::error!(
"Material shader creation failed for {}. The most likely cause is invalid shader payload data.",
self.request.key
);
})
.ok()?;
Some(
frame.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&self.request.push_constant_ranges,
ghi::ShaderParameter::new(&shader_handle, shader.stage)
.with_specialization_map(&self.request.specialization_map_entries),
)),
)
}
}
#[derive(Default)]
struct QueuedMaterialPipeline {
pipeline: Option<ghi::PipelineHandle>,
pending_pipeline: Option<PendingMaterialPipeline>,
}
pub(crate) struct MaterialPipelineConfig {
push_constant_ranges: Vec<ghi::pipelines::PushConstantRange>,
pipeline_factory: Option<ghi::implementation::Factory>,
}
impl MaterialPipelineConfig {
pub(crate) fn new(
push_constant_ranges: Vec<ghi::pipelines::PushConstantRange>,
pipeline_factory: Option<ghi::implementation::Factory>,
) -> Self {
Self {
push_constant_ranges,
pipeline_factory,
}
}
}
pub(crate) struct TextureUpload {
pub(crate) data: Vec<u8>,
pub(crate) source_bytes_per_row: usize,
pub(crate) source_bytes_per_image: usize,
}
enum PipelineStatus {
Pending,
Ready(ghi::PipelineHandle),
Failed,
}
enum OwnedShaderSource {
DXIL(ResourceReaderBacking),
HLSL {
source: String,
entry_point: String,
},
MTLB {
binary: ResourceReaderBacking,
entry_point: String,
threadgroup_size: Option<Extent>,
},
MTL {
source: String,
entry_point: String,
},
SPIRV(ResourceReaderBacking),
}
impl OwnedShaderSource {
fn sources(&self) -> ghi::shader::Sources<'_> {
match self {
OwnedShaderSource::DXIL(binary) => ghi::shader::Sources::DXIL(binary.as_slice()),
OwnedShaderSource::HLSL { source, entry_point } => ghi::shader::Sources::HLSL { source, entry_point },
OwnedShaderSource::MTLB {
binary,
entry_point,
threadgroup_size,
} => ghi::shader::Sources::MTLB {
binary: binary.as_slice(),
entry_point,
threadgroup_size: *threadgroup_size,
},
OwnedShaderSource::MTL { source, entry_point } => ghi::shader::Sources::MTL { source, entry_point },
OwnedShaderSource::SPIRV(binary) => ghi::shader::Sources::SPIRV(binary.as_slice()),
}
}
}
struct OwnedShader {
name: Option<String>,
source: OwnedShaderSource,
stage: ghi::ShaderTypes,
resource_descriptors: Vec<ghi::ShaderResourceDescriptor>,
}
struct ComputePipelineRequest {
key: String,
push_constant_ranges: Vec<ghi::pipelines::PushConstantRange>,
shader: Arc<OwnedShader>,
specialization_map_entries: Vec<ghi::pipelines::SpecializationMapEntry>,
}
enum ComputePipelineResult {
Ready {
key: String,
pipeline: ghi::implementation::ComputePipeline,
},
Failed {
key: String,
reason: String,
},
}
impl VisibilityPipelineResourceManager {
fn compile_compute_pipeline(
device: &mut ghi::implementation::Factory,
request: ComputePipelineRequest,
) -> Result<ghi::implementation::ComputePipeline, String> {
use ghi::Device as _;
let shader = request.shader;
let shader_handle = device.create_shader(
shader.name.as_deref(),
shader.source.sources(),
shader.stage,
shader.resource_descriptors.iter().copied(),
)
.map_err(|_| {
format!(
"shader creation failed for {}. The most likely cause is that the active backend does not support detached shader creation for this shader source or the shader payload is invalid.",
request.key
)
})?;
Ok(device.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&request.push_constant_ranges,
ghi::ShaderParameter::new(&shader_handle, shader.stage)
.with_specialization_map(&request.specialization_map_entries),
)))
}
fn queue_compute_pipeline(&mut self, request: ComputePipelineRequest) {
let key = request.key.clone();
let Some(pipeline_factory) = self.factory.as_mut() else {
self.pipelines.write().insert(key.clone(), PipelineStatus::Failed);
log::error!(
"Pipeline compilation failed for {}. The most likely cause is that material pipeline creation was configured without a pipeline factory.",
key
);
return;
};
let result = catch_unwind(AssertUnwindSafe(|| Self::compile_compute_pipeline(pipeline_factory, request)));
match result {
Ok(Ok(pipeline)) => {
self.pipelines.write().insert(key.clone(), PipelineStatus::Pending);
if self
.work_completions
.send(VisibilityResourceCompletion::PipelineReady { name: key, pipeline })
.is_err()
{
log::error!(
"Visibility pipeline completion failed. The most likely cause is that the render thread stopped receiving worker results."
);
}
}
Ok(Err(reason)) => {
self.pipelines.write().insert(key.clone(), PipelineStatus::Failed);
log::error!(
"Pipeline compilation failed for {}: {}. The most likely cause is that shader creation or pipeline specialization failed on the resource-manager thread.",
key,
reason
);
}
Err(_) => {
self.pipelines.write().insert(key.clone(), PipelineStatus::Failed);
log::error!(
"Pipeline compilation panicked for {}. The most likely cause is that shader creation or pipeline specialization failed on the resource-manager thread.",
key
);
}
}
}
pub(crate) fn poll_pipelines(
&mut self,
_frame: &mut ghi::implementation::Frame,
_max_results: usize,
) -> Vec<(String, ghi::PipelineHandle)> {
Vec::new()
}
pub(crate) fn drain_pipeline_completions(&mut self, _max_results: usize) {}
fn load_cached_shader_request(&self, shader: &mut Reference<Shader>) -> Result<Arc<OwnedShader>, ()> {
if let StaleEntry::Fresh(shader_request) = self.shader_requests.read().entry(&shader.id, shader.get_hash()) {
return Ok(Arc::clone(shader_request));
}
let resource_descriptors = shader
.resource()
.interface
.bindings
.iter()
.map(crate::rendering::shader_store::binding_to_descriptor)
.collect::<Vec<_>>();
let stage = crate::rendering::shader_store::shader_type_to_ghi(shader.resource().stage);
let shader_backing = Self::load_shader_backing(shader)?;
let source = match &shader.resource().artifact {
ShaderArtifact::Dxil => OwnedShaderSource::DXIL(shader_backing),
ShaderArtifact::Hlsl { entry_point } => OwnedShaderSource::HLSL {
source: std::str::from_utf8(shader_backing.as_slice())
.map_err(|_| {
log::error!(
"Failed to load HLSL shader {}. The most likely cause is invalid UTF-8 shader bytes.",
shader.id()
);
})?
.to_string(),
entry_point: entry_point.clone(),
},
ShaderArtifact::Msl { entry_point } => OwnedShaderSource::MTL {
source: std::str::from_utf8(shader_backing.as_slice())
.map_err(|_| {
log::error!(
"Failed to load MSL shader {}. The most likely cause is invalid UTF-8 shader bytes.",
shader.id()
);
})?
.to_string(),
entry_point: entry_point.clone(),
},
ShaderArtifact::Mtlb { entry_point } => OwnedShaderSource::MTLB {
binary: shader_backing,
entry_point: entry_point.clone(),
threadgroup_size: shader
.resource()
.interface
.workgroup_size
.map(|(width, height, depth)| Extent::new(width, height, depth)),
},
ShaderArtifact::Spirv => OwnedShaderSource::SPIRV(shader_backing),
};
let shader_request = Arc::new(OwnedShader {
name: Some(shader.id().to_string()),
source,
stage,
resource_descriptors,
});
self.shader_requests
.write()
.insert(shader.id().to_string(), shader.get_hash(), Arc::clone(&shader_request));
Ok(shader_request)
}
fn load_shader_backing(shader: &mut Reference<Shader>) -> Result<ResourceReaderBacking, ()> {
match shader.consume_reader().into_backing_storage() {
Ok(backing) => Ok(backing),
Err(mut reader) => {
let read_target = ReadTargetsMut::create_buffer(shader);
let load_request = reader.read_into(None, read_target).map_err(|_| {
log::error!(
"Failed to load shader bytes for {}. The most likely cause is that the shader resource no longer has an available read target.",
shader.id(),
);
})?;
match load_request {
ReadTargets::Box(buffer) => Ok(ResourceReaderBacking::Buffer(buffer)),
ReadTargets::Buffer(buffer) => Ok(ResourceReaderBacking::Buffer(buffer.into())),
ReadTargets::Backing(backing) => Ok(backing),
ReadTargets::Streams(_) => {
log::error!(
"Shader {} produced stream-backed data. The most likely cause is that the shader resource was loaded with an unexpected read target.",
shader.id(),
);
Err(())
}
}
}
}
}
fn queue_material_pipeline(
&mut self,
resource_id: String,
push_constant_ranges: &[ghi::pipelines::PushConstantRange],
material: &mut ResourceMaterial,
) -> QueuedMaterialPipeline {
self.queue_material_pipeline_with_specialization(resource_id, push_constant_ranges, material, Vec::new())
}
fn queue_material_pipeline_with_specialization(
&mut self,
resource_id: String,
push_constant_ranges: &[ghi::pipelines::PushConstantRange],
material: &mut ResourceMaterial,
specialization_map_entries: Vec<ghi::pipelines::SpecializationMapEntry>,
) -> QueuedMaterialPipeline {
if let Some(status) = self.pipelines.read().get(&resource_id) {
return match status {
PipelineStatus::Pending | PipelineStatus::Failed => QueuedMaterialPipeline::default(),
PipelineStatus::Ready(handle) => QueuedMaterialPipeline {
pipeline: Some(*handle),
pending_pipeline: None,
},
};
}
self.pipelines.write().insert(resource_id.clone(), PipelineStatus::Pending);
let request = match material.shaders_mut().iter_mut().next() {
Some(shader) => self.load_cached_shader_request(shader).map(|shader| ComputePipelineRequest {
key: resource_id.clone(),
push_constant_ranges: push_constant_ranges.to_vec(),
shader,
specialization_map_entries,
}),
None => Err(()),
};
match request {
Ok(request) => {
if Self::supports_async_material_pipeline_creation() {
self.queue_compute_pipeline(request);
} else {
return QueuedMaterialPipeline {
pipeline: None,
pending_pipeline: Some(PendingMaterialPipeline { request }),
};
}
}
Err(()) => {
self.pipelines.write().insert(resource_id, PipelineStatus::Failed);
}
}
QueuedMaterialPipeline::default()
}
fn supports_async_material_pipeline_creation() -> bool {
ghi::implementation::USES_DX12 || ghi::implementation::USES_METAL
}
}
fn environment_mip_extent(base_extent: [u32; 3], level: u32) -> Extent {
Extent::new(
(base_extent[0] >> level).max(1),
(base_extent[1] >> level).max(1),
base_extent[2].max(1),
)
}
fn compact_image_byte_size(format: ghi::Formats, extent: Extent) -> usize {
format.compact_copy_layout(extent.width().max(1), extent.height().max(1)).2
}
fn stage_texture_upload(
slice: &mut utils::BufferAllocator<'_>,
staging_data_buffer: ghi::BaseBufferHandle,
image: ghi::BaseImageHandle,
upload: &TextureUpload,
alignment: usize,
) -> ghi::BufferImageCopyDescriptor {
let (source_offset, source_buffer) = slice.take_with_offset_aligned(upload.data.len(), alignment);
source_buffer.copy_from_slice(&upload.data);
ghi::BufferImageCopyDescriptor::new(
staging_data_buffer,
source_offset,
upload.source_bytes_per_row,
upload.source_bytes_per_image,
image,
)
}
fn make_texture_upload(format: ghi::Formats, extent: Extent, source: &[u8]) -> Option<TextureUpload> {
let (source_bytes_per_row, row_count, compact_bytes_per_image) =
format.compact_copy_layout(extent.width().max(1), extent.height().max(1));
if source.len() < compact_bytes_per_image {
return None;
}
assert_eq!(
source.len(),
compact_bytes_per_image,
"Texture upload source size mismatch. The most likely cause is that the baked texture payload does not match the runtime texture layout. format={format:?}, extent={extent:?}, source_len={}, source_bytes_per_row={source_bytes_per_row}, row_count={row_count}, expected={compact_bytes_per_image}",
source.len()
);
let padded_bytes_per_row = source_bytes_per_row.next_multiple_of(256);
let source_bytes_per_image = padded_bytes_per_row * row_count;
assert_eq!(
padded_bytes_per_row % 256,
0,
"Texture upload row pitch alignment mismatch. The most likely cause is that the Metal upload layout was built without 256-byte row alignment. format={format:?}, extent={extent:?}, source_bytes_per_row={source_bytes_per_row}, padded_bytes_per_row={padded_bytes_per_row}"
);
assert!(
source_bytes_per_image >= compact_bytes_per_image,
"Texture upload padded image is smaller than compact image. The most likely cause is an invalid row count or row pitch. format={format:?}, extent={extent:?}, compact_bytes_per_image={compact_bytes_per_image}, source_bytes_per_image={source_bytes_per_image}, row_count={row_count}, padded_bytes_per_row={padded_bytes_per_row}"
);
let mut data = vec![0u8; source_bytes_per_image];
for row in 0..row_count {
let source_offset = row * source_bytes_per_row;
let destination_offset = row * padded_bytes_per_row;
let source_end = source_offset + source_bytes_per_row;
let destination_end = destination_offset + source_bytes_per_row;
assert!(
source_end <= source.len(),
"Texture upload source row is out of bounds. The most likely cause is a bad compact row pitch for this format. format={format:?}, extent={extent:?}, row={row}, row_count={row_count}, source_offset={source_offset}, source_end={source_end}, source_len={}, source_bytes_per_row={source_bytes_per_row}",
source.len()
);
assert!(
destination_end <= data.len(),
"Texture upload padded row is out of bounds. The most likely cause is a bad padded row pitch for this format. format={format:?}, extent={extent:?}, row={row}, row_count={row_count}, destination_offset={destination_offset}, destination_end={destination_end}, data_len={}, padded_bytes_per_row={padded_bytes_per_row}",
data.len()
);
let source_row = &source[source_offset..source_end];
data[destination_offset..destination_end].copy_from_slice(source_row);
}
assert_eq!(
data.len(),
source_bytes_per_image,
"Texture upload output size mismatch. The most likely cause is that the padded upload allocation changed during row copy. format={format:?}, extent={extent:?}, data_len={}, expected={source_bytes_per_image}",
data.len()
);
Some(TextureUpload {
data,
source_bytes_per_row: padded_bytes_per_row,
source_bytes_per_image,
})
}
fn resource_image_format_to_ghi(format: resource_management::types::Formats) -> ghi::Formats {
match format {
resource_management::types::Formats::RG8 => ghi::Formats::RG8UNORM,
resource_management::types::Formats::RGB8 => ghi::Formats::RGB8UNORM,
resource_management::types::Formats::RGB16 => ghi::Formats::RGB16UNORM,
resource_management::types::Formats::RGBA8 => ghi::Formats::RGBA8UNORM,
resource_management::types::Formats::RGBA16 => ghi::Formats::RGBA16UNORM,
resource_management::types::Formats::RGBA16F => ghi::Formats::RGBA16F,
resource_management::types::Formats::BC5 => ghi::Formats::BC5,
resource_management::types::Formats::BC5SNORM => ghi::Formats::BC5SNORM,
resource_management::types::Formats::BC7 => ghi::Formats::BC7,
resource_management::types::Formats::BC7SRGB => ghi::Formats::BC7SRGB,
}
}
pub(crate) fn default_material_sampler_builder() -> ghi::sampler::Builder {
ghi::sampler::Builder::new()
.filtering_mode(ghi::FilteringModes::Linear)
.reduction_mode(ghi::SamplingReductionModes::WeightedAverage)
.mip_map_mode(ghi::FilteringModes::Linear)
.addressing_mode(ghi::SamplerAddressingModes::Repeat)
.min_lod(0f32)
.max_lod(0f32)
}
fn panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
return (*message).to_string();
}
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
"pipeline worker panicked with a non-string payload. The most likely cause is that backend pipeline creation hit an unexpected assertion.".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn owned_dxil_source_maps_to_native_ghi_bytecode() {
let source = OwnedShaderSource::DXIL(ResourceReaderBacking::Buffer(vec![1, 2, 3, 4].into_boxed_slice()));
assert!(matches!(
source.sources(),
ghi::shader::Sources::DXIL(bytes) if bytes == [1, 2, 3, 4]
));
}
#[test]
fn texture_upload_preserves_minimum_extent_and_bc_row_contents() {
let extent = Extent::rectangle(5, 7);
let compact_row = 2 * 16;
let source = (0..(compact_row * 2)).map(|value| value as u8).collect::<Vec<_>>();
let upload = make_texture_upload(ghi::Formats::BC7, extent, &source).unwrap();
assert_eq!(upload.source_bytes_per_row, 256);
assert_eq!(upload.source_bytes_per_image, 256 * 2);
assert_eq!(&upload.data[0..compact_row], &source[0..compact_row]);
assert!(upload.data[compact_row..256].iter().all(|byte| *byte == 0));
assert_eq!(&upload.data[256..256 + compact_row], &source[compact_row..compact_row * 2]);
let zero_extent = make_texture_upload(ghi::Formats::RGBA8UNORM, Extent::rectangle(0, 0), &[1, 2, 3, 4]).unwrap();
assert_eq!(zero_extent.source_bytes_per_row, 256);
assert_eq!(zero_extent.source_bytes_per_image, 256);
assert_eq!(&zero_extent.data[..4], &[1, 2, 3, 4]);
}
#[test]
fn texture_upload_preserves_rgba16f_environment_rows() {
let extent = Extent::rectangle(2, 2);
let compact_row = 2 * 8;
let source = (0..compact_row * 2).map(|value| value as u8).collect::<Vec<_>>();
let upload = make_texture_upload(ghi::Formats::RGBA16F, extent, &source).unwrap();
assert_eq!(
resource_image_format_to_ghi(resource_management::types::Formats::RGBA16F),
ghi::Formats::RGBA16F
);
assert_eq!(upload.source_bytes_per_row, 256);
assert_eq!(upload.source_bytes_per_image, 512);
assert_eq!(&upload.data[..compact_row], &source[..compact_row]);
assert_eq!(&upload.data[256..256 + compact_row], &source[compact_row..]);
}
#[test]
fn environment_specular_levels_are_independent_single_mip_images() {
let extents: [Extent; IBL_SPECULAR_LEVEL_COUNT] =
std::array::from_fn(|level| environment_mip_extent([1024, 512, 1], level as u32));
assert_eq!(extents[0], Extent::new(1024, 512, 1));
assert_eq!(extents[1], Extent::new(512, 256, 1));
assert_eq!(extents[7], Extent::new(8, 4, 1));
assert_eq!(compact_image_byte_size(ghi::Formats::RGBA16F, extents[0]), 1024 * 512 * 8);
assert_eq!(compact_image_byte_size(ghi::Formats::RGBA16F, extents[7]), 8 * 4 * 8);
}
}
pub enum ResourceStates<P, L> {
Pending(P),
Loading(ghi::FrameKey, L),
Loaded(L),
Failed,
}
impl<P, L> ResourceStates<P, L> {
pub fn pending(v: P) -> Self {
ResourceStates::Pending(v)
}
pub fn is_ready(&self) -> bool {
match self {
ResourceStates::Loaded(_) => true,
_ => false,
}
}
pub fn is_pending(&self) -> bool {
matches!(self, ResourceStates::Pending(_))
}
pub fn is_failed(&self) -> bool {
matches!(self, ResourceStates::Failed)
}
pub fn get(&self) -> &L {
match self {
ResourceStates::Loading(_, v) => v,
ResourceStates::Loaded(v) => v,
_ => panic!(),
}
}
pub fn get_mut(&mut self) -> &mut L {
match self {
ResourceStates::Loading(_, v) => v,
ResourceStates::Loaded(v) => v,
_ => panic!(),
}
}
pub fn frame_finished(self, frame_key: ghi::FrameKey) -> Self {
match self {
ResourceStates::Loading(loading_frame_key, v) => {
if loading_frame_key == frame_key {
ResourceStates::Loaded(v)
} else {
ResourceStates::Loading(loading_frame_key, v)
}
}
_ => self,
}
}
}
struct Settings {
max_pipeline_adoptions_per_frame: usize,
}
impl Default for Settings {
fn default() -> Self {
Self {
max_pipeline_adoptions_per_frame: 8,
}
}
}
use std::collections::VecDeque;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::Arc;
use std::time::Duration;
use ghi::context::{Context as _, ContextCreate as _};
use ghi::frame::Frame as _;
use ghi::Device as _;
use ghi::{
command_buffer::{
BoundComputePipelineMode as _, BoundPipelineLayoutMode as _, CommandBufferRecording as _, CommonCommandBufferMode as _,
},
Size as _,
};
use math::Vector3;
use resource_management::resource::reader::ResourceReaderBacking;
use resource_management::resource::resource_manager::ResourceManager;
use resource_management::resource::{ReadTargets, ReadTargetsMut};
use resource_management::resources::image::Image as ResourceImage;
use resource_management::resources::material::{
Material as ResourceMaterial, Shader, ShaderArtifact, Value, Variant as ResourceVariant,
};
use resource_management::resources::mesh::Mesh as ResourceMesh;
use resource_management::types::{AlphaMode, ShaderTypes};
use resource_management::Reference;
use smallvec::SmallVec;
use utils::hash::{HashMap, HashMapExt};
use utils::stale_map::{Entry as StaleEntry, StaleHashMap};
use utils::sync::RwLock;
use utils::Extent;
use crate::core::EntityHandle;
use crate::rendering::pipelines::visibility::gpu_vertex_data_manager::{GPUVertexDataManager, MeshData as GpuMeshData};
use crate::rendering::pipelines::visibility::{MAX_BINDLESS_TEXTURES, MAX_MATERIALS};
use crate::rendering::renderable::mesh::MeshSource;
use crate::resource_management::{self};