Skip to main content

scenevm/
lib.rs

1// pub mod app;
2#[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/// Error types for SceneVM operations
22#[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// Embedded shader/assets payload used at runtime by SceneVM.
38#[derive(RustEmbed)]
39#[folder = "embedded/"]
40#[exclude = "*.txt"]
41#[exclude = "*.DS_Store"]
42pub struct Embedded;
43
44pub mod prelude {
45    //! Prelude module with commonly used types for SceneVM applications
46
47    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/// Result of a call to `render_frame`.
137#[derive(Copy, Clone, Debug, Eq, PartialEq)]
138pub enum RenderResult {
139    /// We copied pixels to the caller's buffer this call (may still have a new frame in flight on WASM)
140    Presented,
141    /// On WASM: GPU init not finished; nothing rendered yet.
142    InitPending,
143    /// On WASM: a GPU readback is in flight; we presented the last completed frame this call.
144    ReadbackPending,
145}
146
147/// Render pipeline that blits the SceneVM storage texture into a window surface.
148#[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
158/// Compositing pipeline for blending VM layers with alpha
159struct 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                    // UI overlay pixels are composited on CPU-style paths with premultiplied-like RGB.
425                    // Composite as premultiplied alpha to match legacy output and avoid white/gray wash.
426                    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/// Optional window surface (swapchain) managed by SceneVM for direct presentation.
468#[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    /// Main render surface for SceneVM
482    surface: Texture,
483    /// Optional wgpu surface when presenting directly to a window.
484    #[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// --- WASM async map flag future support ---
755#[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            // Re-schedule ourselves to be polled again soon.
768            cx.waker().wake_by_ref();
769            Poll::Pending
770        }
771    }
772}
773
774pub struct SceneVM {
775    /// The intended render target size; used by either backend.
776    size: (u32, u32),
777
778    /// When `Some`, GPU rendering is enabled and initialized; otherwise CPU path.
779    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/// Result of shader compilation with detailed diagnostics
798#[derive(Debug, Clone)]
799pub struct ShaderCompilationResult {
800    /// Whether compilation succeeded (true if only warnings, false if errors)
801    pub success: bool,
802    /// List of compilation warnings with line numbers relative to body source
803    pub warnings: Vec<ShaderDiagnostic>,
804    /// List of compilation errors with line numbers relative to body source
805    pub errors: Vec<ShaderDiagnostic>,
806}
807
808/// Individual shader diagnostic (warning or error)
809#[derive(Debug, Clone)]
810pub struct ShaderDiagnostic {
811    /// Line number in the body source (0-based)
812    pub line: u32,
813    /// Diagnostic message
814    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        // The surface texture is always created with Rgba8Unorm in `Texture::ensure_gpu_with`
868        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        // Ensure surface has GPU resources
884        surface.ensure_gpu_with(device);
885
886        // Initialize compositing pipeline if needed
887        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        // Create command encoder for compositing
898        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
899            label: Some("scenevm-compositing-encoder"),
900        });
901
902        // Composite all layers onto the surface
903        let surface_view = &surface.gpu.as_ref().unwrap().view;
904
905        // Collect all VMs that are enabled
906        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        // Composite each layer in order
917        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                    // Create bind group for this layer
921                    let mode_u32: u32 = match vm.blend_mode() {
922                        vm::LayerBlendMode::Alpha => 0,
923                        vm::LayerBlendMode::AlphaLinear => 1,
924                    };
925                    // Upload mode
926                    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                    // Begin render pass
948                    // First layer: clear surface to black (layer texture has background baked in)
949                    // Subsequent layers: load existing content and blend on top
950                    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    /// Total number of VM layers (base + overlays).
1097    pub fn vm_layer_count(&self) -> usize {
1098        self.total_vm_count()
1099    }
1100
1101    /// Append a new VM layer that will render on top of the existing ones. Returns its layer index.
1102    pub fn add_vm_layer(&mut self) -> usize {
1103        // Overlays default to transparent so they don't hide layers below unless drawn into.
1104        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    /// Remove a VM layer by index (cannot remove the base layer at index 0).
1112    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    /// Switch the VM layer targeted by `execute`. Returns `true` if the index existed.
1129    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    /// Index of the currently active VM used by `execute`.
1139    pub fn active_vm_index(&self) -> usize {
1140        self.active_vm_index
1141    }
1142
1143    /// Normalized atlas rect (ofs.x, ofs.y, scale.x, scale.y) for a tile/frame, useful for SDF packing.
1144    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    /// Enable or disable drawing for a VM layer. Disabled layers still receive commands.
1149    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    /// Returns whether a VM layer is enabled.
1159    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    /// Toggle verbose per-layer logging for uploads/atlas/grid events.
1164    pub fn set_layer_activity_logging(&mut self, enabled: bool) {
1165        self.log_layer_activity = enabled;
1166        self.refresh_layer_metadata();
1167    }
1168
1169    /// Borrow the currently active VM immutably.
1170    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    /// Borrow the currently active VM mutably.
1176    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    /// Ray-pick against the active VM layer using normalized screen UVs.
1182    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    /// Build a world-space ray from screen uv (0..1) using the active VM's camera and a provided framebuffer size.
1200    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    /// Build a world-space ray from screen uv (0..1) using the active VM's camera and the current SceneVM size.
1210    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    /// Prints statistics about 2D and 3D polygons currently loaded in all chunks.
1216    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    /// Executes a single atom on the currently active VM layer.
1326    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    /// Is the GPU initialized and ready?
1340    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    /// Is a GPU readback currently in flight (WASM only)? Always false on native.
1355    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    /// Create a new SceneVM. Always uses GPU backend.
1374    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    /// Create a SceneVM that is configured to present directly into a winit window surface.
1453    #[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        // Prefer non-sRGB swapchain when scene output is already tonemapped/gamma-encoded.
1487        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    /// Create a SceneVM that presents into an existing CoreAnimation layer (Metal) without winit.
1553    #[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        // Prefer non-sRGB swapchain when scene output is already tonemapped/gamma-encoded.
1588        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    /// Initialize GPU backend asynchronously on WASM. On native, this will initialize synchronously if not already.
1654    pub async fn init_async(&mut self) {
1655        // If already initialized, nothing to do.
1656        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    /// Blit a `Texture` via GPU to the main surface texture, if GPU is ready.
1730    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    /// Update the window surface size and internal storage texture (native only).
1743    #[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        // Force recreation of the present pipeline/bindings on next render.
1768        ws.present_pipeline = None;
1769    }
1770
1771    /// Render directly into the configured window surface (native only, no CPU readback).
1772    #[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    /// Draw: if GPU is present, run the compute path. Returns immediately if GPU is not yet ready (WASM before init).
1933    #[cfg(not(target_arch = "wasm32"))]
1934    fn draw(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
1935        // GPU-only: do nothing if GPU is not ready (e.g., WASM before init)
1936        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        // Resize surface if needed (bind group managed internally by VM)
1945        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        // Delegate rendering to all VM layers in order (each overlays the previous result)
1955        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        // Readback into the surface's CPU memory (blocking on native, non-blocking noop on wasm)
1978        let device = gpu.device.clone();
1979        let queue = gpu.queue.clone();
1980        gpu.surface.download_from_gpu_with(&device, &queue);
1981
1982        // On native, pixels are now in `surface.data`; copy them to the output buffer.
1983        // On WASM, if you need the pixels immediately, prefer `draw_async`.
1984        gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
1985    }
1986
1987    /// Cross-platform async render: same call on native & WASM.
1988    #[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        // Start readback and await readiness
2021        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    /// Single cross-platform async entrypoint for rendering a frame.
2037    #[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    /// Cross-platform synchronous render entrypoint (one function for Native & WASM). Returns a RenderResult.
2043    /// Native: blocks until pixels are ready. WASM: presents the last completed frame
2044    /// and kicks off a new GPU frame if none is in flight. Call this every frame.
2045    /// On WASM, you must call `init_async().await` once before rendering.
2046    pub fn render_frame(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) -> RenderResult {
2047        // let start = std::time::Instant::now();
2048
2049        #[cfg(not(target_arch = "wasm32"))]
2050        {
2051            // Native path just does the full render and readback synchronously
2052            self.draw(out_pixels, out_w, out_h);
2053
2054            // let elapsed = start.elapsed();
2055            // println!("Frame time: {:.2}ms", elapsed.as_secs_f64() * 1000.0);
2056
2057            return RenderResult::Presented;
2058        }
2059
2060        #[cfg(target_arch = "wasm32")]
2061        {
2062            // WASM path: auto-init GPU if needed, else non-blocking render logic.
2063            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                        // SAFETY: we rely on the caller to call `render_frame` from the UI thread.
2069                        // We only flip flags and build GPU state; no aliasing mutable accesses occur concurrently
2070                        // because the user code keeps calling `render_frame`, which is single-threaded on wasm.
2071                        unsafe {
2072                            (&mut *this).init_async().await;
2073                            (&mut *this).init_in_flight = false;
2074                        }
2075                    });
2076                }
2077                // Nothing to render until init finishes; return quietly.
2078                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            // Ensure surface size (bind group managed internally by VM)
2085            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            // If a readback is already in flight, try to finish it; otherwise kick off a new one.
2093            let inflight = gpu
2094                .surface
2095                .gpu
2096                .as_ref()
2097                .and_then(|g| g.map_ready.as_ref())
2098                .is_some();
2099
2100            // If a readback is in flight, try to finish it and present whatever CPU pixels we have.
2101            // When the download completes, continue on to kick off the next frame immediately
2102            // instead of skipping a render for a whole call (which caused visible stutter on WASM).
2103            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                // No download in flight yet; present whatever pixels are already on the CPU.
2113                gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
2114            }
2115
2116            // Render a new frame and start a download for the next call.
2117            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    /// Load an image from various inputs (file path on native, raw bytes, &str) and decode to RGBA8.
2153    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    /// Compile a 2D body shader with the header and return detailed diagnostics.
2168    /// If compilation succeeds (only warnings), the shader is automatically set as active.
2169    pub fn compile_shader_2d(&mut self, body_source: &str) -> ShaderCompilationResult {
2170        self.compile_shader_internal(body_source, true)
2171    }
2172
2173    /// Compile a 3D body shader with the header and return detailed diagnostics.
2174    /// If compilation succeeds (only warnings), the shader is automatically set as active.
2175    pub fn compile_shader_3d(&mut self, body_source: &str) -> ShaderCompilationResult {
2176        self.compile_shader_internal(body_source, false)
2177    }
2178
2179    /// Compile an SDF body shader with the header and return detailed diagnostics.
2180    /// If compilation succeeds (only warnings), the shader is automatically set as active.
2181    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    /// Fetch the source of a built-in shader body by name (e.g. "ui", "2d", "3d", "sdf", "noise").
2223    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            // Convert embedded bytes to owned string; avoids borrowing the embedded buffer.
2234            String::from_utf8_lossy(bytes.data.as_ref()).into_owned()
2235        })
2236    }
2237
2238    /// Internal shader compilation with diagnostics
2239    fn compile_shader_internal(
2240        &mut self,
2241        body_source: &str,
2242        is_2d: bool,
2243    ) -> ShaderCompilationResult {
2244        use wgpu::ShaderSource;
2245
2246        // Get the appropriate header
2247        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        // Combine header and body
2266        let full_source = format!("{}\n{}", header_source, body_source);
2267
2268        // Try to create shader module to trigger compilation
2269        let device = if let Some(gpu) = &self.gpu {
2270            // We have a device from previous initialization
2271            &gpu.device
2272        } else {
2273            // No device available, return compilation failure
2274            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        // Note: wgpu doesn't provide direct access to compilation warnings/errors at module creation.
2294        // The compilation happens asynchronously and errors surface when the pipeline is created.
2295        // For now, we'll assume success if the module was created without panic.
2296        // In a real implementation, you'd want to use wgpu's validation layers or compile offline.
2297
2298        // For the purpose of this implementation, we'll simulate successful compilation
2299        // and set the source if we got this far without panic
2300        let success = true; // Module creation succeeded
2301
2302        if success {
2303            // Set the source if compilation succeeded
2304            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![], // Currently empty - would be populated with real compilation info
2316            errors: vec![],   // Currently empty - would be populated with real compilation info
2317        }
2318    }
2319}
2320
2321// --- Global GPU helpers ---
2322#[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// -------------------------
2383// Minimal cross-platform app runner
2384// -------------------------
2385
2386#[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/// Run a `SceneVMApp` on native (winit) with GPU presentation to a window.
2432#[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        // Scale logical coordinates into the framebuffer when hi-dpi.
2459        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); // Native wgpu runner
2484            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                                // Handle app events after render
2580                                #[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                                                // Other events
2643                                            }
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                                // Check for Cmd/Ctrl+Z (Undo)
2680                                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                                        // Undo: Cmd+Z (macOS) or Ctrl+Z (other platforms)
2688                                        app.undo(vm_ref);
2689                                    } else if cmd_pressed && modifiers.state().shift_key() {
2690                                        // Redo: Cmd+Shift+Z (macOS) or Ctrl+Shift+Z (other platforms)
2691                                        app.redo(vm_ref);
2692                                    }
2693                                }
2694                                // Check for Ctrl+Y (Redo on Windows/Linux)
2695                                #[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    /// True when the last render was not fully presented (Init/Readback pending).
2739    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 the frame wasn't ready yet, try to finish the readback immediately.
2766        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        // Always blit whatever pixels we have (latest completed frame).
2776        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/// Run a `SceneVMApp` in the browser using a canvas + ImageData blit.
2808#[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, // force initial render until Presented lands
2847    };
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    // Resize handler
2856    {
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    // Pointer down handler
2875    {
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    // Animation loop
2895    {
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// -------------------------
2926// C FFI for CoreAnimation layer (Metal) presentation (macOS/iOS)
2927// -------------------------
2928#[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}