@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,502 @@
1
+ use crate::renderer::Renderer;
2
+ use fontdue::{Font, FontSettings};
3
+ use std::collections::HashMap;
4
+
5
+ static DEFAULT_FONT_DATA: &[u8] = include_bytes!("../assets/default_font.ttf");
6
+
7
+ struct GlyphInfo {
8
+ atlas_x: u32,
9
+ atlas_y: u32,
10
+ width: u32,
11
+ height: u32,
12
+ advance: f32,
13
+ x_offset: f32,
14
+ y_offset: f32,
15
+ }
16
+
17
+ /// Generate a signed distance field from a bitmap glyph.
18
+ /// `bitmap` is the alpha channel, `w`/`h` are dimensions.
19
+ /// `spread` is the distance range in pixels.
20
+ /// Returns an RGBA SDF texture (white + SDF in alpha).
21
+ fn generate_sdf(bitmap: &[u8], w: u32, h: u32, spread: f32) -> Vec<u8> {
22
+ let ww = w as i32;
23
+ let hh = h as i32;
24
+ let spread_i = spread as i32;
25
+ let mut sdf = vec![0u8; (w * h * 4) as usize];
26
+
27
+ for y in 0..hh {
28
+ for x in 0..ww {
29
+ let inside = bitmap[(y * ww + x) as usize] > 127;
30
+ let mut min_dist = spread;
31
+
32
+ // Search in a window around the pixel for the nearest edge
33
+ let x0 = (x - spread_i).max(0);
34
+ let x1 = (x + spread_i).min(ww - 1);
35
+ let y0 = (y - spread_i).max(0);
36
+ let y1 = (y + spread_i).min(hh - 1);
37
+
38
+ for sy in y0..=y1 {
39
+ for sx in x0..=x1 {
40
+ let other_inside = bitmap[(sy * ww + sx) as usize] > 127;
41
+ if other_inside != inside {
42
+ let dx = (sx - x) as f32;
43
+ let dy = (sy - y) as f32;
44
+ let dist = (dx * dx + dy * dy).sqrt();
45
+ if dist < min_dist { min_dist = dist; }
46
+ }
47
+ }
48
+ }
49
+
50
+ let normalized = if inside {
51
+ 0.5 + 0.5 * (min_dist / spread)
52
+ } else {
53
+ 0.5 - 0.5 * (min_dist / spread)
54
+ };
55
+ let alpha = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
56
+ let idx = ((y * ww + x) * 4) as usize;
57
+ sdf[idx] = 255;
58
+ sdf[idx + 1] = 255;
59
+ sdf[idx + 2] = 255;
60
+ sdf[idx + 3] = alpha;
61
+ }
62
+ }
63
+ sdf
64
+ }
65
+
66
+ pub struct TextRenderer {
67
+ fonts: Vec<Font>,
68
+ glyph_cache: HashMap<(usize, char, u32), GlyphInfo>,
69
+ atlas_data: Vec<u8>,
70
+ atlas_width: u32,
71
+ atlas_height: u32,
72
+ atlas_cursor_x: u32,
73
+ atlas_cursor_y: u32,
74
+ atlas_row_height: u32,
75
+ atlas_bind_group_idx: Option<u32>,
76
+ atlas_dirty: bool,
77
+ /// One-shot guard for the atlas-full warning.
78
+ atlas_overflow_warned: bool,
79
+ // SDF atlas (separate from bitmap atlas)
80
+ sdf_glyph_cache: HashMap<(usize, char), GlyphInfo>,
81
+ sdf_atlas_data: Vec<u8>,
82
+ sdf_atlas_cursor_x: u32,
83
+ sdf_atlas_cursor_y: u32,
84
+ sdf_atlas_row_height: u32,
85
+ sdf_atlas_bind_group_idx: Option<u32>,
86
+ sdf_atlas_dirty: bool,
87
+ }
88
+
89
+ impl TextRenderer {
90
+ /// Cheap placeholder that allocates nothing. Used as a temporary swap target
91
+ /// when we need to split borrows on EngineState (text + renderer).
92
+ pub fn empty() -> Self {
93
+ Self {
94
+ fonts: Vec::new(),
95
+ glyph_cache: HashMap::new(),
96
+ atlas_data: Vec::new(),
97
+ atlas_width: 0,
98
+ atlas_height: 0,
99
+ atlas_cursor_x: 0,
100
+ atlas_cursor_y: 0,
101
+ atlas_row_height: 0,
102
+ atlas_bind_group_idx: None,
103
+ atlas_dirty: false,
104
+ atlas_overflow_warned: false,
105
+ sdf_glyph_cache: HashMap::new(),
106
+ sdf_atlas_data: Vec::new(),
107
+ sdf_atlas_cursor_x: 0,
108
+ sdf_atlas_cursor_y: 0,
109
+ sdf_atlas_row_height: 0,
110
+ sdf_atlas_bind_group_idx: None,
111
+ sdf_atlas_dirty: false,
112
+ }
113
+ }
114
+
115
+ pub fn new() -> Self {
116
+ let default_font = Font::from_bytes(DEFAULT_FONT_DATA, FontSettings::default())
117
+ .expect("Failed to load default font");
118
+
119
+ let atlas_width = 1024;
120
+ let atlas_height = 1024;
121
+
122
+ Self {
123
+ fonts: vec![default_font],
124
+ glyph_cache: HashMap::new(),
125
+ atlas_data: vec![0u8; (atlas_width * atlas_height * 4) as usize],
126
+ atlas_width,
127
+ atlas_height,
128
+ atlas_cursor_x: 0,
129
+ atlas_cursor_y: 0,
130
+ atlas_row_height: 0,
131
+ atlas_bind_group_idx: None,
132
+ atlas_dirty: true,
133
+ atlas_overflow_warned: false,
134
+ sdf_glyph_cache: HashMap::new(),
135
+ sdf_atlas_data: vec![0u8; (atlas_width * atlas_height * 4) as usize],
136
+ sdf_atlas_cursor_x: 0,
137
+ sdf_atlas_cursor_y: 0,
138
+ sdf_atlas_row_height: 0,
139
+ sdf_atlas_bind_group_idx: None,
140
+ sdf_atlas_dirty: true,
141
+ }
142
+ }
143
+
144
+ pub fn load_font(&mut self, data: &[u8]) -> usize {
145
+ let font = Font::from_bytes(data, FontSettings::default())
146
+ .expect("Failed to load font");
147
+ self.fonts.push(font);
148
+ self.fonts.len() - 1
149
+ }
150
+
151
+ fn rasterize_glyph(&mut self, font_idx: usize, ch: char, size: u32) -> &GlyphInfo {
152
+ let key = (font_idx, ch, size);
153
+ if self.glyph_cache.contains_key(&key) {
154
+ return &self.glyph_cache[&key];
155
+ }
156
+
157
+ let font = &self.fonts[font_idx];
158
+ let (metrics, bitmap) = font.rasterize(ch, size as f32);
159
+
160
+ let gw = metrics.width as u32;
161
+ let gh = metrics.height as u32;
162
+
163
+ if self.atlas_cursor_x + gw > self.atlas_width {
164
+ self.atlas_cursor_x = 0;
165
+ self.atlas_cursor_y += self.atlas_row_height;
166
+ self.atlas_row_height = 0;
167
+ }
168
+
169
+ // Round-2 audit F5: the atlas used to overflow SILENTLY — writes
170
+ // were clipped by the bounds check below and the quads then
171
+ // sampled wrapped UVs (garbage glyphs). Warn once so the failure
172
+ // mode is named; grow-or-evict is the durable follow-up.
173
+ if self.atlas_cursor_y + gh > self.atlas_height && !self.atlas_overflow_warned {
174
+ self.atlas_overflow_warned = true;
175
+ eprintln!(
176
+ "bloom text: glyph atlas ({}x{}) is full — new glyphs will render corrupted \
177
+ until eviction lands (too many distinct size/char combinations)",
178
+ self.atlas_width, self.atlas_height
179
+ );
180
+ }
181
+
182
+ let ax = self.atlas_cursor_x;
183
+ let ay = self.atlas_cursor_y;
184
+
185
+ for row in 0..gh {
186
+ for col in 0..gw {
187
+ let src = (row * gw + col) as usize;
188
+ let dst = (((ay + row) * self.atlas_width + ax + col) * 4) as usize;
189
+ if src < bitmap.len() && dst + 3 < self.atlas_data.len() {
190
+ let alpha = bitmap[src];
191
+ self.atlas_data[dst] = 255;
192
+ self.atlas_data[dst + 1] = 255;
193
+ self.atlas_data[dst + 2] = 255;
194
+ self.atlas_data[dst + 3] = alpha;
195
+ }
196
+ }
197
+ }
198
+
199
+ self.atlas_cursor_x += gw + 1;
200
+ if gh + 1 > self.atlas_row_height {
201
+ self.atlas_row_height = gh + 1;
202
+ }
203
+ self.atlas_dirty = true;
204
+
205
+ self.glyph_cache.insert(key, GlyphInfo {
206
+ atlas_x: ax,
207
+ atlas_y: ay,
208
+ width: gw,
209
+ height: gh,
210
+ advance: metrics.advance_width,
211
+ x_offset: metrics.xmin as f32,
212
+ y_offset: metrics.ymin as f32,
213
+ });
214
+
215
+ &self.glyph_cache[&key]
216
+ }
217
+
218
+ fn ensure_atlas_uploaded(&mut self, renderer: &mut Renderer) {
219
+ if !self.atlas_dirty { return; }
220
+
221
+ match self.atlas_bind_group_idx {
222
+ None => {
223
+ let idx = renderer.register_texture_no_mips(self.atlas_width, self.atlas_height, &self.atlas_data);
224
+ self.atlas_bind_group_idx = Some(idx);
225
+ }
226
+ Some(idx) => {
227
+ renderer.replace_texture_no_mips(idx, self.atlas_width, self.atlas_height, &self.atlas_data);
228
+ }
229
+ }
230
+ self.atlas_dirty = false;
231
+ }
232
+
233
+ pub fn measure_text(&mut self, text: &str, size: u32) -> f64 {
234
+ self.measure_text_ex(0, text, size, 0.0)
235
+ }
236
+
237
+ pub fn measure_text_ex(&mut self, font_idx: usize, text: &str, size: u32, spacing: f32) -> f64 {
238
+ let idx = if font_idx < self.fonts.len() { font_idx } else { 0 };
239
+ let mut width = 0.0f32;
240
+ let mut first = true;
241
+ for ch in text.chars() {
242
+ if !first && spacing != 0.0 {
243
+ width += spacing;
244
+ }
245
+ first = false;
246
+ let glyph = self.rasterize_glyph(idx, ch, size);
247
+ width += glyph.advance;
248
+ }
249
+ width as f64
250
+ }
251
+
252
+ pub fn draw_text(
253
+ &mut self,
254
+ renderer: &mut Renderer,
255
+ text: &str,
256
+ x: f64,
257
+ y: f64,
258
+ size: u32,
259
+ r: f64, g: f64, b: f64, a: f64,
260
+ ) {
261
+ self.draw_text_ex(renderer, 0, text, x, y, size, 0.0, r, g, b, a);
262
+ }
263
+
264
+ pub fn draw_text_ex(
265
+ &mut self,
266
+ renderer: &mut Renderer,
267
+ font_idx: usize,
268
+ text: &str,
269
+ x: f64,
270
+ y: f64,
271
+ size: u32,
272
+ spacing: f32,
273
+ r: f64, g: f64, b: f64, a: f64,
274
+ ) {
275
+ let idx = if font_idx < self.fonts.len() { font_idx } else { 0 };
276
+
277
+ // Round-2 audit F5: glyphs were rasterized at LOGICAL pixel size
278
+ // and stretched ×dpi by the 2D projection — a soft, bilinear-
279
+ // magnified HUD at 150% display scaling. Rasterize at PHYSICAL
280
+ // size and divide the layout metrics back to logical. fontdue
281
+ // advances are exactly linear in pixel size, so measureText
282
+ // (which stays in logical units) remains consistent with the
283
+ // drawn advances.
284
+ let dpi = if renderer.logical_width > 0 {
285
+ (renderer.surface_config.width as f32 / renderer.logical_width as f32).max(0.25)
286
+ } else {
287
+ 1.0
288
+ };
289
+ let phys_size = (((size as f32) * dpi).round().max(1.0)) as u32;
290
+ // Exact logical/physical ratio actually in use (phys_size is
291
+ // rounded, so derive from it rather than from dpi).
292
+ let inv_dpi = size as f32 / phys_size as f32;
293
+
294
+ // Ensure all glyphs are rasterized first
295
+ for ch in text.chars() {
296
+ self.rasterize_glyph(idx, ch, phys_size);
297
+ }
298
+
299
+ // Upload atlas if dirty
300
+ self.ensure_atlas_uploaded(renderer);
301
+
302
+ let atlas_bg = match self.atlas_bind_group_idx {
303
+ Some(idx) => idx,
304
+ None => return,
305
+ };
306
+
307
+ // sRGB-decode to linear like every other 2D vertex color — the
308
+ // swapchain view's hardware encode expects linear shader output
309
+ // (see renderer::srgb_u8_to_linear).
310
+ let color = [
311
+ crate::renderer::srgb_u8_to_linear(r),
312
+ crate::renderer::srgb_u8_to_linear(g),
313
+ crate::renderer::srgb_u8_to_linear(b),
314
+ (a / 255.0) as f32,
315
+ ];
316
+
317
+ let aw = self.atlas_width as f32;
318
+ let ah = self.atlas_height as f32;
319
+
320
+ let mut cursor_x = x as f32;
321
+ let mut first = true;
322
+ for ch in text.chars() {
323
+ if !first && spacing != 0.0 {
324
+ cursor_x += spacing;
325
+ }
326
+ first = false;
327
+ let key = (idx, ch, phys_size);
328
+ if let Some(glyph) = self.glyph_cache.get(&key) {
329
+ // Physical-res glyph metrics, scaled back to the logical
330
+ // coordinate space the 2D pass works in. The projection's
331
+ // ×dpi stretch then lands the bitmap 1:1 on physical
332
+ // pixels instead of magnifying a logical-res raster.
333
+ let gx = cursor_x + glyph.x_offset * inv_dpi;
334
+ let gy = y as f32
335
+ - (glyph.y_offset + glyph.height as f32) * inv_dpi
336
+ + size as f32;
337
+ let gw = glyph.width as f32 * inv_dpi;
338
+ let gh = glyph.height as f32 * inv_dpi;
339
+
340
+ if gw > 0.0 && gh > 0.0 {
341
+ let u0 = glyph.atlas_x as f32 / aw;
342
+ let v0 = glyph.atlas_y as f32 / ah;
343
+ let u1 = (glyph.atlas_x + glyph.width) as f32 / aw;
344
+ let v1 = (glyph.atlas_y + glyph.height) as f32 / ah;
345
+
346
+ renderer.draw_textured_quad(gx, gy, gw, gh, u0, v0, u1, v1, color, atlas_bg);
347
+ }
348
+
349
+ cursor_x += glyph.advance * inv_dpi;
350
+ }
351
+ }
352
+ }
353
+
354
+ pub fn unload_font(&mut self, font_idx: usize) {
355
+ if font_idx > 0 && font_idx < self.fonts.len() {
356
+ self.glyph_cache.retain(|k, _| k.0 != font_idx);
357
+ self.sdf_glyph_cache.retain(|k, _| k.0 != font_idx);
358
+ }
359
+ }
360
+
361
+ // SDF text rendering: rasterize at a fixed large size, generate SDF, then render at any size
362
+
363
+ const SDF_BASE_SIZE: u32 = 48;
364
+ const SDF_SPREAD: f32 = 6.0;
365
+
366
+ fn rasterize_sdf_glyph(&mut self, font_idx: usize, ch: char) -> &GlyphInfo {
367
+ let key = (font_idx, ch);
368
+ if self.sdf_glyph_cache.contains_key(&key) {
369
+ return &self.sdf_glyph_cache[&key];
370
+ }
371
+
372
+ let font = &self.fonts[font_idx];
373
+ let (metrics, bitmap) = font.rasterize(ch, Self::SDF_BASE_SIZE as f32);
374
+ let gw = metrics.width as u32;
375
+ let gh = metrics.height as u32;
376
+
377
+ // Generate SDF from bitmap
378
+ let sdf_data = if gw > 0 && gh > 0 {
379
+ generate_sdf(&bitmap, gw, gh, Self::SDF_SPREAD)
380
+ } else {
381
+ Vec::new()
382
+ };
383
+
384
+ // Place in SDF atlas
385
+ if self.sdf_atlas_cursor_x + gw > self.atlas_width {
386
+ self.sdf_atlas_cursor_x = 0;
387
+ self.sdf_atlas_cursor_y += self.sdf_atlas_row_height;
388
+ self.sdf_atlas_row_height = 0;
389
+ }
390
+
391
+ let ax = self.sdf_atlas_cursor_x;
392
+ let ay = self.sdf_atlas_cursor_y;
393
+
394
+ for row in 0..gh {
395
+ for col in 0..gw {
396
+ let src = ((row * gw + col) * 4) as usize;
397
+ let dst = (((ay + row) * self.atlas_width + ax + col) * 4) as usize;
398
+ if src + 3 < sdf_data.len() && dst + 3 < self.sdf_atlas_data.len() {
399
+ self.sdf_atlas_data[dst] = sdf_data[src];
400
+ self.sdf_atlas_data[dst + 1] = sdf_data[src + 1];
401
+ self.sdf_atlas_data[dst + 2] = sdf_data[src + 2];
402
+ self.sdf_atlas_data[dst + 3] = sdf_data[src + 3];
403
+ }
404
+ }
405
+ }
406
+
407
+ self.sdf_atlas_cursor_x += gw + 1;
408
+ if gh + 1 > self.sdf_atlas_row_height {
409
+ self.sdf_atlas_row_height = gh + 1;
410
+ }
411
+ self.sdf_atlas_dirty = true;
412
+
413
+ self.sdf_glyph_cache.insert(key, GlyphInfo {
414
+ atlas_x: ax,
415
+ atlas_y: ay,
416
+ width: gw,
417
+ height: gh,
418
+ advance: metrics.advance_width,
419
+ x_offset: metrics.xmin as f32,
420
+ y_offset: metrics.ymin as f32,
421
+ });
422
+
423
+ &self.sdf_glyph_cache[&key]
424
+ }
425
+
426
+ fn ensure_sdf_atlas_uploaded(&mut self, renderer: &mut Renderer) {
427
+ if !self.sdf_atlas_dirty { return; }
428
+ match self.sdf_atlas_bind_group_idx {
429
+ None => {
430
+ let idx = renderer.register_texture(self.atlas_width, self.atlas_height, &self.sdf_atlas_data);
431
+ self.sdf_atlas_bind_group_idx = Some(idx);
432
+ }
433
+ Some(idx) => {
434
+ renderer.update_texture(idx, self.atlas_width, self.atlas_height, &self.sdf_atlas_data);
435
+ }
436
+ }
437
+ self.sdf_atlas_dirty = false;
438
+ }
439
+
440
+ /// Draw text using SDF atlas. The text scales smoothly to any size.
441
+ pub fn draw_text_sdf(
442
+ &mut self,
443
+ renderer: &mut Renderer,
444
+ font_idx: usize,
445
+ text: &str,
446
+ x: f64,
447
+ y: f64,
448
+ size: u32,
449
+ spacing: f32,
450
+ r: f64, g: f64, b: f64, a: f64,
451
+ ) {
452
+ let idx = if font_idx < self.fonts.len() { font_idx } else { 0 };
453
+ let scale = size as f32 / Self::SDF_BASE_SIZE as f32;
454
+
455
+ for ch in text.chars() {
456
+ self.rasterize_sdf_glyph(idx, ch);
457
+ }
458
+ self.ensure_sdf_atlas_uploaded(renderer);
459
+
460
+ let atlas_bg = match self.sdf_atlas_bind_group_idx {
461
+ Some(idx) => idx,
462
+ None => return,
463
+ };
464
+
465
+ // sRGB-decode to linear like every other 2D vertex color — the
466
+ // swapchain view's hardware encode expects linear shader output
467
+ // (see renderer::srgb_u8_to_linear).
468
+ let color = [
469
+ crate::renderer::srgb_u8_to_linear(r),
470
+ crate::renderer::srgb_u8_to_linear(g),
471
+ crate::renderer::srgb_u8_to_linear(b),
472
+ (a / 255.0) as f32,
473
+ ];
474
+
475
+ let aw = self.atlas_width as f32;
476
+ let ah = self.atlas_height as f32;
477
+
478
+ let mut cursor_x = x as f32;
479
+ let mut first = true;
480
+ for ch in text.chars() {
481
+ if !first && spacing != 0.0 { cursor_x += spacing * scale; }
482
+ first = false;
483
+ let key = (idx, ch);
484
+ if let Some(glyph) = self.sdf_glyph_cache.get(&key) {
485
+ let gx = cursor_x + glyph.x_offset * scale;
486
+ let gy = y as f32 - glyph.y_offset * scale - glyph.height as f32 * scale + size as f32;
487
+ let gw = glyph.width as f32 * scale;
488
+ let gh = glyph.height as f32 * scale;
489
+
490
+ if gw > 0.0 && gh > 0.0 {
491
+ let u0 = glyph.atlas_x as f32 / aw;
492
+ let v0 = glyph.atlas_y as f32 / ah;
493
+ let u1 = (glyph.atlas_x + glyph.width) as f32 / aw;
494
+ let v1 = (glyph.atlas_y + glyph.height) as f32 / ah;
495
+ renderer.draw_textured_quad(gx, gy, gw, gh, u0, v0, u1, v1, color, atlas_bg);
496
+ }
497
+
498
+ cursor_x += glyph.advance * scale;
499
+ }
500
+ }
501
+ }
502
+ }
@@ -0,0 +1,197 @@
1
+ use crate::handles::HandleRegistry;
2
+ use crate::renderer::Renderer;
3
+
4
+ pub struct TextureData {
5
+ pub bind_group_idx: u32,
6
+ pub width: u32,
7
+ pub height: u32,
8
+ }
9
+
10
+ pub struct ImageData {
11
+ pub data: Vec<u8>,
12
+ pub width: u32,
13
+ pub height: u32,
14
+ }
15
+
16
+ /// Placeholder for Q1 render texture data. Actual GPU implementation
17
+ /// requires creating a wgpu::Texture with RENDER_ATTACHMENT + TEXTURE_BINDING
18
+ /// usage and wiring it into the renderer's pass management. The FFI surface
19
+ /// is stable; the implementation lands in a focused GPU session.
20
+ pub struct RenderTextureData {
21
+ pub width: u32,
22
+ pub height: u32,
23
+ pub texture_handle: f64, // Points to the corresponding TextureData entry.
24
+ }
25
+
26
+ pub struct TextureManager {
27
+ pub textures: HandleRegistry<TextureData>,
28
+ pub images: HandleRegistry<ImageData>,
29
+ pub render_textures: HandleRegistry<RenderTextureData>,
30
+ }
31
+
32
+ impl TextureManager {
33
+ pub fn new() -> Self {
34
+ Self {
35
+ textures: HandleRegistry::new(),
36
+ images: HandleRegistry::new(),
37
+ render_textures: HandleRegistry::new(),
38
+ }
39
+ }
40
+
41
+ /// Q1: Create a render texture. Returns a handle. The actual wgpu texture
42
+ /// is created by the calling FFI function which has access to the Renderer.
43
+ pub fn load_render_texture(&mut self, width: u32, height: u32) -> f64 {
44
+ self.render_textures.alloc(RenderTextureData {
45
+ width, height, texture_handle: 0.0,
46
+ })
47
+ }
48
+
49
+ /// Set the texture handle for a render texture (called after GPU creation).
50
+ pub fn set_render_texture_handle(&mut self, rt_handle: f64, tex_handle: f64) {
51
+ if let Some(rt) = self.render_textures.get_mut(rt_handle) {
52
+ rt.texture_handle = tex_handle;
53
+ }
54
+ }
55
+
56
+ pub fn unload_render_texture(&mut self, handle: f64) {
57
+ self.render_textures.free(handle);
58
+ }
59
+
60
+ pub fn get_render_texture_texture(&self, handle: f64) -> f64 {
61
+ match self.render_textures.get(handle) {
62
+ Some(rt) => rt.texture_handle,
63
+ None => 0.0,
64
+ }
65
+ }
66
+
67
+ /// Decode an in-memory image (PNG/JPEG/…, whatever `image` is built with)
68
+ /// to raw RGBA8. Shared by the web texture-array-from-files path, which
69
+ /// fetches file bytes in JS and needs the same decode the native
70
+ /// from-files FFI gets from `image::open`.
71
+ pub fn decode_rgba8(file_data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
72
+ let img = image::load_from_memory(file_data).ok()?.to_rgba8();
73
+ let (w, h) = img.dimensions();
74
+ Some((img.into_raw(), w, h))
75
+ }
76
+
77
+ pub fn load_texture(&mut self, renderer: &mut Renderer, file_data: &[u8]) -> f64 {
78
+ // Cooked textures (bloom-cook BC7 DDS) take the compressed path:
79
+ // direct mip-chain upload where the adapter has BC support, CPU
80
+ // decode to RGBA elsewhere. Magic-sniffed so loadTexture() is the
81
+ // one entry point for raw and cooked assets alike.
82
+ if file_data.len() >= 4 && &file_data[..4] == b"DDS " {
83
+ #[cfg(feature = "image-extras")]
84
+ return self.load_texture_dds(renderer, file_data);
85
+ #[cfg(not(feature = "image-extras"))]
86
+ {
87
+ crate::ffi::feature_off_warn_once("loadTexture(.dds)", "image-extras");
88
+ return 0.0;
89
+ }
90
+ }
91
+ let img = match image::load_from_memory(file_data) {
92
+ Ok(img) => img.to_rgba8(),
93
+ Err(_) => return 0.0,
94
+ };
95
+ let width = img.width();
96
+ let height = img.height();
97
+ let data = img.into_raw();
98
+
99
+ let bind_group_idx = renderer.register_texture(width, height, &data);
100
+ self.textures.alloc(TextureData { bind_group_idx, width, height })
101
+ }
102
+
103
+ pub fn unload_texture(&mut self, handle: f64, renderer: &mut Renderer) {
104
+ if let Some(tex) = self.textures.free(handle) {
105
+ renderer.unload_texture(tex.bind_group_idx);
106
+ }
107
+ }
108
+
109
+ pub fn get(&self, handle: f64) -> Option<&TextureData> {
110
+ self.textures.get(handle)
111
+ }
112
+
113
+ pub fn load_image(&mut self, file_data: &[u8]) -> f64 {
114
+ let img = match image::load_from_memory(file_data) {
115
+ Ok(img) => img.to_rgba8(),
116
+ Err(_) => return 0.0,
117
+ };
118
+ let width = img.width();
119
+ let height = img.height();
120
+ let data = img.into_raw();
121
+
122
+ self.images.alloc(ImageData { data, width, height })
123
+ }
124
+
125
+ pub fn image_resize(&mut self, handle: f64, new_w: u32, new_h: u32) {
126
+ if let Some(img_data) = self.images.get_mut(handle) {
127
+ let src = image::RgbaImage::from_raw(img_data.width, img_data.height, std::mem::take(&mut img_data.data));
128
+ if let Some(src) = src {
129
+ let resized = image::imageops::resize(&src, new_w, new_h, image::imageops::FilterType::Triangle);
130
+ img_data.width = new_w;
131
+ img_data.height = new_h;
132
+ img_data.data = resized.into_raw();
133
+ }
134
+ }
135
+ }
136
+
137
+ pub fn image_crop(&mut self, handle: f64, x: u32, y: u32, w: u32, h: u32) {
138
+ if let Some(img_data) = self.images.get_mut(handle) {
139
+ let src = image::RgbaImage::from_raw(img_data.width, img_data.height, std::mem::take(&mut img_data.data));
140
+ if let Some(mut src) = src {
141
+ let cropped = image::imageops::crop(&mut src, x, y, w, h).to_image();
142
+ img_data.width = w;
143
+ img_data.height = h;
144
+ img_data.data = cropped.into_raw();
145
+ }
146
+ }
147
+ }
148
+
149
+ pub fn image_flip_h(&mut self, handle: f64) {
150
+ if let Some(img_data) = self.images.get_mut(handle) {
151
+ let src = image::RgbaImage::from_raw(img_data.width, img_data.height, std::mem::take(&mut img_data.data));
152
+ if let Some(src) = src {
153
+ let flipped = image::imageops::flip_horizontal(&src);
154
+ img_data.data = flipped.into_raw();
155
+ }
156
+ }
157
+ }
158
+
159
+ pub fn image_flip_v(&mut self, handle: f64) {
160
+ if let Some(img_data) = self.images.get_mut(handle) {
161
+ let src = image::RgbaImage::from_raw(img_data.width, img_data.height, std::mem::take(&mut img_data.data));
162
+ if let Some(src) = src {
163
+ let flipped = image::imageops::flip_vertical(&src);
164
+ img_data.data = flipped.into_raw();
165
+ }
166
+ }
167
+ }
168
+
169
+ #[cfg(feature = "image-extras")]
170
+ fn load_texture_dds(&mut self, renderer: &mut Renderer, file_data: &[u8]) -> f64 {
171
+ let Ok(dds) = image_dds::ddsfile::Dds::read(std::io::Cursor::new(file_data)) else {
172
+ return 0.0;
173
+ };
174
+ let (width, height) = (dds.get_width(), dds.get_height());
175
+ if let Some(bind_group_idx) = renderer.register_texture_dds(&dds) {
176
+ return self.textures.alloc(TextureData { bind_group_idx, width, height });
177
+ }
178
+ // No BC support on this adapter (mobile GL): CPU-decode the top
179
+ // mip and feed the regular RGBA path (which regenerates mips).
180
+ match image_dds::image_from_dds(&dds, 0) {
181
+ Ok(rgba) => {
182
+ let bind_group_idx = renderer.register_texture(width, height, rgba.as_raw());
183
+ self.textures.alloc(TextureData { bind_group_idx, width, height })
184
+ }
185
+ Err(_) => 0.0,
186
+ }
187
+ }
188
+
189
+ pub fn load_texture_from_image(&mut self, handle: f64, renderer: &mut Renderer) -> f64 {
190
+ if let Some(img_data) = self.images.get(handle) {
191
+ let bind_group_idx = renderer.register_texture(img_data.width, img_data.height, &img_data.data);
192
+ self.textures.alloc(TextureData { bind_group_idx, width: img_data.width, height: img_data.height })
193
+ } else {
194
+ 0.0
195
+ }
196
+ }
197
+ }