Skip to content

Commit e1c4cce

Browse files
committed
refactor: update
Signed-off-by: Niu Zhihong <zhihong@nzhnb.com>
1 parent 3bf8126 commit e1c4cce

18 files changed

Lines changed: 1826 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 337 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

simple_renderer/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ edition = "2021"
55

66
[dependencies]
77
glam = "0.29"
8-
tobj = { version = "4.0", features = ["async"] }
8+
tobj = "4.0"
99
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tga"] }
1010
rayon = "1.10"
1111
log = "0.4"
1212
thiserror = "2"
1313

1414
[dev-dependencies]
1515
env_logger = "0.11"
16+
proptest = "1"
17+
criterion = { version = "0.5", features = ["html_reports"] }
18+
19+
[[bench]]
20+
name = "render_bench"
21+
harness = false
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use glam::{Mat4, Vec3};
3+
use simple_renderer::{Light, Model, RenderingMode, Shader, SimpleRenderer};
4+
5+
const BENCH_W: usize = 200;
6+
const BENCH_H: usize = 150;
7+
const TEAPOT_PATH: &str = "../obj/utah-teapot-texture/teapot.obj";
8+
9+
fn setup_shader() -> Shader {
10+
let model_matrix = Mat4::from_scale(Vec3::splat(0.02))
11+
* Mat4::from_translation(Vec3::new(0.0, -5.0, 0.0))
12+
* Mat4::from_rotation_x((-105.0_f32).to_radians());
13+
14+
let mut shader = Shader::new();
15+
shader.set_uniform("modelMatrix", model_matrix);
16+
shader.set_uniform(
17+
"viewMatrix",
18+
Mat4::look_at_rh(Vec3::new(0.0, 0.0, 1.0), Vec3::ZERO, Vec3::Y),
19+
);
20+
shader.set_uniform(
21+
"projectionMatrix",
22+
Mat4::perspective_rh_gl(
23+
60.0_f32.to_radians(),
24+
BENCH_W as f32 / BENCH_H as f32,
25+
0.1,
26+
100.0,
27+
),
28+
);
29+
shader.set_uniform("cameraPos", Vec3::new(0.0, 0.0, 1.0));
30+
shader.set_lights(&[
31+
Light {
32+
direction: Vec3::new(1.0, 5.0, 1.0),
33+
..Light::default()
34+
},
35+
Light {
36+
direction: Vec3::new(-3.0, -2.0, 2.0),
37+
..Light::default()
38+
},
39+
]);
40+
shader
41+
}
42+
43+
fn bench_render_mode(c: &mut Criterion, name: &str, mode: RenderingMode) {
44+
let model = Model::load(TEAPOT_PATH).expect("load model");
45+
let mut shader = setup_shader();
46+
let mut renderer = SimpleRenderer::new(BENCH_W, BENCH_H);
47+
renderer.set_rendering_mode(mode);
48+
let mut buffer = vec![0u32; BENCH_W * BENCH_H];
49+
50+
c.bench_function(name, |b| {
51+
b.iter(|| {
52+
buffer.fill(0);
53+
renderer
54+
.draw_model(
55+
black_box(&model),
56+
black_box(&mut shader),
57+
black_box(&mut buffer),
58+
)
59+
.unwrap();
60+
})
61+
});
62+
}
63+
64+
fn render_benchmarks(c: &mut Criterion) {
65+
bench_render_mode(c, "per_triangle_200x150", RenderingMode::PerTriangle);
66+
bench_render_mode(c, "tile_based_200x150", RenderingMode::TileBased);
67+
bench_render_mode(c, "deferred_200x150", RenderingMode::Deferred);
68+
bench_render_mode(
69+
c,
70+
"tile_based_deferred_200x150",
71+
RenderingMode::TileBasedDeferred,
72+
);
73+
}
74+
75+
criterion_group!(benches, render_benchmarks);
76+
criterion_main!(benches);

simple_renderer/src/color.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,32 @@ impl Color {
3535
/// Create a color from floats in the `[0.0, 255.0]` range.
3636
///
3737
/// Values are rounded via `+0.5` then truncated to `u8`.
38+
/// Out-of-range values are clamped to `[0.0, 255.0]` before casting.
3839
/// Note: these are raw `[0, 255]` floats, NOT normalized `[0, 1]`.
3940
#[inline]
4041
pub fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
4142
Self {
4243
channels: [
43-
(r + 0.5) as u8,
44-
(g + 0.5) as u8,
45-
(b + 0.5) as u8,
46-
(a + 0.5) as u8,
44+
((r + 0.5).clamp(0.0, 255.0)) as u8,
45+
((g + 0.5).clamp(0.0, 255.0)) as u8,
46+
((b + 0.5).clamp(0.0, 255.0)) as u8,
47+
((a + 0.5).clamp(0.0, 255.0)) as u8,
4748
],
4849
}
4950
}
5051

