Skip to content

代码说明

页面右上角的 github 图标可跳转到代码库

使用的同一份代码编译为 WebGL 或 WebGPU 目标

wgpu (dawn 也是) 实现的 WebGPU 接口支持通过编译参数设置来运行时要使用的图形接口后端,但在使用 WebGL 时功能特性会受到限制。这与 WebGL 库对接 WebGPU 接口有所不同:因为 WebGPU 接口是基于管线的,由管线映可直接射回状态机。然而,基于状态机的接口映射到管线时,不仅功能受限,还会极大地降低管线的复用性,从而导致性能损耗。

sh
# 构建 webgpu 包并运行
cargo run-wasm
# 构建 webgl 包并运行
cargo run-wasm --features=webgl
# 构建 webgpu 包并运行
cargo run-wasm
# 构建 webgl 包并运行
cargo run-wasm --features=webgl

帧时间统计

帧时间统计由是前 200 帧计算的平均值:

rust
pub struct FrameHistory {
    // 上一帧开始到当前帧开始的时间间隔历史
    frame_times: History<f32>,
    // 一帧内的 cpu 耗时历史
    cpu_usage_times: History<f32>,
    // UI 刷新计数,用于降低 UI 上数值的刷新率(不影响准确性)
    refresh_count: u64,
    // 帧率
    frame_rate: f32,
    // 单帧 cpu 耗时
    cpu_usage: f32,
}

impl Default for FrameHistory {
    fn default() -> Self {
        // 最大时间步长为 1 秒
        let max_age: f32 = 1.0;
        Self {
            frame_times: History::new(0..200, max_age),
            cpu_usage_times: History::new(0..200, max_age),
            refresh_count: 0,
            frame_rate: 0.,
            cpu_usage: 0.,
        }
    }
}

impl FrameHistory {
    pub fn on_new_frame(&mut self, now: f64, frame_time: f32, cpu_usage_time: f32) {
        self.frame_times.add(now, frame_time);
        self.cpu_usage_times.add(now, cpu_usage_time);
        self.refresh_count += 1;
        if self.refresh_count % 20 == 0 {
            // 每 20 帧重新计算一次待显示的帧数据
            self.frame_rate = 1.0 / self.frame_times.average().unwrap_or_default();
            self.cpu_usage = self.cpu_usage_times.average().unwrap_or_default() * 1000.
        }
    }
}
pub struct FrameHistory {
    // 上一帧开始到当前帧开始的时间间隔历史
    frame_times: History<f32>,
    // 一帧内的 cpu 耗时历史
    cpu_usage_times: History<f32>,
    // UI 刷新计数,用于降低 UI 上数值的刷新率(不影响准确性)
    refresh_count: u64,
    // 帧率
    frame_rate: f32,
    // 单帧 cpu 耗时
    cpu_usage: f32,
}

impl Default for FrameHistory {
    fn default() -> Self {
        // 最大时间步长为 1 秒
        let max_age: f32 = 1.0;
        Self {
            frame_times: History::new(0..200, max_age),
            cpu_usage_times: History::new(0..200, max_age),
            refresh_count: 0,
            frame_rate: 0.,
            cpu_usage: 0.,
        }
    }
}

impl FrameHistory {
    pub fn on_new_frame(&mut self, now: f64, frame_time: f32, cpu_usage_time: f32) {
        self.frame_times.add(now, frame_time);
        self.cpu_usage_times.add(now, cpu_usage_time);
        self.refresh_count += 1;
        if self.refresh_count % 20 == 0 {
            // 每 20 帧重新计算一次待显示的帧数据
            self.frame_rate = 1.0 / self.frame_times.average().unwrap_or_default();
            self.cpu_usage = self.cpu_usage_times.average().unwrap_or_default() * 1000.
        }
    }
}

测试场景

每帧 1000 个 draw call + 1000 次 buffer 修改:

rust
/// 管线数量
/// 管线本身是可重用的,为了测试,用最差的做法
const PSO_COUNT: usize = 1000;

impl CompScenario for ThousandsEntity {
    fn draw_by_pass<'a, 'b: 'a>(&'b mut self, app: &AppSurface, rpass: &mut wgpu::RenderPass<'b>) {
        // 先更新数据
        for i in 0..PSO_COUNT {
            let original_mat = self.model_uniform_data[i as usize];
            let model_mat = glam::Mat4::from_cols_array_2d(&original_mat.model_mat);
            let rotation_data = &mut self.model_rotation_data[i as usize];
            rotation_data.1 += 0.05 * rotation_data.2;
            let rotation_mat = glam::Mat4::from_axis_angle(rotation_data.0, rotation_data.1);
            let a_model_mat = (model_mat * rotation_mat).to_cols_array_2d();
            app.queue.write_buffer(
                &self.model_uniform_buf[i].buffer,
                0,
                bytemuck::bytes_of(&a_model_mat),
            );
        }

        // 再绘制
        for i in 0..PSO_COUNT {
            rpass.set_pipeline(&self.pipeline_list[i]);
            rpass.set_bind_group(0, &self.bg_setting_list[i].bind_group, &[]);
            rpass.set_index_buffer(
                self.model.index_buf_list[i].slice(..),
                wgpu::IndexFormat::Uint32,
            );
            rpass.set_vertex_buffer(0, self.model.vertex_buf_list[i].slice(..));
            rpass.draw_indexed(0..self.model.index_count, 0, 0..1);
        }
    }
}
/// 管线数量
/// 管线本身是可重用的,为了测试,用最差的做法
const PSO_COUNT: usize = 1000;

impl CompScenario for ThousandsEntity {
    fn draw_by_pass<'a, 'b: 'a>(&'b mut self, app: &AppSurface, rpass: &mut wgpu::RenderPass<'b>) {
        // 先更新数据
        for i in 0..PSO_COUNT {
            let original_mat = self.model_uniform_data[i as usize];
            let model_mat = glam::Mat4::from_cols_array_2d(&original_mat.model_mat);
            let rotation_data = &mut self.model_rotation_data[i as usize];
            rotation_data.1 += 0.05 * rotation_data.2;
            let rotation_mat = glam::Mat4::from_axis_angle(rotation_data.0, rotation_data.1);
            let a_model_mat = (model_mat * rotation_mat).to_cols_array_2d();
            app.queue.write_buffer(
                &self.model_uniform_buf[i].buffer,
                0,
                bytemuck::bytes_of(&a_model_mat),
            );
        }

        // 再绘制
        for i in 0..PSO_COUNT {
            rpass.set_pipeline(&self.pipeline_list[i]);
            rpass.set_bind_group(0, &self.bg_setting_list[i].bind_group, &[]);
            rpass.set_index_buffer(
                self.model.index_buf_list[i].slice(..),
                wgpu::IndexFormat::Uint32,
            );
            rpass.set_vertex_buffer(0, self.model.vertex_buf_list[i].slice(..));
            rpass.draw_indexed(0..self.model.index_count, 0, 0..1);
        }
    }
}