@bornengine/engine 0.4.16 → 0.5.0

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 (62) hide show
  1. package/README.md +24 -14
  2. package/native/android/Cargo.lock +577 -28
  3. package/native/android/Cargo.toml +2 -0
  4. package/native/android/src/lib.rs +39 -3
  5. package/native/ios/Cargo.lock +573 -24
  6. package/native/ios/Cargo.toml +2 -0
  7. package/native/ios/src/lib.rs +72 -9
  8. package/native/ios/src/text_input.rs +48 -0
  9. package/native/linux/Cargo.lock +623 -35
  10. package/native/linux/Cargo.toml +3 -0
  11. package/native/macos/Cargo.lock +630 -35
  12. package/native/macos/Cargo.toml +3 -0
  13. package/native/shared/Cargo.lock +430 -27
  14. package/native/shared/Cargo.toml +5 -0
  15. package/native/shared/build.rs +41 -1
  16. package/native/shared/src/engine.rs +54 -2
  17. package/native/shared/src/ffi.rs +54 -1
  18. package/native/shared/src/ffi_core/mod.rs +2 -0
  19. package/native/shared/src/ffi_core/ui.rs +182 -0
  20. package/native/shared/src/input.rs +317 -12
  21. package/native/shared/src/lib.rs +1 -0
  22. package/native/shared/src/renderer/mod.rs +47 -201
  23. package/native/shared/src/renderer/texture_store.rs +5 -0
  24. package/native/shared/src/renderer/ui_pass.rs +742 -0
  25. package/native/shared/src/textures.rs +2 -0
  26. package/native/shared/src/ui/commands.rs +213 -0
  27. package/native/shared/src/ui/egui.rs +1282 -0
  28. package/native/shared/src/ui/imgui.rs +1056 -0
  29. package/native/shared/src/ui/input.rs +173 -0
  30. package/native/shared/src/ui/mod.rs +252 -0
  31. package/native/shared/src/ui/responses.rs +61 -0
  32. package/native/tvos/Cargo.lock +576 -27
  33. package/native/tvos/Cargo.toml +2 -0
  34. package/native/visionos/Cargo.lock +571 -22
  35. package/native/visionos/Cargo.toml +2 -0
  36. package/native/watchos/src/ffi_stubs.rs +27 -0
  37. package/native/web/Cargo.lock +346 -11
  38. package/native/web/Cargo.toml +6 -0
  39. package/native/web/bloom_glue.js +84 -11
  40. package/native/web/build.sh +150 -34
  41. package/native/web/src/lib.rs +2 -0
  42. package/native/web/src/ui_ffi.rs +148 -0
  43. package/native/windows/Cargo.lock +638 -38
  44. package/native/windows/Cargo.toml +3 -0
  45. package/native/windows/src/lib.rs +50 -1
  46. package/native/windows/src/windows_text.rs +69 -0
  47. package/package.json +91 -2
  48. package/src/debug-ui/index.ts +4 -0
  49. package/src/debug-ui/types.ts +1 -0
  50. package/src/game/adapters/audio-source-component.ts +67 -0
  51. package/src/game/adapters/rigid-body-component.ts +69 -0
  52. package/src/game/adapters/scene-node-component.ts +49 -0
  53. package/src/game/game-component.ts +89 -0
  54. package/src/game/game-object.ts +338 -0
  55. package/src/game/game-scene.ts +303 -0
  56. package/src/game/index.ts +7 -0
  57. package/src/game/transform.ts +315 -0
  58. package/src/index.ts +27 -0
  59. package/src/ui/api.ts +216 -0
  60. package/src/ui/index.ts +5 -0
  61. package/src/ui/opcodes.ts +66 -0
  62. package/src/ui/types.ts +82 -0
