Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 262 additions & 25 deletions Cargo.lock

Large diffs are not rendered by default.

57 changes: 30 additions & 27 deletions bench.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,48 @@
| **lookup** | Replace 36-branch vertex shader switch with a uniform-buffer lookup table |
| **passes** | CPU layer culling for line draws (LineBatch); opaque/transparent pipeline split; infill promoted to opaque |
| **packed** | Rhombus instance data packed 48 B → 28 B (f32 dir, palette UBO, half_width moved to FrameUniforms) |
| **simplify** | Douglas–Peucker perimeter simplification (0.01 mm tol) — drops near-collinear vertices before toolpaths become rhombus instances |

> **simplify** is the largest render win to date: on the VS-bound liver model FPS jumped **6.0 → 21.6 (3.6×)** purely from emitting fewer rhombus instances. Note `offset ms` ticks *up* slightly (the DP pass is extra CPU work), but `plan ms` drops sharply (172 → 45 ms on liver) because nearest-neighbor ordering now walks far fewer vertices. Non-VS-bound models (sphere, block) are vsync-capped at ~31 fps, so their gains show up in `plan ms` instead.

---

## liver.stl (38 k triangles, 998 layers, dense toolpaths) (VS-bound)

| metric | legacy | wgpu | lookup | passes | packed |
|---|--:|--:|--:|--:|--:|
| load ms | 20.4 | 21.1 | 18.4 | 17.4 | 18.4 |
| slice ms | 20.4 | 21.1 | 18.4 | 17.4 | 18.4 |
| offset ms | 2,877.9 | 2,893.5 | 2,733.2 | **2,741.0** | **2,836.5** |
| plan ms | 175.2 | 175.3 | 177.1 | **146.8** | **172.4** |
| **init total ms** | **3,093.9** | **3,111.0** | 2,947.1 | **2,922.6** | **3,045.7** |
| avg fps | 3.1 | 3.4 | 5.0 | **5.7** | **6.0** |
| avg frame ms | 322.6 | 294.1 | 200.0 | **175.4** | **166.7** |
| metric | legacy | wgpu | lookup | passes | packed | simplify |
|---|--:|--:|--:|--:|--:|--:|
| load ms | 20.4 | 21.1 | 18.4 | 17.4 | 18.4 | 18.8 |
| slice ms | 20.4 | 21.1 | 18.4 | 17.4 | 18.4 | 18.8 |
| offset ms | 2,877.9 | 2,893.5 | 2,733.2 | **2,741.0** | **2,836.5** | 2,995.6 |
| plan ms | 175.2 | 175.3 | 177.1 | **146.8** | **172.4** | **44.6** |
| **init total ms** | **3,093.9** | **3,111.0** | 2,947.1 | **2,922.6** | **3,045.7** | 3,077.8 |
| avg fps | 3.1 | 3.4 | 5.0 | **5.7** | **6.0** | **21.6** |
| avg frame ms | 322.6 | 294.1 | 200.0 | **175.4** | **166.7** | **46.3** |

---

## sphere_smooth.stl (29 k triangles, 249 layers) (not VS-bound)

| metric | legacy | wgpu | lookup | passes | packed |
|---|--:|--:|--:|--:|--:|
| load ms | 5.6 | 5.6 | 6.0 | 5.0 | 4.6 |
| slice ms | 5.6 | 5.6 | 6.0 | 5.0 | 4.6 |
| offset ms | 85.9 | 83.1 | 76.0 | **69.9** | **80.8** |
| plan ms | 7.9 | 8.0 | 7.9 | **6.3** | **8.5** |
| **init total ms** | 105.0 | 102.3 | 95.9 | **86.2** | **98.5** |
| avg fps | 31.6 | 32.1 | 30.2 | **34.1** | **30.9** |
| avg frame ms | 31.6 | 31.2 | 33.1 | **29.3** | **32.4** |
| metric | legacy | wgpu | lookup | passes | packed | simplify |
|---|--:|--:|--:|--:|--:|--:|
| load ms | 5.6 | 5.6 | 6.0 | 5.0 | 4.6 | 5.8 |
| slice ms | 5.6 | 5.6 | 6.0 | 5.0 | 4.6 | 5.8 |
| offset ms | 85.9 | 83.1 | 76.0 | **69.9** | **80.8** | 79.4 |
| plan ms | 7.9 | 8.0 | 7.9 | **6.3** | **8.5** | **2.7** |
| **init total ms** | 105.0 | 102.3 | 95.9 | **86.2** | **98.5** | 93.7 |
| avg fps | 31.6 | 32.1 | 30.2 | **34.1** | **30.9** | 30.9 |
| avg frame ms | 31.6 | 31.2 | 33.1 | **29.3** | **32.4** | 32.4 |

