├── .rustfmt.toml ├── demos ├── .gitignore ├── constraints │ ├── .gitignore │ ├── screenshot.png │ ├── Cargo.toml │ ├── README.md │ └── index.html ├── web-editor │ ├── web │ │ ├── src │ │ │ ├── global.d.ts │ │ │ ├── constants.ts │ │ │ ├── rhai.grammar.d.ts │ │ │ ├── camera.ts │ │ │ ├── rhai.ts │ │ │ ├── rhai.grammar │ │ │ ├── message.ts │ │ │ ├── index.html │ │ │ └── worker.ts │ │ ├── tsconfig.json │ │ ├── serve.json │ │ ├── webpack.config.js │ │ └── package.json │ ├── .gitignore │ ├── screenshot.png │ ├── crate │ │ ├── rust-toolchain.toml │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── README.md ├── viewer │ ├── screenshot.png │ ├── Cargo.toml │ └── src │ │ ├── watcher.rs │ │ ├── shaders │ │ ├── image.wgsl │ │ └── geometry.wgsl │ │ └── script.rs ├── cli │ └── Cargo.toml └── README.md ├── workspace-hack ├── src │ └── lib.rs ├── build.rs ├── .gitattributes └── Cargo.toml ├── .gitignore ├── .cargo └── config.toml ├── fidget-jit ├── src │ ├── point.rs │ ├── interval.rs │ ├── float_slice.rs │ ├── grad_slice.rs │ ├── permit.rs │ ├── x86_64 │ │ └── mod.rs │ ├── aarch64 │ │ └── mod.rs │ └── mmap.rs ├── README.md ├── Cargo.toml └── build.rs ├── fidget-core ├── src │ ├── types │ │ └── mod.rs │ ├── compiler │ │ ├── mod.rs │ │ ├── reg_tape.rs │ │ └── lru.rs │ ├── vm │ │ └── choice.rs │ ├── eval │ │ ├── tracing.rs │ │ ├── bulk.rs │ │ ├── test │ │ │ └── symbolic_deriv.rs │ │ └── mod.rs │ ├── render │ │ └── config.rs │ ├── error.rs │ ├── context │ │ ├── op.rs │ │ └── indexed.rs │ ├── lib.rs │ └── var │ │ └── mod.rs ├── README.md └── Cargo.toml ├── fidget-mesh ├── src │ ├── codegen.rs │ ├── frame.rs │ ├── output.rs │ ├── builder.rs │ ├── lib.rs │ ├── qef.rs │ ├── cell.rs │ └── dc.rs ├── Cargo.toml └── README.md ├── models ├── quarter.vm ├── gyroid-sphere.rhai ├── tanglecube.vm ├── sponge.rhai ├── hi.vm └── cabin.rhai ├── fidget-gui ├── Cargo.toml └── README.md ├── fidget-bytecode ├── Cargo.toml └── README.md ├── fidget-solver ├── Cargo.toml └── README.md ├── fidget-shapes ├── Cargo.toml └── README.md ├── fidget-raster ├── Cargo.toml ├── README.md └── src │ └── config.rs ├── fidget-rhai ├── README.md ├── Cargo.toml └── src │ ├── constants.rs │ └── tree.rs ├── .github ├── workflows │ ├── hakari.yml │ ├── check-docs.yml │ ├── check-x86_64.yml │ ├── check-aarch64.yml │ ├── test.yml │ ├── constraints-demo.yml │ ├── check-wasm.yml │ └── wasm-demo.yml └── actions │ ├── rust-check │ └── action.yml │ └── rust-cache │ └── action.yml ├── .typos.toml ├── fidget ├── tests │ ├── octree.rs │ └── render3d.rs ├── benches │ ├── mesh.rs │ ├── function_call.rs │ └── render.rs └── Cargo.toml ├── .config └── hakari.toml └── Cargo.toml /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /demos/.gitignore: -------------------------------------------------------------------------------- 1 | !*/screenshot.png 2 | -------------------------------------------------------------------------------- /demos/constraints/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /workspace-hack/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This is a stub lib.rs. 2 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.rhai"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /local 3 | *.dot 4 | *.pdf 5 | *.svg 6 | *.png 7 | *.stl 8 | -------------------------------------------------------------------------------- /demos/viewer/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeeter/fidget/HEAD/demos/viewer/screenshot.png -------------------------------------------------------------------------------- /demos/web-editor/.gitignore: -------------------------------------------------------------------------------- 1 | web/node_modules 2 | web/dist 3 | web/pkg 4 | crate/pkg 5 | crate/target 6 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""] 3 | -------------------------------------------------------------------------------- /demos/web-editor/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeeter/fidget/HEAD/demos/web-editor/screenshot.png -------------------------------------------------------------------------------- /demos/constraints/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeeter/fidget/HEAD/demos/constraints/screenshot.png -------------------------------------------------------------------------------- /workspace-hack/build.rs: -------------------------------------------------------------------------------- 1 | // A build script is required for cargo to consider build dependencies. 2 | fn main() {} 3 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const RENDER_SIZE = 512; 2 | export const MAX_DEPTH = 4; // 16x reduction 3 | -------------------------------------------------------------------------------- /fidget-jit/src/point.rs: -------------------------------------------------------------------------------- 1 | use crate::AssemblerData; 2 | 3 | pub struct PointAssembler(pub(crate) AssemblerData); 4 | -------------------------------------------------------------------------------- /fidget-jit/src/interval.rs: -------------------------------------------------------------------------------- 1 | use crate::AssemblerData; 2 | 3 | pub struct IntervalAssembler(pub(crate) AssemblerData<[f32; 2]>); 4 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/rhai.grammar.d.ts: -------------------------------------------------------------------------------- 1 | import { LRParser } from "@lezer/lr"; 2 | 3 | declare const parser: LRParser; 4 | export default parser; 5 | -------------------------------------------------------------------------------- /demos/web-editor/crate/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-04-05" 3 | components = ["rust-src"] 4 | targets = ["wasm32-unknown-unknown"] 5 | -------------------------------------------------------------------------------- /fidget-core/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom types used during evaluation 2 | 3 | mod grad; 4 | mod interval; 5 | pub use grad::Grad; 6 | pub use interval::Interval; 7 | -------------------------------------------------------------------------------- /fidget-mesh/src/codegen.rs: -------------------------------------------------------------------------------- 1 | //! Generated tables 2 | use super::types::{Corner, DirectedEdge, Intersection, Offset}; 3 | 4 | include!(concat!(env!("OUT_DIR"), "/mdc_tables.rs")); 5 | -------------------------------------------------------------------------------- /models/quarter.vm: -------------------------------------------------------------------------------- 1 | # A quarter-circle in the lower-left quadrant 2 | y var-y 3 | x var-x 4 | 5 | mxy max x y 6 | 7 | y2 square y 8 | x2 square x 9 | r2 add x2 y2 10 | f const 0.5 11 | circle sub r2 f 12 | 13 | out max mxy circle 14 | -------------------------------------------------------------------------------- /demos/web-editor/crate/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", "--cfg", "getrandom_backend=\"wasm_js\""] 3 | 4 | [unstable] 5 | build-std = ["panic_abort", "std"] 6 | -------------------------------------------------------------------------------- /workspace-hack/.gitattributes: -------------------------------------------------------------------------------- 1 | # Avoid putting conflict markers in the generated Cargo.toml file, since their presence breaks 2 | # Cargo. 3 | # Also do not check out the file as CRLF on Windows, as that's what hakari needs. 4 | Cargo.toml merge=binary -crlf 5 | -------------------------------------------------------------------------------- /fidget-jit/src/float_slice.rs: -------------------------------------------------------------------------------- 1 | use crate::{AssemblerData, SimdSize, arch::float_slice::SIMD_WIDTH}; 2 | 3 | pub struct FloatSliceAssembler(pub(crate) AssemblerData<[f32; SIMD_WIDTH]>); 4 | 5 | impl SimdSize for f32 { 6 | const SIMD_SIZE: usize = SIMD_WIDTH; 7 | } 8 | -------------------------------------------------------------------------------- /demos/web-editor/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es2020", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demos/web-editor/web/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "**/*.@(js|html)", 5 | "headers": [ 6 | { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }, 7 | { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" } 8 | ] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /models/gyroid-sphere.rhai: -------------------------------------------------------------------------------- 1 | let scale = 30; 2 | 3 | let x = x * scale; 4 | let y = y * scale; 5 | let z = z * scale; 6 | 7 | let gyroid = sin(x)*cos(y) + sin(y)*cos(z) + sin(z)*cos(x); 8 | let fill = abs(gyroid) - 0.2; 9 | 10 | let sphere = sqrt(square(x) + square(y) + square(z)) - 25; 11 | draw(max(sphere, fill)); 12 | -------------------------------------------------------------------------------- /demos/web-editor/crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-wasm-demo" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | bincode = "1.3.3" 12 | nalgebra = "0.34" 13 | 14 | wasm-bindgen = "0.2.100" 15 | wasm-bindgen-rayon = "1.3" 16 | 17 | fidget = { path = "../../../fidget" } 18 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/camera.ts: -------------------------------------------------------------------------------- 1 | import * as fidget from "../../crate/pkg/fidget_wasm_demo"; 2 | 3 | export type CameraKind = "2d" | "3d"; 4 | export type Camera2D = { 5 | kind: "2d"; 6 | camera: fidget.JsCanvas2; 7 | }; 8 | 9 | export type Camera3D = { 10 | kind: "3d"; 11 | camera: fidget.JsCanvas3; 12 | }; 13 | 14 | export type Camera = Camera2D | Camera3D; 15 | -------------------------------------------------------------------------------- /fidget-jit/src/grad_slice.rs: -------------------------------------------------------------------------------- 1 | use crate::{AssemblerData, SimdSize}; 2 | use fidget_core::types::Grad; 3 | 4 | /// Assembler for automatic differentiation / gradient evaluation 5 | pub struct GradSliceAssembler(pub(crate) AssemblerData); 6 | 7 | // Both x86_64 and AArch64 process 1 gradient per register 8 | impl SimdSize for Grad { 9 | const SIMD_SIZE: usize = 1; 10 | } 11 | -------------------------------------------------------------------------------- /models/tanglecube.vm: -------------------------------------------------------------------------------- 1 | # Recommended settings: --scale 0.222 --pitch -25 --yaw 30 2 | _x var-x 3 | _y var-y 4 | _z var-z 5 | _x2 square _x 6 | _x4 square _x2 7 | _y2 square _y 8 | _y4 square _y2 9 | _z2 square _z 10 | _z4 square _z2 11 | _5 const 5.0 12 | _x2_5 mul _x2 _5 13 | _y2_5 mul _y2 _5 14 | _z2_5 mul _z2 _5 15 | _s0 sub _x4 _x2_5 16 | _s1 add _s0 _y4 17 | _s2 sub _s1 _y2_5 18 | _s3 add _s2 _z4 19 | _s4 sub _s3 _z2_5 20 | _10 const 10.0 21 | _out add _s4 _10 22 | -------------------------------------------------------------------------------- /fidget-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-gui" 3 | description = "Platform-independent GUI abstractions for Fidget" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | nalgebra.workspace = true 18 | serde.workspace = true 19 | -------------------------------------------------------------------------------- /fidget-bytecode/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-bytecode" 3 | description = "Bytecode representation for Fidget expression tapes" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | strum.workspace = true 18 | zerocopy.workspace = true 19 | -------------------------------------------------------------------------------- /fidget-solver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-solver" 3 | description = "Constraint solver using Fidget as a backend" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | rand.workspace = true 18 | nalgebra.workspace = true 19 | 20 | [dev-dependencies] 21 | approx.workspace = true 22 | -------------------------------------------------------------------------------- /fidget-shapes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-shapes" 3 | description = "Shapes and transforms for use with Fidget" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | enum-map.workspace = true 18 | facet.workspace = true 19 | nalgebra.workspace = true 20 | strum.workspace = true 21 | thiserror.workspace = true 22 | -------------------------------------------------------------------------------- /fidget-raster/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-raster" 3 | description = "Bitmap and heightmap rendering for Fidget" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | nalgebra.workspace = true 18 | ordered-float.workspace = true 19 | rand.workspace = true 20 | rayon.workspace = true 21 | zerocopy.workspace = true 22 | -------------------------------------------------------------------------------- /fidget-mesh/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-mesh" 3 | description = "Meshing of complex closed-form implicit surfaces" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | arrayvec.workspace = true 18 | nalgebra.workspace = true 19 | ordered-float.workspace = true 20 | rayon.workspace = true 21 | static_assertions.workspace = true 22 | -------------------------------------------------------------------------------- /fidget-gui/README.md: -------------------------------------------------------------------------------- 1 | `fidget-gui` implements platform-independent GUI abstractions. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `gui` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-gui)](https://crates.io/crates/fidget-gui) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-gui/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | -------------------------------------------------------------------------------- /fidget-rhai/README.md: -------------------------------------------------------------------------------- 1 | `fidget-rhai` adds Rhai bindings to Fidget types. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `rhai` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-rhai)](https://crates.io/crates/fidget-rhai) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-rhai/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | 11 | -------------------------------------------------------------------------------- /fidget-jit/README.md: -------------------------------------------------------------------------------- 1 | `fidget-jit` implements native JIT compilation of math expressions. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `jit` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-jit)](https://crates.io/crates/fidget-jit) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-jit/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | -------------------------------------------------------------------------------- /demos/constraints/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "constraints" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow.workspace = true 9 | eframe.workspace = true 10 | log.workspace = true 11 | 12 | fidget.workspace = true 13 | workspace-hack.workspace = true 14 | 15 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 16 | env_logger.workspace = true 17 | 18 | [target.'cfg(target_arch = "wasm32")'.dependencies] 19 | wasm-bindgen-futures.workspace = true 20 | web-sys = "0.3.70" 21 | 22 | [[bin]] 23 | name = "constraints" 24 | test = false 25 | doctest = false 26 | -------------------------------------------------------------------------------- /fidget-solver/README.md: -------------------------------------------------------------------------------- 1 | `fidget-solver` implements a simple constraint solver. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `solver` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-solver)](https://crates.io/crates/fidget-solver) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-solver/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | 11 | -------------------------------------------------------------------------------- /fidget-mesh/README.md: -------------------------------------------------------------------------------- 1 | `fidget-mesh` implements meshing of complex closed-form implicit surfaces. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `mesh` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-mesh)](https://crates.io/crates/fidget-mesh) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-mesh/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | -------------------------------------------------------------------------------- /fidget-shapes/README.md: -------------------------------------------------------------------------------- 1 | `fidget-shapes` declares a standard set of shapes and transforms. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `shapes` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-shapes)](https://crates.io/crates/fidget-shapes) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-shapes/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | -------------------------------------------------------------------------------- /fidget-bytecode/README.md: -------------------------------------------------------------------------------- 1 | `fidget-bytecode` implements a `u32` bytecode tape for math expressions. 2 | 3 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 4 | crate, which imports it under the `bytecode` namespace 5 | 6 | [![» Crate](https://badgen.net/crates/v/fidget-bytecode)](https://crates.io/crates/fidget-bytecode) 7 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-bytecode/) 8 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 9 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 10 | 11 | -------------------------------------------------------------------------------- /fidget-raster/README.md: -------------------------------------------------------------------------------- 1 | `fidget-raster` implements image rendering for complex closed-form implicit 2 | surfaces. 3 | 4 | t is typically used through the [`fidget`](https://crates.io/crate/fidget) 5 | crate, which imports it under the `raster` namespace 6 | 7 | [![» Crate](https://badgen.net/crates/v/fidget-raster)](https://crates.io/crates/fidget-raster) 8 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-raster/) 9 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 10 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 11 | -------------------------------------------------------------------------------- /demos/viewer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-viewer" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow.workspace = true 9 | clap.workspace = true 10 | crossbeam-channel.workspace = true 11 | eframe.workspace = true 12 | env_logger.workspace = true 13 | log.workspace = true 14 | nalgebra.workspace = true 15 | notify.workspace = true 16 | rhai.workspace = true 17 | zerocopy.workspace = true 18 | 19 | fidget.workspace = true 20 | workspace-hack.workspace = true 21 | 22 | [features] 23 | default = ["jit"] 24 | jit = ["fidget/jit"] 25 | 26 | [[bin]] 27 | name = "fidget-viewer" 28 | doctest = false 29 | -------------------------------------------------------------------------------- /fidget-core/README.md: -------------------------------------------------------------------------------- 1 | `fidget-core` implements core types, traits, and algorithms for complex 2 | closed-form implicit surfaces. 3 | 4 | It is typically used through the [`fidget`](https://crates.io/crate/fidget) 5 | crate, which imports it wholesale in the root namespace. 6 | 7 | [![» Crate](https://badgen.net/crates/v/fidget-core)](https://crates.io/crates/fidget-core) 8 | [![» Docs](https://badgen.net/badge/api/docs.rs/df3600)](https://docs.rs/fidget-core/) 9 | [![» CI](https://badgen.net/github/checks/mkeeter/fidget/main)](https://github.com/mkeeter/fidget/actions/) 10 | [![» MPL-2.0](https://badgen.net/github/license/mkeeter/fidget)](../LICENSE.txt) 11 | -------------------------------------------------------------------------------- /.github/workflows/hakari.yml: -------------------------------------------------------------------------------- 1 | name: cargo hakari 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | workspace-hack-check: 13 | runs-on: ubuntu-latest 14 | env: 15 | RUSTFLAGS: -D warnings 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install cargo-hakari 19 | uses: taiki-e/install-action@v2 20 | with: 21 | tool: cargo-hakari 22 | - name: Check workspace-hack Cargo.toml is up-to-date 23 | run: cargo hakari generate --diff 24 | - name: Check all crates depend on workspace-hack 25 | run: cargo hakari manage-deps --dry-run 26 | -------------------------------------------------------------------------------- /.github/workflows/check-docs.yml: -------------------------------------------------------------------------------- 1 | name: Check documentation 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUSTDOCFLAGS: '--cfg docsrs -D warnings' 12 | 13 | jobs: 14 | # We test documentation using nightly to match docs.rs. 15 | check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ./.github/actions/rust-cache 20 | with: 21 | cache-key: check-docs 22 | - name: Install nightly Rust 23 | run: rustup default nightly 24 | - name: Check docs 25 | run: cargo doc --workspace --no-deps --document-private-items 26 | -------------------------------------------------------------------------------- /.github/workflows/check-x86_64.yml: -------------------------------------------------------------------------------- 1 | name: Check native builds 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUSTFLAGS: -Dwarnings 12 | 13 | jobs: 14 | check: 15 | strategy: 16 | matrix: 17 | target: [ 18 | "x86_64-unknown-linux-gnu", 19 | "x86_64-pc-windows-msvc", 20 | ] 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: ./.github/actions/rust-cache 25 | with: 26 | cache-key: check-x86_64 27 | - uses: ./.github/actions/rust-check 28 | with: 29 | target: ${{ matrix.target }} 30 | -------------------------------------------------------------------------------- /.github/actions/rust-check/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Rust check' 2 | description: 'Run `cargo check`, `cargo clippy`, and `cargo fmt`' 3 | inputs: 4 | target: 5 | description: 'rustc target' 6 | required: true 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Install target 11 | run: rustup target add ${{ inputs.target }} 12 | shell: bash 13 | - name: Check 14 | run: cargo check --target=${{ inputs.target }} --all-targets --verbose 15 | shell: bash 16 | - name: Clippy 17 | run: cargo clippy --target=${{ inputs.target }} --all-targets --verbose 18 | shell: bash 19 | - name: Check format 20 | run: cargo fmt -- --check || exit 1 21 | shell: bash 22 | -------------------------------------------------------------------------------- /demos/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-cli" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow.workspace = true 9 | clap.workspace = true 10 | env_logger.workspace = true 11 | image.workspace = true 12 | log.workspace = true 13 | nalgebra.workspace = true 14 | rayon.workspace = true 15 | rhai.workspace = true 16 | strum.workspace = true 17 | 18 | fidget.workspace = true 19 | workspace-hack.workspace = true 20 | 21 | [features] 22 | jit = ["fidget/jit"] 23 | logger-full = ["env_logger/auto-color", "env_logger/humantime", "env_logger/regex"] 24 | default = ["jit", "logger-full"] 25 | 26 | [[bin]] 27 | name = "fidget-cli" 28 | test = false 29 | doctest = false 30 | -------------------------------------------------------------------------------- /demos/constraints/README.md: -------------------------------------------------------------------------------- 1 | # Demo of constraint solving 2 | ## WebAssembly 3 | ### Setup 4 | Install the [`trunk`](https://trunkrs.dev/) bundler with 5 | ``` 6 | cargo install +locked trunk 7 | ``` 8 | ### Developing 9 | In this folder, run 10 | ``` 11 | trunk serve 12 | ``` 13 | `trunk` will serve the webpage at `127.0.0.1:8080` 14 | 15 | ### Deploying 16 | In this folder, run 17 | ``` 18 | trunk build --release 19 | ``` 20 | `trunk` will populate the `dist/` subfolder with assets. 21 | 22 | If the site will be hosted at a non-root URL, then add `--public-url`, e.g. 23 | ``` 24 | trunk build --release --public-url=/projects/fidget/constraints/ 25 | ``` 26 | 27 | ## Native 28 | ``` 29 | cargo run -pconstraints 30 | ``` 31 | -------------------------------------------------------------------------------- /fidget-rhai/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-rhai" 3 | description = "Rhai script evaluation for complex closed-form implicit surfaces" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | fidget-shapes.workspace = true 16 | workspace-hack.workspace = true 17 | 18 | enum-map.workspace = true 19 | facet.workspace = true 20 | rhai.workspace = true 21 | strum.workspace = true 22 | heck.workspace = true 23 | 24 | [target.'cfg(target_arch = "wasm32")'.dependencies] 25 | rhai = { workspace = true, features = ["wasm-bindgen"] } 26 | -------------------------------------------------------------------------------- /.github/workflows/check-aarch64.yml: -------------------------------------------------------------------------------- 1 | name: Check native builds 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUSTFLAGS: -Dwarnings 12 | 13 | jobs: 14 | check: 15 | strategy: 16 | matrix: 17 | target: [ 18 | "aarch64-apple-darwin", 19 | "aarch64-pc-windows-msvc", 20 | "aarch64-unknown-linux-gnu", 21 | ] 22 | runs-on: macos-14 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: ./.github/actions/rust-cache 26 | with: 27 | cache-key: check-aarch64 28 | - uses: ./.github/actions/rust-check 29 | with: 30 | target: ${{ matrix.target }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test native builds 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | os: ["ubuntu-latest", "macos-14", "windows-latest"] 17 | runs-on: ${{ matrix.os }} 18 | timeout-minutes: 15 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: taiki-e/install-action@nextest 22 | - uses: ./.github/actions/rust-cache 23 | with: 24 | cache-key: test 25 | - name: Run crate tests 26 | run: cargo nextest run --verbose --cargo-verbose --retries 2 27 | - name: Run doc tests 28 | run: cargo test --verbose --doc 29 | -------------------------------------------------------------------------------- /fidget-jit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-jit" 3 | description = "Native JIT compiler for Fidget" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core.workspace = true 15 | workspace-hack.workspace = true 16 | 17 | arrayvec.workspace = true 18 | dynasmrt.workspace = true 19 | static_assertions.workspace = true 20 | 21 | [target.'cfg(not(target_os = "windows"))'.dependencies] 22 | libc.workspace = true 23 | 24 | [target.'cfg(target_os = "windows")'.dependencies] 25 | windows.workspace = true 26 | 27 | [dev-dependencies] 28 | fidget-core = { workspace = true, features= ["eval-tests"] } 29 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/rhai.ts: -------------------------------------------------------------------------------- 1 | import { LRLanguage, LanguageSupport } from "@codemirror/language"; 2 | import { styleTags, tags } from "@lezer/highlight"; 3 | import parser from "./rhai.grammar"; 4 | 5 | let parserWithMetadata = parser.configure({ 6 | props: [ 7 | styleTags({ 8 | DefinitionKeyword: tags.definitionKeyword, 9 | "Call/Identifier": tags.function(tags.name), 10 | ControlKeyword: tags.controlKeyword, 11 | Identifier: tags.name, 12 | Number: tags.number, 13 | String: tags.string, 14 | LineComment: tags.comment, 15 | }), 16 | ], 17 | }); 18 | 19 | export const rhaiLanguage = LRLanguage.define({ 20 | parser: parserWithMetadata, 21 | }); 22 | 23 | export function rhai() { 24 | return new LanguageSupport(rhaiLanguage, []); 25 | } 26 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/rhai.grammar: -------------------------------------------------------------------------------- 1 | @top File { ( 2 | Identifier 3 | | Call 4 | | DefinitionKeyword 5 | | ControlKeyword 6 | | Number 7 | | String 8 | )+ } 9 | 10 | @skip { space | LineComment } 11 | 12 | Identifier { identifier } 13 | Call { Identifier '(' } 14 | 15 | @tokens { 16 | @precedence { 17 | DefinitionKeyword, 18 | ControlKeyword, 19 | identifier 20 | } 21 | 22 | space { @whitespace+ } 23 | identifier { $[A-Za-z_]$[A-Za-z_0-9]* } 24 | LineComment { "//" ![\n]* } 25 | 26 | DefinitionKeyword { 'let' | 'const' } 27 | controlKeyword { 'if' | 'else' | 'switch' | 'do' | 'while' | 'loop' | 'until' | 'for' | 'in' | 'continue' | 'break' | 'return' | 'throw' | 'try' | 'catch' | 'fn' } 28 | ControlKeyword { controlKeyword ![a-z] } 29 | Number { $[0-9]+ } 30 | String { '"' !["]* '"' } 31 | } 32 | -------------------------------------------------------------------------------- /.github/actions/rust-cache/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Rust cache' 2 | description: 'Load Rust folders from the Github cache' 3 | inputs: 4 | cache-key: 5 | description: 'Key to include in the cache key' 6 | required: true 7 | default: '' 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Update rustup 12 | run: rustup update 13 | shell: bash 14 | - uses: actions/cache@v4 15 | with: 16 | path: | 17 | ~/.cargo/bin/ 18 | ~/.cargo/registry/index/ 19 | ~/.cargo/registry/cache/ 20 | ~/.cargo/git/db/ 21 | target/ 22 | demos/web-editor/crate/target 23 | key: ${{ runner.os }}-${{ inputs.cache-key }}-${{ hashFiles('**/Cargo.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-${{ inputs.cache-key }}- 26 | ${{ runner.os }}- 27 | -------------------------------------------------------------------------------- /.github/workflows/constraints-demo.yml: -------------------------------------------------------------------------------- 1 | name: constraints build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./demos/constraints 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ./.github/actions/rust-cache 21 | with: 22 | cache-key: constraints 23 | - name: Install wasm target 24 | run: rustup target add wasm32-unknown-unknown 25 | - name: Install trunk 26 | run: | 27 | wget -qO- https://github.com/trunk-rs/trunk/releases/download/v0.20.2/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 28 | - name: Build wasm-demo 29 | run: ./trunk build --release 30 | 31 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at 2 | # https://github.com/crate-ci/typos/blob/master/docs/reference.md 3 | 4 | # Corrections take the form of a key/value pair. The key is the incorrect word 5 | # and the value is the correct word. If the key and value are the same, the 6 | # word is treated as always correct. If the value is an empty string, the word 7 | # is treated as always incorrect. 8 | 9 | # Match Identifier - Case Sensitive 10 | [default.extend-identifiers] 11 | ba = "ba" 12 | iy = "iy" 13 | 14 | # Match Inside a Word - Case Insensitive 15 | [default.extend-words] 16 | leafs = "leafs" 17 | subtile = "subtile" 18 | 19 | [files] 20 | # Include .github, .cargo, etc. 21 | ignore-hidden = false 22 | extend-exclude = [ 23 | "/models", 24 | # /.git isn't in .gitignore, because git never tracks it. 25 | # Typos doesn't know that, though. 26 | "/.git" 27 | ] 28 | -------------------------------------------------------------------------------- /demos/web-editor/web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/index.ts", 5 | devtool: "source-map", 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: "ts-loader", 11 | exclude: /node_modules/, 12 | }, 13 | { 14 | test: /\.rhai$/i, 15 | use: "raw-loader", 16 | }, 17 | { 18 | test: /\.grammar$/i, 19 | use: "lezer-loader", 20 | }, 21 | { 22 | test: /\.js$/, 23 | resolve: { 24 | // https://github.com/RReverser/wasm-bindgen-rayon/issues/9 25 | fullySpecified: false, 26 | }, 27 | }, 28 | ], 29 | }, 30 | resolve: { 31 | extensions: [".tsx", ".ts", ".js"], 32 | }, 33 | output: { 34 | path: path.resolve(__dirname, "dist"), 35 | filename: "index.js", 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /fidget/tests/octree.rs: -------------------------------------------------------------------------------- 1 | use fidget::{ 2 | gui::View3, 3 | mesh::{Octree, Settings}, 4 | vm::VmShape, 5 | }; 6 | use nalgebra::Vector3; 7 | 8 | #[test] 9 | fn test_octree_camera() { 10 | let (x, y, z) = fidget::context::Tree::axes(); 11 | let sphere = ((x - 1.0).square() + (y - 1.0).square() + (z - 1.0).square()) 12 | .sqrt() 13 | - 0.25; 14 | let shape = VmShape::from(sphere); 15 | 16 | let center = Vector3::new(1.0, 1.0, 1.0); 17 | let settings = Settings { 18 | depth: 4, 19 | world_to_model: View3::from_center_and_scale(center, 0.5) 20 | .world_to_model(), 21 | threads: None, 22 | ..Default::default() 23 | }; 24 | 25 | let octree = Octree::build(&shape, &settings).unwrap().walk_dual(); 26 | for v in octree.vertices.iter() { 27 | let n = (v - center).norm(); 28 | assert!(n > 0.2 && n < 0.3, "invalid vertex at {v:?}: {n}"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fidget-jit/src/permit.rs: -------------------------------------------------------------------------------- 1 | //! Permits for per-thread `W^X` behavior 2 | 3 | /// Holding a `WritePermit` allows writes to memory-mapped regions 4 | pub struct WritePermit { 5 | _marker: std::marker::PhantomData<*const ()>, 6 | } 7 | static_assertions::assert_not_impl_any!(WritePermit: Send); 8 | 9 | impl WritePermit { 10 | pub fn new() -> Self { 11 | #[cfg(target_os = "macos")] 12 | unsafe { 13 | pthread_jit_write_protect_np(0); 14 | } 15 | 16 | Self { 17 | _marker: std::marker::PhantomData, 18 | } 19 | } 20 | } 21 | 22 | impl Drop for WritePermit { 23 | fn drop(&mut self) { 24 | #[cfg(target_os = "macos")] 25 | unsafe { 26 | pthread_jit_write_protect_np(1); 27 | } 28 | } 29 | } 30 | 31 | #[cfg(target_os = "macos")] 32 | #[link(name = "pthread")] 33 | unsafe extern "C" { 34 | pub fn pthread_jit_write_protect_np(enabled: std::ffi::c_int); 35 | } 36 | -------------------------------------------------------------------------------- /models/sponge.rhai: -------------------------------------------------------------------------------- 1 | // Menger sponge, with optional sphere-ification 2 | // Recommended render settings: --scale 0.75 --pitch -25 --yaw -30 3 | fn recurse(x, y, z, depth) { 4 | let r = ((x + 1) % 2 - 1).abs(); 5 | let base = intersection(r, r.remap(y, x, z)) - 1/3.; 6 | let out = base; 7 | for i in 0..depth { 8 | out = union(base, out.remap(x * 3, y * 3, z)) 9 | } 10 | out 11 | } 12 | 13 | let square = intersection(x.abs() - 1, y.abs() - 1); 14 | let xy = difference(square, recurse(x, y, z, 3)); 15 | let yz = xy.remap(y, z, x); 16 | let zx = xy.remap(z, x, y); 17 | let sponge = intersection(intersection(xy, yz), zx); 18 | 19 | let radius = (x.square() + y.square() + z.square()).sqrt(); 20 | let manhattan = max(x.abs(), max(y.abs(), z.abs())); 21 | let rescale = manhattan / radius; 22 | let blend = 1.0; // adjust the sphere-ness of the sponge 23 | let r = (rescale * blend) + (1.0 - blend); 24 | 25 | draw(sponge.remap(x / r, y / r, z / r)); 26 | -------------------------------------------------------------------------------- /demos/web-editor/README.md: -------------------------------------------------------------------------------- 1 | The `web-editor` subfolder embeds Fidget into a web application. 2 | 3 | Building this demo requires [`wasm-pack`](https://rustwasm.github.io/wasm-pack/) 4 | to be installed on the host system. 5 | 6 | Run the editor demo in the `web` subfolder with 7 | 8 | ``` 9 | npm install 10 | npm run serve 11 | ``` 12 | 13 | Or bundle files for distribution with 14 | 15 | ``` 16 | npm run dist 17 | ``` 18 | 19 | The web application must be served with cross-origin isolation, because it uses 20 | [shared memory](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements) 21 | for multithreading. 22 | 23 | The demo server is configured to add those headers in `serve.json`. 24 | 25 | To serve the application with Apache, add the following to an `.htaccess` file: 26 | ``` 27 | Header add Cross-Origin-Embedder-Policy: "require-corp" 28 | Header add Cross-Origin-Opener-Policy: "same-origin" 29 | ``` 30 | -------------------------------------------------------------------------------- /fidget-jit/src/x86_64/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementation for various assemblers on the `x86_64` platform 2 | //! 3 | //! We dedicate 12 registers (`xmm4-15`) to tape data storage, meaning the input 4 | //! tape must be planned with a <= 12 register limit; any spills will live on 5 | //! the stack. 6 | //! 7 | //! Right now, we never call anything, so don't worry about saving stuff. 8 | //! 9 | //! Within a single operation, you'll often need to make use of scratch 10 | //! registers. `xmm0` is used when loading immediates, and should not be used 11 | //! as a scratch register (this is the `IMM_REG` constant). `xmm1-3` are all 12 | //! available. 13 | 14 | /// We use `xmm4-15` (all caller-saved) for graph variables 15 | pub const REGISTER_LIMIT: usize = 12; 16 | /// `xmm0` is used for immediates 17 | pub const IMM_REG: u8 = 0; 18 | /// `xmm1-3` are available for use as temporaries. 19 | pub const OFFSET: u8 = 4; 20 | 21 | pub mod float_slice; 22 | pub mod grad_slice; 23 | pub mod interval; 24 | pub mod point; 25 | -------------------------------------------------------------------------------- /fidget-core/src/compiler/mod.rs: -------------------------------------------------------------------------------- 1 | //! Compiler infrastructure 2 | //! 3 | //! The Fidget compiler operates in several stages: 4 | //! - A math graph (specified as a [`Context`](crate::Context) and 5 | //! [`Node`](crate::context::Node)) is flattened into an [`SsaTape`], i.e. a 6 | //! set of operations in single-static assignment form. 7 | //! - The [`SsaTape`] goes through [register allocation](RegisterAllocator) and 8 | //! becomes a [`RegTape`], planned with some number of registers. 9 | 10 | mod alloc; 11 | pub use alloc::RegisterAllocator; 12 | 13 | mod op; 14 | 15 | mod lru; 16 | pub(crate) use lru::Lru; 17 | pub use op::{RegOp, RegOpDiscriminants, SsaOp}; 18 | 19 | mod reg_tape; 20 | mod ssa_tape; 21 | 22 | pub use reg_tape::RegTape; 23 | pub use ssa_tape::SsaTape; 24 | 25 | #[cfg(test)] 26 | mod test { 27 | use super::*; 28 | #[test] 29 | fn test_vm_op_size() { 30 | assert_eq!(std::mem::size_of::(), 8); 31 | assert_eq!(std::mem::size_of::(), 16); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.config/hakari.toml: -------------------------------------------------------------------------------- 1 | # This file contains settings for `cargo hakari`. 2 | # See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options. 3 | 4 | hakari-package = "workspace-hack" 5 | 6 | # Format version for hakari's output. Version 4 requires cargo-hakari 0.9.22 or above. 7 | dep-format-version = "4" 8 | 9 | # Setting workspace.resolver = "2" in the root Cargo.toml is HIGHLY recommended. 10 | # Hakari works much better with the new feature resolver. 11 | # For more about the new feature resolver, see: 12 | # https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#cargos-new-feature-resolver 13 | resolver = "2" 14 | 15 | # Add triples corresponding to platforms commonly used by developers here. 16 | # https://doc.rust-lang.org/rustc/platform-support.html 17 | platforms = [ 18 | "x86_64-unknown-linux-gnu", 19 | "aarch64-apple-darwin", 20 | "aarch64-unknown-linux-gnu", 21 | # "x86_64-apple-darwin", 22 | # "x86_64-pc-windows-msvc", 23 | ] 24 | 25 | # Write out exact versions rather than a semver range. (Defaults to false.) 26 | # exact-versions = true 27 | -------------------------------------------------------------------------------- /.github/workflows/check-wasm.yml: -------------------------------------------------------------------------------- 1 | name: Check wasm build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ./.github/actions/rust-cache 18 | with: 19 | cache-key: check-wasm 20 | - name: Install wasm target 21 | run: rustup target add wasm32-unknown-unknown 22 | - name: Check 23 | # `cargo check` doesn't find MIR diagnostics (rust#49292), so we have to 24 | # compile instead. We're using `cargo rustc` instead of `cargo build` to 25 | # pass `-Dwarnings`; we don't want to use `RUSTFLAGS` because that will 26 | # override customization in `.cargo/config.toml` 27 | run: cargo rustc --target=wasm32-unknown-unknown -pfidget --no-default-features --features="rhai" -- -Dwarnings 28 | - name: Clippy 29 | run: cargo clippy --target=wasm32-unknown-unknown -pfidget --no-default-features --features="rhai" -- -Dwarnings 30 | -------------------------------------------------------------------------------- /fidget-mesh/src/frame.rs: -------------------------------------------------------------------------------- 1 | //! Coordinate frames 2 | use super::types::{Axis, X, Y, Z}; 3 | 4 | /// Marker trait for a right-handed coordinate frame 5 | pub trait Frame { 6 | /// Next frame, i.e. a left rotation of [`Self::frame()`] 7 | type Next: Frame; 8 | 9 | /// Returns the right-handed frame 10 | fn frame() -> (Axis<3>, Axis<3>, Axis<3>); 11 | } 12 | 13 | /// The X-Y-Z coordinate frame 14 | #[allow(clippy::upper_case_acronyms)] 15 | pub struct XYZ; 16 | 17 | /// The Y-Z-X coordinate frame 18 | #[allow(clippy::upper_case_acronyms)] 19 | pub struct YZX; 20 | 21 | /// The Z-X-Y coordinate frame 22 | #[allow(clippy::upper_case_acronyms)] 23 | pub struct ZXY; 24 | 25 | impl Frame for XYZ { 26 | type Next = YZX; 27 | fn frame() -> (Axis<3>, Axis<3>, Axis<3>) { 28 | (X, Y, Z) 29 | } 30 | } 31 | 32 | impl Frame for YZX { 33 | type Next = ZXY; 34 | fn frame() -> (Axis<3>, Axis<3>, Axis<3>) { 35 | (Y, Z, X) 36 | } 37 | } 38 | impl Frame for ZXY { 39 | type Next = XYZ; 40 | fn frame() -> (Axis<3>, Axis<3>, Axis<3>) { 41 | (Z, X, Y) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /models/hi.vm: -------------------------------------------------------------------------------- 1 | # The letters 'hi' in the upper-right quadrant 2 | _0 var-y 3 | _1 const 0.55 4 | _2 sub _0 _1 5 | _3 neg _0 6 | _4 max _2 _3 7 | _5 var-x 8 | _6 const 0.825 9 | _7 sub _5 _6 10 | _8 max _4 _7 11 | _9 const 0.725 12 | _a sub _9 _5 13 | _b max _8 _a 14 | _c const 0.7 15 | _d sub _0 _c 16 | _e square _d 17 | _f const 0.775 18 | _10 sub _5 _f 19 | _11 square _10 20 | _12 add _e _11 21 | _13 sqrt _12 22 | _14 const 0.075 23 | _15 sub _13 _14 24 | _16 min _b _15 25 | _17 const 0.275 26 | _18 sub _0 _17 27 | _19 max _3 _18 28 | _1a sub _5 _1 29 | _1b max _19 _1a 30 | _1c const 0.45 31 | _1d sub _1c _5 32 | _1e max _1b _1d 33 | _1f min _16 _1e 34 | _20 const 1 35 | _21 sub _0 _20 36 | _22 max _3 _21 37 | _23 const 0.1 38 | _24 sub _5 _23 39 | _25 max _22 _24 40 | _26 neg _5 41 | _27 max _25 _26 42 | _28 min _1f _27 43 | _29 max _2 _1a 44 | _2a max _29 _26 45 | _2b sub _17 _0 46 | _2c max _2a _2b 47 | _2d const 0.175 48 | _2e square _18 49 | _2f sub _5 _17 50 | _30 square _2f 51 | _31 add _2e _30 52 | _32 sqrt _31 53 | _33 sub _2d _32 54 | _34 max _2c _33 55 | _35 sub _32 _17 56 | _36 max _34 _35 57 | _37 min _28 _36 58 | -------------------------------------------------------------------------------- /demos/web-editor/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build-wasm": "cd ../crate && wasm-pack build --target web --release", 4 | "build-web": "webpack --mode production", 5 | "build-static": "sed \"s/VERSION/$(git log --pretty=format:'%h' -n 1)/g;s/MOD/$(git diff --quiet --exit-code || echo +)/g\" src/index.html > dist/index.html && grep 'Header add' ../README.md > dist/.htaccess", 6 | "dist": "npm run build-wasm && npm run build-web && npm run build-static", 7 | "serve": "npm run dist && serve -c ../serve.json dist", 8 | "format": "prettier . --write", 9 | "deploy": "rm dist/* && npm run dist && rsync -avz --delete -e ssh ./dist/ mkeeter@mattkeeter.com:mattkeeter.com/projects/fidget/demo" 10 | }, 11 | "dependencies": { 12 | "@lezer/lr": "^1.0.0", 13 | "codemirror": "6.0.1" 14 | }, 15 | "devDependencies": { 16 | "lezer-loader": "^0.3.0", 17 | "prettier": "3.2.5", 18 | "raw-loader": "^4.0.2", 19 | "serve": "^14.2.4", 20 | "ts-loader": "^9.5.1", 21 | "typescript": "^5.4.5", 22 | "webpack": ">=5.94.0", 23 | "webpack-cli": "^4.7.2" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /fidget-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget-core" 3 | description = "Core infrastructure for Fidget" 4 | readme = "README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | facet.workspace = true 15 | nalgebra.workspace = true 16 | ordered-float.workspace = true 17 | rand.workspace = true 18 | rayon.workspace = true 19 | serde.workspace = true 20 | strum.workspace = true 21 | thiserror.workspace = true 22 | 23 | workspace-hack.workspace = true 24 | 25 | [target.'cfg(target_arch = "wasm32")'.dependencies] 26 | # Feature unification hacks to get webassembly working 27 | getrandom-03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] } 28 | getrandom-02 = { package = "getrandom", version = "0.2", features = ["js"] } 29 | 30 | [features] 31 | ## Enable `eval-tests` if you're writing your own evaluators and want to 32 | ## unit-test them. When enabled, the crate exports a set of macros to test each 33 | ## evaluator type, e.g. `float_slice_tests!(...)`. 34 | eval-tests = [] 35 | -------------------------------------------------------------------------------- /fidget-jit/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // The build script stands alone; ignore other changes (e.g. edits to 3 | // benchmarks in the benches subfolder). 4 | println!("cargo:rerun-if-changed=build.rs"); 5 | 6 | // Check CPU feature support and error out if we don't have the appropriate 7 | // features. This isn't a fool-proof – someone could build on a machine with 8 | // AVX2 support, then try running those binaries elsewhere – but is a good 9 | // first line of defense. 10 | if std::env::var("CARGO_FEATURE_JIT").is_ok() { 11 | #[cfg(target_arch = "x86_64")] 12 | if !std::arch::is_x86_feature_detected!("avx2") { 13 | eprintln!( 14 | "`x86_64` build with `jit` enabled requires AVX2 instructions" 15 | ); 16 | std::process::exit(1); 17 | } 18 | 19 | #[cfg(target_arch = "aarch64")] 20 | if !std::arch::is_aarch64_feature_detected!("neon") { 21 | eprintln!( 22 | "`aarch64` build with `jit` enabled requires NEON instructions" 23 | ); 24 | std::process::exit(1); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/wasm-demo.yml: -------------------------------------------------------------------------------- 1 | name: wasm-demo build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./demos/web-editor/web 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ./.github/actions/rust-cache 21 | with: 22 | cache-key: wasm-demo 23 | - name: Install wasm target 24 | run: rustup target add wasm32-unknown-unknown 25 | - name: Install wasm-pack 26 | run: | 27 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 28 | - name: Install npm dependencies 29 | run: npm install 30 | - name: Check Prettier 31 | run: npx prettier . --check 32 | - name: Build wasm-demo 33 | run: npm run dist 34 | - name: Check `git` cleanliness 35 | run: | 36 | if [[ -n $(git status --porcelain ../crate/Cargo.lock) ]]; then 37 | echo "Error: demos/web-editor/crate/Cargo.lock needs to be updated" 38 | git diff ../crate/Cargo.lock 39 | exit 1 40 | fi 41 | -------------------------------------------------------------------------------- /demos/viewer/src/watcher.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossbeam_channel::{Receiver, Sender}; 3 | use log::{debug, warn}; 4 | use std::path::Path; 5 | 6 | /// Watches for changes to the given file and sends it on `tx` 7 | pub(crate) fn file_watcher_thread( 8 | path: &Path, 9 | rx: Receiver<()>, 10 | tx: Sender, 11 | ) -> Result<()> { 12 | let read_file = || -> Result { 13 | let out = String::from_utf8(std::fs::read(path)?).unwrap(); 14 | Ok(out) 15 | }; 16 | let mut contents = read_file()?; 17 | tx.send(contents.clone())?; 18 | 19 | loop { 20 | // Wait for a file change notification 21 | rx.recv()?; 22 | let new_contents = loop { 23 | match read_file() { 24 | Ok(c) => break c, 25 | Err(e) => { 26 | warn!("file read error: {e:?}"); 27 | std::thread::sleep(std::time::Duration::from_millis(10)); 28 | } 29 | } 30 | }; 31 | if contents != new_contents { 32 | contents = new_contents; 33 | debug!("file contents changed!"); 34 | tx.send(contents.clone())?; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demos/constraints/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Fidget constraint demo 10 | 11 | 12 | 13 | 14 | 41 | 42 | 43 | 44 |
45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /fidget-jit/src/aarch64/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementation for various assemblers on the `aarch64` platform 2 | //! 3 | //! We dedicate 24 registers to tape data storage: 4 | //! - Floating point registers `s8-15` (callee-saved, but only the lower 64 5 | //! bits) 6 | //! - Floating-point registers `s16-31` (caller-saved) 7 | //! 8 | //! This means that the input tape must be planned with a <= 24 register limit; 9 | //! any spills will live on the stack. 10 | //! 11 | //! Right now, we never call anything, so don't worry about saving stuff. 12 | //! 13 | //! Within a single operation, you'll often need to make use of scratch 14 | //! registers. `s3` / `v3` is used when loading immediates, and should not be 15 | //! used as a scratch register (this is the `IMM_REG` constant). `s4-7`/`v4-7` 16 | //! are all available, and are callee-saved. 17 | //! 18 | //! For general-purpose registers, `x9-15` (also called `w9-15`) are reasonable 19 | //! choices; they are caller-saved, so we can trash them at will. 20 | 21 | /// We can use registers `v8-15` (callee saved) and `v16-31` (caller saved) 22 | pub const REGISTER_LIMIT: usize = 24; 23 | /// `v3` is used for immediates, because `v0-2` contain inputs 24 | pub const IMM_REG: u8 = 3; 25 | /// `v4-7` are used for as temporary variables 26 | pub const OFFSET: u8 = 8; 27 | 28 | pub mod float_slice; 29 | pub mod grad_slice; 30 | pub mod interval; 31 | pub mod point; 32 | -------------------------------------------------------------------------------- /fidget-mesh/src/output.rs: -------------------------------------------------------------------------------- 1 | //! Mesh output implementation 2 | use super::Mesh; 3 | use std::io::{BufWriter, Write}; 4 | 5 | impl Mesh { 6 | /// Writes a binary STL to the given output 7 | pub fn write_stl( 8 | &self, 9 | out: &mut F, 10 | ) -> std::io::Result<()> { 11 | // We're going to do many small writes and will typically be writing to 12 | // a file, so using a `BufWriter` saves excessive syscalls. 13 | let mut out = BufWriter::new(out); 14 | const HEADER: &[u8] = b"This is a binary STL file exported by Fidget"; 15 | static_assertions::const_assert!(HEADER.len() <= 80); 16 | out.write_all(HEADER)?; 17 | out.write_all(&[0u8; 80 - HEADER.len()])?; 18 | out.write_all(&(self.triangles.len() as u32).to_le_bytes())?; 19 | for t in &self.triangles { 20 | // Not the _best_ way to calculate a normal, but good enough 21 | let a = self.vertices[t.x]; 22 | let b = self.vertices[t.y]; 23 | let c = self.vertices[t.z]; 24 | let ab = b - a; 25 | let ac = c - a; 26 | let normal = ab.cross(&ac); 27 | for p in &normal { 28 | out.write_all(&p.to_le_bytes())?; 29 | } 30 | for v in t { 31 | for p in &self.vertices[*v] { 32 | out.write_all(&p.to_le_bytes())?; 33 | } 34 | } 35 | out.write_all(&[0u8; std::mem::size_of::()])?; // attributes 36 | } 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- 1 | # Fidget demos 2 | ## Command-line demo ([`cli`](cli/)) 3 | 4 | Bitmap rendering and meshing from the command line 5 | ```shell 6 | $ cargo run -pfidget-cli --release render2d -i models/prospero.vm -s 512 --eval=vm -o out.png 7 | Finished release [optimized + debuginfo] target(s) in 0.07s 8 | Running `target/release/fidget-cli render2d -i models/prospero.vm -s 512 --eval=vm -o out.png` 9 | [2024-06-06T16:08:12Z INFO fidget_cli] Loaded file in 4.528208ms 10 | [2024-06-06T16:08:12Z INFO fidget_cli] Built shape in 2.375208ms 11 | [2024-06-06T16:08:12Z INFO fidget_cli] Rendered 1x at 14.489 ms/frame 12 | ``` 13 | 14 | ## Script viewer ([`viewer`](viewer/)) 15 | Minimal desktop GUI for interactive exploration, 16 | using [`egui`](https://github.com/emilk/egui) 17 | 18 | ```shell 19 | cargo run --release -pfidget-viewer 20 | ``` 21 | 22 | ![screenshot of script viewer](viewer/screenshot.png) 23 | 24 | ## Constraint solving ([`constraints`](constraints/)) 25 | Example of using Fidget for constraint solving. 26 | Uses [`egui`](https://github.com/emilk/egui) 27 | and runs either on the desktop or as a web app. 28 | ```shell 29 | cargo run --release -pconstraints 30 | ``` 31 | 32 | ![screenshot of constraint editor](constraints/screenshot.png) 33 | 34 | See the [subfolder](constraints/) for details on bundling for the web. 35 | 36 | ## Web-based editor ([`web-editor`](web-editor/)) 37 | Integrates Fidget into a TypeScript project (web only) 38 | 39 | ![screenshot of web editor](web-editor/screenshot.png) 40 | 41 | See the [subfolder](web-editor/) for details on bundling for the web. 42 | -------------------------------------------------------------------------------- /demos/viewer/src/shaders/image.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position: vec4, 3 | @location(0) tex_coords: vec2, 4 | } 5 | 6 | // Vertex shader to render a full-screen quad 7 | @vertex 8 | fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 9 | // Create a "full screen quad" from just the vertex_index 10 | // Maps vertex indices (0,1,2,3) to positions: 11 | // (-1,1)----(1,1) 12 | // | | 13 | // (-1,-1)---(1,-1) 14 | var pos = array, 6>( 15 | vec2(-1.0, -1.0), 16 | vec2(1.0, -1.0), 17 | vec2(-1.0, 1.0), 18 | vec2(-1.0, 1.0), 19 | vec2(1.0, -1.0), 20 | vec2(1.0, 1.0), 21 | ); 22 | 23 | // UV coordinates for the quad 24 | var uv = array, 6>( 25 | vec2(0.0, 1.0), 26 | vec2(1.0, 1.0), 27 | vec2(0.0, 0.0), 28 | vec2(0.0, 0.0), 29 | vec2(1.0, 1.0), 30 | vec2(1.0, 0.0), 31 | ); 32 | 33 | var output: VertexOutput; 34 | output.position = vec4(pos[vertex_index], 0.0, 1.0); 35 | output.tex_coords = uv[vertex_index]; 36 | return output; 37 | } 38 | 39 | @group(0) @binding(0) var t_diffuse: texture_2d; 40 | @group(0) @binding(1) var s_diffuse: sampler; 41 | 42 | // Fragment shader 43 | @fragment 44 | fn fs_main(@location(0) tex_coords: vec2) -> @location(0) vec4 { 45 | var rgba = textureSample(t_diffuse, s_diffuse, tex_coords); 46 | if (rgba.a == 0.0) { 47 | discard; 48 | } else { 49 | return rgba; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /models/cabin.rhai: -------------------------------------------------------------------------------- 1 | // Isometric render: --scale 0.05 --pitch 80 --roll 130 --center=0,0,-14 --isometric 2 | // Perspective render: --scale 0.05 --pitch 80 --roll 130 --center=0,0,-14 --perspective=0.2 --zflatten 2 3 | let logs = x.abs() - 10 - max(0.05, 1.0 - (y % 2 - 1).square()).sqrt(); 4 | let cabin = max( 5 | logs.remap(x, z, y), 6 | logs.remap(y, z, x) 7 | ); 8 | 9 | let roof = z/2 + y.abs()/2 - 15; 10 | 11 | // extrude the roof 12 | let lower_roof = -roof; 13 | let upper_roof = roof - 1 + (((-y.abs()/2 + z/2) % 1) - 0.5).abs() / 4; 14 | let cabin = max(cabin, upper_roof); 15 | let roof = max(lower_roof, upper_roof); 16 | let x_clamp = x.abs() - 13; 17 | let roof = max(max(x_clamp, roof), 18 - z); 18 | 19 | // Build a door frame 20 | let door_frame = max(y.abs() - 5, z - 14); 21 | let door_frame_inner = -(door_frame + 1.5); 22 | let door_width = max(10 - x, x - 11); 23 | let door_frame = max(max(door_frame, door_frame_inner), 24 | door_width); 25 | let doorknob = (x.square() + y.square() + z.square()).sqrt() - 0.6; 26 | let door = min(door_frame, doorknob.remap(x - 10.5, y - 2, z - 6)); 27 | let cabin = max(cabin, -max(-door_frame_inner, door_width)); 28 | let cabin = min(cabin, door); 29 | 30 | // Build a window 31 | let window_root = max(x.abs(), (z - 9).abs()); 32 | let cabin = max(cabin, -max(window_root - 4, 10 - y.abs())); // cut out window 33 | let window_cross = max(window_root - 4, min(x.abs() - 0.2, (z - 9).abs() - 0.2)); 34 | let window_frame = max(window_root - 4, 3 - window_root); 35 | let cabin = min(cabin, max(min(window_frame, window_cross), y.abs() - 11)); 36 | let cabin = min(cabin, max(window_root - 3, y.abs() - 10.6)); 37 | 38 | let cabin = min(max(-z, cabin), roof); 39 | draw(cabin) 40 | -------------------------------------------------------------------------------- /fidget-core/src/vm/choice.rs: -------------------------------------------------------------------------------- 1 | /// A single choice made at a min/max node. 2 | /// 3 | /// Explicitly stored in a `u8` so that this can be written by JIT functions, 4 | /// which have no notion of Rust enums. 5 | /// 6 | /// Note that this is a bitfield such that 7 | /// ```rust 8 | /// # use fidget_core::vm::Choice; 9 | /// # assert!( 10 | /// Choice::Both as u8 == Choice::Left as u8 | Choice::Right as u8 11 | /// # ); 12 | /// ``` 13 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 14 | #[repr(u8)] 15 | pub enum Choice { 16 | /// This choice has not yet been assigned 17 | /// 18 | /// A value of `Unknown` is invalid after evaluation 19 | Unknown = 0, 20 | 21 | /// The operation always picks the left-hand input 22 | Left = 1, 23 | 24 | /// The operation always picks the right-hand input 25 | Right = 2, 26 | 27 | /// The operation may pick either input 28 | Both = 3, 29 | } 30 | 31 | impl std::ops::BitOrAssign for Choice { 32 | fn bitor_assign(&mut self, other: Self) { 33 | *self = match (*self as u8) | (other as u8) { 34 | 0 => Self::Unknown, 35 | 1 => Self::Left, 36 | 2 => Self::Right, 37 | 3 => Self::Both, 38 | _ => unreachable!(), 39 | } 40 | } 41 | } 42 | 43 | impl std::ops::Not for Choice { 44 | type Output = Choice; 45 | fn not(self) -> Self { 46 | match self { 47 | Self::Unknown => Self::Both, 48 | Self::Left => Self::Right, 49 | Self::Right => Self::Left, 50 | Self::Both => Self::Unknown, 51 | } 52 | } 53 | } 54 | 55 | impl std::ops::BitAndAssign for Choice { 56 | fn bitand_assign(&mut self, other: Self) { 57 | *self = match (*self as u8) | ((!other as u8) & 0b11) { 58 | 0 => Self::Unknown, 59 | 1 => Self::Left, 60 | 2 => Self::Right, 61 | 3 => Self::Both, 62 | _ => unreachable!(), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/message.ts: -------------------------------------------------------------------------------- 1 | import * as fidget from "../../crate/pkg/fidget_wasm_demo"; 2 | import { Camera } from "./camera"; 3 | 4 | export class ScriptRequest { 5 | kind: "script"; 6 | script: string; 7 | 8 | constructor(script: string) { 9 | this.script = script; 10 | this.kind = "script"; 11 | } 12 | } 13 | 14 | type RenderMode = "bitmap" | "heightmap" | "normals"; 15 | export class RenderRequest { 16 | kind: "shape"; 17 | tape: Uint8Array; 18 | camera: Uint8Array; 19 | depth: number; 20 | mode: RenderMode; 21 | cancel_token_ptr: number; // pointer 22 | 23 | constructor( 24 | tape: Uint8Array, 25 | camera: Camera, 26 | depth: number, 27 | mode: RenderMode, 28 | cancel_token_ptr: number, 29 | ) { 30 | this.tape = tape; 31 | this.kind = "shape"; 32 | this.camera = camera.camera.serialize_view(); 33 | this.depth = depth; 34 | this.mode = mode; 35 | this.cancel_token_ptr = cancel_token_ptr; 36 | } 37 | } 38 | 39 | export class StartRequest { 40 | kind: "start"; 41 | init: object; 42 | 43 | constructor(init: object) { 44 | this.init = init; 45 | this.kind = "start"; 46 | } 47 | } 48 | 49 | export type WorkerRequest = StartRequest | ScriptRequest | RenderRequest; 50 | 51 | //////////////////////////////////////////////////////////////////////////////// 52 | 53 | export class StartedResponse { 54 | kind: "started"; 55 | 56 | constructor() { 57 | this.kind = "started"; 58 | } 59 | } 60 | 61 | export class ScriptResponse { 62 | kind: "script"; 63 | output: string; 64 | tape: Uint8Array | null; 65 | 66 | constructor(output: string, tape: Uint8Array | null) { 67 | this.output = output; 68 | this.tape = tape; 69 | this.kind = "script"; 70 | } 71 | } 72 | 73 | export class ImageResponse { 74 | kind: "image"; 75 | data: Uint8Array; 76 | depth: number; 77 | 78 | constructor(data: Uint8Array, depth: number) { 79 | this.data = data; 80 | this.depth = depth; 81 | this.kind = "image"; 82 | } 83 | } 84 | 85 | export class CancelledResponse { 86 | kind: "cancelled"; 87 | 88 | constructor() { 89 | this.kind = "cancelled"; 90 | } 91 | } 92 | 93 | export type WorkerResponse = 94 | | StartedResponse 95 | | ScriptResponse 96 | | ImageResponse 97 | | CancelledResponse; 98 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fidget Demo 6 | 48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 | 57 | Loading... 58 | 63 |
64 |
65 |
66 |

