1#[cfg(feature = "ui")]
3pub mod app_event;
4pub mod app_trait;
5pub mod atlas;
6pub mod bbox2d;
7pub mod camera3d;
8pub mod chunk;
9pub mod dynamic;
10pub mod intodata;
11pub mod light;
12#[cfg(all(feature = "ui", not(target_arch = "wasm32")))]
13pub mod native_dialogs;
14pub mod poly2d;
15pub mod poly3d;
16pub mod texture;
17#[cfg(feature = "ui")]
18pub mod ui;
19pub mod vm;
20
21#[derive(Debug, Clone)]
23pub enum SceneVMError {
24 GpuInitFailed(String),
25 BufferAllocationFailed(String),
26 ShaderCompilationFailed(String),
27 TextureUploadFailed(String),
28 InvalidGeometry(String),
29 AtlasFull(String),
30 InvalidOperation(String),
31}
32
33pub type SceneVMResult<T> = Result<T, SceneVMError>;
34
35use instant::Instant;
36use rust_embed::RustEmbed;
37#[derive(RustEmbed)]
39#[folder = "embedded/"]
40#[exclude = "*.txt"]
41#[exclude = "*.DS_Store"]
42pub struct Embedded;
43
44pub mod prelude {
45 pub use crate::{
48 Embedded, SceneVM, SceneVMError, SceneVMResult,
49 app_trait::{SceneVMApp, SceneVMRenderCtx},
50 atlas::{AtlasEntry, SharedAtlas},
51 bbox2d::BBox2D,
52 camera3d::{Camera3D, CameraKind},
53 chunk::Chunk,
54 dynamic::{AlphaMode, DynamicKind, DynamicObject, RepeatMode},
55 intodata::IntoDataInput,
56 light::{Light, LightType},
57 poly2d::Poly2D,
58 poly3d::Poly3D,
59 texture::Texture,
60 vm::{Atom, GeoId, LayerBlendMode, LineStrip2D, RenderMode, VM},
61 };
62
63 #[cfg(feature = "ui")]
64 pub use crate::{
65 RenderResult,
66 app_event::{AppEvent, AppEventQueue},
67 ui::{
68 Alignment, Button, ButtonGroup, ButtonGroupOrientation, ButtonGroupStyle, ButtonKind,
69 ButtonStyle, Canvas, ColorButton, ColorButtonStyle, ColorWheel, Drawable, DropdownList,
70 DropdownListStyle, HAlign, HStack, Image, ImageStyle, Label, LabelRect, NodeId,
71 ParamList, ParamListStyle, PopupAlignment, Project, ProjectBrowser, ProjectBrowserItem,
72 ProjectBrowserStyle, ProjectError, ProjectMetadata, RecentProject, RecentProjects,
73 Slider, SliderStyle, Spacer, TabbedPanel, TabbedPanelStyle, TextButton, Theme, Toolbar,
74 ToolbarOrientation, ToolbarSeparator, ToolbarStyle, UiAction, UiEvent, UiEventKind,
75 UiRenderer, UiView, UndoCommand, UndoStack, VAlign, VStack, ViewContext, Workspace,
76 create_tile_material,
77 },
78 };
79
80 pub use rustc_hash::{FxHashMap, FxHashSet};
81 pub use vek::{Mat3, Mat4, Vec2, Vec3, Vec4};
82}
83
84#[cfg(feature = "ui")]
85pub use crate::ui::{
86 Alignment, Button, ButtonGroup, ButtonGroupStyle, ButtonKind, ButtonStyle, Canvas, Drawable,
87 HAlign, HStack, Image, ImageStyle, Label, LabelRect, NodeId, ParamList, ParamListStyle,
88 PopupAlignment, Slider, SliderStyle, TextButton, Toolbar, ToolbarOrientation, ToolbarSeparator,
89 ToolbarStyle, UiAction, UiEvent, UiEventKind, UiRenderer, UiView, UndoCommand, UndoStack,
90 VAlign, VStack, ViewContext, Workspace,
91};
92pub use crate::{
93 app_trait::{SceneVMApp, SceneVMRenderCtx},
94 atlas::{AtlasEntry, SharedAtlas},
95 bbox2d::BBox2D,
96 camera3d::{Camera3D, CameraKind},
97 chunk::Chunk,
98 dynamic::{AlphaMode, DynamicKind, DynamicObject, RepeatMode},
99 intodata::IntoDataInput,
100 light::{Light, LightType},
101 poly2d::Poly2D,
102 poly3d::Poly3D,
103 texture::Texture,
104 vm::{Atom, GeoId, LayerBlendMode, LineStrip2D, RenderMode, VM},
105};
106use image;
107use std::borrow::Cow;
108#[cfg(target_arch = "wasm32")]
109use std::cell::RefCell;
110#[cfg(not(target_arch = "wasm32"))]
111use std::ffi::c_void;
112#[cfg(not(target_arch = "wasm32"))]
113use std::sync::OnceLock;
114#[cfg(target_arch = "wasm32")]
115use std::{cell::Cell, future::Future, rc::Rc};
116#[cfg(target_arch = "wasm32")]
117use std::{
118 pin::Pin,
119 task::{Context, Poll},
120};
121#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
122use vek::Mat3;
123#[cfg(target_arch = "wasm32")]
124use wasm_bindgen::JsCast;
125#[cfg(target_arch = "wasm32")]
126use wasm_bindgen::prelude::*;
127#[cfg(target_arch = "wasm32")]
128use wasm_bindgen_futures::spawn_local;
129#[cfg(target_arch = "wasm32")]
130use web_sys::{CanvasRenderingContext2d, Document, HtmlCanvasElement, Window as WebWindow};
131#[cfg(all(feature = "windowing", not(target_arch = "wasm32")))]
132use winit::window::Window;
133#[cfg(all(feature = "windowing", not(target_arch = "wasm32")))]
134use winit::{dpi::PhysicalPosition, event::ElementState, event::MouseButton, event::WindowEvent};
135
136#[derive(Copy, Clone, Debug, Eq, PartialEq)]
138pub enum RenderResult {
139 Presented,
141 InitPending,
143 ReadbackPending,
145}
146
147#[cfg(not(target_arch = "wasm32"))]
149struct PresentPipeline {
150 pipeline: wgpu::RenderPipeline,
151 bind_group_layout: wgpu::BindGroupLayout,
152 bind_group: wgpu::BindGroup,
153 rect_buf: wgpu::Buffer,
154 sampler: wgpu::Sampler,
155 surface_format: wgpu::TextureFormat,
156}
157
158struct CompositingPipeline {
160 pipeline: wgpu::RenderPipeline,
161 bind_group_layout: wgpu::BindGroupLayout,
162 mode_buf: wgpu::Buffer,
163 sampler: wgpu::Sampler,
164 target_format: wgpu::TextureFormat,
165}
166
167impl CompositingPipeline {
168 fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
169 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
170 label: Some("scenevm-composite-shader"),
171 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
172 "
173@group(0) @binding(0) var layer_tex: texture_2d<f32>;
174@group(0) @binding(1) var layer_sampler: sampler;
175@group(0) @binding(2) var<uniform> blend_mode_buf: u32;
176
177struct VsOut {
178 @builtin(position) pos: vec4<f32>,
179 @location(0) uv: vec2<f32>,
180};
181
182@vertex
183fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
184 var positions = array<vec2<f32>, 3>(
185 vec2<f32>(-1.0, -3.0),
186 vec2<f32>(3.0, 1.0),
187 vec2<f32>(-1.0, 1.0)
188 );
189 var uvs = array<vec2<f32>, 3>(
190 vec2<f32>(0.0, 2.0),
191 vec2<f32>(2.0, 0.0),
192 vec2<f32>(0.0, 0.0)
193 );
194 var out: VsOut;
195 out.pos = vec4<f32>(positions[vi], 0.0, 1.0);
196 out.uv = uvs[vi];
197 return out;
198}
199
200fn linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
201 return pow(c, vec3<f32>(1.0 / 2.2));
202}
203
204@fragment
205fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
206 let src = textureSample(layer_tex, layer_sampler, in.uv);
207 if (blend_mode_buf == 1u) {
208 return vec4<f32>(linear_to_srgb(src.rgb), src.a);
209 }
210 return src;
211}
212 ",
213 )),
214 });
215
216 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
217 label: Some("scenevm-composite-bgl"),
218 entries: &[
219 wgpu::BindGroupLayoutEntry {
220 binding: 0,
221 visibility: wgpu::ShaderStages::FRAGMENT,
222 ty: wgpu::BindingType::Texture {
223 sample_type: wgpu::TextureSampleType::Float { filterable: true },
224 view_dimension: wgpu::TextureViewDimension::D2,
225 multisampled: false,
226 },
227 count: None,
228 },
229 wgpu::BindGroupLayoutEntry {
230 binding: 1,
231 visibility: wgpu::ShaderStages::FRAGMENT,
232 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
233 count: None,
234 },
235 wgpu::BindGroupLayoutEntry {
236 binding: 2,
237 visibility: wgpu::ShaderStages::FRAGMENT,
238 ty: wgpu::BindingType::Buffer {
239 ty: wgpu::BufferBindingType::Uniform,
240 has_dynamic_offset: false,
241 min_binding_size: None,
242 },
243 count: None,
244 },
245 ],
246 });
247
248 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
249 label: Some("scenevm-composite-pipeline-layout"),
250 bind_group_layouts: &[&bind_group_layout],
251 push_constant_ranges: &[],
252 });
253 let mode_buf = device.create_buffer(&wgpu::BufferDescriptor {
254 label: Some("scenevm-composite-mode"),
255 size: std::mem::size_of::<u32>() as u64,
256 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
257 mapped_at_creation: false,
258 });
259
260 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
261 label: Some("scenevm-composite-sampler"),
262 address_mode_u: wgpu::AddressMode::ClampToEdge,
263 address_mode_v: wgpu::AddressMode::ClampToEdge,
264 mag_filter: wgpu::FilterMode::Linear,
265 min_filter: wgpu::FilterMode::Linear,
266 ..Default::default()
267 });
268
269 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
270 label: Some("scenevm-composite-pipeline"),
271 layout: Some(&pipeline_layout),
272 vertex: wgpu::VertexState {
273 module: &shader,
274 entry_point: Some("vs_main"),
275 buffers: &[],
276 compilation_options: wgpu::PipelineCompilationOptions::default(),
277 },
278 fragment: Some(wgpu::FragmentState {
279 module: &shader,
280 entry_point: Some("fs_main"),
281 targets: &[Some(wgpu::ColorTargetState {
282 format: target_format,
283 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
284 write_mask: wgpu::ColorWrites::ALL,
285 })],
286 compilation_options: wgpu::PipelineCompilationOptions::default(),
287 }),
288 primitive: wgpu::PrimitiveState {
289 topology: wgpu::PrimitiveTopology::TriangleList,
290 ..Default::default()
291 },
292 depth_stencil: None,
293 multisample: wgpu::MultisampleState::default(),
294 multiview: None,
295 cache: None,
296 });
297
298 Self {
299 pipeline,
300 bind_group_layout,
301 mode_buf,
302 sampler,
303 target_format,
304 }
305 }
306}
307
308struct RgbaOverlayCompositingPipeline {
309 pipeline: wgpu::RenderPipeline,
310 bind_group_layout: wgpu::BindGroupLayout,
311 rect_buf: wgpu::Buffer,
312 sampler: wgpu::Sampler,
313 target_format: wgpu::TextureFormat,
314}
315
316impl RgbaOverlayCompositingPipeline {
317 fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
318 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
319 label: Some("scenevm-rgba-overlay-shader"),
320 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
321 "
322@group(0) @binding(0) var overlay_tex: texture_2d<f32>;
323@group(0) @binding(1) var overlay_sampler: sampler;
324@group(0) @binding(2) var<uniform> rect: vec4<f32>;
325
326struct VsOut {
327 @builtin(position) pos: vec4<f32>,
328};
329
330@vertex
331fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
332 var positions = array<vec2<f32>, 3>(
333 vec2<f32>(-1.0, -3.0),
334 vec2<f32>(3.0, 1.0),
335 vec2<f32>(-1.0, 1.0)
336 );
337 var out: VsOut;
338 out.pos = vec4<f32>(positions[vi], 0.0, 1.0);
339 return out;
340}
341
342@fragment
343fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
344 let x = pos.x;
345 let y = pos.y;
346 if (x < rect.x || y < rect.y || x >= (rect.x + rect.z) || y >= (rect.y + rect.w)) {
347 return vec4<f32>(0.0);
348 }
349 let uv = vec2<f32>((x - rect.x) / max(rect.z, 1.0), (y - rect.y) / max(rect.w, 1.0));
350 return textureSample(overlay_tex, overlay_sampler, uv);
351}
352 ",
353 )),
354 });
355
356 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
357 label: Some("scenevm-rgba-overlay-bgl"),
358 entries: &[
359 wgpu::BindGroupLayoutEntry {
360 binding: 0,
361 visibility: wgpu::ShaderStages::FRAGMENT,
362 ty: wgpu::BindingType::Texture {
363 sample_type: wgpu::TextureSampleType::Float { filterable: true },
364 view_dimension: wgpu::TextureViewDimension::D2,
365 multisampled: false,
366 },
367 count: None,
368 },
369 wgpu::BindGroupLayoutEntry {
370 binding: 1,
371 visibility: wgpu::ShaderStages::FRAGMENT,
372 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
373 count: None,
374 },
375 wgpu::BindGroupLayoutEntry {
376 binding: 2,
377 visibility: wgpu::ShaderStages::FRAGMENT,
378 ty: wgpu::BindingType::Buffer {
379 ty: wgpu::BufferBindingType::Uniform,
380 has_dynamic_offset: false,
381 min_binding_size: None,
382 },
383 count: None,
384 },
385 ],
386 });
387
388 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
389 label: Some("scenevm-rgba-overlay-pipeline-layout"),
390 bind_group_layouts: &[&bind_group_layout],
391 push_constant_ranges: &[],
392 });
393
394 let rect_buf = device.create_buffer(&wgpu::BufferDescriptor {
395 label: Some("scenevm-rgba-overlay-rect"),
396 size: (std::mem::size_of::<f32>() * 4) as u64,
397 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
398 mapped_at_creation: false,
399 });
400
401 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
402 label: Some("scenevm-rgba-overlay-sampler"),
403 address_mode_u: wgpu::AddressMode::ClampToEdge,
404 address_mode_v: wgpu::AddressMode::ClampToEdge,
405 mag_filter: wgpu::FilterMode::Linear,
406 min_filter: wgpu::FilterMode::Linear,
407 ..Default::default()
408 });
409
410 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
411 label: Some("scenevm-rgba-overlay-pipeline"),
412 layout: Some(&pipeline_layout),
413 vertex: wgpu::VertexState {
414 module: &shader,
415 entry_point: Some("vs_main"),
416 buffers: &[],
417 compilation_options: wgpu::PipelineCompilationOptions::default(),
418 },
419 fragment: Some(wgpu::FragmentState {
420 module: &shader,
421 entry_point: Some("fs_main"),
422 targets: &[Some(wgpu::ColorTargetState {
423 format: target_format,
424 blend: Some(wgpu::BlendState {
427 color: wgpu::BlendComponent {
428 src_factor: wgpu::BlendFactor::One,
429 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
430 operation: wgpu::BlendOperation::Add,
431 },
432 alpha: wgpu::BlendComponent {
433 src_factor: wgpu::BlendFactor::One,
434 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
435 operation: wgpu::BlendOperation::Add,
436 },
437 }),
438 write_mask: wgpu::ColorWrites::ALL,
439 })],
440 compilation_options: wgpu::PipelineCompilationOptions::default(),
441 }),
442 primitive: wgpu::PrimitiveState {
443 topology: wgpu::PrimitiveTopology::TriangleList,
444 ..Default::default()
445 },
446 depth_stencil: None,
447 multisample: wgpu::MultisampleState::default(),
448 multiview: None,
449 cache: None,
450 });
451
452 Self {
453 pipeline,
454 bind_group_layout,
455 rect_buf,
456 sampler,
457 target_format,
458 }
459 }
460}
461
462struct RgbaOverlayState {
463 texture: Texture,
464 rect: [f32; 4],
465}
466
467#[cfg(not(target_arch = "wasm32"))]
469struct WindowSurface {
470 surface: wgpu::Surface<'static>,
471 config: wgpu::SurfaceConfiguration,
472 format: wgpu::TextureFormat,
473 present_pipeline: Option<PresentPipeline>,
474}
475
476pub struct GPUState {
477 _instance: wgpu::Instance,
478 _adapter: wgpu::Adapter,
479 device: wgpu::Device,
480 queue: wgpu::Queue,
481 surface: Texture,
483 #[cfg(not(target_arch = "wasm32"))]
485 window_surface: Option<WindowSurface>,
486}
487
488#[allow(dead_code)]
489#[derive(Clone)]
490struct GlobalGpu {
491 instance: wgpu::Instance,
492 adapter: wgpu::Adapter,
493 device: wgpu::Device,
494 queue: wgpu::Queue,
495}
496
497#[allow(dead_code)]
498#[cfg(not(target_arch = "wasm32"))]
499static GLOBAL_GPU: OnceLock<GlobalGpu> = OnceLock::new();
500
501#[cfg(target_arch = "wasm32")]
502thread_local! {
503 static GLOBAL_GPU_WASM: RefCell<Option<GlobalGpu>> = RefCell::new(None);
504}
505
506#[cfg(not(target_arch = "wasm32"))]
507impl PresentPipeline {
508 fn new(
509 device: &wgpu::Device,
510 queue: &wgpu::Queue,
511 format: wgpu::TextureFormat,
512 source_view: &wgpu::TextureView,
513 overlay_view: &wgpu::TextureView,
514 overlay_rect: [f32; 4],
515 ) -> Self {
516 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
517 label: Some("scenevm-present-shader"),
518 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
519 "
520@group(0) @binding(0) var src_tex: texture_2d<f32>;
521@group(0) @binding(1) var src_sampler: sampler;
522@group(0) @binding(2) var overlay_tex: texture_2d<f32>;
523@group(0) @binding(3) var overlay_sampler: sampler;
524@group(0) @binding(4) var<uniform> overlay_rect: vec4<f32>;
525
526struct VsOut {
527 @builtin(position) pos: vec4<f32>,
528 @location(0) uv: vec2<f32>,
529};
530
531@vertex
532fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
533 var positions = array<vec2<f32>, 3>(
534 vec2<f32>(-1.0, -3.0),
535 vec2<f32>(3.0, 1.0),
536 vec2<f32>(-1.0, 1.0)
537 );
538 var uvs = array<vec2<f32>, 3>(
539 vec2<f32>(0.0, 2.0),
540 vec2<f32>(2.0, 0.0),
541 vec2<f32>(0.0, 0.0)
542 );
543 var out: VsOut;
544 out.pos = vec4<f32>(positions[vi], 0.0, 1.0);
545 out.uv = uvs[vi];
546 return out;
547}
548
549@fragment
550fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
551 let base = textureSample(src_tex, src_sampler, in.uv);
552 if (overlay_rect.z <= 0.0 || overlay_rect.w <= 0.0) {
553 return base;
554 }
555
556 let x = in.uv.x;
557 let y = in.uv.y;
558 if (x < overlay_rect.x || y < overlay_rect.y || x >= (overlay_rect.x + overlay_rect.z) || y >= (overlay_rect.y + overlay_rect.w)) {
559 return base;
560 }
561
562 let uv = vec2<f32>((x - overlay_rect.x) / max(overlay_rect.z, 1e-6), (y - overlay_rect.y) / max(overlay_rect.w, 1e-6));
563 let over = textureSample(overlay_tex, overlay_sampler, uv);
564 // Premultiplied alpha over operation.
565 let out_rgb = over.rgb + base.rgb * (1.0 - over.a);
566 let out_a = over.a + base.a * (1.0 - over.a);
567 return vec4<f32>(out_rgb, out_a);
568}
569",
570 )),
571 });
572
573 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
574 label: Some("scenevm-present-bgl"),
575 entries: &[
576 wgpu::BindGroupLayoutEntry {
577 binding: 0,
578 visibility: wgpu::ShaderStages::FRAGMENT,
579 ty: wgpu::BindingType::Texture {
580 sample_type: wgpu::TextureSampleType::Float { filterable: true },
581 view_dimension: wgpu::TextureViewDimension::D2,
582 multisampled: false,
583 },
584 count: None,
585 },
586 wgpu::BindGroupLayoutEntry {
587 binding: 1,
588 visibility: wgpu::ShaderStages::FRAGMENT,
589 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
590 count: None,
591 },
592 wgpu::BindGroupLayoutEntry {
593 binding: 2,
594 visibility: wgpu::ShaderStages::FRAGMENT,
595 ty: wgpu::BindingType::Texture {
596 sample_type: wgpu::TextureSampleType::Float { filterable: true },
597 view_dimension: wgpu::TextureViewDimension::D2,
598 multisampled: false,
599 },
600 count: None,
601 },
602 wgpu::BindGroupLayoutEntry {
603 binding: 3,
604 visibility: wgpu::ShaderStages::FRAGMENT,
605 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
606 count: None,
607 },
608 wgpu::BindGroupLayoutEntry {
609 binding: 4,
610 visibility: wgpu::ShaderStages::FRAGMENT,
611 ty: wgpu::BindingType::Buffer {
612 ty: wgpu::BufferBindingType::Uniform,
613 has_dynamic_offset: false,
614 min_binding_size: None,
615 },
616 count: None,
617 },
618 ],
619 });
620
621 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
622 label: Some("scenevm-present-sampler"),
623 mag_filter: wgpu::FilterMode::Linear,
624 min_filter: wgpu::FilterMode::Linear,
625 mipmap_filter: wgpu::FilterMode::Nearest,
626 ..Default::default()
627 });
628
629 let rect_buf = device.create_buffer(&wgpu::BufferDescriptor {
630 label: Some("scenevm-present-overlay-rect"),
631 size: (std::mem::size_of::<f32>() * 4) as u64,
632 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
633 mapped_at_creation: false,
634 });
635 queue.write_buffer(&rect_buf, 0, bytemuck::cast_slice(&overlay_rect));
636
637 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
638 label: Some("scenevm-present-bind-group"),
639 layout: &bind_group_layout,
640 entries: &[
641 wgpu::BindGroupEntry {
642 binding: 0,
643 resource: wgpu::BindingResource::TextureView(source_view),
644 },
645 wgpu::BindGroupEntry {
646 binding: 1,
647 resource: wgpu::BindingResource::Sampler(&sampler),
648 },
649 wgpu::BindGroupEntry {
650 binding: 2,
651 resource: wgpu::BindingResource::TextureView(overlay_view),
652 },
653 wgpu::BindGroupEntry {
654 binding: 3,
655 resource: wgpu::BindingResource::Sampler(&sampler),
656 },
657 wgpu::BindGroupEntry {
658 binding: 4,
659 resource: rect_buf.as_entire_binding(),
660 },
661 ],
662 });
663
664 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
665 label: Some("scenevm-present-pipeline-layout"),
666 bind_group_layouts: &[&bind_group_layout],
667 push_constant_ranges: &[],
668 });
669
670 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
671 label: Some("scenevm-present-pipeline"),
672 layout: Some(&pipeline_layout),
673 vertex: wgpu::VertexState {
674 module: &shader,
675 entry_point: Some("vs_main"),
676 buffers: &[],
677 compilation_options: wgpu::PipelineCompilationOptions::default(),
678 },
679 fragment: Some(wgpu::FragmentState {
680 module: &shader,
681 entry_point: Some("fs_main"),
682 targets: &[Some(wgpu::ColorTargetState {
683 format,
684 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
685 write_mask: wgpu::ColorWrites::ALL,
686 })],
687 compilation_options: wgpu::PipelineCompilationOptions::default(),
688 }),
689 primitive: wgpu::PrimitiveState {
690 topology: wgpu::PrimitiveTopology::TriangleList,
691 ..Default::default()
692 },
693 depth_stencil: None,
694 multisample: wgpu::MultisampleState::default(),
695 multiview: None,
696 cache: None,
697 });
698
699 Self {
700 pipeline,
701 bind_group_layout,
702 bind_group,
703 rect_buf,
704 sampler,
705 surface_format: format,
706 }
707 }
708
709 fn update_bind_group(
710 &mut self,
711 device: &wgpu::Device,
712 queue: &wgpu::Queue,
713 source_view: &wgpu::TextureView,
714 overlay_view: &wgpu::TextureView,
715 overlay_rect: [f32; 4],
716 ) {
717 queue.write_buffer(&self.rect_buf, 0, bytemuck::cast_slice(&overlay_rect));
718 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
719 label: Some("scenevm-present-bind-group"),
720 layout: &self.bind_group_layout,
721 entries: &[
722 wgpu::BindGroupEntry {
723 binding: 0,
724 resource: wgpu::BindingResource::TextureView(source_view),
725 },
726 wgpu::BindGroupEntry {
727 binding: 1,
728 resource: wgpu::BindingResource::Sampler(&self.sampler),
729 },
730 wgpu::BindGroupEntry {
731 binding: 2,
732 resource: wgpu::BindingResource::TextureView(overlay_view),
733 },
734 wgpu::BindGroupEntry {
735 binding: 3,
736 resource: wgpu::BindingResource::Sampler(&self.sampler),
737 },
738 wgpu::BindGroupEntry {
739 binding: 4,
740 resource: self.rect_buf.as_entire_binding(),
741 },
742 ],
743 });
744 }
745}
746
747#[cfg(not(target_arch = "wasm32"))]
748impl WindowSurface {
749 fn reconfigure(&mut self, device: &wgpu::Device) {
750 self.surface.configure(device, &self.config);
751 }
752}
753
754#[cfg(target_arch = "wasm32")]
756struct MapReadyFuture {
757 flag: Rc<Cell<bool>>,
758}
759
760#[cfg(target_arch = "wasm32")]
761impl Future for MapReadyFuture {
762 type Output = ();
763 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
764 if self.flag.get() {
765 Poll::Ready(())
766 } else {
767 cx.waker().wake_by_ref();
769 Poll::Pending
770 }
771 }
772}
773
774pub struct SceneVM {
775 size: (u32, u32),
777
778 gpu: Option<GPUState>,
780 #[cfg(target_arch = "wasm32")]
781 needs_gpu_init: bool,
782 #[cfg(target_arch = "wasm32")]
783 init_in_flight: bool,
784
785 atlas: SharedAtlas,
786 pub vm: VM,
787 overlay_vms: Vec<VM>,
788 active_vm_index: usize,
789 log_layer_activity: bool,
790 compositing_pipeline: Option<CompositingPipeline>,
791 rgba_overlay_pipeline: Option<RgbaOverlayCompositingPipeline>,
792 rgba_overlay: Option<RgbaOverlayState>,
793 stats_last_log: Instant,
794 stats_frames_since_log: u32,
795}
796
797#[derive(Debug, Clone)]
799pub struct ShaderCompilationResult {
800 pub success: bool,
802 pub warnings: Vec<ShaderDiagnostic>,
804 pub errors: Vec<ShaderDiagnostic>,
806}
807
808#[derive(Debug, Clone)]
810pub struct ShaderDiagnostic {
811 pub line: u32,
813 pub message: String,
815}
816
817impl Default for SceneVM {
818 fn default() -> Self {
819 Self::new(100, 100)
820 }
821}
822
823impl SceneVM {
824 fn refresh_layer_metadata(&mut self) {
825 self.vm.set_layer_index(0);
826 self.vm.set_activity_logging(self.log_layer_activity);
827 for (i, vm) in self.overlay_vms.iter_mut().enumerate() {
828 vm.set_layer_index(i + 1);
829 vm.set_activity_logging(self.log_layer_activity);
830 }
831 }
832
833 fn total_vm_count(&self) -> usize {
834 1 + self.overlay_vms.len()
835 }
836
837 fn vm_ref_by_index(&self, index: usize) -> Option<&VM> {
838 if index == 0 {
839 Some(&self.vm)
840 } else {
841 self.overlay_vms.get(index.saturating_sub(1))
842 }
843 }
844
845 fn vm_mut_by_index(&mut self, index: usize) -> Option<&mut VM> {
846 if index == 0 {
847 Some(&mut self.vm)
848 } else {
849 self.overlay_vms.get_mut(index.saturating_sub(1))
850 }
851 }
852
853 fn draw_all_vms(
854 base_vm: &mut VM,
855 overlays: &mut [VM],
856 device: &wgpu::Device,
857 queue: &wgpu::Queue,
858 surface: &mut Texture,
859 w: u32,
860 h: u32,
861 log_errors: bool,
862 compositing_pipeline: &mut Option<CompositingPipeline>,
863 rgba_overlay: &mut Option<RgbaOverlayState>,
864 rgba_overlay_pipeline: &mut Option<RgbaOverlayCompositingPipeline>,
865 composite_rgba_overlay_in_scene: bool,
866 ) {
867 let target_format = wgpu::TextureFormat::Rgba8Unorm;
869 if let Err(e) = base_vm.draw_into(device, queue, surface, w, h) {
870 if log_errors {
871 println!("[SceneVM] Error drawing base VM: {:?}", e);
872 }
873 }
874
875 for vm in overlays.iter_mut() {
876 if let Err(e) = vm.draw_into(device, queue, surface, w, h) {
877 if log_errors {
878 println!("[SceneVM] Error drawing overlay VM: {:?}", e);
879 }
880 }
881 }
882
883 surface.ensure_gpu_with(device);
885
886 if compositing_pipeline
888 .as_ref()
889 .map(|p| p.target_format != target_format)
890 .unwrap_or(true)
891 {
892 *compositing_pipeline = Some(CompositingPipeline::new(device, target_format));
893 }
894
895 let pipeline = compositing_pipeline.as_ref().unwrap();
896
897 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
899 label: Some("scenevm-compositing-encoder"),
900 });
901
902 let surface_view = &surface.gpu.as_ref().unwrap().view;
904
905 let mut vms_to_composite: Vec<&VM> = Vec::new();
907 if base_vm.is_enabled() {
908 vms_to_composite.push(base_vm);
909 }
910 for vm in overlays.iter() {
911 if vm.is_enabled() {
912 vms_to_composite.push(vm);
913 }
914 }
915
916 for (i, vm) in vms_to_composite.iter().enumerate() {
918 if let Some(layer_texture) = vm.composite_texture() {
919 if let Some(layer_gpu) = &layer_texture.gpu {
920 let mode_u32: u32 = match vm.blend_mode() {
922 vm::LayerBlendMode::Alpha => 0,
923 vm::LayerBlendMode::AlphaLinear => 1,
924 };
925 queue.write_buffer(&pipeline.mode_buf, 0, bytemuck::bytes_of(&mode_u32));
927
928 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
929 label: Some("scenevm-compositing-bind-group"),
930 layout: &pipeline.bind_group_layout,
931 entries: &[
932 wgpu::BindGroupEntry {
933 binding: 0,
934 resource: wgpu::BindingResource::TextureView(&layer_gpu.view),
935 },
936 wgpu::BindGroupEntry {
937 binding: 1,
938 resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
939 },
940 wgpu::BindGroupEntry {
941 binding: 2,
942 resource: pipeline.mode_buf.as_entire_binding(),
943 },
944 ],
945 });
946
947 let load_op = if i == 0 {
951 wgpu::LoadOp::Clear(wgpu::Color::BLACK)
952 } else {
953 wgpu::LoadOp::Load
954 };
955
956 {
957 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
958 label: Some("scenevm-compositing-pass"),
959 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
960 view: surface_view,
961 resolve_target: None,
962 ops: wgpu::Operations {
963 load: load_op,
964 store: wgpu::StoreOp::Store,
965 },
966 depth_slice: None,
967 })],
968 depth_stencil_attachment: None,
969 timestamp_writes: None,
970 occlusion_query_set: None,
971 });
972
973 rpass.set_pipeline(&pipeline.pipeline);
974 rpass.set_bind_group(0, &bind_group, &[]);
975 rpass.draw(0..3, 0..1);
976 }
977 }
978 }
979 }
980
981 if composite_rgba_overlay_in_scene && let Some(overlay) = rgba_overlay.as_mut() {
982 overlay.texture.ensure_gpu_with(device);
983 overlay.texture.upload_to_gpu_with(device, queue);
984 if let Some(overlay_gpu) = overlay.texture.gpu.as_ref() {
985 if rgba_overlay_pipeline
986 .as_ref()
987 .map(|p| p.target_format != target_format)
988 .unwrap_or(true)
989 {
990 *rgba_overlay_pipeline =
991 Some(RgbaOverlayCompositingPipeline::new(device, target_format));
992 }
993
994 let pipeline = rgba_overlay_pipeline.as_ref().unwrap();
995 queue.write_buffer(&pipeline.rect_buf, 0, bytemuck::cast_slice(&overlay.rect));
996
997 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
998 label: Some("scenevm-rgba-overlay-bind-group"),
999 layout: &pipeline.bind_group_layout,
1000 entries: &[
1001 wgpu::BindGroupEntry {
1002 binding: 0,
1003 resource: wgpu::BindingResource::TextureView(&overlay_gpu.view),
1004 },
1005 wgpu::BindGroupEntry {
1006 binding: 1,
1007 resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
1008 },
1009 wgpu::BindGroupEntry {
1010 binding: 2,
1011 resource: pipeline.rect_buf.as_entire_binding(),
1012 },
1013 ],
1014 });
1015
1016 {
1017 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1018 label: Some("scenevm-rgba-overlay-pass"),
1019 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1020 view: surface_view,
1021 resolve_target: None,
1022 ops: wgpu::Operations {
1023 load: wgpu::LoadOp::Load,
1024 store: wgpu::StoreOp::Store,
1025 },
1026 depth_slice: None,
1027 })],
1028 depth_stencil_attachment: None,
1029 timestamp_writes: None,
1030 occlusion_query_set: None,
1031 });
1032 rpass.set_pipeline(&pipeline.pipeline);
1033 rpass.set_bind_group(0, &bind_group, &[]);
1034 rpass.draw(0..3, 0..1);
1035 }
1036 }
1037 }
1038
1039 queue.submit(Some(encoder.finish()));
1040 }
1041
1042 pub fn set_rgba_overlay(&mut self, width: u32, height: u32, rgba: Vec<u8>, rect: [f32; 4]) {
1043 let w = width.max(1);
1044 let h = height.max(1);
1045 let needed = (w as usize) * (h as usize) * 4;
1046 let mut data = rgba;
1047 if data.len() < needed {
1048 data.resize(needed, 0);
1049 }
1050 if data.len() > needed {
1051 data.truncate(needed);
1052 }
1053
1054 match self.rgba_overlay.as_mut() {
1055 Some(existing) if existing.texture.width == w && existing.texture.height == h => {
1056 existing.texture.data = data;
1057 existing.rect = rect;
1058 }
1059 _ => {
1060 let mut texture = Texture::new(w, h);
1061 texture.data = data;
1062 self.rgba_overlay = Some(RgbaOverlayState { texture, rect });
1063 }
1064 }
1065 }
1066
1067 pub fn set_rgba_overlay_bytes(&mut self, width: u32, height: u32, rgba: &[u8], rect: [f32; 4]) {
1068 let w = width.max(1);
1069 let h = height.max(1);
1070 let needed = (w as usize) * (h as usize) * 4;
1071
1072 match self.rgba_overlay.as_mut() {
1073 Some(existing) if existing.texture.width == w && existing.texture.height == h => {
1074 existing.texture.data.resize(needed, 0);
1075 let copy_len = rgba.len().min(needed);
1076 existing.texture.data[..copy_len].copy_from_slice(&rgba[..copy_len]);
1077 if copy_len < needed {
1078 existing.texture.data[copy_len..].fill(0);
1079 }
1080 existing.rect = rect;
1081 }
1082 _ => {
1083 let mut texture = Texture::new(w, h);
1084 texture.data.resize(needed, 0);
1085 let copy_len = rgba.len().min(needed);
1086 texture.data[..copy_len].copy_from_slice(&rgba[..copy_len]);
1087 self.rgba_overlay = Some(RgbaOverlayState { texture, rect });
1088 }
1089 }
1090 }
1091
1092 pub fn clear_rgba_overlay(&mut self) {
1093 self.rgba_overlay = None;
1094 }
1095
1096 pub fn vm_layer_count(&self) -> usize {
1098 self.total_vm_count()
1099 }
1100
1101 pub fn add_vm_layer(&mut self) -> usize {
1103 let mut vm = VM::new_with_shared_atlas(self.atlas.clone());
1105 vm.background = vek::Vec4::new(0.0, 0.0, 0.0, 0.0);
1106 self.overlay_vms.push(vm);
1107 self.refresh_layer_metadata();
1108 self.total_vm_count() - 1
1109 }
1110
1111 pub fn remove_vm_layer(&mut self, index: usize) -> Option<VM> {
1113 if index == 0 {
1114 return None;
1115 }
1116 let idx = index - 1;
1117 if idx >= self.overlay_vms.len() {
1118 return None;
1119 }
1120 let removed = self.overlay_vms.remove(idx);
1121 if self.active_vm_index >= self.total_vm_count() {
1122 self.active_vm_index = self.total_vm_count().saturating_sub(1);
1123 }
1124 self.refresh_layer_metadata();
1125 Some(removed)
1126 }
1127
1128 pub fn set_active_vm(&mut self, index: usize) -> bool {
1130 if index < self.total_vm_count() {
1131 self.active_vm_index = index;
1132 true
1133 } else {
1134 false
1135 }
1136 }
1137
1138 pub fn active_vm_index(&self) -> usize {
1140 self.active_vm_index
1141 }
1142
1143 pub fn atlas_sdf_uv4(&self, id: &uuid::Uuid, anim_frame: u32) -> Option<[f32; 4]> {
1145 self.atlas.sdf_uv4(id, anim_frame)
1146 }
1147
1148 pub fn set_layer_enabled(&mut self, index: usize, enabled: bool) -> bool {
1150 if let Some(vm) = self.vm_mut_by_index(index) {
1151 vm.set_enabled(enabled);
1152 true
1153 } else {
1154 false
1155 }
1156 }
1157
1158 pub fn is_layer_enabled(&self, index: usize) -> Option<bool> {
1160 self.vm_ref_by_index(index).map(|vm| vm.is_enabled())
1161 }
1162
1163 pub fn set_layer_activity_logging(&mut self, enabled: bool) {
1165 self.log_layer_activity = enabled;
1166 self.refresh_layer_metadata();
1167 }
1168
1169 pub fn active_vm(&self) -> &VM {
1171 self.vm_ref_by_index(self.active_vm_index)
1172 .expect("active VM index out of range")
1173 }
1174
1175 pub fn active_vm_mut(&mut self) -> &mut VM {
1177 self.vm_mut_by_index(self.active_vm_index)
1178 .expect("active VM index out of range")
1179 }
1180
1181 pub fn pick_geo_id_at_uv(
1183 &self,
1184 fb_w: u32,
1185 fb_h: u32,
1186 screen_uv: [f32; 2],
1187 include_hidden: bool,
1188 include_billboards: bool,
1189 ) -> Option<(GeoId, vek::Vec3<f32>, f32)> {
1190 self.active_vm().pick_geo_id_at_uv(
1191 fb_w,
1192 fb_h,
1193 screen_uv,
1194 include_hidden,
1195 include_billboards,
1196 )
1197 }
1198
1199 pub fn ray_from_uv_with_size(
1201 &self,
1202 fb_w: u32,
1203 fb_h: u32,
1204 screen_uv: [f32; 2],
1205 ) -> Option<(vek::Vec3<f32>, vek::Vec3<f32>)> {
1206 self.active_vm().ray_from_uv(fb_w, fb_h, screen_uv)
1207 }
1208
1209 pub fn ray_from_uv(&self, screen_uv: [f32; 2]) -> Option<(vek::Vec3<f32>, vek::Vec3<f32>)> {
1211 let (w, h) = self.size;
1212 self.active_vm().ray_from_uv(w, h, screen_uv)
1213 }
1214
1215 pub fn print_geometry_stats(&self) {
1217 let mut total_2d = 0usize;
1218 let mut total_3d = 0usize;
1219 let mut total_lines = 0usize;
1220
1221 for vm in std::iter::once(&self.vm).chain(self.overlay_vms.iter()) {
1222 for (_cid, ch) in &vm.chunks_map {
1223 total_2d += ch.polys_map.len();
1224 total_3d += ch.polys3d_map.values().map(|v| v.len()).sum::<usize>();
1225 total_lines += ch.lines2d_px.len();
1226 }
1227 }
1228
1229 println!(
1230 "[SceneVM] Geometry Stats → 2D polys: {} | 3D polys: {} | 2D lines: {} | Total: {}",
1231 total_2d,
1232 total_3d,
1233 total_lines,
1234 total_2d + total_3d + total_lines
1235 );
1236 }
1237
1238 fn maybe_log_runtime_stats(
1239 log_layer_activity: bool,
1240 base_vm: &VM,
1241 overlays: &[VM],
1242 stats_last_log: &mut Instant,
1243 stats_frames_since_log: &mut u32,
1244 ) {
1245 if !log_layer_activity {
1246 return;
1247 }
1248
1249 *stats_frames_since_log = stats_frames_since_log.saturating_add(1);
1250 let now = Instant::now();
1251 let elapsed = now.saturating_duration_since(*stats_last_log);
1252 if elapsed.as_secs_f32() < 2.0 {
1253 return;
1254 }
1255
1256 let base = base_vm.debug_stats();
1257 let mut totals = base;
1258 let mut dirty_accel = if base_vm.is_enabled() {
1259 base.accel_dirty
1260 } else {
1261 false
1262 };
1263 let mut dirty_visibility = if base_vm.is_enabled() {
1264 base.visibility_dirty
1265 } else {
1266 false
1267 };
1268 let mut dirty_g3 = if base_vm.is_enabled() {
1269 base.geometry3d_dirty
1270 } else {
1271 false
1272 };
1273 let mut dirty_g2 = if base_vm.is_enabled() {
1274 base.geometry2d_dirty
1275 } else {
1276 false
1277 };
1278 let mut enabled_layers = if base_vm.is_enabled() { 1usize } else { 0usize };
1279 for vm in overlays {
1280 let s = vm.debug_stats();
1281 totals.chunks += s.chunks;
1282 totals.polys2d += s.polys2d;
1283 totals.polys3d += s.polys3d;
1284 totals.tris3d += s.tris3d;
1285 totals.lines2d += s.lines2d;
1286 totals.dynamics += s.dynamics;
1287 totals.lights += s.lights;
1288 totals.cached_v3 += s.cached_v3;
1289 totals.cached_i3 += s.cached_i3;
1290 if vm.is_enabled() {
1291 enabled_layers += 1;
1292 dirty_accel |= s.accel_dirty;
1293 dirty_visibility |= s.visibility_dirty;
1294 dirty_g3 |= s.geometry3d_dirty;
1295 dirty_g2 |= s.geometry2d_dirty;
1296 }
1297 }
1298
1299 let secs = elapsed.as_secs_f32().max(1e-3);
1300 let fps = *stats_frames_since_log as f32 / secs;
1301 println!(
1302 "[SceneVM Stats] layers={}/{} fps={:.1} chunks={} polys3d={} tris3d={} polys2d={} lines2d={} dynamics={} lights={} cache(v3/i3)={}/{} dirty(a/v/g3/g2)={}/{}/{}/{}",
1303 enabled_layers,
1304 1 + overlays.len(),
1305 fps,
1306 totals.chunks,
1307 totals.polys3d,
1308 totals.tris3d,
1309 totals.polys2d,
1310 totals.lines2d,
1311 totals.dynamics,
1312 totals.lights,
1313 totals.cached_v3,
1314 totals.cached_i3,
1315 dirty_accel as u8,
1316 dirty_visibility as u8,
1317 dirty_g3 as u8,
1318 dirty_g2 as u8
1319 );
1320
1321 *stats_last_log = now;
1322 *stats_frames_since_log = 0;
1323 }
1324
1325 pub fn execute(&mut self, atom: Atom) {
1327 let affects_atlas = SceneVM::atom_touches_atlas(&atom);
1328 let active = self.active_vm_index;
1329 if active == 0 {
1330 self.vm.execute(atom);
1331 } else if let Some(vm) = self.vm_mut_by_index(active) {
1332 vm.execute(atom);
1333 }
1334 if affects_atlas {
1335 self.for_each_vm_mut(|vm| vm.mark_all_geometry_dirty());
1336 }
1337 }
1338
1339 pub fn is_gpu_ready(&self) -> bool {
1341 if self.gpu.is_some() {
1342 #[cfg(target_arch = "wasm32")]
1343 {
1344 return !self.needs_gpu_init && !self.init_in_flight;
1345 }
1346 #[cfg(not(target_arch = "wasm32"))]
1347 {
1348 return true;
1349 }
1350 }
1351 false
1352 }
1353
1354 pub fn frame_in_flight(&self) -> bool {
1356 #[cfg(target_arch = "wasm32")]
1357 {
1358 if let Some(gpu) = &self.gpu {
1359 return gpu
1360 .surface
1361 .gpu
1362 .as_ref()
1363 .and_then(|g| g.map_ready.as_ref())
1364 .is_some();
1365 }
1366 return false;
1367 }
1368 #[cfg(not(target_arch = "wasm32"))]
1369 {
1370 false
1371 }
1372 }
1373 pub fn new(initial_width: u32, initial_height: u32) -> Self {
1375 #[cfg(target_arch = "wasm32")]
1376 {
1377 let atlas = SharedAtlas::new(4096, 4096);
1378 let mut this = Self {
1379 size: (initial_width, initial_height),
1380 gpu: None,
1381 needs_gpu_init: true,
1382 init_in_flight: false,
1383 atlas: atlas.clone(),
1384 vm: VM::new_with_shared_atlas(atlas.clone()),
1385 overlay_vms: Vec::new(),
1386 active_vm_index: 0,
1387 log_layer_activity: false,
1388 compositing_pipeline: None,
1389 rgba_overlay_pipeline: None,
1390 rgba_overlay: None,
1391 stats_last_log: Instant::now(),
1392 stats_frames_since_log: 0,
1393 };
1394 this.refresh_layer_metadata();
1395 this
1396 }
1397 #[cfg(not(target_arch = "wasm32"))]
1398 {
1399 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
1400 backends: { wgpu::Backends::all() },
1401 ..Default::default()
1402 });
1403 let adapter =
1404 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1405 power_preference: wgpu::PowerPreference::HighPerformance,
1406 force_fallback_adapter: false,
1407 compatible_surface: None,
1408 }))
1409 .expect("No compatible GPU adapter found");
1410
1411 let (device, queue) =
1412 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1413 label: Some("scenevm-device"),
1414 required_features: wgpu::Features::empty(),
1415 required_limits: wgpu::Limits::default(),
1416 ..Default::default()
1417 }))
1418 .expect("Failed to create wgpu device");
1419
1420 let mut surface = Texture::new(initial_width, initial_height);
1421 surface.ensure_gpu_with(&device);
1422
1423 let gpu = GPUState {
1424 _instance: instance,
1425 _adapter: adapter,
1426 device,
1427 queue,
1428 surface,
1429 window_surface: None,
1430 };
1431
1432 let atlas = SharedAtlas::new(4096, 4096);
1433 let mut this = Self {
1434 size: (initial_width, initial_height),
1435 gpu: Some(gpu),
1436 atlas: atlas.clone(),
1437 vm: VM::new_with_shared_atlas(atlas.clone()),
1438 overlay_vms: Vec::new(),
1439 active_vm_index: 0,
1440 log_layer_activity: false,
1441 compositing_pipeline: None,
1442 rgba_overlay_pipeline: None,
1443 rgba_overlay: None,
1444 stats_last_log: Instant::now(),
1445 stats_frames_since_log: 0,
1446 };
1447 this.refresh_layer_metadata();
1448 this
1449 }
1450 }
1451
1452 #[cfg(all(feature = "windowing", not(target_arch = "wasm32")))]
1454 pub fn new_with_window(window: &Window) -> Self {
1455 let initial_size = window.inner_size();
1456 let width = initial_size.width.max(1);
1457 let height = initial_size.height.max(1);
1458
1459 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
1460 backends: { wgpu::Backends::all() },
1461 ..Default::default()
1462 });
1463 let surface = unsafe {
1464 instance.create_surface_unsafe(
1465 wgpu::SurfaceTargetUnsafe::from_window(window)
1466 .expect("Failed to access raw window handle"),
1467 )
1468 }
1469 .expect("Failed to create wgpu surface for window");
1470 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1471 power_preference: wgpu::PowerPreference::HighPerformance,
1472 force_fallback_adapter: false,
1473 compatible_surface: Some(&surface),
1474 }))
1475 .expect("No compatible GPU adapter found");
1476
1477 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1478 label: Some("scenevm-device"),
1479 required_features: wgpu::Features::empty(),
1480 required_limits: wgpu::Limits::default(),
1481 ..Default::default()
1482 }))
1483 .expect("Failed to create wgpu device");
1484
1485 let caps = surface.get_capabilities(&adapter);
1486 let surface_format = caps
1488 .formats
1489 .iter()
1490 .copied()
1491 .find(|f| !f.is_srgb())
1492 .unwrap_or(caps.formats[0]);
1493 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
1494 wgpu::PresentMode::Fifo
1495 } else {
1496 caps.present_modes[0]
1497 };
1498 let alpha_mode = caps
1499 .alpha_modes
1500 .get(0)
1501 .copied()
1502 .unwrap_or(wgpu::CompositeAlphaMode::Auto);
1503
1504 let surface_config = wgpu::SurfaceConfiguration {
1505 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
1506 format: surface_format,
1507 width,
1508 height,
1509 present_mode,
1510 alpha_mode,
1511 view_formats: vec![],
1512 desired_maximum_frame_latency: 2,
1513 };
1514 surface.configure(&device, &surface_config);
1515
1516 let mut storage_surface = Texture::new(width, height);
1517 storage_surface.ensure_gpu_with(&device);
1518
1519 let gpu = GPUState {
1520 _instance: instance,
1521 _adapter: adapter,
1522 device,
1523 queue,
1524 surface: storage_surface,
1525 window_surface: Some(WindowSurface {
1526 surface,
1527 config: surface_config,
1528 format: surface_format,
1529 present_pipeline: None,
1530 }),
1531 };
1532
1533 let atlas = SharedAtlas::new(4096, 4096);
1534 let mut this = Self {
1535 size: (width, height),
1536 gpu: Some(gpu),
1537 atlas: atlas.clone(),
1538 vm: VM::new_with_shared_atlas(atlas.clone()),
1539 overlay_vms: Vec::new(),
1540 active_vm_index: 0,
1541 log_layer_activity: false,
1542 compositing_pipeline: None,
1543 rgba_overlay_pipeline: None,
1544 rgba_overlay: None,
1545 stats_last_log: Instant::now(),
1546 stats_frames_since_log: 0,
1547 };
1548 this.refresh_layer_metadata();
1549 this
1550 }
1551
1552 #[cfg(all(
1554 not(target_arch = "wasm32"),
1555 any(target_os = "macos", target_os = "ios")
1556 ))]
1557 pub fn new_with_metal_layer(layer_ptr: *mut c_void, width: u32, height: u32) -> Self {
1558 let width = width.max(1);
1559 let height = height.max(1);
1560
1561 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
1562 backends: { wgpu::Backends::all() },
1563 ..Default::default()
1564 });
1565
1566 let surface = unsafe {
1567 instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer_ptr))
1568 }
1569 .expect("Failed to create wgpu surface for CoreAnimationLayer");
1570
1571 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1572 power_preference: wgpu::PowerPreference::HighPerformance,
1573 force_fallback_adapter: false,
1574 compatible_surface: Some(&surface),
1575 }))
1576 .expect("No compatible GPU adapter found");
1577
1578 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1579 label: Some("scenevm-device"),
1580 required_features: wgpu::Features::empty(),
1581 required_limits: wgpu::Limits::default(),
1582 ..Default::default()
1583 }))
1584 .expect("Failed to create wgpu device");
1585
1586 let caps = surface.get_capabilities(&adapter);
1587 let surface_format = caps
1589 .formats
1590 .iter()
1591 .copied()
1592 .find(|f| !f.is_srgb())
1593 .unwrap_or(caps.formats[0]);
1594 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
1595 wgpu::PresentMode::Fifo
1596 } else {
1597 caps.present_modes[0]
1598 };
1599 let alpha_mode = caps
1600 .alpha_modes
1601 .get(0)
1602 .copied()
1603 .unwrap_or(wgpu::CompositeAlphaMode::Auto);
1604
1605 let surface_config = wgpu::SurfaceConfiguration {
1606 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
1607 format: surface_format,
1608 width,
1609 height,
1610 present_mode,
1611 alpha_mode,
1612 view_formats: vec![],
1613 desired_maximum_frame_latency: 2,
1614 };
1615 surface.configure(&device, &surface_config);
1616
1617 let mut storage_surface = Texture::new(width, height);
1618 storage_surface.ensure_gpu_with(&device);
1619
1620 let gpu = GPUState {
1621 _instance: instance,
1622 _adapter: adapter,
1623 device,
1624 queue,
1625 surface: storage_surface,
1626 window_surface: Some(WindowSurface {
1627 surface,
1628 config: surface_config,
1629 format: surface_format,
1630 present_pipeline: None,
1631 }),
1632 };
1633
1634 let atlas = SharedAtlas::new(4096, 4096);
1635 let mut this = Self {
1636 size: (width, height),
1637 gpu: Some(gpu),
1638 atlas: atlas.clone(),
1639 vm: VM::new_with_shared_atlas(atlas.clone()),
1640 overlay_vms: Vec::new(),
1641 active_vm_index: 0,
1642 log_layer_activity: false,
1643 compositing_pipeline: None,
1644 rgba_overlay_pipeline: None,
1645 rgba_overlay: None,
1646 stats_last_log: Instant::now(),
1647 stats_frames_since_log: 0,
1648 };
1649 this.refresh_layer_metadata();
1650 this
1651 }
1652
1653 pub async fn init_async(&mut self) {
1655 if self.gpu.is_some() {
1657 return;
1658 }
1659
1660 #[cfg(target_arch = "wasm32")]
1661 {
1662 if !self.needs_gpu_init {
1663 return;
1664 }
1665 if global_gpu_get().is_none() {
1666 global_gpu_init_async().await;
1667 }
1668 let gg = global_gpu_get().expect("Global GPU not initialized");
1669 let (w, h) = self.size;
1670 let mut surface = Texture::new(w, h);
1671 surface.ensure_gpu_with(&gg.device);
1672 let gpu = GPUState {
1673 _instance: gg.instance,
1674 _adapter: gg.adapter,
1675 device: gg.device,
1676 queue: gg.queue,
1677 surface,
1678 };
1679 self.gpu = Some(gpu);
1680 self.needs_gpu_init = false;
1681 #[cfg(debug_assertions)]
1682 {
1683 web_sys::console::log_1(&"SceneVM WebGPU initialized (global)".into());
1684 }
1685 }
1686
1687 #[cfg(not(target_arch = "wasm32"))]
1688 {
1689 if self.gpu.is_some() {
1690 return;
1691 }
1692 let (w, h) = self.size;
1693 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
1694 backends: { wgpu::Backends::all() },
1695 ..Default::default()
1696 });
1697 let adapter =
1698 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1699 power_preference: wgpu::PowerPreference::HighPerformance,
1700 force_fallback_adapter: false,
1701 compatible_surface: None,
1702 }))
1703 .expect("No compatible GPU adapter found");
1704
1705 let (device, queue) =
1706 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1707 label: Some("scenevm-device"),
1708 required_features: wgpu::Features::empty(),
1709 required_limits: wgpu::Limits::default(),
1710 ..Default::default()
1711 }))
1712 .expect("Failed to create wgpu device");
1713
1714 let mut surface = Texture::new(w, h);
1715 surface.ensure_gpu_with(&device);
1716
1717 let gpu = GPUState {
1718 _instance: instance,
1719 _adapter: adapter,
1720 device,
1721 queue,
1722 surface,
1723 window_surface: None,
1724 };
1725 self.gpu = Some(gpu);
1726 }
1727 }
1728
1729 pub fn blit_texture(
1731 &mut self,
1732 tex: &mut Texture,
1733 _cpu_pixels: &mut [u8],
1734 _buf_w: u32,
1735 _buf_h: u32,
1736 ) {
1737 if let Some(g) = self.gpu.as_ref() {
1738 tex.gpu_blit_to_storage(g, &g.surface.gpu.as_ref().unwrap().texture);
1739 }
1740 }
1741
1742 #[cfg(not(target_arch = "wasm32"))]
1744 pub fn resize_window_surface(&mut self, width: u32, height: u32) {
1745 let Some(gpu) = self.gpu.as_mut() else {
1746 return;
1747 };
1748 let Some(ws) = gpu.window_surface.as_mut() else {
1749 return;
1750 };
1751
1752 let w = width.max(1);
1753 let h = height.max(1);
1754 if ws.config.width == w && ws.config.height == h {
1755 return;
1756 }
1757
1758 ws.config.width = w;
1759 ws.config.height = h;
1760 ws.reconfigure(&gpu.device);
1761
1762 self.size = (w, h);
1763 gpu.surface.width = w;
1764 gpu.surface.height = h;
1765 gpu.surface.ensure_gpu_with(&gpu.device);
1766
1767 ws.present_pipeline = None;
1769 }
1770
1771 #[cfg(not(target_arch = "wasm32"))]
1773 pub fn render_to_window(&mut self) -> SceneVMResult<RenderResult> {
1774 let (gpu_slot, base_vm, overlays) = (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
1775 let Some(gpu) = gpu_slot.as_mut() else {
1776 return Err(SceneVMError::InvalidOperation(
1777 "GPU not initialized".to_string(),
1778 ));
1779 };
1780 let Some(ws) = gpu.window_surface.as_mut() else {
1781 return Err(SceneVMError::InvalidOperation(
1782 "No window surface configured".to_string(),
1783 ));
1784 };
1785
1786 let target_w = ws.config.width.max(1);
1787 let target_h = ws.config.height.max(1);
1788
1789 if self.size != (target_w, target_h) {
1790 self.size = (target_w, target_h);
1791 gpu.surface.width = target_w;
1792 gpu.surface.height = target_h;
1793 gpu.surface.ensure_gpu_with(&gpu.device);
1794 ws.present_pipeline = None;
1795 }
1796
1797 let (w, h) = self.size;
1798 SceneVM::draw_all_vms(
1799 base_vm,
1800 overlays,
1801 &gpu.device,
1802 &gpu.queue,
1803 &mut gpu.surface,
1804 w,
1805 h,
1806 self.log_layer_activity,
1807 &mut self.compositing_pipeline,
1808 &mut self.rgba_overlay,
1809 &mut self.rgba_overlay_pipeline,
1810 false,
1811 );
1812 SceneVM::maybe_log_runtime_stats(
1813 self.log_layer_activity,
1814 base_vm,
1815 overlays,
1816 &mut self.stats_last_log,
1817 &mut self.stats_frames_since_log,
1818 );
1819
1820 let frame = match ws.surface.get_current_texture() {
1821 Ok(frame) => frame,
1822 Err(wgpu::SurfaceError::Lost) | Err(wgpu::SurfaceError::Outdated) => {
1823 ws.reconfigure(&gpu.device);
1824 return Ok(RenderResult::InitPending);
1825 }
1826 Err(wgpu::SurfaceError::Timeout) => {
1827 return Ok(RenderResult::ReadbackPending);
1828 }
1829 Err(wgpu::SurfaceError::Other) => {
1830 return Err(SceneVMError::InvalidOperation(
1831 "Surface returned an unspecified error".to_string(),
1832 ));
1833 }
1834 Err(wgpu::SurfaceError::OutOfMemory) => {
1835 return Err(SceneVMError::BufferAllocationFailed(
1836 "Surface out of memory".to_string(),
1837 ));
1838 }
1839 };
1840
1841 let frame_view = frame
1842 .texture
1843 .create_view(&wgpu::TextureViewDescriptor::default());
1844 let src_view = gpu
1845 .surface
1846 .gpu
1847 .as_ref()
1848 .expect("Surface GPU not allocated")
1849 .view
1850 .clone();
1851 let (overlay_view, overlay_rect_px): (wgpu::TextureView, [f32; 4]) =
1852 if let Some(overlay) = self.rgba_overlay.as_mut() {
1853 overlay.texture.ensure_gpu_with(&gpu.device);
1854 overlay.texture.upload_to_gpu_with(&gpu.device, &gpu.queue);
1855 if let Some(overlay_gpu) = overlay.texture.gpu.as_ref() {
1856 (overlay_gpu.view.clone(), overlay.rect)
1857 } else {
1858 (src_view.clone(), [0.0, 0.0, 0.0, 0.0])
1859 }
1860 } else {
1861 (src_view.clone(), [0.0, 0.0, 0.0, 0.0])
1862 };
1863 let fw = ws.config.width.max(1) as f32;
1864 let fh = ws.config.height.max(1) as f32;
1865 let overlay_rect = [
1866 overlay_rect_px[0] / fw,
1867 overlay_rect_px[1] / fh,
1868 overlay_rect_px[2] / fw,
1869 overlay_rect_px[3] / fh,
1870 ];
1871
1872 if ws
1873 .present_pipeline
1874 .as_ref()
1875 .map(|p| p.surface_format != ws.format)
1876 .unwrap_or(true)
1877 {
1878 ws.present_pipeline = Some(PresentPipeline::new(
1879 &gpu.device,
1880 &gpu.queue,
1881 ws.format,
1882 &src_view,
1883 &overlay_view,
1884 overlay_rect,
1885 ));
1886 } else if let Some(pipeline) = ws.present_pipeline.as_mut() {
1887 pipeline.update_bind_group(
1888 &gpu.device,
1889 &gpu.queue,
1890 &src_view,
1891 &overlay_view,
1892 overlay_rect,
1893 );
1894 }
1895
1896 let present = ws
1897 .present_pipeline
1898 .as_ref()
1899 .expect("Present pipeline should be initialized");
1900
1901 let mut encoder = gpu
1902 .device
1903 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1904 label: Some("scenevm-present-encoder"),
1905 });
1906 {
1907 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1908 label: Some("scenevm-present-pass"),
1909 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1910 view: &frame_view,
1911 depth_slice: None,
1912 resolve_target: None,
1913 ops: wgpu::Operations {
1914 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1915 store: wgpu::StoreOp::Store,
1916 },
1917 })],
1918 depth_stencil_attachment: None,
1919 occlusion_query_set: None,
1920 timestamp_writes: None,
1921 });
1922 pass.set_pipeline(&present.pipeline);
1923 pass.set_bind_group(0, &present.bind_group, &[]);
1924 pass.draw(0..3, 0..1);
1925 }
1926 gpu.queue.submit(std::iter::once(encoder.finish()));
1927 frame.present();
1928
1929 Ok(RenderResult::Presented)
1930 }
1931
1932 #[cfg(not(target_arch = "wasm32"))]
1934 fn draw(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
1935 let (gpu_slot, base_vm, overlays) = (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
1937 let Some(gpu) = gpu_slot.as_mut() else {
1938 return;
1939 };
1940
1941 let buffer_width = out_w;
1942 let buffer_height = out_h;
1943
1944 if self.size != (buffer_width, buffer_height) {
1946 self.size = (buffer_width, buffer_height);
1947 gpu.surface.width = buffer_width;
1948 gpu.surface.height = buffer_height;
1949 gpu.surface.ensure_gpu_with(&gpu.device);
1950 }
1951
1952 let (w, h) = self.size;
1953
1954 SceneVM::draw_all_vms(
1956 base_vm,
1957 overlays,
1958 &gpu.device,
1959 &gpu.queue,
1960 &mut gpu.surface,
1961 w,
1962 h,
1963 self.log_layer_activity,
1964 &mut self.compositing_pipeline,
1965 &mut self.rgba_overlay,
1966 &mut self.rgba_overlay_pipeline,
1967 true,
1968 );
1969 SceneVM::maybe_log_runtime_stats(
1970 self.log_layer_activity,
1971 base_vm,
1972 overlays,
1973 &mut self.stats_last_log,
1974 &mut self.stats_frames_since_log,
1975 );
1976
1977 let device = gpu.device.clone();
1979 let queue = gpu.queue.clone();
1980 gpu.surface.download_from_gpu_with(&device, &queue);
1981
1982 gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
1985 }
1986
1987 #[cfg(target_arch = "wasm32")]
1989 pub async fn render_frame_async(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
1990 let (gpu_slot, base_vm, overlays) = (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
1991 let Some(gpu) = gpu_slot.as_mut() else {
1992 return;
1993 };
1994 let buffer_width = out_w;
1995 let buffer_height = out_h;
1996
1997 if self.size != (buffer_width, buffer_height) {
1998 self.size = (buffer_width, buffer_height);
1999 gpu.surface.width = buffer_width;
2000 gpu.surface.height = buffer_height;
2001 gpu.surface.ensure_gpu_with(&gpu.device);
2002 }
2003
2004 let (w, h) = self.size;
2005 SceneVM::draw_all_vms(
2006 base_vm,
2007 overlays,
2008 &gpu.device,
2009 &gpu.queue,
2010 &mut gpu.surface,
2011 w,
2012 h,
2013 self.log_layer_activity,
2014 &mut self.compositing_pipeline,
2015 &mut self.rgba_overlay,
2016 &mut self.rgba_overlay_pipeline,
2017 true,
2018 );
2019
2020 let device = gpu.device.clone();
2022 let queue = gpu.queue.clone();
2023 gpu.surface.download_from_gpu_with(&device, &queue);
2024 let flag = gpu
2025 .surface
2026 .gpu
2027 .as_ref()
2028 .and_then(|g| g.map_ready.as_ref().map(|f| std::rc::Rc::clone(f)));
2029 if let Some(flag) = flag {
2030 MapReadyFuture { flag }.await;
2031 }
2032 let _ = gpu.surface.try_finish_download_from_gpu();
2033 gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
2034 }
2035
2036 #[cfg(not(target_arch = "wasm32"))]
2038 pub async fn render_frame_async(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
2039 self.draw(out_pixels, out_w, out_h);
2040 }
2041
2042 pub fn render_frame(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) -> RenderResult {
2047 #[cfg(not(target_arch = "wasm32"))]
2050 {
2051 self.draw(out_pixels, out_w, out_h);
2053
2054 return RenderResult::Presented;
2058 }
2059
2060 #[cfg(target_arch = "wasm32")]
2061 {
2062 if self.gpu.is_none() {
2064 if !self.init_in_flight && self.needs_gpu_init {
2065 self.init_in_flight = true;
2066 let this: *mut SceneVM = self as *mut _;
2067 spawn_local(async move {
2068 unsafe {
2072 (&mut *this).init_async().await;
2073 (&mut *this).init_in_flight = false;
2074 }
2075 });
2076 }
2077 return RenderResult::InitPending;
2079 }
2080 let (gpu_slot, base_vm, overlays) =
2081 (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
2082 let gpu = gpu_slot.as_mut().unwrap();
2083
2084 if self.size != (out_w, out_h) {
2086 self.size = (out_w, out_h);
2087 gpu.surface.width = out_w;
2088 gpu.surface.height = out_h;
2089 gpu.surface.ensure_gpu_with(&gpu.device);
2090 }
2091
2092 let inflight = gpu
2094 .surface
2095 .gpu
2096 .as_ref()
2097 .and_then(|g| g.map_ready.as_ref())
2098 .is_some();
2099
2100 let mut presented_frame = false;
2104 if inflight {
2105 let ready = gpu.surface.try_finish_download_from_gpu();
2106 gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
2107 if !ready {
2108 return RenderResult::ReadbackPending;
2109 }
2110 presented_frame = true;
2111 } else {
2112 gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
2114 }
2115
2116 let (w, h) = self.size;
2118 SceneVM::draw_all_vms(
2119 base_vm,
2120 overlays,
2121 &gpu.device,
2122 &gpu.queue,
2123 &mut gpu.surface,
2124 w,
2125 h,
2126 self.log_layer_activity,
2127 &mut self.compositing_pipeline,
2128 &mut self.rgba_overlay,
2129 &mut self.rgba_overlay_pipeline,
2130 true,
2131 );
2132 SceneVM::maybe_log_runtime_stats(
2133 self.log_layer_activity,
2134 base_vm,
2135 overlays,
2136 &mut self.stats_last_log,
2137 &mut self.stats_frames_since_log,
2138 );
2139
2140 let device = gpu.device.clone();
2141 let queue = gpu.queue.clone();
2142 gpu.surface.download_from_gpu_with(&device, &queue);
2143
2144 if presented_frame {
2145 RenderResult::Presented
2146 } else {
2147 RenderResult::ReadbackPending
2148 }
2149 }
2150 }
2151
2152 pub fn load_image_rgba<I: IntoDataInput>(&self, input: I) -> Option<(Vec<u8>, u32, u32)> {
2154 let bytes = match input.load_data() {
2155 Ok(b) => b,
2156 Err(_) => return None,
2157 };
2158 let img = match image::load_from_memory(&bytes) {
2159 Ok(i) => i,
2160 Err(_) => return None,
2161 };
2162 let rgba = img.to_rgba8();
2163 let (w, h) = rgba.dimensions();
2164 Some((rgba.into_raw(), w, h))
2165 }
2166
2167 pub fn compile_shader_2d(&mut self, body_source: &str) -> ShaderCompilationResult {
2170 self.compile_shader_internal(body_source, true)
2171 }
2172
2173 pub fn compile_shader_3d(&mut self, body_source: &str) -> ShaderCompilationResult {
2176 self.compile_shader_internal(body_source, false)
2177 }
2178
2179 pub fn compile_shader_sdf(&mut self, body_source: &str) -> ShaderCompilationResult {
2182 use wgpu::ShaderSource;
2183
2184 let header_source = if let Some(bytes) = Embedded::get("sdf_header.wgsl") {
2185 std::str::from_utf8(bytes.data.as_ref())
2186 .unwrap_or("")
2187 .to_string()
2188 } else {
2189 "".to_string()
2190 };
2191
2192 let full_source = format!("{}\n{}", header_source, body_source);
2193
2194 let device = if let Some(gpu) = &self.gpu {
2195 &gpu.device
2196 } else {
2197 return ShaderCompilationResult {
2198 success: false,
2199 warnings: vec![],
2200 errors: vec![ShaderDiagnostic {
2201 line: 0,
2202 message: "GPU device not initialized. Cannot compile shader.".to_string(),
2203 }],
2204 };
2205 };
2206
2207 let _shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2208 label: Some("scenevm-compile-sdf"),
2209 source: ShaderSource::Wgsl(full_source.into()),
2210 });
2211
2212 self.vm
2213 .execute(vm::Atom::SetSourceSdf(body_source.to_string()));
2214
2215 ShaderCompilationResult {
2216 success: true,
2217 warnings: vec![],
2218 errors: vec![],
2219 }
2220 }
2221
2222 pub fn default_shader_source(kind: &str) -> Option<String> {
2224 let file_name = match kind {
2225 "ui" => "ui_body.wgsl",
2226 "2d" => "2d_body.wgsl",
2227 "3d" => "3d_body.wgsl",
2228 "sdf" => "sdf_body.wgsl",
2229 _ => return None,
2230 };
2231
2232 Embedded::get(file_name).map(|bytes| {
2233 String::from_utf8_lossy(bytes.data.as_ref()).into_owned()
2235 })
2236 }
2237
2238 fn compile_shader_internal(
2240 &mut self,
2241 body_source: &str,
2242 is_2d: bool,
2243 ) -> ShaderCompilationResult {
2244 use wgpu::ShaderSource;
2245
2246 let header_source = if is_2d {
2248 if let Some(bytes) = Embedded::get("2d_header.wgsl") {
2249 std::str::from_utf8(bytes.data.as_ref())
2250 .unwrap_or("")
2251 .to_string()
2252 } else {
2253 "".to_string()
2254 }
2255 } else {
2256 if let Some(bytes) = Embedded::get("3d_header.wgsl") {
2257 std::str::from_utf8(bytes.data.as_ref())
2258 .unwrap_or("")
2259 .to_string()
2260 } else {
2261 "".to_string()
2262 }
2263 };
2264
2265 let full_source = format!("{}\n{}", header_source, body_source);
2267
2268 let device = if let Some(gpu) = &self.gpu {
2270 &gpu.device
2272 } else {
2273 return ShaderCompilationResult {
2275 success: false,
2276 warnings: vec![],
2277 errors: vec![ShaderDiagnostic {
2278 line: 0,
2279 message: "GPU device not initialized. Cannot compile shader.".to_string(),
2280 }],
2281 };
2282 };
2283
2284 let _shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2285 label: Some(if is_2d {
2286 "scenevm-compile-2d"
2287 } else {
2288 "scenevm-compile-3d"
2289 }),
2290 source: ShaderSource::Wgsl(full_source.into()),
2291 });
2292
2293 let success = true; if success {
2303 if is_2d {
2305 self.vm
2306 .execute(vm::Atom::SetSource2D(body_source.to_string()));
2307 } else {
2308 self.vm
2309 .execute(vm::Atom::SetSource3D(body_source.to_string()));
2310 }
2311 }
2312
2313 ShaderCompilationResult {
2314 success,
2315 warnings: vec![], errors: vec![], }
2318 }
2319}
2320
2321#[cfg(target_arch = "wasm32")]
2323fn global_gpu_get() -> Option<GlobalGpu> {
2324 GLOBAL_GPU_WASM.with(|c| c.borrow().clone())
2325}
2326
2327#[cfg(target_arch = "wasm32")]
2328async fn global_gpu_init_async() {
2329 if global_gpu_get().is_some() {
2330 return;
2331 }
2332 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
2333 backends: wgpu::Backends::BROWSER_WEBGPU,
2334 ..Default::default()
2335 });
2336 let adapter = instance
2337 .request_adapter(&wgpu::RequestAdapterOptions {
2338 power_preference: wgpu::PowerPreference::HighPerformance,
2339 force_fallback_adapter: false,
2340 compatible_surface: None,
2341 })
2342 .await
2343 .expect("No compatible GPU adapter found (WebGPU)");
2344 let (device, queue) = adapter
2345 .request_device(&wgpu::DeviceDescriptor {
2346 label: Some("scenevm-device"),
2347 required_features: wgpu::Features::empty(),
2348 required_limits: wgpu::Limits::default(),
2349 ..Default::default()
2350 })
2351 .await
2352 .expect("Failed to create wgpu device (WebGPU)");
2353 let gg = GlobalGpu {
2354 instance,
2355 adapter,
2356 device,
2357 queue,
2358 };
2359 GLOBAL_GPU_WASM.with(|c| *c.borrow_mut() = Some(gg));
2360}
2361impl SceneVM {
2362 fn for_each_vm_mut(&mut self, mut f: impl FnMut(&mut VM)) {
2363 f(&mut self.vm);
2364 for vm in &mut self.overlay_vms {
2365 f(vm);
2366 }
2367 }
2368
2369 fn atom_touches_atlas(atom: &Atom) -> bool {
2370 matches!(
2371 atom,
2372 Atom::AddTile { .. }
2373 | Atom::AddSolid { .. }
2374 | Atom::SetTileMaterialFrames { .. }
2375 | Atom::BuildAtlas
2376 | Atom::Clear
2377 | Atom::ClearTiles
2378 )
2379 }
2380}
2381
2382#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
2387struct NativeRenderCtx {
2388 size: (u32, u32),
2389 last_result: RenderResult,
2390 present_called: bool,
2391}
2392
2393#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
2394impl NativeRenderCtx {
2395 fn new(size: (u32, u32)) -> Self {
2396 Self {
2397 size,
2398 last_result: RenderResult::InitPending,
2399 present_called: false,
2400 }
2401 }
2402
2403 fn begin_frame(&mut self) {
2404 self.present_called = false;
2405 }
2406
2407 fn ensure_presented(&mut self, vm: &mut SceneVM) -> SceneVMResult<RenderResult> {
2408 if !self.present_called {
2409 self.present(vm)?;
2410 }
2411 Ok(self.last_result)
2412 }
2413}
2414
2415#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
2416impl SceneVMRenderCtx for NativeRenderCtx {
2417 fn size(&self) -> (u32, u32) {
2418 self.size
2419 }
2420
2421 fn present(&mut self, vm: &mut SceneVM) -> SceneVMResult<RenderResult> {
2422 let res = vm.render_to_window();
2423 if let Ok(r) = res {
2424 self.last_result = r;
2425 }
2426 self.present_called = true;
2427 res
2428 }
2429}
2430
2431#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
2433pub fn run_scenevm_app<A: SceneVMApp + 'static>(
2434 mut app: A,
2435) -> Result<(), Box<dyn std::error::Error>> {
2436 use winit::dpi::LogicalSize;
2437 use winit::event::{Event, StartCause};
2438 use winit::event_loop::{ControlFlow, EventLoop};
2439 use winit::window::WindowAttributes;
2440
2441 let frame_interval = app.target_fps().and_then(|fps| {
2442 if fps > 0.0 {
2443 Some(std::time::Duration::from_secs_f32(1.0 / fps))
2444 } else {
2445 None
2446 }
2447 });
2448
2449 let event_loop = EventLoop::new()?;
2450 let mut window: Option<winit::window::Window> = None;
2451 let mut vm: Option<SceneVM> = None;
2452 let mut ctx: Option<NativeRenderCtx> = None;
2453 let mut cursor_pos: PhysicalPosition<f64> = PhysicalPosition { x: 0.0, y: 0.0 };
2454 let mut last_frame_at = std::time::Instant::now();
2455 #[cfg(feature = "ui")]
2456 let mut modifiers = winit::event::Modifiers::default();
2457 let apply_logical_scale = |vm_ref: &mut SceneVM, scale: f64| {
2458 let s = scale as f32;
2460 let m = Mat3::<f32>::new(s, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 1.0);
2461 vm_ref.execute(Atom::SetTransform2D(m));
2462 };
2463 #[allow(deprecated)]
2464 event_loop.run(move |event, target| match event {
2465 Event::NewEvents(StartCause::Init) => {
2466 let mut attrs = WindowAttributes::default()
2467 .with_title(app.window_title().unwrap_or_else(|| "SceneVM".to_string()));
2468 if let Some((w, h)) = app.initial_window_size() {
2469 attrs = attrs.with_inner_size(LogicalSize::new(w as f64, h as f64));
2470 }
2471 let win = target
2472 .create_window(attrs)
2473 .expect("failed to create window");
2474 win.set_cursor_visible(false);
2475 let size = win.inner_size();
2476 let scale = win.scale_factor();
2477 let logical = size.to_logical::<f64>(scale);
2478 let logical_size = (logical.width.round() as u32, logical.height.round() as u32);
2479 let mut new_vm = SceneVM::new_with_window(&win);
2480 apply_logical_scale(&mut new_vm, scale);
2481 let new_ctx = NativeRenderCtx::new(logical_size);
2482 app.set_scale(scale as f32);
2483 app.set_native_mode(true); app.init(&mut new_vm, logical_size);
2485 window = Some(win);
2486 vm = Some(new_vm);
2487 ctx = Some(new_ctx);
2488 target.set_control_flow(ControlFlow::Poll);
2489 }
2490 Event::WindowEvent { window_id, event } => {
2491 if let (Some(win), Some(vm_ref), Some(ctx_ref)) =
2492 (window.as_ref(), vm.as_mut(), ctx.as_mut())
2493 {
2494 if window_id == win.id() {
2495 match event {
2496 WindowEvent::CloseRequested => target.exit(),
2497 WindowEvent::Resized(size) => {
2498 let scale = win.scale_factor();
2499 let logical = size.to_logical::<f64>(scale);
2500 let logical_size =
2501 (logical.width.round() as u32, logical.height.round() as u32);
2502 ctx_ref.size = logical_size;
2503 vm_ref.resize_window_surface(size.width, size.height);
2504 apply_logical_scale(vm_ref, scale);
2505 app.set_scale(scale as f32);
2506 app.resize(vm_ref, logical_size);
2507 }
2508 WindowEvent::ScaleFactorChanged {
2509 scale_factor,
2510 mut inner_size_writer,
2511 } => {
2512 let size = win.inner_size();
2513 let _ = inner_size_writer.request_inner_size(size);
2514 let logical = size.to_logical::<f64>(scale_factor);
2515 let logical_size =
2516 (logical.width.round() as u32, logical.height.round() as u32);
2517 ctx_ref.size = logical_size;
2518 vm_ref.resize_window_surface(size.width, size.height);
2519 app.set_scale(scale_factor as f32);
2520 apply_logical_scale(vm_ref, scale_factor);
2521 }
2522 WindowEvent::CursorMoved { position, .. } => {
2523 cursor_pos = position;
2524 let scale = win.scale_factor() as f32;
2525 app.mouse_move(
2526 vm_ref,
2527 (cursor_pos.x as f32) / scale,
2528 (cursor_pos.y as f32) / scale,
2529 );
2530 }
2531 WindowEvent::MouseInput {
2532 state,
2533 button: MouseButton::Left,
2534 ..
2535 } => match state {
2536 ElementState::Pressed => {
2537 let scale = win.scale_factor() as f32;
2538 app.mouse_down(
2539 vm_ref,
2540 (cursor_pos.x as f32) / scale,
2541 (cursor_pos.y as f32) / scale,
2542 );
2543 }
2544 ElementState::Released => {
2545 let scale = win.scale_factor() as f32;
2546 app.mouse_up(
2547 vm_ref,
2548 (cursor_pos.x as f32) / scale,
2549 (cursor_pos.y as f32) / scale,
2550 );
2551 }
2552 },
2553 WindowEvent::MouseWheel { delta, .. } => {
2554 let (dx, dy) = match delta {
2555 winit::event::MouseScrollDelta::LineDelta(x, y) => {
2556 (x * 120.0, y * 120.0)
2557 }
2558 winit::event::MouseScrollDelta::PixelDelta(pos) => {
2559 (pos.x as f32, pos.y as f32)
2560 }
2561 };
2562 let scale = win.scale_factor() as f32;
2563 app.scroll(vm_ref, dx / scale, dy / scale);
2564 }
2565 WindowEvent::RedrawRequested => {
2566 if let Some(dt) = frame_interval {
2567 let now = std::time::Instant::now();
2568 if now.duration_since(last_frame_at) < dt {
2569 return;
2570 }
2571 last_frame_at = now;
2572 }
2573 if app.needs_update(vm_ref) {
2574 ctx_ref.begin_frame();
2575 app.update(vm_ref);
2576 let _ = app.render(vm_ref, ctx_ref);
2577 let _ = ctx_ref.ensure_presented(vm_ref);
2578
2579 #[cfg(feature = "ui")]
2581 {
2582 use crate::app_event::AppEvent;
2583 let events = app.take_app_events();
2584 for event in events {
2585 match event {
2586 AppEvent::RequestUndo => {
2587 app.undo(vm_ref);
2588 }
2589 AppEvent::RequestRedo => {
2590 app.redo(vm_ref);
2591 }
2592 AppEvent::RequestExport { format, filename } => {
2593 #[cfg(all(
2594 not(target_arch = "wasm32"),
2595 not(target_os = "ios")
2596 ))]
2597 {
2598 crate::native_dialogs::handle_export(
2599 &mut app, vm_ref, &format, &filename,
2600 );
2601 }
2602 }
2603 AppEvent::RequestSave {
2604 filename,
2605 extension,
2606 } => {
2607 #[cfg(all(
2608 not(target_arch = "wasm32"),
2609 not(target_os = "ios")
2610 ))]
2611 {
2612 crate::native_dialogs::handle_save(
2613 &mut app, vm_ref, &filename, &extension,
2614 );
2615 }
2616 }
2617 AppEvent::RequestOpen { extension } => {
2618 #[cfg(all(
2619 not(target_arch = "wasm32"),
2620 not(target_os = "ios")
2621 ))]
2622 {
2623 crate::native_dialogs::handle_open(
2624 &mut app, vm_ref, &extension,
2625 );
2626 }
2627 }
2628 AppEvent::RequestImport { file_types } => {
2629 #[cfg(all(
2630 not(target_arch = "wasm32"),
2631 not(target_os = "ios")
2632 ))]
2633 {
2634 crate::native_dialogs::handle_import(
2635 &mut app,
2636 vm_ref,
2637 &file_types,
2638 );
2639 }
2640 }
2641 _ => {
2642 }
2644 }
2645 }
2646 }
2647 }
2648 }
2649 #[cfg(feature = "ui")]
2650 WindowEvent::ModifiersChanged(new_modifiers) => {
2651 modifiers = new_modifiers;
2652 }
2653 WindowEvent::KeyboardInput { event, .. } => {
2654 use winit::keyboard::{Key, NamedKey};
2655 #[cfg(feature = "ui")]
2656 use winit::keyboard::{KeyCode, PhysicalKey};
2657
2658 let key = match &event.logical_key {
2659 Key::Character(text) => text.to_lowercase(),
2660 Key::Named(NamedKey::ArrowUp) => "up".to_string(),
2661 Key::Named(NamedKey::ArrowDown) => "down".to_string(),
2662 Key::Named(NamedKey::ArrowLeft) => "left".to_string(),
2663 Key::Named(NamedKey::ArrowRight) => "right".to_string(),
2664 Key::Named(NamedKey::Space) => "space".to_string(),
2665 Key::Named(NamedKey::Enter) => "enter".to_string(),
2666 Key::Named(NamedKey::Tab) => "tab".to_string(),
2667 Key::Named(NamedKey::Escape) => "escape".to_string(),
2668 _ => String::new(),
2669 };
2670 if !key.is_empty() {
2671 match event.state {
2672 ElementState::Pressed => app.key_down(vm_ref, &key),
2673 ElementState::Released => app.key_up(vm_ref, &key),
2674 }
2675 }
2676
2677 #[cfg(feature = "ui")]
2678 if event.state == ElementState::Pressed {
2679 if event.physical_key == PhysicalKey::Code(KeyCode::KeyZ) {
2681 #[cfg(target_os = "macos")]
2682 let cmd_pressed = modifiers.state().super_key();
2683 #[cfg(not(target_os = "macos"))]
2684 let cmd_pressed = modifiers.state().control_key();
2685
2686 if cmd_pressed && !modifiers.state().shift_key() {
2687 app.undo(vm_ref);
2689 } else if cmd_pressed && modifiers.state().shift_key() {
2690 app.redo(vm_ref);
2692 }
2693 }
2694 #[cfg(not(target_os = "macos"))]
2696 if event.physical_key == PhysicalKey::Code(KeyCode::KeyY) {
2697 if modifiers.state().control_key() {
2698 app.redo(vm_ref);
2699 }
2700 }
2701 }
2702 }
2703 _ => {}
2704 }
2705 }
2706 }
2707 }
2708 Event::AboutToWait => {
2709 if let (Some(win), Some(vm_ref)) = (window.as_ref(), vm.as_mut()) {
2710 let wants_frame = app.needs_update(vm_ref);
2711 if let Some(dt) = frame_interval {
2712 let next = std::time::Instant::now() + dt;
2713 target.set_control_flow(ControlFlow::WaitUntil(next));
2714 if wants_frame {
2715 win.request_redraw();
2716 }
2717 } else if wants_frame {
2718 target.set_control_flow(ControlFlow::Poll);
2719 win.request_redraw();
2720 } else {
2721 target.set_control_flow(ControlFlow::Wait);
2722 }
2723 }
2724 }
2725 _ => {}
2726 })?;
2727 #[allow(unreachable_code)]
2728 Ok(())
2729}
2730
2731#[cfg(target_arch = "wasm32")]
2732struct WasmRenderCtx {
2733 buffer: Vec<u8>,
2734 width: u32,
2735 height: u32,
2736 canvas: HtmlCanvasElement,
2737 ctx: CanvasRenderingContext2d,
2738 pending_present: bool,
2740}
2741
2742#[cfg(target_arch = "wasm32")]
2743impl WasmRenderCtx {
2744 fn resize(&mut self, width: u32, height: u32) {
2745 if width == 0 || height == 0 {
2746 return;
2747 }
2748 self.width = width;
2749 self.height = height;
2750 self.canvas.set_width(width);
2751 self.canvas.set_height(height);
2752 self.buffer.resize((width * height * 4) as usize, 0);
2753 }
2754}
2755
2756#[cfg(target_arch = "wasm32")]
2757impl SceneVMRenderCtx for WasmRenderCtx {
2758 fn size(&self) -> (u32, u32) {
2759 (self.width, self.height)
2760 }
2761
2762 fn present(&mut self, vm: &mut SceneVM) -> SceneVMResult<RenderResult> {
2763 let mut res = vm.render_frame(&mut self.buffer, self.width, self.height);
2764
2765 if res != RenderResult::Presented {
2767 if let Some(gpu) = vm.gpu.as_mut() {
2768 let ready = gpu.surface.try_finish_download_from_gpu();
2769 if ready {
2770 res = RenderResult::Presented;
2771 }
2772 }
2773 }
2774
2775 let clamped = wasm_bindgen::Clamped(&self.buffer[..]);
2777 let image_data =
2778 web_sys::ImageData::new_with_u8_clamped_array_and_sh(clamped, self.width, self.height)
2779 .map_err(|e| SceneVMError::InvalidOperation(format!("{:?}", e)))?;
2780 self.ctx
2781 .put_image_data(&image_data, 0.0, 0.0)
2782 .map_err(|e| SceneVMError::InvalidOperation(format!("{:?}", e)))?;
2783
2784 self.pending_present = res != RenderResult::Presented;
2785 Ok(res)
2786 }
2787}
2788
2789#[cfg(target_arch = "wasm32")]
2790fn create_or_get_canvas(document: &Document) -> Result<HtmlCanvasElement, JsValue> {
2791 if let Some(existing) = document
2792 .get_element_by_id("canvas")
2793 .and_then(|el| el.dyn_into::<HtmlCanvasElement>().ok())
2794 {
2795 return Ok(existing);
2796 }
2797 let canvas: HtmlCanvasElement = document
2798 .create_element("canvas")?
2799 .dyn_into::<HtmlCanvasElement>()?;
2800 document
2801 .body()
2802 .ok_or_else(|| JsValue::from_str("no body"))?
2803 .append_child(&canvas)?;
2804 Ok(canvas)
2805}
2806
2807#[cfg(target_arch = "wasm32")]
2809pub fn run_scenevm_app<A: SceneVMApp + 'static>(mut app: A) -> Result<(), JsValue> {
2810 let window: WebWindow = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
2811 let document = window
2812 .document()
2813 .ok_or_else(|| JsValue::from_str("no document"))?;
2814 let canvas = create_or_get_canvas(&document)?;
2815
2816 let (width, height) = app.initial_window_size().unwrap_or_else(|| {
2817 let w = window
2818 .inner_width()
2819 .ok()
2820 .and_then(|v| v.as_f64())
2821 .unwrap_or(800.0)
2822 .round() as u32;
2823 let h = window
2824 .inner_height()
2825 .ok()
2826 .and_then(|v| v.as_f64())
2827 .unwrap_or(600.0)
2828 .round() as u32;
2829 (w, h)
2830 });
2831 canvas.set_width(width);
2832 canvas.set_height(height);
2833
2834 let ctx = canvas
2835 .get_context("2d")?
2836 .ok_or_else(|| JsValue::from_str("2d context missing"))?
2837 .dyn_into::<CanvasRenderingContext2d>()?;
2838
2839 let mut vm = SceneVM::new(width, height);
2840 let render_ctx = WasmRenderCtx {
2841 buffer: vec![0u8; (width * height * 4) as usize],
2842 width,
2843 height,
2844 canvas,
2845 ctx,
2846 pending_present: true, };
2848 app.init(&mut vm, (width, height));
2849
2850 let app_rc = Rc::new(RefCell::new(app));
2851 let vm_rc = Rc::new(RefCell::new(vm));
2852 let ctx_rc = Rc::new(RefCell::new(render_ctx));
2853 let first_frame = Rc::new(Cell::new(true));
2854
2855 {
2857 let app = Rc::clone(&app_rc);
2858 let vm = Rc::clone(&vm_rc);
2859 let ctx = Rc::clone(&ctx_rc);
2860 let window_resize = window.clone();
2861 let resize_closure = Closure::<dyn FnMut()>::new(move || {
2862 if let (Ok(w), Ok(h)) = (window_resize.inner_width(), window_resize.inner_height()) {
2863 let w = w.as_f64().unwrap_or(800.0).round() as u32;
2864 let h = h.as_f64().unwrap_or(600.0).round() as u32;
2865 ctx.borrow_mut().resize(w, h);
2866 app.borrow_mut().resize(&mut vm.borrow_mut(), (w, h));
2867 }
2868 });
2869 window
2870 .add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?;
2871 resize_closure.forget();
2872 }
2873
2874 {
2876 let app = Rc::clone(&app_rc);
2877 let vm = Rc::clone(&vm_rc);
2878 let canvas = ctx_rc.borrow().canvas.clone();
2879 let down_closure =
2880 Closure::<dyn FnMut(web_sys::PointerEvent)>::new(move |e: web_sys::PointerEvent| {
2881 let rect = canvas.get_bounding_client_rect();
2882 let x = e.client_x() as f64 - rect.left();
2883 let y = e.client_y() as f64 - rect.top();
2884 app.borrow_mut()
2885 .mouse_down(&mut vm.borrow_mut(), x as f32, y as f32);
2886 });
2887 ctx_rc.borrow().canvas.add_event_listener_with_callback(
2888 "pointerdown",
2889 down_closure.as_ref().unchecked_ref(),
2890 )?;
2891 down_closure.forget();
2892 }
2893
2894 {
2896 let app = Rc::clone(&app_rc);
2897 let vm = Rc::clone(&vm_rc);
2898 let ctx = Rc::clone(&ctx_rc);
2899 let first = Rc::clone(&first_frame);
2900 let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
2901 let f_clone = Rc::clone(&f);
2902 let window_clone = window.clone();
2903 *f.borrow_mut() = Some(Closure::<dyn FnMut()>::new(move || {
2904 {
2905 let mut app_mut = app.borrow_mut();
2906 let mut vm_mut = vm.borrow_mut();
2907 let ctx_pending = ctx.borrow().pending_present;
2908 let do_render = app_mut.needs_update(&vm_mut) || first.get() || ctx_pending;
2909 if do_render {
2910 first.set(false);
2911 app_mut.update(&mut vm_mut);
2912 app_mut.render(&mut vm_mut, &mut *ctx.borrow_mut());
2913 }
2914 }
2915 let _ = window_clone.request_animation_frame(
2916 f_clone.borrow().as_ref().unwrap().as_ref().unchecked_ref(),
2917 );
2918 }));
2919 let _ =
2920 window.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref());
2921 }
2922 Ok(())
2923}
2924
2925#[cfg(all(
2929 not(target_arch = "wasm32"),
2930 any(target_os = "macos", target_os = "ios")
2931))]
2932#[unsafe(no_mangle)]
2933pub unsafe extern "C" fn scenevm_ca_create(
2934 layer_ptr: *mut c_void,
2935 width: u32,
2936 height: u32,
2937) -> *mut SceneVM {
2938 if layer_ptr.is_null() {
2939 return std::ptr::null_mut();
2940 }
2941 let vm = SceneVM::new_with_metal_layer(layer_ptr, width, height);
2942 Box::into_raw(Box::new(vm))
2943}
2944
2945#[cfg(all(
2946 not(target_arch = "wasm32"),
2947 any(target_os = "macos", target_os = "ios")
2948))]
2949#[unsafe(no_mangle)]
2950pub unsafe extern "C" fn scenevm_ca_destroy(ptr: *mut SceneVM) {
2951 if ptr.is_null() {
2952 return;
2953 }
2954 unsafe {
2955 drop(Box::from_raw(ptr));
2956 }
2957}
2958
2959#[cfg(all(
2960 not(target_arch = "wasm32"),
2961 any(target_os = "macos", target_os = "ios")
2962))]
2963#[unsafe(no_mangle)]
2964pub unsafe extern "C" fn scenevm_ca_resize(ptr: *mut SceneVM, width: u32, height: u32) {
2965 if let Some(vm) = unsafe { ptr.as_mut() } {
2966 vm.resize_window_surface(width, height);
2967 }
2968}
2969
2970#[cfg(all(
2971 not(target_arch = "wasm32"),
2972 any(target_os = "macos", target_os = "ios")
2973))]
2974#[unsafe(no_mangle)]
2975pub unsafe extern "C" fn scenevm_ca_render(ptr: *mut SceneVM) -> i32 {
2976 if let Some(vm) = unsafe { ptr.as_mut() } {
2977 match vm.render_to_window() {
2978 Ok(RenderResult::Presented) => 0,
2979 Ok(RenderResult::InitPending) => 1,
2980 Ok(RenderResult::ReadbackPending) => 2,
2981 Err(_) => -1,
2982 }
2983 } else {
2984 -1
2985 }
2986}