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 ≎ 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.