@@ -0,0 +1,742 @@
1
+ use std::collections::HashMap;
2
+
3
+ use super::*;
4
+
5
+ pub(super) struct UiRendererState {
6
+ egui_renderer: egui_wgpu::Renderer,
7
+ pending_egui_frame: Option<(crate::ui::EguiFrameOutput, HashMap<u64, u32>)>,
8
+ egui_texture_ids: HashMap<u64, (u32, egui::TextureId)>,
9
+ egui_pending_frees: Vec<egui::TextureId>,
10
+ #[cfg(feature = "debug-ui")]
11
+ dear_imgui_renderer: Option<imgui_wgpu::Renderer>,
12
+ #[cfg(feature = "debug-ui")]
13
+ dear_imgui_textures: HashMap<u64, (u32, imgui::TextureId)>,
14
+ }
15
+
16
+ impl UiRendererState {
17
+ pub(super) fn new(device: &wgpu::Device, output_format: wgpu::TextureFormat) -> Self {
18
+ Self {
19
+ egui_renderer: egui_wgpu::Renderer::new(
20
+ device,
21
+ output_format,
22
+ egui_wgpu::RendererOptions::default(),
23
+ ),
24
+ pending_egui_frame: None,
25
+ egui_texture_ids: HashMap::new(),
26
+ egui_pending_frees: Vec::new(),
27
+ #[cfg(feature = "debug-ui")]
28
+ dear_imgui_renderer: None,
29
+ #[cfg(feature = "debug-ui")]
30
+ dear_imgui_textures: HashMap::new(),
31
+ }
32
+ }
33
+ }
34
+
35
+ fn should_render_ui(is_render_texture: bool) -> bool {
36
+ !is_render_texture
37
+ }
38
+
39
+ #[cfg(feature = "debug-ui")]
40
+ fn imgui_texture_from_native(
41
+ device: &wgpu::Device,
42
+ renderer: &imgui_wgpu::Renderer,
43
+ texture: &wgpu::Texture,
44
+ width: u32,
45
+ height: u32,
46
+ ) -> imgui_wgpu::Texture {
47
+ use std::sync::Arc;
48
+
49
+ let texture = Arc::new(texture.clone());
50
+ let view = Arc::new(texture.create_view(&wgpu::TextureViewDescriptor::default()));
51
+ let sampler_desc = wgpu::SamplerDescriptor {
52
+ label: Some("bornengine_imgui_texture_sampler"),
53
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
54
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
55
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
56
+ mag_filter: wgpu::FilterMode::Linear,
57
+ min_filter: wgpu::FilterMode::Linear,
58
+ mipmap_filter: wgpu::MipmapFilterMode::Linear,
59
+ ..Default::default()
60
+ };
61
+ let config = imgui_wgpu::RawTextureConfig {
62
+ label: Some("bornengine_imgui_native_texture"),
63
+ sampler_desc,
64
+ };
65
+ imgui_wgpu::Texture::from_raw_parts(
66
+ device,
67
+ renderer,
68
+ texture,
69
+ view,
70
+ None,
71
+ Some(&config),
72
+ wgpu::Extent3d {
73
+ width,
74
+ height,
75
+ depth_or_array_layers: 1,
76
+ },
77
+ )
78
+ }
79
+
80
+ impl Renderer {
81
+ pub fn set_egui_frame(
82
+ &mut self,
83
+ frame: crate::ui::EguiFrameOutput,
84
+ native_texture_indices: HashMap<u64, u32>,
85
+ ) {
86
+ self.ui_renderer.pending_egui_frame = Some((frame, native_texture_indices));
87
+ }
88
+
89
+ #[cfg(feature = "debug-ui")]
90
+ pub(crate) fn init_dear_imgui(&mut self) {
91
+ // Headless renderers never submit the Dear ImGui surface pass. Avoid
92
+ // creating a process-global ImGui context for offscreen renderers,
93
+ // which can be constructed concurrently by tests or server tools.
94
+ if self.surface.is_none() {
95
+ return;
96
+ }
97
+ crate::ui::with_dear_imgui(|ui| {
98
+ self.ui_renderer.dear_imgui_renderer = Some(imgui_wgpu::Renderer::new(
99
+ ui.context_mut(),
100
+ &self.device,
101
+ &self.queue,
102
+ imgui_wgpu::RendererConfig {
103
+ texture_format: self.output_format,
104
+ ..Default::default()
105
+ },
106
+ ));
107
+ });
108
+ }
109
+
110
+ pub fn unload_egui_texture(&mut self, engine_handle: u64) {
111
+ if let Some((_, texture_id)) = self.ui_renderer.egui_texture_ids.remove(&engine_handle) {
112
+ self.ui_renderer.egui_renderer.free_texture(&texture_id);
113
+ }
114
+ }
115
+
116
+ pub(crate) fn refresh_egui_texture_index(&mut self, texture_index: u32) {
117
+ let affected: Vec<_> = self
118
+ .ui_renderer
119
+ .egui_texture_ids
120
+ .iter()
121
+ .filter_map(|(&handle, &(index, texture_id))| {
122
+ (index == texture_index).then_some((handle, texture_id))
123
+ })
124
+ .collect();
125
+ if affected.is_empty() {
126
+ return;
127
+ }
128
+ let index = texture_index as usize;
129
+ let Some(texture) = self.textures.get(index) else {
130
+ return;
131
+ };
132
+ let size = self.texture_sizes.get(index).copied().unwrap_or_default();
133
+ for (handle, texture_id) in affected {
134
+ if size.0 == 0 || size.1 == 0 {
135
+ self.ui_renderer.egui_renderer.free_texture(&texture_id);
136
+ self.ui_renderer.egui_texture_ids.remove(&handle);
137
+ continue;
138
+ }
139
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
140
+ self.ui_renderer
141
+ .egui_renderer
142
+ .update_egui_texture_from_wgpu_texture(
143
+ &self.device,
144
+ &view,
145
+ wgpu::FilterMode::Linear,
146
+ texture_id,
147
+ );
148
+ }
149
+ }
150
+
151
+ pub fn unload_imgui_texture(&mut self, engine_handle: u64) {
152
+ #[cfg(feature = "debug-ui")]
153
+ if let (Some(renderer), Some((_, texture_id))) = (
154
+ self.ui_renderer.dear_imgui_renderer.as_mut(),
155
+ self.ui_renderer.dear_imgui_textures.remove(&engine_handle),
156
+ ) {
157
+ renderer.textures.remove(texture_id);
158
+ }
159
+ #[cfg(not(feature = "debug-ui"))]
160
+ let _ = engine_handle;
161
+ }
162
+
163
+ pub(super) fn encode_egui_pass(
164
+ &mut self,
165
+ encoder: &mut wgpu::CommandEncoder,
166
+ target: &wgpu::TextureView,
167
+ is_render_texture: bool,
168
+ ) -> Vec<wgpu::CommandBuffer> {
169
+ let Some((frame, native_texture_indices)) = self.ui_renderer.pending_egui_frame.take()
170
+ else {
171
+ return Vec::new();
172
+ };
173
+
174
+ for (texture_id, delta) in &frame.textures_delta.set {
175
+ self.ui_renderer.egui_renderer.update_texture(
176
+ &self.device,
177
+ &self.queue,
178
+ *texture_id,
179
+ delta,
180
+ );
181
+ }
182
+
183
+ for (engine_handle, texture_index) in native_texture_indices {
184
+ let Some(texture) = self.textures.get(texture_index as usize) else {
185
+ continue;
186
+ };
187
+ let Some(size) = self.texture_sizes.get(texture_index as usize) else {
188
+ continue;
189
+ };
190
+ if size.0 == 0 || size.1 == 0 {
191
+ continue;
192
+ }
193
+
194
+ let Some(existing) = self
195
+ .ui_renderer
196
+ .egui_texture_ids
197
+ .get(&engine_handle)
198
+ .copied()
199
+ else {
200
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
201
+ let texture_id = self.ui_renderer.egui_renderer.register_native_texture(
202
+ &self.device,
203
+ &view,
204
+ wgpu::FilterMode::Linear,
205
+ );
206
+ self.ui_renderer
207
+ .egui_texture_ids
208
+ .insert(engine_handle, (texture_index, texture_id));
209
+ continue;
210
+ };
211
+
212
+ if existing.0 != texture_index {
213
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
214
+ self.ui_renderer
215
+ .egui_renderer
216
+ .update_egui_texture_from_wgpu_texture(
217
+ &self.device,
218
+ &view,
219
+ wgpu::FilterMode::Linear,
220
+ existing.1,
221
+ );
222
+ self.ui_renderer
223
+ .egui_texture_ids
224
+ .insert(engine_handle, (texture_index, existing.1));
225
+ }
226
+ }
227
+
228
+ let can_paint = should_render_ui(is_render_texture) && self.surface.is_some();
229
+ let mut user_command_buffers = Vec::new();
230
+ if can_paint {
231
+ let mut paint_jobs = frame.paint_jobs;
232
+ for clipped in &mut paint_jobs {
233
+ if let egui::epaint::Primitive::Mesh(mesh) = &mut clipped.primitive {
234
+ if let egui::TextureId::User(engine_handle) = mesh.texture_id {
235
+ if let Some((_, texture_id)) =
236
+ self.ui_renderer.egui_texture_ids.get(&engine_handle)
237
+ {
238
+ mesh.texture_id = *texture_id;
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ let pixels_per_point = frame.pixels_per_point.max(0.1);
245
+ let screen_descriptor = egui_wgpu::ScreenDescriptor {
246
+ size_in_pixels: [self.surface_config.width, self.surface_config.height],
247
+ pixels_per_point,
248
+ };
249
+ user_command_buffers = self.ui_renderer.egui_renderer.update_buffers(
250
+ &self.device,
251
+ &self.queue,
252
+ encoder,
253
+ &paint_jobs,
254
+ &screen_descriptor,
255
+ );
256
+
257
+ if !paint_jobs.is_empty() {
258
+ let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
259
+ label: Some("egui_ui_pass"),
260
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
261
+ view: target,
262
+ resolve_target: None,
263
+ depth_slice: None,
264
+ ops: wgpu::Operations {
265
+ load: wgpu::LoadOp::Load,
266
+ store: wgpu::StoreOp::Store,
267
+ },
268
+ })],
269
+ depth_stencil_attachment: None,
270
+ timestamp_writes: None,
271
+ occlusion_query_set: None,
272
+ multiview_mask: None,
273
+ });
274
+ self.ui_renderer.egui_renderer.render(
275
+ &mut pass.forget_lifetime(),
276
+ &paint_jobs,
277
+ &screen_descriptor,
278
+ );
279
+ }
280
+ }
281
+
282
+ self.ui_renderer
283
+ .egui_pending_frees
284
+ .extend(frame.textures_delta.free.iter().copied());
285
+ user_command_buffers
286
+ }
287
+
288
+ pub(super) fn free_pending_egui_textures(&mut self) {
289
+ for texture_id in self.ui_renderer.egui_pending_frees.drain(..) {
290
+ self.ui_renderer.egui_renderer.free_texture(&texture_id);
291
+ }
292
+ }
293
+
294
+ #[cfg(feature = "debug-ui")]
295
+ fn sync_dear_imgui_textures(
296
+ &mut self,
297
+ native_texture_indices: &HashMap<u64, u32>,
298
+ ) -> HashMap<u64, (imgui::TextureId, [f32; 2])> {
299
+ let mut texture_ids = HashMap::new();
300
+ let Some(renderer) = self.ui_renderer.dear_imgui_renderer.as_mut() else {
301
+ return texture_ids;
302
+ };
303
+ for (&engine_handle, &texture_index) in native_texture_indices {
304
+ let index = texture_index as usize;
305
+ let Some(texture) = self.textures.get(index) else {
306
+ continue;
307
+ };
308
+ let Some(&(width, height)) = self.texture_sizes.get(index) else {
309
+ continue;
310
+ };
311
+ if width == 0 || height == 0 {
312
+ continue;
313
+ }
314
+ if let Some((cached_index, texture_id)) = self
315
+ .ui_renderer
316
+ .dear_imgui_textures
317
+ .get(&engine_handle)
318
+ .copied()
319
+ {
320
+ if cached_index == texture_index {
321
+ continue;
322
+ }
323
+ renderer.textures.remove(texture_id);
324
+ }
325
+
326
+ let imgui_texture =
327
+ imgui_texture_from_native(&self.device, renderer, texture, width, height);
328
+ let texture_id = renderer.textures.insert(imgui_texture);
329
+ self.ui_renderer
330
+ .dear_imgui_textures
331
+ .insert(engine_handle, (texture_index, texture_id));
332
+ }
333
+ texture_ids.extend(self.ui_renderer.dear_imgui_textures.iter().filter_map(
334
+ |(&handle, &(index, texture_id))| {
335
+ let &(width, height) = self.texture_sizes.get(index as usize)?;
336
+ (width > 0 && height > 0)
337
+ .then_some((handle, (texture_id, [width as f32, height as f32])))
338
+ },
339
+ ));
340
+ texture_ids
341
+ }
342
+
343
+ #[cfg(feature = "debug-ui")]
344
+ pub(crate) fn refresh_dear_imgui_texture_index(&mut self, texture_index: u32) {
345
+ let Some(renderer) = self.ui_renderer.dear_imgui_renderer.as_mut() else {
346
+ return;
347
+ };
348
+ let affected: Vec<_> = self
349
+ .ui_renderer
350
+ .dear_imgui_textures
351
+ .iter()
352
+ .filter_map(|(&handle, &(index, texture_id))| {
353
+ (index == texture_index).then_some((handle, texture_id))
354
+ })
355
+ .collect();
356
+ let index = texture_index as usize;
357
+ let Some(texture) = self.textures.get(index) else {
358
+ return;
359
+ };
360
+ let size = self.texture_sizes.get(index).copied().unwrap_or_default();
361
+ for (handle, texture_id) in affected {
362
+ if size.0 == 0 || size.1 == 0 {
363
+ renderer.textures.remove(texture_id);
364
+ self.ui_renderer.dear_imgui_textures.remove(&handle);
365
+ continue;
366
+ }
367
+ let imgui_texture =
368
+ imgui_texture_from_native(&self.device, renderer, texture, size.0, size.1);
369
+ renderer.textures.replace(texture_id, imgui_texture);
370
+ }
371
+ }
372
+
373
+ #[cfg(feature = "debug-ui")]
374
+ pub(super) fn encode_dear_imgui_pass(
375
+ &mut self,
376
+ encoder: &mut wgpu::CommandEncoder,
377
+ target: &wgpu::TextureView,
378
+ is_render_texture: bool,
379
+ ui_system: &mut crate::ui::UiSystem,
380
+ dt: f64,
381
+ native_texture_indices: &HashMap<u64, u32>,
382
+ ) {
383
+ if is_render_texture || self.surface.is_none() {
384
+ return;
385
+ }
386
+ if self.ui_renderer.dear_imgui_renderer.is_none() {
387
+ return;
388
+ }
389
+
390
+ let commands = ui_system.take_commands(crate::ui::UiBackend::DearImGui);
391
+ let texture_ids = self.sync_dear_imgui_textures(native_texture_indices);
392
+ let input = ui_system.input_snapshot().clone();
393
+ let display_size = [self.width().max(1) as f32, self.height().max(1) as f32];
394
+ let framebuffer_scale = self.physical_width() as f32 / display_size[0];
395
+ let (responses, capture) = crate::ui::with_dear_imgui(|ui| {
396
+ if ui.register_fonts(&commands) {
397
+ if let Some(renderer) = self.ui_renderer.dear_imgui_renderer.as_mut() {
398
+ renderer.reload_font_texture(ui.context_mut(), &self.device, &self.queue);
399
+ }
400
+ }
401
+ let draw_data = ui.prepare_frame(
402
+ &commands,
403
+ &input,
404
+ display_size,
405
+ framebuffer_scale,
406
+ dt,
407
+ &texture_ids,
408
+ );
409
+ if draw_data.total_vtx_count > 0 {
410
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
411
+ label: Some("dear_imgui_debug_ui_pass"),
412
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
413
+ view: target,
414
+ resolve_target: None,
415
+ depth_slice: None,
416
+ ops: wgpu::Operations {
417
+ load: wgpu::LoadOp::Load,
418
+ store: wgpu::StoreOp::Store,
419
+ },
420
+ })],
421
+ depth_stencil_attachment: None,
422
+ timestamp_writes: None,
423
+ occlusion_query_set: None,
424
+ multiview_mask: None,
425
+ });
426
+ if let Some(renderer) = self.ui_renderer.dear_imgui_renderer.as_mut() {
427
+ if let Err(error) =
428
+ renderer.render(draw_data, &self.queue, &self.device, &mut pass)
429
+ {
430
+ eprintln!("bloom: Dear ImGui rendering failed: {error}");
431
+ }
432
+ }
433
+ }
434
+ ui.take_frame_readback()
435
+ });
436
+ ui_system.publish_completed_responses(crate::ui::UiBackend::DearImGui, responses);
437
+ ui_system.set_capture(crate::ui::UiBackend::DearImGui, capture);
438
+ }
439
+ }
440
+
441
+ #[cfg(test)]
442
+ mod tests {
443
+ #[test]
444
+ fn ui_paint_is_skipped_for_render_texture_frames() {
445
+ assert!(super::should_render_ui(false));
446
+ assert!(!super::should_render_ui(true));
447
+ }
448
+ }
449
+
450
+ impl Renderer {
451
+ pub fn end_frame(&mut self) {
452
+ self.end_frame_internal(None, 1.0 / 60.0, HashMap::new());
453
+ }
454
+
455
+ pub fn end_frame_with_ui(
456
+ &mut self,
457
+ ui: &mut crate::ui::UiSystem,
458
+ dt: f64,
459
+ native_texture_indices: HashMap<u64, u32>,
460
+ ) {
461
+ self.end_frame_internal(Some(ui), dt, native_texture_indices);
462
+ }
463
+
464
+ #[allow(unused_variables)]
465
+ fn end_frame_internal(
466
+ &mut self,
467
+ ui: Option<&mut crate::ui::UiSystem>,
468
+ dt: f64,
469
+ native_texture_indices: HashMap<u64, u32>,
470
+ ) {
471
+ if !self.material_per_view_bg_live {
472
+ self.refresh_material_per_view_bg();
473
+ }
474
+ // Flush pending joint matrices to GPU right before rendering
475
+ self.flush_joint_matrices();
476
+ // One pooled upload for every cached-model draw's uniforms.
477
+ self.flush_model_uniforms();
478
+
479
+ // Q1: If rendering to a texture, use the RT view. Otherwise use the surface.
480
+ // We take ownership of the RT views (via Option::take) to avoid holding a
481
+ // borrow on `self` while the rest of end_frame mutates it.
482
+ let rt_color = self.rt_color_view.take();
483
+ let rt_depth = self.rt_depth_view.take();
484
+ let using_rt = rt_color.is_some();
485
+
486
+ let surface_output = if using_rt {
487
+ None
488
+ } else {
489
+ match self.acquire_frame() {
490
+ Some(t) => Some(t),
491
+ None => {
492
+ // Swapchain lost+reconfigured. Restore RT views if set.
493
+ self.rt_color_view = rt_color;
494
+ self.rt_depth_view = rt_depth;
495
+ return;
496
+ }
497
+ }
498
+ };
499
+
500
+ let view: wgpu::TextureView;
501
+ let owned_depth_view: wgpu::TextureView;
502
+
503
+ if let Some(ref rt_view) = rt_color {
504
+ view = rt_view.clone();
505
+ owned_depth_view = rt_depth.as_ref().unwrap().clone();
506
+ } else {
507
+ view = self
508
+ .frame_texture(surface_output.as_ref().unwrap())
509
+ .create_view(&wgpu::TextureViewDescriptor {
510
+ format: Some(self.output_format),
511
+ ..Default::default()
512
+ });
513
+ owned_depth_view = self
514
+ .depth_texture
515
+ .create_view(&wgpu::TextureViewDescriptor::default());
516
+ }
517
+
518
+ // Restore RT views so they persist across frames.
519
+ self.rt_color_view = rt_color;
520
+ self.rt_depth_view = rt_depth;
521
+
522
+ let mut encoder = self
523
+ .device
524
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
525
+ label: Some("bloom_encoder"),
526
+ });
527
+
528
+ // Upload 2D data to persistent GPU buffers
529
+ let has_2d = !self.vertices_2d.is_empty();
530
+ if has_2d {
531
+ let vb_size = std::mem::size_of_val(self.vertices_2d.as_slice());
532
+ let ib_size = std::mem::size_of_val(self.indices_2d.as_slice());
533
+ self.ensure_buffer_capacity_2d(vb_size, ib_size);
534
+ self.queue.write_buffer(
535
+ &self.persistent_vb_2d,
536
+ 0,
537
+ bytemuck::cast_slice(&self.vertices_2d),
538
+ );
539
+ self.queue.write_buffer(
540
+ &self.persistent_ib_2d,
541
+ 0,
542
+ bytemuck::cast_slice(&self.indices_2d),
543
+ );
544
+ }
545
+
546
+ // Upload 3D data to persistent GPU buffers
547
+ let has_3d = !self.vertices_3d.is_empty();
548
+ if has_3d {
549
+ let vb_size = std::mem::size_of_val(self.vertices_3d.as_slice());
550
+ let ib_size = std::mem::size_of_val(self.indices_3d.as_slice());
551
+ self.ensure_buffer_capacity_3d(vb_size, ib_size);
552
+ self.queue.write_buffer(
553
+ &self.persistent_vb_3d,
554
+ 0,
555
+ bytemuck::cast_slice(&self.vertices_3d),
556
+ );
557
+ self.queue.write_buffer(
558
+ &self.persistent_ib_3d,
559
+ 0,
560
+ bytemuck::cast_slice(&self.indices_3d),
561
+ );
562
+ }
563
+
564
+ {
565
+ // Only attach a depth target when we're drawing 3D. pipeline_2d is
566
+ // depth-less; on some mobile Vulkan drivers (Adreno) pairing a
567
+ // depth-less pipeline with a pass that carries a depth attachment
568
+ // discards all draws silently. Matches the overlay_2d pass in
569
+ // end_frame_with_scene, which also omits depth.
570
+ let depth_attachment = if has_3d {
571
+ Some(wgpu::RenderPassDepthStencilAttachment {
572
+ view: &owned_depth_view,
573
+ depth_ops: Some(wgpu::Operations {
574
+ load: wgpu::LoadOp::Clear(1.0),
575
+ store: wgpu::StoreOp::Store,
576
+ }),
577
+ stencil_ops: None,
578
+ })
579
+ } else {
580
+ None
581
+ };
582
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
583
+ label: Some("bloom_pass"),
584
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
585
+ view: &view,
586
+ resolve_target: None,
587
+ depth_slice: None,
588
+ ops: wgpu::Operations {
589
+ load: wgpu::LoadOp::Clear(self.clear_color),
590
+ store: wgpu::StoreOp::Store,
591
+ },
592
+ })],
593
+ depth_stencil_attachment: depth_attachment,
594
+ timestamp_writes: None,
595
+ occlusion_query_set: None,
596
+ multiview_mask: None,
597
+ });
598
+
599
+ // Draw 3D geometry first (with depth testing), batched by texture
600
+ if has_3d {
601
+ pass.set_pipeline(&self.pipeline_3d);
602
+ pass.set_bind_group(0, &self.uniform_bind_group_3d, &[]);
603
+ pass.set_bind_group(1, &self.lighting_bind_group, &[]);
604
+ pass.set_bind_group(3, &self.joint_bind_group, &[]);
605
+ pass.set_vertex_buffer(0, self.persistent_vb_3d.slice(..));
606
+ pass.set_index_buffer(self.persistent_ib_3d.slice(..), wgpu::IndexFormat::Uint32);
607
+
608
+ if self.draw_calls_3d.is_empty() {
609
+ // No draw calls tracked — draw all with white texture (backward compat)
610
+ pass.set_bind_group(2, &self.texture_bind_groups[0], &[]);
611
+ pass.draw_indexed(0..self.indices_3d.len() as u32, 0, 0..1);
612
+ } else {
613
+ let num_calls = self.draw_calls_3d.len();
614
+ for i in 0..num_calls {
615
+ let call = &self.draw_calls_3d[i];
616
+ let next_start = if i + 1 < num_calls {
617
+ self.draw_calls_3d[i + 1].index_start
618
+ } else {
619
+ self.indices_3d.len() as u32
620
+ };
621
+ let count = next_start - call.index_start;
622
+ if count == 0 {
623
+ continue;
624
+ }
625
+ let tex_idx = call.texture_idx as usize;
626
+ if tex_idx < self.texture_bind_groups.len() {
627
+ pass.set_bind_group(2, &self.texture_bind_groups[tex_idx], &[]);
628
+ } else {
629
+ pass.set_bind_group(2, &self.texture_bind_groups[0], &[]);
630
+ }
631
+ pass.draw_indexed(call.index_start..next_start, 0, 0..1);
632
+ }
633
+ }
634
+ }
635
+
636
+ // Draw cached models (static models with GPU-resident buffers).
637
+ // Use the scene pipeline so PBR-style material bindings (base
638
+ // color + normal map) apply — drawModel should behave the same
639
+ // as attachModelToNode for PBR purposes.
640
+ if !self.model_draw_commands.is_empty() {
641
+ pass.set_pipeline(&self.scene_pipeline);
642
+ pass.set_bind_group(1, &self.lighting_bind_group, &[]);
643
+ pass.set_bind_group(3, &self.joint_bind_group, &[]);
644
+
645
+ for cmd in &self.model_draw_commands {
646
+ if let Some(Some(meshes)) = self.model_gpu_cache.get(&cmd.cache_handle) {
647
+ if cmd.mesh_idx < meshes.len() {
648
+ let mesh = &meshes[cmd.mesh_idx];
649
+ pass.set_bind_group(
650
+ 0,
651
+ &self.model_uniform_bind_groups[cmd.uniform_slot],
652
+ &[],
653
+ );
654
+ pass.set_bind_group(2, &mesh.material_bg, &[]);
655
+ pass.set_vertex_buffer(0, mesh.vb.slice(..));
656
+ pass.set_index_buffer(mesh.ib.slice(..), wgpu::IndexFormat::Uint32);
657
+ pass.draw_indexed(0..mesh.index_count, 0, 0..1);
658
+ }
659
+ }
660
+ }
661
+ }
662
+
663
+ // Draw 2D geometry (no depth testing, always passes)
664
+ if has_2d {
665
+ pass.set_pipeline(&self.pipeline_2d);
666
+ pass.set_vertex_buffer(0, self.persistent_vb_2d.slice(..));
667
+ pass.set_index_buffer(self.persistent_ib_2d.slice(..), wgpu::IndexFormat::Uint32);
668
+
669
+ let num_calls = self.draw_calls_2d.len();
670
+ for i in 0..num_calls {
671
+ let call = &self.draw_calls_2d[i];
672
+ let next_start = if i + 1 < num_calls {
673
+ self.draw_calls_2d[i + 1].index_start
674
+ } else {
675
+ self.indices_2d.len() as u32
676
+ };
677
+ let count = next_start - call.index_start;
678
+ if count == 0 {
679
+ continue;
680
+ }
681
+
682
+ pass.set_bind_group(
683
+ 0,
684
+ &self.uniform_bind_groups[call.uniform_idx as usize],
685
+ &[],
686
+ );
687
+ if (call.texture_idx as usize) < self.texture_bind_groups.len() {
688
+ pass.set_bind_group(
689
+ 1,
690
+ &self.texture_bind_groups[call.texture_idx as usize],
691
+ &[],
692
+ );
693
+ }
694
+ pass.draw_indexed(call.index_start..next_start, 0, 0..1);
695
+ }
696
+ }
697
+ }
698
+
699
+ let ui_command_buffers =
700
+ self.encode_egui_pass(&mut encoder, &view, using_rt || self.surface.is_none());
701
+ #[cfg(feature = "debug-ui")]
702
+ if let Some(ui) = ui {
703
+ self.encode_dear_imgui_pass(
704
+ &mut encoder,
705
+ &view,
706
+ using_rt || self.surface.is_none(),
707
+ ui,
708
+ dt,
709
+ &native_texture_indices,
710
+ );
711
+ }
712
+ self.queue.submit(
713
+ ui_command_buffers
714
+ .into_iter()
715
+ .chain(std::iter::once(encoder.finish())),
716
+ );
717
+ self.free_pending_egui_textures();
718
+ if let Some(out) = surface_output {
719
+ self.present_frame(out);
720
+ }
721
+ }
722
+
723
+ /// Like end_frame, but also renders retained scene graph nodes.
724
+ pub fn end_frame_with_scene(
725
+ &mut self,
726
+ scene: &mut crate::scene::SceneGraph,
727
+ profiler: &mut crate::profiler::Profiler,
728
+ ) {
729
+ self.end_frame_with_scene_internal(scene, profiler, None, 1.0 / 60.0, HashMap::new());
730
+ }
731
+
732
+ pub fn end_frame_with_scene_ui(
733
+ &mut self,
734
+ scene: &mut crate::scene::SceneGraph,
735
+ profiler: &mut crate::profiler::Profiler,
736
+ ui: &mut crate::ui::UiSystem,
737
+ dt: f64,
738
+ native_texture_indices: HashMap<u64, u32>,
739
+ ) {
740
+ self.end_frame_with_scene_internal(scene, profiler, Some(ui), dt, native_texture_indices);
741
+ }
742
+ }