Nothing appears on screen except purple (The clear color)

main.rs:

use std::sync::Arc;

use bumpalo::Bump;
use winit::{
    application::ApplicationHandler,
    event::WindowEvent,
    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
    window::{WindowAttributes, WindowId},
};

mod rendering;
use rendering::*;
mod object;
use object::*;
mod camera;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let el = EventLoop::new()?;
    let bump = Bump::with_capacity(1000000);
    el.set_control_flow(ControlFlow::Poll);
    let mut ws = WindowState::new(&bump);
    el.run_app(&mut ws)?;
    Ok(())
}

struct WindowState<'a> {
    renderer: Option<Renderer<'a>>,
    bump: &'a Bump,
}

impl<'a> WindowState<'a> {
    fn new(bump: &'a Bump) -> Self {
        Self {
            renderer: None,
            bump,
        }
    }
}

impl<'a> ApplicationHandler for WindowState<'a> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        self.renderer = Some(
            Renderer::new(
                Arc::new(
                    event_loop
                        .create_window(WindowAttributes::default().with_title("Heaven"))
                        .unwrap(),
                ),
                self.bump,
            )
            .unwrap(),
        )
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        window_id: WindowId,
        event: WindowEvent,
    ) {
        let renderer = self.renderer.as_mut().unwrap();
        match event {
            WindowEvent::Resized(size) => renderer.resize(size),
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::RedrawRequested => renderer.render().unwrap(),
            _ => {}
        }
    }
}

object.rs:

use std::{default, fs, path::Path};

use glam::{Mat4, Quat, Vec3, Vec3A};
use speedy::{LittleEndian, Readable};
use wgpu::{
    BindGroup, BindGroupDescriptor, BindGroupEntry, Buffer, BufferUsages, wgt::BufferDescriptor,
};
use zstd::decode_all;

use crate::rendering::{IMMUTABLE_RENDERER, mapped_buffer};

pub struct Object {
    pub vertex_buffer: Buffer,
    pub index_buffer: Buffer,
    pub index_amount: u32,
    pub transform_buffer: Buffer,
    pub transform_bg: BindGroup,
    pub index_format: wgpu::IndexFormat,
    pub transform: Transform,
}

pub struct Transform {
    position: Vec3,
    rotation: Quat,
    scale: Vec3,
}

impl Default for Transform {
    fn default() -> Self {
        Self {
            position: Vec3::default(),
            rotation: Quat::default(),
            scale: Vec3::ONE,
        }
    }
}

#[derive(Readable)]
struct Model<'a> {
    vertex_buffer: &'a [u8],
    index_buffer: &'a [u8],
    index_amount: u32,
    index_format: IndexFormat,
}

#[repr(u8)]
#[derive(Readable)]
pub enum IndexFormat {
    U16,
    U32,
}

impl Transform {
    pub fn to_byte_matrix(&self) -> &[u8] {
        unsafe {
            std::slice::from_raw_parts(
                Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
                    .to_cols_array()
                    .as_ptr() as *const u8,
                64,
            )
        }
    }
}

impl Object {
    pub fn new<P: AsRef<Path>>(path: P, transform: Transform) -> anyhow::Result<Self> {
        let ir = IMMUTABLE_RENDERER.get().unwrap();

        let decompressed = decode_all(&fs::read(path)?[..])?;

        let model = &<Vec<Model>>::read_from_buffer(&decompressed)?[0];

        let transform_buffer = mapped_buffer(
            transform.to_byte_matrix(),
            BufferUsages::UNIFORM | BufferUsages::COPY_DST,
        )?;

        let transform_bg = ir.device.create_bind_group(&BindGroupDescriptor {
            label: None,
            layout: &ir.default_bgl,
            entries: &[BindGroupEntry {
                binding: 0,
                resource: transform_buffer.as_entire_binding(),
            }],
        });

        Ok(Self {
            vertex_buffer: mapped_buffer(model.vertex_buffer, BufferUsages::VERTEX)?,
            index_buffer: mapped_buffer(model.index_buffer, BufferUsages::INDEX)?,
            index_amount: model.index_amount,
            index_format: match model.index_format {
                IndexFormat::U16 => wgpu::IndexFormat::Uint16,
                IndexFormat::U32 => wgpu::IndexFormat::Uint32,
            },
            transform_buffer,
            transform_bg,
            transform,
        })
    }
}

