wgpu
wgpu copied to clipboard
Strange random behavior when creating an adapter (running in web only with webgl as fallback)
Description So it's a strange behavior that I cannot always reproduce. Sometimes when I reload my browser I get the error, that I cannot crate an adapter. Sometimes it strangely works, its a question of how much I am willing to refresh the page.
Repro steps
Just remove the macros from below, they just do some console.log
stuff for me.
You will see three behaviors:
- The page is running (you see a black cube)
- The Canvas is empty (thats when it could not create the adapter, sometimes you might also get a BorrowMutex error I think thats a platform dependend bug from winit - happens most on mac with safari)
- Sometimes the Shader dont appear to be working and you see a black page.
use std::borrow::Cow;
use std::mem;
use std::panic;
use std::sync::Arc;
use bytemuck::Pod;
use bytemuck::Zeroable;
use glam::Mat4;
use macros::debug;
use macros::error;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::js_sys::Function;
use web_sys::js_sys::Promise;
use wgpu::util::DeviceExt;
use wgpu::Backends;
use wgpu::Device;
use wgpu::Instance;
use wgpu::InstanceDescriptor;
use wgpu::StoreOp;
use wgpu::Surface;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalPosition;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::event_loop::ControlFlow;
use winit::event_loop::EventLoop;
use winit::event_loop::EventLoopProxy;
use winit::platform::web::EventLoopExtWebSys;
use winit::platform::web::WindowAttributesExtWebSys;
use winit::window::Window;
use winit::window::WindowId;
mod macros;
#[wasm_bindgen]
#[allow(dead_code)]
struct State {
proxy: EventLoopProxy<UserEvent>,
}
#[derive(Debug)]
pub enum UserEvent {
WakeUp,
Initialized(InitializedApp),
}
pub async fn sleep_impl(dur: web_time::Duration) {
let mut cb = |resolve: Function, _reject: Function| {
if let Some(window) = web_sys::window() {
if let Err(err) = window.set_timeout_with_callback_and_timeout_and_arguments_0(
&resolve,
dur.as_secs() as i32,
) {
console_error!("failed to call window.set_timeout: {err:?}");
}
}
};
if let Err(err) = JsFuture::from(Promise::new(&mut cb)).await {
console_error!("failed to create sleep future: {err:?}");
}
}
#[wasm_bindgen]
impl State {
#[wasm_bindgen(constructor)]
pub async fn new(canvas: web_sys::HtmlCanvasElement) -> Result<State, JsValue> {
panic::set_hook(Box::new(console_error_panic_hook::hook));
let event_loop = EventLoop::<UserEvent>::with_user_event()
.build()
.expect("Failed to create event loop");
event_loop.set_control_flow(ControlFlow::Wait);
let event_loop_proxy = event_loop.create_proxy();
let app = App::create::<UserEvent>(canvas, event_loop_proxy.clone()).await;
event_loop.spawn_app(app);
Ok(State {
proxy: event_loop_proxy,
})
}
}
#[derive(Debug)]
pub struct InitializedApp {
window: Arc<Window>,
surface: Surface<'static>,
device: Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
vertex_buf: wgpu::Buffer,
index_buf: wgpu::Buffer,
index_count: usize,
bind_group: wgpu::BindGroup,
uniform_buf: wgpu::Buffer,
pipeline: wgpu::RenderPipeline,
}
impl InitializedApp {
fn generate_matrix(aspect_ratio: f32) -> glam::Mat4 {
// let projection = glam::Mat4::perspective_rh(consts::FRAC_PI_4, aspect_ratio, 1.0, 10.0);
// let view = glam::Mat4::look_at_rh(
// glam::Vec3::new(1.5f32, -5.0, 3.0),
// glam::Vec3::ZERO,
// glam::Vec3::Z,
// );
// projection * view
let projection = glam::Mat4::perspective_rh_gl(45 as f32, aspect_ratio, 0.1, 100.0);
let view = glam::Mat4::look_at_rh(
glam::Vec3::new(2.0, 2.0, 2.0), // Make sure this is positioned to see the cube
glam::Vec3::new(0.0, 0.0, 0.0),
glam::Vec3::Y,
);
projection * view
}
}
struct App {
initialized_app: Option<InitializedApp>,
canvas: web_sys::HtmlCanvasElement,
event_loop_proxy: EventLoopProxy<UserEvent>,
}
impl App {
pub async fn create<T>(
canvas: web_sys::HtmlCanvasElement,
proxy: EventLoopProxy<UserEvent>,
) -> Self {
App {
canvas,
initialized_app: None,
event_loop_proxy: proxy,
}
}
}
async fn init(w: Arc<Window>) -> InitializedApp {
let instance = Instance::new(InstanceDescriptor {
backends: Backends::BROWSER_WEBGPU,
flags: Default::default(),
dx12_shader_compiler: Default::default(),
gles_minor_version: Default::default(),
});
let surface = instance
.create_surface(w.clone())
.map_err(|e| console_error!("CreateSurfaceError: {:?}", e))
.unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: Some(&surface),
})
.await
.expect("Failed to find an appropriate adapter");
// Create the logical device and command queue
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: adapter.features(),
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits()),
},
None,
)
.await
.expect("Failed to create device");
let config = surface
.get_default_config(&adapter, w.inner_size().width, w.inner_size().height)
.unwrap();
surface.configure(&device, &config);
w.request_redraw();
// Create the vertex and index buffers
let vertex_size = mem::size_of::<Vertex>();
let vertex_data = VERTICES;
let index_data = INDICES;
let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(&vertex_data),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(&index_data),
usage: wgpu::BufferUsages::INDEX,
});
// Create pipeline layout
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(64),
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
// Create other resources
let mx_total = InitializedApp::generate_matrix(config.width as f32 / config.height as f32);
let mx_ref: &[f32; 16] = mx_total.as_ref();
let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Uniform Buffer"),
contents: bytemuck::cast_slice(mx_ref),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
// Create bind group
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buf.as_entire_binding(),
}],
label: None,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
});
let vertex_buffers = [wgpu::VertexBufferLayout {
array_stride: vertex_size as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
// Position
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 0,
shader_location: 0,
},
// Color
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 4 * 4,
shader_location: 1,
},
// Edge
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32,
offset: 8 * 4,
shader_location: 2,
},
],
}];
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
compilation_options: Default::default(),
buffers: &vertex_buffers,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
compilation_options: Default::default(),
targets: &[Some(config.format.into())],
}),
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
InitializedApp {
window: w,
surface,
device,
queue,
config,
vertex_buf,
index_buf,
index_count: index_data.len(),
bind_group,
uniform_buf,
pipeline,
}
}
impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
console_debug!("Resumed");
if self.initialized_app.is_some() {
return;
}
let window_attributes = WindowAttributesExtWebSys::with_canvas(
Window::default_attributes(),
Some(self.canvas.clone()),
)
.with_append(true);
let window = Arc::new(event_loop.create_window(window_attributes.clone()).unwrap());
let proxy_clone = self.event_loop_proxy.clone();
wasm_bindgen_futures::spawn_local(async move {
let x = init(window).await;
proxy_clone
.send_event(UserEvent::Initialized(x))
.expect("Initialized web");
});
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
console_debug!("User event received");
match event {
UserEvent::WakeUp => {
console_debug!("WakeUp");
}
UserEvent::Initialized(initialized) => {
self.initialized_app = Some(initialized);
console_debug!("Initialized");
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
if !self.initialized_app.is_some() {
console_error!("App is not initialized, exiting");
return;
}
let initialized = self.initialized_app.as_mut().unwrap();
console_debug!("Event Received");
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::CursorMoved {
position: PhysicalPosition { x, y },
device_id: _,
} => {
if let Some(app) = &mut self.initialized_app {
console_debug!("Rotating app to x: {} y: {}", x, y);
let width = app.config.width as f32;
let height = app.config.height as f32;
// Normalize cursor coordinates to range [-1, 1]
let x_norm = x as f32 / width * 2.0 - 1.0;
let y_norm = y as f32 / height * 2.0 - 1.0;
// Update view matrix with new angles
let rotation_x = Mat4::from_rotation_x(x_norm);
let rotation_y = Mat4::from_rotation_x(y_norm);
// Combine rotations with the existing projection
let aspect_ratio = width / height;
let projection = glam::Mat4::perspective_rh_gl(45.0, aspect_ratio, 0.1, 100.0);
let view = glam::Mat4::look_at_rh(
glam::Vec3::new(2.0, 2.0, 2.0), // Make sure this is positioned to see the cube
glam::Vec3::new(0.0, 0.0, 0.0),
glam::Vec3::Y,
);
let mx_total = projection * view * rotation_x * rotation_y;
let mx_ref: &[f32; 16] = mx_total.as_ref();
app.queue
.write_buffer(&app.uniform_buf, 0, bytemuck::cast_slice(mx_ref));
// Request redraw to update the scene with new rotation
app.window.request_redraw();
}
}
WindowEvent::Resized(new_size) => {
console_debug!("Resizing");
let mx_total =
InitializedApp::generate_matrix(new_size.width as f32 / new_size.height as f32);
let mx_ref: &[f32; 16] = mx_total.as_ref();
initialized.queue.write_buffer(
&initialized.uniform_buf,
0,
bytemuck::cast_slice(mx_ref),
);
// let mut config = initialized.config.clone();
// config.width = new_size.width.max(1);
// config.height = new_size.height.max(1);
//
// initialized.surface.configure(&initialized.device, &config);
//
// initialized.config = config;
//
// // On macos the window needs to be redrawn manually after resizing
initialized.window.request_redraw();
}
WindowEvent::RedrawRequested => {
console_debug!("Redraw Requested");
let frame = initialized
.surface
.get_current_texture()
.expect("Failed to acquire next swap chain texture");
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = initialized
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&initialized.pipeline);
rpass.set_bind_group(0, &initialized.bind_group, &[]);
rpass.set_index_buffer(
initialized.index_buf.slice(..),
wgpu::IndexFormat::Uint16,
);
rpass.set_vertex_buffer(0, initialized.vertex_buf.slice(..));
rpass.draw_indexed(0..initialized.index_count as u32, 0, 0..1);
}
initialized.queue.submit(Some(encoder.finish()));
frame.present();
}
_ => (),
}
}
}
#[repr(C)]
#[derive(Copy, Clone)]
struct Vertex {
_pos: [f32; 4],
_color: [f32; 4],
_edge: f32,
}
unsafe impl Zeroable for Vertex {}
unsafe impl Pod for Vertex {}
const VERTICES: &[Vertex] = &[
Vertex {
_pos: [-1.0, 1.0, -1.0, 1.0],
_color: [1.0, 0.0, 0.0, 1.0],
_edge: 1.0,
},
Vertex {
_pos: [1.0, 1.0, -1.0, 1.0],
_color: [1.0, 0.0, 0.0, 1.0],
_edge: 1.0,
},
Vertex {
_pos: [1.0, -1.0, -1.0, 1.0],
_color: [1.0, 0.0, 0.0, 1.0],
_edge: 1.0,
},
Vertex {
_pos: [1.0, -1.0, -1.0, 1.0],
_color: [1.0, 0.0, 0.0, 1.0],
_edge: 1.0,
},
// back
Vertex {
_pos: [-1.0, 1.0, 1.0, 1.0],
_color: [0.0, 0.0, 1.0, 1.0],
_edge: 1.0,
},
Vertex {
_pos: [1.0, 1.0, 1.0, 1.0],
_color: [0.0, 0.0, 1.0, 1.0],
_edge: 1.0,
},
Vertex {
_pos: [-1.0, -1.0, 1.0, 1.0],
_color: [0.0, 0.0, 1.0, 1.0],
_edge: 1.0,
},
Vertex {
_pos: [1.0, -1.0, 1.0, 1.0],
_color: [0.0, 0.0, 1.0, 1.0],
_edge: 1.0,
},
];
// Indices for each triangle in a cube
const INDICES: [u16; 36] = [
2, 0, 1, 1, 3, 2, // bottom
6, 7, 5, 5, 4, 6, // top
0, 4, 5, 5, 1, 0, // back
2, 3, 7, 7, 6, 2, // front
3, 1, 5, 5, 7, 3, // right
2, 6, 4, 4, 0, 2, // left
];
cargo.toml (For some reason enabling WebGL features forcing me to include getrandom)
[package]
name = "rusty_cube_wasm"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.42"
wgpu = { version = "0.20.1", features = ["webgpu", "webgl"] }
bytemuck = "1.16.1"
console_error_panic_hook = "0.1.7"
web-time = "1.1.0"
rusty_cube_geometry = { path = "../geometry" }
getrandom = { version = "0.2.15", features = ["js"] }
glam = "0.28.0"
[lib]
crate-type = ["cdylib"]
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Document",
"HtmlCanvasElement",
"console",
]
[dependencies.winit]
version = "0.30.3"
features = ["default"]
[features]
default = []
web_platform = []
winit_platform = ["web_platform"]
.cargo/config.toml:
[build]
rustflags = ["--cfg", "web_platform"]
target = "wasm32-unknown-unknown"
Expected vs observed behavior My expected behavior is that my cube should draw every time I refresh the page
Platform
Right now I'm running Arch with a AMD gpu and the vulkan-amdgpu-pro
driver but I get the same behavior on my Macbook with Chrome and Safari. Strangly I can't get WebGPU running on Chrome on arch at all - but that seems more like a chrome bug. Chromium on Arch is the same as Firefox for me.