67 | Github | 68 | Writeup 69 |

70 | 71 |

72 | Version: 73 | 74 | VERSIONMOD 76 |

77 |
78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /fidget/benches/mesh.rs: -------------------------------------------------------------------------------- 1 | use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; 2 | use std::hint::black_box; 3 | 4 | const COLONNADE: &str = include_str!("../../models/colonnade.vm"); 5 | 6 | pub fn colonnade_octree_thread_sweep(c: &mut Criterion) { 7 | let (ctx, root) = fidget::Context::from_text(COLONNADE.as_bytes()).unwrap(); 8 | let shape_vm = &fidget::vm::VmShape::new(&ctx, root).unwrap(); 9 | #[cfg(feature = "jit")] 10 | let shape_jit = &fidget::jit::JitShape::new(&ctx, root).unwrap(); 11 | 12 | let mut group = 13 | c.benchmark_group("speed vs threads (colonnade, octree) (depth 6)"); 14 | 15 | for threads in [None, Some(1), Some(4), Some(8)] { 16 | let pool = threads.map(|n| { 17 | fidget::render::ThreadPool::Custom( 18 | rayon::ThreadPoolBuilder::new() 19 | .num_threads(n) 20 | .build() 21 | .unwrap(), 22 | ) 23 | }); 24 | let cfg = &fidget::mesh::Settings { 25 | depth: 6, 26 | threads: pool.as_ref(), 27 | ..Default::default() 28 | }; 29 | let threads = threads.unwrap_or(0); 30 | 31 | #[cfg(feature = "jit")] 32 | group.bench_function(BenchmarkId::new("jit", threads), move |b| { 33 | b.iter(|| black_box(fidget::mesh::Octree::build(shape_jit, cfg))) 34 | }); 35 | group.bench_function(BenchmarkId::new("vm", threads), move |b| { 36 | b.iter(|| black_box(fidget::mesh::Octree::build(shape_vm, cfg))) 37 | }); 38 | } 39 | } 40 | 41 | pub fn colonnade_mesh(c: &mut Criterion) { 42 | let (ctx, root) = fidget::Context::from_text(COLONNADE.as_bytes()).unwrap(); 43 | let shape_vm = &fidget::vm::VmShape::new(&ctx, root).unwrap(); 44 | let cfg = fidget::mesh::Settings { 45 | depth: 8, 46 | ..Default::default() 47 | }; 48 | let octree = &fidget::mesh::Octree::build(shape_vm, &cfg).unwrap(); 49 | 50 | let mut group = c.benchmark_group("speed (colonnade, meshing) (depth 8)"); 51 | group 52 | .bench_function(BenchmarkId::new("walk_dual", "colonnade"), move |b| { 53 | b.iter(|| black_box(octree.walk_dual())) 54 | }); 55 | } 56 | 57 | criterion_group!(benches, colonnade_octree_thread_sweep, colonnade_mesh); 58 | criterion_main!(benches); 59 | -------------------------------------------------------------------------------- /fidget/benches/function_call.rs: -------------------------------------------------------------------------------- 1 | use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; 2 | use fidget_core::{ 3 | context::{Context, Node}, 4 | eval::{Function, MathFunction}, 5 | shape::{EzShape, Shape}, 6 | }; 7 | use std::hint::black_box; 8 | 9 | pub fn run_bench( 10 | c: &mut Criterion, 11 | ctx: Context, 12 | node: Node, 13 | test_name: &'static str, 14 | name: &'static str, 15 | ) { 16 | let shape_vm = &Shape::::new(&ctx, node).unwrap(); 17 | 18 | let mut eval = Shape::::new_float_slice_eval(); 19 | let tape = shape_vm.ez_float_slice_tape(); 20 | 21 | let mut group = c.benchmark_group(test_name); 22 | for n in [10, 100, 1000] { 23 | let data = (0..n).map(|i| i as f32 / n as f32).collect::>(); 24 | let t = &tape; 25 | group.bench_function(BenchmarkId::new(name, n), |b| { 26 | b.iter(|| { 27 | black_box(eval.eval(t, &data, &data, &data).unwrap()); 28 | }) 29 | }); 30 | } 31 | } 32 | 33 | pub fn test_single_fn( 34 | c: &mut Criterion, 35 | name: &'static str, 36 | ) { 37 | let mut ctx = Context::new(); 38 | let x = ctx.x(); 39 | let f = ctx.sin(x).unwrap(); 40 | 41 | run_bench::(c, ctx, f, "single function", name); 42 | } 43 | 44 | pub fn test_many_fn( 45 | c: &mut Criterion, 46 | name: &'static str, 47 | ) { 48 | let mut ctx = Context::new(); 49 | let x = ctx.x(); 50 | let f = ctx.sin(x).unwrap(); 51 | let y = ctx.y(); 52 | let g = ctx.cos(y).unwrap(); 53 | let z = ctx.z(); 54 | let h = ctx.exp(z).unwrap(); 55 | 56 | let out = ctx.add(f, g).unwrap(); 57 | let out = ctx.add(out, h).unwrap(); 58 | 59 | run_bench::(c, ctx, out, "many functions", name); 60 | } 61 | 62 | pub fn test_single_fns(c: &mut Criterion) { 63 | test_single_fn::(c, "vm"); 64 | #[cfg(feature = "jit")] 65 | test_single_fn::(c, "jit"); 66 | } 67 | 68 | pub fn test_many_fns(c: &mut Criterion) { 69 | test_many_fn::(c, "vm"); 70 | #[cfg(feature = "jit")] 71 | test_many_fn::(c, "jit"); 72 | } 73 | 74 | criterion_group!(benches, test_single_fns, test_many_fns); 75 | criterion_main!(benches); 76 | -------------------------------------------------------------------------------- /fidget-core/src/eval/tracing.rs: -------------------------------------------------------------------------------- 1 | //! Capturing a trace of function evaluation for further optimization 2 | //! 3 | //! Tracing evaluators are run on a single data type and capture a trace of 4 | //! execution, which is the [`Trace` associated type](TracingEvaluator::Trace). 5 | //! 6 | //! The resulting trace can be used to simplify the original function. 7 | //! 8 | //! It is unlikely that you'll want to use these traits or types directly; 9 | //! they're implementation details to minimize code duplication. 10 | 11 | use crate::{Error, eval::Tape}; 12 | 13 | /// Evaluator for single values which simultaneously captures an execution trace 14 | /// 15 | /// The trace can later be used to simplify the 16 | /// [`Function`](crate::eval::Function) 17 | /// using [`Function::simplify`](crate::eval::Function::simplify). 18 | /// 19 | /// Tracing evaluators may contain intermediate storage (e.g. an array of VM 20 | /// registers), and should be constructed on a per-thread basis. 21 | pub trait TracingEvaluator: Default { 22 | /// Data type used during evaluation 23 | type Data: From + Copy + Clone; 24 | 25 | /// Instruction tape used during evaluation 26 | /// 27 | /// This may be a literal instruction tape (in the case of VM evaluation), 28 | /// or a metaphorical instruction tape (e.g. a JIT function). 29 | type Tape: Tape; 30 | 31 | /// Associated type for tape storage 32 | /// 33 | /// This is a workaround for plumbing purposes 34 | type TapeStorage; 35 | 36 | /// Associated type for the trace captured during evaluation 37 | type Trace; 38 | 39 | /// Evaluates the given tape at a particular position 40 | /// 41 | /// `vars` should be a slice of values representing input arguments for each 42 | /// of the tape's variables; use [`Tape::vars`] to map from 43 | /// [`Var`](crate::var::Var) to position in the list. 44 | /// 45 | /// Returns an error if the `var` slice is not of sufficient length. 46 | fn eval( 47 | &mut self, 48 | tape: &Self::Tape, 49 | vars: &[Self::Data], 50 | ) -> Result, Error>; 51 | 52 | /// Build a new empty evaluator 53 | fn new() -> Self { 54 | Self::default() 55 | } 56 | } 57 | 58 | /// Tuple of tracing evaluation result 59 | type TracingResult<'a, Data, Trace> = (&'a [Data], Option<&'a Trace>); 60 | -------------------------------------------------------------------------------- /fidget-core/src/render/config.rs: -------------------------------------------------------------------------------- 1 | //! Types used in configuration structures 2 | use std::sync::{ 3 | Arc, 4 | atomic::{AtomicBool, Ordering}, 5 | }; 6 | 7 | /// Thread pool to use for multithreaded rendering 8 | /// 9 | /// Most users will use the global Rayon pool, but it's possible to provide your 10 | /// own as well. 11 | pub enum ThreadPool { 12 | /// User-provided pool 13 | Custom(rayon::ThreadPool), 14 | /// Global Rayon pool 15 | Global, 16 | } 17 | 18 | impl ThreadPool { 19 | /// Runs a function across the thread pool 20 | pub fn run V + Send, V: Send>(&self, f: F) -> V { 21 | match self { 22 | ThreadPool::Custom(p) => p.install(f), 23 | ThreadPool::Global => f(), 24 | } 25 | } 26 | 27 | /// Returns the number of threads in the pool 28 | pub fn thread_count(&self) -> usize { 29 | match self { 30 | ThreadPool::Custom(p) => p.current_num_threads(), 31 | ThreadPool::Global => rayon::current_num_threads(), 32 | } 33 | } 34 | } 35 | 36 | /// Token to cancel an in-progress operation 37 | #[derive(Clone, Default)] 38 | pub struct CancelToken(Arc); 39 | 40 | impl CancelToken { 41 | /// Build a new token, which is initialize as "not cancelled" 42 | pub fn new() -> Self { 43 | Self::default() 44 | } 45 | 46 | /// Mark this token as cancelled 47 | pub fn cancel(&self) { 48 | self.0.store(true, Ordering::Relaxed); 49 | } 50 | 51 | /// Check if the token is cancelled 52 | pub fn is_cancelled(&self) -> bool { 53 | self.0.load(Ordering::Relaxed) 54 | } 55 | 56 | /// Returns a raw pointer to the inner flag 57 | /// 58 | /// This is used in shared memory environments where the `CancelToken` 59 | /// itself cannot be passed between threads, i.e. to send a cancel token to 60 | /// a web worker. 61 | /// 62 | /// To avoid a memory leak, the pointer must be converted back to a 63 | /// `CancelToken` using [`CancelToken::from_raw`]. In the meantime, users 64 | /// should refrain from writing to the raw pointer. 65 | #[doc(hidden)] 66 | pub fn into_raw(self) -> *const AtomicBool { 67 | Arc::into_raw(self.0) 68 | } 69 | 70 | /// Reclaims a released cancel token pointer 71 | /// 72 | /// # Safety 73 | /// The pointer must have been previously returned by a call to 74 | /// [`CancelToken::into_raw`]. 75 | #[doc(hidden)] 76 | pub unsafe fn from_raw(ptr: *const AtomicBool) -> Self { 77 | let a = unsafe { Arc::from_raw(ptr) }; 78 | Self(a) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /fidget-core/src/compiler/reg_tape.rs: -------------------------------------------------------------------------------- 1 | //! Tape used for evaluation 2 | use crate::compiler::{RegOp, RegisterAllocator, SsaTape}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Low-level tape for use with the Fidget virtual machine (or to be lowered 6 | /// further into machine instructions). 7 | #[derive(Clone, Default, Serialize, Deserialize)] 8 | pub struct RegTape { 9 | tape: Vec, 10 | 11 | /// Total allocated slots 12 | pub(super) slot_count: u32, 13 | } 14 | 15 | impl RegTape { 16 | /// Lowers the tape to assembly with a particular register limit 17 | /// 18 | /// Note that if you _also_ want to simplify the tape, it's more efficient 19 | /// to use [`VmData::simplify`](crate::vm::VmData::simplify), which 20 | /// simultaneously simplifies **and** performs register allocation in a 21 | /// single pass. 22 | pub fn new(ssa: &SsaTape) -> Self { 23 | let mut alloc = RegisterAllocator::::new(ssa.len()); 24 | for &op in ssa.iter() { 25 | alloc.op(op) 26 | } 27 | alloc.finalize() 28 | } 29 | 30 | /// Builds a new empty tape 31 | pub(crate) fn empty() -> Self { 32 | Self { 33 | tape: vec![], 34 | slot_count: 0, 35 | } 36 | } 37 | 38 | /// Resets this tape, retaining its allocations 39 | pub fn reset(&mut self) { 40 | self.tape.clear(); 41 | self.slot_count = 0; 42 | } 43 | 44 | /// Returns the number of unique register and memory locations that are used 45 | /// by this tape. 46 | #[inline] 47 | pub fn slot_count(&self) -> usize { 48 | self.slot_count as usize 49 | } 50 | /// Returns the number of elements in the tape 51 | #[inline] 52 | pub fn len(&self) -> usize { 53 | self.tape.len() 54 | } 55 | /// Returns `true` if the tape contains no elements 56 | #[inline] 57 | pub fn is_empty(&self) -> bool { 58 | self.tape.is_empty() 59 | } 60 | /// Returns a front-to-back iterator 61 | /// 62 | /// This is the opposite of evaluation order; it will visit the root of the 63 | /// tree first, and end at the leaves. 64 | #[inline] 65 | pub fn iter(&self) -> impl DoubleEndedIterator { 66 | self.into_iter() 67 | } 68 | #[inline] 69 | pub(crate) fn push(&mut self, op: RegOp) { 70 | self.tape.push(op) 71 | } 72 | } 73 | 74 | impl<'a> IntoIterator for &'a RegTape { 75 | type Item = &'a RegOp; 76 | type IntoIter = std::slice::Iter<'a, RegOp>; 77 | fn into_iter(self) -> Self::IntoIter { 78 | self.tape.iter() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /fidget/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fidget" 3 | description = "Infrastructure for complex closed-form implicit surfaces" 4 | readme = "../README.md" 5 | version = "0.4.1" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | authors.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | fidget-core = { workspace = true } 15 | 16 | fidget-bytecode = { workspace = true, optional = true } 17 | fidget-gui = { workspace = true, optional = true } 18 | fidget-mesh = { workspace = true, optional = true } 19 | fidget-raster = { workspace = true, optional = true } 20 | fidget-rhai = { workspace = true, optional = true } 21 | fidget-shapes = { workspace = true, optional = true } 22 | fidget-solver = { workspace = true, optional = true } 23 | 24 | document-features.workspace = true 25 | workspace-hack.workspace = true 26 | 27 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 28 | fidget-jit = { workspace = true, optional = true } 29 | 30 | [features] 31 | default = [ 32 | "bytecode", 33 | "gui", 34 | "jit", 35 | "mesh", 36 | "raster", 37 | "rhai", 38 | "shapes", 39 | "solver", 40 | ] 41 | 42 | ## Enables fast evaluation via a JIT compiler. This is exposed in the 43 | ## [`fidget::jit`](crate::jit) module, and is supported on macOS, Linux, and 44 | ## Windows (i.e. all supported platforms except WebAssembly). 45 | jit = ["dep:fidget-jit"] 46 | 47 | ## Enable [Rhai](https://rhai.rs/) bindings, in the 48 | ## [`fidget::rhai`](crate::rhai) module 49 | rhai = ["dep:fidget-rhai"] 50 | 51 | ## Enable meshing in the [`fidget::mesh`](crate::mesh) module 52 | mesh = ["dep:fidget-mesh"] 53 | 54 | ## Enables constraint solving in the [`fidget::solver`](crate::solver) module 55 | solver = ["dep:fidget-solver"] 56 | 57 | ## Enables a library of standard shapes and transforms in the [`fidget::shapes`](crate::shapes) module 58 | shapes = ["dep:fidget-shapes"] 59 | 60 | ## Enables bytecode generation in the [`fidget::bytecode`](crate::bytecode) module 61 | bytecode = ["dep:fidget-bytecode"] 62 | 63 | ## Enables image rendering in the [`fidget::raster`](crate::raster) module 64 | raster = ["dep:fidget-raster"] 65 | 66 | ## Enables GUI abstractions in the [`fidget::gui`](crate::gui) module 67 | gui = ["dep:fidget-gui"] 68 | 69 | [[bench]] 70 | name = "render" 71 | harness = false 72 | 73 | [[bench]] 74 | name = "mesh" 75 | harness = false 76 | 77 | [[bench]] 78 | name = "function_call" 79 | harness = false 80 | 81 | [lib] 82 | bench = false 83 | 84 | [dev-dependencies] 85 | criterion.workspace = true 86 | rayon.workspace = true 87 | nalgebra.workspace = true 88 | -------------------------------------------------------------------------------- /fidget-rhai/src/constants.rs: -------------------------------------------------------------------------------- 1 | //! Mathematical constants for Rhai scripts 2 | use std::f64::consts; 3 | 4 | /// Get a mathematical constant by name 5 | pub fn get_constant(name: &str) -> Option { 6 | match name { 7 | // Basic mathematical constants 8 | "PI" => Some(consts::PI), 9 | "E" => Some(consts::E), 10 | "TAU" => Some(consts::TAU), 11 | 12 | // Square roots 13 | "SQRT_2" => Some(consts::SQRT_2), 14 | 15 | // Logarithms 16 | "LN_2" => Some(consts::LN_2), 17 | "LN_10" => Some(consts::LN_10), 18 | "LOG2_E" => Some(consts::LOG2_E), 19 | "LOG10_E" => Some(consts::LOG10_E), 20 | 21 | // Fractions of PI 22 | "FRAC_PI_2" => Some(consts::FRAC_PI_2), 23 | "FRAC_PI_3" => Some(consts::FRAC_PI_3), 24 | "FRAC_PI_4" => Some(consts::FRAC_PI_4), 25 | "FRAC_PI_6" => Some(consts::FRAC_PI_6), 26 | "FRAC_PI_8" => Some(consts::FRAC_PI_8), 27 | 28 | // Reciprocals of PI 29 | "FRAC_1_PI" => Some(consts::FRAC_1_PI), 30 | "FRAC_2_PI" => Some(consts::FRAC_2_PI), 31 | "FRAC_2_SQRT_PI" => Some(consts::FRAC_2_SQRT_PI), 32 | 33 | // Golden ratio and related constants 34 | "PHI" | "GOLDEN_RATIO" => Some(1.618033988749895_f64), // Golden ratio (1 + sqrt(5)) / 2 35 | 36 | // Common fractions 37 | "FRAC_1_SQRT_2" => Some(consts::FRAC_1_SQRT_2), 38 | 39 | _ => None, 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | use super::*; 46 | 47 | #[test] 48 | fn test_get_constant() { 49 | // Test basic constants 50 | assert!( 51 | (get_constant("PI").unwrap() - std::f64::consts::PI).abs() 52 | < f64::EPSILON 53 | ); 54 | assert!( 55 | (get_constant("E").unwrap() - std::f64::consts::E).abs() 56 | < f64::EPSILON 57 | ); 58 | assert!( 59 | (get_constant("TAU").unwrap() - std::f64::consts::TAU).abs() 60 | < f64::EPSILON 61 | ); 62 | 63 | // Test golden ratio 64 | assert!( 65 | (get_constant("PHI").unwrap() - 1.618033988749895_f64).abs() 66 | < f64::EPSILON 67 | ); 68 | assert!( 69 | (get_constant("GOLDEN_RATIO").unwrap() - 1.618033988749895_f64) 70 | .abs() 71 | < f64::EPSILON 72 | ); 73 | 74 | // Test fractions of PI 75 | assert!( 76 | (get_constant("FRAC_PI_2").unwrap() - std::f64::consts::FRAC_PI_2) 77 | .abs() 78 | < f64::EPSILON 79 | ); 80 | 81 | // Test unknown constant 82 | assert!(get_constant("UNKNOWN").is_none()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /fidget-mesh/src/builder.rs: -------------------------------------------------------------------------------- 1 | //! Mesh builder data structure and implementation 2 | use super::{ 3 | Mesh, Octree, 4 | cell::{CellIndex, CellVertex}, 5 | dc, 6 | frame::Frame, 7 | }; 8 | 9 | /// Container used during construction of a [`Mesh`] 10 | #[derive(Default)] 11 | pub struct MeshBuilder { 12 | /// Map from indexes in [`Octree::verts`](super::Octree::verts) to 13 | /// `out.vertices` 14 | /// 15 | /// `usize::MAX` is used a marker for an unmapped vertex 16 | map: Vec, 17 | out: Mesh, 18 | } 19 | 20 | impl MeshBuilder { 21 | pub fn take(self) -> Mesh { 22 | self.out 23 | } 24 | 25 | pub(crate) fn cell(&mut self, octree: &Octree, cell: CellIndex<3>) { 26 | dc::dc_cell(octree, cell, self); 27 | } 28 | 29 | pub(crate) fn face( 30 | &mut self, 31 | octree: &Octree, 32 | a: CellIndex<3>, 33 | b: CellIndex<3>, 34 | ) { 35 | dc::dc_face::(octree, a, b, self) 36 | } 37 | 38 | /// Handles four cells that share a common edge aligned on axis `T` 39 | /// 40 | /// Cells positions are in the order `[0, U, U | V, U]`, i.e. a right-handed 41 | /// winding about `+T` (where `T, U, V` is a right-handed coordinate frame) 42 | pub(crate) fn edge( 43 | &mut self, 44 | octree: &Octree, 45 | a: CellIndex<3>, 46 | b: CellIndex<3>, 47 | c: CellIndex<3>, 48 | d: CellIndex<3>, 49 | ) { 50 | dc::dc_edge::(octree, a, b, c, d, self) 51 | } 52 | 53 | /// Record the given triangle 54 | /// 55 | /// Vertices are indices given by calls to [`Self::vertex`] 56 | /// 57 | /// The vertices are given in a clockwise winding with the intersection 58 | /// vertex (i.e. the one on the edge) always last. 59 | pub(crate) fn triangle(&mut self, a: usize, b: usize, c: usize) { 60 | self.out.triangles.push(nalgebra::Vector3::new(a, b, c)) 61 | } 62 | 63 | /// Looks up the given vertex, localizing it within a cell 64 | /// 65 | /// `v` is an absolute offset into `verts`, which should be a reference to 66 | /// [`Octree::verts`](super::Octree::verts). 67 | pub(crate) fn vertex( 68 | &mut self, 69 | v: usize, 70 | verts: &[CellVertex<3>], 71 | ) -> usize { 72 | if v >= self.map.len() { 73 | self.map.resize(v + 1, usize::MAX); 74 | } 75 | match self.map[v] { 76 | usize::MAX => { 77 | let next_vert = self.out.vertices.len(); 78 | self.out.vertices.push(verts[v].pos); 79 | self.map[v] = next_vert; 80 | 81 | next_vert 82 | } 83 | u => u, 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /fidget-core/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Module containing the Fidget universal error type 2 | use crate::var::Var; 3 | use thiserror::Error; 4 | 5 | /// Universal error type for Fidget 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | /// Node is not present in this `Context` 9 | #[error("node is not present in this `Context`")] 10 | BadNode, 11 | 12 | /// Variable is not present in this `Context` 13 | #[error("variable is not present in this `Context`")] 14 | BadVar, 15 | 16 | /// Variable is missing in the evaluation map 17 | #[error("variable {0} is missing in the evaluation map")] 18 | MissingVar(Var), 19 | 20 | /// The given node does not have an associated variable 21 | #[error("node does not have an associated variable")] 22 | NotAVar, 23 | 24 | /// The given node is not a constant 25 | #[error("node is not a constant")] 26 | NotAConst, 27 | 28 | /// `Context` is empty 29 | #[error("`Context` is empty")] 30 | EmptyContext, 31 | 32 | /// `IndexMap` is empty 33 | #[error("`IndexMap` is empty")] 34 | EmptyMap, 35 | 36 | /// Unknown opcode {0} 37 | #[error("unknown opcode {0}")] 38 | UnknownOpcode(String), 39 | 40 | /// Unknown variable {0} 41 | #[error("unknown variable {0}")] 42 | UnknownVariable(String), 43 | 44 | /// Empty file 45 | #[error("empty file")] 46 | EmptyFile, 47 | 48 | /// Choice slice length does not match choice count 49 | #[error("choice slice length ({0}) does not match choice count ({1})")] 50 | BadChoiceSlice(usize, usize), 51 | 52 | /// Variable slice lengths are mismatched 53 | #[error("variable slice lengths are mismatched")] 54 | MismatchedSlices, 55 | 56 | /// Variable slice length does not match expected count 57 | #[error("variable slice length ({0}) does not match expected count ({1})")] 58 | BadVarSlice(usize, usize), 59 | 60 | /// Variable index exceeds max var index for this tape 61 | #[error("variable index ({0}) exceeds max var index for this tape ({1})")] 62 | BadVarIndex(usize, usize), 63 | 64 | /// Could not solve for matrix pseudo-inverse 65 | #[error("could not solve for matrix pseudo-inverse: {0}")] 66 | SingularMatrix(&'static str), 67 | 68 | /// IO error; see inner code for details 69 | #[error("io error: {0}")] 70 | IoError(#[from] std::io::Error), 71 | 72 | /// Each tile must be divisible by subsequent tiles 73 | #[error("bad tile sizes; {0} is not divisible by {1}")] 74 | BadTileSize(usize, usize), 75 | 76 | /// Tile size list must be in descending order 77 | #[error("bad tile order; {0} is not larger than {1}")] 78 | BadTileOrder(usize, usize), 79 | 80 | /// Tile size list must not be empty 81 | #[error("tile size list must not be empty")] 82 | EmptyTileSizes, 83 | } 84 | -------------------------------------------------------------------------------- /demos/web-editor/web/src/worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImageResponse, 3 | CancelledResponse, 4 | StartRequest, 5 | ScriptRequest, 6 | ScriptResponse, 7 | RenderRequest, 8 | StartedResponse, 9 | WorkerRequest, 10 | } from "./message"; 11 | 12 | import { RENDER_SIZE } from "./constants"; 13 | 14 | import * as fidget from "../../crate/pkg/fidget_wasm_demo"; 15 | 16 | class Worker { 17 | render(s: RenderRequest) { 18 | const shape = fidget.deserialize_tape(s.tape); 19 | const cancel = fidget.JsCancelToken.from_ptr(s.cancel_token_ptr); 20 | let out: Uint8Array; 21 | const size = Math.round(RENDER_SIZE / Math.pow(2, s.depth)); 22 | try { 23 | switch (s.mode) { 24 | case "bitmap": { 25 | const camera = fidget.JsCamera2.deserialize(s.camera); 26 | out = fidget.render_2d(shape, size, camera, cancel); 27 | break; 28 | } 29 | case "heightmap": { 30 | const camera = fidget.JsCamera3.deserialize(s.camera); 31 | out = fidget.render_heightmap(shape, size, camera, cancel); 32 | break; 33 | } 34 | case "normals": { 35 | const camera = fidget.JsCamera3.deserialize(s.camera); 36 | out = fidget.render_normals(shape, size, camera, cancel); 37 | break; 38 | } 39 | } 40 | postMessage(new ImageResponse(out, s.depth), { transfer: [out.buffer] }); 41 | } catch (e) { 42 | postMessage(new CancelledResponse()); 43 | } 44 | } 45 | 46 | run(s: ScriptRequest) { 47 | let shape = null; 48 | let result = "Ok(..)"; 49 | try { 50 | shape = fidget.eval_script(s.script); 51 | } catch (error) { 52 | // Do some string formatting to make errors cleaner 53 | result = error 54 | .toString() 55 | .replace("Rhai evaluation error: ", "Rhai evaluation error:\n") 56 | .replace(" (line ", "\n(line ") 57 | .replace(" (expecting ", "\n(expecting "); 58 | } 59 | 60 | let tape = null; 61 | if (shape) { 62 | tape = fidget.serialize_into_tape(shape); 63 | postMessage(new ScriptResponse(result, tape), { 64 | transfer: [tape.buffer], 65 | }); 66 | } else { 67 | postMessage(new ScriptResponse(result, tape)); 68 | } 69 | } 70 | } 71 | 72 | async function run() { 73 | let worker = new Worker(); 74 | onmessage = function (e: any) { 75 | let req = e.data as WorkerRequest; 76 | switch (req.kind) { 77 | case "start": { 78 | fidget.initSync((req as StartRequest).init); 79 | postMessage(new StartedResponse()); 80 | break; 81 | } 82 | case "shape": { 83 | worker!.render(req as RenderRequest); 84 | break; 85 | } 86 | case "script": { 87 | worker!.run(req as ScriptRequest); 88 | break; 89 | } 90 | default: 91 | console.error(`unknown worker request ${req}`); 92 | } 93 | }; 94 | } 95 | run(); 96 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | # Fidget crates 5 | "fidget", 6 | "fidget-core", 7 | "fidget-bytecode", 8 | "fidget-gui", 9 | "fidget-jit", 10 | "fidget-mesh", 11 | "fidget-raster", 12 | "fidget-rhai", 13 | "fidget-shapes", 14 | "fidget-solver", 15 | 16 | # Non-WASM demos 17 | "demos/constraints", 18 | "demos/cli", 19 | "demos/viewer", 20 | 21 | # `cargo hakari` package 22 | "workspace-hack", 23 | ] 24 | exclude = ["demos/web-editor/crate"] 25 | 26 | [workspace.package] 27 | rust-version = "1.87" 28 | edition = "2024" 29 | license = "MPL-2.0" 30 | repository = "https://github.com/mkeeter/fidget" 31 | description = "Infrastructure for complex closed-form implicit surfaces" 32 | authors = ["Matt Keeter (()) 44 | //! ``` 45 | #![warn(missing_docs)] 46 | 47 | mod builder; 48 | mod cell; 49 | mod codegen; 50 | mod dc; 51 | mod frame; 52 | mod octree; 53 | mod output; 54 | mod qef; 55 | 56 | use fidget_core::render::{CancelToken, ThreadPool}; 57 | 58 | #[doc(hidden)] 59 | pub mod types; 60 | 61 | // Re-export the main Octree type as public 62 | pub use octree::Octree; 63 | 64 | //////////////////////////////////////////////////////////////////////////////// 65 | 66 | /// An indexed 3D mesh 67 | #[derive(Default, Debug)] 68 | pub struct Mesh { 69 | /// Triangles, as indexes into [`self.vertices`](Self::vertices) 70 | pub triangles: Vec>, 71 | /// Vertex positions 72 | pub vertices: Vec>, 73 | } 74 | 75 | impl Mesh { 76 | /// Builds a new mesh 77 | pub fn new() -> Self { 78 | Self::default() 79 | } 80 | } 81 | 82 | /// Settings when building an octree and mesh 83 | pub struct Settings<'a> { 84 | /// Depth to recurse in the octree 85 | pub depth: u8, 86 | 87 | /// Viewport to provide a world-to-model transform 88 | pub world_to_model: nalgebra::Matrix4, 89 | 90 | /// Thread pool to use for rendering 91 | /// 92 | /// If this is `None`, then rendering is done in a single thread; otherwise, 93 | /// the provided pool is used. 94 | pub threads: Option<&'a ThreadPool>, 95 | 96 | /// Token to cancel rendering 97 | pub cancel: CancelToken, 98 | } 99 | 100 | impl Default for Settings<'_> { 101 | fn default() -> Self { 102 | Self { 103 | depth: 3, 104 | world_to_model: nalgebra::Matrix4::identity(), 105 | threads: Some(&ThreadPool::Global), 106 | cancel: CancelToken::new(), 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /fidget/tests/render3d.rs: -------------------------------------------------------------------------------- 1 | //! Integration test for 3D rendering with VM and JIT evaluators 2 | use fidget::{ 3 | context::Tree, 4 | eval::{Function, MathFunction}, 5 | gui::View3, 6 | raster::VoxelRenderConfig, 7 | render::VoxelSize, 8 | shape::{Shape, ShapeVars}, 9 | var::Var, 10 | }; 11 | use nalgebra::Vector3; 12 | 13 | fn sphere_var() { 14 | let (x, y, z) = Tree::axes(); 15 | let v = Var::new(); 16 | let c = Tree::from(v); 17 | let sphere = (x.square() + y.square() + z.square()).sqrt() - c; 18 | let shape = Shape::::from(sphere); 19 | 20 | let size = 32; 21 | for scale in [1.0, 0.5] { 22 | let cfg = VoxelRenderConfig { 23 | image_size: VoxelSize::from(size), 24 | world_to_model: View3::from_center_and_scale( 25 | Vector3::zeros(), 26 | scale, 27 | ) 28 | .world_to_model(), 29 | ..Default::default() 30 | }; 31 | let m = cfg.image_size.screen_to_world(); 32 | 33 | for r in [0.5, 0.75] { 34 | let mut vars = ShapeVars::new(); 35 | vars.insert(v.index().unwrap(), r); 36 | let image = cfg.run_with_vars::<_>(shape.clone(), &vars).unwrap(); 37 | 38 | // Handwavey calculation: ±1 split into `size` voxels, max error 39 | // of two voxels (top to bottom), and dividing for `scale` for 40 | // bonus corrections. 41 | let epsilon = 2.0 / size as f32 / scale * 2.0; 42 | for (i, p) in image.iter().enumerate() { 43 | let p = p.depth; 44 | if p == size { 45 | // Skip saturated voxels 46 | continue; 47 | } 48 | let size = size as i32; 49 | let i = i as i32; 50 | let x = (i % size) as f32; 51 | let y = (i / size) as f32; 52 | let z = p as f32; 53 | let pos = 54 | m.transform_point(&nalgebra::Point3::new(x, y, z)) * scale; 55 | if p == 0 { 56 | let v = (pos.x.powi(2) + pos.y.powi(2)).sqrt(); 57 | assert!( 58 | v + epsilon > r, 59 | "got z = 0 inside the sphere ({x}, {y}, {z}); \ 60 | radius is {v}" 61 | ); 62 | } else { 63 | let v = 64 | (pos.x.powi(2) + pos.y.powi(2) + pos.z.powi(2)).sqrt(); 65 | let err = (r - v).abs(); 66 | assert!( 67 | err < epsilon, 68 | "too much error {err} at ({x}, {y}, {z}) ({pos}) \ 69 | (scale = {scale}); radius is {v}, expected {r}" 70 | ); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | macro_rules! render_tests { 78 | ($i:ident, $ty:ty) => { 79 | mod $i { 80 | #[test] 81 | fn render_sphere_var() { 82 | super::sphere_var::<$ty>(); 83 | } 84 | } 85 | }; 86 | } 87 | 88 | render_tests!(vm, fidget::vm::VmFunction); 89 | render_tests!(vm3, fidget::vm::GenericVmFunction<3>); 90 | 91 | #[cfg(feature = "jit")] 92 | render_tests!(jit, fidget::jit::JitFunction); 93 | -------------------------------------------------------------------------------- /fidget-core/src/eval/bulk.rs: -------------------------------------------------------------------------------- 1 | //! Evaluates many points in a single call 2 | //! 3 | //! Doing bulk evaluations helps limit to overhead of instruction dispatch, and 4 | //! can take advantage of SIMD. 5 | //! 6 | //! It is unlikely that you'll want to use these traits or types directly; 7 | //! they're implementation details to minimize code duplication. 8 | 9 | use crate::{Error, eval::Tape}; 10 | 11 | /// Trait for bulk evaluation returning the given type `T` 12 | /// 13 | /// Bulk evaluators should usually be constructed on a per-thread basis. 14 | /// 15 | /// They contain (at minimum) output array storage, which is borrowed in the 16 | /// return from [`eval`](BulkEvaluator::eval). They may also contain 17 | /// intermediate storage (e.g. an array of VM registers). 18 | pub trait BulkEvaluator: Default { 19 | /// Data type used during evaluation 20 | type Data: From + Copy + Clone; 21 | 22 | /// Instruction tape used during evaluation 23 | /// 24 | /// This may be a literal instruction tape (in the case of VM evaluation), 25 | /// or a metaphorical instruction tape (e.g. a JIT function). 26 | type Tape: Tape; 27 | 28 | /// Associated type for tape storage 29 | /// 30 | /// This is a workaround for plumbing purposes 31 | type TapeStorage; 32 | 33 | /// Evaluates many points using the given instruction tape 34 | /// 35 | /// `vars` should be a slice-of-slices (or a slice-of-`Vec`s) representing 36 | /// input arguments for each of the tape's variables; use [`Tape::vars`] to 37 | /// map from [`Var`](crate::var::Var) to position in the list. 38 | /// 39 | /// The returned slice is borrowed from the evaluator. 40 | /// 41 | /// Returns an error if any of the `var` slices are of different lengths, or 42 | /// if all variables aren't present. 43 | fn eval>( 44 | &mut self, 45 | tape: &Self::Tape, 46 | vars: &[V], 47 | ) -> Result, Error>; 48 | 49 | /// Build a new empty evaluator 50 | fn new() -> Self { 51 | Self::default() 52 | } 53 | } 54 | 55 | /// Container for bulk output results 56 | /// 57 | /// This container represents an array-of-arrays. It is indexed first by 58 | /// output index, then by index within the evaluation array. 59 | pub struct BulkOutput<'a, T> { 60 | data: &'a Vec>, 61 | len: usize, 62 | } 63 | 64 | impl<'a, T> BulkOutput<'a, T> { 65 | /// Builds a new output handle 66 | /// 67 | /// Within each array in `data`, only the first `len` values are valid 68 | pub fn new(data: &'a Vec>, len: usize) -> Self { 69 | Self { data, len } 70 | } 71 | 72 | /// Returns the number of output variables 73 | /// 74 | /// Note that this is **not** the length of each individual output slice; 75 | /// that can be found with `out[0].len()` (assuming there is at least one 76 | /// output variable). 77 | pub fn len(&self) -> usize { 78 | self.data.len() 79 | } 80 | 81 | /// Checks whether the output contains zero variables 82 | pub fn is_empty(&self) -> bool { 83 | self.data.is_empty() 84 | } 85 | } 86 | 87 | impl<'a, T> std::ops::Index for BulkOutput<'a, T> { 88 | type Output = [T]; 89 | fn index(&self, i: usize) -> &'a Self::Output { 90 | &self.data[i][0..self.len] 91 | } 92 | } 93 | 94 | impl<'a, T> BulkOutput<'a, T> { 95 | /// Helper function to borrow using the original reference lifetime 96 | pub(crate) fn borrow(&self, i: usize) -> &'a [T] { 97 | &self.data[i][0..self.len] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /fidget-core/src/compiler/lru.rs: -------------------------------------------------------------------------------- 1 | /// Single node in the doubly-linked list 2 | #[derive(Copy, Clone, Default)] 3 | struct LruNode { 4 | prev: u8, 5 | next: u8, 6 | } 7 | 8 | /// Dead-simple LRU cache, implemented as a doubly-linked list with a static 9 | /// backing array. 10 | /// 11 | /// ```text 12 | /// <-- prev next --> 13 | /// ------- ------- ------- -------- 14 | /// | a | <--> | b | <--> | c | <--> | head | <--| 15 | /// ------- ------- ------- -------- | 16 | /// ^ oldest newest | 17 | /// |-----------------------------------------------| 18 | /// ``` 19 | pub struct Lru { 20 | data: [LruNode; N], 21 | head: u8, 22 | } 23 | 24 | impl Lru { 25 | pub fn new() -> Self { 26 | let mut out = Self { 27 | data: [LruNode::default(); N], 28 | head: 0, 29 | }; 30 | for i in 0..N { 31 | out.data[i].next = ((i + 1) % N) as u8; 32 | out.data[i].prev = (i.checked_sub(1).unwrap_or(N - 1)) as u8; 33 | } 34 | out 35 | } 36 | 37 | /// Remove a node from the linked list 38 | #[inline] 39 | fn remove(&mut self, i: u8) { 40 | let node = self.data[i as usize]; 41 | self.data[node.prev as usize].next = self.data[i as usize].next; 42 | self.data[node.next as usize].prev = self.data[i as usize].prev; 43 | } 44 | 45 | /// Inserts node `i` before location `next` 46 | #[inline] 47 | fn insert_before(&mut self, i: u8, next: u8) { 48 | let prev = self.data[next as usize].prev; 49 | self.data[prev as usize].next = i; 50 | self.data[next as usize].prev = i; 51 | self.data[i as usize] = LruNode { next, prev }; 52 | } 53 | 54 | /// Mark the given node as newest 55 | #[inline] 56 | pub fn poke(&mut self, i: u8) { 57 | let prev_newest = self.head; 58 | if prev_newest == i { 59 | return; 60 | } else if self.data[prev_newest as usize].prev != i { 61 | // If this wasn't the oldest node, then remove it and reinsert it 62 | // right before the head of the list. 63 | self.remove(i); 64 | self.insert_before(i, self.head); 65 | } 66 | self.head = i; // rotate the head back by one 67 | } 68 | 69 | /// Look up the oldest node in the list, marking it as newest 70 | #[inline] 71 | pub fn pop(&mut self) -> u8 { 72 | let out = self.data[self.head as usize].prev; 73 | self.head = out; // rotate 74 | out 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod test { 80 | use super::*; 81 | 82 | #[test] 83 | fn test_tiny_lru() { 84 | let mut lru: Lru<2> = Lru::new(); 85 | lru.poke(0); 86 | assert!(lru.pop() == 1); 87 | assert!(lru.pop() == 0); 88 | 89 | lru.poke(1); 90 | assert!(lru.pop() == 0); 91 | assert!(lru.pop() == 1); 92 | } 93 | 94 | #[test] 95 | fn test_medium_lru() { 96 | let mut lru: Lru<10> = Lru::new(); 97 | lru.poke(0); 98 | for _ in 0..9 { 99 | assert!(lru.pop() != 0); 100 | } 101 | assert!(lru.pop() == 0); 102 | 103 | lru.poke(1); 104 | for _ in 0..9 { 105 | assert!(lru.pop() != 1); 106 | } 107 | assert!(lru.pop() == 1); 108 | 109 | lru.poke(4); 110 | lru.poke(5); 111 | for _ in 0..8 { 112 | assert!(!matches!(lru.pop(), 4 | 5)); 113 | } 114 | assert!(lru.pop() == 4); 115 | assert!(lru.pop() == 5); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /demos/viewer/src/shaders/geometry.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position: vec4, 3 | @location(0) tex_coords: vec2, 4 | } 5 | 6 | // Vertex shader to render a full-screen quad 7 | @vertex 8 | fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 9 | // Create a "full screen quad" from just the vertex_index 10 | // Maps vertex indices (0,1,2,3) to positions: 11 | // (-1,1)----(1,1) 12 | // | | 13 | // (-1,-1)---(1,-1) 14 | const POS = array, 6>( 15 | vec2(-1.0, -1.0), 16 | vec2(1.0, -1.0), 17 | vec2(-1.0, 1.0), 18 | vec2(-1.0, 1.0), 19 | vec2(1.0, -1.0), 20 | vec2(1.0, 1.0), 21 | ); 22 | 23 | // UV coordinates for the quad 24 | const UV = array, 6>( 25 | vec2(0.0, 1.0), 26 | vec2(1.0, 1.0), 27 | vec2(0.0, 0.0), 28 | vec2(0.0, 0.0), 29 | vec2(1.0, 1.0), 30 | vec2(1.0, 0.0), 31 | ); 32 | 33 | var output: VertexOutput; 34 | output.position = vec4(POS[vertex_index], 0.0, 1.0); 35 | output.tex_coords = UV[vertex_index]; 36 | return output; 37 | } 38 | 39 | struct GeometryPixel { 40 | depth: u32, 41 | normal: vec3, 42 | } 43 | 44 | struct Light { 45 | position: vec3, 46 | intensity: f32, 47 | } 48 | 49 | struct RenderConfig { 50 | render_mode: u32, // 0 = heightmap, 1 = shaded 51 | max_depth: u32, 52 | } 53 | 54 | @group(0) @binding(0) var t_geometry: texture_2d; 55 | @group(0) @binding(1) var s_geometry: sampler; 56 | @group(0) @binding(2) var config: RenderConfig; 57 | 58 | // Fragment shader for geometry 59 | @fragment 60 | fn fs_main(@location(0) tex_coords: vec2) -> @location(0) vec4 { 61 | let texel = textureLoad( 62 | t_geometry, 63 | vec2(tex_coords * vec2(textureDimensions(t_geometry))), 64 | 0); 65 | 66 | // First 32 bits are depth, next 32 bits are normal.x, etc. 67 | let depth = texel.x; 68 | 69 | // If depth is 0, this pixel is transparent 70 | if (depth == 0u) { 71 | discard; 72 | } 73 | 74 | // Extract normal (stored as bits in texel.yzw) 75 | let normal_x = bitcast(texel.y); 76 | let normal_y = bitcast(texel.z); 77 | let normal_z = bitcast(texel.w); 78 | let normal = vec3(normal_x, normal_y, normal_z); 79 | 80 | if (config.render_mode == 0u) { 81 | // Heightmap mode - use grayscale based on depth 82 | let gray = f32(depth) / f32(config.max_depth); 83 | return vec4(gray, gray, gray, 1.0); 84 | } else if (config.render_mode == 1u) { 85 | // RGB mode, using normals 86 | let norm_normal = normalize(normal); 87 | return vec4(abs(norm_normal), 1.0); 88 | } else { 89 | // Shaded mode 90 | let p = vec3( 91 | (tex_coords.xy - 0.5) * 2.0, 92 | 2.0 * (f32(depth) / f32(config.max_depth) - 0.5) 93 | ); 94 | let n = normalize(normal); 95 | const LIGHTS = array( 96 | Light(vec3(5.0, -5.0, 10.0), 0.5), 97 | Light(vec3(-5.0, 0.0, 10.0), 0.15), 98 | Light(vec3(0.0, -5.0, 10.0), 0.15) 99 | ); 100 | var accum: f32 = 0.2; 101 | for (var i = 0u; i < 3u; i = i + 1u) { 102 | let light = LIGHTS[i]; 103 | let light_dir = normalize(light.position - p); 104 | accum = accum + max(dot(light_dir, n), 0.0) * light.intensity; 105 | } 106 | accum = clamp(accum, 0.0, 1.0); 107 | return vec4(accum, accum, accum, 1.0); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /fidget-core/src/context/op.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | context::{Node, indexed::Index}, 3 | var::Var, 4 | }; 5 | use ordered_float::OrderedFloat; 6 | 7 | /// A one-argument math operation 8 | #[allow(missing_docs)] 9 | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] 10 | pub enum UnaryOpcode { 11 | Neg, 12 | Abs, 13 | Recip, 14 | Sqrt, 15 | Square, 16 | Floor, 17 | Ceil, 18 | Round, 19 | Sin, 20 | Cos, 21 | Tan, 22 | Asin, 23 | Acos, 24 | Atan, 25 | Exp, 26 | Ln, 27 | Not, 28 | } 29 | 30 | /// A two-argument math operation 31 | #[allow(missing_docs)] 32 | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] 33 | pub enum BinaryOpcode { 34 | Add, 35 | Sub, 36 | Mul, 37 | Div, 38 | Atan, 39 | Min, 40 | Max, 41 | Compare, 42 | Mod, 43 | And, 44 | Or, 45 | } 46 | 47 | /// An operation in a math expression 48 | /// 49 | /// `Op`s should be constructed by calling functions on 50 | /// [`Context`](crate::context::Context), e.g. 51 | /// [`Context::add`](crate::context::Context::add) will generate an 52 | /// `Op::Binary(BinaryOpcode::Add, .., ..)` node and return an opaque handle. 53 | /// 54 | /// Each `Op` is tightly coupled to the [`Context`](crate::context::Context) 55 | /// which generated it, and will not be valid for a different `Context`. 56 | #[allow(missing_docs)] 57 | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] 58 | pub enum Op { 59 | Input(Var), 60 | Const(OrderedFloat), 61 | Binary(BinaryOpcode, Node, Node), 62 | Unary(UnaryOpcode, Node), 63 | } 64 | 65 | fn dot_color_to_rgb(s: &str) -> &'static str { 66 | match s { 67 | "red" => "#FF0000", 68 | "green" => "#00FF00", 69 | "goldenrod" => "#DAA520", 70 | "dodgerblue" => "#1E90FF", 71 | s => panic!("Unknown X11 color '{s}'"), 72 | } 73 | } 74 | 75 | impl Op { 76 | /// Returns the color to be used in a GraphViz drawing for this node 77 | pub fn dot_node_color(&self) -> &str { 78 | match self { 79 | Op::Const(..) => "green", 80 | Op::Input(..) => "red", 81 | Op::Binary(BinaryOpcode::Min | BinaryOpcode::Max, ..) => { 82 | "dodgerblue" 83 | } 84 | Op::Binary(..) | Op::Unary(..) => "goldenrod", 85 | } 86 | } 87 | 88 | /// Returns the shape to be used in a GraphViz drawing for this node 89 | pub fn dot_node_shape(&self) -> &str { 90 | match self { 91 | Op::Const(..) => "oval", 92 | Op::Input(..) => "circle", 93 | Op::Binary(..) | Op::Unary(..) => "box", 94 | } 95 | } 96 | 97 | /// Iterates over children, producing 0, 1, or 2 values 98 | pub fn iter_children(&self) -> impl Iterator { 99 | let out = match self { 100 | Op::Binary(_, a, b) => [Some(*a), Some(*b)], 101 | Op::Unary(_, a) => [Some(*a), None], 102 | Op::Input(..) | Op::Const(..) => [None, None], 103 | }; 104 | out.into_iter().flatten() 105 | } 106 | 107 | /// Returns a GraphViz string of edges from this node to its children 108 | pub fn dot_edges(&self, i: Node) -> String { 109 | let mut out = String::new(); 110 | for c in self.iter_children() { 111 | out += &self.dot_edge(i, c, "FF"); 112 | } 113 | out 114 | } 115 | 116 | /// Returns a single edge with user-specified transparency 117 | pub fn dot_edge(&self, a: Node, b: Node, alpha: &str) -> String { 118 | let color = dot_color_to_rgb(self.dot_node_color()).to_owned() + alpha; 119 | format!("n{} -> n{} [color = \"{color}\"]\n", a.get(), b.get(),) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /fidget-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Core infrastructure for evaluating complex closed-form implicit surfaces. 2 | //! 3 | //! ``` 4 | //! use fidget_core::{ 5 | //! context::Context, 6 | //! shape::EzShape, 7 | //! vm::VmShape 8 | //! }; 9 | //! let mut ctx = Context::new(); 10 | //! let x = ctx.x(); 11 | //! let y = ctx.y(); 12 | //! let x_squared = ctx.mul(x, x)?; 13 | //! let y_squared = ctx.mul(y, y)?; 14 | //! let radius = ctx.add(x_squared, y_squared)?; 15 | //! let circle = ctx.sub(radius, 1.0)?; 16 | //! 17 | //! let shape = VmShape::new(&ctx, circle)?; 18 | //! let mut eval = VmShape::new_point_eval(); 19 | //! let tape = shape.ez_point_tape(); 20 | //! 21 | //! let (v, _trace) = eval.eval(&tape, 0.0, 0.0, 0.0)?; 22 | //! assert_eq!(v, -1.0); 23 | //! 24 | //! let (v, _trace) = eval.eval(&tape, 1.0, 0.0, 0.0)?; 25 | //! assert_eq!(v, 0.0); 26 | //! 27 | //! const N: usize = 15; 28 | //! for i in 0..N { 29 | //! for j in 0..N { 30 | //! let x = (i as f32 + 0.5) / (N as f32 / 2.0) - 1.0; 31 | //! let y = (j as f32 + 0.5) / (N as f32 / 2.0) - 1.0; 32 | //! let (v, _trace) = eval.eval(&tape, x, y, 0.0)?; 33 | //! print!("{}", if v < 0.0 { "##" } else { " " }); 34 | //! } 35 | //! println!(); 36 | //! } 37 | //! 38 | //! // This will print 39 | //! // ########## 40 | //! // ################## 41 | //! // ###################### 42 | //! // ########################## 43 | //! // ########################## 44 | //! // ############################## 45 | //! // ############################## 46 | //! // ############################## 47 | //! // ############################## 48 | //! // ############################## 49 | //! // ########################## 50 | //! // ########################## 51 | //! // ###################### 52 | //! // ################## 53 | //! // ########## 54 | //! # Ok::<(), fidget_core::Error>(()) 55 | //! ``` 56 | #![warn(missing_docs)] 57 | 58 | pub mod context; 59 | pub use context::Context; 60 | 61 | pub mod compiler; 62 | pub mod eval; 63 | pub mod render; 64 | pub mod shape; 65 | pub mod types; 66 | pub mod var; 67 | pub mod vm; 68 | 69 | mod error; 70 | pub use error::Error; 71 | 72 | #[cfg(test)] 73 | mod test { 74 | use crate::Error; 75 | use crate::context::*; 76 | use crate::var::Var; 77 | 78 | #[test] 79 | fn it_works() { 80 | let mut ctx = Context::new(); 81 | let x1 = ctx.x(); 82 | let x2 = ctx.x(); 83 | assert_eq!(x1, x2); 84 | 85 | let a = ctx.constant(1.0); 86 | let b = ctx.constant(1.0); 87 | assert_eq!(a, b); 88 | assert_eq!(ctx.get_const(a).unwrap(), 1.0); 89 | assert!(matches!(ctx.get_const(x1), Err(Error::NotAConst))); 90 | 91 | let c = ctx.add(a, b).unwrap(); 92 | assert_eq!(ctx.get_const(c).unwrap(), 2.0); 93 | 94 | let c = ctx.neg(c).unwrap(); 95 | assert_eq!(ctx.get_const(c).unwrap(), -2.0); 96 | } 97 | 98 | #[test] 99 | fn test_constant_folding() { 100 | let mut ctx = Context::new(); 101 | let a = ctx.constant(1.0); 102 | assert_eq!(ctx.len(), 1); 103 | let b = ctx.constant(-1.0); 104 | assert_eq!(ctx.len(), 2); 105 | let _ = ctx.add(a, b); 106 | assert_eq!(ctx.len(), 3); 107 | let _ = ctx.add(a, b); 108 | assert_eq!(ctx.len(), 3); 109 | let _ = ctx.mul(a, b); 110 | assert_eq!(ctx.len(), 3); 111 | } 112 | 113 | #[test] 114 | fn test_eval() { 115 | let mut ctx = Context::new(); 116 | let x = ctx.x(); 117 | let y = ctx.y(); 118 | let v = ctx.add(x, y).unwrap(); 119 | 120 | assert_eq!( 121 | ctx.eval(v, &[(Var::X, 1.0), (Var::Y, 2.0)].into_iter().collect()) 122 | .unwrap(), 123 | 3.0 124 | ); 125 | assert_eq!(ctx.eval_xyz(v, 2.0, 3.0, 0.0).unwrap(), 5.0); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /fidget/benches/render.rs: -------------------------------------------------------------------------------- 1 | use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; 2 | use fidget::render::{ImageSize, RenderHints, ThreadPool}; 3 | use std::hint::black_box; 4 | 5 | const PROSPERO: &str = include_str!("../../models/prospero.vm"); 6 | 7 | pub fn prospero_size_sweep(c: &mut Criterion) { 8 | let (ctx, root) = fidget::Context::from_text(PROSPERO.as_bytes()).unwrap(); 9 | let shape_vm = &fidget::vm::VmShape::new(&ctx, root).unwrap(); 10 | 11 | #[cfg(feature = "jit")] 12 | let shape_jit = &fidget::jit::JitShape::new(&ctx, root).unwrap(); 13 | 14 | let mut group = 15 | c.benchmark_group("speed vs image size (prospero, 2d) (8 threads)"); 16 | for size in [256, 512, 768, 1024, 1280, 1546, 1792, 2048] { 17 | let cfg = &fidget::raster::ImageRenderConfig { 18 | image_size: fidget::render::ImageSize::from(size), 19 | tile_sizes: fidget::vm::VmFunction::tile_sizes_2d(), 20 | ..Default::default() 21 | }; 22 | group.bench_function(BenchmarkId::new("vm", size), move |b| { 23 | b.iter(|| { 24 | let tape = shape_vm.clone(); 25 | black_box(cfg.run(tape)) 26 | }) 27 | }); 28 | 29 | #[cfg(feature = "jit")] 30 | { 31 | let cfg = &fidget::raster::ImageRenderConfig { 32 | image_size: fidget::render::ImageSize::from(size), 33 | tile_sizes: fidget::jit::JitFunction::tile_sizes_2d(), 34 | ..Default::default() 35 | }; 36 | group.bench_function(BenchmarkId::new("jit", size), move |b| { 37 | b.iter(|| { 38 | let tape = shape_jit.clone(); 39 | black_box(cfg.run(tape)) 40 | }) 41 | }); 42 | } 43 | } 44 | } 45 | 46 | pub fn prospero_thread_sweep(c: &mut Criterion) { 47 | let (ctx, root) = fidget::Context::from_text(PROSPERO.as_bytes()).unwrap(); 48 | let shape_vm = &fidget::vm::VmShape::new(&ctx, root).unwrap(); 49 | 50 | #[cfg(feature = "jit")] 51 | let shape_jit = &fidget::jit::JitShape::new(&ctx, root).unwrap(); 52 | 53 | let mut group = 54 | c.benchmark_group("speed vs threads (prospero, 2d) (1024 x 1024)"); 55 | let pools = [1, 2, 4, 8, 16].map(|i| { 56 | Some(ThreadPool::Custom( 57 | rayon::ThreadPoolBuilder::new() 58 | .num_threads(i) 59 | .build() 60 | .unwrap(), 61 | )) 62 | }); 63 | for threads in [None, Some(ThreadPool::Global)].into_iter().chain(pools) { 64 | let threads = threads.as_ref(); 65 | let name = match &threads { 66 | None => "-".to_string(), 67 | Some(ThreadPool::Custom(i)) => i.current_num_threads().to_string(), 68 | Some(ThreadPool::Global) => "N".to_string(), 69 | }; 70 | let cfg = &fidget::raster::ImageRenderConfig { 71 | image_size: ImageSize::from(1024), 72 | tile_sizes: fidget::vm::VmFunction::tile_sizes_2d(), 73 | threads, 74 | ..Default::default() 75 | }; 76 | group.bench_function(BenchmarkId::new("vm", &name), move |b| { 77 | b.iter(|| { 78 | let tape = shape_vm.clone(); 79 | black_box(cfg.run(tape)) 80 | }) 81 | }); 82 | #[cfg(feature = "jit")] 83 | { 84 | let cfg = &fidget::raster::ImageRenderConfig { 85 | image_size: ImageSize::from(1024), 86 | tile_sizes: fidget::jit::JitFunction::tile_sizes_2d(), 87 | threads, 88 | ..Default::default() 89 | }; 90 | group.bench_function(BenchmarkId::new("jit", &name), move |b| { 91 | b.iter(|| { 92 | let tape = shape_jit.clone(); 93 | black_box(cfg.run(tape)) 94 | }) 95 | }); 96 | } 97 | } 98 | } 99 | 100 | criterion_group!(benches, prospero_size_sweep, prospero_thread_sweep); 101 | criterion_main!(benches); 102 | -------------------------------------------------------------------------------- /demos/viewer/src/script.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossbeam_channel::{Receiver, Sender}; 3 | use fidget::{context::Tree, rhai::FromDynamic}; 4 | use log::debug; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | /// Receives scripts and executes them with Fidget 8 | pub(crate) fn rhai_script_thread( 9 | rx: Receiver, 10 | tx: Sender>, 11 | ) -> Result<()> { 12 | let mut engine = Engine::new(); 13 | 14 | loop { 15 | let script = rx.recv()?; 16 | debug!("rhai script thread received script"); 17 | let r = engine.run(&script).map_err(|e| e.to_string()); 18 | debug!("rhai script thread is sending result to render thread"); 19 | tx.send(r)?; 20 | } 21 | } 22 | 23 | /// Engine for evaluating a Rhai script with Fidget-specific bindings 24 | pub struct Engine { 25 | engine: rhai::Engine, 26 | context: Arc>, 27 | } 28 | 29 | impl Default for Engine { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | impl Engine { 36 | pub fn new() -> Self { 37 | let mut engine = fidget::rhai::engine(); 38 | 39 | engine.register_fn("draw", draw); 40 | engine.register_fn("draw_rgb", draw_rgb); 41 | 42 | let context = Arc::new(Mutex::new(ScriptContext::new())); 43 | engine.set_default_tag(rhai::Dynamic::from(context.clone())); 44 | engine.set_max_expr_depths(64, 32); 45 | 46 | Self { engine, context } 47 | } 48 | 49 | /// Executes a full script 50 | pub fn run( 51 | &mut self, 52 | script: &str, 53 | ) -> Result> { 54 | self.context.lock().unwrap().clear(); 55 | 56 | self.engine.run(script)?; 57 | 58 | // Steal the ScriptContext's contents 59 | let mut lock = self.context.lock().unwrap(); 60 | Ok(std::mem::take(&mut lock)) 61 | } 62 | } 63 | 64 | /// Shape to render 65 | /// 66 | /// Populated by calls to `draw(...)` or `draw_rgb(...)` in a Rhai script 67 | pub struct DrawShape { 68 | /// Tree to render 69 | pub tree: Tree, 70 | /// Color to use when drawing the shape 71 | pub color_rgb: [u8; 3], 72 | } 73 | 74 | /// Context for shape evaluation 75 | /// 76 | /// This object stores a set of shapes, which is populated by calls to `draw` or 77 | /// `draw_rgb` during script evaluation. 78 | pub struct ScriptContext { 79 | /// List of shapes populated since the last call to [`clear`](Self::clear) 80 | pub shapes: Vec, 81 | } 82 | 83 | impl Default for ScriptContext { 84 | fn default() -> Self { 85 | Self::new() 86 | } 87 | } 88 | 89 | impl ScriptContext { 90 | /// Builds a new empty script context 91 | pub fn new() -> Self { 92 | Self { shapes: vec![] } 93 | } 94 | /// Resets the script context 95 | pub fn clear(&mut self) { 96 | self.shapes.clear(); 97 | } 98 | } 99 | 100 | fn draw( 101 | ctx: rhai::NativeCallContext, 102 | tree: rhai::Dynamic, 103 | ) -> Result<(), Box> { 104 | let tree = Tree::from_dynamic(&ctx, tree, None)?; 105 | let ctx = ctx.tag().unwrap().clone_cast::>>(); 106 | ctx.lock().unwrap().shapes.push(DrawShape { 107 | tree, 108 | color_rgb: [u8::MAX; 3], 109 | }); 110 | Ok(()) 111 | } 112 | 113 | fn draw_rgb( 114 | ctx: rhai::NativeCallContext, 115 | tree: rhai::Dynamic, 116 | r: f64, 117 | g: f64, 118 | b: f64, 119 | ) -> Result<(), Box> { 120 | let tree = Tree::from_dynamic(&ctx, tree, None)?; 121 | let ctx = ctx.tag().unwrap().clone_cast::>>(); 122 | let f = |a| { 123 | if a < 0.0 { 124 | 0 125 | } else if a > 1.0 { 126 | 255 127 | } else { 128 | (a * 255.0) as u8 129 | } 130 | }; 131 | ctx.lock().unwrap().shapes.push(DrawShape { 132 | tree, 133 | color_rgb: [f(r), f(g), f(b)], 134 | }); 135 | Ok(()) 136 | } 137 | 138 | #[cfg(test)] 139 | mod test { 140 | use super::*; 141 | use fidget::context::{BinaryOpcode, Context, Op}; 142 | 143 | #[test] 144 | fn test_simple_script() { 145 | let mut engine = Engine::new(); 146 | let out = engine 147 | .run( 148 | " 149 | let s = circle(#{ center: [0, 0], radius: 2 }); 150 | draw(s.move([1, 3])); 151 | ", 152 | ) 153 | .unwrap(); 154 | assert_eq!(out.shapes.len(), 1); 155 | let mut ctx = Context::new(); 156 | let sum = ctx.import(&out.shapes[0].tree); 157 | assert_eq!(ctx.eval_xyz(sum, 1.0, 3.0, 0.0).unwrap(), -2.0); 158 | } 159 | 160 | #[test] 161 | fn test_gyroid_sphere() { 162 | let mut engine = Engine::new(); 163 | let s = include_str!("../../../models/gyroid-sphere.rhai"); 164 | let out = engine.run(s).unwrap(); 165 | assert_eq!(out.shapes.len(), 1); 166 | let mut ctx = Context::new(); 167 | let sphere = ctx.import(&out.shapes[0].tree); 168 | assert!(matches!( 169 | ctx.get_op(sphere).unwrap(), 170 | Op::Binary(BinaryOpcode::Max, _, _) 171 | )); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /fidget-core/src/context/indexed.rs: -------------------------------------------------------------------------------- 1 | //! Container types with strongly-typed indexes. 2 | use crate::Error; 3 | use std::collections::HashMap; 4 | 5 | /// Stores a set of `(V, I)` tuples, with lookup in both directions. 6 | /// 7 | /// Implemented using a `Vec` and a `HashMap`. 8 | /// 9 | /// The index type `I` should be a wrapper around a `usize` and be convertible 10 | /// in both directions using the `Index` trait; it is typically passed around 11 | /// using `Copy`. A suitable index type can be constructed with [`define_index`]. 12 | /// 13 | /// The `V` type may be larger and is passed around by reference. However, 14 | /// it must be `Clone`, because it is stored twice in the data structure (once 15 | /// in the `Vec` and once in the `HashMap`). 16 | #[derive(Clone, Debug)] 17 | pub(crate) struct IndexMap { 18 | data: Vec, 19 | map: HashMap, 20 | } 21 | 22 | impl Default for IndexMap { 23 | fn default() -> Self { 24 | Self { 25 | data: vec![], 26 | map: HashMap::new(), 27 | } 28 | } 29 | } 30 | 31 | pub(crate) trait Index { 32 | fn new(i: usize) -> Self; 33 | fn get(&self) -> usize; 34 | } 35 | 36 | impl IndexMap 37 | where 38 | V: Eq + std::hash::Hash + Clone, 39 | I: Eq + std::hash::Hash + Copy + Index, 40 | { 41 | pub fn clear(&mut self) { 42 | self.data.clear(); 43 | self.map.clear(); 44 | } 45 | 46 | pub fn len(&self) -> usize { 47 | self.data.len() 48 | } 49 | pub fn is_empty(&self) -> bool { 50 | self.data.is_empty() 51 | } 52 | pub fn get_by_index(&self, i: I) -> Option<&V> { 53 | self.data.get(i.get()) 54 | } 55 | /// Insert the given value into the map, returning a handle. 56 | /// 57 | /// If the value is already in the map, the handle will be to the existing 58 | /// instance (so it will not be inserted twice). 59 | pub fn insert(&mut self, v: V) -> I { 60 | *self.map.entry(v.clone()).or_insert_with(|| { 61 | let out = I::new(self.data.len()); 62 | self.data.push(v); 63 | out 64 | }) 65 | } 66 | 67 | /// Removes the last value stored in the container. 68 | /// 69 | /// This is _usually_ the most recently inserted value, except when 70 | /// `insert` is called on a duplicate. 71 | pub fn pop(&mut self) -> Result { 72 | match self.data.pop() { 73 | Some(v) => { 74 | self.map.remove(&v); 75 | Ok(v) 76 | } 77 | None => Err(Error::EmptyMap), 78 | } 79 | } 80 | pub fn keys(&self) -> impl Iterator { 81 | (0..self.data.len()).map(I::new) 82 | } 83 | } 84 | 85 | //////////////////////////////////////////////////////////////////////////////// 86 | 87 | /// A `Vec` with strongly-typed indexes, used to improve the type-safety 88 | /// of data storage. 89 | /// 90 | /// The `Index` type should be a wrapper around a `usize` and be convertible 91 | /// in both directions; it is typically passed around using `Copy`. A suitable 92 | /// index type can be constructed with [`define_index`]. 93 | #[derive(Clone, Debug)] 94 | pub struct IndexVec { 95 | data: Vec, 96 | _phantom: std::marker::PhantomData<*const I>, 97 | } 98 | 99 | impl Default for IndexVec { 100 | fn default() -> Self { 101 | Self { 102 | data: vec![], 103 | _phantom: std::marker::PhantomData, 104 | } 105 | } 106 | } 107 | 108 | impl std::iter::IntoIterator for IndexVec { 109 | type Item = V; 110 | type IntoIter = std::vec::IntoIter; 111 | fn into_iter(self) -> Self::IntoIter { 112 | self.data.into_iter() 113 | } 114 | } 115 | 116 | impl FromIterator for IndexVec { 117 | fn from_iter>(iter: T) -> Self { 118 | Vec::from_iter(iter).into() 119 | } 120 | } 121 | 122 | impl std::ops::Index for IndexVec 123 | where 124 | I: Index, 125 | { 126 | type Output = V; 127 | fn index(&self, i: I) -> &V { 128 | &self.data[i.get()] 129 | } 130 | } 131 | 132 | impl std::ops::IndexMut for IndexVec 133 | where 134 | I: Index, 135 | { 136 | fn index_mut(&mut self, i: I) -> &mut V { 137 | &mut self.data[i.get()] 138 | } 139 | } 140 | 141 | impl From> for IndexVec { 142 | fn from(data: Vec) -> Self { 143 | Self { 144 | data, 145 | _phantom: std::marker::PhantomData, 146 | } 147 | } 148 | } 149 | 150 | impl IndexVec { 151 | pub fn len(&self) -> usize { 152 | self.data.len() 153 | } 154 | } 155 | 156 | //////////////////////////////////////////////////////////////////////////////// 157 | 158 | /// Defines an index type suitable for use in an [`IndexMap`] or [`IndexVec`]. 159 | macro_rules! define_index { 160 | ($name:ident, $doc:literal) => { 161 | #[doc = $doc] 162 | #[derive( 163 | Copy, Clone, Default, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, 164 | )] 165 | pub struct $name(usize); 166 | impl crate::context::indexed::Index for $name { 167 | fn new(i: usize) -> Self { 168 | Self(i) 169 | } 170 | fn get(&self) -> usize { 171 | self.0 172 | } 173 | } 174 | }; 175 | } 176 | pub(crate) use define_index; 177 | -------------------------------------------------------------------------------- /workspace-hack/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is generated by `cargo hakari`. 2 | # To regenerate, run: 3 | # cargo hakari generate 4 | 5 | [package] 6 | name = "workspace-hack" 7 | version = "0.1.0" 8 | description = "workspace-hack package, managed by hakari" 9 | # You can choose to publish this crate: see https://docs.rs/cargo-hakari/latest/cargo_hakari/publishing. 10 | publish = false 11 | edition = "2024" 12 | 13 | # The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments 14 | # are managed by hakari. 15 | 16 | ### BEGIN HAKARI SECTION 17 | [dependencies] 18 | ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "no-rng", "runtime-rng", "std"] } 19 | approx = { version = "0.5" } 20 | bytemuck = { version = "1", default-features = false, features = ["derive", "extern_crate_alloc"] } 21 | clap = { version = "4", features = ["derive"] } 22 | clap_builder = { version = "4", default-features = false, features = ["color", "help", "std", "suggestions", "usage"] } 23 | either = { version = "1", default-features = false, features = ["use_std"] } 24 | getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } 25 | num-traits = { version = "0.2", features = ["i128"] } 26 | once_cell = { version = "1", default-features = false, features = ["portable-atomic", "std"] } 27 | regex = { version = "1", default-features = false, features = ["perf", "std"] } 28 | regex-automata = { version = "0.4", default-features = false, features = ["dfa-onepass", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "std"] } 29 | serde = { version = "1", features = ["alloc", "derive", "rc"] } 30 | serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } 31 | zerocopy = { version = "0.8", default-features = false, features = ["derive", "simd"] } 32 | 33 | [build-dependencies] 34 | once_cell = { version = "1", default-features = false, features = ["portable-atomic", "std"] } 35 | proc-macro2 = { version = "1", features = ["span-locations"] } 36 | quote = { version = "1" } 37 | syn = { version = "2", features = ["extra-traits", "full"] } 38 | 39 | [target.x86_64-unknown-linux-gnu.dependencies] 40 | ahash = { version = "0.8" } 41 | anstream = { version = "0.6" } 42 | bitflags = { version = "2", default-features = false, features = ["serde", "std"] } 43 | bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "min_const_generics"] } 44 | getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } 45 | libc = { version = "0.2", features = ["extra_traits"] } 46 | log = { version = "0.4", default-features = false, features = ["std"] } 47 | memchr = { version = "2" } 48 | once_cell = { version = "1" } 49 | 50 | [target.x86_64-unknown-linux-gnu.build-dependencies] 51 | bitflags = { version = "2", default-features = false, features = ["serde", "std"] } 52 | getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } 53 | libc = { version = "0.2", features = ["extra_traits"] } 54 | log = { version = "0.4", default-features = false, features = ["std"] } 55 | memchr = { version = "2" } 56 | once_cell = { version = "1" } 57 | serde = { version = "1", features = ["alloc", "derive", "rc"] } 58 | serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } 59 | syn = { version = "2", default-features = false, features = ["fold", "visit", "visit-mut"] } 60 | 61 | [target.aarch64-apple-darwin.dependencies] 62 | anstream = { version = "0.6" } 63 | bitflags = { version = "2", default-features = false, features = ["serde", "std"] } 64 | half = { version = "2" } 65 | image = { version = "0.25", default-features = false, features = ["png", "tiff"] } 66 | libc = { version = "0.2" } 67 | once_cell = { version = "1" } 68 | 69 | [target.aarch64-apple-darwin.build-dependencies] 70 | bitflags = { version = "2", default-features = false, features = ["serde", "std"] } 71 | libc = { version = "0.2" } 72 | once_cell = { version = "1" } 73 | serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } 74 | syn = { version = "2", default-features = false, features = ["fold", "visit"] } 75 | 76 | [target.aarch64-unknown-linux-gnu.dependencies] 77 | ahash = { version = "0.8" } 78 | anstream = { version = "0.6" } 79 | bitflags = { version = "2", default-features = false, features = ["serde", "std"] } 80 | bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "min_const_generics"] } 81 | getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } 82 | libc = { version = "0.2", features = ["extra_traits"] } 83 | log = { version = "0.4", default-features = false, features = ["std"] } 84 | memchr = { version = "2" } 85 | once_cell = { version = "1" } 86 | 87 | [target.aarch64-unknown-linux-gnu.build-dependencies] 88 | bitflags = { version = "2", default-features = false, features = ["serde", "std"] } 89 | getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } 90 | libc = { version = "0.2", features = ["extra_traits"] } 91 | log = { version = "0.4", default-features = false, features = ["std"] } 92 | memchr = { version = "2" } 93 | once_cell = { version = "1" } 94 | serde = { version = "1", features = ["alloc", "derive", "rc"] } 95 | serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } 96 | syn = { version = "2", default-features = false, features = ["fold", "visit", "visit-mut"] } 97 | 98 | ### END HAKARI SECTION 99 | -------------------------------------------------------------------------------- /fidget-core/src/eval/test/symbolic_deriv.rs: -------------------------------------------------------------------------------- 1 | use super::{CanonicalBinaryOp, CanonicalUnaryOp, test_args}; 2 | use crate::{ 3 | context::Context, 4 | eval::{BulkEvaluator, Function, MathFunction}, 5 | types::Grad, 6 | var::Var, 7 | vm::VmFunction, 8 | }; 9 | 10 | /// Helper struct to test symbolic differentiation 11 | pub struct TestSymbolicDerivs; 12 | 13 | impl TestSymbolicDerivs { 14 | pub fn test_unary() { 15 | let args = test_args(); 16 | 17 | let mut ctx = Context::new(); 18 | let v = ctx.var(Var::new()); 19 | let node = C::build(&mut ctx, v); 20 | let shape = VmFunction::new(&ctx, &[node]).unwrap(); 21 | let tape = shape.grad_slice_tape(Default::default()); 22 | let mut eval = VmFunction::new_grad_slice_eval(); 23 | 24 | let node_deriv = ctx.deriv(node, ctx.get_var(v).unwrap()).unwrap(); 25 | let shape_deriv = VmFunction::new(&ctx, &[node_deriv]).unwrap(); 26 | let tape_deriv = shape_deriv.float_slice_tape(Default::default()); 27 | let mut eval_deriv = VmFunction::new_float_slice_eval(); 28 | 29 | let args_g = args 30 | .iter() 31 | .map(|&v| Grad::new(v, 1.0, 0.0, 0.0)) 32 | .collect::>(); 33 | let out = eval.eval(&tape, &[args_g.as_slice()]).unwrap(); 34 | 35 | // Check symbolic differentiation results 36 | let out_deriv = 37 | eval_deriv.eval(&tape_deriv, &[args.as_slice()]).unwrap(); 38 | for (v, (a, b)) in args.iter().zip(out[0].iter().zip(&out_deriv[0])) { 39 | let a = a.dx; 40 | let err = a - b; 41 | let err_frac = err / a.abs().max(b.abs()); 42 | assert!( 43 | a == *b 44 | || err < 1e-6 45 | || err_frac < 1e-6 46 | || (a.is_nan() && b.is_nan()) 47 | || v.is_nan(), 48 | "mismatch in '{}' at {v}: {a} != {b} ({err})", 49 | C::NAME, 50 | ); 51 | } 52 | } 53 | 54 | pub fn test_binary() { 55 | let args = test_args(); 56 | 57 | let mut ctx = Context::new(); 58 | let va = Var::new(); 59 | let vb = Var::new(); 60 | let a = ctx.var(va); 61 | let b = ctx.var(vb); 62 | 63 | let mut eval = VmFunction::new_grad_slice_eval(); 64 | let mut eval_deriv = VmFunction::new_float_slice_eval(); 65 | 66 | let node = C::build(&mut ctx, a, b); 67 | let shape = VmFunction::new(&ctx, &[node]).unwrap(); 68 | let tape = shape.grad_slice_tape(Default::default()); 69 | 70 | let node_a_deriv = ctx.deriv(node, va).unwrap(); 71 | let shape_a_deriv = VmFunction::new(&ctx, &[node_a_deriv]).unwrap(); 72 | let tape_a_deriv = shape_a_deriv.float_slice_tape(Default::default()); 73 | 74 | let node_b_deriv = ctx.deriv(node, vb).unwrap(); 75 | let shape_b_deriv = VmFunction::new(&ctx, &[node_b_deriv]).unwrap(); 76 | let tape_b_deriv = shape_b_deriv.float_slice_tape(Default::default()); 77 | 78 | for rot in 0..args.len() { 79 | let mut rgsa = args.clone(); 80 | rgsa.rotate_left(rot); 81 | 82 | let args_g = args 83 | .iter() 84 | .map(|v| Grad::new(*v, 1.0, 0.0, 0.0)) 85 | .collect::>(); 86 | let rgsa_g = rgsa 87 | .iter() 88 | .map(|v| Grad::new(*v, 0.0, 1.0, 0.0)) 89 | .collect::>(); 90 | 91 | let ia = shape.vars().get(&va).unwrap(); 92 | let ib = shape.vars().get(&vb).unwrap(); 93 | let mut vs = [[].as_slice(), [].as_slice()]; 94 | vs[ia] = args_g.as_slice(); 95 | vs[ib] = rgsa_g.as_slice(); 96 | let out = eval.eval(&tape, &vs).unwrap(); 97 | 98 | // Check symbolic differentiation results 99 | let mut vs = [args.as_slice(), args.as_slice()]; 100 | if let Some(ia) = shape_a_deriv.vars().get(&va) { 101 | vs[ia] = args.as_slice(); 102 | } 103 | if let Some(ib) = shape_a_deriv.vars().get(&vb) { 104 | vs[ib] = rgsa.as_slice(); 105 | } 106 | let out_a_deriv = 107 | eval_deriv.eval(&tape_a_deriv, &vs).unwrap()[0].to_vec(); 108 | 109 | let mut vs = [args.as_slice(), args.as_slice()]; 110 | if let Some(ia) = shape_b_deriv.vars().get(&va) { 111 | vs[ia] = args.as_slice(); 112 | } 113 | if let Some(ib) = shape_b_deriv.vars().get(&vb) { 114 | vs[ib] = rgsa.as_slice(); 115 | } 116 | let out_b_deriv = &eval_deriv.eval(&tape_b_deriv, &vs).unwrap()[0]; 117 | 118 | for i in 0..out[0].len() { 119 | let v = out[0][i]; 120 | let da = out_a_deriv[i]; 121 | 122 | let a = args[i]; 123 | let b = rgsa[i]; 124 | 125 | let err = v.dx - da; 126 | let err_frac = err / da.abs().max(v.dx.abs()); 127 | assert!( 128 | v.dx == da 129 | || err < 1e-6 130 | || err_frac < 1e-6 131 | || (v.dx.is_nan() && da.is_nan()) 132 | || v.v.is_nan(), 133 | "mismatch in 'd {}(a, b) / da' at ({a}, {b}): \ 134 | {} != {da} ({err})", 135 | C::NAME, 136 | v.dx 137 | ); 138 | 139 | let db = out_b_deriv[i]; 140 | let err = v.dy - db; 141 | let err_frac = err / db.abs().max(v.dy.abs()); 142 | assert!( 143 | v.dy == db 144 | || err < 1e-6 145 | || err_frac < 1e-6 146 | || (v.dy.is_nan() && db.is_nan()) 147 | || v.v.is_nan(), 148 | "mismatch in 'd {}(a, b) / db' at ({a}, {b}): \ 149 | {} != {db} ({err})", 150 | C::NAME, 151 | v.dx 152 | ); 153 | } 154 | } 155 | } 156 | } 157 | 158 | crate::all_unary_tests!(TestSymbolicDerivs); 159 | crate::all_binary_tests!(TestSymbolicDerivs); 160 | -------------------------------------------------------------------------------- /fidget-mesh/src/qef.rs: -------------------------------------------------------------------------------- 1 | use super::cell::CellVertex; 2 | 3 | /// Solver for a quadratic error function to position a vertex within a cell 4 | #[derive(Copy, Clone, Debug, Default)] 5 | pub struct QuadraticErrorSolver { 6 | /// A^T A term 7 | ata: nalgebra::Matrix3, 8 | 9 | /// A^T B term 10 | atb: nalgebra::Vector3, 11 | 12 | /// B^T B term 13 | btb: f32, 14 | 15 | /// Mass point of intersections is stored as XYZ / W, so that summing works 16 | mass_point: nalgebra::Vector4, 17 | } 18 | 19 | impl std::ops::AddAssign for QuadraticErrorSolver { 20 | fn add_assign(&mut self, rhs: Self) { 21 | self.ata += rhs.ata; 22 | self.atb += rhs.atb; 23 | self.btb += rhs.btb; 24 | self.mass_point += rhs.mass_point; 25 | } 26 | } 27 | 28 | impl QuadraticErrorSolver { 29 | pub fn new() -> Self { 30 | Self { 31 | ata: nalgebra::Matrix3::zeros(), 32 | atb: nalgebra::Vector3::zeros(), 33 | btb: 0.0, 34 | mass_point: nalgebra::Vector4::zeros(), 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | pub fn mass_point(&self) -> nalgebra::Vector4 { 40 | self.mass_point 41 | } 42 | 43 | /// Adds a new intersection to the QEF 44 | /// 45 | /// `pos` is the position of the intersection and is accumulated in the mass 46 | /// point. `grad` is the gradient at the surface, and is normalized in this 47 | /// function. 48 | pub fn add_intersection( 49 | &mut self, 50 | pos: nalgebra::Vector3, 51 | grad: nalgebra::Vector4, 52 | ) { 53 | // TODO: correct for non-zero distance value in grad.w 54 | self.mass_point += nalgebra::Vector4::new(pos.x, pos.y, pos.z, 1.0); 55 | let norm = grad.xyz().normalize(); 56 | self.ata += norm * norm.transpose(); 57 | self.atb += norm * norm.dot(&pos); 58 | self.btb += norm.dot(&pos).powi(2); 59 | } 60 | 61 | /// Solve the given QEF, minimizing towards the mass point 62 | /// 63 | /// Returns a vertex localized within the given cell, and adjusts the solver 64 | /// to increase the likelihood that the vertex is bounded in the cell. 65 | /// 66 | /// Also returns the QEF error as the second item in the tuple 67 | pub fn solve(&self) -> (CellVertex<3>, f32) { 68 | // This gets a little tricky; see 69 | // https://www.mattkeeter.com/projects/qef for a walkthrough of QEF math 70 | // and references to primary sources. 71 | let center = self.mass_point.xyz() / self.mass_point.w; 72 | let atb = self.atb - self.ata * center; 73 | 74 | let svd = nalgebra::linalg::SVD::new(self.ata, true, true); 75 | 76 | // nalgebra doesn't always actually order singular values (?!?) 77 | // https://github.com/dimforge/nalgebra/issues/1215 78 | let mut singular_values = 79 | svd.singular_values.data.0[0].map(ordered_float::OrderedFloat); 80 | singular_values.sort(); 81 | singular_values.reverse(); 82 | let singular_values = singular_values.map(|o| o.0); 83 | 84 | // Skip any eigenvalues that are small relative to the maximum 85 | // eigenvalue. This is very much a tuned value (alas!). If the value 86 | // is too small, then we incorrectly pick high-rank solutions, which may 87 | // shoot vertices out of their cells in near-planar situations. If the 88 | // value is too large, then we incorrectly pick low-rank solutions, 89 | // which makes us less likely to snap to sharp features. 90 | // 91 | // For example, our cone test needs to use a rank-3 solver for 92 | // eigenvalues of [1.5633028, 1.430821, 0.0058764853] (a dynamic range 93 | // of 2e3); while the bear model needs to use a rank-2 solver for 94 | // eigenvalues of [2.87, 0.13, 5.64e-7] (a dynamic range of 10^7). We 95 | // pick 10^3 here somewhat arbitrarily to be within that range. 96 | const EIGENVALUE_CUTOFF_RELATIVE: f32 = 1e-3; 97 | let cutoff = singular_values[0].abs() * EIGENVALUE_CUTOFF_RELATIVE; 98 | 99 | // Intuition about `rank`: 100 | // 0 => all eigenvalues are invalid (?!), use the center point 101 | // 1 => the first eigenvalue is valid, this must be planar 102 | // 2 => the first two eigenvalues are valid, this is a planar or an edge 103 | // 3 => all eigenvalues are valid, this is a planar, edge, or corner 104 | let rank = (0..3) 105 | .find(|i| singular_values[*i].abs() < cutoff) 106 | .unwrap_or(3); 107 | 108 | let epsilon = singular_values.get(rank).cloned().unwrap_or(0.0); 109 | let sol = svd.solve(&atb, epsilon); 110 | let pos = sol.map(|c| c + center).unwrap_or(center); 111 | // We'll clamp the error to a small > 0 value for ease of comparison 112 | let err = ((pos.transpose() * self.ata * pos 113 | - 2.0 * pos.transpose() * self.atb)[0] 114 | + self.btb) 115 | .max(1e-6); 116 | 117 | (CellVertex { pos }, err) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod test { 123 | use super::*; 124 | use nalgebra::{Vector3, Vector4}; 125 | 126 | #[test] 127 | fn qef_rank2() { 128 | let mut q = QuadraticErrorSolver::new(); 129 | q.add_intersection( 130 | Vector3::new(-0.5, -0.75, -0.75), 131 | Vector4::new(0.24, 0.12, 0.0, 0.0), 132 | ); 133 | q.add_intersection( 134 | Vector3::new(-0.75, -1.0, -0.6), 135 | Vector4::new(0.0, 0.0, 0.31, 0.0), 136 | ); 137 | q.add_intersection( 138 | Vector3::new(-0.50, -1.0, -0.6), 139 | Vector4::new(0.0, 0.0, 0.31, 0.0), 140 | ); 141 | let (_out, err) = q.solve(); 142 | assert_eq!(err, 1e-6); 143 | } 144 | 145 | #[test] 146 | fn qef_near_planar() { 147 | let mut q = QuadraticErrorSolver::new(); 148 | q.add_intersection( 149 | Vector3::new(-0.5, -0.25, 0.4999981), 150 | Vector4::new(-0.66666776, -0.33333388, 0.66666526, -1.2516975e-6), 151 | ); 152 | q.add_intersection( 153 | Vector3::new(-0.5, -0.25, 0.50), 154 | Vector4::new(-0.6666667, -0.33333334, 0.6666667, 0.0), 155 | ); 156 | q.add_intersection( 157 | Vector3::new(-0.5, -0.25, 0.50), 158 | Vector4::new(-0.6666667, -0.33333334, 0.6666667, 0.0), 159 | ); 160 | let (out, err) = q.solve(); 161 | assert_eq!(err, 1e-6); 162 | let expected = Vector3::new(-0.5, -0.25, 0.5); 163 | assert!( 164 | (out.pos - expected).norm() < 1e-3, 165 | "expected {expected:?}, got {:?}", 166 | out.pos 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /fidget-core/src/var/mod.rs: -------------------------------------------------------------------------------- 1 | //! Input variables to math expressions 2 | //! 3 | //! A [`Var`] maintains a persistent identity from 4 | //! [`Tree`](crate::context::Tree) to [`Context`](crate::context::Node) (where 5 | //! it is wrapped in a [`Op::Input`](crate::context::Op::Input)) to evaluation 6 | //! (where [`Tape::vars`](crate::eval::Tape::vars) maps from `Var` to index in 7 | //! the argument list). 8 | use crate::Error; 9 | use crate::context::{Context, IntoNode, Node}; 10 | use serde::{Deserialize, Serialize}; 11 | use std::collections::HashMap; 12 | 13 | /// The [`Var`] type is an input to a math expression 14 | /// 15 | /// We pre-define common variables (e.g. `X`, `Y`, `Z`) but also allow for fully 16 | /// customized values (using [`Var::V`]). 17 | /// 18 | /// Variables are "global", in that every instance of `Var::X` represents the 19 | /// same thing. To generate a "local" variable, [`Var::new`] picks a random 20 | /// 64-bit value, which is very unlikely to collide with anything else. 21 | #[derive( 22 | Copy, 23 | Clone, 24 | Debug, 25 | Hash, 26 | Eq, 27 | PartialEq, 28 | Ord, 29 | PartialOrd, 30 | Serialize, 31 | Deserialize, 32 | )] 33 | pub enum Var { 34 | /// Variable representing the X axis for 2D / 3D shapes 35 | X, 36 | /// Variable representing the Y axis for 2D / 3D shapes 37 | Y, 38 | /// Variable representing the Z axis for 3D shapes 39 | Z, 40 | /// Generic variable 41 | V(VarIndex), 42 | } 43 | 44 | /// Type for a variable index (implemented as a `u64`), used in [`Var::V`] 45 | #[derive( 46 | Copy, 47 | Clone, 48 | Debug, 49 | Hash, 50 | Eq, 51 | PartialEq, 52 | Ord, 53 | PartialOrd, 54 | Serialize, 55 | Deserialize, 56 | )] 57 | #[serde(transparent)] 58 | pub struct VarIndex(u64); 59 | 60 | impl Var { 61 | /// Returns a new variable, with a random 64-bit index 62 | /// 63 | /// The odds of collision with any previous variable are infintesimally 64 | /// small; if you are generating billions of random variables, something 65 | /// else in the system is likely to break before collisions become an issue. 66 | #[allow(clippy::new_without_default)] 67 | pub fn new() -> Self { 68 | let v: u64 = rand::random(); 69 | Var::V(VarIndex(v)) 70 | } 71 | 72 | /// Returns the [`VarIndex`] from a [`Var::V`] instance, or `None` 73 | pub fn index(&self) -> Option { 74 | if let Var::V(i) = *self { Some(i) } else { None } 75 | } 76 | } 77 | 78 | impl std::fmt::Display for Var { 79 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 80 | match self { 81 | Var::X => write!(f, "X"), 82 | Var::Y => write!(f, "Y"), 83 | Var::Z => write!(f, "Z"), 84 | Var::V(VarIndex(v)) if *v < 256 => write!(f, "v_{v}"), 85 | Var::V(VarIndex(v)) => write!(f, "V({v:x})"), 86 | } 87 | } 88 | } 89 | 90 | impl IntoNode for Var { 91 | fn into_node(self, ctx: &mut Context) -> Result { 92 | Ok(ctx.var(self)) 93 | } 94 | } 95 | 96 | /// Map from [`Var`] to a particular index 97 | /// 98 | /// Variable indexes are automatically assigned the first time 99 | /// [`VarMap::insert`] is called on that variable. 100 | /// 101 | /// Indexes are guaranteed to be tightly packed, i.e. a map `vars` will contains 102 | /// values from `0..vars.len()`. 103 | /// 104 | /// For efficiency, this type does not allocate heap memory for `Var::X/Y/Z`. 105 | #[derive(Default, Serialize, Deserialize)] 106 | pub struct VarMap { 107 | x: Option, 108 | y: Option, 109 | z: Option, 110 | v: HashMap, 111 | } 112 | 113 | #[allow(missing_docs)] 114 | impl VarMap { 115 | pub fn new() -> Self { 116 | Self::default() 117 | } 118 | pub fn len(&self) -> usize { 119 | self.x.is_some() as usize 120 | + self.y.is_some() as usize 121 | + self.z.is_some() as usize 122 | + self.v.len() 123 | } 124 | pub fn is_empty(&self) -> bool { 125 | self.x.is_none() 126 | && self.y.is_none() 127 | && self.z.is_none() 128 | && self.v.is_empty() 129 | } 130 | pub fn get(&self, v: &Var) -> Option { 131 | match v { 132 | Var::X => self.x, 133 | Var::Y => self.y, 134 | Var::Z => self.z, 135 | Var::V(v) => self.v.get(v).cloned(), 136 | } 137 | } 138 | /// Inserts a variable if not already present in the map 139 | /// 140 | /// The index is automatically assigned. 141 | pub fn insert(&mut self, v: Var) { 142 | let next = self.len(); 143 | match v { 144 | Var::X => self.x.get_or_insert(next), 145 | Var::Y => self.y.get_or_insert(next), 146 | Var::Z => self.z.get_or_insert(next), 147 | Var::V(v) => self.v.entry(v).or_insert(next), 148 | }; 149 | } 150 | 151 | /// Checks whether tracing arguments are valid 152 | pub fn check_tracing_arguments(&self, vars: &[T]) -> Result<(), Error> { 153 | if vars.len() < self.len() { 154 | // It's okay to be passed extra vars, because expressions may have 155 | // been simplified. 156 | Err(Error::BadVarSlice(vars.len(), self.len())) 157 | } else { 158 | Ok(()) 159 | } 160 | } 161 | 162 | /// Check whether bulk arguments are valid 163 | pub fn check_bulk_arguments>( 164 | &self, 165 | vars: &[V], 166 | ) -> Result<(), Error> { 167 | // It's fine if the caller has given us extra variables (e.g. due to 168 | // tape simplification), but it must have given us enough. 169 | if vars.len() < self.len() { 170 | Err(Error::BadVarSlice(vars.len(), self.len())) 171 | } else { 172 | let Some(n) = vars.first().map(|v| v.len()) else { 173 | return Ok(()); 174 | }; 175 | if vars.iter().any(|v| v.len() == n) { 176 | Ok(()) 177 | } else { 178 | Err(Error::MismatchedSlices) 179 | } 180 | } 181 | } 182 | } 183 | 184 | impl std::ops::Index<&Var> for VarMap { 185 | type Output = usize; 186 | fn index(&self, v: &Var) -> &Self::Output { 187 | match v { 188 | Var::X => self.x.as_ref().unwrap(), 189 | Var::Y => self.y.as_ref().unwrap(), 190 | Var::Z => self.z.as_ref().unwrap(), 191 | Var::V(v) => &self.v[v], 192 | } 193 | } 194 | } 195 | 196 | #[cfg(test)] 197 | mod test { 198 | use super::*; 199 | 200 | #[test] 201 | fn var_identity() { 202 | let v1 = Var::new(); 203 | let v2 = Var::new(); 204 | assert_ne!(v1, v2); 205 | } 206 | 207 | #[test] 208 | fn var_map() { 209 | let v = Var::new(); 210 | let mut m = VarMap::new(); 211 | assert!(m.get(&v).is_none()); 212 | m.insert(v); 213 | assert_eq!(m.get(&v), Some(0)); 214 | m.insert(v); 215 | assert_eq!(m.get(&v), Some(0)); 216 | 217 | let u = Var::new(); 218 | assert!(m.get(&u).is_none()); 219 | m.insert(u); 220 | assert_eq!(m.get(&u), Some(1)); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /fidget-mesh/src/cell.rs: -------------------------------------------------------------------------------- 1 | //! Data types used in the octree 2 | use fidget_core::types::Interval; 3 | 4 | use super::{ 5 | codegen::CELL_TO_EDGE_TO_VERT, 6 | types::{Axis, CellMask, Corner, Edge, Intersection}, 7 | }; 8 | 9 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 10 | pub enum Cell { 11 | Invalid, 12 | Empty, 13 | Full, 14 | Branch { 15 | /// Index of the next cell in 16 | /// [`Octree::cells`](super::octree::Octree::cells) 17 | index: usize, 18 | }, 19 | Leaf(Leaf), 20 | } 21 | 22 | impl Cell { 23 | /// Checks whether the given corner is empty (`false`) or full (`true`) 24 | /// 25 | /// # Panics 26 | /// If the cell is a branch or invalid 27 | pub fn corner(self, c: Corner) -> bool { 28 | match self { 29 | Cell::Leaf(Leaf { mask, .. }) => mask & c, 30 | Cell::Empty => false, 31 | Cell::Full => true, 32 | Cell::Branch { .. } | Cell::Invalid => panic!(), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 38 | pub struct Leaf { 39 | /// Mask of corner occupancy 40 | pub mask: CellMask, 41 | 42 | /// Index of first vertex in [`Octree::verts`](super::octree::Octree::verts) 43 | pub index: usize, 44 | } 45 | 46 | impl Leaf { 47 | /// Returns the edge intersection for the given edge (if present) 48 | pub fn edge(&self, e: Edge) -> Option { 49 | CELL_TO_EDGE_TO_VERT[self.mask.index()][e.index()] 50 | } 51 | } 52 | 53 | #[derive(Copy, Clone, Debug)] 54 | pub struct CellVertex { 55 | /// Position of this vertex 56 | pub pos: nalgebra::OVector>, 57 | } 58 | 59 | impl Default for CellVertex { 60 | fn default() -> Self { 61 | Self { 62 | pos: nalgebra::OVector::>::from_element( 63 | f32::NAN, 64 | ), 65 | } 66 | } 67 | } 68 | 69 | impl std::ops::Index> for CellVertex { 70 | type Output = f32; 71 | 72 | fn index(&self, axis: Axis) -> &Self::Output { 73 | &self.pos[axis.index()] 74 | } 75 | } 76 | 77 | //////////////////////////////////////////////////////////////////////////////// 78 | 79 | /// Cell index used during iteration 80 | /// 81 | /// Instead of storing the cell bounds in the leaf itself, we build them when 82 | /// descending the tree. 83 | /// 84 | /// `index` points to where this cell is stored in 85 | /// [`Octree::cells`](super::Octree::cells) 86 | #[derive(Copy, Clone, Debug)] 87 | pub struct CellIndex { 88 | /// Cell index in `Octree::cells`; `None` is the root 89 | pub index: Option<(usize, u8)>, 90 | pub depth: usize, 91 | pub bounds: CellBounds, 92 | } 93 | 94 | impl Default for CellIndex { 95 | fn default() -> Self { 96 | Self::new() 97 | } 98 | } 99 | 100 | impl CellIndex { 101 | pub fn new() -> Self { 102 | CellIndex { 103 | index: None, 104 | bounds: CellBounds::default(), 105 | depth: 0, 106 | } 107 | } 108 | 109 | /// Returns a child cell for the given corner, rooted at the given index 110 | pub fn child(&self, index: usize, i: Corner) -> Self { 111 | let bounds = self.bounds.child(i); 112 | CellIndex { 113 | index: Some((index, i.get())), 114 | bounds, 115 | depth: self.depth + 1, 116 | } 117 | } 118 | 119 | /// Returns the position of the given corner 120 | /// 121 | /// Vertices are numbered as follows in 3D: 122 | /// 123 | /// ```text 124 | /// 6 -------- 7 125 | /// / / Z 126 | /// / | / | ^ _ Y 127 | /// 4----------5 | | / 128 | /// | | | | |/ 129 | /// | 2-------|--3 ---> X 130 | /// | / | / 131 | /// |/ |/ 132 | /// 0----------1 133 | /// ``` 134 | /// 135 | /// The 8 octree cells are numbered equivalently, based on their corner 136 | /// vertex. 137 | /// 138 | /// In 2D, only corners on the XY plane (0-4) are valid. 139 | pub fn corner(&self, i: Corner) -> [f32; D] { 140 | self.bounds.corner(i) 141 | } 142 | } 143 | 144 | impl CellIndex<3> { 145 | /// Converts from a relative position in the cell to an absolute position 146 | pub fn pos(&self, p: nalgebra::Vector3) -> nalgebra::Vector3 { 147 | self.bounds.pos(p) 148 | } 149 | } 150 | 151 | #[derive(Copy, Clone, Debug)] 152 | pub struct CellBounds { 153 | pub bounds: [Interval; D], 154 | } 155 | 156 | impl std::ops::Index> for CellBounds { 157 | type Output = Interval; 158 | 159 | fn index(&self, axis: Axis) -> &Self::Output { 160 | &self.bounds[axis.index()] 161 | } 162 | } 163 | 164 | impl Default for CellBounds { 165 | fn default() -> Self { 166 | Self::new() 167 | } 168 | } 169 | 170 | impl CellBounds { 171 | pub fn new() -> Self { 172 | Self { 173 | bounds: [Interval::new(-1.0, 1.0); D], 174 | } 175 | } 176 | 177 | /// Checks whether the given position is within the cell 178 | pub fn contains(&self, p: CellVertex) -> bool { 179 | Axis::array() 180 | .into_iter() 181 | .all(|axis| self[axis].contains(p[axis])) 182 | } 183 | 184 | pub fn child(&self, corner: Corner) -> Self { 185 | let bounds = Axis::array().map(|axis| { 186 | let i = axis.index(); 187 | if corner & axis { 188 | Interval::new(self.bounds[i].midpoint(), self.bounds[i].upper()) 189 | } else { 190 | Interval::new(self.bounds[i].lower(), self.bounds[i].midpoint()) 191 | } 192 | }); 193 | Self { bounds } 194 | } 195 | 196 | pub fn corner(&self, corner: Corner) -> [f32; D] { 197 | Axis::array().map(|axis| { 198 | let i = axis.index(); 199 | if corner & axis { 200 | self.bounds[i].upper() 201 | } else { 202 | self.bounds[i].lower() 203 | } 204 | }) 205 | } 206 | 207 | /// Converts from a relative position in the cell to an absolute position 208 | pub fn pos( 209 | &self, 210 | p: nalgebra::OVector>, 211 | ) -> nalgebra::OVector> { 212 | let mut out = nalgebra::OVector::>::zeros(); 213 | for i in 0..D { 214 | out[i] = self.bounds[i].lerp(p[i] as f32 / u16::MAX as f32); 215 | } 216 | out 217 | } 218 | } 219 | 220 | //////////////////////////////////////////////////////////////////////////////// 221 | 222 | #[cfg(test)] 223 | mod test { 224 | use super::*; 225 | 226 | #[test] 227 | fn test_cell_corner() { 228 | let c = Cell::<3>::Empty; 229 | for i in Corner::iter() { 230 | assert!(!c.corner(i)); 231 | } 232 | let c = Cell::<3>::Full; 233 | for i in Corner::iter() { 234 | assert!(c.corner(i)); 235 | } 236 | let c = Cell::<3>::Leaf(Leaf { 237 | mask: CellMask::new(0b00000010), 238 | index: 0, 239 | }); 240 | assert!(!c.corner(Corner::new(0))); 241 | assert!(c.corner(Corner::new(1))); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /fidget-core/src/eval/mod.rs: -------------------------------------------------------------------------------- 1 | //! Traits and data structures for function evaluation 2 | use crate::{ 3 | Error, 4 | context::{Context, Node}, 5 | types::{Grad, Interval}, 6 | var::VarMap, 7 | }; 8 | 9 | #[cfg(any(test, feature = "eval-tests"))] 10 | #[allow(missing_docs)] 11 | pub mod test; 12 | 13 | mod bulk; 14 | mod tracing; 15 | 16 | // Reexport a few types 17 | pub use bulk::{BulkEvaluator, BulkOutput}; 18 | pub use tracing::TracingEvaluator; 19 | 20 | /// A tape represents something that can be evaluated by an evaluator 21 | /// 22 | /// It includes some kind of storage (which could be empty) and the ability to 23 | /// look up variable mapping. 24 | /// 25 | /// Tapes may be shared between threads, so they should be cheap to clone (i.e. 26 | /// a wrapper around an `Arc<..>`). 27 | pub trait Tape: Send + Sync + Clone { 28 | /// Associated type for this tape's data storage 29 | type Storage: Default; 30 | 31 | /// Tries to retrieve the internal storage from this tape 32 | /// 33 | /// This matters most for JIT evaluators, whose tapes are regions of 34 | /// executable memory-mapped RAM (which is expensive to map and unmap). 35 | fn recycle(self) -> Option; 36 | 37 | /// Returns a mapping from [`Var`](crate::var::Var) to evaluation index 38 | /// 39 | /// This must be identical to [`Function::vars`] on the `Function` which 40 | /// produced this tape. 41 | fn vars(&self) -> &VarMap; 42 | 43 | /// Returns the number of outputs written by this tape 44 | /// 45 | /// The order of outputs is set by the caller at tape construction, so we 46 | /// don't need a map to determine the index of a particular output (unlike 47 | /// variables). 48 | fn output_count(&self) -> usize; 49 | } 50 | 51 | /// Represents the trace captured by a tracing evaluation 52 | /// 53 | /// The only property enforced on the trait is that we must have a way of 54 | /// reusing trace allocations. Because [`Trace`] implies `Clone` where it's 55 | /// used in [`Function`], this is trivial, but we can't provide a default 56 | /// implementation because it would fall afoul of `impl` specialization. 57 | pub trait Trace { 58 | /// Copies the contents of `other` into `self` 59 | fn copy_from(&mut self, other: &Self); 60 | } 61 | 62 | impl Trace for Vec { 63 | fn copy_from(&mut self, other: &Self) { 64 | self.resize(other.len(), T::default()); 65 | self.copy_from_slice(other); 66 | } 67 | } 68 | 69 | /// A function represents something that can be evaluated 70 | /// 71 | /// It is mostly agnostic to _how_ that something is represented; we simply 72 | /// require that it can generate evaluators of various kinds. 73 | /// 74 | /// Inputs to the function should be represented as [`Var`](crate::var::Var) 75 | /// values; the [`vars()`](Function::vars) function returns the mapping from 76 | /// `Var` to position in the input slice. 77 | /// 78 | /// Functions are shared between threads, so they should be cheap to clone. In 79 | /// most cases, they're a thin wrapper around an `Arc<..>`. 80 | pub trait Function: Send + Sync + Clone { 81 | /// Associated type traces collected during tracing evaluation 82 | /// 83 | /// This type must implement [`Eq`] so that traces can be compared; calling 84 | /// [`Function::simplify`] with traces that compare equal should produce an 85 | /// identical result and may be cached. 86 | type Trace: Clone + Eq + Send + Sync + Trace; 87 | 88 | /// Associated type for storage used by the function itself 89 | type Storage: Default + Send; 90 | 91 | /// Associated type for workspace used during function simplification 92 | type Workspace: Default + Send; 93 | 94 | /// Associated type for storage used by tapes 95 | /// 96 | /// For simplicity, we require that every tape use the same type for storage. 97 | /// This could change in the future! 98 | type TapeStorage: Default + Send; 99 | 100 | /// Associated type for single-point tracing evaluation 101 | type PointEval: TracingEvaluator< 102 | Data = f32, 103 | Trace = Self::Trace, 104 | TapeStorage = Self::TapeStorage, 105 | > + Send 106 | + Sync; 107 | 108 | /// Builds a new point evaluator 109 | fn new_point_eval() -> Self::PointEval { 110 | Self::PointEval::new() 111 | } 112 | 113 | /// Associated type for single interval tracing evaluation 114 | type IntervalEval: TracingEvaluator< 115 | Data = Interval, 116 | Trace = Self::Trace, 117 | TapeStorage = Self::TapeStorage, 118 | > + Send 119 | + Sync; 120 | 121 | /// Builds a new interval evaluator 122 | fn new_interval_eval() -> Self::IntervalEval { 123 | Self::IntervalEval::new() 124 | } 125 | 126 | /// Associated type for evaluating many points in one call 127 | type FloatSliceEval: BulkEvaluator 128 | + Send 129 | + Sync; 130 | 131 | /// Builds a new float slice evaluator 132 | fn new_float_slice_eval() -> Self::FloatSliceEval { 133 | Self::FloatSliceEval::new() 134 | } 135 | 136 | /// Associated type for evaluating many gradients in one call 137 | type GradSliceEval: BulkEvaluator 138 | + Send 139 | + Sync; 140 | 141 | /// Builds a new gradient slice evaluator 142 | fn new_grad_slice_eval() -> Self::GradSliceEval { 143 | Self::GradSliceEval::new() 144 | } 145 | 146 | /// Returns an evaluation tape for a point evaluator 147 | fn point_tape( 148 | &self, 149 | storage: Self::TapeStorage, 150 | ) -> ::Tape; 151 | 152 | /// Returns an evaluation tape for an interval evaluator 153 | fn interval_tape( 154 | &self, 155 | storage: Self::TapeStorage, 156 | ) -> ::Tape; 157 | 158 | /// Returns an evaluation tape for a float slice evaluator 159 | fn float_slice_tape( 160 | &self, 161 | storage: Self::TapeStorage, 162 | ) -> ::Tape; 163 | 164 | /// Returns an evaluation tape for a float slice evaluator 165 | fn grad_slice_tape( 166 | &self, 167 | storage: Self::TapeStorage, 168 | ) -> ::Tape; 169 | 170 | /// Computes a simplified tape using the given trace, and reusing storage 171 | fn simplify( 172 | &self, 173 | trace: &Self::Trace, 174 | storage: Self::Storage, 175 | workspace: &mut Self::Workspace, 176 | ) -> Result 177 | where 178 | Self: Sized; 179 | 180 | /// Attempt to reclaim storage from this function 181 | /// 182 | /// This may fail, because functions are `Clone` and are often implemented 183 | /// using an `Arc` around a heavier data structure. 184 | fn recycle(self) -> Option; 185 | 186 | /// Returns a size associated with this function 187 | /// 188 | /// This is underspecified and only used for unit testing; for tape-based 189 | /// functions, it's typically the length of the tape, 190 | fn size(&self) -> usize; 191 | 192 | /// Returns the map from [`Var`](crate::var::Var) to input index 193 | fn vars(&self) -> &VarMap; 194 | 195 | /// Checks to see whether this function can ever be simplified 196 | fn can_simplify(&self) -> bool; 197 | } 198 | 199 | /// A [`Function`] which can be built from a math expression 200 | pub trait MathFunction: Function { 201 | /// Builds a new function from the given context and node 202 | fn new(ctx: &Context, nodes: &[Node]) -> Result 203 | where 204 | Self: Sized; 205 | } 206 | -------------------------------------------------------------------------------- /fidget-jit/src/mmap.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "windows")] 2 | use windows::Win32::System::Memory::{ 3 | MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READWRITE, VirtualAlloc, 4 | VirtualFree, 5 | }; 6 | 7 | pub struct Mmap { 8 | /// Pointer to a memory-mapped region, which may be uninitialized 9 | ptr: *mut std::ffi::c_void, 10 | 11 | /// Total length of the allocation 12 | capacity: usize, 13 | } 14 | 15 | // SAFETY: this is philosophically a `Vec`, so can be sent to other threads 16 | unsafe impl Send for Mmap {} 17 | 18 | impl Default for Mmap { 19 | fn default() -> Self { 20 | Self::empty() 21 | } 22 | } 23 | 24 | impl Mmap { 25 | pub fn empty() -> Self { 26 | Self { 27 | ptr: std::ptr::null_mut::(), 28 | capacity: 0, 29 | } 30 | } 31 | 32 | #[inline(always)] 33 | pub fn capacity(&self) -> usize { 34 | self.capacity 35 | } 36 | 37 | /// Builds a new `Mmap` that can hold at least `len` bytes. 38 | /// 39 | /// If `len == 0`, this will return an `Mmap` of size `PAGE_SIZE`; for a 40 | /// empty `Mmap` (which makes no system calls), use `Mmap::empty` instead. 41 | #[cfg(not(target_os = "windows"))] 42 | pub fn new(capacity: usize) -> Result { 43 | let capacity = capacity.max(1).next_multiple_of(Self::PAGE_SIZE); 44 | 45 | let ptr = unsafe { 46 | libc::mmap( 47 | std::ptr::null_mut(), 48 | capacity, 49 | Self::MMAP_PROT, 50 | Self::MMAP_FLAGS, 51 | -1, 52 | 0, 53 | ) 54 | }; 55 | 56 | if std::ptr::eq(ptr, libc::MAP_FAILED) { 57 | Err(std::io::Error::last_os_error()) 58 | } else { 59 | Ok(Self { ptr, capacity }) 60 | } 61 | } 62 | 63 | #[cfg(target_os = "windows")] 64 | pub fn new(capacity: usize) -> Result { 65 | let capacity = capacity.max(1).next_multiple_of(Self::PAGE_SIZE); 66 | 67 | let ptr = unsafe { 68 | VirtualAlloc( 69 | None, 70 | capacity, 71 | MEM_COMMIT | MEM_RESERVE, 72 | PAGE_EXECUTE_READWRITE, 73 | ) 74 | }; 75 | 76 | if ptr.is_null() { 77 | Err(std::io::Error::last_os_error()) 78 | } else { 79 | Ok(Self { ptr, capacity }) 80 | } 81 | } 82 | 83 | /// Returns the inner pointer 84 | pub fn as_ptr(&self) -> *const std::ffi::c_void { 85 | self.ptr 86 | } 87 | } 88 | 89 | #[cfg(any(target_os = "linux", target_os = "windows"))] 90 | impl Mmap { 91 | #[cfg(target_arch = "aarch64")] 92 | fn flush_cache(&self, size: usize) { 93 | use std::arch::asm; 94 | let mut cache_type: usize; 95 | // Loosely based on code from mono; see mono/mono#3549 for a good 96 | // writeup and associated PR. 97 | unsafe { 98 | asm!( 99 | "mrs {tmp}, ctr_el0", 100 | tmp = out(reg) cache_type, 101 | ); 102 | } 103 | let icache_line_size = (cache_type & 0xF) << 4; 104 | let dcache_line_size = ((cache_type >> 16) & 0xF) << 4; 105 | 106 | let mut addr = self.as_ptr() as usize & !(dcache_line_size - 1); 107 | let end = self.as_ptr() as usize + size; 108 | while addr < end { 109 | unsafe { 110 | asm!( 111 | "dc civac, {tmp}", 112 | tmp = in(reg) addr, 113 | ); 114 | } 115 | addr += dcache_line_size; 116 | } 117 | unsafe { 118 | asm!("dsb ish"); 119 | } 120 | 121 | let mut addr = self.as_ptr() as usize & !(icache_line_size - 1); 122 | while addr < end { 123 | unsafe { 124 | asm!( 125 | "ic ivau, {tmp}", 126 | tmp = in(reg) addr, 127 | ); 128 | } 129 | addr += icache_line_size; 130 | } 131 | unsafe { 132 | asm!("dsb ish", "isb"); 133 | } 134 | } 135 | 136 | #[cfg(not(target_arch = "aarch64"))] 137 | fn flush_cache(&self, _size: usize) { 138 | // Nothing to do here 139 | } 140 | } 141 | 142 | #[cfg(target_os = "macos")] 143 | impl Mmap { 144 | pub const MMAP_PROT: i32 = 145 | libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC; 146 | pub const MMAP_FLAGS: i32 = 147 | libc::MAP_PRIVATE | libc::MAP_ANON | libc::MAP_JIT; 148 | pub const PAGE_SIZE: usize = 4096; 149 | 150 | /// Invalidates the caches for the first `size` bytes of the mmap 151 | /// 152 | /// Note that you will still need to change the global W^X mode before 153 | /// evaluation, but that's on a per-thread (rather than per-mmap) basis. 154 | pub fn flush_cache(&self, size: usize) { 155 | #[link(name = "c")] 156 | unsafe extern "C" { 157 | pub fn sys_icache_invalidate( 158 | start: *const std::ffi::c_void, 159 | size: libc::size_t, 160 | ); 161 | } 162 | unsafe { 163 | sys_icache_invalidate(self.ptr, size); 164 | } 165 | } 166 | } 167 | 168 | #[cfg(target_os = "linux")] 169 | impl Mmap { 170 | pub const MMAP_PROT: i32 = 171 | libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC; 172 | 173 | pub const MMAP_FLAGS: i32 = libc::MAP_PRIVATE | libc::MAP_ANON; 174 | pub const PAGE_SIZE: usize = 4096; 175 | } 176 | 177 | #[cfg(target_os = "windows")] 178 | impl Mmap { 179 | pub const PAGE_SIZE: usize = 4096; 180 | } 181 | 182 | #[cfg(not(target_os = "windows"))] 183 | impl Drop for Mmap { 184 | fn drop(&mut self) { 185 | if self.capacity > 0 { 186 | unsafe { 187 | libc::munmap(self.ptr, self.capacity as libc::size_t); 188 | } 189 | } 190 | } 191 | } 192 | 193 | #[cfg(target_os = "windows")] 194 | impl Drop for Mmap { 195 | fn drop(&mut self) { 196 | if self.capacity > 0 { 197 | unsafe { 198 | let _ = VirtualFree(self.ptr, 0, MEM_RELEASE); 199 | } 200 | } 201 | } 202 | } 203 | 204 | use crate::WritePermit; 205 | 206 | pub struct MmapWriter { 207 | mmap: Mmap, 208 | 209 | /// Number of bytes that have been written 210 | len: usize, 211 | 212 | _permit: WritePermit, 213 | } 214 | 215 | impl From for MmapWriter { 216 | fn from(mmap: Mmap) -> MmapWriter { 217 | MmapWriter { 218 | mmap, 219 | len: 0, 220 | _permit: WritePermit::new(), 221 | } 222 | } 223 | } 224 | 225 | impl MmapWriter { 226 | /// Writes a byte to the next uninitialized position, resizing if necessary 227 | #[inline(always)] 228 | pub fn push(&mut self, b: u8) { 229 | if self.len == self.mmap.capacity { 230 | self.double_capacity() 231 | } 232 | unsafe { 233 | *(self.mmap.ptr as *mut u8).add(self.len) = b; 234 | } 235 | self.len += 1; 236 | } 237 | 238 | #[inline(never)] 239 | fn double_capacity(&mut self) { 240 | let mut next = Mmap::new(self.mmap.capacity * 2).unwrap(); 241 | unsafe { 242 | std::ptr::copy_nonoverlapping(self.mmap.ptr, next.ptr, self.len()); 243 | } 244 | std::mem::swap(&mut self.mmap, &mut next); 245 | } 246 | 247 | /// Finalizes the mmap, invalidating the system icache 248 | pub fn finalize(self) -> Mmap { 249 | self.mmap.flush_cache(self.len); 250 | self.mmap 251 | } 252 | 253 | /// Returns the number of bytes written 254 | #[inline(always)] 255 | pub fn len(&self) -> usize { 256 | self.len 257 | } 258 | 259 | /// Returns the written portion of memory as a mutable slice 260 | #[inline(always)] 261 | pub fn as_mut_slice(&mut self) -> &mut [u8] { 262 | unsafe { 263 | std::slice::from_raw_parts_mut(self.mmap.ptr as *mut u8, self.len) 264 | } 265 | } 266 | 267 | /// Returns the inner pointer 268 | #[inline(always)] 269 | pub fn as_ptr(&self) -> *const std::ffi::c_void { 270 | self.mmap.ptr 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /demos/web-editor/crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | use fidget::{ 2 | context::{Context, Tree}, 3 | gui::{Canvas2, Canvas3, DragMode, View2, View3}, 4 | raster::{GeometryBuffer, ImageRenderConfig, VoxelRenderConfig}, 5 | render::{CancelToken, ImageSize, ThreadPool, TileSizes, VoxelSize}, 6 | var::Var, 7 | vm::{VmData, VmShape}, 8 | }; 9 | use nalgebra::Point2; 10 | 11 | use wasm_bindgen::prelude::*; 12 | pub use wasm_bindgen_rayon::init_thread_pool; 13 | 14 | #[derive(Clone)] 15 | #[wasm_bindgen] 16 | pub struct JsTree(Tree); 17 | 18 | #[derive(Clone)] 19 | #[wasm_bindgen] 20 | pub struct JsVmShape(VmShape); 21 | 22 | #[wasm_bindgen] 23 | pub fn eval_script(s: &str) -> Result { 24 | let engine = fidget::rhai::engine(); 25 | let out = engine.eval(s); 26 | out.map(JsTree).map_err(|e| format!("{e}")) 27 | } 28 | 29 | /// Serializes a `JsTree` into a `bincode`-packed `VmData` 30 | #[wasm_bindgen] 31 | pub fn serialize_into_tape(t: JsTree) -> Result, String> { 32 | let mut ctx = Context::new(); 33 | let root = ctx.import(&t.0); 34 | let shape = VmShape::new(&ctx, root).map_err(|e| format!("{e}"))?; 35 | let vm_data = shape.inner().data(); 36 | let axes = shape.axes(); 37 | bincode::serialize(&(vm_data, axes)).map_err(|e| format!("{e}")) 38 | } 39 | 40 | /// Deserialize a `bincode`-packed `VmData` into a `VmShape` 41 | #[wasm_bindgen] 42 | pub fn deserialize_tape(data: Vec) -> Result { 43 | let (d, axes): (VmData<255>, [Var; 3]) = 44 | bincode::deserialize(&data).map_err(|e| format!("{e}"))?; 45 | Ok(JsVmShape(VmShape::new_raw(d.into(), axes))) 46 | } 47 | 48 | /// Renders the image in 2D 49 | #[wasm_bindgen] 50 | pub fn render_2d( 51 | shape: JsVmShape, 52 | image_size: usize, 53 | camera: JsCamera2, 54 | cancel: JsCancelToken, 55 | ) -> Result, String> { 56 | fn inner( 57 | shape: VmShape, 58 | image_size: usize, 59 | view: View2, 60 | cancel: CancelToken, 61 | ) -> Option> { 62 | let cfg = ImageRenderConfig { 63 | image_size: ImageSize::from(image_size as u32), 64 | threads: Some(&ThreadPool::Global), 65 | tile_sizes: TileSizes::new(&[64, 16, 8]).unwrap(), 66 | pixel_perfect: false, 67 | world_to_model: view.world_to_model(), 68 | cancel, 69 | }; 70 | 71 | let tmp = cfg.run(shape)?; 72 | let out = 73 | fidget::raster::effects::to_rgba_bitmap(tmp, false, cfg.threads); 74 | Some(out.into_iter().flatten().collect()) 75 | } 76 | inner(shape.0, image_size, camera.0, cancel.0) 77 | .ok_or_else(|| "cancelled".to_owned()) 78 | } 79 | 80 | /// Renders a heightmap image 81 | #[wasm_bindgen] 82 | pub fn render_heightmap( 83 | shape: JsVmShape, 84 | image_size: usize, 85 | camera: JsCamera3, 86 | cancel: JsCancelToken, 87 | ) -> Result, String> { 88 | let image = render_3d_inner(shape.0, image_size, camera.0, cancel.0) 89 | .ok_or_else(|| "cancelled".to_string())?; 90 | 91 | // Convert into an image 92 | Ok(image 93 | .into_iter() 94 | .flat_map(|v| { 95 | let d = (v.depth as usize * 255 / image_size) as u8; 96 | [d, d, d, 255] 97 | }) 98 | .collect()) 99 | } 100 | 101 | /// Renders a shaded image 102 | #[wasm_bindgen] 103 | pub fn render_normals( 104 | shape: JsVmShape, 105 | image_size: usize, 106 | camera: JsCamera3, 107 | cancel: JsCancelToken, 108 | ) -> Result, String> { 109 | let image = render_3d_inner(shape.0, image_size, camera.0, cancel.0) 110 | .ok_or_else(|| "cancelled".to_string())?; 111 | 112 | // Convert into an image 113 | Ok(image 114 | .into_iter() 115 | .flat_map(|p| { 116 | let [r, g, b] = if p.depth > 0 { p.to_color() } else { [0; 3] }; 117 | [r, g, b, 255] 118 | }) 119 | .collect()) 120 | } 121 | 122 | fn render_3d_inner( 123 | shape: VmShape, 124 | image_size: usize, 125 | view: View3, 126 | cancel: CancelToken, 127 | ) -> Option { 128 | let cfg = VoxelRenderConfig { 129 | image_size: VoxelSize::from(image_size as u32), 130 | threads: Some(&ThreadPool::Global), 131 | tile_sizes: TileSizes::new(&[128, 64, 32, 16, 8]).unwrap(), 132 | world_to_model: view.world_to_model(), 133 | cancel, 134 | }; 135 | cfg.run(shape.clone()) 136 | } 137 | 138 | //////////////////////////////////////////////////////////////////////////////// 139 | 140 | #[wasm_bindgen] 141 | pub struct JsCamera3(View3); 142 | 143 | #[wasm_bindgen] 144 | impl JsCamera3 { 145 | #[wasm_bindgen] 146 | pub fn deserialize(data: &[u8]) -> Self { 147 | Self(bincode::deserialize::(data).unwrap()) 148 | } 149 | } 150 | 151 | #[wasm_bindgen] 152 | pub struct JsCanvas3(Canvas3); 153 | 154 | #[wasm_bindgen] 155 | impl JsCanvas3 { 156 | #[wasm_bindgen(constructor)] 157 | pub fn new(width: u32, height: u32) -> Self { 158 | Self(Canvas3::new(VoxelSize::new( 159 | width, 160 | height, 161 | width.max(height), 162 | ))) 163 | } 164 | 165 | #[wasm_bindgen] 166 | pub fn serialize_view(&self) -> Vec { 167 | bincode::serialize(&self.0.view()).unwrap() 168 | } 169 | 170 | #[wasm_bindgen] 171 | pub fn begin_drag(&mut self, x: i32, y: i32, button: bool) { 172 | self.0.begin_drag( 173 | Point2::new(x, y), 174 | if button { 175 | DragMode::Pan 176 | } else { 177 | DragMode::Rotate 178 | }, 179 | ) 180 | } 181 | 182 | #[wasm_bindgen] 183 | pub fn drag(&mut self, x: i32, y: i32) -> bool { 184 | self.0.drag(Point2::new(x, y)) 185 | } 186 | 187 | #[wasm_bindgen] 188 | pub fn end_drag(&mut self) { 189 | self.0.end_drag() 190 | } 191 | 192 | #[wasm_bindgen] 193 | pub fn zoom_about(&mut self, amount: f32, x: i32, y: i32) -> bool { 194 | self.0.zoom(amount, Some(Point2::new(x, y))) 195 | } 196 | } 197 | 198 | //////////////////////////////////////////////////////////////////////////////// 199 | 200 | #[wasm_bindgen] 201 | pub struct JsCamera2(View2); 202 | 203 | #[wasm_bindgen] 204 | pub struct JsCanvas2(Canvas2); 205 | 206 | #[wasm_bindgen] 207 | impl JsCanvas2 { 208 | #[wasm_bindgen(constructor)] 209 | pub fn new(width: u32, height: u32) -> Self { 210 | Self(Canvas2::new(ImageSize::new(width, height))) 211 | } 212 | 213 | #[wasm_bindgen] 214 | pub fn serialize_view(&self) -> Vec { 215 | bincode::serialize(&self.0.view()).unwrap() 216 | } 217 | 218 | #[wasm_bindgen] 219 | pub fn begin_drag(&mut self, x: i32, y: i32) { 220 | self.0.begin_drag(Point2::new(x, y)) 221 | } 222 | 223 | #[wasm_bindgen] 224 | pub fn drag(&mut self, x: i32, y: i32) -> bool { 225 | self.0.drag(Point2::new(x, y)) 226 | } 227 | 228 | #[wasm_bindgen] 229 | pub fn end_drag(&mut self) { 230 | self.0.end_drag() 231 | } 232 | 233 | #[wasm_bindgen] 234 | pub fn zoom_about(&mut self, amount: f32, x: i32, y: i32) -> bool { 235 | self.0.zoom(amount, Some(Point2::new(x, y))) 236 | } 237 | } 238 | 239 | #[wasm_bindgen] 240 | impl JsCamera2 { 241 | #[wasm_bindgen] 242 | pub fn deserialize(data: &[u8]) -> Self { 243 | Self(bincode::deserialize::(data).unwrap()) 244 | } 245 | } 246 | 247 | //////////////////////////////////////////////////////////////////////////////// 248 | 249 | #[wasm_bindgen] 250 | pub struct JsCancelToken(CancelToken); 251 | 252 | #[wasm_bindgen] 253 | impl JsCancelToken { 254 | #[wasm_bindgen(constructor)] 255 | pub fn new() -> Self { 256 | Self(CancelToken::new()) 257 | } 258 | 259 | #[wasm_bindgen] 260 | pub fn cancel(&self) { 261 | self.0.cancel() 262 | } 263 | 264 | #[wasm_bindgen] 265 | pub fn get_ptr(&self) -> *const std::sync::atomic::AtomicBool { 266 | self.0.clone().into_raw() 267 | } 268 | 269 | #[wasm_bindgen] 270 | pub unsafe fn from_ptr(ptr: *const std::sync::atomic::AtomicBool) -> Self { 271 | let token = unsafe { CancelToken::from_raw(ptr) }; 272 | Self(token) 273 | } 274 | } 275 | 276 | //////////////////////////////////////////////////////////////////////////////// 277 | 278 | #[wasm_bindgen] 279 | pub fn get_module() -> wasm_bindgen::JsValue { 280 | wasm_bindgen::module() 281 | } 282 | 283 | #[wasm_bindgen] 284 | pub fn get_memory() -> wasm_bindgen::JsValue { 285 | wasm_bindgen::memory() 286 | } 287 | -------------------------------------------------------------------------------- /fidget-rhai/src/tree.rs: -------------------------------------------------------------------------------- 1 | //! Rhai bindings for the Fidget [`Tree`] type 2 | use crate::FromDynamic; 3 | use fidget_core::{ 4 | context::{Tree, TreeOp}, 5 | var::Var, 6 | }; 7 | use rhai::{EvalAltResult, NativeCallContext}; 8 | 9 | impl FromDynamic for Tree { 10 | fn from_dynamic( 11 | ctx: &rhai::NativeCallContext, 12 | d: rhai::Dynamic, 13 | _default: Option<&Tree>, 14 | ) -> Result> { 15 | if let Some(t) = d.clone().try_cast::() { 16 | Ok(t) 17 | } else if let Ok(v) = f64::from_dynamic(ctx, d.clone(), None) { 18 | Ok(Tree::constant(v)) 19 | } else if let Ok(v) = >::from_dynamic(ctx, d.clone(), None) { 20 | Ok(fidget_shapes::Union { input: v }.into()) 21 | } else { 22 | Err(Box::new(rhai::EvalAltResult::ErrorMismatchDataType( 23 | "tree".to_string(), 24 | d.type_name().to_string(), 25 | ctx.call_position(), 26 | ))) 27 | } 28 | } 29 | } 30 | 31 | impl FromDynamic for Vec { 32 | fn from_dynamic( 33 | ctx: &rhai::NativeCallContext, 34 | d: rhai::Dynamic, 35 | _default: Option<&Vec>, 36 | ) -> Result> { 37 | if let Ok(d) = d.clone().into_array() { 38 | d.into_iter() 39 | .map(|v| Tree::from_dynamic(ctx, v, None)) 40 | .collect::, _>>() 41 | } else { 42 | Err(Box::new(rhai::EvalAltResult::ErrorMismatchDataType( 43 | "Vec".to_string(), 44 | d.type_name().to_string(), 45 | ctx.call_position(), 46 | ))) 47 | } 48 | } 49 | } 50 | 51 | fn register_tree(engine: &mut rhai::Engine) { 52 | engine 53 | .register_type_with_name::("Tree") 54 | .register_fn("to_string", |t: &mut Tree| { 55 | match &**t { 56 | TreeOp::Input(Var::X) => "x", 57 | TreeOp::Input(Var::Y) => "y", 58 | TreeOp::Input(Var::Z) => "z", 59 | _ => "Tree(..)", 60 | } 61 | .to_owned() 62 | }) 63 | .register_fn("remap", remap_xyz) 64 | .register_fn("remap", remap_xy); 65 | } 66 | 67 | /// Installs the [`Tree`] type into a Rhai engine, with various overloads 68 | /// 69 | /// Also installs `axes() -> {x, y, z}` 70 | pub fn register(engine: &mut rhai::Engine) { 71 | register_tree(engine); 72 | engine.register_fn("axes", || { 73 | use fidget_core::context::Tree; 74 | let mut out = rhai::Map::new(); 75 | out.insert("x".into(), rhai::Dynamic::from(Tree::x())); 76 | out.insert("y".into(), rhai::Dynamic::from(Tree::y())); 77 | out.insert("z".into(), rhai::Dynamic::from(Tree::z())); 78 | out 79 | }); 80 | 81 | macro_rules! register_binary_fns { 82 | ($op:literal, $name:ident, $engine:ident) => { 83 | $engine.register_fn($op, $name::tree_dyn); 84 | $engine.register_fn($op, $name::dyn_tree); 85 | }; 86 | } 87 | macro_rules! register_unary_fns { 88 | ($op:literal, $name:ident, $engine:ident) => { 89 | $engine.register_fn($op, $name::tree); 90 | }; 91 | } 92 | 93 | register_binary_fns!("+", add, engine); 94 | register_binary_fns!("-", sub, engine); 95 | register_binary_fns!("*", mul, engine); 96 | register_binary_fns!("/", div, engine); 97 | register_binary_fns!("%", modulo, engine); 98 | register_binary_fns!("min", min, engine); 99 | register_binary_fns!("max", max, engine); 100 | register_binary_fns!("compare", compare, engine); 101 | register_binary_fns!("and", and, engine); 102 | register_binary_fns!("or", or, engine); 103 | register_binary_fns!("atan2", atan2, engine); 104 | register_unary_fns!("abs", abs, engine); 105 | register_unary_fns!("sqrt", sqrt, engine); 106 | register_unary_fns!("square", square, engine); 107 | register_unary_fns!("sin", sin, engine); 108 | register_unary_fns!("cos", cos, engine); 109 | register_unary_fns!("tan", tan, engine); 110 | register_unary_fns!("asin", asin, engine); 111 | register_unary_fns!("acos", acos, engine); 112 | register_unary_fns!("atan", atan, engine); 113 | register_unary_fns!("exp", exp, engine); 114 | register_unary_fns!("ln", ln, engine); 115 | register_unary_fns!("not", not, engine); 116 | register_unary_fns!("ceil", ceil, engine); 117 | register_unary_fns!("floor", floor, engine); 118 | register_unary_fns!("round", round, engine); 119 | register_unary_fns!("-", neg, engine); 120 | 121 | // Ban comparison operators 122 | for op in ["==", "!=", "<", ">", "<=", ">="] { 123 | engine.register_fn(op, bad_cmp_tree_dyn); 124 | engine.register_fn(op, bad_cmp_dyn_tree); 125 | } 126 | } 127 | 128 | fn remap_xyz( 129 | ctx: NativeCallContext, 130 | shape: rhai::Dynamic, 131 | x: Tree, 132 | y: Tree, 133 | z: Tree, 134 | ) -> Result> { 135 | let shape = Tree::from_dynamic(&ctx, shape, None)?; 136 | Ok(shape.remap_xyz(x, y, z)) 137 | } 138 | 139 | fn remap_xy( 140 | ctx: NativeCallContext, 141 | shape: rhai::Dynamic, 142 | x: Tree, 143 | y: Tree, 144 | ) -> Result> { 145 | let shape = Tree::from_dynamic(&ctx, shape, None)?; 146 | Ok(shape.remap_xyz(x, y, Tree::z())) 147 | } 148 | 149 | macro_rules! define_binary_fns { 150 | ($name:ident $(, $op:ident)?) => { 151 | mod $name { 152 | use super::*; 153 | use NativeCallContext; 154 | $( 155 | use std::ops::$op; 156 | )? 157 | pub fn tree_dyn( 158 | ctx: NativeCallContext, 159 | a: Tree, 160 | b: rhai::Dynamic, 161 | ) -> Result> { 162 | let b = Tree::from_dynamic(&ctx, b, None)?; 163 | Ok(a.$name(b)) 164 | } 165 | pub fn dyn_tree( 166 | ctx: NativeCallContext, 167 | a: rhai::Dynamic, 168 | b: Tree, 169 | ) -> Result> { 170 | let a = Tree::from_dynamic(&ctx, a, None)?; 171 | Ok(a.$name(b)) 172 | } 173 | } 174 | }; 175 | } 176 | 177 | macro_rules! define_unary_fns { 178 | ($name:ident) => { 179 | mod $name { 180 | use super::*; 181 | pub fn tree( 182 | ctx: NativeCallContext, 183 | a: rhai::Dynamic, 184 | ) -> Result> { 185 | let a = Tree::from_dynamic(&ctx, a, None)?; 186 | Ok(a.$name()) 187 | } 188 | } 189 | }; 190 | } 191 | 192 | fn bad_cmp_tree_dyn( 193 | _ctx: NativeCallContext, 194 | _a: Tree, 195 | _b: rhai::Dynamic, 196 | ) -> Result> { 197 | let e = "cannot compare Tree types during function tracing"; 198 | Err(e.into()) 199 | } 200 | 201 | fn bad_cmp_dyn_tree( 202 | _ctx: NativeCallContext, 203 | _a: rhai::Dynamic, 204 | _b: Tree, 205 | ) -> Result> { 206 | let e = "cannot compare Tree types during function tracing"; 207 | Err(e.into()) 208 | } 209 | 210 | define_binary_fns!(add, Add); 211 | define_binary_fns!(sub, Sub); 212 | define_binary_fns!(mul, Mul); 213 | define_binary_fns!(div, Div); 214 | define_binary_fns!(min); 215 | define_binary_fns!(max); 216 | define_binary_fns!(compare); 217 | define_binary_fns!(modulo); 218 | define_binary_fns!(and); 219 | define_binary_fns!(or); 220 | define_binary_fns!(atan2); 221 | define_unary_fns!(sqrt); 222 | define_unary_fns!(square); 223 | define_unary_fns!(neg); 224 | define_unary_fns!(sin); 225 | define_unary_fns!(cos); 226 | define_unary_fns!(tan); 227 | define_unary_fns!(asin); 228 | define_unary_fns!(acos); 229 | define_unary_fns!(atan); 230 | define_unary_fns!(exp); 231 | define_unary_fns!(ln); 232 | define_unary_fns!(not); 233 | define_unary_fns!(abs); 234 | define_unary_fns!(floor); 235 | define_unary_fns!(ceil); 236 | define_unary_fns!(round); 237 | 238 | #[cfg(test)] 239 | mod test { 240 | use super::*; 241 | 242 | #[test] 243 | fn tree_build_print() { 244 | let mut e = rhai::Engine::new(); 245 | register(&mut e); 246 | assert_eq!(e.eval::("to_string(axes().x)").unwrap(), "x"); 247 | assert_eq!( 248 | e.eval::("to_string(axes().x + 1)").unwrap(), 249 | "Tree(..)" 250 | ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /fidget-mesh/src/dc.rs: -------------------------------------------------------------------------------- 1 | //! Dual contouring implementation 2 | 3 | use crate::{ 4 | Octree, 5 | builder::MeshBuilder, 6 | cell::{Cell, CellIndex}, 7 | frame::{Frame, XYZ, YZX, ZXY}, 8 | types::{Corner, Edge, X, Y, Z}, 9 | }; 10 | 11 | pub fn dc_cell(octree: &Octree, cell: CellIndex<3>, out: &mut MeshBuilder) { 12 | if let Cell::Branch { .. } = octree[cell] { 13 | for i in Corner::iter() { 14 | out.cell(octree, octree.child(cell, i)); 15 | } 16 | 17 | // Helper function for DC face calls 18 | fn dc_faces( 19 | octree: &Octree, 20 | cell: CellIndex<3>, 21 | out: &mut MeshBuilder, 22 | ) { 23 | let (t, u, v) = T::frame(); 24 | for c in [Corner::new(0), u.into(), v.into(), u | v] { 25 | out.face::( 26 | octree, 27 | octree.child(cell, c), 28 | octree.child(cell, c | t), 29 | ); 30 | } 31 | } 32 | dc_faces::(octree, cell, out); 33 | dc_faces::(octree, cell, out); 34 | dc_faces::(octree, cell, out); 35 | 36 | #[allow(unused_parens)] 37 | for i in [false, true] { 38 | out.edge::( 39 | octree, 40 | octree.child(cell, (X * i)), 41 | octree.child(cell, (X * i) | Y), 42 | octree.child(cell, (X * i) | Y | Z), 43 | octree.child(cell, (X * i) | Z), 44 | ); 45 | out.edge::( 46 | octree, 47 | octree.child(cell, (Y * i)), 48 | octree.child(cell, (Y * i) | Z), 49 | octree.child(cell, (Y * i) | X | Z), 50 | octree.child(cell, (Y * i) | X), 51 | ); 52 | out.edge::( 53 | octree, 54 | octree.child(cell, (Z * i)), 55 | octree.child(cell, (Z * i) | X), 56 | octree.child(cell, (Z * i) | X | Y), 57 | octree.child(cell, (Z * i) | Y), 58 | ); 59 | } 60 | } 61 | } 62 | 63 | /// Handles two cells which share a common face 64 | /// 65 | /// `lo` is below `hi` on the `T` axis; the cells share a `UV` face where 66 | /// `T-U-V` is a right-handed coordinate system. 67 | pub fn dc_face( 68 | octree: &Octree, 69 | lo: CellIndex<3>, 70 | hi: CellIndex<3>, 71 | out: &mut MeshBuilder, 72 | ) { 73 | if octree.is_leaf(lo) && octree.is_leaf(hi) { 74 | return; 75 | } 76 | let (t, u, v) = T::frame(); 77 | out.face::( 78 | octree, 79 | octree.child(lo, t), 80 | octree.child(hi, Corner::new(0)), 81 | ); 82 | out.face::(octree, octree.child(lo, t | u), octree.child(hi, u)); 83 | out.face::(octree, octree.child(lo, t | v), octree.child(hi, v)); 84 | out.face::(octree, octree.child(lo, t | u | v), octree.child(hi, u | v)); 85 | #[allow(unused_parens)] 86 | for i in [false, true] { 87 | out.edge::( 88 | octree, 89 | octree.child(lo, (u * i) | t), 90 | octree.child(lo, (u * i) | v | t), 91 | octree.child(hi, (u * i) | v), 92 | octree.child(hi, (u * i)), 93 | ); 94 | out.edge::<::Next>( 95 | octree, 96 | octree.child(lo, (v * i) | t), 97 | octree.child(hi, (v * i)), 98 | octree.child(hi, (v * i) | u), 99 | octree.child(lo, (v * i) | u | t), 100 | ); 101 | } 102 | } 103 | 104 | /// Handles four cells that share a common edge aligned on axis `T` 105 | /// 106 | /// Cells positions are in the order `[0, U, U | V, U]`, i.e. a right-handed 107 | /// winding about `+T` (where `T, U, V` is a right-handed coordinate frame) 108 | /// 109 | /// - `dc_edge` is `[0, Y, Y | Z, Z]` 110 | /// - `dc_edge` is `[0, Z, Z | X, X]` 111 | /// - `dc_edge` is `[0, X, X | Y, Y]` 112 | pub fn dc_edge( 113 | octree: &Octree, 114 | a: CellIndex<3>, 115 | b: CellIndex<3>, 116 | c: CellIndex<3>, 117 | d: CellIndex<3>, 118 | out: &mut MeshBuilder, 119 | ) { 120 | let cs = [a, b, c, d]; 121 | if cs.iter().all(|v| octree.is_leaf(*v)) { 122 | // If any of the leafs are Empty or Full, then this edge can't 123 | // include a sign change. TODO: can we make this any -> all if we 124 | // collapse empty / filled leafs into Empty / Full cells? 125 | let leafs = cs.map(|cell| match octree[cell] { 126 | Cell::Leaf(leaf) => Some(leaf), 127 | Cell::Empty | Cell::Full => None, 128 | Cell::Branch { .. } => unreachable!(), 129 | Cell::Invalid => panic!(), 130 | }); 131 | if leafs.iter().any(Option::is_none) { 132 | return; 133 | } 134 | let leafs = leafs.map(Option::unwrap); 135 | 136 | // TODO: should we pick a canonically deepest leaf instead of the first 137 | // among the four that's at the deepest depth? 138 | let deepest = (0..4).max_by_key(|i| cs[*i].depth).unwrap(); 139 | 140 | let (t, _u, _v) = T::frame(); 141 | 142 | // Each leaf has an edge associated with it 143 | #[allow(clippy::identity_op)] 144 | let edges = [ 145 | Edge::new((t.index() * 4 + 3) as u8), 146 | Edge::new((t.index() * 4 + 2) as u8), 147 | Edge::new((t.index() * 4 + 0) as u8), 148 | Edge::new((t.index() * 4 + 1) as u8), 149 | ]; 150 | 151 | // Find the starting sign of the relevant edge, bailing out early if 152 | // there isn't a sign change here. All of the deepest edges should show 153 | // the same sign change, so it doesn't matter which one we pick here. 154 | let starting_sign = { 155 | let (start, end) = edges[deepest].corners(); 156 | let start = !(leafs[deepest].mask & start); 157 | let end = !(leafs[deepest].mask & end); 158 | // If there is no sign change, then there's nothing to do here. 159 | if start == end { 160 | return; 161 | } 162 | start 163 | }; 164 | 165 | // Iterate over each of the edges, assigning a vertex if the sign change 166 | // lines up. 167 | let mut verts = [None; 4]; 168 | for i in 0..4 { 169 | if cs[i].depth == cs[deepest].depth { 170 | let (start, end) = edges[i].corners(); 171 | let s = !(leafs[i].mask & start); 172 | let e = !(leafs[i].mask & end); 173 | debug_assert_eq!(s, starting_sign); 174 | debug_assert_eq!(e, !starting_sign); 175 | verts[i] = leafs[i].edge(edges[i]); 176 | } else { 177 | // We declare that only *manifold* leaf cells can be neighbors 178 | // to smaller leaf cells. This means that there's only one 179 | // vertex to pick here. 180 | let mut iter = 181 | (0..12).filter_map(|j| leafs[i].edge(Edge::new(j))); 182 | verts[i] = iter.next(); 183 | if iter.any(|other| other.vert != verts[i].unwrap().vert) { 184 | panic!("invalid leaf vertex at {a:?}"); 185 | } 186 | } 187 | } 188 | 189 | let verts = verts.map(Option::unwrap); 190 | 191 | // Pick the intersection vertex based on the deepest cell 192 | let i = out.vertex( 193 | leafs[deepest].index + verts[deepest].edge.0 as usize, 194 | &octree.verts, 195 | ); 196 | // Helper function to extract other vertices 197 | let mut vert = |i: usize| { 198 | out.vertex(leafs[i].index + verts[i].vert.0 as usize, &octree.verts) 199 | }; 200 | let vs = [vert(0), vert(1), vert(2), vert(3)]; 201 | 202 | // Pick a triangle winding depending on the edge direction 203 | // 204 | // As always, we have to sample the deepest leaf's edge to be sure that 205 | // we get the correct value. 206 | let winding = if starting_sign { 3 } else { 1 }; 207 | for j in 0..4 { 208 | if cs[j].index != cs[(j + winding) % 4].index { 209 | out.triangle(vs[j], vs[(j + winding) % 4], i) 210 | } 211 | } 212 | } else { 213 | let (t, u, v) = T::frame(); 214 | 215 | #[allow(unused_parens)] 216 | for i in [false, true] { 217 | out.edge::( 218 | octree, 219 | octree.child(a, (t * i) | u | v), 220 | octree.child(b, (t * i) | v), 221 | octree.child(c, (t * i)), 222 | octree.child(d, (t * i) | u), 223 | ) 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /fidget-raster/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::{DistancePixel, GeometryBuffer, Image, RenderConfig, TileSizesRef}; 2 | use fidget_core::{ 3 | eval::Function, 4 | render::{CancelToken, ImageSize, ThreadPool, TileSizes, VoxelSize}, 5 | shape::{Shape, ShapeVars}, 6 | }; 7 | use nalgebra::{Const, Matrix3, Matrix4, OPoint, Point2, Vector2}; 8 | 9 | /// Settings for 2D rendering 10 | pub struct ImageRenderConfig<'a> { 11 | /// Render size 12 | pub image_size: ImageSize, 13 | 14 | /// World-to-model transform 15 | pub world_to_model: Matrix3, 16 | 17 | /// Render the distance values of individual pixels 18 | pub pixel_perfect: bool, 19 | 20 | /// Tile sizes to use during evaluation. 21 | /// 22 | /// You'll likely want to use 23 | /// [`RenderHints::tile_sizes_2d`](fidget_core::render::RenderHints::tile_sizes_2d) 24 | /// to select this based on evaluator type. 25 | pub tile_sizes: TileSizes, 26 | 27 | /// Thread pool to use for rendering 28 | /// 29 | /// If this is `None`, then rendering is done in a single thread; otherwise, 30 | /// the provided pool is used. 31 | pub threads: Option<&'a ThreadPool>, 32 | 33 | /// Token to cancel rendering 34 | pub cancel: CancelToken, 35 | } 36 | 37 | impl Default for ImageRenderConfig<'_> { 38 | fn default() -> Self { 39 | Self { 40 | image_size: ImageSize::from(512), 41 | tile_sizes: TileSizes::new(&[128, 32, 8]).unwrap(), 42 | world_to_model: Matrix3::identity(), 43 | pixel_perfect: false, 44 | threads: Some(&ThreadPool::Global), 45 | cancel: CancelToken::new(), 46 | } 47 | } 48 | } 49 | 50 | impl RenderConfig for ImageRenderConfig<'_> { 51 | fn width(&self) -> u32 { 52 | self.image_size.width() 53 | } 54 | fn height(&self) -> u32 { 55 | self.image_size.height() 56 | } 57 | fn threads(&self) -> Option<&ThreadPool> { 58 | self.threads 59 | } 60 | fn tile_sizes(&self) -> TileSizesRef<'_> { 61 | let max_size = self.width().max(self.height()) as usize; 62 | TileSizesRef::new(&self.tile_sizes, max_size) 63 | } 64 | fn is_cancelled(&self) -> bool { 65 | self.cancel.is_cancelled() 66 | } 67 | } 68 | 69 | impl ImageRenderConfig<'_> { 70 | /// Render a shape in 2D using this configuration 71 | pub fn run( 72 | &self, 73 | shape: Shape, 74 | ) -> Option> { 75 | self.run_with_vars::(shape, &ShapeVars::new()) 76 | } 77 | 78 | /// Render a shape in 2D using this configuration and variables 79 | pub fn run_with_vars( 80 | &self, 81 | shape: Shape, 82 | vars: &ShapeVars, 83 | ) -> Option> { 84 | crate::render2d::(shape, vars, self) 85 | } 86 | 87 | /// Returns the combined screen-to-model transform matrix 88 | pub fn mat(&self) -> Matrix3 { 89 | self.world_to_model * self.image_size.screen_to_world() 90 | } 91 | } 92 | 93 | /// Settings for 3D rendering 94 | pub struct VoxelRenderConfig<'a> { 95 | /// Render size 96 | /// 97 | /// The resulting image will have the given width and height; depth sets the 98 | /// number of voxels to evaluate within each pixel of the image (stacked 99 | /// into a column going into the screen). 100 | pub image_size: VoxelSize, 101 | 102 | /// World-to-model transform 103 | pub world_to_model: Matrix4, 104 | 105 | /// Tile sizes to use during evaluation. 106 | /// 107 | /// You'll likely want to use 108 | /// [`RenderHints::tile_sizes_3d`](fidget_core::render::RenderHints::tile_sizes_3d) 109 | /// to select this based on evaluator type. 110 | pub tile_sizes: TileSizes, 111 | 112 | /// Thread pool to use for rendering 113 | /// 114 | /// If this is `None`, then rendering is done in a single thread; otherwise, 115 | /// the provided pool is used. 116 | pub threads: Option<&'a ThreadPool>, 117 | 118 | /// Token to cancel rendering 119 | pub cancel: CancelToken, 120 | } 121 | 122 | impl Default for VoxelRenderConfig<'_> { 123 | fn default() -> Self { 124 | Self { 125 | image_size: VoxelSize::from(512), 126 | tile_sizes: TileSizes::new(&[128, 64, 32, 16, 8]).unwrap(), 127 | world_to_model: Matrix4::identity(), 128 | threads: Some(&ThreadPool::Global), 129 | cancel: CancelToken::new(), 130 | } 131 | } 132 | } 133 | 134 | impl RenderConfig for VoxelRenderConfig<'_> { 135 | fn width(&self) -> u32 { 136 | self.image_size.width() 137 | } 138 | fn height(&self) -> u32 { 139 | self.image_size.height() 140 | } 141 | fn threads(&self) -> Option<&ThreadPool> { 142 | self.threads 143 | } 144 | fn tile_sizes(&self) -> TileSizesRef<'_> { 145 | let max_size = self.width().max(self.height()) as usize; 146 | TileSizesRef::new(&self.tile_sizes, max_size) 147 | } 148 | fn is_cancelled(&self) -> bool { 149 | self.cancel.is_cancelled() 150 | } 151 | } 152 | 153 | impl VoxelRenderConfig<'_> { 154 | /// Render a shape in 3D using this configuration 155 | /// 156 | /// Returns a [`GeometryBuffer`] of pixel data, or `None` if rendering was 157 | /// cancelled. 158 | /// 159 | /// In the resulting image, saturated pixels (i.e. pixels in the image which 160 | /// are fully occupied up to the camera) are represented with `depth = 161 | /// self.image_size.depth()` and a normal of `[0, 0, 1]`. 162 | pub fn run(&self, shape: Shape) -> Option { 163 | self.run_with_vars::(shape, &ShapeVars::new()) 164 | } 165 | 166 | /// Render a shape in 3D using this configuration and variables 167 | pub fn run_with_vars( 168 | &self, 169 | shape: Shape, 170 | vars: &ShapeVars, 171 | ) -> Option { 172 | crate::render3d::(shape, vars, self) 173 | } 174 | 175 | /// Returns the combined screen-to-model transform matrix 176 | pub fn mat(&self) -> Matrix4 { 177 | self.world_to_model * self.image_size.screen_to_world() 178 | } 179 | } 180 | 181 | //////////////////////////////////////////////////////////////////////////////// 182 | 183 | #[derive(Copy, Clone, Debug)] 184 | pub(crate) struct Tile { 185 | /// Corner of this tile, in global screen (pixel) coordinates 186 | pub corner: OPoint>, 187 | } 188 | 189 | impl Tile { 190 | /// Build a new tile from its global coordinates 191 | #[inline] 192 | pub(crate) fn new(corner: OPoint>) -> Tile { 193 | Tile { corner } 194 | } 195 | 196 | /// Converts a relative position within the tile into a global position 197 | /// 198 | /// This function operates in pixel space, using the `.xy` coordinates 199 | pub(crate) fn add(&self, pos: Vector2) -> Point2 { 200 | let corner = Point2::new(self.corner[0], self.corner[1]); 201 | corner + pos 202 | } 203 | } 204 | 205 | //////////////////////////////////////////////////////////////////////////////// 206 | 207 | #[cfg(test)] 208 | mod test { 209 | use super::*; 210 | use fidget_core::render::ImageSize; 211 | 212 | #[test] 213 | fn test_default_render_config() { 214 | let config = ImageRenderConfig { 215 | image_size: ImageSize::from(512), 216 | ..Default::default() 217 | }; 218 | let mat = config.mat(); 219 | assert_eq!( 220 | mat.transform_point(&Point2::new(0.0, -1.0)), 221 | Point2::new(-1.0, 1.0) 222 | ); 223 | assert_eq!( 224 | mat.transform_point(&Point2::new(512.0, -1.0)), 225 | Point2::new(1.0, 1.0) 226 | ); 227 | assert_eq!( 228 | mat.transform_point(&Point2::new(512.0, 511.0)), 229 | Point2::new(1.0, -1.0) 230 | ); 231 | 232 | let config = ImageRenderConfig { 233 | image_size: ImageSize::from(575), 234 | ..Default::default() 235 | }; 236 | let mat = config.mat(); 237 | assert_eq!( 238 | mat.transform_point(&Point2::new(0.0, -1.0)), 239 | Point2::new(-1.0, 1.0) 240 | ); 241 | assert_eq!( 242 | mat.transform_point(&Point2::new( 243 | config.image_size.width() as f32, 244 | -1.0 245 | )), 246 | Point2::new(1.0, 1.0) 247 | ); 248 | assert_eq!( 249 | mat.transform_point(&Point2::new( 250 | config.image_size.width() as f32, 251 | config.image_size.height() as f32 - 1.0, 252 | )), 253 | Point2::new(1.0, -1.0) 254 | ); 255 | } 256 | } 257 | --------------------------------------------------------------------------------