#[cfg(feature = "ui")]
pub mod app_event;
#[cfg(feature = "gpu")]
pub mod app_trait;
pub mod atlas;
pub mod bbox2d;
pub mod camera3d;
pub mod chunk;
pub mod core;
pub mod dynamic;
pub mod intodata;
pub mod light;
#[cfg(all(feature = "ui", not(target_arch = "wasm32")))]
pub mod native_dialogs;
pub mod poly2d;
pub mod poly3d;
pub mod texture;
#[cfg(feature = "ui")]
pub mod ui;
#[cfg(feature = "gpu")]
pub mod vm;
#[derive(Debug, Clone)]
pub enum SceneVMError {
GpuInitFailed(String),
BufferAllocationFailed(String),
ShaderCompilationFailed(String),
TextureUploadFailed(String),
InvalidGeometry(String),
AtlasFull(String),
InvalidOperation(String),
}
pub type SceneVMResult<T> = Result<T, SceneVMError>;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "embedded/"]
#[exclude = "*.txt"]
#[exclude = "*.DS_Store"]
pub struct Embedded;
pub mod prelude {
pub use crate::{
Embedded, SceneVMError, SceneVMResult,
atlas::{AtlasEntry, SharedAtlas},
bbox2d::BBox2D,
camera3d::{Camera3D, CameraKind},
chunk::Chunk,
core::{
Atom, GeoId, LayerBlendMode, LineStrip2D, PaletteRemap2DMode, RenderMode, VMDebugStats,
},
dynamic::{AlphaMode, DynamicKind, DynamicMeshVertex, DynamicObject, RepeatMode},
intodata::IntoDataInput,
light::{Light, LightType},
poly2d::Poly2D,
poly3d::{OrganicSurfaceDetail, Poly3D},
texture::Texture,
};
#[cfg(feature = "gpu")]
pub use crate::vm::VM;
#[cfg(feature = "gpu")]
pub use crate::app_trait::{SceneVMApp, SceneVMRenderCtx};
#[cfg(all(feature = "ui", feature = "gpu"))]
pub use crate::{
RenderResult,
app_event::{AppEvent, AppEventQueue},
ui::{
Alignment, Button, ButtonGroup, ButtonGroupOrientation, ButtonGroupStyle, ButtonKind,
ButtonStyle, Canvas, ColorButton, ColorButtonStyle, ColorWheel, Drawable, DropdownList,
DropdownListStyle, HAlign, HStack, Image, ImageStyle, Label, LabelRect, NodeId,
ParamList, ParamListStyle, PopupAlignment, Project, ProjectBrowser, ProjectBrowserItem,
ProjectBrowserStyle, ProjectError, ProjectMetadata, RecentProject, RecentProjects,
Slider, SliderStyle, Spacer, TabbedPanel, TabbedPanelStyle, TextButton, Theme, Toolbar,
ToolbarOrientation, ToolbarSeparator, ToolbarStyle, UiAction, UiEvent, UiEventKind,
UiRenderer, UiView, UndoCommand, UndoStack, VAlign, VStack, ViewContext, Workspace,
create_tile_material,
},
};
pub use rustc_hash::{FxHashMap, FxHashSet};
pub use vek::{Mat3, Mat4, Vec2, Vec3, Vec4};
}
#[cfg(feature = "gpu")]
pub use crate::app_trait::{SceneVMApp, SceneVMRenderCtx};
#[cfg(feature = "ui")]
pub use crate::ui::{
Alignment, Button, ButtonGroup, ButtonGroupStyle, ButtonKind, ButtonStyle, Canvas, Drawable,
HAlign, HStack, Image, ImageStyle, Label, LabelRect, NodeId, ParamList, ParamListStyle,
PopupAlignment, Slider, SliderStyle, TextButton, Toolbar, ToolbarOrientation, ToolbarSeparator,
ToolbarStyle, UiAction, UiEvent, UiEventKind, UiRenderer, UiView, UndoCommand, UndoStack,
VAlign, VStack, ViewContext, Workspace,
};
#[cfg(feature = "gpu")]
pub use crate::vm::VM;
pub use crate::{
atlas::{AtlasEntry, SharedAtlas},
bbox2d::BBox2D,
camera3d::{Camera3D, CameraKind},
chunk::Chunk,
core::{
Atom, GeoId, LayerBlendMode, LineStrip2D, PaletteRemap2DMode, RenderMode, VMDebugStats,
},
dynamic::{AlphaMode, DynamicKind, DynamicMeshVertex, DynamicObject, RepeatMode},
intodata::IntoDataInput,
light::{Light, LightType},
poly2d::Poly2D,
poly3d::{OrganicSurfaceDetail, Poly3D},
texture::Texture,
};
#[cfg(feature = "gpu")]
use image;
#[cfg(feature = "gpu")]
use std::borrow::Cow;
#[cfg(target_arch = "wasm32")]
use std::cell::RefCell;
#[cfg(all(not(target_arch = "wasm32"), feature = "gpu"))]
use std::ffi::c_void;
#[cfg(all(not(target_arch = "wasm32"), feature = "gpu"))]
use std::sync::OnceLock;
#[cfg(target_arch = "wasm32")]
use std::{cell::Cell, future::Future, rc::Rc};
#[cfg(target_arch = "wasm32")]
use std::{
pin::Pin,
task::{Context, Poll},
};
#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
use vek::Mat3;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
#[cfg(target_arch = "wasm32")]
use web_sys::{CanvasRenderingContext2d, Document, HtmlCanvasElement, Window as WebWindow};
#[cfg(all(feature = "windowing", not(target_arch = "wasm32")))]
use winit::window::Window;
#[cfg(all(feature = "windowing", not(target_arch = "wasm32")))]
use winit::{dpi::PhysicalPosition, event::ElementState, event::MouseButton, event::WindowEvent};
#[cfg(feature = "gpu")]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RenderResult {
Presented,
InitPending,
ReadbackPending,
}
#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
struct PresentPipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
bind_group: wgpu::BindGroup,
rect_buf: wgpu::Buffer,
sampler: wgpu::Sampler,
surface_format: wgpu::TextureFormat,
}
#[cfg(feature = "gpu")]
struct CompositingPipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
mode_buf: wgpu::Buffer,
sampler: wgpu::Sampler,
target_format: wgpu::TextureFormat,
}
#[cfg(feature = "gpu")]
impl CompositingPipeline {
fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scenevm-composite-shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
"
@group(0) @binding(0) var layer_tex: texture_2d<f32>;
@group(0) @binding(1) var layer_sampler: sampler;
@group(0) @binding(2) var<uniform> blend_mode_buf: u32;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
var positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(3.0, 1.0),
vec2<f32>(-1.0, 1.0)
);
var uvs = array<vec2<f32>, 3>(
vec2<f32>(0.0, 2.0),
vec2<f32>(2.0, 0.0),
vec2<f32>(0.0, 0.0)
);
var out: VsOut;
out.pos = vec4<f32>(positions[vi], 0.0, 1.0);
out.uv = uvs[vi];
return out;
}
fn linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
return pow(c, vec3<f32>(1.0 / 2.2));
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let src = textureSample(layer_tex, layer_sampler, in.uv);
if (blend_mode_buf == 1u) {
return vec4<f32>(linear_to_srgb(src.rgb), src.a);
}
return src;
}
",
)),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("scenevm-composite-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("scenevm-composite-pipeline-layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let mode_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scenevm-composite-mode"),
size: std::mem::size_of::<u32>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("scenevm-composite-sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("scenevm-composite-pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
pipeline,
bind_group_layout,
mode_buf,
sampler,
target_format,
}
}
}
#[cfg(feature = "gpu")]
struct RgbaOverlayCompositingPipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
rect_buf: wgpu::Buffer,
sampler: wgpu::Sampler,
target_format: wgpu::TextureFormat,
}
#[cfg(feature = "gpu")]
impl RgbaOverlayCompositingPipeline {
fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scenevm-rgba-overlay-shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
"
@group(0) @binding(0) var overlay_tex: texture_2d<f32>;
@group(0) @binding(1) var overlay_sampler: sampler;
@group(0) @binding(2) var<uniform> rect: vec4<f32>;
struct VsOut {
@builtin(position) pos: vec4<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
var positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(3.0, 1.0),
vec2<f32>(-1.0, 1.0)
);
var out: VsOut;
out.pos = vec4<f32>(positions[vi], 0.0, 1.0);
return out;
}
@fragment
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let x = pos.x;
let y = pos.y;
if (x < rect.x || y < rect.y || x >= (rect.x + rect.z) || y >= (rect.y + rect.w)) {
return vec4<f32>(0.0);
}
let uv = vec2<f32>((x - rect.x) / max(rect.z, 1.0), (y - rect.y) / max(rect.w, 1.0));
return textureSample(overlay_tex, overlay_sampler, uv);
}
",
)),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("scenevm-rgba-overlay-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("scenevm-rgba-overlay-pipeline-layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let rect_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scenevm-rgba-overlay-rect"),
size: (std::mem::size_of::<f32>() * 4) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("scenevm-rgba-overlay-sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("scenevm-rgba-overlay-pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
pipeline,
bind_group_layout,
rect_buf,
sampler,
target_format,
}
}
}
#[cfg(feature = "gpu")]
struct RgbaOverlayState {
texture: Texture,
rect: [f32; 4],
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "gpu")]
struct WindowSurface {
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
format: wgpu::TextureFormat,
present_pipeline: Option<PresentPipeline>,
}
#[cfg(feature = "gpu")]
pub struct GPUState {
_instance: wgpu::Instance,
_adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
surface: Texture,
#[cfg(not(target_arch = "wasm32"))]
window_surface: Option<WindowSurface>,
}
#[allow(dead_code)]
#[derive(Clone)]
#[cfg(feature = "gpu")]
struct GlobalGpu {
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
#[allow(dead_code)]
#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
static GLOBAL_GPU: OnceLock<GlobalGpu> = OnceLock::new();
#[cfg(all(feature = "gpu", target_arch = "wasm32"))]
thread_local! {
static GLOBAL_GPU_WASM: RefCell<Option<GlobalGpu>> = RefCell::new(None);
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "gpu")]
impl PresentPipeline {
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
source_view: &wgpu::TextureView,
overlay_view: &wgpu::TextureView,
overlay_rect: [f32; 4],
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scenevm-present-shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
"
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@group(0) @binding(1) var src_sampler: sampler;
@group(0) @binding(2) var overlay_tex: texture_2d<f32>;
@group(0) @binding(3) var overlay_sampler: sampler;
@group(0) @binding(4) var<uniform> overlay_rect: vec4<f32>;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
var positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(3.0, 1.0),
vec2<f32>(-1.0, 1.0)
);
var uvs = array<vec2<f32>, 3>(
vec2<f32>(0.0, 2.0),
vec2<f32>(2.0, 0.0),
vec2<f32>(0.0, 0.0)
);
var out: VsOut;
out.pos = vec4<f32>(positions[vi], 0.0, 1.0);
out.uv = uvs[vi];
return out;
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let base = textureSample(src_tex, src_sampler, in.uv);
if (overlay_rect.z <= 0.0 || overlay_rect.w <= 0.0) {
return base;
}
let x = in.uv.x;
let y = in.uv.y;
if (x < overlay_rect.x || y < overlay_rect.y || x >= (overlay_rect.x + overlay_rect.z) || y >= (overlay_rect.y + overlay_rect.w)) {
return base;
}
let uv = vec2<f32>((x - overlay_rect.x) / max(overlay_rect.z, 1e-6), (y - overlay_rect.y) / max(overlay_rect.w, 1e-6));
let over = textureSample(overlay_tex, overlay_sampler, uv);
// Premultiplied alpha over operation.
let out_rgb = over.rgb + base.rgb * (1.0 - over.a);
let out_a = over.a + base.a * (1.0 - over.a);
return vec4<f32>(out_rgb, out_a);
}
",
)),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("scenevm-present-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("scenevm-present-sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let rect_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scenevm-present-overlay-rect"),
size: (std::mem::size_of::<f32>() * 4) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&rect_buf, 0, bytemuck::cast_slice(&overlay_rect));
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scenevm-present-bind-group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(overlay_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 4,
resource: rect_buf.as_entire_binding(),
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("scenevm-present-pipeline-layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("scenevm-present-pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
pipeline,
bind_group_layout,
bind_group,
rect_buf,
sampler,
surface_format: format,
}
}
fn update_bind_group(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
source_view: &wgpu::TextureView,
overlay_view: &wgpu::TextureView,
overlay_rect: [f32; 4],
) {
queue.write_buffer(&self.rect_buf, 0, bytemuck::cast_slice(&overlay_rect));
self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scenevm-present-bind-group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(overlay_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 4,
resource: self.rect_buf.as_entire_binding(),
},
],
});
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "gpu")]
impl WindowSurface {
fn reconfigure(&mut self, device: &wgpu::Device) {
self.surface.configure(device, &self.config);
}
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "gpu")]
struct MapReadyFuture {
flag: Rc<Cell<bool>>,
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "gpu")]
impl Future for MapReadyFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.flag.get() {
Poll::Ready(())
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
#[cfg(feature = "gpu")]
pub struct SceneVM {
size: (u32, u32),
gpu: Option<GPUState>,
#[cfg(target_arch = "wasm32")]
needs_gpu_init: bool,
#[cfg(target_arch = "wasm32")]
init_in_flight: bool,
atlas: SharedAtlas,
pub vm: VM,
overlay_vms: Vec<VM>,
active_vm_index: usize,
log_layer_activity: bool,
compositing_pipeline: Option<CompositingPipeline>,
rgba_overlay_pipeline: Option<RgbaOverlayCompositingPipeline>,
rgba_overlay: Option<RgbaOverlayState>,
}
#[derive(Debug, Clone)]
pub struct ShaderCompilationResult {
pub success: bool,
pub warnings: Vec<ShaderDiagnostic>,
pub errors: Vec<ShaderDiagnostic>,
}
#[derive(Debug, Clone)]
pub struct ShaderDiagnostic {
pub line: u32,
pub message: String,
}
#[cfg(feature = "gpu")]
impl Default for SceneVM {
fn default() -> Self {
Self::new(100, 100)
}
}
#[cfg(feature = "gpu")]
impl SceneVM {
fn refresh_layer_metadata(&mut self) {
self.vm.set_layer_index(0);
self.vm.set_activity_logging(self.log_layer_activity);
for (i, vm) in self.overlay_vms.iter_mut().enumerate() {
vm.set_layer_index(i + 1);
vm.set_activity_logging(self.log_layer_activity);
}
}
fn total_vm_count(&self) -> usize {
1 + self.overlay_vms.len()
}
fn vm_ref_by_index(&self, index: usize) -> Option<&VM> {
if index == 0 {
Some(&self.vm)
} else {
self.overlay_vms.get(index.saturating_sub(1))
}
}
fn vm_mut_by_index(&mut self, index: usize) -> Option<&mut VM> {
if index == 0 {
Some(&mut self.vm)
} else {
self.overlay_vms.get_mut(index.saturating_sub(1))
}
}
fn draw_all_vms(
base_vm: &mut VM,
overlays: &mut [VM],
device: &wgpu::Device,
queue: &wgpu::Queue,
surface: &mut Texture,
w: u32,
h: u32,
log_errors: bool,
compositing_pipeline: &mut Option<CompositingPipeline>,
rgba_overlay: &mut Option<RgbaOverlayState>,
rgba_overlay_pipeline: &mut Option<RgbaOverlayCompositingPipeline>,
composite_rgba_overlay_in_scene: bool,
) {
let target_format = wgpu::TextureFormat::Rgba8Unorm;
if let Err(e) = base_vm.draw_into(device, queue, surface, w, h) {
if log_errors {
println!("[SceneVM] Error drawing base VM: {:?}", e);
}
}
for vm in overlays.iter_mut() {
if let Err(e) = vm.draw_into(device, queue, surface, w, h) {
if log_errors {
println!("[SceneVM] Error drawing overlay VM: {:?}", e);
}
}
}
surface.ensure_gpu_with(device);
if compositing_pipeline
.as_ref()
.map(|p| p.target_format != target_format)
.unwrap_or(true)
{
*compositing_pipeline = Some(CompositingPipeline::new(device, target_format));
}
let pipeline = compositing_pipeline.as_ref().unwrap();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("scenevm-compositing-encoder"),
});
let surface_view = &surface.gpu.as_ref().unwrap().view;
let mut vms_to_composite: Vec<&VM> = Vec::new();
if base_vm.is_enabled() {
vms_to_composite.push(base_vm);
}
for vm in overlays.iter() {
if vm.is_enabled() {
vms_to_composite.push(vm);
}
}
for (i, vm) in vms_to_composite.iter().enumerate() {
if let Some(layer_texture) = vm.composite_texture() {
if let Some(layer_gpu) = &layer_texture.gpu {
let mode_u32: u32 = match vm.blend_mode() {
LayerBlendMode::Alpha => 0,
LayerBlendMode::AlphaLinear => 1,
};
queue.write_buffer(&pipeline.mode_buf, 0, bytemuck::bytes_of(&mode_u32));
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scenevm-compositing-bind-group"),
layout: &pipeline.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&layer_gpu.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: pipeline.mode_buf.as_entire_binding(),
},
],
});
let load_op = if i == 0 {
wgpu::LoadOp::Clear(wgpu::Color::BLACK)
} else {
wgpu::LoadOp::Load
};
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scenevm-compositing-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&pipeline.pipeline);
rpass.set_bind_group(0, &bind_group, &[]);
rpass.draw(0..3, 0..1);
}
}
}
}
if composite_rgba_overlay_in_scene && let Some(overlay) = rgba_overlay.as_mut() {
overlay.texture.ensure_gpu_with(device);
overlay.texture.upload_to_gpu_with(device, queue);
if let Some(overlay_gpu) = overlay.texture.gpu.as_ref() {
if rgba_overlay_pipeline
.as_ref()
.map(|p| p.target_format != target_format)
.unwrap_or(true)
{
*rgba_overlay_pipeline =
Some(RgbaOverlayCompositingPipeline::new(device, target_format));
}
let pipeline = rgba_overlay_pipeline.as_ref().unwrap();
queue.write_buffer(&pipeline.rect_buf, 0, bytemuck::cast_slice(&overlay.rect));
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scenevm-rgba-overlay-bind-group"),
layout: &pipeline.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&overlay_gpu.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: pipeline.rect_buf.as_entire_binding(),
},
],
});
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scenevm-rgba-overlay-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&pipeline.pipeline);
rpass.set_bind_group(0, &bind_group, &[]);
rpass.draw(0..3, 0..1);
}
}
}
queue.submit(Some(encoder.finish()));
}
pub fn set_rgba_overlay(&mut self, width: u32, height: u32, rgba: Vec<u8>, rect: [f32; 4]) {
let w = width.max(1);
let h = height.max(1);
let needed = (w as usize) * (h as usize) * 4;
let mut data = rgba;
if data.len() < needed {
data.resize(needed, 0);
}
if data.len() > needed {
data.truncate(needed);
}
match self.rgba_overlay.as_mut() {
Some(existing) if existing.texture.width == w && existing.texture.height == h => {
existing.texture.data = data;
existing.rect = rect;
}
_ => {
let mut texture = Texture::new(w, h);
texture.data = data;
self.rgba_overlay = Some(RgbaOverlayState { texture, rect });
}
}
}
pub fn set_rgba_overlay_bytes(&mut self, width: u32, height: u32, rgba: &[u8], rect: [f32; 4]) {
let w = width.max(1);
let h = height.max(1);
let needed = (w as usize) * (h as usize) * 4;
match self.rgba_overlay.as_mut() {
Some(existing) if existing.texture.width == w && existing.texture.height == h => {
existing.texture.data.resize(needed, 0);
let copy_len = rgba.len().min(needed);
existing.texture.data[..copy_len].copy_from_slice(&rgba[..copy_len]);
if copy_len < needed {
existing.texture.data[copy_len..].fill(0);
}
existing.rect = rect;
}
_ => {
let mut texture = Texture::new(w, h);
texture.data.resize(needed, 0);
let copy_len = rgba.len().min(needed);
texture.data[..copy_len].copy_from_slice(&rgba[..copy_len]);
self.rgba_overlay = Some(RgbaOverlayState { texture, rect });
}
}
}
pub fn clear_rgba_overlay(&mut self) {
self.rgba_overlay = None;
}
pub fn vm_layer_count(&self) -> usize {
self.total_vm_count()
}
pub fn add_vm_layer(&mut self) -> usize {
let mut vm = VM::new_with_shared_atlas(self.atlas.clone());
vm.background = vek::Vec4::new(0.0, 0.0, 0.0, 0.0);
self.overlay_vms.push(vm);
self.refresh_layer_metadata();
self.total_vm_count() - 1
}
pub fn remove_vm_layer(&mut self, index: usize) -> Option<VM> {
if index == 0 {
return None;
}
let idx = index - 1;
if idx >= self.overlay_vms.len() {
return None;
}
let removed = self.overlay_vms.remove(idx);
if self.active_vm_index >= self.total_vm_count() {
self.active_vm_index = self.total_vm_count().saturating_sub(1);
}
self.refresh_layer_metadata();
Some(removed)
}
pub fn set_active_vm(&mut self, index: usize) -> bool {
if index < self.total_vm_count() {
self.active_vm_index = index;
true
} else {
false
}
}
pub fn active_vm_index(&self) -> usize {
self.active_vm_index
}
pub fn atlas_sdf_uv4(&self, id: &uuid::Uuid, anim_frame: u32) -> Option<[f32; 4]> {
self.atlas.sdf_uv4(id, anim_frame)
}
pub fn atlas_dims(&self) -> (u32, u32) {
self.atlas.dims()
}
pub fn set_layer_enabled(&mut self, index: usize, enabled: bool) -> bool {
if let Some(vm) = self.vm_mut_by_index(index) {
vm.set_enabled(enabled);
true
} else {
false
}
}
pub fn is_layer_enabled(&self, index: usize) -> Option<bool> {
self.vm_ref_by_index(index).map(|vm| vm.is_enabled())
}
pub fn set_layer_activity_logging(&mut self, enabled: bool) {
self.log_layer_activity = enabled;
self.refresh_layer_metadata();
}
pub fn active_vm(&self) -> &VM {
self.vm_ref_by_index(self.active_vm_index)
.expect("active VM index out of range")
}
pub fn active_vm_mut(&mut self) -> &mut VM {
self.vm_mut_by_index(self.active_vm_index)
.expect("active VM index out of range")
}
pub fn pick_geo_id_at_uv(
&self,
fb_w: u32,
fb_h: u32,
screen_uv: [f32; 2],
include_hidden: bool,
include_billboards: bool,
) -> Option<(GeoId, vek::Vec3<f32>, f32)> {
self.active_vm().pick_geo_id_at_uv(
fb_w,
fb_h,
screen_uv,
include_hidden,
include_billboards,
)
}
pub fn ray_from_uv_with_size(
&self,
fb_w: u32,
fb_h: u32,
screen_uv: [f32; 2],
) -> Option<(vek::Vec3<f32>, vek::Vec3<f32>)> {
self.active_vm().ray_from_uv(fb_w, fb_h, screen_uv)
}
pub fn ray_from_uv(&self, screen_uv: [f32; 2]) -> Option<(vek::Vec3<f32>, vek::Vec3<f32>)> {
let (w, h) = self.size;
self.active_vm().ray_from_uv(w, h, screen_uv)
}
pub fn print_geometry_stats(&self) {
let mut total_2d = 0usize;
let mut total_3d = 0usize;
let mut total_lines = 0usize;
for vm in std::iter::once(&self.vm).chain(self.overlay_vms.iter()) {
for (_cid, ch) in &vm.chunks_map {
total_2d += ch.polys_map.len();
total_3d += ch.polys3d_map.values().map(|v| v.len()).sum::<usize>();
total_lines += ch.lines2d_px.len();
}
}
println!(
"[SceneVM] Geometry Stats → 2D polys: {} | 3D polys: {} | 2D lines: {} | Total: {}",
total_2d,
total_3d,
total_lines,
total_2d + total_3d + total_lines
);
}
pub fn execute(&mut self, atom: Atom) {
let affects_atlas = SceneVM::atom_touches_atlas(&atom);
let active = self.active_vm_index;
if active == 0 {
self.vm.execute(atom);
} else if let Some(vm) = self.vm_mut_by_index(active) {
vm.execute(atom);
}
if affects_atlas {
self.for_each_vm_mut(|vm| vm.mark_all_geometry_dirty());
}
}
pub fn is_gpu_ready(&self) -> bool {
if self.gpu.is_some() {
#[cfg(target_arch = "wasm32")]
{
return !self.needs_gpu_init && !self.init_in_flight;
}
#[cfg(not(target_arch = "wasm32"))]
{
return true;
}
}
false
}
pub fn frame_in_flight(&self) -> bool {
#[cfg(target_arch = "wasm32")]
{
if let Some(gpu) = &self.gpu {
return gpu
.surface
.gpu
.as_ref()
.and_then(|g| g.map_ready.as_ref())
.is_some();
}
return false;
}
#[cfg(not(target_arch = "wasm32"))]
{
false
}
}
pub fn new(initial_width: u32, initial_height: u32) -> Self {
#[cfg(target_arch = "wasm32")]
{
let atlas = SharedAtlas::new(4096, 4096);
let mut this = Self {
size: (initial_width, initial_height),
gpu: None,
needs_gpu_init: true,
init_in_flight: false,
atlas: atlas.clone(),
vm: VM::new_with_shared_atlas(atlas.clone()),
overlay_vms: Vec::new(),
active_vm_index: 0,
log_layer_activity: false,
compositing_pipeline: None,
rgba_overlay_pipeline: None,
rgba_overlay: None,
};
this.refresh_layer_metadata();
this
}
#[cfg(not(target_arch = "wasm32"))]
{
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: { wgpu::Backends::all() },
..Default::default()
});
let adapter =
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
}))
.expect("No compatible GPU adapter found");
let (device, queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("scenevm-device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("Failed to create wgpu device");
let mut surface = Texture::new(initial_width, initial_height);
surface.ensure_gpu_with(&device);
let gpu = GPUState {
_instance: instance,
_adapter: adapter,
device,
queue,
surface,
window_surface: None,
};
let atlas = SharedAtlas::new(4096, 4096);
let mut this = Self {
size: (initial_width, initial_height),
gpu: Some(gpu),
atlas: atlas.clone(),
vm: VM::new_with_shared_atlas(atlas.clone()),
overlay_vms: Vec::new(),
active_vm_index: 0,
log_layer_activity: false,
compositing_pipeline: None,
rgba_overlay_pipeline: None,
rgba_overlay: None,
};
this.refresh_layer_metadata();
this
}
}
#[cfg(all(feature = "windowing", not(target_arch = "wasm32")))]
pub fn new_with_window(window: &Window) -> Self {
let initial_size = window.inner_size();
let width = initial_size.width.max(1);
let height = initial_size.height.max(1);
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: { wgpu::Backends::all() },
..Default::default()
});
let surface = unsafe {
instance.create_surface_unsafe(
wgpu::SurfaceTargetUnsafe::from_window(window)
.expect("Failed to access raw window handle"),
)
}
.expect("Failed to create wgpu surface for window");
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: Some(&surface),
}))
.expect("No compatible GPU adapter found");
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("scenevm-device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("Failed to create wgpu device");
let caps = surface.get_capabilities(&adapter);
let surface_format = caps
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(caps.formats[0]);
let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
wgpu::PresentMode::Fifo
} else {
caps.present_modes[0]
};
let alpha_mode = caps
.alpha_modes
.get(0)
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Auto);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: surface_format,
width,
height,
present_mode,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
let mut storage_surface = Texture::new(width, height);
storage_surface.ensure_gpu_with(&device);
let gpu = GPUState {
_instance: instance,
_adapter: adapter,
device,
queue,
surface: storage_surface,
window_surface: Some(WindowSurface {
surface,
config: surface_config,
format: surface_format,
present_pipeline: None,
}),
};
let atlas = SharedAtlas::new(4096, 4096);
let mut this = Self {
size: (width, height),
gpu: Some(gpu),
atlas: atlas.clone(),
vm: VM::new_with_shared_atlas(atlas.clone()),
overlay_vms: Vec::new(),
active_vm_index: 0,
log_layer_activity: false,
compositing_pipeline: None,
rgba_overlay_pipeline: None,
rgba_overlay: None,
};
this.refresh_layer_metadata();
this
}
#[cfg(all(
not(target_arch = "wasm32"),
any(target_os = "macos", target_os = "ios")
))]
pub fn new_with_metal_layer(layer_ptr: *mut c_void, width: u32, height: u32) -> Self {
let width = width.max(1);
let height = height.max(1);
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: { wgpu::Backends::all() },
..Default::default()
});
let surface = unsafe {
instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer_ptr))
}
.expect("Failed to create wgpu surface for CoreAnimationLayer");
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: Some(&surface),
}))
.expect("No compatible GPU adapter found");
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("scenevm-device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("Failed to create wgpu device");
let caps = surface.get_capabilities(&adapter);
let surface_format = caps
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(caps.formats[0]);
let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
wgpu::PresentMode::Fifo
} else {
caps.present_modes[0]
};
let alpha_mode = caps
.alpha_modes
.get(0)
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Auto);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: surface_format,
width,
height,
present_mode,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
let mut storage_surface = Texture::new(width, height);
storage_surface.ensure_gpu_with(&device);
let gpu = GPUState {
_instance: instance,
_adapter: adapter,
device,
queue,
surface: storage_surface,
window_surface: Some(WindowSurface {
surface,
config: surface_config,
format: surface_format,
present_pipeline: None,
}),
};
let atlas = SharedAtlas::new(4096, 4096);
let mut this = Self {
size: (width, height),
gpu: Some(gpu),
atlas: atlas.clone(),
vm: VM::new_with_shared_atlas(atlas.clone()),
overlay_vms: Vec::new(),
active_vm_index: 0,
log_layer_activity: false,
compositing_pipeline: None,
rgba_overlay_pipeline: None,
rgba_overlay: None,
};
this.refresh_layer_metadata();
this
}
pub async fn init_async(&mut self) {
if self.gpu.is_some() {
return;
}
#[cfg(target_arch = "wasm32")]
{
if !self.needs_gpu_init {
return;
}
if global_gpu_get().is_none() {
global_gpu_init_async().await;
}
let gg = global_gpu_get().expect("Global GPU not initialized");
let (w, h) = self.size;
let mut surface = Texture::new(w, h);
surface.ensure_gpu_with(&gg.device);
let gpu = GPUState {
_instance: gg.instance,
_adapter: gg.adapter,
device: gg.device,
queue: gg.queue,
surface,
};
self.gpu = Some(gpu);
self.needs_gpu_init = false;
#[cfg(debug_assertions)]
{
web_sys::console::log_1(&"SceneVM WebGPU initialized (global)".into());
}
}
#[cfg(not(target_arch = "wasm32"))]
{
if self.gpu.is_some() {
return;
}
let (w, h) = self.size;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: { wgpu::Backends::all() },
..Default::default()
});
let adapter =
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
}))
.expect("No compatible GPU adapter found");
let (device, queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("scenevm-device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("Failed to create wgpu device");
let mut surface = Texture::new(w, h);
surface.ensure_gpu_with(&device);
let gpu = GPUState {
_instance: instance,
_adapter: adapter,
device,
queue,
surface,
window_surface: None,
};
self.gpu = Some(gpu);
}
}
pub fn blit_texture(
&mut self,
tex: &mut Texture,
_cpu_pixels: &mut [u8],
_buf_w: u32,
_buf_h: u32,
) {
if let Some(g) = self.gpu.as_ref() {
tex.gpu_blit_to_storage(g, &g.surface.gpu.as_ref().unwrap().texture);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn resize_window_surface(&mut self, width: u32, height: u32) {
let Some(gpu) = self.gpu.as_mut() else {
return;
};
let Some(ws) = gpu.window_surface.as_mut() else {
return;
};
let w = width.max(1);
let h = height.max(1);
if ws.config.width == w && ws.config.height == h {
return;
}
ws.config.width = w;
ws.config.height = h;
ws.reconfigure(&gpu.device);
self.size = (w, h);
gpu.surface.width = w;
gpu.surface.height = h;
gpu.surface.ensure_gpu_with(&gpu.device);
ws.present_pipeline = None;
}
#[cfg(not(target_arch = "wasm32"))]
pub fn render_to_window(&mut self) -> SceneVMResult<RenderResult> {
let (gpu_slot, base_vm, overlays) = (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
let Some(gpu) = gpu_slot.as_mut() else {
return Err(SceneVMError::InvalidOperation(
"GPU not initialized".to_string(),
));
};
let Some(ws) = gpu.window_surface.as_mut() else {
return Err(SceneVMError::InvalidOperation(
"No window surface configured".to_string(),
));
};
let target_w = ws.config.width.max(1);
let target_h = ws.config.height.max(1);
if self.size != (target_w, target_h) {
self.size = (target_w, target_h);
gpu.surface.width = target_w;
gpu.surface.height = target_h;
gpu.surface.ensure_gpu_with(&gpu.device);
ws.present_pipeline = None;
}
let (w, h) = self.size;
SceneVM::draw_all_vms(
base_vm,
overlays,
&gpu.device,
&gpu.queue,
&mut gpu.surface,
w,
h,
self.log_layer_activity,
&mut self.compositing_pipeline,
&mut self.rgba_overlay,
&mut self.rgba_overlay_pipeline,
false,
);
let frame = match ws.surface.get_current_texture() {
Ok(frame) => frame,
Err(wgpu::SurfaceError::Lost) | Err(wgpu::SurfaceError::Outdated) => {
ws.reconfigure(&gpu.device);
return Ok(RenderResult::InitPending);
}
Err(wgpu::SurfaceError::Timeout) => {
return Ok(RenderResult::ReadbackPending);
}
Err(wgpu::SurfaceError::Other) => {
return Err(SceneVMError::InvalidOperation(
"Surface returned an unspecified error".to_string(),
));
}
Err(wgpu::SurfaceError::OutOfMemory) => {
return Err(SceneVMError::BufferAllocationFailed(
"Surface out of memory".to_string(),
));
}
};
let frame_view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let src_view = gpu
.surface
.gpu
.as_ref()
.expect("Surface GPU not allocated")
.view
.clone();
let (overlay_view, overlay_rect_px): (wgpu::TextureView, [f32; 4]) =
if let Some(overlay) = self.rgba_overlay.as_mut() {
overlay.texture.ensure_gpu_with(&gpu.device);
overlay.texture.upload_to_gpu_with(&gpu.device, &gpu.queue);
if let Some(overlay_gpu) = overlay.texture.gpu.as_ref() {
(overlay_gpu.view.clone(), overlay.rect)
} else {
(src_view.clone(), [0.0, 0.0, 0.0, 0.0])
}
} else {
(src_view.clone(), [0.0, 0.0, 0.0, 0.0])
};
let fw = ws.config.width.max(1) as f32;
let fh = ws.config.height.max(1) as f32;
let overlay_rect = [
overlay_rect_px[0] / fw,
overlay_rect_px[1] / fh,
overlay_rect_px[2] / fw,
overlay_rect_px[3] / fh,
];
if ws
.present_pipeline
.as_ref()
.map(|p| p.surface_format != ws.format)
.unwrap_or(true)
{
ws.present_pipeline = Some(PresentPipeline::new(
&gpu.device,
&gpu.queue,
ws.format,
&src_view,
&overlay_view,
overlay_rect,
));
} else if let Some(pipeline) = ws.present_pipeline.as_mut() {
pipeline.update_bind_group(
&gpu.device,
&gpu.queue,
&src_view,
&overlay_view,
overlay_rect,
);
}
let present = ws
.present_pipeline
.as_ref()
.expect("Present pipeline should be initialized");
let mut encoder = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("scenevm-present-encoder"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scenevm-present-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
pass.set_pipeline(&present.pipeline);
pass.set_bind_group(0, &present.bind_group, &[]);
pass.draw(0..3, 0..1);
}
gpu.queue.submit(std::iter::once(encoder.finish()));
frame.present();
Ok(RenderResult::Presented)
}
#[cfg(not(target_arch = "wasm32"))]
fn draw(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
let (gpu_slot, base_vm, overlays) = (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
let Some(gpu) = gpu_slot.as_mut() else {
return;
};
let buffer_width = out_w;
let buffer_height = out_h;
if self.size != (buffer_width, buffer_height) {
self.size = (buffer_width, buffer_height);
gpu.surface.width = buffer_width;
gpu.surface.height = buffer_height;
gpu.surface.ensure_gpu_with(&gpu.device);
}
let (w, h) = self.size;
SceneVM::draw_all_vms(
base_vm,
overlays,
&gpu.device,
&gpu.queue,
&mut gpu.surface,
w,
h,
self.log_layer_activity,
&mut self.compositing_pipeline,
&mut self.rgba_overlay,
&mut self.rgba_overlay_pipeline,
true,
);
let device = gpu.device.clone();
let queue = gpu.queue.clone();
gpu.surface.download_from_gpu_with(&device, &queue);
gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
}
#[cfg(target_arch = "wasm32")]
pub async fn render_frame_async(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
let (gpu_slot, base_vm, overlays) = (&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
let Some(gpu) = gpu_slot.as_mut() else {
return;
};
let buffer_width = out_w;
let buffer_height = out_h;
if self.size != (buffer_width, buffer_height) {
self.size = (buffer_width, buffer_height);
gpu.surface.width = buffer_width;
gpu.surface.height = buffer_height;
gpu.surface.ensure_gpu_with(&gpu.device);
}
let (w, h) = self.size;
SceneVM::draw_all_vms(
base_vm,
overlays,
&gpu.device,
&gpu.queue,
&mut gpu.surface,
w,
h,
self.log_layer_activity,
&mut self.compositing_pipeline,
&mut self.rgba_overlay,
&mut self.rgba_overlay_pipeline,
true,
);
let device = gpu.device.clone();
let queue = gpu.queue.clone();
gpu.surface.download_from_gpu_with(&device, &queue);
let flag = gpu
.surface
.gpu
.as_ref()
.and_then(|g| g.map_ready.as_ref().map(|f| std::rc::Rc::clone(f)));
if let Some(flag) = flag {
MapReadyFuture { flag }.await;
}
let _ = gpu.surface.try_finish_download_from_gpu();
gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn render_frame_async(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) {
self.draw(out_pixels, out_w, out_h);
}
pub fn render_frame(&mut self, out_pixels: &mut [u8], out_w: u32, out_h: u32) -> RenderResult {
#[cfg(not(target_arch = "wasm32"))]
{
self.draw(out_pixels, out_w, out_h);
return RenderResult::Presented;
}
#[cfg(target_arch = "wasm32")]
{
if self.gpu.is_none() {
if !self.init_in_flight && self.needs_gpu_init {
self.init_in_flight = true;
let this: *mut SceneVM = self as *mut _;
spawn_local(async move {
unsafe {
(&mut *this).init_async().await;
(&mut *this).init_in_flight = false;
}
});
}
return RenderResult::InitPending;
}
let (gpu_slot, base_vm, overlays) =
(&mut self.gpu, &mut self.vm, &mut self.overlay_vms);
let gpu = gpu_slot.as_mut().unwrap();
if self.size != (out_w, out_h) {
self.size = (out_w, out_h);
gpu.surface.width = out_w;
gpu.surface.height = out_h;
gpu.surface.ensure_gpu_with(&gpu.device);
}
let inflight = gpu
.surface
.gpu
.as_ref()
.and_then(|g| g.map_ready.as_ref())
.is_some();
let mut presented_frame = false;
if inflight {
let ready = gpu.surface.try_finish_download_from_gpu();
gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
if !ready {
return RenderResult::ReadbackPending;
}
presented_frame = true;
} else {
gpu.surface.copy_to_slice(out_pixels, out_w, out_h);
}
let (w, h) = self.size;
SceneVM::draw_all_vms(
base_vm,
overlays,
&gpu.device,
&gpu.queue,
&mut gpu.surface,
w,
h,
self.log_layer_activity,
&mut self.compositing_pipeline,
&mut self.rgba_overlay,
&mut self.rgba_overlay_pipeline,
true,
);
let device = gpu.device.clone();
let queue = gpu.queue.clone();
gpu.surface.download_from_gpu_with(&device, &queue);
if presented_frame {
RenderResult::Presented
} else {
RenderResult::ReadbackPending
}
}
}
pub fn load_image_rgba<I: IntoDataInput>(&self, input: I) -> Option<(Vec<u8>, u32, u32)> {
let bytes = match input.load_data() {
Ok(b) => b,
Err(_) => return None,
};
let img = match image::load_from_memory(&bytes) {
Ok(i) => i,
Err(_) => return None,
};
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
Some((rgba.into_raw(), w, h))
}
pub fn compile_shader_2d(&mut self, body_source: &str) -> ShaderCompilationResult {
self.compile_shader_internal(body_source, true)
}
pub fn compile_shader_3d(&mut self, body_source: &str) -> ShaderCompilationResult {
self.compile_shader_internal(body_source, false)
}
pub fn compile_shader_sdf(&mut self, body_source: &str) -> ShaderCompilationResult {
use wgpu::ShaderSource;
let header_source = if let Some(bytes) = Embedded::get("sdf_header.wgsl") {
std::str::from_utf8(bytes.data.as_ref())
.unwrap_or("")
.to_string()
} else {
"".to_string()
};
let full_source = format!("{}\n{}", header_source, body_source);
let device = if let Some(gpu) = &self.gpu {
&gpu.device
} else {
return ShaderCompilationResult {
success: false,
warnings: vec![],
errors: vec![ShaderDiagnostic {
line: 0,
message: "GPU device not initialized. Cannot compile shader.".to_string(),
}],
};
};
let _shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scenevm-compile-sdf"),
source: ShaderSource::Wgsl(full_source.into()),
});
self.vm.execute(Atom::SetSourceSdf(body_source.to_string()));
ShaderCompilationResult {
success: true,
warnings: vec![],
errors: vec![],
}
}
pub fn default_shader_source(kind: &str) -> Option<String> {
let file_name = match kind {
"ui" => "ui_body.wgsl",
"2d" => "2d_body.wgsl",
"3d" => "3d_body.wgsl",
"sdf" => "sdf_body.wgsl",
_ => return None,
};
Embedded::get(file_name).map(|bytes| {
String::from_utf8_lossy(bytes.data.as_ref()).into_owned()
})
}
fn compile_shader_internal(
&mut self,
body_source: &str,
is_2d: bool,
) -> ShaderCompilationResult {
use wgpu::ShaderSource;
let header_source = if is_2d {
if let Some(bytes) = Embedded::get("2d_header.wgsl") {
std::str::from_utf8(bytes.data.as_ref())
.unwrap_or("")
.to_string()
} else {
"".to_string()
}
} else {
if let Some(bytes) = Embedded::get("3d_header.wgsl") {
std::str::from_utf8(bytes.data.as_ref())
.unwrap_or("")
.to_string()
} else {
"".to_string()
}
};
let full_source = format!("{}\n{}", header_source, body_source);
let device = if let Some(gpu) = &self.gpu {
&gpu.device
} else {
return ShaderCompilationResult {
success: false,
warnings: vec![],
errors: vec![ShaderDiagnostic {
line: 0,
message: "GPU device not initialized. Cannot compile shader.".to_string(),
}],
};
};
let _shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(if is_2d {
"scenevm-compile-2d"
} else {
"scenevm-compile-3d"
}),
source: ShaderSource::Wgsl(full_source.into()),
});
let success = true;
if success {
if is_2d {
self.vm.execute(Atom::SetSource2D(body_source.to_string()));
} else {
self.vm.execute(Atom::SetSource3D(body_source.to_string()));
}
}
ShaderCompilationResult {
success,
warnings: vec![], errors: vec![], }
}
}
#[cfg(target_arch = "wasm32")]
fn global_gpu_get() -> Option<GlobalGpu> {
GLOBAL_GPU_WASM.with(|c| c.borrow().clone())
}
#[cfg(target_arch = "wasm32")]
async fn global_gpu_init_async() {
if global_gpu_get().is_some() {
return;
}
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::BROWSER_WEBGPU,
..Default::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.expect("No compatible GPU adapter found (WebGPU)");
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("scenevm-device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("Failed to create wgpu device (WebGPU)");
let gg = GlobalGpu {
instance,
adapter,
device,
queue,
};
GLOBAL_GPU_WASM.with(|c| *c.borrow_mut() = Some(gg));
}
#[cfg(feature = "gpu")]
impl SceneVM {
fn for_each_vm_mut(&mut self, mut f: impl FnMut(&mut VM)) {
f(&mut self.vm);
for vm in &mut self.overlay_vms {
f(vm);
}
}
fn atom_touches_atlas(atom: &Atom) -> bool {
matches!(
atom,
Atom::AddTile { .. }
| Atom::AddSolid { .. }
| Atom::SetTileMaterialFrames { .. }
| Atom::BuildAtlas
| Atom::Clear
| Atom::ClearTiles
)
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
#[cfg(feature = "gpu")]
struct NativeRenderCtx {
size: (u32, u32),
last_result: RenderResult,
present_called: bool,
}
#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
#[cfg(feature = "gpu")]
impl NativeRenderCtx {
fn new(size: (u32, u32)) -> Self {
Self {
size,
last_result: RenderResult::InitPending,
present_called: false,
}
}
fn begin_frame(&mut self) {
self.present_called = false;
}
fn ensure_presented(&mut self, vm: &mut SceneVM) -> SceneVMResult<RenderResult> {
if !self.present_called {
self.present(vm)?;
}
Ok(self.last_result)
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
#[cfg(feature = "gpu")]
impl SceneVMRenderCtx for NativeRenderCtx {
fn size(&self) -> (u32, u32) {
self.size
}
fn present(&mut self, vm: &mut SceneVM) -> SceneVMResult<RenderResult> {
let res = vm.render_to_window();
if let Ok(r) = res {
self.last_result = r;
}
self.present_called = true;
res
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "windowing"))]
#[cfg(feature = "gpu")]
pub fn run_scenevm_app<A: SceneVMApp + 'static>(
mut app: A,
) -> Result<(), Box<dyn std::error::Error>> {
use winit::dpi::LogicalSize;
use winit::event::{Event, StartCause};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowAttributes;
let frame_interval = app.target_fps().and_then(|fps| {
if fps > 0.0 {
Some(std::time::Duration::from_secs_f32(1.0 / fps))
} else {
None
}
});
let event_loop = EventLoop::new()?;
let mut window: Option<winit::window::Window> = None;
let mut vm: Option<SceneVM> = None;
let mut ctx: Option<NativeRenderCtx> = None;
let mut cursor_pos: PhysicalPosition<f64> = PhysicalPosition { x: 0.0, y: 0.0 };
let mut last_frame_at = std::time::Instant::now();
#[cfg(feature = "ui")]
let mut modifiers = winit::event::Modifiers::default();
let apply_logical_scale = |vm_ref: &mut SceneVM, scale: f64| {
let s = scale as f32;
let m = Mat3::<f32>::new(s, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 1.0);
vm_ref.execute(Atom::SetTransform2D(m));
};
#[allow(deprecated)]
event_loop.run(move |event, target| match event {
Event::NewEvents(StartCause::Init) => {
let mut attrs = WindowAttributes::default()
.with_title(app.window_title().unwrap_or_else(|| "SceneVM".to_string()));
if let Some((w, h)) = app.initial_window_size() {
attrs = attrs.with_inner_size(LogicalSize::new(w as f64, h as f64));
}
let win = target
.create_window(attrs)
.expect("failed to create window");
win.set_cursor_visible(false);
let size = win.inner_size();
let scale = win.scale_factor();
let logical = size.to_logical::<f64>(scale);
let logical_size = (logical.width.round() as u32, logical.height.round() as u32);
let mut new_vm = SceneVM::new_with_window(&win);
apply_logical_scale(&mut new_vm, scale);
let new_ctx = NativeRenderCtx::new(logical_size);
app.set_scale(scale as f32);
app.set_native_mode(true); app.init(&mut new_vm, logical_size);
window = Some(win);
vm = Some(new_vm);
ctx = Some(new_ctx);
target.set_control_flow(ControlFlow::Poll);
}
Event::WindowEvent { window_id, event } => {
if let (Some(win), Some(vm_ref), Some(ctx_ref)) =
(window.as_ref(), vm.as_mut(), ctx.as_mut())
{
if window_id == win.id() {
match event {
WindowEvent::CloseRequested => target.exit(),
WindowEvent::Resized(size) => {
let scale = win.scale_factor();
let logical = size.to_logical::<f64>(scale);
let logical_size =
(logical.width.round() as u32, logical.height.round() as u32);
ctx_ref.size = logical_size;
vm_ref.resize_window_surface(size.width, size.height);
apply_logical_scale(vm_ref, scale);
app.set_scale(scale as f32);
app.resize(vm_ref, logical_size);
}
WindowEvent::ScaleFactorChanged {
scale_factor,
mut inner_size_writer,
} => {
let size = win.inner_size();
let _ = inner_size_writer.request_inner_size(size);
let logical = size.to_logical::<f64>(scale_factor);
let logical_size =
(logical.width.round() as u32, logical.height.round() as u32);
ctx_ref.size = logical_size;
vm_ref.resize_window_surface(size.width, size.height);
app.set_scale(scale_factor as f32);
apply_logical_scale(vm_ref, scale_factor);
}
WindowEvent::CursorMoved { position, .. } => {
cursor_pos = position;
let scale = win.scale_factor() as f32;
app.mouse_move(
vm_ref,
(cursor_pos.x as f32) / scale,
(cursor_pos.y as f32) / scale,
);
}
WindowEvent::MouseInput {
state,
button: MouseButton::Left,
..
} => match state {
ElementState::Pressed => {
let scale = win.scale_factor() as f32;
app.mouse_down(
vm_ref,
(cursor_pos.x as f32) / scale,
(cursor_pos.y as f32) / scale,
);
}
ElementState::Released => {
let scale = win.scale_factor() as f32;
app.mouse_up(
vm_ref,
(cursor_pos.x as f32) / scale,
(cursor_pos.y as f32) / scale,
);
}
},
WindowEvent::MouseWheel { delta, .. } => {
let (dx, dy) = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => {
(x * 120.0, y * 120.0)
}
winit::event::MouseScrollDelta::PixelDelta(pos) => {
(pos.x as f32, pos.y as f32)
}
};
let scale = win.scale_factor() as f32;
app.scroll(vm_ref, dx / scale, dy / scale);
}
WindowEvent::RedrawRequested => {
if let Some(dt) = frame_interval {
let now = std::time::Instant::now();
if now.duration_since(last_frame_at) < dt {
return;
}
last_frame_at = now;
}
if app.needs_update(vm_ref) {
ctx_ref.begin_frame();
app.update(vm_ref);
let _ = app.render(vm_ref, ctx_ref);
let _ = ctx_ref.ensure_presented(vm_ref);
#[cfg(feature = "ui")]
{
use crate::app_event::AppEvent;
let events = app.take_app_events();
for event in events {
match event {
AppEvent::RequestUndo => {
app.undo(vm_ref);
}
AppEvent::RequestRedo => {
app.redo(vm_ref);
}
AppEvent::RequestExport { format, filename } => {
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "ios")
))]
{
crate::native_dialogs::handle_export(
&mut app, vm_ref, &format, &filename,
);
}
}
AppEvent::RequestSave {
filename,
extension,
} => {
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "ios")
))]
{
crate::native_dialogs::handle_save(
&mut app, vm_ref, &filename, &extension,
);
}
}
AppEvent::RequestOpen { extension } => {
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "ios")
))]
{
crate::native_dialogs::handle_open(
&mut app, vm_ref, &extension,
);
}
}
AppEvent::RequestImport { file_types } => {
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "ios")
))]
{
crate::native_dialogs::handle_import(
&mut app,
vm_ref,
&file_types,
);
}
}
_ => {
}
}
}
}
}
}
#[cfg(feature = "ui")]
WindowEvent::ModifiersChanged(new_modifiers) => {
modifiers = new_modifiers;
}
WindowEvent::KeyboardInput { event, .. } => {
use winit::keyboard::{Key, NamedKey};
#[cfg(feature = "ui")]
use winit::keyboard::{KeyCode, PhysicalKey};
let key = match &event.logical_key {
Key::Character(text) => text.to_lowercase(),
Key::Named(NamedKey::ArrowUp) => "up".to_string(),
Key::Named(NamedKey::ArrowDown) => "down".to_string(),
Key::Named(NamedKey::ArrowLeft) => "left".to_string(),
Key::Named(NamedKey::ArrowRight) => "right".to_string(),
Key::Named(NamedKey::Space) => "space".to_string(),
Key::Named(NamedKey::Enter) => "enter".to_string(),
Key::Named(NamedKey::Tab) => "tab".to_string(),
Key::Named(NamedKey::Escape) => "escape".to_string(),
_ => String::new(),
};
if !key.is_empty() {
match event.state {
ElementState::Pressed => app.key_down(vm_ref, &key),
ElementState::Released => app.key_up(vm_ref, &key),
}
}
#[cfg(feature = "ui")]
if event.state == ElementState::Pressed {
if event.physical_key == PhysicalKey::Code(KeyCode::KeyZ) {
#[cfg(target_os = "macos")]
let cmd_pressed = modifiers.state().super_key();
#[cfg(not(target_os = "macos"))]
let cmd_pressed = modifiers.state().control_key();
if cmd_pressed && !modifiers.state().shift_key() {
app.undo(vm_ref);
} else if cmd_pressed && modifiers.state().shift_key() {
app.redo(vm_ref);
}
}
#[cfg(not(target_os = "macos"))]
if event.physical_key == PhysicalKey::Code(KeyCode::KeyY) {
if modifiers.state().control_key() {
app.redo(vm_ref);
}
}
}
}
_ => {}
}
}
}
}
Event::AboutToWait => {
if let (Some(win), Some(vm_ref)) = (window.as_ref(), vm.as_mut()) {
let wants_frame = app.needs_update(vm_ref);
if let Some(dt) = frame_interval {
let next = std::time::Instant::now() + dt;
target.set_control_flow(ControlFlow::WaitUntil(next));
if wants_frame {
win.request_redraw();
}
} else if wants_frame {
target.set_control_flow(ControlFlow::Poll);
win.request_redraw();
} else {
target.set_control_flow(ControlFlow::Wait);
}
}
}
_ => {}
})?;
#[allow(unreachable_code)]
Ok(())
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "gpu")]
struct WasmRenderCtx {
buffer: Vec<u8>,
width: u32,
height: u32,
canvas: HtmlCanvasElement,
ctx: CanvasRenderingContext2d,
pending_present: bool,
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "gpu")]
impl WasmRenderCtx {
fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
self.width = width;
self.height = height;
self.canvas.set_width(width);
self.canvas.set_height(height);
self.buffer.resize((width * height * 4) as usize, 0);
}
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "gpu")]
impl SceneVMRenderCtx for WasmRenderCtx {
fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
fn present(&mut self, vm: &mut SceneVM) -> SceneVMResult<RenderResult> {
let mut res = vm.render_frame(&mut self.buffer, self.width, self.height);
if res != RenderResult::Presented {
if let Some(gpu) = vm.gpu.as_mut() {
let ready = gpu.surface.try_finish_download_from_gpu();
if ready {
res = RenderResult::Presented;
}
}
}
let clamped = wasm_bindgen::Clamped(&self.buffer[..]);
let image_data =
web_sys::ImageData::new_with_u8_clamped_array_and_sh(clamped, self.width, self.height)
.map_err(|e| SceneVMError::InvalidOperation(format!("{:?}", e)))?;
self.ctx
.put_image_data(&image_data, 0.0, 0.0)
.map_err(|e| SceneVMError::InvalidOperation(format!("{:?}", e)))?;
self.pending_present = res != RenderResult::Presented;
Ok(res)
}
}
#[cfg(target_arch = "wasm32")]
fn create_or_get_canvas(document: &Document) -> Result<HtmlCanvasElement, JsValue> {
if let Some(existing) = document
.get_element_by_id("canvas")
.and_then(|el| el.dyn_into::<HtmlCanvasElement>().ok())
{
return Ok(existing);
}
let canvas: HtmlCanvasElement = document
.create_element("canvas")?
.dyn_into::<HtmlCanvasElement>()?;
document
.body()
.ok_or_else(|| JsValue::from_str("no body"))?
.append_child(&canvas)?;
Ok(canvas)
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "gpu")]
pub fn run_scenevm_app<A: SceneVMApp + 'static>(mut app: A) -> Result<(), JsValue> {
let window: WebWindow = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
let document = window
.document()
.ok_or_else(|| JsValue::from_str("no document"))?;
let canvas = create_or_get_canvas(&document)?;
let (width, height) = app.initial_window_size().unwrap_or_else(|| {
let w = window
.inner_width()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(800.0)
.round() as u32;
let h = window
.inner_height()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(600.0)
.round() as u32;
(w, h)
});
canvas.set_width(width);
canvas.set_height(height);
let ctx = canvas
.get_context("2d")?
.ok_or_else(|| JsValue::from_str("2d context missing"))?
.dyn_into::<CanvasRenderingContext2d>()?;
let mut vm = SceneVM::new(width, height);
let render_ctx = WasmRenderCtx {
buffer: vec![0u8; (width * height * 4) as usize],
width,
height,
canvas,
ctx,
pending_present: true, };
app.init(&mut vm, (width, height));
let app_rc = Rc::new(RefCell::new(app));
let vm_rc = Rc::new(RefCell::new(vm));
let ctx_rc = Rc::new(RefCell::new(render_ctx));
let first_frame = Rc::new(Cell::new(true));
{
let app = Rc::clone(&app_rc);
let vm = Rc::clone(&vm_rc);
let ctx = Rc::clone(&ctx_rc);
let window_resize = window.clone();
let resize_closure = Closure::<dyn FnMut()>::new(move || {
if let (Ok(w), Ok(h)) = (window_resize.inner_width(), window_resize.inner_height()) {
let w = w.as_f64().unwrap_or(800.0).round() as u32;
let h = h.as_f64().unwrap_or(600.0).round() as u32;
ctx.borrow_mut().resize(w, h);
app.borrow_mut().resize(&mut vm.borrow_mut(), (w, h));
}
});
window
.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?;
resize_closure.forget();
}
{
let app = Rc::clone(&app_rc);
let vm = Rc::clone(&vm_rc);
let canvas = ctx_rc.borrow().canvas.clone();
let down_closure =
Closure::<dyn FnMut(web_sys::PointerEvent)>::new(move |e: web_sys::PointerEvent| {
let rect = canvas.get_bounding_client_rect();
let x = e.client_x() as f64 - rect.left();
let y = e.client_y() as f64 - rect.top();
app.borrow_mut()
.mouse_down(&mut vm.borrow_mut(), x as f32, y as f32);
});
ctx_rc.borrow().canvas.add_event_listener_with_callback(
"pointerdown",
down_closure.as_ref().unchecked_ref(),
)?;
down_closure.forget();
}
{
let app = Rc::clone(&app_rc);
let vm = Rc::clone(&vm_rc);
let ctx = Rc::clone(&ctx_rc);
let first = Rc::clone(&first_frame);
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let f_clone = Rc::clone(&f);
let window_clone = window.clone();
*f.borrow_mut() = Some(Closure::<dyn FnMut()>::new(move || {
{
let mut app_mut = app.borrow_mut();
let mut vm_mut = vm.borrow_mut();
let ctx_pending = ctx.borrow().pending_present;
let do_render = app_mut.needs_update(&vm_mut) || first.get() || ctx_pending;
if do_render {
first.set(false);
app_mut.update(&mut vm_mut);
app_mut.render(&mut vm_mut, &mut *ctx.borrow_mut());
}
}
let _ = window_clone.request_animation_frame(
f_clone.borrow().as_ref().unwrap().as_ref().unchecked_ref(),
);
}));
let _ =
window.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref());
}
Ok(())
}
#[cfg(all(
feature = "gpu",
not(target_arch = "wasm32"),
any(target_os = "macos", target_os = "ios")
))]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn scenevm_ca_create(
layer_ptr: *mut c_void,
width: u32,
height: u32,
) -> *mut SceneVM {
if layer_ptr.is_null() {
return std::ptr::null_mut();
}
let vm = SceneVM::new_with_metal_layer(layer_ptr, width, height);
Box::into_raw(Box::new(vm))
}
#[cfg(all(
feature = "gpu",
not(target_arch = "wasm32"),
any(target_os = "macos", target_os = "ios")
))]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn scenevm_ca_destroy(ptr: *mut SceneVM) {
if ptr.is_null() {
return;
}
unsafe {
drop(Box::from_raw(ptr));
}
}
#[cfg(all(
feature = "gpu",
not(target_arch = "wasm32"),
any(target_os = "macos", target_os = "ios")
))]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn scenevm_ca_resize(ptr: *mut SceneVM, width: u32, height: u32) {
if let Some(vm) = unsafe { ptr.as_mut() } {
vm.resize_window_surface(width, height);
}
}
#[cfg(all(
feature = "gpu",
not(target_arch = "wasm32"),
any(target_os = "macos", target_os = "ios")
))]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn scenevm_ca_render(ptr: *mut SceneVM) -> i32 {
if let Some(vm) = unsafe { ptr.as_mut() } {
match vm.render_to_window() {
Ok(RenderResult::Presented) => 0,
Ok(RenderResult::InitPending) => 1,
Ok(RenderResult::ReadbackPending) => 2,
Err(_) => -1,
}
} else {
-1
}
}