use concinnity_core::bake;
use concinnity_core::gfx::mesh_payload::Vertex;
use concinnity_core::gfx::render_types::{AreaLightData, GpuLight};
use concinnity_core::render::backend_init::{MediaPayloads, SceneData};
use concinnity_core::render::cluster_range::ClusterReach;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::ltc;
use objc2_metal::{
MTLDevice as _, MTLResourceOptions, MTLSamplerAddressMode, MTLSamplerDescriptor,
MTLSamplerMinMagFilter,
};
use super::InitGpu;
use crate::metal::context::{BINDLESS_TEXTURE_COUNT, MtlSceneAssets, bytes_of_slice};
use crate::metal::texture::{
EnvironmentMapTextures, create_fallback_color_lut, create_fallback_cubemap,
create_fallback_texture, create_lut_texture, upload_color_lut, upload_environment_map,
upload_texture, upload_texture_image,
};
pub(super) struct SceneInputs<'a> {
pub(super) world: &'a SceneData<'a>,
pub(super) media: &'a MediaPayloads<'a>,
pub(super) local_lights: &'a [GpuLight],
pub(super) area_lights: &'a [AreaLightData],
pub(super) anisotropy: u32,
pub(super) bindless: bool,
}
pub(super) fn build_scene_assets(
gpu: &InitGpu<'_>,
inputs: SceneInputs<'_>,
) -> RenderResult<MtlSceneAssets> {
let device = &*gpu.hw.device;
let allocator = &gpu.hw.allocator;
let SceneInputs {
world,
media,
local_lights,
area_lights,
anisotropy,
bindless,
} = inputs;
let (vertices, indices) = (world.vertices, world.indices);
let vertex_buffer = if vertices.is_empty() {
allocator.alloc_buffer(
std::mem::size_of::<Vertex>(),
MTLResourceOptions::StorageModeShared,
)
} else {
allocator.alloc_buffer_with_bytes(
bytes_of_slice(vertices),
MTLResourceOptions::StorageModeShared,
)
}
.map_err(|e| e.context("vertex buffer"))?;
let index_buffer = if indices.is_empty() {
allocator.alloc_buffer(
std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModeShared,
)
} else {
allocator.alloc_buffer_with_bytes(
bytes_of_slice(indices),
MTLResourceOptions::StorageModeShared,
)
}
.map_err(|e| e.context("index buffer"))?;
let local_light_buffer = if local_lights.is_empty() {
allocator.alloc_buffer(
std::mem::size_of::<GpuLight>(),
MTLResourceOptions::StorageModeShared,
)
} else {
allocator.alloc_buffer_with_bytes(
bytes_of_slice(local_lights),
MTLResourceOptions::StorageModeShared,
)
}
.map_err(|e| e.context("local-light buffer"))?;
let area_light_buffer = if area_lights.is_empty() {
allocator.alloc_buffer(
std::mem::size_of::<AreaLightData>(),
MTLResourceOptions::StorageModeShared,
)
} else {
allocator.alloc_buffer_with_bytes(
bytes_of_slice(area_lights),
MTLResourceOptions::StorageModeShared,
)
}
.map_err(|e| e.context("area-light buffer"))?;
let ltc_matrix_texture =
create_lut_texture(allocator, ltc::matrix_texels(), ltc::LTC_LUT_SIZE as u32, 4)?;
let ltc_magnitude_texture = create_lut_texture(
allocator,
ltc::magnitude_texels(),
ltc::LTC_LUT_SIZE as u32,
2,
)?;
let textures = if media.textures.is_empty() {
vec![create_fallback_texture(allocator)?]
} else {
media
.textures
.iter()
.enumerate()
.map(|(i, image)| {
upload_texture_image(allocator, image)
.map_err(|e| e.context(format_args!("texture[{i}]")))
})
.collect::<Result<Vec<_>, _>>()?
};
let flat_normal = upload_texture(allocator, 1, 1, &[128u8, 128, 255, 255])
.map_err(|e| e.context("flat normal fallback"))?;
let white = upload_texture(allocator, 1, 1, &[255u8, 255, 255, 255])
.map_err(|e| e.context("white fallback"))?;
let fallback_textures = vec![flat_normal, white];
if bindless && textures.len() + fallback_textures.len() > BINDLESS_TEXTURE_COUNT {
tracing::warn!(
"Metal: texture pool ({} textures + 2 fallbacks) exceeds bindless \
capacity {}; some objects will sample a clamped texture",
textures.len(),
BINDLESS_TEXTURE_COUNT,
);
}
let sampler = {
let desc = MTLSamplerDescriptor::new();
desc.setMinFilter(MTLSamplerMinMagFilter::Linear);
desc.setMagFilter(MTLSamplerMinMagFilter::Linear);
desc.setMipFilter(objc2_metal::MTLSamplerMipFilter::Linear);
desc.setSAddressMode(MTLSamplerAddressMode::Repeat);
desc.setTAddressMode(MTLSamplerAddressMode::Repeat);
desc.setMaxAnisotropy(anisotropy.clamp(1, 16) as usize);
desc.setSupportArgumentBuffers(true);
device
.newSamplerStateWithDescriptor(&desc)
.ok_or_else(|| RenderError::Other("failed to create sampler state".into()))?
};
let cube_sampler = {
let desc = MTLSamplerDescriptor::new();
desc.setMinFilter(MTLSamplerMinMagFilter::Linear);
desc.setMagFilter(MTLSamplerMinMagFilter::Linear);
desc.setMipFilter(objc2_metal::MTLSamplerMipFilter::Linear);
desc.setSAddressMode(MTLSamplerAddressMode::ClampToEdge);
desc.setTAddressMode(MTLSamplerAddressMode::ClampToEdge);
desc.setRAddressMode(MTLSamplerAddressMode::ClampToEdge);
desc.setSupportArgumentBuffers(true);
device
.newSamplerStateWithDescriptor(&desc)
.ok_or_else(|| RenderError::Other("failed to create cube sampler state".into()))?
};
let env_map = if let Some(bytes) = media.env_map_bytes {
let view = bake::environment_map::deserialize(bytes)
.map_err(|e| RenderError::Other(format!("EnvironmentMap payload malformed: {e}")))?;
upload_environment_map(
allocator,
view.irradiance_face,
view.irradiance_bytes,
view.prefilter_face,
&view.prefilter_mip_bytes,
)?
} else {
EnvironmentMapTextures {
irradiance: create_fallback_cubemap(allocator, [0.05, 0.05, 0.05, 1.0])?,
prefilter: create_fallback_cubemap(allocator, [0.05, 0.05, 0.05, 1.0])?,
prefilter_mip_count: 0,
}
};
let color_lut = if let Some(bytes) = media.color_lut_bytes {
let (size, data) = bake::color_lut::deserialize(bytes)
.map_err(|e| RenderError::Other(format!("ColorLut payload malformed: {e}")))?;
upload_color_lut(allocator, size, data)?
} else {
create_fallback_color_lut(allocator)?
};
Ok(MtlSceneAssets {
vertex_buffer,
index_buffer,
textures,
fallback_textures,
local_light_buffer,
cluster_reach: ClusterReach::new(local_lights),
area_light_buffer,
ltc_matrix_texture,
ltc_magnitude_texture,
env_map,
color_lut,
sampler,
cube_sampler,
})
}