@bornengine/engine 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,473 @@
1
+ //! Game-texture storage on the Renderer: registration (with mip
2
+ //! generation), in-place updates, unload (VRAM release + white
3
+ //! placeholder), filter selection, and the model GPU-cache eviction
4
+ //! hook. Split out of renderer/mod.rs (2000-line file policy); the
5
+ //! backing fields (`textures`, `texture_bind_groups`, `texture_sizes`)
6
+ //! still live on [`Renderer`].
7
+
8
+ use super::Renderer;
9
+ use wgpu::util::DeviceExt;
10
+
11
+ impl Renderer {
12
+
13
+ // (encode_png_simple is defined as a free function below the impl
14
+ // block so it can be reused by other capture paths if needed.)
15
+
16
+ pub fn register_texture(&mut self, width: u32, height: u32, data: &[u8]) -> u32 {
17
+ self.register_texture_kind(width, height, data, false)
18
+ }
19
+
20
+ /// Single-mip texture for dynamically updated atlases.
21
+ pub fn register_texture_no_mips(&mut self, width: u32, height: u32, data: &[u8]) -> u32 {
22
+ let texture = self.device.create_texture_with_data(
23
+ &self.queue,
24
+ &wgpu::TextureDescriptor {
25
+ label: Some("atlas_no_mips"),
26
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
27
+ mip_level_count: 1, sample_count: 1,
28
+ dimension: wgpu::TextureDimension::D2,
29
+ format: wgpu::TextureFormat::Rgba8Unorm,
30
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
31
+ view_formats: &[],
32
+ },
33
+ wgpu::util::TextureDataOrder::LayerMajor, data,
34
+ );
35
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
36
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
37
+ label: Some("atlas_bg"), layout: &self.texture_bind_group_layout,
38
+ entries: &[
39
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
40
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
41
+ ],
42
+ });
43
+ let idx = self.texture_bind_groups.len() as u32;
44
+ self.texture_bind_groups.push(bind_group);
45
+ self.textures.push(texture);
46
+ self.texture_sizes.push((width, height));
47
+ idx
48
+ }
49
+
50
+ /// Replace an existing no-mips texture in-place.
51
+ pub fn replace_texture_no_mips(&mut self, idx: u32, width: u32, height: u32, data: &[u8]) {
52
+ let i = idx as usize;
53
+ if i >= self.textures.len() { return; }
54
+ let texture = self.device.create_texture_with_data(
55
+ &self.queue,
56
+ &wgpu::TextureDescriptor {
57
+ label: Some("atlas_replaced"),
58
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
59
+ mip_level_count: 1, sample_count: 1,
60
+ dimension: wgpu::TextureDimension::D2,
61
+ format: wgpu::TextureFormat::Rgba8Unorm,
62
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
63
+ view_formats: &[],
64
+ },
65
+ wgpu::util::TextureDataOrder::LayerMajor, data,
66
+ );
67
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
68
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
69
+ label: Some("atlas_replaced_bg"), layout: &self.texture_bind_group_layout,
70
+ entries: &[
71
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
72
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
73
+ ],
74
+ });
75
+ self.textures[i] = texture;
76
+ self.texture_bind_groups[i] = bind_group;
77
+ self.texture_sizes[i] = (width, height);
78
+ }
79
+
80
+ /// Register a texture with optional normal-map preprocessing.
81
+ ///
82
+ /// For normal maps (is_normal_map=true), mip chain is built with
83
+ /// vector-space averaging instead of scalar RGB averaging, and
84
+ /// per-mip variance (1 - |vector_avg|²) is baked into the alpha
85
+ /// channel. The shader reads alpha as a Toksvig-style σ² addition
86
+ /// that accumulates normal-direction disagreement across the
87
+ /// footprint the sampler ends up integrating — the simplified
88
+ /// scalar LEADR/LEAN filter. Alpha is unused by glTF normal maps
89
+ /// (they carry (x,y,z) in RGB) so we can safely repurpose it.
90
+ pub fn register_texture_kind(
91
+ &mut self,
92
+ width: u32,
93
+ height: u32,
94
+ data: &[u8],
95
+ is_normal_map: bool,
96
+ ) -> u32 {
97
+ let max_dim = if width > height { width } else { height };
98
+ // On Android/Vulkan, multi-level mipmap upload was observed to
99
+ // fail silently (black textures) when this path was written.
100
+ // Single mip costs aliasing + texture bandwidth on exactly the
101
+ // tier of GPU that needs mips most, so this exclusion is worth
102
+ // re-testing on-device against current wgpu (the failure may
103
+ // have been the pre-#61 buffer-limit configuration or an older
104
+ // wgpu bug). Until verified on hardware, keep the safe path.
105
+ #[cfg(target_os = "android")]
106
+ let mip_count = 1u32;
107
+ #[cfg(not(target_os = "android"))]
108
+ let mip_count = (max_dim as f32).log2().floor() as u32 + 1;
109
+
110
+ // Generate mip chain data
111
+ let mut mip_data = Vec::with_capacity(data.len() * 2); // overallocate
112
+ if is_normal_map {
113
+ // Level 0: normalize input RGB and clear alpha to 0 (no
114
+ // variance at the finest level — each texel is assumed unit).
115
+ mip_data.reserve(data.len());
116
+ for i in 0..(width as usize * height as usize) {
117
+ let r = data[i * 4];
118
+ let g = data[i * 4 + 1];
119
+ let b = data[i * 4 + 2];
120
+ mip_data.push(r);
121
+ mip_data.push(g);
122
+ mip_data.push(b);
123
+ mip_data.push(0);
124
+ }
125
+ } else {
126
+ mip_data.extend_from_slice(data);
127
+ }
128
+ let mut mip_offsets = vec![0usize]; // byte offset of each mip level
129
+ let mut mw = width;
130
+ let mut mh = height;
131
+ for _ in 1..mip_count {
132
+ let prev_offset = *mip_offsets.last().unwrap();
133
+ let pw = mw as usize; // previous width
134
+ let ph = mh as usize; // previous height
135
+ mw = if mw > 1 { mw / 2 } else { 1 };
136
+ mh = if mh > 1 { mh / 2 } else { 1 };
137
+ mip_offsets.push(mip_data.len());
138
+ for y in 0..mh as usize {
139
+ for x in 0..mw as usize {
140
+ let sx = x * 2;
141
+ let sy = y * 2;
142
+ let sx1 = (sx + 1).min(pw - 1);
143
+ let sy1 = (sy + 1).min(ph - 1);
144
+ if is_normal_map {
145
+ // Decode 4 children to signed [-1, 1] vectors
146
+ let dec = |r: u8, g: u8, b: u8| -> [f32; 3] {
147
+ [
148
+ r as f32 * (2.0 / 255.0) - 1.0,
149
+ g as f32 * (2.0 / 255.0) - 1.0,
150
+ b as f32 * (2.0 / 255.0) - 1.0,
151
+ ]
152
+ };
153
+ let idx = |sx: usize, sy: usize| -> usize {
154
+ prev_offset + (sy * pw + sx) * 4
155
+ };
156
+ let n00 = dec(mip_data[idx(sx, sy)], mip_data[idx(sx, sy) + 1], mip_data[idx(sx, sy) + 2]);
157
+ let n10 = dec(mip_data[idx(sx1, sy)], mip_data[idx(sx1, sy) + 1], mip_data[idx(sx1, sy) + 2]);
158
+ let n01 = dec(mip_data[idx(sx, sy1)], mip_data[idx(sx, sy1) + 1], mip_data[idx(sx, sy1) + 2]);
159
+ let n11 = dec(mip_data[idx(sx1, sy1)], mip_data[idx(sx1, sy1) + 1], mip_data[idx(sx1, sy1) + 2]);
160
+ // Previous-mip baked variances
161
+ let v00 = mip_data[idx(sx, sy) + 3] as f32 / 255.0;
162
+ let v10 = mip_data[idx(sx1, sy) + 3] as f32 / 255.0;
163
+ let v01 = mip_data[idx(sx, sy1) + 3] as f32 / 255.0;
164
+ let v11 = mip_data[idx(sx1, sy1) + 3] as f32 / 255.0;
165
+ // Average the vectors
166
+ let avg_x = (n00[0] + n10[0] + n01[0] + n11[0]) * 0.25;
167
+ let avg_y = (n00[1] + n10[1] + n01[1] + n11[1]) * 0.25;
168
+ let avg_z = (n00[2] + n10[2] + n01[2] + n11[2]) * 0.25;
169
+ let len_sq = avg_x * avg_x + avg_y * avg_y + avg_z * avg_z;
170
+ let len = len_sq.sqrt().max(1e-6);
171
+ // Normalize direction (what the shader reads as
172
+ // the shading normal). Re-encode to [0, 255].
173
+ let encode = |v: f32| -> u8 {
174
+ ((v * 0.5 + 0.5).clamp(0.0, 1.0) * 255.0 + 0.5) as u8
175
+ };
176
+ mip_data.push(encode(avg_x / len));
177
+ mip_data.push(encode(avg_y / len));
178
+ mip_data.push(encode(avg_z / len));
179
+ // Variance at this mip = disagreement among the
180
+ // 4 children (1 - |avg|²) PLUS the weighted mean
181
+ // of the children's own variances. Both live in
182
+ // [0, 1]; combined variance clamped.
183
+ let v_children_avg = (v00 + v10 + v01 + v11) * 0.25;
184
+ let v_local = (1.0 - len_sq).max(0.0);
185
+ let v_out = (v_local + v_children_avg).min(1.0);
186
+ mip_data.push((v_out * 255.0).round().clamp(0.0, 255.0) as u8);
187
+ } else {
188
+ for c in 0..4usize {
189
+ let p00 = mip_data[prev_offset + (sy * pw + sx) * 4 + c] as u32;
190
+ let p10 = mip_data[prev_offset + (sy * pw + sx1) * 4 + c] as u32;
191
+ let p01 = mip_data[prev_offset + (sy1 * pw + sx) * 4 + c] as u32;
192
+ let p11 = mip_data[prev_offset + (sy1 * pw + sx1) * 4 + c] as u32;
193
+ mip_data.push(((p00 + p10 + p01 + p11 + 2) / 4) as u8);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ let texture = if mip_count == 1 {
201
+ // Simple path: single mip level, use create_texture_with_data
202
+ self.device.create_texture_with_data(
203
+ &self.queue,
204
+ &wgpu::TextureDescriptor {
205
+ label: Some("registered_texture"),
206
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
207
+ mip_level_count: 1,
208
+ sample_count: 1,
209
+ dimension: wgpu::TextureDimension::D2,
210
+ format: wgpu::TextureFormat::Rgba8Unorm,
211
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
212
+ view_formats: &[],
213
+ },
214
+ wgpu::util::TextureDataOrder::LayerMajor,
215
+ &mip_data[..((width * height * 4) as usize)],
216
+ )
217
+ } else {
218
+ // Multi-mip path: create texture, upload each level
219
+ let tex = self.device.create_texture(&wgpu::TextureDescriptor {
220
+ label: Some("registered_texture"),
221
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
222
+ mip_level_count: mip_count,
223
+ sample_count: 1,
224
+ dimension: wgpu::TextureDimension::D2,
225
+ format: wgpu::TextureFormat::Rgba8Unorm,
226
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
227
+ view_formats: &[],
228
+ });
229
+ let mut lw = width;
230
+ let mut lh = height;
231
+ for level in 0..mip_count {
232
+ let offset = mip_offsets[level as usize];
233
+ let level_size = (lw * lh * 4) as usize;
234
+ self.queue.write_texture(
235
+ wgpu::TexelCopyTextureInfo {
236
+ texture: &tex, mip_level: level,
237
+ origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All,
238
+ },
239
+ &mip_data[offset..offset + level_size],
240
+ wgpu::TexelCopyBufferLayout {
241
+ offset: 0, bytes_per_row: Some(4 * lw), rows_per_image: Some(lh),
242
+ },
243
+ wgpu::Extent3d { width: lw, height: lh, depth_or_array_layers: 1 },
244
+ );
245
+ lw = if lw > 1 { lw / 2 } else { 1 };
246
+ lh = if lh > 1 { lh / 2 } else { 1 };
247
+ }
248
+ tex
249
+ };
250
+
251
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
252
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
253
+ label: Some("texture_bg"),
254
+ layout: &self.texture_bind_group_layout,
255
+ entries: &[
256
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
257
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
258
+ ],
259
+ });
260
+
261
+ let idx = self.texture_bind_groups.len() as u32;
262
+ self.texture_bind_groups.push(bind_group);
263
+ self.textures.push(texture);
264
+ self.texture_sizes.push((width, height));
265
+ idx
266
+ }
267
+
268
+ pub fn update_texture(&mut self, idx: u32, width: u32, height: u32, data: &[u8]) {
269
+ let i = idx as usize;
270
+ if i >= self.textures.len() { return; }
271
+ self.queue.write_texture(
272
+ wgpu::TexelCopyTextureInfo {
273
+ texture: &self.textures[i],
274
+ mip_level: 0,
275
+ origin: wgpu::Origin3d::ZERO,
276
+ aspect: wgpu::TextureAspect::All,
277
+ },
278
+ data,
279
+ wgpu::TexelCopyBufferLayout {
280
+ offset: 0,
281
+ bytes_per_row: Some(4 * width),
282
+ rows_per_image: Some(height),
283
+ },
284
+ wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
285
+ );
286
+ }
287
+
288
+ pub fn unload_texture(&mut self, idx: u32) {
289
+ // Replace the slot's texture with a 1×1 white placeholder so the
290
+ // heavyweight texture drops (wgpu releases the VRAM once the
291
+ // queue is done with it). The previous implementation only
292
+ // zeroed the size entry and kept the texture alive forever —
293
+ // load/unload cycles grew VRAM unboundedly.
294
+ //
295
+ // Slots are intentionally NOT reused: a stale bind_group_idx
296
+ // held by a scene material renders white rather than ever
297
+ // aliasing a texture loaded later. The retired slot costs a
298
+ // handful of bytes, not VRAM.
299
+ let i = idx as usize;
300
+ if i == 0 || i >= self.textures.len() {
301
+ return;
302
+ }
303
+ let white = self.device.create_texture_with_data(
304
+ &self.queue,
305
+ &wgpu::TextureDescriptor {
306
+ label: Some("unloaded_texture_placeholder"),
307
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
308
+ mip_level_count: 1,
309
+ sample_count: 1,
310
+ dimension: wgpu::TextureDimension::D2,
311
+ // Matches register_texture's non-sRGB convention.
312
+ format: wgpu::TextureFormat::Rgba8Unorm,
313
+ usage: wgpu::TextureUsages::TEXTURE_BINDING,
314
+ view_formats: &[],
315
+ },
316
+ wgpu::util::TextureDataOrder::LayerMajor,
317
+ &[255, 255, 255, 255],
318
+ );
319
+ let view = white.create_view(&wgpu::TextureViewDescriptor::default());
320
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
321
+ label: Some("unloaded_texture_placeholder_bg"),
322
+ layout: &self.texture_bind_group_layout,
323
+ entries: &[
324
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
325
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
326
+ ],
327
+ });
328
+ self.textures[i] = white;
329
+ self.texture_bind_groups[i] = bind_group;
330
+ self.texture_sizes[i] = (0, 0);
331
+ }
332
+
333
+ /// Drop a model's cached GPU meshes. Must be called when the model is
334
+ /// unloaded: the cache is keyed by handle bits, so without eviction
335
+ /// the buffers leak — and a future model whose handle reuses the slot
336
+ /// would render the stale cached geometry.
337
+ pub fn evict_model_cache(&mut self, handle_bits: u64) {
338
+ self.model_gpu_cache.remove(&handle_bits);
339
+ self.model_skinned.remove(&handle_bits);
340
+ }
341
+
342
+ pub fn set_texture_filter(&mut self, idx: u32, nearest: bool) {
343
+ let i = idx as usize;
344
+ if i >= self.textures.len() { return; }
345
+ let view = self.textures[i].create_view(&wgpu::TextureViewDescriptor::default());
346
+ let chosen_sampler = if nearest { &self.nearest_sampler } else { &self.sampler };
347
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
348
+ label: Some("texture_bg_refiltered"),
349
+ layout: &self.texture_bind_group_layout,
350
+ entries: &[
351
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
352
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(chosen_sampler) },
353
+ ],
354
+ });
355
+ self.texture_bind_groups[i] = bind_group;
356
+ }
357
+ /// Returns (bind_group_index, texture_vec_index).
358
+ pub fn create_render_texture(&mut self, width: u32, height: u32) -> (u32, usize) {
359
+ let texture = self.device.create_texture(&wgpu::TextureDescriptor {
360
+ label: Some("render_texture"),
361
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
362
+ mip_level_count: 1, sample_count: 1,
363
+ dimension: wgpu::TextureDimension::D2,
364
+ format: self.surface_config.format,
365
+ // COPY_SRC so render-target contents are readable (tests,
366
+ // screenshots of offscreen renders).
367
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
368
+ | wgpu::TextureUsages::TEXTURE_BINDING
369
+ | wgpu::TextureUsages::COPY_SRC,
370
+ view_formats: &[],
371
+ });
372
+ let tex_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
373
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
374
+ label: Some("rt_bg"), layout: &self.texture_bind_group_layout,
375
+ entries: &[
376
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&tex_view) },
377
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
378
+ ],
379
+ });
380
+ let idx = self.texture_bind_groups.len() as u32;
381
+ let tex_idx = self.textures.len();
382
+ self.texture_bind_groups.push(bind_group);
383
+ self.textures.push(texture);
384
+ self.texture_sizes.push((width, height));
385
+ (idx, tex_idx)
386
+ }
387
+
388
+ /// Q1: Get a reference to an internal texture by index.
389
+ pub fn get_texture_ref(&self, index: usize) -> Option<&wgpu::Texture> {
390
+ self.textures.get(index)
391
+ }
392
+ }
393
+
394
+ impl Renderer {
395
+ /// Register a cooked BC7 DDS texture (output of bloom-cook).
396
+ /// Gated on `image-extras` (DDS is a beyond-PNG runtime codec).
397
+ ///
398
+ /// On adapters with `TEXTURE_COMPRESSION_BC` the precomputed mip
399
+ /// chain uploads compressed — 4x less VRAM than RGBA8. Returns None
400
+ /// when the device lacks BC support; the caller falls back to CPU
401
+ /// decode through the normal RGBA path.
402
+ #[cfg(feature = "image-extras")]
403
+ pub fn register_texture_dds(&mut self, dds: &image_dds::ddsfile::Dds) -> Option<u32> {
404
+ if !self.device.features().contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
405
+ return None;
406
+ }
407
+ let surface = image_dds::Surface::from_dds(dds).ok()?;
408
+ // Engine convention: game textures bind as non-sRGB (the
409
+ // pipeline applies the transfer itself — see register_texture's
410
+ // Rgba8Unorm). Both BC7 variants therefore map to the Unorm
411
+ // view; the sRGB flag in the DDS only records authoring intent.
412
+ let format = match surface.image_format {
413
+ image_dds::ImageFormat::BC7RgbaUnormSrgb
414
+ | image_dds::ImageFormat::BC7RgbaUnorm => wgpu::TextureFormat::Bc7RgbaUnorm,
415
+ // Other BCn variants can map here as the cook tool grows.
416
+ _ => return None,
417
+ };
418
+ let width = surface.width;
419
+ let height = surface.height;
420
+ let mip_count = surface.mipmaps.max(1);
421
+
422
+ let texture = self.device.create_texture(&wgpu::TextureDescriptor {
423
+ label: Some("cooked_bc7"),
424
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
425
+ mip_level_count: mip_count,
426
+ sample_count: 1,
427
+ dimension: wgpu::TextureDimension::D2,
428
+ format,
429
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
430
+ view_formats: &[],
431
+ });
432
+
433
+ // BC7: 4x4 blocks, 16 bytes each; Surface::get hands back each
434
+ // mip's slice (truncated files yield None — bail cleanly).
435
+ for mip in 0..mip_count {
436
+ let mw = (width >> mip).max(1);
437
+ let mh = (height >> mip).max(1);
438
+ let blocks_w = mw.div_ceil(4);
439
+ let blocks_h = mh.div_ceil(4);
440
+ let mip_data = surface.get(0, 0, mip)?;
441
+ self.queue.write_texture(
442
+ wgpu::TexelCopyTextureInfo {
443
+ texture: &texture,
444
+ mip_level: mip,
445
+ origin: wgpu::Origin3d::ZERO,
446
+ aspect: wgpu::TextureAspect::All,
447
+ },
448
+ mip_data,
449
+ wgpu::TexelCopyBufferLayout {
450
+ offset: 0,
451
+ bytes_per_row: Some(blocks_w * 16),
452
+ rows_per_image: Some(blocks_h),
453
+ },
454
+ wgpu::Extent3d { width: mw, height: mh, depth_or_array_layers: 1 },
455
+ );
456
+ }
457
+
458
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
459
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
460
+ label: Some("cooked_bc7_bg"),
461
+ layout: &self.texture_bind_group_layout,
462
+ entries: &[
463
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
464
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
465
+ ],
466
+ });
467
+ let idx = self.texture_bind_groups.len() as u32;
468
+ self.texture_bind_groups.push(bind_group);
469
+ self.textures.push(texture);
470
+ self.texture_sizes.push((width, height));
471
+ Some(idx)
472
+ }
473
+ }