Skip to main content

scenevm/
lib.rs

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