fix(many_cubes): use dynamic uniform buffer for per-entity rendering
Previous approach called write_buffer inside render pass which doesn't work — GPU only sees the last value at submit time. Now pre-computes all entity uniforms into a dynamic UBO and uses dynamic offsets per draw call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ use wgpu::util::DeviceExt;
|
|||||||
/// App-level component: index into the mesh list.
|
/// App-level component: index into the mesh list.
|
||||||
struct MeshHandle(#[allow(dead_code)] u32);
|
struct MeshHandle(#[allow(dead_code)] u32);
|
||||||
|
|
||||||
|
const MAX_ENTITIES: usize = 1024;
|
||||||
|
|
||||||
struct ManyCubesApp {
|
struct ManyCubesApp {
|
||||||
state: Option<AppState>,
|
state: Option<AppState>,
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,7 @@ struct AppState {
|
|||||||
timer: GameTimer,
|
timer: GameTimer,
|
||||||
world: World,
|
world: World,
|
||||||
time: f32,
|
time: f32,
|
||||||
|
uniform_alignment: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn camera_light_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
fn camera_light_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||||
@@ -48,8 +51,10 @@ fn camera_light_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayou
|
|||||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: true,
|
||||||
min_binding_size: None,
|
min_binding_size: wgpu::BufferSize::new(
|
||||||
|
std::mem::size_of::<CameraUniform>() as u64
|
||||||
|
),
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
@@ -78,12 +83,17 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
let window = VoltexWindow::new(event_loop, &config);
|
let window = VoltexWindow::new(event_loop, &config);
|
||||||
let gpu = GpuContext::new(window.handle.clone());
|
let gpu = GpuContext::new(window.handle.clone());
|
||||||
|
|
||||||
|
// Dynamic uniform buffer alignment
|
||||||
|
let uniform_alignment = gpu.device.limits().min_uniform_buffer_offset_alignment;
|
||||||
|
let uniform_size = std::mem::size_of::<CameraUniform>() as u32;
|
||||||
|
let aligned_size = ((uniform_size + uniform_alignment - 1) / uniform_alignment) * uniform_alignment;
|
||||||
|
|
||||||
// Parse OBJ
|
// Parse OBJ
|
||||||
let obj_src = include_str!("../../../assets/cube.obj");
|
let obj_src = include_str!("../../../assets/cube.obj");
|
||||||
let obj_data = obj::parse_obj(obj_src);
|
let obj_data = obj::parse_obj(obj_src);
|
||||||
let mesh = Mesh::new(&gpu.device, &obj_data.vertices, &obj_data.indices);
|
let mesh = Mesh::new(&gpu.device, &obj_data.vertices, &obj_data.indices);
|
||||||
|
|
||||||
// Camera at (0, 15, 25) looking down at the grid
|
// Camera
|
||||||
let aspect = gpu.config.width as f32 / gpu.config.height as f32;
|
let aspect = gpu.config.width as f32 / gpu.config.height as f32;
|
||||||
let mut camera = Camera::new(Vec3::new(0.0, 15.0, 25.0), aspect);
|
let mut camera = Camera::new(Vec3::new(0.0, 15.0, 25.0), aspect);
|
||||||
camera.pitch = -0.5;
|
camera.pitch = -0.5;
|
||||||
@@ -94,10 +104,12 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
let camera_uniform = CameraUniform::new();
|
let camera_uniform = CameraUniform::new();
|
||||||
let light_uniform = LightUniform::new();
|
let light_uniform = LightUniform::new();
|
||||||
|
|
||||||
let camera_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
// Dynamic uniform buffer: room for MAX_ENTITIES camera uniforms
|
||||||
label: Some("Camera Uniform Buffer"),
|
let camera_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
contents: bytemuck::cast_slice(&[camera_uniform]),
|
label: Some("Camera Dynamic Uniform Buffer"),
|
||||||
|
size: (aligned_size as usize * MAX_ENTITIES) as u64,
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let light_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let light_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
@@ -110,14 +122,18 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
let cl_layout = camera_light_bind_group_layout(&gpu.device);
|
let cl_layout = camera_light_bind_group_layout(&gpu.device);
|
||||||
let tex_layout = GpuTexture::bind_group_layout(&gpu.device);
|
let tex_layout = GpuTexture::bind_group_layout(&gpu.device);
|
||||||
|
|
||||||
// Bind groups
|
// Bind group: camera binding uses dynamic offset, size = one CameraUniform
|
||||||
let camera_light_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let camera_light_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some("Camera+Light Bind Group"),
|
label: Some("Camera+Light Bind Group"),
|
||||||
layout: &cl_layout,
|
layout: &cl_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: camera_buffer.as_entire_binding(),
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &camera_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: wgpu::BufferSize::new(std::mem::size_of::<CameraUniform>() as u64),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
@@ -126,10 +142,8 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default white texture
|
|
||||||
let texture = GpuTexture::white_1x1(&gpu.device, &gpu.queue, &tex_layout);
|
let texture = GpuTexture::white_1x1(&gpu.device, &gpu.queue, &tex_layout);
|
||||||
|
|
||||||
// Pipeline
|
|
||||||
let render_pipeline = pipeline::create_mesh_pipeline(
|
let render_pipeline = pipeline::create_mesh_pipeline(
|
||||||
&gpu.device,
|
&gpu.device,
|
||||||
gpu.surface_format,
|
gpu.surface_format,
|
||||||
@@ -168,6 +182,7 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
timer: GameTimer::new(60),
|
timer: GameTimer::new(60),
|
||||||
world,
|
world,
|
||||||
time: 0.0,
|
time: 0.0,
|
||||||
|
uniform_alignment: aligned_size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,16 +240,14 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
// 1. Tick timer
|
|
||||||
state.timer.tick();
|
state.timer.tick();
|
||||||
let dt = state.timer.frame_dt();
|
let dt = state.timer.frame_dt();
|
||||||
|
|
||||||
// 2. Read input state BEFORE begin_frame clears it
|
// Input
|
||||||
if state.input.is_mouse_button_pressed(winit::event::MouseButton::Right) {
|
if state.input.is_mouse_button_pressed(winit::event::MouseButton::Right) {
|
||||||
let (dx, dy) = state.input.mouse_delta();
|
let (dx, dy) = state.input.mouse_delta();
|
||||||
state.fps_controller.process_mouse(&mut state.camera, dx, dy);
|
state.fps_controller.process_mouse(&mut state.camera, dx, dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut forward = 0.0f32;
|
let mut forward = 0.0f32;
|
||||||
let mut right = 0.0f32;
|
let mut right = 0.0f32;
|
||||||
let mut up = 0.0f32;
|
let mut up = 0.0f32;
|
||||||
@@ -245,29 +258,49 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
if state.input.is_key_pressed(KeyCode::Space) { up += 1.0; }
|
if state.input.is_key_pressed(KeyCode::Space) { up += 1.0; }
|
||||||
if state.input.is_key_pressed(KeyCode::ShiftLeft) { up -= 1.0; }
|
if state.input.is_key_pressed(KeyCode::ShiftLeft) { up -= 1.0; }
|
||||||
state.fps_controller.process_movement(&mut state.camera, forward, right, up, dt);
|
state.fps_controller.process_movement(&mut state.camera, forward, right, up, dt);
|
||||||
|
|
||||||
// 3. Clear per-frame input state for next frame
|
|
||||||
state.input.begin_frame();
|
state.input.begin_frame();
|
||||||
|
|
||||||
// 4. Accumulate time for rotation
|
|
||||||
state.time += dt;
|
state.time += dt;
|
||||||
|
|
||||||
// Update view_proj and light (shared across all entities)
|
// Pre-compute all entity uniforms and write to dynamic buffer
|
||||||
let view_proj = state.camera.view_projection();
|
let view_proj = state.camera.view_projection();
|
||||||
state.camera_uniform.view_proj = view_proj.cols;
|
let cam_pos = [
|
||||||
state.camera_uniform.camera_pos = [
|
|
||||||
state.camera.position.x,
|
state.camera.position.x,
|
||||||
state.camera.position.y,
|
state.camera.position.y,
|
||||||
state.camera.position.z,
|
state.camera.position.z,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let entities = state.world.query2::<Transform, MeshHandle>();
|
||||||
|
let aligned = state.uniform_alignment as usize;
|
||||||
|
|
||||||
|
// Build staging data: one CameraUniform per entity, padded to alignment
|
||||||
|
let total_bytes = entities.len() * aligned;
|
||||||
|
let mut staging = vec![0u8; total_bytes];
|
||||||
|
|
||||||
|
for (i, (_, transform, _)) in entities.iter().enumerate() {
|
||||||
|
let mut uniform = state.camera_uniform;
|
||||||
|
uniform.view_proj = view_proj.cols;
|
||||||
|
uniform.camera_pos = cam_pos;
|
||||||
|
|
||||||
|
let mut t = **transform;
|
||||||
|
t.rotation.y = state.time * 0.5 + t.position.x * 0.1 + t.position.z * 0.1;
|
||||||
|
uniform.model = t.matrix().cols;
|
||||||
|
|
||||||
|
let bytes = bytemuck::bytes_of(&uniform);
|
||||||
|
let offset = i * aligned;
|
||||||
|
staging[offset..offset + bytes.len()].copy_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.gpu.queue.write_buffer(&state.camera_buffer, 0, &staging);
|
||||||
|
|
||||||
|
// Write light uniform
|
||||||
state.gpu.queue.write_buffer(
|
state.gpu.queue.write_buffer(
|
||||||
&state.light_buffer,
|
&state.light_buffer,
|
||||||
0,
|
0,
|
||||||
bytemuck::cast_slice(&[state.light_uniform]),
|
bytemuck::cast_slice(&[state.light_uniform]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Render
|
// Render
|
||||||
let output = match state.gpu.surface.get_current_texture() {
|
let output = match state.gpu.surface.get_current_texture() {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
@@ -296,10 +329,7 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
r: 0.1,
|
r: 0.1, g: 0.1, b: 0.15, a: 1.0,
|
||||||
g: 0.1,
|
|
||||||
b: 0.15,
|
|
||||||
a: 1.0,
|
|
||||||
}),
|
}),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
@@ -317,9 +347,7 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set pipeline and bind groups ONCE
|
|
||||||
render_pass.set_pipeline(&state.pipeline);
|
render_pass.set_pipeline(&state.pipeline);
|
||||||
render_pass.set_bind_group(0, &state.camera_light_bind_group, &[]);
|
|
||||||
render_pass.set_bind_group(1, &state._texture.bind_group, &[]);
|
render_pass.set_bind_group(1, &state._texture.bind_group, &[]);
|
||||||
render_pass.set_vertex_buffer(0, state.mesh.vertex_buffer.slice(..));
|
render_pass.set_vertex_buffer(0, state.mesh.vertex_buffer.slice(..));
|
||||||
render_pass.set_index_buffer(
|
render_pass.set_index_buffer(
|
||||||
@@ -327,21 +355,14 @@ impl ApplicationHandler for ManyCubesApp {
|
|||||||
wgpu::IndexFormat::Uint32,
|
wgpu::IndexFormat::Uint32,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Per-entity draw: query ECS for all (Transform, MeshHandle) pairs
|
// Draw each entity with its dynamic offset
|
||||||
let entities = state.world.query2::<Transform, MeshHandle>();
|
for (i, _) in entities.iter().enumerate() {
|
||||||
for (_entity, transform, _mesh_handle) in &entities {
|
let dynamic_offset = (i as u32) * state.uniform_alignment;
|
||||||
// Compute model matrix with time-based Y rotation
|
render_pass.set_bind_group(
|
||||||
let model = transform.matrix()
|
|
||||||
* Mat4::rotation_y(state.time * 0.5);
|
|
||||||
state.camera_uniform.model = model.cols;
|
|
||||||
|
|
||||||
// Write updated uniform to GPU buffer
|
|
||||||
state.gpu.queue.write_buffer(
|
|
||||||
&state.camera_buffer,
|
|
||||||
0,
|
0,
|
||||||
bytemuck::cast_slice(&[state.camera_uniform]),
|
&state.camera_light_bind_group,
|
||||||
|
&[dynamic_offset],
|
||||||
);
|
);
|
||||||
|
|
||||||
render_pass.draw_indexed(0..state.mesh.num_indices, 0, 0..1);
|
render_pass.draw_indexed(0..state.mesh.num_indices, 0, 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user