---

## block100.stl (12 triangles, 500 layers) (not VS-bound)

| metric | legacy | wgpu | lookup | passes | packed |
|---|--:|--:|--:|--:|--:|
| load ms | 1.2 | 0.8 | 0.8 | 1.2 | 0.9 |
| slice ms | 1.2 | 0.8 | 0.8 | 1.2 | 0.9 |
| offset ms | 3.7 | 4.1 | 4.0 | **4.2** | **3.7** |
| plan ms | 11.6 | 11.5 | 13.4 | **13.6** | **12.4** |
| **init total ms** | 17.7 | **17.2** | 19.0 | 20.2 | **17.9** |
| avg fps | 22.1 | 19.3 | 19.4 | **28.2** | **27.7** |
| avg frame ms | 45.2 | 51.8 | 51.6 | **35.5** | **36.1** |
| metric | legacy | wgpu | lookup | passes | packed | simplify |
|---|--:|--:|--:|--:|--:|--:|
| load ms | 1.2 | 0.8 | 0.8 | 1.2 | 0.9 | 0.7 |
| slice ms | 1.2 | 0.8 | 0.8 | 1.2 | 0.9 | 0.7 |
| offset ms | 3.7 | 4.1 | 4.0 | **4.2** | **3.7** | 3.2 |
| plan ms | 11.6 | 11.5 | 13.4 | **13.6** | **12.4** | **5.0** |
| **init total ms** | 17.7 | **17.2** | 19.0 | 20.2 | **17.9** | **9.6** |
| avg fps | 22.1 | 19.3 | 19.4 | **28.2** | **27.7** | **30.9** |
| avg frame ms | 45.2 | 51.8 | 51.6 | **35.5** | **36.1** | **32.4** |
99 changes: 61 additions & 38 deletions crates/katana-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use std::fs;
use std::time::Instant;

use clap::{Parser, Subcommand};
use katana_core::{offset, slicer, stl, svg};
use rayon::prelude::*;
use clap::{ Parser, Subcommand };
use katana_core::{ gcode::{ self, GcodeConfig }, offset, slicer, stl, planner };

#[derive(Parser)]
#[command(name = "katana", about = "3D printing slicer")]
Expand Down Expand Up @@ -61,7 +60,17 @@ fn main() {
infill_density,
bottom_layers,
top_layers,
} => cmd_slice(&file, layer_height, &output, nozzle_width, perimeters, infill_density, bottom_layers, top_layers),
} =>
cmd_slice(
&file,
layer_height,
&output,
nozzle_width,
perimeters,
infill_density,
bottom_layers,
top_layers
),
}
}

Expand All @@ -86,16 +95,20 @@ fn cmd_info(file: &str) {
println!(" Bounding box:");
println!(" min: ({:.3}, {:.3}, {:.3})", min.x, min.y, min.z);
println!(" max: ({:.3}, {:.3}, {:.3})", max.x, max.y, max.z);
println!(
" size: {:.3} x {:.3} x {:.3}",
max.x - min.x,
max.y - min.y,
max.z - min.z
);
println!(" size: {:.3} x {:.3} x {:.3}", max.x - min.x, max.y - min.y, max.z - min.z);
println!(" Volume: {:.3}", mesh.volume());
}