5152
/// Create a color from normalized floats in the `[0.0, 1.0]` range.
5253
///
5354
/// Each component is multiplied by 255.0, then truncated to `u8`.
55+
/// Out-of-range values are clamped to `[0.0, 255.0]` before casting.
5456
#[inline]
5557
pub fn from_normalized(r: f32, g: f32, b: f32, a: f32) -> Self {
5658
Self {
5759
channels: [
58-
(r * 255.0) as u8,
59-
(g * 255.0) as u8,
60-
(b * 255.0) as u8,
61-
(a * 255.0) as u8,
60+
((r * 255.0).clamp(0.0, 255.0)) as u8,
61+
((g * 255.0).clamp(0.0, 255.0)) as u8,
62+
((b * 255.0).clamp(0.0, 255.0)) as u8,
63+
((a * 255.0).clamp(0.0, 255.0)) as u8,
6264
],
6365
}
6466
}
@@ -253,6 +255,33 @@ mod tests {
253255
assert_eq!(c, Color::new(255, 255, 255, 255));
254256
}
255257

258+
#[test]
259+
fn from_f32_clamps_negative() {
260+
let c = Color::from_f32(-10.0, -5.0, 0.0, 255.0);
261+
assert_eq!(c.r(), 0);
262+
assert_eq!(c.g(), 0);
263+
assert_eq!(c.b(), 0);
264+
assert_eq!(c.a(), 255);
265+
}
266+
267+
#[test]
268+
fn from_f32_clamps_overflow() {
269+
let c = Color::from_f32(300.0, 256.0, 255.0, 255.0);
270+
assert_eq!(c.r(), 255);
271+
assert_eq!(c.g(), 255);
272+
assert_eq!(c.b(), 255);
273+
assert_eq!(c.a(), 255);
274+
}
275+
276+
#[test]
277+
fn from_normalized_clamps_overflow() {
278+
let c = Color::from_normalized(1.5, -0.5, 0.5, 1.0);
279+
assert_eq!(c.r(), 255);
280+
assert_eq!(c.g(), 0);
281+
assert_eq!(c.b(), 127);
282+
assert_eq!(c.a(), 255);
283+
}
284+
256285
// ── Constant tests ─────────────────────────────────────────────────
257286

258287
#[test]

simple_renderer/src/error.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
use thiserror::Error;
22

33
/// Errors that can occur in the renderer.
4+
#[non_exhaustive]
45
#[derive(Debug, Error)]
56
pub enum RendererError {
6-
#[error("Model loading failed: {0}")]
7-
ModelLoad(String),
7+
#[error("Model loading failed: {path}")]
8+
ModelLoad {
9+
path: String,
10+
#[source]
11+
source: Box<dyn std::error::Error + Send + Sync>,
12+
},
813

9-
#[error("Texture loading failed: {0}")]
10-
TextureLoad(String),
14+
#[error("Texture loading failed: {path}")]
15+
TextureLoad {
16+
path: String,
17+
#[source]
18+
source: Box<dyn std::error::Error + Send + Sync>,
19+
},
1120

1221
#[error("Rendering failed: {0}")]
1322
RenderFailed(String),
@@ -25,8 +34,11 @@ mod tests {
2534

2635
#[test]
2736
fn error_display() {
28-
let e = RendererError::ModelLoad("file not found".into());
29-
assert_eq!(e.to_string(), "Model loading failed: file not found");
37+
let e = RendererError::ModelLoad {
38+
path: "test.obj".into(),
39+
source: "file not found".into(),
40+
};
41+
assert!(e.to_string().contains("test.obj"));
3042
}
3143

3244
#[test]
@@ -45,4 +57,14 @@ mod tests {
4557
let err: Result<i32> = Err(RendererError::RenderFailed("oops".into()));
4658
assert!(err.is_err());
4759
}
60+
61+
#[test]
62+
fn error_source_chain() {
63+
use std::error::Error;
64+
let e = RendererError::ModelLoad {
65+
path: "model.obj".into(),
66+
source: Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "missing")),
67+
};
68+
assert!(e.source().is_some());
69+
}
4870
}

simple_renderer/src/material.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::path::Path;
2+
use std::sync::Arc;
23