rendering.rs:
'''
use crate::{camera::Camera, object::*};
use bumpalo::Bump;
use glam::{
    Mat4, Quat, Vec3, camera::rh::proj::vulkan::perspective, camera::rh::view::look_at_mat4,
};
use pollster::block_on;
use wgpu::{
    BackendOptions, Backends, BindGroup, BindGroupDescriptor, BindGroupLayout,
    BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState, Buffer,
    BufferBindingType, BufferUsages, Color, ColorTargetState, ColorWrites,
    CommandEncoderDescriptor, CurrentSurfaceTexture, Device, ExperimentalFeatures, Extent3d, Face,
    Features, FragmentState, FrontFace, IndexFormat, Instance, InstanceDescriptor, InstanceFlags,
    Limits, LoadOp, MemoryBudgetThresholds, MemoryHints, MultisampleState, Operations,
    PipelineCompilationOptions, PipelineLayoutDescriptor, PolygonMode, PowerPreference,
    PrimitiveState, PrimitiveTopology, Queue, RenderPassDescriptor, RenderPipeline,
    RenderPipelineDescriptor, RequestAdapterOptions, ShaderSource, ShaderStages, StoreOp, Surface,
    SurfaceColorSpace, TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor,
    Trace, VertexBufferLayout, VertexState, VertexStepMode, include_wgsl,
    util::DeviceExt,
    vertex_attr_array,
    wgt::{BufferDescriptor, DeviceDescriptor, SurfaceConfiguration, TextureDescriptor},
};
use winit::{dpi::PhysicalSize, window::Window};

pub static IMMUTABLE_RENDERER: OnceLock<ImmutableRenderer> = OnceLock::new();

pub struct ImmutableRenderer {
    pub surface: Surface<'static>,
    pub device: Device,
    pub queue: Queue,
    pub default_bgl: BindGroupLayout,
}

pub struct Renderer<'a> {
    window: Arc<Window>,
    config: wgpu::SurfaceConfiguration,
    bump: &'a Bump,
    objects: Vec<Object>,
    render_pipeline: RenderPipeline,
    camera: Camera,
}

impl<'a> Renderer<'a> {
    pub fn new(window: Arc<Window>, bump: &'a Bump) -> anyhow::Result<Self> {
        let size = window.inner_size();

        let instance = Instance::new(InstanceDescriptor {
            #[cfg(not(target_arch = "wasm32"))]
            backends: wgpu::Backends::PRIMARY,
            #[cfg(target_arch = "wasm32")]
            backends: wgpu::Backends::GL,
            flags: InstanceFlags::default(),
            memory_budget_thresholds: MemoryBudgetThresholds::default(),
            backend_options: BackendOptions::default(),
            display: None,
        });

        let surface = instance.create_surface(window.clone()).unwrap();

        let adapter = block_on(instance.request_adapter(&RequestAdapterOptions {
            power_preference: PowerPreference::HighPerformance,
            compatible_surface: Some(&surface),
            force_fallback_adapter: false,
            apply_limit_buckets: false,
        }))?;

        let (device, queue) = block_on(adapter.request_device(&DeviceDescriptor {
            label: None,
            required_features: Features::empty(),
            required_limits: Limits::default(),
            memory_hints: MemoryHints::default(),
            trace: Trace::Off,
            experimental_features: ExperimentalFeatures::disabled(),
        }))?;

        let surface_caps = surface.get_capabilities(&adapter);

        let surface_format = surface_caps
            .formats
            .iter()
            .find(|f| f.is_srgb())
            .unwrap_or(&surface_caps.formats[0]);

        let config = SurfaceConfiguration {
            usage: TextureUsages::RENDER_ATTACHMENT,
            format: *surface_format,
            width: size.width.max(1),
            height: size.height.max(1),
            present_mode: surface_caps.present_modes[0],
            alpha_mode: surface_caps.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
            color_space: SurfaceColorSpace::Srgb,
        };

        let depth_texture = device.create_texture(&TextureDescriptor {
            label: None,
            size: Extent3d {
                width: config.width,
                height: config.height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: TextureDimension::D2,
            format: TextureFormat::Depth32Float,
            usage: TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });

        let depth_view = depth_texture.create_view(&Default::default());

        surface.configure(&device, &config);

        let default_bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
            label: None,
            entries: &[BindGroupLayoutEntry {
                binding: 0,
                visibility: ShaderStages::VERTEX,
                ty: BindingType::Buffer {
                    ty: BufferBindingType::Uniform,
                    has_dynamic_offset: false,
                    min_binding_size: None,
                },
                count: None,
            }],
        });

        let render_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
            label: None,
            bind_group_layouts: &[Some(&default_bgl), Some(&default_bgl)],
            immediate_size: 0,
        });

        let shader = device.create_shader_module(include_wgsl!("shaders/shader.wgsl"));

        let render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
            label: None,
            layout: Some(&render_pipeline_layout),
            vertex: VertexState {
                module: &shader,
                entry_point: Some("vs_main"),
                buffers: &[Some(VertexBufferLayout {
                    array_stride: 32,
                    step_mode: VertexStepMode::Vertex,
                    attributes: &vertex_attr_array![
                        0 => Float32x3,
                        1 => Float32x2,
                        2 => Float32x3,
                    ],
                })],
                compilation_options: PipelineCompilationOptions::default(),
            },
            fragment: Some(FragmentState {
                // 3.
                module: &shader,
                entry_point: Some("fs_main"),
                targets: &[Some(ColorTargetState {
                    // 4.
                    format: config.format,
                    blend: Some(BlendState::REPLACE),
                    write_mask: ColorWrites::ALL,
                })],
                compilation_options: PipelineCompilationOptions::default(),
            }),
            primitive: PrimitiveState {
                topology: PrimitiveTopology::TriangleList, // 1.
                strip_index_format: None,
                front_face: FrontFace::Ccw, // 2.
                cull_mode: Some(Face::Back),
                // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
                polygon_mode: PolygonMode::Fill,
                // Requires Features::DEPTH_CLIP_CONTROL
                unclipped_depth: false,
                // Requires Features::CONSERVATIVE_RASTERIZATION
                conservative: false,
            },
            depth_stencil: None,
            multisample: MultisampleState {
                count: 1,
                mask: !0,
                alpha_to_coverage_enabled: false,
            },
            multiview_mask: None,
            cache: None,
        });

        IMMUTABLE_RENDERER.get_or_init(|| ImmutableRenderer {
            surface,
            device,
            queue,
            default_bgl,
        });

        Ok(Self {
            window,
            config,
            bump,
            render_pipeline,
            objects: vec![Object::new("assets/cube.g", Transform::default())?],
            camera: Camera::new(size),
        })
    }

    pub fn render(&mut self) -> anyhow::Result<()> {
        let ir = IMMUTABLE_RENDERER.get().unwrap();
        self.window.request_redraw();

        let frame = match ir.surface.get_current_texture() {
            CurrentSurfaceTexture::Success(surface_texture) => surface_texture,
            CurrentSurfaceTexture::Suboptimal(_) => {
                ir.surface.configure(&ir.device, &self.config);
                return Ok(());
            }
            CurrentSurfaceTexture::Timeout
            | CurrentSurfaceTexture::Occluded
            | CurrentSurfaceTexture::Validation => {
                // Skip this frame
                return Ok(());
            }
            CurrentSurfaceTexture::Outdated => {
                ir.surface.configure(&ir.device, &self.config);
                return Ok(());
            }
            CurrentSurfaceTexture::Lost => {
                // You could recreate the devices and all resources
                // created with it here, but we'll just bail
                anyhow::bail!("Lost device");
            }
        };

        let view = frame.texture.create_view(&TextureViewDescriptor::default());

        let mut encoder = ir
            .device
            .create_command_encoder(&CommandEncoderDescriptor::default());

        {
            let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
                label: None,
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    depth_slice: None,
                    ops: Operations {
                        load: LoadOp::Clear(Color {
                            r: 0.2,
                            g: 0.,
                            b: 0.2,
                            a: 1.0,
                        }),
                        store: StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                occlusion_query_set: None,
                timestamp_writes: None,
                multiview_mask: None,
            });

            render_pass.set_bind_group(0, &self.camera.bg, &[]);

            self.objects.iter().for_each(|o| {
                render_pass.set_pipeline(&self.render_pipeline);
                render_pass.set_bind_group(1, &o.transform_bg, &[]);
                render_pass.set_vertex_buffer(0, o.vertex_buffer.slice(..));
                render_pass.set_index_buffer(o.index_buffer.slice(..), o.index_format);
                render_pass.draw_indexed(0..o.index_amount, 0, 0..1);
            });
        }

        ir.queue.submit(std::iter::once(encoder.finish()));
        ir.queue.present(frame);

        Ok(())
    }

    pub fn resize(&mut self, size: PhysicalSize<u32>) {
        if size.width > 0 && size.height > 0 {
            let ir = IMMUTABLE_RENDERER.get().unwrap();
            self.config.width = size.width;
            self.config.height = size.height;
            ir.surface.configure(&ir.device, &self.config);
        }
    }
}

pub fn mapped_buffer(contents: &[u8], usage: BufferUsages) -> anyhow::Result<Buffer> {
    let ir = IMMUTABLE_RENDERER.get().unwrap();
    let buffer = ir.device.create_buffer(&BufferDescriptor {
        label: None,
        usage,
        size: (contents.len() as u64 + 3) & !3,
        mapped_at_creation: true,
    });
    buffer
        .get_mapped_range_mut(..)?
        .slice(..)
        .copy_from_slice(contents);
    buffer.unmap();
    Ok(buffer)
}

camera.rs:

use winit::dpi::PhysicalSize;

use crate::rendering::{IMMUTABLE_RENDERER, mapped_buffer};

pub struct Camera {
    pub position: Vec3,
    pub rotation: Quat,
    pub buffer: Buffer,
    pub bg: BindGroup,
}

impl Camera {
    pub fn new(size: PhysicalSize<u32>) -> Self {
        let ir = IMMUTABLE_RENDERER.get().unwrap();

        let buffer = mapped_buffer(
            unsafe {
                std::slice::from_raw_parts(
                    (perspective(
                        60_f32.to_radians(),
                        size.width as f32 / size.height as f32,
                        0.1,
                        100.,
                    ) * look_at_mat4(Vec3::new(0., 0., 5.), Vec3::default(), Vec3::Y))
                    .to_cols_array()
                    .as_ptr() as *const u8,
                    64,
                )
            },
            BufferUsages::UNIFORM | BufferUsages::COPY_DST,
        )
        .unwrap();

        let bg = ir.device.create_bind_group(&BindGroupDescriptor {
            label: None,
            layout: &ir.default_bgl,
            entries: &[BindGroupEntry {
                binding: 0,
                resource: buffer.as_entire_binding(),
            }],
        });

        Self {
            position: Vec3::default(),
            rotation: Quat::default(),
            buffer,
            bg,
        }
    }
}

shader.wgsl:

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv: vec2<f32>,
    @location(1) normal: vec3<f32>,
};

@group(0) @binding(0)
var<uniform> camera: mat4x4<f32>;

@group(1) @binding(0)
var<uniform> transform: mat4x4<f32>;

@vertex
fn vs_main(
    model: VertexInput,
) -> VertexOutput {
    var out: VertexOutput;
    out.uv = model.uv;
    out.normal = model.normal;
    out.position = transform * camera * vec4<f32>(model.position, 1.0);
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    return vec4<f32>(0.5, 0.5, 0.5, 1.0);
}

Cargo.toml:

[package]
name = "gdl_learning"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.104"
bumpalo = { version = "3.20.3", features = ["collections"] }
gilrs = "0.11.2"
glam = "0.33.6"
pollster = "1.0.1"
spacetimedb = "2.8.3"
spacetimedb-sdk = "2.8.3"
speedy = "0.8.7"
tokio = { version = "1.53.1", features = ["full"] }
wgpu = "30.0.1"
winit = "0.30.13"
zstd = "0.14.0"

# Enable more optimization in the release profile at the cost of compile time.
[profile.release]
# Compile the entire crate as one unit.
# Slows compile times, marginal improvements.
codegen-units = 1
# Do a second optimization pass over the entire program, including dependencies.
# Slows compile times, marginal improvements.
lto = "thin"

# Optimize for size in the wasm-release profile to reduce load times and bandwidth usage on web.
[profile.wasm-release]
# Default to release profile values.
inherits = "release"
# Optimize with size in mind (also try "z", sometimes it is better).
# Slightly slows compile times, great improvements to file size and runtime performance.
opt-level = "s"
# Strip all debugging information from the binary to slightly reduce file size.
strip = "debuginfo"

# Enable a small amount of optimization in the dev profile.
[profile.dev]
opt-level = 1

# Enable a large amount of optimization in the dev profile for dependencies.
[profile.dev.package."*"]
opt-level = 3

main.rs (lion mesh-converter):

use std::{fs, mem::ManuallyDrop};

use asset_importer::{Importer, Vector3D};
use bumpalo::{
    Bump,
    collections::{CollectIn, Vec},
    vec,
};
use clap::{Parser, Subcommand};
use glam::Vec2;
use speedy::{LittleEndian, Writable};
use zstd::encode_all;

#[derive(Parser)]
#[command(name = "lion")]
#[command(about = "GDL mesh converter", long_about = None)]
struct Cli {
    src: String,
    dst: String,
}

#[derive(Writable)]
struct Model<'a> {
    vertex_buffer: &'a [u8],
    index_buffer: &'a [u8],
    index_amount: u32,
    index_format: IndexFormat,
}

#[repr(u8)]
#[derive(Writable)]
enum IndexFormat {
    U16,
    U32,
}

#[repr(C)]
struct Vertex {
    position: [f32; 3],
    uv: [f32; 2],
    normal: [f32; 3],
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    let scene = Importer::new().read_file(cli.src).import()?;
    let bump = Bump::with_capacity(10000000);
    let models = &scene
            .meshes()
            .map(|m| {
                let v: Vec<u32> = m.triangle_indices_iter().collect_in(&bump);
                let len = v.len();

                let (index_buffer, index_format) = if len <= u16::MAX as usize {
                    (
                        unsafe {
                            std::slice::from_raw_parts(
                                v.into_iter()
                                    .map(|i| i as u16)
                                    .collect_in::<Vec<u16>>(&bump)
                                    .into_bump_slice()
                                    .as_ptr() as *const u8,
                                len * 2,
                            )
                        },
                        IndexFormat::U16,
                    )
                } else {
                    (
                        unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, len * 4) },
                        IndexFormat::U32,
                    )
                };
                Model {
                    vertex_buffer: {
                        let v = m
                            .vertices_iter()
                            .zip(m.texture_coords_iter(0))
                            .zip(m.normals_iter())
                            //.zip(m.bones().map(|b|{}))
                            .map(|(((i1, i2), i3))| Vertex {position: [i1.x,i1.y,i1.z], uv: [i2.x, i2.y], normal: [i3.x, i3.y, i3.z]})
                            .collect_in::<Vec<Vertex>>(&bump)
                            .into_bump_slice();
                        unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 32) }
                    },
                    index_buffer,
                    index_amount: len as u32,
                    index_format,
                }
            })
            .collect::<std::vec::Vec<Model>>();
            
    let mut data =
        vec![in &bump; 0u8; <std::vec::Vec<Model<'_>> as Writable<LittleEndian>>::bytes_needed(&models)?];

    models.write_to_buffer(&mut data)?;

    fs::write(cli.dst, encode_all(data.into_bump_slice(), 22)?)?;

    Ok(())
}

Cargo.toml (lion mesh-converter):

[package]
name = "lion"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.104"
asset-importer = { version = "0.8.0", features = ["prebuilt"] }
bumpalo = { version = "3.20.3", features = ["collections"] }
clap = { version = "4.6.6", features = ["derive"] }
glam = { version = "0.33.7", default-features = false, features = ["std"] }
speedy = "0.8.7"
zstd = "0.14.0"

# Enable more optimization in the release profile at the cost of compile time.
[profile.release]
# Compile the entire crate as one unit.
# Slows compile times, marginal improvements.
codegen-units = 1
# Do a second optimization pass over the entire program, including dependencies.
# Slows compile times, marginal improvements.
lto = "thin"

# Optimize for size in the wasm-release profile to reduce load times and bandwidth usage on web.
[profile.wasm-release]
# Default to release profile values.
inherits = "release"
# Optimize with size in mind (also try "z", sometimes it is better).
# Slightly slows compile times, great improvements to file size and runtime performance.
opt-level = "s"
# Strip all debugging information from the binary to slightly reduce file size.
strip = "debuginfo"

# Enable a small amount of optimization in the dev profile.
[profile.dev]
opt-level = 1

# Enable a large amount of optimization in the dev profile for dependencies.
[profile.dev.package."*"]
opt-level = 3

Apologies for the massive code dump, it's just I couldn't find any answers for this issue.

didn't read the code yet, but a generic advice for debugging graphics code: run you program under renderdoc, it captures the API calls and visualizes them, which gives you clue about what went wrong. it's really intuitive to use, and there are plenty of documentation and tutorials.

btw, your rendering.rs is not formatted correctly.

In what sense is it not formatted correctly? Also, renderdoc gave me this error:

thread 'main' (87803) panicked at src/rendering.rs:60:63:
called `Result::unwrap()` on an `Err` value: CreateSurfaceError { inner: Hal(FailedToCreateSurfaceForAnyBackend({Vulkan: InstanceError { message: "Vulkan driver does not support VK_KHR_wayland_surface", source: None }})) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

oh, renderdoc does not support wayland (yet?), you'll need to switch to X11 for presentation. unset WAYLAND_DISPLAY or set it to empty, e.g.

$ WAYLAND_DISPLAY=""  qrenderdoc

see Vulkan backend in renderdoc with wayland · Issue #3889 · gfx-rs/wgpu · GitHub

don't see anything wrong at first glance, i say start with rendering just a handwritten quad to make sure it's visible from the camera and then try to gradually go towards the original project and see when it breaks

Alright, i've done it, how do I look for the issue? VS out also showed nothing.

the screenshot is the "event viewer", click the draw call you are interested in (here you only have a single draw call, i.e. vkCmdDrawIndexed(36, 1)), and you'' be able to view the pipeline state for each stages for the given draw, such as the vertices, shader input and output, the rasterizer parameters, etc. see:

I find myself use the mesh viewer and texture viewer the most. typically a rendering glitch is very obvious to spot simply by visualizing the geometry and color data.

you can also debug the shader code if you found the some output of the shaders are wrong:

if you need more advanced debug features, read the documentation or watch a renderdoc tutorial video.

How odd...



It seems to me like it doesn't like the matrices it's being multiplied against.

You didn't show the shader definition for VertexInput, but assuming you're using vec3f's there you'll be getting incorrectly aligned data:

#[repr(C)]
struct Vertex {
    position: [f32; 3],
    uv: [f32; 2],
    normal: [f32; 3],
}

Here your position and normal have a size of 4*3 and an alignment of 4, but in WGSL vec3f's have a 16 byte alignment, so there's an extra 4 bytes of padding. I suggest switching your Vertex definition to stick to glam types and use Vec3A to get automatically correct alignment.

You should also just use bytemuck to cast the entire buffer data slice at once, you just need to derive Zeroable and Pod on your Vertex type and use bytemuck::cast_slice() (or a few other options). You can do that with glam types too, if you enable the bytemuck feature for it.

Your camera projection is applied before the transform which isn't right, but I'm not sure that you're actually applying any non-identity transform there at the moment (I don't see any changes to the initial scale, etc), so I don't know if that's a problem.

Your model loading code is trying to use u16 indices if it can, but it's checking the number of indices, rather than vertices (which determines the maximum index value). The number of indices is normally more than vertices so this isn't likely to matter either, but worth noting. You should probably just be using u32 anyway, the complexity isn't really worth the minor memory improvement.

It does look like you're correctly using column-major matrices and using them on the left, but that's another common issue to watch out for.

You've also specified backface culling and depth buffers, which depending on the model and setup can cause confusion. Best to start with the minimum and add things one at a time as you get things working.

A few things. Does recommending me to use bytemuck imply that the cost of the checks that it performs is free or almost free? and also I use the amount of indices to determine the amount of bytes needed for the indices. I do this for file size, not memory improvement. And which camera and transform is out of order? rust side or wgsl?

bytemuck's checks are almost entirely compile time, yes: there's a few small trivial, but necessary, checks like for alignment and length in cases like casting from &[u8] to &[T]. For casting to [u8], it should compile to nothing more than multiplying the length exactly like you do, LLVM is perfectly capable of noticing that every number evenly divides by 1 and eliminating that code.

File size is a bit more laudable, but you should still first ensure everything is working. You can then iterate on all sorts of tricks like compressing the buffers, sorting vertex data, converting to a triangle strip representation, and so on. Or just go shopping for someone who's done the hard work, of course

The shader code is where you apply the model transform and the camera in the wrong order, here:

Apologies for the overly detailed recap, but I find it helpful to go over this stuff in detail to really cement it:

This shader code logically passes the position through first your camera transform then the model transform, as if it were moving through the multiplications from right to left. You generally want to think of transforms being from one "space" (or coordinate system) that positions are relative to on their right, to another on their left, and combining transforms only make sense when they match spaces in the middle. In your code, it's moving the position from world space to "clip" space with the camera transform, then trying to move from object/model space to world space.

The transforms you conventionally do in a vertex shader can be broken down as:

// stored positions are relative to the model/object's origin
let object_space = in.position;
// everything in the scene/world shares this space
let world_space = object_transform * object_space;
// essentially "move the world" to be in front of the camera, at the origin of the view space
let view_space = view_transform * world_space; 
// finally, project the view frustum to fit in the "clip" space used by hardware
// everything visible will be inside this [-1..1]x[-1..1]x[0..1] box
// sometimes also called NDC, "normalized device coordinates"
let clip_space = projection_transform * view_space; 
out.position = clip_space; 

So long as you maintain this ordering, you can combine the matrices before applying them, (A * B) * C == A * (B * C), but the order of multiplication matters for matrices, eg A * B != B * A.

For one last trick to be aware of here: this "right to left" ordering is essentially only a convention: if you transpose the matrices (swap rows and columns) you can do everything left to right. Best to stick to the convention, but it's an easy way to check if your matrix library and API didn't agree on matrix layout, column major or row major. Annoyingly, graphics tends to use the former, and general maths tends to use the latter.

Did you say it was right for me to use column major matrices because I used Mat4::to_cols_array()?

Basically, yeah? It's nearly all convention, so "right" is just a matter of keeping your ducks all in a row, it's just easier to do so when you follow the convention.

For example, you don't even need to use to_cols_array because the internal data is already in column major order (that's why the docs say to use transpose to get row major) - just use a Mat4 on one side and a mat4x4f on the other and you're fine.

If you did happen to have a row major library, some graphics APIs will let you declare that you're providing that... but I'm betting all that really does is add a transpose when copying over to the GPU.

And finally, if you do "get it wrong" on the layout side, it's just an implicit transpose, so you can cancel that it by flipping your mutiplies in the shader: out. position = in.position * transform * camera for example. Two wrongs do make a right, here!