fn cmd_slice(file: &str, layer_height: f32, output_dir: &str, nozzle_width: f32, perimeters: usize, infill_density: f32, bottom_layers: usize, top_layers: usize) {
fn cmd_slice(
file: &str,
layer_height: f32,
output_dir: &str,
nozzle_width: f32,
perimeters: usize,
infill_density: f32,
bottom_layers: usize,
top_layers: usize
) {
let t_load = Instant::now();
let mesh = load_mesh(file);
let load_ms = t_load.elapsed().as_secs_f64() * 1000.0;
Expand All @@ -104,12 +117,12 @@ fn cmd_slice(file: &str, layer_height: f32, output_dir: &str, nozzle_width: f32,
println!("Slicing: {file}");
println!(" Triangles: {} (loaded in {:.1}ms)", mesh.triangles.len(), load_ms);
println!(" Layer height: {layer_height} mm");
println!(" Nozzle: {nozzle_width} mm, {perimeters} perimeters, {:.0}% infill", infill_density * 100.0);
println!(" Bottom layers: {bottom_layers}, Top layers: {top_layers}");
println!(
" Z range: {:.3} to {:.3}",
min.z, max.z
" Nozzle: {nozzle_width} mm, {perimeters} perimeters, {:.0}% infill",
infill_density * 100.0
);
println!(" Bottom layers: {bottom_layers}, Top layers: {top_layers}");
println!(" Z range: {:.3} to {:.3}", min.z, max.z);

let t_slice = Instant::now();
let result = slicer::slice_mesh(&mesh, layer_height);
Expand All @@ -132,32 +145,42 @@ fn cmd_slice(file: &str, layer_height: f32, output_dir: &str, nozzle_width: f32,
};

let t_offset = Instant::now();
let toolpath_result = offset::generate_toolpaths(&result, &perim_config, &infill_config, &surface_config);
let toolpath_result = offset::generate_toolpaths(
&result,
&perim_config,
&infill_config,
&surface_config
);
let offset_ms = t_offset.elapsed().as_secs_f64() * 1000.0;

println!(" Perimeters generated in {:.1}ms", offset_ms);

let t_svg = Instant::now();
fs::create_dir_all(output_dir).unwrap_or_else(|e| {
eprintln!("Failed to create output directory: {e}");
std::process::exit(1);
});
let t_plan = Instant::now();
let speed_config = planner::SpeedConfig {
travel: 150.0,
perimeter: 30.0,
infill: 60.0,
surface: 40.0,
};
let planned_result = planner::plan_toolpaths(&toolpath_result, &speed_config);
let plan_ms = t_plan.elapsed().as_secs_f64() * 1000.0;

println!(" Planned result generated in {:.1}ms", plan_ms);

let t_gcode = Instant::now();
let mut exporter = gcode::Gcode {
e: 0.0,
config: GcodeConfig {
filament_diameter: 1.75,
nozzle_width,
layer_height,
},
out: String::new(),
};
let out = exporter.export(&planned_result);
let gcode_file = file.replace(".stl", ".gcode");
fs::write(gcode_file, out).unwrap_err();
let gcode_ms = t_gcode.elapsed().as_secs_f64() * 1000.0;

let output_dir_owned = output_dir.to_string();
toolpath_result
.layers
.par_iter()
.zip(result.layers.par_iter())
.enumerate()
.for_each(|(i, (tp_layer, orig_layer))| {
let svg_content = svg::toolpath_layer_to_svg(tp_layer, orig_layer, 2.0);
let path = format!("{output_dir_owned}/layer_{i:04}.svg");
fs::write(&path, &svg_content).unwrap_or_else(|e| {
eprintln!("Failed to write {path}: {e}");
std::process::exit(1);
});
});
let svg_ms = t_svg.elapsed().as_secs_f64() * 1000.0;

println!(" SVGs written to: {output_dir}/ ({:.1}ms)", svg_ms);
println!(" G-code written to: {output_dir}/ ({:.1}ms)", gcode_ms);
}
3 changes: 3 additions & 0 deletions crates/katana-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ byteorder = "1"
thiserror = "2"
i_overlay = "4.4"
rayon = "1"

[dev-dependencies]
nalgebra = "0.33"
Loading
Loading