34
use crate::color::Color;
45
use crate::error::{RendererError, Result};
@@ -17,8 +18,10 @@ impl Texture {
1718
/// Load a texture from an image file (PNG, JPEG, BMP, TGA).
1819
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Texture> {
1920
let path = path.as_ref();
20-
let img = image::open(path)
21-
.map_err(|e| RendererError::TextureLoad(format!("{}: {}", path.display(), e)))?;
21+
let img = image::open(path).map_err(|e| RendererError::TextureLoad {
22+
path: path.display().to_string(),
23+
source: Box::new(e),
24+
})?;
2225

2326
let rgba = img.to_rgba8();
2427
let (width, height) = rgba.dimensions();
@@ -64,9 +67,9 @@ pub struct Material {
6467
pub ambient: Vec3,
6568
pub diffuse: Vec3,
6669
pub specular: Vec3,
67-
pub ambient_texture: Option<Texture>,
68-
pub diffuse_texture: Option<Texture>,
69-
pub specular_texture: Option<Texture>,
70+
pub ambient_texture: Option<Arc<Texture>>,
71+
pub diffuse_texture: Option<Arc<Texture>>,
72+
pub specular_texture: Option<Arc<Texture>>,
7073
}
7174

7275
impl Default for Material {

simple_renderer/src/model.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ impl Model {
4141
..Default::default()
4242
};
4343

44-
let (models, materials_result) = tobj::load_obj(obj_path, &load_options)
45-
.map_err(|e| RendererError::ModelLoad(format!("{}: {}", path, e)))?;
44+
let (models, materials_result) =
45+
tobj::load_obj(obj_path, &load_options).map_err(|e| RendererError::ModelLoad {
46+
path: path.to_string(),
47+
source: Box::new(e),
48+
})?;
4649

4750
// Load materials — warn on failure, fall back to empty vec.
4851
let materials = match materials_result {
@@ -55,7 +58,7 @@ impl Model {
5558

5659
let mut vertices: Vec<Vertex> = Vec::new();
5760
let mut faces: Vec<Face> = Vec::new();
58-
let mut texture_cache: HashMap<PathBuf, Texture> = HashMap::new();
61+
let mut texture_cache: HashMap<PathBuf, Arc<Texture>> = HashMap::new();
5962

6063
for model in &models {
6164
let mesh = &model.mesh;
@@ -134,7 +137,7 @@ impl Model {
134137
material_id: Option<usize>,
135138
materials: &[tobj::Material],
136139
directory: &str,
137-
texture_cache: &mut HashMap<PathBuf, Texture>,
140+
texture_cache: &mut HashMap<PathBuf, Arc<Texture>>,
138141
) -> Material {
139142
let mat_idx = match material_id {
140143
Some(idx) if idx < materials.len() => idx,
@@ -193,22 +196,23 @@ impl Model {
193196
fn load_texture_cached(
194197
texture_name: Option<&str>,
195198
directory: &str,
196-
cache: &mut HashMap<PathBuf, Texture>,
197-
) -> Option<Texture> {
199+
cache: &mut HashMap<PathBuf, Arc<Texture>>,
200+
) -> Option<Arc<Texture>> {
198201
let name = texture_name?;
199202
if name.is_empty() {
200203
return None;
201204
}
202205

203206
let full_path = PathBuf::from(directory).join(name);
204207

205-
if let Some(cached) = cache.get(&full_path) {
206-
return Some(cached.clone());
208+
if cache.contains_key(&full_path) {
209+
return Some(Arc::clone(cache.get(&full_path).unwrap()));
207210
}
208211

209212
match Texture::load_from_file(&full_path) {
210213
Ok(tex) => {
211-
cache.insert(full_path, tex.clone());
214+
let tex = Arc::new(tex);
215+
cache.insert(full_path, Arc::clone(&tex));
212216
Some(tex)
213217
}
214218
Err(e) => {
@@ -244,8 +248,8 @@ mod tests {
244248
let result = Model::load("/nonexistent/path/model.obj");
245249
assert!(result.is_err());
246250
match result.unwrap_err() {
247-
RendererError::ModelLoad(msg) => {
248-
assert!(!msg.is_empty());
251+
RendererError::ModelLoad { path, .. } => {
252+
assert!(!path.is_empty());
249253
}
250254
other => panic!("Expected ModelLoad error, got: {:?}", other),
251255
}

simple_renderer/src/renderer.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::renderers::Renderer;
77
use crate::shader::Shader;
88
use log::info;
99

10+
#[non_exhaustive]
1011
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112
#[repr(u8)]
1213
pub enum RenderingMode {
@@ -27,6 +28,20 @@ impl std::fmt::Display for RenderingMode {
2728
}
2829
}
2930

31+
impl TryFrom<u8> for RenderingMode {
32+
type Error = u8;
33+
34+
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
35+
match value {
36+
0 => Ok(Self::PerTriangle),
37+
1 => Ok(Self::TileBased),
38+
2 => Ok(Self::Deferred),
39+
3 => Ok(Self::TileBasedDeferred),
40+
other => Err(other),
41+
}
42+
}
43+
}
44+
3045
pub struct SimpleRenderer {
3146
width: usize,
3247
height: usize,
@@ -167,4 +182,21 @@ mod tests {
167182
assert_eq!(r.rendering_mode(), *mode);
168183
}
169184
}
185+
186+
#[test]
187+
fn try_from_u8_valid() {
188+
assert_eq!(RenderingMode::try_from(0), Ok(RenderingMode::PerTriangle));
189+
assert_eq!(RenderingMode::try_from(1), Ok(RenderingMode::TileBased));
190+
assert_eq!(RenderingMode::try_from(2), Ok(RenderingMode::Deferred));
191+
assert_eq!(
192+
RenderingMode::try_from(3),
193+
Ok(RenderingMode::TileBasedDeferred)
194+
);
195+
}
196+
197+
#[test]
198+
fn try_from_u8_invalid() {
199+
assert_eq!(RenderingMode::try_from(4), Err(4));
200+
assert_eq!(RenderingMode::try_from(255), Err(255));
201+
}
170202
}

0 commit comments

Comments
 (0)