A lightweight software rasterizer written in Rust with a focus on type-safe rendering pipelines and minimal dependencies.
- Software Rasterization - Triangle rasterization with per-pixel processing
- Depth Testing - Z-buffer for correct depth ordering
- Color Blending - Custom blending through
FragmentShader::blend - Texture Sampling Examples - 2D texture mapping with nearest-neighbor sampling
- Perspective-Correct Interpolation - Correct varying interpolation in screen space
- Multi-threaded - Parallel rasterization using Rayon
- Type-Safe Pipeline - Compile-time vertex/fragment shader validation
- Generic Rasterizer - Support for different primitive types (triangles, lines, etc.)
- Flexible Shaders - Trait-based vertex and fragment shader system
- Varying Interpolation - Customizable per-vertex attribute interpolation
- Flexible State Management - Compose rendering features at compile time:
.with_depth(depth_buffer)- Enable depth testing.with_blend()- Enable color blending- Combine freely:
.with_depth(...).with_blend()
- Type-Safe Composition - Invalid state combinations fail at compile time
use mini_renderer::{
graphics::{Face, primitive::PrimitiveState},
math::Vec4,
pipeline::shader::{FragmentShader, VertexOutput, VertexShader},
renderer::{Renderer, create_render_pipeline},
};
struct MyVertex {
position: (f32, f32),
color: (f32, f32, f32),
}
#[derive(Clone, Copy, mini_renderer::Varying)]
struct MyVarying {
color: (f32, f32, f32),
}
struct MyVertexShader;
impl VertexShader for MyVertexShader {
type Vertex = MyVertex;
type Varying = MyVarying;
type Uniform = ();
fn vs_main(
&self,
_index: usize,
vertex: &Self::Vertex,
_uniform: &Self::Uniform,
) -> VertexOutput<Self::Varying> {
VertexOutput {
position: Vec4::new(vertex.position.0, vertex.position.1, 0.0, 1.0),
varying: MyVarying { color: vertex.color },
}
}
}
struct MyFragmentShader;
impl FragmentShader for MyFragmentShader {
type Varying = MyVarying;
type Output = u32;
type Uniform = ();
fn fs_main(
&self,
varying: &Self::Varying,
_uniform: &Self::Uniform,
) -> Option<Self::Output> {
let (r, g, b) = varying.color;
let r = (r.clamp(0.0, 1.0) * 255.0) as u32;
let g = (g.clamp(0.0, 1.0) * 255.0) as u32;
let b = (b.clamp(0.0, 1.0) * 255.0) as u32;
Some((r << 24) | (g << 16) | (b << 8) | 0xff)
}
}
let vertices = [
MyVertex { position: (-0.5, -0.5), color: (1.0, 0.0, 0.0) },
MyVertex { position: (0.5, -0.5), color: (0.0, 1.0, 0.0) },
MyVertex { position: (0.0, 0.5), color: (0.0, 0.0, 1.0) },
];
let indices = [0usize, 1, 2];
let mut framebuffer = vec![0u32; 800 * 600];
let mut depth_buffer = vec![1.0; 800 * 600];
let renderer = Renderer::new(800, 600);
let mut pipeline = create_render_pipeline(
MyVertexShader,
MyFragmentShader,
PrimitiveState::default().with_cull_mode(Face::Back),
);
renderer
.begin_render_pass()
.set_pipeline(&mut pipeline)
.with_depth(&mut depth_buffer)
.with_blend()
.draw_indexed(&vertices, indices.into_iter(), &mut framebuffer, &());PrimitiveState::default() uses triangle-list topology, counter-clockwise front faces, and no culling. Use PrimitiveState::new(PrimitiveTopology::...) to select another topology.
mini-renderer requires Rust 1.94 or newer. The default feature set enables the standard library, glam, the Varying derive macro, and Rayon-based parallel rendering.
For a serial no_std build with libm, run:
cargo check --no-default-features --features libmsrc/
├── lib.rs # Library entry point
├── renderer.rs # Rendering pass and pipeline binding
├── pipeline/
│ ├── mod.rs # Pipeline definition
│ ├── shader.rs # Vertex/Fragment shader traits
│ └── varying.rs # Varying interpolation trait
├── graphics/
│ ├── mod.rs
│ ├── primitive.rs # Primitive pipeline state
│ ├── rasterizer.rs # Point, line, and triangle rasterization
│ └── topology.rs # Primitive assembly and topology markers
└── math.rs # Vector types and math utilities
Main rendering interface:
pub struct Renderer {
width: usize,
height: usize,
}
impl Renderer {
pub fn begin_render_pass(&self) -> RenderPass<'_>;
pub fn width(&self) -> usize;
pub fn height(&self) -> usize;
}Type-safe pipeline state with depth and blend modes:
D: Depth mode (NoDepthorWithDepth)B: Blend mode (NoBlendorWithBlend)
Methods available depend on state:
// Only on NoDepth
.with_depth(depth_buffer)
// Only on NoBlend
.with_blend()
// Available in appropriate states
.draw(vertices, framebuffer, uniform)
.draw_indexed(vertices, indices, framebuffer, uniform)Low-level rendering pipeline:
T: Primitive typeV: Vertex shaderF: Fragment shader
VertexShader
pub trait VertexShader {
type Vertex;
type Varying;
type Uniform;
fn vs_main(
&self,
index: usize,
vertex: &Self::Vertex,
uniform: &Self::Uniform,
) -> VertexOutput<Self::Varying>;
}FragmentShader
pub trait FragmentShader {
type Varying;
type Output: Copy;
type Uniform;
fn fs_main(
&self,
varying: &Self::Varying,
uniform: &Self::Uniform,
) -> Option<Self::Output>;
fn blend(output: Self::Output, background: Self::Output) -> Self::Output {
output
}
}Custom interpolation for vertex attributes:
pub trait Varying: Sized + Copy {
fn interpolate(v0: Self, v1: Self, v2: Self, w0: f32, w1: f32, w2: f32) -> Self;
}The renderer uses Rust's type system to prevent invalid state combinations:
// Compile error: can't call with_blend() twice
pipeline.with_blend().with_blend().draw_indexed(...);
// Compile error: can't draw without fragment shader state
pipeline.draw_indexed(...); // Missing method in initial stateFluent API for composing rendering operations:
renderer
.begin_render_pass()
.set_pipeline(&mut pipeline)
.with_depth(&mut depth_buffer) // Optional
.with_blend() // Optional
.draw_indexed(vertices, indices, framebuffer, uniform);Users define custom behavior via traits:
VertexShader- Vertex transformation and varying outputFragmentShader- Fragment color and blendingVarying- Attribute interpolation strategy
The rasterizer uses Rayon to parallelize per-tile processing. Work is distributed across CPU cores for better performance on large framebuffers.
The with_depth() and with_blend() methods use Rust's type system with zero runtime cost (compile-time specialization via monomorphization).
- Vertex layout is defined by the user's
VertexShader::Vertextype - Pipeline-owned vertex and index caches are reused between draw calls
- Depth buffers use
f32 - Framebuffer element types are generic
- rayon (1.11.0) - Parallel rasterization
- glam (0.32.0) - Math library (enabled by default)
- GPU acceleration (pure software rasterization)
- Compute shaders
- Shaders execute on the CPU; the default
rayonfeature parallelizes their work - Vertex and fragment shaders currently share one uniform type per draw
- No GPU acceleration or hardware texture sampling
- Texture sampling is implemented by examples rather than a core texture abstraction
The codebase is organized for clarity and extensibility:
- New Primitive Type? Implement
Primitivetrait ingraphics/topology.rs - New Rasterizer? Implement
Rasterizertrait ingraphics/rasterizer.rs - Custom Shaders? Implement
VertexShaderandFragmentShadertraits
- SIMD optimizations for rasterization
- Homogeneous clipping before perspective division
- Texture compression support
- Material system with multiple render passes
MIT
For issues, questions, or suggestions, please open an issue on GitHub or contact the maintainers.
Happy rendering! 🎨