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:
2026-03-24 20:14:18 +09:00
parent 19e37f7f96
commit ecf876d249

View File

@@ -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);
} }
} }