1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// src/metal/resources/textures.rs
//
// Texture-pool slot updates + IBL / colour-grading hot-swap. Driven both by
// the streaming subsystem (per-slot upload + eviction placeholders) and by
// asset hot-reload (`cn debug` only) for envmaps + LUTs.
#![deny(unsafe_op_in_unsafe_fn)]
use objc2::runtime::ProtocolObject;
use crate::metal::context::MtlContext;
use crate::metal::texture::{upload_texture, upload_texture_image};
impl MtlContext {
// The texture a `normal_map_slot` samples for the legacy per-draw normal
// binding: a real normal map is a texture in the shared pool at its own slot;
// `NO_NORMAL_MAP_SLOT` selects the flat-normal fallback (the first entry of
// `fallback_textures`).
pub(in crate::metal) fn normal_pool_texture(
&self,
normal_map_slot: usize,
) -> &ProtocolObject<dyn objc2_metal::MTLTexture> {
if normal_map_slot == crate::gfx::render_types::NO_NORMAL_MAP_SLOT {
self.fallback_textures[0].as_ref()
} else {
let last = self.textures.len().saturating_sub(1);
self.textures[normal_map_slot.min(last)].as_ref()
}
}
// The same for an albedo `texture_slot`: `NO_ALBEDO_SLOT` selects the white
// fallback (the second entry), so an untextured material shows its tint
// rather than whichever texture holds slot 0.
pub(in crate::metal) fn albedo_pool_texture(
&self,
texture_slot: usize,
) -> &ProtocolObject<dyn objc2_metal::MTLTexture> {
if texture_slot == crate::gfx::render_types::NO_ALBEDO_SLOT {
self.fallback_textures[1].as_ref()
} else {
let last = self.textures.len().saturating_sub(1);
self.textures[texture_slot.min(last)].as_ref()
}
}
// Replace albedo texture-pool `slot` with freshly decoded RGBA8 pixels.
//
// The asset-streaming subsystem calls this to bring a texture resident
// after init. Both the bindless pool bind and the per-draw fallback bind
// read `self.textures` fresh each frame, so the swapped texture is picked
// up on the next `draw_frame` with no pipeline rebuild.
pub(crate) fn update_texture_slot(
&mut self,
slot: usize,
image: &concinnity_core::build::texture::TextureImage,
) -> Result<(), String> {
if slot >= self.textures.len() {
return Err(format!(
"update_texture_slot: slot {} out of range (pool size {})",
slot,
self.textures.len()
));
}
self.textures[slot] = upload_texture_image(&self.allocator, image)?;
Ok(())
}
// Reset albedo texture-pool `slot` to a 1x1 mid-grey placeholder.
//
// Used by the asset-streaming subsystem to mark a slot whose texture is
// not yet resident; a later `update_texture_slot` brings the real texture
// back. The grey is distinct from the white no-texture fallback so a
// not-yet-streamed slot reads differently under inspection.
pub(crate) fn evict_texture_slot(&mut self, slot: usize) -> Result<(), String> {
if slot >= self.textures.len() {
return Err(format!(
"evict_texture_slot: slot {} out of range (pool size {})",
slot,
self.textures.len()
));
}
self.textures[slot] = upload_texture(&self.allocator, 1, 1, &[128, 128, 128, 255])?;
Ok(())
}
// Swap the live 3D colour-grading LUT for a fresh payload. Driven by
// asset hot-reload (`cn debug` only). The composite pass binds
// `self.color_lut` every frame, so the new texture is sampled on the
// next `draw_frame` with no pipeline rebuild.
pub(crate) fn update_color_lut(&mut self, size: u32, data: &[u8]) -> Result<(), String> {
let tex = crate::metal::texture::upload_color_lut(&self.allocator, size, data)?;
self.color_lut = tex;
Ok(())
}
// Swap the live IBL cubemap pair for a freshly precomputed envmap payload.
// Driven by asset hot-reload (`cn debug` only). The fragment shader binds
// `self.env_map.irradiance` and `self.env_map.prefilter` every frame, so
// the new cubes are sampled on the next `draw_frame` with no pipeline
// rebuild. The new payload may declare different mip / face sizes than
// the original -- `EnvironmentMapTextures` is replaced wholesale.
pub(crate) fn update_environment_map(&mut self, payload: &[u8]) -> Result<(), String> {
let view = crate::build::environment_map::deserialise(payload)
.map_err(|e| format!("envmap hot-reload payload malformed: {}", e))?;
let new_env = crate::metal::texture::upload_environment_map(
&self.allocator,
view.irradiance_face,
view.irradiance_bytes,
view.prefilter_face,
&view.prefilter_mip_bytes,
)?;
self.env_map = new_env;
Ok(())
}
// Upload a baked reflection-probe payload to GPU cube textures and return
// them. The caller installs the result into `probe.maps` (the specular
// reflection source, distinct from `env_map`); the skybox + diffuse irradiance
// keep sampling `env_map`, so the visible sky is never replaced by the capture.
pub(in crate::metal) fn build_probe_textures(
&self,
payload: &[u8],
) -> Result<super::super::texture::EnvironmentMapTextures, String> {
let view = crate::build::environment_map::deserialise(payload)
.map_err(|e| format!("reflection probe payload malformed: {}", e))?;
crate::metal::texture::upload_environment_map(
&self.allocator,
view.irradiance_face,
view.irradiance_bytes,
view.prefilter_face,
&view.prefilter_mip_bytes,
)
}
}